gipity 1.0.405 → 1.0.407
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/api.js +55 -9
- package/dist/auth.js +27 -6
- package/dist/commands/claude.js +10 -0
- package/dist/commands/credits.js +12 -32
- package/dist/commands/fn.js +11 -3
- package/dist/commands/job.js +9 -0
- package/dist/commands/logs.js +7 -0
- package/dist/commands/notify.js +7 -4
- package/dist/commands/page-inspect.js +25 -18
- package/dist/commands/page-screenshot.js +10 -8
- package/dist/commands/relay.js +8 -0
- package/dist/commands/sandbox.js +5 -0
- package/dist/commands/secrets.js +73 -0
- package/dist/commands/workflow.js +18 -6
- package/dist/helpers/command.js +9 -1
- package/dist/index.js +6 -2
- package/dist/knowledge.js +2 -2
- package/dist/relay/setup.js +3 -0
- package/dist/sync.js +7 -5
- package/dist/updater/shim.js +3 -0
- package/package.json +2 -2
package/dist/api.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Readable } from 'stream';
|
|
2
2
|
import * as tar from 'tar-stream';
|
|
3
|
-
import { getAuth, refreshTokenIfNeeded } from './auth.js';
|
|
3
|
+
import { getAuth, refreshTokenIfNeeded, forceRefreshAccessToken } from './auth.js';
|
|
4
4
|
import { resolveApiBase, requireConfig, saveConfig } from './config.js';
|
|
5
5
|
import { clientHeaders } from './client-context.js';
|
|
6
6
|
export class ApiError extends Error {
|
|
@@ -53,6 +53,40 @@ async function bearerToken() {
|
|
|
53
53
|
throw new Error('Not authenticated. Run: gipity login');
|
|
54
54
|
return auth.accessToken;
|
|
55
55
|
}
|
|
56
|
+
/** True when a long-lived agent API token (GIPITY_TOKEN) is in play. Such a token
|
|
57
|
+
* is static — it can't be refreshed — so the 401 self-heal path must skip it and
|
|
58
|
+
* the "run: gipity login" hint must not be shown (that's the wrong recovery for
|
|
59
|
+
* env-token callers). */
|
|
60
|
+
function usingEnvToken() {
|
|
61
|
+
return !!process.env.GIPITY_TOKEN?.trim();
|
|
62
|
+
}
|
|
63
|
+
// A transient 401 on a session token means the access token the server just saw
|
|
64
|
+
// is dead NOW — it lapsed at the refresh boundary, a sibling `gipity` process
|
|
65
|
+
// sharing this auth.json rotated the single-use refresh token out from under us,
|
|
66
|
+
// or clock skew put us past expiry while the 5-min refresh buffer still read
|
|
67
|
+
// "fresh". refreshTokenIfNeeded's fast-path skips a locally-fresh token, so we
|
|
68
|
+
// force a refresh and replay the call once. This self-heals the intermittent
|
|
69
|
+
// "Session expired" that surfaced on `page eval` (a kickoff POST plus a long GET
|
|
70
|
+
// poll loop crosses the refresh boundary many times). These two helpers carry
|
|
71
|
+
// that behavior to EVERY authenticated entry point — including the streaming and
|
|
72
|
+
// download paths (downloadStream → sync, postForTarEntries → page screenshot)
|
|
73
|
+
// that bypass request() — so one raced 401 can't hard-fail them either.
|
|
74
|
+
/** Decide whether to replay an authenticated call after a 401: force one token
|
|
75
|
+
* refresh and return true if the caller should retry. False for an env-token
|
|
76
|
+
* caller (static, unrefreshable), an already-retried call, a non-401, or an
|
|
77
|
+
* unrecoverable session — the caller then surfaces the error. */
|
|
78
|
+
async function shouldRetryAfter401(status, retried) {
|
|
79
|
+
if (status !== 401 || retried || usingEnvToken())
|
|
80
|
+
return false;
|
|
81
|
+
return forceRefreshAccessToken();
|
|
82
|
+
}
|
|
83
|
+
/** Append a re-login hint to a 401 message for session users. A 401 that
|
|
84
|
+
* survived the refresh-and-retry is an unrecoverable session (the refresh token
|
|
85
|
+
* is gone too); env-token callers authenticate differently, so don't misdirect
|
|
86
|
+
* them. */
|
|
87
|
+
function with401Hint(status, message) {
|
|
88
|
+
return status === 401 && !usingEnvToken() ? `${message} — run: gipity login` : message;
|
|
89
|
+
}
|
|
56
90
|
async function getHeaders() {
|
|
57
91
|
return {
|
|
58
92
|
...clientHeaders(),
|
|
@@ -74,7 +108,7 @@ export async function getAuthHeader() {
|
|
|
74
108
|
const headers = await getHeaders();
|
|
75
109
|
return headers.Authorization;
|
|
76
110
|
}
|
|
77
|
-
async function request(method, path, body) {
|
|
111
|
+
async function request(method, path, body, retrying = false) {
|
|
78
112
|
const headers = await getHeaders();
|
|
79
113
|
const url = `${baseUrl()}${path}`;
|
|
80
114
|
const res = await fetchWithTimeout(url, {
|
|
@@ -82,10 +116,13 @@ async function request(method, path, body) {
|
|
|
82
116
|
headers,
|
|
83
117
|
body: body ? JSON.stringify(body) : undefined,
|
|
84
118
|
}, REQUEST_TIMEOUT_MS, `${method} ${path}`);
|
|
119
|
+
if (await shouldRetryAfter401(res.status, retrying)) {
|
|
120
|
+
return request(method, path, body, true);
|
|
121
|
+
}
|
|
85
122
|
if (!res.ok) {
|
|
86
123
|
const json = await res.json().catch(() => ({ error: { code: 'UNKNOWN', message: res.statusText } }));
|
|
87
124
|
const err = json.error || { code: 'UNKNOWN', message: res.statusText };
|
|
88
|
-
throw new ApiError(res.status, err.code, err.message, json.data);
|
|
125
|
+
throw new ApiError(res.status, err.code, with401Hint(res.status, err.message), json.data);
|
|
89
126
|
}
|
|
90
127
|
return res.json();
|
|
91
128
|
}
|
|
@@ -97,7 +134,7 @@ export function post(path, body) {
|
|
|
97
134
|
}
|
|
98
135
|
/** POST JSON and consume the response as a tar stream, returning each entry as
|
|
99
136
|
* { name, buffer } in arrival order. Non-2xx responses are parsed as JSON errors. */
|
|
100
|
-
export async function postForTarEntries(path, body) {
|
|
137
|
+
export async function postForTarEntries(path, body, retried = false) {
|
|
101
138
|
const headers = await getHeaders();
|
|
102
139
|
const url = `${baseUrl()}${path}`;
|
|
103
140
|
const res = await fetch(url, {
|
|
@@ -105,10 +142,13 @@ export async function postForTarEntries(path, body) {
|
|
|
105
142
|
headers,
|
|
106
143
|
body: body ? JSON.stringify(body) : undefined,
|
|
107
144
|
});
|
|
145
|
+
if (await shouldRetryAfter401(res.status, retried)) {
|
|
146
|
+
return postForTarEntries(path, body, true);
|
|
147
|
+
}
|
|
108
148
|
if (!res.ok) {
|
|
109
149
|
const json = await res.json().catch(() => ({ error: { code: 'UNKNOWN', message: res.statusText } }));
|
|
110
150
|
const err = json.error || { code: 'UNKNOWN', message: res.statusText };
|
|
111
|
-
throw new ApiError(res.status, err.code, err.message, json.data);
|
|
151
|
+
throw new ApiError(res.status, err.code, with401Hint(res.status, err.message), json.data);
|
|
112
152
|
}
|
|
113
153
|
if (!res.body)
|
|
114
154
|
throw new ApiError(500, 'EMPTY_RESPONSE', 'Server returned no body');
|
|
@@ -158,18 +198,21 @@ export async function sendMessage(message) {
|
|
|
158
198
|
return res.data.content;
|
|
159
199
|
}
|
|
160
200
|
/** Download a file as raw bytes (no JSON parsing) */
|
|
161
|
-
export async function download(path) {
|
|
201
|
+
export async function download(path, retried = false) {
|
|
162
202
|
const url = `${baseUrl()}${path}`;
|
|
163
203
|
const res = await fetch(url, {
|
|
164
204
|
headers: { ...clientHeaders(), 'Authorization': `Bearer ${await bearerToken()}` },
|
|
165
205
|
});
|
|
206
|
+
if (await shouldRetryAfter401(res.status, retried)) {
|
|
207
|
+
return download(path, true);
|
|
208
|
+
}
|
|
166
209
|
if (!res.ok) {
|
|
167
|
-
throw new ApiError(res.status, 'DOWNLOAD_ERROR', `Download failed: ${res.statusText}`);
|
|
210
|
+
throw new ApiError(res.status, 'DOWNLOAD_ERROR', with401Hint(res.status, `Download failed: ${res.statusText}`));
|
|
168
211
|
}
|
|
169
212
|
return Buffer.from(await res.arrayBuffer());
|
|
170
213
|
}
|
|
171
214
|
/** Download a response as a Node.js Readable stream */
|
|
172
|
-
export async function downloadStream(path) {
|
|
215
|
+
export async function downloadStream(path, retried = false) {
|
|
173
216
|
const { Readable } = await import('stream');
|
|
174
217
|
const url = `${baseUrl()}${path}`;
|
|
175
218
|
// Bound time-to-first-byte only: abort if no RESPONSE within the header window,
|
|
@@ -194,8 +237,11 @@ export async function downloadStream(path) {
|
|
|
194
237
|
finally {
|
|
195
238
|
clearTimeout(headerTimer);
|
|
196
239
|
}
|
|
240
|
+
if (await shouldRetryAfter401(res.status, retried)) {
|
|
241
|
+
return downloadStream(path, true);
|
|
242
|
+
}
|
|
197
243
|
if (!res.ok) {
|
|
198
|
-
throw new ApiError(res.status, 'DOWNLOAD_ERROR', `Download failed: ${res.statusText}`);
|
|
244
|
+
throw new ApiError(res.status, 'DOWNLOAD_ERROR', with401Hint(res.status, `Download failed: ${res.statusText}`));
|
|
199
245
|
}
|
|
200
246
|
return Readable.fromWeb(res.body);
|
|
201
247
|
}
|
package/dist/auth.js
CHANGED
|
@@ -149,15 +149,22 @@ export async function acquireRefreshLock() {
|
|
|
149
149
|
* others wait, re-read the file, and ADOPT the freshly-rotated token. If the lock
|
|
150
150
|
* can't be had in time we still try (re-reading first), so this never hangs a command.
|
|
151
151
|
* Stays void / never throws / never clears auth: a genuine dead token still flows to
|
|
152
|
-
* the caller's existing 401 path (which messages "run: gipity login").
|
|
153
|
-
|
|
152
|
+
* the caller's existing 401 path (which messages "run: gipity login").
|
|
153
|
+
*
|
|
154
|
+
* `force` is for the post-401 recovery path: when the server has just REJECTED the
|
|
155
|
+
* access token, the local 5-min freshness check is no longer trustworthy (clock
|
|
156
|
+
* skew put us past expiry while the buffer still read "fresh", or a sibling rotated
|
|
157
|
+
* the refresh token out from under us). Force skips every "looks fresh, skip the
|
|
158
|
+
* refresh" shortcut and performs the refresh round-trip unconditionally, so a token
|
|
159
|
+
* the server considers dead actually gets replaced instead of re-sent. */
|
|
160
|
+
export async function refreshTokenIfNeeded(force = false) {
|
|
154
161
|
const auth = readAuthFresh(); // never the cache — a sibling may have rotated
|
|
155
162
|
if (!auth)
|
|
156
163
|
return; // not logged in - caller throws the clean error
|
|
157
164
|
cached = auth;
|
|
158
165
|
const buffer = 5 * 60 * 1000; // refresh 5 min before the access token lapses
|
|
159
166
|
const fresh = (a) => Date.now() <= new Date(a.expiresAt).getTime() - buffer;
|
|
160
|
-
if (fresh(auth))
|
|
167
|
+
if (!force && fresh(auth))
|
|
161
168
|
return; // fast path: token still good, no lock needed
|
|
162
169
|
// If the refresh token itself has expired, re-login is genuinely required; leave the
|
|
163
170
|
// expired auth in place so the caller's existing 401 path prompts `gipity login`.
|
|
@@ -167,9 +174,11 @@ export async function refreshTokenIfNeeded() {
|
|
|
167
174
|
// Serialize with sibling processes so we don't race the single-use refresh token.
|
|
168
175
|
const release = await acquireRefreshLock();
|
|
169
176
|
try {
|
|
170
|
-
// Re-check under the lock: a sibling may have refreshed while we waited.
|
|
177
|
+
// Re-check under the lock: a sibling may have refreshed while we waited. Under
|
|
178
|
+
// force we skip this adopt — a locally-"fresh" token is exactly what the server
|
|
179
|
+
// just rejected, so we must actually refresh rather than re-adopt it.
|
|
171
180
|
const held = readAuthFresh();
|
|
172
|
-
if (held && fresh(held)) {
|
|
181
|
+
if (!force && held && fresh(held)) {
|
|
173
182
|
cached = held;
|
|
174
183
|
return;
|
|
175
184
|
}
|
|
@@ -177,7 +186,7 @@ export async function refreshTokenIfNeeded() {
|
|
|
177
186
|
const apiBase = resolveApiBase();
|
|
178
187
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
179
188
|
const cur = readAuthFresh(); // a sibling may have just refreshed for us
|
|
180
|
-
if (cur && fresh(cur)) {
|
|
189
|
+
if (!force && cur && fresh(cur)) {
|
|
181
190
|
cached = cur;
|
|
182
191
|
return;
|
|
183
192
|
}
|
|
@@ -225,4 +234,16 @@ export async function refreshTokenIfNeeded() {
|
|
|
225
234
|
release();
|
|
226
235
|
}
|
|
227
236
|
}
|
|
237
|
+
/** Recover from a live 401 on a session token: force a refresh round-trip
|
|
238
|
+
* (bypassing the local freshness fast-path, since the server has already
|
|
239
|
+
* rejected the token the local clock still thinks is fine) and report whether
|
|
240
|
+
* we now hold a usable, unexpired access token. The API layer calls this once
|
|
241
|
+
* on a 401 and retries the request only when it returns true — turning a
|
|
242
|
+
* transient/raced "Session expired" into a self-healing retry instead of a hard
|
|
243
|
+
* failure. Returns false when the refresh token itself is gone (genuine
|
|
244
|
+
* re-login required), so the caller surfaces the error instead of looping. */
|
|
245
|
+
export async function forceRefreshAccessToken() {
|
|
246
|
+
await refreshTokenIfNeeded(true);
|
|
247
|
+
return !accessTokenExpired();
|
|
248
|
+
}
|
|
228
249
|
//# sourceMappingURL=auth.js.map
|
package/dist/commands/claude.js
CHANGED
|
@@ -849,6 +849,16 @@ export const claudeCommand = new Command('claude')
|
|
|
849
849
|
cwd: process.cwd(),
|
|
850
850
|
env: childEnv,
|
|
851
851
|
});
|
|
852
|
+
// A spawn failure (missing/non-executable binary) arrives asynchronously via
|
|
853
|
+
// 'error', which the surrounding try/catch can't catch - without this handler
|
|
854
|
+
// Node crashes with a raw stack trace. claude is normally installed above, so
|
|
855
|
+
// this is a defensive fallback.
|
|
856
|
+
child.on('error', (err) => {
|
|
857
|
+
console.error(`\n ${clrError(err.code === 'ENOENT'
|
|
858
|
+
? `Claude Code not found. Install it: npm install -g ${CLAUDE_PACKAGE}`
|
|
859
|
+
: `Failed to launch Claude Code: ${err.message}`)}`);
|
|
860
|
+
process.exit(1);
|
|
861
|
+
});
|
|
852
862
|
child.on('exit', (code) => {
|
|
853
863
|
const doneLine = `Done (${formatElapsed(Date.now() - runStart)})`;
|
|
854
864
|
if (nonInteractive)
|
package/dist/commands/credits.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { get } from '../api.js';
|
|
4
|
-
import { brand, muted
|
|
4
|
+
import { brand, muted } from '../colors.js';
|
|
5
5
|
import { run, printList } from '../helpers/index.js';
|
|
6
6
|
const PRICING_URL = 'https://prompt.gipity.ai/pricing';
|
|
7
7
|
function openInBrowser(url) {
|
|
@@ -12,10 +12,17 @@ function openInBrowser(url) {
|
|
|
12
12
|
process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]] :
|
|
13
13
|
['xdg-open', [url]];
|
|
14
14
|
try {
|
|
15
|
-
spawn(cmd, args, { stdio: 'ignore', detached: true })
|
|
15
|
+
const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
|
|
16
|
+
// A missing/non-executable launcher (ENOENT, or EACCES on WSL/minimal Linux
|
|
17
|
+
// where xdg-open isn't runnable) is reported ASYNCHRONOUSLY via an 'error'
|
|
18
|
+
// event - not a throw - so the try/catch alone can't stop it. With no
|
|
19
|
+
// listener Node escalates it to an unhandled 'error' and crashes the whole
|
|
20
|
+
// process. Swallow it; the caller already printed the URL as the fallback.
|
|
21
|
+
child.on('error', () => { });
|
|
22
|
+
child.unref();
|
|
16
23
|
}
|
|
17
24
|
catch {
|
|
18
|
-
//
|
|
25
|
+
// Synchronous spawn failure - caller prints the URL.
|
|
19
26
|
}
|
|
20
27
|
}
|
|
21
28
|
export const creditsCommand = new Command('credits')
|
|
@@ -50,40 +57,13 @@ creditsCommand
|
|
|
50
57
|
openInBrowser(PRICING_URL);
|
|
51
58
|
console.log(muted("If your browser didn't open automatically, copy the URL above."));
|
|
52
59
|
}));
|
|
53
|
-
creditsCommand
|
|
54
|
-
.command('status')
|
|
55
|
-
.description('Show billing/Stripe configuration health (admin only)')
|
|
56
|
-
.option('--json', 'Output as JSON')
|
|
57
|
-
// optsWithGlobals(): the parent `credits` command also declares --json, which
|
|
58
|
-
// commander binds to the parent - so read merged opts to see the flag here.
|
|
59
|
-
.action((_opts, cmd) => run('Billing status', async () => {
|
|
60
|
-
const opts = cmd.optsWithGlobals();
|
|
61
|
-
const res = await get('/admin/billing-status');
|
|
62
|
-
const s = res.data;
|
|
63
|
-
if (opts.json) {
|
|
64
|
-
console.log(JSON.stringify(s));
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
const mark = (ok) => (ok ? success('✓') : clrError('✗'));
|
|
68
|
-
console.log(`Stripe mode: ${brand(s.mode)}`);
|
|
69
|
-
console.log(`Secret key: ${mark(s.secretKeyConfigured)}`);
|
|
70
|
-
console.log(`Webhook secret: ${mark(s.webhookSecretConfigured)} ${muted(`endpoint ${s.webhookPath}`)}`);
|
|
71
|
-
console.log(`Credit packs: ${mark(s.packsConfigured)}${s.packsError ? ` ${clrError(s.packsError)}` : ''}`);
|
|
72
|
-
for (const p of s.packs) {
|
|
73
|
-
console.log(` ${p.name} ${muted(p.priceId)}${p.hidden ? muted(' [hidden]') : ''}`);
|
|
74
|
-
}
|
|
75
|
-
console.log(`Test pack ($1): ${mark(s.testPackConfigured)} ${muted('set STRIPE_PACK_TEST_PRICE_ID to enable a cheap purchase smoke test')}`);
|
|
76
|
-
console.log('Subscription plans:');
|
|
77
|
-
for (const pl of s.subscriptionPlans) {
|
|
78
|
-
console.log(` ${pl.name} (${pl.tier}) ${mark(pl.priceIdConfigured)}`);
|
|
79
|
-
}
|
|
80
|
-
}));
|
|
81
60
|
creditsCommand
|
|
82
61
|
.command('usage')
|
|
83
62
|
.description('Show recent usage')
|
|
84
63
|
.option('--limit <n>', 'Number of entries', '20')
|
|
85
64
|
.option('--json', 'Output as JSON')
|
|
86
|
-
// optsWithGlobals(): parent `credits` also declares --json
|
|
65
|
+
// optsWithGlobals(): the parent `credits` command also declares --json, which
|
|
66
|
+
// commander binds to the parent - so read merged opts to see the flag here.
|
|
87
67
|
.action((_opts, cmd) => run('Usage', async () => {
|
|
88
68
|
const opts = cmd.optsWithGlobals();
|
|
89
69
|
const limit = parseInt(opts.limit, 10) || 20;
|
package/dist/commands/fn.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { get, post, del } from '../api.js';
|
|
3
3
|
import { requireConfig } from '../config.js';
|
|
4
|
-
import { error as clrError, bold, muted, success } from '../colors.js';
|
|
4
|
+
import { error as clrError, bold, muted, success, warning } from '../colors.js';
|
|
5
5
|
import { run, printList, emitField } from '../helpers/index.js';
|
|
6
6
|
import { confirm } from '../utils.js';
|
|
7
7
|
export const fnCommand = new Command('fn')
|
|
@@ -30,8 +30,16 @@ fnCommand
|
|
|
30
30
|
const dur = log.duration_ms != null ? `${log.duration_ms}ms` : '?';
|
|
31
31
|
const ts = new Date(log.created_at).toLocaleString();
|
|
32
32
|
const statusColor = log.status === 'success' ? success : log.status === 'error' ? clrError : muted;
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
let line = `${statusColor(log.status)} ${dur} ${muted(log.trigger_type || 'http')} ${muted(ts)}`;
|
|
34
|
+
if (log.error_message)
|
|
35
|
+
line += `\n${clrError(`error: ${log.error_message}`)}`;
|
|
36
|
+
// Captured console.log/warn/error output for this invocation.
|
|
37
|
+
for (const entry of (log.logs ?? [])) {
|
|
38
|
+
const lvlColor = entry.level === 'error' ? clrError : entry.level === 'warn' ? warning : muted;
|
|
39
|
+
const tag = entry.level === 'log' ? '' : `${entry.level}: `;
|
|
40
|
+
line += `\n ${lvlColor(`${tag}${entry.message}`)}`;
|
|
41
|
+
}
|
|
42
|
+
return line;
|
|
35
43
|
});
|
|
36
44
|
}));
|
|
37
45
|
fnCommand
|
package/dist/commands/job.js
CHANGED
|
@@ -240,6 +240,15 @@ jobCommand
|
|
|
240
240
|
}
|
|
241
241
|
const runArgs = ['run', ...sharedArgs, '--entrypoint', 'sh', opts.image, '-c', shellCmd];
|
|
242
242
|
const child = spawn('docker', runArgs, { stdio: 'inherit' });
|
|
243
|
+
// A missing `docker` binary is reported asynchronously via 'error' (not a
|
|
244
|
+
// throw), so a try/catch can't stop it - without this handler Node crashes
|
|
245
|
+
// with a raw stack trace instead of a clear message.
|
|
246
|
+
child.on('error', (err) => {
|
|
247
|
+
console.error(clrError(err.code === 'ENOENT'
|
|
248
|
+
? 'Docker not found. Install Docker to use `gipity job run-local`.'
|
|
249
|
+
: `Failed to launch docker: ${err.message}`));
|
|
250
|
+
process.exit(1);
|
|
251
|
+
});
|
|
243
252
|
child.on('exit', (code) => {
|
|
244
253
|
if (code === 0)
|
|
245
254
|
console.error(muted(`[run-local] ${success('done')}`));
|
package/dist/commands/logs.js
CHANGED
|
@@ -31,6 +31,13 @@ logsCommand
|
|
|
31
31
|
const trigger = muted(log.trigger_type.padEnd(8));
|
|
32
32
|
const err = log.error_message ? ` ${clrError(`"${log.error_message}"`)}` : '';
|
|
33
33
|
console.log(`${muted(time)} ${status} ${dur} ${trigger}${err}`);
|
|
34
|
+
// Captured console.log/warn/error output for this invocation, indented
|
|
35
|
+
// under its summary line. Empty for calls that printed nothing.
|
|
36
|
+
for (const line of log.logs ?? []) {
|
|
37
|
+
const lvlColor = line.level === 'error' ? clrError : line.level === 'warn' ? warning : muted;
|
|
38
|
+
const tag = line.level === 'log' ? '' : `${line.level}: `;
|
|
39
|
+
console.log(`${muted(' │')} ${lvlColor(`${tag}${line.message}`)}`);
|
|
40
|
+
}
|
|
34
41
|
}
|
|
35
42
|
}));
|
|
36
43
|
logsCommand
|
package/dist/commands/notify.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { get, post } from '../api.js';
|
|
3
|
-
import {
|
|
3
|
+
import { resolveProjectContext } from '../config.js';
|
|
4
4
|
import { bold, muted, success, warning } from '../colors.js';
|
|
5
5
|
import { run } from '../helpers/index.js';
|
|
6
6
|
export const notifyCommand = new Command('notify')
|
|
@@ -12,9 +12,10 @@ notifyCommand
|
|
|
12
12
|
.option('--title <text>', 'Notification title', 'Test notification')
|
|
13
13
|
.option('--body <text>', 'Notification body', 'Hello from Gipity Notify 👋')
|
|
14
14
|
.option('--url <url>', 'URL to open when the notification is clicked')
|
|
15
|
+
.option('--project <guid-or-slug>', 'Target a specific project instead of cwd / Home')
|
|
15
16
|
.option('--json', 'Output raw JSON')
|
|
16
17
|
.action((opts) => run('Notify', async () => {
|
|
17
|
-
const config =
|
|
18
|
+
const { config } = await resolveProjectContext({ projectOverride: opts.project });
|
|
18
19
|
const to = opts.to === 'all' ? 'all' : (opts.to.includes(',') ? opts.to.split(',').map((s) => s.trim()) : opts.to);
|
|
19
20
|
const notification = { title: opts.title, body: opts.body };
|
|
20
21
|
if (opts.url)
|
|
@@ -37,9 +38,10 @@ notifyCommand
|
|
|
37
38
|
notifyCommand
|
|
38
39
|
.command('subs')
|
|
39
40
|
.description('Show how many push subscriptions this app has, per user')
|
|
41
|
+
.option('--project <guid-or-slug>', 'Target a specific project instead of cwd / Home')
|
|
40
42
|
.option('--json', 'Output raw JSON')
|
|
41
43
|
.action((opts) => run('Notify', async () => {
|
|
42
|
-
const config =
|
|
44
|
+
const { config } = await resolveProjectContext({ projectOverride: opts.project });
|
|
43
45
|
const res = await get(`/api/${config.projectGuid}/services/notify/subs`);
|
|
44
46
|
if (opts.json) {
|
|
45
47
|
console.log(JSON.stringify(res.data));
|
|
@@ -54,9 +56,10 @@ notifyCommand
|
|
|
54
56
|
.command('rm')
|
|
55
57
|
.description("Remove a user's push subscriptions (e.g. on account deletion)")
|
|
56
58
|
.requiredOption('--user <guid>', 'User guid whose subscriptions to remove')
|
|
59
|
+
.option('--project <guid-or-slug>', 'Target a specific project instead of cwd / Home')
|
|
57
60
|
.option('--json', 'Output raw JSON')
|
|
58
61
|
.action((opts) => run('Notify', async () => {
|
|
59
|
-
const config =
|
|
62
|
+
const { config } = await resolveProjectContext({ projectOverride: opts.project });
|
|
60
63
|
const res = await post(`/api/${config.projectGuid}/services/notify/remove`, { user_guid: opts.user });
|
|
61
64
|
if (opts.json) {
|
|
62
65
|
console.log(JSON.stringify(res.data));
|
|
@@ -64,14 +64,21 @@ export const pageInspectCommand = new Command('inspect')
|
|
|
64
64
|
};
|
|
65
65
|
const res = await post(`/tools/browser/inspect`, inspectBody);
|
|
66
66
|
const b = res.data;
|
|
67
|
-
// ──
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
67
|
+
// ── Move resource-load failures out of the console, where they're noise ──
|
|
68
|
+
// A sub-resource that returns an HTTP 4xx/5xx is reported twice: once in
|
|
69
|
+
// `failedResources` (CDP network layer — carries the full URL, method, and
|
|
70
|
+
// status) and once as a generic, URL-less console echo ("Failed to load
|
|
71
|
+
// resource: the server responded with a status of 404 ()"). The echo names
|
|
72
|
+
// nothing, so it can't be triaged on its own and only duplicates — worse —
|
|
73
|
+
// what `failedResources` already says with a URL. Drop one echo per failed
|
|
74
|
+
// resource we actually have, so every attributed failure (a real app 404 AND
|
|
75
|
+
// the platform's own injected-SDK telemetry-beacon 404, which 404s on
|
|
76
|
+
// essentially every deployed page) is surfaced exactly once, under Failed
|
|
77
|
+
// resources, with its URL — never as a bare, unattributable console line.
|
|
78
|
+
// Count BEFORE stripping the platform log endpoints below, so their echoes go
|
|
79
|
+
// too. Any SURPLUS echo (a failure the network drain missed, leaving
|
|
80
|
+
// failedResources short) is KEPT — a real problem is never hidden just because
|
|
81
|
+
// we couldn't name it.
|
|
75
82
|
const isPlatformLog = (entry) => {
|
|
76
83
|
const urlPart = entry.replace(/\s*\([^)]*\)\s*$/, '');
|
|
77
84
|
try {
|
|
@@ -82,22 +89,22 @@ export const pageInspectCommand = new Command('inspect')
|
|
|
82
89
|
return false;
|
|
83
90
|
}
|
|
84
91
|
};
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// "Failed to load resource" console error. Drop one per platform failure —
|
|
89
|
-
// the text is identical, so removing by count is exact and any genuine app
|
|
90
|
-
// 404 keeps its own (indistinguishable) line.
|
|
91
|
-
let platformConsoleToDrop = platformFailures.length;
|
|
92
|
-
if (platformConsoleToDrop > 0) {
|
|
92
|
+
let resourceEchoesToDrop = (b.failedResources || []).length;
|
|
93
|
+
if (resourceEchoesToDrop > 0) {
|
|
94
|
+
const isHttpStatusResourceError = (l) => /^error:\s*Failed to load resource: the server responded with a status of \d{3}/i.test(l);
|
|
93
95
|
b.console = (b.console || []).filter((l) => {
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
+
if (resourceEchoesToDrop > 0 && isHttpStatusResourceError(l)) {
|
|
97
|
+
resourceEchoesToDrop--;
|
|
96
98
|
return false;
|
|
97
99
|
}
|
|
98
100
|
return true;
|
|
99
101
|
});
|
|
100
102
|
}
|
|
103
|
+
// The injected analytics SDK POSTs to `/log/{traffic,error}` on the Gipity
|
|
104
|
+
// host; a failure there is platform infrastructure, not the app's, so strip it
|
|
105
|
+
// from the failed-resource list entirely (its console echo is already gone,
|
|
106
|
+
// dropped above with every other attributed failure).
|
|
107
|
+
b.failedResources = (b.failedResources || []).filter((r) => !isPlatformLog(r));
|
|
101
108
|
// Pull message-less cross-origin "Script error." lines out first. They carry
|
|
102
109
|
// no source/stack, so they're never actionable as app-code defects, and on a
|
|
103
110
|
// Gipity-deployed page the platform's own injected SDK is itself a
|
|
@@ -102,7 +102,7 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
102
102
|
// is applied in the merge instead.
|
|
103
103
|
.option('--post-load-delay <ms>', 'Delay after DOMContentLoaded before capture, in ms (default: 1000)')
|
|
104
104
|
.option('--action <js>', 'Run JS in the page before capturing — e.g. click a button to enter a state ("document.getElementById(\'play\').click()"). Runs after the post-load delay, then settles again before the shot.')
|
|
105
|
-
.option('--full', 'Capture the full scrollable page (default: viewport only)')
|
|
105
|
+
.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.')
|
|
106
106
|
.option('-o, --output <file>', 'Output path (single viewport only; default .gipity/screenshots/ss-<host>-<timestamp>.png)')
|
|
107
107
|
.option('--device <names>', `Viewport preset(s): ${Object.keys(DEVICE_PRESETS).join(', ')} (comma-separated or repeat flag)`, appendOption, [])
|
|
108
108
|
.option('--viewport <dims>', 'Raw viewport(s): WxH or WxH@dpr (comma-separated or repeat flag)', appendOption, [])
|
|
@@ -242,16 +242,18 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
242
242
|
console.log(`${label('Screenshot file')} ${success(savedFiles[i])}`);
|
|
243
243
|
}
|
|
244
244
|
}));
|
|
245
|
-
// `screenshot` captures the page AFTER load + settle (+ optional --action).
|
|
246
|
-
//
|
|
247
|
-
// --selector and get an unknown-option detour)
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
245
|
+
// `screenshot` captures the page AFTER load + settle (+ optional --action). There
|
|
246
|
+
// is no scroll-to-a-position or wait-for-a-selector lever (agents reach for
|
|
247
|
+
// --scroll/--selector and get an unknown-option detour) — but `--full` does walk
|
|
248
|
+
// the page top→bottom→top before the shot so scroll-reveal content paints in.
|
|
249
|
+
// State the supported levers right here, so the help (rendered on any bad flag,
|
|
250
|
+
// and this 'after' block survives `| tail`/`| grep`) ends the hunt in one shot.
|
|
251
|
+
// --action covers "click, then shoot"; --full + crop covers off-screen regions
|
|
252
|
+
// (and triggers reveals); `page eval` reads data without a picture.
|
|
251
253
|
pageScreenshotCommand.addHelpText('after', `
|
|
252
254
|
Examples:
|
|
253
255
|
gipity page screenshot "https://dev.gipity.ai/me/app/"
|
|
254
|
-
gipity page screenshot "https://dev.gipity.ai/me/app/" --full # whole scrollable page
|
|
256
|
+
gipity page screenshot "https://dev.gipity.ai/me/app/" --full # whole scrollable page (scroll-reveal sections triggered)
|
|
255
257
|
gipity page screenshot "https://dev.gipity.ai/me/app/" --device mobile,desktop
|
|
256
258
|
gipity page screenshot "https://dev.gipity.ai/me/app/" \\
|
|
257
259
|
--action "document.getElementById('play').click()" # capture an in-game frame
|
package/dist/commands/relay.js
CHANGED
|
@@ -302,6 +302,14 @@ relayCommand
|
|
|
302
302
|
process.exit(1);
|
|
303
303
|
}
|
|
304
304
|
const child = spawn(tailCmd, ['-f', '-n', '0', path], { stdio: 'inherit' });
|
|
305
|
+
// `tail` missing surfaces asynchronously via 'error' (not a throw); without
|
|
306
|
+
// this handler Node would crash with a raw stack trace.
|
|
307
|
+
child.on('error', (err) => {
|
|
308
|
+
console.error(clrError(err.code === 'ENOENT'
|
|
309
|
+
? "`tail` not found - can't follow the log on this system."
|
|
310
|
+
: `Failed to follow log: ${err.message}`));
|
|
311
|
+
process.exit(1);
|
|
312
|
+
});
|
|
305
313
|
process.on('SIGINT', () => child.kill('SIGINT'));
|
|
306
314
|
child.on('exit', code => process.exit(code ?? 0));
|
|
307
315
|
}
|
package/dist/commands/sandbox.js
CHANGED
|
@@ -190,6 +190,11 @@ GCC/Rust).
|
|
|
190
190
|
if (res.data.autoMirrorSkipped) {
|
|
191
191
|
console.error(dim(`Note: ${res.data.autoMirrorSkipped.reason}`));
|
|
192
192
|
}
|
|
193
|
+
if (res.data.mirrorWarnings && res.data.mirrorWarnings.length > 0) {
|
|
194
|
+
console.error(dim(`Note: ${res.data.mirrorWarnings.length} project file(s) could not be mirrored into the sandbox and were skipped:`));
|
|
195
|
+
for (const w of res.data.mirrorWarnings)
|
|
196
|
+
console.error(dim(` - ${w}`));
|
|
197
|
+
}
|
|
193
198
|
if (res.data.stdout)
|
|
194
199
|
console.log(res.data.stdout);
|
|
195
200
|
if (res.data.stderr)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { get, put, del } from '../api.js';
|
|
3
|
+
import { resolveProjectContext } from '../config.js';
|
|
4
|
+
import { bold, muted, success } from '../colors.js';
|
|
5
|
+
import { run } from '../helpers/index.js';
|
|
6
|
+
/** Build the query string for a scope. Account scope is project-independent;
|
|
7
|
+
* project scope needs the linked app's guid. */
|
|
8
|
+
async function scopeQuery(opts) {
|
|
9
|
+
if (opts.account)
|
|
10
|
+
return 'scope=account';
|
|
11
|
+
const { config } = await resolveProjectContext({ projectOverride: opts.project });
|
|
12
|
+
return `scope=project&app_guid=${config.projectGuid}`;
|
|
13
|
+
}
|
|
14
|
+
export const secretsCommand = new Command('secrets')
|
|
15
|
+
.description('Encrypted secrets (API keys, tokens) for your app — read in functions via secrets.get("NAME")');
|
|
16
|
+
secretsCommand
|
|
17
|
+
.command('list')
|
|
18
|
+
.alias('ls')
|
|
19
|
+
.description('List secret names (never values). Project secrets by default; --account for account-wide')
|
|
20
|
+
.option('--account', 'Account-wide secrets shared across all your projects')
|
|
21
|
+
.option('--project <guid-or-slug>', 'Target a specific project instead of cwd / Home')
|
|
22
|
+
.option('--json', 'Output raw JSON')
|
|
23
|
+
.action((opts) => run('Secrets', async () => {
|
|
24
|
+
const res = await get(`/secrets?${await scopeQuery(opts)}`);
|
|
25
|
+
if (opts.json) {
|
|
26
|
+
console.log(JSON.stringify(res.data));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const scope = opts.account ? 'account-wide' : 'this project';
|
|
30
|
+
if (res.data.length === 0) {
|
|
31
|
+
console.log(muted(`No ${scope} secrets yet. Add one with: gipity secrets set NAME VALUE${opts.account ? ' --account' : ''}`));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
console.log(bold(`${res.data.length} ${scope} secret${res.data.length === 1 ? '' : 's'}:`));
|
|
35
|
+
for (const s of res.data) {
|
|
36
|
+
const masked = s.preview ? muted(`…${s.preview}`) : muted('(hidden)');
|
|
37
|
+
console.log(` ${s.name} ${masked} ${muted(`updated ${new Date(s.updated_at).toLocaleDateString()}`)}`);
|
|
38
|
+
}
|
|
39
|
+
}));
|
|
40
|
+
secretsCommand
|
|
41
|
+
.command('set <name> <value>')
|
|
42
|
+
.description('Create or update a secret (UPPER_SNAKE_CASE name). Encrypted at rest; never echoed back')
|
|
43
|
+
.option('--account', 'Store account-wide (shared across all your projects)')
|
|
44
|
+
.option('--project <guid-or-slug>', 'Target a specific project instead of cwd / Home')
|
|
45
|
+
.option('--json', 'Output raw JSON')
|
|
46
|
+
.action((name, value, opts) => run('Secrets', async () => {
|
|
47
|
+
const res = await put(`/secrets?${await scopeQuery(opts)}`, { name, value });
|
|
48
|
+
if (opts.json) {
|
|
49
|
+
console.log(JSON.stringify(res.data));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const scope = opts.account ? 'account-wide' : 'this project';
|
|
53
|
+
console.log(success(`✓ Secret '${res.data.name}' saved (encrypted, ${scope}). Read it in a function with secrets.get('${res.data.name}').`));
|
|
54
|
+
}));
|
|
55
|
+
secretsCommand
|
|
56
|
+
.command('rm <name>')
|
|
57
|
+
.alias('delete')
|
|
58
|
+
.description('Delete a secret')
|
|
59
|
+
.option('--account', 'Delete from account-wide secrets')
|
|
60
|
+
.option('--project <guid-or-slug>', 'Target a specific project instead of cwd / Home')
|
|
61
|
+
.option('--json', 'Output raw JSON')
|
|
62
|
+
.action((name, opts) => run('Secrets', async () => {
|
|
63
|
+
const res = await del(`/secrets/${encodeURIComponent(name)}?${await scopeQuery(opts)}`);
|
|
64
|
+
if (opts.json) {
|
|
65
|
+
console.log(JSON.stringify(res.data));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (res.data.deleted)
|
|
69
|
+
console.log(success(`✓ Secret '${name}' deleted.`));
|
|
70
|
+
else
|
|
71
|
+
console.log(muted(`No secret named '${name}' found.`));
|
|
72
|
+
}));
|
|
73
|
+
//# sourceMappingURL=secrets.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { get, post, put, del } from '../api.js';
|
|
3
|
-
import { requireConfig } from '../config.js';
|
|
3
|
+
import { getConfig, requireConfig } from '../config.js';
|
|
4
4
|
import { success, error as clrError, muted, bold } from '../colors.js';
|
|
5
5
|
import { run, printList, printResult } from '../helpers/index.js';
|
|
6
6
|
// `--json` is declared on the parent `workflow` command (so the bare list
|
|
@@ -60,14 +60,24 @@ async function waitForRun(wfGuid, prevGuid, timeoutSec) {
|
|
|
60
60
|
}
|
|
61
61
|
async function listWorkflows(opts) {
|
|
62
62
|
const res = await get('/workflows');
|
|
63
|
+
// The endpoint is account-wide. Default to the linked project so the list
|
|
64
|
+
// isn't drowned in every other project's workflows; --all (or running outside
|
|
65
|
+
// a linked project) shows everything.
|
|
66
|
+
const config = getConfig();
|
|
67
|
+
const scoped = !opts.all && config
|
|
68
|
+
? { ...res, data: res.data.filter(w => w.project_slug === config.projectSlug) }
|
|
69
|
+
: res;
|
|
63
70
|
if (opts.json) {
|
|
64
|
-
console.log(JSON.stringify(
|
|
71
|
+
console.log(JSON.stringify(scoped));
|
|
65
72
|
return;
|
|
66
73
|
}
|
|
67
|
-
if (
|
|
68
|
-
console.log(`Active workflows: ${
|
|
74
|
+
if (scoped.meta) {
|
|
75
|
+
console.log(`Active workflows: ${scoped.meta.activeCount}/${scoped.meta.activeLimit}`);
|
|
69
76
|
}
|
|
70
|
-
|
|
77
|
+
if (!opts.all && config) {
|
|
78
|
+
console.log(muted(`Project: ${config.projectSlug} (use --all for every project)`));
|
|
79
|
+
}
|
|
80
|
+
printList(scoped.data, opts, opts.all || !config ? 'No workflows.' : `No workflows in ${config.projectSlug}.`, w => {
|
|
71
81
|
const statusText = w.is_active ? success('on') : clrError('off');
|
|
72
82
|
const cron = w.cron_expression ? ` ${muted(`cron: ${w.cron_expression}`)}` : '';
|
|
73
83
|
const table = w.trigger_table ? ` ${muted(`table: ${w.trigger_table}`)}` : '';
|
|
@@ -79,11 +89,13 @@ async function listWorkflows(opts) {
|
|
|
79
89
|
export const workflowCommand = new Command('workflow')
|
|
80
90
|
.description('Manage workflows')
|
|
81
91
|
.option('--json', 'Output as JSON')
|
|
92
|
+
.option('--all', 'List workflows across all projects (default: the linked project only)')
|
|
82
93
|
.action((_opts, cmd) => run('Workflow', () => listWorkflows(mergedOpts(cmd))));
|
|
83
94
|
workflowCommand
|
|
84
95
|
.command('list')
|
|
85
|
-
.description('List workflows')
|
|
96
|
+
.description('List workflows (linked project by default; --all for every project)')
|
|
86
97
|
.option('--json', 'Output as JSON')
|
|
98
|
+
.option('--all', 'List workflows across all projects')
|
|
87
99
|
.action((_opts, cmd) => run('List', () => listWorkflows(mergedOpts(cmd))));
|
|
88
100
|
workflowCommand
|
|
89
101
|
.command('info <name>')
|
package/dist/helpers/command.js
CHANGED
|
@@ -21,12 +21,20 @@ export function printCommandError(label, err) {
|
|
|
21
21
|
if (err?.code === 'INSUFFICIENT_CREDITS') {
|
|
22
22
|
console.error(clrError(`${label} failed - out of Gipity credits.`));
|
|
23
23
|
console.error(err.message);
|
|
24
|
+
console.error("Run 'gipity credits' to see your balance, or 'gipity credits buy' to top up.");
|
|
24
25
|
return;
|
|
25
26
|
}
|
|
26
27
|
console.error(clrError(`${label} failed: ${err.message}`));
|
|
27
28
|
}
|
|
29
|
+
// Returns the action's promise so Commander can await it. This matters for the
|
|
30
|
+
// central output frame (installOutputFrame): Commander chains the `postAction`
|
|
31
|
+
// hook off whatever the action handler returns, and only waits when that value
|
|
32
|
+
// is thenable. Return `void` here and the trailing blank line fires before the
|
|
33
|
+
// command's async output has even printed — the frame's leading blank then reads
|
|
34
|
+
// as a double blank above the result, with none below it. Returning the promise
|
|
35
|
+
// keeps the frame's boundaries (one blank above, one below) correctly ordered.
|
|
28
36
|
export function run(label, action) {
|
|
29
|
-
action().catch((err) => {
|
|
37
|
+
return action().catch((err) => {
|
|
30
38
|
printCommandError(label, err);
|
|
31
39
|
process.exit(1);
|
|
32
40
|
});
|
package/dist/index.js
CHANGED
|
@@ -35,6 +35,7 @@ import { pageCommand } from './commands/page.js';
|
|
|
35
35
|
import { recordsCommand } from './commands/records.js';
|
|
36
36
|
import { fnCommand } from './commands/fn.js';
|
|
37
37
|
import { serviceCommand } from './commands/service.js';
|
|
38
|
+
import { secretsCommand } from './commands/secrets.js';
|
|
38
39
|
import { notifyCommand } from './commands/notify.js';
|
|
39
40
|
import { paymentsCommand } from './commands/payments.js';
|
|
40
41
|
import { jobCommand } from './commands/job.js';
|
|
@@ -126,7 +127,7 @@ const commonGroup = [skillCommand, projectCommand, addCommand, removeCommand, de
|
|
|
126
127
|
const connectGroup = [claudeCommand, relayCommand];
|
|
127
128
|
const projectGroup = [domainCommand, statusCommand, initCommand];
|
|
128
129
|
const filesGroup = [fileCommand, syncCommand, pushCommand, uploadCommand];
|
|
129
|
-
const appBuildingGroup = [testCommand, fnCommand, serviceCommand, notifyCommand, paymentsCommand, jobCommand, dbCommand, logsCommand, workflowCommand, realtimeCommand, rbacCommand, auditCommand, recordsCommand];
|
|
130
|
+
const appBuildingGroup = [testCommand, fnCommand, serviceCommand, secretsCommand, notifyCommand, paymentsCommand, jobCommand, dbCommand, logsCommand, workflowCommand, realtimeCommand, rbacCommand, auditCommand, recordsCommand];
|
|
130
131
|
const utilitiesGroup = [pageCommand, sandboxCommand, generateCommand, emailCommand, gmailCommand, locationCommand, textCommand];
|
|
131
132
|
const agentGroup = [chatCommand, memoryCommand, agentCommand, approvalCommand];
|
|
132
133
|
const setupGroup = [loginCommand, logoutCommand, tokenCommand, creditsCommand, planCommand, doctorCommand, updateCommand, uninstallCommand];
|
|
@@ -310,5 +311,8 @@ if (mappedCmd) {
|
|
|
310
311
|
process.exit(0);
|
|
311
312
|
}
|
|
312
313
|
}
|
|
313
|
-
|
|
314
|
+
// parseAsync (not parse) so commander awaits each command's returned promise
|
|
315
|
+
// before its postAction hook - that ordering is what keeps the output frame's
|
|
316
|
+
// trailing blank line after the command's async output instead of before it.
|
|
317
|
+
await program.parseAsync(normalizeAliases(process.argv, program));
|
|
314
318
|
//# sourceMappingURL=index.js.map
|
package/dist/knowledge.js
CHANGED
|
@@ -102,14 +102,14 @@ mkdir -p ~/GipityProjects/<slug> && cd ~/GipityProjects/<slug> && gipity init <s
|
|
|
102
102
|
|
|
103
103
|
## CLI quick reference
|
|
104
104
|
|
|
105
|
-
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 email send --to <addr> --subject <s> --body <b>\` (sends as \`gipity@gipity.ai\`; omit \`--to\` to self-send), \`gipity skill read <name>\`.
|
|
105
|
+
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')\` — 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>\`.
|
|
106
106
|
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").
|
|
107
107
|
For deterministic text questions (letter/word counts, substring occurrences, nth word/char, anagrams), use \`gipity text analyze "<text>"\` - local and instant, no sandbox or LLM needed.
|
|
108
108
|
Run \`gipity --help\` for the full list. Use \`--help\` on any command for details.
|
|
109
109
|
|
|
110
110
|
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.
|
|
111
111
|
|
|
112
|
-
Tests
|
|
112
|
+
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 every run - so 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)\` → \`ctx.fixtures\`, and namespace unique values with \`ctx.testId\`.
|
|
113
113
|
|
|
114
114
|
## Tool output is complete and synchronous
|
|
115
115
|
|
package/dist/relay/setup.js
CHANGED
|
@@ -91,6 +91,9 @@ export function startDaemon() {
|
|
|
91
91
|
detached: true,
|
|
92
92
|
stdio: 'ignore',
|
|
93
93
|
});
|
|
94
|
+
// A spawn 'error' arrives asynchronously and bypasses the try/catch; swallow
|
|
95
|
+
// it so a launch failure stays best-effort instead of crashing the process.
|
|
96
|
+
child.on('error', () => { });
|
|
94
97
|
child.unref();
|
|
95
98
|
}
|
|
96
99
|
catch {
|
package/dist/sync.js
CHANGED
|
@@ -347,12 +347,14 @@ async function downloadAll(projectGuid, onBytes) {
|
|
|
347
347
|
return extractTarToMap(stream, DOWNLOAD_IDLE_MS, onBytes);
|
|
348
348
|
}
|
|
349
349
|
async function fetchOne(projectGuid, path) {
|
|
350
|
-
// Exact single-file read first.
|
|
351
|
-
// as a DIRECTORY prefix, so a single root file (e.g. `gipity.yaml`) comes back
|
|
352
|
-
// empty — which silently broke conflict restores and trapped sync in an
|
|
353
|
-
// unresolvable delete-vs-newer loop. `/files/read` is the exact-path endpoint
|
|
350
|
+
// Exact single-file read first. `/files/read` is the exact-path endpoint
|
|
354
351
|
// (what `gipity file cat` uses); it returns text content, reliable for the
|
|
355
|
-
// config/code files that actually hit a restore. Binary falls through to
|
|
352
|
+
// config/code files that actually hit a restore. Binary falls through to the
|
|
353
|
+
// tar path below, which now resolves an exact file path server-side (the
|
|
354
|
+
// /files/tree endpoint stats a non-directory `path` and packs that one file
|
|
355
|
+
// under its full name). Before that fix the tar endpoint treated `path` as a
|
|
356
|
+
// DIRECTORY prefix, so a single file came back empty and a dropped binary
|
|
357
|
+
// (e.g. an agent-generated image) could never be recovered.
|
|
356
358
|
try {
|
|
357
359
|
const res = await get(`/projects/${projectGuid}/files/read?path=${encodeURIComponent(path)}`);
|
|
358
360
|
const content = res?.data?.content;
|
package/dist/updater/shim.js
CHANGED
|
@@ -27,6 +27,9 @@ function startBackgroundUpdater() {
|
|
|
27
27
|
stdio: 'ignore',
|
|
28
28
|
env: process.env,
|
|
29
29
|
});
|
|
30
|
+
// Async spawn 'error' bypasses the try/catch; swallow so the best-effort
|
|
31
|
+
// updater never crashes the CLI.
|
|
32
|
+
child.on('error', () => { });
|
|
30
33
|
child.unref();
|
|
31
34
|
}
|
|
32
35
|
catch { /* updater is best-effort, never block */ }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gipity",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.407",
|
|
4
4
|
"description": "The full-stack platform tuned for AI agents. Database, storage, auth, functions, deploy, and drop-in kits - all agent-tuned. Pair with Claude Code or use standalone.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"gipity": "dist/updater/shim.js",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"postbuild": "node scripts/gen-build-info.mjs",
|
|
14
14
|
"dev": "tsc --watch",
|
|
15
15
|
"test": "npm run test:smoke",
|
|
16
|
-
"test:smoke": "tsc && node --test dist/__tests__/utils.test.js dist/__tests__/colors.test.js dist/__tests__/config.test.js dist/__tests__/sync.test.js dist/__tests__/sync-apply.test.js dist/__tests__/sync-lock.test.js dist/__tests__/auth-lock.test.js dist/__tests__/push-cas.test.js dist/__tests__/upload.test.js dist/__tests__/progress.test.js dist/__tests__/updater.test.js dist/__tests__/cli-smoke.test.js dist/__tests__/claude-noninteractive.test.js dist/__tests__/claude-trust.test.js dist/__tests__/relay-state.test.js dist/__tests__/relay-daemon.test.js dist/__tests__/relay-installers.test.js dist/__tests__/relay-bridge-abort.test.js dist/__tests__/relay-redact.test.js dist/__tests__/relay-machine-id.test.js dist/__tests__/stream-json.test.js dist/__tests__/relay-ingest-contract.test.js dist/__tests__/prompts.test.js dist/__tests__/capture-transcript.test.js dist/__tests__/flag-aliases.test.js dist/__tests__/client-context.test.js dist/__tests__/adopt-cwd.test.js dist/__tests__/cli-cmd-agent.test.js dist/__tests__/cli-cmd-approval.test.js dist/__tests__/cli-cmd-audit.test.js dist/__tests__/cli-cmd-chat.test.js dist/__tests__/cli-cmd-credits.test.js dist/__tests__/cli-cmd-db.test.js dist/__tests__/cli-cmd-deploy.test.js dist/__tests__/cli-cmd-domain.test.js dist/__tests__/cli-cmd-email.test.js dist/__tests__/cli-cmd-file.test.js dist/__tests__/cli-cmd-fn.test.js dist/__tests__/cli-cmd-service.test.js dist/__tests__/cli-cmd-job.test.js dist/__tests__/cli-cmd-generate.test.js dist/__tests__/cli-cmd-gmail.test.js dist/__tests__/cli-cmd-info.test.js dist/__tests__/cli-cmd-init.test.js dist/__tests__/cli-cmd-location.test.js dist/__tests__/cli-cmd-text.test.js dist/__tests__/cli-cmd-login.test.js dist/__tests__/cli-cmd-logout.test.js dist/__tests__/cli-cmd-token.test.js dist/__tests__/cli-cmd-logs.test.js dist/__tests__/cli-cmd-memory.test.js dist/__tests__/cli-cmd-page.test.js dist/__tests__/cli-cmd-plan.test.js dist/__tests__/cli-cmd-project.test.js dist/__tests__/cli-cmd-rbac.test.js dist/__tests__/cli-cmd-realtime.test.js dist/__tests__/cli-cmd-records.test.js dist/__tests__/cli-cmd-relay.test.js dist/__tests__/cli-cmd-doctor.test.js dist/__tests__/claude-setup.test.js dist/__tests__/cli-cmd-sandbox.test.js dist/__tests__/cli-cmd-add.test.js dist/__tests__/cli-cmd-remove.test.js dist/__tests__/cli-cmd-skill.test.js dist/__tests__/cli-cmd-test.test.js dist/__tests__/cli-cmd-workflow.test.js dist/__tests__/setup-skills-block.test.js dist/__tests__/setup-hooks.test.js",
|
|
16
|
+
"test:smoke": "tsc && node --test dist/__tests__/utils.test.js dist/__tests__/colors.test.js dist/__tests__/config.test.js dist/__tests__/sync.test.js dist/__tests__/sync-apply.test.js dist/__tests__/sync-lock.test.js dist/__tests__/auth-lock.test.js dist/__tests__/api-401-retry.test.js dist/__tests__/push-cas.test.js dist/__tests__/upload.test.js dist/__tests__/progress.test.js dist/__tests__/updater.test.js dist/__tests__/cli-smoke.test.js dist/__tests__/claude-noninteractive.test.js dist/__tests__/claude-trust.test.js dist/__tests__/relay-state.test.js dist/__tests__/relay-daemon.test.js dist/__tests__/relay-installers.test.js dist/__tests__/relay-bridge-abort.test.js dist/__tests__/relay-redact.test.js dist/__tests__/relay-machine-id.test.js dist/__tests__/stream-json.test.js dist/__tests__/relay-ingest-contract.test.js dist/__tests__/prompts.test.js dist/__tests__/capture-transcript.test.js dist/__tests__/flag-aliases.test.js dist/__tests__/client-context.test.js dist/__tests__/adopt-cwd.test.js dist/__tests__/cli-cmd-agent.test.js dist/__tests__/cli-cmd-approval.test.js dist/__tests__/cli-cmd-audit.test.js dist/__tests__/cli-cmd-chat.test.js dist/__tests__/cli-cmd-credits.test.js dist/__tests__/cli-cmd-db.test.js dist/__tests__/cli-cmd-deploy.test.js dist/__tests__/cli-cmd-domain.test.js dist/__tests__/cli-cmd-email.test.js dist/__tests__/cli-cmd-file.test.js dist/__tests__/cli-cmd-fn.test.js dist/__tests__/cli-cmd-service.test.js dist/__tests__/cli-cmd-job.test.js dist/__tests__/cli-cmd-generate.test.js dist/__tests__/cli-cmd-gmail.test.js dist/__tests__/cli-cmd-info.test.js dist/__tests__/cli-cmd-init.test.js dist/__tests__/cli-cmd-location.test.js dist/__tests__/cli-cmd-text.test.js dist/__tests__/cli-cmd-login.test.js dist/__tests__/cli-cmd-logout.test.js dist/__tests__/cli-cmd-token.test.js dist/__tests__/cli-cmd-logs.test.js dist/__tests__/cli-cmd-memory.test.js dist/__tests__/cli-cmd-page.test.js dist/__tests__/cli-cmd-plan.test.js dist/__tests__/cli-cmd-project.test.js dist/__tests__/cli-cmd-rbac.test.js dist/__tests__/cli-cmd-realtime.test.js dist/__tests__/cli-cmd-records.test.js dist/__tests__/cli-cmd-relay.test.js dist/__tests__/cli-cmd-doctor.test.js dist/__tests__/claude-setup.test.js dist/__tests__/cli-cmd-sandbox.test.js dist/__tests__/cli-cmd-add.test.js dist/__tests__/cli-cmd-remove.test.js dist/__tests__/cli-cmd-skill.test.js dist/__tests__/cli-cmd-test.test.js dist/__tests__/cli-cmd-workflow.test.js dist/__tests__/setup-skills-block.test.js dist/__tests__/setup-hooks.test.js",
|
|
17
17
|
"test:e2e": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-live.test.js dist/__tests__/cli-e2e-sync-live.test.js dist/__tests__/cli-e2e-services-media-live.test.js dist/__tests__/cli-e2e-workflow-live.test.js dist/__tests__/cli-e2e-sandbox-live.test.js dist/__tests__/cli-e2e-page-fetch-live.test.js dist/__tests__/cli-e2e-page-test-live.test.js",
|
|
18
18
|
"test:e2e:sync": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-sync-live.test.js",
|
|
19
19
|
"test:e2e:sandbox": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-sandbox-live.test.js"
|