speculos-toolkit 1.2.3 → 1.2.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/package.json +1 -1
- package/skill/SKILL.md +9 -0
- package/src/client.js +2 -1
- package/src/index.js +37 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "speculos-toolkit",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.5",
|
|
4
4
|
"description": "The Speculos toolkit for coding agents \u2014 deploy any frontend/backend to a live URL and build against your linked data connectors (BigQuery, Postgres, Snowflake, Salesforce, \u2026). Built for Claude Code, Codex, Cursor, and friends.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"speculos-toolkit": "bin/speculos-toolkit.js"
|
package/skill/SKILL.md
CHANGED
|
@@ -149,6 +149,15 @@ The last stdout line is `{ ok, brokerUrl, connectors: [{ alias, name, kind, acco
|
|
|
149
149
|
- `ok:false` + `code:"LOGIN_REQUIRED"` on a deploy → `--private`/`--org` was asked for on a
|
|
150
150
|
machine with no account. Nothing was published. Either run `login` and deploy again, or
|
|
151
151
|
drop the flag (the app ships public) and lock it down after linking.
|
|
152
|
+
- `ok:false` + `code:"ALREADY_DEPLOYING"` (with `jobId`) → another deploy of this app is
|
|
153
|
+
still installing. Nothing was written. Watch it with `status <jobId>` and re-run after,
|
|
154
|
+
rather than racing it - two concurrent full-stack deploys used to clobber each other
|
|
155
|
+
inside the sandbox and leave the backend failed.
|
|
156
|
+
- **A frontend-only deploy keeps the app's backend.** `--no-backend` (or a repo where the
|
|
157
|
+
backend did not change) redeploys the frontend against the SAME backend URL and says so
|
|
158
|
+
(`backendKept: true`); it used to silently bake `null` and the live app started 404ing
|
|
159
|
+
its own API. Pass `--unset-backend` to actually detach it — that sticks: the app
|
|
160
|
+
stays detached until a deploy actually ships a backend again.
|
|
152
161
|
- `ok:true` with empty `connectors` → nothing linked (or nothing granted to this user).
|
|
153
162
|
If the app clearly wants external data, tell the user to link a source (or ask their org
|
|
154
163
|
admin for access) at **https://unified.speculos.ai/?tab=deploys**, then **re-run the list** —
|
package/src/client.js
CHANGED
|
@@ -13,7 +13,8 @@ async function post(url, body, token) {
|
|
|
13
13
|
if (token) headers["Authorization"] = "Bearer " + token;
|
|
14
14
|
const res = await fetch(url, { method: "POST", headers, body: JSON.stringify(body) });
|
|
15
15
|
const data = await res.json().catch(() => ({}));
|
|
16
|
-
|
|
16
|
+
// Some refusals name a job to follow (ALREADY_DEPLOYING); keep it on the error.
|
|
17
|
+
if (!res.ok) { const e = new Error(data.error || `HTTP ${res.status}`); e.code = data.code; e.status = res.status; e.jobId = data.jobId || null; throw e; }
|
|
17
18
|
return data;
|
|
18
19
|
}
|
|
19
20
|
async function get(url, token, extra) {
|
package/src/index.js
CHANGED
|
@@ -41,6 +41,9 @@ function parseArgs(argv) {
|
|
|
41
41
|
else if (a === "--org") opts.visibility = "org";
|
|
42
42
|
else if (a === "--public") opts.visibility = "public";
|
|
43
43
|
else if (a === "--no-backend") opts.noBackend = true;
|
|
44
|
+
// Explicitly clear the app's saved backend URL. A deploy that simply ships
|
|
45
|
+
// no backend KEEPS it (see cmdDeploy) — this is how you say "no backend".
|
|
46
|
+
else if (a === "--unset-backend") opts.unsetBackend = true;
|
|
44
47
|
else if (a === "--no-frontend") opts.noFrontend = true;
|
|
45
48
|
else if (a === "--json") opts.json = true;
|
|
46
49
|
else if (a === "--help" || a === "-h") opts.help = true;
|
|
@@ -115,7 +118,11 @@ OPTIONS
|
|
|
115
118
|
--start <cmd> backend start command (auto-detected; must bind 0.0.0.0:$PORT)
|
|
116
119
|
--build force the frontend through its build step (vs serve as static)
|
|
117
120
|
--static serve the frontend dir as-is even if it has a build script
|
|
118
|
-
--no-backend frontend-only: skip backend even if one is detected
|
|
121
|
+
--no-backend frontend-only: skip backend even if one is detected (the app
|
|
122
|
+
keeps pointing at the backend it already has)
|
|
123
|
+
--unset-backend clear this app's backend URL: the frontend stops calling a
|
|
124
|
+
backend (the sandbox itself stays up — use teardown for that).
|
|
125
|
+
It stays cleared until you deploy a backend again
|
|
119
126
|
--private only you can open the deployed app (needs an account;
|
|
120
127
|
the DEFAULT for a new app once this machine is linked)
|
|
121
128
|
--org anyone in your org can open it (needs an account)
|
|
@@ -244,6 +251,14 @@ async function cmdDeploy(root, opts) {
|
|
|
244
251
|
// deploy; ship the frontend so the user still gets a live URL, and surface
|
|
245
252
|
// how to proceed. Only genuine backend build/start errors fail the deploy.
|
|
246
253
|
if (e.code === "BACKEND_DISABLED" || e.code === "TOO_MANY") { log(opts, `↪ backend skipped — ${e.message}`); d.backend = null; backendNote = e.message; }
|
|
254
|
+
// Another deploy of THIS app is still installing. Both would overwrite
|
|
255
|
+
// each other's files inside the sandbox, so stop and say so plainly —
|
|
256
|
+
// with the job to follow — rather than shipping a half-deploy.
|
|
257
|
+
else if (e.code === "ALREADY_DEPLOYING") {
|
|
258
|
+
log(opts, `✗ ${e.message}`);
|
|
259
|
+
emit({ ok: false, slug: d.slug, status: "error", error: e.message, code: "ALREADY_DEPLOYING", ...(e.jobId ? { jobId: e.jobId } : {}) });
|
|
260
|
+
return 1;
|
|
261
|
+
}
|
|
247
262
|
else { emit({ ok: false, error: e.message, code: e.code || "BACKEND" }); return 1; }
|
|
248
263
|
}
|
|
249
264
|
if (created) {
|
|
@@ -261,6 +276,22 @@ async function cmdDeploy(root, opts) {
|
|
|
261
276
|
}
|
|
262
277
|
}
|
|
263
278
|
|
|
279
|
+
// A deploy that ships no backend this time is NOT a deploy of an app with no
|
|
280
|
+
// backend: `--no-backend` (or a skipped/absent backend) keeps the app pointed
|
|
281
|
+
// at the backend it already has, which allocate reports. Without this, the
|
|
282
|
+
// rebuilt bundle and the regenerated speculos-env.js both said "no backend"
|
|
283
|
+
// and a live app started 404ing its own API. --unset-backend is the explicit
|
|
284
|
+
// way to clear it, and the orchestrator remembers that: later frontend-only
|
|
285
|
+
// deploys stay detached (the backend sandbox itself stays up; `teardown`
|
|
286
|
+
// removes it).
|
|
287
|
+
let keptBackendUrl = null;
|
|
288
|
+
const liveBackendUrl = (alloc && alloc.backendUrl) || null;
|
|
289
|
+
if (!backendUrl && liveBackendUrl) {
|
|
290
|
+
if (opts.unsetBackend) log(opts, `↪ clearing this app's backend URL (--unset-backend) — it stays cleared until you deploy a backend again; the backend itself stays up`);
|
|
291
|
+
else { keptBackendUrl = liveBackendUrl; log(opts, `↪ keeping this app's existing backend: ${keptBackendUrl} (--unset-backend to clear it)`); }
|
|
292
|
+
}
|
|
293
|
+
const feBackendUrl = backendUrl || keptBackendUrl;
|
|
294
|
+
|
|
264
295
|
// ---- frontend: build locally (if needed), then upload static output ----
|
|
265
296
|
let frontendUrl = null;
|
|
266
297
|
if (d.frontend) {
|
|
@@ -270,7 +301,7 @@ async function cmdDeploy(root, opts) {
|
|
|
270
301
|
try {
|
|
271
302
|
outDir = runBuild({
|
|
272
303
|
dir: d.frontend.dir, framework: d.frontend.framework, buildCmd: d.frontend.buildCmd,
|
|
273
|
-
base: alloc.base, backendUrl,
|
|
304
|
+
base: alloc.base, backendUrl: feBackendUrl,
|
|
274
305
|
connectorsUrl: alloc.connectorsUrl, connectorsToken: alloc.connectorsToken,
|
|
275
306
|
outputDir: d.frontend.outputDir, log: (m) => log(opts, m),
|
|
276
307
|
});
|
|
@@ -296,7 +327,7 @@ async function cmdDeploy(root, opts) {
|
|
|
296
327
|
// output shouldn't have its assets stripped just because one is named like a key.
|
|
297
328
|
const feTar = packDir(outDir, { dropBuildOutput: false, frontend: rootStatic, excludeDirs });
|
|
298
329
|
let fe;
|
|
299
|
-
try { fe = await client.putFrontend({ userId, userKey, slug: d.slug, slugUuid, tarB64: feTar.base64, backendUrl }, opts); }
|
|
330
|
+
try { fe = await client.putFrontend({ userId, userKey, slug: d.slug, slugUuid, tarB64: feTar.base64, backendUrl: feBackendUrl, unsetBackend: !!opts.unsetBackend }, opts); }
|
|
300
331
|
catch (e) { emit({ ok: false, error: e.message, code: e.code || "FRONTEND" }); return 1; }
|
|
301
332
|
frontendUrl = fe.frontendUrl;
|
|
302
333
|
if (fe.historyId) historyIds.push(fe.historyId);
|
|
@@ -304,10 +335,10 @@ async function cmdDeploy(root, opts) {
|
|
|
304
335
|
|
|
305
336
|
const urls = {};
|
|
306
337
|
if (frontendUrl) urls.frontend = frontendUrl;
|
|
307
|
-
if (
|
|
338
|
+
if (feBackendUrl) urls.backend = feBackendUrl;
|
|
308
339
|
log(opts, `✓ live`);
|
|
309
340
|
if (urls.frontend) log(opts, ` frontend: ${urls.frontend}`);
|
|
310
|
-
if (urls.backend) log(opts, ` backend: ${urls.backend}`);
|
|
341
|
+
if (urls.backend) log(opts, ` backend: ${urls.backend}${keptBackendUrl ? " (kept — not redeployed this time)" : ""}`);
|
|
311
342
|
if (backendNote) log(opts, ` note: ${backendNote}`);
|
|
312
343
|
// Say who can open it. A new app is private now, and a URL printed with no
|
|
313
344
|
// word about visibility is the kind of thing people paste into Slack and
|
|
@@ -322,6 +353,7 @@ async function cmdDeploy(root, opts) {
|
|
|
322
353
|
// What an agent needs to follow up without a second lookup: the backend
|
|
323
354
|
// job id for `status`, and the console history ids the gateway filed.
|
|
324
355
|
if (jobId) out.jobId = jobId;
|
|
356
|
+
if (keptBackendUrl) out.backendKept = true; // the URL above is the live one, not a new deploy
|
|
325
357
|
if (historyIds.length) out.historyIds = historyIds;
|
|
326
358
|
if (visibility) out.visibility = visibility;
|
|
327
359
|
if (alloc && alloc.visibilityNote) out.visibilityNote = alloc.visibilityNote;
|