gipity 1.0.374 → 1.0.380
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/commands/claude.js +18 -69
- package/dist/commands/deploy.js +6 -1
- package/dist/commands/page-eval.js +16 -3
- package/dist/commands/page-screenshot.js +5 -1
- package/dist/commands/page-test.js +188 -18
- package/dist/commands/project.js +2 -1
- package/dist/commands/sandbox.js +32 -5
- package/dist/commands/status.js +3 -1
- package/dist/config.js +11 -0
- package/dist/flag-aliases.js +11 -3
- package/dist/knowledge.js +4 -4
- package/dist/progress.js +18 -1
- package/dist/prompts.js +0 -8
- package/dist/setup.js +14 -3
- package/dist/sync.js +23 -4
- package/dist/template-vars.js +4 -1
- package/package.json +1 -1
package/dist/commands/claude.js
CHANGED
|
@@ -22,10 +22,10 @@ import { get, post, publicPost, ApiError, getAccountSlug } from '../api.js';
|
|
|
22
22
|
import { getConfig, saveConfigAt, clearConfigCache, getApiBaseOverride, getConfigPath } from '../config.js';
|
|
23
23
|
import { sync } from '../sync.js';
|
|
24
24
|
import { slugify, setupClaudeHooks, setupClaudeMd, setupAgentsMd, setupGitignore, DEFAULT_SYNC_IGNORE, isSyncIgnored } from '../setup.js';
|
|
25
|
-
import { buildProjectContextBlock as buildProjectContextBlockText,
|
|
25
|
+
import { buildProjectContextBlock as buildProjectContextBlockText, buildNewProjectPrompt, buildResumeWrap, buildFreshWrap, } from '../prompts.js';
|
|
26
26
|
import * as relayState from '../relay/state.js';
|
|
27
27
|
import { maybeOfferRelayOn, ensureDaemonRunning } from '../relay/onboarding.js';
|
|
28
|
-
import { prompt,
|
|
28
|
+
import { prompt, pickOne, decodeJwtExp, confirm } from '../utils.js';
|
|
29
29
|
import { brand, bold, info, success, error as clrError, muted } from '../colors.js';
|
|
30
30
|
import { createProgressReporter } from '../progress.js';
|
|
31
31
|
import { printBanner } from '../banner.js';
|
|
@@ -107,10 +107,6 @@ async function buildProjectContextBlock(opts) {
|
|
|
107
107
|
const stats = await fetchProjectStats(opts.projectGuid, opts.cwd);
|
|
108
108
|
return buildProjectContextBlockText({ ...opts, ...stats });
|
|
109
109
|
}
|
|
110
|
-
async function buildExistingProjectPrompt(opts) {
|
|
111
|
-
const stats = await fetchProjectStats(opts.projectGuid, opts.cwd);
|
|
112
|
-
return buildExistingProjectPromptText({ ...opts, ...stats });
|
|
113
|
-
}
|
|
114
110
|
/** Interactive email+code login flow. Used on first login and when the
|
|
115
111
|
* server returns 401 mid-command (session expired). Writes the new auth
|
|
116
112
|
* to disk and returns it. */
|
|
@@ -410,30 +406,12 @@ export const claudeCommand = new Command('claude')
|
|
|
410
406
|
// whether to use the new-project framing for that wrap.
|
|
411
407
|
headlessNewProject = isNewProject;
|
|
412
408
|
}
|
|
413
|
-
else if (isNewProject) {
|
|
414
|
-
console.log('');
|
|
415
|
-
console.log(` ${bold("What's next? What would you like to build?")}`);
|
|
416
|
-
console.log('');
|
|
417
|
-
const buildIdea = (await promptBoxed()).trim();
|
|
418
|
-
const stats = await fetchProjectStats(project.short_guid, process.cwd());
|
|
419
|
-
initialPrompt = buildNewProjectPrompt({
|
|
420
|
-
projectName: project.name,
|
|
421
|
-
projectSlug: project.slug,
|
|
422
|
-
projectGuid: project.short_guid,
|
|
423
|
-
accountSlug,
|
|
424
|
-
cwd: process.cwd(),
|
|
425
|
-
...stats,
|
|
426
|
-
buildIdea,
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
409
|
else {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
cwd: process.cwd(),
|
|
436
|
-
});
|
|
410
|
+
// Interactive: launch with no seeded first message. The per-project
|
|
411
|
+
// CLAUDE.md (refreshed just above) carries the project identity,
|
|
412
|
+
// scaffold rule, and definition of done, so Claude has full context
|
|
413
|
+
// the moment the user types. A welcome banner prints before launch.
|
|
414
|
+
initialPrompt = '';
|
|
437
415
|
}
|
|
438
416
|
console.log(` ${success(`Project "${project.name}" ready.`)}\n`);
|
|
439
417
|
}
|
|
@@ -470,13 +448,8 @@ export const claudeCommand = new Command('claude')
|
|
|
470
448
|
console.log(' Could not sync files (will retry on next prompt).');
|
|
471
449
|
}
|
|
472
450
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
projectSlug: existing.projectSlug,
|
|
476
|
-
projectGuid: existing.projectGuid,
|
|
477
|
-
accountSlug: existing.accountSlug,
|
|
478
|
-
cwd: process.cwd(),
|
|
479
|
-
});
|
|
451
|
+
// Interactive: no seeded prompt - CLAUDE.md carries the context now.
|
|
452
|
+
initialPrompt = '';
|
|
480
453
|
}
|
|
481
454
|
else {
|
|
482
455
|
// Fetch user's projects. If the session expired (401), re-run the
|
|
@@ -596,38 +569,11 @@ export const claudeCommand = new Command('claude')
|
|
|
596
569
|
console.log(' Could not sync files (will retry on next prompt).');
|
|
597
570
|
}
|
|
598
571
|
}
|
|
599
|
-
//
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
console.log(` ${bold("What's next? What would you like to build?")}`);
|
|
605
|
-
console.log(` ${muted('Examples: a landing page, a Pac-Man game, a full web app,')}`);
|
|
606
|
-
console.log(` ${muted('an API that returns random facts, an image, just answer questions?')}`);
|
|
607
|
-
console.log('');
|
|
608
|
-
console.log(` ${muted('Claude Code with Gipity can do everything your old Claude Code could do but so much more now!')}`);
|
|
609
|
-
console.log('');
|
|
610
|
-
const buildIdea = (await promptBoxed()).trim();
|
|
611
|
-
const stats = await fetchProjectStats(project.short_guid, process.cwd());
|
|
612
|
-
initialPrompt = buildNewProjectPrompt({
|
|
613
|
-
projectName: project.name,
|
|
614
|
-
projectSlug: project.slug,
|
|
615
|
-
projectGuid: project.short_guid,
|
|
616
|
-
accountSlug,
|
|
617
|
-
cwd: process.cwd(),
|
|
618
|
-
...stats,
|
|
619
|
-
buildIdea,
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
else {
|
|
623
|
-
initialPrompt = await buildExistingProjectPrompt({
|
|
624
|
-
projectName: project.name,
|
|
625
|
-
projectSlug: project.slug,
|
|
626
|
-
projectGuid: project.short_guid,
|
|
627
|
-
accountSlug,
|
|
628
|
-
cwd: process.cwd(),
|
|
629
|
-
});
|
|
630
|
-
}
|
|
572
|
+
// Interactive launch: no seeded first message (new or existing). The
|
|
573
|
+
// per-project CLAUDE.md carries identity + scaffold rule + definition
|
|
574
|
+
// of done, and a welcome banner prints before launch - the user tells
|
|
575
|
+
// Claude directly what they want to build or do.
|
|
576
|
+
initialPrompt = '';
|
|
631
577
|
setupClaudeHooks();
|
|
632
578
|
setupClaudeMd();
|
|
633
579
|
setupAgentsMd();
|
|
@@ -729,7 +675,10 @@ export const claudeCommand = new Command('claude')
|
|
|
729
675
|
claudeArgs.push(arg);
|
|
730
676
|
}
|
|
731
677
|
if (!nonInteractive) {
|
|
732
|
-
console.log(` ${
|
|
678
|
+
console.log(` ${bold('Launching Claude Code, powered by Gipity.')}`);
|
|
679
|
+
console.log(` ${muted("Just tell Claude what you'd like to build or do - everything Claude could do")}`);
|
|
680
|
+
console.log(` ${muted('before, and now, on Gipity, so much more.')}`);
|
|
681
|
+
console.log('');
|
|
733
682
|
}
|
|
734
683
|
// In non-interactive (-p) mode, prepend a Gipity preamble to the
|
|
735
684
|
// user's raw message. Two flavors:
|
package/dist/commands/deploy.js
CHANGED
|
@@ -56,7 +56,7 @@ export const deployCommand = new Command('deploy')
|
|
|
56
56
|
else {
|
|
57
57
|
// Fallback for simple deploys without phases
|
|
58
58
|
const size = formatSize(d.totalBytes);
|
|
59
|
-
console.log(`${success('✓')} ${d.fileCount} files (${size})
|
|
59
|
+
console.log(`${success('✓')} ${d.fileCount} files (${size})`);
|
|
60
60
|
}
|
|
61
61
|
if (d.customDomains?.length) {
|
|
62
62
|
console.log(`${muted('Also:')} ${d.customDomains.join(', ')}`);
|
|
@@ -80,6 +80,11 @@ export const deployCommand = new Command('deploy')
|
|
|
80
80
|
}
|
|
81
81
|
else {
|
|
82
82
|
console.log(success(`✓ Deployed to ${target}`) + muted(` (${d.elapsedMs}ms)`));
|
|
83
|
+
// The live URL is the one thing the caller (often an agent) needs next
|
|
84
|
+
// - to open it, inspect it, or report it. Always surface it so nobody
|
|
85
|
+
// has to reconstruct the URL convention or guess a subdomain.
|
|
86
|
+
if (d.url)
|
|
87
|
+
console.log(`${muted('Live:')} ${brand(d.url)}`);
|
|
83
88
|
}
|
|
84
89
|
}));
|
|
85
90
|
//# sourceMappingURL=deploy.js.map
|
|
@@ -5,11 +5,13 @@ import { run } from '../helpers/index.js';
|
|
|
5
5
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
6
6
|
/** Poll the async eval job until it finishes. Eval runs server-side as a
|
|
7
7
|
* short-lived job (so a long --wait can't trip the gateway idle timeout);
|
|
8
|
-
* we submit, then poll the result out of the job store.
|
|
9
|
-
|
|
8
|
+
* we submit, then poll the result out of the job store. `expectedWorkMs` is
|
|
9
|
+
* the time the server-side work is expected to take (settle + any in-page
|
|
10
|
+
* awaits); the client budget is that plus 60s of headroom. */
|
|
11
|
+
export async function pollEvalResult(evalJobId, expectedWorkMs) {
|
|
10
12
|
// Generous client budget: the server work is bounded by --wait plus browser
|
|
11
13
|
// open/settle overhead; give it that plus headroom before giving up.
|
|
12
|
-
const deadline = Date.now() +
|
|
14
|
+
const deadline = Date.now() + expectedWorkMs + 60_000;
|
|
13
15
|
let missCount = 0;
|
|
14
16
|
while (Date.now() < deadline) {
|
|
15
17
|
let rec;
|
|
@@ -69,4 +71,15 @@ export const pageEvalCommand = new Command('eval')
|
|
|
69
71
|
if (d.truncated)
|
|
70
72
|
console.log(muted('\n(result truncated to fit context - narrow the expression for the full value)'));
|
|
71
73
|
}));
|
|
74
|
+
// Each `page eval` call runs to completion before the next starts, so two evals
|
|
75
|
+
// fired back-to-back never coexist in time - they CANNOT test whether two live
|
|
76
|
+
// clients see each other (presence, shared state). For that, use the genuinely-
|
|
77
|
+
// concurrent `page test --observe` instead, which overlaps N clients and reports
|
|
78
|
+
// whether they actually ran together.
|
|
79
|
+
pageEvalCommand.addHelpText('after', `
|
|
80
|
+
Testing realtime/shared state across clients?
|
|
81
|
+
Separate 'page eval' calls run sequentially (one finishes before the next
|
|
82
|
+
starts), so they never overlap and will each see only themselves - a false
|
|
83
|
+
negative. Use 'gipity page test <url> --observe <expr>' for genuinely
|
|
84
|
+
concurrent clients with overlap verification.`);
|
|
72
85
|
//# sourceMappingURL=page-eval.js.map
|
|
@@ -105,6 +105,10 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
105
105
|
.option('--fake-media', 'Grant a synthetic microphone + camera and auto-accept the getUserMedia prompt, so voice/camera apps render headlessly (audio is a built-in tone, not real speech)')
|
|
106
106
|
.option('--json', 'Output JSON metadata instead of a friendly summary')
|
|
107
107
|
.addOption(new Option('--wait <ms>', 'Alias for --post-load-delay').hideHelp())
|
|
108
|
+
// `--full-page` is the Puppeteer/Playwright name for this (their `fullPage`),
|
|
109
|
+
// so agents reach for it by reflex. Accept it as a hidden alias for `--full`
|
|
110
|
+
// rather than reject it as an unknown option and send them on a --help detour.
|
|
111
|
+
.addOption(new Option('--full-page', 'Alias for --full').hideHelp())
|
|
108
112
|
.action((url, opts) => run('Page screenshot', async () => {
|
|
109
113
|
const delayRaw = opts.postLoadDelay ?? opts.wait;
|
|
110
114
|
const postLoadDelayMs = delayRaw !== undefined ? parseInt(String(delayRaw), 10) : undefined;
|
|
@@ -126,7 +130,7 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
126
130
|
const body = {
|
|
127
131
|
url,
|
|
128
132
|
postLoadDelayMs,
|
|
129
|
-
full: !!opts.full,
|
|
133
|
+
full: !!(opts.full || opts.fullPage),
|
|
130
134
|
reloadBetween: opts.reloadBetween !== false,
|
|
131
135
|
...(userSpecifiedViewports ? { viewports: customViewports } : {}),
|
|
132
136
|
...(opts.fakeMedia ? { fakeMedia: true } : {}),
|
|
@@ -2,6 +2,7 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { post } from '../api.js';
|
|
3
3
|
import { brand, bold, muted, warning, success, error as clrError } from '../colors.js';
|
|
4
4
|
import { run } from '../helpers/index.js';
|
|
5
|
+
import { pollEvalResult } from './page-eval.js';
|
|
5
6
|
// Lines worth surfacing - genuine errors and crash signatures, not benign warnings.
|
|
6
7
|
const BAD = /^error:|uncaught|unhandled|message handler error|\bcrash|RuntimeError/i;
|
|
7
8
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -15,25 +16,148 @@ async function inspectClient(url, waitMs, i) {
|
|
|
15
16
|
return { i, lines: [], error: err instanceof Error ? err.message : String(err) };
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
19
|
+
const MAX_HOLD_MS = 15_000; // keep each in-page await under the ~20s browser action timeout
|
|
20
|
+
const MIN_HOLD_MS = 1_000;
|
|
21
|
+
/** Splice per-client values into a user expression. `{{label}}` → the client's
|
|
22
|
+
* label, `{{i}}` → its 0-based index. Plain string replace (no regex) so the
|
|
23
|
+
* expression's own characters are never treated as patterns. */
|
|
24
|
+
function subst(expr, label, i) {
|
|
25
|
+
return expr.split('{{label}}').join(label).split('{{i}}').join(String(i));
|
|
26
|
+
}
|
|
27
|
+
/** Build the statement-body script one client runs: do the one-time action,
|
|
28
|
+
* then sample `observe` `samples` times across `holdMs`, stamping in-page
|
|
29
|
+
* start/end so the caller can confirm the clients overlapped. */
|
|
30
|
+
function buildHarness(action, observe, label, holdMs, samples) {
|
|
31
|
+
const n = Math.max(2, samples);
|
|
32
|
+
const interval = Math.max(0, Math.floor(holdMs / (n - 1)));
|
|
33
|
+
const lines = [
|
|
34
|
+
`const __label=${JSON.stringify(label)};`,
|
|
35
|
+
`const __t0=Date.now();`,
|
|
36
|
+
];
|
|
37
|
+
if (action && action.trim()) {
|
|
38
|
+
lines.push(`try{ ${action} }catch(__e){ return {label:__label,startedAt:__t0,endedAt:Date.now(),samples:[],actionError:String((__e&&__e.message)||__e)}; }`);
|
|
39
|
+
}
|
|
40
|
+
lines.push(`const __s=[];`, `for(let __k=0;__k<${n};__k++){`, ` let __v; try{ __v=(${observe}); }catch(__e){ __v='ObserveError: '+String((__e&&__e.message)||__e); }`, ` __s.push(__v);`, ` if(__k<${n - 1}) await new Promise(function(r){setTimeout(r,${interval});});`, `}`, `return {label:__label,startedAt:__t0,endedAt:Date.now(),samples:__s};`);
|
|
41
|
+
return lines.join('\n');
|
|
42
|
+
}
|
|
43
|
+
/** Kick off and poll one client's eval job, then parse the harness payload. */
|
|
44
|
+
async function observeClient(url, expr, i, label, settleMs, holdMs, waitForSelector) {
|
|
45
|
+
const base = { i, label, samples: [], startedAt: 0, endedAt: 0 };
|
|
46
|
+
try {
|
|
47
|
+
const kickoff = await post('/tools/browser/eval', {
|
|
48
|
+
url,
|
|
49
|
+
expr,
|
|
50
|
+
waitMs: settleMs,
|
|
51
|
+
waitForSelector: waitForSelector || undefined,
|
|
52
|
+
waitForTimeoutMs: waitForSelector ? 5000 : undefined,
|
|
53
|
+
});
|
|
54
|
+
// Server work ≈ nav + settle + the in-page hold; pollEvalResult adds 60s headroom.
|
|
55
|
+
const d = await pollEvalResult(kickoff.data.evalJobId, settleMs + holdMs);
|
|
56
|
+
const raw = d.result?.trim() ?? '';
|
|
57
|
+
if (raw.startsWith('EvalError:') || raw.startsWith('ObserveError:')) {
|
|
58
|
+
return { ...base, error: raw };
|
|
59
|
+
}
|
|
60
|
+
let parsed;
|
|
61
|
+
try {
|
|
62
|
+
parsed = JSON.parse(raw);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return { ...base, error: `unparseable client result: ${raw.slice(0, 200)}` };
|
|
66
|
+
}
|
|
67
|
+
if (parsed.actionError) {
|
|
68
|
+
return { ...base, error: `action failed: ${parsed.actionError}` };
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
i,
|
|
72
|
+
label: typeof parsed.label === 'string' ? parsed.label : label,
|
|
73
|
+
samples: Array.isArray(parsed.samples) ? parsed.samples : [],
|
|
74
|
+
startedAt: typeof parsed.startedAt === 'number' ? parsed.startedAt : 0,
|
|
75
|
+
endedAt: typeof parsed.endedAt === 'number' ? parsed.endedAt : 0,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
return { ...base, error: err instanceof Error ? err.message : String(err) };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Compute the window during which ALL successful clients were live at once.
|
|
83
|
+
* Intersection of every [startedAt, endedAt]; positive duration ⇒ they
|
|
84
|
+
* genuinely coexisted, so any shared-state reading is trustworthy. */
|
|
85
|
+
function overlapMs(results) {
|
|
86
|
+
const live = results.filter((r) => !r.error && r.startedAt > 0 && r.endedAt > r.startedAt);
|
|
87
|
+
if (live.length < 2)
|
|
88
|
+
return 0;
|
|
89
|
+
const start = Math.max(...live.map((r) => r.startedAt));
|
|
90
|
+
const end = Math.min(...live.map((r) => r.endedAt));
|
|
91
|
+
return Math.max(0, end - start);
|
|
92
|
+
}
|
|
93
|
+
function fmtSamples(samples) {
|
|
94
|
+
if (samples.length === 0)
|
|
95
|
+
return muted('(no samples)');
|
|
96
|
+
return samples.map((s) => (typeof s === 'string' ? s : JSON.stringify(s))).join(' → ');
|
|
97
|
+
}
|
|
98
|
+
async function runInteractive(url, observe, opts) {
|
|
99
|
+
const clients = Math.max(1, parseInt(opts.clients, 10) || 2);
|
|
100
|
+
const stagger = opts.stagger != null ? Math.max(0, parseInt(opts.stagger, 10) || 0) : 0;
|
|
101
|
+
const hold = Math.min(MAX_HOLD_MS, Math.max(MIN_HOLD_MS, parseInt(opts.hold, 10) || 8000));
|
|
102
|
+
const samples = Math.min(30, Math.max(2, parseInt(opts.samples, 10) || 6));
|
|
103
|
+
const settle = opts.waitFor ? 200 : 1000;
|
|
104
|
+
const labels = (opts.labels ? String(opts.labels).split(',').map((s) => s.trim()) : []).filter(Boolean);
|
|
105
|
+
const labelFor = (i) => labels[i] ?? `client-${i}`;
|
|
106
|
+
if (!opts.json) {
|
|
107
|
+
console.log(`${brand('Page test')} ${muted('(interactive)')} ${bold(url)}`);
|
|
108
|
+
console.log(muted(`${clients} client(s), stagger ${stagger}s, hold ${hold}ms, ${samples} samples each`));
|
|
109
|
+
}
|
|
110
|
+
const runs = [];
|
|
111
|
+
for (let i = 0; i < clients; i++) {
|
|
112
|
+
runs.push((async () => {
|
|
113
|
+
await sleep(i * stagger * 1000);
|
|
114
|
+
if (!opts.json)
|
|
115
|
+
console.log(muted(`client ${i} (${labelFor(i)}) joining`));
|
|
116
|
+
const expr = buildHarness(opts.action ? subst(opts.action, labelFor(i), i) : undefined, subst(observe, labelFor(i), i), labelFor(i), hold, samples);
|
|
117
|
+
return observeClient(url, expr, i, labelFor(i), settle, hold, opts.waitFor);
|
|
118
|
+
})());
|
|
119
|
+
}
|
|
120
|
+
const results = (await Promise.all(runs)).sort((a, b) => a.i - b.i);
|
|
121
|
+
const errored = results.filter((r) => r.error);
|
|
122
|
+
const ovl = overlapMs(results);
|
|
123
|
+
const overlapped = ovl > 0;
|
|
124
|
+
if (opts.json) {
|
|
125
|
+
console.log(JSON.stringify({
|
|
126
|
+
url, mode: 'interactive', clients, stagger, hold, samples,
|
|
127
|
+
overlapMs: ovl, overlapped, results,
|
|
128
|
+
}));
|
|
129
|
+
if (errored.length > 0 || (clients > 1 && !overlapped))
|
|
130
|
+
process.exitCode = 1;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
for (const r of results) {
|
|
134
|
+
console.log(`\n${bold(`=== client ${r.i} (${r.label}) ===`)}`);
|
|
135
|
+
if (r.error) {
|
|
136
|
+
console.log(clrError(`✗ ${r.error}`));
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
console.log(`${muted('samples:')} ${fmtSamples(r.samples)}`);
|
|
140
|
+
}
|
|
141
|
+
console.log('');
|
|
142
|
+
if (errored.length > 0) {
|
|
143
|
+
console.log(clrError(`⚠ ${errored.length} client(s) failed (see above)`));
|
|
144
|
+
}
|
|
145
|
+
if (clients < 2) {
|
|
146
|
+
console.log(muted('Note: a single client cannot verify cross-client visibility — run with --clients 2+.'));
|
|
147
|
+
}
|
|
148
|
+
else if (overlapped) {
|
|
149
|
+
console.log(success(`✓ all clients overlapped for ~${(ovl / 1000).toFixed(1)}s — genuine concurrency, so the readings above are trustworthy`));
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
console.log(clrError('⚠ clients did NOT overlap in time — each ran in isolation, so any shared-state reading here is a FALSE NEGATIVE, not proof the app is broken.'));
|
|
153
|
+
console.log(muted(' Likely causes: --stagger ≥ --hold, or more --clients than free browser slots (they queued). Lower --stagger / raise --hold / fewer --clients and retry.'));
|
|
154
|
+
}
|
|
155
|
+
if (errored.length > 0 || (clients > 1 && !overlapped))
|
|
156
|
+
process.exitCode = 1;
|
|
157
|
+
}
|
|
158
|
+
async function runPassive(url, opts) {
|
|
35
159
|
const clients = Math.max(1, parseInt(opts.clients, 10) || 2);
|
|
36
|
-
const stagger = Math.max(0, parseInt(opts.stagger, 10) || 0);
|
|
160
|
+
const stagger = opts.stagger != null ? Math.max(0, parseInt(opts.stagger, 10) || 0) : 12;
|
|
37
161
|
const wait = Math.min(30000, Math.max(2000, parseInt(opts.wait, 10) || 24000));
|
|
38
162
|
if (!opts.json) {
|
|
39
163
|
console.log(`${brand('Page test')} ${bold(url)}`);
|
|
@@ -82,5 +206,51 @@ export const pageTestCommand = new Command('test')
|
|
|
82
206
|
: `\n${clrError(`⚠ ${problems} error/crash line(s) flagged above`)}`);
|
|
83
207
|
if (problems > 0)
|
|
84
208
|
process.exitCode = 1;
|
|
209
|
+
}
|
|
210
|
+
// Headless multi-client realtime check. Two modes:
|
|
211
|
+
//
|
|
212
|
+
// Passive (default): spin N staggered browser clients at a deployed URL and
|
|
213
|
+
// flag error/crash lines across their consoles. Each client just loads the URL
|
|
214
|
+
// and settles - good for apps that connect on load (or via a URL-param test
|
|
215
|
+
// mode; see the app-realtime skill).
|
|
216
|
+
//
|
|
217
|
+
// Interactive (--observe, optionally with --action): each client loads the
|
|
218
|
+
// URL, runs a one-time --action (e.g. submit a name), then samples --observe
|
|
219
|
+
// across a hold window. The clients run genuinely concurrently and the command
|
|
220
|
+
// VERIFIES they overlapped in time - so a presence/shared-state app whose
|
|
221
|
+
// readings only make sense when clients coexist can't be misread as broken
|
|
222
|
+
// just because the clients never actually ran together.
|
|
223
|
+
export const pageTestCommand = new Command('test')
|
|
224
|
+
.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')
|
|
226
|
+
.option('--clients <n>', 'Number of headless clients to launch', '2')
|
|
227
|
+
.option('--stagger <s>', 'Seconds between client starts (passive default 12; interactive default 0)')
|
|
228
|
+
.option('--wait <ms>', 'Passive mode: ms each client stays open after load (max 30000)', '24000')
|
|
229
|
+
// Interactive mode (--observe drives it):
|
|
230
|
+
.option('--observe <expr>', 'Interactive: JS expression sampled in each client to read shared state (e.g. presence count). Switches on interactive mode.')
|
|
231
|
+
.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>', 'Interactive: per-client labels substituted for {{label}} (default client-0, client-1, …)')
|
|
233
|
+
.option('--hold <ms>', `Interactive: total observe window per client (${MIN_HOLD_MS}-${MAX_HOLD_MS}ms)`, '8000')
|
|
234
|
+
.option('--samples <k>', 'Interactive: number of observations across the hold window (2-30)', '6')
|
|
235
|
+
.option('--wait-for <selector>', 'Interactive: wait for this CSS selector before running --action (deterministic readiness gate)')
|
|
236
|
+
.option('--json', 'Output as JSON')
|
|
237
|
+
.addHelpText('after', `
|
|
238
|
+
Examples:
|
|
239
|
+
# Passive: load in 3 staggered clients, flag console errors
|
|
240
|
+
gipity page test "https://dev.gipity.ai/me/app/" --clients 3 --stagger 8
|
|
241
|
+
|
|
242
|
+
# Interactive: two concurrent clients each join with a name, then watch the
|
|
243
|
+
# live presence count. The command confirms the clients actually overlapped.
|
|
244
|
+
gipity page test "https://dev.gipity.ai/me/app/" --clients 2 \\
|
|
245
|
+
--action "document.querySelector('#name').value='{{label}}'; document.querySelector('form').requestSubmit();" \\
|
|
246
|
+
--observe "document.querySelectorAll('.present').length" \\
|
|
247
|
+
--labels Alice,Bob`)
|
|
248
|
+
.action((url, opts) => run('Page test', async () => {
|
|
249
|
+
if (opts.observe) {
|
|
250
|
+
await runInteractive(url, opts.observe, opts);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
await runPassive(url, opts);
|
|
254
|
+
}
|
|
85
255
|
}));
|
|
86
256
|
//# sourceMappingURL=page-test.js.map
|
package/dist/commands/project.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { mkdirSync } from 'fs';
|
|
4
4
|
import { get, post, put, del, getAccountSlug } from '../api.js';
|
|
5
|
-
import { requireConfig, saveConfig } from '../config.js';
|
|
5
|
+
import { requireConfig, saveConfig, liveUrl } from '../config.js';
|
|
6
6
|
import { slugify } from '../setup.js';
|
|
7
7
|
import { error as clrError, brand, muted, info, success } from '../colors.js';
|
|
8
8
|
import { confirm } from '../utils.js';
|
|
@@ -161,6 +161,7 @@ projectCommand
|
|
|
161
161
|
console.log(`Name: ${p.name}`);
|
|
162
162
|
console.log(`Slug: ${p.slug}`);
|
|
163
163
|
console.log(`GUID: ${p.short_guid}`);
|
|
164
|
+
console.log(`Live: ${liveUrl(config)}`);
|
|
164
165
|
console.log(`Created: ${new Date(p.created_at).toLocaleDateString()}`);
|
|
165
166
|
if (p.description)
|
|
166
167
|
console.log(`Desc: ${p.description}`);
|
package/dist/commands/sandbox.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { dirname, extname, relative } from 'path';
|
|
3
4
|
import { post } from '../api.js';
|
|
4
5
|
import { resolveProjectContext, getConfigPath } from '../config.js';
|
|
5
6
|
import { sync } from '../sync.js';
|
|
@@ -28,9 +29,10 @@ function resolveRelativeCwd() {
|
|
|
28
29
|
export const sandboxCommand = new Command('sandbox')
|
|
29
30
|
.description('Run code in a sandbox');
|
|
30
31
|
sandboxCommand
|
|
31
|
-
.command('run
|
|
32
|
+
.command('run [code]')
|
|
32
33
|
.description('Run code')
|
|
33
34
|
.option('--language <language>', 'Language: js, py, or bash', 'js')
|
|
35
|
+
.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')
|
|
34
36
|
.option('--timeout <seconds>', 'Execution timeout in seconds', '30')
|
|
35
37
|
.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])
|
|
36
38
|
.option('--json', 'Output as JSON')
|
|
@@ -55,6 +57,9 @@ Examples:
|
|
|
55
57
|
$ gipity sandbox run --language python \\
|
|
56
58
|
"import pandas as pd; print(pd.read_csv('data/sales.csv').describe())"
|
|
57
59
|
|
|
60
|
+
# Run a script file directly (language inferred from .py)
|
|
61
|
+
$ gipity sandbox run --file build_report.py
|
|
62
|
+
|
|
58
63
|
# Surgical: only these files are mirrored in
|
|
59
64
|
$ gipity sandbox run --language bash \\
|
|
60
65
|
--input src/images/hero.png \\
|
|
@@ -68,9 +73,31 @@ Pre-installed: Python (pandas, numpy, matplotlib, Pillow, scipy, bs4),
|
|
|
68
73
|
CLI tools (ImageMagick, FFmpeg, webp/cwebp, optipng, jq, pandoc, exiftool,
|
|
69
74
|
GCC/Rust).
|
|
70
75
|
`)
|
|
71
|
-
.action((code, opts) => run('Sandbox', async () => {
|
|
76
|
+
.action((code, opts, command) => run('Sandbox', async () => {
|
|
72
77
|
const { config } = await resolveProjectContext();
|
|
73
|
-
|
|
78
|
+
if (code !== undefined && opts.file) {
|
|
79
|
+
console.error(clrError('Pass either an inline <code> arg or --file <path>, not both'));
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
if (code === undefined && !opts.file) {
|
|
83
|
+
console.error(clrError('Provide an inline <code> arg or --file <path>'));
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
let source = code;
|
|
87
|
+
if (opts.file) {
|
|
88
|
+
try {
|
|
89
|
+
source = readFileSync(opts.file, 'utf8');
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
console.error(clrError(`Cannot read file: ${opts.file}`));
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Infer language from the file extension unless --language was given explicitly.
|
|
97
|
+
const fromExt = opts.file && command.getOptionValueSource('language') === 'default'
|
|
98
|
+
? LANG_MAP[extname(opts.file).slice(1).toLowerCase()]
|
|
99
|
+
: undefined;
|
|
100
|
+
const language = fromExt || LANG_MAP[opts.language] || opts.language;
|
|
74
101
|
if (!['javascript', 'python', 'bash'].includes(language)) {
|
|
75
102
|
console.error(clrError(`Invalid language: ${opts.language}. Use: js, py, or bash`));
|
|
76
103
|
process.exit(1);
|
|
@@ -78,7 +105,7 @@ GCC/Rust).
|
|
|
78
105
|
const timeout = parseInt(opts.timeout, 10);
|
|
79
106
|
const cwd = resolveRelativeCwd();
|
|
80
107
|
const res = await post(`/projects/${config.projectGuid}/sandbox/execute`, {
|
|
81
|
-
code,
|
|
108
|
+
code: source,
|
|
82
109
|
language,
|
|
83
110
|
timeout: isNaN(timeout) ? 30 : timeout,
|
|
84
111
|
input_files: opts.input,
|
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 { getAuth, getTimeRemaining } from '../auth.js';
|
|
5
|
-
import { getConfig } from '../config.js';
|
|
5
|
+
import { getConfig, liveUrl } from '../config.js';
|
|
6
6
|
import { brand, success, warning, muted, error as clrError } from '../colors.js';
|
|
7
7
|
import { HOOKS_SETTINGS, setupClaudeHooks } from '../setup.js';
|
|
8
8
|
/** Inspect `.claude/settings.json` against the current `HOOKS_SETTINGS`.
|
|
@@ -50,6 +50,7 @@ export const statusCommand = new Command('status')
|
|
|
50
50
|
slug: config.projectSlug,
|
|
51
51
|
account: config.accountSlug,
|
|
52
52
|
apiBase: config.apiBase,
|
|
53
|
+
url: liveUrl(config),
|
|
53
54
|
} : null,
|
|
54
55
|
auth: auth ? {
|
|
55
56
|
email: auth.email,
|
|
@@ -66,6 +67,7 @@ export const statusCommand = new Command('status')
|
|
|
66
67
|
else {
|
|
67
68
|
console.log(`${muted('Project:')} ${brand(config.projectSlug)} ${muted(`(${config.projectGuid})`)}`);
|
|
68
69
|
console.log(`${muted('Account:')} ${config.accountSlug}`);
|
|
70
|
+
console.log(`${muted('Live:')} ${liveUrl(config)}`);
|
|
69
71
|
console.log(`${muted('API:')} ${config.apiBase}`);
|
|
70
72
|
if (config.agentGuid)
|
|
71
73
|
console.log(`${muted('Agent:')} ${config.agentGuid}`);
|
package/dist/config.js
CHANGED
|
@@ -127,6 +127,17 @@ export async function resolveProjectContext(opts) {
|
|
|
127
127
|
oneOff: true,
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
|
+
/** The canonical live URL for a deployed project. This is THE place the
|
|
131
|
+
* dev/prod URL convention lives - every command that tells the user (or an
|
|
132
|
+
* agent) where their app is (`deploy`, `status`, `project info`) derives it
|
|
133
|
+
* here, so nothing ever has to reconstruct `dev.gipity.ai/<account>/<slug>/`
|
|
134
|
+
* by hand or guess a subdomain like `<slug>.gipity.app` (which doesn't
|
|
135
|
+
* resolve). Mirrors the server's deploy URL; `deploy` itself prints the
|
|
136
|
+
* server-authoritative URL, the read-only commands derive it from config. */
|
|
137
|
+
export function liveUrl(config, target = 'dev') {
|
|
138
|
+
const host = target === 'prod' ? 'app.gipity.ai' : 'dev.gipity.ai';
|
|
139
|
+
return `https://${host}/${config.accountSlug}/${config.projectSlug}/`;
|
|
140
|
+
}
|
|
130
141
|
export function clearConfigCache() {
|
|
131
142
|
cached = null;
|
|
132
143
|
cachedPath = null;
|
package/dist/flag-aliases.js
CHANGED
|
@@ -62,9 +62,17 @@ function collectRealFlags(argv, program) {
|
|
|
62
62
|
const args = argv.slice(2);
|
|
63
63
|
let cmd = program;
|
|
64
64
|
const chain = [program];
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
// Root value-options (e.g. `--api-base <url>`) may legitimately precede the
|
|
66
|
+
// subcommand; skip them (and their value token) rather than ending resolution
|
|
67
|
+
// there, so a command's own flag is still discovered when a global flag leads.
|
|
68
|
+
const rootValueFlags = new Set(program.options.filter(o => o.long && o.required).map(o => o.long));
|
|
69
|
+
for (let i = 0; i < args.length; i++) {
|
|
70
|
+
const tok = args[i];
|
|
71
|
+
if (tok.startsWith('-')) {
|
|
72
|
+
if (!tok.includes('=') && rootValueFlags.has(tok))
|
|
73
|
+
i++; // consume its value
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
68
76
|
const next = cmd.commands.find(c => c.name() === tok || c.aliases().includes(tok));
|
|
69
77
|
if (!next)
|
|
70
78
|
break;
|
package/dist/knowledge.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* AUTO-GENERATED - do not edit directly.
|
|
5
5
|
* Source: platform/docs/knowledge/*.md + docs/skills/*.md frontmatter + gipity-overview.ts
|
|
6
|
-
* Run `just
|
|
6
|
+
* Run `just build-knowledge` to refresh.
|
|
7
7
|
*/
|
|
8
8
|
export const BUILD_VS_NON_BUILD_RULE = `## When to add a template
|
|
9
9
|
If the user wants a deployable app (web, game, API): run \`gipity add <template>\` before writing any files. A template wires up \`gipity.yaml\`, deploy config, and sync; hand-written files miss all of it.
|
|
@@ -63,9 +63,9 @@ When a user asks for a foreign stack ("build it in React", "use MS SQL Server",
|
|
|
63
63
|
|
|
64
64
|
The one exception is app-level libraries the user imports into their own \`src/\` code - Three.js, Rapier, Phaser, MediaPipe, a charting or animation library. Those are fine. The opinionation is about the *platform* layer (framework, backend, database, styling system, hosting, auth, services), not every npm package.
|
|
65
65
|
|
|
66
|
-
##
|
|
66
|
+
## Build loop
|
|
67
67
|
|
|
68
|
-
The full rule and definition of done are
|
|
68
|
+
The full "when to add a template" rule and the definition of done are spelled out in the two sections at the end of this document. In short: if the user wants something deployable (web app, game, API), \`gipity add <template>\` first (default \`web-simple\`); for a one-off task (analysis, PDFs, data work), use \`gipity sandbox run\` instead; to add a reusable building block to an existing app (e.g. multiplayer), \`gipity add <kit>\`.
|
|
69
69
|
|
|
70
70
|
Build loop: \`gipity add\` → edit files → \`gipity deploy dev\` → \`gipity page inspect <url>\` → fix any errors → repeat until the definition of done is met.
|
|
71
71
|
|
|
@@ -75,7 +75,7 @@ Before telling the user the app is online, verify the source tree is consistent:
|
|
|
75
75
|
|
|
76
76
|
## CLI quick reference
|
|
77
77
|
|
|
78
|
-
Key commands: \`gipity add <template|kit>\`, \`gipity deploy dev\`, \`gipity sandbox run\`, \`gipity page inspect <url>\`, \`gipity db query "SQL"\`, \`gipity fn call <name>\`, \`gipity logs fn <name>\`, \`gipity skill read <name>\`.
|
|
78
|
+
Key commands: \`gipity add <template|kit>\`, \`gipity deploy dev\`, \`gipity sandbox run\`, \`gipity page inspect <url>\`, \`gipity page screenshot <url>\`, \`gipity db query "SQL"\`, \`gipity fn call <name>\`, \`gipity logs fn <name>\`, \`gipity skill read <name>\`.
|
|
79
79
|
Run \`gipity --help\` for the full list. Use \`--help\` on any command for details.
|
|
80
80
|
|
|
81
81
|
## Tool output is complete and synchronous
|
package/dist/progress.js
CHANGED
|
@@ -19,11 +19,26 @@ class TerminalProgress {
|
|
|
19
19
|
/** True while an in-place transfer line is on screen and not yet committed. */
|
|
20
20
|
liveOpen = false;
|
|
21
21
|
lastRenderAt = 0;
|
|
22
|
+
/** The label of the current transfer session; a change starts a fresh one. */
|
|
23
|
+
barLabel = null;
|
|
24
|
+
/** True once the current session hit 100% - late/overshoot ticks are dropped. */
|
|
25
|
+
barSettled = false;
|
|
22
26
|
phase(message) {
|
|
23
27
|
this.commitLive();
|
|
24
28
|
process.stdout.write(` ${muted(message)}\n`);
|
|
25
29
|
}
|
|
26
30
|
transfer(label, doneBytes, totalBytes) {
|
|
31
|
+
// A new label begins a fresh transfer session (e.g. downloads → uploads on
|
|
32
|
+
// the same reporter). Within a session, once we've drawn the 100% frame we
|
|
33
|
+
// drop any further ticks - download byte totals are estimated, so the wire
|
|
34
|
+
// can deliver a hair more or fewer bytes than expected and we don't want a
|
|
35
|
+
// late chunk reopening a second "100%" line.
|
|
36
|
+
if (label !== this.barLabel) {
|
|
37
|
+
this.barLabel = label;
|
|
38
|
+
this.barSettled = false;
|
|
39
|
+
}
|
|
40
|
+
if (this.barSettled)
|
|
41
|
+
return;
|
|
27
42
|
const finished = totalBytes > 0 && doneBytes >= totalBytes;
|
|
28
43
|
// Throttle mid-flight redraws; always paint the first and final frames.
|
|
29
44
|
const now = Date.now();
|
|
@@ -32,8 +47,10 @@ class TerminalProgress {
|
|
|
32
47
|
this.lastRenderAt = now;
|
|
33
48
|
this.liveOpen = true;
|
|
34
49
|
process.stdout.write('\r' + this.frame(label, doneBytes, totalBytes) + CLEAR_TO_EOL);
|
|
35
|
-
if (finished)
|
|
50
|
+
if (finished) {
|
|
36
51
|
this.commitLive();
|
|
52
|
+
this.barSettled = true;
|
|
53
|
+
}
|
|
37
54
|
}
|
|
38
55
|
finish() {
|
|
39
56
|
this.commitLive();
|
package/dist/prompts.js
CHANGED
|
@@ -105,14 +105,6 @@ export function buildProjectContextBlock(opts) {
|
|
|
105
105
|
DEFINITION_OF_DONE,
|
|
106
106
|
].join('\n').replace(/\n{3,}/g, '\n\n');
|
|
107
107
|
}
|
|
108
|
-
/** Project-context block + a brief greeting instruction. */
|
|
109
|
-
export function buildExistingProjectPrompt(opts) {
|
|
110
|
-
const isEmpty = opts.fileCount === 0;
|
|
111
|
-
const greeting = isEmpty
|
|
112
|
-
? `Briefly greet the user and ask what they want to build.`
|
|
113
|
-
: `Briefly greet the user, summarize what this project appears to be (based on the file listing and any README/CLAUDE.md/gipity.yaml), and ask what they want to work on next.`;
|
|
114
|
-
return [buildProjectContextBlock(opts), ``, greeting].join('\n');
|
|
115
|
-
}
|
|
116
108
|
/** First-launch prompt for a brand-new (empty) project. Reuses buildProjectContextBlock. */
|
|
117
109
|
export function buildNewProjectPrompt(opts) {
|
|
118
110
|
const base = buildProjectContextBlock(opts);
|
package/dist/setup.js
CHANGED
|
@@ -5,7 +5,7 @@ import { resolve, join, dirname } from 'path';
|
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
7
7
|
import { SCAFFOLD_HOOK_WARNING } from './prompts.js';
|
|
8
|
-
import { SKILLS_CONTENT } from './knowledge.js';
|
|
8
|
+
import { SKILLS_CONTENT, BUILD_VS_NON_BUILD_RULE, DEFINITION_OF_DONE } from './knowledge.js';
|
|
9
9
|
import { getConfig } from './config.js';
|
|
10
10
|
export { SKILLS_CONTENT };
|
|
11
11
|
/** Canonical list of workstation artifacts that are NOT part of the project.
|
|
@@ -218,9 +218,20 @@ export function setupClaudeHooks() {
|
|
|
218
218
|
* are stable - changing them would orphan the blocks in existing files. */
|
|
219
219
|
export const GIPITY_BLOCK_BEGIN = '<!-- BEGIN GIPITY INTEGRATION - auto-generated by gipity, do not edit this block -->';
|
|
220
220
|
export const GIPITY_BLOCK_END = '<!-- END GIPITY INTEGRATION -->';
|
|
221
|
-
/** The Gipity-owned section:
|
|
221
|
+
/** The Gipity-owned section, marker-wrapped: the integration guide + the full
|
|
222
|
+
* scaffold rule + the definition of done. The rule and DoD used to be injected
|
|
223
|
+
* only into the interactive `gipity claude` seed; folding the *static* parts
|
|
224
|
+
* into the primer means every agent (Claude, Codex, Gemini, ...) gets them, and
|
|
225
|
+
* the seed no longer has to carry that context.
|
|
226
|
+
*
|
|
227
|
+
* Per-project values (GUID, live URL) deliberately do NOT live here - baking
|
|
228
|
+
* them into a generated doc is the wrong layer. The CLI surfaces them where the
|
|
229
|
+
* agent actually looks: `gipity deploy` prints the live URL, `gipity status` and
|
|
230
|
+
* `gipity project info` show the URL + GUID. That keeps them authoritative and
|
|
231
|
+
* avoids stale values frozen into a file. */
|
|
222
232
|
function renderManagedBlock() {
|
|
223
|
-
|
|
233
|
+
const body = [SKILLS_CONTENT, BUILD_VS_NON_BUILD_RULE, DEFINITION_OF_DONE].join('\n\n');
|
|
234
|
+
return `${GIPITY_BLOCK_BEGIN}\n${body}\n${GIPITY_BLOCK_END}`;
|
|
224
235
|
}
|
|
225
236
|
/**
|
|
226
237
|
* Pure core of `writeSkillsFile`: given a file's current content (or `null`
|
package/dist/sync.js
CHANGED
|
@@ -195,14 +195,14 @@ async function fetchRemote(projectGuid) {
|
|
|
195
195
|
}
|
|
196
196
|
return out;
|
|
197
197
|
}
|
|
198
|
-
async function downloadAll(projectGuid) {
|
|
198
|
+
async function downloadAll(projectGuid, onBytes) {
|
|
199
199
|
const stream = await downloadStream(`/projects/${projectGuid}/files/tree?content=tar`);
|
|
200
200
|
const extract = tar.extract();
|
|
201
201
|
const files = new Map();
|
|
202
202
|
return new Promise((resolve, reject) => {
|
|
203
203
|
extract.on('entry', (header, entryStream, next) => {
|
|
204
204
|
const chunks = [];
|
|
205
|
-
entryStream.on('data', (c) => chunks.push(c));
|
|
205
|
+
entryStream.on('data', (c) => { chunks.push(c); onBytes?.(c.length); });
|
|
206
206
|
entryStream.on('end', () => { files.set(header.name, Buffer.concat(chunks)); next(); });
|
|
207
207
|
entryStream.resume();
|
|
208
208
|
});
|
|
@@ -519,9 +519,23 @@ async function syncInner(projectGuid, root, ignore, opts, interactive) {
|
|
|
519
519
|
const downloadedBytes = new Map();
|
|
520
520
|
const needsBulkDownload = plannedToApply.some(a => a.kind === 'download' || a.kind === 'conflict');
|
|
521
521
|
if (needsBulkDownload) {
|
|
522
|
-
|
|
522
|
+
// The tree endpoint streams the *whole* remote tree as one tar (the caller
|
|
523
|
+
// then picks out only the paths it planned to apply), so the bytes that
|
|
524
|
+
// actually move = the sum of every remote file's size. That's the honest
|
|
525
|
+
// denominator for the bar - it tracks real wire progress, not just the
|
|
526
|
+
// handful of changed files.
|
|
527
|
+
const downloadLabel = 'Downloading updates from Gipity';
|
|
528
|
+
const totalDownloadBytes = [...remote.values()].reduce((sum, r) => sum + r.size, 0);
|
|
529
|
+
let recvBytes = 0;
|
|
530
|
+
p?.transfer(downloadLabel, 0, totalDownloadBytes);
|
|
531
|
+
const onBytes = p
|
|
532
|
+
? (delta) => {
|
|
533
|
+
recvBytes = Math.min(recvBytes + delta, totalDownloadBytes);
|
|
534
|
+
p.transfer(downloadLabel, recvBytes, totalDownloadBytes);
|
|
535
|
+
}
|
|
536
|
+
: undefined;
|
|
523
537
|
try {
|
|
524
|
-
const all = await downloadAll(config.projectGuid);
|
|
538
|
+
const all = await downloadAll(config.projectGuid, onBytes);
|
|
525
539
|
for (const a of plannedToApply) {
|
|
526
540
|
if (a.kind === 'download' || a.kind === 'conflict') {
|
|
527
541
|
const buf = all.get(a.path);
|
|
@@ -533,6 +547,11 @@ async function syncInner(projectGuid, root, ignore, opts, interactive) {
|
|
|
533
547
|
catch (err) {
|
|
534
548
|
errors.push(`Download batch failed: ${err.message}`);
|
|
535
549
|
}
|
|
550
|
+
finally {
|
|
551
|
+
// Settle the bar even if the extracted-byte tally fell short of the
|
|
552
|
+
// estimate (the live line stays open until something hits 100% or finish()).
|
|
553
|
+
p?.finish();
|
|
554
|
+
}
|
|
536
555
|
}
|
|
537
556
|
// ── Writes pass: uploads, downloads, conflicts (rename + download + upload copy) ──
|
|
538
557
|
// We serialize conflicts; uploads run with bounded concurrency.
|
package/dist/template-vars.js
CHANGED
|
@@ -68,7 +68,10 @@ export function buildTemplateVars(v) {
|
|
|
68
68
|
'{{DESCRIPTION_META}}': v.description ? `\n <meta name="description" content="${safeDesc}">` : '',
|
|
69
69
|
'{{OG_DESCRIPTION}}': v.description ? `\n <meta property="og:description" content="${safeDesc}">` : '',
|
|
70
70
|
'{{JSON_LD_BLOCK}}': `<script type="application/ld+json">\n${jsonLd}\n </script>`,
|
|
71
|
-
|
|
71
|
+
// `crossorigin="anonymous"` so SDK errors surface with a real message/stack
|
|
72
|
+
// (CORS mode) instead of a sanitized message-less "Script error". The CDN
|
|
73
|
+
// returns Access-Control-Allow-Origin:*, so it works on any app domain.
|
|
74
|
+
'{{ANALYTICS_SCRIPT}}': `<script defer crossorigin="anonymous" src="https://media.gipity.ai/client/v1/gipity.js" data-app="${v.projectGuid}"></script>`,
|
|
72
75
|
};
|
|
73
76
|
}
|
|
74
77
|
/** Pure string substitution — exported so the test can exercise it without
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gipity",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.380",
|
|
4
4
|
"description": "The full-stack platform tuned for AI agents. Database, storage, auth, functions, deploy, and drop-in kits - all agent-tuned. Pair with Claude Code or use standalone.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"gipity": "dist/updater/shim.js",
|