gipity 1.0.422 → 1.0.425
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.js +5 -3
- package/dist/commands/add.js +14 -3
- package/dist/commands/bug.js +21 -1
- package/dist/commands/claude.js +20 -2
- package/dist/commands/deploy.js +29 -1
- package/dist/commands/fn.js +21 -2
- package/dist/commands/page-eval.js +87 -7
- package/dist/commands/page-inspect.js +201 -186
- package/dist/commands/page-screenshot.js +27 -2
- package/dist/commands/page.js +1 -1
- package/dist/commands/push.js +12 -7
- package/dist/commands/sandbox.js +76 -14
- package/dist/commands/test.js +52 -3
- package/dist/commands/workflow.js +24 -16
- package/dist/helpers/sync.js +8 -1
- package/dist/index.js +20126 -386
- package/dist/knowledge.js +7 -5
- package/dist/sync.js +46 -3
- package/dist/trace.js +69 -0
- package/dist/updater/check.js +159 -84
- package/dist/updater/shim.js +203 -70
- package/package.json +5 -3
package/dist/commands/sandbox.js
CHANGED
|
@@ -35,19 +35,57 @@ const PIN_LANGUAGE_HELP = [
|
|
|
35
35
|
' gipity sandbox run --language bash "<code>" # explicit flag (js | py | bash)',
|
|
36
36
|
' gipity sandbox run --file script.sh # inferred from the file extension',
|
|
37
37
|
].join('\n');
|
|
38
|
+
// Words that open a code statement, not a command line. A single positional
|
|
39
|
+
// starting with one of these is a JS/Python snippet whose language we must not
|
|
40
|
+
// guess (see resolveLanguage) - everything here is either invalid in bash or,
|
|
41
|
+
// worse, VALID in bash with a different meaning (`export FOO=1`, `for ...`).
|
|
42
|
+
const CODE_OPENERS = new Set([
|
|
43
|
+
'const', 'let', 'var', 'function', 'async', 'await', 'import', 'export',
|
|
44
|
+
'console', 'require', 'print', 'def', 'class', 'lambda', 'from', 'return',
|
|
45
|
+
'for', 'while', 'if', 'try', 'with',
|
|
46
|
+
]);
|
|
47
|
+
/**
|
|
48
|
+
* True when a bare inline string is unmistakably a shell COMMAND LINE
|
|
49
|
+
* (`node tests/game.test.js`, `ls -la`, `ffmpeg -i in.mp4 out.gif`) rather
|
|
50
|
+
* than a code snippet. Deliberately conservative: one line, starts with a
|
|
51
|
+
* plain command word (no parens/quotes/operators in it), not a code opener,
|
|
52
|
+
* not an assignment (`x = 1` is Python-or-bash-ambiguous). Anything that
|
|
53
|
+
* fails these checks still goes through the explicit-pin error below.
|
|
54
|
+
*/
|
|
55
|
+
export function looksLikeShellCommand(code) {
|
|
56
|
+
if (code.includes('\n'))
|
|
57
|
+
return false;
|
|
58
|
+
const m = /^([A-Za-z0-9_.~/-]+)(\s|$)/.exec(code.trim());
|
|
59
|
+
if (!m)
|
|
60
|
+
return false;
|
|
61
|
+
const token = m[1].toLowerCase();
|
|
62
|
+
if (CODE_OPENERS.has(token))
|
|
63
|
+
return false;
|
|
64
|
+
if (/^\s*[A-Za-z_][A-Za-z0-9_]*\s*=/.test(code))
|
|
65
|
+
return false; // assignment
|
|
66
|
+
// A lone bare word (`foo`) is as likely a typo as a command; require either
|
|
67
|
+
// arguments after the word or a path-shaped word (`./build.sh`, `bin/run`).
|
|
68
|
+
return /\s\S/.test(code.trim()) || token.includes('/');
|
|
69
|
+
}
|
|
38
70
|
/**
|
|
39
71
|
* Resolve the language from an explicit signal, or exit.
|
|
40
72
|
*
|
|
41
|
-
* Precedence: interpreter token > --language > --file extension
|
|
73
|
+
* Precedence: interpreter token > --language > --file extension > the
|
|
74
|
+
* command-line heuristic above (bash).
|
|
42
75
|
*
|
|
43
|
-
*
|
|
44
|
-
* snippets parse as more than one of them (`x = 1`, `a[0]`, `foo()`),
|
|
45
|
-
* default silently runs some fraction of input in the wrong
|
|
46
|
-
* behavior defaulted to JavaScript, which meant a shell
|
|
47
|
-
* died with a Node `SyntaxError` at `/work/_run.js` -
|
|
48
|
-
* sync and a server round trip.
|
|
49
|
-
*
|
|
50
|
-
*
|
|
76
|
+
* Beyond that there is no default. js/python/bash are mutually exclusive, and
|
|
77
|
+
* plenty of snippets parse as more than one of them (`x = 1`, `a[0]`, `foo()`),
|
|
78
|
+
* so a blanket default silently runs some fraction of input in the wrong
|
|
79
|
+
* interpreter. The old behavior defaulted to JavaScript, which meant a shell
|
|
80
|
+
* one-liner ran as JS and died with a Node `SyntaxError` at `/work/_run.js` -
|
|
81
|
+
* after paying for a project sync and a server round trip. The one shape we DO
|
|
82
|
+
* default is the unambiguous command line (`gipity sandbox run "node
|
|
83
|
+
* tests/game.test.js"`): it was the single most common rejected invocation,
|
|
84
|
+
* the CLI had everything it needed to run it, and no code snippet matches the
|
|
85
|
+
* heuristic. Ambiguous snippets still fail here, costing nothing and saying
|
|
86
|
+
* what to type. (`docs/skills/sandbox-tools.md` used to carry a hand-written
|
|
87
|
+
* "always pin the language" warning to work around this; the CLI enforces it
|
|
88
|
+
* now.)
|
|
51
89
|
*/
|
|
52
90
|
export function resolveLanguage(opts) {
|
|
53
91
|
const explicit = opts.langFromInterp
|
|
@@ -68,6 +106,8 @@ export function resolveLanguage(opts) {
|
|
|
68
106
|
console.error(dim(`Pass --language explicitly:\n gipity sandbox run --language py --file ${opts.filePath}`));
|
|
69
107
|
process.exit(1);
|
|
70
108
|
}
|
|
109
|
+
if (opts.inlineCode && looksLikeShellCommand(opts.inlineCode))
|
|
110
|
+
return 'bash';
|
|
71
111
|
console.error(clrError('No language specified for inline code.'));
|
|
72
112
|
console.error(dim(`Pin it one of three ways:\n${PIN_LANGUAGE_HELP}`));
|
|
73
113
|
process.exit(1);
|
|
@@ -129,6 +169,11 @@ sandboxCommand
|
|
|
129
169
|
.option('--file <path>', 'Read the code body from a file instead of the inline <code> arg; --language is inferred from the extension when not given')
|
|
130
170
|
.option('--timeout <seconds>', 'Execution timeout in seconds', '30')
|
|
131
171
|
.option('--input <path>', 'Narrow to specific project files instead of auto-mirroring the whole tree (repeatable). Use this only for >1 GB projects or when you want surgical control.', (v, prev) => [...(prev ?? []), v])
|
|
172
|
+
// Commander maps a `--no-` prefixed flag to the un-prefixed camelCase name
|
|
173
|
+
// (`opts.syncOutput`); the explicit `[]` default stops commander's negated-
|
|
174
|
+
// boolean convention from defaulting it to `true`. Collected as an array so
|
|
175
|
+
// the flag is repeatable.
|
|
176
|
+
.option('--no-sync-output <glob>', 'Do not persist run outputs matching this glob back to the project (repeatable). For byproducts you want to inspect once but never keep, e.g. --no-sync-output "docs/preview*". Supports *, **, and dir/ prefixes.', (v, prev) => [...prev, v], [])
|
|
132
177
|
.option('--json', 'Output as JSON')
|
|
133
178
|
.addHelpText('after', `
|
|
134
179
|
By default the whole project is auto-mirrored into /work/ (up to 1 GB) -
|
|
@@ -217,11 +262,19 @@ GCC/Rust).
|
|
|
217
262
|
process.exit(1);
|
|
218
263
|
}
|
|
219
264
|
}
|
|
220
|
-
// Language precedence: interpreter token > --language > file extension
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
// server round trip below, so a
|
|
224
|
-
|
|
265
|
+
// Language precedence: interpreter token > --language > file extension >
|
|
266
|
+
// unambiguous-command-line heuristic (bash). resolveLanguage() exits when
|
|
267
|
+
// nothing pins one and the input isn't command-shaped, rather than guessing.
|
|
268
|
+
// This runs BEFORE the project sync and the server round trip below, so a
|
|
269
|
+
// missing language costs nothing but the message.
|
|
270
|
+
const language = resolveLanguage({ langFromInterp, langOpt: opts.language, filePath, inlineCode });
|
|
271
|
+
// Validate --no-sync-output globs while we're still pre-network: an empty
|
|
272
|
+
// pattern (e.g. a quoting mishap) would silently match nothing server-side.
|
|
273
|
+
const noSyncOutput = opts.syncOutput ?? [];
|
|
274
|
+
if (noSyncOutput.some((g) => !g.trim())) {
|
|
275
|
+
console.error(clrError('--no-sync-output requires a non-empty glob (e.g. --no-sync-output "docs/preview*")'));
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
225
278
|
// Args are good - now it's worth resolving (and announcing) the project.
|
|
226
279
|
const { config } = await resolveProjectContext();
|
|
227
280
|
const timeout = parseInt(opts.timeout, 10);
|
|
@@ -256,6 +309,10 @@ GCC/Rust).
|
|
|
256
309
|
timeout: isNaN(timeout) ? 30 : timeout,
|
|
257
310
|
input_files: opts.input,
|
|
258
311
|
cwd,
|
|
312
|
+
// The filter must run SERVER-side (in the output extractor): skipping
|
|
313
|
+
// only in the CLI would still write the files into project storage and
|
|
314
|
+
// make every later sync propose deleting them.
|
|
315
|
+
noSyncOutput: noSyncOutput.length ? noSyncOutput : undefined,
|
|
259
316
|
});
|
|
260
317
|
const res = opts.json
|
|
261
318
|
? await doRun()
|
|
@@ -292,6 +349,11 @@ GCC/Rust).
|
|
|
292
349
|
for (const f of res.data.outputFiles)
|
|
293
350
|
console.log(`${f}`);
|
|
294
351
|
}
|
|
352
|
+
if (res.data.skippedOutputFiles && res.data.skippedOutputFiles.length > 0) {
|
|
353
|
+
console.log(dim('\nNot persisted (--no-sync-output):'));
|
|
354
|
+
for (const f of res.data.skippedOutputFiles)
|
|
355
|
+
console.log(dim(`${f}`));
|
|
356
|
+
}
|
|
295
357
|
if (res.data.exitCode !== 0) {
|
|
296
358
|
// No "did you mean another language?" hint is needed: the language is now
|
|
297
359
|
// always something the caller pinned, never a silent default we chose.
|
package/dist/commands/test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { get, post } from '../api.js';
|
|
3
|
-
import { requireConfig } from '../config.js';
|
|
3
|
+
import { requireConfig, getProjectRoot } from '../config.js';
|
|
4
4
|
import { success, error as clrError, warning, muted, bold, dim } from '../colors.js';
|
|
5
5
|
import { run, syncBeforeAction } from '../helpers/index.js';
|
|
6
6
|
// Absolute poll ceiling - the server reaps stalled runs (~65 min) well before
|
|
@@ -15,6 +15,22 @@ function statusIcon(status) {
|
|
|
15
15
|
return muted('→');
|
|
16
16
|
return muted('?');
|
|
17
17
|
}
|
|
18
|
+
/** When the runner finds nothing, list the test-looking files it deliberately
|
|
19
|
+
* skipped (kit tests under src/packages/, .spec.* files) so "no tests" never
|
|
20
|
+
* contradicts what's on disk, and point at the sandbox as the way to run
|
|
21
|
+
* frontend/kit module tests (cli#120/cli#121). */
|
|
22
|
+
function printSkippedCandidates(skipped) {
|
|
23
|
+
if (skipped.length === 0)
|
|
24
|
+
return;
|
|
25
|
+
console.log('');
|
|
26
|
+
console.log(muted(`Found ${skipped.length} test-looking file${skipped.length === 1 ? '' : 's'} that gipity test does not run:`));
|
|
27
|
+
for (const s of skipped.slice(0, 10)) {
|
|
28
|
+
console.log(muted(` ${s.path} (${s.reason})`));
|
|
29
|
+
}
|
|
30
|
+
if (skipped.length > 10)
|
|
31
|
+
console.log(muted(` ... and ${skipped.length - 10} more`));
|
|
32
|
+
console.log(muted('Run frontend/kit module tests in the sandbox: gipity sandbox run bash "node <file>"'));
|
|
33
|
+
}
|
|
18
34
|
// Long-run hint for non-TTY runs: after this, print a one-time "not hung" + faster-path hint.
|
|
19
35
|
const LONG_RUN_MS = 60000;
|
|
20
36
|
async function pollTestStatus(projectGuid, runGuid, opts) {
|
|
@@ -169,15 +185,40 @@ export const testCommand = new Command('test')
|
|
|
169
185
|
process.exit(1);
|
|
170
186
|
}
|
|
171
187
|
if (!filterPath && data.total === 0 && data.results.length === 0) {
|
|
172
|
-
|
|
188
|
+
// Name the searched root: discovery is server-side over the synced
|
|
189
|
+
// project tree's tests/, never the shell cwd - an agent running from a
|
|
190
|
+
// subdirectory must not read this as "the project has no tests" for
|
|
191
|
+
// the wrong reason (cli#120).
|
|
192
|
+
const root = getProjectRoot();
|
|
193
|
+
console.log(muted(`No tests ran - no tests/*.test.js files under the project root${root ? ` (${root})` : ''}.`));
|
|
173
194
|
console.log(muted('gipity test runs server-function tests only; a frontend-only app (no functions/) has nothing here to run.'));
|
|
174
195
|
console.log(muted('Verify a frontend app by driving the live page: gipity page inspect <url> (or gipity page eval).'));
|
|
196
|
+
// Enumerate test-looking files the runner deliberately skips (kit
|
|
197
|
+
// tests, .spec.*) so this answer never contradicts what's on disk.
|
|
198
|
+
try {
|
|
199
|
+
const listRes = await get(`/projects/${config.projectGuid}/test/list`);
|
|
200
|
+
printSkippedCandidates(listRes.data.skipped ?? []);
|
|
201
|
+
}
|
|
202
|
+
catch { /* the skipped list is advisory - never fail the run over it */ }
|
|
175
203
|
return;
|
|
176
204
|
}
|
|
177
205
|
if (data.status === 'failed' && data.errorMessage) {
|
|
178
206
|
console.log(clrError(`Run failed: ${data.errorMessage}`));
|
|
179
207
|
console.log('');
|
|
180
208
|
}
|
|
209
|
+
// Failures recap. Results stream incrementally above, but agents usually
|
|
210
|
+
// read this output through a `tail` window, so anything printed early is
|
|
211
|
+
// lost - restate every failure (name + assertion message) right before
|
|
212
|
+
// the summary, where it's guaranteed to survive truncation from the top.
|
|
213
|
+
if (data.failed > 0) {
|
|
214
|
+
console.log(clrError('Failures:'));
|
|
215
|
+
for (const r of data.results.filter(r => r.status === 'failed')) {
|
|
216
|
+
const where = r.path ? `${r.path}/` : '';
|
|
217
|
+
console.log(` ${statusIcon('failed')} ${where}${r.name || '(unnamed test - the file may have crashed outside a test)'}`);
|
|
218
|
+
console.log(` ${clrError(r.error || `(no assertion message recorded - inspect with: gipity test status ${runGuid} --json)`)}`);
|
|
219
|
+
}
|
|
220
|
+
console.log('');
|
|
221
|
+
}
|
|
181
222
|
// Summary
|
|
182
223
|
const parts = [];
|
|
183
224
|
if (data.passed > 0)
|
|
@@ -255,9 +296,17 @@ testCommand
|
|
|
255
296
|
return;
|
|
256
297
|
}
|
|
257
298
|
if (total === 0) {
|
|
299
|
+
// Discovery is server-side over the synced project tree's tests/ -
|
|
300
|
+
// never the shell cwd. Say which root was searched so this can't be
|
|
301
|
+
// misread as a definitive project-level fact from a subdirectory
|
|
302
|
+
// (cli#120), and list any test-looking files deliberately skipped
|
|
303
|
+
// (kit tests, .spec.*) instead of asserting none exist (cli#121).
|
|
304
|
+
const root = getProjectRoot();
|
|
258
305
|
console.log(muted(pathFilter
|
|
259
306
|
? `No test files matched filter: ${pathFilter}`
|
|
260
|
-
:
|
|
307
|
+
: `No test files found under tests/ at the project root${root ? ` (${root})` : ''}. Add *.test.js files under tests/.`));
|
|
308
|
+
if (!pathFilter)
|
|
309
|
+
printSkippedCandidates(res.data.skipped ?? []);
|
|
261
310
|
return;
|
|
262
311
|
}
|
|
263
312
|
console.log(bold(`Test files${pathFilter ? ` (filter: ${pathFilter})` : ''}: ${total}`));
|
|
@@ -29,6 +29,25 @@ 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
|
+
/** Print each step's status, tokens, model, error and output. A run line alone
|
|
33
|
+
* says a run finished, not what it did — without the steps you can't tell a
|
|
34
|
+
* workflow that wrote a row from one that silently skipped every step. */
|
|
35
|
+
function printStepRuns(steps, emptyNote) {
|
|
36
|
+
if (steps.length === 0) {
|
|
37
|
+
console.log(` ${muted(emptyNote)}`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
for (const s of steps) {
|
|
41
|
+
const statusColor = s.status === 'completed' ? success : s.status === 'failed' ? clrError : muted;
|
|
42
|
+
const model = s.model_used ? ` ${muted(`[${s.model_used}]`)}` : '';
|
|
43
|
+
console.log(` ${s.step_order}. ${statusColor(s.status)} ${s.tokens_used ?? 0} tokens${model}`);
|
|
44
|
+
if (s.error_message)
|
|
45
|
+
console.log(` ${clrError(s.error_message)}`);
|
|
46
|
+
if (s.output_json !== null && s.output_json !== undefined) {
|
|
47
|
+
console.log(JSON.stringify(s.output_json, null, 2).split('\n').map(l => ` ${l}`).join('\n'));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
32
51
|
const TERMINAL_RUN_STATUSES = new Set(['completed', 'failed', 'cancelled']);
|
|
33
52
|
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
34
53
|
/**
|
|
@@ -155,6 +174,10 @@ workflowCommand
|
|
|
155
174
|
console.log(formatRunLine(r));
|
|
156
175
|
if (r.error_message)
|
|
157
176
|
console.log(` ${clrError(r.error_message)}`);
|
|
177
|
+
// The whole point of --wait is to see what the run did. The detail endpoint
|
|
178
|
+
// we just polled already carries the steps, so show them rather than make
|
|
179
|
+
// the caller re-query the database to find out whether anything happened.
|
|
180
|
+
printStepRuns(r.step_runs ?? [], '(no steps recorded)');
|
|
158
181
|
}
|
|
159
182
|
if (r.status !== 'completed')
|
|
160
183
|
process.exit(1);
|
|
@@ -176,22 +199,7 @@ workflowCommand
|
|
|
176
199
|
return;
|
|
177
200
|
}
|
|
178
201
|
console.log(formatRunLine(r));
|
|
179
|
-
|
|
180
|
-
if (steps.length === 0) {
|
|
181
|
-
console.log(' (no steps recorded)');
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
for (const s of steps) {
|
|
185
|
-
const statusColor = s.status === 'completed' ? success : s.status === 'failed' ? clrError : muted;
|
|
186
|
-
const model = s.model_used ? ` ${muted(`[${s.model_used}]`)}` : '';
|
|
187
|
-
console.log(` ${s.step_order}. ${statusColor(s.status)} ${s.tokens_used ?? 0} tokens${model}`);
|
|
188
|
-
if (s.error_message)
|
|
189
|
-
console.log(` ${clrError(s.error_message)}`);
|
|
190
|
-
if (s.output_json !== null && s.output_json !== undefined) {
|
|
191
|
-
const pretty = JSON.stringify(s.output_json, null, 2).split('\n').map(l => ` ${l}`).join('\n');
|
|
192
|
-
console.log(pretty);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
202
|
+
printStepRuns(r.step_runs ?? [], '(no steps recorded)');
|
|
195
203
|
return;
|
|
196
204
|
}
|
|
197
205
|
const res = await get(`/workflows/${wf.short_guid}/runs`);
|
package/dist/helpers/sync.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* sync.ts - Shared sync-before-action helper.
|
|
3
3
|
*/
|
|
4
|
-
import { sync } from '../sync.js';
|
|
4
|
+
import { sync, isLocalTreeClean } from '../sync.js';
|
|
5
5
|
import { muted, error as clrError } from '../colors.js';
|
|
6
6
|
import { createProgressReporter } from '../progress.js';
|
|
7
7
|
/**
|
|
@@ -15,6 +15,13 @@ import { createProgressReporter } from '../progress.js';
|
|
|
15
15
|
export async function syncBeforeAction(opts) {
|
|
16
16
|
if (opts.sync === false)
|
|
17
17
|
return;
|
|
18
|
+
// Nothing changed locally since the last sync → nothing to push, and the
|
|
19
|
+
// action (deploy etc.) reads server-side state anyway. Skip the whole sync
|
|
20
|
+
// round trip. The probe is local-only stat checks and conservative: any
|
|
21
|
+
// doubt (size/mtime moved, never synced, no baseline) falls through to a
|
|
22
|
+
// real sync. `--force` always takes the full path.
|
|
23
|
+
if (!opts.force && isLocalTreeClean())
|
|
24
|
+
return;
|
|
18
25
|
// Pass a progress reporter so a large pre-action upload shows the transfer
|
|
19
26
|
// bar instead of a silent pause (the reporter is a no-op on non-TTY / when
|
|
20
27
|
// piped, so JSON and headless output stay clean).
|