gipity 1.1.5 → 1.1.7
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/agents/agy.js +52 -0
- package/dist/agents/index.js +2 -0
- package/dist/api.js +19 -3
- package/dist/capture/sources/agy.js +88 -0
- package/dist/catalog.js +2 -2
- package/dist/client-context.js +9 -0
- package/dist/commands/brand.js +125 -0
- package/dist/commands/build.js +1 -1
- package/dist/commands/db.js +61 -0
- package/dist/commands/fn.js +2 -8
- package/dist/commands/init.js +2 -2
- package/dist/commands/key.js +91 -0
- package/dist/commands/page-eval.js +70 -3
- package/dist/commands/page-screenshot.js +34 -6
- package/dist/commands/records.js +93 -23
- package/dist/commands/service.js +71 -7
- package/dist/commands/token.js +6 -1
- package/dist/commands/uninstall.js +14 -1
- package/dist/commands/workflow.js +59 -6
- package/dist/db-checkpoint.js +42 -0
- package/dist/hooks/capture-runner.js +20 -8
- package/dist/index.js +777 -137
- package/dist/knowledge.js +3 -3
- package/dist/project-setup.js +2 -0
- package/dist/relay/diagnostics.js +4 -2
- package/dist/relay/onboarding.js +3 -3
- package/dist/setup.js +198 -16
- package/dist/template-vars.js +74 -0
- package/dist/utils.js +16 -5
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -4002,9 +4002,10 @@ async function confirm(question, opts = {}) {
|
|
|
4002
4002
|
const defaultYes = opts.default === "yes";
|
|
4003
4003
|
if (opts.skip ?? _autoConfirm) return true;
|
|
4004
4004
|
if (!process.stdin.isTTY) {
|
|
4005
|
+
if (opts.headless === "no") return false;
|
|
4005
4006
|
console.error(`Confirmation required (non-interactive). Re-run with --yes:
|
|
4006
4007
|
${rerunWithYes()}`);
|
|
4007
|
-
|
|
4008
|
+
process.exit(1);
|
|
4008
4009
|
}
|
|
4009
4010
|
const hint = defaultYes ? dim("[Y/n]") : dim("[y/N]");
|
|
4010
4011
|
process.stdout.write(`${question} ${hint}: `);
|
|
@@ -6597,6 +6598,9 @@ function detectHarness() {
|
|
|
6597
6598
|
}
|
|
6598
6599
|
if (hasEnvPrefix("CODEX_")) return { harness: "codex" };
|
|
6599
6600
|
if (hasEnvPrefix("GROK_")) return { harness: "grok", harnessSession: process.env.GROK_SESSION_ID };
|
|
6601
|
+
if (process.env.ANTIGRAVITY_CONVERSATION_ID) {
|
|
6602
|
+
return { harness: "agy", harnessSession: process.env.ANTIGRAVITY_CONVERSATION_ID };
|
|
6603
|
+
}
|
|
6600
6604
|
if (process.env.CURSOR_TRACE_ID || hasEnvPrefix("CURSOR_") || (process.env.TERM_PROGRAM ?? "").toLowerCase().includes("cursor")) {
|
|
6601
6605
|
return { harness: "cursor" };
|
|
6602
6606
|
}
|
|
@@ -6643,11 +6647,13 @@ __export(api_exports, {
|
|
|
6643
6647
|
getAccountSlug: () => getAccountSlug,
|
|
6644
6648
|
getAuthHeader: () => getAuthHeader,
|
|
6645
6649
|
getBaseUrl: () => getBaseUrl,
|
|
6650
|
+
mintAppToken: () => mintAppToken,
|
|
6646
6651
|
patch: () => patch,
|
|
6647
6652
|
post: () => post,
|
|
6648
6653
|
postBinary: () => postBinary,
|
|
6649
6654
|
postForTarEntries: () => postForTarEntries,
|
|
6650
6655
|
publicPost: () => publicPost,
|
|
6656
|
+
publicRequest: () => publicRequest,
|
|
6651
6657
|
put: () => put,
|
|
6652
6658
|
putTimeoutMs: () => putTimeoutMs,
|
|
6653
6659
|
putToPresignedUrl: () => putToPresignedUrl,
|
|
@@ -6903,12 +6909,12 @@ async function getAccountSlug() {
|
|
|
6903
6909
|
accountSlugCache = res.data.accountSlug;
|
|
6904
6910
|
return accountSlugCache;
|
|
6905
6911
|
}
|
|
6906
|
-
async function
|
|
6912
|
+
async function publicRequest(method, path5, body, extraHeaders) {
|
|
6907
6913
|
const url = `${baseUrl()}${path5}`;
|
|
6908
6914
|
const res = await fetch(url, {
|
|
6909
|
-
method
|
|
6915
|
+
method,
|
|
6910
6916
|
headers: { ...clientHeaders(), "Content-Type": "application/json", ...extraHeaders },
|
|
6911
|
-
body: JSON.stringify(body)
|
|
6917
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
6912
6918
|
});
|
|
6913
6919
|
if (!res.ok) {
|
|
6914
6920
|
const json = await res.json().catch(() => ({ error: { code: "UNKNOWN", message: res.statusText } }));
|
|
@@ -6917,6 +6923,17 @@ async function publicPost(path5, body, extraHeaders) {
|
|
|
6917
6923
|
}
|
|
6918
6924
|
return res.json();
|
|
6919
6925
|
}
|
|
6926
|
+
function publicPost(path5, body, extraHeaders) {
|
|
6927
|
+
return publicRequest("POST", path5, body, extraHeaders);
|
|
6928
|
+
}
|
|
6929
|
+
async function mintAppToken(projectGuid2) {
|
|
6930
|
+
try {
|
|
6931
|
+
const minted = await publicPost("/api/token", { app: projectGuid2 });
|
|
6932
|
+
return { "X-App-Token": minted.data.token };
|
|
6933
|
+
} catch {
|
|
6934
|
+
return void 0;
|
|
6935
|
+
}
|
|
6936
|
+
}
|
|
6920
6937
|
var tar, ApiError, REQUEST_TIMEOUT_MS, DOWNLOAD_HEADER_TIMEOUT_MS, PUT_TIMEOUT_FLOOR_MS, PUT_MIN_BYTES_PER_SEC, accountSlugCache;
|
|
6921
6938
|
var init_api = __esm({
|
|
6922
6939
|
"src/api.ts"() {
|
|
@@ -32075,7 +32092,7 @@ mkdir -p ~/GipityProjects/<slug> && cd ~/GipityProjects/<slug> && gipity init <s
|
|
|
32075
32092
|
|
|
32076
32093
|
## CLI quick reference
|
|
32077
32094
|
|
|
32078
|
-
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 secrets set <NAME> <value> [--account]\` (store an API key/token encrypted; read in functions via \`secrets.get('NAME')\` \u2014 never hardcode keys), \`gipity email send --to <addr> --subject <s> --body <b>\` (sends as \`gipity@gipity.ai\`; omit \`--to\` to self-send), \`gipity skill read <name>\`.
|
|
32095
|
+
Key commands: \`gipity add <template|kit>\`, \`gipity brand set --emoji <e>|--color <hex>\` (regenerate the app's icons + social share card), \`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 secrets set <NAME> <value> [--account]\` (store an API key/token encrypted; read in functions via \`secrets.get('NAME')\` \u2014 never hardcode keys), \`gipity email send --to <addr> --subject <s> --body <b>\` (sends as \`gipity@gipity.ai\`; omit \`--to\` to self-send), \`gipity skill read <name>\`.
|
|
32079
32096
|
Rename for findability: \`gipity project rename <name>\` renames the current project's display name (the slug and deployed URLs never change); \`gipity chat rename <title>\` renames the current chat's tab title. Both are the display label users scan to switch between tabs \u2014 retitle a chat when the conversation clearly shifts to a new topic (sparingly, not every turn), and keep every project/chat title SHORT: 2-4 words, \u226440 characters, no trailing punctuation (e.g. "Stripe checkout", "Tetris game").
|
|
32080
32097
|
Pull an existing remote project local (given its URL/slug): \`mkdir -p ~/GipityProjects/<slug> && cd ~/GipityProjects/<slug> && gipity init <slug>\` (adopts the matching project and syncs files down - this is the "clone").
|
|
32081
32098
|
Move whole apps in/out: \`gipity save\` (export this project as a portable \`.gip\` bundle), \`gipity load <file.gip | github:owner/repo>\` (import as a NEW project; \`--inspect\` to preview), \`gipity github connect\` (1-2 click GitHub access for imports). Porting a Vercel/Replit/Lovable app? Load the \`app-import\` skill first.
|
|
@@ -32085,7 +32102,7 @@ Run \`gipity --help\` for the full list. Use \`--help\` on any command for detai
|
|
|
32085
32102
|
|
|
32086
32103
|
Function return shape: \`gipity fn call\`, the in-test \`ctx.fn.call\`/\`callAs\`, and the client \`Gipity.fn\` all return your function's value **unwrapped** - read/assert \`result.field\`. Only raw HTTP/\`curl\` wraps it as \`{ data: ... }\`; never write \`result.data.field\` in a test.
|
|
32087
32104
|
|
|
32088
|
-
Tests are isolated, not run against your live DB: \`gipity test\` points \`ctx.fn.call\`/\`callAs\` at a throwaway copy of your database (your \`migrations/\` + \`seeds/\`), reset (truncate + reseed) before
|
|
32105
|
+
Tests are isolated, not run against your live DB: \`gipity test\` points \`ctx.fn.call\`/\`callAs\` at a throwaway copy of your database (your \`migrations/\` + \`seeds/\`), one per test FILE, reset (truncate + reseed) before the file runs - so files can't interfere, test rows never reach the deployed app, and you don't write teardown. Functions see \`ctx.isTest === true\` during a run (use it to skip your own rate limiting); the platform also suppresses \`notify()\` push so a suite can't spam subscribers. Reference data tests need goes in seed files; a runtime-written settings table that isn't seeded goes under \`test.preserve\` in \`gipity.yaml\`. Build per-file fixtures in \`setup(fn)\` \u2192 \`ctx.fixtures\`.
|
|
32089
32106
|
|
|
32090
32107
|
## Hit friction on the platform? Report it in real time
|
|
32091
32108
|
|
|
@@ -32156,7 +32173,7 @@ App development skills:
|
|
|
32156
32173
|
- \`app-development\` - functions, database, and API
|
|
32157
32174
|
- \`app-import\` - import apps from GitHub/.gip bundles (incl. Vercel/Replit/Lovable porting) and export any project as a portable .gip - app_import tool, gipity save/load
|
|
32158
32175
|
- \`app-records\` - Gipity Records: declared tables \u2192 validated CRUD, provenance, workflows, auto UI
|
|
32159
|
-
- \`app-testing\` - testing deployed app functions (ctx.fn.call/callAs, the
|
|
32176
|
+
- \`app-testing\` - testing deployed app functions (ctx.fn.call/callAs, the per-file test DB)
|
|
32160
32177
|
- \`deploy\` - the deploy pipeline & gipity.yaml manifest
|
|
32161
32178
|
- \`jobs\` - long-running CPU + GPU compute jobs (Python / Node / bash)
|
|
32162
32179
|
- \`realtime-scheduled-app\` - recipe: realtime presence/messages + DB function + scheduled poster, end-to-end
|
|
@@ -32513,7 +32530,7 @@ var UploadConflictError = class extends Error {
|
|
|
32513
32530
|
currentServerVersion;
|
|
32514
32531
|
path;
|
|
32515
32532
|
};
|
|
32516
|
-
async function uploadOneFile(
|
|
32533
|
+
async function uploadOneFile(projectGuid2, localPath, virtualPath, opts = {}) {
|
|
32517
32534
|
const { sha256, size } = await hashFile(localPath);
|
|
32518
32535
|
const mime = opts.mime ?? guessMime(virtualPath);
|
|
32519
32536
|
const initBody = { path: virtualPath, size, sha256, mime };
|
|
@@ -32523,7 +32540,7 @@ async function uploadOneFile(projectGuid, localPath, virtualPath, opts = {}) {
|
|
|
32523
32540
|
let init;
|
|
32524
32541
|
try {
|
|
32525
32542
|
init = await post(
|
|
32526
|
-
`/projects/${
|
|
32543
|
+
`/projects/${projectGuid2}/files/upload-init`,
|
|
32527
32544
|
initBody
|
|
32528
32545
|
);
|
|
32529
32546
|
} catch (err) {
|
|
@@ -32545,7 +32562,7 @@ async function uploadOneFile(projectGuid, localPath, virtualPath, opts = {}) {
|
|
|
32545
32562
|
}
|
|
32546
32563
|
let comp;
|
|
32547
32564
|
try {
|
|
32548
|
-
comp = await post(`/projects/${
|
|
32565
|
+
comp = await post(`/projects/${projectGuid2}/files/upload-complete`, completeBody);
|
|
32549
32566
|
} catch (err) {
|
|
32550
32567
|
if (err instanceof ApiError && err.statusCode === 409) {
|
|
32551
32568
|
const current = typeof err.data?.current_server_version === "number" ? err.data.current_server_version : null;
|
|
@@ -32627,16 +32644,16 @@ async function transferToS3(localPath, size, mime, data, opts = {}) {
|
|
|
32627
32644
|
completed.sort((a, b7) => a.part_number - b7.part_number);
|
|
32628
32645
|
return { parts: completed };
|
|
32629
32646
|
}
|
|
32630
|
-
async function uploadInitBatch(
|
|
32647
|
+
async function uploadInitBatch(projectGuid2, files) {
|
|
32631
32648
|
const res = await post(
|
|
32632
|
-
`/projects/${
|
|
32649
|
+
`/projects/${projectGuid2}/files/upload-init-batch`,
|
|
32633
32650
|
{ files }
|
|
32634
32651
|
);
|
|
32635
32652
|
return res.data.results;
|
|
32636
32653
|
}
|
|
32637
|
-
async function uploadCompleteBatch(
|
|
32654
|
+
async function uploadCompleteBatch(projectGuid2, items) {
|
|
32638
32655
|
const res = await post(
|
|
32639
|
-
`/projects/${
|
|
32656
|
+
`/projects/${projectGuid2}/files/upload-complete-batch`,
|
|
32640
32657
|
{ items }
|
|
32641
32658
|
);
|
|
32642
32659
|
return res.data.results;
|
|
@@ -32653,6 +32670,8 @@ var PRIMER_FILES = {
|
|
|
32653
32670
|
codex: "AGENTS.md",
|
|
32654
32671
|
grok: "AGENTS.md",
|
|
32655
32672
|
// Grok Build reads the AGENTS.md family (and CLAUDE.md) natively
|
|
32673
|
+
agy: "AGENTS.md",
|
|
32674
|
+
// Antigravity reads the same AGENTS.md/GEMINI.md rules family natively
|
|
32656
32675
|
aider: "AGENTS.md",
|
|
32657
32676
|
// shares the Codex primer; aider is pointed at it via .aider.conf.yml
|
|
32658
32677
|
gemini: "GEMINI.md",
|
|
@@ -32668,6 +32687,7 @@ var DEFAULT_SYNC_IGNORE = [
|
|
|
32668
32687
|
".gipity/",
|
|
32669
32688
|
".claude/",
|
|
32670
32689
|
".codex/",
|
|
32690
|
+
".agents/",
|
|
32671
32691
|
".gitignore",
|
|
32672
32692
|
AIDER_CONF_FILE,
|
|
32673
32693
|
// Home-directory junk: a project created inside a real home dir (or one that
|
|
@@ -32867,9 +32887,11 @@ function ensureGrokPluginInstalled() {
|
|
|
32867
32887
|
var AGENTS_SKILLS_DIR = join4(homedir3(), ".agents", "skills");
|
|
32868
32888
|
var AGENT_HOOKS_DIR = join4(homedir3(), ".gipity", "agent-hooks");
|
|
32869
32889
|
var AGENT_SKILLS_MANIFEST = join4(homedir3(), ".gipity", "agent-skills.json");
|
|
32870
|
-
|
|
32890
|
+
var AGY_SKILLS_DIR = join4(homedir3(), ".gemini", "config", "skills");
|
|
32891
|
+
var AGY_SKILLS_MANIFEST = join4(homedir3(), ".gipity", "agy-skills.json");
|
|
32892
|
+
function skillsManifestState(manifestPath) {
|
|
32871
32893
|
try {
|
|
32872
|
-
const m = JSON.parse(readFileSync6(
|
|
32894
|
+
const m = JSON.parse(readFileSync6(manifestPath, "utf-8"));
|
|
32873
32895
|
return {
|
|
32874
32896
|
current: typeof m?.version === "string" && versionGte(m.version, GIPITY_PLUGIN_VERSION),
|
|
32875
32897
|
skills: Array.isArray(m?.skills) ? m.skills : []
|
|
@@ -32878,8 +32900,13 @@ function agentSkillsState() {
|
|
|
32878
32900
|
return { current: false, skills: [] };
|
|
32879
32901
|
}
|
|
32880
32902
|
}
|
|
32881
|
-
function
|
|
32882
|
-
|
|
32903
|
+
function agentSkillsState() {
|
|
32904
|
+
return skillsManifestState(AGENT_SKILLS_MANIFEST);
|
|
32905
|
+
}
|
|
32906
|
+
function agySkillsState() {
|
|
32907
|
+
return skillsManifestState(AGY_SKILLS_MANIFEST);
|
|
32908
|
+
}
|
|
32909
|
+
function installSkillsAndHooks(skillsDir, manifestPath, harnessLabel) {
|
|
32883
32910
|
if (!binaryOnPath("git")) return;
|
|
32884
32911
|
const tmp = mkdtempSync(join4(tmpdir(), "gipity-skills-"));
|
|
32885
32912
|
try {
|
|
@@ -32901,8 +32928,8 @@ function ensureAgentSkillsInstalled() {
|
|
|
32901
32928
|
for (const entry of readdirSync3(skillsSrc, { withFileTypes: true })) {
|
|
32902
32929
|
if (!entry.isDirectory()) continue;
|
|
32903
32930
|
if (!existsSync4(join4(skillsSrc, entry.name, "SKILL.md"))) continue;
|
|
32904
|
-
mkdirSync3(
|
|
32905
|
-
cpSync(join4(skillsSrc, entry.name), join4(
|
|
32931
|
+
mkdirSync3(skillsDir, { recursive: true });
|
|
32932
|
+
cpSync(join4(skillsSrc, entry.name), join4(skillsDir, entry.name), {
|
|
32906
32933
|
recursive: true,
|
|
32907
32934
|
force: true
|
|
32908
32935
|
});
|
|
@@ -32912,13 +32939,21 @@ function ensureAgentSkillsInstalled() {
|
|
|
32912
32939
|
for (const script of readdirSync3(join4(repo, "hooks", "scripts"))) {
|
|
32913
32940
|
cpSync(join4(repo, "hooks", "scripts", script), join4(AGENT_HOOKS_DIR, script), { force: true });
|
|
32914
32941
|
}
|
|
32915
|
-
writeFileSync4(
|
|
32916
|
-
console.log(`Installed ${names.length} Gipity skills for
|
|
32942
|
+
writeFileSync4(manifestPath, JSON.stringify({ version, skills: names }, null, 2) + "\n");
|
|
32943
|
+
console.log(`Installed ${names.length} Gipity skills for ${harnessLabel} (${skillsDir}).`);
|
|
32917
32944
|
} catch {
|
|
32918
32945
|
} finally {
|
|
32919
32946
|
rmSync(tmp, { recursive: true, force: true });
|
|
32920
32947
|
}
|
|
32921
32948
|
}
|
|
32949
|
+
function ensureAgentSkillsInstalled() {
|
|
32950
|
+
if (agentSkillsState().current) return;
|
|
32951
|
+
installSkillsAndHooks(AGENTS_SKILLS_DIR, AGENT_SKILLS_MANIFEST, "Codex");
|
|
32952
|
+
}
|
|
32953
|
+
function ensureAgySkillsInstalled() {
|
|
32954
|
+
if (agySkillsState().current) return;
|
|
32955
|
+
installSkillsAndHooks(AGY_SKILLS_DIR, AGY_SKILLS_MANIFEST, "Antigravity");
|
|
32956
|
+
}
|
|
32922
32957
|
function applyCodexHooks(existing) {
|
|
32923
32958
|
const launcher = join4(AGENT_HOOKS_DIR, "launch.sh");
|
|
32924
32959
|
const cmd = (script, ...args) => [`sh "${launcher}" "${join4(AGENT_HOOKS_DIR, script)}"`, ...args].join(" ");
|
|
@@ -32976,6 +33011,102 @@ function setupCodexIntegration() {
|
|
|
32976
33011
|
ensureAgentSkillsInstalled();
|
|
32977
33012
|
setupCodexHooks();
|
|
32978
33013
|
}
|
|
33014
|
+
var AGY_HOOKS_SCRIPT = `#!/usr/bin/env node
|
|
33015
|
+
'use strict';
|
|
33016
|
+
const { spawnSync } = require('child_process');
|
|
33017
|
+
const { join } = require('path');
|
|
33018
|
+
|
|
33019
|
+
const WRITE_TOOLS = new Set(['write_to_file', 'replace_file_content']);
|
|
33020
|
+
|
|
33021
|
+
function readStdin() {
|
|
33022
|
+
return new Promise((res) => {
|
|
33023
|
+
let data = '';
|
|
33024
|
+
process.stdin.setEncoding('utf-8');
|
|
33025
|
+
process.stdin.on('data', (c) => { data += c; });
|
|
33026
|
+
process.stdin.on('end', () => res(data));
|
|
33027
|
+
process.stdin.on('error', () => res(data));
|
|
33028
|
+
});
|
|
33029
|
+
}
|
|
33030
|
+
|
|
33031
|
+
async function main() {
|
|
33032
|
+
const event = process.argv[2];
|
|
33033
|
+
const raw = await readStdin();
|
|
33034
|
+
let payload = {};
|
|
33035
|
+
try { payload = JSON.parse(raw); } catch { /* keep {} */ }
|
|
33036
|
+
|
|
33037
|
+
try {
|
|
33038
|
+
if (event === 'post-tool-use') {
|
|
33039
|
+
const toolCall = payload.toolCall;
|
|
33040
|
+
const filePath = toolCall && toolCall.args && toolCall.args.TargetFile;
|
|
33041
|
+
if (toolCall && WRITE_TOOLS.has(toolCall.name) && typeof filePath === 'string' && filePath) {
|
|
33042
|
+
spawnSync(process.execPath, [join(__dirname, 'sync-push.cjs')], {
|
|
33043
|
+
input: JSON.stringify({ tool_input: { file_path: filePath } }),
|
|
33044
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
33045
|
+
windowsHide: true,
|
|
33046
|
+
});
|
|
33047
|
+
}
|
|
33048
|
+
spawnSync(process.execPath, [join(__dirname, 'capture.cjs'), 'agy', 'post-tool-use'], {
|
|
33049
|
+
input: raw,
|
|
33050
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
33051
|
+
windowsHide: true,
|
|
33052
|
+
});
|
|
33053
|
+
} else if (event === 'stop') {
|
|
33054
|
+
spawnSync(process.execPath, [join(__dirname, 'capture.cjs'), 'agy', 'stop'], {
|
|
33055
|
+
input: raw,
|
|
33056
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
33057
|
+
windowsHide: true,
|
|
33058
|
+
});
|
|
33059
|
+
}
|
|
33060
|
+
} catch { /* never let a side effect break the response below */ }
|
|
33061
|
+
|
|
33062
|
+
process.stdout.write('{}');
|
|
33063
|
+
}
|
|
33064
|
+
|
|
33065
|
+
main();
|
|
33066
|
+
`;
|
|
33067
|
+
function applyAgyHooks(existing) {
|
|
33068
|
+
const wrapper = join4(AGENT_HOOKS_DIR, "agy-hooks.cjs");
|
|
33069
|
+
const launcher = join4(AGENT_HOOKS_DIR, "launch.sh");
|
|
33070
|
+
const wrapCmd = (event) => `sh "${launcher}" "${wrapper}" ${event}`;
|
|
33071
|
+
const block = {
|
|
33072
|
+
PostToolUse: [
|
|
33073
|
+
{ matcher: ".*", hooks: [{ type: "command", command: wrapCmd("post-tool-use"), timeout: 30 }] }
|
|
33074
|
+
],
|
|
33075
|
+
Stop: [
|
|
33076
|
+
{ hooks: [{ type: "command", command: wrapCmd("stop"), timeout: 60 }] }
|
|
33077
|
+
]
|
|
33078
|
+
};
|
|
33079
|
+
let settings = {};
|
|
33080
|
+
if (existing !== null) {
|
|
33081
|
+
try {
|
|
33082
|
+
settings = JSON.parse(existing);
|
|
33083
|
+
} catch {
|
|
33084
|
+
return null;
|
|
33085
|
+
}
|
|
33086
|
+
}
|
|
33087
|
+
if (JSON.stringify(settings.gipity ?? null) === JSON.stringify(block)) return null;
|
|
33088
|
+
settings.gipity = block;
|
|
33089
|
+
return JSON.stringify(settings, null, 2) + "\n";
|
|
33090
|
+
}
|
|
33091
|
+
function setupAgyHooks() {
|
|
33092
|
+
if (process.platform === "win32") return;
|
|
33093
|
+
const cwd = resolve3(process.cwd());
|
|
33094
|
+
if (cwd === resolve3(homedir3())) return;
|
|
33095
|
+
mkdirSync3(AGENT_HOOKS_DIR, { recursive: true });
|
|
33096
|
+
writeFileSync4(join4(AGENT_HOOKS_DIR, "agy-hooks.cjs"), AGY_HOOKS_SCRIPT);
|
|
33097
|
+
const path5 = join4(cwd, ".agents", "hooks.json");
|
|
33098
|
+
const existing = existsSync4(path5) ? readFileSync6(path5, "utf-8") : null;
|
|
33099
|
+
const next = applyAgyHooks(existing);
|
|
33100
|
+
if (next === null) return;
|
|
33101
|
+
mkdirSync3(dirname3(path5), { recursive: true });
|
|
33102
|
+
writeFileSync4(path5, next);
|
|
33103
|
+
console.log(existing === null ? "Wrote Antigravity sync + session-capture hooks (.agents/hooks.json)." : "Updated Antigravity hooks (.agents/hooks.json).");
|
|
33104
|
+
}
|
|
33105
|
+
function setupAgyIntegration() {
|
|
33106
|
+
if (!binaryOnPath("agy")) return;
|
|
33107
|
+
ensureAgySkillsInstalled();
|
|
33108
|
+
setupAgyHooks();
|
|
33109
|
+
}
|
|
32979
33110
|
function setupClaudeHooks() {
|
|
32980
33111
|
ensureGipityPlugin();
|
|
32981
33112
|
const cwd = resolve3(process.cwd());
|
|
@@ -33097,6 +33228,7 @@ var SUPPORTED_TOOLS = [
|
|
|
33097
33228
|
{ key: "claude", label: "Claude Code (CLAUDE.md + Gipity plugin)", setup: setupClaudeMd, integrate: setupClaudeHooks },
|
|
33098
33229
|
{ key: "codex", label: "OpenAI Codex (AGENTS.md + skills + sync hooks)", setup: setupAgentsMd, integrate: setupCodexIntegration },
|
|
33099
33230
|
{ key: "grok", label: "Grok Build (AGENTS.md + Gipity plugin)", setup: setupAgentsMd, integrate: ensureGrokPluginInstalled },
|
|
33231
|
+
{ key: "agy", label: "Antigravity (AGENTS.md + skills + sync hooks)", setup: setupAgentsMd, integrate: setupAgyIntegration },
|
|
33100
33232
|
{ key: "aider", label: "Aider (AGENTS.md + .aider.conf.yml)", setup: setupAiderMd, optIn: true },
|
|
33101
33233
|
{ key: "gemini", label: "Gemini CLI (GEMINI.md)", setup: setupGeminiMd },
|
|
33102
33234
|
{ key: "copilot", label: "GitHub Copilot (.github/copilot-instructions.md)", setup: setupCopilotMd },
|
|
@@ -33226,22 +33358,22 @@ async function acquireLock(progress) {
|
|
|
33226
33358
|
}
|
|
33227
33359
|
}
|
|
33228
33360
|
}
|
|
33229
|
-
function readBaseline(
|
|
33361
|
+
function readBaseline(projectGuid2) {
|
|
33230
33362
|
const path5 = syncStatePath();
|
|
33231
|
-
if (!existsSync5(path5)) return { projectGuid, files: {}, lastFullSync: null };
|
|
33363
|
+
if (!existsSync5(path5)) return { projectGuid: projectGuid2, files: {}, lastFullSync: null };
|
|
33232
33364
|
try {
|
|
33233
33365
|
const parsed = JSON.parse(readFileSync7(path5, "utf-8"));
|
|
33234
|
-
if (parsed.projectGuid !==
|
|
33235
|
-
return { projectGuid, files: {}, lastFullSync: null };
|
|
33366
|
+
if (parsed.projectGuid !== projectGuid2) {
|
|
33367
|
+
return { projectGuid: projectGuid2, files: {}, lastFullSync: null };
|
|
33236
33368
|
}
|
|
33237
33369
|
return {
|
|
33238
|
-
projectGuid,
|
|
33370
|
+
projectGuid: projectGuid2,
|
|
33239
33371
|
files: parsed.files ?? {},
|
|
33240
33372
|
lastFullSync: parsed.lastFullSync ?? null,
|
|
33241
33373
|
...parsed.deletesSkippedStreak ? { deletesSkippedStreak: parsed.deletesSkippedStreak } : {}
|
|
33242
33374
|
};
|
|
33243
33375
|
} catch {
|
|
33244
|
-
return { projectGuid, files: {}, lastFullSync: null };
|
|
33376
|
+
return { projectGuid: projectGuid2, files: {}, lastFullSync: null };
|
|
33245
33377
|
}
|
|
33246
33378
|
}
|
|
33247
33379
|
function writeBaseline(b7) {
|
|
@@ -33314,8 +33446,8 @@ function resolveInRoot(root, relPath) {
|
|
|
33314
33446
|
}
|
|
33315
33447
|
return full;
|
|
33316
33448
|
}
|
|
33317
|
-
async function fetchRemote(
|
|
33318
|
-
const res = await get(`/projects/${
|
|
33449
|
+
async function fetchRemote(projectGuid2) {
|
|
33450
|
+
const res = await get(`/projects/${projectGuid2}/files/tree`);
|
|
33319
33451
|
const out = /* @__PURE__ */ new Map();
|
|
33320
33452
|
for (const f of res.data) {
|
|
33321
33453
|
if (f.type !== "file") continue;
|
|
@@ -33376,14 +33508,14 @@ function extractTarToMap(stream, idleMs, onBytes, keep) {
|
|
|
33376
33508
|
stream.pipe(extract3);
|
|
33377
33509
|
});
|
|
33378
33510
|
}
|
|
33379
|
-
async function downloadAll(
|
|
33380
|
-
const stream = await downloadStream(`/projects/${
|
|
33511
|
+
async function downloadAll(projectGuid2, onBytes) {
|
|
33512
|
+
const stream = await downloadStream(`/projects/${projectGuid2}/files/tree?content=tar`);
|
|
33381
33513
|
return extractTarToMap(stream, DOWNLOAD_IDLE_MS, onBytes);
|
|
33382
33514
|
}
|
|
33383
|
-
async function fetchOneOutcome(
|
|
33515
|
+
async function fetchOneOutcome(projectGuid2, path5, expectedSha) {
|
|
33384
33516
|
try {
|
|
33385
33517
|
const stream = await downloadStream(
|
|
33386
|
-
`/projects/${
|
|
33518
|
+
`/projects/${projectGuid2}/files/tree?content=tar&path=${encodeURIComponent(path5)}`
|
|
33387
33519
|
);
|
|
33388
33520
|
const want = normalizeTreePath(path5);
|
|
33389
33521
|
const files = await extractTarToMap(stream, DOWNLOAD_IDLE_MS, void 0, (p) => p === want);
|
|
@@ -33399,8 +33531,8 @@ async function fetchOneOutcome(projectGuid, path5, expectedSha) {
|
|
|
33399
33531
|
return { kind: "transient" };
|
|
33400
33532
|
}
|
|
33401
33533
|
}
|
|
33402
|
-
async function fetchOne(
|
|
33403
|
-
const out = await fetchOneOutcome(
|
|
33534
|
+
async function fetchOne(projectGuid2, path5, expectedSha) {
|
|
33535
|
+
const out = await fetchOneOutcome(projectGuid2, path5, expectedSha);
|
|
33404
33536
|
return out.kind === "ok" ? out.buf : null;
|
|
33405
33537
|
}
|
|
33406
33538
|
function classifyLocal(info2, base) {
|
|
@@ -33632,14 +33764,14 @@ async function sync(opts = {}) {
|
|
|
33632
33764
|
releaseLock();
|
|
33633
33765
|
}
|
|
33634
33766
|
}
|
|
33635
|
-
async function syncInner(
|
|
33636
|
-
const config = { projectGuid, ignore: ignore2 };
|
|
33767
|
+
async function syncInner(projectGuid2, root, ignore2, opts, interactive) {
|
|
33768
|
+
const config = { projectGuid: projectGuid2, ignore: ignore2 };
|
|
33637
33769
|
const p = opts.progress;
|
|
33638
|
-
const baseline = readBaseline(
|
|
33770
|
+
const baseline = readBaseline(projectGuid2);
|
|
33639
33771
|
p?.phase("Scanning local files...");
|
|
33640
33772
|
const local = walkLocal(root, ignore2, baseline.files);
|
|
33641
33773
|
p?.phase("Checking Gipity for changes...");
|
|
33642
|
-
const remote = await fetchRemote(
|
|
33774
|
+
const remote = await fetchRemote(projectGuid2);
|
|
33643
33775
|
for (const path5 of [...remote.keys()]) {
|
|
33644
33776
|
if (shouldIgnore(path5, ignore2)) remote.delete(path5);
|
|
33645
33777
|
}
|
|
@@ -34439,7 +34571,12 @@ function parseDuration(raw, unit) {
|
|
|
34439
34571
|
}
|
|
34440
34572
|
|
|
34441
34573
|
// src/commands/token.ts
|
|
34442
|
-
var tokenCommand = new Command("token").description("Manage API tokens").addHelpText("after",
|
|
34574
|
+
var tokenCommand = new Command("token").description("Manage API tokens").addHelpText("after", `
|
|
34575
|
+
Long-lived agent API tokens (gip_at_*) for headless agents and CI - they drive
|
|
34576
|
+
the gipity CLI as YOU.
|
|
34577
|
+
|
|
34578
|
+
To let a script or cron write to one app instead, mint a project API key:
|
|
34579
|
+
gipity key create "my script" --role editor (sent as X-Api-Key).`);
|
|
34443
34580
|
var fmtDate = (d) => d ? new Date(d).toLocaleDateString() : "never";
|
|
34444
34581
|
tokenCommand.command("create").description("Mint a long-lived agent API token (shown once)").option("--name <name>", 'Label for the token, e.g. "Hermes on my VPS"').option("--expires <days>", "Days until the token expires (default: never)").option("--json", "Output as JSON").action((opts) => run("Create", async () => {
|
|
34445
34582
|
const body = {};
|
|
@@ -34484,6 +34621,73 @@ tokenCommand.command("revoke <short_guid>").alias("rm").description("Revoke an a
|
|
|
34484
34621
|
console.log(success(`Revoked token ${bold(shortGuid)}.`));
|
|
34485
34622
|
}));
|
|
34486
34623
|
|
|
34624
|
+
// src/commands/key.ts
|
|
34625
|
+
init_api();
|
|
34626
|
+
init_config();
|
|
34627
|
+
init_colors();
|
|
34628
|
+
var keyCommand = new Command("key").description("Manage project API keys for scripts and agents (X-Api-Key)").addHelpText("after", `
|
|
34629
|
+
Give a script, cron, or agent write access to your app without a login:
|
|
34630
|
+
|
|
34631
|
+
gipity key create "laptop importer" --role editor
|
|
34632
|
+
# the script sends: X-Api-Key: <the key printed once above>
|
|
34633
|
+
|
|
34634
|
+
Inside a function the caller shows up as ctx.auth.via === 'api_key' with
|
|
34635
|
+
ctx.auth.apiKeyName set to the key's name - stamp that on rows to tell
|
|
34636
|
+
script-written entries from hand-entered ones. Never build your own key table.
|
|
34637
|
+
|
|
34638
|
+
Account-level tokens that run the CLI headlessly are a different thing: see
|
|
34639
|
+
gipity token --help.`);
|
|
34640
|
+
var fmtDate2 = (d) => d ? new Date(d).toLocaleDateString() : "never";
|
|
34641
|
+
var projectOpt = ["--project <guid-or-slug>", "Target a specific project instead of cwd / Home"];
|
|
34642
|
+
async function projectGuid(opts) {
|
|
34643
|
+
const { config } = await resolveProjectContext({ projectOverride: opts.project });
|
|
34644
|
+
return config.projectGuid;
|
|
34645
|
+
}
|
|
34646
|
+
keyCommand.command("create <name>").description("Mint a project API key (shown once). Give it a name you will recognize later").option("--role <role>", "viewer | editor | owner (default: viewer)", "viewer").option("--expires-days <n>", "Days until the key expires (default: never)").option(...projectOpt).option("--json", "Output as JSON").action((name, opts) => run("Create", async () => {
|
|
34647
|
+
const body = { name, role: opts.role };
|
|
34648
|
+
if (opts.expiresDays !== void 0) {
|
|
34649
|
+
const days = parseInt(opts.expiresDays, 10);
|
|
34650
|
+
if (!Number.isFinite(days) || days <= 0) throw new Error("--expires-days must be a positive number of days");
|
|
34651
|
+
body.expires_in_days = days;
|
|
34652
|
+
}
|
|
34653
|
+
const res = await post(
|
|
34654
|
+
`/projects/${await projectGuid(opts)}/api-keys`,
|
|
34655
|
+
body
|
|
34656
|
+
);
|
|
34657
|
+
const k7 = res.data;
|
|
34658
|
+
if (opts.json) {
|
|
34659
|
+
console.log(JSON.stringify(k7));
|
|
34660
|
+
return;
|
|
34661
|
+
}
|
|
34662
|
+
const expNote = k7.expires_at ? ` (expires ${fmtDate2(k7.expires_at)})` : " (never expires)";
|
|
34663
|
+
console.log(success(`Created API key ${bold(k7.short_guid)} "${k7.name}" as ${k7.role}${muted(expNote)}.`));
|
|
34664
|
+
console.log("");
|
|
34665
|
+
console.log(k7.key);
|
|
34666
|
+
console.log("");
|
|
34667
|
+
console.log(muted("Send it from your script on every request:"));
|
|
34668
|
+
console.log(muted(` curl -H "X-Api-Key: ${k7.key}" ...`));
|
|
34669
|
+
console.log(muted(`Revoke it any time: gipity key revoke ${k7.short_guid}`));
|
|
34670
|
+
console.log("");
|
|
34671
|
+
console.log(warning("Copy it now - it will not be shown again."));
|
|
34672
|
+
}));
|
|
34673
|
+
keyCommand.command("list").alias("ls").description("List this project's API keys (values are never shown again)").option(...projectOpt).option("--json", "Output as JSON").action((opts) => run("List", async () => {
|
|
34674
|
+
const res = await get(`/projects/${await projectGuid(opts)}/api-keys`);
|
|
34675
|
+
printList(
|
|
34676
|
+
res.data,
|
|
34677
|
+
opts,
|
|
34678
|
+
'No API keys. Mint one with: gipity key create "my script" --role editor',
|
|
34679
|
+
(k7) => `${bold(k7.short_guid)} ${k7.name} ${muted(`${k7.role} ${k7.prefix}\u2026 last used ${fmtDate2(k7.last_used_at)} expires ${fmtDate2(k7.expires_at)}`)}`
|
|
34680
|
+
);
|
|
34681
|
+
}));
|
|
34682
|
+
keyCommand.command("revoke <short_guid>").alias("rm").description("Revoke a project API key (instant, irreversible)").option(...projectOpt).option("--json", "Output as JSON").action((shortGuid, opts) => run("Revoke", async () => {
|
|
34683
|
+
await del(`/projects/${await projectGuid(opts)}/api-keys/${encodeURIComponent(shortGuid)}`);
|
|
34684
|
+
if (opts.json) {
|
|
34685
|
+
console.log(JSON.stringify({ short_guid: shortGuid, revoked: true }));
|
|
34686
|
+
return;
|
|
34687
|
+
}
|
|
34688
|
+
console.log(success(`Revoked API key ${bold(shortGuid)}.`));
|
|
34689
|
+
}));
|
|
34690
|
+
|
|
34487
34691
|
// src/commands/init.ts
|
|
34488
34692
|
init_api();
|
|
34489
34693
|
init_config();
|
|
@@ -34529,6 +34733,80 @@ function escapeHtml(s) {
|
|
|
34529
34733
|
function jsEscape(s) {
|
|
34530
34734
|
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
34531
34735
|
}
|
|
34736
|
+
var ACCENT_PALETTE = [
|
|
34737
|
+
"#f59e0b",
|
|
34738
|
+
"#f97316",
|
|
34739
|
+
"#ef4444",
|
|
34740
|
+
"#ec4899",
|
|
34741
|
+
"#a855f7",
|
|
34742
|
+
"#6366f1",
|
|
34743
|
+
"#3b82f6",
|
|
34744
|
+
"#0ea5e9",
|
|
34745
|
+
"#14b8a6",
|
|
34746
|
+
"#22c55e",
|
|
34747
|
+
"#84cc16",
|
|
34748
|
+
"#eab308"
|
|
34749
|
+
];
|
|
34750
|
+
function hashString(s) {
|
|
34751
|
+
let h = 2166136261;
|
|
34752
|
+
for (let i = 0; i < s.length; i++) {
|
|
34753
|
+
h ^= s.charCodeAt(i);
|
|
34754
|
+
h = Math.imul(h, 16777619);
|
|
34755
|
+
}
|
|
34756
|
+
return h >>> 0;
|
|
34757
|
+
}
|
|
34758
|
+
function darkTint(hex) {
|
|
34759
|
+
const n = parseInt(hex.slice(1), 16);
|
|
34760
|
+
const base = 1052692;
|
|
34761
|
+
const ch = (shift) => {
|
|
34762
|
+
const a = n >> shift & 255;
|
|
34763
|
+
const b7 = base >> shift & 255;
|
|
34764
|
+
return Math.round(b7 + (a - b7) * 0.14).toString(16).padStart(2, "0");
|
|
34765
|
+
};
|
|
34766
|
+
return `#${ch(16)}${ch(8)}${ch(0)}`;
|
|
34767
|
+
}
|
|
34768
|
+
function buildHeadBlock(v7) {
|
|
34769
|
+
const t = escapeHtml(v7.projectName);
|
|
34770
|
+
const d = v7.description ? escapeHtml(v7.description) : "";
|
|
34771
|
+
const url = v7.accountSlug && v7.projectSlug ? `https://app.gipity.ai/${v7.accountSlug}/${v7.projectSlug}/` : void 0;
|
|
34772
|
+
const themeColor = darkTint(ACCENT_PALETTE[hashString(v7.projectGuid || v7.projectName) % ACCENT_PALETTE.length]);
|
|
34773
|
+
const jsonLd = JSON.stringify({
|
|
34774
|
+
"@context": "https://schema.org",
|
|
34775
|
+
"@type": "WebApplication",
|
|
34776
|
+
name: v7.projectName,
|
|
34777
|
+
...v7.description ? { description: v7.description } : {},
|
|
34778
|
+
...url ? { url } : {}
|
|
34779
|
+
}, null, 2).replace(/<\//g, "<\\/");
|
|
34780
|
+
const lines = [
|
|
34781
|
+
`<title>${t}</title>`,
|
|
34782
|
+
...d ? [`<meta name="description" content="${d}">`] : [],
|
|
34783
|
+
...url ? [`<link rel="canonical" href="${url}">`] : [],
|
|
34784
|
+
`<meta property="og:title" content="${t}">`,
|
|
34785
|
+
`<meta property="og:type" content="website">`,
|
|
34786
|
+
...d ? [`<meta property="og:description" content="${d}">`] : [],
|
|
34787
|
+
...url ? [
|
|
34788
|
+
`<meta property="og:url" content="${url}">`,
|
|
34789
|
+
`<meta property="og:image" content="${url}images/og-image.png">`,
|
|
34790
|
+
`<meta property="og:image:width" content="1200">`,
|
|
34791
|
+
`<meta property="og:image:height" content="630">`
|
|
34792
|
+
] : [],
|
|
34793
|
+
`<meta name="twitter:card" content="summary_large_image">`,
|
|
34794
|
+
`<meta name="twitter:title" content="${t}">`,
|
|
34795
|
+
...d ? [`<meta name="twitter:description" content="${d}">`] : [],
|
|
34796
|
+
...url ? [`<meta name="twitter:image" content="${url}images/og-image.png">`] : [],
|
|
34797
|
+
`<meta name="theme-color" content="${themeColor}">`,
|
|
34798
|
+
`<link rel="icon" type="image/png" sizes="192x192" href="./images/favicon-192.png">`,
|
|
34799
|
+
`<link rel="icon" type="image/png" sizes="512x512" href="./images/favicon-512.png">`,
|
|
34800
|
+
`<link rel="icon" type="image/x-icon" href="./images/favicon.ico">`,
|
|
34801
|
+
`<link rel="apple-touch-icon" href="./images/apple-touch-icon.png">`,
|
|
34802
|
+
`<link rel="manifest" href="./manifest.webmanifest">`,
|
|
34803
|
+
`<script type="application/ld+json">
|
|
34804
|
+
${jsonLd}
|
|
34805
|
+
</script>`
|
|
34806
|
+
];
|
|
34807
|
+
return lines.map((l7) => `
|
|
34808
|
+
${l7}`).join("");
|
|
34809
|
+
}
|
|
34532
34810
|
function buildTemplateVars(v7) {
|
|
34533
34811
|
const safeTitle = escapeHtml(v7.projectName);
|
|
34534
34812
|
const safeDesc = v7.description ? escapeHtml(v7.description) : "";
|
|
@@ -34544,6 +34822,9 @@ function buildTemplateVars(v7) {
|
|
|
34544
34822
|
"{{JS_TITLE}}": jsEscape(v7.projectName),
|
|
34545
34823
|
"{{PROJECT_GUID}}": v7.projectGuid,
|
|
34546
34824
|
"{{DATABASE}}": slug,
|
|
34825
|
+
"{{HEAD_BLOCK}}": buildHeadBlock(v7),
|
|
34826
|
+
// Legacy per-tag placeholders — current templates carry only {{HEAD_BLOCK}},
|
|
34827
|
+
// but older local template copies may still reference these.
|
|
34547
34828
|
"{{DESCRIPTION_META}}": v7.description ? `
|
|
34548
34829
|
<meta name="description" content="${safeDesc}">` : "",
|
|
34549
34830
|
"{{OG_DESCRIPTION}}": v7.description ? `
|
|
@@ -34638,7 +34919,9 @@ async function finalizeLocalProject(opts) {
|
|
|
34638
34919
|
try {
|
|
34639
34920
|
const sub = await substituteDir(opts.dir, {
|
|
34640
34921
|
projectGuid: opts.projectGuid,
|
|
34641
|
-
projectName: opts.projectName
|
|
34922
|
+
projectName: opts.projectName,
|
|
34923
|
+
accountSlug: opts.accountSlug,
|
|
34924
|
+
projectSlug: opts.projectSlug
|
|
34642
34925
|
});
|
|
34643
34926
|
if (sub.changed.length) {
|
|
34644
34927
|
console.log(muted(`Resolved template vars in ${sub.changed.length} file${sub.changed.length > 1 ? "s" : ""}.`));
|
|
@@ -35024,7 +35307,7 @@ function resolveTools(forFlag) {
|
|
|
35024
35307
|
(t) => requested.includes(t.key) || !t.optIn && requested.includes("all")
|
|
35025
35308
|
);
|
|
35026
35309
|
}
|
|
35027
|
-
var initCommand = new Command("init").description("Link this directory to a project").addHelpText("after", "\nWrites CLAUDE.md/AGENTS.md primer files so your AI coding tool understands Gipity, and installs the Gipity skills + file-sync hooks into the agent CLIs found on this machine (Claude Code, Codex, Grok).").argument("[name]", "Project name/slug (defaults to current directory name)").option("--agent <guid>", "Agent GUID to use").option("--no-capture", "Don't record Claude Code sessions in this directory to your Gipity project (sets captureHooks: false in .gipity.json)").option(
|
|
35310
|
+
var initCommand = new Command("init").description("Link this directory to a project").addHelpText("after", "\nWrites CLAUDE.md/AGENTS.md primer files so your AI coding tool understands Gipity, and installs the Gipity skills + file-sync hooks into the agent CLIs found on this machine (Claude Code, Codex, Grok, Antigravity).").argument("[name]", "Project name/slug (defaults to current directory name)").option("--agent <guid>", "Agent GUID to use").option("--no-capture", "Don't record Claude Code sessions in this directory to your Gipity project (sets captureHooks: false in .gipity.json)").option(
|
|
35028
35311
|
"--for <tools>",
|
|
35029
35312
|
`Which AI tool primer files to write (comma-separated). Default: all except aider (opt-in - it also writes .aider.conf.yml). Choices: ${TOOL_KEYS.join(", ")}, all`
|
|
35030
35313
|
).addHelpText("after", `
|
|
@@ -35152,7 +35435,7 @@ Working with an existing Gipity project:
|
|
|
35152
35435
|
}
|
|
35153
35436
|
console.log(success(`Wrote primer files: ${primerSummary}.`));
|
|
35154
35437
|
if (wantsClaude) {
|
|
35155
|
-
console.log(success("Ready! Run your coding agent here (claude, codex, grok), or `gipity build` to launch one with a picker."));
|
|
35438
|
+
console.log(success("Ready! Run your coding agent here (claude, codex, grok, agy), or `gipity build` to launch one with a picker."));
|
|
35156
35439
|
if (opts.capture === false) {
|
|
35157
35440
|
console.log(muted("Session recording is off for this project (captureHooks: false in .gipity.json)."));
|
|
35158
35441
|
} else {
|
|
@@ -35448,6 +35731,30 @@ init_colors();
|
|
|
35448
35731
|
init_auth();
|
|
35449
35732
|
init_config();
|
|
35450
35733
|
|
|
35734
|
+
// src/db-checkpoint.ts
|
|
35735
|
+
init_api();
|
|
35736
|
+
async function resolveDatabase(projectGuid2, explicit) {
|
|
35737
|
+
if (explicit) return explicit;
|
|
35738
|
+
const res = await get(`/projects/${projectGuid2}/databases`);
|
|
35739
|
+
return res.data.length > 0 ? res.data[0].friendlyName : null;
|
|
35740
|
+
}
|
|
35741
|
+
async function checkpoint(projectGuid2, database, action, keep) {
|
|
35742
|
+
const res = await post(
|
|
35743
|
+
`/projects/${projectGuid2}/db/checkpoint`,
|
|
35744
|
+
{ database, action, ...keep ? { keep } : {} }
|
|
35745
|
+
);
|
|
35746
|
+
return res.data;
|
|
35747
|
+
}
|
|
35748
|
+
function createCheckpoint(projectGuid2, database) {
|
|
35749
|
+
return checkpoint(projectGuid2, database, "create");
|
|
35750
|
+
}
|
|
35751
|
+
function restoreCheckpoint(projectGuid2, database, opts = {}) {
|
|
35752
|
+
return checkpoint(projectGuid2, database, "restore", opts.keep);
|
|
35753
|
+
}
|
|
35754
|
+
function dropCheckpoint(projectGuid2, database) {
|
|
35755
|
+
return checkpoint(projectGuid2, database, "drop");
|
|
35756
|
+
}
|
|
35757
|
+
|
|
35451
35758
|
// src/page-fixtures.ts
|
|
35452
35759
|
init_api();
|
|
35453
35760
|
init_config();
|
|
@@ -35490,13 +35797,13 @@ Nothing named "${basename4(localPath)}" under ${root} either. Generate a frame w
|
|
|
35490
35797
|
`${flag} ${localPath}: no such file \u2014 looked for ${abs} (relative paths resolve against the current directory, ${process.cwd()}).${found}`
|
|
35491
35798
|
);
|
|
35492
35799
|
}
|
|
35493
|
-
async function uploadPublicFixture(
|
|
35800
|
+
async function uploadPublicFixture(projectGuid2, localPath, flag = "--fixture") {
|
|
35494
35801
|
assertLocalAsset(flag, localPath);
|
|
35495
35802
|
const name = basename4(localPath);
|
|
35496
35803
|
const size = statSync6(localPath).size;
|
|
35497
35804
|
const contentType = guessMime(localPath);
|
|
35498
35805
|
const init = await post(
|
|
35499
|
-
`/api/${
|
|
35806
|
+
`/api/${projectGuid2}/uploads/init`,
|
|
35500
35807
|
{ filename: name, content_type: contentType, size, public: true }
|
|
35501
35808
|
);
|
|
35502
35809
|
if (init.data.method !== "PUT" || !init.data.url) {
|
|
@@ -35511,13 +35818,13 @@ async function uploadPublicFixture(projectGuid, localPath, flag = "--fixture") {
|
|
|
35511
35818
|
throw new Error(`upload of fixture "${name}" failed: ${res.status} ${res.statusText}`);
|
|
35512
35819
|
}
|
|
35513
35820
|
const done = await post(
|
|
35514
|
-
`/api/${
|
|
35821
|
+
`/api/${projectGuid2}/uploads/complete`,
|
|
35515
35822
|
{ upload_guid: init.data.upload_guid }
|
|
35516
35823
|
);
|
|
35517
35824
|
return { guid: done.data.guid, url: done.data.url, name, localPath };
|
|
35518
35825
|
}
|
|
35519
|
-
async function deleteFixture(
|
|
35520
|
-
await del(`/api/${
|
|
35826
|
+
async function deleteFixture(projectGuid2, guid) {
|
|
35827
|
+
await del(`/api/${projectGuid2}/uploads/${guid}`);
|
|
35521
35828
|
}
|
|
35522
35829
|
var CAMERA_EXTS = [".png", ".jpg", ".jpeg", ".webp", ".mp4", ".webm", ".y4m", ".mjpeg"];
|
|
35523
35830
|
function assertCameraFile(localPath) {
|
|
@@ -35530,9 +35837,9 @@ A still image is played as a looping single-frame feed, which is what a gesture/
|
|
|
35530
35837
|
No frame to hand? Generate one: gipity generate image "a hand making a closed fist, palm to camera, plain background"`
|
|
35531
35838
|
);
|
|
35532
35839
|
}
|
|
35533
|
-
async function uploadCameraFeed(
|
|
35840
|
+
async function uploadCameraFeed(projectGuid2, localPath) {
|
|
35534
35841
|
assertCameraFile(localPath);
|
|
35535
|
-
return uploadPublicFixture(
|
|
35842
|
+
return uploadPublicFixture(projectGuid2, localPath, "--camera");
|
|
35536
35843
|
}
|
|
35537
35844
|
|
|
35538
35845
|
// src/commands/page-eval.ts
|
|
@@ -35655,7 +35962,15 @@ function budgetOverrunHint(reason, usedBudgetMs) {
|
|
|
35655
35962
|
}
|
|
35656
35963
|
return `That budget was ${Math.round(usedBudgetMs / 1e3)}s. Raise it with --timeout <ms> (up to ${EVAL_SCRIPT_BUDGET_MAX_MS}), e.g. --timeout ${EVAL_SCRIPT_BUDGET_MAX_MS} \u2014 or gate on a ready signal instead: --wait-for '<selector>' --wait-timeout ${MAX_WAIT_MS}.`;
|
|
35657
35964
|
}
|
|
35965
|
+
function navigationAbortHint(reason) {
|
|
35966
|
+
if (!/navigated or reloaded/i.test(reason)) return null;
|
|
35967
|
+
return `If you were verifying that state SURVIVES a reload, that is what --reload is for (one command, one page): gipity page eval "<url>" '<seed/assert expr>' --reload '<assert-restored expr>'. It reloads the page in place (localStorage/sessionStorage/cookies preserved) and reports the second result separately. Splitting into two 'page eval' calls does NOT do this: each call is its own browser session, so the second one starts from empty storage and never exercises the restore path.`;
|
|
35968
|
+
}
|
|
35969
|
+
function stepsDeterministically(script) {
|
|
35970
|
+
return /\badvance\s*\(/.test(script);
|
|
35971
|
+
}
|
|
35658
35972
|
function slowRenderMessage(fps, o) {
|
|
35973
|
+
if (!o.camera && stepsDeterministically(o.script ?? "")) return null;
|
|
35659
35974
|
if (o.camera) {
|
|
35660
35975
|
const frames = Math.max(1, Math.round(fps * (o.waitMs / 1e3)));
|
|
35661
35976
|
return `${warning("\u26A0 Slow render:")} page painted at ${fps} fps, so the app's vision pipeline ran on roughly ${bold(`${frames} frame${frames === 1 ? "" : "s"}`)} during the ${Math.round(o.waitMs / 1e3)}s before this eval (it infers once per painted frame). ${bold("That is enough:")} --camera loops your still image, so every one of those frames is the SAME pixels and the model returns the SAME answer on each \u2014 re-running with a bigger --wait/--timeout cannot change the result. ${bold("Do not escalate the wait.")} If a detection landed, it is real. If nothing was detected, the suspect is the ${bold("frame")} (a model needs the whole subject in shot \u2014 a tight crop, an odd angle or a busy background reads as nothing) or the ${bold("app")} (frames never reach the model) \u2014 never the frame rate. Settle it in ONE run: ${CAMERA_FRAME_CHECK}`;
|
|
@@ -35713,14 +36028,14 @@ var pageEvalCommand = new Command("eval").description("Evaluate JS in a real bro
|
|
|
35713
36028
|
[]
|
|
35714
36029
|
).option(
|
|
35715
36030
|
"--reload <expr>",
|
|
35716
|
-
|
|
36031
|
+
'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.'
|
|
35717
36032
|
).option("--reload-file <path>", "Read the post-reload expression from a file instead of inline --reload (mutually exclusive)").option(
|
|
35718
36033
|
"--camera <path>",
|
|
35719
36034
|
`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 \u2192 MediaPipe/YOLOX \u2192 your app logic) runs headlessly on a frame you choose. Implies --fake-media and waits ${CAMERA_DEFAULT_WAIT_MS / 1e3}s for the vision model to load. No frame handy? gipity generate image "a hand making a closed fist, palm to camera".`
|
|
35720
36035
|
).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) \u2014 nothing a vision model can recognize; to drive a vision app use --camera <path> instead.").option("--wait <ms>", "Sleep this many ms after DOMContentLoaded before evaluating (lets late async work settle; max 30000). With --wait-for, it elapses AFTER the selector appears - gate, then settle.", "500").option("--wait-for <selector>", `Wait until this CSS selector appears before evaluating, then evaluate (deterministic - beats guessing a --wait). Gate on the state you are ASSERTING, not just a ready flag: '#verdict:not(:empty)' returns the instant the round lands, where a fixed wait snapshots mid-sequence. Same flag on page inspect/screenshot. Max ${WAIT_FOR_MAX_MS}ms (--wait-timeout).`).option("--wait-timeout <ms>", `Max ms to wait for --wait-for before giving up (max ${WAIT_FOR_MAX_MS})`, "5000").option(
|
|
35721
36036
|
"--timeout <ms>",
|
|
35722
36037
|
`How long the script itself may run IN the page. Bare number = MILLISECONDS (so 90s = 90000, NOT 90); or pass an explicit unit that means the same on both this and \`sandbox run --timeout\` \u2014 --timeout 90s. Its own await/setTimeout pauses count (default ${EVAL_SCRIPT_BUDGET_MS}, ${EVAL_SCRIPT_BUDGET_CAMERA_MS} with --camera/--fake-media; floor 1000, max ${EVAL_SCRIPT_BUDGET_MAX_MS}). Raise it to trace a sequence that unfolds over time (a game round, an animation). Distinct from --wait, which only sleeps BEFORE the script.`
|
|
35723
|
-
).option("--auth", "Evaluate signed in as you (your Gipity account), so a page behind a Sign-in-with-Gipity login is reachable. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai. Without this flag the page loads as a genuinely anonymous, signed-out visitor \u2014 nothing carries over from earlier --auth runs.").option("--json", "Output as JSON").action((url, exprArg, opts) => run("Page eval", async () => {
|
|
36038
|
+
).option("--auth", "Evaluate signed in as you (your Gipity account), so a page behind a Sign-in-with-Gipity login is reachable. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai. Without this flag the page loads as a genuinely anonymous, signed-out visitor \u2014 nothing carries over from earlier --auth runs.").option("--restore-db", "Snapshot the app database before the script runs and roll it back after, so a write-path check (click Approve, submit the form, edit a row) leaves the real data untouched. Use this whenever the eval WRITES - it is the undo for driving the deployed app against real project data. Same snapshot/undo standalone: gipity db checkpoint / gipity db restore.").option("--json", "Output as JSON").action((url, exprArg, opts) => run("Page eval", async () => {
|
|
35724
36039
|
const decoy = JS_DECOY_FLAGS.find((f) => opts[f.slice(2)] !== void 0);
|
|
35725
36040
|
if (decoy) {
|
|
35726
36041
|
pageEvalCommand.error(
|
|
@@ -35797,23 +36112,36 @@ var pageEvalCommand = new Command("eval").description("Evaluate JS in a real bro
|
|
|
35797
36112
|
const fixturePaths = opts.fixture ?? [];
|
|
35798
36113
|
const hosted = [];
|
|
35799
36114
|
let camera;
|
|
35800
|
-
let
|
|
36115
|
+
let projectGuid2;
|
|
35801
36116
|
let sentExpr = expr;
|
|
35802
36117
|
let sentSteps = steps;
|
|
35803
36118
|
if (opts.camera) assertCameraFile(opts.camera);
|
|
36119
|
+
let restoreDb;
|
|
35804
36120
|
try {
|
|
36121
|
+
if (opts.restoreDb) {
|
|
36122
|
+
const { config } = await resolveProjectContext({});
|
|
36123
|
+
projectGuid2 = config.projectGuid;
|
|
36124
|
+
const database = await resolveDatabase(config.projectGuid);
|
|
36125
|
+
if (!database) {
|
|
36126
|
+
console.error(muted("--restore-db: this project has no database - nothing to roll back."));
|
|
36127
|
+
} else {
|
|
36128
|
+
const cp2 = await createCheckpoint(config.projectGuid, database);
|
|
36129
|
+
console.error(muted(`Checkpointed ${cp2.tables.length} table(s), ${cp2.rows} row(s) in '${database}' - rolled back when this eval finishes.`));
|
|
36130
|
+
restoreDb = { projectGuid: config.projectGuid, database };
|
|
36131
|
+
}
|
|
36132
|
+
}
|
|
35805
36133
|
if (fixturePaths.length || opts.camera) {
|
|
35806
36134
|
const { config } = await resolveProjectContext({});
|
|
35807
|
-
|
|
36135
|
+
projectGuid2 = config.projectGuid;
|
|
35808
36136
|
}
|
|
35809
36137
|
if (opts.camera) {
|
|
35810
36138
|
console.log(muted(`Hosting camera feed ${opts.camera}...`));
|
|
35811
|
-
camera = await uploadCameraFeed(
|
|
36139
|
+
camera = await uploadCameraFeed(projectGuid2, opts.camera);
|
|
35812
36140
|
}
|
|
35813
36141
|
if (fixturePaths.length) {
|
|
35814
36142
|
for (const p of fixturePaths) {
|
|
35815
36143
|
console.log(muted(`Hosting fixture ${p}...`));
|
|
35816
|
-
hosted.push(await uploadPublicFixture(
|
|
36144
|
+
hosted.push(await uploadPublicFixture(projectGuid2, p));
|
|
35817
36145
|
}
|
|
35818
36146
|
const map = {};
|
|
35819
36147
|
for (const h of hosted) map[h.name] = h.url;
|
|
@@ -35851,7 +36179,7 @@ var pageEvalCommand = new Command("eval").description("Evaluate JS in a real bro
|
|
|
35851
36179
|
}));
|
|
35852
36180
|
} catch (err) {
|
|
35853
36181
|
const msg = err?.message ?? "";
|
|
35854
|
-
const hint = budgetOverrunHint(msg, scriptBudgetMs);
|
|
36182
|
+
const hint = budgetOverrunHint(msg, scriptBudgetMs) ?? (reloadExpr === void 0 ? navigationAbortHint(msg) : null);
|
|
35855
36183
|
if (!hint) throw err;
|
|
35856
36184
|
throw new Error(`${msg}
|
|
35857
36185
|
${hint}`);
|
|
@@ -35883,7 +36211,12 @@ ${hint}`);
|
|
|
35883
36211
|
console.log(`${warning("\u26A0 Navigation incomplete:")} ${d.note || "page did not reach full load"}`);
|
|
35884
36212
|
}
|
|
35885
36213
|
if (d.slowRender) {
|
|
35886
|
-
|
|
36214
|
+
const slow = slowRenderMessage(d.slowRender.fps, {
|
|
36215
|
+
camera: !!camera,
|
|
36216
|
+
waitMs,
|
|
36217
|
+
script: [expr, ...steps, reloadExpr ?? ""].join("\n")
|
|
36218
|
+
});
|
|
36219
|
+
if (slow) console.log(slow);
|
|
35887
36220
|
}
|
|
35888
36221
|
if (d.auth?.requested) {
|
|
35889
36222
|
const who = getAuth()?.email;
|
|
@@ -35916,9 +36249,17 @@ ${bold("After reload")} ${muted("(page reloaded in place \u2014 storage preserve
|
|
|
35916
36249
|
if (d.reloadTruncated) console.log(muted("(reload result truncated to fit context - narrow the expression for the full value)"));
|
|
35917
36250
|
}
|
|
35918
36251
|
} finally {
|
|
36252
|
+
if (restoreDb) {
|
|
36253
|
+
try {
|
|
36254
|
+
const r = await restoreCheckpoint(restoreDb.projectGuid, restoreDb.database);
|
|
36255
|
+
console.error(muted(`Rolled ${r.tables.length} table(s) back to the checkpoint (${r.rows} row(s)) - nothing this eval wrote survives.`));
|
|
36256
|
+
} catch (err) {
|
|
36257
|
+
console.error(warning(`\u26A0 Could not roll back the database - the checkpoint is still there, retry with: gipity db restore (${err.message})`));
|
|
36258
|
+
}
|
|
36259
|
+
}
|
|
35919
36260
|
for (const h of [...hosted, ...camera ? [camera] : []]) {
|
|
35920
36261
|
try {
|
|
35921
|
-
await deleteFixture(
|
|
36262
|
+
await deleteFixture(projectGuid2, h.guid);
|
|
35922
36263
|
} catch (err) {
|
|
35923
36264
|
console.error(warning(`\u26A0 Could not auto-delete fixture "${h.name}" (${h.guid}) \u2014 still hosted at ${h.url}: ${err.message}`));
|
|
35924
36265
|
}
|
|
@@ -36012,7 +36353,7 @@ init_auth();
|
|
|
36012
36353
|
init_colors();
|
|
36013
36354
|
init_utils();
|
|
36014
36355
|
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
|
|
36015
|
-
import { dirname as dirname7, join as join12, resolve as resolvePath } from "path";
|
|
36356
|
+
import { dirname as dirname7, extname as extname4, join as join12, resolve as resolvePath } from "path";
|
|
36016
36357
|
var DEVICE_PRESETS = {
|
|
36017
36358
|
default: { width: 1280, height: 720 },
|
|
36018
36359
|
desktop: { width: 1920, height: 1080 },
|
|
@@ -36123,6 +36464,14 @@ function resolveDevice(name) {
|
|
|
36123
36464
|
const available = [...Object.keys(DEVICE_PRESETS), ...TOUCH_DEVICES].join(", ");
|
|
36124
36465
|
throw new Error(`Unknown --device preset: "${name}" (known: ${available})`);
|
|
36125
36466
|
}
|
|
36467
|
+
function viewportSlug(name) {
|
|
36468
|
+
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "shot";
|
|
36469
|
+
}
|
|
36470
|
+
function stemPath(output, suffix) {
|
|
36471
|
+
const ext = extname4(output) || ".png";
|
|
36472
|
+
const base = ext && output.endsWith(ext) ? output.slice(0, -ext.length) : output;
|
|
36473
|
+
return `${base}-${suffix}${ext}`;
|
|
36474
|
+
}
|
|
36126
36475
|
function splitCsv(values) {
|
|
36127
36476
|
if (!values || values.length === 0) return [];
|
|
36128
36477
|
return values.flatMap((v7) => v7.split(",").map((s) => s.trim()).filter(Boolean));
|
|
@@ -36139,7 +36488,7 @@ var WAIT_FOR_MAX_MS2 = 15e3;
|
|
|
36139
36488
|
var WAIT_FOR_DEFAULT_MS = 15e3;
|
|
36140
36489
|
var MAX_POST_LOAD_DELAY_MS = 3e4;
|
|
36141
36490
|
var CAMERA_DEFAULT_DELAY_MS = 15e3;
|
|
36142
|
-
var pageScreenshotCommand = new Command("screenshot").description("Screenshot a web page").argument("<url>", "URL to screenshot").option("--device <names>", `Device preset(s): ${Object.keys(DEVICE_PRESETS).join(", ")} (comma-separated or repeat flag). mobile/tablet emulate a real touch device \u2014 touch events, mobile user-agent, DPR \u2014 so touch-gated mobile UI actually renders.`, appendOption, []).option("--viewport <dims>", "Raw viewport(s): WxH or WxH@dpr (comma-separated or repeat flag), e.g. --viewport 390x844 for a phone-sized window", appendOption, []).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.`).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_MS2}ms (--wait-timeout).`).option("--wait-timeout <ms>", `Max ms to wait for --wait-for before giving up (default ${WAIT_FOR_DEFAULT_MS}, max ${WAIT_FOR_MAX_MS2}). Timing out is reported - the capture still happens, so you see the state it got stuck in.`).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.`).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.").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.").option("-o, --output <file>", "Output path (
|
|
36491
|
+
var pageScreenshotCommand = new Command("screenshot").description("Screenshot a web page").argument("<url>", "URL to screenshot").option("--device <names>", `Device preset(s): ${Object.keys(DEVICE_PRESETS).join(", ")} (comma-separated or repeat flag). mobile/tablet emulate a real touch device \u2014 touch events, mobile user-agent, DPR \u2014 so touch-gated mobile UI actually renders.`, appendOption, []).option("--viewport <dims>", "Raw viewport(s): WxH or WxH@dpr (comma-separated or repeat flag), e.g. --viewport 390x844 for a phone-sized window", appendOption, []).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.`).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_MS2}ms (--wait-timeout).`).option("--wait-timeout <ms>", `Max ms to wait for --wait-for before giving up (default ${WAIT_FOR_DEFAULT_MS}, max ${WAIT_FOR_MAX_MS2}). Timing out is reported - the capture still happens, so you see the state it got stuck in.`).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.`).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.").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.").option("-o, --output <file>", "Output path (default .gipity/screenshots/ss-<host>-<timestamp>.png). With several viewports it is a stem: -o tmp/shot.png --device desktop,mobile writes tmp/shot-desktop.png and tmp/shot-mobile.png.").option("--no-reload-between", "Skip reload between viewports (faster, lower fidelity - only safe for static pages)").option("--fake-media", "Grant a synthetic microphone + camera and auto-accept the getUserMedia prompt, so voice/camera apps render headlessly. The video feed is a built-in test pattern \u2014 to capture what the app does with a REAL frame (a hand, a face, an object), use --camera <path> instead.").option("--camera <path>", "Play a local image or video (.png/.jpg/.webp/.mp4/.webm/.y4m/.mjpeg) as the browser's WEBCAM feed, then capture \u2014 so the shot shows the app reacting to a frame you chose (detected gesture, boxes, labels). Implies --fake-media.").option("--auth", "Capture the page signed in as you (your Gipity account), so UI behind a Sign-in-with-Gipity login is shown. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai. Without this flag the page loads as a genuinely anonymous, signed-out visitor \u2014 nothing carries over from earlier --auth runs.").option("--ephemeral", "Skip the project screenshot history: do not persist this capture to Gipity (screenshots/ in the project). Local file is still written.").option("--json", "Output JSON metadata instead of a friendly summary").addOption(new Option("--post-load-delay <ms>", "Alias for --wait").hideHelp()).addOption(new Option("--full-page", "Alias for --full").hideHelp()).addOption(new Option("--width <px>", "Alias: --viewport WxH").hideHelp()).addOption(new Option("--height <px>", "Alias: --viewport WxH").hideHelp()).action((url, opts) => run("Page screenshot", async () => {
|
|
36143
36492
|
const aliasFlag = ACTION_ALIAS_FLAGS.find((f) => opts[f.slice(2)] !== void 0);
|
|
36144
36493
|
if (aliasFlag) {
|
|
36145
36494
|
console.error(muted(
|
|
@@ -36209,22 +36558,23 @@ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a
|
|
|
36209
36558
|
...deviceNames.map(resolveDevice),
|
|
36210
36559
|
...viewportStrs.map(parseViewportString)
|
|
36211
36560
|
];
|
|
36212
|
-
|
|
36213
|
-
|
|
36214
|
-
|
|
36561
|
+
const viewportLabels = [
|
|
36562
|
+
...deviceNames.map(viewportSlug),
|
|
36563
|
+
...viewportStrs.map(viewportSlug)
|
|
36564
|
+
];
|
|
36215
36565
|
const userSpecifiedViewports = customViewports.length > 0;
|
|
36216
36566
|
const slug = slugFromUrl(url);
|
|
36217
36567
|
const ts2 = timestampSlug();
|
|
36218
36568
|
const shotName = (vp2) => defaultFilename(slug, ts2, vp2 && userSpecifiedViewports ? dimSuffix(vp2) : void 0);
|
|
36219
36569
|
const names = userSpecifiedViewports ? customViewports.map(shotName) : [shotName()];
|
|
36220
|
-
const
|
|
36221
|
-
const save = !opts.ephemeral &&
|
|
36570
|
+
const projectGuid2 = getConfig()?.projectGuid;
|
|
36571
|
+
const save = !opts.ephemeral && projectGuid2 ? { project_guid: projectGuid2, names } : void 0;
|
|
36222
36572
|
let camera;
|
|
36223
36573
|
if (opts.camera) {
|
|
36224
36574
|
assertCameraFile(opts.camera);
|
|
36225
|
-
if (!
|
|
36575
|
+
if (!projectGuid2) throw new Error("--camera needs a linked project (the frame is hosted for the browser to fetch) \u2014 run `gipity link` first.");
|
|
36226
36576
|
console.log(muted(`Hosting camera feed ${opts.camera}...`));
|
|
36227
|
-
camera = await uploadCameraFeed(
|
|
36577
|
+
camera = await uploadCameraFeed(projectGuid2, opts.camera);
|
|
36228
36578
|
}
|
|
36229
36579
|
const preCapture = [
|
|
36230
36580
|
opts.waitFor ? buildWaitForGate(opts.waitFor, waitForTimeoutMs) : "",
|
|
@@ -36255,7 +36605,7 @@ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a
|
|
|
36255
36605
|
} finally {
|
|
36256
36606
|
if (camera) {
|
|
36257
36607
|
try {
|
|
36258
|
-
await deleteFixture(
|
|
36608
|
+
await deleteFixture(projectGuid2, camera.guid);
|
|
36259
36609
|
} catch (err) {
|
|
36260
36610
|
console.error(warning(`\u26A0 Could not auto-delete camera feed "${camera.name}" (${camera.guid}) \u2014 still hosted at ${camera.url}: ${err.message}`));
|
|
36261
36611
|
}
|
|
@@ -36270,8 +36620,16 @@ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a
|
|
|
36270
36620
|
}
|
|
36271
36621
|
const dir = defaultScreenshotDir();
|
|
36272
36622
|
const savedFiles = [];
|
|
36623
|
+
const usedStems = /* @__PURE__ */ new Set();
|
|
36624
|
+
const outputPath = (i) => {
|
|
36625
|
+
if (pngs.length === 1) return opts.output;
|
|
36626
|
+
let suffix = viewportLabels[i] ?? dimSuffix(meta.screenshots[i].viewport);
|
|
36627
|
+
while (usedStems.has(suffix)) suffix += `-${i + 1}`;
|
|
36628
|
+
usedStems.add(suffix);
|
|
36629
|
+
return stemPath(opts.output, suffix);
|
|
36630
|
+
};
|
|
36273
36631
|
for (let i = 0; i < pngs.length; i++) {
|
|
36274
|
-
const target = opts.output ?
|
|
36632
|
+
const target = opts.output ? outputPath(i) : join12(dir, names[i] ?? shotName(meta.screenshots[i].viewport));
|
|
36275
36633
|
mkdirSync7(dirname7(target), { recursive: true });
|
|
36276
36634
|
writeFileSync8(target, pngs[i].buffer);
|
|
36277
36635
|
savedFiles.push(resolvePath(target));
|
|
@@ -36757,10 +37115,10 @@ dbCommand.command("list").description("List databases").option("--all", "List da
|
|
|
36757
37115
|
grouped.get(key).push(db2);
|
|
36758
37116
|
}
|
|
36759
37117
|
let first = true;
|
|
36760
|
-
for (const [
|
|
37118
|
+
for (const [projectGuid2, dbs] of grouped) {
|
|
36761
37119
|
if (!first) console.log("");
|
|
36762
37120
|
first = false;
|
|
36763
|
-
const label2 = dbs[0].projectSlug || dbs[0].projectName ||
|
|
37121
|
+
const label2 = dbs[0].projectSlug || dbs[0].projectName || projectGuid2;
|
|
36764
37122
|
console.log(label2);
|
|
36765
37123
|
for (const db2 of dbs) {
|
|
36766
37124
|
console.log(`${db2.friendlyName}`);
|
|
@@ -36816,6 +37174,47 @@ dbCommand.command("drop <name>").description("Drop a database").option("--projec
|
|
|
36816
37174
|
console.log(success(`Dropped database '${name}'.`));
|
|
36817
37175
|
}
|
|
36818
37176
|
}));
|
|
37177
|
+
dbCommand.command("checkpoint").description("Snapshot every table so a write test can be undone. Take one before exercising a real write path (page eval on the deployed app, fn call, workflow run), then `gipity db restore` to put the data back exactly as it was - no hand-written SQL to scrub test strings out of real content.").option("--database <name>", "Database name").option("--drop", "Discard the existing checkpoint without restoring (keep whatever the run wrote)").option("--json", "Output as JSON").action((opts) => run("Checkpoint", async () => {
|
|
37178
|
+
const config = requireConfig();
|
|
37179
|
+
const dbName = await resolveDatabase(config.projectGuid, opts.database);
|
|
37180
|
+
if (!dbName) {
|
|
37181
|
+
console.error(error("No databases in this project - nothing to checkpoint."));
|
|
37182
|
+
process.exit(1);
|
|
37183
|
+
}
|
|
37184
|
+
if (opts.drop) {
|
|
37185
|
+
const dropped = await dropCheckpoint(config.projectGuid, dbName);
|
|
37186
|
+
if (opts.json) {
|
|
37187
|
+
console.log(JSON.stringify(dropped));
|
|
37188
|
+
} else {
|
|
37189
|
+
console.log(dropped.tables.length === 0 ? `No checkpoint in '${dbName}'.` : success(`Discarded the checkpoint of ${dropped.tables.length} table(s) in '${dbName}'. Current data kept.`));
|
|
37190
|
+
}
|
|
37191
|
+
return;
|
|
37192
|
+
}
|
|
37193
|
+
const res = await createCheckpoint(config.projectGuid, dbName);
|
|
37194
|
+
if (opts.json) {
|
|
37195
|
+
console.log(JSON.stringify(res));
|
|
37196
|
+
} else if (res.tables.length === 0) {
|
|
37197
|
+
console.log(`Database '${dbName}' has no tables - nothing to checkpoint.`);
|
|
37198
|
+
} else {
|
|
37199
|
+
console.log(success(`Checkpointed ${res.tables.length} table(s), ${res.rows} row(s) in '${dbName}'.`));
|
|
37200
|
+
console.log("Undo everything written since: gipity db restore");
|
|
37201
|
+
}
|
|
37202
|
+
}));
|
|
37203
|
+
dbCommand.command("restore").description("Roll every table back to the last `gipity db checkpoint` and drop the checkpoint. The undo for a live write test.").option("--database <name>", "Database name").option("--keep", "Keep the checkpoint after restoring, so you can run the same write test again").option("--json", "Output as JSON").action((opts) => run("Restore", async () => {
|
|
37204
|
+
const config = requireConfig();
|
|
37205
|
+
const dbName = await resolveDatabase(config.projectGuid, opts.database);
|
|
37206
|
+
if (!dbName) {
|
|
37207
|
+
console.error(error("No databases in this project - nothing to restore."));
|
|
37208
|
+
process.exit(1);
|
|
37209
|
+
}
|
|
37210
|
+
const res = await restoreCheckpoint(config.projectGuid, dbName, { keep: opts.keep });
|
|
37211
|
+
if (opts.json) {
|
|
37212
|
+
console.log(JSON.stringify(res));
|
|
37213
|
+
} else {
|
|
37214
|
+
console.log(success(`Restored ${res.tables.length} table(s) to the checkpoint (${res.rows} row(s)) in '${dbName}'.`));
|
|
37215
|
+
if (opts.keep) console.log("Checkpoint kept - restore again any time.");
|
|
37216
|
+
}
|
|
37217
|
+
}));
|
|
36819
37218
|
|
|
36820
37219
|
// src/commands/memory.ts
|
|
36821
37220
|
init_api();
|
|
@@ -36870,7 +37269,7 @@ memoryCommand.command("delete <topic>").description("Delete a topic").option("--
|
|
|
36870
37269
|
init_api();
|
|
36871
37270
|
init_config();
|
|
36872
37271
|
import { readFileSync as readFileSync15, existsSync as existsSync11, statSync as statSync7, mkdirSync as mkdirSync8, writeFileSync as writeFileSync9 } from "fs";
|
|
36873
|
-
import { dirname as dirname8, extname as
|
|
37272
|
+
import { dirname as dirname8, extname as extname5, relative as relative4, resolve as resolve11, sep as sep3 } from "path";
|
|
36874
37273
|
init_colors();
|
|
36875
37274
|
var LANG_MAP = {
|
|
36876
37275
|
js: "javascript",
|
|
@@ -36934,7 +37333,7 @@ function resolveLanguage(opts) {
|
|
|
36934
37333
|
process.exit(1);
|
|
36935
37334
|
}
|
|
36936
37335
|
if (explicit) return explicit;
|
|
36937
|
-
const fromExt = opts.filePath ? LANG_MAP[
|
|
37336
|
+
const fromExt = opts.filePath ? LANG_MAP[extname5(opts.filePath).slice(1).toLowerCase()] : void 0;
|
|
36938
37337
|
if (fromExt) return fromExt;
|
|
36939
37338
|
if (opts.filePath) {
|
|
36940
37339
|
console.error(error(`Cannot infer the language of ${opts.filePath} from its extension (expected .js, .py, or .sh).`));
|
|
@@ -37645,6 +38044,33 @@ function formatRunLine(r) {
|
|
|
37645
38044
|
const statusColor = r.status === "completed" ? success : r.status === "failed" ? error : muted;
|
|
37646
38045
|
return `${muted(r.short_guid)} ${statusColor(r.status)} ${dur} ${runTokens(r)} tokens ${muted(fmtTime(r.started_at))}`;
|
|
37647
38046
|
}
|
|
38047
|
+
function jsonStringInfo(v7) {
|
|
38048
|
+
if (typeof v7 !== "string") return null;
|
|
38049
|
+
const t = v7.trim();
|
|
38050
|
+
if (!t.startsWith("{") && !t.startsWith("[")) return null;
|
|
38051
|
+
try {
|
|
38052
|
+
const parsed = JSON.parse(t);
|
|
38053
|
+
return typeof parsed === "object" && parsed !== null ? { ok: true, value: parsed } : null;
|
|
38054
|
+
} catch {
|
|
38055
|
+
return { ok: false };
|
|
38056
|
+
}
|
|
38057
|
+
}
|
|
38058
|
+
function labelFor(info2) {
|
|
38059
|
+
return info2.ok ? "(JSON string)" : error("(truncated/invalid JSON string)");
|
|
38060
|
+
}
|
|
38061
|
+
function renderStepOutput(output) {
|
|
38062
|
+
if (output && typeof output === "object" && !Array.isArray(output)) {
|
|
38063
|
+
return Object.entries(output).map(([k7, v7]) => {
|
|
38064
|
+
const info3 = jsonStringInfo(v7);
|
|
38065
|
+
if (!info3) return `${JSON.stringify(k7)}: ${JSON.stringify(v7, null, 2)}`;
|
|
38066
|
+
const body = info3.ok ? JSON.stringify(info3.value, null, 2) : JSON.stringify(v7);
|
|
38067
|
+
return `${JSON.stringify(k7)} ${labelFor(info3)}: ${body}`;
|
|
38068
|
+
}).join("\n");
|
|
38069
|
+
}
|
|
38070
|
+
const info2 = jsonStringInfo(output);
|
|
38071
|
+
if (info2?.ok) return JSON.stringify(info2.value, null, 2);
|
|
38072
|
+
return JSON.stringify(output, null, 2);
|
|
38073
|
+
}
|
|
37648
38074
|
function printStepRuns(steps, emptyNote) {
|
|
37649
38075
|
if (steps.length === 0) {
|
|
37650
38076
|
console.log(` ${muted(emptyNote)}`);
|
|
@@ -37657,7 +38083,7 @@ function printStepRuns(steps, emptyNote) {
|
|
|
37657
38083
|
console.log(` ${s.step_order}. ${name}${statusColor(s.status)} ${s.tokens_used ?? 0} tokens${model}`);
|
|
37658
38084
|
if (s.error_message) console.log(` ${error(s.error_message)}`);
|
|
37659
38085
|
if (s.output_json !== null && s.output_json !== void 0) {
|
|
37660
|
-
console.log(
|
|
38086
|
+
console.log(renderStepOutput(s.output_json).split("\n").map((l7) => ` ${l7}`).join("\n"));
|
|
37661
38087
|
}
|
|
37662
38088
|
}
|
|
37663
38089
|
}
|
|
@@ -37728,7 +38154,7 @@ workflowCommand.command("info <name>").description("Show workflow details").opti
|
|
|
37728
38154
|
}
|
|
37729
38155
|
}
|
|
37730
38156
|
}));
|
|
37731
|
-
workflowCommand.command("run <name>").description("
|
|
38157
|
+
workflowCommand.command("run <name>").description("Run a workflow: waits for it to finish and prints each step's status and output (--no-wait to fire and forget)").option("--json", "Output as JSON").option("--no-wait", "Trigger and return immediately instead of waiting for the run").addOption(new Option("--wait", "Wait for the run (already the default)").hideHelp()).option("--timeout <s>", "Max seconds to wait for the run to finish", "300").action((name, _opts, cmd) => run("Run", async () => {
|
|
37732
38158
|
const opts = mergedOpts(cmd);
|
|
37733
38159
|
const wf2 = await resolveWorkflow(name);
|
|
37734
38160
|
if (!opts.wait) {
|
|
@@ -37739,7 +38165,7 @@ workflowCommand.command("run <name>").description("Trigger a workflow (add --wai
|
|
|
37739
38165
|
const before = await get(`/workflows/${wf2.short_guid}/runs?limit=1`);
|
|
37740
38166
|
const prevGuid = before.data[0]?.short_guid;
|
|
37741
38167
|
await post(`/workflows/${wf2.short_guid}/run`, {});
|
|
37742
|
-
const r = await waitForRun(wf2.short_guid, prevGuid, Number(opts.timeout) ||
|
|
38168
|
+
const r = await waitForRun(wf2.short_guid, prevGuid, Number(opts.timeout) || 300);
|
|
37743
38169
|
if (opts.json) {
|
|
37744
38170
|
console.log(JSON.stringify(r));
|
|
37745
38171
|
} else {
|
|
@@ -38751,7 +39177,7 @@ async function runRelaySetup(opts) {
|
|
|
38751
39177
|
}
|
|
38752
39178
|
console.log("");
|
|
38753
39179
|
const promptText = opts.mode === "run-now" ? " Set up remote control on this computer" : " Enable remote control";
|
|
38754
|
-
const enable = await confirm(promptText, { default: "yes" });
|
|
39180
|
+
const enable = await confirm(promptText, { default: "yes", headless: "no" });
|
|
38755
39181
|
if (!enable) {
|
|
38756
39182
|
setRelayEnabled(false);
|
|
38757
39183
|
console.log(` ${muted("Skipped. Turn on later with")} ${brand("gipity connect")}${muted(".")}`);
|
|
@@ -38778,7 +39204,7 @@ async function runRelaySetup(opts) {
|
|
|
38778
39204
|
}
|
|
38779
39205
|
startDaemon();
|
|
38780
39206
|
console.log("");
|
|
38781
|
-
const autostartOs = await confirm(" Also start at OS login", { default: "yes" });
|
|
39207
|
+
const autostartOs = await confirm(" Also start at OS login", { default: "yes", headless: "no" });
|
|
38782
39208
|
if (autostartOs) {
|
|
38783
39209
|
try {
|
|
38784
39210
|
const res = installAutostart();
|
|
@@ -38801,7 +39227,7 @@ async function runRelaySetup(opts) {
|
|
|
38801
39227
|
}
|
|
38802
39228
|
if (getDiagnosticsConsent() === void 0) {
|
|
38803
39229
|
console.log("");
|
|
38804
|
-
const diag = await confirm(" Share anonymous diagnostics info", { default: "yes" });
|
|
39230
|
+
const diag = await confirm(" Share anonymous diagnostics info", { default: "yes", headless: "no" });
|
|
38805
39231
|
setDiagnosticsConsent(diag);
|
|
38806
39232
|
}
|
|
38807
39233
|
console.log("");
|
|
@@ -39240,11 +39666,53 @@ var grokAdapter = {
|
|
|
39240
39666
|
installHint: "curl -fsSL https://grok.x.ai/install.sh | sh (or see docs.x.ai/grok-build)"
|
|
39241
39667
|
};
|
|
39242
39668
|
|
|
39669
|
+
// src/agents/agy.ts
|
|
39670
|
+
var MODEL_ID_TO_AGY_DISPLAY = {
|
|
39671
|
+
"gemini-3.5-flash": "Gemini 3.5 Flash (Medium)",
|
|
39672
|
+
"gemini-3.1-pro-preview": "Gemini 3.1 Pro (High)"
|
|
39673
|
+
};
|
|
39674
|
+
function translateModel(model) {
|
|
39675
|
+
return MODEL_ID_TO_AGY_DISPLAY[model] ?? model;
|
|
39676
|
+
}
|
|
39677
|
+
function projectArgs(resume) {
|
|
39678
|
+
return resume ? ["--conversation", resume] : ["--new-project"];
|
|
39679
|
+
}
|
|
39680
|
+
var agyAdapter = {
|
|
39681
|
+
key: "agy",
|
|
39682
|
+
source: "agy",
|
|
39683
|
+
displayName: "Antigravity",
|
|
39684
|
+
providerName: "Google",
|
|
39685
|
+
binary: "agy",
|
|
39686
|
+
buildInteractiveArgs({ resume, model }) {
|
|
39687
|
+
const args = [...projectArgs(resume)];
|
|
39688
|
+
if (model) args.push("--model", translateModel(model));
|
|
39689
|
+
return args;
|
|
39690
|
+
},
|
|
39691
|
+
buildHeadlessArgs({ message, resume, model, bypassApprovals }) {
|
|
39692
|
+
const args = ["-p", message, ...projectArgs(resume)];
|
|
39693
|
+
if (model) args.push("--model", translateModel(model));
|
|
39694
|
+
if (bypassApprovals) args.push("--dangerously-skip-permissions");
|
|
39695
|
+
return args;
|
|
39696
|
+
},
|
|
39697
|
+
sessionIdFromStreamEvent() {
|
|
39698
|
+
return null;
|
|
39699
|
+
},
|
|
39700
|
+
hooksSupportedOnPlatform(platform2) {
|
|
39701
|
+
return platform2 !== "win32";
|
|
39702
|
+
},
|
|
39703
|
+
// No stream-json ingest mapper for agy: hook capture carries the full
|
|
39704
|
+
// conversationId on every event (confirmed live, including headless -p),
|
|
39705
|
+
// so the daemon doesn't need to parse agy's stdout at all.
|
|
39706
|
+
daemonStreamCapture: false,
|
|
39707
|
+
installHint: "see https://antigravity.google (Google account sign-in required)"
|
|
39708
|
+
};
|
|
39709
|
+
|
|
39243
39710
|
// src/agents/index.ts
|
|
39244
39711
|
var AGENT_ADAPTERS = [
|
|
39245
39712
|
claudeCodeAdapter,
|
|
39246
39713
|
codexAdapter,
|
|
39247
|
-
grokAdapter
|
|
39714
|
+
grokAdapter,
|
|
39715
|
+
agyAdapter
|
|
39248
39716
|
];
|
|
39249
39717
|
var AGENT_KEYS = AGENT_ADAPTERS.map((a) => a.key);
|
|
39250
39718
|
function getAdapter(key) {
|
|
@@ -39316,10 +39784,10 @@ function reportSyncResult(result) {
|
|
|
39316
39784
|
console.log(` ${muted("Nothing was deleted. Re-run `gipity sync` to finish the pull.")}`);
|
|
39317
39785
|
}
|
|
39318
39786
|
}
|
|
39319
|
-
async function fetchProjectStats(
|
|
39320
|
-
if (
|
|
39787
|
+
async function fetchProjectStats(projectGuid2, cwd) {
|
|
39788
|
+
if (projectGuid2) {
|
|
39321
39789
|
try {
|
|
39322
|
-
const res = await get(`/projects/${encodeURIComponent(
|
|
39790
|
+
const res = await get(`/projects/${encodeURIComponent(projectGuid2)}/files/stats`);
|
|
39323
39791
|
const d = res.data;
|
|
39324
39792
|
const topLevelNames = d.top_level.map((e) => e.type === "directory" ? `${e.name}/` : e.name);
|
|
39325
39793
|
return {
|
|
@@ -39913,7 +40381,7 @@ async function pickAgent(lastUsed) {
|
|
|
39913
40381
|
AGENT_ADAPTERS.forEach((a, i) => {
|
|
39914
40382
|
const notes = [];
|
|
39915
40383
|
if (a.key === lastUsed) notes.push("last used");
|
|
39916
|
-
if (a.key === "codex" && !a.hooksSupportedOnPlatform(process.platform)) {
|
|
40384
|
+
if ((a.key === "codex" || a.key === "agy") && !a.hooksSupportedOnPlatform(process.platform)) {
|
|
39917
40385
|
notes.push("session recording unavailable on Windows");
|
|
39918
40386
|
}
|
|
39919
40387
|
const note = notes.length ? ` ${muted(`(${notes.join(", ")})`)}` : "";
|
|
@@ -40286,9 +40754,9 @@ var STARTERS = [
|
|
|
40286
40754
|
];
|
|
40287
40755
|
var BLANK = [
|
|
40288
40756
|
{ key: "web-simple", hint: "static frontend-only site - pages, dashboards, simple games" },
|
|
40289
|
-
{ key: "web-fullstack", hint: "backend API + database wiring - frontend, functions, migrations; deploys green" },
|
|
40757
|
+
{ key: "web-fullstack", hint: "backend API + database wiring - frontend, functions, migrations; deploys green empty" },
|
|
40290
40758
|
{ key: "3d-engine", hint: "3D multiplayer wiring - Three.js + Rapier + Gipity Realtime" },
|
|
40291
|
-
{ key: "api", hint: "pure API backend, no frontend -
|
|
40759
|
+
{ key: "api", hint: "pure API backend, no frontend - functions + tests, deploys green" }
|
|
40292
40760
|
];
|
|
40293
40761
|
var KITS = [
|
|
40294
40762
|
{ key: "realtime", hint: "multiplayer / presence / shared state" },
|
|
@@ -40490,6 +40958,87 @@ Pulled ${syncResult.applied} files to local.`);
|
|
|
40490
40958
|
}
|
|
40491
40959
|
}));
|
|
40492
40960
|
|
|
40961
|
+
// src/commands/brand.ts
|
|
40962
|
+
init_api();
|
|
40963
|
+
init_config();
|
|
40964
|
+
init_colors();
|
|
40965
|
+
function printBrand(d) {
|
|
40966
|
+
console.log(`${bold("Glyph:")} ${d.resolved.glyph}${d.resolved.isEmoji ? muted(" (emoji)") : ""}`);
|
|
40967
|
+
console.log(`${bold("Accent:")} ${d.resolved.accent}${d.branding.primary_color ? "" : muted(" (derived from project)")}`);
|
|
40968
|
+
if (d.branding.app_name) console.log(`${bold("Name:")} ${d.branding.app_name}`);
|
|
40969
|
+
if (d.branding.tagline) console.log(`${bold("Tagline:")} ${d.branding.tagline}`);
|
|
40970
|
+
}
|
|
40971
|
+
var DEPLOY_HINT = "Deploy to publish the new assets: gipity deploy dev";
|
|
40972
|
+
var brandCommand = new Command("brand").description("The app's generated icons + share card (favicons, iOS icon, og-image)").addHelpText("after", `
|
|
40973
|
+
Examples:
|
|
40974
|
+
gipity brand Show the current brand
|
|
40975
|
+
gipity brand set --emoji \u{1F98D} Emoji icon + share card, regenerate all assets
|
|
40976
|
+
gipity brand set --glyph R --color "#3b82f6" Letter glyph on a blue accent
|
|
40977
|
+
gipity brand set --tagline "Bananas at 60fps" Subtitle on the share card
|
|
40978
|
+
gipity brand apply Re-render assets from the stored brand
|
|
40979
|
+
gipity brand apply --fix-head Migrate an older app: assets + splice the
|
|
40980
|
+
share/SEO tags into src/index.html
|
|
40981
|
+
|
|
40982
|
+
Assets are generated server-side into src/images/ (+ src/manifest.webmanifest)
|
|
40983
|
+
and are versioned like any other file - rollback restores previous ones.`);
|
|
40984
|
+
brandCommand.command("show", { isDefault: true }).description("Show the stored brand and the resolved render spec").option("--project <guid-or-slug>", "Target a specific project instead of cwd / Home").option("--json", "Output raw JSON").action((opts) => run("Brand", async () => {
|
|
40985
|
+
const { config } = await resolveProjectContext({ projectOverride: opts.project });
|
|
40986
|
+
const res = await get(`/projects/${config.projectGuid}/brand`);
|
|
40987
|
+
if (opts.json) {
|
|
40988
|
+
console.log(JSON.stringify(res.data));
|
|
40989
|
+
return;
|
|
40990
|
+
}
|
|
40991
|
+
printBrand(res.data);
|
|
40992
|
+
console.log(muted(`
|
|
40993
|
+
Assets: ${(res.data.assets ?? []).join(", ")}`));
|
|
40994
|
+
console.log(muted(`Change it: gipity brand set --emoji \u{1F3A8} | --glyph A | --color "#RRGGBB" | --tagline "..."`));
|
|
40995
|
+
}));
|
|
40996
|
+
brandCommand.command("set").description("Change brand fields and regenerate all assets").option("--emoji <emoji>", "Emoji glyph for the icon + share card (e.g. \u{1F98D})").option("--glyph <char>", "Letter/digit glyph (alias of --emoji, any single character)").option("--color <hex>", "Accent color, #RRGGBB").option("--name <name>", "Display name override (og/share title stays the page title)").option("--tagline <text>", "One-line subtitle on the share card (max 120 chars)").option("--fix-head", "Also splice the share/SEO head block into src/index.html (for apps installed before templates shipped it)").option("--no-regenerate", "Store the fields without re-rendering assets").option("--project <guid-or-slug>", "Target a specific project instead of cwd / Home").option("--json", "Output raw JSON").action((opts) => run("Brand", async () => {
|
|
40997
|
+
const { config } = await resolveProjectContext({ projectOverride: opts.project });
|
|
40998
|
+
const body = {};
|
|
40999
|
+
const glyph = opts.emoji ?? opts.glyph;
|
|
41000
|
+
if (glyph) body.icon_glyph = glyph;
|
|
41001
|
+
if (opts.color) body.primary_color = opts.color;
|
|
41002
|
+
if (opts.name) body.app_name = opts.name;
|
|
41003
|
+
if (opts.tagline) body.tagline = opts.tagline;
|
|
41004
|
+
if (opts.regenerate === false) body.regenerate = false;
|
|
41005
|
+
if (opts.fixHead) body.fix_head = true;
|
|
41006
|
+
if (!glyph && !opts.color && !opts.name && !opts.tagline) {
|
|
41007
|
+
console.error("Nothing to set - pass --emoji, --glyph, --color, --name, or --tagline. (gipity brand set --help; use gipity brand apply to just re-render)");
|
|
41008
|
+
process.exitCode = 1;
|
|
41009
|
+
return;
|
|
41010
|
+
}
|
|
41011
|
+
const res = await post(`/projects/${config.projectGuid}/brand`, body);
|
|
41012
|
+
if (opts.json) {
|
|
41013
|
+
console.log(JSON.stringify(res.data));
|
|
41014
|
+
return;
|
|
41015
|
+
}
|
|
41016
|
+
printBrand(res.data);
|
|
41017
|
+
reportFiles(res.data.files ?? []);
|
|
41018
|
+
}));
|
|
41019
|
+
brandCommand.command("apply").description("Re-render all brand assets from the stored brand (no field changes)").option("--fix-head", "Also splice the share/SEO head block into src/index.html (for apps installed before templates shipped it)").option("--project <guid-or-slug>", "Target a specific project instead of cwd / Home").option("--json", "Output raw JSON").action((opts) => run("Brand", async () => {
|
|
41020
|
+
const { config } = await resolveProjectContext({ projectOverride: opts.project });
|
|
41021
|
+
const body = opts.fixHead ? { fix_head: true } : {};
|
|
41022
|
+
const res = await post(`/projects/${config.projectGuid}/brand`, body);
|
|
41023
|
+
if (opts.json) {
|
|
41024
|
+
console.log(JSON.stringify(res.data));
|
|
41025
|
+
return;
|
|
41026
|
+
}
|
|
41027
|
+
reportFiles(res.data.files ?? []);
|
|
41028
|
+
}));
|
|
41029
|
+
function reportFiles(files) {
|
|
41030
|
+
if (!files.length) {
|
|
41031
|
+
console.log(muted("\nStored (assets not regenerated - run gipity brand apply)."));
|
|
41032
|
+
return;
|
|
41033
|
+
}
|
|
41034
|
+
console.log(success(`
|
|
41035
|
+
\u2713 Regenerated ${files.length} asset${files.length === 1 ? "" : "s"}.`));
|
|
41036
|
+
if (files.includes("src/index.html")) {
|
|
41037
|
+
console.log(success("\u2713 src/index.html head updated (page title/description preserved)."));
|
|
41038
|
+
}
|
|
41039
|
+
console.log(muted(` ${DEPLOY_HINT}`));
|
|
41040
|
+
}
|
|
41041
|
+
|
|
40493
41042
|
// src/commands/remove.ts
|
|
40494
41043
|
init_api();
|
|
40495
41044
|
init_config();
|
|
@@ -40984,7 +41533,7 @@ async function runInteractive(url, observe, opts) {
|
|
|
40984
41533
|
const samples = Math.min(30, Math.max(2, parseInt(opts.samples, 10) || 6));
|
|
40985
41534
|
const settle = opts.waitFor ? 200 : 1e3;
|
|
40986
41535
|
const labels = (opts.labels ? String(opts.labels).split(",").map((s) => s.trim()) : []).filter(Boolean);
|
|
40987
|
-
const
|
|
41536
|
+
const labelFor2 = (i) => labels[i] ?? `client-${i}`;
|
|
40988
41537
|
warnUnknownTokens(unknownTokens(url, opts.action, observe));
|
|
40989
41538
|
if (!opts.json) {
|
|
40990
41539
|
console.log(`${brand("Page test")} ${muted("(interactive)")} ${bold(url)}`);
|
|
@@ -40994,16 +41543,16 @@ async function runInteractive(url, observe, opts) {
|
|
|
40994
41543
|
for (let i = 0; i < clients; i++) {
|
|
40995
41544
|
runs.push((async () => {
|
|
40996
41545
|
await sleep3(i * stagger * 1e3);
|
|
40997
|
-
if (!opts.json) console.log(muted(`client ${i} (${
|
|
40998
|
-
const clientUrl = subst(url,
|
|
41546
|
+
if (!opts.json) console.log(muted(`client ${i} (${labelFor2(i)}) joining`));
|
|
41547
|
+
const clientUrl = subst(url, labelFor2(i), i);
|
|
40999
41548
|
const expr = buildHarness(
|
|
41000
|
-
opts.action ? subst(opts.action,
|
|
41001
|
-
subst(observe,
|
|
41002
|
-
|
|
41549
|
+
opts.action ? subst(opts.action, labelFor2(i), i) : void 0,
|
|
41550
|
+
subst(observe, labelFor2(i), i),
|
|
41551
|
+
labelFor2(i),
|
|
41003
41552
|
hold,
|
|
41004
41553
|
samples
|
|
41005
41554
|
);
|
|
41006
|
-
return observeClient(clientUrl, expr, i,
|
|
41555
|
+
return observeClient(clientUrl, expr, i, labelFor2(i), settle, hold, opts.waitFor);
|
|
41007
41556
|
})());
|
|
41008
41557
|
}
|
|
41009
41558
|
const results = (await Promise.all(runs)).sort((a, b7) => a.i - b7.i);
|
|
@@ -41057,7 +41606,7 @@ async function runPassive(url, opts) {
|
|
|
41057
41606
|
const stagger = opts.stagger != null ? Math.max(0, parseInt(opts.stagger, 10) || 0) : 12;
|
|
41058
41607
|
const wait = Math.min(3e4, Math.max(2e3, parseInt(opts.wait, 10) || 24e3));
|
|
41059
41608
|
const labels = (opts.labels ? String(opts.labels).split(",").map((s) => s.trim()) : []).filter(Boolean);
|
|
41060
|
-
const
|
|
41609
|
+
const labelFor2 = (i) => labels[i] ?? `client-${i}`;
|
|
41061
41610
|
warnUnknownTokens(unknownTokens(url));
|
|
41062
41611
|
if (!opts.json) {
|
|
41063
41612
|
console.log(`${brand("Page test")} ${bold(url)}`);
|
|
@@ -41068,7 +41617,7 @@ async function runPassive(url, opts) {
|
|
|
41068
41617
|
runs.push((async () => {
|
|
41069
41618
|
await sleep3(i * stagger * 1e3);
|
|
41070
41619
|
if (!opts.json) console.log(`${muted(`client ${i}${i === 0 ? " (first)" : ""} starting`)}`);
|
|
41071
|
-
return inspectClient(subst(url,
|
|
41620
|
+
return inspectClient(subst(url, labelFor2(i), i), wait, i);
|
|
41072
41621
|
})());
|
|
41073
41622
|
}
|
|
41074
41623
|
const results = (await Promise.all(runs)).sort((a, b7) => a.i - b7.i);
|
|
@@ -41262,10 +41811,31 @@ pageCommand.action(() => {
|
|
|
41262
41811
|
|
|
41263
41812
|
// src/commands/records.ts
|
|
41264
41813
|
init_api();
|
|
41814
|
+
init_auth();
|
|
41265
41815
|
init_config();
|
|
41266
41816
|
init_colors();
|
|
41267
41817
|
init_utils();
|
|
41268
41818
|
var recordsCommand = new Command("records").description("Manage app records (Gipity Records - validated CRUD with audit history)");
|
|
41819
|
+
var ANON_FLAG = "--anon";
|
|
41820
|
+
var ANON_HELP = "Call as an anonymous visitor (the public path a signed-out user hits) instead of as your signed-in account";
|
|
41821
|
+
async function recordsHttp(opts) {
|
|
41822
|
+
const config = requireConfig();
|
|
41823
|
+
if (!opts.anon) {
|
|
41824
|
+
const who = getAuth()?.email;
|
|
41825
|
+
console.error(muted(`Auth: calling as ${who ?? "your signed-in account"} (the owner persona; use ${ANON_FLAG} for the public visitor path)`));
|
|
41826
|
+
return { guid: config.projectGuid, get, post, put, patch, del };
|
|
41827
|
+
}
|
|
41828
|
+
console.error(muted("Auth: anonymous visitor (the public path a signed-out user hits)"));
|
|
41829
|
+
const headers = await mintAppToken(config.projectGuid);
|
|
41830
|
+
return {
|
|
41831
|
+
guid: config.projectGuid,
|
|
41832
|
+
get: (path5) => publicRequest("GET", path5, void 0, headers),
|
|
41833
|
+
post: (path5, body) => publicRequest("POST", path5, body, headers),
|
|
41834
|
+
put: (path5, body) => publicRequest("PUT", path5, body, headers),
|
|
41835
|
+
patch: (path5, body) => publicRequest("PATCH", path5, body, headers),
|
|
41836
|
+
del: (path5, body) => publicRequest("DELETE", path5, body, headers)
|
|
41837
|
+
};
|
|
41838
|
+
}
|
|
41269
41839
|
recordsCommand.command("list").description("List record tables").option("--json", "Output as JSON").action((opts) => run("List", async () => {
|
|
41270
41840
|
const config = requireConfig();
|
|
41271
41841
|
const res = await get(`/api/${config.projectGuid}/records-config`);
|
|
@@ -41297,16 +41867,20 @@ recordsCommand.command("config <table>").description("Show or set a table's Reco
|
|
|
41297
41867
|
res.data
|
|
41298
41868
|
);
|
|
41299
41869
|
}));
|
|
41300
|
-
recordsCommand.command("query <table>").description("List records").option("--filter <filter>", '
|
|
41301
|
-
const
|
|
41870
|
+
recordsCommand.command("query <table>").description("List records (full-text search, filter, sort, recycle bin)").option("-q, --q <text>", "Full-text search across the table's text columns (needs `--searchable true` on the table)").option("--filter <filter>", 'AND filter string (e.g., "status:eq:active")').option("--any <filter>", 'OR filter group, same grammar as --filter (e.g., "owner:eq:me,shared:eq:true")').option("--sort <sort>", 'Sort string (e.g., "created_at:desc")').option("--limit <n>", "Max rows", "20").option("--offset <n>", "Offset", "0").option("--fields <fields>", "Comma-separated column names").option("--include-deleted", "Include soft-deleted rows (owner/editor)").option("--only-deleted", "Only soft-deleted rows - the recycle bin (owner/editor)").option(ANON_FLAG, ANON_HELP).option("--json", "Output as JSON").action((table, opts) => run("Query", async () => {
|
|
41871
|
+
const api = await recordsHttp(opts);
|
|
41302
41872
|
const params = new URLSearchParams();
|
|
41873
|
+
if (opts.q) params.set("q", opts.q);
|
|
41303
41874
|
if (opts.filter) params.set("filter", opts.filter);
|
|
41875
|
+
if (opts.any) params.set("any", opts.any);
|
|
41304
41876
|
if (opts.sort) params.set("sort", opts.sort);
|
|
41305
41877
|
params.set("limit", opts.limit);
|
|
41306
41878
|
params.set("offset", opts.offset);
|
|
41307
41879
|
if (opts.fields) params.set("fields", opts.fields);
|
|
41308
|
-
|
|
41309
|
-
|
|
41880
|
+
if (opts.includeDeleted) params.set("include_deleted", "1");
|
|
41881
|
+
if (opts.onlyDeleted) params.set("only_deleted", "1");
|
|
41882
|
+
const res = await api.get(
|
|
41883
|
+
`/api/${api.guid}/records/${table}?${params}`
|
|
41310
41884
|
);
|
|
41311
41885
|
if (opts.json) {
|
|
41312
41886
|
console.log(JSON.stringify(res));
|
|
@@ -41319,41 +41893,55 @@ recordsCommand.command("query <table>").description("List records").option("--fi
|
|
|
41319
41893
|
console.log("");
|
|
41320
41894
|
}
|
|
41321
41895
|
}));
|
|
41322
|
-
recordsCommand.command("get <table> <id>").description("Get a record").option("--json", "Output as JSON").action((table, id2, opts) => run("Get", async () => {
|
|
41323
|
-
const
|
|
41324
|
-
const res = await get(`/api/${
|
|
41896
|
+
recordsCommand.command("get <table> <id>").description("Get a record").option(ANON_FLAG, ANON_HELP).option("--json", "Output as JSON").action((table, id2, opts) => run("Get", async () => {
|
|
41897
|
+
const api = await recordsHttp(opts);
|
|
41898
|
+
const res = await api.get(`/api/${api.guid}/records/${table}/${id2}`);
|
|
41325
41899
|
console.log(opts.json ? JSON.stringify(res.data) : JSON.stringify(res.data, null, 2));
|
|
41326
41900
|
}));
|
|
41327
|
-
recordsCommand.command("history <table>
|
|
41328
|
-
const
|
|
41329
|
-
const
|
|
41330
|
-
|
|
41901
|
+
recordsCommand.command("history <table> [id]").description("Audit history (who/what changed it, with English summaries). Omit <id> for the whole table's feed.").option("--limit <n>", "Max events", "20").option(ANON_FLAG, ANON_HELP).option("--json", "Output as JSON").action((table, id2, opts) => run("History", async () => {
|
|
41902
|
+
const api = await recordsHttp(opts);
|
|
41903
|
+
const scope = id2 ? `${table}/${encodeURIComponent(id2)}` : table;
|
|
41904
|
+
const res = await api.get(
|
|
41905
|
+
`/api/${api.guid}/records/${scope}/history?limit=${encodeURIComponent(opts.limit)}`
|
|
41331
41906
|
);
|
|
41332
|
-
printList(res.data, opts, "No history for this record."
|
|
41907
|
+
printList(res.data, opts, id2 ? "No history for this record." : `No history for "${table}".`, (e) => {
|
|
41333
41908
|
const summary = e.detail?.summary || `${e.action} ${e.entity_type} ${e.entity_id}`;
|
|
41334
41909
|
return `${muted(e.created_at)} ${bold(e.source || "-")} ${summary}`;
|
|
41335
41910
|
});
|
|
41336
41911
|
}));
|
|
41337
|
-
recordsCommand.command("create <table>").description("Create a record").requiredOption("--data <json>", "JSON object with field values").option("--json", "Output as JSON").action((table, opts) => run("Create", async () => {
|
|
41338
|
-
const
|
|
41912
|
+
recordsCommand.command("create <table>").description("Create a record").requiredOption("--data <json>", "JSON object with field values").option(ANON_FLAG, ANON_HELP).option("--json", "Output as JSON").action((table, opts) => run("Create", async () => {
|
|
41913
|
+
const api = await recordsHttp(opts);
|
|
41339
41914
|
const data = JSON.parse(opts.data);
|
|
41340
|
-
const res = await post(`/api/${
|
|
41915
|
+
const res = await api.post(`/api/${api.guid}/records/${table}`, data);
|
|
41341
41916
|
printResult(`Created: ${JSON.stringify(res.data)}`, opts, res.data);
|
|
41342
41917
|
}));
|
|
41343
|
-
recordsCommand.command("update <table> <id>").description("Update a record").requiredOption("--data <json>", "JSON object with fields to update").option("--json", "Output as JSON").action((table, id2, opts) => run("Update", async () => {
|
|
41344
|
-
const
|
|
41918
|
+
recordsCommand.command("update <table> <id>").description("Update a record").requiredOption("--data <json>", "JSON object with fields to update").option(ANON_FLAG, ANON_HELP).option("--json", "Output as JSON").action((table, id2, opts) => run("Update", async () => {
|
|
41919
|
+
const api = await recordsHttp(opts);
|
|
41345
41920
|
const data = JSON.parse(opts.data);
|
|
41346
|
-
const res = await put(`/api/${
|
|
41921
|
+
const res = await api.put(`/api/${api.guid}/records/${table}/${id2}`, data);
|
|
41347
41922
|
printResult(`Updated: ${JSON.stringify(res.data)}`, opts, res.data);
|
|
41348
41923
|
}));
|
|
41349
|
-
recordsCommand.command("delete <table> <id>").description("Delete a record").action((table, id2) => run("Delete", async () => {
|
|
41350
|
-
|
|
41351
|
-
|
|
41924
|
+
recordsCommand.command("delete <table> <id>").description("Delete a record (soft-delete when the table declares one; --purge removes it for good)").option("--purge", "Hard-delete: remove the row AND erase its audit history (owner/editor). Use it to scrub probe rows.").option(ANON_FLAG, ANON_HELP).option("--json", "Output as JSON").action((table, id2, opts) => run("Delete", async () => {
|
|
41925
|
+
const what = opts.purge ? `Purge record ${id2} from "${table}" (also erases its history)?` : `Delete record ${id2} from "${table}"?`;
|
|
41926
|
+
if (!await confirm(what)) {
|
|
41927
|
+
printResult("Cancelled.", opts, { table, id: id2, deleted: false, cancelled: true });
|
|
41352
41928
|
return;
|
|
41353
41929
|
}
|
|
41354
|
-
const
|
|
41355
|
-
await del(
|
|
41356
|
-
|
|
41930
|
+
const api = await recordsHttp(opts);
|
|
41931
|
+
const res = await api.del(
|
|
41932
|
+
`/api/${api.guid}/records/${table}/${id2}${opts.purge ? "?purge=1" : ""}`
|
|
41933
|
+
);
|
|
41934
|
+
const data = res?.data ?? { table, id: id2, deleted: true, purged: !!opts.purge };
|
|
41935
|
+
printResult(
|
|
41936
|
+
data.purged ? "Purged - the row and its audit history are gone." : `Deleted. If "${table}" declares a soft-delete column the row is only stamped deleted (see it with \`gipity records query ${table} --only-deleted\`, bring it back with \`gipity records restore ${table} ${id2}\`, remove it for good with \`--purge\`).`,
|
|
41937
|
+
opts,
|
|
41938
|
+
data
|
|
41939
|
+
);
|
|
41940
|
+
}));
|
|
41941
|
+
recordsCommand.command("restore <table> <id>").description("Un-delete a soft-deleted record (owner/editor)").option("--json", "Output as JSON").action((table, id2, opts) => run("Restore", async () => {
|
|
41942
|
+
const api = await recordsHttp(opts);
|
|
41943
|
+
const res = await api.post(`/api/${api.guid}/records/${table}/${id2}/restore`);
|
|
41944
|
+
printResult("Restored.", opts, res?.data ?? { table, id: id2, restored: true });
|
|
41357
41945
|
}));
|
|
41358
41946
|
|
|
41359
41947
|
// src/commands/fn.ts
|
|
@@ -41398,17 +41986,11 @@ ${error(`error: ${log3.error_message}`)}`;
|
|
|
41398
41986
|
return line;
|
|
41399
41987
|
});
|
|
41400
41988
|
}));
|
|
41401
|
-
async function callAnon(
|
|
41402
|
-
let appToken;
|
|
41403
|
-
try {
|
|
41404
|
-
const minted = await publicPost("/api/token", { app: projectGuid });
|
|
41405
|
-
appToken = minted.data.token;
|
|
41406
|
-
} catch {
|
|
41407
|
-
}
|
|
41989
|
+
async function callAnon(projectGuid2, name, body) {
|
|
41408
41990
|
return publicPost(
|
|
41409
|
-
`/api/${
|
|
41991
|
+
`/api/${projectGuid2}/fn/${encodeURIComponent(name)}`,
|
|
41410
41992
|
body,
|
|
41411
|
-
|
|
41993
|
+
await mintAppToken(projectGuid2)
|
|
41412
41994
|
);
|
|
41413
41995
|
}
|
|
41414
41996
|
fnCommand.command("call <name> [body]").description("Call a function").option("-d, --data <json>", "JSON request body: inline JSON, @file to read a file, or @- / - for stdin").option("--file <field=@path>", "Attach a file as { data, media_type } under <field> (the vision/media service shape), repeatable, e.g. --file image=@receipt.png", (v7, acc) => (acc || []).concat(v7)).option("--anon", "Call as an anonymous visitor (the public path a signed-out user hits) instead of as your signed-in account").option("--field <path>", "Print only this field of the result (dot path, e.g. items.0.short_guid)").option("--json", "Output as JSON").action((name, bodyArg, opts) => run("Call", async () => {
|
|
@@ -41446,6 +42028,7 @@ fnCommand.command("delete <name>").alias("rm").description("Delete a function").
|
|
|
41446
42028
|
|
|
41447
42029
|
// src/commands/service.ts
|
|
41448
42030
|
init_api();
|
|
42031
|
+
init_auth();
|
|
41449
42032
|
init_config();
|
|
41450
42033
|
init_colors();
|
|
41451
42034
|
var SERVICES = [
|
|
@@ -41471,16 +42054,59 @@ serviceCommand.command("list").description("List callable app services").option(
|
|
|
41471
42054
|
const width = SERVICES.reduce((m, s) => Math.max(m, s.name.length), 0);
|
|
41472
42055
|
console.log('Call one with `gipity service call <name> \'{"json":"body"}\'`');
|
|
41473
42056
|
console.log(muted("(GET endpoints like llm/models, tts/voices take --get and no body)"));
|
|
42057
|
+
console.log(muted("Verifying what a signed-out visitor of your deployed app gets? Add --anon."));
|
|
41474
42058
|
console.log("");
|
|
41475
42059
|
for (const s of SERVICES) {
|
|
41476
42060
|
console.log(`${bold(s.name.padEnd(width))} ${muted(`[${s.method}] ${s.desc}`)}`);
|
|
41477
42061
|
}
|
|
41478
42062
|
}));
|
|
41479
|
-
|
|
42063
|
+
function anonBillingHint(name, err) {
|
|
42064
|
+
if (!(err instanceof ApiError) || err.statusCode !== 401) return null;
|
|
42065
|
+
if (err.code !== "LOGIN_REQUIRED") return null;
|
|
42066
|
+
const service = name.split("/")[0];
|
|
42067
|
+
return `An anonymous visitor cannot call '${service}': it is on the user_pays default, so every signed-out visitor to a public page is asked to sign in and pay from their own credits. If this app serves the public (a help bubble, a landing page), switch it to owner-pays in gipity.yaml and redeploy:
|
|
42068
|
+
deploy:
|
|
42069
|
+
phases:
|
|
42070
|
+
- services
|
|
42071
|
+
service_definitions:
|
|
42072
|
+
- service: ${service}
|
|
42073
|
+
billing_mode: owner_pays
|
|
42074
|
+
gipity deploy dev --only services`;
|
|
42075
|
+
}
|
|
42076
|
+
serviceCommand.command("call <name> [body]").description("Call an app service by name (e.g. llm, image, music). Uses your logged-in session - no token wrangling; --anon calls it as a signed-out visitor instead.").option("-d, --data <json>", "JSON request body: inline JSON, @file to read a file, or @- / - for stdin (alternative to the positional [body])").option("--file <field=@path>", "Attach a file as { data, media_type } under <field> (the vision/media shape these services expect), repeatable, e.g. --file image=@receipt.png", (v7, acc) => (acc || []).concat(v7)).option("--anon", "Call as an anonymous visitor (the public path a signed-out user of your deployed app hits) instead of as your signed-in account").option("--get", "Issue a GET (for listing endpoints like llm/models, tts/voices)").option("--field <path>", "Print only this field of the result (dot path, e.g. choices.0.message.content)").option("--json", "Output compact JSON").action((name, bodyArg, opts) => run("Call", async () => {
|
|
41480
42077
|
const config = requireConfig();
|
|
41481
42078
|
const path5 = name.split("/").map(encodeURIComponent).join("/");
|
|
41482
42079
|
const url = `/api/${config.projectGuid}/services/${path5}`;
|
|
41483
|
-
const
|
|
42080
|
+
const body = resolveBody(bodyArg || opts.data, opts.file);
|
|
42081
|
+
if (opts.anon) {
|
|
42082
|
+
console.error(muted("Auth: anonymous visitor (the public path a signed-out user of your deployed app hits)"));
|
|
42083
|
+
} else {
|
|
42084
|
+
const who = getAuth()?.email;
|
|
42085
|
+
console.error(muted(`Auth: calling as ${who ?? "your signed-in account"} (the owner persona - always billed to you and never blocked by billing_mode; use --anon for the public visitor path)`));
|
|
42086
|
+
}
|
|
42087
|
+
let res;
|
|
42088
|
+
try {
|
|
42089
|
+
if (opts.anon) {
|
|
42090
|
+
const headers = await mintAppToken(config.projectGuid);
|
|
42091
|
+
if (!headers) {
|
|
42092
|
+
throw new Error(
|
|
42093
|
+
`Could not mint a visitor token for this project, so there is no anonymous path to call. That means the app is not deployed on this API base yet: run \`gipity deploy dev\` first, then re-run this command.`
|
|
42094
|
+
);
|
|
42095
|
+
}
|
|
42096
|
+
res = await publicRequest(opts.get ? "GET" : "POST", url, opts.get ? void 0 : body, headers);
|
|
42097
|
+
} else {
|
|
42098
|
+
res = opts.get ? await get(url) : await post(url, body);
|
|
42099
|
+
}
|
|
42100
|
+
} catch (err) {
|
|
42101
|
+
const hint = opts.anon ? anonBillingHint(name, err) : null;
|
|
42102
|
+
if (!hint) throw err;
|
|
42103
|
+
throw new Error(`${err.message}
|
|
42104
|
+
${hint}`);
|
|
42105
|
+
}
|
|
42106
|
+
if (opts.field) {
|
|
42107
|
+
emitField(res, opts.field);
|
|
42108
|
+
return;
|
|
42109
|
+
}
|
|
41484
42110
|
console.log(opts.json ? JSON.stringify(res) : JSON.stringify(res, null, 2));
|
|
41485
42111
|
}));
|
|
41486
42112
|
|
|
@@ -42845,7 +43471,7 @@ function undeployedFunctionsFromFailures(results) {
|
|
|
42845
43471
|
return [...names];
|
|
42846
43472
|
}
|
|
42847
43473
|
var LONG_RUN_MS = 6e4;
|
|
42848
|
-
async function pollTestStatus(
|
|
43474
|
+
async function pollTestStatus(projectGuid2, runGuid, opts) {
|
|
42849
43475
|
const startTime = Date.now();
|
|
42850
43476
|
const getPollInterval = () => {
|
|
42851
43477
|
const elapsed = Date.now() - startTime;
|
|
@@ -42867,7 +43493,7 @@ async function pollTestStatus(projectGuid, runGuid, opts) {
|
|
|
42867
43493
|
if (Date.now() - startTime > POLL_HARD_CAP_MS) {
|
|
42868
43494
|
throw new Error(`Test run ${runGuid} still not finished after ${Math.round(POLL_HARD_CAP_MS / 6e4)} minutes - giving up on the poll. Check it later with \`gipity test status ${runGuid}\` or re-run.`);
|
|
42869
43495
|
}
|
|
42870
|
-
const res = await get(`/projects/${
|
|
43496
|
+
const res = await get(`/projects/${projectGuid2}/test/status/${runGuid}`);
|
|
42871
43497
|
const data = res.data;
|
|
42872
43498
|
if (!opts.json && data.results.length > lastResultCount) {
|
|
42873
43499
|
const newResults = data.results.slice(lastResultCount);
|
|
@@ -44388,10 +45014,11 @@ async function collectDiagnostics() {
|
|
|
44388
45014
|
return [];
|
|
44389
45015
|
}
|
|
44390
45016
|
})();
|
|
44391
|
-
const [claude, codex, grok, cursor, gpu] = await Promise.all([
|
|
45017
|
+
const [claude, codex, grok, agy, cursor, gpu] = await Promise.all([
|
|
44392
45018
|
probeVersion("claude"),
|
|
44393
45019
|
probeVersion("codex"),
|
|
44394
45020
|
probeVersion("grok"),
|
|
45021
|
+
probeVersion("agy"),
|
|
44395
45022
|
probeVersion("cursor"),
|
|
44396
45023
|
detectGpu()
|
|
44397
45024
|
]);
|
|
@@ -44399,6 +45026,7 @@ async function collectDiagnostics() {
|
|
|
44399
45026
|
if (claude) agents.claude_code = claude;
|
|
44400
45027
|
if (codex) agents.codex = codex;
|
|
44401
45028
|
if (grok) agents.grok = grok;
|
|
45029
|
+
if (agy) agents.agy = agy;
|
|
44402
45030
|
if (cursor) agents.cursor = cursor;
|
|
44403
45031
|
return {
|
|
44404
45032
|
collected_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -46619,6 +47247,16 @@ var uninstallCommand = new Command("uninstall").description("Uninstall Gipity").
|
|
|
46619
47247
|
}
|
|
46620
47248
|
console.log(`${success(`Removed ${agentSkills.skills.length} Gipity skills from ~/.agents/skills.`)}`);
|
|
46621
47249
|
}
|
|
47250
|
+
const agySkills = agySkillsState();
|
|
47251
|
+
if (agySkills.skills.length) {
|
|
47252
|
+
for (const name of agySkills.skills) {
|
|
47253
|
+
try {
|
|
47254
|
+
rmSync4(join25(AGY_SKILLS_DIR, name), { recursive: true, force: true });
|
|
47255
|
+
} catch {
|
|
47256
|
+
}
|
|
47257
|
+
}
|
|
47258
|
+
console.log(`${success(`Removed ${agySkills.skills.length} Gipity skills from ~/.gemini/config/skills.`)}`);
|
|
47259
|
+
}
|
|
46622
47260
|
if (existsSync23(gipityDir)) {
|
|
46623
47261
|
try {
|
|
46624
47262
|
rmSync4(gipityDir, { recursive: true, force: true });
|
|
@@ -47238,8 +47876,8 @@ function configureHelp(cmd) {
|
|
|
47238
47876
|
var program2 = new Command();
|
|
47239
47877
|
program2.enablePositionalOptions();
|
|
47240
47878
|
var startGroup = [statusCommand, initCommand, skillCommand, projectCommand];
|
|
47241
|
-
var buildGroup = [addCommand, removeCommand, saveCommand, loadCommand, deployCommand, pageCommand, testCommand];
|
|
47242
|
-
var backendGroup = [dbCommand, fnCommand, secretsCommand, logsCommand, jobCommand, workflowCommand];
|
|
47879
|
+
var buildGroup = [addCommand, removeCommand, brandCommand, saveCommand, loadCommand, deployCommand, pageCommand, testCommand];
|
|
47880
|
+
var backendGroup = [dbCommand, fnCommand, secretsCommand, keyCommand, logsCommand, jobCommand, workflowCommand];
|
|
47243
47881
|
var servicesGroup = [serviceCommand, generateCommand, notifyCommand, paymentsCommand, realtimeCommand, recordsCommand, rbacCommand, auditCommand, domainCommand, tokenCommand];
|
|
47244
47882
|
var filesGroup = [syncCommand, fileCommand, pushCommand, uploadCommand, storageCommand];
|
|
47245
47883
|
var gipGroup = [chatCommand, memoryCommand, agentCommand, approvalCommand, gmailCommand];
|
|
@@ -47256,6 +47894,7 @@ var HELP_SECTIONS = [
|
|
|
47256
47894
|
{ title: "Connect & setup", cmds: connectGroup }
|
|
47257
47895
|
];
|
|
47258
47896
|
var SKILL_DOCS = {
|
|
47897
|
+
brand: "web-app-basics",
|
|
47259
47898
|
save: "app-import",
|
|
47260
47899
|
load: "app-import",
|
|
47261
47900
|
github: "app-import",
|
|
@@ -47268,6 +47907,7 @@ var SKILL_DOCS = {
|
|
|
47268
47907
|
notify: "app-notify",
|
|
47269
47908
|
payments: "app-payments",
|
|
47270
47909
|
records: "app-records",
|
|
47910
|
+
key: "app-auth",
|
|
47271
47911
|
realtime: "app-realtime",
|
|
47272
47912
|
job: "jobs",
|
|
47273
47913
|
sandbox: "sandbox-tools",
|