gipity 1.0.397 → 1.0.399
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/adopt-cwd.js +10 -1
- package/dist/api.js +5 -3
- package/dist/client-context.js +63 -0
- package/dist/commands/add.js +8 -2
- package/dist/commands/chat.js +11 -2
- package/dist/commands/claude.js +47 -34
- package/dist/commands/credits.js +53 -14
- package/dist/commands/deploy.js +5 -2
- package/dist/commands/generate.js +24 -13
- package/dist/commands/page-screenshot.js +8 -1
- package/dist/commands/remove.js +6 -2
- package/dist/commands/sandbox.js +9 -3
- package/dist/commands/sync.js +8 -2
- package/dist/helpers/command.js +16 -1
- package/dist/helpers/sync.js +9 -1
- package/dist/index.js +20 -2
- package/dist/platform.js +26 -0
- package/dist/progress.js +115 -8
- package/dist/relay/daemon.js +15 -2
- package/dist/relay/onboarding.js +5 -2
- package/dist/setup.js +11 -3
- package/dist/sync.js +112 -9
- package/dist/updater/bootstrap.js +6 -2
- package/dist/updater/check.js +4 -1
- package/dist/upload.js +27 -2
- package/package.json +5 -3
package/dist/adopt-cwd.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* `.gipity.json`, sync, and install hooks/skills/gitignore.
|
|
12
12
|
*/
|
|
13
13
|
import { readdirSync, statSync } from 'fs';
|
|
14
|
-
import { join, resolve, sep } from 'path';
|
|
14
|
+
import { join, resolve, sep, parse as parsePath } from 'path';
|
|
15
15
|
import { homedir } from 'os';
|
|
16
16
|
import { get, post, ApiError } from './api.js';
|
|
17
17
|
import { isSyncIgnored } from './setup.js';
|
|
@@ -103,6 +103,15 @@ export function canAdoptCwd(cwd) {
|
|
|
103
103
|
'/tmp', '/var', '/etc', '/usr', '/opt',
|
|
104
104
|
'/mnt', '/mnt/c', '/mnt/d', '/mnt/wsl',
|
|
105
105
|
]);
|
|
106
|
+
// Filesystem root (POSIX `/` already covered by `sep`, but on Windows this
|
|
107
|
+
// catches drive roots like `C:\`). Plus the common Windows system dirs.
|
|
108
|
+
if (norm === parsePath(norm).root)
|
|
109
|
+
return false;
|
|
110
|
+
for (const envVar of ['windir', 'SystemRoot', 'ProgramFiles', 'ProgramFiles(x86)']) {
|
|
111
|
+
const v = process.env[envVar];
|
|
112
|
+
if (v && norm === resolve(v))
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
106
115
|
if (blocked.has(norm))
|
|
107
116
|
return false;
|
|
108
117
|
// Workspace-parent heuristic: a directory at depth ≤1 below $HOME that
|
package/dist/api.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Readable } from 'stream';
|
|
|
2
2
|
import * as tar from 'tar-stream';
|
|
3
3
|
import { getAuth, refreshTokenIfNeeded } from './auth.js';
|
|
4
4
|
import { resolveApiBase, requireConfig, saveConfig } from './config.js';
|
|
5
|
+
import { clientHeaders } from './client-context.js';
|
|
5
6
|
export class ApiError extends Error {
|
|
6
7
|
statusCode;
|
|
7
8
|
code;
|
|
@@ -32,6 +33,7 @@ async function bearerToken() {
|
|
|
32
33
|
}
|
|
33
34
|
async function getHeaders() {
|
|
34
35
|
return {
|
|
36
|
+
...clientHeaders(),
|
|
35
37
|
'Authorization': `Bearer ${await bearerToken()}`,
|
|
36
38
|
'Content-Type': 'application/json',
|
|
37
39
|
};
|
|
@@ -137,7 +139,7 @@ export async function sendMessage(message) {
|
|
|
137
139
|
export async function download(path) {
|
|
138
140
|
const url = `${baseUrl()}${path}`;
|
|
139
141
|
const res = await fetch(url, {
|
|
140
|
-
headers: { 'Authorization': `Bearer ${await bearerToken()}` },
|
|
142
|
+
headers: { ...clientHeaders(), 'Authorization': `Bearer ${await bearerToken()}` },
|
|
141
143
|
});
|
|
142
144
|
if (!res.ok) {
|
|
143
145
|
throw new ApiError(res.status, 'DOWNLOAD_ERROR', `Download failed: ${res.statusText}`);
|
|
@@ -149,7 +151,7 @@ export async function downloadStream(path) {
|
|
|
149
151
|
const { Readable } = await import('stream');
|
|
150
152
|
const url = `${baseUrl()}${path}`;
|
|
151
153
|
const res = await fetch(url, {
|
|
152
|
-
headers: { 'Authorization': `Bearer ${await bearerToken()}` },
|
|
154
|
+
headers: { ...clientHeaders(), 'Authorization': `Bearer ${await bearerToken()}` },
|
|
153
155
|
});
|
|
154
156
|
if (!res.ok) {
|
|
155
157
|
throw new ApiError(res.status, 'DOWNLOAD_ERROR', `Download failed: ${res.statusText}`);
|
|
@@ -199,7 +201,7 @@ export async function publicPost(path, body) {
|
|
|
199
201
|
const url = `${baseUrl()}${path}`;
|
|
200
202
|
const res = await fetch(url, {
|
|
201
203
|
method: 'POST',
|
|
202
|
-
headers: { 'Content-Type': 'application/json' },
|
|
204
|
+
headers: { ...clientHeaders(), 'Content-Type': 'application/json' },
|
|
203
205
|
body: JSON.stringify(body),
|
|
204
206
|
});
|
|
205
207
|
if (!res.ok) {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { resolve, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
function cliVersion() {
|
|
5
|
+
try {
|
|
6
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const pkg = JSON.parse(readFileSync(resolve(dir, '../package.json'), 'utf-8'));
|
|
8
|
+
return typeof pkg.version === 'string' ? pkg.version : 'unknown';
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return 'unknown';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function hasEnvPrefix(prefix) {
|
|
15
|
+
return Object.keys(process.env).some((k) => k.startsWith(prefix));
|
|
16
|
+
}
|
|
17
|
+
function isCi() {
|
|
18
|
+
const ci = (process.env.CI ?? '').toLowerCase();
|
|
19
|
+
return ci === '1' || ci === 'true' || !!process.env.GITHUB_ACTIONS;
|
|
20
|
+
}
|
|
21
|
+
/** Identify the coding harness from its env footprint. Order = specificity. Exported for tests. */
|
|
22
|
+
export function detectHarness() {
|
|
23
|
+
// Claude Code is the most reliable: CLAUDECODE=1 plus a versioned exec path.
|
|
24
|
+
if (process.env.CLAUDECODE === '1' || process.env.CLAUDE_CODE_ENTRYPOINT) {
|
|
25
|
+
const version = process.env.CLAUDE_CODE_EXECPATH?.match(/versions\/([^/\\]+)/)?.[1];
|
|
26
|
+
return { harness: 'claude-code', harnessVersion: version, harnessSession: process.env.CLAUDE_CODE_SESSION_ID };
|
|
27
|
+
}
|
|
28
|
+
if (hasEnvPrefix('CODEX_'))
|
|
29
|
+
return { harness: 'codex' };
|
|
30
|
+
if (process.env.CURSOR_TRACE_ID || hasEnvPrefix('CURSOR_') || (process.env.TERM_PROGRAM ?? '').toLowerCase().includes('cursor')) {
|
|
31
|
+
return { harness: 'cursor' };
|
|
32
|
+
}
|
|
33
|
+
if (hasEnvPrefix('AIDER_'))
|
|
34
|
+
return { harness: 'aider' };
|
|
35
|
+
if (process.env.GEMINI_CLI)
|
|
36
|
+
return { harness: 'gemini' };
|
|
37
|
+
if (isCi())
|
|
38
|
+
return { harness: 'ci' };
|
|
39
|
+
return { harness: 'manual' };
|
|
40
|
+
}
|
|
41
|
+
let cached;
|
|
42
|
+
export function clientContext() {
|
|
43
|
+
if (cached)
|
|
44
|
+
return cached;
|
|
45
|
+
cached = {
|
|
46
|
+
cli: cliVersion(),
|
|
47
|
+
...detectHarness(),
|
|
48
|
+
node: process.versions.node,
|
|
49
|
+
os: process.platform,
|
|
50
|
+
arch: process.arch,
|
|
51
|
+
ci: isCi() || undefined,
|
|
52
|
+
};
|
|
53
|
+
return cached;
|
|
54
|
+
}
|
|
55
|
+
/** Headers to merge into every API request so the server can attribute the call. */
|
|
56
|
+
export function clientHeaders() {
|
|
57
|
+
const ctx = clientContext();
|
|
58
|
+
return {
|
|
59
|
+
'User-Agent': `gipity-cli/${ctx.cli} (${ctx.os}; ${ctx.arch}; harness=${ctx.harness})`,
|
|
60
|
+
'X-Gipity-Client': JSON.stringify(ctx),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=client-context.js.map
|
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
|
@@ -4,20 +4,8 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, readdir
|
|
|
4
4
|
import { execSync, spawn } from 'child_process';
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (process.platform !== 'win32')
|
|
10
|
-
return cmd;
|
|
11
|
-
try {
|
|
12
|
-
const lines = execSync(`where ${cmd}`, { encoding: 'utf-8' }).split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
|
13
|
-
// Prefer .exe (native) over .cmd (npm shim)
|
|
14
|
-
return lines.find(l => l.endsWith('.exe')) || lines.find(l => l.endsWith('.cmd')) || cmd;
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
return `${cmd}.cmd`;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
import { getAuth, saveAuth } from '../auth.js';
|
|
7
|
+
import { resolveCommand } from '../platform.js';
|
|
8
|
+
import { getAuth, saveAuth, sessionExpired } from '../auth.js';
|
|
21
9
|
import { get, post, publicPost, ApiError, getAccountSlug } from '../api.js';
|
|
22
10
|
import { getConfig, saveConfigAt, clearConfigCache, getApiBaseOverride, DEFAULT_API_BASE, getConfigPath } from '../config.js';
|
|
23
11
|
import { sync } from '../sync.js';
|
|
@@ -26,12 +14,35 @@ import { buildProjectContextBlock as buildProjectContextBlockText, buildNewProje
|
|
|
26
14
|
import * as relayState from '../relay/state.js';
|
|
27
15
|
import { maybeOfferRelayOn, ensureDaemonRunning } from '../relay/onboarding.js';
|
|
28
16
|
import { prompt, pickOne, decodeJwtExp, confirm } from '../utils.js';
|
|
29
|
-
import { brand, bold, info, success, error as clrError, muted } from '../colors.js';
|
|
17
|
+
import { brand, bold, info, success, warning, error as clrError, muted } from '../colors.js';
|
|
30
18
|
import { createProgressReporter } from '../progress.js';
|
|
31
19
|
import { printBanner } from '../banner.js';
|
|
32
20
|
import { scanForAdoption, isLikelyEmpty, canAdoptCwd, formatCwdLabel, formatBytes, adoptCurrentDir, ADOPT_THRESHOLDS, } from '../adopt-cwd.js';
|
|
33
21
|
const __clDir = dirname(fileURLToPath(import.meta.url));
|
|
34
22
|
const __clPkg = JSON.parse(readFileSync(resolve(__clDir, '../../package.json'), 'utf-8'));
|
|
23
|
+
/** Report a sync run to the user. Beyond the applied-changes line, this SURFACES
|
|
24
|
+
* sync.errors - a download that came back incomplete (truncated bulk tar, a file
|
|
25
|
+
* that couldn't be refetched) lands here, so we never print "ready" over a
|
|
26
|
+
* half-synced project as if nothing went wrong. Deletions are disarmed on an
|
|
27
|
+
* incomplete pull, so we can tell the user plainly that nothing was deleted. */
|
|
28
|
+
function reportSyncResult(result) {
|
|
29
|
+
if (result.aborted) {
|
|
30
|
+
console.log(` ${warning('Merge cancelled — this folder was NOT synced with the project.')}`);
|
|
31
|
+
console.log(` ${muted('For a clean copy, quit and open the project in an empty folder. To merge anyway, run `gipity sync`.')}`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (result.applied > 0) {
|
|
35
|
+
console.log(` Synced ${result.applied} change${result.applied > 1 ? 's' : ''} with Gipity.`);
|
|
36
|
+
}
|
|
37
|
+
if (result.errors.length) {
|
|
38
|
+
console.log(` ${warning(`Sync finished with ${result.errors.length} problem${result.errors.length === 1 ? '' : 's'} — your local copy may be incomplete:`)}`);
|
|
39
|
+
for (const e of result.errors.slice(0, 8))
|
|
40
|
+
console.log(` - ${e}`);
|
|
41
|
+
if (result.errors.length > 8)
|
|
42
|
+
console.log(` …and ${result.errors.length - 8} more.`);
|
|
43
|
+
console.log(` ${muted('Nothing was deleted. Re-run `gipity sync` to finish the pull.')}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
35
46
|
import { getProjectsRoot } from '../relay/paths.js';
|
|
36
47
|
/** Ask the server for recursive VFS counts. Server owns the file metadata
|
|
37
48
|
* (counts, bytes, paths), so a single aggregate query beats walking the
|
|
@@ -280,7 +291,15 @@ export const claudeCommand = new Command('claude')
|
|
|
280
291
|
if (!nonInteractive) {
|
|
281
292
|
printBanner({ version: __clPkg.version, email: auth?.email, cwd: process.cwd() });
|
|
282
293
|
}
|
|
283
|
-
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) {
|
|
284
303
|
console.log(` Logged in (${auth.email}).`);
|
|
285
304
|
}
|
|
286
305
|
else {
|
|
@@ -395,13 +414,11 @@ export const claudeCommand = new Command('claude')
|
|
|
395
414
|
});
|
|
396
415
|
console.log(`\n Using ${projectDir}`);
|
|
397
416
|
try {
|
|
398
|
-
const result = await sync({ interactive: !nonInteractive, progress: syncProgress });
|
|
399
|
-
|
|
400
|
-
console.log(` Synced ${result.applied} change${result.applied > 1 ? 's' : ''} with Gipity.`);
|
|
401
|
-
}
|
|
417
|
+
const result = await sync({ interactive: !nonInteractive, confirmMerge: !nonInteractive, progress: syncProgress });
|
|
418
|
+
reportSyncResult(result);
|
|
402
419
|
}
|
|
403
|
-
catch {
|
|
404
|
-
console.log('
|
|
420
|
+
catch (err) {
|
|
421
|
+
console.log(` ${warning('Could not sync files (will retry on next prompt):')} ${err.message}`);
|
|
405
422
|
}
|
|
406
423
|
setupClaudeHooks();
|
|
407
424
|
setupClaudeMd();
|
|
@@ -445,13 +462,11 @@ export const claudeCommand = new Command('claude')
|
|
|
445
462
|
}
|
|
446
463
|
if (doSync) {
|
|
447
464
|
try {
|
|
448
|
-
const result = await sync({ interactive: !nonInteractive, progress: syncProgress });
|
|
449
|
-
|
|
450
|
-
console.log(` Synced ${result.applied} change${result.applied > 1 ? 's' : ''} with Gipity.`);
|
|
451
|
-
}
|
|
465
|
+
const result = await sync({ interactive: !nonInteractive, confirmMerge: !nonInteractive, progress: syncProgress });
|
|
466
|
+
reportSyncResult(result);
|
|
452
467
|
}
|
|
453
|
-
catch {
|
|
454
|
-
console.log('
|
|
468
|
+
catch (err) {
|
|
469
|
+
console.log(` ${warning('Could not sync files (will retry on next prompt):')} ${err.message}`);
|
|
455
470
|
}
|
|
456
471
|
}
|
|
457
472
|
// Interactive: no seeded prompt - CLAUDE.md carries the context now.
|
|
@@ -570,13 +585,11 @@ export const claudeCommand = new Command('claude')
|
|
|
570
585
|
console.log(`\n Using ${projectDir}`);
|
|
571
586
|
// Unified sync - push and pull resolved via three-way merge (non-fatal)
|
|
572
587
|
try {
|
|
573
|
-
const result = await sync({ interactive: !nonInteractive, progress: syncProgress });
|
|
574
|
-
|
|
575
|
-
console.log(` Synced ${result.applied} change${result.applied > 1 ? 's' : ''} with Gipity.`);
|
|
576
|
-
}
|
|
588
|
+
const result = await sync({ interactive: !nonInteractive, confirmMerge: !nonInteractive, progress: syncProgress });
|
|
589
|
+
reportSyncResult(result);
|
|
577
590
|
}
|
|
578
|
-
catch {
|
|
579
|
-
console.log('
|
|
591
|
+
catch (err) {
|
|
592
|
+
console.log(` ${warning('Could not sync files (will retry on next prompt):')} ${err.message}`);
|
|
580
593
|
}
|
|
581
594
|
}
|
|
582
595
|
// Interactive launch: no seeded first message (new or existing). The
|
package/dist/commands/credits.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { get } from '../api.js';
|
|
4
|
-
import { brand, muted } from '../colors.js';
|
|
4
|
+
import { brand, muted, success, error as clrError } from '../colors.js';
|
|
5
5
|
import { run, printList } from '../helpers/index.js';
|
|
6
6
|
const PRICING_URL = 'https://prompt.gipity.ai/pricing';
|
|
7
7
|
function openInBrowser(url) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
// Windows `start` is a cmd.exe builtin, not an executable - spawn can't
|
|
9
|
+
// launch it directly, so go through `cmd /c start`. The empty "" is the
|
|
10
|
+
// window-title arg; without it a quoted URL would be swallowed as the title.
|
|
11
|
+
const [cmd, args] = process.platform === 'darwin' ? ['open', [url]] :
|
|
12
|
+
process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]] :
|
|
13
|
+
['xdg-open', [url]];
|
|
11
14
|
try {
|
|
12
|
-
spawn(cmd,
|
|
15
|
+
spawn(cmd, args, { stdio: 'ignore', detached: true }).unref();
|
|
13
16
|
}
|
|
14
17
|
catch {
|
|
15
18
|
// Spawn failed (no xdg-open on minimal Linux, etc.) - caller prints the URL.
|
|
@@ -19,17 +22,23 @@ export const creditsCommand = new Command('credits')
|
|
|
19
22
|
.description('View credits')
|
|
20
23
|
.option('--json', 'Output as JSON')
|
|
21
24
|
.action((opts) => run('Credits', async () => {
|
|
22
|
-
const res = await get('/credits/balance');
|
|
23
25
|
if (opts.json) {
|
|
26
|
+
const res = await get('/credits/balance');
|
|
24
27
|
console.log(JSON.stringify(res.data));
|
|
28
|
+
return;
|
|
25
29
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
const [balRes, subRes] = await Promise.all([
|
|
31
|
+
get('/credits/balance'),
|
|
32
|
+
get('/credits/subscription'),
|
|
33
|
+
]);
|
|
34
|
+
const sub = subRes.data;
|
|
35
|
+
const tierLabel = sub.tier === 'pro' ? 'Gipity Pro' : 'Free';
|
|
36
|
+
console.log(`Plan: ${brand(tierLabel)} ${muted(`(${sub.status})`)}`);
|
|
37
|
+
console.log(`Credits: ${brand(balRes.data.available.toLocaleString())}`);
|
|
38
|
+
if (balRes.data.balances.length > 0) {
|
|
39
|
+
for (const b of balRes.data.balances) {
|
|
40
|
+
const exp = new Date(b.expiresAt).toLocaleDateString();
|
|
41
|
+
console.log(`${b.source}: ${b.creditsRemaining.toLocaleString()} ${muted(`expires ${exp}`)}`);
|
|
33
42
|
}
|
|
34
43
|
}
|
|
35
44
|
}));
|
|
@@ -41,12 +50,42 @@ creditsCommand
|
|
|
41
50
|
openInBrowser(PRICING_URL);
|
|
42
51
|
console.log(muted("If your browser didn't open automatically, copy the URL above."));
|
|
43
52
|
}));
|
|
53
|
+
creditsCommand
|
|
54
|
+
.command('status')
|
|
55
|
+
.description('Show billing/Stripe configuration health (admin only)')
|
|
56
|
+
.option('--json', 'Output as JSON')
|
|
57
|
+
// optsWithGlobals(): the parent `credits` command also declares --json, which
|
|
58
|
+
// commander binds to the parent - so read merged opts to see the flag here.
|
|
59
|
+
.action((_opts, cmd) => run('Billing status', async () => {
|
|
60
|
+
const opts = cmd.optsWithGlobals();
|
|
61
|
+
const res = await get('/admin/billing-status');
|
|
62
|
+
const s = res.data;
|
|
63
|
+
if (opts.json) {
|
|
64
|
+
console.log(JSON.stringify(s));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const mark = (ok) => (ok ? success('✓') : clrError('✗'));
|
|
68
|
+
console.log(`Stripe mode: ${brand(s.mode)}`);
|
|
69
|
+
console.log(`Secret key: ${mark(s.secretKeyConfigured)}`);
|
|
70
|
+
console.log(`Webhook secret: ${mark(s.webhookSecretConfigured)} ${muted(`endpoint ${s.webhookPath}`)}`);
|
|
71
|
+
console.log(`Credit packs: ${mark(s.packsConfigured)}${s.packsError ? ` ${clrError(s.packsError)}` : ''}`);
|
|
72
|
+
for (const p of s.packs) {
|
|
73
|
+
console.log(` ${p.name} ${muted(p.priceId)}${p.hidden ? muted(' [hidden]') : ''}`);
|
|
74
|
+
}
|
|
75
|
+
console.log(`Test pack ($1): ${mark(s.testPackConfigured)} ${muted('set STRIPE_PACK_TEST_PRICE_ID to enable a cheap purchase smoke test')}`);
|
|
76
|
+
console.log('Subscription plans:');
|
|
77
|
+
for (const pl of s.subscriptionPlans) {
|
|
78
|
+
console.log(` ${pl.name} (${pl.tier}) ${mark(pl.priceIdConfigured)}`);
|
|
79
|
+
}
|
|
80
|
+
}));
|
|
44
81
|
creditsCommand
|
|
45
82
|
.command('usage')
|
|
46
83
|
.description('Show recent usage')
|
|
47
84
|
.option('--limit <n>', 'Number of entries', '20')
|
|
48
85
|
.option('--json', 'Output as JSON')
|
|
49
|
-
|
|
86
|
+
// optsWithGlobals(): parent `credits` also declares --json (see status above).
|
|
87
|
+
.action((_opts, cmd) => run('Usage', async () => {
|
|
88
|
+
const opts = cmd.optsWithGlobals();
|
|
50
89
|
const limit = parseInt(opts.limit, 10) || 20;
|
|
51
90
|
const res = await get(`/credits/usage?limit=${limit}`);
|
|
52
91
|
printList(res.data, opts, 'No usage history.', u => {
|
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,7 +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
|
+
import { printCommandError } from '../helpers/command.js';
|
|
9
|
+
import { withSpinner } from '../progress.js';
|
|
8
10
|
import { IMAGE_MODELS_DOC, IMAGE_GEMINI_ASPECT_RATIOS, IMAGE_GEMINI_SIZES, VIDEO_MODELS_DOC, TTS_PROVIDER_DESCRIPTIONS } from '../provider-docs.js';
|
|
9
11
|
/** Download a URL and save to a local file, then push it up to the project so
|
|
10
12
|
* the cloud (and anything that mirrors it) immediately matches local disk.
|
|
@@ -72,7 +74,7 @@ Examples:
|
|
|
72
74
|
.action(async (prompt, opts) => {
|
|
73
75
|
try {
|
|
74
76
|
const { config } = await resolveProjectContext();
|
|
75
|
-
const
|
|
77
|
+
const doGenerate = () => post(`/projects/${config.projectGuid}/generate/image`, {
|
|
76
78
|
prompt,
|
|
77
79
|
provider: opts.provider,
|
|
78
80
|
model: opts.model,
|
|
@@ -82,6 +84,9 @@ Examples:
|
|
|
82
84
|
image_size: opts.imageSize,
|
|
83
85
|
seed: Number.isFinite(opts.seed) ? opts.seed : undefined,
|
|
84
86
|
});
|
|
87
|
+
const result = opts.json
|
|
88
|
+
? await doGenerate()
|
|
89
|
+
: await withSpinner('Generating image…', doGenerate, { done: null });
|
|
85
90
|
const ext = result.content_type.includes('png') ? 'png' : 'jpg';
|
|
86
91
|
const filename = opts.output || `generated.${ext}`;
|
|
87
92
|
const savedPath = await downloadFile(result.url, filename);
|
|
@@ -98,7 +103,7 @@ Examples:
|
|
|
98
103
|
}
|
|
99
104
|
}
|
|
100
105
|
catch (err) {
|
|
101
|
-
|
|
106
|
+
printCommandError('Image generation', err);
|
|
102
107
|
process.exit(1);
|
|
103
108
|
}
|
|
104
109
|
});
|
|
@@ -130,14 +135,16 @@ Examples:
|
|
|
130
135
|
.action(async (prompt, opts) => {
|
|
131
136
|
try {
|
|
132
137
|
const { config } = await resolveProjectContext();
|
|
133
|
-
|
|
134
|
-
console.log(info('Generating video (this may take 30-120 seconds)...')); // keep --json stdout pure JSON
|
|
135
|
-
const result = await post(`/projects/${config.projectGuid}/generate/video`, {
|
|
138
|
+
const doGenerate = () => post(`/projects/${config.projectGuid}/generate/video`, {
|
|
136
139
|
prompt,
|
|
137
140
|
model: opts.model,
|
|
138
141
|
aspect_ratio: opts.aspect,
|
|
139
142
|
resolution: opts.resolution,
|
|
140
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 });
|
|
141
148
|
const filename = opts.output || 'generated.mp4';
|
|
142
149
|
const savedPath = await downloadFile(result.url, filename);
|
|
143
150
|
if (opts.json) {
|
|
@@ -150,7 +157,7 @@ Examples:
|
|
|
150
157
|
}
|
|
151
158
|
}
|
|
152
159
|
catch (err) {
|
|
153
|
-
|
|
160
|
+
printCommandError('Video generation', err);
|
|
154
161
|
process.exit(1);
|
|
155
162
|
}
|
|
156
163
|
});
|
|
@@ -190,13 +197,16 @@ Examples:
|
|
|
190
197
|
process.exit(1);
|
|
191
198
|
}
|
|
192
199
|
}
|
|
193
|
-
const
|
|
200
|
+
const doGenerate = () => post(`/projects/${config.projectGuid}/generate/speech`, {
|
|
194
201
|
text,
|
|
195
202
|
provider: opts.provider,
|
|
196
203
|
voice: opts.voice,
|
|
197
204
|
language: opts.language,
|
|
198
205
|
speakers,
|
|
199
206
|
});
|
|
207
|
+
const result = opts.json
|
|
208
|
+
? await doGenerate()
|
|
209
|
+
: await withSpinner('Generating speech…', doGenerate, { done: null });
|
|
200
210
|
const filename = opts.output || 'speech.mp3';
|
|
201
211
|
const savedPath = await downloadFile(result.url, filename);
|
|
202
212
|
if (opts.json) {
|
|
@@ -209,7 +219,7 @@ Examples:
|
|
|
209
219
|
}
|
|
210
220
|
}
|
|
211
221
|
catch (err) {
|
|
212
|
-
|
|
222
|
+
printCommandError('Speech generation', err);
|
|
213
223
|
process.exit(1);
|
|
214
224
|
}
|
|
215
225
|
});
|
|
@@ -239,14 +249,15 @@ Examples:
|
|
|
239
249
|
.action(async (prompt, opts) => {
|
|
240
250
|
try {
|
|
241
251
|
const { config } = await resolveProjectContext();
|
|
242
|
-
|
|
243
|
-
console.log(info('Generating music...')); // keep --json stdout pure JSON
|
|
244
|
-
const result = await post(`/projects/${config.projectGuid}/generate/music`, {
|
|
252
|
+
const doGenerate = () => post(`/projects/${config.projectGuid}/generate/music`, {
|
|
245
253
|
prompt,
|
|
246
254
|
duration_seconds: opts.duration,
|
|
247
255
|
model: opts.model,
|
|
248
256
|
instrumental: !opts.vocals,
|
|
249
257
|
});
|
|
258
|
+
const result = opts.json
|
|
259
|
+
? await doGenerate()
|
|
260
|
+
: await withSpinner('Generating music…', doGenerate, { done: null });
|
|
250
261
|
const filename = opts.output || 'music.mp3';
|
|
251
262
|
const savedPath = await downloadFile(result.url, filename);
|
|
252
263
|
if (opts.json) {
|
|
@@ -259,7 +270,7 @@ Examples:
|
|
|
259
270
|
}
|
|
260
271
|
}
|
|
261
272
|
catch (err) {
|
|
262
|
-
|
|
273
|
+
printCommandError('Music generation', err);
|
|
263
274
|
process.exit(1);
|
|
264
275
|
}
|
|
265
276
|
});
|
|
@@ -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 }));
|