gipity 1.0.409 → 1.0.411
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/claude-setup.js +3 -2
- package/dist/commands/add.js +10 -1
- package/dist/commands/bug.js +69 -0
- package/dist/commands/credits.js +247 -15
- package/dist/commands/job.js +3 -3
- package/dist/commands/push.js +7 -3
- package/dist/commands/relay.js +26 -2
- package/dist/commands/remove.js +9 -3
- package/dist/commands/storage.js +64 -0
- package/dist/commands/uninstall.js +2 -2
- package/dist/commands/upload.js +1 -2
- package/dist/hooks/capture-runner.js +15 -2
- package/dist/index.js +5 -4
- package/dist/knowledge.js +22 -0
- package/dist/platform.js +9 -5
- package/dist/relay/daemon.js +517 -69
- package/dist/relay/ingest-queue.js +148 -0
- package/dist/relay/installers.js +10 -2
- package/dist/relay/onboarding.js +5 -2
- package/dist/relay/redact.js +27 -4
- package/dist/relay/setup.js +4 -4
- package/dist/relay/state.js +6 -3
- package/dist/relay/stream-delta.js +235 -0
- package/dist/relay/stream-json.js +61 -15
- package/dist/setup.js +16 -7
- package/dist/sync.js +227 -106
- package/dist/updater/shim.js +4 -4
- package/dist/upload.js +1 -1
- package/package.json +6 -3
package/dist/claude-setup.js
CHANGED
|
@@ -24,7 +24,7 @@ export function claudeInstallPlan(platformOverride) {
|
|
|
24
24
|
/** Whether the `claude` binary resolves on PATH. */
|
|
25
25
|
export function isClaudeInstalled(platformOverride) {
|
|
26
26
|
try {
|
|
27
|
-
execSync(claudeInstallPlan(platformOverride).checkCmd, { stdio: 'ignore' });
|
|
27
|
+
execSync(claudeInstallPlan(platformOverride).checkCmd, { stdio: 'ignore', windowsHide: true });
|
|
28
28
|
return true;
|
|
29
29
|
}
|
|
30
30
|
catch {
|
|
@@ -59,6 +59,7 @@ export function probeClaudeAuthenticated() {
|
|
|
59
59
|
timeout: 60_000,
|
|
60
60
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
61
61
|
encoding: 'utf-8',
|
|
62
|
+
windowsHide: true,
|
|
62
63
|
});
|
|
63
64
|
return typeof out === 'string' && out.trim().length > 0;
|
|
64
65
|
}
|
|
@@ -77,7 +78,7 @@ export function ensureClaudeInstalled(opts = {}) {
|
|
|
77
78
|
return { installed: true, alreadyPresent: true };
|
|
78
79
|
const { installArgv } = claudeInstallPlan();
|
|
79
80
|
try {
|
|
80
|
-
execSync(installArgv.join(' '), { stdio: opts.quiet ? 'ignore' : 'inherit' });
|
|
81
|
+
execSync(installArgv.join(' '), { stdio: opts.quiet ? 'ignore' : 'inherit', windowsHide: true });
|
|
81
82
|
}
|
|
82
83
|
catch {
|
|
83
84
|
// Fall through to a definitive PATH re-check rather than trusting the throw.
|
package/dist/commands/add.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Command } from 'commander';
|
|
|
5
5
|
import { post } from '../api.js';
|
|
6
6
|
import { requireConfig } from '../config.js';
|
|
7
7
|
import { sync } from '../sync.js';
|
|
8
|
-
import { success, muted, bold } from '../colors.js';
|
|
8
|
+
import { success, muted, bold, warning } from '../colors.js';
|
|
9
9
|
import { run } from '../helpers/index.js';
|
|
10
10
|
import { createProgressReporter, withSpinner } from '../progress.js';
|
|
11
11
|
const STARTERS = [
|
|
@@ -225,6 +225,15 @@ export const addCommand = new Command('add')
|
|
|
225
225
|
for (const n of data.notes)
|
|
226
226
|
console.log(n);
|
|
227
227
|
}
|
|
228
|
+
// Partial install (server HTTP 207): some files were dropped. Surface them so
|
|
229
|
+
// the user knows the install is incomplete rather than reading a clean success.
|
|
230
|
+
if (data.failed?.length) {
|
|
231
|
+
console.log('');
|
|
232
|
+
console.warn(warning(`${data.failed.length} file(s) failed to install:`));
|
|
233
|
+
for (const f of data.failed)
|
|
234
|
+
console.warn(warning(` ! ${f.path}: ${f.error}`));
|
|
235
|
+
console.warn(warning(`Re-run \`gipity add ${data.kit ?? name}${opts.force ? ' --force' : ''}\` to retry the dropped files.`));
|
|
236
|
+
}
|
|
228
237
|
// After scaffolding an app, point at kits as the next step - they add
|
|
229
238
|
// features (multiplayer, etc.) into the app you just created.
|
|
230
239
|
if (data.kind !== 'kit' && KITS.length > 0) {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { get, post } from '../api.js';
|
|
3
|
+
import { resolveProjectContext } from '../config.js';
|
|
4
|
+
import { bold, muted, success, warning } from '../colors.js';
|
|
5
|
+
import { run } from '../helpers/index.js';
|
|
6
|
+
// Kept in sync with @easyclaw/shared BUG_REPORT_CATEGORIES / _SEVERITIES (the CLI
|
|
7
|
+
// package can't import the server's shared package). The server validates
|
|
8
|
+
// authoritatively; these are for --help and a friendly client-side check.
|
|
9
|
+
const CATEGORIES = ['cli', 'deploy', 'template', 'kit', 'db', 'docs', 'skill', 'service', 'sandbox', 'other'];
|
|
10
|
+
const SEVERITIES = ['S1', 'S2', 'S3', 'S4'];
|
|
11
|
+
const SEVERITY_HINT = 'S1=blocker, S2=major, S3=minor, S4=friction';
|
|
12
|
+
export const bugCommand = new Command('bug')
|
|
13
|
+
.description('Report Gipity platform bugs / friction in real time')
|
|
14
|
+
.addHelpText('after', `\nCapture platform friction the moment you hit it — even one you worked around —` +
|
|
15
|
+
`\nso the team can triage it into a fix.\n` +
|
|
16
|
+
`\nCategories: ${CATEGORIES.join(', ')}` +
|
|
17
|
+
`\nSeverity: ${SEVERITY_HINT}` +
|
|
18
|
+
`\nNever include PII or user data — describe the platform problem in the abstract.`);
|
|
19
|
+
bugCommand
|
|
20
|
+
.command('report')
|
|
21
|
+
.description('File a bug / friction report about the Gipity platform')
|
|
22
|
+
.requiredOption('--category <cat>', `One of: ${CATEGORIES.join(', ')}`)
|
|
23
|
+
.requiredOption('--severity <sev>', SEVERITY_HINT)
|
|
24
|
+
.requiredOption('--summary <text>', 'One line, 7 words max. No PII/user data.')
|
|
25
|
+
.option('--detail <text>', 'Succinct: what you did, what failed, the workaround. No PII/user data.')
|
|
26
|
+
.option('--project <guid-or-slug>', 'Attribute to a specific project instead of cwd / Home')
|
|
27
|
+
.option('--json', 'Output raw JSON')
|
|
28
|
+
.action((opts) => run('Bug report', async () => {
|
|
29
|
+
const category = String(opts.category).toLowerCase();
|
|
30
|
+
const severity = /^[1-4]$/.test(opts.severity) ? `S${opts.severity}` : String(opts.severity).toUpperCase();
|
|
31
|
+
if (!CATEGORIES.includes(category))
|
|
32
|
+
throw new Error(`--category must be one of: ${CATEGORIES.join(', ')}`);
|
|
33
|
+
if (!SEVERITIES.includes(severity))
|
|
34
|
+
throw new Error(`--severity must be one of: ${SEVERITY_HINT}`);
|
|
35
|
+
if (String(opts.summary).trim().split(/\s+/).filter(Boolean).length > 7) {
|
|
36
|
+
throw new Error('--summary must be 7 words or fewer');
|
|
37
|
+
}
|
|
38
|
+
const { config } = await resolveProjectContext({ projectOverride: opts.project });
|
|
39
|
+
const res = await post(`/api/${config.projectGuid}/services/bug-report/submit`, { category, severity, summary: opts.summary, detail: opts.detail });
|
|
40
|
+
if (opts.json) {
|
|
41
|
+
console.log(JSON.stringify(res.data));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
console.log(success(`✓ Bug report filed (${res.data.report_guid}) — queued for triage.`));
|
|
45
|
+
}));
|
|
46
|
+
bugCommand
|
|
47
|
+
.command('list')
|
|
48
|
+
.description('List the bug / friction reports you have filed')
|
|
49
|
+
.option('--project <guid-or-slug>', 'Resolve auth against a specific project instead of cwd / Home')
|
|
50
|
+
.option('--json', 'Output raw JSON')
|
|
51
|
+
.action((opts) => run('Bug report', async () => {
|
|
52
|
+
const { config } = await resolveProjectContext({ projectOverride: opts.project });
|
|
53
|
+
const res = await get(`/api/${config.projectGuid}/services/bug-report/list`);
|
|
54
|
+
if (opts.json) {
|
|
55
|
+
console.log(JSON.stringify(res.data));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const rows = res.data;
|
|
59
|
+
if (!rows.length) {
|
|
60
|
+
console.log(warning('No bug reports filed yet.'));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
console.log(bold(`${rows.length} report${rows.length === 1 ? '' : 's'}`));
|
|
64
|
+
for (const r of rows) {
|
|
65
|
+
console.log(` ${r.severity} ${muted(r.category.padEnd(8))} ${r.summary} ${muted(`[${r.status}]`)}`);
|
|
66
|
+
console.log(muted(` ${r.report_guid} ${r.created_at}`));
|
|
67
|
+
}
|
|
68
|
+
}));
|
|
69
|
+
//# sourceMappingURL=bug.js.map
|
package/dist/commands/credits.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import {
|
|
3
|
-
import { get } from '../api.js';
|
|
4
|
-
import { brand, muted } from '../colors.js';
|
|
2
|
+
import { spawnCommand } from '../platform.js';
|
|
3
|
+
import { get, post, ApiError } from '../api.js';
|
|
4
|
+
import { brand, dim, bold, muted, success } from '../colors.js';
|
|
5
5
|
import { run, printList } from '../helpers/index.js';
|
|
6
6
|
const PRICING_URL = 'https://prompt.gipity.ai/pricing';
|
|
7
7
|
function openInBrowser(url) {
|
|
@@ -12,7 +12,7 @@ function openInBrowser(url) {
|
|
|
12
12
|
process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]] :
|
|
13
13
|
['xdg-open', [url]];
|
|
14
14
|
try {
|
|
15
|
-
const child =
|
|
15
|
+
const child = spawnCommand(cmd, args, { stdio: 'ignore', detached: true });
|
|
16
16
|
// A missing/non-executable launcher (ENOENT, or EACCES on WSL/minimal Linux
|
|
17
17
|
// where xdg-open isn't runnable) is reported ASYNCHRONOUSLY via an 'error'
|
|
18
18
|
// event - not a throw - so the try/catch alone can't stop it. With no
|
|
@@ -25,18 +25,92 @@ function openInBrowser(url) {
|
|
|
25
25
|
// Synchronous spawn failure - caller prints the URL.
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
// ---- Plan limits formatting (folded in from the old `plan` command) ----
|
|
29
|
+
// Every key the plan definition enforces is rendered, so `credits` /
|
|
30
|
+
// `credits list` are a full-transparency view of what your plan actually caps.
|
|
31
|
+
const LIMIT_LABELS = {
|
|
32
|
+
maxProjects: 'Projects',
|
|
33
|
+
maxDatabases: 'Databases',
|
|
34
|
+
storageQuotaBytes: 'Storage',
|
|
35
|
+
maxWorkflows: 'Workflows',
|
|
36
|
+
minCronIntervalHours: 'Cron frequency',
|
|
37
|
+
maxConcurrentChats: 'Concurrent chats',
|
|
38
|
+
deployRatePerMinute: 'Deploys/min',
|
|
39
|
+
testFileConcurrency: 'Parallel tests',
|
|
40
|
+
};
|
|
41
|
+
// Nested serviceLimits (media generation entitlements). Convention:
|
|
42
|
+
// -1 = unlimited, 0 = Pro-only (blocked on free), N = N free uses/month.
|
|
43
|
+
const SERVICE_LABELS = {
|
|
44
|
+
video: 'Video generation',
|
|
45
|
+
music: 'Music generation',
|
|
46
|
+
image: 'Image generation',
|
|
47
|
+
audio: 'Speech & sound FX',
|
|
48
|
+
};
|
|
49
|
+
function formatLimit(key, value) {
|
|
50
|
+
if (typeof value !== 'number')
|
|
51
|
+
return String(value);
|
|
52
|
+
if (key === 'storageQuotaBytes') {
|
|
53
|
+
const gb = value / (1024 ** 3);
|
|
54
|
+
return `${gb.toFixed(gb >= 1 ? 1 : 2)} GB`;
|
|
55
|
+
}
|
|
56
|
+
if (key === 'minCronIntervalHours') {
|
|
57
|
+
return value === 0 ? 'no minimum' : `${value}h minimum`;
|
|
58
|
+
}
|
|
59
|
+
return value.toLocaleString();
|
|
60
|
+
}
|
|
61
|
+
function formatServiceLimit(value) {
|
|
62
|
+
if (value === -1)
|
|
63
|
+
return 'unlimited';
|
|
64
|
+
if (value === 0)
|
|
65
|
+
return 'Pro only';
|
|
66
|
+
if (typeof value === 'number')
|
|
67
|
+
return `${value.toLocaleString()}/mo free`;
|
|
68
|
+
return String(value);
|
|
69
|
+
}
|
|
70
|
+
function renderLimits(limits, indent = '') {
|
|
71
|
+
for (const key of Object.keys(LIMIT_LABELS)) {
|
|
72
|
+
const value = limits[key];
|
|
73
|
+
if (value !== undefined) {
|
|
74
|
+
console.log(`${indent}${LIMIT_LABELS[key].padEnd(18)} ${formatLimit(key, value)}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const services = limits.serviceLimits;
|
|
78
|
+
if (services && typeof services === 'object') {
|
|
79
|
+
console.log(`${indent}Media generation`);
|
|
80
|
+
for (const key of Object.keys(SERVICE_LABELS)) {
|
|
81
|
+
const value = services[key];
|
|
82
|
+
if (value !== undefined) {
|
|
83
|
+
console.log(`${indent} ${SERVICE_LABELS[key].padEnd(16)} ${formatServiceLimit(value)}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// `credits` is the single account/billing hub: tier, live balance, the full set
|
|
89
|
+
// of plan limits, and an upgrade nudge. `plan` aliases to it (so old muscle
|
|
90
|
+
// memory resolves to one implementation); compare plans with `plan list`.
|
|
28
91
|
export const creditsCommand = new Command('credits')
|
|
29
|
-
.
|
|
92
|
+
.alias('plan')
|
|
93
|
+
.description('Show your plan, credit balance, and limits')
|
|
30
94
|
.option('--json', 'Output as JSON')
|
|
31
95
|
.action((opts) => run('Credits', async () => {
|
|
96
|
+
// Version retention rides alongside the Storage limit; a failure fetching it
|
|
97
|
+
// must not sink the whole view, so degrade to null and omit the line.
|
|
98
|
+
const fetchRetention = () => get('/users/me/retention').then(r => r.data).catch(() => null);
|
|
32
99
|
if (opts.json) {
|
|
33
|
-
const
|
|
34
|
-
|
|
100
|
+
const [balRes, limitsRes, subRes, retention] = await Promise.all([
|
|
101
|
+
get('/credits/balance'),
|
|
102
|
+
get('/users/me/limits'),
|
|
103
|
+
get('/credits/subscription'),
|
|
104
|
+
fetchRetention(),
|
|
105
|
+
]);
|
|
106
|
+
console.log(JSON.stringify({ subscription: subRes.data, balance: balRes.data, limits: limitsRes.data, retention }));
|
|
35
107
|
return;
|
|
36
108
|
}
|
|
37
|
-
const [balRes, subRes] = await Promise.all([
|
|
109
|
+
const [balRes, subRes, limitsRes, retention] = await Promise.all([
|
|
38
110
|
get('/credits/balance'),
|
|
39
111
|
get('/credits/subscription'),
|
|
112
|
+
get('/users/me/limits'),
|
|
113
|
+
fetchRetention(),
|
|
40
114
|
]);
|
|
41
115
|
const sub = subRes.data;
|
|
42
116
|
const tierLabel = sub.tier === 'pro' ? 'Gipity Pro' : 'Free';
|
|
@@ -45,17 +119,175 @@ export const creditsCommand = new Command('credits')
|
|
|
45
119
|
if (balRes.data.balances.length > 0) {
|
|
46
120
|
for (const b of balRes.data.balances) {
|
|
47
121
|
const exp = new Date(b.expiresAt).toLocaleDateString();
|
|
48
|
-
console.log(
|
|
122
|
+
console.log(` ${b.source}: ${b.creditsRemaining.toLocaleString()} ${muted(`expires ${exp}`)}`);
|
|
49
123
|
}
|
|
50
124
|
}
|
|
125
|
+
console.log('\nLimits:');
|
|
126
|
+
renderLimits(limitsRes.data.limits, ' ');
|
|
127
|
+
if (retention) {
|
|
128
|
+
console.log(` ${'Version retention'.padEnd(18)} ${retention.days} days / ${retention.count} copies`);
|
|
129
|
+
}
|
|
130
|
+
console.log('');
|
|
131
|
+
if (sub.tier !== 'pro') {
|
|
132
|
+
console.log(dim('Upgrade to Gipity Pro for higher limits and 20,000 credits/mo — run `gipity credits buy`.'));
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
console.log(dim('Need more credits? Run `gipity credits list` to see credit packs, then `gipity credits buy <pack>`.'));
|
|
136
|
+
}
|
|
51
137
|
}));
|
|
138
|
+
// `credits list` compares every plan (with full limits) and the available
|
|
139
|
+
// credit packs — the "what can I upgrade to" view.
|
|
52
140
|
creditsCommand
|
|
53
|
-
.command('
|
|
54
|
-
.description('
|
|
55
|
-
.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
141
|
+
.command('list')
|
|
142
|
+
.description('Compare all plans and credit packs')
|
|
143
|
+
.option('--json', 'Output as JSON')
|
|
144
|
+
// optsWithGlobals(): the parent `credits` command also declares --json, which
|
|
145
|
+
// commander binds to the parent - read merged opts so --json is seen here.
|
|
146
|
+
.action((_opts, cmd) => run('Plans', async () => {
|
|
147
|
+
const opts = cmd.optsWithGlobals();
|
|
148
|
+
// Plans + subscription are required; credit packs are best-effort - the plan
|
|
149
|
+
// comparison must still render if /credits/products is unavailable (e.g.
|
|
150
|
+
// credit packs aren't configured in this environment).
|
|
151
|
+
const [plansRes, subRes] = await Promise.all([
|
|
152
|
+
get('/plans'),
|
|
153
|
+
get('/credits/subscription'),
|
|
154
|
+
]);
|
|
155
|
+
const products = await get('/credits/products')
|
|
156
|
+
.then(r => r.data).catch(() => []);
|
|
157
|
+
if (opts.json) {
|
|
158
|
+
console.log(JSON.stringify({ currentTier: subRes.data.tier, plans: plansRes.data, products }));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const currentTier = subRes.data.tier;
|
|
162
|
+
const plans = plansRes.data;
|
|
163
|
+
if (plans.length === 0) {
|
|
164
|
+
console.log('No active plans.');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
console.log(bold('Plans'));
|
|
168
|
+
for (const plan of plans) {
|
|
169
|
+
const marker = plan.tier === currentTier ? brand('* ') : ' ';
|
|
170
|
+
const price = plan.monthlyPriceUsd > 0 ? ` - $${plan.monthlyPriceUsd}/mo` : ' - Free';
|
|
171
|
+
const credits = plan.monthlyCredits > 0
|
|
172
|
+
? ` - ${plan.monthlyCredits.toLocaleString()} credits/mo (${plan.creditExpiryDays}-day expiry)`
|
|
173
|
+
: '';
|
|
174
|
+
console.log(`${marker}${plan.displayName} (${plan.tier})${price}${credits}`);
|
|
175
|
+
renderLimits(plan.limits, ' ');
|
|
176
|
+
console.log('');
|
|
177
|
+
}
|
|
178
|
+
const packs = products.filter(p => p.type === 'one_time');
|
|
179
|
+
if (packs.length > 0) {
|
|
180
|
+
console.log(bold('Credit packs') + muted(' (require an active Pro subscription)'));
|
|
181
|
+
for (const pack of packs) {
|
|
182
|
+
const lock = pack.available ? '' : muted(' [Pro only]');
|
|
183
|
+
console.log(` ${pack.name} - $${pack.amountUsd} - ${pack.credits.toLocaleString()} credits${lock}`);
|
|
184
|
+
}
|
|
185
|
+
console.log('');
|
|
186
|
+
}
|
|
187
|
+
console.log(dim('* = your current plan. Upgrade or top up with `gipity credits buy [pro|<pack-credits>]`.'));
|
|
188
|
+
}));
|
|
189
|
+
// `credits buy` is the ONE purchase command. It resolves the target to a
|
|
190
|
+
// Stripe price and prints a checkout link (agent-friendly: the link is always
|
|
191
|
+
// printed so it can be relayed to the user; --open also launches a browser).
|
|
192
|
+
creditsCommand
|
|
193
|
+
.command('buy [target]')
|
|
194
|
+
.alias('upgrade')
|
|
195
|
+
.description('Upgrade your plan or buy a credit pack (target: a tier like "pro", or a pack credit amount)')
|
|
196
|
+
.option('--open', 'Open the checkout page in your browser')
|
|
197
|
+
.option('--json', 'Output the checkout URL as JSON')
|
|
198
|
+
// optsWithGlobals(): the parent `credits` command also declares --json, which
|
|
199
|
+
// commander binds to the parent - read merged opts so --json/--open are seen here.
|
|
200
|
+
.action((target, _opts, cmd) => run('Buy', async () => {
|
|
201
|
+
const opts = cmd.optsWithGlobals();
|
|
202
|
+
const subRes = await get('/credits/subscription');
|
|
203
|
+
// Products are best-effort: if the catalog is unavailable we can't build a
|
|
204
|
+
// direct checkout link, so we fall back to the pricing page below rather
|
|
205
|
+
// than erroring out.
|
|
206
|
+
const products = await get('/credits/products')
|
|
207
|
+
.then(r => r.data).catch(() => []);
|
|
208
|
+
const currentTier = subRes.data.tier;
|
|
209
|
+
// Resolve the target product. With no arg, default to the first available
|
|
210
|
+
// paid subscription (the upgrade path). With an arg, match a plan/pack by
|
|
211
|
+
// name or credit amount, mirroring the cloud agent's product keys.
|
|
212
|
+
// Non-happy exits must stay parseable under --json (agents rely on it), so
|
|
213
|
+
// emit a JSON { error } instead of prose when the flag is set.
|
|
214
|
+
const bail = (message, humanLines) => {
|
|
215
|
+
if (opts.json)
|
|
216
|
+
console.log(JSON.stringify({ error: message }));
|
|
217
|
+
else
|
|
218
|
+
for (const line of humanLines)
|
|
219
|
+
console.log(line);
|
|
220
|
+
};
|
|
221
|
+
let product;
|
|
222
|
+
if (!target) {
|
|
223
|
+
product = products.find(p => p.type === 'subscription' && p.available);
|
|
224
|
+
if (!product) {
|
|
225
|
+
bail(currentTier === 'pro'
|
|
226
|
+
? "You're already on Gipity Pro. To top up credits, run `gipity credits list` and buy a pack, e.g. `gipity credits buy 20000`."
|
|
227
|
+
: 'No upgrade plan is available right now.', [
|
|
228
|
+
currentTier === 'pro'
|
|
229
|
+
? `You're already on ${brand('Gipity Pro')}. To top up credits, run \`gipity credits list\` and buy a pack, e.g. \`gipity credits buy 20000\`.`
|
|
230
|
+
: 'No upgrade plan is available right now.',
|
|
231
|
+
dim(`Browse everything at ${PRICING_URL}`),
|
|
232
|
+
]);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
const key = target.toLowerCase();
|
|
238
|
+
product = products.find(p => p.type === 'subscription' && p.name.toLowerCase() === key)
|
|
239
|
+
// pack keyed by credit amount, e.g. "20000" - MUST be a pack, not a
|
|
240
|
+
// subscription (Pro's monthly_credits can equal a pack's credit count).
|
|
241
|
+
?? products.find(p => p.type === 'one_time' && String(p.credits) === key)
|
|
242
|
+
// accept "pro" against the single paid plan when only one exists
|
|
243
|
+
?? (key === 'pro' ? products.find(p => p.type === 'subscription') : undefined);
|
|
244
|
+
if (!product) {
|
|
245
|
+
bail(`Unknown option "${target}". Run \`gipity credits list\` to see plans and packs.`, [`Unknown option "${target}". Run \`gipity credits list\` to see plans and packs.`]);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (!product.available) {
|
|
249
|
+
bail(product.type === 'subscription'
|
|
250
|
+
? `You're already on ${product.name}.`
|
|
251
|
+
: 'Credit packs require an active Pro subscription. Run `gipity credits buy pro` first.', [product.type === 'subscription'
|
|
252
|
+
? `You're already on ${brand(product.name)}.`
|
|
253
|
+
: 'Credit packs require an active Pro subscription. Run `gipity credits buy pro` first.']);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
const res = await post('/credits/purchase', { priceId: product.priceId });
|
|
259
|
+
const url = res.data.checkoutUrl;
|
|
260
|
+
if (opts.json) {
|
|
261
|
+
console.log(JSON.stringify({ product: product.name, amountUsd: product.amountUsd, checkoutUrl: url }));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const verb = product.type === 'subscription' ? 'Upgrading to' : 'Buying';
|
|
265
|
+
const per = product.type === 'subscription' ? '/mo' : '';
|
|
266
|
+
console.log(`${verb} ${brand(product.name)} ${muted(`($${product.amountUsd}${per}, ${product.credits.toLocaleString()} credits)`)}`);
|
|
267
|
+
console.log('');
|
|
268
|
+
console.log(` ${bold('Checkout:')} ${success(url)}`);
|
|
269
|
+
console.log('');
|
|
270
|
+
console.log(dim('Open the link to complete your purchase — 2 minutes, cancel anytime. Your plan unlocks the moment payment clears.'));
|
|
271
|
+
if (opts.open)
|
|
272
|
+
openInBrowser(url);
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
// Payments not configured / Stripe hiccup: fall back to the pricing page
|
|
276
|
+
// so the user is never dead-ended.
|
|
277
|
+
if (err instanceof ApiError) {
|
|
278
|
+
if (opts.json) {
|
|
279
|
+
console.log(JSON.stringify({ error: err.message, checkoutUrl: null, fallbackUrl: PRICING_URL }));
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
console.log(muted(`Couldn't create a direct checkout link (${err.message}).`));
|
|
283
|
+
console.log(`Complete your purchase at ${brand(PRICING_URL)}`);
|
|
284
|
+
}
|
|
285
|
+
if (opts.open)
|
|
286
|
+
openInBrowser(PRICING_URL);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
throw err;
|
|
290
|
+
}
|
|
59
291
|
}));
|
|
60
292
|
creditsCommand
|
|
61
293
|
.command('usage')
|
package/dist/commands/job.js
CHANGED
|
@@ -193,7 +193,7 @@ jobCommand
|
|
|
193
193
|
.action(async (name, opts) => {
|
|
194
194
|
const { existsSync, statSync } = await import('node:fs');
|
|
195
195
|
const { resolve, join } = await import('node:path');
|
|
196
|
-
const {
|
|
196
|
+
const { spawnCommand, spawnSyncCommand } = await import('../platform.js');
|
|
197
197
|
const jobDir = resolve(process.cwd(), 'jobs', name);
|
|
198
198
|
if (!existsSync(jobDir) || !statSync(jobDir).isDirectory()) {
|
|
199
199
|
console.error(clrError(`jobs/${name}/ not found in current directory`));
|
|
@@ -213,7 +213,7 @@ jobCommand
|
|
|
213
213
|
process.exit(1);
|
|
214
214
|
}
|
|
215
215
|
// Confirm docker is reachable before bothering to build args.
|
|
216
|
-
const probe =
|
|
216
|
+
const probe = spawnSyncCommand('docker', ['info'], { stdio: 'ignore' });
|
|
217
217
|
if (probe.status !== 0) {
|
|
218
218
|
console.error(clrError('docker daemon not reachable. Start Docker Desktop / dockerd and retry.'));
|
|
219
219
|
process.exit(1);
|
|
@@ -240,7 +240,7 @@ jobCommand
|
|
|
240
240
|
shellCmd = `echo '[run-local] running ${handlerCmd} (${picked.runtime})' >&2 && exec ${handlerCmd}`;
|
|
241
241
|
}
|
|
242
242
|
const runArgs = ['run', ...sharedArgs, '--entrypoint', 'sh', opts.image, '-c', shellCmd];
|
|
243
|
-
const child =
|
|
243
|
+
const child = spawnCommand('docker', runArgs, { stdio: 'inherit' });
|
|
244
244
|
// A missing `docker` binary is reported asynchronously via 'error' (not a
|
|
245
245
|
// throw), so a try/catch can't stop it - without this handler Node crashes
|
|
246
246
|
// with a raw stack trace instead of a clear message.
|
package/dist/commands/push.js
CHANGED
|
@@ -11,9 +11,13 @@ export const pushCommand = new Command('push')
|
|
|
11
11
|
try {
|
|
12
12
|
const fullPath = resolve(file);
|
|
13
13
|
if (opts.background) {
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
// Detach a background `gipity push` and exit immediately. Goes through
|
|
15
|
+
// spawnCommand (Node binary running our own entry script - no IPC
|
|
16
|
+
// channel is needed, so spawn beats fork), which defaults
|
|
17
|
+
// `windowsHide: true` so the detached child never flashes a console
|
|
18
|
+
// window on Windows.
|
|
19
|
+
const { spawnCommand } = await import('../platform.js');
|
|
20
|
+
const child = spawnCommand(process.execPath, [process.argv[1], 'push', fullPath, '--quiet'], {
|
|
17
21
|
detached: true,
|
|
18
22
|
stdio: 'ignore',
|
|
19
23
|
});
|
package/dist/commands/relay.js
CHANGED
|
@@ -14,7 +14,7 @@ import { bold, brand, dim, success, error as clrError, muted, } from '../colors.
|
|
|
14
14
|
import * as state from '../relay/state.js';
|
|
15
15
|
import * as daemon from '../relay/daemon.js';
|
|
16
16
|
import { UnsupportedPlatformError } from '../relay/installers.js';
|
|
17
|
-
import { pairDevice, startDaemon, installAutostart } from '../relay/setup.js';
|
|
17
|
+
import { pairDevice, startDaemon, installAutostart, removeAutostart } from '../relay/setup.js';
|
|
18
18
|
import { registerInstallCommands } from './relay-install.js';
|
|
19
19
|
export const relayCommand = new Command('relay')
|
|
20
20
|
.description('Pair with the web CLI');
|
|
@@ -269,8 +269,32 @@ relayCommand
|
|
|
269
269
|
console.error(muted('Local token cleared anyway. Visit the web CLI to confirm the server-side revoke.'));
|
|
270
270
|
}
|
|
271
271
|
state.clearDevice();
|
|
272
|
+
// Remove the OS autostart unit too. Otherwise the login service relaunches
|
|
273
|
+
// `relay run`, which - finding no device but a valid login - silently
|
|
274
|
+
// re-registers a NEW device, undoing this revoke (on macOS the old
|
|
275
|
+
// KeepAlive=true even relaunched on the clean exit). Best-effort: an
|
|
276
|
+
// unsupported OS or a missing unit is fine.
|
|
277
|
+
let autostartRemoved = false;
|
|
278
|
+
try {
|
|
279
|
+
autostartRemoved = removeAutostart().ok;
|
|
280
|
+
}
|
|
281
|
+
catch { /* unsupported platform / no unit - ignore */ }
|
|
282
|
+
// Stop the currently-running daemon now rather than waiting for it to
|
|
283
|
+
// notice via a 401 on its next poll (~up to a hold cycle).
|
|
284
|
+
const pidPath = state.getDaemonPidPath();
|
|
285
|
+
if (existsSync(pidPath)) {
|
|
286
|
+
const pid = parseInt(readFileSync(pidPath, 'utf-8').trim(), 10);
|
|
287
|
+
if (pid && !isNaN(pid)) {
|
|
288
|
+
try {
|
|
289
|
+
process.kill(pid, 'SIGTERM');
|
|
290
|
+
}
|
|
291
|
+
catch { /* already gone */ }
|
|
292
|
+
}
|
|
293
|
+
}
|
|
272
294
|
console.log(success('Device revoked + local state cleared.'));
|
|
273
|
-
console.log(muted(
|
|
295
|
+
console.log(muted(autostartRemoved
|
|
296
|
+
? 'Auto-start removed and the background service was signalled to stop.'
|
|
297
|
+
: 'The background service was signalled to stop.'));
|
|
274
298
|
});
|
|
275
299
|
// ─── gipity relay log ──────────────────────────────────────────────────
|
|
276
300
|
relayCommand
|
package/dist/commands/remove.js
CHANGED
|
@@ -23,10 +23,16 @@ export const removeCommand = new Command('remove')
|
|
|
23
23
|
const res = opts.json
|
|
24
24
|
? await doRemove()
|
|
25
25
|
: await withSpinner('Removing…', doRemove, { done: null });
|
|
26
|
-
// Force the pull so the kit's deletions land locally without tripping the
|
|
27
|
-
// bulk-deletion guard - the removal is an explicit, user-invoked action.
|
|
28
|
-
const syncResult = await sync({ interactive: false, force: true, progress: opts.json ? undefined : createProgressReporter() });
|
|
29
26
|
const data = res.data;
|
|
27
|
+
// Pull the kit's deletions locally. Whitelist ONLY the kit's own removed files
|
|
28
|
+
// so they bypass the bulk-delete guard (the removal is explicit), while any
|
|
29
|
+
// unrelated mass deletion pending on either side is still guarded - a blanket
|
|
30
|
+
// `force` here would let that ride in silently.
|
|
31
|
+
const syncResult = await sync({
|
|
32
|
+
interactive: false,
|
|
33
|
+
deleteWhitelist: data.removed ?? [],
|
|
34
|
+
progress: opts.json ? undefined : createProgressReporter(),
|
|
35
|
+
});
|
|
30
36
|
if (opts.json) {
|
|
31
37
|
console.log(JSON.stringify({ ...data, synced: syncResult.applied }));
|
|
32
38
|
return;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { get, patch } from '../api.js';
|
|
3
|
+
import { brand, muted } from '../colors.js';
|
|
4
|
+
import { run } from '../helpers/index.js';
|
|
5
|
+
/** Parse a CLI flag value as a positive whole number, or throw a friendly error.
|
|
6
|
+
* The server is the source of truth for the plan-cap bound; this just rejects
|
|
7
|
+
* the obviously-invalid input locally before spending a round trip. */
|
|
8
|
+
function parsePositiveInt(raw, flag) {
|
|
9
|
+
if (!/^\d+$/.test(raw.trim())) {
|
|
10
|
+
throw new Error(`${flag} must be a positive whole number.`);
|
|
11
|
+
}
|
|
12
|
+
const n = parseInt(raw, 10);
|
|
13
|
+
if (n < 1)
|
|
14
|
+
throw new Error(`${flag} must be a positive whole number.`);
|
|
15
|
+
return n;
|
|
16
|
+
}
|
|
17
|
+
function printRetention(d, updated) {
|
|
18
|
+
console.log(updated ? 'Version retention updated:' : 'Version retention:');
|
|
19
|
+
console.log(` ${brand(`${d.days} days`)} / ${brand(`${d.count} copies`)} ${muted('(whichever prunes first)')}`);
|
|
20
|
+
const daysNote = d.customDays ? 'custom' : 'plan default';
|
|
21
|
+
const countNote = d.customCount ? 'custom' : 'plan default';
|
|
22
|
+
console.log(` ${muted(`days: ${daysNote}, copies: ${countNote}`)}`);
|
|
23
|
+
console.log(muted(`Plan allows up to ${d.maxDays} days / ${d.maxCount} copies.`));
|
|
24
|
+
}
|
|
25
|
+
export const storageCommand = new Command('storage')
|
|
26
|
+
.description('Manage file storage settings');
|
|
27
|
+
storageCommand
|
|
28
|
+
.command('retention')
|
|
29
|
+
.description('View or adjust your file version-retention policy')
|
|
30
|
+
.option('--days <n>', 'Keep versions for at most N days (1..plan cap)')
|
|
31
|
+
.option('--count <n>', 'Keep at most N copies per file (1..plan cap)')
|
|
32
|
+
.option('--reset', 'Reset both days and copies to the plan default')
|
|
33
|
+
.option('--json', 'Output as JSON')
|
|
34
|
+
.action((opts) => run('Retention', async () => {
|
|
35
|
+
const hasSet = opts.days !== undefined || opts.count !== undefined;
|
|
36
|
+
if (opts.reset && hasSet) {
|
|
37
|
+
throw new Error('Use either --reset or --days/--count, not both.');
|
|
38
|
+
}
|
|
39
|
+
let data;
|
|
40
|
+
if (opts.reset) {
|
|
41
|
+
// null resets a field to the plan default; send both.
|
|
42
|
+
const res = await patch('/users/me/retention', { days: null, count: null });
|
|
43
|
+
data = res.data;
|
|
44
|
+
}
|
|
45
|
+
else if (hasSet) {
|
|
46
|
+
const body = {};
|
|
47
|
+
if (opts.days !== undefined)
|
|
48
|
+
body.days = parsePositiveInt(opts.days, '--days');
|
|
49
|
+
if (opts.count !== undefined)
|
|
50
|
+
body.count = parsePositiveInt(opts.count, '--count');
|
|
51
|
+
const res = await patch('/users/me/retention', body);
|
|
52
|
+
data = res.data;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const res = await get('/users/me/retention');
|
|
56
|
+
data = res.data;
|
|
57
|
+
}
|
|
58
|
+
if (opts.json) {
|
|
59
|
+
console.log(JSON.stringify(data));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
printRetention(data, opts.reset === true || hasSet);
|
|
63
|
+
}));
|
|
64
|
+
//# sourceMappingURL=storage.js.map
|
|
@@ -12,7 +12,7 @@ import { Command } from 'commander';
|
|
|
12
12
|
import { existsSync, rmSync, unlinkSync, readFileSync, writeFileSync } from 'fs';
|
|
13
13
|
import { homedir } from 'os';
|
|
14
14
|
import { join, resolve } from 'path';
|
|
15
|
-
import {
|
|
15
|
+
import { spawnSyncCommand } from '../platform.js';
|
|
16
16
|
import { post } from '../api.js';
|
|
17
17
|
import { getAuth } from '../auth.js';
|
|
18
18
|
import { confirm, getAutoConfirm } from '../utils.js';
|
|
@@ -92,7 +92,7 @@ function removeServiceUnit() {
|
|
|
92
92
|
// is fine if the service was never installed.
|
|
93
93
|
let allOk = true;
|
|
94
94
|
for (const argv of plan.disableCmds) {
|
|
95
|
-
const r =
|
|
95
|
+
const r = spawnSyncCommand(argv[0], argv.slice(1), { stdio: 'ignore' });
|
|
96
96
|
if (r.status !== 0)
|
|
97
97
|
allOk = false;
|
|
98
98
|
}
|
package/dist/commands/upload.js
CHANGED
|
@@ -32,7 +32,6 @@ export const uploadCommand = new Command('upload')
|
|
|
32
32
|
.argument('<src>', 'Local source file or directory')
|
|
33
33
|
.argument('[dest]', 'Destination path in the project (defaults to /)')
|
|
34
34
|
.option('-r, --recursive', 'Upload a directory recursively')
|
|
35
|
-
.option('--overwrite', 'Force a new version even if the file is byte-identical to the current version')
|
|
36
35
|
.option('--mime <type>', 'Override the content-type (default: detect from extension)')
|
|
37
36
|
.option('--concurrency <n>', `Parallel files (default ${UPLOAD_CONCURRENCY})`)
|
|
38
37
|
.option('--dry-run', 'Print what would be uploaded; do not call the network')
|
|
@@ -85,7 +84,7 @@ export const uploadCommand = new Command('upload')
|
|
|
85
84
|
return;
|
|
86
85
|
}
|
|
87
86
|
const concurrency = Math.max(1, parseInt(opts.concurrency ?? String(UPLOAD_CONCURRENCY), 10));
|
|
88
|
-
const uploadOpts = { mime: opts.mime
|
|
87
|
+
const uploadOpts = { mime: opts.mime };
|
|
89
88
|
let cursor = 0;
|
|
90
89
|
let uploaded = 0, skipped = 0, resumed = 0, failed = 0;
|
|
91
90
|
const workers = [];
|