gipity 1.0.398 → 1.0.400
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/commands/add.js +8 -2
- package/dist/commands/chat.js +11 -2
- package/dist/commands/claude.js +10 -2
- package/dist/commands/deploy.js +5 -2
- package/dist/commands/generate.js +19 -9
- package/dist/commands/page-screenshot.js +8 -1
- package/dist/commands/remove.js +6 -2
- package/dist/commands/sandbox.js +9 -3
- package/dist/helpers/sync.js +9 -1
- package/dist/index.js +20 -2
- package/dist/progress.js +115 -8
- package/dist/relay/daemon.js +6 -0
- package/dist/upload.js +27 -2
- package/package.json +2 -1
package/dist/commands/add.js
CHANGED
|
@@ -7,6 +7,7 @@ import { requireConfig } from '../config.js';
|
|
|
7
7
|
import { sync } from '../sync.js';
|
|
8
8
|
import { success, muted, bold } from '../colors.js';
|
|
9
9
|
import { run } from '../helpers/index.js';
|
|
10
|
+
import { createProgressReporter, withSpinner } from '../progress.js';
|
|
10
11
|
const STARTERS = [
|
|
11
12
|
{ key: 'web-vision-cam', hint: 'fullscreen camera app with on-device vision (MediaPipe)' },
|
|
12
13
|
{ key: 'object-spotter', hint: 'camera app that boxes, labels, and counts objects (YOLOX on-device)' },
|
|
@@ -198,9 +199,14 @@ export const addCommand = new Command('add')
|
|
|
198
199
|
force: opts.force,
|
|
199
200
|
};
|
|
200
201
|
}
|
|
201
|
-
|
|
202
|
+
// The server runs the whole install pipeline before responding; animate the
|
|
203
|
+
// wait, then clear the spinner so the installed-files list is the result.
|
|
204
|
+
const doAdd = () => post(`/projects/${config.projectGuid}/add`, body);
|
|
205
|
+
const res = opts.json
|
|
206
|
+
? await doAdd()
|
|
207
|
+
: await withSpinner('Installing…', doAdd, { done: null });
|
|
202
208
|
// Pull the created/installed files down to local.
|
|
203
|
-
const syncResult = await sync({ interactive: false });
|
|
209
|
+
const syncResult = await sync({ interactive: false, progress: opts.json ? undefined : createProgressReporter() });
|
|
204
210
|
const data = res.data;
|
|
205
211
|
if (opts.json) {
|
|
206
212
|
console.log(JSON.stringify({ ...data, synced: syncResult.applied }));
|
package/dist/commands/chat.js
CHANGED
|
@@ -4,6 +4,7 @@ import { resolveProjectContext, 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';
|
|
7
|
+
import { createProgressReporter, withSpinner } from '../progress.js';
|
|
7
8
|
export const chatCommand = new Command('chat')
|
|
8
9
|
.description('Send a message to your agent')
|
|
9
10
|
.argument('<message>', 'Message to send')
|
|
@@ -19,7 +20,12 @@ export const chatCommand = new Command('chat')
|
|
|
19
20
|
const body = useExisting
|
|
20
21
|
? { content: message, projectGuid: config.projectGuid }
|
|
21
22
|
: { agentGuid: config.agentGuid, content: message, projectGuid: config.projectGuid };
|
|
22
|
-
|
|
23
|
+
// The agent can think for many seconds; animate the wait, then clear the
|
|
24
|
+
// spinner (done:null) so the reply itself is the result. JSON mode skips it.
|
|
25
|
+
const doChat = () => post(endpoint, body);
|
|
26
|
+
const res = opts.json
|
|
27
|
+
? await doChat()
|
|
28
|
+
: await withSpinner('Thinking…', doChat, { done: null });
|
|
23
29
|
// Save conversation guid for continuity. Skipped in one-off mode: the
|
|
24
30
|
// config was resolved from the server's Home project and there is no
|
|
25
31
|
// local `.gipity.json` to update - persisting here would create one in
|
|
@@ -31,7 +37,10 @@ export const chatCommand = new Command('chat')
|
|
|
31
37
|
let syncSummary = '';
|
|
32
38
|
let syncChanges = [];
|
|
33
39
|
if (res.data.filesChanged) {
|
|
34
|
-
const syncResult = await sync({
|
|
40
|
+
const syncResult = await sync({
|
|
41
|
+
interactive: false,
|
|
42
|
+
progress: opts.json ? undefined : createProgressReporter(),
|
|
43
|
+
});
|
|
35
44
|
if (syncResult.applied > 0) {
|
|
36
45
|
syncSummary = `\nSynced ${syncResult.applied} change${syncResult.applied > 1 ? 's' : ''}:\n${syncResult.summary}`;
|
|
37
46
|
}
|
package/dist/commands/claude.js
CHANGED
|
@@ -5,7 +5,7 @@ import { execSync, spawn } from 'child_process';
|
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { resolveCommand } from '../platform.js';
|
|
8
|
-
import { getAuth, saveAuth } from '../auth.js';
|
|
8
|
+
import { getAuth, saveAuth, sessionExpired } from '../auth.js';
|
|
9
9
|
import { get, post, publicPost, ApiError, getAccountSlug } from '../api.js';
|
|
10
10
|
import { getConfig, saveConfigAt, clearConfigCache, getApiBaseOverride, DEFAULT_API_BASE, getConfigPath } from '../config.js';
|
|
11
11
|
import { sync } from '../sync.js';
|
|
@@ -291,7 +291,15 @@ export const claudeCommand = new Command('claude')
|
|
|
291
291
|
if (!nonInteractive) {
|
|
292
292
|
printBanner({ version: __clPkg.version, email: auth?.email, cwd: process.cwd() });
|
|
293
293
|
}
|
|
294
|
-
if (auth) {
|
|
294
|
+
if (auth && sessionExpired()) {
|
|
295
|
+
// The cached auth.json exists but its refresh token has lapsed, so the
|
|
296
|
+
// first API call below would 401 anyway. Re-login up front instead of
|
|
297
|
+
// printing "Logged in" and immediately contradicting it with "session
|
|
298
|
+
// expired" once the projects fetch fails.
|
|
299
|
+
console.log(` ${muted('Your session expired. Let\'s sign you back in.')}\n`);
|
|
300
|
+
auth = await interactiveLogin();
|
|
301
|
+
}
|
|
302
|
+
else if (auth) {
|
|
295
303
|
console.log(` Logged in (${auth.email}).`);
|
|
296
304
|
}
|
|
297
305
|
else {
|
package/dist/commands/deploy.js
CHANGED
|
@@ -4,6 +4,7 @@ import { requireConfig } from '../config.js';
|
|
|
4
4
|
import { formatSize } from '../utils.js';
|
|
5
5
|
import { success, error as clrError, warning, muted, bold, brand } from '../colors.js';
|
|
6
6
|
import { run, syncBeforeAction } from '../helpers/index.js';
|
|
7
|
+
import { withSpinner } from '../progress.js';
|
|
7
8
|
// ── Status icons ───────────────────────────────────────────────────────
|
|
8
9
|
function statusIcon(status) {
|
|
9
10
|
if (status === 'ok')
|
|
@@ -32,14 +33,16 @@ export const deployCommand = new Command('deploy')
|
|
|
32
33
|
}
|
|
33
34
|
const config = requireConfig();
|
|
34
35
|
await syncBeforeAction(opts);
|
|
35
|
-
|
|
36
|
-
const res = await post(`/projects/${config.projectGuid}/deploy`, {
|
|
36
|
+
const doDeploy = () => post(`/projects/${config.projectGuid}/deploy`, {
|
|
37
37
|
target,
|
|
38
38
|
sourceDir: opts.sourceDir,
|
|
39
39
|
optimize: opts.optimize,
|
|
40
40
|
force: opts.force,
|
|
41
41
|
only: opts.only?.split(',').map((s) => s.trim()),
|
|
42
42
|
});
|
|
43
|
+
const res = opts.json
|
|
44
|
+
? await doDeploy()
|
|
45
|
+
: await withSpinner(`Deploying to ${target}…`, doDeploy, { done: null });
|
|
43
46
|
const d = res.data;
|
|
44
47
|
if (opts.json) {
|
|
45
48
|
console.log(JSON.stringify(d));
|
|
@@ -4,8 +4,9 @@ import { resolveProjectContext, getConfigPath } from '../config.js';
|
|
|
4
4
|
import { pushFile } from '../sync.js';
|
|
5
5
|
import { writeFileSync } from 'fs';
|
|
6
6
|
import { resolve as resolvePath, dirname, relative, isAbsolute } from 'path';
|
|
7
|
-
import { error as clrError, success, muted
|
|
7
|
+
import { error as clrError, success, muted } from '../colors.js';
|
|
8
8
|
import { printCommandError } from '../helpers/command.js';
|
|
9
|
+
import { withSpinner } from '../progress.js';
|
|
9
10
|
import { IMAGE_MODELS_DOC, IMAGE_GEMINI_ASPECT_RATIOS, IMAGE_GEMINI_SIZES, VIDEO_MODELS_DOC, TTS_PROVIDER_DESCRIPTIONS } from '../provider-docs.js';
|
|
10
11
|
/** Download a URL and save to a local file, then push it up to the project so
|
|
11
12
|
* the cloud (and anything that mirrors it) immediately matches local disk.
|
|
@@ -73,7 +74,7 @@ Examples:
|
|
|
73
74
|
.action(async (prompt, opts) => {
|
|
74
75
|
try {
|
|
75
76
|
const { config } = await resolveProjectContext();
|
|
76
|
-
const
|
|
77
|
+
const doGenerate = () => post(`/projects/${config.projectGuid}/generate/image`, {
|
|
77
78
|
prompt,
|
|
78
79
|
provider: opts.provider,
|
|
79
80
|
model: opts.model,
|
|
@@ -83,6 +84,9 @@ Examples:
|
|
|
83
84
|
image_size: opts.imageSize,
|
|
84
85
|
seed: Number.isFinite(opts.seed) ? opts.seed : undefined,
|
|
85
86
|
});
|
|
87
|
+
const result = opts.json
|
|
88
|
+
? await doGenerate()
|
|
89
|
+
: await withSpinner('Generating image…', doGenerate, { done: null });
|
|
86
90
|
const ext = result.content_type.includes('png') ? 'png' : 'jpg';
|
|
87
91
|
const filename = opts.output || `generated.${ext}`;
|
|
88
92
|
const savedPath = await downloadFile(result.url, filename);
|
|
@@ -131,14 +135,16 @@ Examples:
|
|
|
131
135
|
.action(async (prompt, opts) => {
|
|
132
136
|
try {
|
|
133
137
|
const { config } = await resolveProjectContext();
|
|
134
|
-
|
|
135
|
-
console.log(info('Generating video (this may take 30-120 seconds)...')); // keep --json stdout pure JSON
|
|
136
|
-
const result = await post(`/projects/${config.projectGuid}/generate/video`, {
|
|
138
|
+
const doGenerate = () => post(`/projects/${config.projectGuid}/generate/video`, {
|
|
137
139
|
prompt,
|
|
138
140
|
model: opts.model,
|
|
139
141
|
aspect_ratio: opts.aspect,
|
|
140
142
|
resolution: opts.resolution,
|
|
141
143
|
});
|
|
144
|
+
// Veo runs 30-120s; the bouncing bar + timer keeps the wait honest.
|
|
145
|
+
const result = opts.json
|
|
146
|
+
? await doGenerate()
|
|
147
|
+
: await withSpinner('Generating video…', doGenerate, { done: null });
|
|
142
148
|
const filename = opts.output || 'generated.mp4';
|
|
143
149
|
const savedPath = await downloadFile(result.url, filename);
|
|
144
150
|
if (opts.json) {
|
|
@@ -191,13 +197,16 @@ Examples:
|
|
|
191
197
|
process.exit(1);
|
|
192
198
|
}
|
|
193
199
|
}
|
|
194
|
-
const
|
|
200
|
+
const doGenerate = () => post(`/projects/${config.projectGuid}/generate/speech`, {
|
|
195
201
|
text,
|
|
196
202
|
provider: opts.provider,
|
|
197
203
|
voice: opts.voice,
|
|
198
204
|
language: opts.language,
|
|
199
205
|
speakers,
|
|
200
206
|
});
|
|
207
|
+
const result = opts.json
|
|
208
|
+
? await doGenerate()
|
|
209
|
+
: await withSpinner('Generating speech…', doGenerate, { done: null });
|
|
201
210
|
const filename = opts.output || 'speech.mp3';
|
|
202
211
|
const savedPath = await downloadFile(result.url, filename);
|
|
203
212
|
if (opts.json) {
|
|
@@ -240,14 +249,15 @@ Examples:
|
|
|
240
249
|
.action(async (prompt, opts) => {
|
|
241
250
|
try {
|
|
242
251
|
const { config } = await resolveProjectContext();
|
|
243
|
-
|
|
244
|
-
console.log(info('Generating music...')); // keep --json stdout pure JSON
|
|
245
|
-
const result = await post(`/projects/${config.projectGuid}/generate/music`, {
|
|
252
|
+
const doGenerate = () => post(`/projects/${config.projectGuid}/generate/music`, {
|
|
246
253
|
prompt,
|
|
247
254
|
duration_seconds: opts.duration,
|
|
248
255
|
model: opts.model,
|
|
249
256
|
instrumental: !opts.vocals,
|
|
250
257
|
});
|
|
258
|
+
const result = opts.json
|
|
259
|
+
? await doGenerate()
|
|
260
|
+
: await withSpinner('Generating music…', doGenerate, { done: null });
|
|
251
261
|
const filename = opts.output || 'music.mp3';
|
|
252
262
|
const savedPath = await downloadFile(result.url, filename);
|
|
253
263
|
if (opts.json) {
|
|
@@ -6,6 +6,7 @@ import { getProjectRoot } from '../config.js';
|
|
|
6
6
|
import { brand, bold, muted, success } from '../colors.js';
|
|
7
7
|
import { formatSize } from '../utils.js';
|
|
8
8
|
import { run } from '../helpers/index.js';
|
|
9
|
+
import { withSpinner } from '../progress.js';
|
|
9
10
|
const DEVICE_PRESETS = {
|
|
10
11
|
default: { width: 1280, height: 720 },
|
|
11
12
|
desktop: { width: 1920, height: 1080 },
|
|
@@ -143,7 +144,13 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
143
144
|
...(opts.fakeMedia ? { fakeMedia: true } : {}),
|
|
144
145
|
...(opts.action ? { action: opts.action } : {}),
|
|
145
146
|
};
|
|
146
|
-
|
|
147
|
+
// Load + render across viewports runs server-side and can take many
|
|
148
|
+
// seconds; animate the wait, then clear so the saved-files summary is the
|
|
149
|
+
// result. JSON mode skips the spinner (shares stdout).
|
|
150
|
+
const doShoot = () => postForTarEntries('/tools/browser/screenshot', body);
|
|
151
|
+
const entries = opts.json
|
|
152
|
+
? await doShoot()
|
|
153
|
+
: await withSpinner('Capturing…', doShoot, { done: null });
|
|
147
154
|
const metaEntry = entries.find((e) => e.name === 'meta.json');
|
|
148
155
|
if (!metaEntry)
|
|
149
156
|
throw new Error('Server response missing meta.json');
|
package/dist/commands/remove.js
CHANGED
|
@@ -5,6 +5,7 @@ import { sync } from '../sync.js';
|
|
|
5
5
|
import { success, muted } from '../colors.js';
|
|
6
6
|
import { run } from '../helpers/index.js';
|
|
7
7
|
import { confirm } from '../utils.js';
|
|
8
|
+
import { createProgressReporter, withSpinner } from '../progress.js';
|
|
8
9
|
export const removeCommand = new Command('remove')
|
|
9
10
|
.description('Remove an installed kit from the project (inverse of `gipity add <kit>`).')
|
|
10
11
|
.argument('<kit>', 'Kit key/directory under src/packages/ to remove')
|
|
@@ -18,10 +19,13 @@ export const removeCommand = new Command('remove')
|
|
|
18
19
|
return;
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
|
-
const
|
|
22
|
+
const doRemove = () => post(`/projects/${config.projectGuid}/remove`, { name: kit });
|
|
23
|
+
const res = opts.json
|
|
24
|
+
? await doRemove()
|
|
25
|
+
: await withSpinner('Removing…', doRemove, { done: null });
|
|
22
26
|
// Force the pull so the kit's deletions land locally without tripping the
|
|
23
27
|
// bulk-deletion guard - the removal is an explicit, user-invoked action.
|
|
24
|
-
const syncResult = await sync({ interactive: false, force: true });
|
|
28
|
+
const syncResult = await sync({ interactive: false, force: true, progress: opts.json ? undefined : createProgressReporter() });
|
|
25
29
|
const data = res.data;
|
|
26
30
|
if (opts.json) {
|
|
27
31
|
console.log(JSON.stringify({ ...data, synced: syncResult.applied }));
|
package/dist/commands/sandbox.js
CHANGED
|
@@ -6,6 +6,7 @@ import { resolveProjectContext, getConfigPath } from '../config.js';
|
|
|
6
6
|
import { sync } from '../sync.js';
|
|
7
7
|
import { error as clrError, dim } from '../colors.js';
|
|
8
8
|
import { run } from '../helpers/index.js';
|
|
9
|
+
import { createProgressReporter, withSpinner } from '../progress.js';
|
|
9
10
|
const LANG_MAP = {
|
|
10
11
|
js: 'javascript',
|
|
11
12
|
javascript: 'javascript',
|
|
@@ -159,15 +160,20 @@ GCC/Rust).
|
|
|
159
160
|
// Bidirectional + CAS, so it's a cheap manifest check when nothing changed.
|
|
160
161
|
// Symmetric with the post-run pull below. Skip in one-off mode (no project).
|
|
161
162
|
if (getConfigPath()) {
|
|
162
|
-
await sync({ interactive: false });
|
|
163
|
+
await sync({ interactive: false, progress: opts.json ? undefined : createProgressReporter() });
|
|
163
164
|
}
|
|
164
|
-
|
|
165
|
+
// The run blocks until the sandbox finishes (up to the timeout); animate the
|
|
166
|
+
// wait, then clear the spinner so stdout/stderr is the result. JSON skips it.
|
|
167
|
+
const doRun = () => post(`/projects/${config.projectGuid}/sandbox/execute`, {
|
|
165
168
|
code: source,
|
|
166
169
|
language,
|
|
167
170
|
timeout: isNaN(timeout) ? 30 : timeout,
|
|
168
171
|
input_files: opts.input,
|
|
169
172
|
cwd,
|
|
170
173
|
});
|
|
174
|
+
const res = opts.json
|
|
175
|
+
? await doRun()
|
|
176
|
+
: await withSpinner('Running in sandbox…', doRun, { done: null });
|
|
171
177
|
// Pull sandbox-written outputs down to the local cwd automatically. The
|
|
172
178
|
// server has already mirrored them into the project (VFS) and handed back
|
|
173
179
|
// the exact list, so honoring it here means files land locally without a
|
|
@@ -175,7 +181,7 @@ GCC/Rust).
|
|
|
175
181
|
// `filesChanged` flag. Skip in one-off mode (no local project to sync into).
|
|
176
182
|
const pulledLocal = !!(res.data.outputFiles?.length && getConfigPath());
|
|
177
183
|
if (pulledLocal) {
|
|
178
|
-
await sync({ interactive: false });
|
|
184
|
+
await sync({ interactive: false, progress: opts.json ? undefined : createProgressReporter() });
|
|
179
185
|
}
|
|
180
186
|
if (opts.json) {
|
|
181
187
|
console.log(JSON.stringify({ ...res.data, filesSynced: pulledLocal }));
|
package/dist/helpers/sync.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { sync } from '../sync.js';
|
|
5
5
|
import { muted, error as clrError } from '../colors.js';
|
|
6
|
+
import { createProgressReporter } from '../progress.js';
|
|
6
7
|
/**
|
|
7
8
|
* Sync local files with the server before an action (deploy, test, scaffold).
|
|
8
9
|
* Respects --no-sync and --json flags. Non-interactive: bulk-deletion guard
|
|
@@ -14,7 +15,14 @@ import { muted, error as clrError } from '../colors.js';
|
|
|
14
15
|
export async function syncBeforeAction(opts) {
|
|
15
16
|
if (opts.sync === false)
|
|
16
17
|
return;
|
|
17
|
-
|
|
18
|
+
// Pass a progress reporter so a large pre-action upload shows the transfer
|
|
19
|
+
// bar instead of a silent pause (the reporter is a no-op on non-TTY / when
|
|
20
|
+
// piped, so JSON and headless output stay clean).
|
|
21
|
+
const result = await sync({
|
|
22
|
+
interactive: false,
|
|
23
|
+
force: opts.force,
|
|
24
|
+
progress: opts.json ? undefined : createProgressReporter(),
|
|
25
|
+
});
|
|
18
26
|
if (result.applied > 0 && !opts.json) {
|
|
19
27
|
console.log(muted(`Synced ${result.applied} change${result.applied > 1 ? 's' : ''}`));
|
|
20
28
|
}
|
package/dist/index.js
CHANGED
|
@@ -57,6 +57,24 @@ import { bold, dim, brand, muted, success } from './colors.js';
|
|
|
57
57
|
import { normalizeAliases } from './flag-aliases.js';
|
|
58
58
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
59
59
|
const pkg = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf-8'));
|
|
60
|
+
// Local builds stamp dist/build-info.json (git SHA + dirty flag) via the npm
|
|
61
|
+
// `postbuild` hook, so `-v` can show whether the linked binary is your latest
|
|
62
|
+
// code. It is intentionally absent from published installs: the npm `files`
|
|
63
|
+
// allowlist ships only dist/**/*.js, so a released CLI prints a clean
|
|
64
|
+
// `v1.0.398` with no dev marker. package.json `version` stays the source of
|
|
65
|
+
// truth for the published release; this marker never touches it.
|
|
66
|
+
function versionLabel() {
|
|
67
|
+
const base = `v${pkg.version}`;
|
|
68
|
+
try {
|
|
69
|
+
const info = JSON.parse(readFileSync(resolve(__dirname, 'build-info.json'), 'utf-8'));
|
|
70
|
+
if (info?.sha)
|
|
71
|
+
return `${base} (dev ${info.sha}${info.dirty ? ', modified' : ''})`;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// No build-info.json (published install or pre-build) → clean version.
|
|
75
|
+
}
|
|
76
|
+
return base;
|
|
77
|
+
}
|
|
60
78
|
// Custom -v/--version output: include auth status so agents know whether
|
|
61
79
|
// the next CLI call will succeed. Intercepted before Commander parses,
|
|
62
80
|
// because Commander's built-in `.version()` only prints a string and exits.
|
|
@@ -79,7 +97,7 @@ const pkg = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf-
|
|
|
79
97
|
authLine = `${success('Logged in')} as ${auth.email}`;
|
|
80
98
|
}
|
|
81
99
|
console.log('');
|
|
82
|
-
console.log(`${brand(bold('Gipity'))} ${muted(
|
|
100
|
+
console.log(`${brand(bold('Gipity'))} ${muted(versionLabel())}`);
|
|
83
101
|
console.log(authLine);
|
|
84
102
|
console.log('');
|
|
85
103
|
process.exit(0);
|
|
@@ -150,7 +168,7 @@ program.configureHelp({
|
|
|
150
168
|
const padOpt = (s) => s.padEnd(optColWidth);
|
|
151
169
|
const lines = [];
|
|
152
170
|
lines.push('');
|
|
153
|
-
lines.push(`${brand(bold('Gipity CLI'))} ${muted(
|
|
171
|
+
lines.push(`${brand(bold('Gipity CLI'))} ${muted(versionLabel())}`);
|
|
154
172
|
lines.push(dim(GIPITY_TAGLINE));
|
|
155
173
|
lines.push(dim('Hosting, databases, deploys, workflows - one place. Pair with Claude Code or use standalone.'));
|
|
156
174
|
lines.push(dim('Works with Claude Code, Codex, Aider, or any AI coding tool - no MCP server needed.'));
|
package/dist/progress.js
CHANGED
|
@@ -1,33 +1,57 @@
|
|
|
1
1
|
// ── Gipity CLI Progress Reporter ────────────────────────────────────────
|
|
2
2
|
// One central place for long-running terminal feedback, so commands don't
|
|
3
|
-
// each reinvent status lines.
|
|
3
|
+
// each reinvent status lines. Three channels, one shared visual vocabulary:
|
|
4
4
|
//
|
|
5
5
|
// phase(msg) - a discrete step with no measurable size
|
|
6
6
|
// (scanning, hashing). Prints one committed line.
|
|
7
|
-
// transfer(label, n, tot) - a
|
|
8
|
-
// in-place bar that
|
|
7
|
+
// transfer(label, n, tot) - a DETERMINATE byte transfer. Renders a single
|
|
8
|
+
// in-place bar that fills as bytes move.
|
|
9
|
+
// spinner(label) - an INDETERMINATE wait (a server call where we
|
|
10
|
+
// can't measure bytes: deploy, generate, chat).
|
|
11
|
+
// Renders the SAME-width track as transfer(), but
|
|
12
|
+
// with a short block that bounces left↔right plus
|
|
13
|
+
// an elapsed timer, so a long wait never reads as
|
|
14
|
+
// frozen. Settles to a committed ✓/✗ line.
|
|
9
15
|
//
|
|
10
16
|
// On a non-TTY (piped output, hook-driven sync, headless -p) the reporter is a
|
|
11
17
|
// silent no-op - no `\r` spam in logs. Colors come from ./colors so the bar
|
|
12
18
|
// matches the rest of the CLI (orange fill + percentage).
|
|
13
|
-
import { brand, brandBold, muted, dim } from './colors.js';
|
|
19
|
+
import { brand, brandBold, muted, dim, success, error as clrError } from './colors.js';
|
|
14
20
|
import { formatSize } from './utils.js';
|
|
15
21
|
const CLEAR_TO_EOL = '\x1b[K';
|
|
16
22
|
const BAR_WIDTH = 18;
|
|
17
23
|
const RENDER_THROTTLE_MS = 60;
|
|
24
|
+
// Indeterminate bounce: a short block sliding across the same BAR_WIDTH track
|
|
25
|
+
// the determinate bar fills. Frame cadence is slow enough to read, fast enough
|
|
26
|
+
// to feel alive.
|
|
27
|
+
const SPIN_BLOCK = 5;
|
|
28
|
+
const SPIN_FRAME_MS = 90;
|
|
29
|
+
/** Compact elapsed: "8s", "1m 04s". Keeps the timer narrow and glanceable. */
|
|
30
|
+
function formatElapsed(ms) {
|
|
31
|
+
const s = Math.floor(ms / 1000);
|
|
32
|
+
if (s < 60)
|
|
33
|
+
return `${s}s`;
|
|
34
|
+
const m = Math.floor(s / 60);
|
|
35
|
+
return `${m}m ${String(s % 60).padStart(2, '0')}s`;
|
|
36
|
+
}
|
|
18
37
|
class TerminalProgress {
|
|
19
|
-
/** True while an in-place transfer line is on screen and not
|
|
38
|
+
/** True while an in-place transfer/spinner line is on screen and not committed. */
|
|
20
39
|
liveOpen = false;
|
|
21
40
|
lastRenderAt = 0;
|
|
22
41
|
/** The label of the current transfer session; a change starts a fresh one. */
|
|
23
42
|
barLabel = null;
|
|
24
43
|
/** True once the current session hit 100% - late/overshoot ticks are dropped. */
|
|
25
44
|
barSettled = false;
|
|
45
|
+
/** Active indeterminate spinner timer, if any. */
|
|
46
|
+
spinTimer = null;
|
|
26
47
|
phase(message) {
|
|
48
|
+
this.stopSpinTimer();
|
|
27
49
|
this.commitLive();
|
|
28
50
|
process.stdout.write(` ${muted(message)}\n`);
|
|
29
51
|
}
|
|
30
52
|
transfer(label, doneBytes, totalBytes) {
|
|
53
|
+
// A determinate transfer takes over the live line from any spinner.
|
|
54
|
+
this.stopSpinTimer();
|
|
31
55
|
// A new label begins a fresh transfer session (e.g. downloads → uploads on
|
|
32
56
|
// the same reporter). Within a session, once we've drawn the 100% frame we
|
|
33
57
|
// drop any further ticks - download byte totals are estimated, so the wire
|
|
@@ -46,32 +70,115 @@ class TerminalProgress {
|
|
|
46
70
|
return;
|
|
47
71
|
this.lastRenderAt = now;
|
|
48
72
|
this.liveOpen = true;
|
|
49
|
-
process.stdout.write('\r' + this.
|
|
73
|
+
process.stdout.write('\r' + this.barFrame(label, doneBytes, totalBytes) + CLEAR_TO_EOL);
|
|
50
74
|
if (finished) {
|
|
51
75
|
this.commitLive();
|
|
52
76
|
this.barSettled = true;
|
|
53
77
|
}
|
|
54
78
|
}
|
|
79
|
+
spinner(label) {
|
|
80
|
+
this.stopSpinTimer();
|
|
81
|
+
this.commitLive();
|
|
82
|
+
const startedAt = Date.now();
|
|
83
|
+
let tick = 0;
|
|
84
|
+
const draw = () => {
|
|
85
|
+
this.liveOpen = true;
|
|
86
|
+
process.stdout.write('\r' + this.spinFrame(label, tick++, Date.now() - startedAt) + CLEAR_TO_EOL);
|
|
87
|
+
};
|
|
88
|
+
draw();
|
|
89
|
+
this.spinTimer = setInterval(draw, SPIN_FRAME_MS);
|
|
90
|
+
// Don't let the animation keep the event loop (and process) alive on its own.
|
|
91
|
+
this.spinTimer.unref?.();
|
|
92
|
+
const settle = (icon, message) => {
|
|
93
|
+
this.stopSpinTimer();
|
|
94
|
+
if (this.liveOpen)
|
|
95
|
+
process.stdout.write('\r');
|
|
96
|
+
if (icon) {
|
|
97
|
+
// Replace the spinner line in place with a committed ✓/✗ result line.
|
|
98
|
+
const elapsed = muted(formatElapsed(Date.now() - startedAt));
|
|
99
|
+
process.stdout.write(` ${icon} ${muted(message ?? label)} ${elapsed}${CLEAR_TO_EOL}\n`);
|
|
100
|
+
}
|
|
101
|
+
else if (this.liveOpen) {
|
|
102
|
+
// Silent stop: clear the spinner row but DON'T advance - leave the
|
|
103
|
+
// cursor at column 0 so the command's own result overwrites the row
|
|
104
|
+
// instead of leaving a blank line behind it.
|
|
105
|
+
process.stdout.write(CLEAR_TO_EOL);
|
|
106
|
+
}
|
|
107
|
+
this.liveOpen = false;
|
|
108
|
+
};
|
|
109
|
+
return {
|
|
110
|
+
succeed: (m) => settle(success('✓'), m),
|
|
111
|
+
fail: (m) => settle(clrError('✗'), m),
|
|
112
|
+
stop: () => settle(null),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
55
115
|
finish() {
|
|
116
|
+
this.stopSpinTimer();
|
|
56
117
|
this.commitLive();
|
|
57
118
|
}
|
|
119
|
+
stopSpinTimer() {
|
|
120
|
+
if (this.spinTimer) {
|
|
121
|
+
clearInterval(this.spinTimer);
|
|
122
|
+
this.spinTimer = null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
58
125
|
commitLive() {
|
|
59
126
|
if (!this.liveOpen)
|
|
60
127
|
return;
|
|
61
128
|
process.stdout.write('\n');
|
|
62
129
|
this.liveOpen = false;
|
|
63
130
|
}
|
|
64
|
-
|
|
131
|
+
barFrame(label, done, total) {
|
|
65
132
|
const pct = total > 0 ? Math.min(100, Math.floor((done / total) * 100)) : 100;
|
|
66
133
|
const filled = Math.round((pct / 100) * BAR_WIDTH);
|
|
67
134
|
const bar = brand('█'.repeat(filled)) + dim('░'.repeat(BAR_WIDTH - filled));
|
|
68
135
|
const sizes = muted(`${formatSize(done)} / ${formatSize(total)}`);
|
|
69
136
|
return ` ${muted(label)} ${bar} ${brandBold(`${pct}%`)} ${sizes}`;
|
|
70
137
|
}
|
|
138
|
+
spinFrame(label, tick, elapsedMs) {
|
|
139
|
+
// Ping-pong the block's left edge between 0 and (BAR_WIDTH - SPIN_BLOCK).
|
|
140
|
+
const span = BAR_WIDTH - SPIN_BLOCK;
|
|
141
|
+
const cycle = span * 2;
|
|
142
|
+
const phase = tick % cycle;
|
|
143
|
+
const pos = phase <= span ? phase : cycle - phase;
|
|
144
|
+
const bar = dim('░'.repeat(pos)) +
|
|
145
|
+
brand('█'.repeat(SPIN_BLOCK)) +
|
|
146
|
+
dim('░'.repeat(BAR_WIDTH - pos - SPIN_BLOCK));
|
|
147
|
+
return ` ${muted(label)} ${bar} ${muted(formatElapsed(elapsedMs))}`;
|
|
148
|
+
}
|
|
71
149
|
}
|
|
72
|
-
const
|
|
150
|
+
const NOOP_SPINNER = { succeed() { }, fail() { }, stop() { } };
|
|
151
|
+
const NOOP = {
|
|
152
|
+
phase() { }, transfer() { }, finish() { },
|
|
153
|
+
spinner: () => NOOP_SPINNER,
|
|
154
|
+
};
|
|
73
155
|
/** A reporter that draws on a TTY and stays silent otherwise. */
|
|
74
156
|
export function createProgressReporter() {
|
|
75
157
|
return process.stdout.isTTY ? new TerminalProgress() : NOOP;
|
|
76
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Run an indeterminate async operation behind the standard bouncing-block
|
|
161
|
+
* spinner: animate `label` while it's in flight, then settle. On success the
|
|
162
|
+
* line becomes a ✓ (`done`, or `label` if omitted) — or, when `done` is null,
|
|
163
|
+
* the spinner just clears silently (use this when the command prints its own
|
|
164
|
+
* result, e.g. a chat reply). On throw it becomes a ✗ and re-throws so the
|
|
165
|
+
* caller's own error handling still runs. On a non-TTY this is a silent
|
|
166
|
+
* pass-through. The single wrapper every command uses for a server call whose
|
|
167
|
+
* size/duration we can't measure (deploy, generate, chat, sandbox, …).
|
|
168
|
+
*/
|
|
169
|
+
export async function withSpinner(label, fn, opts = {}) {
|
|
170
|
+
const sp = (opts.reporter ?? createProgressReporter()).spinner(label);
|
|
171
|
+
try {
|
|
172
|
+
const result = await fn();
|
|
173
|
+
if (opts.done === null)
|
|
174
|
+
sp.stop();
|
|
175
|
+
else
|
|
176
|
+
sp.succeed(opts.done);
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
sp.fail();
|
|
181
|
+
throw e;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
77
184
|
//# sourceMappingURL=progress.js.map
|
package/dist/relay/daemon.js
CHANGED
|
@@ -632,6 +632,12 @@ async function handleDispatch(d) {
|
|
|
632
632
|
// prompt is correct (same authority as running `claude -p` in a local
|
|
633
633
|
// terminal yourself).
|
|
634
634
|
const args = ['claude', '-p', d.message, '--permission-mode', 'bypassPermissions'];
|
|
635
|
+
// Per-chat model: the user picked it with `/model` in the web CLI. `gipity
|
|
636
|
+
// claude` forwards --model straight through to the `claude` binary, which
|
|
637
|
+
// honors it on both a fresh session and a --resume. null => agent default.
|
|
638
|
+
if (d.model) {
|
|
639
|
+
args.push('--model', d.model);
|
|
640
|
+
}
|
|
635
641
|
if (d.kind === 'resume' && d.remote_session_id) {
|
|
636
642
|
args.push('--resume', d.remote_session_id);
|
|
637
643
|
}
|
package/dist/upload.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createReadStream } from 'fs';
|
|
2
|
+
import { Transform } from 'stream';
|
|
2
3
|
import { extname } from 'path';
|
|
3
4
|
import { createHash } from 'crypto';
|
|
4
5
|
import { post, putToPresignedUrl, ApiError } from './api.js';
|
|
@@ -170,11 +171,35 @@ export async function uploadOneFile(projectGuid, localPath, virtualPath, opts =
|
|
|
170
171
|
export async function transferToS3(localPath, size, mime, data, opts = {}) {
|
|
171
172
|
// Single-part (covers fresh + resumed PUT - single PUT is idempotent on the staging key).
|
|
172
173
|
if (data.method === 'PUT') {
|
|
174
|
+
// Report bytes as they flow to S3 so the progress bar moves during the
|
|
175
|
+
// transfer instead of sitting at 0% until the whole file lands (a large
|
|
176
|
+
// file - e.g. a 674 MB video - otherwise looks hung for minutes). A
|
|
177
|
+
// pass-through Transform counts each chunk as fetch pulls it; we don't
|
|
178
|
+
// attach a 'data' listener, which would drain the stream before fetch
|
|
179
|
+
// reads it.
|
|
180
|
+
let reported = 0;
|
|
173
181
|
const etag = await withRetry('PUT', async () => {
|
|
182
|
+
// A retried attempt re-streams from the top, so back out whatever the
|
|
183
|
+
// failed attempt already counted to keep the shared total accurate.
|
|
184
|
+
if (reported) {
|
|
185
|
+
opts.onBytes?.(-reported);
|
|
186
|
+
reported = 0;
|
|
187
|
+
}
|
|
174
188
|
const stream = createReadStream(localPath);
|
|
175
|
-
|
|
189
|
+
let body = stream;
|
|
190
|
+
if (opts.onBytes) {
|
|
191
|
+
const counter = new Transform({
|
|
192
|
+
transform(chunk, _enc, cb) { reported += chunk.length; opts.onBytes(chunk.length); cb(null, chunk); },
|
|
193
|
+
});
|
|
194
|
+
stream.on('error', err => counter.destroy(err));
|
|
195
|
+
body = stream.pipe(counter);
|
|
196
|
+
}
|
|
197
|
+
return putToPresignedUrl(data.url, body, size, data.headers?.['Content-Type'] ?? mime);
|
|
176
198
|
});
|
|
177
|
-
|
|
199
|
+
// True up to exactly `size`: a dedup/no-op PUT streams nothing, and chunk
|
|
200
|
+
// sums can fall a hair short of the stat size on the final read.
|
|
201
|
+
if (reported !== size)
|
|
202
|
+
opts.onBytes?.(size - reported);
|
|
178
203
|
return { etag };
|
|
179
204
|
}
|
|
180
205
|
// Multipart - start with any parts that already landed (resume case).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gipity",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.400",
|
|
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",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"type": "module",
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "tsc && chmod +x dist/index.js dist/gipcc.js dist/gipccd.js dist/updater/shim.js dist/updater/check.js",
|
|
13
|
+
"postbuild": "node scripts/gen-build-info.mjs",
|
|
13
14
|
"dev": "tsc --watch",
|
|
14
15
|
"test": "npm run test:smoke",
|
|
15
16
|
"test:smoke": "tsc && node --test dist/__tests__/utils.test.js dist/__tests__/colors.test.js dist/__tests__/config.test.js dist/__tests__/sync.test.js dist/__tests__/sync-apply.test.js dist/__tests__/sync-lock.test.js dist/__tests__/auth-lock.test.js dist/__tests__/push-cas.test.js dist/__tests__/upload.test.js dist/__tests__/progress.test.js dist/__tests__/updater.test.js dist/__tests__/cli-smoke.test.js dist/__tests__/claude-noninteractive.test.js dist/__tests__/claude-trust.test.js dist/__tests__/relay-state.test.js dist/__tests__/relay-daemon.test.js dist/__tests__/relay-installers.test.js dist/__tests__/relay-bridge-abort.test.js dist/__tests__/relay-redact.test.js dist/__tests__/relay-machine-id.test.js dist/__tests__/stream-json.test.js dist/__tests__/relay-ingest-contract.test.js dist/__tests__/prompts.test.js dist/__tests__/capture-transcript.test.js dist/__tests__/flag-aliases.test.js dist/__tests__/client-context.test.js dist/__tests__/adopt-cwd.test.js dist/__tests__/cli-cmd-agent.test.js dist/__tests__/cli-cmd-approval.test.js dist/__tests__/cli-cmd-audit.test.js dist/__tests__/cli-cmd-chat.test.js dist/__tests__/cli-cmd-credits.test.js dist/__tests__/cli-cmd-db.test.js dist/__tests__/cli-cmd-deploy.test.js dist/__tests__/cli-cmd-domain.test.js dist/__tests__/cli-cmd-email.test.js dist/__tests__/cli-cmd-file.test.js dist/__tests__/cli-cmd-fn.test.js dist/__tests__/cli-cmd-service.test.js dist/__tests__/cli-cmd-job.test.js dist/__tests__/cli-cmd-generate.test.js dist/__tests__/cli-cmd-gmail.test.js dist/__tests__/cli-cmd-info.test.js dist/__tests__/cli-cmd-init.test.js dist/__tests__/cli-cmd-location.test.js dist/__tests__/cli-cmd-text.test.js dist/__tests__/cli-cmd-login.test.js dist/__tests__/cli-cmd-logout.test.js dist/__tests__/cli-cmd-token.test.js dist/__tests__/cli-cmd-logs.test.js dist/__tests__/cli-cmd-memory.test.js dist/__tests__/cli-cmd-page.test.js dist/__tests__/cli-cmd-plan.test.js dist/__tests__/cli-cmd-project.test.js dist/__tests__/cli-cmd-rbac.test.js dist/__tests__/cli-cmd-realtime.test.js dist/__tests__/cli-cmd-records.test.js dist/__tests__/cli-cmd-relay.test.js dist/__tests__/cli-cmd-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",
|