gipity 1.0.413 → 1.0.414
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 +45 -0
- package/dist/client-context.js +1 -1
- package/dist/commands/github.js +80 -0
- package/dist/commands/load.js +221 -0
- package/dist/commands/relay.js +29 -0
- package/dist/commands/save.js +90 -0
- package/dist/commands/status.js +27 -6
- package/dist/index.js +5 -2
- package/dist/knowledge.js +2 -0
- package/dist/relay/agent-token.js +6 -1
- package/dist/relay/daemon.js +21 -1
- package/dist/relay/diagnostics.js +173 -0
- package/dist/relay/onboarding.js +8 -0
- package/dist/relay/state.js +18 -0
- package/dist/setup.js +39 -18
- package/package.json +2 -2
package/dist/api.js
CHANGED
|
@@ -211,6 +211,51 @@ export async function download(path, retried = false) {
|
|
|
211
211
|
}
|
|
212
212
|
return Buffer.from(await res.arrayBuffer());
|
|
213
213
|
}
|
|
214
|
+
/** Download raw bytes plus the response headers - for binary endpoints whose
|
|
215
|
+
* metadata rides headers (e.g. `GET /projects/:guid/export`, where
|
|
216
|
+
* `X-Gip-Skipped` and the Content-Disposition filename matter to the caller).
|
|
217
|
+
* Non-2xx responses are parsed as standard JSON errors. */
|
|
218
|
+
export async function downloadWithHeaders(path, retried = false) {
|
|
219
|
+
const url = `${baseUrl()}${path}`;
|
|
220
|
+
const res = await fetch(url, {
|
|
221
|
+
headers: { ...clientHeaders(), 'Authorization': `Bearer ${await bearerToken()}` },
|
|
222
|
+
});
|
|
223
|
+
if (await shouldRetryAfter401(res.status, retried)) {
|
|
224
|
+
return downloadWithHeaders(path, true);
|
|
225
|
+
}
|
|
226
|
+
if (!res.ok) {
|
|
227
|
+
const json = await res.json().catch(() => ({ error: { code: 'UNKNOWN', message: res.statusText } }));
|
|
228
|
+
const err = json.error || { code: 'UNKNOWN', message: res.statusText };
|
|
229
|
+
throw new ApiError(res.status, err.code, with401Hint(res.status, err.message), json.data);
|
|
230
|
+
}
|
|
231
|
+
return { buffer: Buffer.from(await res.arrayBuffer()), headers: res.headers };
|
|
232
|
+
}
|
|
233
|
+
/** POST raw bytes (e.g. a .gip bundle) and parse the JSON response, returning
|
|
234
|
+
* the HTTP status alongside it so callers can tell a clean 201 from a 207
|
|
235
|
+
* partial success. The timeout scales with the body size (same policy as the
|
|
236
|
+
* presigned PUT path) so a large-but-progressing upload isn't cut off. */
|
|
237
|
+
export async function postBinary(path, body, contentType = 'application/zip', retried = false) {
|
|
238
|
+
const url = `${baseUrl()}${path}`;
|
|
239
|
+
const headers = {
|
|
240
|
+
...clientHeaders(),
|
|
241
|
+
'Authorization': `Bearer ${await bearerToken()}`,
|
|
242
|
+
'Content-Type': contentType,
|
|
243
|
+
};
|
|
244
|
+
const res = await fetchWithTimeout(url, {
|
|
245
|
+
method: 'POST',
|
|
246
|
+
headers,
|
|
247
|
+
body: new Uint8Array(body),
|
|
248
|
+
}, putTimeoutMs(body.length), `POST ${path}`);
|
|
249
|
+
if (await shouldRetryAfter401(res.status, retried)) {
|
|
250
|
+
return postBinary(path, body, contentType, true);
|
|
251
|
+
}
|
|
252
|
+
if (!res.ok) {
|
|
253
|
+
const json = await res.json().catch(() => ({ error: { code: 'UNKNOWN', message: res.statusText } }));
|
|
254
|
+
const err = json.error || { code: 'UNKNOWN', message: res.statusText };
|
|
255
|
+
throw new ApiError(res.status, err.code, with401Hint(res.status, err.message), json.data);
|
|
256
|
+
}
|
|
257
|
+
return { status: res.status, json: await res.json() };
|
|
258
|
+
}
|
|
214
259
|
/** Download a response as a Node.js Readable stream */
|
|
215
260
|
export async function downloadStream(path, retried = false) {
|
|
216
261
|
const { Readable } = await import('stream');
|
package/dist/client-context.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'fs';
|
|
2
2
|
import { resolve, dirname } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
-
function cliVersion() {
|
|
4
|
+
export function cliVersion() {
|
|
5
5
|
try {
|
|
6
6
|
const dir = dirname(fileURLToPath(import.meta.url));
|
|
7
7
|
const pkg = JSON.parse(readFileSync(resolve(dir, '../package.json'), 'utf-8'));
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gipity github <connect|status|repos|disconnect>` - manage the GitHub
|
|
3
|
+
* connection that powers `gipity load github:owner/repo`.
|
|
4
|
+
*
|
|
5
|
+
* Connect uses the GitHub App install flow: the URL is GitHub's own repo
|
|
6
|
+
* picker, so the user chooses exactly which repositories Gipity can read.
|
|
7
|
+
* Re-running connect doubles as the "grant more repositories" flow.
|
|
8
|
+
*/
|
|
9
|
+
import { Command } from 'commander';
|
|
10
|
+
import { get, post } from '../api.js';
|
|
11
|
+
import { brand, bold, muted } from '../colors.js';
|
|
12
|
+
import { run, printList, printResult } from '../helpers/index.js';
|
|
13
|
+
export const githubCommand = new Command('github')
|
|
14
|
+
.description('Connect GitHub for repo imports')
|
|
15
|
+
.addHelpText('after', '\nConnect once, then import any shared repo with `gipity load github:owner/repo`.');
|
|
16
|
+
githubCommand
|
|
17
|
+
.command('connect')
|
|
18
|
+
.description('Connect your GitHub account - prints a link; GitHub lets you pick which repositories to share')
|
|
19
|
+
.option('--json', 'Output as JSON')
|
|
20
|
+
.action((opts) => run('Connect', async () => {
|
|
21
|
+
const res = await get('/services/github/authorize');
|
|
22
|
+
if (opts.json) {
|
|
23
|
+
console.log(JSON.stringify({ url: res.url }));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
console.log('Open this link in your browser to connect GitHub - GitHub lets you pick which repositories to share:\n');
|
|
27
|
+
console.log(brand(res.url));
|
|
28
|
+
console.log(muted('\nWhen done, run `gipity github status` to confirm.'));
|
|
29
|
+
console.log(muted('Re-run `gipity github connect` any time to grant more repositories.'));
|
|
30
|
+
}));
|
|
31
|
+
githubCommand
|
|
32
|
+
.command('status', { isDefault: true })
|
|
33
|
+
.description('Show whether GitHub is connected')
|
|
34
|
+
.option('--json', 'Output as JSON')
|
|
35
|
+
.action((opts) => run('Status', async () => {
|
|
36
|
+
const res = await get('/services/github/status');
|
|
37
|
+
if (opts.json) {
|
|
38
|
+
console.log(JSON.stringify(res.data));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const s = res.data;
|
|
42
|
+
if (!s.connected) {
|
|
43
|
+
console.log('GitHub: not connected. Run `gipity github connect` to set it up.');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
console.log(`GitHub: ${bold('connected')}`);
|
|
47
|
+
if (s.email)
|
|
48
|
+
console.log(` Account: ${s.email}`);
|
|
49
|
+
if (s.connectedAt)
|
|
50
|
+
console.log(` Connected: ${new Date(s.connectedAt).toLocaleDateString()}`);
|
|
51
|
+
console.log(muted('\nList reachable repositories with `gipity github repos`.'));
|
|
52
|
+
console.log(muted('Import one with `gipity load github:owner/repo`.'));
|
|
53
|
+
}));
|
|
54
|
+
githubCommand
|
|
55
|
+
.command('repos')
|
|
56
|
+
.description('List the repositories your GitHub connection can reach')
|
|
57
|
+
.option('--json', 'Output as JSON')
|
|
58
|
+
.action((opts) => run('Repos', async () => {
|
|
59
|
+
const res = await get('/services/github/repos').catch((err) => {
|
|
60
|
+
if (err?.code === 'NOT_CONNECTED') {
|
|
61
|
+
throw new Error('GitHub is not connected - run `gipity github connect` first.');
|
|
62
|
+
}
|
|
63
|
+
throw err;
|
|
64
|
+
});
|
|
65
|
+
const width = res.data.length ? Math.max(...res.data.map(r => r.full_name.length)) : 0;
|
|
66
|
+
printList(res.data, opts, 'No repositories shared yet - run `gipity github connect` to grant some.', r => {
|
|
67
|
+
const vis = r.private ? 'private' : 'public ';
|
|
68
|
+
const desc = r.description ? ` ${muted(r.description)}` : '';
|
|
69
|
+
return `${r.full_name.padEnd(width)} ${muted(vis)} ${r.default_branch}${desc}`;
|
|
70
|
+
});
|
|
71
|
+
}));
|
|
72
|
+
githubCommand
|
|
73
|
+
.command('disconnect')
|
|
74
|
+
.description('Disconnect GitHub (imports via github: sources will stop working)')
|
|
75
|
+
.option('--json', 'Output as JSON')
|
|
76
|
+
.action((opts) => run('Disconnect', async () => {
|
|
77
|
+
await post('/services/github/disconnect');
|
|
78
|
+
printResult('GitHub disconnected.', opts, { disconnected: true });
|
|
79
|
+
}));
|
|
80
|
+
//# sourceMappingURL=github.js.map
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gipity load <source>` - create a brand-new project from a .gip bundle or
|
|
3
|
+
* a GitHub repository.
|
|
4
|
+
*
|
|
5
|
+
* The counterpart of `gipity save`. Load ALWAYS creates a new project (the
|
|
6
|
+
* server retries slug collisions); it never merges into an existing one. Use
|
|
7
|
+
* `--inspect` to peek inside a source - metadata, contents, deploy phases,
|
|
8
|
+
* and each function's permission surface - without creating anything.
|
|
9
|
+
*
|
|
10
|
+
* Sources:
|
|
11
|
+
* - a local .gip file (from `gipity save`) - uploaded as raw zip bytes
|
|
12
|
+
* - `github:owner/repo[/sub/path][@ref]` or a github.com URL - resolved
|
|
13
|
+
* server-side through the user's GitHub connection (`gipity github connect`);
|
|
14
|
+
* the CLI sends a JSON `{source}` body, no repo bytes travel through the
|
|
15
|
+
* client. Both transports feed the same import pipeline and response shapes.
|
|
16
|
+
*/
|
|
17
|
+
import fs from 'fs';
|
|
18
|
+
import os from 'os';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import { Command } from 'commander';
|
|
21
|
+
import { get, post, postBinary, getAccountSlug } from '../api.js';
|
|
22
|
+
import { success, muted, warning, bold, info, brand } from '../colors.js';
|
|
23
|
+
import { run } from '../helpers/index.js';
|
|
24
|
+
import { withSpinner } from '../progress.js';
|
|
25
|
+
import { formatBytes } from '../adopt-cwd.js';
|
|
26
|
+
import { getProjectsRoot } from '../relay/paths.js';
|
|
27
|
+
import { finalizeLocalProject } from '../project-setup.js';
|
|
28
|
+
/** Server transport cap for a .gip upload - checked locally first so a too-big
|
|
29
|
+
* bundle fails in milliseconds instead of after a full upload. */
|
|
30
|
+
const MAX_BUNDLE_BYTES = 200 * 1024 * 1024;
|
|
31
|
+
/** A source the server resolves via the user's GitHub connection: the
|
|
32
|
+
* `github:owner/repo[/sub/path][@ref]` shorthand or a github.com URL. */
|
|
33
|
+
function isGithubSource(source) {
|
|
34
|
+
return /^github:/i.test(source) || /^https?:\/\/(www\.)?github\.com\//i.test(source);
|
|
35
|
+
}
|
|
36
|
+
/** Append the recovery path when a GitHub-source call fails because the repo
|
|
37
|
+
* isn't reachable through the user's connection (not connected, or connected
|
|
38
|
+
* but this repo wasn't granted). The server's message names the problem; the
|
|
39
|
+
* hint names the fix. Re-throws always. */
|
|
40
|
+
function rethrowWithConnectHint(err) {
|
|
41
|
+
const e = err;
|
|
42
|
+
const msg = e?.message ?? '';
|
|
43
|
+
if (e?.code === 'NOT_CONNECTED' || /not covered by your GitHub connection|connect github/i.test(msg)) {
|
|
44
|
+
e.message = `${msg}\nRun \`gipity github connect\` to connect GitHub - re-run it any time to grant more repositories.`;
|
|
45
|
+
}
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
function readBundle(source) {
|
|
49
|
+
const resolved = path.resolve(source.startsWith('~') ? os.homedir() + source.slice(1) : source);
|
|
50
|
+
if (!fs.existsSync(resolved)) {
|
|
51
|
+
throw new Error(`Bundle not found: ${resolved}`);
|
|
52
|
+
}
|
|
53
|
+
const stat = fs.statSync(resolved);
|
|
54
|
+
if (stat.isDirectory()) {
|
|
55
|
+
throw new Error(`${resolved} is a directory - pass a .gip file (create one with \`gipity save\`) or a github: source.`);
|
|
56
|
+
}
|
|
57
|
+
if (stat.size > MAX_BUNDLE_BYTES) {
|
|
58
|
+
throw new Error(`Bundle is ${formatBytes(stat.size)} - over the ${formatBytes(MAX_BUNDLE_BYTES)} upload cap. Move large assets to Gipity Storage and re-save.`);
|
|
59
|
+
}
|
|
60
|
+
return { resolved, buffer: fs.readFileSync(resolved) };
|
|
61
|
+
}
|
|
62
|
+
/** Manifest permission surface, one line per function - what an importing user
|
|
63
|
+
* needs to see before trusting a bundle. */
|
|
64
|
+
function printManifest(m) {
|
|
65
|
+
if (!m.found) {
|
|
66
|
+
console.log(muted('No gipity.yaml in the source - static app (files deploy only).'));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (!m.valid) {
|
|
70
|
+
console.log(warning(`gipity.yaml is present but invalid: ${m.error ?? 'unparseable'}`));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
console.log(`Deploy phases: ${m.phases.map(p => `${p.name} (${p.type})`).join(', ') || 'none'}`);
|
|
74
|
+
console.log(`Database: ${m.hasDatabase ? 'yes' : 'no'}`);
|
|
75
|
+
if (m.functions.length) {
|
|
76
|
+
console.log('');
|
|
77
|
+
console.log(bold('Functions and permissions:'));
|
|
78
|
+
const width = Math.max(...m.functions.map(f => f.name.length));
|
|
79
|
+
for (const f of m.functions) {
|
|
80
|
+
const parts = [`auth=${f.auth ?? 'public'}`];
|
|
81
|
+
if (f.tables?.length)
|
|
82
|
+
parts.push(`tables=${f.tables.join(',')}`);
|
|
83
|
+
if (f.fetch_domains?.length)
|
|
84
|
+
parts.push(`fetch=${f.fetch_domains.join(',')}`);
|
|
85
|
+
if (f.services?.length)
|
|
86
|
+
parts.push(`services=${f.services.join(',')}`);
|
|
87
|
+
console.log(` ${f.name.padEnd(width)} ${muted(parts.join(' '))}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function printInspect(d, source) {
|
|
92
|
+
if (d.meta) {
|
|
93
|
+
console.log(bold(d.meta.name));
|
|
94
|
+
if (d.meta.description)
|
|
95
|
+
console.log(muted(d.meta.description));
|
|
96
|
+
console.log(`Source: project ${d.meta.source_project}, exported ${d.meta.exported_at}`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
console.log(bold(isGithubSource(source) ? source : path.basename(source)));
|
|
100
|
+
console.log(muted('No Gipity bundle metadata - imports as a plain file tree.'));
|
|
101
|
+
}
|
|
102
|
+
console.log(`Contents: ${d.fileCount} file(s), ${formatBytes(d.totalBytes)}`);
|
|
103
|
+
console.log(`Top level: ${d.topLevel.join(', ') || '(empty)'}`);
|
|
104
|
+
console.log('');
|
|
105
|
+
printManifest(d.manifest);
|
|
106
|
+
console.log('');
|
|
107
|
+
console.log(muted('Load it with `gipity load ' + source + '` - always creates a NEW project.'));
|
|
108
|
+
}
|
|
109
|
+
export const loadCommand = new Command('load')
|
|
110
|
+
.description('Create a new app from a .gip bundle or a GitHub repo')
|
|
111
|
+
.argument('<source>', 'A .gip file (from `gipity save`), github:owner/repo[/sub/path][@ref], or a github.com URL')
|
|
112
|
+
.option('--name <name>', 'Name for the new project (default: the source\'s saved name)')
|
|
113
|
+
.option('--inspect', 'Peek inside the source - contents, deploy phases, function permissions - without creating anything')
|
|
114
|
+
.option('--json', 'Output as JSON')
|
|
115
|
+
.addHelpText('after', '\nLoad always creates a NEW project - it never merges into an existing one.'
|
|
116
|
+
+ '\nGitHub sources need a one-time `gipity github connect` (re-run it to grant more repos).')
|
|
117
|
+
.action((source, opts) => run('Load', async () => {
|
|
118
|
+
const github = isGithubSource(source);
|
|
119
|
+
if (!github && /^https?:\/\//i.test(source)) {
|
|
120
|
+
throw new Error('Only GitHub URLs are supported for now - pass github:owner/repo, a github.com URL, or a local .gip file.');
|
|
121
|
+
}
|
|
122
|
+
// Local .gip bundles upload raw zip bytes; GitHub sources send a tiny JSON
|
|
123
|
+
// body and the server pulls the tree through the user's connection.
|
|
124
|
+
let sourceLabel;
|
|
125
|
+
let doInspect;
|
|
126
|
+
let doImport;
|
|
127
|
+
if (github) {
|
|
128
|
+
sourceLabel = source;
|
|
129
|
+
doInspect = () => post('/projects/inspect', { source }).catch(rethrowWithConnectHint);
|
|
130
|
+
doImport = async () => {
|
|
131
|
+
const body = { source };
|
|
132
|
+
if (opts.name)
|
|
133
|
+
body.name = opts.name;
|
|
134
|
+
const res = await post('/projects/import', body).catch(rethrowWithConnectHint);
|
|
135
|
+
// The JSON helper hides the 201-vs-207 status; `failed` carries the
|
|
136
|
+
// same signal (the server only sets it on a partial write).
|
|
137
|
+
return { data: res.data, partial: !!res.data.failed?.length };
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
const { resolved, buffer } = readBundle(source);
|
|
142
|
+
sourceLabel = path.basename(resolved);
|
|
143
|
+
doInspect = async () => (await postBinary('/projects/inspect', buffer)).json;
|
|
144
|
+
doImport = async () => {
|
|
145
|
+
const qs = new URLSearchParams();
|
|
146
|
+
if (opts.name)
|
|
147
|
+
qs.set('name', opts.name);
|
|
148
|
+
const { status, json } = await postBinary(`/projects/import${qs.size ? `?${qs}` : ''}`, buffer);
|
|
149
|
+
return { data: json.data, partial: status === 207 };
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (opts.inspect) {
|
|
153
|
+
const res = opts.json
|
|
154
|
+
? await doInspect()
|
|
155
|
+
: await withSpinner('Inspecting…', doInspect, { done: null });
|
|
156
|
+
if (opts.json) {
|
|
157
|
+
console.log(JSON.stringify(res.data));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
printInspect(res.data, source);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const { data, partial } = opts.json
|
|
164
|
+
? await doImport()
|
|
165
|
+
: await withSpinner('Importing…', doImport, { done: null });
|
|
166
|
+
const project = data.project;
|
|
167
|
+
// Materialize a local dir and link it, exactly like `gipity project create`:
|
|
168
|
+
// `.gipity.json` is written directly inside the new dir, then a soft sync
|
|
169
|
+
// pulls the imported files down.
|
|
170
|
+
const dir = path.join(getProjectsRoot(), project.slug);
|
|
171
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
172
|
+
const accountSlug = await getAccountSlug();
|
|
173
|
+
// Resolve the first assigned agent (if any) - not fatal if missing.
|
|
174
|
+
let agentGuid = '';
|
|
175
|
+
try {
|
|
176
|
+
const agents = await get(`/projects/${project.short_guid}/agents`);
|
|
177
|
+
if (agents.data.length > 0)
|
|
178
|
+
agentGuid = agents.data[0].short_guid;
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// offline or no agents - non-fatal
|
|
182
|
+
}
|
|
183
|
+
const { applied } = await finalizeLocalProject({
|
|
184
|
+
dir,
|
|
185
|
+
projectGuid: project.short_guid,
|
|
186
|
+
projectSlug: project.slug,
|
|
187
|
+
projectName: project.name,
|
|
188
|
+
accountSlug,
|
|
189
|
+
agentGuid,
|
|
190
|
+
sync: 'soft',
|
|
191
|
+
interactive: false,
|
|
192
|
+
});
|
|
193
|
+
if (opts.json) {
|
|
194
|
+
console.log(JSON.stringify({
|
|
195
|
+
created: project.slug,
|
|
196
|
+
name: project.name,
|
|
197
|
+
guid: project.short_guid,
|
|
198
|
+
dir,
|
|
199
|
+
written: data.written,
|
|
200
|
+
failed: data.failed,
|
|
201
|
+
partial,
|
|
202
|
+
applied,
|
|
203
|
+
}));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
console.log(success(`Created "${project.name}" (${project.slug}) from ${sourceLabel}`));
|
|
207
|
+
console.log(`Wrote ${data.written} file(s) to the new project.`);
|
|
208
|
+
console.log(`Initialized ${info(dir)}`);
|
|
209
|
+
if (applied > 0)
|
|
210
|
+
console.log(`Pulled ${applied} file(s) to local.`);
|
|
211
|
+
if (partial || data.failed?.length) {
|
|
212
|
+
console.log('');
|
|
213
|
+
console.warn(warning(`Partial import - ${data.failed?.length ?? 0} file(s) could not be written:`));
|
|
214
|
+
for (const f of data.failed ?? [])
|
|
215
|
+
console.warn(warning(` ! ${f.path}: ${f.error}`));
|
|
216
|
+
console.warn(warning('The project was still created and linked; the files above are missing from it.'));
|
|
217
|
+
}
|
|
218
|
+
console.log('');
|
|
219
|
+
console.log(`${muted('Next:')} cd ${dir} && ${brand('gipity deploy dev')}`);
|
|
220
|
+
}));
|
|
221
|
+
//# sourceMappingURL=load.js.map
|
package/dist/commands/relay.js
CHANGED
|
@@ -225,6 +225,35 @@ relayCommand
|
|
|
225
225
|
state.setPaused(false);
|
|
226
226
|
console.log(success('Resumed.'));
|
|
227
227
|
});
|
|
228
|
+
// ─── gipity relay diagnostics [on|off] ─────────────────────────────────
|
|
229
|
+
relayCommand
|
|
230
|
+
.command('diagnostics [state]')
|
|
231
|
+
.description('View or set sharing of anonymous host/version diagnostics (on|off)')
|
|
232
|
+
.action((arg) => {
|
|
233
|
+
if (arg === undefined) {
|
|
234
|
+
const stored = state.getDiagnosticsConsent();
|
|
235
|
+
const effective = state.diagnosticsConsented();
|
|
236
|
+
const label = stored === undefined ? 'on (default, not yet chosen)' : stored ? 'on' : 'off';
|
|
237
|
+
console.log(`Diagnostics sharing: ${effective ? success(label) : muted(label)}`);
|
|
238
|
+
if (effective !== (stored !== false)) {
|
|
239
|
+
console.log(muted(' (overridden off by GIPITY_NO_DIAGNOSTICS / DO_NOT_TRACK)'));
|
|
240
|
+
}
|
|
241
|
+
console.log(muted(' Non-PII only: CPU/GPU/memory/disk specs, OS + tool versions. No file paths, hostname, or username.'));
|
|
242
|
+
console.log(muted(' Set with `gipity relay diagnostics on` / `gipity relay diagnostics off`.'));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const on = /^(on|true|yes|1|enable)$/i.test(arg);
|
|
246
|
+
const off = /^(off|false|no|0|disable)$/i.test(arg);
|
|
247
|
+
if (!on && !off) {
|
|
248
|
+
console.log(clrError("Expected 'on' or 'off'."));
|
|
249
|
+
process.exitCode = 1;
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
state.setDiagnosticsConsent(on);
|
|
253
|
+
console.log(on
|
|
254
|
+
? `${success('Diagnostics sharing on.')} ${muted('Reported on the next daily heartbeat.')}`
|
|
255
|
+
: `${success('Diagnostics sharing off.')} ${muted('No host/version data will be sent.')}`);
|
|
256
|
+
});
|
|
228
257
|
// ─── gipity relay rename <name> ────────────────────────────────────────
|
|
229
258
|
relayCommand
|
|
230
259
|
.command('rename <new-name>')
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gipity save [output]` - export the linked project as a portable .gip bundle.
|
|
3
|
+
*
|
|
4
|
+
* A .gip is a zip of the project's file tree plus a reserved
|
|
5
|
+
* `_gip/manifest.json` metadata entry. It pairs with `gipity load`, which
|
|
6
|
+
* creates a brand-new project from a bundle. Code-only: secrets, custom
|
|
7
|
+
* domains, chats, and database rows are never in a .gip.
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { createHash } from 'crypto';
|
|
12
|
+
import { Command } from 'commander';
|
|
13
|
+
import { downloadWithHeaders } from '../api.js';
|
|
14
|
+
import { requireConfig } from '../config.js';
|
|
15
|
+
import { success, muted, warning } from '../colors.js';
|
|
16
|
+
import { run } from '../helpers/index.js';
|
|
17
|
+
import { withSpinner } from '../progress.js';
|
|
18
|
+
import { formatBytes } from '../adopt-cwd.js';
|
|
19
|
+
/** Count archive entries by reading the zip end-of-central-directory record -
|
|
20
|
+
* cheap (no inflation, no dependency) and good enough for a summary line.
|
|
21
|
+
* Returns null when the record can't be found or the count is zip64-saturated,
|
|
22
|
+
* in which case the line is simply omitted. */
|
|
23
|
+
export function zipEntryCount(buf) {
|
|
24
|
+
const EOCD_SIG = 0x06054b50;
|
|
25
|
+
const MIN_EOCD = 22; // fixed EOCD size with an empty comment
|
|
26
|
+
if (buf.length < MIN_EOCD)
|
|
27
|
+
return null;
|
|
28
|
+
// The EOCD sits at the end, preceded only by an optional comment (<= 64KB).
|
|
29
|
+
const floor = Math.max(0, buf.length - MIN_EOCD - 0xffff);
|
|
30
|
+
for (let i = buf.length - MIN_EOCD; i >= floor; i--) {
|
|
31
|
+
if (buf.readUInt32LE(i) === EOCD_SIG) {
|
|
32
|
+
const n = buf.readUInt16LE(i + 10); // total central-directory records
|
|
33
|
+
return n === 0xffff ? null : n;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
/** Pull the server's suggested filename out of Content-Disposition
|
|
39
|
+
* (`attachment; filename="<slug>.gip"`). The server slug is fresher than the
|
|
40
|
+
* locally cached one if the project was renamed. */
|
|
41
|
+
function dispositionFilename(headers) {
|
|
42
|
+
const m = /filename="([^"]+)"/.exec(headers.get('content-disposition') ?? '');
|
|
43
|
+
return m ? m[1] : null;
|
|
44
|
+
}
|
|
45
|
+
export const saveCommand = new Command('save')
|
|
46
|
+
.description('Save this app as a portable .gip bundle')
|
|
47
|
+
.argument('[output]', 'Output .gip path, or a directory to write <slug>.gip into (default: ./<slug>.gip)')
|
|
48
|
+
.option('--json', 'Output as JSON')
|
|
49
|
+
.addHelpText('after', '\nRestore anywhere with `gipity load <file>.gip` - it always creates a NEW project.')
|
|
50
|
+
.action((output, opts) => run('Save', async () => {
|
|
51
|
+
const config = requireConfig();
|
|
52
|
+
const doExport = () => downloadWithHeaders(`/projects/${config.projectGuid}/export`);
|
|
53
|
+
const { buffer, headers } = opts.json
|
|
54
|
+
? await doExport()
|
|
55
|
+
: await withSpinner('Exporting…', doExport, { done: null });
|
|
56
|
+
const filename = dispositionFilename(headers) ?? `${config.projectSlug}.gip`;
|
|
57
|
+
let dest = path.resolve(output ?? filename);
|
|
58
|
+
if (output && fs.existsSync(dest) && fs.statSync(dest).isDirectory()) {
|
|
59
|
+
dest = path.join(dest, filename);
|
|
60
|
+
}
|
|
61
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
62
|
+
fs.writeFileSync(dest, buffer);
|
|
63
|
+
const sha256 = createHash('sha256').update(buffer).digest('hex');
|
|
64
|
+
const skipped = Number(headers.get('x-gip-skipped') ?? 0);
|
|
65
|
+
const entries = zipEntryCount(buffer);
|
|
66
|
+
if (opts.json) {
|
|
67
|
+
console.log(JSON.stringify({
|
|
68
|
+
path: dest,
|
|
69
|
+
bytes: buffer.length,
|
|
70
|
+
sha256,
|
|
71
|
+
...(entries !== null ? { entries } : {}),
|
|
72
|
+
skipped,
|
|
73
|
+
}));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
console.log(success(`Saved ${path.basename(dest)}`));
|
|
77
|
+
console.log(` Path: ${dest}`);
|
|
78
|
+
console.log(` Size: ${formatBytes(buffer.length)}`);
|
|
79
|
+
if (entries !== null)
|
|
80
|
+
console.log(` Entries: ${entries} (including the bundle manifest)`);
|
|
81
|
+
console.log(` SHA-256: ${sha256}`);
|
|
82
|
+
if (skipped > 0) {
|
|
83
|
+
console.log('');
|
|
84
|
+
console.warn(warning(`${skipped} file(s) were skipped (unreadable or over 100 MB) - this bundle is partial.`));
|
|
85
|
+
console.warn(warning('Large assets belong in Gipity Storage, not in a code bundle.'));
|
|
86
|
+
}
|
|
87
|
+
console.log('');
|
|
88
|
+
console.log(muted('Restore anywhere with `gipity load ' + path.basename(dest) + '` (creates a new project).'));
|
|
89
|
+
}));
|
|
90
|
+
//# sourceMappingURL=save.js.map
|
package/dist/commands/status.js
CHANGED
|
@@ -5,7 +5,7 @@ import { homedir } from 'os';
|
|
|
5
5
|
import { getAuth, sessionExpired } from '../auth.js';
|
|
6
6
|
import { getConfig, liveUrl } from '../config.js';
|
|
7
7
|
import { brand, success, warning, muted, error as clrError } from '../colors.js';
|
|
8
|
-
import { GIPITY_PLUGIN_ID, GIPITY_MARKETPLACE_NAME, setupClaudeHooks, ensureGipityPlugin, ensureGipityPluginInstalled,
|
|
8
|
+
import { GIPITY_PLUGIN_ID, GIPITY_MARKETPLACE_NAME, setupClaudeHooks, ensureGipityPlugin, ensureGipityPluginInstalled, userScopeInstallState } from '../setup.js';
|
|
9
9
|
/** Hooks ship in the Gipity Claude Code plugin now. "Installed" means three
|
|
10
10
|
* things must all hold: the user-scope settings register the marketplace and
|
|
11
11
|
* enable the plugin (declarative), AND Claude Code actually has a user-scope
|
|
@@ -27,9 +27,15 @@ function checkGipityPlugin() {
|
|
|
27
27
|
missing.push('marketplace');
|
|
28
28
|
if (settings?.enabledPlugins?.[GIPITY_PLUGIN_ID] !== true)
|
|
29
29
|
missing.push('plugin');
|
|
30
|
-
|
|
30
|
+
const install = userScopeInstallState();
|
|
31
|
+
if (!install.current)
|
|
31
32
|
missing.push('install');
|
|
32
|
-
|
|
33
|
+
// "stale" = declaratively enabled AND a user-scope install exists; it's just
|
|
34
|
+
// behind the version this CLI needs. The hooks still LOAD (at the old
|
|
35
|
+
// version), so this is a version-lag update, not a dead plugin - don't warn
|
|
36
|
+
// as if files aren't syncing at all. Only true when `install` is the sole gap.
|
|
37
|
+
const stale = missing.length === 1 && missing[0] === 'install' && install.exists;
|
|
38
|
+
return { missing, ok: missing.length === 0, stale };
|
|
33
39
|
}
|
|
34
40
|
export const statusCommand = new Command('status')
|
|
35
41
|
.description('Show project and login status')
|
|
@@ -89,13 +95,28 @@ export const statusCommand = new Command('status')
|
|
|
89
95
|
ensureGipityPlugin(true);
|
|
90
96
|
setupClaudeHooks();
|
|
91
97
|
// Re-enabling the declarative keys isn't enough on CC >=2.1.x - also
|
|
92
|
-
// materialize the user-scope install so the hooks
|
|
98
|
+
// materialize (or update) the user-scope install so the hooks load.
|
|
93
99
|
ensureGipityPluginInstalled();
|
|
94
|
-
|
|
100
|
+
// Re-check rather than claim success blindly: a stale user-scope
|
|
101
|
+
// install only advances via `plugin update`, and if `claude` is off
|
|
102
|
+
// PATH nothing changed at all - reporting "repaired" then would be a
|
|
103
|
+
// lie the next `gipity status` immediately contradicts.
|
|
104
|
+
const after = checkGipityPlugin();
|
|
105
|
+
if (after.ok) {
|
|
106
|
+
console.log(`${muted('Hooks:')} ${success('repaired - Gipity plugin enabled')}`);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
console.log(`${muted('Hooks:')} ${warning(`repair incomplete (still missing: ${after.missing.join(', ')})`)}`);
|
|
110
|
+
console.log(muted('Ensure `claude` is on PATH, then re-run. Restart Claude Code to load the update.'));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else if (hookCheck.stale) {
|
|
114
|
+
console.log(`${muted('Hooks:')} ${warning('Gipity plugin out of date (an update is available)')}`);
|
|
115
|
+
console.log(muted('Hooks still load, but run `gipity status --repair-hooks` to update to the latest.'));
|
|
95
116
|
}
|
|
96
117
|
else {
|
|
97
118
|
console.log(`${muted('Hooks:')} ${warning(`Gipity plugin not enabled (missing: ${hookCheck.missing.join(', ')})`)}`);
|
|
98
|
-
console.log(muted('Run `gipity status --repair-hooks` to
|
|
119
|
+
console.log(muted('Run `gipity status --repair-hooks` to enable.'));
|
|
99
120
|
console.log(muted('Without it, files don\'t auto-sync and web CLI dispatches can\'t show Claude Code output.'));
|
|
100
121
|
}
|
|
101
122
|
}
|
package/dist/index.js
CHANGED
|
@@ -30,6 +30,8 @@ import { storageCommand } from './commands/storage.js';
|
|
|
30
30
|
import { claudeCommand } from './commands/claude.js';
|
|
31
31
|
import { addCommand } from './commands/add.js';
|
|
32
32
|
import { removeCommand } from './commands/remove.js';
|
|
33
|
+
import { saveCommand } from './commands/save.js';
|
|
34
|
+
import { loadCommand } from './commands/load.js';
|
|
33
35
|
import { logsCommand } from './commands/logs.js';
|
|
34
36
|
import { pageCommand } from './commands/page.js';
|
|
35
37
|
import { recordsCommand } from './commands/records.js';
|
|
@@ -39,6 +41,7 @@ import { secretsCommand } from './commands/secrets.js';
|
|
|
39
41
|
import { notifyCommand } from './commands/notify.js';
|
|
40
42
|
import { bugCommand } from './commands/bug.js';
|
|
41
43
|
import { paymentsCommand } from './commands/payments.js';
|
|
44
|
+
import { githubCommand } from './commands/github.js';
|
|
42
45
|
import { jobCommand } from './commands/job.js';
|
|
43
46
|
import { rbacCommand } from './commands/rbac.js';
|
|
44
47
|
import { auditCommand } from './commands/audit.js';
|
|
@@ -125,11 +128,11 @@ const program = new Command();
|
|
|
125
128
|
// --from Y`). enablePositionalOptions draws the boundary at the first command.
|
|
126
129
|
program.enablePositionalOptions();
|
|
127
130
|
// ── Command groups (logical ordering within each) ──────────────────────
|
|
128
|
-
const commonGroup = [skillCommand, projectCommand, addCommand, removeCommand, deployCommand];
|
|
131
|
+
const commonGroup = [skillCommand, projectCommand, addCommand, removeCommand, saveCommand, loadCommand, deployCommand];
|
|
129
132
|
const connectGroup = [claudeCommand, setupCommand, relayCommand];
|
|
130
133
|
const projectGroup = [domainCommand, statusCommand, initCommand];
|
|
131
134
|
const filesGroup = [fileCommand, storageCommand, syncCommand, pushCommand, uploadCommand];
|
|
132
|
-
const appBuildingGroup = [testCommand, fnCommand, serviceCommand, secretsCommand, notifyCommand, paymentsCommand, jobCommand, dbCommand, logsCommand, workflowCommand, realtimeCommand, rbacCommand, auditCommand, recordsCommand];
|
|
135
|
+
const appBuildingGroup = [testCommand, fnCommand, serviceCommand, secretsCommand, notifyCommand, paymentsCommand, githubCommand, jobCommand, dbCommand, logsCommand, workflowCommand, realtimeCommand, rbacCommand, auditCommand, recordsCommand];
|
|
133
136
|
const utilitiesGroup = [pageCommand, sandboxCommand, generateCommand, emailCommand, gmailCommand, locationCommand, textCommand];
|
|
134
137
|
const agentGroup = [chatCommand, memoryCommand, agentCommand, approvalCommand, bugCommand];
|
|
135
138
|
const setupGroup = [loginCommand, logoutCommand, tokenCommand, creditsCommand, doctorCommand, updateCommand, uninstallCommand];
|
package/dist/knowledge.js
CHANGED
|
@@ -108,6 +108,7 @@ mkdir -p ~/GipityProjects/<slug> && cd ~/GipityProjects/<slug> && gipity init <s
|
|
|
108
108
|
|
|
109
109
|
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>\`.
|
|
110
110
|
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").
|
|
111
|
+
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.
|
|
111
112
|
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.
|
|
112
113
|
Hit a platform bug or friction? File it in real time: \`gipity bug report --category <cli|deploy|template|kit|db|docs|skill|service|sandbox|other> --severity <S1|S2|S3|S4> --summary "<7 words max>" [--detail "<what failed + workaround>"]\` (see below).
|
|
113
114
|
Run \`gipity --help\` for the full list. Use \`--help\` on any command for details.
|
|
@@ -180,6 +181,7 @@ App development skills:
|
|
|
180
181
|
- \`app-database\` - app Postgres database: migrations, the db helper, transactions, table permissions
|
|
181
182
|
- \`app-debugging\` - debug a deployed app: page inspect/eval, screenshots, function logs
|
|
182
183
|
- \`app-development\` - functions, database, and API
|
|
184
|
+
- \`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
|
|
183
185
|
- \`app-testing\` - testing deployed app functions (ctx.fn.call/callAs, the isolated test DB)
|
|
184
186
|
- \`deploy\` - the deploy pipeline & gipity.yaml manifest
|
|
185
187
|
- \`jobs\` - long-running CPU + GPU compute jobs (Python / Node / bash)
|
|
@@ -19,6 +19,11 @@ import { hostname } from 'os';
|
|
|
19
19
|
import { post, del } from '../api.js';
|
|
20
20
|
import { resolveApiBase } from '../config.js';
|
|
21
21
|
import * as state from './state.js';
|
|
22
|
+
/** Relay agent tokens carry a finite expiry so an orphaned one - left behind
|
|
23
|
+
* when a user deletes ~/.gipity by hand, losing the guid we'd revoke it with -
|
|
24
|
+
* self-expires instead of living forever. A live relay is unaffected:
|
|
25
|
+
* ensureRelayAgentToken() re-mints on the first 401 after expiry. */
|
|
26
|
+
const RELAY_AGENT_TOKEN_EXPIRY_DAYS = 60;
|
|
22
27
|
/** Validate a stored token against the API. Only a definitive 401 counts as
|
|
23
28
|
* invalid - transient failures (network, 5xx) must not discard a good token
|
|
24
29
|
* and trigger a re-mint storm. */
|
|
@@ -48,7 +53,7 @@ export async function ensureRelayAgentToken() {
|
|
|
48
53
|
}
|
|
49
54
|
try {
|
|
50
55
|
const name = `Relay on ${state.getDevice()?.name ?? hostname()}`;
|
|
51
|
-
const res = await post('/auth/agent-tokens', { name });
|
|
56
|
+
const res = await post('/auth/agent-tokens', { name, expiresInDays: RELAY_AGENT_TOKEN_EXPIRY_DAYS });
|
|
52
57
|
state.setAgentToken(res.data.token, res.data.shortGuid);
|
|
53
58
|
return res.data.token;
|
|
54
59
|
}
|
package/dist/relay/daemon.js
CHANGED
|
@@ -18,6 +18,7 @@ import { deviceFetch, bridgeAbort as bridgeAbortImpl } from './device-http.js';
|
|
|
18
18
|
import { ensureRelayAgentToken } from './agent-token.js';
|
|
19
19
|
import { redactEntries, redactString, normalizeSecrets } from './redact.js';
|
|
20
20
|
import { getMachineId } from './machine-id.js';
|
|
21
|
+
import { collectDiagnostics } from './diagnostics.js';
|
|
21
22
|
// Re-exported so the existing `relay-bridge-abort.test.ts` keeps working.
|
|
22
23
|
// New callers should import from device-http.js directly.
|
|
23
24
|
export const bridgeAbort = bridgeAbortImpl;
|
|
@@ -28,6 +29,10 @@ export const RELAY_LOG_PATH = join(homedir(), '.gipity', 'relay.log');
|
|
|
28
29
|
// slightly after its own deadline; we accept that. Values can be overridden
|
|
29
30
|
// by env for tests.
|
|
30
31
|
const HEARTBEAT_INTERVAL_MS = parseInt(process.env.GIPITY_RELAY_HEARTBEAT_MS || '60000', 10);
|
|
32
|
+
// How often the heartbeat carries a fresh diagnostics snapshot (host specs +
|
|
33
|
+
// versions). The 60s liveness ping stays bare; only this cadence runs the
|
|
34
|
+
// costlier version/GPU probes. First tick fires immediately at daemon start.
|
|
35
|
+
const DIAGNOSTICS_INTERVAL_MS = parseInt(process.env.GIPITY_RELAY_DIAGNOSTICS_MS || String(24 * 60 * 60 * 1000), 10);
|
|
31
36
|
const LONG_POLL_TIMEOUT_MS = parseInt(process.env.GIPITY_RELAY_POLL_TIMEOUT_MS || '35000', 10);
|
|
32
37
|
const BACKOFF_BASE_MS = parseInt(process.env.GIPITY_RELAY_BACKOFF_BASE_MS || '1000', 10);
|
|
33
38
|
const BACKOFF_MAX_MS = parseInt(process.env.GIPITY_RELAY_BACKOFF_MAX_MS || '30000', 10);
|
|
@@ -284,9 +289,24 @@ async function heartbeatLoop(ctx) {
|
|
|
284
289
|
// Log the "session expired" warning only on the transition into that state,
|
|
285
290
|
// not every 60s, so a genuinely-lapsed session doesn't spam the relay log.
|
|
286
291
|
let sessionWarnLogged = false;
|
|
292
|
+
// Diagnostics: attach a fresh snapshot on the first heartbeat and every
|
|
293
|
+
// DIAGNOSTICS_INTERVAL_MS after, but only if the user consented. Between
|
|
294
|
+
// refreshes the heartbeat is a bare liveness ping.
|
|
295
|
+
let lastDiagnosticsAt = 0;
|
|
287
296
|
while (!ctx.abort.signal.aborted) {
|
|
288
297
|
try {
|
|
289
|
-
|
|
298
|
+
let body = {};
|
|
299
|
+
if (state.diagnosticsConsented() && Date.now() - lastDiagnosticsAt >= DIAGNOSTICS_INTERVAL_MS) {
|
|
300
|
+
try {
|
|
301
|
+
body = { diagnostics: await collectDiagnostics() };
|
|
302
|
+
lastDiagnosticsAt = Date.now();
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
// Never let a diagnostics probe failure block the liveness ping.
|
|
306
|
+
log('debug', 'diagnostics collection failed', { err: err?.message });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const r = await deviceFetch('POST', '/remote-devices/heartbeat', body, 10_000, ctx.abort.signal);
|
|
290
310
|
if (r.status === 401) {
|
|
291
311
|
log('warn', 'heartbeat 401 - device revoked, exiting clean');
|
|
292
312
|
ctx.abort.abort('revoked');
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relay diagnostics snapshot: non-PII host + version telemetry the daemon
|
|
3
|
+
* reports on its heartbeat (once at startup, then ~daily). Sent as the
|
|
4
|
+
* heartbeat body `{ diagnostics }` and stored on the server as
|
|
5
|
+
* `remote_devices.diagnostics` (JSONB); the web client surfaces it in the
|
|
6
|
+
* Relays table (versions inline, host specs on hover).
|
|
7
|
+
*
|
|
8
|
+
* Design rules:
|
|
9
|
+
* - EVERY probe is best-effort: a failure yields undefined/null for that
|
|
10
|
+
* field, never a thrown error. `collectDiagnostics()` always resolves.
|
|
11
|
+
* - NO personally-identifying data: no paths, no hostname, no username -
|
|
12
|
+
* only counts and non-identifying machine specs.
|
|
13
|
+
* - Cheap in-memory `os.*` reads are gathered inline; the costlier probes
|
|
14
|
+
* (agent `--version` subprocesses, GPU detection) are bounded with a short
|
|
15
|
+
* timeout and only run when the daemon refreshes the snapshot (startup +
|
|
16
|
+
* ~daily), never on the plain 60s liveness ping.
|
|
17
|
+
*
|
|
18
|
+
* Wire shape mirrors packages/shared `RelayDiagnostics` on the platform side.
|
|
19
|
+
*/
|
|
20
|
+
import { cpus, freemem, totalmem, loadavg, release, uptime, homedir } from 'os';
|
|
21
|
+
import { statfsSync, readdirSync } from 'fs';
|
|
22
|
+
import { join } from 'path';
|
|
23
|
+
import { resolveCommand, spawnCommand } from '../platform.js';
|
|
24
|
+
import { cliVersion } from '../client-context.js';
|
|
25
|
+
const PROBE_TIMEOUT_MS = 4000;
|
|
26
|
+
/** Extract a semver-ish token from a `--version` line ("2.0.14 (Claude Code)"
|
|
27
|
+
* → "2.0.14"). Returns undefined when nothing version-like is present.
|
|
28
|
+
* Exported for unit tests. */
|
|
29
|
+
export function parseVersion(out) {
|
|
30
|
+
const m = out.match(/\d+\.\d+(?:\.\d+)?(?:[-.\w]*)?/);
|
|
31
|
+
return m ? m[0] : undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Run a command and capture stdout, ASYNC + non-blocking with a hard timeout.
|
|
35
|
+
* This must not be `spawnSync`: `collectDiagnostics()` runs inside the daemon's
|
|
36
|
+
* heartbeat loop, which shares one event loop with the dispatch + cancellation
|
|
37
|
+
* loops - a synchronous spawn would freeze all of them for the probe's
|
|
38
|
+
* duration. Resolves '' on spawn error, timeout (child SIGKILL'd), or non-zero
|
|
39
|
+
* exit with no output; never rejects.
|
|
40
|
+
*/
|
|
41
|
+
function runProbe(cmd, args) {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
let out = '';
|
|
44
|
+
let settled = false;
|
|
45
|
+
const done = (s) => { if (!settled) {
|
|
46
|
+
settled = true;
|
|
47
|
+
resolve(s);
|
|
48
|
+
} };
|
|
49
|
+
let child;
|
|
50
|
+
try {
|
|
51
|
+
child = spawnCommand(resolveCommand(cmd), args, { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return done('');
|
|
55
|
+
}
|
|
56
|
+
const timer = setTimeout(() => { try {
|
|
57
|
+
child.kill('SIGKILL');
|
|
58
|
+
}
|
|
59
|
+
catch { /* gone */ } done(out); }, PROBE_TIMEOUT_MS);
|
|
60
|
+
child.stdout?.on('data', (d) => { out += d.toString(); });
|
|
61
|
+
child.on('error', () => { clearTimeout(timer); done(''); });
|
|
62
|
+
child.on('close', () => { clearTimeout(timer); done(out); });
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/** Run `<cmd> --version` (bounded, best-effort). undefined if the command is
|
|
66
|
+
* absent, errors, or prints nothing version-like. */
|
|
67
|
+
async function probeVersion(cmd) {
|
|
68
|
+
return parseVersion(await runProbe(cmd, ['--version']));
|
|
69
|
+
}
|
|
70
|
+
/** Best-effort GPU description. Short-timeout, platform-specific, null on any
|
|
71
|
+
* failure - never blocks the snapshot. */
|
|
72
|
+
async function detectGpu() {
|
|
73
|
+
if (process.platform === 'darwin') {
|
|
74
|
+
const m = (await runProbe('system_profiler', ['SPDisplaysDataType'])).match(/Chipset Model:\s*(.+)/);
|
|
75
|
+
return m ? m[1].trim() : null;
|
|
76
|
+
}
|
|
77
|
+
if (process.platform === 'linux') {
|
|
78
|
+
// nvidia-smi is fast + precise when present; fall back to lspci.
|
|
79
|
+
const nvName = (await runProbe('nvidia-smi', ['--query-gpu=name', '--format=csv,noheader'])).split('\n')[0]?.trim();
|
|
80
|
+
if (nvName)
|
|
81
|
+
return nvName;
|
|
82
|
+
const line = (await runProbe('sh', ['-c', "lspci | grep -iE 'vga|3d|display'"])).split('\n')[0]?.trim();
|
|
83
|
+
if (!line)
|
|
84
|
+
return null;
|
|
85
|
+
// Strip the "00:02.0 VGA compatible controller: " prefix.
|
|
86
|
+
const after = line.split(/:\s/).slice(2).join(': ').trim();
|
|
87
|
+
return after || line;
|
|
88
|
+
}
|
|
89
|
+
if (process.platform === 'win32') {
|
|
90
|
+
const line = (await runProbe('wmic', ['path', 'win32_VideoController', 'get', 'name']))
|
|
91
|
+
.split('\n').map(l => l.trim()).filter(l => l && l !== 'Name')[0];
|
|
92
|
+
return line ?? null;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
/** Free/total bytes on the projects volume (best-effort). */
|
|
97
|
+
function diskUsage() {
|
|
98
|
+
try {
|
|
99
|
+
if (typeof statfsSync !== 'function')
|
|
100
|
+
return undefined;
|
|
101
|
+
const st = statfsSync(join(homedir(), 'GipityProjects'), { bigint: false });
|
|
102
|
+
if (!st || !st.bsize)
|
|
103
|
+
return undefined;
|
|
104
|
+
return { total: st.bsize * st.blocks, free: st.bsize * st.bavail };
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Projects dir may not exist yet - try the home volume instead.
|
|
108
|
+
try {
|
|
109
|
+
const st = statfsSync(homedir(), { bigint: false });
|
|
110
|
+
return { total: st.bsize * st.blocks, free: st.bsize * st.bavail };
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** Count of directories under ~/GipityProjects (no names). */
|
|
118
|
+
function localProjectCount() {
|
|
119
|
+
try {
|
|
120
|
+
return readdirSync(join(homedir(), 'GipityProjects'), { withFileTypes: true })
|
|
121
|
+
.filter(e => e.isDirectory() && !e.name.startsWith('.')).length;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Gather the full snapshot. Cheap `os.*` reads plus the bounded subprocess
|
|
129
|
+
* probes. Always resolves (best-effort per field).
|
|
130
|
+
*/
|
|
131
|
+
export async function collectDiagnostics() {
|
|
132
|
+
const cores = (() => { try {
|
|
133
|
+
return cpus();
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return [];
|
|
137
|
+
} })();
|
|
138
|
+
// Run the subprocess probes concurrently (each already bounded + best-effort)
|
|
139
|
+
// so the whole snapshot costs one timeout, not the sum of four.
|
|
140
|
+
const [claude, codex, cursor, gpu] = await Promise.all([
|
|
141
|
+
probeVersion('claude'), probeVersion('codex'), probeVersion('cursor'), detectGpu(),
|
|
142
|
+
]);
|
|
143
|
+
const agents = {};
|
|
144
|
+
if (claude)
|
|
145
|
+
agents.claude_code = claude;
|
|
146
|
+
if (codex)
|
|
147
|
+
agents.codex = codex;
|
|
148
|
+
if (cursor)
|
|
149
|
+
agents.cursor = cursor;
|
|
150
|
+
return {
|
|
151
|
+
collected_at: new Date().toISOString(),
|
|
152
|
+
gipity_version: cliVersion(),
|
|
153
|
+
node_version: process.versions.node,
|
|
154
|
+
os: { platform: process.platform, release: safe(() => release()), arch: process.arch },
|
|
155
|
+
cpu: { count: cores.length || undefined, model: cores[0]?.model?.trim() || undefined },
|
|
156
|
+
mem: { total: safe(() => totalmem()), free: safe(() => freemem()) },
|
|
157
|
+
load1: safe(() => Math.round(loadavg()[0] * 100) / 100),
|
|
158
|
+
uptime_s: safe(() => Math.round(uptime())),
|
|
159
|
+
disk: diskUsage(),
|
|
160
|
+
gpu,
|
|
161
|
+
agents: Object.keys(agents).length ? agents : undefined,
|
|
162
|
+
projects_local: localProjectCount(),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function safe(fn) {
|
|
166
|
+
try {
|
|
167
|
+
return fn();
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=diagnostics.js.map
|
package/dist/relay/onboarding.js
CHANGED
|
@@ -109,6 +109,14 @@ export async function runRelaySetup(opts) {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
+
// Diagnostics consent - default on, clearly opt-out. Only ask once.
|
|
113
|
+
if (state.getDiagnosticsConsent() === undefined) {
|
|
114
|
+
const diag = await confirm(' Share anonymous diagnostics (CPU/GPU/memory/disk/versions) to improve reliability?', { default: 'yes' });
|
|
115
|
+
state.setDiagnosticsConsent(diag);
|
|
116
|
+
console.log(` ${dim(diag
|
|
117
|
+
? 'Thanks - no personal data or file paths are ever sent. Turn off anytime with `gipity relay diagnostics off`.'
|
|
118
|
+
: 'Diagnostics off. Turn on anytime with `gipity relay diagnostics on`.')}`);
|
|
119
|
+
}
|
|
112
120
|
console.log('');
|
|
113
121
|
console.log(` ${success(`Registered as ${bold(device.name)} (${device.guid}).`)}`);
|
|
114
122
|
console.log(` ${dim('In the Gipity web CLI, type `/claude` to dispatch messages to this PC.')}`);
|
package/dist/relay/state.js
CHANGED
|
@@ -38,6 +38,7 @@ export function loadState() {
|
|
|
38
38
|
paused: Boolean(raw.paused),
|
|
39
39
|
relay_enabled: typeof raw.relay_enabled === 'boolean' ? raw.relay_enabled : undefined,
|
|
40
40
|
onboard_shown: Boolean(raw.onboard_shown),
|
|
41
|
+
diagnostics_consent: typeof raw.diagnostics_consent === 'boolean' ? raw.diagnostics_consent : undefined,
|
|
41
42
|
agent_token: typeof raw.agent_token === 'string' ? raw.agent_token : null,
|
|
42
43
|
agent_token_guid: typeof raw.agent_token_guid === 'string' ? raw.agent_token_guid : null,
|
|
43
44
|
};
|
|
@@ -113,6 +114,23 @@ export function isRelayEnabled() {
|
|
|
113
114
|
export function setRelayEnabled(enabled) {
|
|
114
115
|
mutate(s => { s.relay_enabled = enabled; });
|
|
115
116
|
}
|
|
117
|
+
// ─── Diagnostics consent (tri-state, default-on) ───────────────────────
|
|
118
|
+
/** Stored preference: `undefined` = never asked; `true`/`false` = explicit. */
|
|
119
|
+
export function getDiagnosticsConsent() {
|
|
120
|
+
return loadState().diagnostics_consent;
|
|
121
|
+
}
|
|
122
|
+
export function setDiagnosticsConsent(consent) {
|
|
123
|
+
mutate(s => { s.diagnostics_consent = consent; });
|
|
124
|
+
}
|
|
125
|
+
/** Effective consent used by the daemon: default-on unless the user explicitly
|
|
126
|
+
* opted out OR a headless opt-out env var (GIPITY_NO_DIAGNOSTICS / DO_NOT_TRACK)
|
|
127
|
+
* is set. Truthy env value ("1"/"true"/anything non-empty) disables. */
|
|
128
|
+
export function diagnosticsConsented() {
|
|
129
|
+
const env = process.env.GIPITY_NO_DIAGNOSTICS ?? process.env.DO_NOT_TRACK;
|
|
130
|
+
if (env && env !== '0' && env.toLowerCase() !== 'false')
|
|
131
|
+
return false;
|
|
132
|
+
return loadState().diagnostics_consent !== false;
|
|
133
|
+
}
|
|
116
134
|
// ─── Daemon PID file (lives at ~/.gipity/relay.pid) ────────────────────
|
|
117
135
|
const RELAY_PID_FILE = join(RELAY_DIR, 'relay.pid');
|
|
118
136
|
export function getDaemonPidPath() {
|
package/dist/setup.js
CHANGED
|
@@ -244,27 +244,39 @@ function versionGte(have, want) {
|
|
|
244
244
|
}
|
|
245
245
|
return true;
|
|
246
246
|
}
|
|
247
|
-
/**
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
* (
|
|
253
|
-
|
|
247
|
+
/** Parsed user-scope install state for the Gipity plugin, read straight from
|
|
248
|
+
* installed_plugins.json (no subprocess). `exists` is true when Claude Code
|
|
249
|
+
* records ANY user-scope install; `current` narrows that to one at >= the
|
|
250
|
+
* version this CLI needs. The two differ exactly when a stale install lags a
|
|
251
|
+
* plugin-version bump - the case that must be UPGRADED, not freshly installed
|
|
252
|
+
* (a bare `claude plugin install` no-ops on an already-present user install
|
|
253
|
+
* and never advances its version). */
|
|
254
|
+
export function userScopeInstallState() {
|
|
254
255
|
try {
|
|
255
256
|
const p = join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
256
257
|
const data = JSON.parse(readFileSync(p, 'utf-8'));
|
|
257
258
|
const entries = data?.plugins?.[GIPITY_PLUGIN_ID];
|
|
258
259
|
if (!Array.isArray(entries))
|
|
259
|
-
return false;
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
260
|
+
return { exists: false, current: false };
|
|
261
|
+
const userEntries = entries.filter((e) => e?.scope === 'user');
|
|
262
|
+
return {
|
|
263
|
+
exists: userEntries.length > 0,
|
|
264
|
+
current: userEntries.some((e) => typeof e?.version === 'string' && versionGte(e.version, GIPITY_PLUGIN_VERSION)),
|
|
265
|
+
};
|
|
263
266
|
}
|
|
264
267
|
catch {
|
|
265
|
-
return false;
|
|
268
|
+
return { exists: false, current: false };
|
|
266
269
|
}
|
|
267
270
|
}
|
|
271
|
+
/** True when Claude Code already records a USER-scope install of the Gipity
|
|
272
|
+
* plugin at >= the version this CLI needs - the common case, letting the
|
|
273
|
+
* caller skip the (slow) reinstall. Reads installed_plugins.json directly so
|
|
274
|
+
* the check costs no subprocess. Exported so `gipity status` can tell an
|
|
275
|
+
* actually-loaded plugin apart from one that's merely enabled-but-uninstalled
|
|
276
|
+
* (which would otherwise read as a false-green "hooks enabled"). */
|
|
277
|
+
export function userScopePluginCurrent() {
|
|
278
|
+
return userScopeInstallState().current;
|
|
279
|
+
}
|
|
268
280
|
function claudeOnPath() {
|
|
269
281
|
const probe = spawnSyncCommand(process.platform === 'win32' ? 'where' : 'which', ['claude'], {
|
|
270
282
|
encoding: 'utf-8',
|
|
@@ -286,22 +298,31 @@ function claudeOnPath() {
|
|
|
286
298
|
* never break `gipity claude`. Skips entirely when the user-scope install is
|
|
287
299
|
* already current, so it shells out at most once per plugin-version bump. */
|
|
288
300
|
export function ensureGipityPluginInstalled() {
|
|
289
|
-
|
|
301
|
+
const state = userScopeInstallState();
|
|
302
|
+
if (state.current)
|
|
290
303
|
return;
|
|
291
304
|
if (!claudeOnPath())
|
|
292
305
|
return;
|
|
293
|
-
// Refresh the marketplace clone so
|
|
294
|
-
// then (re)install at user scope - idempotent, and upgrades an older or
|
|
295
|
-
// project-scoped install to the current one at user scope.
|
|
306
|
+
// Refresh the marketplace clone so install/update resolves the current version.
|
|
296
307
|
// resolveCommand: on Windows `claude` is a .cmd shim that spawn can't launch
|
|
297
|
-
// without an explicit path, so resolve it (otherwise the
|
|
308
|
+
// without an explicit path, so resolve it (otherwise the command silently
|
|
298
309
|
// ENOENTs and the plugin's hooks never land at user scope).
|
|
299
310
|
const claudeCmd = resolveCommand('claude');
|
|
300
311
|
spawnSyncCommand(claudeCmd, ['plugin', 'marketplace', 'update', GIPITY_MARKETPLACE_NAME], {
|
|
301
312
|
stdio: 'ignore',
|
|
302
313
|
timeout: 120_000,
|
|
303
314
|
});
|
|
304
|
-
|
|
315
|
+
// A bare `plugin install` only materializes a MISSING install - on an
|
|
316
|
+
// already-present but stale user-scope install (the version this CLI just
|
|
317
|
+
// bumped past) it no-ops and leaves the old version registered, so
|
|
318
|
+
// userScopePluginCurrent() stays false forever and `gipity status` reports
|
|
319
|
+
// `missing: install` on every run. `plugin update` is the command that
|
|
320
|
+
// actually advances a registered user-scope install to the marketplace's
|
|
321
|
+
// current version; `install` is only right when nothing is installed yet.
|
|
322
|
+
const verb = state.exists
|
|
323
|
+
? ['plugin', 'update', GIPITY_PLUGIN_ID, '--scope', 'user']
|
|
324
|
+
: ['plugin', 'install', GIPITY_PLUGIN_ID, '--scope', 'user'];
|
|
325
|
+
spawnSyncCommand(claudeCmd, verb, {
|
|
305
326
|
stdio: 'ignore',
|
|
306
327
|
timeout: 120_000,
|
|
307
328
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gipity",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.414",
|
|
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__/platform.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__/ingest-queue.test.js dist/__tests__/stream-delta.test.js dist/__tests__/phase-tracker.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-storage.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-setup.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__/platform.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-diagnostics.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__/ingest-queue.test.js dist/__tests__/stream-delta.test.js dist/__tests__/phase-tracker.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-storage.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-setup.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:smoke:quick": "node scripts/smoke-quick.mjs",
|
|
18
18
|
"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-sync-stress-live.test.js dist/__tests__/cli-e2e-rollback-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",
|
|
19
19
|
"test:e2e:sync": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-sync-live.test.js",
|