speculos-toolkit 1.2.2 → 1.2.3
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/src/client.js +2 -1
- package/src/index.js +25 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "speculos-toolkit",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
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/src/client.js
CHANGED
|
@@ -36,7 +36,8 @@ async function linkPoll(code, opts = {}) { return post(base(opts) + "/api/cli/li
|
|
|
36
36
|
// verify a pasted account token (login --token) and get its account/org
|
|
37
37
|
async function whoami(token, opts = {}) { return get(base(opts) + "/api/account/whoami", token); }
|
|
38
38
|
async function linkMachine(token, payload, opts = {}) { return post(base(opts) + "/api/account/link", payload, token); }
|
|
39
|
-
|
|
39
|
+
// The machine identity rides along so the server can unlink it too.
|
|
40
|
+
async function logout(token, machine = {}, opts = {}) { return post(base(opts) + "/api/account/logout", { userId: machine.userId, userKey: machine.userKey }, token); }
|
|
40
41
|
// poll a backend job (owner-authenticated)
|
|
41
42
|
// The machine credentials travel as headers, not a query string: a query
|
|
42
43
|
// string is written to every access log between here and the orchestrator.
|
package/src/index.js
CHANGED
|
@@ -100,7 +100,8 @@ WHO CAN SEE IT
|
|
|
100
100
|
apps come with it — then deploy again with --private, or flip it
|
|
101
101
|
on https://unified.speculos.ai/?tab=deploys.
|
|
102
102
|
|
|
103
|
-
A redeploy
|
|
103
|
+
A redeploy with no visibility flag never changes a live app's visibility;
|
|
104
|
+
an explicit --private / --org / --public applies immediately, both ways.
|
|
104
105
|
Every Speculos account includes one backend app free (an isolated sandbox per app) —
|
|
105
106
|
sign up at https://unified.speculos.ai and run 'speculos-toolkit login'.
|
|
106
107
|
Without an account a detected backend is skipped (frontend still ships free).
|
|
@@ -185,11 +186,16 @@ async function cmdDeploy(root, opts) {
|
|
|
185
186
|
const accountToken = identity && identity.accountToken;
|
|
186
187
|
if (accountToken) opts.token = accountToken; // sent as Bearer on allocate/backend
|
|
187
188
|
const hadBackend = !!d.backend;
|
|
189
|
+
let backendNote = null;
|
|
188
190
|
if (d.backend && !override && !accountToken) {
|
|
189
|
-
|
|
191
|
+
// Said in the JSON too: an agent reads only stdout, and "ok: true" with a
|
|
192
|
+
// frontend URL used to hide that the API it built never shipped.
|
|
193
|
+
backendNote = "backend detected but skipped: this machine is not linked to a Speculos account (every account includes one backend app). Run `speculos-toolkit login`, then deploy again.";
|
|
194
|
+
log(opts, `↪ ${backendNote}`);
|
|
190
195
|
d.backend = null;
|
|
191
196
|
}
|
|
192
197
|
if (!d.frontend && !d.backend) {
|
|
198
|
+
if (opts.noFrontend && opts.noBackend) { emit({ ok: false, error: "both --no-frontend and --no-backend were given - nothing to deploy", code: "NOTHING_SELECTED" }); return 2; }
|
|
193
199
|
if (hadBackend) { emit({ ok: false, error: `nothing to deploy: Speculos hosts frontends free, but this project is backend-only — sign in to deploy the backend (every Speculos account includes one free backend app). Run \`speculos-toolkit login\` (https://unified.speculos.ai), or add a frontend.`, code: "BETA_BACKEND_ONLY" }); return 2; }
|
|
194
200
|
emit({ ok: false, error: `could not detect a frontend in ${root}. Pass --frontend <dir>.`, code: "DETECT" });
|
|
195
201
|
return 2;
|
|
@@ -221,7 +227,7 @@ async function cmdDeploy(root, opts) {
|
|
|
221
227
|
creds.save(root, { slug: d.slug, slugUuid });
|
|
222
228
|
|
|
223
229
|
// ---- backend first (so its URL can be baked into the frontend) ----
|
|
224
|
-
let backendUrl = null,
|
|
230
|
+
let backendUrl = null, jobId = null, historyIds = [];
|
|
225
231
|
if (d.backend) {
|
|
226
232
|
if (opts.envFileError) { emit({ ok: false, error: opts.envFileError, code: "ENV_FILE" }); log(opts, `✗ ${opts.envFileError}`); return 1; }
|
|
227
233
|
log(opts, `→ packing backend (${path.relative(root, d.backend.dir) || "."}, ${d.backend.runtime})`);
|
|
@@ -241,6 +247,8 @@ async function cmdDeploy(root, opts) {
|
|
|
241
247
|
else { emit({ ok: false, error: e.message, code: e.code || "BACKEND" }); return 1; }
|
|
242
248
|
}
|
|
243
249
|
if (created) {
|
|
250
|
+
jobId = created.jobId;
|
|
251
|
+
if (created.historyId) historyIds.push(created.historyId);
|
|
244
252
|
const st = await pollBackend(created.jobId, { userId, userKey }, opts);
|
|
245
253
|
if (st.status !== "success" || !(st.urls && st.urls.backend)) {
|
|
246
254
|
emit({ ok: false, slug: d.slug, jobId: created.jobId, status: "error", error: st.error || "backend failed", logTail: st.logTail });
|
|
@@ -291,6 +299,7 @@ async function cmdDeploy(root, opts) {
|
|
|
291
299
|
try { fe = await client.putFrontend({ userId, userKey, slug: d.slug, slugUuid, tarB64: feTar.base64, backendUrl }, opts); }
|
|
292
300
|
catch (e) { emit({ ok: false, error: e.message, code: e.code || "FRONTEND" }); return 1; }
|
|
293
301
|
frontendUrl = fe.frontendUrl;
|
|
302
|
+
if (fe.historyId) historyIds.push(fe.historyId);
|
|
294
303
|
}
|
|
295
304
|
|
|
296
305
|
const urls = {};
|
|
@@ -310,6 +319,10 @@ async function cmdDeploy(root, opts) {
|
|
|
310
319
|
else if (visibility === "public") log(opts, ` visibility: public — anyone with the link.`);
|
|
311
320
|
if (alloc && alloc.visibilityNote) log(opts, ` note: ${alloc.visibilityNote}`);
|
|
312
321
|
const out = { ok: true, slug: d.slug, userId, urls };
|
|
322
|
+
// What an agent needs to follow up without a second lookup: the backend
|
|
323
|
+
// job id for `status`, and the console history ids the gateway filed.
|
|
324
|
+
if (jobId) out.jobId = jobId;
|
|
325
|
+
if (historyIds.length) out.historyIds = historyIds;
|
|
313
326
|
if (visibility) out.visibility = visibility;
|
|
314
327
|
if (alloc && alloc.visibilityNote) out.visibilityNote = alloc.visibilityNote;
|
|
315
328
|
if (backendNote) out.backendNote = backendNote;
|
|
@@ -339,8 +352,9 @@ async function cmdTeardown(root, opts) {
|
|
|
339
352
|
if (identity.accountToken) opts.token = identity.accountToken;
|
|
340
353
|
const r = await client.teardown({ userId: identity.userId, userKey: identity.userKey, slug }, opts);
|
|
341
354
|
if (saved && saved.slug === slug) creds.remove(root);
|
|
355
|
+
if (r.notFound) log(opts, `! nothing was deployed under "${slug}" from this machine - nothing to tear down`);
|
|
342
356
|
emit({ ok: true, ...r });
|
|
343
|
-
return 0;
|
|
357
|
+
return r.notFound ? 3 : 0;
|
|
344
358
|
} catch (e) { emit({ ok: false, error: e.message, code: e.code || "TEARDOWN" }); return 1; }
|
|
345
359
|
}
|
|
346
360
|
|
|
@@ -429,12 +443,14 @@ async function cmdLogout(opts) {
|
|
|
429
443
|
const identity = creds.loadIdentity();
|
|
430
444
|
const token = identity && identity.accountToken;
|
|
431
445
|
if (!token) { emit({ ok: true, alreadyLoggedOut: true }); return 0; }
|
|
432
|
-
let revoked = false;
|
|
433
|
-
try {
|
|
434
|
-
|
|
446
|
+
let revoked = false, unlinked = false;
|
|
447
|
+
try {
|
|
448
|
+
const r = await client.logout(token, { userId: identity.userId, userKey: identity.userKey }, opts);
|
|
449
|
+
revoked = !!r.revoked; unlinked = !!r.unlinked;
|
|
450
|
+
} catch { /* revoke best-effort; still clear locally */ }
|
|
435
451
|
creds.clearAccountToken();
|
|
436
|
-
log(opts, `✓ this device is signed out of your Speculos account${revoked ? " (token revoked)" : ""}.`);
|
|
437
|
-
emit({ ok: true, loggedOut: true, revoked });
|
|
452
|
+
log(opts, `✓ this device is signed out of your Speculos account${revoked ? " (token revoked)" : ""}${unlinked ? "; the machine is no longer linked to it - `login` links it again" : ""}.`);
|
|
453
|
+
emit({ ok: true, loggedOut: true, revoked, unlinked });
|
|
438
454
|
return 0;
|
|
439
455
|
}
|
|
440
456
|
|