gipity 1.0.386 → 1.0.388
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/README.md +18 -12
- package/dist/api.js +5 -2
- package/dist/auth.js +12 -4
- package/dist/banner.js +11 -9
- package/dist/colors.js +109 -9
- package/dist/commands/add.js +2 -1
- package/dist/commands/approval.js +10 -0
- package/dist/commands/claude.js +8 -4
- package/dist/commands/deploy.js +1 -1
- package/dist/commands/fn.js +22 -1
- package/dist/commands/page-eval.js +65 -22
- package/dist/commands/records.js +42 -8
- package/dist/commands/remove.js +42 -0
- package/dist/commands/skill.js +27 -1
- package/dist/commands/test.js +14 -1
- package/dist/commands/workflow.js +57 -3
- package/dist/config.js +71 -18
- package/dist/helpers/sync.js +4 -2
- package/dist/index.js +11 -3
- package/dist/knowledge.js +3 -0
- package/dist/page-fixtures.js +41 -0
- package/dist/project-setup.js +2 -2
- package/dist/relay/daemon.js +2 -2
- package/dist/relay/device-http.js +2 -2
- package/dist/sync.js +261 -90
- package/dist/updater/bootstrap.js +4 -1
- package/dist/updater/check.js +4 -1
- package/dist/upload.js +55 -43
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -8,27 +8,33 @@ This CLI connects [Claude Code](https://claude.ai/claude-code) to Gipity's cloud
|
|
|
8
8
|
|
|
9
9
|
## Getting Started
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
One line installs everything. It sets up Node 18+ (if you don't already have it) and the Gipity CLI, with no sudo required:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
#
|
|
14
|
+
# macOS / Linux / WSL
|
|
15
|
+
curl -fsSL https://gipity.ai/install.sh | bash
|
|
15
16
|
|
|
16
|
-
#
|
|
17
|
-
|
|
17
|
+
# Windows (PowerShell)
|
|
18
|
+
irm https://gipity.ai/install.ps1 | iex
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then launch your coding agent wired into Gipity:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
gipity claude
|
|
25
|
+
```
|
|
18
26
|
|
|
19
|
-
|
|
27
|
+
`gipity claude` walks you through login, project setup, and launches Claude Code. Using Codex, Gemini, or Cursor instead? Run `gipity init`.
|
|
20
28
|
|
|
21
|
-
|
|
22
|
-
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt install -y nodejs
|
|
29
|
+
### Prefer npm
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
npm install -g gipity @anthropic-ai/claude-code
|
|
31
|
+
If you already have **Node.js 18+** you can install directly:
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
gipity
|
|
33
|
+
```bash
|
|
34
|
+
npm install -g gipity
|
|
29
35
|
```
|
|
30
36
|
|
|
31
|
-
|
|
37
|
+
If that fails with `EACCES`, your npm global prefix is root-owned. Don't reach for `sudo`: point npm at a user-owned prefix instead (`npm config set prefix ~/.npm-global` and add `~/.npm-global/bin` to your `PATH`), or just use the one-line installer above, which does this for you. See https://docs.npmjs.com/resolving-eacces-permissions-errors.
|
|
32
38
|
|
|
33
39
|
## Updates
|
|
34
40
|
|
package/dist/api.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Readable } from 'stream';
|
|
2
2
|
import * as tar from 'tar-stream';
|
|
3
3
|
import { getAuth, refreshTokenIfNeeded } from './auth.js';
|
|
4
|
-
import {
|
|
4
|
+
import { resolveApiBase, requireConfig, saveConfig } from './config.js';
|
|
5
5
|
export class ApiError extends Error {
|
|
6
6
|
statusCode;
|
|
7
7
|
code;
|
|
@@ -27,7 +27,7 @@ async function getHeaders() {
|
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
29
|
function baseUrl() {
|
|
30
|
-
return
|
|
30
|
+
return resolveApiBase();
|
|
31
31
|
}
|
|
32
32
|
/** Exposed so streaming consumers (SSE) can build URLs without re-implementing
|
|
33
33
|
* the override / config resolution. */
|
|
@@ -101,6 +101,9 @@ export async function postForTarEntries(path, body) {
|
|
|
101
101
|
export function put(path, body) {
|
|
102
102
|
return request('PUT', path, body);
|
|
103
103
|
}
|
|
104
|
+
export function patch(path, body) {
|
|
105
|
+
return request('PATCH', path, body);
|
|
106
|
+
}
|
|
104
107
|
export function del(path, body) {
|
|
105
108
|
return request('DELETE', path, body);
|
|
106
109
|
}
|
package/dist/auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'fs';
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync, chmodSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import { decodeJwtExp } from './utils.js';
|
|
@@ -33,8 +33,16 @@ export function readAuthFresh() {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
export function saveAuth(data) {
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
// Lock down to owner-only: this file holds the account access + 7-day refresh
|
|
37
|
+
// tokens, so a default 0644/0755 would let any other local user read them.
|
|
38
|
+
// (The relay state file already does this; auth.json is the more sensitive of
|
|
39
|
+
// the two.) chmod after write to also tighten any pre-existing loose file.
|
|
40
|
+
mkdirSync(AUTH_DIR, { recursive: true, mode: 0o700 });
|
|
41
|
+
writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
42
|
+
try {
|
|
43
|
+
chmodSync(AUTH_FILE, 0o600);
|
|
44
|
+
}
|
|
45
|
+
catch { /* best-effort on platforms without chmod */ }
|
|
38
46
|
cached = data;
|
|
39
47
|
}
|
|
40
48
|
export function clearAuth() {
|
|
@@ -73,7 +81,7 @@ export async function refreshTokenIfNeeded() {
|
|
|
73
81
|
return; // not logged in, caller will handle
|
|
74
82
|
try {
|
|
75
83
|
const config = await import('./config.js');
|
|
76
|
-
const apiBase = config.
|
|
84
|
+
const apiBase = config.resolveApiBase();
|
|
77
85
|
const res = await fetch(`${apiBase}/auth/refresh`, {
|
|
78
86
|
method: 'POST',
|
|
79
87
|
headers: { 'Content-Type': 'application/json' },
|
package/dist/banner.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// ── Gipity CLI Startup Banner ──────────────────────────────────────────
|
|
2
2
|
// Two-panel box showing all AI models, platform tools, and sandbox capabilities.
|
|
3
|
-
import { brand, bold, faint, muted } from './colors.js';
|
|
3
|
+
import { brand, bold, faint, muted, fg } from './colors.js';
|
|
4
4
|
// ── Static content ─────────────────────────────────────────────────────
|
|
5
5
|
// ── Feature groups ────────────────────────────────────────────────────
|
|
6
6
|
const AI_MODELS = [
|
|
@@ -38,11 +38,13 @@ const INFRASTRUCTURE = [
|
|
|
38
38
|
'Deploy', 'Uploads', 'Rollback',
|
|
39
39
|
];
|
|
40
40
|
// ── Egg color palette (shared) - gradient built around #FEA60E ───────
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
41
|
+
// Colors route through fg() so they downgrade (256 / 16 / none) on terminals
|
|
42
|
+
// without truecolor support instead of rendering as misparsed garbage.
|
|
43
|
+
const _hi = fg(255, 215, 80); // golden highlight
|
|
44
|
+
const _lt = fg(254, 190, 45); // light
|
|
45
|
+
const _br = fg(254, 166, 14); // base - #FEA60E
|
|
46
|
+
const _md = fg(218, 112, 8); // medium-dark
|
|
47
|
+
const _sh = fg(170, 68, 4); // shadow
|
|
46
48
|
// Wobbly egg - asymmetric left/right, organic feel (8 rows)
|
|
47
49
|
// Widths: 6 → 8 → 10 → 12 → 14 → 12 → 10 → 8 | Widest at row 5/8 (62%)
|
|
48
50
|
function eggWobbly() {
|
|
@@ -76,8 +78,8 @@ function eggSym() {
|
|
|
76
78
|
// Edge pairs: ▗/▖ cap → ▟/▙ expand → ▐/▌ straight → ▜/▛ contract → ▝/▘ cap
|
|
77
79
|
function eggTall() {
|
|
78
80
|
// Extra intermediate tones for a smoother gradient
|
|
79
|
-
const _m1 = (
|
|
80
|
-
const _dk = (
|
|
81
|
+
const _m1 = fg(238, 138, 10); // base to medium
|
|
82
|
+
const _dk = fg(195, 88, 6); // medium to shadow
|
|
81
83
|
return [
|
|
82
84
|
_lt('▗▄') + _br('██') + _md('▄▖'), // 6 - top cap
|
|
83
85
|
_lt('▟') + _hi('█') + _br('████') + _m1('█▙'), // 8 - expanding, highlight
|
|
@@ -161,7 +163,7 @@ function buildLeftPanel(opts, panelW) {
|
|
|
161
163
|
const nameDisplay = opts.email
|
|
162
164
|
? opts.email.split('@')[0].replace(/^./, c => c.toUpperCase())
|
|
163
165
|
: null;
|
|
164
|
-
const white = (
|
|
166
|
+
const white = fg(255, 255, 255);
|
|
165
167
|
const welcome = nameDisplay
|
|
166
168
|
? white(bold(`Welcome back ${nameDisplay}!`))
|
|
167
169
|
: white(bold('Welcome to Gipity'));
|
package/dist/colors.js
CHANGED
|
@@ -1,27 +1,127 @@
|
|
|
1
1
|
// ── Gipity CLI Color System ─────────────────────────────────────────────
|
|
2
2
|
// Centralized color definitions matching the Gipity platform palette.
|
|
3
3
|
// All command files should import from here - no inline ANSI codes.
|
|
4
|
+
//
|
|
5
|
+
// Color depth is detected once at load. RGB colors automatically downgrade:
|
|
6
|
+
// level 3 → 24-bit truecolor \x1b[38;2;R;G;Bm
|
|
7
|
+
// level 2 → 256-color \x1b[38;5;Nm
|
|
8
|
+
// level 1 → 16-color \x1b[3Xm / \x1b[9Xm
|
|
9
|
+
// level 0 → no color (plain text)
|
|
10
|
+
// This avoids the failure mode where a terminal that does not understand the
|
|
11
|
+
// 24-bit escape misparses it and renders garbage (e.g. the orange egg coming
|
|
12
|
+
// out purple on consoles without truecolor support).
|
|
4
13
|
const ESC = '\x1b';
|
|
5
|
-
//
|
|
6
|
-
|
|
14
|
+
// ── Color-depth detection ───────────────────────────────────────────────
|
|
15
|
+
// 0 = none, 1 = 16-color, 2 = 256-color, 3 = truecolor. Mirrors the common
|
|
16
|
+
// supports-color / chalk heuristics, biased toward NOT emitting truecolor
|
|
17
|
+
// unless the terminal advertises it, so unknown terminals get a safe 256 or
|
|
18
|
+
// 16-color approximation instead of a misparsed 24-bit sequence.
|
|
19
|
+
function detectColorLevel() {
|
|
20
|
+
if (process.env['NO_COLOR'])
|
|
21
|
+
return 0;
|
|
22
|
+
// FORCE_COLOR overrides detection (matches Node / chalk semantics).
|
|
23
|
+
const force = process.env['FORCE_COLOR'];
|
|
24
|
+
if (force !== undefined) {
|
|
25
|
+
if (force === '0' || force === 'false')
|
|
26
|
+
return 0;
|
|
27
|
+
if (force === '3')
|
|
28
|
+
return 3;
|
|
29
|
+
if (force === '2')
|
|
30
|
+
return 2;
|
|
31
|
+
// '1', 'true', '' → at least basic color
|
|
32
|
+
if (force === '1' || force === 'true' || force === '')
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
if (!process.stdout.isTTY)
|
|
36
|
+
return 0;
|
|
37
|
+
const term = (process.env['TERM'] || '').toLowerCase();
|
|
38
|
+
if (term === 'dumb')
|
|
39
|
+
return 0;
|
|
40
|
+
const colorterm = (process.env['COLORTERM'] || '').toLowerCase();
|
|
41
|
+
if (colorterm === 'truecolor' || colorterm === '24bit')
|
|
42
|
+
return 3;
|
|
43
|
+
const termProgram = process.env['TERM_PROGRAM'] || '';
|
|
44
|
+
if (termProgram === 'iTerm.app' || termProgram === 'vscode')
|
|
45
|
+
return 3;
|
|
46
|
+
if (termProgram === 'Apple_Terminal')
|
|
47
|
+
return 2;
|
|
48
|
+
if (/-256(color)?$/.test(term) || term.includes('256'))
|
|
49
|
+
return 2;
|
|
50
|
+
// Modern Windows consoles set COLORTERM (caught above). Older cmd.exe /
|
|
51
|
+
// conhost only do 16-color reliably.
|
|
52
|
+
if (process.platform === 'win32')
|
|
53
|
+
return 1;
|
|
54
|
+
if (term)
|
|
55
|
+
return 1;
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
const COLOR_LEVEL = detectColorLevel();
|
|
7
59
|
// Identity function for when colors are disabled
|
|
8
60
|
const identity = (s) => s;
|
|
61
|
+
// ── RGB downgrade helpers ───────────────────────────────────────────────
|
|
62
|
+
// RGB → nearest xterm-256 palette index (6x6x6 cube + grayscale ramp).
|
|
63
|
+
function rgbTo256(r, g, b) {
|
|
64
|
+
if (r === g && g === b) {
|
|
65
|
+
if (r < 8)
|
|
66
|
+
return 16;
|
|
67
|
+
if (r > 248)
|
|
68
|
+
return 231;
|
|
69
|
+
return Math.round(((r - 8) / 247) * 24) + 232;
|
|
70
|
+
}
|
|
71
|
+
return (16 +
|
|
72
|
+
36 * Math.round((r / 255) * 5) +
|
|
73
|
+
6 * Math.round((g / 255) * 5) +
|
|
74
|
+
Math.round((b / 255) * 5));
|
|
75
|
+
}
|
|
76
|
+
// RGB → nearest 16-color SGR foreground code (30-37 / 90-97).
|
|
77
|
+
function rgbTo16(r, g, b) {
|
|
78
|
+
const value = Math.round((Math.max(r, g, b) / 255) * 3);
|
|
79
|
+
if (value === 0)
|
|
80
|
+
return 30;
|
|
81
|
+
let code = 30 +
|
|
82
|
+
((Math.round(b / 255) << 2) |
|
|
83
|
+
(Math.round(g / 255) << 1) |
|
|
84
|
+
Math.round(r / 255));
|
|
85
|
+
if (value === 3)
|
|
86
|
+
code += 60; // bright variant
|
|
87
|
+
return code;
|
|
88
|
+
}
|
|
9
89
|
// ── Low-level builders ──────────────────────────────────────────────────
|
|
10
|
-
function makeFg(r, g, b) {
|
|
11
|
-
if (
|
|
90
|
+
export function makeFg(r, g, b) {
|
|
91
|
+
if (COLOR_LEVEL === 0)
|
|
12
92
|
return identity;
|
|
13
|
-
|
|
93
|
+
if (COLOR_LEVEL === 3) {
|
|
94
|
+
return (s) => `${ESC}[38;2;${r};${g};${b}m${s}${ESC}[39m`;
|
|
95
|
+
}
|
|
96
|
+
if (COLOR_LEVEL === 2) {
|
|
97
|
+
const n = rgbTo256(r, g, b);
|
|
98
|
+
return (s) => `${ESC}[38;5;${n}m${s}${ESC}[39m`;
|
|
99
|
+
}
|
|
100
|
+
const code = rgbTo16(r, g, b);
|
|
101
|
+
return (s) => `${ESC}[${code}m${s}${ESC}[39m`;
|
|
14
102
|
}
|
|
15
|
-
function makeBg(r, g, b) {
|
|
16
|
-
if (
|
|
103
|
+
export function makeBg(r, g, b) {
|
|
104
|
+
if (COLOR_LEVEL === 0)
|
|
17
105
|
return identity;
|
|
18
|
-
|
|
106
|
+
if (COLOR_LEVEL === 3) {
|
|
107
|
+
return (s) => `${ESC}[48;2;${r};${g};${b}m${s}${ESC}[49m`;
|
|
108
|
+
}
|
|
109
|
+
if (COLOR_LEVEL === 2) {
|
|
110
|
+
const n = rgbTo256(r, g, b);
|
|
111
|
+
return (s) => `${ESC}[48;5;${n}m${s}${ESC}[49m`;
|
|
112
|
+
}
|
|
113
|
+
const code = rgbTo16(r, g, b) + 10; // fg 30-97 → bg 40-107
|
|
114
|
+
return (s) => `${ESC}[${code}m${s}${ESC}[49m`;
|
|
19
115
|
}
|
|
20
116
|
function makeStyle(open, close) {
|
|
21
|
-
if (
|
|
117
|
+
if (COLOR_LEVEL === 0)
|
|
22
118
|
return identity;
|
|
23
119
|
return (s) => `${ESC}[${open}m${s}${ESC}[${close}m`;
|
|
24
120
|
}
|
|
121
|
+
// Convenience alias for callers that just want an RGB foreground (e.g. banner).
|
|
122
|
+
export const fg = makeFg;
|
|
123
|
+
// The detected level, exported for callers that want to branch on it.
|
|
124
|
+
export const colorLevel = COLOR_LEVEL;
|
|
25
125
|
// ── Text style helpers ──────────────────────────────────────────────────
|
|
26
126
|
export const bold = makeStyle(1, 22);
|
|
27
127
|
export const dim = makeStyle(2, 22);
|
package/dist/commands/add.js
CHANGED
|
@@ -179,7 +179,8 @@ export const addCommand = new Command('add')
|
|
|
179
179
|
}
|
|
180
180
|
const { name: labelName, files } = buildLocalPayload(resolved);
|
|
181
181
|
const kind = sniffPayloadKind(files);
|
|
182
|
-
|
|
182
|
+
// Progress goes to stderr so `gipity add … --json` keeps stdout pure JSON.
|
|
183
|
+
console.error(muted(`Uploading ${files.length} file(s) from ${resolved} (${kind}) ...`));
|
|
183
184
|
body = {
|
|
184
185
|
name: labelName,
|
|
185
186
|
title: opts.title,
|
|
@@ -58,11 +58,21 @@ approvalCommand
|
|
|
58
58
|
approvalCommand
|
|
59
59
|
.command('answer <guid> [selection...]')
|
|
60
60
|
.description('Answer an approval: a = approve, b = deny, c = ignore, or free text for text-type')
|
|
61
|
+
.option('--deny', 'Resolve as denied; any trailing text is sent as feedback (e.g. requested edits)')
|
|
61
62
|
.option('--json', 'Output as JSON')
|
|
62
63
|
.action((guid, selectionParts, opts) => run('Answer', async () => {
|
|
63
64
|
if (!guid.startsWith('ap_')) {
|
|
64
65
|
throw new Error('Expected approval guid like ap_xxxxxxxx');
|
|
65
66
|
}
|
|
67
|
+
// Deny with optional free-text feedback works for every response type -
|
|
68
|
+
// it's how a workflow gate's revision loop gets the user's edits
|
|
69
|
+
// ({{gate.action}} == rejected + {{gate.human_response}}).
|
|
70
|
+
if (opts.deny) {
|
|
71
|
+
const feedback = selectionParts.join(' ').trim();
|
|
72
|
+
await post(`/approvals/${guid}/resolve`, { status: 'denied', response: feedback || undefined });
|
|
73
|
+
printResult(`Denied${feedback ? `: ${feedback}` : '.'}`, opts, { guid, status: 'denied' });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
66
76
|
const detailRes = await get(`/approvals/${guid}`);
|
|
67
77
|
const detail = detailRes.data;
|
|
68
78
|
const first = selectionParts[0];
|
package/dist/commands/claude.js
CHANGED
|
@@ -19,7 +19,7 @@ function resolveCommand(cmd) {
|
|
|
19
19
|
}
|
|
20
20
|
import { getAuth, saveAuth, clearAuth } from '../auth.js';
|
|
21
21
|
import { get, post, publicPost, ApiError, getAccountSlug } from '../api.js';
|
|
22
|
-
import { getConfig, saveConfigAt, clearConfigCache, getApiBaseOverride, getConfigPath } from '../config.js';
|
|
22
|
+
import { getConfig, saveConfigAt, clearConfigCache, getApiBaseOverride, DEFAULT_API_BASE, getConfigPath } from '../config.js';
|
|
23
23
|
import { sync } from '../sync.js';
|
|
24
24
|
import { slugify, setupClaudeHooks, setupClaudeMd, setupAgentsMd, setupGitignore, DEFAULT_SYNC_IGNORE, isSyncIgnored } from '../setup.js';
|
|
25
25
|
import { buildProjectContextBlock as buildProjectContextBlockText, buildNewProjectPrompt, buildResumeWrap, buildFreshWrap, } from '../prompts.js';
|
|
@@ -219,6 +219,10 @@ export const claudeCommand = new Command('claude')
|
|
|
219
219
|
.option('--project <slug>', 'Open an existing project by slug or id')
|
|
220
220
|
.option('--here', 'Use the current directory instead of ~/GipityProjects/<slug>/')
|
|
221
221
|
.option('--quiet', "Suppress Claude's live progress output (headless --new-project/--project runs)")
|
|
222
|
+
// Forwarded to `claude` via the unknown-arg passthrough below (NOT in the
|
|
223
|
+
// gipity strip lists). Declared here only so it shows up in --help — without
|
|
224
|
+
// this, callers can't discover that the session model is selectable.
|
|
225
|
+
.option('--model <model>', 'Model for the Claude session, forwarded to claude (e.g. sonnet, opus, or a full id like claude-sonnet-4-6)')
|
|
222
226
|
.allowUnknownOption(true)
|
|
223
227
|
.allowExcessArguments(true)
|
|
224
228
|
.action(async (opts) => {
|
|
@@ -384,7 +388,7 @@ export const claudeCommand = new Command('claude')
|
|
|
384
388
|
accountSlug,
|
|
385
389
|
agentGuid,
|
|
386
390
|
conversationGuid: null,
|
|
387
|
-
apiBase: getApiBaseOverride() ||
|
|
391
|
+
apiBase: getApiBaseOverride() || DEFAULT_API_BASE,
|
|
388
392
|
ignore: DEFAULT_SYNC_IGNORE,
|
|
389
393
|
});
|
|
390
394
|
console.log(`\n Using ${projectDir}`);
|
|
@@ -470,7 +474,7 @@ export const claudeCommand = new Command('claude')
|
|
|
470
474
|
err?.code === 'ETIMEDOUT' || err?.cause?.code === 'ECONNREFUSED' ||
|
|
471
475
|
err?.cause?.code === 'ENOTFOUND' || err?.cause?.code === 'ETIMEDOUT';
|
|
472
476
|
if (isConnectionError) {
|
|
473
|
-
const apiBase = getApiBaseOverride() ||
|
|
477
|
+
const apiBase = getApiBaseOverride() || DEFAULT_API_BASE;
|
|
474
478
|
console.error(` ${clrError(`Could not connect to ${apiBase}`)}`);
|
|
475
479
|
console.error(` ${muted('Check your connection and try again.')}`);
|
|
476
480
|
process.exit(1);
|
|
@@ -554,7 +558,7 @@ export const claudeCommand = new Command('claude')
|
|
|
554
558
|
accountSlug,
|
|
555
559
|
agentGuid,
|
|
556
560
|
conversationGuid: null,
|
|
557
|
-
apiBase: getApiBaseOverride() ||
|
|
561
|
+
apiBase: getApiBaseOverride() || DEFAULT_API_BASE,
|
|
558
562
|
ignore: DEFAULT_SYNC_IGNORE,
|
|
559
563
|
});
|
|
560
564
|
console.log(`\n Using ${projectDir}`);
|
package/dist/commands/deploy.js
CHANGED
|
@@ -20,7 +20,7 @@ export const deployCommand = new Command('deploy')
|
|
|
20
20
|
.argument('[target]', 'dev or prod', 'dev')
|
|
21
21
|
.option('--source-dir <dir>', 'Source directory to deploy from')
|
|
22
22
|
.option('--only <phases>', 'Run only specific phases (comma-separated)')
|
|
23
|
-
.option('--force', 'Re-run all phases
|
|
23
|
+
.option('--force', 'Re-run all phases (ignore checksums) and bypass the sync bulk-deletion guard')
|
|
24
24
|
.option('--no-sync', 'Skip sync-up before deploy')
|
|
25
25
|
.option('--optimize', 'Run build optimization')
|
|
26
26
|
.option('--json', 'Output as JSON')
|
package/dist/commands/fn.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { get, post } from '../api.js';
|
|
2
|
+
import { get, post, del } from '../api.js';
|
|
3
3
|
import { requireConfig } from '../config.js';
|
|
4
4
|
import { error as clrError, bold, muted, success } from '../colors.js';
|
|
5
5
|
import { run, printList } from '../helpers/index.js';
|
|
6
|
+
import { confirm } from '../utils.js';
|
|
6
7
|
export const fnCommand = new Command('fn')
|
|
7
8
|
.description('Manage functions');
|
|
8
9
|
fnCommand
|
|
@@ -45,4 +46,24 @@ fnCommand
|
|
|
45
46
|
const res = await post(`/api/${config.projectGuid}/fn/${encodeURIComponent(name)}`, body);
|
|
46
47
|
console.log(opts.json ? JSON.stringify(res.data) : JSON.stringify(res.data, null, 2));
|
|
47
48
|
}));
|
|
49
|
+
fnCommand
|
|
50
|
+
.command('delete <name>')
|
|
51
|
+
.alias('rm')
|
|
52
|
+
.description('Delete a function')
|
|
53
|
+
.option('--yes', 'Skip confirmation')
|
|
54
|
+
.option('--json', 'Output as JSON')
|
|
55
|
+
.action((name, opts) => run('Delete', async () => {
|
|
56
|
+
const config = requireConfig();
|
|
57
|
+
if (!await confirm(`Delete function '${name}'? This cannot be undone.`, { skip: opts.yes })) {
|
|
58
|
+
console.log('Cancelled.');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
await del(`/projects/${config.projectGuid}/functions/${encodeURIComponent(name)}`);
|
|
62
|
+
if (opts.json) {
|
|
63
|
+
console.log(JSON.stringify({ name, deleted: true }));
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
console.log(success(`Deleted function '${name}'.`));
|
|
67
|
+
}
|
|
68
|
+
}));
|
|
48
69
|
//# sourceMappingURL=fn.js.map
|
|
@@ -3,6 +3,8 @@ import { Command } from 'commander';
|
|
|
3
3
|
import { post, get, ApiError } from '../api.js';
|
|
4
4
|
import { brand, bold, muted, warning } from '../colors.js';
|
|
5
5
|
import { run } from '../helpers/index.js';
|
|
6
|
+
import { resolveProjectContext } from '../config.js';
|
|
7
|
+
import { uploadPublicFixture, deleteFixture } from '../page-fixtures.js';
|
|
6
8
|
// Shown when an eval runs cleanly but returns nothing serializable. Turns a
|
|
7
9
|
// bare/opaque `null` into a deterministic, actionable nudge so the agent shapes
|
|
8
10
|
// a returnable value instead of guessing and retrying.
|
|
@@ -135,6 +137,7 @@ export const pageEvalCommand = new Command('eval')
|
|
|
135
137
|
.argument('<url>', 'URL to load')
|
|
136
138
|
.argument('[expr]', 'JavaScript to evaluate in page context (inline expression or statement body with return/await; result is JSON-serialized). Omit when using --file.')
|
|
137
139
|
.option('--file <path>', 'Read the script body from a file instead of the inline <expr> arg (mutually exclusive). Runs as an async function body, so top-level return/await work.')
|
|
140
|
+
.option('--fixture <path>', 'Host a local file and expose it to the eval as `fixtureUrl` (and under `fixtures` by basename) to fetch in-page. For verifying a render/parse path against a real binary (an MP3, an image) - no size limit, auto-deleted after the run. Repeat for several files (single-value so it never swallows the inline <expr>).', (val, prev) => [...prev, val], [])
|
|
138
141
|
.option('--wait <ms>', 'Sleep this many ms after DOMContentLoaded before evaluating (lets late async work settle; max 30000)', '500')
|
|
139
142
|
.option('--wait-for <selector>', 'Wait until this CSS selector appears before evaluating (deterministic; replaces --wait)')
|
|
140
143
|
.option('--wait-timeout <ms>', 'Max ms to wait for --wait-for before giving up', '5000')
|
|
@@ -161,30 +164,66 @@ export const pageEvalCommand = new Command('eval')
|
|
|
161
164
|
const waitMs = capWaitMs(opts.wait, url);
|
|
162
165
|
const parsedTimeout = parseInt(opts.waitTimeout, 10);
|
|
163
166
|
const waitForTimeoutMs = Number.isFinite(parsedTimeout) && parsedTimeout >= 0 ? parsedTimeout : 5000;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
167
|
+
// --fixture: host each file publicly, then splice `fixtures` / `fixtureUrl`
|
|
168
|
+
// into the eval scope so the page can fetch the bytes. The prelude makes the
|
|
169
|
+
// body a statement (const/return), so the server's expression form fails to
|
|
170
|
+
// parse and it falls back to the function-body form - which runs both inline
|
|
171
|
+
// exprs (wrapped in `return (...)`) and --file scripts. Cleanup in `finally`.
|
|
172
|
+
const fixturePaths = opts.fixture ?? [];
|
|
173
|
+
const hosted = [];
|
|
174
|
+
let projectGuid;
|
|
175
|
+
let sentExpr = expr;
|
|
176
|
+
try {
|
|
177
|
+
if (fixturePaths.length) {
|
|
178
|
+
const { config } = await resolveProjectContext({});
|
|
179
|
+
projectGuid = config.projectGuid;
|
|
180
|
+
for (const p of fixturePaths) {
|
|
181
|
+
console.log(muted(`Hosting fixture ${p}…`));
|
|
182
|
+
hosted.push(await uploadPublicFixture(projectGuid, p));
|
|
183
|
+
}
|
|
184
|
+
const map = {};
|
|
185
|
+
for (const h of hosted)
|
|
186
|
+
map[h.name] = h.url;
|
|
187
|
+
const prelude = `const fixtures=${JSON.stringify(map)};const fixtureUrl=${JSON.stringify(hosted[0].url)};`;
|
|
188
|
+
sentExpr = opts.file ? `${prelude}\n${expr}` : `${prelude}\nreturn (${expr});`;
|
|
189
|
+
}
|
|
190
|
+
const kickoff = await post('/tools/browser/eval', {
|
|
191
|
+
url, expr: sentExpr, waitMs,
|
|
192
|
+
waitForSelector: opts.waitFor || undefined,
|
|
193
|
+
waitForTimeoutMs: opts.waitFor ? waitForTimeoutMs : undefined,
|
|
194
|
+
});
|
|
195
|
+
const d = await pollEvalResult(kickoff.data.evalJobId, waitMs);
|
|
196
|
+
const { result, noValue } = normalizeEvalResult(d.result);
|
|
197
|
+
const execTimeout = evalExecTimeoutMessage(d.result);
|
|
198
|
+
if (execTimeout)
|
|
199
|
+
throw new Error(execTimeout);
|
|
200
|
+
if (opts.json) {
|
|
201
|
+
console.log(JSON.stringify(noValue ? { ...d, result, hint: EVAL_NO_VALUE_HINT } : { ...d, result }));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
console.log(`${brand('Eval')} ${bold(d.url || url)}`);
|
|
205
|
+
if (d.navigationIncomplete) {
|
|
206
|
+
console.log(`${warning('⚠ Navigation incomplete:')} ${d.note || 'page did not reach full load'}`);
|
|
207
|
+
}
|
|
208
|
+
if (hosted.length)
|
|
209
|
+
console.log(`${muted('Fixtures:')} ${hosted.map((h) => h.name).join(', ')}`);
|
|
210
|
+
console.log(opts.file ? `${muted('Script:')} ${opts.file}` : `${muted('Expression:')} ${expr}`);
|
|
211
|
+
console.log(`\n${result.trim() ? result : muted('(empty result)')}`);
|
|
212
|
+
if (noValue)
|
|
213
|
+
console.log(muted(`\n${EVAL_NO_VALUE_HINT}`));
|
|
214
|
+
if (d.truncated)
|
|
215
|
+
console.log(muted('\n(result truncated to fit context - narrow the expression for the full value)'));
|
|
177
216
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
217
|
+
finally {
|
|
218
|
+
for (const h of hosted) {
|
|
219
|
+
try {
|
|
220
|
+
await deleteFixture(projectGuid, h.guid);
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
console.error(warning(`⚠ Could not auto-delete fixture "${h.name}" (${h.guid}) — still hosted at ${h.url}: ${err.message}`));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
181
226
|
}
|
|
182
|
-
console.log(opts.file ? `${muted('Script:')} ${opts.file}` : `${muted('Expression:')} ${expr}`);
|
|
183
|
-
console.log(`\n${result.trim() ? result : muted('(empty result)')}`);
|
|
184
|
-
if (noValue)
|
|
185
|
-
console.log(muted(`\n${EVAL_NO_VALUE_HINT}`));
|
|
186
|
-
if (d.truncated)
|
|
187
|
-
console.log(muted('\n(result truncated to fit context - narrow the expression for the full value)'));
|
|
188
227
|
}));
|
|
189
228
|
// Each `page eval` call runs to completion before the next starts, so two evals
|
|
190
229
|
// fired back-to-back never coexist in time - they CANNOT test whether two live
|
|
@@ -197,6 +236,10 @@ Examples:
|
|
|
197
236
|
# Functionally test a page's own code paths: save a script that drives the UI
|
|
198
237
|
# and returns a JSON-serializable result, then run it (no /tmp + shell quoting):
|
|
199
238
|
gipity page eval "https://dev.gipity.ai/me/app/" --file ./tests/draw-flow.js --json
|
|
239
|
+
# Verify a render/parse path against a REAL file: --fixture hosts it, injects a
|
|
240
|
+
# fetch-able 'fixtureUrl', runs the eval, then deletes the hosted copy:
|
|
241
|
+
gipity page eval "https://dev.gipity.ai/me/app/" --fixture ./sample.mp3 \\
|
|
242
|
+
"(async()=>{ const b = await fetch(fixtureUrl).then(r=>r.arrayBuffer()); return window.App.parseId3(b); })()"
|
|
200
243
|
|
|
201
244
|
The eval body runs under a ~20s in-page execution budget (its own await/setTimeout
|
|
202
245
|
pauses count; --wait only sleeps BEFORE the eval and does not extend it). For a long
|
package/dist/commands/records.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { get, post, put, del } from '../api.js';
|
|
2
|
+
import { get, post, put, patch, del } from '../api.js';
|
|
3
3
|
import { requireConfig } from '../config.js';
|
|
4
4
|
import { bold, muted } from '../colors.js';
|
|
5
5
|
import { run, printList, printResult } from '../helpers/index.js';
|
|
6
6
|
import { confirm } from '../utils.js';
|
|
7
|
+
// All commands hit the app API (https://a.gipity.ai/api/<guid>/records/...),
|
|
8
|
+
// which authorizes the logged-in owner via their Bearer token. (The native
|
|
9
|
+
// Records API is the only records surface that exists server-side; there is no
|
|
10
|
+
// /projects/<guid>/records mirror.)
|
|
7
11
|
export const recordsCommand = new Command('records')
|
|
8
12
|
.description('Manage records');
|
|
9
13
|
recordsCommand
|
|
@@ -12,8 +16,38 @@ recordsCommand
|
|
|
12
16
|
.option('--json', 'Output as JSON')
|
|
13
17
|
.action((opts) => run('List', async () => {
|
|
14
18
|
const config = requireConfig();
|
|
15
|
-
const res = await get(`/
|
|
16
|
-
printList(res.data, opts, 'No tables configured for Records API.', t => `${bold(t.table_name)} ${muted(t.auth_level)} ${muted(`pk=${t.primary_key_column}`)} ${muted(`db=${t.database_name}`)}`);
|
|
19
|
+
const res = await get(`/api/${config.projectGuid}/records-config`);
|
|
20
|
+
printList(res.data, opts, 'No tables configured for Records API. Configure one with `gipity records config <table> --auth <level>`.', t => `${bold(t.table_name)} ${muted(t.auth_level)} ${muted(`pk=${t.primary_key_column}`)} ${muted(`db=${t.database_name}`)}`);
|
|
21
|
+
}));
|
|
22
|
+
recordsCommand
|
|
23
|
+
.command('config <table>')
|
|
24
|
+
.description('Show or set a table\'s Records API config (auth level, search, etc.)')
|
|
25
|
+
.option('--auth <level>', 'Auth level: public (anonymous writes), member (sign-in), or user')
|
|
26
|
+
.option('--searchable <bool>', 'Enable full-text search (true/false)')
|
|
27
|
+
.option('--primary-key <col>', 'Primary key column (default: id)')
|
|
28
|
+
.option('--soft-delete <col>', 'Soft-delete column (pass "none" to clear)')
|
|
29
|
+
.option('--json', 'Output as JSON')
|
|
30
|
+
.action((table, opts) => run('Config', async () => {
|
|
31
|
+
const config = requireConfig();
|
|
32
|
+
const base = `/api/${config.projectGuid}/records/${table}/config`;
|
|
33
|
+
// No setter flags → just show the current config.
|
|
34
|
+
const setting = opts.auth || opts.searchable !== undefined || opts.primaryKey || opts.softDelete;
|
|
35
|
+
if (!setting) {
|
|
36
|
+
const res = await get(base);
|
|
37
|
+
printResult(JSON.stringify(res.data, null, 2), opts, res.data);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const body = {};
|
|
41
|
+
if (opts.auth)
|
|
42
|
+
body.auth_level = opts.auth;
|
|
43
|
+
if (opts.searchable !== undefined)
|
|
44
|
+
body.searchable = /^(true|1|on|yes)$/i.test(String(opts.searchable));
|
|
45
|
+
if (opts.primaryKey)
|
|
46
|
+
body.primary_key_column = opts.primaryKey;
|
|
47
|
+
if (opts.softDelete)
|
|
48
|
+
body.soft_delete_column = opts.softDelete === 'none' ? null : opts.softDelete;
|
|
49
|
+
const res = await patch(base, body);
|
|
50
|
+
printResult(`Configured "${table}": auth=${res.data.auth_level}, searchable=${res.data.searchable}, pk=${res.data.primary_key_column}`, opts, res.data);
|
|
17
51
|
}));
|
|
18
52
|
recordsCommand
|
|
19
53
|
.command('query <table>')
|
|
@@ -35,7 +69,7 @@ recordsCommand
|
|
|
35
69
|
params.set('offset', opts.offset);
|
|
36
70
|
if (opts.fields)
|
|
37
71
|
params.set('fields', opts.fields);
|
|
38
|
-
const res = await get(`/
|
|
72
|
+
const res = await get(`/api/${config.projectGuid}/records/${table}?${params}`);
|
|
39
73
|
if (opts.json) {
|
|
40
74
|
console.log(JSON.stringify(res));
|
|
41
75
|
}
|
|
@@ -54,7 +88,7 @@ recordsCommand
|
|
|
54
88
|
.option('--json', 'Output as JSON')
|
|
55
89
|
.action((table, id, opts) => run('Get', async () => {
|
|
56
90
|
const config = requireConfig();
|
|
57
|
-
const res = await get(`/
|
|
91
|
+
const res = await get(`/api/${config.projectGuid}/records/${table}/${id}`);
|
|
58
92
|
console.log(opts.json ? JSON.stringify(res.data) : JSON.stringify(res.data, null, 2));
|
|
59
93
|
}));
|
|
60
94
|
recordsCommand
|
|
@@ -65,7 +99,7 @@ recordsCommand
|
|
|
65
99
|
.action((table, opts) => run('Create', async () => {
|
|
66
100
|
const config = requireConfig();
|
|
67
101
|
const data = JSON.parse(opts.data);
|
|
68
|
-
const res = await post(`/
|
|
102
|
+
const res = await post(`/api/${config.projectGuid}/records/${table}`, data);
|
|
69
103
|
printResult(`Created: ${JSON.stringify(res.data)}`, opts, res.data);
|
|
70
104
|
}));
|
|
71
105
|
recordsCommand
|
|
@@ -76,7 +110,7 @@ recordsCommand
|
|
|
76
110
|
.action((table, id, opts) => run('Update', async () => {
|
|
77
111
|
const config = requireConfig();
|
|
78
112
|
const data = JSON.parse(opts.data);
|
|
79
|
-
const res = await put(`/
|
|
113
|
+
const res = await put(`/api/${config.projectGuid}/records/${table}/${id}`, data);
|
|
80
114
|
printResult(`Updated: ${JSON.stringify(res.data)}`, opts, res.data);
|
|
81
115
|
}));
|
|
82
116
|
recordsCommand
|
|
@@ -88,7 +122,7 @@ recordsCommand
|
|
|
88
122
|
return;
|
|
89
123
|
}
|
|
90
124
|
const config = requireConfig();
|
|
91
|
-
await del(`/
|
|
125
|
+
await del(`/api/${config.projectGuid}/records/${table}/${id}`);
|
|
92
126
|
printResult('Deleted.', { json: false });
|
|
93
127
|
}));
|
|
94
128
|
//# sourceMappingURL=records.js.map
|