speculos-toolkit 1.2.6 → 1.2.8
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/README.md +33 -1
- package/package.json +2 -2
- package/skill/SKILL.md +3 -2
- package/src/client.js +2 -1
- package/src/creds.js +13 -4
- package/src/index.js +66 -27
- package/src/pack.js +6 -1
package/README.md
CHANGED
|
@@ -61,10 +61,24 @@ The first deploy mints a machine-global `~/.speculos/identity.json` (`{ userId,
|
|
|
61
61
|
that owns every URL deployed from this machine; each project records its slug id in a
|
|
62
62
|
gitignored `.speculos.json`. Keep both to retain ownership.
|
|
63
63
|
|
|
64
|
+
Set `SPECULOS_CONFIG_DIR` to an absolute directory path to use a separate identity
|
|
65
|
+
and account token stored at `<directory>/identity.json`. For example,
|
|
66
|
+
`SPECULOS_CONFIG_DIR=/absolute/path/to/profile speculos-toolkit login` signs into
|
|
67
|
+
that profile. Leave the variable unset to use `~/.speculos`. Relative paths,
|
|
68
|
+
unexpanded `~` paths, and empty values fail with `CREDENTIALS` before any API call
|
|
69
|
+
or credential write. Each project's `.speculos.json` stays in its project directory.
|
|
70
|
+
The credentials directory is excluded from frontend and backend upload archives.
|
|
71
|
+
It cannot also be selected as the deployment source or frontend build output.
|
|
72
|
+
|
|
64
73
|
If either identity file is unreadable or damaged, the CLI stops so you can restore it
|
|
65
74
|
before deploying. A failed login or logout reports the failure; rerun the same command
|
|
66
75
|
to retry linking or revoking the device.
|
|
67
76
|
|
|
77
|
+
`logout` signs the terminal out and revokes its account token. Existing apps, URLs,
|
|
78
|
+
and their account ownership stay in place. The signed-out terminal cannot deploy,
|
|
79
|
+
change, remove, or poll those apps until an explicit `login` succeeds again. If the
|
|
80
|
+
server cannot confirm sign-out, the CLI keeps its credential so you can retry.
|
|
81
|
+
|
|
68
82
|
## Conventions
|
|
69
83
|
|
|
70
84
|
- **Backend** listens on `process.env.PORT`, binds `0.0.0.0`. Node or Python.
|
|
@@ -80,7 +94,7 @@ speculos-toolkit status <jobId> poll a deployment
|
|
|
80
94
|
speculos-toolkit teardown --slug <s> remove a deployment
|
|
81
95
|
speculos-toolkit login [--token …] link this machine (browser approval at
|
|
82
96
|
https://unified.speculos.ai/link)
|
|
83
|
-
speculos-toolkit logout
|
|
97
|
+
speculos-toolkit logout sign out (preserves apps and URLs)
|
|
84
98
|
speculos-toolkit connectors [list|exec] your linked data sources, and one tool call
|
|
85
99
|
speculos-toolkit install-skill install the Claude Code skill
|
|
86
100
|
|
|
@@ -103,3 +117,21 @@ configuration into the CLI process itself.
|
|
|
103
117
|
Docs: https://unified.speculos.ai · Source: https://github.com/speculosai/unified_platform
|
|
104
118
|
|
|
105
119
|
MIT
|
|
120
|
+
|
|
121
|
+
### Deployment consistency (1.2.8)
|
|
122
|
+
|
|
123
|
+
An existing application's visibility changes only when the requested release is
|
|
124
|
+
published. Allocation or a backend quota refusal preserves the live audience.
|
|
125
|
+
Private code closes the frontend/backend gates before upload. A definitive bad
|
|
126
|
+
frontend archive restores the prior audience; an ambiguous network failure keeps
|
|
127
|
+
the application private until you retry. Use `npx -y speculos-toolkit@latest` for
|
|
128
|
+
visibility changes to existing applications; older clients receive an upgrade
|
|
129
|
+
message before any visibility change.
|
|
130
|
+
|
|
131
|
+
The CLI carries an app-specific deployment ID through backend and frontend
|
|
132
|
+
publication. Retried requests cannot publish different content under an already
|
|
133
|
+
used ID. A backend release reserves its matching frontend while that release is
|
|
134
|
+
unfinished, and a later dashboard visibility change invalidates an earlier
|
|
135
|
+
allocation. After linking an anonymous terminal, its anonymous deployment and
|
|
136
|
+
usage history is adopted into the verified account; pending adoption retries on
|
|
137
|
+
history reads.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "speculos-toolkit",
|
|
3
|
-
"version": "1.2.
|
|
4
|
-
"description": "The Speculos toolkit for coding agents
|
|
3
|
+
"version": "1.2.8",
|
|
4
|
+
"description": "The Speculos toolkit for coding agents — deploy any frontend/backend to a live URL and build against your linked data connectors (BigQuery, Postgres, Snowflake, Salesforce, …). Built for Claude Code, Codex, Cursor, and friends.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"speculos-toolkit": "bin/speculos-toolkit.js"
|
|
7
7
|
},
|
package/skill/SKILL.md
CHANGED
|
@@ -292,8 +292,9 @@ Builds run locally (this machine already has the toolchain); only static output
|
|
|
292
292
|
link from that first line and **relay it to the user** — ask them to open it, sign in, and
|
|
293
293
|
click Approve. It links this machine to their account (already linked? it no-ops — pass
|
|
294
294
|
`--relink` to switch accounts). Backend deploys then work with no password — **every
|
|
295
|
-
account includes one backend app free**, so there's no enablement wait.
|
|
296
|
-
`speculos-toolkit logout`.
|
|
295
|
+
account includes one backend app free**, so there's no enablement wait. Sign out with
|
|
296
|
+
`speculos-toolkit logout`. Apps and URLs stay owned by the account; run `login`
|
|
297
|
+
again before this terminal can manage them.
|
|
297
298
|
- **Frontend + backend (signed in):** just run the normal deploy — the saved sign-in
|
|
298
299
|
authorizes the backend:
|
|
299
300
|
```bash
|
package/src/client.js
CHANGED
|
@@ -48,7 +48,8 @@ async function linkPoll(code, opts = {}) { return post(base(opts) + "/api/cli/li
|
|
|
48
48
|
// verify a pasted account token (login --token) and get its account/org
|
|
49
49
|
async function whoami(token, opts = {}) { return get(base(opts) + "/api/account/whoami", token); }
|
|
50
50
|
async function linkMachine(token, payload, opts = {}) { return post(base(opts) + "/api/account/link", payload, token); }
|
|
51
|
-
// The machine identity rides along so the server can
|
|
51
|
+
// The machine identity rides along so the server can revoke its authority while
|
|
52
|
+
// preserving account ownership of its published applications.
|
|
52
53
|
async function logout(token, machine = {}, opts = {}) { return post(base(opts) + "/api/account/logout", { userId: machine.userId, userKey: machine.userKey }, token); }
|
|
53
54
|
// poll a backend job (owner-authenticated)
|
|
54
55
|
// The machine credentials travel as headers, not a query string: a query
|
package/src/creds.js
CHANGED
|
@@ -14,7 +14,12 @@ function credentialError(message, cause) {
|
|
|
14
14
|
|
|
15
15
|
// ---- machine-global identity --------------------------------------------
|
|
16
16
|
|
|
17
|
-
function identityDir() {
|
|
17
|
+
function identityDir() {
|
|
18
|
+
const configured = process.env.SPECULOS_CONFIG_DIR;
|
|
19
|
+
if (configured === undefined) return path.join(os.homedir(), ".speculos");
|
|
20
|
+
if (!path.isAbsolute(configured)) throw credentialError("SPECULOS_CONFIG_DIR must be an absolute path");
|
|
21
|
+
return configured;
|
|
22
|
+
}
|
|
18
23
|
function identityFile() { return path.join(identityDir(), "identity.json"); }
|
|
19
24
|
|
|
20
25
|
function loadIdentity() {
|
|
@@ -58,11 +63,15 @@ function saveAccountToken(accountToken) {
|
|
|
58
63
|
const cur = loadIdentity() || {};
|
|
59
64
|
writeIdentity({ ...cur, accountToken });
|
|
60
65
|
}
|
|
61
|
-
function clearAccountToken() {
|
|
66
|
+
function clearAccountToken(expectedToken) {
|
|
62
67
|
const cur = loadIdentity();
|
|
63
|
-
if (!cur) return;
|
|
68
|
+
if (!cur) return true;
|
|
69
|
+
// A login can finish while an earlier logout request is in flight. Never
|
|
70
|
+
// erase that newer credential when the old request eventually responds.
|
|
71
|
+
if (arguments.length && cur.accountToken !== expectedToken) return false;
|
|
64
72
|
delete cur.accountToken;
|
|
65
73
|
writeIdentity(cur);
|
|
74
|
+
return true;
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
// ---- per-project slug record --------------------------------------------
|
|
@@ -112,4 +121,4 @@ function ensureGitignored(root) {
|
|
|
112
121
|
} catch { /* best effort */ }
|
|
113
122
|
}
|
|
114
123
|
|
|
115
|
-
module.exports = { loadIdentity, saveIdentity, saveAccountToken, clearAccountToken, load, save, remove, file };
|
|
124
|
+
module.exports = { identityDir, loadIdentity, saveIdentity, saveAccountToken, clearAccountToken, load, save, remove, file };
|
package/src/index.js
CHANGED
|
@@ -96,7 +96,7 @@ USAGE
|
|
|
96
96
|
--token <spec_tok_…> link with a token you already
|
|
97
97
|
have (no browser); add --relink to move a device
|
|
98
98
|
already linked to a different/personal account
|
|
99
|
-
npx speculos-toolkit logout
|
|
99
|
+
npx speculos-toolkit logout sign this terminal out (keep its apps and URLs)
|
|
100
100
|
npx speculos-toolkit install-skill install the Claude Code /speculos-toolkit skill
|
|
101
101
|
(+ allow the deploy command once, no more prompts)
|
|
102
102
|
npx speculos-toolkit connectors [list]
|
|
@@ -163,7 +163,8 @@ OPTIONS
|
|
|
163
163
|
--json machine-readable only (auto-on when non-TTY/CI)
|
|
164
164
|
|
|
165
165
|
Identity: the first deploy mints a machine-global { userId, userKey } saved to
|
|
166
|
-
~/.speculos/identity.json — keep it to retain
|
|
166
|
+
~/.speculos/identity.json (or an absolute SPECULOS_CONFIG_DIR) — keep it to retain
|
|
167
|
+
ownership of your URLs. Each project
|
|
167
168
|
records its slug's stable id in .speculos.json (gitignored).
|
|
168
169
|
|
|
169
170
|
The frontend is automatically pointed at the deployed backend URL via
|
|
@@ -210,7 +211,22 @@ async function cmdDeploy(root, opts) {
|
|
|
210
211
|
// Keep the source directory even if an anonymous/frontend-only deploy skips
|
|
211
212
|
// running it. Skipping its sandbox must never publish its source as frontend.
|
|
212
213
|
const backendSourceDir = (d.backend && d.backend.dir) || (opts.backend && path.resolve(root, opts.backend));
|
|
213
|
-
|
|
214
|
+
const canonicalPath = (dir) => { try { return fs.realpathSync(dir); } catch { return path.resolve(dir); } };
|
|
215
|
+
const credentialDir = creds.identityDir();
|
|
216
|
+
const privateDirExclusions = (archiveDir, privateDir) => {
|
|
217
|
+
const paths = new Set();
|
|
218
|
+
for (const [base, target] of [[path.resolve(archiveDir), path.resolve(privateDir)], [canonicalPath(archiveDir), canonicalPath(privateDir)]]) {
|
|
219
|
+
const rel = path.relative(base, target);
|
|
220
|
+
if (rel && rel !== ".." && !rel.startsWith(".." + path.sep) && !path.isAbsolute(rel)) paths.add(rel);
|
|
221
|
+
}
|
|
222
|
+
return paths;
|
|
223
|
+
};
|
|
224
|
+
const isCredentialDir = (dir) => canonicalPath(dir) === canonicalPath(credentialDir);
|
|
225
|
+
if ([d.frontend && d.frontend.dir, d.backend && d.backend.dir].filter(Boolean).some(isCredentialDir)) {
|
|
226
|
+
emit({ ok: false, error: "the deployment source is the credentials directory; select a separate frontend or backend directory", code: "CREDENTIALS_SCOPE" });
|
|
227
|
+
return 2;
|
|
228
|
+
}
|
|
229
|
+
if (d.frontend && d.frontend.kind === "static" && backendSourceDir && canonicalPath(d.frontend.dir) === canonicalPath(backendSourceDir)) {
|
|
214
230
|
emit({ ok: false, error: "the static frontend and backend share a directory; select a separate frontend directory or build output so backend source is not published", code: "FRONTEND_SCOPE" });
|
|
215
231
|
return 2;
|
|
216
232
|
}
|
|
@@ -245,7 +261,7 @@ async function cmdDeploy(root, opts) {
|
|
|
245
261
|
// ---- identity + slug allocation (mints a machine identity on first use) ----
|
|
246
262
|
let alloc;
|
|
247
263
|
try {
|
|
248
|
-
alloc = await client.allocate({ userId: identity && identity.userId, userKey: identity && identity.userKey, slug: d.slug, visibility: opts.visibility }, opts);
|
|
264
|
+
alloc = await client.allocate({ userId: identity && identity.userId, userKey: identity && identity.userKey, slug: d.slug, visibility: opts.visibility, deploymentProtocol: 2 }, opts);
|
|
249
265
|
} catch (e) {
|
|
250
266
|
// Readable first: emit() is JSON for the agent case, but a person who typed
|
|
251
267
|
// --private with no account needs to see WHY in plain words.
|
|
@@ -256,7 +272,7 @@ async function cmdDeploy(root, opts) {
|
|
|
256
272
|
if (alloc.userKey) { // freshly minted on the server
|
|
257
273
|
identity = { userId: alloc.userId, userKey: alloc.userKey };
|
|
258
274
|
creds.saveIdentity(identity);
|
|
259
|
-
log(opts, `→ new device identity ${alloc.userId} (saved to
|
|
275
|
+
log(opts, `→ new device identity ${alloc.userId} (saved to ${path.join(credentialDir, "identity.json")} — keep it)`);
|
|
260
276
|
}
|
|
261
277
|
const userId = alloc.userId, userKey = identity.userKey, slugUuid = alloc.slugUuid;
|
|
262
278
|
creds.save(root, { slug: d.slug, slugUuid });
|
|
@@ -265,12 +281,13 @@ async function cmdDeploy(root, opts) {
|
|
|
265
281
|
let backendUrl = null, jobId = null, historyIds = [];
|
|
266
282
|
if (d.backend) {
|
|
267
283
|
log(opts, `→ packing backend (${path.relative(root, d.backend.dir) || "."}, ${d.backend.runtime})`);
|
|
268
|
-
const backendTar = packDir(d.backend.dir, { dropBuildOutput: false });
|
|
284
|
+
const backendTar = packDir(d.backend.dir, { dropBuildOutput: false, excludeDirs: [...privateDirExclusions(d.backend.dir, credentialDir)] });
|
|
269
285
|
log(opts, `→ deploying backend (${Math.round(backendTar.bytes / 1024)} KB) to an isolated sandbox`);
|
|
270
286
|
let created;
|
|
271
287
|
try {
|
|
272
288
|
created = await client.startBackend({
|
|
273
289
|
userId, userKey, slug: d.slug, slugUuid, override,
|
|
290
|
+
deploymentId: alloc.deploymentId, finalizeVisibility: !d.frontend,
|
|
274
291
|
backend: { tarB64: backendTar.base64, runtime: d.backend.runtime, startCmd: d.backend.startCmd, env: opts.env },
|
|
275
292
|
}, opts);
|
|
276
293
|
} catch (e) {
|
|
@@ -343,29 +360,37 @@ async function cmdDeploy(root, opts) {
|
|
|
343
360
|
});
|
|
344
361
|
} catch (e) { emit({ ok: false, slug: d.slug, status: "error", error: e.message, code: e.code || "BUILD" }); log(opts, `✗ build failed: ${e.message}`); return 1; }
|
|
345
362
|
}
|
|
363
|
+
if (isCredentialDir(outDir)) {
|
|
364
|
+
emit({ ok: false, error: "the frontend output is the credentials directory; select a separate build output directory", code: "CREDENTIALS_SCOPE" });
|
|
365
|
+
return 2;
|
|
366
|
+
}
|
|
346
367
|
log(opts, `→ packing + uploading frontend (${path.relative(root, outDir) || "."})`);
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
// backend
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
368
|
+
// Any static frontend can contain the selected backend, including a custom
|
|
369
|
+
// frontend directory. Exclude both the selected path and its real location
|
|
370
|
+
// so a symlink to the backend does not leave the source in the public bundle.
|
|
371
|
+
const set = privateDirExclusions(outDir, credentialDir);
|
|
372
|
+
if (d.frontend.kind === "static" && backendSourceDir) {
|
|
373
|
+
for (const rel of privateDirExclusions(outDir, backendSourceDir)) set.add(rel);
|
|
374
|
+
}
|
|
375
|
+
// Serving the repo root also excludes other top-level backend-looking dirs.
|
|
376
|
+
// Keep this heuristic at the repo root so ordinary static api/ content stays.
|
|
377
|
+
const rootStatic = d.frontend.kind === "static" && canonicalPath(outDir) === canonicalPath(root);
|
|
354
378
|
if (rootStatic) {
|
|
355
|
-
const set = new Set();
|
|
356
|
-
if (backendSourceDir) { const rel = path.relative(root, backendSourceDir); if (rel && rel !== ".." && !rel.startsWith(".." + path.sep) && !path.isAbsolute(rel)) set.add(rel); }
|
|
357
379
|
const looksBackend = (p) => fs.existsSync(path.join(p, "package.json")) || fs.existsSync(path.join(p, "requirements.txt")) || fs.existsSync(path.join(p, "pyproject.toml"));
|
|
358
380
|
for (const name of BACKEND_DIRS) { const p = path.join(root, name); if (fs.existsSync(p) && looksBackend(p)) set.add(name); }
|
|
359
|
-
|
|
381
|
+
}
|
|
382
|
+
const excludeDirs = [...set];
|
|
383
|
+
if (rootStatic) {
|
|
360
384
|
log(opts, ` note: serving the repo root — excluding secrets${excludeDirs.length ? " + " + excludeDirs.join(", ") : ""} from the public bundle`);
|
|
361
385
|
}
|
|
362
386
|
// Every frontend bundle is downloadable, including explicitly selected
|
|
363
387
|
// directories and build output that copied a public/ credential file.
|
|
364
388
|
const feTar = packDir(outDir, { dropBuildOutput: false, frontend: true, excludeDirs });
|
|
365
389
|
let fe;
|
|
366
|
-
try { fe = await client.putFrontend({ userId, userKey, slug: d.slug, slugUuid, tarB64: feTar.base64, backendUrl: feBackendUrl, unsetBackend: !!opts.unsetBackend }, opts); }
|
|
390
|
+
try { fe = await client.putFrontend({ userId, userKey, slug: d.slug, slugUuid, deploymentId: alloc.deploymentId, tarB64: feTar.base64, backendUrl: feBackendUrl, unsetBackend: !!opts.unsetBackend }, opts); }
|
|
367
391
|
catch (e) { emit({ ok: false, error: e.message, code: e.code || "FRONTEND" }); return 1; }
|
|
368
392
|
frontendUrl = fe.frontendUrl;
|
|
393
|
+
if (fe.visibility) alloc.visibility = fe.visibility;
|
|
369
394
|
if (fe.historyId) historyIds.push(fe.historyId);
|
|
370
395
|
}
|
|
371
396
|
|
|
@@ -401,7 +426,7 @@ async function cmdDeploy(root, opts) {
|
|
|
401
426
|
async function cmdStatus(jobId, opts) {
|
|
402
427
|
if (!jobId) { emit({ ok: false, error: "jobId required" }); return 2; }
|
|
403
428
|
const identity = creds.loadIdentity();
|
|
404
|
-
if (!identity || !identity.userId) { emit({ ok: false, error:
|
|
429
|
+
if (!identity || !identity.userId) { emit({ ok: false, error: `no identity (${path.join(creds.identityDir(), "identity.json")}) — cannot poll` }); return 1; }
|
|
405
430
|
const st = await client.backendStatus(jobId, { userId: identity.userId, userKey: identity.userKey }, opts);
|
|
406
431
|
// `ok` for contract-consistency with every other command (false only on a
|
|
407
432
|
// terminal failure; a still-running poll is ok:true).
|
|
@@ -415,7 +440,7 @@ async function cmdTeardown(root, opts) {
|
|
|
415
440
|
const identity = creds.loadIdentity();
|
|
416
441
|
const slug = opts.slug || (saved && saved.slug);
|
|
417
442
|
if (!slug) { emit({ ok: false, error: "--slug required (or run from the project dir with a .speculos.json)" }); return 2; }
|
|
418
|
-
if (!identity || !identity.userId) { emit({ ok: false, error:
|
|
443
|
+
if (!identity || !identity.userId) { emit({ ok: false, error: `no identity found (${path.join(creds.identityDir(), "identity.json")}) — nothing to tear down from this machine` }); return 1; }
|
|
419
444
|
try {
|
|
420
445
|
if (identity.accountToken) opts.token = identity.accountToken;
|
|
421
446
|
const r = await client.teardown({ userId: identity.userId, userKey: identity.userKey, slug }, opts);
|
|
@@ -528,24 +553,38 @@ async function cmdLogin(opts) {
|
|
|
528
553
|
return 0;
|
|
529
554
|
}
|
|
530
555
|
|
|
531
|
-
// ---- logout: revoke this
|
|
556
|
+
// ---- logout: revoke this terminal's authority, preserve its published apps ----
|
|
532
557
|
async function cmdLogout(opts) {
|
|
533
558
|
const identity = creds.loadIdentity();
|
|
534
559
|
const token = identity && identity.accountToken;
|
|
535
|
-
if (!token) { emit({ ok: true, alreadyLoggedOut: true }); return 0; }
|
|
536
|
-
let revoked = false, unlinked = false;
|
|
560
|
+
if (!token && !(identity && identity.userId)) { emit({ ok: true, alreadyLoggedOut: true }); return 0; }
|
|
561
|
+
let revoked = false, unlinked = false, signedOut = false, ownershipPreserved = false, alreadyRevoked = false, tokenAbsent = false;
|
|
537
562
|
try {
|
|
538
563
|
const r = await client.logout(token, { userId: identity.userId, userKey: identity.userKey }, opts);
|
|
539
|
-
|
|
564
|
+
const hasMachine = !!(identity.userId || identity.userKey);
|
|
565
|
+
// Older servers can return HTTP 200 after swallowing revocation/unlink
|
|
566
|
+
// failures. Success must establish both token revocation and (when there
|
|
567
|
+
// is a machine) loss of machine authority before discarding our retry key.
|
|
568
|
+
if (r.ok !== true || !(r.revoked === true || r.alreadyRevoked === true || (!token && r.tokenAbsent === true)) ||
|
|
569
|
+
(hasMachine && r.signedOut !== true && r.unlinked !== true)) {
|
|
570
|
+
throw Object.assign(new Error("the server did not confirm that this terminal was signed out"), { code: "LOGOUT_INCOMPLETE" });
|
|
571
|
+
}
|
|
572
|
+
revoked = r.revoked === true; alreadyRevoked = r.alreadyRevoked === true;
|
|
573
|
+
tokenAbsent = r.tokenAbsent === true;
|
|
574
|
+
unlinked = r.unlinked === true; signedOut = r.signedOut === true || unlinked;
|
|
575
|
+
ownershipPreserved = r.ownershipPreserved === true;
|
|
540
576
|
} catch (e) {
|
|
541
|
-
// Keep the token so a retry can revoke it and
|
|
577
|
+
// Keep the token so a retry can revoke it and sign the machine out. Clearing
|
|
542
578
|
// it during an outage makes an active server credential unrecoverable.
|
|
543
579
|
emit({ ok: false, loggedOut: false, error: `could not sign out: ${e.message}; retry logout when the service is reachable`, code: e.code || "LOGOUT" });
|
|
544
580
|
return 1;
|
|
545
581
|
}
|
|
546
|
-
creds.clearAccountToken()
|
|
547
|
-
|
|
548
|
-
|
|
582
|
+
if (!creds.clearAccountToken(token)) {
|
|
583
|
+
emit({ ok: false, loggedOut: false, revoked, code: "LOGOUT_SUPERSEDED", error: "the saved sign-in changed while logout was pending; the newer credential was kept" });
|
|
584
|
+
return 1;
|
|
585
|
+
}
|
|
586
|
+
log(opts, `✓ this terminal is signed out of your Speculos account.${ownershipPreserved ? " Its apps and URLs are preserved; run `login` to manage them again." : ""}`);
|
|
587
|
+
emit({ ok: true, loggedOut: true, revoked, alreadyRevoked, tokenAbsent, signedOut, ownershipPreserved, unlinked });
|
|
549
588
|
return 0;
|
|
550
589
|
}
|
|
551
590
|
|
package/src/pack.js
CHANGED
|
@@ -39,7 +39,12 @@ function packDir(dir, { dropBuildOutput = false, frontend = false, excludeDirs =
|
|
|
39
39
|
for (const e of excludes) args.push("--exclude=" + e);
|
|
40
40
|
// excludeDirs are TOP-LEVEL paths (e.g. a backend dir) — anchor with ./ so they
|
|
41
41
|
// exclude only that top-level dir, never a same-named dir nested in the site.
|
|
42
|
-
for (const e of (excludeDirs || []))
|
|
42
|
+
for (const e of (excludeDirs || [])) {
|
|
43
|
+
// These are literal paths, unlike the wildcard patterns above. A custom
|
|
44
|
+
// credentials directory may itself contain brackets, stars or question marks.
|
|
45
|
+
const literal = String(e).replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+$/, "").replace(/[\\*?\[\]]/g, (character) => "\\" + character);
|
|
46
|
+
args.push("--exclude=./" + literal);
|
|
47
|
+
}
|
|
43
48
|
args.push("-C", dir, ".");
|
|
44
49
|
try {
|
|
45
50
|
execFileSync("tar", args, { stdio: ["ignore", "ignore", "pipe"] });
|