gipity 1.1.6 → 1.1.7
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/catalog.js +2 -2
- package/dist/commands/db.js +61 -0
- package/dist/commands/page-eval.js +28 -0
- package/dist/commands/page-screenshot.js +34 -6
- package/dist/commands/records.js +38 -6
- package/dist/commands/service.js +71 -7
- package/dist/commands/workflow.js +59 -6
- package/dist/db-checkpoint.js +42 -0
- package/dist/index.js +228 -36
- package/dist/knowledge.js +2 -2
- package/dist/relay/onboarding.js +3 -3
- package/dist/utils.js +16 -5
- package/package.json +2 -2
package/dist/catalog.js
CHANGED
|
@@ -19,9 +19,9 @@ export const STARTERS = [
|
|
|
19
19
|
/** Visible blank-wiring templates. */
|
|
20
20
|
export const BLANK = [
|
|
21
21
|
{ key: 'web-simple', hint: 'static frontend-only site - pages, dashboards, simple games' },
|
|
22
|
-
{ key: 'web-fullstack', hint: 'backend API + database wiring - frontend, functions, migrations; deploys green' },
|
|
22
|
+
{ key: 'web-fullstack', hint: 'backend API + database wiring - frontend, functions, migrations; deploys green empty' },
|
|
23
23
|
{ key: '3d-engine', hint: '3D multiplayer wiring - Three.js + Rapier + Gipity Realtime' },
|
|
24
|
-
{ key: 'api', hint: 'pure API backend, no frontend -
|
|
24
|
+
{ key: 'api', hint: 'pure API backend, no frontend - functions + tests, deploys green' },
|
|
25
25
|
];
|
|
26
26
|
/** Hidden templates - installable by exact key, omitted from listings. */
|
|
27
27
|
export const HIDDEN = [
|
package/dist/commands/db.js
CHANGED
|
@@ -4,6 +4,7 @@ import { requireConfig } from '../config.js';
|
|
|
4
4
|
import { error as clrError, success } from '../colors.js';
|
|
5
5
|
import { run, printList, emitField } from '../helpers/index.js';
|
|
6
6
|
import { confirm } from '../utils.js';
|
|
7
|
+
import { createCheckpoint, restoreCheckpoint, dropCheckpoint, resolveDatabase } from '../db-checkpoint.js';
|
|
7
8
|
export const dbCommand = new Command('db')
|
|
8
9
|
.description('Manage databases');
|
|
9
10
|
dbCommand
|
|
@@ -171,4 +172,64 @@ dbCommand
|
|
|
171
172
|
console.log(success(`Dropped database '${name}'.`));
|
|
172
173
|
}
|
|
173
174
|
}));
|
|
175
|
+
dbCommand
|
|
176
|
+
.command('checkpoint')
|
|
177
|
+
.description('Snapshot every table so a write test can be undone. Take one before exercising a real write path (page eval on the deployed app, fn call, workflow run), then `gipity db restore` to put the data back exactly as it was - no hand-written SQL to scrub test strings out of real content.')
|
|
178
|
+
.option('--database <name>', 'Database name')
|
|
179
|
+
.option('--drop', 'Discard the existing checkpoint without restoring (keep whatever the run wrote)')
|
|
180
|
+
.option('--json', 'Output as JSON')
|
|
181
|
+
.action((opts) => run('Checkpoint', async () => {
|
|
182
|
+
const config = requireConfig();
|
|
183
|
+
const dbName = await resolveDatabase(config.projectGuid, opts.database);
|
|
184
|
+
if (!dbName) {
|
|
185
|
+
console.error(clrError('No databases in this project - nothing to checkpoint.'));
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
if (opts.drop) {
|
|
189
|
+
const dropped = await dropCheckpoint(config.projectGuid, dbName);
|
|
190
|
+
if (opts.json) {
|
|
191
|
+
console.log(JSON.stringify(dropped));
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
console.log(dropped.tables.length === 0
|
|
195
|
+
? `No checkpoint in '${dbName}'.`
|
|
196
|
+
: success(`Discarded the checkpoint of ${dropped.tables.length} table(s) in '${dbName}'. Current data kept.`));
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const res = await createCheckpoint(config.projectGuid, dbName);
|
|
201
|
+
if (opts.json) {
|
|
202
|
+
console.log(JSON.stringify(res));
|
|
203
|
+
}
|
|
204
|
+
else if (res.tables.length === 0) {
|
|
205
|
+
console.log(`Database '${dbName}' has no tables - nothing to checkpoint.`);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
console.log(success(`Checkpointed ${res.tables.length} table(s), ${res.rows} row(s) in '${dbName}'.`));
|
|
209
|
+
console.log('Undo everything written since: gipity db restore');
|
|
210
|
+
}
|
|
211
|
+
}));
|
|
212
|
+
dbCommand
|
|
213
|
+
.command('restore')
|
|
214
|
+
.description('Roll every table back to the last `gipity db checkpoint` and drop the checkpoint. The undo for a live write test.')
|
|
215
|
+
.option('--database <name>', 'Database name')
|
|
216
|
+
.option('--keep', 'Keep the checkpoint after restoring, so you can run the same write test again')
|
|
217
|
+
.option('--json', 'Output as JSON')
|
|
218
|
+
.action((opts) => run('Restore', async () => {
|
|
219
|
+
const config = requireConfig();
|
|
220
|
+
const dbName = await resolveDatabase(config.projectGuid, opts.database);
|
|
221
|
+
if (!dbName) {
|
|
222
|
+
console.error(clrError('No databases in this project - nothing to restore.'));
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
const res = await restoreCheckpoint(config.projectGuid, dbName, { keep: opts.keep });
|
|
226
|
+
if (opts.json) {
|
|
227
|
+
console.log(JSON.stringify(res));
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
console.log(success(`Restored ${res.tables.length} table(s) to the checkpoint (${res.rows} row(s)) in '${dbName}'.`));
|
|
231
|
+
if (opts.keep)
|
|
232
|
+
console.log('Checkpoint kept - restore again any time.');
|
|
233
|
+
}
|
|
234
|
+
}));
|
|
174
235
|
//# sourceMappingURL=db.js.map
|
|
@@ -5,6 +5,7 @@ import { brand, bold, muted, warning, success } from '../colors.js';
|
|
|
5
5
|
import { run, parseDuration } from '../helpers/index.js';
|
|
6
6
|
import { getAuth } from '../auth.js';
|
|
7
7
|
import { resolveProjectContext } from '../config.js';
|
|
8
|
+
import { createCheckpoint, restoreCheckpoint, resolveDatabase } from '../db-checkpoint.js';
|
|
8
9
|
import { uploadPublicFixture, uploadCameraFeed, assertCameraFile, deleteFixture } from '../page-fixtures.js';
|
|
9
10
|
// Shown when an eval runs cleanly but returns nothing serializable. Turns a
|
|
10
11
|
// bare/opaque `null` into a deterministic, actionable nudge so the agent shapes
|
|
@@ -418,6 +419,7 @@ export const pageEvalCommand = new Command('eval')
|
|
|
418
419
|
.option('--wait-timeout <ms>', `Max ms to wait for --wait-for before giving up (max ${WAIT_FOR_MAX_MS})`, '5000')
|
|
419
420
|
.option('--timeout <ms>', `How long the script itself may run IN the page. Bare number = MILLISECONDS (so 90s = 90000, NOT 90); or pass an explicit unit that means the same on both this and \`sandbox run --timeout\` — --timeout 90s. Its own await/setTimeout pauses count (default ${EVAL_SCRIPT_BUDGET_MS}, ${EVAL_SCRIPT_BUDGET_CAMERA_MS} with --camera/--fake-media; floor 1000, max ${EVAL_SCRIPT_BUDGET_MAX_MS}). Raise it to trace a sequence that unfolds over time (a game round, an animation). Distinct from --wait, which only sleeps BEFORE the script.`)
|
|
420
421
|
.option('--auth', 'Evaluate signed in as you (your Gipity account), so a page behind a Sign-in-with-Gipity login is reachable. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai. Without this flag the page loads as a genuinely anonymous, signed-out visitor — nothing carries over from earlier --auth runs.')
|
|
422
|
+
.option('--restore-db', "Snapshot the app database before the script runs and roll it back after, so a write-path check (click Approve, submit the form, edit a row) leaves the real data untouched. Use this whenever the eval WRITES - it is the undo for driving the deployed app against real project data. Same snapshot/undo standalone: gipity db checkpoint / gipity db restore.")
|
|
421
423
|
.option('--json', 'Output as JSON')
|
|
422
424
|
.action((url, exprArg, opts) => run('Page eval', async () => {
|
|
423
425
|
// A JS-intent flag guess (captured as a hidden decoy below): redirect to the
|
|
@@ -561,7 +563,24 @@ export const pageEvalCommand = new Command('eval')
|
|
|
561
563
|
// instant error, not an upload plus an opaque browser-side failure.
|
|
562
564
|
if (opts.camera)
|
|
563
565
|
assertCameraFile(opts.camera);
|
|
566
|
+
// Set when --restore-db took a checkpoint; the finally below rolls the app
|
|
567
|
+
// database back to it, so a write-path smoke test leaves no residue in the
|
|
568
|
+
// real data the user is about to look at.
|
|
569
|
+
let restoreDb;
|
|
564
570
|
try {
|
|
571
|
+
if (opts.restoreDb) {
|
|
572
|
+
const { config } = await resolveProjectContext({});
|
|
573
|
+
projectGuid = config.projectGuid;
|
|
574
|
+
const database = await resolveDatabase(config.projectGuid);
|
|
575
|
+
if (!database) {
|
|
576
|
+
console.error(muted('--restore-db: this project has no database - nothing to roll back.'));
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
const cp = await createCheckpoint(config.projectGuid, database);
|
|
580
|
+
console.error(muted(`Checkpointed ${cp.tables.length} table(s), ${cp.rows} row(s) in '${database}' - rolled back when this eval finishes.`));
|
|
581
|
+
restoreDb = { projectGuid: config.projectGuid, database };
|
|
582
|
+
}
|
|
583
|
+
}
|
|
565
584
|
if (fixturePaths.length || opts.camera) {
|
|
566
585
|
const { config } = await resolveProjectContext({});
|
|
567
586
|
projectGuid = config.projectGuid;
|
|
@@ -715,6 +734,15 @@ export const pageEvalCommand = new Command('eval')
|
|
|
715
734
|
}
|
|
716
735
|
}
|
|
717
736
|
finally {
|
|
737
|
+
if (restoreDb) {
|
|
738
|
+
try {
|
|
739
|
+
const r = await restoreCheckpoint(restoreDb.projectGuid, restoreDb.database);
|
|
740
|
+
console.error(muted(`Rolled ${r.tables.length} table(s) back to the checkpoint (${r.rows} row(s)) - nothing this eval wrote survives.`));
|
|
741
|
+
}
|
|
742
|
+
catch (err) {
|
|
743
|
+
console.error(warning(`⚠ Could not roll back the database - the checkpoint is still there, retry with: gipity db restore (${err.message})`));
|
|
744
|
+
}
|
|
745
|
+
}
|
|
718
746
|
for (const h of [...hosted, ...(camera ? [camera] : [])]) {
|
|
719
747
|
try {
|
|
720
748
|
await deleteFixture(projectGuid, h.guid);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command, Option } from 'commander';
|
|
2
2
|
import { mkdirSync, writeFileSync } from 'fs';
|
|
3
|
-
import { dirname, join, resolve as resolvePath } from 'path';
|
|
3
|
+
import { dirname, extname, join, resolve as resolvePath } from 'path';
|
|
4
4
|
import { postForTarEntries } from '../api.js';
|
|
5
5
|
import { getConfig, getProjectRoot } from '../config.js';
|
|
6
6
|
import { getAuth } from '../auth.js';
|
|
@@ -171,6 +171,19 @@ function resolveDevice(name) {
|
|
|
171
171
|
const available = [...Object.keys(DEVICE_PRESETS), ...TOUCH_DEVICES].join(', ');
|
|
172
172
|
throw new Error(`Unknown --device preset: "${name}" (known: ${available})`);
|
|
173
173
|
}
|
|
174
|
+
/** Slug used to tell one viewport's `-o` file from another's: the device name the
|
|
175
|
+
* caller typed (`desktop`, `Pixel 9` → `pixel-9`) or the raw dimensions. */
|
|
176
|
+
function viewportSlug(name) {
|
|
177
|
+
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'shot';
|
|
178
|
+
}
|
|
179
|
+
/** Expand an `--output` path into one path per viewport by treating it as a stem:
|
|
180
|
+
* `tmp/shot.png` + `mobile` → `tmp/shot-mobile.png`. Keeps the caller's extension
|
|
181
|
+
* (defaulting to .png) so a multi-device capture writes every file it took. */
|
|
182
|
+
function stemPath(output, suffix) {
|
|
183
|
+
const ext = extname(output) || '.png';
|
|
184
|
+
const base = ext && output.endsWith(ext) ? output.slice(0, -ext.length) : output;
|
|
185
|
+
return `${base}-${suffix}${ext}`;
|
|
186
|
+
}
|
|
174
187
|
function splitCsv(values) {
|
|
175
188
|
if (!values || values.length === 0)
|
|
176
189
|
return [];
|
|
@@ -238,7 +251,7 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
238
251
|
.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
252
|
.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
253
|
.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
|
-
.option('-o, --output <file>', 'Output path (
|
|
254
|
+
.option('-o, --output <file>', 'Output path (default .gipity/screenshots/ss-<host>-<timestamp>.png). With several viewports it is a stem: -o tmp/shot.png --device desktop,mobile writes tmp/shot-desktop.png and tmp/shot-mobile.png.')
|
|
242
255
|
.option('--no-reload-between', 'Skip reload between viewports (faster, lower fidelity - only safe for static pages)')
|
|
243
256
|
.option('--fake-media', 'Grant a synthetic microphone + camera and auto-accept the getUserMedia prompt, so voice/camera apps render headlessly. The video feed is a built-in test pattern — to capture what the app does with a REAL frame (a hand, a face, an object), use --camera <path> instead.')
|
|
244
257
|
.option('--camera <path>', 'Play a local image or video (.png/.jpg/.webp/.mp4/.webm/.y4m/.mjpeg) as the browser\'s WEBCAM feed, then capture — so the shot shows the app reacting to a frame you chose (detected gesture, boxes, labels). Implies --fake-media.')
|
|
@@ -347,9 +360,12 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
347
360
|
...deviceNames.map(resolveDevice),
|
|
348
361
|
...viewportStrs.map(parseViewportString),
|
|
349
362
|
];
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
363
|
+
// Parallel to customViewports: what each capture is called when `--output` has
|
|
364
|
+
// to name more than one file.
|
|
365
|
+
const viewportLabels = [
|
|
366
|
+
...deviceNames.map(viewportSlug),
|
|
367
|
+
...viewportStrs.map(viewportSlug),
|
|
368
|
+
];
|
|
353
369
|
// Server defaults to 1280×720 when viewports is omitted - don't send it in
|
|
354
370
|
// the no-flag case so the filename stays unsuffixed (no viewport segment).
|
|
355
371
|
const userSpecifiedViewports = customViewports.length > 0;
|
|
@@ -437,9 +453,21 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
437
453
|
}
|
|
438
454
|
const dir = defaultScreenshotDir();
|
|
439
455
|
const savedFiles = [];
|
|
456
|
+
// With more than one viewport, `--output` is a stem: every capture gets its own
|
|
457
|
+
// file (shot.png → shot-desktop.png, shot-mobile.png) instead of overwriting.
|
|
458
|
+
const usedStems = new Set();
|
|
459
|
+
const outputPath = (i) => {
|
|
460
|
+
if (pngs.length === 1)
|
|
461
|
+
return opts.output;
|
|
462
|
+
let suffix = viewportLabels[i] ?? dimSuffix(meta.screenshots[i].viewport);
|
|
463
|
+
while (usedStems.has(suffix))
|
|
464
|
+
suffix += `-${i + 1}`;
|
|
465
|
+
usedStems.add(suffix);
|
|
466
|
+
return stemPath(opts.output, suffix);
|
|
467
|
+
};
|
|
440
468
|
for (let i = 0; i < pngs.length; i++) {
|
|
441
469
|
const target = opts.output
|
|
442
|
-
?
|
|
470
|
+
? outputPath(i)
|
|
443
471
|
: join(dir, names[i] ?? shotName(meta.screenshots[i].viewport));
|
|
444
472
|
// Create the target's parent dir so a `-o` path under a not-yet-existing
|
|
445
473
|
// directory (e.g. .gipity/screenshots/home.png) writes cleanly instead of
|
package/dist/commands/records.js
CHANGED
|
@@ -79,25 +79,37 @@ recordsCommand
|
|
|
79
79
|
}));
|
|
80
80
|
recordsCommand
|
|
81
81
|
.command('query <table>')
|
|
82
|
-
.description('List records')
|
|
83
|
-
.option('--
|
|
82
|
+
.description('List records (full-text search, filter, sort, recycle bin)')
|
|
83
|
+
.option('-q, --q <text>', 'Full-text search across the table\'s text columns (needs `--searchable true` on the table)')
|
|
84
|
+
.option('--filter <filter>', 'AND filter string (e.g., "status:eq:active")')
|
|
85
|
+
.option('--any <filter>', 'OR filter group, same grammar as --filter (e.g., "owner:eq:me,shared:eq:true")')
|
|
84
86
|
.option('--sort <sort>', 'Sort string (e.g., "created_at:desc")')
|
|
85
87
|
.option('--limit <n>', 'Max rows', '20')
|
|
86
88
|
.option('--offset <n>', 'Offset', '0')
|
|
87
89
|
.option('--fields <fields>', 'Comma-separated column names')
|
|
90
|
+
.option('--include-deleted', 'Include soft-deleted rows (owner/editor)')
|
|
91
|
+
.option('--only-deleted', 'Only soft-deleted rows - the recycle bin (owner/editor)')
|
|
88
92
|
.option(ANON_FLAG, ANON_HELP)
|
|
89
93
|
.option('--json', 'Output as JSON')
|
|
90
94
|
.action((table, opts) => run('Query', async () => {
|
|
91
95
|
const api = await recordsHttp(opts);
|
|
92
96
|
const params = new URLSearchParams();
|
|
97
|
+
if (opts.q)
|
|
98
|
+
params.set('q', opts.q);
|
|
93
99
|
if (opts.filter)
|
|
94
100
|
params.set('filter', opts.filter);
|
|
101
|
+
if (opts.any)
|
|
102
|
+
params.set('any', opts.any);
|
|
95
103
|
if (opts.sort)
|
|
96
104
|
params.set('sort', opts.sort);
|
|
97
105
|
params.set('limit', opts.limit);
|
|
98
106
|
params.set('offset', opts.offset);
|
|
99
107
|
if (opts.fields)
|
|
100
108
|
params.set('fields', opts.fields);
|
|
109
|
+
if (opts.includeDeleted)
|
|
110
|
+
params.set('include_deleted', '1');
|
|
111
|
+
if (opts.onlyDeleted)
|
|
112
|
+
params.set('only_deleted', '1');
|
|
101
113
|
const res = await api.get(`/api/${api.guid}/records/${table}?${params}`);
|
|
102
114
|
if (opts.json) {
|
|
103
115
|
console.log(JSON.stringify(res));
|
|
@@ -162,18 +174,38 @@ recordsCommand
|
|
|
162
174
|
const res = await api.put(`/api/${api.guid}/records/${table}/${id}`, data);
|
|
163
175
|
printResult(`Updated: ${JSON.stringify(res.data)}`, opts, res.data);
|
|
164
176
|
}));
|
|
177
|
+
// A table that declares `soft_delete_column` only STAMPS the row on delete - it
|
|
178
|
+
// stays queryable via `query --only-deleted` and keeps its audit history. That's
|
|
179
|
+
// right for user data and wrong for a probe row you just minted to test the
|
|
180
|
+
// wiring, so `--purge` (the server's ?purge=1) is the one call that really
|
|
181
|
+
// removes it. The plain success line says which of the two happened and names
|
|
182
|
+
// the escape hatch, so nobody has to go spelunking through gipity.js to find it.
|
|
165
183
|
recordsCommand
|
|
166
184
|
.command('delete <table> <id>')
|
|
167
|
-
.description('Delete a record')
|
|
185
|
+
.description('Delete a record (soft-delete when the table declares one; --purge removes it for good)')
|
|
186
|
+
.option('--purge', 'Hard-delete: remove the row AND erase its audit history (owner/editor). Use it to scrub probe rows.')
|
|
168
187
|
.option(ANON_FLAG, ANON_HELP)
|
|
169
188
|
.option('--json', 'Output as JSON')
|
|
170
189
|
.action((table, id, opts) => run('Delete', async () => {
|
|
171
|
-
|
|
190
|
+
const what = opts.purge ? `Purge record ${id} from "${table}" (also erases its history)?` : `Delete record ${id} from "${table}"?`;
|
|
191
|
+
if (!await confirm(what)) {
|
|
172
192
|
printResult('Cancelled.', opts, { table, id, deleted: false, cancelled: true });
|
|
173
193
|
return;
|
|
174
194
|
}
|
|
175
195
|
const api = await recordsHttp(opts);
|
|
176
|
-
const res = await api.del(`/api/${api.guid}/records/${table}/${id}`);
|
|
177
|
-
|
|
196
|
+
const res = await api.del(`/api/${api.guid}/records/${table}/${id}${opts.purge ? '?purge=1' : ''}`);
|
|
197
|
+
const data = res?.data ?? { table, id, deleted: true, purged: !!opts.purge };
|
|
198
|
+
printResult(data.purged
|
|
199
|
+
? 'Purged - the row and its audit history are gone.'
|
|
200
|
+
: `Deleted. If "${table}" declares a soft-delete column the row is only stamped deleted (see it with \`gipity records query ${table} --only-deleted\`, bring it back with \`gipity records restore ${table} ${id}\`, remove it for good with \`--purge\`).`, opts, data);
|
|
201
|
+
}));
|
|
202
|
+
recordsCommand
|
|
203
|
+
.command('restore <table> <id>')
|
|
204
|
+
.description('Un-delete a soft-deleted record (owner/editor)')
|
|
205
|
+
.option('--json', 'Output as JSON')
|
|
206
|
+
.action((table, id, opts) => run('Restore', async () => {
|
|
207
|
+
const api = await recordsHttp(opts);
|
|
208
|
+
const res = await api.post(`/api/${api.guid}/records/${table}/${id}/restore`);
|
|
209
|
+
printResult('Restored.', opts, res?.data ?? { table, id, restored: true });
|
|
178
210
|
}));
|
|
179
211
|
//# sourceMappingURL=records.js.map
|
package/dist/commands/service.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { get, post } from '../api.js';
|
|
2
|
+
import { get, post, publicRequest, mintAppToken, ApiError } from '../api.js';
|
|
3
|
+
import { getAuth } from '../auth.js';
|
|
3
4
|
import { requireConfig } from '../config.js';
|
|
4
5
|
import { bold, muted } from '../colors.js';
|
|
5
|
-
import { run } from '../helpers/index.js';
|
|
6
|
+
import { run, emitField, resolveBody } from '../helpers/index.js';
|
|
6
7
|
/** Known app-service endpoints, shown by `gipity service list` and used to
|
|
7
8
|
* give a helpful hint on typos. POST endpoints take a JSON body; GET
|
|
8
9
|
* endpoints (model/voice listings) are reached with `--get`. */
|
|
@@ -35,16 +36,41 @@ serviceCommand
|
|
|
35
36
|
const width = SERVICES.reduce((m, s) => Math.max(m, s.name.length), 0);
|
|
36
37
|
console.log('Call one with `gipity service call <name> \'{"json":"body"}\'`');
|
|
37
38
|
console.log(muted('(GET endpoints like llm/models, tts/voices take --get and no body)'));
|
|
39
|
+
console.log(muted('Verifying what a signed-out visitor of your deployed app gets? Add --anon.'));
|
|
38
40
|
console.log('');
|
|
39
41
|
for (const s of SERVICES) {
|
|
40
42
|
console.log(`${bold(s.name.padEnd(width))} ${muted(`[${s.method}] ${s.desc}`)}`);
|
|
41
43
|
}
|
|
42
44
|
}));
|
|
45
|
+
/** A signed-out visitor's 401 on a service is almost never "the caller needs to
|
|
46
|
+
* log in" — it is the app's service `billing_mode` still on the `user_pays`
|
|
47
|
+
* default, which asks every anonymous visitor of a PUBLIC page to sign in and
|
|
48
|
+
* pay from their own credits. The server's raw body says LOGIN_REQUIRED and
|
|
49
|
+
* nothing about the manifest, so the fix costs a doc hunt across skills. Name
|
|
50
|
+
* the cause and the exact edit here, at the moment it happens. */
|
|
51
|
+
export function anonBillingHint(name, err) {
|
|
52
|
+
if (!(err instanceof ApiError) || err.statusCode !== 401)
|
|
53
|
+
return null;
|
|
54
|
+
// Only the "sign in to use this" refusal is a billing-mode symptom. A missing
|
|
55
|
+
// credential (the visitor token never minted) is a different failure with a
|
|
56
|
+
// different fix, and must not be dressed up as one.
|
|
57
|
+
if (err.code !== 'LOGIN_REQUIRED')
|
|
58
|
+
return null;
|
|
59
|
+
const service = name.split('/')[0];
|
|
60
|
+
return (`An anonymous visitor cannot call '${service}': it is on the user_pays default, so every signed-out ` +
|
|
61
|
+
`visitor to a public page is asked to sign in and pay from their own credits. If this app serves the ` +
|
|
62
|
+
`public (a help bubble, a landing page), switch it to owner-pays in gipity.yaml and redeploy:\n` +
|
|
63
|
+
` deploy:\n phases:\n - services\n service_definitions:\n - service: ${service}\n billing_mode: owner_pays\n` +
|
|
64
|
+
` gipity deploy dev --only services`);
|
|
65
|
+
}
|
|
43
66
|
serviceCommand
|
|
44
67
|
.command('call <name> [body]')
|
|
45
|
-
.description('Call an app service by name (e.g. llm, image, music). Uses your logged-in session - no token wrangling.')
|
|
46
|
-
.option('--data <json>', 'JSON request body (alternative to the positional [body])')
|
|
68
|
+
.description('Call an app service by name (e.g. llm, image, music). Uses your logged-in session - no token wrangling; --anon calls it as a signed-out visitor instead.')
|
|
69
|
+
.option('-d, --data <json>', 'JSON request body: inline JSON, @file to read a file, or @- / - for stdin (alternative to the positional [body])')
|
|
70
|
+
.option('--file <field=@path>', 'Attach a file as { data, media_type } under <field> (the vision/media shape these services expect), repeatable, e.g. --file image=@receipt.png', (v, acc) => (acc || []).concat(v))
|
|
71
|
+
.option('--anon', 'Call as an anonymous visitor (the public path a signed-out user of your deployed app hits) instead of as your signed-in account')
|
|
47
72
|
.option('--get', 'Issue a GET (for listing endpoints like llm/models, tts/voices)')
|
|
73
|
+
.option('--field <path>', 'Print only this field of the result (dot path, e.g. choices.0.message.content)')
|
|
48
74
|
.option('--json', 'Output compact JSON')
|
|
49
75
|
.action((name, bodyArg, opts) => run('Call', async () => {
|
|
50
76
|
const config = requireConfig();
|
|
@@ -52,9 +78,47 @@ serviceCommand
|
|
|
52
78
|
// like `location/geocode` and `llm/models` resolve correctly.
|
|
53
79
|
const path = name.split('/').map(encodeURIComponent).join('/');
|
|
54
80
|
const url = `/api/${config.projectGuid}/services/${path}`;
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
81
|
+
const body = resolveBody(bodyArg || opts.data, opts.file);
|
|
82
|
+
// Name the calling identity (stderr, so stdout stays parseable). An owner
|
|
83
|
+
// call always succeeds regardless of billing mode, so without this line a
|
|
84
|
+
// "the service works" verification silently proves nothing about the
|
|
85
|
+
// visitors the deployed app actually serves.
|
|
86
|
+
if (opts.anon) {
|
|
87
|
+
console.error(muted('Auth: anonymous visitor (the public path a signed-out user of your deployed app hits)'));
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const who = getAuth()?.email;
|
|
91
|
+
console.error(muted(`Auth: calling as ${who ?? 'your signed-in account'} (the owner persona - always billed to you and never blocked by billing_mode; use --anon for the public visitor path)`));
|
|
92
|
+
}
|
|
93
|
+
let res;
|
|
94
|
+
try {
|
|
95
|
+
if (opts.anon) {
|
|
96
|
+
// The short-lived app token IS the visitor's credential here (unlike a
|
|
97
|
+
// public function, a service is never callable with no token at all).
|
|
98
|
+
// If it can't be minted, say so — otherwise the request comes back as a
|
|
99
|
+
// bare 401 that reads like an app-permissions problem.
|
|
100
|
+
const headers = await mintAppToken(config.projectGuid);
|
|
101
|
+
if (!headers) {
|
|
102
|
+
throw new Error(`Could not mint a visitor token for this project, so there is no anonymous path to call. ` +
|
|
103
|
+
`That means the app is not deployed on this API base yet: run \`gipity deploy dev\` first, ` +
|
|
104
|
+
`then re-run this command.`);
|
|
105
|
+
}
|
|
106
|
+
res = await publicRequest(opts.get ? 'GET' : 'POST', url, opts.get ? undefined : body, headers);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
res = opts.get ? await get(url) : await post(url, body);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
const hint = opts.anon ? anonBillingHint(name, err) : null;
|
|
114
|
+
if (!hint)
|
|
115
|
+
throw err;
|
|
116
|
+
throw new Error(`${err.message}\n${hint}`);
|
|
117
|
+
}
|
|
118
|
+
if (opts.field) {
|
|
119
|
+
emitField(res, opts.field);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
58
122
|
console.log(opts.json ? JSON.stringify(res) : JSON.stringify(res, null, 2));
|
|
59
123
|
}));
|
|
60
124
|
//# sourceMappingURL=service.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
1
|
+
import { Command, Option } from 'commander';
|
|
2
2
|
import { get, post, put, del } from '../api.js';
|
|
3
3
|
import { getConfig, requireConfig } from '../config.js';
|
|
4
4
|
import { success, error as clrError, muted, bold } from '../colors.js';
|
|
@@ -29,6 +29,51 @@ function formatRunLine(r) {
|
|
|
29
29
|
const statusColor = r.status === 'completed' ? success : r.status === 'failed' ? clrError : muted;
|
|
30
30
|
return `${muted(r.short_guid)} ${statusColor(r.status)} ${dur} ${runTokens(r)} tokens ${muted(fmtTime(r.started_at))}`;
|
|
31
31
|
}
|
|
32
|
+
/** Classify a step-output value that is a *string* holding JSON: parsed, or
|
|
33
|
+
* looks-like-JSON-but-doesn't-parse (a truncated / cut-off llm response — the
|
|
34
|
+
* step reports `completed` and the next step then can't read a field out of
|
|
35
|
+
* it). `null` means "not a JSON string at all". */
|
|
36
|
+
function jsonStringInfo(v) {
|
|
37
|
+
if (typeof v !== 'string')
|
|
38
|
+
return null;
|
|
39
|
+
const t = v.trim();
|
|
40
|
+
if (!t.startsWith('{') && !t.startsWith('['))
|
|
41
|
+
return null;
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(t);
|
|
44
|
+
return typeof parsed === 'object' && parsed !== null ? { ok: true, value: parsed } : null;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return { ok: false };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function labelFor(info) {
|
|
51
|
+
return info.ok ? '(JSON string)' : clrError('(truncated/invalid JSON string)');
|
|
52
|
+
}
|
|
53
|
+
/** Render one step's output for humans.
|
|
54
|
+
*
|
|
55
|
+
* Step outputs routinely carry a JSON *string* under a key — an `llm` step's
|
|
56
|
+
* `result` is the common case. Dumped through a plain JSON.stringify that
|
|
57
|
+
* arrives as one enormous backslash-escaped line: unreadable, and it hides the
|
|
58
|
+
* single fact you need most, that the NEXT step sees a string, not an object
|
|
59
|
+
* (so `{{step.summary}}` resolves to nothing). Label such keys
|
|
60
|
+
* `"key" (JSON string)` and print the decoded value indented under it, so both
|
|
61
|
+
* the content and the string-vs-object handoff are obvious at a glance. */
|
|
62
|
+
function renderStepOutput(output) {
|
|
63
|
+
if (output && typeof output === 'object' && !Array.isArray(output)) {
|
|
64
|
+
return Object.entries(output).map(([k, v]) => {
|
|
65
|
+
const info = jsonStringInfo(v);
|
|
66
|
+
if (!info)
|
|
67
|
+
return `${JSON.stringify(k)}: ${JSON.stringify(v, null, 2)}`;
|
|
68
|
+
const body = info.ok ? JSON.stringify(info.value, null, 2) : JSON.stringify(v);
|
|
69
|
+
return `${JSON.stringify(k)} ${labelFor(info)}: ${body}`;
|
|
70
|
+
}).join('\n');
|
|
71
|
+
}
|
|
72
|
+
const info = jsonStringInfo(output);
|
|
73
|
+
if (info?.ok)
|
|
74
|
+
return JSON.stringify(info.value, null, 2);
|
|
75
|
+
return JSON.stringify(output, null, 2);
|
|
76
|
+
}
|
|
32
77
|
/** Print each step's status, tokens, model, error and output. A run line alone
|
|
33
78
|
* says a run finished, not what it did — without the steps you can't tell a
|
|
34
79
|
* workflow that wrote a row from one that silently skipped every step. */
|
|
@@ -46,7 +91,7 @@ function printStepRuns(steps, emptyNote) {
|
|
|
46
91
|
if (s.error_message)
|
|
47
92
|
console.log(` ${clrError(s.error_message)}`);
|
|
48
93
|
if (s.output_json !== null && s.output_json !== undefined) {
|
|
49
|
-
console.log(
|
|
94
|
+
console.log(renderStepOutput(s.output_json).split('\n').map(l => ` ${l}`).join('\n'));
|
|
50
95
|
}
|
|
51
96
|
}
|
|
52
97
|
}
|
|
@@ -149,10 +194,18 @@ workflowCommand
|
|
|
149
194
|
}));
|
|
150
195
|
workflowCommand
|
|
151
196
|
.command('run <name>')
|
|
152
|
-
.description(
|
|
197
|
+
.description("Run a workflow: waits for it to finish and prints each step's status and output (--no-wait to fire and forget)")
|
|
153
198
|
.option('--json', 'Output as JSON')
|
|
154
|
-
|
|
155
|
-
|
|
199
|
+
// Waiting is the default: a bare trigger tells you nothing about what the
|
|
200
|
+
// workflow did, so every caller followed it with a poll-and-drill-in loop.
|
|
201
|
+
// `--no-wait` is the escape hatch for fire-and-forget.
|
|
202
|
+
.option('--no-wait', 'Trigger and return immediately instead of waiting for the run')
|
|
203
|
+
// Waiting is already the default, but `--wait` reads naturally and older
|
|
204
|
+
// docs/muscle memory reach for it — accept it silently rather than erroring.
|
|
205
|
+
.addOption(new Option('--wait', 'Wait for the run (already the default)').hideHelp())
|
|
206
|
+
// LLM steps routinely take a couple of minutes; a 120s default timed out on
|
|
207
|
+
// healthy runs and taught callers to always pass --timeout.
|
|
208
|
+
.option('--timeout <s>', 'Max seconds to wait for the run to finish', '300')
|
|
156
209
|
.action((name, _opts, cmd) => run('Run', async () => {
|
|
157
210
|
const opts = mergedOpts(cmd);
|
|
158
211
|
const wf = await resolveWorkflow(name);
|
|
@@ -168,7 +221,7 @@ workflowCommand
|
|
|
168
221
|
const before = await get(`/workflows/${wf.short_guid}/runs?limit=1`);
|
|
169
222
|
const prevGuid = before.data[0]?.short_guid;
|
|
170
223
|
await post(`/workflows/${wf.short_guid}/run`, {});
|
|
171
|
-
const r = await waitForRun(wf.short_guid, prevGuid, Number(opts.timeout) ||
|
|
224
|
+
const r = await waitForRun(wf.short_guid, prevGuid, Number(opts.timeout) || 300);
|
|
172
225
|
if (opts.json) {
|
|
173
226
|
console.log(JSON.stringify(r));
|
|
174
227
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database checkpoint / restore — the undo for a LIVE write test.
|
|
3
|
+
*
|
|
4
|
+
* Verifying a write path for real (clicking Approve in the deployed UI, calling
|
|
5
|
+
* a function that inserts, letting a scheduled workflow run) mutates the
|
|
6
|
+
* project's real data, and there was no way to put it back - agents ended up
|
|
7
|
+
* hand-writing SQL to scrub their own test strings out.
|
|
8
|
+
*
|
|
9
|
+
* The snapshot itself is the SERVER's job: it lives in a sibling schema the app
|
|
10
|
+
* can't see, and restore runs in one transaction. This module is just the
|
|
11
|
+
* client - the CLI never composes checkpoint DDL, so a snapshot can never show
|
|
12
|
+
* up in `db query`, in a migration diff, or in app code that enumerates tables.
|
|
13
|
+
*/
|
|
14
|
+
import { get, post } from './api.js';
|
|
15
|
+
/** First database in the project, or null when the project has none. */
|
|
16
|
+
export async function resolveDatabase(projectGuid, explicit) {
|
|
17
|
+
if (explicit)
|
|
18
|
+
return explicit;
|
|
19
|
+
const res = await get(`/projects/${projectGuid}/databases`);
|
|
20
|
+
return res.data.length > 0 ? res.data[0].friendlyName : null;
|
|
21
|
+
}
|
|
22
|
+
async function checkpoint(projectGuid, database, action, keep) {
|
|
23
|
+
const res = await post(`/projects/${projectGuid}/db/checkpoint`, { database, action, ...(keep ? { keep } : {}) });
|
|
24
|
+
return res.data;
|
|
25
|
+
}
|
|
26
|
+
/** Snapshot every app table. Replaces any previous checkpoint. */
|
|
27
|
+
export function createCheckpoint(projectGuid, database) {
|
|
28
|
+
return checkpoint(projectGuid, database, 'create');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Put every checkpointed table back to its snapshot contents and drop the
|
|
32
|
+
* snapshot (unless `keep`). Sequences are deliberately NOT rewound - leaving
|
|
33
|
+
* them ahead keeps ids unique against anything the test run handed out.
|
|
34
|
+
*/
|
|
35
|
+
export function restoreCheckpoint(projectGuid, database, opts = {}) {
|
|
36
|
+
return checkpoint(projectGuid, database, 'restore', opts.keep);
|
|
37
|
+
}
|
|
38
|
+
/** Drop the checkpoint without restoring (keep whatever the run wrote). */
|
|
39
|
+
export function dropCheckpoint(projectGuid, database) {
|
|
40
|
+
return checkpoint(projectGuid, database, 'drop');
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=db-checkpoint.js.map
|
package/dist/index.js
CHANGED
|
@@ -4002,9 +4002,10 @@ async function confirm(question, opts = {}) {
|
|
|
4002
4002
|
const defaultYes = opts.default === "yes";
|
|
4003
4003
|
if (opts.skip ?? _autoConfirm) return true;
|
|
4004
4004
|
if (!process.stdin.isTTY) {
|
|
4005
|
+
if (opts.headless === "no") return false;
|
|
4005
4006
|
console.error(`Confirmation required (non-interactive). Re-run with --yes:
|
|
4006
4007
|
${rerunWithYes()}`);
|
|
4007
|
-
|
|
4008
|
+
process.exit(1);
|
|
4008
4009
|
}
|
|
4009
4010
|
const hint = defaultYes ? dim("[Y/n]") : dim("[y/N]");
|
|
4010
4011
|
process.stdout.write(`${question} ${hint}: `);
|
|
@@ -32101,7 +32102,7 @@ Run \`gipity --help\` for the full list. Use \`--help\` on any command for detai
|
|
|
32101
32102
|
|
|
32102
32103
|
Function return shape: \`gipity fn call\`, the in-test \`ctx.fn.call\`/\`callAs\`, and the client \`Gipity.fn\` all return your function's value **unwrapped** - read/assert \`result.field\`. Only raw HTTP/\`curl\` wraps it as \`{ data: ... }\`; never write \`result.data.field\` in a test.
|
|
32103
32104
|
|
|
32104
|
-
Tests are isolated, not run against your live DB: \`gipity test\` points \`ctx.fn.call\`/\`callAs\` at a throwaway copy of your database (your \`migrations/\` + \`seeds/\`), reset (truncate + reseed) before
|
|
32105
|
+
Tests are isolated, not run against your live DB: \`gipity test\` points \`ctx.fn.call\`/\`callAs\` at a throwaway copy of your database (your \`migrations/\` + \`seeds/\`), one per test FILE, reset (truncate + reseed) before the file runs - so files can't interfere, test rows never reach the deployed app, and you don't write teardown. Functions see \`ctx.isTest === true\` during a run (use it to skip your own rate limiting); the platform also suppresses \`notify()\` push so a suite can't spam subscribers. Reference data tests need goes in seed files; a runtime-written settings table that isn't seeded goes under \`test.preserve\` in \`gipity.yaml\`. Build per-file fixtures in \`setup(fn)\` \u2192 \`ctx.fixtures\`.
|
|
32105
32106
|
|
|
32106
32107
|
## Hit friction on the platform? Report it in real time
|
|
32107
32108
|
|
|
@@ -32172,7 +32173,7 @@ App development skills:
|
|
|
32172
32173
|
- \`app-development\` - functions, database, and API
|
|
32173
32174
|
- \`app-import\` - import apps from GitHub/.gip bundles (incl. Vercel/Replit/Lovable porting) and export any project as a portable .gip - app_import tool, gipity save/load
|
|
32174
32175
|
- \`app-records\` - Gipity Records: declared tables \u2192 validated CRUD, provenance, workflows, auto UI
|
|
32175
|
-
- \`app-testing\` - testing deployed app functions (ctx.fn.call/callAs, the
|
|
32176
|
+
- \`app-testing\` - testing deployed app functions (ctx.fn.call/callAs, the per-file test DB)
|
|
32176
32177
|
- \`deploy\` - the deploy pipeline & gipity.yaml manifest
|
|
32177
32178
|
- \`jobs\` - long-running CPU + GPU compute jobs (Python / Node / bash)
|
|
32178
32179
|
- \`realtime-scheduled-app\` - recipe: realtime presence/messages + DB function + scheduled poster, end-to-end
|
|
@@ -35730,6 +35731,30 @@ init_colors();
|
|
|
35730
35731
|
init_auth();
|
|
35731
35732
|
init_config();
|
|
35732
35733
|
|
|
35734
|
+
// src/db-checkpoint.ts
|
|
35735
|
+
init_api();
|
|
35736
|
+
async function resolveDatabase(projectGuid2, explicit) {
|
|
35737
|
+
if (explicit) return explicit;
|
|
35738
|
+
const res = await get(`/projects/${projectGuid2}/databases`);
|
|
35739
|
+
return res.data.length > 0 ? res.data[0].friendlyName : null;
|
|
35740
|
+
}
|
|
35741
|
+
async function checkpoint(projectGuid2, database, action, keep) {
|
|
35742
|
+
const res = await post(
|
|
35743
|
+
`/projects/${projectGuid2}/db/checkpoint`,
|
|
35744
|
+
{ database, action, ...keep ? { keep } : {} }
|
|
35745
|
+
);
|
|
35746
|
+
return res.data;
|
|
35747
|
+
}
|
|
35748
|
+
function createCheckpoint(projectGuid2, database) {
|
|
35749
|
+
return checkpoint(projectGuid2, database, "create");
|
|
35750
|
+
}
|
|
35751
|
+
function restoreCheckpoint(projectGuid2, database, opts = {}) {
|
|
35752
|
+
return checkpoint(projectGuid2, database, "restore", opts.keep);
|
|
35753
|
+
}
|
|
35754
|
+
function dropCheckpoint(projectGuid2, database) {
|
|
35755
|
+
return checkpoint(projectGuid2, database, "drop");
|
|
35756
|
+
}
|
|
35757
|
+
|
|
35733
35758
|
// src/page-fixtures.ts
|
|
35734
35759
|
init_api();
|
|
35735
35760
|
init_config();
|
|
@@ -36010,7 +36035,7 @@ var pageEvalCommand = new Command("eval").description("Evaluate JS in a real bro
|
|
|
36010
36035
|
).option("--fake-media", "Grant a synthetic microphone + camera and auto-accept the getUserMedia prompt, so a voice/camera app runs headlessly instead of hitting its no-camera path. The feed is a built-in test pattern (and a tone) \u2014 nothing a vision model can recognize; to drive a vision app use --camera <path> instead.").option("--wait <ms>", "Sleep this many ms after DOMContentLoaded before evaluating (lets late async work settle; max 30000). With --wait-for, it elapses AFTER the selector appears - gate, then settle.", "500").option("--wait-for <selector>", `Wait until this CSS selector appears before evaluating, then evaluate (deterministic - beats guessing a --wait). Gate on the state you are ASSERTING, not just a ready flag: '#verdict:not(:empty)' returns the instant the round lands, where a fixed wait snapshots mid-sequence. Same flag on page inspect/screenshot. Max ${WAIT_FOR_MAX_MS}ms (--wait-timeout).`).option("--wait-timeout <ms>", `Max ms to wait for --wait-for before giving up (max ${WAIT_FOR_MAX_MS})`, "5000").option(
|
|
36011
36036
|
"--timeout <ms>",
|
|
36012
36037
|
`How long the script itself may run IN the page. Bare number = MILLISECONDS (so 90s = 90000, NOT 90); or pass an explicit unit that means the same on both this and \`sandbox run --timeout\` \u2014 --timeout 90s. Its own await/setTimeout pauses count (default ${EVAL_SCRIPT_BUDGET_MS}, ${EVAL_SCRIPT_BUDGET_CAMERA_MS} with --camera/--fake-media; floor 1000, max ${EVAL_SCRIPT_BUDGET_MAX_MS}). Raise it to trace a sequence that unfolds over time (a game round, an animation). Distinct from --wait, which only sleeps BEFORE the script.`
|
|
36013
|
-
).option("--auth", "Evaluate signed in as you (your Gipity account), so a page behind a Sign-in-with-Gipity login is reachable. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai. Without this flag the page loads as a genuinely anonymous, signed-out visitor \u2014 nothing carries over from earlier --auth runs.").option("--json", "Output as JSON").action((url, exprArg, opts) => run("Page eval", async () => {
|
|
36038
|
+
).option("--auth", "Evaluate signed in as you (your Gipity account), so a page behind a Sign-in-with-Gipity login is reachable. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai. Without this flag the page loads as a genuinely anonymous, signed-out visitor \u2014 nothing carries over from earlier --auth runs.").option("--restore-db", "Snapshot the app database before the script runs and roll it back after, so a write-path check (click Approve, submit the form, edit a row) leaves the real data untouched. Use this whenever the eval WRITES - it is the undo for driving the deployed app against real project data. Same snapshot/undo standalone: gipity db checkpoint / gipity db restore.").option("--json", "Output as JSON").action((url, exprArg, opts) => run("Page eval", async () => {
|
|
36014
36039
|
const decoy = JS_DECOY_FLAGS.find((f) => opts[f.slice(2)] !== void 0);
|
|
36015
36040
|
if (decoy) {
|
|
36016
36041
|
pageEvalCommand.error(
|
|
@@ -36091,7 +36116,20 @@ var pageEvalCommand = new Command("eval").description("Evaluate JS in a real bro
|
|
|
36091
36116
|
let sentExpr = expr;
|
|
36092
36117
|
let sentSteps = steps;
|
|
36093
36118
|
if (opts.camera) assertCameraFile(opts.camera);
|
|
36119
|
+
let restoreDb;
|
|
36094
36120
|
try {
|
|
36121
|
+
if (opts.restoreDb) {
|
|
36122
|
+
const { config } = await resolveProjectContext({});
|
|
36123
|
+
projectGuid2 = config.projectGuid;
|
|
36124
|
+
const database = await resolveDatabase(config.projectGuid);
|
|
36125
|
+
if (!database) {
|
|
36126
|
+
console.error(muted("--restore-db: this project has no database - nothing to roll back."));
|
|
36127
|
+
} else {
|
|
36128
|
+
const cp2 = await createCheckpoint(config.projectGuid, database);
|
|
36129
|
+
console.error(muted(`Checkpointed ${cp2.tables.length} table(s), ${cp2.rows} row(s) in '${database}' - rolled back when this eval finishes.`));
|
|
36130
|
+
restoreDb = { projectGuid: config.projectGuid, database };
|
|
36131
|
+
}
|
|
36132
|
+
}
|
|
36095
36133
|
if (fixturePaths.length || opts.camera) {
|
|
36096
36134
|
const { config } = await resolveProjectContext({});
|
|
36097
36135
|
projectGuid2 = config.projectGuid;
|
|
@@ -36211,6 +36249,14 @@ ${bold("After reload")} ${muted("(page reloaded in place \u2014 storage preserve
|
|
|
36211
36249
|
if (d.reloadTruncated) console.log(muted("(reload result truncated to fit context - narrow the expression for the full value)"));
|
|
36212
36250
|
}
|
|
36213
36251
|
} finally {
|
|
36252
|
+
if (restoreDb) {
|
|
36253
|
+
try {
|
|
36254
|
+
const r = await restoreCheckpoint(restoreDb.projectGuid, restoreDb.database);
|
|
36255
|
+
console.error(muted(`Rolled ${r.tables.length} table(s) back to the checkpoint (${r.rows} row(s)) - nothing this eval wrote survives.`));
|
|
36256
|
+
} catch (err) {
|
|
36257
|
+
console.error(warning(`\u26A0 Could not roll back the database - the checkpoint is still there, retry with: gipity db restore (${err.message})`));
|
|
36258
|
+
}
|
|
36259
|
+
}
|
|
36214
36260
|
for (const h of [...hosted, ...camera ? [camera] : []]) {
|
|
36215
36261
|
try {
|
|
36216
36262
|
await deleteFixture(projectGuid2, h.guid);
|
|
@@ -36307,7 +36353,7 @@ init_auth();
|
|
|
36307
36353
|
init_colors();
|
|
36308
36354
|
init_utils();
|
|
36309
36355
|
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
|
|
36310
|
-
import { dirname as dirname7, join as join12, resolve as resolvePath } from "path";
|
|
36356
|
+
import { dirname as dirname7, extname as extname4, join as join12, resolve as resolvePath } from "path";
|
|
36311
36357
|
var DEVICE_PRESETS = {
|
|
36312
36358
|
default: { width: 1280, height: 720 },
|
|
36313
36359
|
desktop: { width: 1920, height: 1080 },
|
|
@@ -36418,6 +36464,14 @@ function resolveDevice(name) {
|
|
|
36418
36464
|
const available = [...Object.keys(DEVICE_PRESETS), ...TOUCH_DEVICES].join(", ");
|
|
36419
36465
|
throw new Error(`Unknown --device preset: "${name}" (known: ${available})`);
|
|
36420
36466
|
}
|
|
36467
|
+
function viewportSlug(name) {
|
|
36468
|
+
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "shot";
|
|
36469
|
+
}
|
|
36470
|
+
function stemPath(output, suffix) {
|
|
36471
|
+
const ext = extname4(output) || ".png";
|
|
36472
|
+
const base = ext && output.endsWith(ext) ? output.slice(0, -ext.length) : output;
|
|
36473
|
+
return `${base}-${suffix}${ext}`;
|
|
36474
|
+
}
|
|
36421
36475
|
function splitCsv(values) {
|
|
36422
36476
|
if (!values || values.length === 0) return [];
|
|
36423
36477
|
return values.flatMap((v7) => v7.split(",").map((s) => s.trim()).filter(Boolean));
|
|
@@ -36434,7 +36488,7 @@ var WAIT_FOR_MAX_MS2 = 15e3;
|
|
|
36434
36488
|
var WAIT_FOR_DEFAULT_MS = 15e3;
|
|
36435
36489
|
var MAX_POST_LOAD_DELAY_MS = 3e4;
|
|
36436
36490
|
var CAMERA_DEFAULT_DELAY_MS = 15e3;
|
|
36437
|
-
var pageScreenshotCommand = new Command("screenshot").description("Screenshot a web page").argument("<url>", "URL to screenshot").option("--device <names>", `Device preset(s): ${Object.keys(DEVICE_PRESETS).join(", ")} (comma-separated or repeat flag). mobile/tablet emulate a real touch device \u2014 touch events, mobile user-agent, DPR \u2014 so touch-gated mobile UI actually renders.`, appendOption, []).option("--viewport <dims>", "Raw viewport(s): WxH or WxH@dpr (comma-separated or repeat flag), e.g. --viewport 390x844 for a phone-sized window", appendOption, []).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.`).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_MS2}ms (--wait-timeout).`).option("--wait-timeout <ms>", `Max ms to wait for --wait-for before giving up (default ${WAIT_FOR_DEFAULT_MS}, max ${WAIT_FOR_MAX_MS2}). Timing out is reported - the capture still happens, so you see the state it got stuck in.`).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.`).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.").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.").option("-o, --output <file>", "Output path (
|
|
36491
|
+
var pageScreenshotCommand = new Command("screenshot").description("Screenshot a web page").argument("<url>", "URL to screenshot").option("--device <names>", `Device preset(s): ${Object.keys(DEVICE_PRESETS).join(", ")} (comma-separated or repeat flag). mobile/tablet emulate a real touch device \u2014 touch events, mobile user-agent, DPR \u2014 so touch-gated mobile UI actually renders.`, appendOption, []).option("--viewport <dims>", "Raw viewport(s): WxH or WxH@dpr (comma-separated or repeat flag), e.g. --viewport 390x844 for a phone-sized window", appendOption, []).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.`).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_MS2}ms (--wait-timeout).`).option("--wait-timeout <ms>", `Max ms to wait for --wait-for before giving up (default ${WAIT_FOR_DEFAULT_MS}, max ${WAIT_FOR_MAX_MS2}). Timing out is reported - the capture still happens, so you see the state it got stuck in.`).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.`).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.").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.").option("-o, --output <file>", "Output path (default .gipity/screenshots/ss-<host>-<timestamp>.png). With several viewports it is a stem: -o tmp/shot.png --device desktop,mobile writes tmp/shot-desktop.png and tmp/shot-mobile.png.").option("--no-reload-between", "Skip reload between viewports (faster, lower fidelity - only safe for static pages)").option("--fake-media", "Grant a synthetic microphone + camera and auto-accept the getUserMedia prompt, so voice/camera apps render headlessly. The video feed is a built-in test pattern \u2014 to capture what the app does with a REAL frame (a hand, a face, an object), use --camera <path> instead.").option("--camera <path>", "Play a local image or video (.png/.jpg/.webp/.mp4/.webm/.y4m/.mjpeg) as the browser's WEBCAM feed, then capture \u2014 so the shot shows the app reacting to a frame you chose (detected gesture, boxes, labels). Implies --fake-media.").option("--auth", "Capture the page signed in as you (your Gipity account), so UI behind a Sign-in-with-Gipity login is shown. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai. Without this flag the page loads as a genuinely anonymous, signed-out visitor \u2014 nothing carries over from earlier --auth runs.").option("--ephemeral", "Skip the project screenshot history: do not persist this capture to Gipity (screenshots/ in the project). Local file is still written.").option("--json", "Output JSON metadata instead of a friendly summary").addOption(new Option("--post-load-delay <ms>", "Alias for --wait").hideHelp()).addOption(new Option("--full-page", "Alias for --full").hideHelp()).addOption(new Option("--width <px>", "Alias: --viewport WxH").hideHelp()).addOption(new Option("--height <px>", "Alias: --viewport WxH").hideHelp()).action((url, opts) => run("Page screenshot", async () => {
|
|
36438
36492
|
const aliasFlag = ACTION_ALIAS_FLAGS.find((f) => opts[f.slice(2)] !== void 0);
|
|
36439
36493
|
if (aliasFlag) {
|
|
36440
36494
|
console.error(muted(
|
|
@@ -36504,9 +36558,10 @@ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a
|
|
|
36504
36558
|
...deviceNames.map(resolveDevice),
|
|
36505
36559
|
...viewportStrs.map(parseViewportString)
|
|
36506
36560
|
];
|
|
36507
|
-
|
|
36508
|
-
|
|
36509
|
-
|
|
36561
|
+
const viewportLabels = [
|
|
36562
|
+
...deviceNames.map(viewportSlug),
|
|
36563
|
+
...viewportStrs.map(viewportSlug)
|
|
36564
|
+
];
|
|
36510
36565
|
const userSpecifiedViewports = customViewports.length > 0;
|
|
36511
36566
|
const slug = slugFromUrl(url);
|
|
36512
36567
|
const ts2 = timestampSlug();
|
|
@@ -36565,8 +36620,16 @@ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a
|
|
|
36565
36620
|
}
|
|
36566
36621
|
const dir = defaultScreenshotDir();
|
|
36567
36622
|
const savedFiles = [];
|
|
36623
|
+
const usedStems = /* @__PURE__ */ new Set();
|
|
36624
|
+
const outputPath = (i) => {
|
|
36625
|
+
if (pngs.length === 1) return opts.output;
|
|
36626
|
+
let suffix = viewportLabels[i] ?? dimSuffix(meta.screenshots[i].viewport);
|
|
36627
|
+
while (usedStems.has(suffix)) suffix += `-${i + 1}`;
|
|
36628
|
+
usedStems.add(suffix);
|
|
36629
|
+
return stemPath(opts.output, suffix);
|
|
36630
|
+
};
|
|
36568
36631
|
for (let i = 0; i < pngs.length; i++) {
|
|
36569
|
-
const target = opts.output ?
|
|
36632
|
+
const target = opts.output ? outputPath(i) : join12(dir, names[i] ?? shotName(meta.screenshots[i].viewport));
|
|
36570
36633
|
mkdirSync7(dirname7(target), { recursive: true });
|
|
36571
36634
|
writeFileSync8(target, pngs[i].buffer);
|
|
36572
36635
|
savedFiles.push(resolvePath(target));
|
|
@@ -37111,6 +37174,47 @@ dbCommand.command("drop <name>").description("Drop a database").option("--projec
|
|
|
37111
37174
|
console.log(success(`Dropped database '${name}'.`));
|
|
37112
37175
|
}
|
|
37113
37176
|
}));
|
|
37177
|
+
dbCommand.command("checkpoint").description("Snapshot every table so a write test can be undone. Take one before exercising a real write path (page eval on the deployed app, fn call, workflow run), then `gipity db restore` to put the data back exactly as it was - no hand-written SQL to scrub test strings out of real content.").option("--database <name>", "Database name").option("--drop", "Discard the existing checkpoint without restoring (keep whatever the run wrote)").option("--json", "Output as JSON").action((opts) => run("Checkpoint", async () => {
|
|
37178
|
+
const config = requireConfig();
|
|
37179
|
+
const dbName = await resolveDatabase(config.projectGuid, opts.database);
|
|
37180
|
+
if (!dbName) {
|
|
37181
|
+
console.error(error("No databases in this project - nothing to checkpoint."));
|
|
37182
|
+
process.exit(1);
|
|
37183
|
+
}
|
|
37184
|
+
if (opts.drop) {
|
|
37185
|
+
const dropped = await dropCheckpoint(config.projectGuid, dbName);
|
|
37186
|
+
if (opts.json) {
|
|
37187
|
+
console.log(JSON.stringify(dropped));
|
|
37188
|
+
} else {
|
|
37189
|
+
console.log(dropped.tables.length === 0 ? `No checkpoint in '${dbName}'.` : success(`Discarded the checkpoint of ${dropped.tables.length} table(s) in '${dbName}'. Current data kept.`));
|
|
37190
|
+
}
|
|
37191
|
+
return;
|
|
37192
|
+
}
|
|
37193
|
+
const res = await createCheckpoint(config.projectGuid, dbName);
|
|
37194
|
+
if (opts.json) {
|
|
37195
|
+
console.log(JSON.stringify(res));
|
|
37196
|
+
} else if (res.tables.length === 0) {
|
|
37197
|
+
console.log(`Database '${dbName}' has no tables - nothing to checkpoint.`);
|
|
37198
|
+
} else {
|
|
37199
|
+
console.log(success(`Checkpointed ${res.tables.length} table(s), ${res.rows} row(s) in '${dbName}'.`));
|
|
37200
|
+
console.log("Undo everything written since: gipity db restore");
|
|
37201
|
+
}
|
|
37202
|
+
}));
|
|
37203
|
+
dbCommand.command("restore").description("Roll every table back to the last `gipity db checkpoint` and drop the checkpoint. The undo for a live write test.").option("--database <name>", "Database name").option("--keep", "Keep the checkpoint after restoring, so you can run the same write test again").option("--json", "Output as JSON").action((opts) => run("Restore", async () => {
|
|
37204
|
+
const config = requireConfig();
|
|
37205
|
+
const dbName = await resolveDatabase(config.projectGuid, opts.database);
|
|
37206
|
+
if (!dbName) {
|
|
37207
|
+
console.error(error("No databases in this project - nothing to restore."));
|
|
37208
|
+
process.exit(1);
|
|
37209
|
+
}
|
|
37210
|
+
const res = await restoreCheckpoint(config.projectGuid, dbName, { keep: opts.keep });
|
|
37211
|
+
if (opts.json) {
|
|
37212
|
+
console.log(JSON.stringify(res));
|
|
37213
|
+
} else {
|
|
37214
|
+
console.log(success(`Restored ${res.tables.length} table(s) to the checkpoint (${res.rows} row(s)) in '${dbName}'.`));
|
|
37215
|
+
if (opts.keep) console.log("Checkpoint kept - restore again any time.");
|
|
37216
|
+
}
|
|
37217
|
+
}));
|
|
37114
37218
|
|
|
37115
37219
|
// src/commands/memory.ts
|
|
37116
37220
|
init_api();
|
|
@@ -37165,7 +37269,7 @@ memoryCommand.command("delete <topic>").description("Delete a topic").option("--
|
|
|
37165
37269
|
init_api();
|
|
37166
37270
|
init_config();
|
|
37167
37271
|
import { readFileSync as readFileSync15, existsSync as existsSync11, statSync as statSync7, mkdirSync as mkdirSync8, writeFileSync as writeFileSync9 } from "fs";
|
|
37168
|
-
import { dirname as dirname8, extname as
|
|
37272
|
+
import { dirname as dirname8, extname as extname5, relative as relative4, resolve as resolve11, sep as sep3 } from "path";
|
|
37169
37273
|
init_colors();
|
|
37170
37274
|
var LANG_MAP = {
|
|
37171
37275
|
js: "javascript",
|
|
@@ -37229,7 +37333,7 @@ function resolveLanguage(opts) {
|
|
|
37229
37333
|
process.exit(1);
|
|
37230
37334
|
}
|
|
37231
37335
|
if (explicit) return explicit;
|
|
37232
|
-
const fromExt = opts.filePath ? LANG_MAP[
|
|
37336
|
+
const fromExt = opts.filePath ? LANG_MAP[extname5(opts.filePath).slice(1).toLowerCase()] : void 0;
|
|
37233
37337
|
if (fromExt) return fromExt;
|
|
37234
37338
|
if (opts.filePath) {
|
|
37235
37339
|
console.error(error(`Cannot infer the language of ${opts.filePath} from its extension (expected .js, .py, or .sh).`));
|
|
@@ -37940,6 +38044,33 @@ function formatRunLine(r) {
|
|
|
37940
38044
|
const statusColor = r.status === "completed" ? success : r.status === "failed" ? error : muted;
|
|
37941
38045
|
return `${muted(r.short_guid)} ${statusColor(r.status)} ${dur} ${runTokens(r)} tokens ${muted(fmtTime(r.started_at))}`;
|
|
37942
38046
|
}
|
|
38047
|
+
function jsonStringInfo(v7) {
|
|
38048
|
+
if (typeof v7 !== "string") return null;
|
|
38049
|
+
const t = v7.trim();
|
|
38050
|
+
if (!t.startsWith("{") && !t.startsWith("[")) return null;
|
|
38051
|
+
try {
|
|
38052
|
+
const parsed = JSON.parse(t);
|
|
38053
|
+
return typeof parsed === "object" && parsed !== null ? { ok: true, value: parsed } : null;
|
|
38054
|
+
} catch {
|
|
38055
|
+
return { ok: false };
|
|
38056
|
+
}
|
|
38057
|
+
}
|
|
38058
|
+
function labelFor(info2) {
|
|
38059
|
+
return info2.ok ? "(JSON string)" : error("(truncated/invalid JSON string)");
|
|
38060
|
+
}
|
|
38061
|
+
function renderStepOutput(output) {
|
|
38062
|
+
if (output && typeof output === "object" && !Array.isArray(output)) {
|
|
38063
|
+
return Object.entries(output).map(([k7, v7]) => {
|
|
38064
|
+
const info3 = jsonStringInfo(v7);
|
|
38065
|
+
if (!info3) return `${JSON.stringify(k7)}: ${JSON.stringify(v7, null, 2)}`;
|
|
38066
|
+
const body = info3.ok ? JSON.stringify(info3.value, null, 2) : JSON.stringify(v7);
|
|
38067
|
+
return `${JSON.stringify(k7)} ${labelFor(info3)}: ${body}`;
|
|
38068
|
+
}).join("\n");
|
|
38069
|
+
}
|
|
38070
|
+
const info2 = jsonStringInfo(output);
|
|
38071
|
+
if (info2?.ok) return JSON.stringify(info2.value, null, 2);
|
|
38072
|
+
return JSON.stringify(output, null, 2);
|
|
38073
|
+
}
|
|
37943
38074
|
function printStepRuns(steps, emptyNote) {
|
|
37944
38075
|
if (steps.length === 0) {
|
|
37945
38076
|
console.log(` ${muted(emptyNote)}`);
|
|
@@ -37952,7 +38083,7 @@ function printStepRuns(steps, emptyNote) {
|
|
|
37952
38083
|
console.log(` ${s.step_order}. ${name}${statusColor(s.status)} ${s.tokens_used ?? 0} tokens${model}`);
|
|
37953
38084
|
if (s.error_message) console.log(` ${error(s.error_message)}`);
|
|
37954
38085
|
if (s.output_json !== null && s.output_json !== void 0) {
|
|
37955
|
-
console.log(
|
|
38086
|
+
console.log(renderStepOutput(s.output_json).split("\n").map((l7) => ` ${l7}`).join("\n"));
|
|
37956
38087
|
}
|
|
37957
38088
|
}
|
|
37958
38089
|
}
|
|
@@ -38023,7 +38154,7 @@ workflowCommand.command("info <name>").description("Show workflow details").opti
|
|
|
38023
38154
|
}
|
|
38024
38155
|
}
|
|
38025
38156
|
}));
|
|
38026
|
-
workflowCommand.command("run <name>").description("
|
|
38157
|
+
workflowCommand.command("run <name>").description("Run a workflow: waits for it to finish and prints each step's status and output (--no-wait to fire and forget)").option("--json", "Output as JSON").option("--no-wait", "Trigger and return immediately instead of waiting for the run").addOption(new Option("--wait", "Wait for the run (already the default)").hideHelp()).option("--timeout <s>", "Max seconds to wait for the run to finish", "300").action((name, _opts, cmd) => run("Run", async () => {
|
|
38027
38158
|
const opts = mergedOpts(cmd);
|
|
38028
38159
|
const wf2 = await resolveWorkflow(name);
|
|
38029
38160
|
if (!opts.wait) {
|
|
@@ -38034,7 +38165,7 @@ workflowCommand.command("run <name>").description("Trigger a workflow (add --wai
|
|
|
38034
38165
|
const before = await get(`/workflows/${wf2.short_guid}/runs?limit=1`);
|
|
38035
38166
|
const prevGuid = before.data[0]?.short_guid;
|
|
38036
38167
|
await post(`/workflows/${wf2.short_guid}/run`, {});
|
|
38037
|
-
const r = await waitForRun(wf2.short_guid, prevGuid, Number(opts.timeout) ||
|
|
38168
|
+
const r = await waitForRun(wf2.short_guid, prevGuid, Number(opts.timeout) || 300);
|
|
38038
38169
|
if (opts.json) {
|
|
38039
38170
|
console.log(JSON.stringify(r));
|
|
38040
38171
|
} else {
|
|
@@ -39046,7 +39177,7 @@ async function runRelaySetup(opts) {
|
|
|
39046
39177
|
}
|
|
39047
39178
|
console.log("");
|
|
39048
39179
|
const promptText = opts.mode === "run-now" ? " Set up remote control on this computer" : " Enable remote control";
|
|
39049
|
-
const enable = await confirm(promptText, { default: "yes" });
|
|
39180
|
+
const enable = await confirm(promptText, { default: "yes", headless: "no" });
|
|
39050
39181
|
if (!enable) {
|
|
39051
39182
|
setRelayEnabled(false);
|
|
39052
39183
|
console.log(` ${muted("Skipped. Turn on later with")} ${brand("gipity connect")}${muted(".")}`);
|
|
@@ -39073,7 +39204,7 @@ async function runRelaySetup(opts) {
|
|
|
39073
39204
|
}
|
|
39074
39205
|
startDaemon();
|
|
39075
39206
|
console.log("");
|
|
39076
|
-
const autostartOs = await confirm(" Also start at OS login", { default: "yes" });
|
|
39207
|
+
const autostartOs = await confirm(" Also start at OS login", { default: "yes", headless: "no" });
|
|
39077
39208
|
if (autostartOs) {
|
|
39078
39209
|
try {
|
|
39079
39210
|
const res = installAutostart();
|
|
@@ -39096,7 +39227,7 @@ async function runRelaySetup(opts) {
|
|
|
39096
39227
|
}
|
|
39097
39228
|
if (getDiagnosticsConsent() === void 0) {
|
|
39098
39229
|
console.log("");
|
|
39099
|
-
const diag = await confirm(" Share anonymous diagnostics info", { default: "yes" });
|
|
39230
|
+
const diag = await confirm(" Share anonymous diagnostics info", { default: "yes", headless: "no" });
|
|
39100
39231
|
setDiagnosticsConsent(diag);
|
|
39101
39232
|
}
|
|
39102
39233
|
console.log("");
|
|
@@ -40623,9 +40754,9 @@ var STARTERS = [
|
|
|
40623
40754
|
];
|
|
40624
40755
|
var BLANK = [
|
|
40625
40756
|
{ key: "web-simple", hint: "static frontend-only site - pages, dashboards, simple games" },
|
|
40626
|
-
{ key: "web-fullstack", hint: "backend API + database wiring - frontend, functions, migrations; deploys green" },
|
|
40757
|
+
{ key: "web-fullstack", hint: "backend API + database wiring - frontend, functions, migrations; deploys green empty" },
|
|
40627
40758
|
{ key: "3d-engine", hint: "3D multiplayer wiring - Three.js + Rapier + Gipity Realtime" },
|
|
40628
|
-
{ key: "api", hint: "pure API backend, no frontend -
|
|
40759
|
+
{ key: "api", hint: "pure API backend, no frontend - functions + tests, deploys green" }
|
|
40629
40760
|
];
|
|
40630
40761
|
var KITS = [
|
|
40631
40762
|
{ key: "realtime", hint: "multiplayer / presence / shared state" },
|
|
@@ -41402,7 +41533,7 @@ async function runInteractive(url, observe, opts) {
|
|
|
41402
41533
|
const samples = Math.min(30, Math.max(2, parseInt(opts.samples, 10) || 6));
|
|
41403
41534
|
const settle = opts.waitFor ? 200 : 1e3;
|
|
41404
41535
|
const labels = (opts.labels ? String(opts.labels).split(",").map((s) => s.trim()) : []).filter(Boolean);
|
|
41405
|
-
const
|
|
41536
|
+
const labelFor2 = (i) => labels[i] ?? `client-${i}`;
|
|
41406
41537
|
warnUnknownTokens(unknownTokens(url, opts.action, observe));
|
|
41407
41538
|
if (!opts.json) {
|
|
41408
41539
|
console.log(`${brand("Page test")} ${muted("(interactive)")} ${bold(url)}`);
|
|
@@ -41412,16 +41543,16 @@ async function runInteractive(url, observe, opts) {
|
|
|
41412
41543
|
for (let i = 0; i < clients; i++) {
|
|
41413
41544
|
runs.push((async () => {
|
|
41414
41545
|
await sleep3(i * stagger * 1e3);
|
|
41415
|
-
if (!opts.json) console.log(muted(`client ${i} (${
|
|
41416
|
-
const clientUrl = subst(url,
|
|
41546
|
+
if (!opts.json) console.log(muted(`client ${i} (${labelFor2(i)}) joining`));
|
|
41547
|
+
const clientUrl = subst(url, labelFor2(i), i);
|
|
41417
41548
|
const expr = buildHarness(
|
|
41418
|
-
opts.action ? subst(opts.action,
|
|
41419
|
-
subst(observe,
|
|
41420
|
-
|
|
41549
|
+
opts.action ? subst(opts.action, labelFor2(i), i) : void 0,
|
|
41550
|
+
subst(observe, labelFor2(i), i),
|
|
41551
|
+
labelFor2(i),
|
|
41421
41552
|
hold,
|
|
41422
41553
|
samples
|
|
41423
41554
|
);
|
|
41424
|
-
return observeClient(clientUrl, expr, i,
|
|
41555
|
+
return observeClient(clientUrl, expr, i, labelFor2(i), settle, hold, opts.waitFor);
|
|
41425
41556
|
})());
|
|
41426
41557
|
}
|
|
41427
41558
|
const results = (await Promise.all(runs)).sort((a, b7) => a.i - b7.i);
|
|
@@ -41475,7 +41606,7 @@ async function runPassive(url, opts) {
|
|
|
41475
41606
|
const stagger = opts.stagger != null ? Math.max(0, parseInt(opts.stagger, 10) || 0) : 12;
|
|
41476
41607
|
const wait = Math.min(3e4, Math.max(2e3, parseInt(opts.wait, 10) || 24e3));
|
|
41477
41608
|
const labels = (opts.labels ? String(opts.labels).split(",").map((s) => s.trim()) : []).filter(Boolean);
|
|
41478
|
-
const
|
|
41609
|
+
const labelFor2 = (i) => labels[i] ?? `client-${i}`;
|
|
41479
41610
|
warnUnknownTokens(unknownTokens(url));
|
|
41480
41611
|
if (!opts.json) {
|
|
41481
41612
|
console.log(`${brand("Page test")} ${bold(url)}`);
|
|
@@ -41486,7 +41617,7 @@ async function runPassive(url, opts) {
|
|
|
41486
41617
|
runs.push((async () => {
|
|
41487
41618
|
await sleep3(i * stagger * 1e3);
|
|
41488
41619
|
if (!opts.json) console.log(`${muted(`client ${i}${i === 0 ? " (first)" : ""} starting`)}`);
|
|
41489
|
-
return inspectClient(subst(url,
|
|
41620
|
+
return inspectClient(subst(url, labelFor2(i), i), wait, i);
|
|
41490
41621
|
})());
|
|
41491
41622
|
}
|
|
41492
41623
|
const results = (await Promise.all(runs)).sort((a, b7) => a.i - b7.i);
|
|
@@ -41736,14 +41867,18 @@ recordsCommand.command("config <table>").description("Show or set a table's Reco
|
|
|
41736
41867
|
res.data
|
|
41737
41868
|
);
|
|
41738
41869
|
}));
|
|
41739
|
-
recordsCommand.command("query <table>").description("List records").option("--filter <filter>", '
|
|
41870
|
+
recordsCommand.command("query <table>").description("List records (full-text search, filter, sort, recycle bin)").option("-q, --q <text>", "Full-text search across the table's text columns (needs `--searchable true` on the table)").option("--filter <filter>", 'AND filter string (e.g., "status:eq:active")').option("--any <filter>", 'OR filter group, same grammar as --filter (e.g., "owner:eq:me,shared:eq:true")').option("--sort <sort>", 'Sort string (e.g., "created_at:desc")').option("--limit <n>", "Max rows", "20").option("--offset <n>", "Offset", "0").option("--fields <fields>", "Comma-separated column names").option("--include-deleted", "Include soft-deleted rows (owner/editor)").option("--only-deleted", "Only soft-deleted rows - the recycle bin (owner/editor)").option(ANON_FLAG, ANON_HELP).option("--json", "Output as JSON").action((table, opts) => run("Query", async () => {
|
|
41740
41871
|
const api = await recordsHttp(opts);
|
|
41741
41872
|
const params = new URLSearchParams();
|
|
41873
|
+
if (opts.q) params.set("q", opts.q);
|
|
41742
41874
|
if (opts.filter) params.set("filter", opts.filter);
|
|
41875
|
+
if (opts.any) params.set("any", opts.any);
|
|
41743
41876
|
if (opts.sort) params.set("sort", opts.sort);
|
|
41744
41877
|
params.set("limit", opts.limit);
|
|
41745
41878
|
params.set("offset", opts.offset);
|
|
41746
41879
|
if (opts.fields) params.set("fields", opts.fields);
|
|
41880
|
+
if (opts.includeDeleted) params.set("include_deleted", "1");
|
|
41881
|
+
if (opts.onlyDeleted) params.set("only_deleted", "1");
|
|
41747
41882
|
const res = await api.get(
|
|
41748
41883
|
`/api/${api.guid}/records/${table}?${params}`
|
|
41749
41884
|
);
|
|
@@ -41786,14 +41921,27 @@ recordsCommand.command("update <table> <id>").description("Update a record").req
|
|
|
41786
41921
|
const res = await api.put(`/api/${api.guid}/records/${table}/${id2}`, data);
|
|
41787
41922
|
printResult(`Updated: ${JSON.stringify(res.data)}`, opts, res.data);
|
|
41788
41923
|
}));
|
|
41789
|
-
recordsCommand.command("delete <table> <id>").description("Delete a record").option(ANON_FLAG, ANON_HELP).option("--json", "Output as JSON").action((table, id2, opts) => run("Delete", async () => {
|
|
41790
|
-
|
|
41924
|
+
recordsCommand.command("delete <table> <id>").description("Delete a record (soft-delete when the table declares one; --purge removes it for good)").option("--purge", "Hard-delete: remove the row AND erase its audit history (owner/editor). Use it to scrub probe rows.").option(ANON_FLAG, ANON_HELP).option("--json", "Output as JSON").action((table, id2, opts) => run("Delete", async () => {
|
|
41925
|
+
const what = opts.purge ? `Purge record ${id2} from "${table}" (also erases its history)?` : `Delete record ${id2} from "${table}"?`;
|
|
41926
|
+
if (!await confirm(what)) {
|
|
41791
41927
|
printResult("Cancelled.", opts, { table, id: id2, deleted: false, cancelled: true });
|
|
41792
41928
|
return;
|
|
41793
41929
|
}
|
|
41794
41930
|
const api = await recordsHttp(opts);
|
|
41795
|
-
const res = await api.del(
|
|
41796
|
-
|
|
41931
|
+
const res = await api.del(
|
|
41932
|
+
`/api/${api.guid}/records/${table}/${id2}${opts.purge ? "?purge=1" : ""}`
|
|
41933
|
+
);
|
|
41934
|
+
const data = res?.data ?? { table, id: id2, deleted: true, purged: !!opts.purge };
|
|
41935
|
+
printResult(
|
|
41936
|
+
data.purged ? "Purged - the row and its audit history are gone." : `Deleted. If "${table}" declares a soft-delete column the row is only stamped deleted (see it with \`gipity records query ${table} --only-deleted\`, bring it back with \`gipity records restore ${table} ${id2}\`, remove it for good with \`--purge\`).`,
|
|
41937
|
+
opts,
|
|
41938
|
+
data
|
|
41939
|
+
);
|
|
41940
|
+
}));
|
|
41941
|
+
recordsCommand.command("restore <table> <id>").description("Un-delete a soft-deleted record (owner/editor)").option("--json", "Output as JSON").action((table, id2, opts) => run("Restore", async () => {
|
|
41942
|
+
const api = await recordsHttp(opts);
|
|
41943
|
+
const res = await api.post(`/api/${api.guid}/records/${table}/${id2}/restore`);
|
|
41944
|
+
printResult("Restored.", opts, res?.data ?? { table, id: id2, restored: true });
|
|
41797
41945
|
}));
|
|
41798
41946
|
|
|
41799
41947
|
// src/commands/fn.ts
|
|
@@ -41880,6 +42028,7 @@ fnCommand.command("delete <name>").alias("rm").description("Delete a function").
|
|
|
41880
42028
|
|
|
41881
42029
|
// src/commands/service.ts
|
|
41882
42030
|
init_api();
|
|
42031
|
+
init_auth();
|
|
41883
42032
|
init_config();
|
|
41884
42033
|
init_colors();
|
|
41885
42034
|
var SERVICES = [
|
|
@@ -41905,16 +42054,59 @@ serviceCommand.command("list").description("List callable app services").option(
|
|
|
41905
42054
|
const width = SERVICES.reduce((m, s) => Math.max(m, s.name.length), 0);
|
|
41906
42055
|
console.log('Call one with `gipity service call <name> \'{"json":"body"}\'`');
|
|
41907
42056
|
console.log(muted("(GET endpoints like llm/models, tts/voices take --get and no body)"));
|
|
42057
|
+
console.log(muted("Verifying what a signed-out visitor of your deployed app gets? Add --anon."));
|
|
41908
42058
|
console.log("");
|
|
41909
42059
|
for (const s of SERVICES) {
|
|
41910
42060
|
console.log(`${bold(s.name.padEnd(width))} ${muted(`[${s.method}] ${s.desc}`)}`);
|
|
41911
42061
|
}
|
|
41912
42062
|
}));
|
|
41913
|
-
|
|
42063
|
+
function anonBillingHint(name, err) {
|
|
42064
|
+
if (!(err instanceof ApiError) || err.statusCode !== 401) return null;
|
|
42065
|
+
if (err.code !== "LOGIN_REQUIRED") return null;
|
|
42066
|
+
const service = name.split("/")[0];
|
|
42067
|
+
return `An anonymous visitor cannot call '${service}': it is on the user_pays default, so every signed-out visitor to a public page is asked to sign in and pay from their own credits. If this app serves the public (a help bubble, a landing page), switch it to owner-pays in gipity.yaml and redeploy:
|
|
42068
|
+
deploy:
|
|
42069
|
+
phases:
|
|
42070
|
+
- services
|
|
42071
|
+
service_definitions:
|
|
42072
|
+
- service: ${service}
|
|
42073
|
+
billing_mode: owner_pays
|
|
42074
|
+
gipity deploy dev --only services`;
|
|
42075
|
+
}
|
|
42076
|
+
serviceCommand.command("call <name> [body]").description("Call an app service by name (e.g. llm, image, music). Uses your logged-in session - no token wrangling; --anon calls it as a signed-out visitor instead.").option("-d, --data <json>", "JSON request body: inline JSON, @file to read a file, or @- / - for stdin (alternative to the positional [body])").option("--file <field=@path>", "Attach a file as { data, media_type } under <field> (the vision/media shape these services expect), repeatable, e.g. --file image=@receipt.png", (v7, acc) => (acc || []).concat(v7)).option("--anon", "Call as an anonymous visitor (the public path a signed-out user of your deployed app hits) instead of as your signed-in account").option("--get", "Issue a GET (for listing endpoints like llm/models, tts/voices)").option("--field <path>", "Print only this field of the result (dot path, e.g. choices.0.message.content)").option("--json", "Output compact JSON").action((name, bodyArg, opts) => run("Call", async () => {
|
|
41914
42077
|
const config = requireConfig();
|
|
41915
42078
|
const path5 = name.split("/").map(encodeURIComponent).join("/");
|
|
41916
42079
|
const url = `/api/${config.projectGuid}/services/${path5}`;
|
|
41917
|
-
const
|
|
42080
|
+
const body = resolveBody(bodyArg || opts.data, opts.file);
|
|
42081
|
+
if (opts.anon) {
|
|
42082
|
+
console.error(muted("Auth: anonymous visitor (the public path a signed-out user of your deployed app hits)"));
|
|
42083
|
+
} else {
|
|
42084
|
+
const who = getAuth()?.email;
|
|
42085
|
+
console.error(muted(`Auth: calling as ${who ?? "your signed-in account"} (the owner persona - always billed to you and never blocked by billing_mode; use --anon for the public visitor path)`));
|
|
42086
|
+
}
|
|
42087
|
+
let res;
|
|
42088
|
+
try {
|
|
42089
|
+
if (opts.anon) {
|
|
42090
|
+
const headers = await mintAppToken(config.projectGuid);
|
|
42091
|
+
if (!headers) {
|
|
42092
|
+
throw new Error(
|
|
42093
|
+
`Could not mint a visitor token for this project, so there is no anonymous path to call. That means the app is not deployed on this API base yet: run \`gipity deploy dev\` first, then re-run this command.`
|
|
42094
|
+
);
|
|
42095
|
+
}
|
|
42096
|
+
res = await publicRequest(opts.get ? "GET" : "POST", url, opts.get ? void 0 : body, headers);
|
|
42097
|
+
} else {
|
|
42098
|
+
res = opts.get ? await get(url) : await post(url, body);
|
|
42099
|
+
}
|
|
42100
|
+
} catch (err) {
|
|
42101
|
+
const hint = opts.anon ? anonBillingHint(name, err) : null;
|
|
42102
|
+
if (!hint) throw err;
|
|
42103
|
+
throw new Error(`${err.message}
|
|
42104
|
+
${hint}`);
|
|
42105
|
+
}
|
|
42106
|
+
if (opts.field) {
|
|
42107
|
+
emitField(res, opts.field);
|
|
42108
|
+
return;
|
|
42109
|
+
}
|
|
41918
42110
|
console.log(opts.json ? JSON.stringify(res) : JSON.stringify(res, null, 2));
|
|
41919
42111
|
}));
|
|
41920
42112
|
|
package/dist/knowledge.js
CHANGED
|
@@ -116,7 +116,7 @@ Run \`gipity --help\` for the full list. Use \`--help\` on any command for detai
|
|
|
116
116
|
|
|
117
117
|
Function return shape: \`gipity fn call\`, the in-test \`ctx.fn.call\`/\`callAs\`, and the client \`Gipity.fn\` all return your function's value **unwrapped** - read/assert \`result.field\`. Only raw HTTP/\`curl\` wraps it as \`{ data: ... }\`; never write \`result.data.field\` in a test.
|
|
118
118
|
|
|
119
|
-
Tests are isolated, not run against your live DB: \`gipity test\` points \`ctx.fn.call\`/\`callAs\` at a throwaway copy of your database (your \`migrations/\` + \`seeds/\`), reset (truncate + reseed) before
|
|
119
|
+
Tests are isolated, not run against your live DB: \`gipity test\` points \`ctx.fn.call\`/\`callAs\` at a throwaway copy of your database (your \`migrations/\` + \`seeds/\`), one per test FILE, reset (truncate + reseed) before the file runs - so files can't interfere, test rows never reach the deployed app, and you don't write teardown. Functions see \`ctx.isTest === true\` during a run (use it to skip your own rate limiting); the platform also suppresses \`notify()\` push so a suite can't spam subscribers. Reference data tests need goes in seed files; a runtime-written settings table that isn't seeded goes under \`test.preserve\` in \`gipity.yaml\`. Build per-file fixtures in \`setup(fn)\` → \`ctx.fixtures\`.
|
|
120
120
|
|
|
121
121
|
## Hit friction on the platform? Report it in real time
|
|
122
122
|
|
|
@@ -187,7 +187,7 @@ App development skills:
|
|
|
187
187
|
- \`app-development\` - functions, database, and API
|
|
188
188
|
- \`app-import\` - import apps from GitHub/.gip bundles (incl. Vercel/Replit/Lovable porting) and export any project as a portable .gip - app_import tool, gipity save/load
|
|
189
189
|
- \`app-records\` - Gipity Records: declared tables → validated CRUD, provenance, workflows, auto UI
|
|
190
|
-
- \`app-testing\` - testing deployed app functions (ctx.fn.call/callAs, the
|
|
190
|
+
- \`app-testing\` - testing deployed app functions (ctx.fn.call/callAs, the per-file test DB)
|
|
191
191
|
- \`deploy\` - the deploy pipeline & gipity.yaml manifest
|
|
192
192
|
- \`jobs\` - long-running CPU + GPU compute jobs (Python / Node / bash)
|
|
193
193
|
- \`realtime-scheduled-app\` - recipe: realtime presence/messages + DB function + scheduled poster, end-to-end
|
package/dist/relay/onboarding.js
CHANGED
|
@@ -82,7 +82,7 @@ export async function runRelaySetup(opts) {
|
|
|
82
82
|
const promptText = opts.mode === 'run-now'
|
|
83
83
|
? ' Set up remote control on this computer'
|
|
84
84
|
: ' Enable remote control';
|
|
85
|
-
const enable = await confirm(promptText, { default: 'yes' });
|
|
85
|
+
const enable = await confirm(promptText, { default: 'yes', headless: 'no' });
|
|
86
86
|
if (!enable) {
|
|
87
87
|
state.setRelayEnabled(false);
|
|
88
88
|
console.log(` ${muted('Skipped. Turn on later with')} ${brand('gipity connect')}${muted('.')}`);
|
|
@@ -119,7 +119,7 @@ export async function runRelaySetup(opts) {
|
|
|
119
119
|
startDaemon();
|
|
120
120
|
console.log('');
|
|
121
121
|
// Offer OS-level autostart (launchd / systemd --user / Task Scheduler).
|
|
122
|
-
const autostartOs = await confirm(' Also start at OS login', { default: 'yes' });
|
|
122
|
+
const autostartOs = await confirm(' Also start at OS login', { default: 'yes', headless: 'no' });
|
|
123
123
|
if (autostartOs) {
|
|
124
124
|
try {
|
|
125
125
|
const res = installAutostart();
|
|
@@ -153,7 +153,7 @@ export async function runRelaySetup(opts) {
|
|
|
153
153
|
// on|off` flips it later.)
|
|
154
154
|
if (state.getDiagnosticsConsent() === undefined) {
|
|
155
155
|
console.log('');
|
|
156
|
-
const diag = await confirm(' Share anonymous diagnostics info', { default: 'yes' });
|
|
156
|
+
const diag = await confirm(' Share anonymous diagnostics info', { default: 'yes', headless: 'no' });
|
|
157
157
|
state.setDiagnosticsConsent(diag);
|
|
158
158
|
}
|
|
159
159
|
console.log('');
|
package/dist/utils.js
CHANGED
|
@@ -119,17 +119,28 @@ function rerunWithYes() {
|
|
|
119
119
|
* - `opts.skip` (or the global `--yes` flag) auto-returns `true`.
|
|
120
120
|
* - Renders a `[Y/n]:` or `[y/N]:` hint automatically - callers should NOT
|
|
121
121
|
* append their own y/N suffix (or a trailing `?`/`:`) to `question`.
|
|
122
|
-
* - In non-TTY environments without `--yes`,
|
|
122
|
+
* - In non-TTY environments without `--yes`, this EXITS the process with code 1
|
|
123
|
+
* (it never returns) - see the comment in the body for why. Pass
|
|
124
|
+
* `{ headless: 'no' }` for prompts that merely OFFER something optional,
|
|
125
|
+
* where declining is a normal successful outcome. */
|
|
123
126
|
export async function confirm(question, opts = {}) {
|
|
124
127
|
const defaultYes = opts.default === 'yes';
|
|
125
128
|
if (opts.skip ?? _autoConfirm)
|
|
126
129
|
return true;
|
|
127
130
|
if (!process.stdin.isTTY) {
|
|
128
|
-
// Headless/agent context: no one can answer the prompt.
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
+
// Headless/agent context: no one can answer the prompt. For an optional
|
|
132
|
+
// offer, declining and carrying on is the right answer (`headless: 'no'`).
|
|
133
|
+
// For everything else the command did NOT do what it was asked to do, so
|
|
134
|
+
// exit non-zero: an agent that pipes or suppresses output
|
|
135
|
+
// (`gipity records delete games 1 --purge >/dev/null 2>&1`) otherwise sees
|
|
136
|
+
// a clean exit 0 and believes the delete happened, then burns turns
|
|
137
|
+
// discovering it silently cancelled. The exit status is the only signal
|
|
138
|
+
// that survives redirection. The message echoes the exact command to
|
|
139
|
+
// re-run so the fix is copy-paste, not a second guessing trip.
|
|
140
|
+
if (opts.headless === 'no')
|
|
141
|
+
return false;
|
|
131
142
|
console.error(`Confirmation required (non-interactive). Re-run with --yes:\n ${rerunWithYes()}`);
|
|
132
|
-
|
|
143
|
+
process.exit(1);
|
|
133
144
|
}
|
|
134
145
|
const hint = defaultYes ? dim('[Y/n]') : dim('[y/N]');
|
|
135
146
|
process.stdout.write(`${question} ${hint}: `);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gipity",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.7",
|
|
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",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"prepack": "npm run build",
|
|
15
15
|
"dev": "tsc --watch",
|
|
16
16
|
"test": "npm run test:smoke",
|
|
17
|
-
"test:smoke": "npm run build && node --test dist/__tests__/utils.test.js dist/__tests__/template-vars.test.js dist/__tests__/platform.test.js dist/__tests__/colors.test.js dist/__tests__/config.test.js dist/__tests__/sync.test.js dist/__tests__/sync-apply.test.js dist/__tests__/sync-unretrievable.test.js dist/__tests__/sync-clean-check.test.js dist/__tests__/sync-lock.test.js dist/__tests__/auth-lock.test.js dist/__tests__/api-401-retry.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__/build-picker.test.js dist/__tests__/relay-state.test.js dist/__tests__/relay-daemon.test.js dist/__tests__/session-pool.test.js dist/__tests__/relay-diagnostics.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__/ingest-queue.test.js dist/__tests__/media-upload.test.js dist/__tests__/stream-delta.test.js dist/__tests__/phase-tracker.test.js dist/__tests__/relay-ingest-contract.test.js dist/__tests__/prompts.test.js dist/__tests__/capture-transcript.test.js dist/__tests__/capture-parsers.test.js dist/__tests__/agents.test.js dist/__tests__/capture-lock.test.js dist/__tests__/capture-resolve.test.js dist/__tests__/flag-aliases.test.js dist/__tests__/client-context.test.js dist/__tests__/adopt-cwd.test.js dist/__tests__/cli-cmd-agent.test.js dist/__tests__/cli-cmd-approval.test.js dist/__tests__/cli-cmd-audit.test.js dist/__tests__/cli-cmd-brand.test.js dist/__tests__/cli-cmd-bug.test.js dist/__tests__/cli-cmd-bug-queue.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-storage.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-status.test.js dist/__tests__/cli-cmd-logout.test.js dist/__tests__/cli-cmd-token.test.js dist/__tests__/cli-cmd-key.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-setup.test.js dist/__tests__/cli-cmd-doctor.test.js dist/__tests__/claude-setup.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 dist/__tests__/setup-codex-hooks.test.js dist/__tests__/trace.test.js",
|
|
17
|
+
"test:smoke": "npm run build && node --test dist/__tests__/utils.test.js dist/__tests__/template-vars.test.js dist/__tests__/platform.test.js dist/__tests__/colors.test.js dist/__tests__/config.test.js dist/__tests__/sync.test.js dist/__tests__/sync-apply.test.js dist/__tests__/sync-unretrievable.test.js dist/__tests__/sync-clean-check.test.js dist/__tests__/sync-lock.test.js dist/__tests__/auth-lock.test.js dist/__tests__/api-401-retry.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__/build-picker.test.js dist/__tests__/relay-state.test.js dist/__tests__/relay-daemon.test.js dist/__tests__/session-pool.test.js dist/__tests__/relay-diagnostics.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__/ingest-queue.test.js dist/__tests__/media-upload.test.js dist/__tests__/stream-delta.test.js dist/__tests__/phase-tracker.test.js dist/__tests__/relay-ingest-contract.test.js dist/__tests__/prompts.test.js dist/__tests__/capture-transcript.test.js dist/__tests__/capture-parsers.test.js dist/__tests__/agents.test.js dist/__tests__/capture-lock.test.js dist/__tests__/capture-resolve.test.js dist/__tests__/flag-aliases.test.js dist/__tests__/client-context.test.js dist/__tests__/adopt-cwd.test.js dist/__tests__/cli-cmd-agent.test.js dist/__tests__/cli-cmd-approval.test.js dist/__tests__/cli-cmd-audit.test.js dist/__tests__/cli-cmd-brand.test.js dist/__tests__/cli-cmd-bug.test.js dist/__tests__/cli-cmd-bug-queue.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-db-checkpoint.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-storage.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-status.test.js dist/__tests__/cli-cmd-logout.test.js dist/__tests__/cli-cmd-token.test.js dist/__tests__/cli-cmd-key.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-setup.test.js dist/__tests__/cli-cmd-doctor.test.js dist/__tests__/claude-setup.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 dist/__tests__/setup-codex-hooks.test.js dist/__tests__/trace.test.js",
|
|
18
18
|
"test:smoke:quick": "node scripts/smoke-quick.mjs",
|
|
19
19
|
"test:e2e": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-live.test.js dist/__tests__/cli-e2e-sync-live.test.js dist/__tests__/cli-e2e-sync-stress-live.test.js dist/__tests__/cli-e2e-rollback-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",
|
|
20
20
|
"test:e2e:sync": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-sync-live.test.js",
|