gipity 1.1.4 → 1.1.6
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/agents/agy.js +52 -0
- package/dist/agents/index.js +2 -0
- package/dist/api.js +19 -3
- package/dist/banner.js +3 -5
- package/dist/capture/sources/agy.js +88 -0
- package/dist/client-context.js +9 -0
- package/dist/commands/add.js +1 -1
- package/dist/commands/brand.js +125 -0
- package/dist/commands/build.js +13 -12
- package/dist/commands/chat.js +1 -1
- package/dist/commands/deploy.js +1 -1
- package/dist/commands/fn.js +2 -8
- package/dist/commands/generate.js +5 -5
- package/dist/commands/gmail.js +1 -1
- package/dist/commands/init.js +2 -2
- package/dist/commands/job.js +10 -5
- package/dist/commands/key.js +91 -0
- package/dist/commands/load.js +2 -2
- package/dist/commands/login.js +7 -2
- package/dist/commands/page-eval.js +47 -8
- package/dist/commands/page-fetch.js +14 -10
- package/dist/commands/page-inspect.js +2 -2
- package/dist/commands/page-screenshot.js +5 -5
- package/dist/commands/page-test.js +1 -1
- package/dist/commands/records.js +57 -19
- package/dist/commands/remove.js +1 -1
- package/dist/commands/sandbox.js +1 -1
- package/dist/commands/save.js +1 -1
- package/dist/commands/secrets.js +1 -1
- package/dist/commands/service.js +1 -1
- 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 +1 -1
- package/dist/commands/token.js +6 -1
- package/dist/commands/uninstall.js +14 -1
- package/dist/commands/upload.js +1 -1
- package/dist/commands/workflow.js +1 -1
- package/dist/hooks/capture-runner.js +20 -8
- package/dist/index.js +1046 -518
- package/dist/knowledge.js +4 -1
- package/dist/login-flow.js +44 -2
- package/dist/project-setup.js +2 -0
- package/dist/relay/diagnostics.js +4 -2
- package/dist/relay/onboarding.js +26 -44
- package/dist/setup.js +198 -16
- package/dist/sync.js +6 -6
- package/dist/template-vars.js +74 -0
- package/dist/utils.js +60 -3
- package/package.json +2 -2
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { get, post, del } from '../api.js';
|
|
3
|
+
import { resolveProjectContext } from '../config.js';
|
|
4
|
+
import { bold, muted, success, warning } from '../colors.js';
|
|
5
|
+
import { run, printList } from '../helpers/index.js';
|
|
6
|
+
/**
|
|
7
|
+
* Project API keys - the revocable secret a script, cron, or agent sends as
|
|
8
|
+
* `X-Api-Key` to write to YOUR app without a browser login.
|
|
9
|
+
*
|
|
10
|
+
* These are per-project and app-facing; `gipity token` mints account-level
|
|
11
|
+
* agent tokens (gip_at_*) that drive the CLI itself. Without this command the
|
|
12
|
+
* only mint path was raw curl, so agents asked for "a key I can generate and
|
|
13
|
+
* revoke" built their own key table instead of using the platform's.
|
|
14
|
+
*/
|
|
15
|
+
export const keyCommand = new Command('key')
|
|
16
|
+
.description('Manage project API keys for scripts and agents (X-Api-Key)')
|
|
17
|
+
.addHelpText('after', `
|
|
18
|
+
Give a script, cron, or agent write access to your app without a login:
|
|
19
|
+
|
|
20
|
+
gipity key create "laptop importer" --role editor
|
|
21
|
+
# the script sends: X-Api-Key: <the key printed once above>
|
|
22
|
+
|
|
23
|
+
Inside a function the caller shows up as ctx.auth.via === 'api_key' with
|
|
24
|
+
ctx.auth.apiKeyName set to the key's name - stamp that on rows to tell
|
|
25
|
+
script-written entries from hand-entered ones. Never build your own key table.
|
|
26
|
+
|
|
27
|
+
Account-level tokens that run the CLI headlessly are a different thing: see
|
|
28
|
+
gipity token --help.`);
|
|
29
|
+
const fmtDate = (d) => (d ? new Date(d).toLocaleDateString() : 'never');
|
|
30
|
+
const projectOpt = ['--project <guid-or-slug>', 'Target a specific project instead of cwd / Home'];
|
|
31
|
+
async function projectGuid(opts) {
|
|
32
|
+
const { config } = await resolveProjectContext({ projectOverride: opts.project });
|
|
33
|
+
return config.projectGuid;
|
|
34
|
+
}
|
|
35
|
+
keyCommand
|
|
36
|
+
.command('create <name>')
|
|
37
|
+
.description('Mint a project API key (shown once). Give it a name you will recognize later')
|
|
38
|
+
.option('--role <role>', 'viewer | editor | owner (default: viewer)', 'viewer')
|
|
39
|
+
.option('--expires-days <n>', 'Days until the key expires (default: never)')
|
|
40
|
+
.option(...projectOpt)
|
|
41
|
+
.option('--json', 'Output as JSON')
|
|
42
|
+
.action((name, opts) => run('Create', async () => {
|
|
43
|
+
const body = { name, role: opts.role };
|
|
44
|
+
if (opts.expiresDays !== undefined) {
|
|
45
|
+
const days = parseInt(opts.expiresDays, 10);
|
|
46
|
+
if (!Number.isFinite(days) || days <= 0)
|
|
47
|
+
throw new Error('--expires-days must be a positive number of days');
|
|
48
|
+
body.expires_in_days = days;
|
|
49
|
+
}
|
|
50
|
+
const res = await post(`/projects/${await projectGuid(opts)}/api-keys`, body);
|
|
51
|
+
const k = res.data;
|
|
52
|
+
if (opts.json) {
|
|
53
|
+
console.log(JSON.stringify(k));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const expNote = k.expires_at ? ` (expires ${fmtDate(k.expires_at)})` : ' (never expires)';
|
|
57
|
+
console.log(success(`Created API key ${bold(k.short_guid)} "${k.name}" as ${k.role}${muted(expNote)}.`));
|
|
58
|
+
console.log('');
|
|
59
|
+
console.log(k.key);
|
|
60
|
+
console.log('');
|
|
61
|
+
console.log(muted('Send it from your script on every request:'));
|
|
62
|
+
console.log(muted(` curl -H "X-Api-Key: ${k.key}" ...`));
|
|
63
|
+
console.log(muted(`Revoke it any time: gipity key revoke ${k.short_guid}`));
|
|
64
|
+
console.log('');
|
|
65
|
+
console.log(warning('Copy it now - it will not be shown again.'));
|
|
66
|
+
}));
|
|
67
|
+
keyCommand
|
|
68
|
+
.command('list')
|
|
69
|
+
.alias('ls')
|
|
70
|
+
.description('List this project\'s API keys (values are never shown again)')
|
|
71
|
+
.option(...projectOpt)
|
|
72
|
+
.option('--json', 'Output as JSON')
|
|
73
|
+
.action((opts) => run('List', async () => {
|
|
74
|
+
const res = await get(`/projects/${await projectGuid(opts)}/api-keys`);
|
|
75
|
+
printList(res.data, opts, 'No API keys. Mint one with: gipity key create "my script" --role editor', (k) => `${bold(k.short_guid)} ${k.name} ${muted(`${k.role} ${k.prefix}… last used ${fmtDate(k.last_used_at)} expires ${fmtDate(k.expires_at)}`)}`);
|
|
76
|
+
}));
|
|
77
|
+
keyCommand
|
|
78
|
+
.command('revoke <short_guid>')
|
|
79
|
+
.alias('rm')
|
|
80
|
+
.description('Revoke a project API key (instant, irreversible)')
|
|
81
|
+
.option(...projectOpt)
|
|
82
|
+
.option('--json', 'Output as JSON')
|
|
83
|
+
.action((shortGuid, opts) => run('Revoke', async () => {
|
|
84
|
+
await del(`/projects/${await projectGuid(opts)}/api-keys/${encodeURIComponent(shortGuid)}`);
|
|
85
|
+
if (opts.json) {
|
|
86
|
+
console.log(JSON.stringify({ short_guid: shortGuid, revoked: true }));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
console.log(success(`Revoked API key ${bold(shortGuid)}.`));
|
|
90
|
+
}));
|
|
91
|
+
//# sourceMappingURL=key.js.map
|
package/dist/commands/load.js
CHANGED
|
@@ -152,7 +152,7 @@ export const loadCommand = new Command('load')
|
|
|
152
152
|
if (opts.inspect) {
|
|
153
153
|
const res = opts.json
|
|
154
154
|
? await doInspect()
|
|
155
|
-
: await withSpinner('Inspecting
|
|
155
|
+
: await withSpinner('Inspecting...', doInspect, { done: null });
|
|
156
156
|
if (opts.json) {
|
|
157
157
|
console.log(JSON.stringify(res.data));
|
|
158
158
|
return;
|
|
@@ -162,7 +162,7 @@ export const loadCommand = new Command('load')
|
|
|
162
162
|
}
|
|
163
163
|
const { data, partial } = opts.json
|
|
164
164
|
? await doImport()
|
|
165
|
-
: await withSpinner('Importing
|
|
165
|
+
: await withSpinner('Importing...', doImport, { done: null });
|
|
166
166
|
const project = data.project;
|
|
167
167
|
// Materialize a local dir and link it, exactly like `gipity project create`:
|
|
168
168
|
// `.gipity.json` is written directly inside the new dir, then a soft sync
|
package/dist/commands/login.js
CHANGED
|
@@ -4,6 +4,7 @@ import { publicPost } from '../api.js';
|
|
|
4
4
|
import { prompt, decodeJwtExp } from '../utils.js';
|
|
5
5
|
import { success, error as clrError, muted } from '../colors.js';
|
|
6
6
|
import { flushBugQueue } from '../bug-queue.js';
|
|
7
|
+
import { warnBeforeCodeIfUnexpectedNewAccount, warnIfUnexpectedNewAccount } from '../login-flow.js';
|
|
7
8
|
export const loginCommand = new Command('login')
|
|
8
9
|
.description('Log in or sign up')
|
|
9
10
|
.option('--email <email>', 'Email address')
|
|
@@ -19,9 +20,10 @@ export const loginCommand = new Command('login')
|
|
|
19
20
|
}
|
|
20
21
|
// Email only → send code and exit (non-interactive step 1)
|
|
21
22
|
if (email && !code) {
|
|
22
|
-
await publicPost('/auth/login', { email });
|
|
23
|
+
const sendRes = await publicPost('/auth/login', { email });
|
|
23
24
|
console.log('Check your email for a 6-digit code.');
|
|
24
25
|
console.log(muted(`Then run: gipity login --email ${email} --code <code>`));
|
|
26
|
+
warnBeforeCodeIfUnexpectedNewAccount(sendRes.isNewUser, email);
|
|
25
27
|
return;
|
|
26
28
|
}
|
|
27
29
|
// Fully interactive flow
|
|
@@ -34,9 +36,10 @@ export const loginCommand = new Command('login')
|
|
|
34
36
|
console.error(clrError('Email required.'));
|
|
35
37
|
process.exit(1);
|
|
36
38
|
}
|
|
37
|
-
await publicPost('/auth/login', { email });
|
|
39
|
+
const sendRes = await publicPost('/auth/login', { email });
|
|
38
40
|
console.log('');
|
|
39
41
|
console.log('Check your email for a 6-digit code.');
|
|
42
|
+
warnBeforeCodeIfUnexpectedNewAccount(sendRes.isNewUser, email);
|
|
40
43
|
code = await prompt('Code: ');
|
|
41
44
|
await verify(email, code);
|
|
42
45
|
}
|
|
@@ -46,6 +49,7 @@ export const loginCommand = new Command('login')
|
|
|
46
49
|
}
|
|
47
50
|
});
|
|
48
51
|
async function verify(email, code) {
|
|
52
|
+
const priorAuth = getAuth();
|
|
49
53
|
const res = await publicPost('/auth/verify', { email, code });
|
|
50
54
|
const exp = decodeJwtExp(res.accessToken);
|
|
51
55
|
if (!exp) {
|
|
@@ -60,6 +64,7 @@ async function verify(email, code) {
|
|
|
60
64
|
expiresAt,
|
|
61
65
|
});
|
|
62
66
|
console.log(success(`Logged in (${email}).`));
|
|
67
|
+
warnIfUnexpectedNewAccount(res.isNewUser, email, priorAuth);
|
|
63
68
|
// A fresh session is the clearest "we're reconnected" signal - clear any bug
|
|
64
69
|
// reports that got stranded while this account's session was expired/offline.
|
|
65
70
|
const delivered = await flushBugQueue().catch(() => 0);
|
|
@@ -106,7 +106,7 @@ export function summarizeExpr(expr) {
|
|
|
106
106
|
if (oneLine && expr.trim().length <= EXPR_ECHO_MAX_CHARS)
|
|
107
107
|
return expr.trim();
|
|
108
108
|
const first = (meaningful[0] ?? '').trim();
|
|
109
|
-
const head = first.length > EXPR_ECHO_MAX_CHARS ? `${first.slice(0, EXPR_ECHO_MAX_CHARS -
|
|
109
|
+
const head = first.length > EXPR_ECHO_MAX_CHARS ? `${first.slice(0, EXPR_ECHO_MAX_CHARS - 3)}...` : first;
|
|
110
110
|
const shape = oneLine
|
|
111
111
|
? `(${expr.trim().length} chars)`
|
|
112
112
|
: `(+${meaningful.length - 1} more ${meaningful.length - 1 === 1 ? 'line' : 'lines'}, ${expr.trim().length} chars)`;
|
|
@@ -257,6 +257,31 @@ export function budgetOverrunHint(reason, usedBudgetMs) {
|
|
|
257
257
|
`(up to ${EVAL_SCRIPT_BUDGET_MAX_MS}), e.g. --timeout ${EVAL_SCRIPT_BUDGET_MAX_MS} — ` +
|
|
258
258
|
`or gate on a ready signal instead: --wait-for '<selector>' --wait-timeout ${MAX_WAIT_MS}.`;
|
|
259
259
|
}
|
|
260
|
+
/** The server aborts a script whose page navigated out from under it, and tells
|
|
261
|
+
* the caller to "split the check into two evals". For the case that actually
|
|
262
|
+
* produces this (seed state, reload, assert the state came back), that advice
|
|
263
|
+
* sends the caller down the wrong road: two evals are two independent page
|
|
264
|
+
* sessions, so the second one has to re-seed before it can read anything, and
|
|
265
|
+
* the thing being verified (a real reload restoring real persisted state) is
|
|
266
|
+
* never exercised. `--reload` is the primitive for it: one page load, reloaded
|
|
267
|
+
* IN PLACE with storage preserved, second expression against the post-reload
|
|
268
|
+
* DOM. Name it here, at the exact moment the caller hand-rolled it. */
|
|
269
|
+
export function navigationAbortHint(reason) {
|
|
270
|
+
if (!/navigated or reloaded/i.test(reason))
|
|
271
|
+
return null;
|
|
272
|
+
return `If you were verifying that state SURVIVES a reload, that is what --reload is for (one command, one page): ` +
|
|
273
|
+
`gipity page eval "<url>" '<seed/assert expr>' --reload '<assert-restored expr>'. ` +
|
|
274
|
+
`It reloads the page in place (localStorage/sessionStorage/cookies preserved) and reports the second result ` +
|
|
275
|
+
`separately. Splitting into two 'page eval' calls does NOT do this: each call is its own browser session, so ` +
|
|
276
|
+
`the second one starts from empty storage and never exercises the restore path.`;
|
|
277
|
+
}
|
|
278
|
+
/** True when the submitted script already drives the app's clock itself
|
|
279
|
+
* (`core.advance(seconds)` on the 3D templates, the imported `advance(seconds)`
|
|
280
|
+
* on the 2D one). Such a script does not care what the renderer paints at, so
|
|
281
|
+
* the slow-render warning below has nothing to tell it. Exported for tests. */
|
|
282
|
+
export function stepsDeterministically(script) {
|
|
283
|
+
return /\badvance\s*\(/.test(script);
|
|
284
|
+
}
|
|
260
285
|
/** The headless browser paints slowly, and what that MEANS depends on what the
|
|
261
286
|
* page is doing — so the one-size warning was actively misleading on a --camera
|
|
262
287
|
* run. A vision app has no loop to step: its pipeline is driven by camera
|
|
@@ -273,6 +298,8 @@ export function budgetOverrunHint(reason, usedBudgetMs) {
|
|
|
273
298
|
* deterministic way to wait (--wait-for) instead of inviting a wall-clock
|
|
274
299
|
* escalation that is guaranteed to be wasted. Exported for tests. */
|
|
275
300
|
export function slowRenderMessage(fps, o) {
|
|
301
|
+
if (!o.camera && stepsDeterministically(o.script ?? ''))
|
|
302
|
+
return null;
|
|
276
303
|
if (o.camera) {
|
|
277
304
|
const frames = Math.max(1, Math.round(fps * (o.waitMs / 1000)));
|
|
278
305
|
return `${warning('⚠ Slow render:')} page painted at ${fps} fps, so the app's vision pipeline ran on roughly `
|
|
@@ -379,10 +406,10 @@ export const pageEvalCommand = new Command('eval')
|
|
|
379
406
|
.description('Evaluate JS in a real browser on a page (DOM, computed styles, element rects; inline expr or --file script). ONE client per call - to verify realtime/presence across concurrent clients use `page test --observe` instead')
|
|
380
407
|
.argument('<url>', 'URL to load')
|
|
381
408
|
.argument('[expr]', `JavaScript to evaluate in page context (inline expression or statement body; await works, and the trailing expression is returned automatically — REPL-style — so no explicit return is needed; result is JSON-serialized). Omit when using --file. Time budget: the body has ${EVAL_SCRIPT_BUDGET_MS / 1000}s to finish after page load (${EVAL_SCRIPT_BUDGET_CAMERA_MS / 1000}s with --camera) - raise it with --timeout, max ${EVAL_SCRIPT_BUDGET_MAX_MS / 1000}s.`)
|
|
382
|
-
.option('--file <path>', `Read the script body from a file instead of the inline <expr> arg (mutually exclusive), or --file - to read it from stdin (pipe a heredoc: --file - <<'EOF'
|
|
409
|
+
.option('--file <path>', `Read the script body from a file instead of the inline <expr> arg (mutually exclusive), or --file - to read it from stdin (pipe a heredoc: --file - <<'EOF' ... EOF) with no tmp file. Runs as an async function body, so top-level return/await work. Same post-load budget as <expr> (--timeout).`)
|
|
383
410
|
.option('--step <expr>', `Run another expression against the SAME loaded page, after <expr> (repeat, max ${MAX_EVAL_STEPS}). Whatever that page load paid for — a vision model coming up, a game booting, a socket connecting — stays up for every step, so an N-part check costs ONE page load instead of N. Each step gets its own in-page budget and its own reported result.`, (val, prev) => [...prev, val], [])
|
|
384
411
|
.option('--fixture <path>', 'Host a local file and expose it to the eval as `fixtureUrl` (and under `fixtures` by basename) to fetch in-page. For verifying a render/parse path against a real binary (an MP3, an image) - no size limit, auto-deleted after the run. The hosted URL has permissive CORS, so `new Image(); img.crossOrigin="anonymous"; img.src=fixtureUrl` decodes untainted for a canvas/pixel read - this is the UPLOAD analog of --camera: feed a known photo/video to a file-upload vision app (web-vision-detect) and run its detector, no need to deploy a throwaway test asset into the app. Repeat for several files (single-value so it never swallows the inline <expr>).', (val, prev) => [...prev, val], [])
|
|
385
|
-
.option('--reload <expr>', 'After the first eval, reload the page IN PLACE (localStorage/sessionStorage/cookies preserved) and evaluate this second expression against the post-reload DOM. One command verifies persisted state survives a reload: seed/assert state with <expr>, then assert the restored UI here.')
|
|
412
|
+
.option('--reload <expr>', 'After the first eval, reload the page IN PLACE (localStorage/sessionStorage/cookies preserved) and evaluate this second expression against the post-reload DOM. One command verifies persisted state survives a reload ("remember it when I come back"): seed/assert state with <expr>, then assert the restored UI here. This is the ONLY way to check that - reloading inside the body aborts the eval (the result is lost with the old page), and two separate `page eval` calls are two fresh browser profiles, so the second starts from empty storage.')
|
|
386
413
|
.option('--reload-file <path>', 'Read the post-reload expression from a file instead of inline --reload (mutually exclusive)')
|
|
387
414
|
.option('--camera <path>', `Play a local image or video (.png/.jpg/.webp/.mp4/.webm/.y4m/.mjpeg) as the browser's WEBCAM feed, so a camera app's real pipeline (getUserMedia → MediaPipe/YOLOX → your app logic) runs headlessly on a frame you choose. Implies --fake-media and waits ${CAMERA_DEFAULT_WAIT_MS / 1000}s for the vision model to load. No frame handy? gipity generate image "a hand making a closed fist, palm to camera".`)
|
|
388
415
|
.option('--fake-media', 'Grant a synthetic microphone + camera and auto-accept the getUserMedia prompt, so a voice/camera app runs headlessly instead of hitting its no-camera path. The feed is a built-in test pattern (and a tone) — nothing a vision model can recognize; to drive a vision app use --camera <path> instead.')
|
|
@@ -426,7 +453,7 @@ export const pageEvalCommand = new Command('eval')
|
|
|
426
453
|
}
|
|
427
454
|
catch {
|
|
428
455
|
pageEvalCommand.error(opts.file === '-'
|
|
429
|
-
? 'error: --file - reads the script from stdin, but stdin was empty/unreadable
|
|
456
|
+
? 'error: --file - reads the script from stdin, but stdin was empty/unreadable - pipe it in, e.g. gipity page eval "<url>" --file - <<\'EOF\' ... EOF'
|
|
430
457
|
: `error: Cannot read file: ${opts.file}`);
|
|
431
458
|
}
|
|
432
459
|
}
|
|
@@ -540,12 +567,12 @@ export const pageEvalCommand = new Command('eval')
|
|
|
540
567
|
projectGuid = config.projectGuid;
|
|
541
568
|
}
|
|
542
569
|
if (opts.camera) {
|
|
543
|
-
console.log(muted(`Hosting camera feed ${opts.camera}
|
|
570
|
+
console.log(muted(`Hosting camera feed ${opts.camera}...`));
|
|
544
571
|
camera = await uploadCameraFeed(projectGuid, opts.camera);
|
|
545
572
|
}
|
|
546
573
|
if (fixturePaths.length) {
|
|
547
574
|
for (const p of fixturePaths) {
|
|
548
|
-
console.log(muted(`Hosting fixture ${p}
|
|
575
|
+
console.log(muted(`Hosting fixture ${p}...`));
|
|
549
576
|
hosted.push(await uploadPublicFixture(projectGuid, p));
|
|
550
577
|
}
|
|
551
578
|
const map = {};
|
|
@@ -589,7 +616,10 @@ export const pageEvalCommand = new Command('eval')
|
|
|
589
616
|
// raises it — and the flag is what the caller needs next. Name it, with
|
|
590
617
|
// the value THIS run used, so the retry is an edit rather than a guess.
|
|
591
618
|
const msg = err?.message ?? '';
|
|
592
|
-
const hint = budgetOverrunHint(msg, scriptBudgetMs)
|
|
619
|
+
const hint = budgetOverrunHint(msg, scriptBudgetMs)
|
|
620
|
+
// A script aborted by its own reload is nearly always a persistence
|
|
621
|
+
// check hand-rolled without --reload; point at the flag that does it.
|
|
622
|
+
?? (reloadExpr === undefined ? navigationAbortHint(msg) : null);
|
|
593
623
|
if (!hint)
|
|
594
624
|
throw err;
|
|
595
625
|
throw new Error(`${msg}\n${hint}`);
|
|
@@ -632,8 +662,17 @@ export const pageEvalCommand = new Command('eval')
|
|
|
632
662
|
// page's rAF loop is its clock, and engines cap the per-frame delta, so
|
|
633
663
|
// `setTimeout(2000)` may advance only a fraction of a second of app time.
|
|
634
664
|
// Without this line the eval returns a plausible-looking false negative.
|
|
665
|
+
// A script that already steps the loop itself is immune to all of that, so
|
|
666
|
+
// slowRenderMessage returns null for it rather than prescribing the fix it
|
|
667
|
+
// is already applying.
|
|
635
668
|
if (d.slowRender) {
|
|
636
|
-
|
|
669
|
+
const slow = slowRenderMessage(d.slowRender.fps, {
|
|
670
|
+
camera: !!camera,
|
|
671
|
+
waitMs,
|
|
672
|
+
script: [expr, ...steps, reloadExpr ?? ''].join('\n'),
|
|
673
|
+
});
|
|
674
|
+
if (slow)
|
|
675
|
+
console.log(slow);
|
|
637
676
|
}
|
|
638
677
|
// Identity line on EVERY run: without it an agent can't distinguish
|
|
639
678
|
// "signed-in eval" from "--auth silently no-op'd against the anonymous
|
|
@@ -30,9 +30,6 @@ function looksLikeHtml(body) {
|
|
|
30
30
|
const head = body.slice(0, 300).toLowerCase();
|
|
31
31
|
return /<!doctype html|<html[\s>]/.test(head);
|
|
32
32
|
}
|
|
33
|
-
function sha256(s) {
|
|
34
|
-
return createHash('sha256').update(s).digest('hex');
|
|
35
|
-
}
|
|
36
33
|
function baseWithSlash(url) {
|
|
37
34
|
return url.endsWith('/') ? url : url + '/';
|
|
38
35
|
}
|
|
@@ -43,8 +40,16 @@ function resolveUrl(base, path) {
|
|
|
43
40
|
async function fetchRaw(url) {
|
|
44
41
|
try {
|
|
45
42
|
const res = await fetch(url, { redirect: 'follow' });
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
// Read raw bytes, not text(): decoding binary as UTF-8 turns every invalid
|
|
44
|
+
// byte into a 3-byte replacement char, inflating the measured size ~1.7x.
|
|
45
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
46
|
+
return {
|
|
47
|
+
status: res.status,
|
|
48
|
+
contentType: res.headers.get('content-type'),
|
|
49
|
+
body: buf.toString('utf-8'),
|
|
50
|
+
bytes: buf.length,
|
|
51
|
+
sha256: createHash('sha256').update(buf).digest('hex'),
|
|
52
|
+
};
|
|
48
53
|
}
|
|
49
54
|
catch {
|
|
50
55
|
return null; // DNS failure, connection refused, etc.
|
|
@@ -59,21 +64,20 @@ async function probeShell(base) {
|
|
|
59
64
|
return {
|
|
60
65
|
served: r.status >= 200 && r.status < 300,
|
|
61
66
|
status: r.status,
|
|
62
|
-
sha256:
|
|
67
|
+
sha256: r.sha256,
|
|
63
68
|
isHtml: looksLikeHtml(r.body),
|
|
64
69
|
};
|
|
65
70
|
}
|
|
66
71
|
function classify(path, r, shell) {
|
|
67
72
|
if (!r)
|
|
68
73
|
return { status: null, contentType: null, bytes: 0, verdict: 'MISSING', detail: 'fetch failed (host unreachable)' };
|
|
69
|
-
const
|
|
70
|
-
const base = { status: r.status, contentType: r.contentType, bytes };
|
|
74
|
+
const base = { status: r.status, contentType: r.contentType, bytes: r.bytes };
|
|
71
75
|
// Honest 404/5xx - the file simply isn't there.
|
|
72
76
|
if (r.status >= 400)
|
|
73
77
|
return { ...base, verdict: 'MISSING', detail: `HTTP ${r.status}` };
|
|
74
78
|
const expect = expectedType(path);
|
|
75
79
|
// 1) Served the catch-all shell verbatim → 200, but the file isn't deployed.
|
|
76
|
-
if (shell.served && shell.sha256 &&
|
|
80
|
+
if (shell.served && shell.sha256 && r.sha256 === shell.sha256) {
|
|
77
81
|
return { ...base, verdict: 'MISSING', detail: `HTTP ${r.status} but served the SPA shell (file not deployed)` };
|
|
78
82
|
}
|
|
79
83
|
// 2) Asked for a non-HTML asset but got an HTML body → a per-path shell variant.
|
|
@@ -92,7 +96,7 @@ const TAG = {
|
|
|
92
96
|
'WRONG-TYPE': warning('WRONG-TYPE'),
|
|
93
97
|
};
|
|
94
98
|
export const pageFetchCommand = new Command('fetch')
|
|
95
|
-
.description('Verify deployed non-rendered files (llms.txt, AGENTS.md, robots.txt, served JSON
|
|
99
|
+
.description('Verify deployed non-rendered files (llms.txt, AGENTS.md, robots.txt, served JSON...) really exist - catches the static-host trap where a missing file returns 200 with the SPA shell instead of a 404')
|
|
96
100
|
.argument('<url>', 'Deployed app base URL; file paths resolve relative to it')
|
|
97
101
|
.argument('<paths...>', 'One or more file paths to verify, e.g. llms.txt AGENTS.md robots.txt')
|
|
98
102
|
.option('--json', 'Output as JSON')
|
|
@@ -26,10 +26,10 @@ function shortUrl(url, truncate = true, maxLen = 100) {
|
|
|
26
26
|
}
|
|
27
27
|
if (!truncate || result.length <= maxLen)
|
|
28
28
|
return result;
|
|
29
|
-
const keep = maxLen -
|
|
29
|
+
const keep = maxLen - 3;
|
|
30
30
|
const headLen = Math.ceil(keep / 2);
|
|
31
31
|
const tailLen = Math.floor(keep / 2);
|
|
32
|
-
return result.slice(0, headLen) + '
|
|
32
|
+
return result.slice(0, headLen) + '...' + result.slice(-tailLen);
|
|
33
33
|
}
|
|
34
34
|
export const pageInspectCommand = new Command('inspect')
|
|
35
35
|
.description('Inspect a web page (console, failed resources, timing, layout overflow). ONE passive load - to verify realtime/presence across concurrent clients use `page test --observe`')
|
|
@@ -235,8 +235,8 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
235
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.`)
|
|
236
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).`)
|
|
237
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.`)
|
|
238
|
-
.option('--action <js>', 'Run JS in the page before capturing
|
|
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\'
|
|
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.')
|
|
240
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.')
|
|
241
241
|
.option('-o, --output <file>', 'Output path (single viewport only; default .gipity/screenshots/ss-<host>-<timestamp>.png)')
|
|
242
242
|
.option('--no-reload-between', 'Skip reload between viewports (faster, lower fidelity - only safe for static pages)')
|
|
@@ -283,7 +283,7 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
283
283
|
}
|
|
284
284
|
catch {
|
|
285
285
|
throw new Error(opts.file === '-'
|
|
286
|
-
? "--file - reads the pre-capture script from stdin, but stdin was empty/unreadable
|
|
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
287
|
: `Cannot read file: ${opts.file}`);
|
|
288
288
|
}
|
|
289
289
|
}
|
|
@@ -372,7 +372,7 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
372
372
|
assertCameraFile(opts.camera);
|
|
373
373
|
if (!projectGuid)
|
|
374
374
|
throw new Error('--camera needs a linked project (the frame is hosted for the browser to fetch) — run `gipity link` first.');
|
|
375
|
-
console.log(muted(`Hosting camera feed ${opts.camera}
|
|
375
|
+
console.log(muted(`Hosting camera feed ${opts.camera}...`));
|
|
376
376
|
camera = await uploadCameraFeed(projectGuid, opts.camera);
|
|
377
377
|
}
|
|
378
378
|
// The pre-capture script the server runs after the post-load delay: the
|
|
@@ -407,7 +407,7 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
407
407
|
try {
|
|
408
408
|
entries = opts.json
|
|
409
409
|
? await doShoot()
|
|
410
|
-
: await withSpinner('Capturing
|
|
410
|
+
: await withSpinner('Capturing...', doShoot, { done: null });
|
|
411
411
|
}
|
|
412
412
|
catch (err) {
|
|
413
413
|
// The sandbox budget covers the WHOLE capture — load, --action, settle,
|
|
@@ -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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { get, post, put, patch, del } from '../api.js';
|
|
2
|
+
import { get, post, put, patch, del, publicRequest, mintAppToken } from '../api.js';
|
|
3
|
+
import { getAuth } from '../auth.js';
|
|
3
4
|
import { requireConfig } from '../config.js';
|
|
4
5
|
import { bold, muted } from '../colors.js';
|
|
5
6
|
import { run, printList, printResult } from '../helpers/index.js';
|
|
@@ -10,6 +11,33 @@ import { confirm } from '../utils.js';
|
|
|
10
11
|
// /projects/<guid>/records mirror.)
|
|
11
12
|
export const recordsCommand = new Command('records')
|
|
12
13
|
.description('Manage app records (Gipity Records - validated CRUD with audit history)');
|
|
14
|
+
const ANON_FLAG = '--anon';
|
|
15
|
+
const ANON_HELP = 'Call as an anonymous visitor (the public path a signed-out user hits) instead of as your signed-in account';
|
|
16
|
+
/** One HTTP surface for every records subcommand, so `--anon` is a persona
|
|
17
|
+
* switch rather than a different code path. Anonymous calls mint the same
|
|
18
|
+
* short-lived app token the browser SDK uses and send no owner credentials -
|
|
19
|
+
* this is exactly what a signed-out visitor's request looks like, so a 401 here
|
|
20
|
+
* is the true answer for an auth-gated table. The persona goes to stderr so
|
|
21
|
+
* stdout stays parseable; without it, "verify the anonymous path" silently runs
|
|
22
|
+
* as the owner and the public path never gets exercised. */
|
|
23
|
+
async function recordsHttp(opts) {
|
|
24
|
+
const config = requireConfig();
|
|
25
|
+
if (!opts.anon) {
|
|
26
|
+
const who = getAuth()?.email;
|
|
27
|
+
console.error(muted(`Auth: calling as ${who ?? 'your signed-in account'} (the owner persona; use ${ANON_FLAG} for the public visitor path)`));
|
|
28
|
+
return { guid: config.projectGuid, get, post, put, patch, del };
|
|
29
|
+
}
|
|
30
|
+
console.error(muted('Auth: anonymous visitor (the public path a signed-out user hits)'));
|
|
31
|
+
const headers = await mintAppToken(config.projectGuid);
|
|
32
|
+
return {
|
|
33
|
+
guid: config.projectGuid,
|
|
34
|
+
get: (path) => publicRequest('GET', path, undefined, headers),
|
|
35
|
+
post: (path, body) => publicRequest('POST', path, body, headers),
|
|
36
|
+
put: (path, body) => publicRequest('PUT', path, body, headers),
|
|
37
|
+
patch: (path, body) => publicRequest('PATCH', path, body, headers),
|
|
38
|
+
del: (path, body) => publicRequest('DELETE', path, body, headers),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
13
41
|
recordsCommand
|
|
14
42
|
.command('list')
|
|
15
43
|
.description('List record tables')
|
|
@@ -57,9 +85,10 @@ recordsCommand
|
|
|
57
85
|
.option('--limit <n>', 'Max rows', '20')
|
|
58
86
|
.option('--offset <n>', 'Offset', '0')
|
|
59
87
|
.option('--fields <fields>', 'Comma-separated column names')
|
|
88
|
+
.option(ANON_FLAG, ANON_HELP)
|
|
60
89
|
.option('--json', 'Output as JSON')
|
|
61
90
|
.action((table, opts) => run('Query', async () => {
|
|
62
|
-
const
|
|
91
|
+
const api = await recordsHttp(opts);
|
|
63
92
|
const params = new URLSearchParams();
|
|
64
93
|
if (opts.filter)
|
|
65
94
|
params.set('filter', opts.filter);
|
|
@@ -69,7 +98,7 @@ recordsCommand
|
|
|
69
98
|
params.set('offset', opts.offset);
|
|
70
99
|
if (opts.fields)
|
|
71
100
|
params.set('fields', opts.fields);
|
|
72
|
-
const res = await get(`/api/${
|
|
101
|
+
const res = await api.get(`/api/${api.guid}/records/${table}?${params}`);
|
|
73
102
|
if (opts.json) {
|
|
74
103
|
console.log(JSON.stringify(res));
|
|
75
104
|
}
|
|
@@ -85,21 +114,26 @@ recordsCommand
|
|
|
85
114
|
recordsCommand
|
|
86
115
|
.command('get <table> <id>')
|
|
87
116
|
.description('Get a record')
|
|
117
|
+
.option(ANON_FLAG, ANON_HELP)
|
|
88
118
|
.option('--json', 'Output as JSON')
|
|
89
119
|
.action((table, id, opts) => run('Get', async () => {
|
|
90
|
-
const
|
|
91
|
-
const res = await get(`/api/${
|
|
120
|
+
const api = await recordsHttp(opts);
|
|
121
|
+
const res = await api.get(`/api/${api.guid}/records/${table}/${id}`);
|
|
92
122
|
console.log(opts.json ? JSON.stringify(res.data) : JSON.stringify(res.data, null, 2));
|
|
93
123
|
}));
|
|
94
124
|
recordsCommand
|
|
95
|
-
.command('history <table>
|
|
96
|
-
.description('Audit history
|
|
125
|
+
.command('history <table> [id]')
|
|
126
|
+
.description('Audit history (who/what changed it, with English summaries). Omit <id> for the whole table\'s feed.')
|
|
97
127
|
.option('--limit <n>', 'Max events', '20')
|
|
128
|
+
.option(ANON_FLAG, ANON_HELP)
|
|
98
129
|
.option('--json', 'Output as JSON')
|
|
99
130
|
.action((table, id, opts) => run('History', async () => {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
131
|
+
const api = await recordsHttp(opts);
|
|
132
|
+
// Table-wide feed when no id is given - the same endpoint an activity/history
|
|
133
|
+
// view reads, so verifying it needs no hand-built HTTP call.
|
|
134
|
+
const scope = id ? `${table}/${encodeURIComponent(id)}` : table;
|
|
135
|
+
const res = await api.get(`/api/${api.guid}/records/${scope}/history?limit=${encodeURIComponent(opts.limit)}`);
|
|
136
|
+
printList(res.data, opts, id ? 'No history for this record.' : `No history for "${table}".`, e => {
|
|
103
137
|
const summary = e.detail?.summary || `${e.action} ${e.entity_type} ${e.entity_id}`;
|
|
104
138
|
return `${muted(e.created_at)} ${bold(e.source || '-')} ${summary}`;
|
|
105
139
|
});
|
|
@@ -108,34 +142,38 @@ recordsCommand
|
|
|
108
142
|
.command('create <table>')
|
|
109
143
|
.description('Create a record')
|
|
110
144
|
.requiredOption('--data <json>', 'JSON object with field values')
|
|
145
|
+
.option(ANON_FLAG, ANON_HELP)
|
|
111
146
|
.option('--json', 'Output as JSON')
|
|
112
147
|
.action((table, opts) => run('Create', async () => {
|
|
113
|
-
const
|
|
148
|
+
const api = await recordsHttp(opts);
|
|
114
149
|
const data = JSON.parse(opts.data);
|
|
115
|
-
const res = await post(`/api/${
|
|
150
|
+
const res = await api.post(`/api/${api.guid}/records/${table}`, data);
|
|
116
151
|
printResult(`Created: ${JSON.stringify(res.data)}`, opts, res.data);
|
|
117
152
|
}));
|
|
118
153
|
recordsCommand
|
|
119
154
|
.command('update <table> <id>')
|
|
120
155
|
.description('Update a record')
|
|
121
156
|
.requiredOption('--data <json>', 'JSON object with fields to update')
|
|
157
|
+
.option(ANON_FLAG, ANON_HELP)
|
|
122
158
|
.option('--json', 'Output as JSON')
|
|
123
159
|
.action((table, id, opts) => run('Update', async () => {
|
|
124
|
-
const
|
|
160
|
+
const api = await recordsHttp(opts);
|
|
125
161
|
const data = JSON.parse(opts.data);
|
|
126
|
-
const res = await put(`/api/${
|
|
162
|
+
const res = await api.put(`/api/${api.guid}/records/${table}/${id}`, data);
|
|
127
163
|
printResult(`Updated: ${JSON.stringify(res.data)}`, opts, res.data);
|
|
128
164
|
}));
|
|
129
165
|
recordsCommand
|
|
130
166
|
.command('delete <table> <id>')
|
|
131
167
|
.description('Delete a record')
|
|
132
|
-
.
|
|
168
|
+
.option(ANON_FLAG, ANON_HELP)
|
|
169
|
+
.option('--json', 'Output as JSON')
|
|
170
|
+
.action((table, id, opts) => run('Delete', async () => {
|
|
133
171
|
if (!await confirm(`Delete record ${id} from "${table}"?`)) {
|
|
134
|
-
|
|
172
|
+
printResult('Cancelled.', opts, { table, id, deleted: false, cancelled: true });
|
|
135
173
|
return;
|
|
136
174
|
}
|
|
137
|
-
const
|
|
138
|
-
await del(`/api/${
|
|
139
|
-
printResult('Deleted.', {
|
|
175
|
+
const api = await recordsHttp(opts);
|
|
176
|
+
const res = await api.del(`/api/${api.guid}/records/${table}/${id}`);
|
|
177
|
+
printResult('Deleted.', opts, res?.data ?? { table, id, deleted: true });
|
|
140
178
|
}));
|
|
141
179
|
//# sourceMappingURL=records.js.map
|
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
|
@@ -464,7 +464,7 @@ GCC/Rust).
|
|
|
464
464
|
});
|
|
465
465
|
const res = opts.json
|
|
466
466
|
? await doRun()
|
|
467
|
-
: await withSpinner('Running in sandbox
|
|
467
|
+
: await withSpinner('Running in sandbox...', doRun, { done: null });
|
|
468
468
|
// Pull sandbox-written outputs down to the local cwd automatically. The
|
|
469
469
|
// server has already mirrored them into the project (VFS) and handed back
|
|
470
470
|
// the exact list, so honoring it here means files land locally without a
|
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,7 +21,7 @@ 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')
|
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.')}`);
|