gipity 1.1.3 → 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adopt-cwd.js +1 -1
- package/dist/banner.js +3 -5
- package/dist/catalog.js +1 -3
- package/dist/commands/add.js +1 -1
- package/dist/commands/build.js +111 -40
- package/dist/commands/chat.js +1 -1
- package/dist/commands/deploy.js +7 -3
- package/dist/commands/file.js +18 -0
- package/dist/commands/fn.js +6 -6
- package/dist/commands/generate.js +80 -16
- package/dist/commands/gmail.js +1 -1
- package/dist/commands/job.js +132 -10
- package/dist/commands/load.js +2 -2
- package/dist/commands/login.js +7 -2
- package/dist/commands/page-eval.js +82 -13
- package/dist/commands/page-fetch.js +14 -10
- package/dist/commands/page-inspect.js +28 -3
- package/dist/commands/page-screenshot.js +36 -7
- package/dist/commands/page-test.js +1 -1
- package/dist/commands/records.js +14 -1
- package/dist/commands/remove.js +1 -1
- package/dist/commands/sandbox.js +182 -10
- package/dist/commands/save.js +1 -1
- package/dist/commands/secrets.js +1 -1
- package/dist/commands/service.js +7 -2
- package/dist/commands/setup.js +4 -6
- package/dist/commands/status.js +41 -12
- package/dist/commands/storage.js +2 -2
- package/dist/commands/sync.js +15 -0
- package/dist/commands/test.js +33 -1
- package/dist/commands/update.js +4 -0
- package/dist/commands/upload.js +69 -121
- package/dist/commands/workflow.js +1 -1
- package/dist/helpers/body.js +117 -0
- package/dist/helpers/duration.js +34 -0
- package/dist/helpers/index.js +2 -0
- package/dist/index.js +1173 -620
- package/dist/knowledge.js +7 -7
- package/dist/login-flow.js +44 -2
- package/dist/relay/onboarding.js +26 -44
- package/dist/sync.js +6 -6
- package/dist/updater/bootstrap.js +27 -16
- package/dist/updater/check.js +86 -10
- package/dist/updater/install.js +82 -0
- package/dist/updater/shim.js +90 -27
- package/dist/utils.js +60 -3
- package/package.json +2 -2
|
@@ -8,6 +8,7 @@ import { brand, bold, muted, success, warning } from '../colors.js';
|
|
|
8
8
|
import { formatSize } from '../utils.js';
|
|
9
9
|
import { run } from '../helpers/index.js';
|
|
10
10
|
import { withSpinner } from '../progress.js';
|
|
11
|
+
import { readScriptFile } from './page-eval.js';
|
|
11
12
|
import { uploadCameraFeed, assertCameraFile, deleteFixture } from '../page-fixtures.js';
|
|
12
13
|
/** `mobile` and `tablet` name a real handset/tablet rather than just a window size.
|
|
13
14
|
* The server emulates it end to end - mobile user-agent, DPR, and crucially TOUCH,
|
|
@@ -234,7 +235,8 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
234
235
|
.option('--wait <ms>', `Sleep this many ms after DOMContentLoaded before capturing (default 1000, max ${MAX_POST_LOAD_DELAY_MS}). With --camera it defaults to ${CAMERA_DEFAULT_DELAY_MS} so the vision model is up. Same flag as page eval/inspect.`)
|
|
235
236
|
.option('--wait-for <selector>', `Wait until this CSS selector appears before capturing, then capture (deterministic - beats guessing a --wait). Same flag as page eval/inspect. Gate on what you are photographing ('[data-vision="ready"]', '#verdict:not(:empty)'). Max ${WAIT_FOR_MAX_MS}ms (--wait-timeout).`)
|
|
236
237
|
.option('--wait-timeout <ms>', `Max ms to wait for --wait-for before giving up (default ${WAIT_FOR_DEFAULT_MS}, max ${WAIT_FOR_MAX_MS}). Timing out is reported - the capture still happens, so you see the state it got stuck in.`)
|
|
237
|
-
.option('--action <js>', 'Run JS in the page before capturing
|
|
238
|
+
.option('--action <js>', 'Run JS in the page before capturing - e.g. click a button to enter a state ("document.getElementById(\'play\').click()"). Runs as an async function body, so const/await and app-relative import(\'./...\') work. Runs after the post-load delay, then settles again before the shot. If it throws, the capture still happens and the failure is reported.')
|
|
239
|
+
.option('--file <path>', 'Read the pre-capture script from a file instead of inline --action (mutually exclusive), or --file - to read it from stdin (pipe a heredoc: --file - <<\'EOF\' ... EOF) with no tmp file. For a multi-step driver - click through a flow, wait for it to render - then capture. Same async-function-body semantics as --action; same --file flag as page eval.')
|
|
238
240
|
.option('--full', 'Capture the full scrollable page (default: viewport only). Scrolls the page through first so scroll-reveal/fade-in-on-scroll (IntersectionObserver) sections render into the shot instead of capturing blank.')
|
|
239
241
|
.option('-o, --output <file>', 'Output path (single viewport only; default .gipity/screenshots/ss-<host>-<timestamp>.png)')
|
|
240
242
|
.option('--no-reload-between', 'Skip reload between viewports (faster, lower fidelity - only safe for static pages)')
|
|
@@ -264,8 +266,27 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
264
266
|
if (aliasFlag) {
|
|
265
267
|
console.error(muted(`Note: treating ${aliasFlag} as --action — it runs your JS in the page before the capture. --action is the canonical flag.`));
|
|
266
268
|
}
|
|
267
|
-
|
|
269
|
+
// The pre-capture script can come inline (--action / its JS-intent aliases) or
|
|
270
|
+
// from a file/stdin (--file, the same script-passing ergonomics as page eval).
|
|
271
|
+
// They are mutually exclusive: one page load runs one pre-capture body, so a
|
|
272
|
+
// caller that passed both meant one of them — say so rather than silently
|
|
273
|
+
// concatenating a driver script onto a stray click.
|
|
274
|
+
const inlineAction = [opts.action, ...ACTION_ALIAS_FLAGS.map((f) => opts[f.slice(2)])]
|
|
268
275
|
.filter(Boolean).join('\n');
|
|
276
|
+
if (opts.file && inlineAction) {
|
|
277
|
+
throw new Error('Pass either --file <path> or an inline --action script, not both');
|
|
278
|
+
}
|
|
279
|
+
let actionScript = inlineAction;
|
|
280
|
+
if (opts.file) {
|
|
281
|
+
try {
|
|
282
|
+
actionScript = readScriptFile(opts.file);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
throw new Error(opts.file === '-'
|
|
286
|
+
? "--file - reads the pre-capture script from stdin, but stdin was empty/unreadable - pipe it in, e.g. gipity page screenshot \"<url>\" --file - <<'EOF' ... EOF"
|
|
287
|
+
: `Cannot read file: ${opts.file}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
269
290
|
// --wait is the canonical name (it is what `page inspect`/`page eval` call it);
|
|
270
291
|
// --post-load-delay stays as a hidden alias. Whether the caller named EITHER is
|
|
271
292
|
// what decides the defaults below, so keep the raw "was it set" signal.
|
|
@@ -351,7 +372,7 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
351
372
|
assertCameraFile(opts.camera);
|
|
352
373
|
if (!projectGuid)
|
|
353
374
|
throw new Error('--camera needs a linked project (the frame is hosted for the browser to fetch) — run `gipity link` first.');
|
|
354
|
-
console.log(muted(`Hosting camera feed ${opts.camera}
|
|
375
|
+
console.log(muted(`Hosting camera feed ${opts.camera}...`));
|
|
355
376
|
camera = await uploadCameraFeed(projectGuid, opts.camera);
|
|
356
377
|
}
|
|
357
378
|
// The pre-capture script the server runs after the post-load delay: the
|
|
@@ -386,7 +407,7 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
386
407
|
try {
|
|
387
408
|
entries = opts.json
|
|
388
409
|
? await doShoot()
|
|
389
|
-
: await withSpinner('Capturing
|
|
410
|
+
: await withSpinner('Capturing...', doShoot, { done: null });
|
|
390
411
|
}
|
|
391
412
|
catch (err) {
|
|
392
413
|
// The sandbox budget covers the WHOLE capture — load, --action, settle,
|
|
@@ -546,9 +567,17 @@ Waiting for the page to reach a state before the shot?
|
|
|
546
567
|
|
|
547
568
|
Capturing a state that needs an interaction (start a game, open a menu, dismiss a modal)?
|
|
548
569
|
Use --action to run JS in the page before the shot — it fires after the wait
|
|
549
|
-
(and after any --wait-for gate), then settles again so the result has painted.
|
|
550
|
-
|
|
551
|
-
|
|
570
|
+
(and after any --wait-for gate), then settles again so the result has painted. For
|
|
571
|
+
a multi-step driver (click, wait, click) pipe it as a heredoc with --file - (same
|
|
572
|
+
as 'page eval') instead of cramming it into one inline string:
|
|
573
|
+
gipity page screenshot "https://dev.gipity.ai/me/app/" --file - <<'EOF'
|
|
574
|
+
document.getElementById('load-sample').click();
|
|
575
|
+
await new Promise(r => setTimeout(r, 500));
|
|
576
|
+
document.getElementById('run').click();
|
|
577
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
578
|
+
EOF
|
|
579
|
+
Do NOT hand-roll a 'page eval' that returns a base64 image: the eval result is
|
|
580
|
+
capped (~16KB) and truncates the PNG.
|
|
552
581
|
|
|
553
582
|
Capturing an off-screen region or reading element data?
|
|
554
583
|
• --full captures the ENTIRE scrollable page (then crop to the region).
|
|
@@ -268,7 +268,7 @@ export const pageTestCommand = new Command('test')
|
|
|
268
268
|
// Interactive mode (--observe drives it):
|
|
269
269
|
.option('--observe <expr>', 'Interactive: JS expression sampled in each client to read shared state (e.g. presence count). Switches on interactive mode.')
|
|
270
270
|
.option('--action <expr>', 'Interactive: one-time JS run in each client before observing (e.g. fill a name + submit). {{label}}/{{i}} are substituted per client.')
|
|
271
|
-
.option('--labels <csv>', 'Per-client labels substituted for {{label}} in the URL/--action/--observe (default client-0, client-1,
|
|
271
|
+
.option('--labels <csv>', 'Per-client labels substituted for {{label}} in the URL/--action/--observe (default client-0, client-1, ...)')
|
|
272
272
|
.option('--hold <ms>', `Interactive: total observe window per client (${MIN_HOLD_MS}-${MAX_HOLD_MS}ms)`, '8000')
|
|
273
273
|
.option('--samples <k>', 'Interactive: number of observations across the hold window (2-30)', '6')
|
|
274
274
|
.option('--wait-for <selector>', 'Interactive: wait for this CSS selector before running --action (deterministic readiness gate)')
|
package/dist/commands/records.js
CHANGED
|
@@ -9,7 +9,7 @@ import { confirm } from '../utils.js';
|
|
|
9
9
|
// Records API is the only records surface that exists server-side; there is no
|
|
10
10
|
// /projects/<guid>/records mirror.)
|
|
11
11
|
export const recordsCommand = new Command('records')
|
|
12
|
-
.description('Manage records
|
|
12
|
+
.description('Manage app records (Gipity Records - validated CRUD with audit history)');
|
|
13
13
|
recordsCommand
|
|
14
14
|
.command('list')
|
|
15
15
|
.description('List record tables')
|
|
@@ -91,6 +91,19 @@ recordsCommand
|
|
|
91
91
|
const res = await get(`/api/${config.projectGuid}/records/${table}/${id}`);
|
|
92
92
|
console.log(opts.json ? JSON.stringify(res.data) : JSON.stringify(res.data, null, 2));
|
|
93
93
|
}));
|
|
94
|
+
recordsCommand
|
|
95
|
+
.command('history <table> <id>')
|
|
96
|
+
.description('Audit history for a record (who/what changed it, with English summaries)')
|
|
97
|
+
.option('--limit <n>', 'Max events', '20')
|
|
98
|
+
.option('--json', 'Output as JSON')
|
|
99
|
+
.action((table, id, opts) => run('History', async () => {
|
|
100
|
+
const config = requireConfig();
|
|
101
|
+
const res = await get(`/api/${config.projectGuid}/records/${table}/${id}/history?limit=${encodeURIComponent(opts.limit)}`);
|
|
102
|
+
printList(res.data, opts, 'No history for this record.', e => {
|
|
103
|
+
const summary = e.detail?.summary || `${e.action} ${e.entity_type} ${e.entity_id}`;
|
|
104
|
+
return `${muted(e.created_at)} ${bold(e.source || '-')} ${summary}`;
|
|
105
|
+
});
|
|
106
|
+
}));
|
|
94
107
|
recordsCommand
|
|
95
108
|
.command('create <table>')
|
|
96
109
|
.description('Create a record')
|
package/dist/commands/remove.js
CHANGED
|
@@ -22,7 +22,7 @@ export const removeCommand = new Command('remove')
|
|
|
22
22
|
const doRemove = () => post(`/projects/${config.projectGuid}/remove`, { name: kit });
|
|
23
23
|
const res = opts.json
|
|
24
24
|
? await doRemove()
|
|
25
|
-
: await withSpinner('Removing
|
|
25
|
+
: await withSpinner('Removing...', doRemove, { done: null });
|
|
26
26
|
const data = res.data;
|
|
27
27
|
// Pull the kit's deletions locally. Whitelist ONLY the kit's own removed files
|
|
28
28
|
// so they bypass the bulk-delete guard (the removal is explicit), while any
|
package/dist/commands/sandbox.js
CHANGED
|
@@ -6,7 +6,7 @@ import { resolveProjectContext, getConfigPath, getProjectRoot, shouldIgnore } fr
|
|
|
6
6
|
import { SCRATCH_IGNORE } from '../setup.js';
|
|
7
7
|
import { sync } from '../sync.js';
|
|
8
8
|
import { error as clrError, dim } from '../colors.js';
|
|
9
|
-
import { run } from '../helpers/index.js';
|
|
9
|
+
import { run, parseDuration } from '../helpers/index.js';
|
|
10
10
|
import { createProgressReporter, withSpinner } from '../progress.js';
|
|
11
11
|
const LANG_MAP = {
|
|
12
12
|
js: 'javascript',
|
|
@@ -144,6 +144,77 @@ function explainSplitArgs(args) {
|
|
|
144
144
|
lines.push('', 'Fix, in order of preference:', dim(' 1. Put the code in a file (best for anything with quotes, $(...), or newlines):'), ' gipity sandbox run --file script.sh', dim(' 2. Use the interpreter shorthand on a file:'), ' gipity sandbox run bash script.sh', dim(' 3. Keep it inline, but as ONE argument your shell will not split:'), " gipity sandbox run --language bash 'echo hi'");
|
|
145
145
|
return lines.join('\n');
|
|
146
146
|
}
|
|
147
|
+
// Regex fragments for the scratch directory names, derived from SCRATCH_IGNORE
|
|
148
|
+
// so this can never drift from what sync actually ignores. Each pattern's
|
|
149
|
+
// trailing '/' is dropped, dots are escaped, and a leading-`*` glob (`*_tmp/`)
|
|
150
|
+
// becomes a filename-char run so `frames_tmp/` is matched too. `shouldIgnore`
|
|
151
|
+
// is the authoritative filter downstream, so a slightly broad candidate here is
|
|
152
|
+
// harmless — it just gets dropped if sync wouldn't actually ignore it.
|
|
153
|
+
const SCRATCH_DIR_PATTERNS = SCRATCH_IGNORE
|
|
154
|
+
.map((p) => p.replace(/\/$/, ''))
|
|
155
|
+
.filter(Boolean)
|
|
156
|
+
.map((p) => p.replace(/[.]/g, '\\.').replace(/\*/g, '[A-Za-z0-9._-]*'));
|
|
157
|
+
/**
|
|
158
|
+
* References to a scratch directory inside a code body — the files the sandbox's
|
|
159
|
+
* auto-mirror will NOT carry, because sync ignores the scratch namespaces. Two
|
|
160
|
+
* shapes of the same trap are matched:
|
|
161
|
+
* - project-relative `tmp/nimbus.pdf` (how you name a root file locally), and
|
|
162
|
+
* - mirror-absolute `/work/tmp/nimbus.pdf` (how you name that same file inside
|
|
163
|
+
* the container, where the auto-mirror lands the project under /work/).
|
|
164
|
+
* Both are returned project-relative (the `/work/` prefix stripped) so the caller
|
|
165
|
+
* can resolve them on local disk and print a clean path. Deliberately NOT matched:
|
|
166
|
+
* `/tmp/out` (the container's own writable /tmp is fine) nor `mytmp/x` (word
|
|
167
|
+
* boundary). Covers every SCRATCH_IGNORE namespace, including the `*_tmp/` glob.
|
|
168
|
+
* Returns the distinct referenced paths. Exported for tests.
|
|
169
|
+
*/
|
|
170
|
+
export function scratchRefsInCode(code) {
|
|
171
|
+
const dirs = SCRATCH_DIR_PATTERNS.join('|');
|
|
172
|
+
if (!dirs)
|
|
173
|
+
return [];
|
|
174
|
+
// Match either a `/work/`-prefixed reference or a bare one that is not preceded
|
|
175
|
+
// by a slash (absolute/nested path) or a word/dot char (mytmp/, a.tmp/).
|
|
176
|
+
const re = new RegExp(`(?:\\/work\\/|(?<![\\w/.]))(?:${dirs})\\/[A-Za-z0-9._\\-\\/]+`, 'g');
|
|
177
|
+
return [...new Set((code.match(re) ?? []).map((m) => m.replace(/^\/work\//, '')))];
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* True when the code WRITES `p` at its FIRST use, so the file is produced inside
|
|
181
|
+
* the sandbox and any later reference (an `ls`/`cat` to inspect it, a second
|
|
182
|
+
* pass over it) reads that in-sandbox copy rather than an unmirrored input. Two
|
|
183
|
+
* output shapes are recognized, judged at the path's first occurrence:
|
|
184
|
+
* - a flagged/redirected output: `-o p`, `-of p`, `--output p`, `> p`, `>> p`, `tee p`;
|
|
185
|
+
* - an ffmpeg trailing-positional output: ffmpeg names its output as the last
|
|
186
|
+
* positional (NO `-o` flag), so a path in an ffmpeg segment that is not the
|
|
187
|
+
* `-i`/`--input` argument is an output.
|
|
188
|
+
* This keeps the write-then-inspect pattern in one script working (ffmpeg writes
|
|
189
|
+
* tmp/clip.mp4, then `ls tmp/clip.mp4`), while still catching a genuine staged
|
|
190
|
+
* INPUT read whose first use is not a write (`cat tmp/x`, `pandoc tmp/x.md ...`,
|
|
191
|
+
* `open('tmp/x')`, `ffmpeg -i tmp/x ...`). It also lets the normal iterate loop
|
|
192
|
+
* through: a scratch OUTPUT lands locally after the first run, so on a re-run it
|
|
193
|
+
* exists, but its first reference is still the write.
|
|
194
|
+
*/
|
|
195
|
+
export function isWriteTarget(code, p) {
|
|
196
|
+
const path = p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
197
|
+
const m = new RegExp(`(?:\\/work\\/)?${path}(?![\\w./-])`).exec(code);
|
|
198
|
+
if (!m)
|
|
199
|
+
return false;
|
|
200
|
+
const idx = m.index;
|
|
201
|
+
const before = code.slice(Math.max(0, idx - 40), idx);
|
|
202
|
+
// Flagged / redirected output position immediately before the path.
|
|
203
|
+
if (/(?:^|[\s'"`;(])(?:-o|-of|--out(?:put)?[= ]|>>?|tee(?:\s+-a)?)\s*['"`]?(?:\/work\/)?$/.test(before))
|
|
204
|
+
return true;
|
|
205
|
+
// ffmpeg's output is a trailing positional (no `-o`): a path in an ffmpeg
|
|
206
|
+
// segment that isn't the `-i`/`--input` argument is an output.
|
|
207
|
+
const segStart = Math.max(code.lastIndexOf('&&', idx), code.lastIndexOf('||', idx), code.lastIndexOf(';', idx), code.lastIndexOf('|', idx), code.lastIndexOf('\n', idx));
|
|
208
|
+
// segStart points at the FIRST char of the separator; a two-char operator
|
|
209
|
+
// (`&&`/`||`) leaves its second char in the slice, so strip any leading
|
|
210
|
+
// operator/space chars before reading the command word. Without this, the
|
|
211
|
+
// canonical `mkdir -p tmp && ffmpeg -i a.png tmp/out.mp4` idiom left a stray
|
|
212
|
+
// `&` that defeated the command extractor and false-blocked the re-run.
|
|
213
|
+
const seg = code.slice(segStart + 1, idx).replace(/^[\s&|;]+/, '');
|
|
214
|
+
const cmd = /^\s*([A-Za-z0-9_./-]+)/.exec(seg)?.[1]?.split('/').pop()?.toLowerCase();
|
|
215
|
+
const afterInputFlag = /(?:^|[\s'"`])(?:-i|--input)[= ]\s*['"`]?$/.test(before);
|
|
216
|
+
return cmd === 'ffmpeg' && !afterInputFlag;
|
|
217
|
+
}
|
|
147
218
|
/** Project-relative path from the process cwd, or undefined when there's
|
|
148
219
|
* no local config (one-off mode) or the cwd is at/above the project root. */
|
|
149
220
|
function resolveRelativeCwd() {
|
|
@@ -171,7 +242,16 @@ sandboxCommand
|
|
|
171
242
|
// positional arg is the canonical spelling). Accept it as a working alias
|
|
172
243
|
// rather than bouncing the guess into an unknown-option --help detour.
|
|
173
244
|
.addOption(new Option('--code <code>', 'Alias for the positional inline <code> arg').hideHelp())
|
|
174
|
-
|
|
245
|
+
// `bash -c '<code>'` and `node -e '<code>'` are the universal ways to run an
|
|
246
|
+
// inline command, and an agent reaches for them by reflex - but the bare
|
|
247
|
+
// `-c` / `-e` used to hit commander's OWN parser first ("unknown option
|
|
248
|
+
// '-c'"), before the interpreter-token logic below ever ran. Declaring them
|
|
249
|
+
// as hidden inline-code aliases makes the idiom just work: `-c`/`--cmd`
|
|
250
|
+
// implies bash, `-e`/`--eval` implies node, and a leading `bash`/`python`/
|
|
251
|
+
// `node` token still pins the language (so `python -c '...'` runs as python).
|
|
252
|
+
.addOption(new Option('-c, --cmd <code>', 'Inline code (bash idiom: `sandbox run bash -c "<code>"`)').hideHelp())
|
|
253
|
+
.addOption(new Option('-e, --eval <code>', 'Inline code (node idiom: `sandbox run node -e "<code>"`)').hideHelp())
|
|
254
|
+
.option('--timeout <seconds>', 'Execution timeout. Bare number = seconds; or pass an explicit unit that means the same on both this and `page eval --timeout`, e.g. --timeout 90s.', '30')
|
|
175
255
|
.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])
|
|
176
256
|
// Named "discard", not "no-sync": in Gipity vocabulary "sync" means the
|
|
177
257
|
// local<->cloud file sync, so a no-sync spelling reads as "give me the file
|
|
@@ -214,6 +294,8 @@ Examples:
|
|
|
214
294
|
$ gipity sandbox run --file build_report.py
|
|
215
295
|
$ gipity sandbox run python build_report.py # same thing, interpreter shorthand
|
|
216
296
|
$ gipity sandbox run bash "echo hi; ffmpeg -version" # inline, language pinned
|
|
297
|
+
$ gipity sandbox run bash -c "ffmpeg -version" # the bash -c idiom also works
|
|
298
|
+
$ gipity sandbox run node -e "console.log(process.version)" # and node -e
|
|
217
299
|
|
|
218
300
|
# Surgical: only these files are mirrored in
|
|
219
301
|
$ gipity sandbox run --language bash \\
|
|
@@ -242,13 +324,26 @@ GCC/Rust).
|
|
|
242
324
|
let inlineCode;
|
|
243
325
|
let filePath = opts.file;
|
|
244
326
|
let langFromInterp;
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
327
|
+
// Inline code may arrive via a flag alias instead of the positional slot:
|
|
328
|
+
// --code language-agnostic (needs --language or an interpreter token)
|
|
329
|
+
// --cmd / -c the `bash -c '<code>'` idiom → implies bash
|
|
330
|
+
// --eval / -e the `node -e '<code>'` idiom → implies node
|
|
331
|
+
// Fold whichever was given into the positional slot before the shape checks,
|
|
332
|
+
// preserving a leading interpreter token (`python -c '...'` stays python).
|
|
333
|
+
const flagCode = opts.code ?? opts.cmd ?? opts.eval;
|
|
334
|
+
if (flagCode !== undefined) {
|
|
335
|
+
// The positional slot may legitimately hold ONLY a leading interpreter
|
|
336
|
+
// token that pins the language; anything more is a duplicate copy of the code.
|
|
337
|
+
const leadIsInterp = args.length === 1 && INTERPRETERS[args[0].toLowerCase()] !== undefined;
|
|
338
|
+
if (args.length > (leadIsInterp ? 1 : 0)) {
|
|
339
|
+
console.error(clrError('Pass the code once: either positionally or via --code/--cmd/-c/--eval/-e, not both'));
|
|
249
340
|
process.exit(1);
|
|
250
341
|
}
|
|
251
|
-
|
|
342
|
+
const interp = leadIsInterp ? args[0]
|
|
343
|
+
: opts.cmd !== undefined ? 'bash'
|
|
344
|
+
: opts.eval !== undefined ? 'node'
|
|
345
|
+
: undefined;
|
|
346
|
+
args = interp ? [interp, flagCode] : [flagCode];
|
|
252
347
|
}
|
|
253
348
|
if (args.length >= 2 && INTERPRETERS[args[0].toLowerCase()] !== undefined) {
|
|
254
349
|
langFromInterp = INTERPRETERS[args[0].toLowerCase()];
|
|
@@ -267,7 +362,9 @@ GCC/Rust).
|
|
|
267
362
|
process.exit(1);
|
|
268
363
|
}
|
|
269
364
|
if (inlineCode !== undefined && filePath) {
|
|
270
|
-
console.error(clrError('Pass
|
|
365
|
+
console.error(clrError('Pass the code ONE way, not both: an inline <code> arg OR --file <path>.'));
|
|
366
|
+
console.error(dim(" inline: gipity sandbox run bash 'echo hi'"));
|
|
367
|
+
console.error(dim(' file: gipity sandbox run --file script.sh'));
|
|
271
368
|
process.exit(1);
|
|
272
369
|
}
|
|
273
370
|
if (inlineCode === undefined && !filePath) {
|
|
@@ -299,7 +396,10 @@ GCC/Rust).
|
|
|
299
396
|
}
|
|
300
397
|
// Args are good - now it's worth resolving (and announcing) the project.
|
|
301
398
|
const { config } = await resolveProjectContext();
|
|
302
|
-
|
|
399
|
+
// A bare number is native SECONDS; an explicit suffix (90s / 1500ms / 2m) is
|
|
400
|
+
// portable with `page eval --timeout` and converted to seconds here.
|
|
401
|
+
const durOpt = parseDuration(opts.timeout, 's');
|
|
402
|
+
const timeout = durOpt ? Math.max(1, Math.round(durOpt.value)) : parseInt(opts.timeout, 10);
|
|
303
403
|
const cwd = resolveRelativeCwd();
|
|
304
404
|
// A scratch path is never synced, so it can never reach the VFS the sandbox
|
|
305
405
|
// mirrors from - `--input tmp/frame.png` would fail inside the container with
|
|
@@ -311,6 +411,31 @@ GCC/Rust).
|
|
|
311
411
|
console.error(dim(' Stage inputs at a real project path (src/, docs/, assets/) and delete them afterward.'));
|
|
312
412
|
process.exit(1);
|
|
313
413
|
}
|
|
414
|
+
// Same trap, one step less obvious: a scratch file referenced as an INPUT
|
|
415
|
+
// from inside the code body (not via --input). The auto-mirror skips the
|
|
416
|
+
// scratch namespaces, so the container hits a bare "No such file" for a path
|
|
417
|
+
// the caller can plainly see on local disk. Catch it here, pre-sync, with the
|
|
418
|
+
// reason. We flag a reference only when that scratch file ACTUALLY EXISTS
|
|
419
|
+
// locally — which cleanly separates a staged input (must exist to be read)
|
|
420
|
+
// from a scratch OUTPUT target like `tmp/preview.png` (valid; doesn't exist
|
|
421
|
+
// yet, and outputs written under tmp/ come back on their own).
|
|
422
|
+
if (source) {
|
|
423
|
+
const scratchReads = scratchRefsInCode(source).filter((p) => shouldIgnore(p, SCRATCH_IGNORE)
|
|
424
|
+
&& existsSync(resolve(process.cwd(), p))
|
|
425
|
+
// A previous run's OUTPUT lands locally, so on a re-run it exists -
|
|
426
|
+
// but a path written at its FIRST use (ffmpeg output, -o, >) is
|
|
427
|
+
// produced in the sandbox, so a later inspect (`ls`/`cat`) in the same
|
|
428
|
+
// script reads that copy, not the mirror. Only a genuine staged input
|
|
429
|
+
// read (first use isn't a write) is blocked.
|
|
430
|
+
&& !isWriteTarget(source, p));
|
|
431
|
+
if (scratchReads.length) {
|
|
432
|
+
console.error(clrError(`Scratch files are never mirrored into the sandbox, so it can't read: ${scratchReads.join(', ')}`));
|
|
433
|
+
console.error(dim(` ${SCRATCH_IGNORE.join(', ')} are ignored by sync, so the sandbox never sees them.`));
|
|
434
|
+
console.error(dim(' Stage the input at a real project path (src/, docs/, assets/) and delete it afterward.'));
|
|
435
|
+
console.error(dim(' (Writing OUTPUT under tmp/ is fine — scratch outputs come back to your local tmp/.)'));
|
|
436
|
+
process.exit(1);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
314
439
|
// Push local working-tree changes up before executing. The sandbox mirrors
|
|
315
440
|
// the *server* (VFS), not the local cwd, so any input staged outside Claude's
|
|
316
441
|
// Write/Edit auto-push hook - a Bash `cp`/`ffmpeg`/redirect, or any external
|
|
@@ -339,7 +464,7 @@ GCC/Rust).
|
|
|
339
464
|
});
|
|
340
465
|
const res = opts.json
|
|
341
466
|
? await doRun()
|
|
342
|
-
: await withSpinner('Running in sandbox
|
|
467
|
+
: await withSpinner('Running in sandbox...', doRun, { done: null });
|
|
343
468
|
// Pull sandbox-written outputs down to the local cwd automatically. The
|
|
344
469
|
// server has already mirrored them into the project (VFS) and handed back
|
|
345
470
|
// the exact list, so honoring it here means files land locally without a
|
|
@@ -407,6 +532,18 @@ GCC/Rust).
|
|
|
407
532
|
if (pulledLocal)
|
|
408
533
|
console.log(dim("Not pulled locally - run 'gipity sync' to fetch them."));
|
|
409
534
|
}
|
|
535
|
+
// A sandbox output that lands directly at the project ROOT (a bare
|
|
536
|
+
// filename, no directory) is almost always a throwaway probe, not a
|
|
537
|
+
// real asset - real assets live under src/, functions/, assets/, etc.
|
|
538
|
+
// Root files sync and are picked up by the deploy `files` phase, so a
|
|
539
|
+
// stray one ships unless the caller remembers to delete it. Point at
|
|
540
|
+
// tmp/ (local-only, never synced or deployed) at the moment of use so
|
|
541
|
+
// the next probe never has to be hand-cleaned off a deployable path.
|
|
542
|
+
const rootStrays = [...onDisk, ...notOnDisk].filter((f) => !f.includes('/'));
|
|
543
|
+
if (rootStrays.length > 0) {
|
|
544
|
+
console.log(dim(`\nNote: ${rootStrays.join(', ')} landed at the project root and will deploy with the files phase.`));
|
|
545
|
+
console.log(dim('For a throwaway probe or fixture, write it under tmp/ instead - scratch outputs stay on local disk and never sync or deploy.'));
|
|
546
|
+
}
|
|
410
547
|
}
|
|
411
548
|
if (scratchWritten.length > 0) {
|
|
412
549
|
console.log('\nScratch outputs (local only - never synced or deployed):');
|
|
@@ -418,7 +555,42 @@ GCC/Rust).
|
|
|
418
555
|
for (const f of res.data.skippedOutputFiles)
|
|
419
556
|
console.log(dim(`${f}`));
|
|
420
557
|
}
|
|
558
|
+
// Silent-failure guard: a command that exits 0 yet leaves a 0-byte output
|
|
559
|
+
// almost always failed silently - a broken ffmpeg filter, or an OOM the
|
|
560
|
+
// shell swallowed. Without this the only signal is an `ls -la` afterward,
|
|
561
|
+
// and the agent burns a turn inferring "the encode failed" from a file
|
|
562
|
+
// size. Name it at the moment it happens, for scratch (bytes returned
|
|
563
|
+
// inline) and synced outputs (stat on local disk) alike.
|
|
564
|
+
if (res.data.exitCode === 0) {
|
|
565
|
+
const emptyScratch = (res.data.scratchFiles ?? [])
|
|
566
|
+
.filter((f) => f.contentBase64.length === 0)
|
|
567
|
+
.map((f) => f.path.replace(/\\/g, '/'));
|
|
568
|
+
const root = getProjectRoot();
|
|
569
|
+
const emptyOutputs = pulledLocal && root
|
|
570
|
+
? (res.data.outputFiles ?? []).filter((f) => {
|
|
571
|
+
try {
|
|
572
|
+
return statSync(resolve(root, f)).size === 0;
|
|
573
|
+
}
|
|
574
|
+
catch {
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
})
|
|
578
|
+
: [];
|
|
579
|
+
const empties = [...new Set([...emptyScratch, ...emptyOutputs])];
|
|
580
|
+
if (empties.length > 0) {
|
|
581
|
+
console.error(dim(`Note: ${empties.join(', ')} was written but is empty (0 bytes) - the command exited 0 but likely failed silently (a broken filter, or an out-of-memory kill the shell swallowed). Re-check the command, or shrink the job if it may have run out of memory.`));
|
|
582
|
+
}
|
|
583
|
+
}
|
|
421
584
|
if (res.data.exitCode !== 0) {
|
|
585
|
+
// A process killed by a signal exits 128+signal. 137 (SIGKILL) on the
|
|
586
|
+
// sandbox is almost always the OS OOM-killer - the raw `/work/_run.sh:
|
|
587
|
+
// Killed` line the shell prints never says so, so a bare retry at the
|
|
588
|
+
// same size just OOMs again. Name it and point at the usual fix.
|
|
589
|
+
if (res.data.exitCode === 137) {
|
|
590
|
+
console.error(clrError('The sandbox process was killed (exit 137) - it ran out of memory.'));
|
|
591
|
+
console.error(dim(' Shrink the job: for ffmpeg, downscale (-vf scale=640:-2) and add -preset ultrafast;'));
|
|
592
|
+
console.error(dim(' for ImageMagick, lower -density/-resize. Process in smaller chunks if it still OOMs.'));
|
|
593
|
+
}
|
|
422
594
|
// No "did you mean another language?" hint is needed: the language is now
|
|
423
595
|
// always something the caller pinned, never a silent default we chose.
|
|
424
596
|
process.exit(res.data.exitCode);
|
package/dist/commands/save.js
CHANGED
|
@@ -52,7 +52,7 @@ export const saveCommand = new Command('save')
|
|
|
52
52
|
const doExport = () => downloadWithHeaders(`/projects/${config.projectGuid}/export`);
|
|
53
53
|
const { buffer, headers } = opts.json
|
|
54
54
|
? await doExport()
|
|
55
|
-
: await withSpinner('Exporting
|
|
55
|
+
: await withSpinner('Exporting...', doExport, { done: null });
|
|
56
56
|
const filename = dispositionFilename(headers) ?? `${config.projectSlug}.gip`;
|
|
57
57
|
let dest = path.resolve(output ?? filename);
|
|
58
58
|
if (output && fs.existsSync(dest) && fs.statSync(dest).isDirectory()) {
|
package/dist/commands/secrets.js
CHANGED
|
@@ -34,7 +34,7 @@ secretsCommand
|
|
|
34
34
|
}
|
|
35
35
|
console.log(bold(`${res.data.length} ${scope} secret${res.data.length === 1 ? '' : 's'}:`));
|
|
36
36
|
for (const s of res.data) {
|
|
37
|
-
const masked = s.preview ? muted(
|
|
37
|
+
const masked = s.preview ? muted(`...${s.preview}`) : muted('(hidden)');
|
|
38
38
|
console.log(` ${s.name} ${masked} ${muted(`updated ${new Date(s.updated_at).toLocaleDateString()}`)}`);
|
|
39
39
|
}
|
|
40
40
|
}));
|
package/dist/commands/service.js
CHANGED
|
@@ -21,12 +21,17 @@ const SERVICES = [
|
|
|
21
21
|
];
|
|
22
22
|
export const serviceCommand = new Command('service')
|
|
23
23
|
.description('Call an app service (llm, tts, image, transcribe, ...)')
|
|
24
|
-
.addHelpText('after', '\nCall a Gipity app service: LLM, image, music, TTS, and more
|
|
24
|
+
.addHelpText('after', '\nCall a Gipity app service: LLM, image, music, TTS, and more.\nPer-service docs: gipity skill read app-llm | app-tts | app-image | app-video | app-audio (transcribe, sound, music) | app-location');
|
|
25
25
|
serviceCommand
|
|
26
26
|
.command('list')
|
|
27
27
|
.description('List callable app services')
|
|
28
|
-
.
|
|
28
|
+
.option('--json', 'Output the service list as JSON')
|
|
29
|
+
.action((opts) => run('Services', async () => {
|
|
29
30
|
requireConfig();
|
|
31
|
+
if (opts.json) {
|
|
32
|
+
console.log(JSON.stringify(SERVICES));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
30
35
|
const width = SERVICES.reduce((m, s) => Math.max(m, s.name.length), 0);
|
|
31
36
|
console.log('Call one with `gipity service call <name> \'{"json":"body"}\'`');
|
|
32
37
|
console.log(muted('(GET endpoints like llm/models, tts/voices take --get and no body)'));
|
package/dist/commands/setup.js
CHANGED
|
@@ -19,8 +19,7 @@ import { Command } from 'commander';
|
|
|
19
19
|
import { getAuth, sessionExpired, refreshTokenIfNeeded } from '../auth.js';
|
|
20
20
|
import { interactiveLogin } from '../login-flow.js';
|
|
21
21
|
import { runRelaySetup } from '../relay/onboarding.js';
|
|
22
|
-
import
|
|
23
|
-
import { bold, brand, success, muted, error as clrError } from '../colors.js';
|
|
22
|
+
import { bold, muted, error as clrError } from '../colors.js';
|
|
24
23
|
export const connectCommand = new Command('connect')
|
|
25
24
|
.alias('setup') // legacy name, kept working but not advertised
|
|
26
25
|
.description('Connect this computer to gipity.ai so the web CLI can drive it (no project, no launch)')
|
|
@@ -46,11 +45,10 @@ export const connectCommand = new Command('connect')
|
|
|
46
45
|
console.log('');
|
|
47
46
|
// ── Step 2: Relay setup (always run — the user asked for it) ───────
|
|
48
47
|
const enabled = await runRelaySetup({ mode: 'run-now' });
|
|
49
|
-
// ── Step 3: Done. No project, no Claude Code launch.
|
|
48
|
+
// ── Step 3: Done. No project, no Claude Code launch. The shared flow
|
|
49
|
+
// already printed its own "Done!" line - just add the manage hint. ──
|
|
50
50
|
if (enabled) {
|
|
51
|
-
|
|
52
|
-
console.log(` ${success('Done')} — your relay ${running ? 'is running in the background' : 'is set up'} and will start with your computer.`);
|
|
53
|
-
console.log(` ${muted('Open')} ${brand('gipity.ai')} ${muted('and start a chat to drive your coding agent here. Manage it with `gipity relay status`.')}`);
|
|
51
|
+
console.log(` ${muted('Manage it anytime with `gipity relay status`.')}`);
|
|
54
52
|
}
|
|
55
53
|
else {
|
|
56
54
|
console.log(` ${muted('No relay set up. Run `gipity connect` again anytime, or `gipity build` to start building.')}`);
|
package/dist/commands/status.js
CHANGED
|
@@ -4,7 +4,7 @@ import { join, resolve } from 'path';
|
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
import { getAuth, sessionExpired } from '../auth.js';
|
|
6
6
|
import { get, usingEnvToken, ApiError } from '../api.js';
|
|
7
|
-
import { getConfig, liveUrl } from '../config.js';
|
|
7
|
+
import { getConfig, liveUrl, resolveApiBase } from '../config.js';
|
|
8
8
|
import { brand, success, warning, muted, error as clrError } from '../colors.js';
|
|
9
9
|
import { GIPITY_PLUGIN_ID, GIPITY_MARKETPLACE_NAME, setupClaudeHooks, ensureGipityPlugin, ensureGipityPluginInstalled, userScopeInstallState } from '../setup.js';
|
|
10
10
|
import { flushBugQueue } from '../bug-queue.js';
|
|
@@ -39,15 +39,20 @@ function checkGipityPlugin() {
|
|
|
39
39
|
const stale = missing.length === 1 && missing[0] === 'install' && install.exists;
|
|
40
40
|
return { missing, ok: missing.length === 0, stale };
|
|
41
41
|
}
|
|
42
|
+
/** `account` is the server's `account_slug` for the authenticated identity
|
|
43
|
+
* (only populated on 'ok' - the /users/me call that proved the probe also
|
|
44
|
+
* returns it) - used for the ownership cross-check against the locally
|
|
45
|
+
* cached project (bug cli#S2: a wrong-account session used to read as
|
|
46
|
+
* fully healthy until a later command 404'd). */
|
|
42
47
|
async function probeAuth(loggedIn) {
|
|
43
48
|
if (!loggedIn && !usingEnvToken())
|
|
44
|
-
return 'none';
|
|
49
|
+
return { state: 'none', account: null };
|
|
45
50
|
if (!usingEnvToken() && sessionExpired())
|
|
46
|
-
return 'expired';
|
|
51
|
+
return { state: 'expired', account: null };
|
|
47
52
|
// Cap the probe well below the API layer's 60s request timeout - status is
|
|
48
53
|
// a diagnostic command and must answer fast even when the network is dark.
|
|
49
|
-
const timeout = new Promise(res => setTimeout(() => res('unreachable'), 5000).unref?.());
|
|
50
|
-
const call = get('/users/me').then(() => 'ok', (err) => (err instanceof ApiError && err.statusCode === 401 ? 'rejected' : 'unreachable'));
|
|
54
|
+
const timeout = new Promise(res => setTimeout(() => res({ state: 'unreachable', account: null }), 5000).unref?.());
|
|
55
|
+
const call = get('/users/me').then((res) => ({ state: 'ok', account: res.data?.accountSlug ?? null }), (err) => ({ state: (err instanceof ApiError && err.statusCode === 401 ? 'rejected' : 'unreachable'), account: null }));
|
|
51
56
|
return Promise.race([call, timeout]);
|
|
52
57
|
}
|
|
53
58
|
// `whoami` is the name agents reach for first when they want the signed-in
|
|
@@ -71,6 +76,12 @@ export const statusCommand = new Command('status')
|
|
|
71
76
|
// is empty, which is the common case, so this stays a no-op most runs.
|
|
72
77
|
const queueDelivered = (auth && !sessionExpired()) ? await flushBugQueue().catch(() => 0) : 0;
|
|
73
78
|
const probe = await probeAuth(!!auth);
|
|
79
|
+
// RBAC lets a project be shared to a collaborator whose own account
|
|
80
|
+
// legitimately differs from the project owner's - so this is advisory,
|
|
81
|
+
// never a hard error. Only meaningful once a live 'ok' call has actually
|
|
82
|
+
// returned an account to compare against config.accountSlug.
|
|
83
|
+
const accountMismatch = probe.state === 'ok' && !!config && !!probe.account && probe.account !== config.accountSlug;
|
|
84
|
+
const apiBaseInUse = resolveApiBase();
|
|
74
85
|
if (opts.json) {
|
|
75
86
|
console.log(JSON.stringify({
|
|
76
87
|
project: config ? {
|
|
@@ -78,17 +89,21 @@ export const statusCommand = new Command('status')
|
|
|
78
89
|
slug: config.projectSlug,
|
|
79
90
|
account: config.accountSlug,
|
|
80
91
|
apiBase: config.apiBase,
|
|
92
|
+
apiBaseInUse,
|
|
81
93
|
url: liveUrl(config),
|
|
82
94
|
} : null,
|
|
83
95
|
// `valid` reflects the refresh token (the real session) - access
|
|
84
96
|
// tokens auto-renew, so their expiry must not read as "invalid".
|
|
85
97
|
// `probe` is what one live call just proved: 'rejected' means every
|
|
86
98
|
// authenticated command will fail even though `valid` reads true.
|
|
99
|
+
// 'mismatch' overrides 'ok' when the live account isn't the one that
|
|
100
|
+
// owns this project - `valid` still reads true (the token IS valid).
|
|
87
101
|
auth: (auth || usingEnvToken()) ? {
|
|
88
102
|
email: auth?.email,
|
|
103
|
+
account: probe.account,
|
|
89
104
|
source: usingEnvToken() ? 'agent-token' : 'session',
|
|
90
|
-
valid: usingEnvToken() ? probe !== 'rejected' : !sessionExpired(),
|
|
91
|
-
probe,
|
|
105
|
+
valid: usingEnvToken() ? probe.state !== 'rejected' : !sessionExpired(),
|
|
106
|
+
probe: accountMismatch ? 'mismatch' : probe.state,
|
|
92
107
|
} : null,
|
|
93
108
|
plugin: hookCheck,
|
|
94
109
|
}, null, 2));
|
|
@@ -102,28 +117,42 @@ export const statusCommand = new Command('status')
|
|
|
102
117
|
console.log(`${muted('Account:')} ${config.accountSlug}`);
|
|
103
118
|
console.log(`${muted('Live:')} ${liveUrl(config)}`);
|
|
104
119
|
console.log(`${muted('API:')} ${config.apiBase}`);
|
|
120
|
+
// apiBase is only what THIS project recorded - resolveApiBase() is what
|
|
121
|
+
// every real request actually uses (it can diverge via GIPITY_API_BASE,
|
|
122
|
+
// --api-base, or a disallowed host being dropped to the default). Surface
|
|
123
|
+
// the divergence rather than silently trusting the recorded value.
|
|
124
|
+
if (apiBaseInUse !== config.apiBase) {
|
|
125
|
+
console.log(`${muted('API (in use):')} ${warning(apiBaseInUse)} ${muted('(overrides .gipity.json — GIPITY_API_BASE / --api-base / allowlist)')}`);
|
|
126
|
+
}
|
|
105
127
|
if (config.agentGuid)
|
|
106
128
|
console.log(`${muted('Agent:')} ${config.agentGuid}`);
|
|
107
129
|
}
|
|
108
130
|
if (usingEnvToken()) {
|
|
109
|
-
console.log(`${muted('Auth:')} ${probe === 'rejected'
|
|
131
|
+
console.log(`${muted('Auth:')} ${probe.state === 'rejected'
|
|
110
132
|
? warning('agent API token (GIPITY_TOKEN) rejected by the server — mint a new one: gipity skill read agent-deploy')
|
|
111
|
-
: success('agent API token (GIPITY_TOKEN)')}${probe === 'unreachable' ? ` ${muted('(unverified — API unreachable)')}` : ''}`);
|
|
133
|
+
: success('agent API token (GIPITY_TOKEN)')}${probe.state === 'unreachable' ? ` ${muted('(unverified — API unreachable)')}` : ''}`);
|
|
112
134
|
}
|
|
113
135
|
else if (!auth) {
|
|
114
136
|
console.log(`${muted('Auth:')} ${warning('not logged in. Run: gipity login')}`);
|
|
115
137
|
}
|
|
116
|
-
else if (probe === 'expired') {
|
|
138
|
+
else if (probe.state === 'expired') {
|
|
117
139
|
console.log(`${muted('Auth:')} ${warning(`session expired for ${auth.email}. Run: gipity login (headless/CI: set GIPITY_TOKEN — gipity skill read agent-deploy)`)}`);
|
|
118
140
|
}
|
|
119
|
-
else if (probe === 'rejected') {
|
|
141
|
+
else if (probe.state === 'rejected') {
|
|
120
142
|
// Locally fresh but the server says no (refresh token rotated away or
|
|
121
143
|
// revoked). Without the live probe this printed a green identity while
|
|
122
144
|
// every authenticated command failed.
|
|
123
145
|
console.log(`${muted('Auth:')} ${warning(`session for ${auth.email} was rejected by the server. Run: gipity login (headless/CI: set GIPITY_TOKEN — gipity skill read agent-deploy)`)}`);
|
|
124
146
|
}
|
|
125
147
|
else {
|
|
126
|
-
console.log(`${muted('Auth:')} ${success(auth.email)}${probe === 'unreachable' ? ` ${muted('(unverified — API unreachable)')}` : ''}`);
|
|
148
|
+
console.log(`${muted('Auth:')} ${success(auth.email)}${probe.state === 'unreachable' ? ` ${muted('(unverified — API unreachable)')}` : ''}`);
|
|
149
|
+
}
|
|
150
|
+
// Source-independent (session or GIPITY_TOKEN): a mismatch under an agent
|
|
151
|
+
// token is the same wrong-account class and must not be hidden inside the
|
|
152
|
+
// cascade above, which only special-cases 'rejected' for that source.
|
|
153
|
+
if (accountMismatch) {
|
|
154
|
+
console.log(`${muted('Account:')} ${warning(`logged-in account (${probe.account}) differs from this project's account (${config.accountSlug}). If you didn't expect this you may be logged into the wrong account — run: gipity login`)}`);
|
|
155
|
+
console.log(muted('(If this project was shared with you via gipity rbac, this is expected.)'));
|
|
127
156
|
}
|
|
128
157
|
if (queueDelivered > 0) {
|
|
129
158
|
console.log(`${muted('Bug queue:')} ${success(`delivered ${queueDelivered} queued bug report${queueDelivered === 1 ? '' : 's'}`)}`);
|
package/dist/commands/storage.js
CHANGED
|
@@ -50,12 +50,12 @@ function printUsage(d) {
|
|
|
50
50
|
console.log(`${bold('By project')} ${muted(scope)}`);
|
|
51
51
|
for (const p of d.projects) {
|
|
52
52
|
const name = p.projectName ?? '(no project)';
|
|
53
|
-
row(name.length > 16 ? `${name.slice(0,
|
|
53
|
+
row(name.length > 16 ? `${name.slice(0, 13)}...` : name, formatBytes(p.liveBytes), `${p.liveFiles.toLocaleString()} files`);
|
|
54
54
|
}
|
|
55
55
|
if (hidden > 0) {
|
|
56
56
|
const shownBytes = d.projects.reduce((n, p) => n + p.liveBytes, 0);
|
|
57
57
|
const shownFiles = d.projects.reduce((n, p) => n + p.liveFiles, 0);
|
|
58
|
-
row(
|
|
58
|
+
row(`...and ${hidden.toLocaleString()} more`, formatBytes(totals.liveBytes - shownBytes), `${(totals.liveFiles - shownFiles).toLocaleString()} files`);
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
const r = d.versionRetention;
|