@stackbone/cli 0.5.2 → 0.5.4
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/CHANGELOG.md +46 -0
- package/README.md +3 -3
- package/main.js +379 -157
- package/package.json +1 -1
- package/stackbone-cli-0.5.4.tgz +0 -0
- package/stackbone-cli-0.5.2.tgz +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.4] - 2026-10-01
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`stackbone dev` says why the tunnel closed.** When `frpc` stops after the
|
|
15
|
+
tunnel came up, `stackbone dev` keeps running and prints the cause: the
|
|
16
|
+
ticket reached its 8-hour limit, the control plane revoked the grant, or
|
|
17
|
+
another cause, with the last lines `frpc` printed. Each message says to
|
|
18
|
+
restart `stackbone dev` for a new URL. Before, the tunnel closed without a
|
|
19
|
+
word (PLA-1892).
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **`frpc` now sends the grant's signed ticket to the relay.** The ticket is
|
|
24
|
+
written to the `frpc` config as global metadata, with a heartbeat every 30
|
|
25
|
+
seconds so the relay sees it on every ping as well as on login. The shared
|
|
26
|
+
token is still sent. A control plane that mints no ticket still opens the
|
|
27
|
+
tunnel on the shared token alone (PLA-1892).
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **An expired tunnel grant could clear the tunnel URL of an unrelated
|
|
32
|
+
`stackbone dev` session.** The control plane matched a lapsed grant to local
|
|
33
|
+
installations by searching their URL text for its subdomain, so one grant
|
|
34
|
+
could null another session's URL, in any organization. `stackbone dev` now
|
|
35
|
+
registers its local-dev installation before the tunnel opens, or registers it
|
|
36
|
+
again when the id in `.stackbone/project.json` no longer resolves, and sends
|
|
37
|
+
that id with the grant request. The control plane writes the tunnel URL when
|
|
38
|
+
it mints the grant and clears it on revoke or expiry, so the CLI no longer
|
|
39
|
+
PATCHes the URL on start or on Ctrl-C. If no installation can be registered,
|
|
40
|
+
the tunnel still opens, with an unbound grant (PLA-1691).
|
|
41
|
+
|
|
42
|
+
- **`stackbone logs --since 5` read the number as a date in 2001 and filtered
|
|
43
|
+
nothing.** A bare number is a duration with the unit forgotten; `--since`
|
|
44
|
+
and `--until` now reject it and suggest one, e.g. `5m` (PLA-1796).
|
|
45
|
+
|
|
46
|
+
## [0.5.3] - 2026-09-25
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- **`stackbone dev` failed at `Starting docker services` with `401 UNAUTHORIZED`
|
|
51
|
+
from quay.io.** MinIO closed anonymous pulls of `minio/minio` there too, after
|
|
52
|
+
Docker Hub. The compose files `stackbone dev` and `stackbone package` write now
|
|
53
|
+
pull the community fork `pgsty/minio:RELEASE.2026-08-04T00-00-00Z`, which needs
|
|
54
|
+
no account (PLA-1794).
|
|
55
|
+
|
|
10
56
|
## [0.5.2] - 2026-09-23
|
|
11
57
|
|
|
12
58
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @stackbone/cli
|
|
2
2
|
|
|
3
|
-
The CLI for [Stackbone](https://stackbone.ai)
|
|
3
|
+
The CLI for [Stackbone](https://stackbone.ai): scaffold, run and deploy agents and workflows written in TypeScript, on Stackbone Cloud or your own infrastructure.
|
|
4
4
|
|
|
5
5
|
> **Status:** `alpha`. The contract may break between releases. Pin a version or rely on the `alpha` dist-tag.
|
|
6
6
|
|
|
@@ -15,7 +15,7 @@ cd my-workspace && npm install
|
|
|
15
15
|
npx @stackbone/cli dev
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
That's it. `init` scaffolds a new workspace shell (agents + workflows + coding-agent skills) and links it to your organization, so you sign in first; `dev` boots the whole workspace locally
|
|
18
|
+
That's it. `init` scaffolds a new workspace shell (agents + workflows + coding-agent skills) and links it to your organization, so you sign in first; `dev` boots the whole workspace locally (Postgres, Redis, MinIO and Stackbone Studio at `http://localhost:4242`) so you can iterate before you deploy.
|
|
19
19
|
|
|
20
20
|
## Commands
|
|
21
21
|
|
|
@@ -45,7 +45,7 @@ Run any command with `--help` for the full flag reference. The full documentatio
|
|
|
45
45
|
## Links
|
|
46
46
|
|
|
47
47
|
- Documentation: <https://docs.stackbone.ai>
|
|
48
|
-
-
|
|
48
|
+
- Website: <https://stackbone.ai>
|
|
49
49
|
- Issues & feedback: contact the team directly.
|
|
50
50
|
|
|
51
51
|
## License
|
package/main.js
CHANGED
|
@@ -9,7 +9,7 @@ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep }
|
|
|
9
9
|
import semver from "semver";
|
|
10
10
|
import pino from "pino";
|
|
11
11
|
import pretty from "pino-pretty";
|
|
12
|
-
import {
|
|
12
|
+
import { execFile, spawn, spawnSync } from "node:child_process";
|
|
13
13
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
14
14
|
import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
15
15
|
import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
@@ -37,9 +37,9 @@ import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
|
|
37
37
|
import PgQueryFactory from "pg-query-emscripten";
|
|
38
38
|
import { createRemoteJWKSet, errors, jwtVerify } from "jose";
|
|
39
39
|
import { CLIENT_CAPABILITIES_META_KEY, McpServer, createMcpHandler, inputRequired, inputResponse } from "@modelcontextprotocol/server";
|
|
40
|
+
import { COMPOSED_HANDLER } from "hono/utils/constants";
|
|
40
41
|
import Ajv from "ajv/dist/2020.js";
|
|
41
42
|
import addFormats from "ajv-formats";
|
|
42
|
-
import { COMPOSED_HANDLER } from "hono/utils/constants";
|
|
43
43
|
import { parse } from "yaml";
|
|
44
44
|
import { Queue, Worker } from "bullmq";
|
|
45
45
|
import Ajv$1 from "ajv";
|
|
@@ -2045,12 +2045,15 @@ var init_user_code = __esmMin((() => {
|
|
|
2045
2045
|
if (clean.length !== 8) return raw;
|
|
2046
2046
|
return `${clean.slice(0, 4)}-${clean.slice(4)}`;
|
|
2047
2047
|
};
|
|
2048
|
-
})), DEV_TUNNEL_GRANT_MAX_TTL_SECONDS, devTunnelGrantResponseSchema;
|
|
2048
|
+
})), DEV_TUNNEL_GRANT_HARD_CAP_SECONDS, DEV_TUNNEL_GRANT_MAX_TTL_SECONDS, buildCanonicalTunnelUrl, devTunnelGrantResponseSchema, devTunnelRevocationListResponseSchema;
|
|
2049
2049
|
var init_dev_tunnel = __esmMin((() => {
|
|
2050
2050
|
init_organizations();
|
|
2051
|
+
DEV_TUNNEL_GRANT_HARD_CAP_SECONDS = 480 * 60;
|
|
2051
2052
|
DEV_TUNNEL_GRANT_MAX_TTL_SECONDS = 86400;
|
|
2053
|
+
buildCanonicalTunnelUrl = (subdomain, relayHost) => `https://${subdomain}.${relayHost}`;
|
|
2052
2054
|
z.object({
|
|
2053
2055
|
agentSlug: agentSlugSchema,
|
|
2056
|
+
installationId: z.uuid().optional(),
|
|
2054
2057
|
ttlSeconds: z.number().int().positive().max(DEV_TUNNEL_GRANT_MAX_TTL_SECONDS, { message: `ttlSeconds must not exceed ${DEV_TUNNEL_GRANT_MAX_TTL_SECONDS}` }).optional()
|
|
2055
2058
|
});
|
|
2056
2059
|
z.object({ subdomain: z.string().min(8).max(64).regex(/^[a-z0-9-]+$/, "subdomain must be lowercase alphanumeric with optional hyphens") });
|
|
@@ -2059,8 +2062,18 @@ var init_dev_tunnel = __esmMin((() => {
|
|
|
2059
2062
|
relayPort: z.number().int().positive(),
|
|
2060
2063
|
subdomain: z.string().min(1),
|
|
2061
2064
|
frpcToken: z.string().min(1),
|
|
2065
|
+
ticket: z.string().min(1),
|
|
2062
2066
|
expiresAt: z.iso.datetime()
|
|
2063
2067
|
});
|
|
2068
|
+
devTunnelRevocationListResponseSchema = z.object({ revocationList: z.string().min(1) });
|
|
2069
|
+
z.object({ keys: z.array(z.object({
|
|
2070
|
+
kty: z.literal("OKP"),
|
|
2071
|
+
crv: z.literal("Ed25519"),
|
|
2072
|
+
x: z.string().min(1),
|
|
2073
|
+
kid: z.string().min(1),
|
|
2074
|
+
alg: z.literal("EdDSA"),
|
|
2075
|
+
use: z.literal("sig")
|
|
2076
|
+
})) });
|
|
2064
2077
|
}));
|
|
2065
2078
|
//#endregion
|
|
2066
2079
|
//#region ../../libs/shared/validators/src/lib/studio/studio-config.ts
|
|
@@ -4846,7 +4859,7 @@ var maybeWarnCliUpgrade = (opts) => {
|
|
|
4846
4859
|
if (!required && !belowMinimum) return;
|
|
4847
4860
|
const channel = channelForVersion$1(cliVersion);
|
|
4848
4861
|
const target = minVersion ?? "the latest release";
|
|
4849
|
-
logger.warn(`Your Stackbone CLI (${cliVersion}) is older than the minimum supported by the control plane (${target}). Upgrade with \`
|
|
4862
|
+
logger.warn(`Your Stackbone CLI (${cliVersion}) is older than the minimum supported by the control plane (${target}). Upgrade with \`pnpm add -g @stackbone/cli@${channel}\`.`);
|
|
4850
4863
|
};
|
|
4851
4864
|
//#endregion
|
|
4852
4865
|
//#region src/infra/url.ts
|
|
@@ -4991,7 +5004,7 @@ var statusToError = (status, body) => {
|
|
|
4991
5004
|
default: code = "generic";
|
|
4992
5005
|
}
|
|
4993
5006
|
const inferredMessage = withIssueLines(typeof body === "object" && body !== null && "message" in body ? String(body.message) : `HTTP ${status}`, body);
|
|
4994
|
-
const serverUpgrade = readUpgradeCommand(body) ?? "
|
|
5007
|
+
const serverUpgrade = readUpgradeCommand(body) ?? "pnpm add -g @stackbone/cli";
|
|
4995
5008
|
const suggestion = code === "auth" ? "Run `stackbone login`" : code === "cli_upgrade_required" ? `Upgrade with \`${serverUpgrade}\`, then retry.` : void 0;
|
|
4996
5009
|
return new StackboneCliError({
|
|
4997
5010
|
code,
|
|
@@ -5273,7 +5286,7 @@ var formatUnsupportedPlatformBanner = (colors) => {
|
|
|
5273
5286
|
` ${dim("•")} Git Bash: open a Git Bash terminal and re-run the command`,
|
|
5274
5287
|
` ${dim("•")} WSL2:`,
|
|
5275
5288
|
` ${dim("1.")} wsl --install -d Ubuntu ${dim("# one-time, requires reboot")}`,
|
|
5276
|
-
` ${dim("2.")} (in the Ubuntu shell) pnpm
|
|
5289
|
+
` ${dim("2.")} (in the Ubuntu shell) pnpm add -g @stackbone/cli`,
|
|
5277
5290
|
` ${dim("3.")} stackbone <command>`,
|
|
5278
5291
|
""
|
|
5279
5292
|
].join("\n");
|
|
@@ -5391,7 +5404,7 @@ var isNewerVersion = (latest, current) => {
|
|
|
5391
5404
|
}
|
|
5392
5405
|
};
|
|
5393
5406
|
/** Discreet two-line banner, terminated with a newline. */
|
|
5394
|
-
var formatUpdateBanner = (current, latest, channel) => `\nA new release of the Stackbone CLI is available: ${current} → ${latest}\nUpgrade with:
|
|
5407
|
+
var formatUpdateBanner = (current, latest, channel) => `\nA new release of the Stackbone CLI is available: ${current} → ${latest}\nUpgrade with: pnpm add -g @stackbone/cli@${channel}\n`;
|
|
5395
5408
|
var isSilenced = (opts) => {
|
|
5396
5409
|
const { env } = opts;
|
|
5397
5410
|
if (env["STACKBONE_NO_UPDATE_CHECK"]) return true;
|
|
@@ -15739,7 +15752,7 @@ var emulatorCors = (allowlist) => cors({
|
|
|
15739
15752
|
var STACKBONE_CLI_BUILD_VERSION, EMULATOR_CAPABILITY_ROUTES, EMULATOR_CAPABILITIES, buildContractResponse;
|
|
15740
15753
|
var init_contract = __esmMin((() => {
|
|
15741
15754
|
init_src$19();
|
|
15742
|
-
STACKBONE_CLI_BUILD_VERSION = "0.5.
|
|
15755
|
+
STACKBONE_CLI_BUILD_VERSION = "0.5.4";
|
|
15743
15756
|
EMULATOR_CAPABILITY_ROUTES = {
|
|
15744
15757
|
"database.postgres_direct": "/api/db",
|
|
15745
15758
|
"rag.basic": "/api/rag",
|
|
@@ -23226,7 +23239,7 @@ var renderStudioStub = (input) => {
|
|
|
23226
23239
|
<li><b>GET</b> <a href="/api/docs">/api/docs</a> <span>· full API reference (Scalar, generated from this box's own route table)</span></li>
|
|
23227
23240
|
<li><b>GET</b> <a href="/api/contract">/api/contract</a> <span>· Stackbone Agent Protocol handshake</span></li>
|
|
23228
23241
|
<li><b>GET</b> <a href="/api/health">/api/health</a> <span>· emulator status</span></li>
|
|
23229
|
-
<li><b>GET</b> <a href="/api/services">/api/services</a> <span>· postgres / s3
|
|
23242
|
+
<li><b>GET</b> <a href="/api/services">/api/services</a> <span>· postgres / s3 endpoints, password removed (a deployed box asks for a credential)</span></li>
|
|
23230
23243
|
<li><b>GET</b> <a href="/api/discovery">/api/discovery</a> <span>· deep agents + workflows in this workspace</span></li>
|
|
23231
23244
|
<li><b>POST</b> <code>/openai/v1/chat/completions</code> <span>· chat with a deep agent (OpenAI-compatible; pick the agent via <code>model</code>)</span></li>
|
|
23232
23245
|
<li><b>POST</b> <code>/anthropic/v1/messages</code> <span>· chat with a deep agent (Anthropic-compatible)</span></li>
|
|
@@ -23239,19 +23252,71 @@ var renderStudioStub = (input) => {
|
|
|
23239
23252
|
<h2>Try it</h2>
|
|
23240
23253
|
<p>Chat with a deep agent from any OpenAI-compatible client — paste this base URL + any bearer key, and select the agent by its name in the <code>model</code> field:</p>
|
|
23241
23254
|
<p><code>curl -N -X POST ${e(input.emulatorUrl)}/openai/v1/chat/completions -H 'content-type: application/json' -H 'authorization: Bearer dev' -d '{"model":"<name>","messages":[{"role":"user","content":"hi"}],"stream":true}'</code></p>
|
|
23242
|
-
|
|
23243
|
-
<h2>Connections</h2>
|
|
23244
|
-
<ul>
|
|
23245
|
-
<li><b>Postgres</b> <span>· ${e(input.postgresUrl)}</span></li>
|
|
23246
|
-
<li><b>S3 (MinIO)</b> <span>· ${e(input.s3Endpoint)}</span></li>
|
|
23247
|
-
<li><b>Agent</b> <span>· ${e(input.agentUrl)}</span></li>
|
|
23248
|
-
</ul>
|
|
23249
23255
|
</main>
|
|
23250
23256
|
</body>
|
|
23251
23257
|
</html>
|
|
23252
23258
|
`;
|
|
23253
23259
|
};
|
|
23254
23260
|
//#endregion
|
|
23261
|
+
//#region ../../libs/runtime/workspace-runtime/src/lib/db/postgres-dsn.ts
|
|
23262
|
+
/** Query parameters libpq reads a password from. */
|
|
23263
|
+
var PASSWORD_PARAM = /^(ssl)?password=/i;
|
|
23264
|
+
/**
|
|
23265
|
+
* The DSN without the userinfo password and the `password` / `sslpassword`
|
|
23266
|
+
* query parameters; everything else stays byte for byte.
|
|
23267
|
+
*
|
|
23268
|
+
* Answers `''` when `URL` cannot parse it (a multi-host DSN) or misread the
|
|
23269
|
+
* userinfo (an unencoded `#`, `?` or `/` in the password), since the password
|
|
23270
|
+
* would survive in the raw string.
|
|
23271
|
+
*/
|
|
23272
|
+
var withoutPostgresPassword = (dsn) => {
|
|
23273
|
+
const url = URL.parse(dsn);
|
|
23274
|
+
if (url === null || url.username === "" && dsn.includes("@")) return "";
|
|
23275
|
+
url.password = "";
|
|
23276
|
+
url.search = url.search.slice(1).split("&").filter((pair) => !PASSWORD_PARAM.test(pair)).join("&");
|
|
23277
|
+
return url.toString();
|
|
23278
|
+
};
|
|
23279
|
+
//#endregion
|
|
23280
|
+
//#region ../../libs/runtime/workspace-runtime/src/lib/http/require-capability.ts
|
|
23281
|
+
var CAPABILITY_NATURE, CAPABILITY_GATE, requireCapability, capabilityGateOf, identityHolds;
|
|
23282
|
+
var init_require_capability = __esmMin((() => {
|
|
23283
|
+
init_box_auth_gate();
|
|
23284
|
+
CAPABILITY_NATURE = {
|
|
23285
|
+
"activity:read": "read",
|
|
23286
|
+
"db:query": "read",
|
|
23287
|
+
"logs:read": "read",
|
|
23288
|
+
"secrets:reveal": "read",
|
|
23289
|
+
"api-keys:manage": "write",
|
|
23290
|
+
"config:write": "write",
|
|
23291
|
+
"connections:manage": "write",
|
|
23292
|
+
"evals:run": "write",
|
|
23293
|
+
"hitl:decide": "write",
|
|
23294
|
+
"rag:write": "write",
|
|
23295
|
+
"runs:invoke": "write",
|
|
23296
|
+
"secrets:rotate": "write"
|
|
23297
|
+
};
|
|
23298
|
+
CAPABILITY_GATE = Symbol("stackbone.capabilityGate");
|
|
23299
|
+
requireCapability = (capability) => {
|
|
23300
|
+
const gate = async (c, next) => {
|
|
23301
|
+
const identity = readVerifiedIdentity(c);
|
|
23302
|
+
if (!identity) return next();
|
|
23303
|
+
if (!identityHolds(identity, capability)) return c.json({
|
|
23304
|
+
code: "forbidden",
|
|
23305
|
+
message: `This action requires the ${capability} capability.`
|
|
23306
|
+
}, 403);
|
|
23307
|
+
return next();
|
|
23308
|
+
};
|
|
23309
|
+
return Object.assign(gate, { [CAPABILITY_GATE]: capability });
|
|
23310
|
+
};
|
|
23311
|
+
capabilityGateOf = (handler) => {
|
|
23312
|
+
if (typeof handler !== "function") return void 0;
|
|
23313
|
+
if (CAPABILITY_GATE in handler) return handler[CAPABILITY_GATE];
|
|
23314
|
+
const composed = handler[COMPOSED_HANDLER];
|
|
23315
|
+
return composed === void 0 ? void 0 : capabilityGateOf(composed);
|
|
23316
|
+
};
|
|
23317
|
+
identityHolds = (identity, capability) => identity.caps?.includes(capability) ?? false;
|
|
23318
|
+
}));
|
|
23319
|
+
//#endregion
|
|
23255
23320
|
//#region ../../libs/runtime/workspace-runtime/src/lib/agent-contract/log-tail.ts
|
|
23256
23321
|
init_src$19();
|
|
23257
23322
|
function recordMatchesFilters(record, filters) {
|
|
@@ -25325,6 +25390,7 @@ var SimulatedMailbox = class {
|
|
|
25325
25390
|
from: draft.from,
|
|
25326
25391
|
to: [...draft.to],
|
|
25327
25392
|
body: draft.body,
|
|
25393
|
+
headers: [...draft.headers ?? []],
|
|
25328
25394
|
receivedAt: this.now().toISOString()
|
|
25329
25395
|
};
|
|
25330
25396
|
const sent = this.runs.get(runId) ?? [];
|
|
@@ -25423,7 +25489,8 @@ var OutlookDouble = class {
|
|
|
25423
25489
|
from: mailboxOwner(args),
|
|
25424
25490
|
to: recipients,
|
|
25425
25491
|
subject: textOf(message["subject"]),
|
|
25426
|
-
body: bodyContent(message["body"])
|
|
25492
|
+
body: bodyContent(message["body"]),
|
|
25493
|
+
headers: internetHeaders(message["internetMessageHeaders"])
|
|
25427
25494
|
});
|
|
25428
25495
|
return {
|
|
25429
25496
|
ok: true,
|
|
@@ -25446,7 +25513,8 @@ var OutlookDouble = class {
|
|
|
25446
25513
|
from: mailboxOwner(args),
|
|
25447
25514
|
to: [parent.from],
|
|
25448
25515
|
subject: replySubject(parent.subject),
|
|
25449
|
-
body
|
|
25516
|
+
body,
|
|
25517
|
+
headers: internetHeaders(draft["internetMessageHeaders"])
|
|
25450
25518
|
});
|
|
25451
25519
|
return {
|
|
25452
25520
|
ok: true,
|
|
@@ -25486,9 +25554,18 @@ function toGraphMessage(message) {
|
|
|
25486
25554
|
},
|
|
25487
25555
|
from: { emailAddress: { address: message.from } },
|
|
25488
25556
|
toRecipients: message.to.map((address) => ({ emailAddress: { address } })),
|
|
25489
|
-
receivedDateTime: message.receivedAt
|
|
25557
|
+
receivedDateTime: message.receivedAt,
|
|
25558
|
+
internetMessageHeaders: message.headers.length > 0 ? message.headers.map((h) => ({ ...h })) : null
|
|
25490
25559
|
};
|
|
25491
25560
|
}
|
|
25561
|
+
/** `[{ name, value }]` → the headers, dropping malformed entries. */
|
|
25562
|
+
function internetHeaders(value) {
|
|
25563
|
+
if (!Array.isArray(value)) return [];
|
|
25564
|
+
return value.map((entry) => ({
|
|
25565
|
+
name: textOf(asObject$2(entry)["name"]),
|
|
25566
|
+
value: textOf(asObject$2(entry)["value"])
|
|
25567
|
+
})).filter((header) => header.name !== "");
|
|
25568
|
+
}
|
|
25492
25569
|
/**
|
|
25493
25570
|
* Whose mailbox this is: Graph names it in the `{user-id}` path segment, so the
|
|
25494
25571
|
* double sends AS that address, the way the real provider would.
|
|
@@ -26037,46 +26114,6 @@ var isMutationOutsideCatalog = (method, path) => matches(MUTATIONS_OUTSIDE_CATAL
|
|
|
26037
26114
|
/** Whether `method`+`path` is a read that happens to travel as POST. Exact, never a prefix. */
|
|
26038
26115
|
var isPostShapedRead = (method, path) => matches(POST_SHAPED_READS, method, path);
|
|
26039
26116
|
//#endregion
|
|
26040
|
-
//#region ../../libs/runtime/workspace-runtime/src/lib/http/require-capability.ts
|
|
26041
|
-
var CAPABILITY_NATURE, CAPABILITY_GATE, requireCapability, capabilityGateOf, identityHolds;
|
|
26042
|
-
var init_require_capability = __esmMin((() => {
|
|
26043
|
-
init_box_auth_gate();
|
|
26044
|
-
CAPABILITY_NATURE = {
|
|
26045
|
-
"activity:read": "read",
|
|
26046
|
-
"db:query": "read",
|
|
26047
|
-
"logs:read": "read",
|
|
26048
|
-
"secrets:reveal": "read",
|
|
26049
|
-
"api-keys:manage": "write",
|
|
26050
|
-
"config:write": "write",
|
|
26051
|
-
"connections:manage": "write",
|
|
26052
|
-
"evals:run": "write",
|
|
26053
|
-
"hitl:decide": "write",
|
|
26054
|
-
"rag:write": "write",
|
|
26055
|
-
"runs:invoke": "write",
|
|
26056
|
-
"secrets:rotate": "write"
|
|
26057
|
-
};
|
|
26058
|
-
CAPABILITY_GATE = Symbol("stackbone.capabilityGate");
|
|
26059
|
-
requireCapability = (capability) => {
|
|
26060
|
-
const gate = async (c, next) => {
|
|
26061
|
-
const identity = readVerifiedIdentity(c);
|
|
26062
|
-
if (!identity) return next();
|
|
26063
|
-
if (!identityHolds(identity, capability)) return c.json({
|
|
26064
|
-
code: "forbidden",
|
|
26065
|
-
message: `This action requires the ${capability} capability.`
|
|
26066
|
-
}, 403);
|
|
26067
|
-
return next();
|
|
26068
|
-
};
|
|
26069
|
-
return Object.assign(gate, { [CAPABILITY_GATE]: capability });
|
|
26070
|
-
};
|
|
26071
|
-
capabilityGateOf = (handler) => {
|
|
26072
|
-
if (typeof handler !== "function") return void 0;
|
|
26073
|
-
if (CAPABILITY_GATE in handler) return handler[CAPABILITY_GATE];
|
|
26074
|
-
const composed = handler[COMPOSED_HANDLER];
|
|
26075
|
-
return composed === void 0 ? void 0 : capabilityGateOf(composed);
|
|
26076
|
-
};
|
|
26077
|
-
identityHolds = (identity, capability) => identity.caps?.includes(capability) ?? false;
|
|
26078
|
-
}));
|
|
26079
|
-
//#endregion
|
|
26080
26117
|
//#region ../../libs/runtime/workspace-runtime/src/lib/server/route-registry-generation.ts
|
|
26081
26118
|
var generations = /* @__PURE__ */ new WeakMap();
|
|
26082
26119
|
/** The generation `app`'s registry is at. Zero until something bumps it. */
|
|
@@ -72090,6 +72127,7 @@ init_invocation_guard();
|
|
|
72090
72127
|
init_well_known();
|
|
72091
72128
|
init_contract();
|
|
72092
72129
|
init_error_boundary();
|
|
72130
|
+
init_require_capability();
|
|
72093
72131
|
init_route_helpers();
|
|
72094
72132
|
init_resource_ledger();
|
|
72095
72133
|
init_teardown_order();
|
|
@@ -72137,11 +72175,12 @@ var SUBSYSTEM_HEALTH = named("SubsystemHealth", z.object({
|
|
|
72137
72175
|
* spell out (a shape a SECOND author reads goes to `@stackbone/validators` beside
|
|
72138
72176
|
* its surface) has nothing to bind here.
|
|
72139
72177
|
*
|
|
72140
|
-
*
|
|
72141
|
-
*
|
|
72178
|
+
* No secret is in it. `s3SecretKey` is on the endpoints object this reads from,
|
|
72179
|
+
* and the answer carries the access key alone; the Postgres DSN goes out through
|
|
72180
|
+
* {@link withoutPostgresPassword} (PLA-1754).
|
|
72142
72181
|
*/
|
|
72143
72182
|
var boxServicesSchema = z.object({
|
|
72144
|
-
postgres: z.object({ url: z.string().openapi({ description: "The Postgres DSN the agent was given,
|
|
72183
|
+
postgres: z.object({ url: z.string().openapi({ description: "The Postgres DSN the agent was given, with the password removed. Under `stackbone dev` it points at the local stack; on a deployed box it is whatever the operator configured. Empty when the box cannot tell where a password in it ends." }) }),
|
|
72145
72184
|
storage: z.object({
|
|
72146
72185
|
endpoint: z.string().openapi({ description: "The S3-compatible endpoint the agent writes to." }),
|
|
72147
72186
|
bucket: z.string(),
|
|
@@ -72190,10 +72229,7 @@ var buildEmulatorApp = (opts, hooks = {}) => {
|
|
|
72190
72229
|
app.get("/", (c) => c.html(renderStudioStub({
|
|
72191
72230
|
agentSlug: opts.agent.slug,
|
|
72192
72231
|
agentName: opts.agent.name,
|
|
72193
|
-
emulatorUrl:
|
|
72194
|
-
agentUrl: ctx.agentBaseUrl,
|
|
72195
|
-
postgresUrl: opts.injection.services.postgresUrl,
|
|
72196
|
-
s3Endpoint: opts.injection.services.s3Endpoint
|
|
72232
|
+
emulatorUrl: new URL(c.req.url).origin
|
|
72197
72233
|
})));
|
|
72198
72234
|
const hostKind = opts.host;
|
|
72199
72235
|
app.openapi(createRoute({
|
|
@@ -72258,15 +72294,20 @@ var buildEmulatorApp = (opts, hooks = {}) => {
|
|
|
72258
72294
|
checks
|
|
72259
72295
|
}, 503);
|
|
72260
72296
|
});
|
|
72297
|
+
const servicesPostgresUrl = withoutPostgresPassword(opts.injection.services.postgresUrl);
|
|
72261
72298
|
app.openapi(createRoute({
|
|
72262
72299
|
method: "get",
|
|
72263
72300
|
path: "/api/services",
|
|
72264
72301
|
tags: [LIFECYCLE_TAG],
|
|
72302
|
+
middleware: [requireCapability("db:query")],
|
|
72265
72303
|
summary: "Injected service endpoints",
|
|
72266
|
-
description: "The Postgres, object-storage and agent-process endpoints this box injected into the agent, read back. A creator uses it to point `psql` or an S3 client at the same stack their agent is running against.
|
|
72267
|
-
responses: {
|
|
72304
|
+
description: "The Postgres, object-storage and agent-process endpoints this box injected into the agent, read back. A creator uses it to point `psql` or an S3 client at the same stack their agent is running against. No secret is in the answer: the Postgres password is removed from the DSN, and the storage secret key is left out (the access key is in).",
|
|
72305
|
+
responses: {
|
|
72306
|
+
200: jsonResponse(boxServicesSchema, "The endpoints this box handed the agent."),
|
|
72307
|
+
403: errorResponse("The caller's role does not grant `db:query`. Only a browser-direct identity token carries a role; a signed call and the self-host operator token are not checked against one.")
|
|
72308
|
+
}
|
|
72268
72309
|
}), (c) => c.json({
|
|
72269
|
-
postgres: { url:
|
|
72310
|
+
postgres: { url: servicesPostgresUrl },
|
|
72270
72311
|
storage: {
|
|
72271
72312
|
endpoint: opts.injection.services.s3Endpoint,
|
|
72272
72313
|
bucket: opts.injection.services.s3Bucket,
|
|
@@ -73665,7 +73706,7 @@ var createAddCommand = defineGroup({
|
|
|
73665
73706
|
}),
|
|
73666
73707
|
workflow: defineVerb({
|
|
73667
73708
|
name: "workflow",
|
|
73668
|
-
description: "Add a durable workflow
|
|
73709
|
+
description: "Add a durable workflow to the current workspace (offline scaffold).",
|
|
73669
73710
|
args: {
|
|
73670
73711
|
name: {
|
|
73671
73712
|
type: "positional",
|
|
@@ -76803,8 +76844,15 @@ var REDIS_IMAGE$1 = "redis:7-alpine";
|
|
|
76803
76844
|
*
|
|
76804
76845
|
* Same image and pin as `stackbone dev`'s compose, so the emulator and the
|
|
76805
76846
|
* deliverable run the identical backend.
|
|
76847
|
+
*
|
|
76848
|
+
* The community fork, not `minio/minio`. MinIO closed anonymous pulls of its own
|
|
76849
|
+
* image on Docker Hub (2026-09-12, PLA-1423) and then on quay.io (401 on every
|
|
76850
|
+
* tag by 2026-09-25, PLA-1794). A first-run `stackbone dev`, and the operator
|
|
76851
|
+
* running a packaged folder, pull with no credentials. `pgsty/minio` publishes
|
|
76852
|
+
* amd64 + arm64, keeps the upstream entrypoint and ships `mc` and `sh`, which the
|
|
76853
|
+
* healthcheck and the bucket-init container call.
|
|
76806
76854
|
*/
|
|
76807
|
-
var MINIO_IMAGE
|
|
76855
|
+
var MINIO_IMAGE = "pgsty/minio:RELEASE.2026-08-04T00-00-00Z";
|
|
76808
76856
|
/** Access key the bundled MinIO is created with. The secret is generated per package. */
|
|
76809
76857
|
var MINIO_ACCESS_KEY = "stackbone";
|
|
76810
76858
|
/** Bucket the init container creates, and the agent's `STACKBONE_S3_BUCKET`. */
|
|
@@ -76902,7 +76950,7 @@ var renderInfraServices = () => ` postgres:
|
|
|
76902
76950
|
minio:
|
|
76903
76951
|
# S3-compatible object storage: the agent's own files (\`client.storage\`), the
|
|
76904
76952
|
# originals managed RAG ingests, and what Studio's file browser lists.
|
|
76905
|
-
image: ${MINIO_IMAGE
|
|
76953
|
+
image: ${MINIO_IMAGE}
|
|
76906
76954
|
command: ['server', '/data']
|
|
76907
76955
|
environment:
|
|
76908
76956
|
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY}
|
|
@@ -76945,7 +76993,7 @@ var renderInfraServices = () => ` postgres:
|
|
|
76945
76993
|
# alphabet contains \`-\`, so about one packaged folder in 64 minted one. One
|
|
76946
76994
|
# of them took \`next\` red on 2026-08-31 (run 33430510314) on a
|
|
76947
76995
|
# comments-only commit.
|
|
76948
|
-
image: ${MINIO_IMAGE
|
|
76996
|
+
image: ${MINIO_IMAGE}
|
|
76949
76997
|
restart: 'no'
|
|
76950
76998
|
depends_on:
|
|
76951
76999
|
minio:
|
|
@@ -80114,11 +80162,23 @@ var EVAL_RUN_SCREEN_PATH = "/studio/eval-runs";
|
|
|
80114
80162
|
/**
|
|
80115
80163
|
* Which Studio a given control plane belongs to.
|
|
80116
80164
|
*
|
|
80117
|
-
* One
|
|
80165
|
+
* One resolver, in one place: `stackbone dev` prints it in its banner and
|
|
80118
80166
|
* `stackbone eval` prints it beside a run's result, and two copies would drift
|
|
80119
80167
|
* the first time either host changed.
|
|
80168
|
+
*
|
|
80169
|
+
* `STACKBONE_WEB_URL` (the web origin, no `/app`) wins when set, even against a
|
|
80170
|
+
* cloud API: `scripts/worktree-env.sh` exports it next to `WEB_PORT`, because a
|
|
80171
|
+
* worktree with a port offset serves its web on `4200 + offset*10` and the
|
|
80172
|
+
* literal 4200 would open another checkout's web. The `STACKBONE_` prefix keeps
|
|
80173
|
+
* it apart from an `APP_URL` a creator's workspace `.env` defines for its own
|
|
80174
|
+
* app. Unset, a local control plane maps to `localhost:4200`, anything else to
|
|
80175
|
+
* the cloud Studio.
|
|
80120
80176
|
*/
|
|
80121
|
-
var resolveStudioBaseUrl = (apiUrl
|
|
80177
|
+
var resolveStudioBaseUrl = (apiUrl, env = {}) => {
|
|
80178
|
+
const webUrl = env["STACKBONE_WEB_URL"]?.trim().replace(/\/+$/, "");
|
|
80179
|
+
if (webUrl) return `${webUrl}/app`;
|
|
80180
|
+
return apiUrl.includes("localhost") ? "http://localhost:4200/app" : "https://app.stackbone.ai/app";
|
|
80181
|
+
};
|
|
80122
80182
|
/**
|
|
80123
80183
|
* The link to one eval run, with NO `?stackbone-dev=` pin.
|
|
80124
80184
|
*
|
|
@@ -80173,7 +80233,6 @@ var readStackboneConfig = async (cwd) => readJsonFile(stackboneConfigPath(cwd),
|
|
|
80173
80233
|
//#region src/dev/compose-template.ts
|
|
80174
80234
|
var POSTGRES_IMAGE = "pgvector/pgvector:pg17";
|
|
80175
80235
|
var REDIS_IMAGE = "redis:7-alpine";
|
|
80176
|
-
var MINIO_IMAGE = "quay.io/minio/minio:RELEASE.2025-03-12T18-04-18Z";
|
|
80177
80236
|
var PGVECTOR_INIT_SQL = `-- Generated by stackbone dev. Runs once on Postgres first boot via
|
|
80178
80237
|
-- /docker-entrypoint-initdb.d. The queue extension is gone (BullMQ over Redis
|
|
80179
80238
|
-- is the only job engine); only pgvector is enabled here.
|
|
@@ -81827,7 +81886,13 @@ var getAgentByOwnerAndSlug = async (ctx, ownerOrgSlug, templateSlug) => {
|
|
|
81827
81886
|
init_src$19();
|
|
81828
81887
|
var GRANT_PATH = "/api/dev-tunnel/grants";
|
|
81829
81888
|
var DEFAULT_STATUS_URL = "https://status.stackbone.ai";
|
|
81830
|
-
var
|
|
81889
|
+
var SIDE_CALL_TIMEOUT_MS = 3e3;
|
|
81890
|
+
var REVOCATIONS_PATH = "/api/dev-tunnel/revocations";
|
|
81891
|
+
var cliHeaders = (cliVersion) => ({
|
|
81892
|
+
Accept: "application/json",
|
|
81893
|
+
"User-Agent": `stackbone-cli/${cliVersion}`
|
|
81894
|
+
});
|
|
81895
|
+
var cliGrantResponseSchema = devTunnelGrantResponseSchema.extend({ ticket: devTunnelGrantResponseSchema.shape.ticket.optional() });
|
|
81831
81896
|
/** Relay status page from the canonical envelope. */
|
|
81832
81897
|
var readRelayStatusUrl = (body) => {
|
|
81833
81898
|
if (typeof body !== "object" || body === null) return null;
|
|
@@ -81849,17 +81914,17 @@ var requestDevTunnelGrant = async (input) => {
|
|
|
81849
81914
|
const url = composeUrl(input.ctx.apiUrl, GRANT_PATH);
|
|
81850
81915
|
const body = {
|
|
81851
81916
|
agentSlug: input.agentSlug,
|
|
81852
|
-
...input.ttlSeconds !== void 0 ? { ttlSeconds: input.ttlSeconds } : {}
|
|
81917
|
+
...input.ttlSeconds !== void 0 ? { ttlSeconds: input.ttlSeconds } : {},
|
|
81918
|
+
...input.installationId !== void 0 ? { installationId: input.installationId } : {}
|
|
81853
81919
|
};
|
|
81854
81920
|
let response;
|
|
81855
81921
|
try {
|
|
81856
81922
|
response = await fetchImpl(url, {
|
|
81857
81923
|
method: "POST",
|
|
81858
81924
|
headers: {
|
|
81925
|
+
...cliHeaders(input.ctx.cliVersion),
|
|
81859
81926
|
"Content-Type": "application/json",
|
|
81860
|
-
|
|
81861
|
-
Authorization: `Bearer ${session.session.token}`,
|
|
81862
|
-
"User-Agent": `stackbone-cli/${input.ctx.cliVersion}`
|
|
81927
|
+
Authorization: `Bearer ${session.session.token}`
|
|
81863
81928
|
},
|
|
81864
81929
|
body: JSON.stringify(body)
|
|
81865
81930
|
});
|
|
@@ -81904,7 +81969,7 @@ var requestDevTunnelGrant = async (input) => {
|
|
|
81904
81969
|
message: `Unexpected ${response.status} response from ${GRANT_PATH}`,
|
|
81905
81970
|
cause: parsedBody
|
|
81906
81971
|
});
|
|
81907
|
-
return parseResponse$1(
|
|
81972
|
+
return parseResponse$1(cliGrantResponseSchema, parsedBody, GRANT_PATH);
|
|
81908
81973
|
};
|
|
81909
81974
|
var revokeDevTunnelGrant = async (input) => {
|
|
81910
81975
|
const session = await input.ctx.tokenStore.activeSession(input.ctx.apiUrl).catch(() => null);
|
|
@@ -81919,11 +81984,10 @@ var revokeDevTunnelGrant = async (input) => {
|
|
|
81919
81984
|
response = await fetchImpl(url, {
|
|
81920
81985
|
method: "DELETE",
|
|
81921
81986
|
headers: {
|
|
81922
|
-
|
|
81923
|
-
Authorization: `Bearer ${session.session.token}
|
|
81924
|
-
"User-Agent": `stackbone-cli/${input.ctx.cliVersion}`
|
|
81987
|
+
...cliHeaders(input.ctx.cliVersion),
|
|
81988
|
+
Authorization: `Bearer ${session.session.token}`
|
|
81925
81989
|
},
|
|
81926
|
-
signal: AbortSignal.timeout(
|
|
81990
|
+
signal: AbortSignal.timeout(SIDE_CALL_TIMEOUT_MS)
|
|
81927
81991
|
});
|
|
81928
81992
|
} catch (err) {
|
|
81929
81993
|
input.ctx.logger.warn({
|
|
@@ -81940,9 +82004,29 @@ var revokeDevTunnelGrant = async (input) => {
|
|
|
81940
82004
|
subdomain: input.subdomain
|
|
81941
82005
|
}, "Unexpected response while revoking dev-tunnel grant (slot will free on TTL)");
|
|
81942
82006
|
};
|
|
82007
|
+
var fetchDevTunnelRevocationList = async (input) => {
|
|
82008
|
+
const fetchImpl = input.fetchImpl ?? fetch;
|
|
82009
|
+
const url = composeUrl(input.ctx.apiUrl, REVOCATIONS_PATH);
|
|
82010
|
+
try {
|
|
82011
|
+
const response = await fetchImpl(url, {
|
|
82012
|
+
method: "GET",
|
|
82013
|
+
headers: cliHeaders(input.ctx.cliVersion),
|
|
82014
|
+
signal: AbortSignal.timeout(SIDE_CALL_TIMEOUT_MS)
|
|
82015
|
+
});
|
|
82016
|
+
if (!response.ok) {
|
|
82017
|
+
input.ctx.logger.debug({ status: response.status }, "dev-tunnel revocation list request was not answered 200");
|
|
82018
|
+
return null;
|
|
82019
|
+
}
|
|
82020
|
+
const parsed = devTunnelRevocationListResponseSchema.safeParse(await response.json());
|
|
82021
|
+
return parsed.success ? parsed.data.revocationList : null;
|
|
82022
|
+
} catch (err) {
|
|
82023
|
+
input.ctx.logger.debug({ err }, "Could not fetch the dev-tunnel revocation list");
|
|
82024
|
+
return null;
|
|
82025
|
+
}
|
|
82026
|
+
};
|
|
81943
82027
|
//#endregion
|
|
81944
82028
|
//#region src/dev/frpc-bin.ts
|
|
81945
|
-
var
|
|
82029
|
+
var execFilep = promisify(execFile);
|
|
81946
82030
|
var PINNED_FRPC_VERSION = "0.68.1";
|
|
81947
82031
|
var assetForPlatform = (platform, arch) => {
|
|
81948
82032
|
const version = PINNED_FRPC_VERSION;
|
|
@@ -81991,9 +82075,27 @@ var streamToFile = async (body, path) => {
|
|
|
81991
82075
|
});
|
|
81992
82076
|
};
|
|
81993
82077
|
var archiveStem = (asset) => asset.name.replace(/\.(tar\.gz|zip)$/, "");
|
|
82078
|
+
var psQuote = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
82079
|
+
var extractCommand = (archive, archivePath, destDir) => archive === "tgz" ? {
|
|
82080
|
+
file: "tar",
|
|
82081
|
+
args: [
|
|
82082
|
+
"-xzf",
|
|
82083
|
+
archivePath,
|
|
82084
|
+
"-C",
|
|
82085
|
+
destDir
|
|
82086
|
+
]
|
|
82087
|
+
} : {
|
|
82088
|
+
file: "powershell",
|
|
82089
|
+
args: [
|
|
82090
|
+
"-NoProfile",
|
|
82091
|
+
"-NonInteractive",
|
|
82092
|
+
"-Command",
|
|
82093
|
+
`Expand-Archive -LiteralPath ${psQuote(archivePath)} -DestinationPath ${psQuote(destDir)} -Force`
|
|
82094
|
+
]
|
|
82095
|
+
};
|
|
81994
82096
|
var extractTarball = async (archivePath, asset, destBinary) => {
|
|
81995
|
-
|
|
81996
|
-
|
|
82097
|
+
const { file, args } = extractCommand(asset.archive, archivePath, cacheDir());
|
|
82098
|
+
await execFilep(file, args);
|
|
81997
82099
|
await rename(join(cacheDir(), archiveStem(asset), process.platform === "win32" ? "frpc.exe" : "frpc"), destBinary);
|
|
81998
82100
|
};
|
|
81999
82101
|
var resolveFrpcBin = async (input) => {
|
|
@@ -82016,24 +82118,27 @@ var resolveFrpcBin = async (input) => {
|
|
|
82016
82118
|
const archivePath = `${cached}.${asset.archive === "tgz" ? "tar.gz" : "zip"}`;
|
|
82017
82119
|
const s = spinner();
|
|
82018
82120
|
s.start(`Downloading frpc ${version} (${asset.name})`);
|
|
82121
|
+
let stage = "download";
|
|
82019
82122
|
try {
|
|
82020
82123
|
const res = await fetch(url, { redirect: "follow" });
|
|
82021
82124
|
if (!res.ok || !res.body) throw new Error(`HTTP ${res.status} ${res.statusText} fetching ${url}`);
|
|
82022
82125
|
await streamToFile(res.body, archivePath);
|
|
82126
|
+
stage = "extract";
|
|
82023
82127
|
await extractTarball(archivePath, asset, cached);
|
|
82024
82128
|
if (process.platform !== "win32") await chmod(cached, 493);
|
|
82025
82129
|
s.stop(`Downloaded frpc ${version}`);
|
|
82026
82130
|
return cached;
|
|
82027
82131
|
} catch (err) {
|
|
82028
|
-
s.error("Failed to download frpc");
|
|
82132
|
+
s.error(stage === "download" ? "Failed to download frpc" : "Failed to extract frpc");
|
|
82029
82133
|
await Promise.all([unlink(cached).catch(() => void 0), rm(join(cacheDir(), archiveStem(asset)), {
|
|
82030
82134
|
recursive: true,
|
|
82031
82135
|
force: true
|
|
82032
82136
|
}).catch(() => void 0)]);
|
|
82137
|
+
const manualInstall = `download frp ${version} from https://github.com/fatedier/frp/releases, and put its frpc binary at ${cached} (or point STACKBONE_FRPC_BIN at it) before re-running.`;
|
|
82033
82138
|
throw new StackboneCliError({
|
|
82034
82139
|
code: "generic",
|
|
82035
|
-
message: "Could not
|
|
82036
|
-
suggestion: "Check your network, or
|
|
82140
|
+
message: stage === "download" ? `Could not download frpc ${version}` : `Downloaded frpc ${version} but could not extract it`,
|
|
82141
|
+
suggestion: stage === "download" ? `Check your network, or ${manualInstall}` : `Extract ${asset.name} by hand: ${manualInstall}`,
|
|
82037
82142
|
cause: err
|
|
82038
82143
|
});
|
|
82039
82144
|
} finally {
|
|
@@ -82109,10 +82214,85 @@ var clearLastTunnelGrant = async (projectRoot, logger) => {
|
|
|
82109
82214
|
}
|
|
82110
82215
|
};
|
|
82111
82216
|
//#endregion
|
|
82217
|
+
//#region src/dev/tunnel-drop.ts
|
|
82218
|
+
init_src$19();
|
|
82219
|
+
var BASE64URL_SEGMENT = /^[A-Za-z0-9_-]+$/;
|
|
82220
|
+
var decodeJwtPayload = (token) => {
|
|
82221
|
+
const segments = token.split(".");
|
|
82222
|
+
const payload = segments[1];
|
|
82223
|
+
if (segments.length !== 3 || payload === void 0 || !BASE64URL_SEGMENT.test(payload)) return null;
|
|
82224
|
+
try {
|
|
82225
|
+
const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
82226
|
+
return typeof claims === "object" && claims !== null && !Array.isArray(claims) ? claims : null;
|
|
82227
|
+
} catch {
|
|
82228
|
+
return null;
|
|
82229
|
+
}
|
|
82230
|
+
};
|
|
82231
|
+
var MAX_EPOCH_SECONDS = 1e10;
|
|
82232
|
+
var readTicketClaims = (ticket) => {
|
|
82233
|
+
const claims = decodeJwtPayload(ticket);
|
|
82234
|
+
if (!claims) return null;
|
|
82235
|
+
const { jti, exp } = claims;
|
|
82236
|
+
if (typeof jti !== "string" || jti.length === 0) return null;
|
|
82237
|
+
if (typeof exp !== "number" || !Number.isFinite(exp) || exp <= 0 || exp >= MAX_EPOCH_SECONDS) return null;
|
|
82238
|
+
return {
|
|
82239
|
+
jti,
|
|
82240
|
+
exp
|
|
82241
|
+
};
|
|
82242
|
+
};
|
|
82243
|
+
var readRevokedIds = (revocationList) => {
|
|
82244
|
+
const claims = decodeJwtPayload(revocationList);
|
|
82245
|
+
return claims && Array.isArray(claims["revoked"]) ? claims["revoked"] : null;
|
|
82246
|
+
};
|
|
82247
|
+
var classifyTunnelDrop = async (input) => {
|
|
82248
|
+
const claims = input.ticket === void 0 ? null : readTicketClaims(input.ticket);
|
|
82249
|
+
if (!claims) return "other";
|
|
82250
|
+
if (input.nowMs / 1e3 > claims.exp) return "cap";
|
|
82251
|
+
const list = await input.fetchRevocationList();
|
|
82252
|
+
return (list === null ? null : readRevokedIds(list))?.some((jti) => jti === claims.jti) ? "revoked" : "other";
|
|
82253
|
+
};
|
|
82254
|
+
var CAP_HOURS = DEV_TUNNEL_GRANT_HARD_CAP_SECONDS / 3600;
|
|
82255
|
+
var RESTART_HINT = "`stackbone dev` is still running, but its public URL no longer works. Restart `stackbone dev` to get a new URL.";
|
|
82256
|
+
var describeExit = (code, signal) => code !== null ? `code ${code}` : `signal ${signal ?? "unknown"}`;
|
|
82257
|
+
var describeCause = (input) => {
|
|
82258
|
+
if (input.cause === "cap") return [`The dev tunnel closed: its ticket reached the ${CAP_HOURS}-hour limit.`];
|
|
82259
|
+
if (input.cause === "revoked") return ["The dev tunnel closed: the control plane revoked its grant."];
|
|
82260
|
+
const tail = input.lastOutput.trim();
|
|
82261
|
+
return [`The dev tunnel closed: frpc exited with ${describeExit(input.code, input.signal)}.`, ...tail.length > 0 ? ["last frpc output:", tail] : []];
|
|
82262
|
+
};
|
|
82263
|
+
var reportTunnelDrop = async (input, code, signal) => {
|
|
82264
|
+
const cause = await classifyTunnelDrop({
|
|
82265
|
+
ticket: input.ticket,
|
|
82266
|
+
nowMs: Date.now(),
|
|
82267
|
+
fetchRevocationList: () => fetchDevTunnelRevocationList({ ctx: input.ctx })
|
|
82268
|
+
}).catch(() => "other");
|
|
82269
|
+
try {
|
|
82270
|
+
const lines = describeCause({
|
|
82271
|
+
cause,
|
|
82272
|
+
code,
|
|
82273
|
+
signal,
|
|
82274
|
+
lastOutput: input.lastOutput()
|
|
82275
|
+
});
|
|
82276
|
+
input.ctx.logger.warn({
|
|
82277
|
+
cause,
|
|
82278
|
+
code,
|
|
82279
|
+
signal
|
|
82280
|
+
}, [...lines, RESTART_HINT].join("\n"));
|
|
82281
|
+
} catch {}
|
|
82282
|
+
};
|
|
82283
|
+
var watchTunnelDrop = (input) => {
|
|
82284
|
+
input.child.once("close", (code, signal) => {
|
|
82285
|
+
if (input.isStopping()) return;
|
|
82286
|
+
reportTunnelDrop(input, code, signal);
|
|
82287
|
+
});
|
|
82288
|
+
};
|
|
82289
|
+
//#endregion
|
|
82112
82290
|
//#region src/dev/tunnel.ts
|
|
82291
|
+
init_src$19();
|
|
82113
82292
|
var STARTUP_TIMEOUT_MS = 3e4;
|
|
82114
82293
|
var STOP_GRACE_MS = 3e3;
|
|
82115
82294
|
var TAIL_BYTES = 4 * 1024;
|
|
82295
|
+
var FRPC_HEARTBEAT_INTERVAL_SECONDS = 30;
|
|
82116
82296
|
var TUNNEL_HOST_SUFFIX = "tun.stackbone.ai";
|
|
82117
82297
|
var TUNNEL_OPEN_DEFAULT_ATTEMPTS = 3;
|
|
82118
82298
|
var TUNNEL_OPEN_DEFAULT_RETRY_DELAY_MS = 2500;
|
|
@@ -82125,13 +82305,21 @@ var frpcNotInstalledError = (cause) => new StackboneCliError({
|
|
|
82125
82305
|
suggestion: "Install frpc from https://github.com/fatedier/frp/releases, or set STACKBONE_FRPC_BIN to point at an existing binary. The tunnel is mandatory — there is no opt-out flag.",
|
|
82126
82306
|
cause
|
|
82127
82307
|
});
|
|
82308
|
+
var frpcBlockedError = (binPath, cause) => new StackboneCliError({
|
|
82309
|
+
code: "permission",
|
|
82310
|
+
message: `The OS refused to run frpc (${binPath})`,
|
|
82311
|
+
suggestion: process.platform === "win32" ? "Windows is blocking the binary. Unblock it (right-click > Properties > Unblock), check Smart App Control and Defender exclusions, or run the CLI from WSL2. See https://docs.stackbone.ai/docs/cli/guides/windows" : `Make it executable with \`chmod +x ${binPath}\`, or point STACKBONE_FRPC_BIN at a binary you can run.`,
|
|
82312
|
+
cause
|
|
82313
|
+
});
|
|
82128
82314
|
var buildFrpcConfig = (grant, targetUrl) => {
|
|
82129
82315
|
const port = targetUrl.port ? Number.parseInt(targetUrl.port, 10) : targetUrl.protocol === "https:" ? 443 : 80;
|
|
82316
|
+
const ticketLines = grant.ticket === void 0 ? [] : [`transport.heartbeatInterval = ${FRPC_HEARTBEAT_INTERVAL_SECONDS}`, `metadatas.ticket = "${grant.ticket}"`];
|
|
82130
82317
|
return [
|
|
82131
82318
|
`serverAddr = "${grant.relayHost}"`,
|
|
82132
82319
|
`serverPort = ${grant.relayPort}`,
|
|
82133
82320
|
`auth.method = "token"`,
|
|
82134
82321
|
`auth.token = "${grant.frpcToken}"`,
|
|
82322
|
+
...ticketLines,
|
|
82135
82323
|
"",
|
|
82136
82324
|
"[[proxies]]",
|
|
82137
82325
|
`name = "stackbone-dev-${grant.subdomain}"`,
|
|
@@ -82182,7 +82370,7 @@ var spawnFrpc = async (input) => {
|
|
|
82182
82370
|
child.once("exit", () => process.off("exit", killOnParentExit));
|
|
82183
82371
|
const tail = createTail();
|
|
82184
82372
|
const successRegex = /start proxy success/i;
|
|
82185
|
-
const expectedPublicUrl =
|
|
82373
|
+
const expectedPublicUrl = buildCanonicalTunnelUrl(input.expectedSubdomain, TUNNEL_HOST_SUFFIX);
|
|
82186
82374
|
const publicUrl = await new Promise((resolve, reject) => {
|
|
82187
82375
|
const timer = setTimeout(() => {
|
|
82188
82376
|
child.kill("SIGINT");
|
|
@@ -82209,6 +82397,10 @@ var spawnFrpc = async (input) => {
|
|
|
82209
82397
|
reject(frpcNotInstalledError(err));
|
|
82210
82398
|
return;
|
|
82211
82399
|
}
|
|
82400
|
+
if (err.code === "EACCES" || err.code === "EPERM") {
|
|
82401
|
+
reject(frpcBlockedError(input.binPath, err));
|
|
82402
|
+
return;
|
|
82403
|
+
}
|
|
82212
82404
|
reject(new StackboneCliError({
|
|
82213
82405
|
code: "generic",
|
|
82214
82406
|
message: "frpc failed to start",
|
|
@@ -82235,7 +82427,8 @@ var spawnFrpc = async (input) => {
|
|
|
82235
82427
|
});
|
|
82236
82428
|
return {
|
|
82237
82429
|
child,
|
|
82238
|
-
publicUrl
|
|
82430
|
+
publicUrl,
|
|
82431
|
+
lastOutput: () => tail.snapshot()
|
|
82239
82432
|
};
|
|
82240
82433
|
};
|
|
82241
82434
|
var stopChild = async (child) => {
|
|
@@ -82268,7 +82461,8 @@ var stopChild = async (child) => {
|
|
|
82268
82461
|
* `auth_required`, `workspace_tunnel_cap_reached`, or
|
|
82269
82462
|
* `tunnel_relay_unhealthy` to the thrown StackboneCliError before
|
|
82270
82463
|
* propagation, and `spawnFrpc` adds the captured output tail when frpc
|
|
82271
|
-
* dies before opening the tunnel.
|
|
82464
|
+
* dies before opening the tunnel. A frpc exit after the tunnel is up never
|
|
82465
|
+
* throws: `watchTunnelDrop` logs its cause and the session keeps running.
|
|
82272
82466
|
*/
|
|
82273
82467
|
var openDevTunnel = async (input) => {
|
|
82274
82468
|
const targetUrl = new URL(input.targetUrl);
|
|
@@ -82296,19 +82490,29 @@ var openDevTunnel = async (input) => {
|
|
|
82296
82490
|
try {
|
|
82297
82491
|
const grant = await requestDevTunnelGrant({
|
|
82298
82492
|
ctx: input.ctx,
|
|
82299
|
-
agentSlug
|
|
82493
|
+
agentSlug,
|
|
82494
|
+
...input.installationId !== void 0 ? { installationId: input.installationId } : {}
|
|
82300
82495
|
});
|
|
82301
82496
|
await writeLastTunnelGrant(input.projectRoot, grant.subdomain, input.ctx.logger);
|
|
82302
82497
|
try {
|
|
82303
|
-
const { child, publicUrl } = await spawnFrpc({
|
|
82498
|
+
const { child, publicUrl, lastOutput } = await spawnFrpc({
|
|
82304
82499
|
binPath,
|
|
82305
82500
|
config: buildFrpcConfig(grant, targetUrl),
|
|
82306
82501
|
logger: input.ctx.logger,
|
|
82307
82502
|
expectedSubdomain: grant.subdomain
|
|
82308
82503
|
});
|
|
82504
|
+
let stopping = false;
|
|
82505
|
+
watchTunnelDrop({
|
|
82506
|
+
child,
|
|
82507
|
+
ctx: input.ctx,
|
|
82508
|
+
ticket: grant.ticket,
|
|
82509
|
+
lastOutput,
|
|
82510
|
+
isStopping: () => stopping
|
|
82511
|
+
});
|
|
82309
82512
|
return {
|
|
82310
82513
|
publicUrl,
|
|
82311
82514
|
stop: async () => {
|
|
82515
|
+
stopping = true;
|
|
82312
82516
|
try {
|
|
82313
82517
|
await revokeDevTunnelGrant({
|
|
82314
82518
|
ctx: input.ctx,
|
|
@@ -82979,7 +83183,8 @@ var startDevSession = async (input) => {
|
|
|
82979
83183
|
tunnel = await openDevTunnel({
|
|
82980
83184
|
targetUrl: emulator.url,
|
|
82981
83185
|
ctx: input.ctx,
|
|
82982
|
-
projectRoot: input.projectRoot
|
|
83186
|
+
projectRoot: input.projectRoot,
|
|
83187
|
+
...input.installationId !== void 0 ? { installationId: input.installationId } : {}
|
|
82983
83188
|
});
|
|
82984
83189
|
onStage({
|
|
82985
83190
|
stage: "tunnel",
|
|
@@ -83109,10 +83314,6 @@ init_src$19();
|
|
|
83109
83314
|
var upsertLocalDevInstallation = async (ctx, body) => {
|
|
83110
83315
|
return parseResponse$1(localDevInstallationResponseSchema, await ctx.http.post("/api/v1/installations/local-dev", body), "POST /api/v1/installations/local-dev");
|
|
83111
83316
|
};
|
|
83112
|
-
var patchLocalDevInstallation = async (ctx, id, body) => {
|
|
83113
|
-
const path = `/api/v1/installations/local-dev/${encodeURIComponent(id)}`;
|
|
83114
|
-
return parseResponse$1(localDevInstallationResponseSchema, await ctx.http.patch(path, body), `PATCH ${path}`);
|
|
83115
|
-
};
|
|
83116
83317
|
var heartbeatLocalDevInstallation = async (ctx, id) => {
|
|
83117
83318
|
const path = `/api/v1/installations/local-dev/${encodeURIComponent(id)}/heartbeat`;
|
|
83118
83319
|
await ctx.http.post(path, {});
|
|
@@ -83435,6 +83636,62 @@ var printContract = (deps = {}) => {
|
|
|
83435
83636
|
writeStdout(`${JSON.stringify(payload)}\n`);
|
|
83436
83637
|
};
|
|
83437
83638
|
//#endregion
|
|
83639
|
+
//#region src/commands/dev/resolve-local-dev-installation.ts
|
|
83640
|
+
/**
|
|
83641
|
+
* - No stored id: register (upsert) and persist the id to project.json.
|
|
83642
|
+
* - A stored id: heartbeat it once. `not_found` means the row is gone (GC'd,
|
|
83643
|
+
* dev DB wiped) or is not the caller's, so register again, once. Any other
|
|
83644
|
+
* failure says nothing about the row: warn and keep the stored id.
|
|
83645
|
+
* - A failed registration warns and resolves no id instead of throwing.
|
|
83646
|
+
*/
|
|
83647
|
+
var resolveLocalDevInstallation = async (ctx, config, agent) => {
|
|
83648
|
+
const linkedOrgSlug = async () => config.organizationSlug ?? (await getActiveOrganization(ctx)).slug;
|
|
83649
|
+
const register = async () => {
|
|
83650
|
+
let upserted;
|
|
83651
|
+
try {
|
|
83652
|
+
upserted = await upsertLocalDevInstallation(ctx, {
|
|
83653
|
+
creatorOrganizationSlug: await linkedOrgSlug(),
|
|
83654
|
+
agentTemplateSlug: agent.slug
|
|
83655
|
+
});
|
|
83656
|
+
} catch (err) {
|
|
83657
|
+
ctx.logger.warn({ err: err.message }, "failed to register the local-dev installation, the tunnel opens unbound (continuing)");
|
|
83658
|
+
return {
|
|
83659
|
+
id: void 0,
|
|
83660
|
+
orgSlug: config.organizationSlug
|
|
83661
|
+
};
|
|
83662
|
+
}
|
|
83663
|
+
try {
|
|
83664
|
+
await ctx.projectConfig.write({
|
|
83665
|
+
...config,
|
|
83666
|
+
localDevInstallationId: upserted.id,
|
|
83667
|
+
organizationSlug: upserted.organizationSlug,
|
|
83668
|
+
agentSlug: upserted.agentSlug
|
|
83669
|
+
});
|
|
83670
|
+
} catch (err) {
|
|
83671
|
+
ctx.logger.warn({ err: err.message }, "registered the local-dev installation but could not persist its id to project.json (continuing)");
|
|
83672
|
+
}
|
|
83673
|
+
return {
|
|
83674
|
+
id: upserted.id,
|
|
83675
|
+
orgSlug: upserted.organizationSlug
|
|
83676
|
+
};
|
|
83677
|
+
};
|
|
83678
|
+
const storedId = config.localDevInstallationId;
|
|
83679
|
+
if (!storedId) return register();
|
|
83680
|
+
try {
|
|
83681
|
+
await heartbeatLocalDevInstallation(ctx, storedId);
|
|
83682
|
+
} catch (err) {
|
|
83683
|
+
if (err instanceof StackboneCliError && err.code === "not_found") {
|
|
83684
|
+
ctx.logger.warn("local-dev row not found, re-registering");
|
|
83685
|
+
return register();
|
|
83686
|
+
}
|
|
83687
|
+
ctx.logger.warn({ err: err.message }, "local-dev pre-flight heartbeat failed, keeping the stored id (continuing)");
|
|
83688
|
+
}
|
|
83689
|
+
return {
|
|
83690
|
+
id: storedId,
|
|
83691
|
+
orgSlug: await linkedOrgSlug()
|
|
83692
|
+
};
|
|
83693
|
+
};
|
|
83694
|
+
//#endregion
|
|
83438
83695
|
//#region src/commands/dev/dev.command.ts
|
|
83439
83696
|
init_src$19();
|
|
83440
83697
|
var STAGE_LABELS = {
|
|
@@ -83546,6 +83803,7 @@ var buildHandler$2 = (ctx, flags) => protectedCommand(async (innerCtx) => {
|
|
|
83546
83803
|
suggestion: "Run `stackbone link --force --agent <slug> --installation <installationId> --url <url> --secret <secret> --tag <tag>` to re-link to a different agent (see `stackbone link --help`)"
|
|
83547
83804
|
});
|
|
83548
83805
|
await assertSdkCompatible(innerCtx, { mode: "warn" });
|
|
83806
|
+
const { id: localDevId, orgSlug: installationOrgSlug } = await resolveLocalDevInstallation(innerCtx, config, agent);
|
|
83549
83807
|
const renderStage = innerCtx.flags.json ? null : createStageRenderer();
|
|
83550
83808
|
const renderReload = innerCtx.flags.json ? null : createReloadRenderer();
|
|
83551
83809
|
let republishPointer = null;
|
|
@@ -83559,6 +83817,7 @@ var buildHandler$2 = (ctx, flags) => protectedCommand(async (innerCtx) => {
|
|
|
83559
83817
|
},
|
|
83560
83818
|
emulatorPort: flags.port,
|
|
83561
83819
|
tunnel: true,
|
|
83820
|
+
...localDevId ? { installationId: localDevId } : {},
|
|
83562
83821
|
listen: flags.listen,
|
|
83563
83822
|
verbose: flags.verbose,
|
|
83564
83823
|
...flags.autoMigrate !== void 0 ? { autoMigrateOverride: flags.autoMigrate } : {},
|
|
@@ -83570,48 +83829,6 @@ var buildHandler$2 = (ctx, flags) => protectedCommand(async (innerCtx) => {
|
|
|
83570
83829
|
}
|
|
83571
83830
|
});
|
|
83572
83831
|
declaredPieces = session.declaredPieces;
|
|
83573
|
-
const registerLocalDev = async () => {
|
|
83574
|
-
const upserted = await upsertLocalDevInstallation(innerCtx, {
|
|
83575
|
-
creatorOrganizationSlug: config.organizationSlug ?? (await getActiveOrganization(innerCtx)).slug,
|
|
83576
|
-
agentTemplateSlug: agent.slug
|
|
83577
|
-
});
|
|
83578
|
-
await innerCtx.projectConfig.write({
|
|
83579
|
-
...config,
|
|
83580
|
-
localDevInstallationId: upserted.id,
|
|
83581
|
-
organizationSlug: upserted.organizationSlug,
|
|
83582
|
-
agentSlug: upserted.agentSlug
|
|
83583
|
-
});
|
|
83584
|
-
return {
|
|
83585
|
-
id: upserted.id,
|
|
83586
|
-
orgSlug: upserted.organizationSlug
|
|
83587
|
-
};
|
|
83588
|
-
};
|
|
83589
|
-
let localDevId = config.localDevInstallationId;
|
|
83590
|
-
let installationOrgSlug = config.organizationSlug;
|
|
83591
|
-
if (!localDevId) {
|
|
83592
|
-
const registered = await registerLocalDev();
|
|
83593
|
-
localDevId = registered.id;
|
|
83594
|
-
installationOrgSlug = registered.orgSlug;
|
|
83595
|
-
} else if (!installationOrgSlug) installationOrgSlug = (await getActiveOrganization(innerCtx)).slug;
|
|
83596
|
-
const publicUrl = session.publicUrl;
|
|
83597
|
-
if (publicUrl) {
|
|
83598
|
-
const patchTunnel = (id) => patchLocalDevInstallation(innerCtx, id, { localTunnelUrl: publicUrl });
|
|
83599
|
-
const patched = await patchTunnel(localDevId).catch(async (err) => {
|
|
83600
|
-
if (err instanceof StackboneCliError && err.code === "not_found") {
|
|
83601
|
-
innerCtx.logger.warn("local-dev row not found, re-registering");
|
|
83602
|
-
const registered = await registerLocalDev();
|
|
83603
|
-
localDevId = registered.id;
|
|
83604
|
-
installationOrgSlug = registered.orgSlug;
|
|
83605
|
-
return patchTunnel(localDevId).catch((retryErr) => {
|
|
83606
|
-
innerCtx.logger.warn({ err: retryErr.message }, "failed to PATCH local-dev tunnel URL after re-register (continuing)");
|
|
83607
|
-
return null;
|
|
83608
|
-
});
|
|
83609
|
-
}
|
|
83610
|
-
innerCtx.logger.warn({ err: err.message }, "failed to PATCH local-dev tunnel URL (continuing)");
|
|
83611
|
-
return null;
|
|
83612
|
-
});
|
|
83613
|
-
if (patched) installationOrgSlug = patched.organizationSlug;
|
|
83614
|
-
}
|
|
83615
83832
|
if (localDevId) {
|
|
83616
83833
|
const installId = localDevId;
|
|
83617
83834
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -83627,7 +83844,7 @@ var buildHandler$2 = (ctx, flags) => protectedCommand(async (innerCtx) => {
|
|
|
83627
83844
|
}, innerCtx.logger);
|
|
83628
83845
|
await republishPointer();
|
|
83629
83846
|
}
|
|
83630
|
-
const studioBaseUrl = resolveStudioBaseUrl(innerCtx.apiUrl);
|
|
83847
|
+
const studioBaseUrl = resolveStudioBaseUrl(innerCtx.apiUrl, innerCtx.env);
|
|
83631
83848
|
const linkTo = (screenPath) => studioDeeplink({
|
|
83632
83849
|
studioBaseUrl,
|
|
83633
83850
|
orgSlug: installationOrgSlug,
|
|
@@ -83691,15 +83908,7 @@ var buildHandler$2 = (ctx, flags) => protectedCommand(async (innerCtx) => {
|
|
|
83691
83908
|
const sigintHandler = () => {
|
|
83692
83909
|
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
83693
83910
|
shutdownAndExit(async () => {
|
|
83694
|
-
|
|
83695
|
-
innerCtx.logger.debug({ err: err.message }, "failed to null local-dev tunnel URL on exit (safety net will catch up)");
|
|
83696
|
-
return null;
|
|
83697
|
-
}) : Promise.resolve(null);
|
|
83698
|
-
await Promise.allSettled([
|
|
83699
|
-
patch,
|
|
83700
|
-
session.stop(),
|
|
83701
|
-
clearDevSession(projectRoot, innerCtx.logger)
|
|
83702
|
-
]);
|
|
83911
|
+
await Promise.allSettled([session.stop(), clearDevSession(projectRoot, innerCtx.logger)]);
|
|
83703
83912
|
goodbye();
|
|
83704
83913
|
}, { onForced: () => {
|
|
83705
83914
|
if (!innerCtx.flags.json) log.warn("Shutdown did not finish in time — leaving anyway.");
|
|
@@ -84370,7 +84579,7 @@ var runEval = async (ctx, args, deps = {}) => {
|
|
|
84370
84579
|
};
|
|
84371
84580
|
const launched = await launch(callCtx, basePath, suite.id, body);
|
|
84372
84581
|
const url = evalRunUrl({
|
|
84373
|
-
studioBaseUrl: resolveStudioBaseUrl(ctx.apiUrl),
|
|
84582
|
+
studioBaseUrl: resolveStudioBaseUrl(ctx.apiUrl, ctx.env),
|
|
84374
84583
|
orgSlug,
|
|
84375
84584
|
installationId,
|
|
84376
84585
|
evalRunId: launched.id
|
|
@@ -86375,16 +86584,29 @@ var parseLimit = (raw) => {
|
|
|
86375
86584
|
* a relative duration (`15m`, `2h`, `30s`, `1d`) measured back from `now`, or an
|
|
86376
86585
|
* absolute ISO timestamp. Returns `undefined` when the flag is absent. A value
|
|
86377
86586
|
* that is neither a known duration nor a parseable date is a CLI-side error.
|
|
86587
|
+
*
|
|
86588
|
+
* A bare number is rejected before `Date.parse`, which would read it as a date
|
|
86589
|
+
* (`5` is April 2001, `2026` is 2026-01-01) and leave the window filtering
|
|
86590
|
+
* nothing. It is a duration with the unit forgotten.
|
|
86378
86591
|
*/
|
|
86379
86592
|
var parseTimeBound = (flag, raw, now) => {
|
|
86380
86593
|
if (raw === void 0) return void 0;
|
|
86381
|
-
const
|
|
86594
|
+
const trimmed = raw.trim();
|
|
86595
|
+
const duration = /^(\d+)(s|m|h|d)$/.exec(trimmed);
|
|
86382
86596
|
if (duration) return now - Number(duration[1]) * ({
|
|
86383
86597
|
s: 1e3,
|
|
86384
86598
|
m: 6e4,
|
|
86385
86599
|
h: 36e5,
|
|
86386
86600
|
d: 864e5
|
|
86387
86601
|
}[duration[2] ?? ""] ?? 0);
|
|
86602
|
+
if (/^[+-]?(\d+\.?\d*|\.\d+)$/.test(trimmed)) {
|
|
86603
|
+
const example = /^\d+$/.test(trimmed) ? `${trimmed}m` : "15m";
|
|
86604
|
+
throw new StackboneCliError({
|
|
86605
|
+
code: "generic",
|
|
86606
|
+
message: `Invalid ${flag} '${raw}'. A duration needs a unit: s, m, h or d.`,
|
|
86607
|
+
suggestion: `Pass e.g. \`${flag} ${example}\`, or an ISO timestamp such as \`${flag} 2026-05-22T00:00:00Z\`.`
|
|
86608
|
+
});
|
|
86609
|
+
}
|
|
86388
86610
|
const ms = Date.parse(raw);
|
|
86389
86611
|
if (Number.isNaN(ms)) throw new StackboneCliError({
|
|
86390
86612
|
code: "generic",
|
|
@@ -88351,7 +88573,7 @@ var buildRootCommand = (ctx) => defineCommand({
|
|
|
88351
88573
|
meta: {
|
|
88352
88574
|
name: "stackbone",
|
|
88353
88575
|
version: STACKBONE_CLI_BUILD_VERSION,
|
|
88354
|
-
description: "
|
|
88576
|
+
description: "Build, run and deploy Stackbone agents and workflows. See `stackbone docs`."
|
|
88355
88577
|
},
|
|
88356
88578
|
args: {
|
|
88357
88579
|
json: {
|
package/package.json
CHANGED
|
Binary file
|
package/stackbone-cli-0.5.2.tgz
DELETED
|
Binary file
|