gipity 1.0.413 → 1.0.416
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/chat.js +11 -5
- package/dist/commands/github.js +80 -0
- package/dist/commands/load.js +221 -0
- package/dist/commands/project.js +25 -14
- package/dist/commands/records.js +1 -1
- package/dist/commands/relay.js +29 -0
- package/dist/commands/save.js +90 -0
- package/dist/commands/service.js +1 -1
- package/dist/commands/setup.js +1 -1
- package/dist/commands/skill.js +1 -1
- package/dist/commands/status.js +27 -6
- package/dist/commands/text.js +1 -1
- package/dist/index.js +106 -15
- package/dist/knowledge.js +3 -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 +69 -5
- package/dist/relay/setup.js +29 -2
- 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'));
|
package/dist/commands/chat.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { get, post, put, del } from '../api.js';
|
|
3
|
-
import { resolveProjectContext, saveConfig } from '../config.js';
|
|
3
|
+
import { resolveProjectContext, requireConfig, saveConfig } from '../config.js';
|
|
4
4
|
import { sync } from '../sync.js';
|
|
5
5
|
import { error as clrError, muted, success } from '../colors.js';
|
|
6
6
|
import { run, printList, printResult } from '../helpers/index.js';
|
|
@@ -98,13 +98,19 @@ chatCommand
|
|
|
98
98
|
});
|
|
99
99
|
}));
|
|
100
100
|
chatCommand
|
|
101
|
-
.command('rename <
|
|
102
|
-
.description('Rename a chat')
|
|
101
|
+
.command('rename <title...>')
|
|
102
|
+
.description('Rename a chat (the current chat by default; changes the tab title only)')
|
|
103
|
+
.option('--guid <guid>', 'Chat guid to rename (defaults to the current chat)')
|
|
103
104
|
.option('--json', 'Output as JSON')
|
|
104
|
-
.action((
|
|
105
|
+
.action((titleParts, opts) => run('Rename', async () => {
|
|
105
106
|
const title = titleParts.join(' ');
|
|
107
|
+
const guid = opts.guid || requireConfig().conversationGuid;
|
|
108
|
+
if (!guid) {
|
|
109
|
+
console.error(clrError('No current chat. Start a chat first, or pass --guid <guid>.'));
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
106
112
|
await put(`/conversations/${guid}`, { title });
|
|
107
|
-
printResult(success(`Renamed
|
|
113
|
+
printResult(success(`Renamed chat → "${title}".`), opts, { guid, title });
|
|
108
114
|
}));
|
|
109
115
|
chatCommand
|
|
110
116
|
.command('archive <guid>')
|
|
@@ -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/project.js
CHANGED
|
@@ -36,6 +36,31 @@ export const projectCommand = new Command('project')
|
|
|
36
36
|
return `${p.slug}${active}${def}`;
|
|
37
37
|
});
|
|
38
38
|
}));
|
|
39
|
+
projectCommand
|
|
40
|
+
.command('rename <name...>')
|
|
41
|
+
.description('Rename the current project (display name only; slug and URLs unchanged)')
|
|
42
|
+
.option('--project <target>', 'Project name, slug, or guid to rename (defaults to the current project)')
|
|
43
|
+
.option('--json', 'Output as JSON')
|
|
44
|
+
.action((nameParts, opts) => run('Rename', async () => {
|
|
45
|
+
const name = nameParts.join(' ');
|
|
46
|
+
const config = requireConfig();
|
|
47
|
+
let guid = config.projectGuid;
|
|
48
|
+
if (opts.project) {
|
|
49
|
+
const res = await get('/projects?limit=1000');
|
|
50
|
+
const match = res.data.find(p => p.slug === opts.project || p.name === opts.project || p.short_guid === opts.project);
|
|
51
|
+
if (!match) {
|
|
52
|
+
console.error(clrError(`Project "${opts.project}" not found.`));
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
guid = match.short_guid;
|
|
56
|
+
}
|
|
57
|
+
if (!guid) {
|
|
58
|
+
console.error(clrError('No current project. Link one first, or pass --project <name>.'));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
await put(`/projects/${guid}`, { name });
|
|
62
|
+
printResult(success(`Renamed project → "${name}".`), opts, { guid, name });
|
|
63
|
+
}));
|
|
39
64
|
projectCommand
|
|
40
65
|
.command('create <name>')
|
|
41
66
|
.description('Create a project')
|
|
@@ -132,20 +157,6 @@ projectCommand
|
|
|
132
157
|
await del(`/projects/${match.short_guid}`);
|
|
133
158
|
printResult(`Deleted "${match.name}".`, opts, { deleted: match.slug });
|
|
134
159
|
}));
|
|
135
|
-
projectCommand
|
|
136
|
-
.command('rename <name> <new-name>')
|
|
137
|
-
.description('Rename a project')
|
|
138
|
-
.option('--json', 'Output as JSON')
|
|
139
|
-
.action((name, newName, opts) => run('Rename', async () => {
|
|
140
|
-
const res = await get('/projects?limit=1000');
|
|
141
|
-
const match = res.data.find(p => p.slug === name || p.name === name || p.short_guid === name);
|
|
142
|
-
if (!match) {
|
|
143
|
-
console.error(clrError(`Project "${name}" not found.`));
|
|
144
|
-
process.exit(1);
|
|
145
|
-
}
|
|
146
|
-
await put(`/projects/${match.short_guid}`, { name: newName });
|
|
147
|
-
printResult(`Renamed "${match.name}" → "${newName}".`, opts, { renamed: match.slug, from: match.name, to: newName });
|
|
148
|
-
}));
|
|
149
160
|
projectCommand
|
|
150
161
|
.command('info')
|
|
151
162
|
.description('Show current project')
|
package/dist/commands/records.js
CHANGED
|
@@ -9,7 +9,7 @@ import { confirm } from '../utils.js';
|
|
|
9
9
|
// Records API is the only records surface that exists server-side; there is no
|
|
10
10
|
// /projects/<guid>/records mirror.)
|
|
11
11
|
export const recordsCommand = new Command('records')
|
|
12
|
-
.description('Manage records');
|
|
12
|
+
.description('Manage records-kit data (registry-driven CRUD)');
|
|
13
13
|
recordsCommand
|
|
14
14
|
.command('list')
|
|
15
15
|
.description('List record tables')
|
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/service.js
CHANGED
|
@@ -20,7 +20,7 @@ const SERVICES = [
|
|
|
20
20
|
{ name: 'location/geocode', method: 'POST', desc: 'Reverse geocode ({ lat, lon })' },
|
|
21
21
|
];
|
|
22
22
|
export const serviceCommand = new Command('service')
|
|
23
|
-
.description('Call an app service')
|
|
23
|
+
.description('Call an app service (llm, tts, image, transcribe, ...)')
|
|
24
24
|
.addHelpText('after', '\nCall a Gipity app service: LLM, image, music, TTS, and more.');
|
|
25
25
|
serviceCommand
|
|
26
26
|
.command('list')
|
package/dist/commands/setup.js
CHANGED
|
@@ -20,7 +20,7 @@ export const setupCommand = new Command('setup')
|
|
|
20
20
|
.description('Set up this computer as a relay (no project, no launch)')
|
|
21
21
|
.action(async () => {
|
|
22
22
|
try {
|
|
23
|
-
|
|
23
|
+
// Leading blank comes from the central output frame (installOutputFrame).
|
|
24
24
|
console.log(` ${bold('Gipity setup')} ${muted('- get this computer ready as a relay')}`);
|
|
25
25
|
console.log('');
|
|
26
26
|
// ── Step 1: Auth ──────────────────────────────────────────────────
|
package/dist/commands/skill.js
CHANGED
|
@@ -17,7 +17,7 @@ function readInstalledKitReadme(name) {
|
|
|
17
17
|
return existsSync(readme) ? readFileSync(readme, 'utf-8') : null;
|
|
18
18
|
}
|
|
19
19
|
export const skillCommand = new Command('skill')
|
|
20
|
-
.description('
|
|
20
|
+
.description('Task docs - read the matching skill before building');
|
|
21
21
|
skillCommand
|
|
22
22
|
.command('list')
|
|
23
23
|
.description('List skills')
|