gipity 1.1.3 → 1.1.5
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 +1 -1
- package/dist/banner.js +3 -5
- package/dist/catalog.js +1 -3
- package/dist/commands/add.js +1 -1
- package/dist/commands/build.js +111 -40
- package/dist/commands/chat.js +1 -1
- package/dist/commands/deploy.js +7 -3
- package/dist/commands/file.js +18 -0
- package/dist/commands/fn.js +6 -6
- package/dist/commands/generate.js +80 -16
- package/dist/commands/gmail.js +1 -1
- package/dist/commands/job.js +132 -10
- package/dist/commands/load.js +2 -2
- package/dist/commands/login.js +7 -2
- package/dist/commands/page-eval.js +82 -13
- package/dist/commands/page-fetch.js +14 -10
- package/dist/commands/page-inspect.js +28 -3
- package/dist/commands/page-screenshot.js +36 -7
- package/dist/commands/page-test.js +1 -1
- package/dist/commands/records.js +14 -1
- package/dist/commands/remove.js +1 -1
- package/dist/commands/sandbox.js +182 -10
- package/dist/commands/save.js +1 -1
- package/dist/commands/secrets.js +1 -1
- package/dist/commands/service.js +7 -2
- package/dist/commands/setup.js +4 -6
- package/dist/commands/status.js +41 -12
- package/dist/commands/storage.js +2 -2
- package/dist/commands/sync.js +15 -0
- package/dist/commands/test.js +33 -1
- package/dist/commands/update.js +4 -0
- package/dist/commands/upload.js +69 -121
- package/dist/commands/workflow.js +1 -1
- package/dist/helpers/body.js +117 -0
- package/dist/helpers/duration.js +34 -0
- package/dist/helpers/index.js +2 -0
- package/dist/index.js +1173 -620
- package/dist/knowledge.js +7 -7
- package/dist/login-flow.js +44 -2
- package/dist/relay/onboarding.js +26 -44
- package/dist/sync.js +6 -6
- package/dist/updater/bootstrap.js +27 -16
- package/dist/updater/check.js +86 -10
- package/dist/updater/install.js +82 -0
- package/dist/updater/shim.js +90 -27
- package/dist/utils.js +60 -3
- package/package.json +2 -2
package/dist/adopt-cwd.js
CHANGED
|
@@ -167,7 +167,7 @@ export function formatCwdLabel(cwd) {
|
|
|
167
167
|
const tail = norm.slice(home.length + 1).split(sep);
|
|
168
168
|
if (tail.length <= 2)
|
|
169
169
|
return '~/' + tail.join('/');
|
|
170
|
-
return '
|
|
170
|
+
return '~/.../' + tail.slice(-2).join('/');
|
|
171
171
|
}
|
|
172
172
|
// Outside home: show parent/this for non-root paths.
|
|
173
173
|
const parts = norm.split(sep).filter(Boolean);
|
package/dist/banner.js
CHANGED
|
@@ -115,15 +115,13 @@ function padR(s, width) {
|
|
|
115
115
|
const gap = width - visLen(s);
|
|
116
116
|
return gap > 0 ? s + ' '.repeat(gap) : s;
|
|
117
117
|
}
|
|
118
|
-
/** Truncate from the left with a leading
|
|
118
|
+
/** Truncate from the left with a leading "..." if longer than maxW (plain text). */
|
|
119
119
|
function leadingEllipsify(s, maxW) {
|
|
120
120
|
if (s.length <= maxW)
|
|
121
121
|
return s;
|
|
122
|
-
if (maxW <=
|
|
122
|
+
if (maxW <= 4)
|
|
123
123
|
return s.slice(-maxW);
|
|
124
|
-
|
|
125
|
-
return '…'.padStart(maxW);
|
|
126
|
-
return '…' + s.slice(-(maxW - 1));
|
|
124
|
+
return '...' + s.slice(-(maxW - 3));
|
|
127
125
|
}
|
|
128
126
|
function center(s, width) {
|
|
129
127
|
const gap = width - visLen(s);
|
package/dist/catalog.js
CHANGED
|
@@ -36,11 +36,9 @@ export const KITS = [
|
|
|
36
36
|
{ key: 'chatbot', hint: 'drop-in chatbot - persona, guardrails, streaming responses' },
|
|
37
37
|
{ key: 'audio-align', hint: 'audio + lyrics -> word-level timing JSON (GPU job)' },
|
|
38
38
|
{ key: 'i18n', hint: 'multi-language web apps - language picker, RTL, translations' },
|
|
39
|
-
{ key: 'records', hint: 'registry-driven data plane - generic CRUD, validation, search, audit spine' },
|
|
40
|
-
{ key: 'views', hint: 'registry-driven UI over records - table, forms, kanban' },
|
|
41
|
-
{ key: 'agent-api', hint: 'named API keys for agent/script writes through the records kit' },
|
|
42
39
|
{ key: 'contacts', hint: 'multi-source contact layer - LinkedIn/Gmail import, dedupe with provenance' },
|
|
43
40
|
{ key: 'stripe', hint: 'charge your users - Stripe checkout, subscriptions, brokered webhooks' },
|
|
44
41
|
{ key: 'notify', hint: 'web push notifications - platform-owned keys, works on iOS home screen' },
|
|
42
|
+
{ key: 'servicenow', hint: 'ServiceNow tables as a data source - OAuth pull/write-back/real-time sync' },
|
|
45
43
|
];
|
|
46
44
|
//# sourceMappingURL=catalog.js.map
|
package/dist/commands/add.js
CHANGED
|
@@ -207,7 +207,7 @@ export const addCommand = new Command('add')
|
|
|
207
207
|
const doAdd = () => post(`/projects/${config.projectGuid}/add`, body);
|
|
208
208
|
const res = opts.json
|
|
209
209
|
? await doAdd()
|
|
210
|
-
: await withSpinner('Installing
|
|
210
|
+
: await withSpinner('Installing...', doAdd, { done: null });
|
|
211
211
|
// Pull the created/installed files down to local.
|
|
212
212
|
const syncResult = await sync({ interactive: false, progress: opts.json ? undefined : createProgressReporter() });
|
|
213
213
|
const data = res.data;
|
package/dist/commands/build.js
CHANGED
|
@@ -50,7 +50,7 @@ const __clPkg = readOwnPkg(__clDir);
|
|
|
50
50
|
* incomplete pull, so we can tell the user plainly that nothing was deleted. */
|
|
51
51
|
function reportSyncResult(result) {
|
|
52
52
|
if (result.aborted) {
|
|
53
|
-
console.log(` ${warning('Merge cancelled
|
|
53
|
+
console.log(` ${warning('Merge cancelled - this folder was NOT synced with the project.')}`);
|
|
54
54
|
console.log(` ${muted('For a clean copy, quit and open the project in an empty folder. To merge anyway, run `gipity sync`.')}`);
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
@@ -58,11 +58,11 @@ function reportSyncResult(result) {
|
|
|
58
58
|
console.log(` Synced ${result.applied} change${result.applied > 1 ? 's' : ''} with Gipity.`);
|
|
59
59
|
}
|
|
60
60
|
if (result.errors.length) {
|
|
61
|
-
console.log(` ${warning(`Sync finished with ${result.errors.length} problem${result.errors.length === 1 ? '' : 's'}
|
|
61
|
+
console.log(` ${warning(`Sync finished with ${result.errors.length} problem${result.errors.length === 1 ? '' : 's'} - your local copy may be incomplete:`)}`);
|
|
62
62
|
for (const e of result.errors.slice(0, 8))
|
|
63
63
|
console.log(` - ${e}`);
|
|
64
64
|
if (result.errors.length > 8)
|
|
65
|
-
console.log(`
|
|
65
|
+
console.log(` ...and ${result.errors.length - 8} more.`);
|
|
66
66
|
console.log(` ${muted('Nothing was deleted. Re-run `gipity sync` to finish the pull.')}`);
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -133,7 +133,7 @@ function localFsFallback(dir) {
|
|
|
133
133
|
};
|
|
134
134
|
walk(dir, 0);
|
|
135
135
|
const topLevel = topLevelEntries.length
|
|
136
|
-
? topLevelEntries.slice(0, 20).join(', ') + (topLevelEntries.length > 20 ? ',
|
|
136
|
+
? topLevelEntries.slice(0, 20).join(', ') + (topLevelEntries.length > 20 ? ', ...' : '')
|
|
137
137
|
: '(empty directory)';
|
|
138
138
|
return { fileCount, folderCount, totalBytes, topLevel };
|
|
139
139
|
}
|
|
@@ -502,6 +502,8 @@ async function runLaunch(cmdName, cmdCfg, opts) {
|
|
|
502
502
|
console.log(` ${warning('Could not sync files (will retry on next prompt):')} ${err.message}`);
|
|
503
503
|
}
|
|
504
504
|
setupProjectTools();
|
|
505
|
+
if (!nonInteractive)
|
|
506
|
+
console.log(` ${muted('Installed Gipity plugin, skills, and hooks.')}`);
|
|
505
507
|
if (nonInteractive) {
|
|
506
508
|
// Headless: the -p message is wrapped later (Step 3). Just record
|
|
507
509
|
// whether to use the new-project framing for that wrap.
|
|
@@ -673,6 +675,8 @@ async function runLaunch(cmdName, cmdCfg, opts) {
|
|
|
673
675
|
// Claude directly what they want to build or do.
|
|
674
676
|
initialPrompt = '';
|
|
675
677
|
setupProjectTools();
|
|
678
|
+
if (!nonInteractive)
|
|
679
|
+
console.log(` ${muted('Installed Gipity plugin, skills, and hooks.')}`);
|
|
676
680
|
console.log(` ${success(`Project "${project.name}" ready.`)}\n`);
|
|
677
681
|
}
|
|
678
682
|
// ── Step 3: Pick the coding agent + model ─────────────────────────
|
|
@@ -812,9 +816,8 @@ async function runLaunch(cmdName, cmdCfg, opts) {
|
|
|
812
816
|
}
|
|
813
817
|
if (!nonInteractive) {
|
|
814
818
|
console.log(` ${bold('Launching Claude Code, powered by Gipity.')}`);
|
|
815
|
-
console.log(` ${muted("Just tell Claude what you'd like to build or do - everything Claude can do,")}`);
|
|
816
|
-
console.log(` ${muted('plus hosting, databases, and live deploys on Gipity.')}`);
|
|
817
819
|
if (convGuidForHooks) {
|
|
820
|
+
console.log('');
|
|
818
821
|
console.log(` ${muted(`This session is saved to Gipity - watch it live at ${brand('prompt.gipity.ai')}. (--no-capture on init to disable.)`)}`);
|
|
819
822
|
}
|
|
820
823
|
console.log('');
|
|
@@ -960,12 +963,11 @@ async function pickAgent(lastUsed) {
|
|
|
960
963
|
const defaultIdx = AGENT_ADAPTERS.findIndex(a => a.key === defaultKey) + 1;
|
|
961
964
|
console.log(` ${bold('Which coding agent?')}\n`);
|
|
962
965
|
AGENT_ADAPTERS.forEach((a, i) => {
|
|
963
|
-
|
|
966
|
+
// Keep the list clean: no install-state notes (picking an uninstalled
|
|
967
|
+
// agent installs it, or fails with the install hint at that point).
|
|
964
968
|
const notes = [];
|
|
965
969
|
if (a.key === lastUsed)
|
|
966
970
|
notes.push('last used');
|
|
967
|
-
if (!installed)
|
|
968
|
-
notes.push(a.ensureInstalled ? "not installed - we'll install it" : 'not installed');
|
|
969
971
|
if (a.key === 'codex' && !a.hooksSupportedOnPlatform(process.platform)) {
|
|
970
972
|
notes.push('session recording unavailable on Windows');
|
|
971
973
|
}
|
|
@@ -1101,9 +1103,8 @@ async function launchNonClaudeAgent(adapter, ctx) {
|
|
|
1101
1103
|
else {
|
|
1102
1104
|
agentArgs = [...adapter.buildInteractiveArgs({ resume, model }), ...extras];
|
|
1103
1105
|
console.log(` ${bold(`Launching ${adapter.displayName}, powered by Gipity.`)}`);
|
|
1104
|
-
console.log(` ${muted(`Just tell ${adapter.displayName} what you'd like to build or do - plus hosting,`)}`);
|
|
1105
|
-
console.log(` ${muted('databases, and live deploys on Gipity.')}`);
|
|
1106
1106
|
if (convGuid) {
|
|
1107
|
+
console.log('');
|
|
1107
1108
|
console.log(` ${muted(`This session is saved to Gipity - watch it live at ${brand('prompt.gipity.ai')}. (--no-capture on init to disable.)`)}`);
|
|
1108
1109
|
}
|
|
1109
1110
|
if (adapter.oneTimeSetupNote) {
|
|
@@ -1171,37 +1172,47 @@ async function pickOrCreateProject(projects, existingSlugs) {
|
|
|
1171
1172
|
const showAdopt = canAdoptCwd(cwd);
|
|
1172
1173
|
// Reserve slot 1 for "create new", slot 2 for "use this dir" when shown.
|
|
1173
1174
|
const reserved = showAdopt ? 2 : 1;
|
|
1174
|
-
// Pickable single-keypress range is 1-9
|
|
1175
|
-
//
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1175
|
+
// Pickable single-keypress range is 1-9. When every project fits, list
|
|
1176
|
+
// them all; otherwise the actions grow a "Search projects" slot right
|
|
1177
|
+
// after the reserved ones and "Show more" takes slot 9 - accounts with
|
|
1178
|
+
// hundreds of projects can't scroll a dump. Action rows render in a
|
|
1179
|
+
// different color than project rows so the two groups read apart.
|
|
1180
|
+
const fitsAll = filtered.length <= 9 - reserved;
|
|
1181
|
+
const hasMore = !fitsAll;
|
|
1182
|
+
const searchSlot = hasMore ? reserved + 1 : 0;
|
|
1183
|
+
const projectBase = reserved + (hasMore ? 1 : 0); // slots projectBase+1.. are projects
|
|
1184
|
+
const recent = fitsAll ? filtered : filtered.slice(0, 9 - reserved - 2);
|
|
1185
|
+
const moreSlot = hasMore ? projectBase + recent.length + 1 : 0; // always 9 when shown
|
|
1179
1186
|
// Loop so that a refused/declined adopt-cwd re-shows the picker rather
|
|
1180
1187
|
// than dropping the user into a shell.
|
|
1181
1188
|
while (true) {
|
|
1182
1189
|
const newProjectLabel = formatNewProjectLabel(existingSlugs);
|
|
1183
1190
|
console.log(` ${bold('Choose project to open:')}\n`);
|
|
1184
|
-
console.log(` ${bold('1.')} Create new project ${muted(`(${newProjectLabel})`)}`);
|
|
1191
|
+
console.log(` ${bold('1.')} ${info('Create new project')} ${muted(`(${newProjectLabel})`)}`);
|
|
1185
1192
|
if (showAdopt) {
|
|
1186
|
-
console.log(` ${bold('2.')} Use this directory ${muted(`(${formatCwdLabel(cwd)})`)}`);
|
|
1193
|
+
console.log(` ${bold('2.')} ${info('Use this directory')} ${muted(`(${formatCwdLabel(cwd)})`)}`);
|
|
1187
1194
|
}
|
|
1188
|
-
recent.forEach((p, i) => console.log(` ${bold(`${i + reserved + 1}.`)} ${p.name} ${muted(`(${p.slug})`)}`));
|
|
1189
1195
|
if (hasMore) {
|
|
1190
|
-
console.log(` ${bold(`${
|
|
1196
|
+
console.log(` ${bold(`${searchSlot}.`)} ${info('Search projects')} ${muted(`(${filtered.length} total)`)}`);
|
|
1197
|
+
}
|
|
1198
|
+
recent.forEach((p, i) => console.log(` ${bold(`${projectBase + i + 1}.`)} ${p.name} ${muted(`(${p.slug})`)}`));
|
|
1199
|
+
if (hasMore) {
|
|
1200
|
+
console.log(` ${bold(`${moreSlot}.`)} ${info('Show more')}`);
|
|
1191
1201
|
}
|
|
1192
1202
|
console.log('');
|
|
1193
|
-
const maxOption = recent.length +
|
|
1203
|
+
const maxOption = projectBase + recent.length + (hasMore ? 1 : 0);
|
|
1194
1204
|
const idx = await pickOne('Choose', maxOption, 1);
|
|
1195
|
-
// Recent project (slots
|
|
1196
|
-
if (idx >
|
|
1197
|
-
return { kind: 'pick', project: recent[idx -
|
|
1205
|
+
// Recent project (slots projectBase+1 .. projectBase+recent.length).
|
|
1206
|
+
if (idx > projectBase && idx <= projectBase + recent.length) {
|
|
1207
|
+
return { kind: 'pick', project: recent[idx - projectBase - 1] };
|
|
1198
1208
|
}
|
|
1199
|
-
//
|
|
1200
|
-
if (hasMore && idx ===
|
|
1201
|
-
const picked = await
|
|
1209
|
+
// Search (right after the reserved slots) / Show more (slot 9).
|
|
1210
|
+
if (hasMore && (idx === searchSlot || idx === moreSlot)) {
|
|
1211
|
+
const picked = await browseProjects(filtered, { search: idx === searchSlot });
|
|
1202
1212
|
if (picked)
|
|
1203
1213
|
return { kind: 'pick', project: picked };
|
|
1204
|
-
|
|
1214
|
+
console.log('');
|
|
1215
|
+
continue; // user backed out; re-show top picker
|
|
1205
1216
|
}
|
|
1206
1217
|
// Slot 2 = "Use this directory" (only when shown).
|
|
1207
1218
|
if (showAdopt && idx === 2) {
|
|
@@ -1217,19 +1228,79 @@ async function pickOrCreateProject(projects, existingSlugs) {
|
|
|
1217
1228
|
return { kind: 'create-new', project };
|
|
1218
1229
|
}
|
|
1219
1230
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1231
|
+
const PROJECT_PAGE_SIZE = 10;
|
|
1232
|
+
/** Case-insensitive substring match on project name or slug. An empty or
|
|
1233
|
+
* whitespace-only query matches everything. Exported for tests. */
|
|
1234
|
+
export function searchProjects(all, query) {
|
|
1235
|
+
const q = query.trim().toLowerCase();
|
|
1236
|
+
if (!q)
|
|
1237
|
+
return all;
|
|
1238
|
+
return all.filter(p => p.name.toLowerCase().includes(q) || p.slug.toLowerCase().includes(q));
|
|
1239
|
+
}
|
|
1240
|
+
/** Paged, searchable project list - the shared renderer behind both the
|
|
1241
|
+
* "Search projects" and "Show more" picker slots. Shows PROJECT_PAGE_SIZE
|
|
1242
|
+
* rows at a time with numbering that stays stable across pages, then reads
|
|
1243
|
+
* a full line (not single-keypress - numbers can exceed 9):
|
|
1244
|
+
* Enter → next page · <number> → open that project
|
|
1245
|
+
* s → new search (always over the FULL list, never the current subset)
|
|
1246
|
+
* q → back to the main picker (returns null).
|
|
1247
|
+
* `opts.search` starts by prompting for a query instead of listing all.
|
|
1248
|
+
* Exported for tests. */
|
|
1249
|
+
export async function browseProjects(all, opts = {}) {
|
|
1250
|
+
let items = all;
|
|
1251
|
+
let heading = 'All projects';
|
|
1252
|
+
// Prompt for a query and swap the working list to the matches. Returns
|
|
1253
|
+
// false (list unchanged) on empty input or zero matches.
|
|
1254
|
+
const runSearch = async () => {
|
|
1255
|
+
const q = await prompt(` ${bold('Search projects:')} `);
|
|
1256
|
+
if (!q)
|
|
1257
|
+
return false;
|
|
1258
|
+
const matches = searchProjects(all, q);
|
|
1259
|
+
if (matches.length === 0) {
|
|
1260
|
+
console.log(` ${muted(`No projects match "${q}".`)}`);
|
|
1261
|
+
return false;
|
|
1262
|
+
}
|
|
1263
|
+
items = matches;
|
|
1264
|
+
heading = `Projects matching "${q}"`;
|
|
1265
|
+
return true;
|
|
1266
|
+
};
|
|
1227
1267
|
console.log('');
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1268
|
+
if (opts.search && !(await runSearch()))
|
|
1269
|
+
return null;
|
|
1270
|
+
let shown = 0;
|
|
1271
|
+
while (true) {
|
|
1272
|
+
if (shown < items.length) {
|
|
1273
|
+
const page = items.slice(shown, shown + PROJECT_PAGE_SIZE);
|
|
1274
|
+
if (shown === 0)
|
|
1275
|
+
console.log(` ${bold(`${heading}`)} ${muted(`(${items.length})`)}\n`);
|
|
1276
|
+
page.forEach((p, i) => console.log(` ${bold(`${String(shown + i + 1).padStart(3)}.`)} ${p.name} ${muted(`(${p.slug})`)}`));
|
|
1277
|
+
shown += page.length;
|
|
1278
|
+
console.log('');
|
|
1279
|
+
}
|
|
1280
|
+
const more = shown < items.length;
|
|
1281
|
+
const hints = [
|
|
1282
|
+
more ? `Enter = next ${Math.min(PROJECT_PAGE_SIZE, items.length - shown)}` : null,
|
|
1283
|
+
'number = open',
|
|
1284
|
+
's = search',
|
|
1285
|
+
'q = back',
|
|
1286
|
+
].filter(Boolean).join(' · ');
|
|
1287
|
+
const answer = (await prompt(` ${bold('Choose')} ${muted(`(${hints})`)}: `)).toLowerCase();
|
|
1288
|
+
if (answer === '' && more)
|
|
1289
|
+
continue; // Enter → next page
|
|
1290
|
+
if (answer === '' || answer === 'q')
|
|
1291
|
+
return null;
|
|
1292
|
+
if (answer === 's') {
|
|
1293
|
+
if (await runSearch()) {
|
|
1294
|
+
shown = 0;
|
|
1295
|
+
console.log('');
|
|
1296
|
+
}
|
|
1297
|
+
continue;
|
|
1298
|
+
}
|
|
1299
|
+
const n = parseInt(answer, 10);
|
|
1300
|
+
if (Number.isInteger(n) && n >= 1 && n <= shown)
|
|
1301
|
+
return items[n - 1];
|
|
1302
|
+
console.log(` ${muted('Type one of the numbers above, Enter for more, s to search, or q to go back.')}`);
|
|
1303
|
+
}
|
|
1233
1304
|
}
|
|
1234
1305
|
/** Show "(~/GipityProjects/project-NNN)" - the exact dir option 1 will
|
|
1235
1306
|
* create, so the user can see where their new project will land. */
|
package/dist/commands/chat.js
CHANGED
|
@@ -25,7 +25,7 @@ export const chatCommand = new Command('chat')
|
|
|
25
25
|
const doChat = () => post(endpoint, body);
|
|
26
26
|
const res = opts.json
|
|
27
27
|
? await doChat()
|
|
28
|
-
: await withSpinner('Thinking
|
|
28
|
+
: await withSpinner('Thinking...', doChat, { done: null });
|
|
29
29
|
// Save conversation guid for continuity. Skipped in one-off mode: the
|
|
30
30
|
// config was resolved from the server's Home project and there is no
|
|
31
31
|
// local `.gipity.json` to update - persisting here would create one in
|
package/dist/commands/deploy.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { post } from '../api.js';
|
|
3
|
-
import { requireConfig } from '../config.js';
|
|
3
|
+
import { requireConfig, resolveApiBase } 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';
|
|
@@ -57,7 +57,7 @@ export const deployCommand = new Command('deploy')
|
|
|
57
57
|
});
|
|
58
58
|
const res = opts.json
|
|
59
59
|
? await doDeploy()
|
|
60
|
-
: await withSpinner(`Deploying to ${target}
|
|
60
|
+
: await withSpinner(`Deploying to ${target}...`, doDeploy, { done: null });
|
|
61
61
|
const d = res.data;
|
|
62
62
|
// Deploy + verify in one command: after a successful deploy, run the
|
|
63
63
|
// same inspect pipeline `page inspect` uses against the live URL. An
|
|
@@ -150,7 +150,11 @@ export const deployCommand = new Command('deploy')
|
|
|
150
150
|
// for an API-only app the endpoint address IS the deliverable, so name
|
|
151
151
|
// it instead of leaving it to be inferred from a template comment.
|
|
152
152
|
if (d.phases?.some(p => p.name === 'functions')) {
|
|
153
|
-
|
|
153
|
+
// Print the base the CLI actually calls (resolveApiBase honors the
|
|
154
|
+
// --api-base flag and GIPITY_API_BASE env), not the raw stored
|
|
155
|
+
// config.apiBase - otherwise on any override we'd advertise a host we
|
|
156
|
+
// didn't deploy to and send the caller probing the wrong instance.
|
|
157
|
+
console.log(`${muted('Functions:')} POST ${brand(`${resolveApiBase()}/api/${config.projectGuid}/fn/<name>`)}`);
|
|
154
158
|
console.log(muted(' (same address on dev and prod; names: gipity fn list; verify the public path: gipity fn call <name> --anon)'));
|
|
155
159
|
}
|
|
156
160
|
await inspectAfter();
|
package/dist/commands/file.js
CHANGED
|
@@ -34,6 +34,24 @@ fileCommand
|
|
|
34
34
|
process.stdout.write(res.data.content);
|
|
35
35
|
}
|
|
36
36
|
}));
|
|
37
|
+
fileCommand
|
|
38
|
+
.command('url <path>')
|
|
39
|
+
.description('Durable public URL for a project file - no deploy needed. Reachable by browsers, GPU jobs, and external services the moment the file syncs; ideal for throwaway test fixtures (delete the file to revoke the link).')
|
|
40
|
+
.option('--download', 'URL forces a download (Content-Disposition: attachment)')
|
|
41
|
+
.option('--json', 'Output as JSON')
|
|
42
|
+
.action((path, opts) => run('Url', async () => {
|
|
43
|
+
const { config } = await resolveProjectContext();
|
|
44
|
+
const qs = new URLSearchParams({ path });
|
|
45
|
+
if (opts.download)
|
|
46
|
+
qs.set('download', '1');
|
|
47
|
+
const res = await get(`/projects/${config.projectGuid}/files/url?${qs.toString()}`);
|
|
48
|
+
if (opts.json) {
|
|
49
|
+
console.log(JSON.stringify(res.data));
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.log(res.data.url);
|
|
53
|
+
}
|
|
54
|
+
}));
|
|
37
55
|
fileCommand
|
|
38
56
|
.command('tree [path]')
|
|
39
57
|
.description('Show the file tree')
|
package/dist/commands/fn.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { get, post, del, publicPost } from '../api.js';
|
|
3
3
|
import { getAuth } from '../auth.js';
|
|
4
|
-
import { requireConfig } from '../config.js';
|
|
4
|
+
import { requireConfig, resolveApiBase } from '../config.js';
|
|
5
5
|
import { error as clrError, bold, muted, success, warning } from '../colors.js';
|
|
6
|
-
import { run, printList, emitField } from '../helpers/index.js';
|
|
6
|
+
import { run, printList, emitField, resolveBody } from '../helpers/index.js';
|
|
7
7
|
import { confirm } from '../utils.js';
|
|
8
8
|
export const fnCommand = new Command('fn')
|
|
9
9
|
.description('Manage functions');
|
|
@@ -21,7 +21,7 @@ fnCommand
|
|
|
21
21
|
// The callable address is the deliverable for API-only apps - name it here
|
|
22
22
|
// rather than leaving it to be inferred from a template comment.
|
|
23
23
|
if (!opts.json && res.data.length > 0) {
|
|
24
|
-
console.log(muted(`Endpoint: POST ${
|
|
24
|
+
console.log(muted(`Endpoint: POST ${resolveApiBase()}/api/${config.projectGuid}/fn/<name> (same on dev and prod; public path: gipity fn call <name> --anon)`));
|
|
25
25
|
}
|
|
26
26
|
}));
|
|
27
27
|
fnCommand
|
|
@@ -73,14 +73,14 @@ async function callAnon(projectGuid, name, body) {
|
|
|
73
73
|
fnCommand
|
|
74
74
|
.command('call <name> [body]')
|
|
75
75
|
.description('Call a function')
|
|
76
|
-
.option('--data <json>', 'JSON request body')
|
|
76
|
+
.option('-d, --data <json>', 'JSON request body: inline JSON, @file to read a file, or @- / - for stdin')
|
|
77
|
+
.option('--file <field=@path>', 'Attach a file as { data, media_type } under <field> (the vision/media service shape), repeatable, e.g. --file image=@receipt.png', (v, acc) => (acc || []).concat(v))
|
|
77
78
|
.option('--anon', 'Call as an anonymous visitor (the public path a signed-out user hits) instead of as your signed-in account')
|
|
78
79
|
.option('--field <path>', 'Print only this field of the result (dot path, e.g. items.0.short_guid)')
|
|
79
80
|
.option('--json', 'Output as JSON')
|
|
80
81
|
.action((name, bodyArg, opts) => run('Call', async () => {
|
|
81
82
|
const config = requireConfig();
|
|
82
|
-
const
|
|
83
|
-
const body = JSON.parse(raw);
|
|
83
|
+
const body = resolveBody(bodyArg || opts.data, opts.file);
|
|
84
84
|
const path = `/api/${config.projectGuid}/fn/${encodeURIComponent(name)}`;
|
|
85
85
|
// Name the calling identity (stderr, so stdout stays parseable). Without
|
|
86
86
|
// this, a "test the anonymous path" call silently runs authenticated as
|
|
@@ -2,11 +2,12 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { post } from '../api.js';
|
|
3
3
|
import { resolveProjectContext, getConfigPath } from '../config.js';
|
|
4
4
|
import { pushFile } from '../sync.js';
|
|
5
|
-
import { mkdirSync, writeFileSync } from 'fs';
|
|
6
|
-
import { resolve as resolvePath, dirname, relative, isAbsolute } from 'path';
|
|
5
|
+
import { mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
6
|
+
import { resolve as resolvePath, dirname, relative, isAbsolute, basename } from 'path';
|
|
7
7
|
import { error as clrError, success, muted } from '../colors.js';
|
|
8
8
|
import { printCommandError } from '../helpers/command.js';
|
|
9
9
|
import { withSpinner } from '../progress.js';
|
|
10
|
+
import { guessMime } from '../upload.js';
|
|
10
11
|
import { IMAGE_MODELS_DOC, IMAGE_GEMINI_ASPECT_RATIOS, IMAGE_GEMINI_SIZES, VIDEO_MODELS_DOC, TTS_PROVIDER_DESCRIPTIONS } from '../provider-docs.js';
|
|
11
12
|
/** Download a URL and save to a local file, then push it up to the project so
|
|
12
13
|
* the cloud (and anything that mirrors it) immediately matches local disk.
|
|
@@ -20,12 +21,25 @@ import { IMAGE_MODELS_DOC, IMAGE_GEMINI_ASPECT_RATIOS, IMAGE_GEMINI_SIZES, VIDEO
|
|
|
20
21
|
* that gap so the "sandbox auto-mirrors the project" contract holds right after
|
|
21
22
|
* generation. Best-effort: the local save already succeeded, so a push failure
|
|
22
23
|
* only warns and points at `gipity sync`. */
|
|
23
|
-
async function downloadFile(url, filename) {
|
|
24
|
+
async function downloadFile(url, filename, explicit) {
|
|
24
25
|
const res = await fetch(url);
|
|
25
26
|
if (!res.ok)
|
|
26
27
|
throw new Error(`Download failed: ${res.status}`);
|
|
27
28
|
const buffer = Buffer.from(await res.arrayBuffer());
|
|
28
|
-
|
|
29
|
+
// An explicit `-o` path is a name the caller has committed to and will chain
|
|
30
|
+
// the NEXT command against (`--input src.png`, `page eval --camera src.png`),
|
|
31
|
+
// so the saved path MUST equal what was asked - a predictable path is worth
|
|
32
|
+
// more than a tidy extension. The model's output format is non-deterministic
|
|
33
|
+
// (BFL's "png" request comes back as JPEG), so renaming to match the bytes
|
|
34
|
+
// moves the file out from under the caller's already-written second command:
|
|
35
|
+
// that is the "saved as .jpg, retry with the right path" wasted turn. Honour
|
|
36
|
+
// the request; every downstream consumer (browsers, image/video tools, the
|
|
37
|
+
// edit + camera services) sniffs the bytes, not the extension, so a jpg-in-a-
|
|
38
|
+
// .png plays and edits fine. For the auto-generated default there is no
|
|
39
|
+
// committed path, so there we DO name the file after its real bytes.
|
|
40
|
+
const outPath = explicit ? filename : correctExtension(filename, buffer);
|
|
41
|
+
if (explicit)
|
|
42
|
+
noteFormatMismatch(filename, buffer);
|
|
29
43
|
ensureOutputDir(outPath);
|
|
30
44
|
writeFileSync(outPath, buffer);
|
|
31
45
|
const savedPath = resolvePath(outPath);
|
|
@@ -85,6 +99,23 @@ function correctExtension(filename, buf) {
|
|
|
85
99
|
+ `saving as ${corrected} so the extension matches the actual bytes.`));
|
|
86
100
|
return corrected;
|
|
87
101
|
}
|
|
102
|
+
/** When the caller committed to an explicit `-o` path we save to it verbatim
|
|
103
|
+
* (see downloadFile), but the model's format may not match the extension. Say
|
|
104
|
+
* so, and say plainly that the file is at the requested path — so an agent that
|
|
105
|
+
* reads this note does NOT go change the path its already-queued next command
|
|
106
|
+
* points at (that "retry with .jpg" turn is the whole bug we're closing). */
|
|
107
|
+
function noteFormatMismatch(filename, buf) {
|
|
108
|
+
const actual = sniffExt(buf);
|
|
109
|
+
if (!actual)
|
|
110
|
+
return;
|
|
111
|
+
const dot = filename.lastIndexOf('.');
|
|
112
|
+
const asked = dot > 0 ? filename.slice(dot + 1).toLowerCase() : '';
|
|
113
|
+
if (!asked || canonicalExt(asked) === actual)
|
|
114
|
+
return;
|
|
115
|
+
console.error(muted(`Note: ${basename(filename)} holds ${formatName(actual)} bytes though its name ends .${asked} — `
|
|
116
|
+
+ `that's fine (browsers and image/video/edit tools sniff the bytes, not the name). `
|
|
117
|
+
+ `Saved at the exact path you requested; reference it as-is.`));
|
|
118
|
+
}
|
|
88
119
|
/** Create the parent directory of an -o path, so `-o src/audio/ding.mp3` works
|
|
89
120
|
* in a tree that has no `src/audio/` yet instead of dying on a raw ENOENT.
|
|
90
121
|
*
|
|
@@ -116,22 +147,52 @@ async function pushGenerated(savedPath) {
|
|
|
116
147
|
console.error(muted(`Note: couldn't sync to the cloud automatically (${err.message}). Run \`gipity sync\` before referencing this file in \`gipity sandbox run\`.`));
|
|
117
148
|
}
|
|
118
149
|
}
|
|
150
|
+
/** Read the `--input` image files an edit request is applied to, as base64 +
|
|
151
|
+
* mime type. The CLI reads the bytes itself so a photo never has to travel
|
|
152
|
+
* through the shell argv (base64 images blow past ARG_MAX — "Argument list too
|
|
153
|
+
* long" — the moment you try to inline them), which is exactly the trap an
|
|
154
|
+
* agent falls into hand-rolling an edit call with a python one-liner. */
|
|
155
|
+
function readInputImages(paths) {
|
|
156
|
+
return paths.map((p) => {
|
|
157
|
+
let buf;
|
|
158
|
+
try {
|
|
159
|
+
buf = readFileSync(p);
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
throw new Error(`can't read --input image ${p} (${e.code || e.message}).`);
|
|
163
|
+
}
|
|
164
|
+
const mime = guessMime(p);
|
|
165
|
+
if (!mime.startsWith('image/')) {
|
|
166
|
+
throw new Error(`--input ${p} is not an image (${mime}). Edit inputs must be image files.`);
|
|
167
|
+
}
|
|
168
|
+
return { data: buf.toString('base64'), mime_type: mime };
|
|
169
|
+
});
|
|
170
|
+
}
|
|
119
171
|
// ── IMAGE ──────────────────────────────────────────────────────────────
|
|
120
172
|
const imageCommand = new Command('image')
|
|
121
|
-
.description(`Generate an image from a text prompt
|
|
173
|
+
.description(`Generate an image from a text prompt, or EDIT existing images with --input.
|
|
122
174
|
|
|
123
175
|
Models: ${IMAGE_MODELS_DOC}
|
|
124
176
|
|
|
177
|
+
Editing (--input): pass one or more source images and the prompt becomes an edit
|
|
178
|
+
instruction applied to them - "make it night time", "add a hat", "remove the car
|
|
179
|
+
in the background", or compose several inputs together. Editing routes to Gemini
|
|
180
|
+
automatically. This is the first-party way to exercise the image-edit flow from
|
|
181
|
+
the CLI; the CLI reads the file bytes itself, so there is no base64/argv fiddling.
|
|
182
|
+
|
|
125
183
|
Gemini-specific options:
|
|
126
184
|
--aspect-ratio Control output shape: ${IMAGE_GEMINI_ASPECT_RATIOS}
|
|
127
185
|
--image-size Control resolution: ${IMAGE_GEMINI_SIZES} (default: 1K)
|
|
128
186
|
|
|
129
187
|
Examples:
|
|
130
188
|
gipity generate image "a cat wearing a top hat"
|
|
189
|
+
gipity generate image "make it night time" --input photo.jpg -o edited.png
|
|
190
|
+
gipity generate image "put the person in the first photo into the second scene" --input person.png --input scene.png
|
|
131
191
|
gipity generate image "landscape sunset" --provider gemini --aspect-ratio 16:9 --image-size 2K
|
|
132
192
|
gipity generate image "product photo" --provider openai --model gpt-image-2 --size 1536x1024 --quality high
|
|
133
193
|
gipity generate image "abstract art" --provider bfl --model flux-2-pro -o art.png`)
|
|
134
|
-
.argument('<prompt>', 'Text description of the image to
|
|
194
|
+
.argument('<prompt>', 'Text description of the image, or (with --input) the edit instruction to apply')
|
|
195
|
+
.option('--input <file>', 'Source image to edit/compose (repeatable). With --input the prompt is an edit instruction; routes to Gemini.', (v, acc) => (acc || []).concat(v))
|
|
135
196
|
.option('--provider <provider>', 'Image provider: openai, bfl, or gemini (default: bfl)')
|
|
136
197
|
.option('--model <model>', 'Model ID (see provider list above)')
|
|
137
198
|
.option('--size <size>', 'Dimensions as WxH, e.g. "1024x1024" (OpenAI/BFL)')
|
|
@@ -146,6 +207,7 @@ Examples:
|
|
|
146
207
|
const { config } = await resolveProjectContext();
|
|
147
208
|
if (opts.output)
|
|
148
209
|
ensureOutputDir(opts.output);
|
|
210
|
+
const inputImages = opts.input ? readInputImages(opts.input) : undefined;
|
|
149
211
|
const doGenerate = () => post(`/projects/${config.projectGuid}/generate/image`, {
|
|
150
212
|
prompt,
|
|
151
213
|
provider: opts.provider,
|
|
@@ -155,13 +217,15 @@ Examples:
|
|
|
155
217
|
aspect_ratio: opts.aspectRatio,
|
|
156
218
|
image_size: opts.imageSize,
|
|
157
219
|
seed: Number.isFinite(opts.seed) ? opts.seed : undefined,
|
|
220
|
+
input_images: inputImages,
|
|
158
221
|
});
|
|
222
|
+
const verb = inputImages ? 'Editing image...' : 'Generating image...';
|
|
159
223
|
const result = opts.json
|
|
160
224
|
? await doGenerate()
|
|
161
|
-
: await withSpinner(
|
|
225
|
+
: await withSpinner(verb, doGenerate, { done: null });
|
|
162
226
|
const ext = result.content_type.includes('png') ? 'png' : 'jpg';
|
|
163
227
|
const filename = opts.output || `generated.${ext}`;
|
|
164
|
-
const savedPath = await downloadFile(result.url, filename);
|
|
228
|
+
const savedPath = await downloadFile(result.url, filename, !!opts.output);
|
|
165
229
|
if (opts.json) {
|
|
166
230
|
console.log(JSON.stringify({ ...result, saved: savedPath }));
|
|
167
231
|
}
|
|
@@ -221,9 +285,9 @@ Examples:
|
|
|
221
285
|
// Veo runs 30-120s; the bouncing bar + timer keeps the wait honest.
|
|
222
286
|
const result = opts.json
|
|
223
287
|
? await doGenerate()
|
|
224
|
-
: await withSpinner('Generating video
|
|
288
|
+
: await withSpinner('Generating video...', doGenerate, { done: null });
|
|
225
289
|
const filename = opts.output || 'generated.mp4';
|
|
226
|
-
const savedPath = await downloadFile(result.url, filename);
|
|
290
|
+
const savedPath = await downloadFile(result.url, filename, !!opts.output);
|
|
227
291
|
if (opts.json) {
|
|
228
292
|
console.log(JSON.stringify({ ...result, saved: savedPath }));
|
|
229
293
|
}
|
|
@@ -285,9 +349,9 @@ Examples:
|
|
|
285
349
|
});
|
|
286
350
|
const result = opts.json
|
|
287
351
|
? await doGenerate()
|
|
288
|
-
: await withSpinner('Generating speech
|
|
352
|
+
: await withSpinner('Generating speech...', doGenerate, { done: null });
|
|
289
353
|
const filename = opts.output || 'speech.mp3';
|
|
290
|
-
const savedPath = await downloadFile(result.url, filename);
|
|
354
|
+
const savedPath = await downloadFile(result.url, filename, !!opts.output);
|
|
291
355
|
if (opts.json) {
|
|
292
356
|
console.log(JSON.stringify({ ...result, saved: savedPath }));
|
|
293
357
|
}
|
|
@@ -338,9 +402,9 @@ Examples:
|
|
|
338
402
|
});
|
|
339
403
|
const result = opts.json
|
|
340
404
|
? await doGenerate()
|
|
341
|
-
: await withSpinner('Generating music
|
|
405
|
+
: await withSpinner('Generating music...', doGenerate, { done: null });
|
|
342
406
|
const filename = opts.output || 'music.mp3';
|
|
343
|
-
const savedPath = await downloadFile(result.url, filename);
|
|
407
|
+
const savedPath = await downloadFile(result.url, filename, !!opts.output);
|
|
344
408
|
if (opts.json) {
|
|
345
409
|
console.log(JSON.stringify({ ...result, saved: savedPath }));
|
|
346
410
|
}
|
|
@@ -389,9 +453,9 @@ Examples:
|
|
|
389
453
|
});
|
|
390
454
|
const result = opts.json
|
|
391
455
|
? await doGenerate()
|
|
392
|
-
: await withSpinner('Generating sound effect
|
|
456
|
+
: await withSpinner('Generating sound effect...', doGenerate, { done: null });
|
|
393
457
|
const filename = opts.output || 'sound.mp3';
|
|
394
|
-
const savedPath = await downloadFile(result.url, filename);
|
|
458
|
+
const savedPath = await downloadFile(result.url, filename, !!opts.output);
|
|
395
459
|
if (opts.json) {
|
|
396
460
|
console.log(JSON.stringify({ ...result, saved: savedPath }));
|
|
397
461
|
}
|
package/dist/commands/gmail.js
CHANGED
|
@@ -61,7 +61,7 @@ gmailCommand
|
|
|
61
61
|
.option('--cc <email>', 'Cc recipient (repeatable)', collect, [])
|
|
62
62
|
.option('--bcc <email>', 'Bcc recipient (repeatable)', collect, [])
|
|
63
63
|
.option('--reply-to <email>', 'Reply-To header address')
|
|
64
|
-
.requiredOption('--subject <s>', 'Subject (usually "Re:
|
|
64
|
+
.requiredOption('--subject <s>', 'Subject (usually "Re: ..." of the original)')
|
|
65
65
|
.requiredOption('--body <text>', 'Reply body (you compose the quote header)')
|
|
66
66
|
.option('--json', 'Output as JSON')
|
|
67
67
|
.action((opts) => run('Reply', async () => {
|