gipity 1.0.384 → 1.0.387
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.js +5 -2
- package/dist/auth.js +22 -13
- package/dist/commands/add.js +2 -1
- package/dist/commands/claude.js +4 -4
- package/dist/commands/db.js +4 -1
- package/dist/commands/deploy.js +15 -3
- package/dist/commands/init.js +21 -8
- package/dist/commands/page-eval.js +191 -22
- package/dist/commands/page-inspect.js +37 -6
- package/dist/commands/page-screenshot.js +13 -5
- package/dist/commands/page-test.js +58 -9
- package/dist/commands/records.js +42 -8
- package/dist/commands/remove.js +42 -0
- package/dist/commands/sandbox.js +67 -14
- package/dist/commands/status.js +8 -4
- package/dist/commands/test.js +16 -4
- package/dist/commands/text.js +1 -1
- package/dist/commands/workflow.js +78 -19
- package/dist/config.js +46 -2
- package/dist/helpers/sync.js +4 -2
- package/dist/helpers/text-analysis.js +9 -5
- package/dist/index.js +27 -14
- package/dist/knowledge.js +8 -1
- package/dist/page-fixtures.js +41 -0
- package/dist/project-setup.js +4 -4
- package/dist/relay/daemon.js +2 -2
- package/dist/relay/device-http.js +2 -2
- package/dist/setup.js +71 -3
- package/dist/sync.js +137 -18
- package/package.json +3 -3
|
@@ -18,12 +18,33 @@ async function inspectClient(url, waitMs, i) {
|
|
|
18
18
|
}
|
|
19
19
|
const MAX_HOLD_MS = 15_000; // keep each in-page await under the ~20s browser action timeout
|
|
20
20
|
const MIN_HOLD_MS = 1_000;
|
|
21
|
-
/** Splice per-client values into a user
|
|
22
|
-
* label, `{{i}}` → its 0-based index. Plain string
|
|
23
|
-
*
|
|
21
|
+
/** Splice per-client values into a user string (URL, --action, or --observe).
|
|
22
|
+
* `{{label}}` → the client's label, `{{i}}` → its 0-based index. Plain string
|
|
23
|
+
* replace (no regex) so the string's own characters are never treated as
|
|
24
|
+
* patterns. */
|
|
24
25
|
function subst(expr, label, i) {
|
|
25
26
|
return expr.split('{{label}}').join(label).split('{{i}}').join(String(i));
|
|
26
27
|
}
|
|
28
|
+
/** Collect any `{{...}}` placeholders the runner does NOT recognize, so an
|
|
29
|
+
* invented token (e.g. `{{name}}`) is flagged instead of passing through
|
|
30
|
+
* verbatim into every client's URL/expression. */
|
|
31
|
+
function unknownTokens(...strings) {
|
|
32
|
+
const out = new Set();
|
|
33
|
+
for (const s of strings) {
|
|
34
|
+
for (const m of (s ?? '').match(/\{\{[^}]*\}\}/g) ?? []) {
|
|
35
|
+
if (m !== '{{i}}' && m !== '{{label}}')
|
|
36
|
+
out.add(m);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return [...out];
|
|
40
|
+
}
|
|
41
|
+
/** One-time warning (to stderr, so --json stdout stays clean) for
|
|
42
|
+
* unrecognized placeholders left as-is. */
|
|
43
|
+
function warnUnknownTokens(unknown) {
|
|
44
|
+
if (unknown.length === 0)
|
|
45
|
+
return;
|
|
46
|
+
console.error(warning(`⚠ Unrecognized placeholder ${unknown.join(', ')} left as-is — only {{i}} (0-based client index) and {{label}} are substituted per client. Set per-client values with --labels and reference them as {{label}}.`));
|
|
47
|
+
}
|
|
27
48
|
/** Build the statement-body script one client runs: do the one-time action,
|
|
28
49
|
* then sample `observe` `samples` times across `holdMs`, stamping in-page
|
|
29
50
|
* start/end so the caller can confirm the clients overlapped. */
|
|
@@ -98,11 +119,22 @@ function fmtSamples(samples) {
|
|
|
98
119
|
async function runInteractive(url, observe, opts) {
|
|
99
120
|
const clients = Math.max(1, parseInt(opts.clients, 10) || 2);
|
|
100
121
|
const stagger = opts.stagger != null ? Math.max(0, parseInt(opts.stagger, 10) || 0) : 0;
|
|
101
|
-
const
|
|
122
|
+
const rawHold = parseInt(opts.hold, 10) || 8000;
|
|
123
|
+
const hold = Math.min(MAX_HOLD_MS, Math.max(MIN_HOLD_MS, rawHold));
|
|
124
|
+
if (rawHold > MAX_HOLD_MS) {
|
|
125
|
+
// Surface the clamp (to stderr, so --json stdout stays clean) instead of
|
|
126
|
+
// leaving the agent to infer it from the printed "hold Nms" line.
|
|
127
|
+
console.error(warning(`--hold ${rawHold}ms exceeds the ${MAX_HOLD_MS}ms per-client cap (each client samples inside one browser eval, bounded by the server's eval budget) — using ${MAX_HOLD_MS}ms. ` +
|
|
128
|
+
`Co-launch every role in this one command (put {{label}}/{{i}} in the URL) so all clients overlap for the whole window; a separately-started background client overlaps only the sliver of its window that lines up.`));
|
|
129
|
+
}
|
|
102
130
|
const samples = Math.min(30, Math.max(2, parseInt(opts.samples, 10) || 6));
|
|
103
131
|
const settle = opts.waitFor ? 200 : 1000;
|
|
104
132
|
const labels = (opts.labels ? String(opts.labels).split(',').map((s) => s.trim()) : []).filter(Boolean);
|
|
105
133
|
const labelFor = (i) => labels[i] ?? `client-${i}`;
|
|
134
|
+
// Only {{label}} and {{i}} are substituted. Warn once on any other {{token}}
|
|
135
|
+
// (a natural guess like {{name}} or {{index}}) so it isn't sent literally to
|
|
136
|
+
// every client — the silent wrong-behavior trap of identical clients.
|
|
137
|
+
warnUnknownTokens(unknownTokens(url, opts.action, observe));
|
|
106
138
|
if (!opts.json) {
|
|
107
139
|
console.log(`${brand('Page test')} ${muted('(interactive)')} ${bold(url)}`);
|
|
108
140
|
console.log(muted(`${clients} client(s), stagger ${stagger}s, hold ${hold}ms, ${samples} samples each`));
|
|
@@ -113,8 +145,12 @@ async function runInteractive(url, observe, opts) {
|
|
|
113
145
|
await sleep(i * stagger * 1000);
|
|
114
146
|
if (!opts.json)
|
|
115
147
|
console.log(muted(`client ${i} (${labelFor(i)}) joining`));
|
|
148
|
+
// {{label}}/{{i}} substitute into the URL too, so one invocation can launch
|
|
149
|
+
// asymmetric roles concurrently (e.g. ?role={{label}} with --labels host,join)
|
|
150
|
+
// and the overlap check still confirms they coexisted.
|
|
151
|
+
const clientUrl = subst(url, labelFor(i), i);
|
|
116
152
|
const expr = buildHarness(opts.action ? subst(opts.action, labelFor(i), i) : undefined, subst(observe, labelFor(i), i), labelFor(i), hold, samples);
|
|
117
|
-
return observeClient(
|
|
153
|
+
return observeClient(clientUrl, expr, i, labelFor(i), settle, hold, opts.waitFor);
|
|
118
154
|
})());
|
|
119
155
|
}
|
|
120
156
|
const results = (await Promise.all(runs)).sort((a, b) => a.i - b.i);
|
|
@@ -159,6 +195,9 @@ async function runPassive(url, opts) {
|
|
|
159
195
|
const clients = Math.max(1, parseInt(opts.clients, 10) || 2);
|
|
160
196
|
const stagger = opts.stagger != null ? Math.max(0, parseInt(opts.stagger, 10) || 0) : 12;
|
|
161
197
|
const wait = Math.min(30000, Math.max(2000, parseInt(opts.wait, 10) || 24000));
|
|
198
|
+
const labels = (opts.labels ? String(opts.labels).split(',').map((s) => s.trim()) : []).filter(Boolean);
|
|
199
|
+
const labelFor = (i) => labels[i] ?? `client-${i}`;
|
|
200
|
+
warnUnknownTokens(unknownTokens(url));
|
|
162
201
|
if (!opts.json) {
|
|
163
202
|
console.log(`${brand('Page test')} ${bold(url)}`);
|
|
164
203
|
console.log(`${muted(`${clients} client(s), stagger ${stagger}s, ${wait}ms open each`)}`);
|
|
@@ -169,7 +208,7 @@ async function runPassive(url, opts) {
|
|
|
169
208
|
await sleep(i * stagger * 1000);
|
|
170
209
|
if (!opts.json)
|
|
171
210
|
console.log(`${muted(`client ${i}${i === 0 ? ' (first)' : ''} starting`)}`);
|
|
172
|
-
return inspectClient(url, wait, i);
|
|
211
|
+
return inspectClient(subst(url, labelFor(i), i), wait, i);
|
|
173
212
|
})());
|
|
174
213
|
}
|
|
175
214
|
const results = (await Promise.all(runs)).sort((a, b) => a.i - b.i);
|
|
@@ -222,14 +261,14 @@ async function runPassive(url, opts) {
|
|
|
222
261
|
// just because the clients never actually ran together.
|
|
223
262
|
export const pageTestCommand = new Command('test')
|
|
224
263
|
.description('Multi-client realtime check: load a URL in N concurrent headless clients; flag console errors, or drive an action and observe shared state (--observe)')
|
|
225
|
-
.argument('<url>', 'Deployed URL to load in every client')
|
|
264
|
+
.argument('<url>', 'Deployed URL to load in every client. {{label}}/{{i}} substitute per client in both modes (e.g. ?name=Bot{{i}}, or ?role={{label}} with --labels host,join), so one invocation can give each client a distinct role.')
|
|
226
265
|
.option('--clients <n>', 'Number of headless clients to launch', '2')
|
|
227
266
|
.option('--stagger <s>', 'Seconds between client starts (passive default 12; interactive default 0)')
|
|
228
267
|
.option('--wait <ms>', 'Passive mode: ms each client stays open after load (max 30000)', '24000')
|
|
229
268
|
// Interactive mode (--observe drives it):
|
|
230
269
|
.option('--observe <expr>', 'Interactive: JS expression sampled in each client to read shared state (e.g. presence count). Switches on interactive mode.')
|
|
231
270
|
.option('--action <expr>', 'Interactive: one-time JS run in each client before observing (e.g. fill a name + submit). {{label}}/{{i}} are substituted per client.')
|
|
232
|
-
.option('--labels <csv>', '
|
|
271
|
+
.option('--labels <csv>', 'Per-client labels substituted for {{label}} in the URL/--action/--observe (default client-0, client-1, …)')
|
|
233
272
|
.option('--hold <ms>', `Interactive: total observe window per client (${MIN_HOLD_MS}-${MAX_HOLD_MS}ms)`, '8000')
|
|
234
273
|
.option('--samples <k>', 'Interactive: number of observations across the hold window (2-30)', '6')
|
|
235
274
|
.option('--wait-for <selector>', 'Interactive: wait for this CSS selector before running --action (deterministic readiness gate)')
|
|
@@ -239,12 +278,22 @@ Examples:
|
|
|
239
278
|
# Passive: load in 3 staggered clients, flag console errors
|
|
240
279
|
gipity page test "https://dev.gipity.ai/me/app/" --clients 3 --stagger 8
|
|
241
280
|
|
|
281
|
+
# Per-client URL params: each client joins under a distinct name (Bot0, Bot1, …)
|
|
282
|
+
gipity page test "https://dev.gipity.ai/me/app/?name=Bot{{i}}" --clients 2
|
|
283
|
+
|
|
242
284
|
# Interactive: two concurrent clients each join with a name, then watch the
|
|
243
285
|
# live presence count. The command confirms the clients actually overlapped.
|
|
244
286
|
gipity page test "https://dev.gipity.ai/me/app/" --clients 2 \\
|
|
245
287
|
--action "document.querySelector('#name').value='{{label}}'; document.querySelector('form').requestSubmit();" \\
|
|
246
288
|
--observe "document.querySelectorAll('.present').length" \\
|
|
247
|
-
--labels Alice,Bob
|
|
289
|
+
--labels Alice,Bob
|
|
290
|
+
|
|
291
|
+
# Asymmetric roles in ONE invocation: {{label}} in the URL routes client 0 to
|
|
292
|
+
# host and client 1 to join. They overlap in time (verified), so the joiner
|
|
293
|
+
# observes the live state the host is driving — no background-process dance.
|
|
294
|
+
gipity page test "https://dev.gipity.ai/me/app/?test-action={{label}}" --clients 2 \\
|
|
295
|
+
--labels host,join \\
|
|
296
|
+
--observe "document.querySelector('[data-screen]')?.dataset.screen"`)
|
|
248
297
|
.action((url, opts) => run('Page test', async () => {
|
|
249
298
|
if (opts.observe) {
|
|
250
299
|
await runInteractive(url, opts.observe, opts);
|
package/dist/commands/records.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { get, post, put, del } from '../api.js';
|
|
2
|
+
import { get, post, put, patch, del } from '../api.js';
|
|
3
3
|
import { requireConfig } from '../config.js';
|
|
4
4
|
import { bold, muted } from '../colors.js';
|
|
5
5
|
import { run, printList, printResult } from '../helpers/index.js';
|
|
6
6
|
import { confirm } from '../utils.js';
|
|
7
|
+
// All commands hit the app API (https://a.gipity.ai/api/<guid>/records/...),
|
|
8
|
+
// which authorizes the logged-in owner via their Bearer token. (The native
|
|
9
|
+
// Records API is the only records surface that exists server-side; there is no
|
|
10
|
+
// /projects/<guid>/records mirror.)
|
|
7
11
|
export const recordsCommand = new Command('records')
|
|
8
12
|
.description('Manage records');
|
|
9
13
|
recordsCommand
|
|
@@ -12,8 +16,38 @@ recordsCommand
|
|
|
12
16
|
.option('--json', 'Output as JSON')
|
|
13
17
|
.action((opts) => run('List', async () => {
|
|
14
18
|
const config = requireConfig();
|
|
15
|
-
const res = await get(`/
|
|
16
|
-
printList(res.data, opts, 'No tables configured for Records API.', t => `${bold(t.table_name)} ${muted(t.auth_level)} ${muted(`pk=${t.primary_key_column}`)} ${muted(`db=${t.database_name}`)}`);
|
|
19
|
+
const res = await get(`/api/${config.projectGuid}/records-config`);
|
|
20
|
+
printList(res.data, opts, 'No tables configured for Records API. Configure one with `gipity records config <table> --auth <level>`.', t => `${bold(t.table_name)} ${muted(t.auth_level)} ${muted(`pk=${t.primary_key_column}`)} ${muted(`db=${t.database_name}`)}`);
|
|
21
|
+
}));
|
|
22
|
+
recordsCommand
|
|
23
|
+
.command('config <table>')
|
|
24
|
+
.description('Show or set a table\'s Records API config (auth level, search, etc.)')
|
|
25
|
+
.option('--auth <level>', 'Auth level: public (anonymous writes), member (sign-in), or user')
|
|
26
|
+
.option('--searchable <bool>', 'Enable full-text search (true/false)')
|
|
27
|
+
.option('--primary-key <col>', 'Primary key column (default: id)')
|
|
28
|
+
.option('--soft-delete <col>', 'Soft-delete column (pass "none" to clear)')
|
|
29
|
+
.option('--json', 'Output as JSON')
|
|
30
|
+
.action((table, opts) => run('Config', async () => {
|
|
31
|
+
const config = requireConfig();
|
|
32
|
+
const base = `/api/${config.projectGuid}/records/${table}/config`;
|
|
33
|
+
// No setter flags → just show the current config.
|
|
34
|
+
const setting = opts.auth || opts.searchable !== undefined || opts.primaryKey || opts.softDelete;
|
|
35
|
+
if (!setting) {
|
|
36
|
+
const res = await get(base);
|
|
37
|
+
printResult(JSON.stringify(res.data, null, 2), opts, res.data);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const body = {};
|
|
41
|
+
if (opts.auth)
|
|
42
|
+
body.auth_level = opts.auth;
|
|
43
|
+
if (opts.searchable !== undefined)
|
|
44
|
+
body.searchable = /^(true|1|on|yes)$/i.test(String(opts.searchable));
|
|
45
|
+
if (opts.primaryKey)
|
|
46
|
+
body.primary_key_column = opts.primaryKey;
|
|
47
|
+
if (opts.softDelete)
|
|
48
|
+
body.soft_delete_column = opts.softDelete === 'none' ? null : opts.softDelete;
|
|
49
|
+
const res = await patch(base, body);
|
|
50
|
+
printResult(`Configured "${table}": auth=${res.data.auth_level}, searchable=${res.data.searchable}, pk=${res.data.primary_key_column}`, opts, res.data);
|
|
17
51
|
}));
|
|
18
52
|
recordsCommand
|
|
19
53
|
.command('query <table>')
|
|
@@ -35,7 +69,7 @@ recordsCommand
|
|
|
35
69
|
params.set('offset', opts.offset);
|
|
36
70
|
if (opts.fields)
|
|
37
71
|
params.set('fields', opts.fields);
|
|
38
|
-
const res = await get(`/
|
|
72
|
+
const res = await get(`/api/${config.projectGuid}/records/${table}?${params}`);
|
|
39
73
|
if (opts.json) {
|
|
40
74
|
console.log(JSON.stringify(res));
|
|
41
75
|
}
|
|
@@ -54,7 +88,7 @@ recordsCommand
|
|
|
54
88
|
.option('--json', 'Output as JSON')
|
|
55
89
|
.action((table, id, opts) => run('Get', async () => {
|
|
56
90
|
const config = requireConfig();
|
|
57
|
-
const res = await get(`/
|
|
91
|
+
const res = await get(`/api/${config.projectGuid}/records/${table}/${id}`);
|
|
58
92
|
console.log(opts.json ? JSON.stringify(res.data) : JSON.stringify(res.data, null, 2));
|
|
59
93
|
}));
|
|
60
94
|
recordsCommand
|
|
@@ -65,7 +99,7 @@ recordsCommand
|
|
|
65
99
|
.action((table, opts) => run('Create', async () => {
|
|
66
100
|
const config = requireConfig();
|
|
67
101
|
const data = JSON.parse(opts.data);
|
|
68
|
-
const res = await post(`/
|
|
102
|
+
const res = await post(`/api/${config.projectGuid}/records/${table}`, data);
|
|
69
103
|
printResult(`Created: ${JSON.stringify(res.data)}`, opts, res.data);
|
|
70
104
|
}));
|
|
71
105
|
recordsCommand
|
|
@@ -76,7 +110,7 @@ recordsCommand
|
|
|
76
110
|
.action((table, id, opts) => run('Update', async () => {
|
|
77
111
|
const config = requireConfig();
|
|
78
112
|
const data = JSON.parse(opts.data);
|
|
79
|
-
const res = await put(`/
|
|
113
|
+
const res = await put(`/api/${config.projectGuid}/records/${table}/${id}`, data);
|
|
80
114
|
printResult(`Updated: ${JSON.stringify(res.data)}`, opts, res.data);
|
|
81
115
|
}));
|
|
82
116
|
recordsCommand
|
|
@@ -88,7 +122,7 @@ recordsCommand
|
|
|
88
122
|
return;
|
|
89
123
|
}
|
|
90
124
|
const config = requireConfig();
|
|
91
|
-
await del(`/
|
|
125
|
+
await del(`/api/${config.projectGuid}/records/${table}/${id}`);
|
|
92
126
|
printResult('Deleted.', { json: false });
|
|
93
127
|
}));
|
|
94
128
|
//# sourceMappingURL=records.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { post } from '../api.js';
|
|
3
|
+
import { requireConfig } from '../config.js';
|
|
4
|
+
import { sync } from '../sync.js';
|
|
5
|
+
import { success, muted } from '../colors.js';
|
|
6
|
+
import { run } from '../helpers/index.js';
|
|
7
|
+
import { confirm } from '../utils.js';
|
|
8
|
+
export const removeCommand = new Command('remove')
|
|
9
|
+
.description('Remove an installed kit from the project (inverse of `gipity add <kit>`).')
|
|
10
|
+
.argument('<kit>', 'Kit key/directory under src/packages/ to remove')
|
|
11
|
+
.option('-y, --yes', 'Skip the confirmation prompt')
|
|
12
|
+
.option('--json', 'Output as JSON')
|
|
13
|
+
.action((kit, opts) => run('Remove', async () => {
|
|
14
|
+
const config = requireConfig();
|
|
15
|
+
if (!opts.yes && !opts.json) {
|
|
16
|
+
if (!await confirm(`Remove the "${kit}" kit (its files, import-map entries, and gipity.yaml wiring)?`)) {
|
|
17
|
+
console.log('Cancelled.');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const res = await post(`/projects/${config.projectGuid}/remove`, { name: kit });
|
|
22
|
+
// Force the pull so the kit's deletions land locally without tripping the
|
|
23
|
+
// bulk-deletion guard - the removal is an explicit, user-invoked action.
|
|
24
|
+
const syncResult = await sync({ interactive: false, force: true });
|
|
25
|
+
const data = res.data;
|
|
26
|
+
if (opts.json) {
|
|
27
|
+
console.log(JSON.stringify({ ...data, synced: syncResult.applied }));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
console.log(success(`Removed the "${data.kit}" kit.`));
|
|
31
|
+
for (const r of data.removed)
|
|
32
|
+
console.log(muted(` - ${r}`));
|
|
33
|
+
if (data.notes?.length) {
|
|
34
|
+
console.log('');
|
|
35
|
+
for (const n of data.notes)
|
|
36
|
+
console.log(n);
|
|
37
|
+
}
|
|
38
|
+
if (syncResult.applied > 0) {
|
|
39
|
+
console.log(`\nPulled ${syncResult.applied} change(s) to local.`);
|
|
40
|
+
}
|
|
41
|
+
}));
|
|
42
|
+
//# sourceMappingURL=remove.js.map
|
package/dist/commands/sandbox.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { readFileSync } from 'fs';
|
|
2
|
+
import { readFileSync, existsSync, statSync } from 'fs';
|
|
3
3
|
import { dirname, extname, relative } from 'path';
|
|
4
4
|
import { post } from '../api.js';
|
|
5
5
|
import { resolveProjectContext, getConfigPath } from '../config.js';
|
|
@@ -14,6 +14,19 @@ const LANG_MAP = {
|
|
|
14
14
|
bash: 'bash',
|
|
15
15
|
sh: 'bash',
|
|
16
16
|
};
|
|
17
|
+
// Interpreter tokens accepted at the head of a `run <interpreter> <file>`
|
|
18
|
+
// invocation (e.g. `gipity sandbox run python build_report.py`), mirroring how
|
|
19
|
+
// you'd launch a script locally. Maps each token to the canonical language.
|
|
20
|
+
const INTERPRETERS = {
|
|
21
|
+
python: 'python',
|
|
22
|
+
python3: 'python',
|
|
23
|
+
py: 'python',
|
|
24
|
+
node: 'javascript',
|
|
25
|
+
js: 'javascript',
|
|
26
|
+
javascript: 'javascript',
|
|
27
|
+
bash: 'bash',
|
|
28
|
+
sh: 'bash',
|
|
29
|
+
};
|
|
17
30
|
/** Project-relative path from the process cwd, or undefined when there's
|
|
18
31
|
* no local config (one-off mode) or the cwd is at/above the project root. */
|
|
19
32
|
function resolveRelativeCwd() {
|
|
@@ -29,7 +42,7 @@ function resolveRelativeCwd() {
|
|
|
29
42
|
export const sandboxCommand = new Command('sandbox')
|
|
30
43
|
.description('Run code in a sandbox');
|
|
31
44
|
sandboxCommand
|
|
32
|
-
.command('run [
|
|
45
|
+
.command('run [args...]')
|
|
33
46
|
.description('Run code')
|
|
34
47
|
.option('--language <language>', 'Language: js, py, or bash', 'js')
|
|
35
48
|
.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')
|
|
@@ -59,6 +72,8 @@ Examples:
|
|
|
59
72
|
|
|
60
73
|
# Run a script file directly (language inferred from .py)
|
|
61
74
|
$ gipity sandbox run --file build_report.py
|
|
75
|
+
$ gipity sandbox run python build_report.py # same thing, interpreter shorthand
|
|
76
|
+
$ gipity sandbox run bash "echo hi; ffmpeg -version" # inline, language pinned
|
|
62
77
|
|
|
63
78
|
# Surgical: only these files are mirrored in
|
|
64
79
|
$ gipity sandbox run --language bash \\
|
|
@@ -73,31 +88,62 @@ Pre-installed: Python (pandas, numpy, matplotlib, Pillow, scipy, bs4),
|
|
|
73
88
|
CLI tools (ImageMagick, FFmpeg, webp/cwebp, optipng, jq, pandoc, exiftool,
|
|
74
89
|
GCC/Rust).
|
|
75
90
|
`)
|
|
76
|
-
.action((
|
|
91
|
+
.action((args = [], opts, command) => run('Sandbox', async () => {
|
|
77
92
|
const { config } = await resolveProjectContext();
|
|
78
|
-
|
|
93
|
+
// Resolve the positional args into either inline code or a script-file path.
|
|
94
|
+
// `run <interpreter> <file>` (e.g. `run python build_report.py`) is the natural
|
|
95
|
+
// mental model, so accept it: a leading interpreter token + a path becomes
|
|
96
|
+
// --file with the language pinned by the interpreter. A single positional is
|
|
97
|
+
// inline code, same as before.
|
|
98
|
+
let inlineCode;
|
|
99
|
+
let filePath = opts.file;
|
|
100
|
+
let langFromInterp;
|
|
101
|
+
if (args.length >= 2 && INTERPRETERS[args[0].toLowerCase()] !== undefined) {
|
|
102
|
+
langFromInterp = INTERPRETERS[args[0].toLowerCase()];
|
|
103
|
+
const rest = args.slice(1).join(' ');
|
|
104
|
+
// `run python build_report.py` -> a script file; `run bash "echo hi"` -> inline code.
|
|
105
|
+
if (existsSync(rest) && statSync(rest).isFile())
|
|
106
|
+
filePath = rest;
|
|
107
|
+
else
|
|
108
|
+
inlineCode = rest;
|
|
109
|
+
}
|
|
110
|
+
else if (args.length === 1) {
|
|
111
|
+
inlineCode = args[0];
|
|
112
|
+
}
|
|
113
|
+
else if (args.length > 1) {
|
|
114
|
+
console.error(clrError('Unrecognized invocation. Pass inline code as a single quoted arg, a script with --file <path>, or use the `run <python|node|bash> <file>` shorthand.'));
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
if (inlineCode !== undefined && filePath) {
|
|
79
118
|
console.error(clrError('Pass either an inline <code> arg or --file <path>, not both'));
|
|
80
119
|
process.exit(1);
|
|
81
120
|
}
|
|
82
|
-
if (
|
|
121
|
+
if (inlineCode === undefined && !filePath) {
|
|
83
122
|
console.error(clrError('Provide an inline <code> arg or --file <path>'));
|
|
84
123
|
process.exit(1);
|
|
85
124
|
}
|
|
86
|
-
let source =
|
|
87
|
-
if (
|
|
125
|
+
let source = inlineCode;
|
|
126
|
+
if (filePath) {
|
|
88
127
|
try {
|
|
89
|
-
source = readFileSync(
|
|
128
|
+
source = readFileSync(filePath, 'utf8');
|
|
90
129
|
}
|
|
91
130
|
catch {
|
|
92
|
-
console.error(clrError(`Cannot read file: ${
|
|
131
|
+
console.error(clrError(`Cannot read file: ${filePath}`));
|
|
93
132
|
process.exit(1);
|
|
94
133
|
}
|
|
95
134
|
}
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
135
|
+
// Language precedence: interpreter token > file extension (unless --language
|
|
136
|
+
// was passed explicitly) > the --language value (default js).
|
|
137
|
+
const fromExt = filePath && !langFromInterp && command.getOptionValueSource('language') === 'default'
|
|
138
|
+
? LANG_MAP[extname(filePath).slice(1).toLowerCase()]
|
|
99
139
|
: undefined;
|
|
100
|
-
const language = fromExt || LANG_MAP[opts.language] || opts.language;
|
|
140
|
+
const language = langFromInterp || fromExt || LANG_MAP[opts.language] || opts.language;
|
|
141
|
+
// True when the run fell back to the implicit JS default - nothing in the
|
|
142
|
+
// command shape pinned a language. Used to explain the execution mode if a
|
|
143
|
+
// shell/Python snippet gets parsed as JavaScript and blows up (see hint below).
|
|
144
|
+
const usedDefaultJs = !langFromInterp
|
|
145
|
+
&& command.getOptionValueSource('language') === 'default'
|
|
146
|
+
&& language === 'javascript';
|
|
101
147
|
if (!['javascript', 'python', 'bash'].includes(language)) {
|
|
102
148
|
console.error(clrError(`Invalid language: ${opts.language}. Use: js, py, or bash`));
|
|
103
149
|
process.exit(1);
|
|
@@ -138,8 +184,15 @@ GCC/Rust).
|
|
|
138
184
|
for (const f of res.data.outputFiles)
|
|
139
185
|
console.log(`${f}`);
|
|
140
186
|
}
|
|
141
|
-
if (res.data.exitCode !== 0)
|
|
187
|
+
if (res.data.exitCode !== 0) {
|
|
188
|
+
// A SyntaxError / CJS-loader trace under the implicit JS default almost
|
|
189
|
+
// always means the input was shell or Python that got run as JavaScript.
|
|
190
|
+
// The raw Node stack trace never says which mode ran, so name it.
|
|
191
|
+
if (usedDefaultJs && /SyntaxError|cjs\/loader|wrapSafe/.test(res.data.stderr || '')) {
|
|
192
|
+
console.error(dim('Hint: ran as JavaScript (the default). For a shell command pass `--language bash` (or `gipity sandbox run bash "<cmd>"`); for Python pass `--language py`.'));
|
|
193
|
+
}
|
|
142
194
|
process.exit(res.data.exitCode);
|
|
195
|
+
}
|
|
143
196
|
}
|
|
144
197
|
}));
|
|
145
198
|
//# sourceMappingURL=sandbox.js.map
|
package/dist/commands/status.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { existsSync, readFileSync } from 'fs';
|
|
3
3
|
import { join, resolve } from 'path';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
|
-
import { getAuth,
|
|
5
|
+
import { getAuth, sessionExpired } from '../auth.js';
|
|
6
6
|
import { getConfig, liveUrl } from '../config.js';
|
|
7
7
|
import { brand, success, warning, muted, error as clrError } from '../colors.js';
|
|
8
8
|
import { GIPITY_PLUGIN_ID, GIPITY_MARKETPLACE_NAME, setupClaudeHooks, ensureGipityPlugin } from '../setup.js';
|
|
@@ -44,10 +44,11 @@ export const statusCommand = new Command('status')
|
|
|
44
44
|
apiBase: config.apiBase,
|
|
45
45
|
url: liveUrl(config),
|
|
46
46
|
} : null,
|
|
47
|
+
// `valid` reflects the refresh token (the real session) - access
|
|
48
|
+
// tokens auto-renew, so their expiry must not read as "invalid".
|
|
47
49
|
auth: auth ? {
|
|
48
50
|
email: auth.email,
|
|
49
|
-
|
|
50
|
-
valid: new Date(auth.expiresAt).getTime() > Date.now(),
|
|
51
|
+
valid: !sessionExpired(),
|
|
51
52
|
} : null,
|
|
52
53
|
plugin: hookCheck,
|
|
53
54
|
}, null, 2));
|
|
@@ -67,8 +68,11 @@ export const statusCommand = new Command('status')
|
|
|
67
68
|
if (!auth) {
|
|
68
69
|
console.log(`${muted('Auth:')} ${warning('not logged in. Run: gipity login')}`);
|
|
69
70
|
}
|
|
71
|
+
else if (sessionExpired()) {
|
|
72
|
+
console.log(`${muted('Auth:')} ${warning(`session expired for ${auth.email}. Run: gipity login`)}`);
|
|
73
|
+
}
|
|
70
74
|
else {
|
|
71
|
-
console.log(`${muted('Auth:')} ${success(auth.email)}
|
|
75
|
+
console.log(`${muted('Auth:')} ${success(auth.email)}`);
|
|
72
76
|
}
|
|
73
77
|
if (hookCheck) {
|
|
74
78
|
if (hookCheck.ok) {
|
package/dist/commands/test.js
CHANGED
|
@@ -100,8 +100,11 @@ async function pollTestStatus(projectGuid, runGuid, opts) {
|
|
|
100
100
|
const progress = data.totalFiles === 0
|
|
101
101
|
? 'starting up'
|
|
102
102
|
: `${data.completedFiles}/${data.totalFiles} files`;
|
|
103
|
+
// "so far" so a heartbeat line, if captured on its own (tail/grep),
|
|
104
|
+
// can't be mistaken for the final tally — the partial count climbing
|
|
105
|
+
// toward the total previously read as "tests vanished".
|
|
103
106
|
const tally = data.passed + data.failed > 0
|
|
104
|
-
? ` (${data.passed} passed${data.failed > 0 ? `, ${data.failed} failed` : ''})`
|
|
107
|
+
? ` (${data.passed} passed${data.failed > 0 ? `, ${data.failed} failed` : ''} so far)`
|
|
105
108
|
: '';
|
|
106
109
|
console.log(muted(` … still running — ${progress}${tally}, ${elapsed}s elapsed`));
|
|
107
110
|
if (now - startTime >= LONG_RUN_MS && !longRunHintShown) {
|
|
@@ -168,17 +171,24 @@ export const testCommand = new Command('test')
|
|
|
168
171
|
if (data.skipped > 0)
|
|
169
172
|
parts.push(muted(`${data.skipped} skipped`));
|
|
170
173
|
console.log(`${parts.join(', ')} ${muted(`(${data.durationMs}ms)`)}`);
|
|
174
|
+
// The run's results are stored; re-fetch them (no re-run) by GUID.
|
|
175
|
+
console.log(muted(`Re-fetch this run's details (no re-run): gipity test status ${runGuid} --json`));
|
|
171
176
|
if (data.failed > 0)
|
|
172
177
|
process.exit(1);
|
|
173
178
|
}));
|
|
174
179
|
// ── Status subcommand (check on a running test) ──────────────────────
|
|
175
180
|
testCommand
|
|
176
181
|
.command('status')
|
|
177
|
-
.
|
|
182
|
+
.alias('results')
|
|
183
|
+
.description('Fetch a finished (or running) test run by GUID — full per-test results, no re-run')
|
|
178
184
|
.argument('<runGuid>', 'Test run GUID (e.g. tr_abc123)')
|
|
179
185
|
.option('--json', 'Output as JSON')
|
|
180
186
|
.option('--follow', 'Follow until complete (poll)')
|
|
181
|
-
|
|
187
|
+
// optsWithGlobals: the parent `test` command also declares `--json`, and with
|
|
188
|
+
// root-level enablePositionalOptions commander attaches a post-subcommand
|
|
189
|
+
// `--json` to that parent — so the subcommand's own opts would miss it.
|
|
190
|
+
.action((runGuid, _o, command) => run('Status', async () => {
|
|
191
|
+
const opts = command.optsWithGlobals();
|
|
182
192
|
const config = requireConfig();
|
|
183
193
|
if (opts.follow) {
|
|
184
194
|
const data = await pollTestStatus(config.projectGuid, runGuid, opts);
|
|
@@ -217,7 +227,9 @@ testCommand
|
|
|
217
227
|
.description('Show recent runs')
|
|
218
228
|
.option('--limit <n>', 'Number of runs to show', '10')
|
|
219
229
|
.option('--json', 'Output as JSON')
|
|
220
|
-
|
|
230
|
+
// optsWithGlobals: see the status subcommand — `--json` lands on the parent.
|
|
231
|
+
.action((_o, command) => run('History', async () => {
|
|
232
|
+
const opts = command.optsWithGlobals();
|
|
221
233
|
const config = requireConfig();
|
|
222
234
|
const res = await get(`/projects/${config.projectGuid}/test/history?limit=${opts.limit}`);
|
|
223
235
|
if (opts.json) {
|
package/dist/commands/text.js
CHANGED
|
@@ -42,7 +42,7 @@ function formatProfile(a) {
|
|
|
42
42
|
row('Sentences', a.sentences);
|
|
43
43
|
row('Lines', a.lines);
|
|
44
44
|
row('Paragraphs', a.paragraphs);
|
|
45
|
-
if (a.
|
|
45
|
+
if (a.longestWord) {
|
|
46
46
|
row('Longest word', `${a.longestWord} (${[...a.longestWord].length})`);
|
|
47
47
|
row('Shortest word', `${a.shortestWord} (${[...a.shortestWord].length})`);
|
|
48
48
|
row('Avg word length', a.averageWordLength);
|