@stackbone/cli 0.1.0-alpha.2 → 0.1.0-alpha.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/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.0-alpha.3] - 2026-05-15
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `stackbone dev` sessions are now first-class `installation` rows
|
|
15
|
+
(`kind='local'`). The CLI registers the row via
|
|
16
|
+
`POST /api/v1/installations/local-dev` during `init`/`link`, `PATCH`es
|
|
17
|
+
`local_tunnel_url` on dev start, and nulls it on Ctrl-C. The dev
|
|
18
|
+
boot banner emits a path-aware deeplink
|
|
19
|
+
(`app.stackbone.ai/app/<orgSlug>/installations/<id>?stackbone-dev=<tunnel>`)
|
|
20
|
+
consumed in one shot by the `stackboneDevBootstrapGuard` so Studio
|
|
21
|
+
opens straight on the local installation. Local-dev rows skip the
|
|
22
|
+
provisioning saga and the tier-quota gate by construction and are
|
|
23
|
+
garbage-collected after 7 days of inactivity.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Unified agent runtime env contract under `STACKBONE_*`. The dev
|
|
28
|
+
emulator now injects `STACKBONE_AGENT_ID` (the agent slug) plus
|
|
29
|
+
`STACKBONE_S3_ACCESS_KEY` / `STACKBONE_S3_SECRET_KEY` /
|
|
30
|
+
`STACKBONE_S3_ENDPOINT` / `STACKBONE_S3_BUCKET` /
|
|
31
|
+
`STACKBONE_S3_REGION` / `STACKBONE_POSTGRES_URL` into the agent
|
|
32
|
+
process, replacing the previous mix of `AWS_*` / `S3_*` /
|
|
33
|
+
`DATABASE_URL` names. Agents that still read the legacy names must
|
|
34
|
+
migrate.
|
|
35
|
+
- Emulator `/api/schema` now returns `{ input: null, output: null }`
|
|
36
|
+
(200) when the manifest declares neither schema or the agent is
|
|
37
|
+
unreachable — was 502 before. Mirrors the cloud `/studio/schema`
|
|
38
|
+
shape so the Studio Playground UI can boot before the agent is
|
|
39
|
+
healthy.
|
|
40
|
+
- `dev/fixtures-store.ts` persists JSONL records using the canonical
|
|
41
|
+
`Fixture` wire schema (camelCase `createdAt`, typed as `Fixture`) so
|
|
42
|
+
list/create return the wire type directly.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- `protectedCommand` no longer triggers the device-code flow on the
|
|
47
|
+
user's behalf. When no valid session exists it fails fast with
|
|
48
|
+
`code: 'auth'` and the standard "Run stackbone login" hint, and it
|
|
49
|
+
no longer retries on a mid-flight 401. Reason: the HTTP adapter
|
|
50
|
+
caches `tokenStore.activeSession()` once per process, so the
|
|
51
|
+
pre-login `POST /api/auth/device/code` poisoned the cache with
|
|
52
|
+
`null` and every subsequent request from the same process went out
|
|
53
|
+
without an Authorization header.
|
|
54
|
+
|
|
10
55
|
## [0.1.0-alpha.2] - 2026-05-12
|
|
11
56
|
|
|
12
57
|
### Added
|
package/main.js
CHANGED
|
@@ -82,6 +82,7 @@ var installationStatusSchema = z.enum([
|
|
|
82
82
|
"deprovisioning",
|
|
83
83
|
"deprovisioned"
|
|
84
84
|
]);
|
|
85
|
+
var installationKindSchema = z.enum(["cloud", "local"]);
|
|
85
86
|
var installationProviderSchema = z.enum([
|
|
86
87
|
"neon",
|
|
87
88
|
"r2",
|
|
@@ -109,17 +110,30 @@ z.object({
|
|
|
109
110
|
slug: organizationSlugSchema,
|
|
110
111
|
id: z.uuid()
|
|
111
112
|
});
|
|
112
|
-
z.object({
|
|
113
|
+
var installationResponseSchema = z.object({
|
|
113
114
|
id: z.uuid(),
|
|
114
115
|
organizationId: z.uuid(),
|
|
115
116
|
agentId: z.uuid(),
|
|
116
117
|
agentVersion: z.string(),
|
|
117
118
|
status: installationStatusSchema,
|
|
119
|
+
kind: installationKindSchema.default("cloud"),
|
|
120
|
+
localTunnelUrl: z.url().nullable().optional(),
|
|
121
|
+
lastActiveAt: z.iso.datetime().nullable().optional(),
|
|
118
122
|
config: jsonObject(),
|
|
119
123
|
createdByUserId: z.uuid(),
|
|
120
124
|
createdAt: z.iso.datetime(),
|
|
121
125
|
updatedAt: z.iso.datetime()
|
|
122
126
|
});
|
|
127
|
+
z.object({
|
|
128
|
+
creatorOrganizationSlug: organizationSlugSchema,
|
|
129
|
+
agentTemplateSlug: z.string().min(1).max(80),
|
|
130
|
+
endUserOrganizationSlug: organizationSlugSchema.optional()
|
|
131
|
+
});
|
|
132
|
+
z.object({ localTunnelUrl: z.url().nullable() });
|
|
133
|
+
var localDevInstallationResponseSchema = installationResponseSchema.extend({
|
|
134
|
+
organizationSlug: organizationSlugSchema,
|
|
135
|
+
agentSlug: z.string().min(1).max(80)
|
|
136
|
+
});
|
|
123
137
|
z.object({
|
|
124
138
|
id: z.uuid(),
|
|
125
139
|
installationId: z.uuid(),
|
|
@@ -468,6 +482,11 @@ z.object({
|
|
|
468
482
|
nextCursor: z.string().nullable(),
|
|
469
483
|
prevCursor: z.string().nullable()
|
|
470
484
|
});
|
|
485
|
+
z.object({ fxtId: z.uuid() });
|
|
486
|
+
z.object({
|
|
487
|
+
input: jsonObject().nullable(),
|
|
488
|
+
output: jsonObject().nullable()
|
|
489
|
+
});
|
|
471
490
|
//#endregion
|
|
472
491
|
//#region ../../libs/validators/src/lib/auth/session.ts
|
|
473
492
|
var sessionUserSchema = z.object({
|
|
@@ -931,12 +950,22 @@ var credentialsSchema = z.object({
|
|
|
931
950
|
* Project-local config is written by `stackbone init`/`link` to
|
|
932
951
|
* `<cwd>/.stackbone/project.json`. Tracks which agent in which organization this
|
|
933
952
|
* directory points at. Excluded from git by the same `.gitignore` patch.
|
|
953
|
+
*
|
|
954
|
+
* Feature 42: `localDevInstallationId` + `organizationSlug` + `agentSlug` are
|
|
955
|
+
* captured at `init`/`link` time so `stackbone dev` can build the path-aware
|
|
956
|
+
* deeplink (`/app/<orgSlug>/installations/<id>?stackbone-dev=…`) and patch
|
|
957
|
+
* the tunnel URL without a separate `GET` round-trip. They are optional so
|
|
958
|
+
* old project.json files continue to load — a future `stackbone dev` against
|
|
959
|
+
* a pre-feature-42 directory simply re-registers the local-dev row.
|
|
934
960
|
*/
|
|
935
961
|
var projectConfigSchema = z.object({
|
|
936
962
|
schemaVersion: z.literal(1),
|
|
937
963
|
organizationId: z.uuid(),
|
|
938
964
|
agentId: z.uuid(),
|
|
939
|
-
controlPlaneUrl: z.url()
|
|
965
|
+
controlPlaneUrl: z.url(),
|
|
966
|
+
localDevInstallationId: z.uuid().optional(),
|
|
967
|
+
organizationSlug: z.string().optional(),
|
|
968
|
+
agentSlug: z.string().optional()
|
|
940
969
|
});
|
|
941
970
|
/**
|
|
942
971
|
* Project-shared config at `<cwd>/stackbone.config.json` (committable, unlike
|
|
@@ -1263,7 +1292,8 @@ var createHttpAdapter = (opts) => {
|
|
|
1263
1292
|
};
|
|
1264
1293
|
return {
|
|
1265
1294
|
get: (path) => request("GET", path),
|
|
1266
|
-
post: (path, body) => request("POST", path, body)
|
|
1295
|
+
post: (path, body) => request("POST", path, body),
|
|
1296
|
+
patch: (path, body) => request("PATCH", path, body)
|
|
1267
1297
|
};
|
|
1268
1298
|
};
|
|
1269
1299
|
//#endregion
|
|
@@ -1361,7 +1391,6 @@ var flagValue = (argv, flag) => {
|
|
|
1361
1391
|
var parseFlags = (argv, env) => ({
|
|
1362
1392
|
json: detectJsonMode(argv, env),
|
|
1363
1393
|
yes: flagPresent(argv, "-y") || flagPresent(argv, "--yes"),
|
|
1364
|
-
autoLogin: env["STACKBONE_AUTO_LOGIN"] !== "0" && !flagPresent(argv, "--no-auto-login"),
|
|
1365
1394
|
apiUrlOverride: flagValue(argv, "--api-url") ?? env["STACKBONE_API_URL"] ?? null,
|
|
1366
1395
|
verbose: flagPresent(argv, "--verbose") || env["STACKBONE_VERBOSE"] === "1"
|
|
1367
1396
|
});
|
|
@@ -1422,7 +1451,7 @@ var isVersionFlag = (rawArgs) => rawArgs.length === 1 && VERSION_FLAGS.has(rawAr
|
|
|
1422
1451
|
var formatVersionOutput = (input) => `stackbone-cli ${input.cliVersion}\ncontract ${input.contractVersion}`;
|
|
1423
1452
|
//#endregion
|
|
1424
1453
|
//#region src/dev/contract.ts
|
|
1425
|
-
var STACKBONE_CLI_BUILD_VERSION = "0.1.0-alpha.
|
|
1454
|
+
var STACKBONE_CLI_BUILD_VERSION = "0.1.0-alpha.3";
|
|
1426
1455
|
/**
|
|
1427
1456
|
* Capabilities the local emulator advertises on `GET /api/contract`. Typed as
|
|
1428
1457
|
* `readonly Capability[]` so the compiler refuses any string outside the
|
|
@@ -1743,55 +1772,25 @@ var clearSession = async (ctx) => {
|
|
|
1743
1772
|
};
|
|
1744
1773
|
//#endregion
|
|
1745
1774
|
//#region src/commands/_shared/protected.ts
|
|
1746
|
-
var isAuthError = (err) => isStackboneCliError(err) && err.code === "auth";
|
|
1747
|
-
var refuseWithAuthError = () => {
|
|
1748
|
-
throw new StackboneCliError({
|
|
1749
|
-
code: "auth",
|
|
1750
|
-
message: "Not authenticated",
|
|
1751
|
-
suggestion: "Run `stackbone login`"
|
|
1752
|
-
});
|
|
1753
|
-
};
|
|
1754
|
-
var ensureSession = async (ctx) => {
|
|
1755
|
-
const existing = await getCurrentSession(ctx);
|
|
1756
|
-
if (existing) return existing;
|
|
1757
|
-
if (!ctx.flags.autoLogin) refuseWithAuthError();
|
|
1758
|
-
await runLoginFlow(ctx);
|
|
1759
|
-
return await getCurrentSession(ctx) ?? refuseWithAuthError();
|
|
1760
|
-
};
|
|
1761
1775
|
/**
|
|
1762
1776
|
* Wraps a command handler so it always receives a non-null `Session`.
|
|
1763
1777
|
*
|
|
1764
|
-
* If the credentials store is empty or the session expired
|
|
1765
|
-
*
|
|
1766
|
-
*
|
|
1767
|
-
*
|
|
1768
|
-
*
|
|
1769
|
-
*
|
|
1770
|
-
* Retries once if the handler itself throws an auth error mid-flight
|
|
1771
|
-
* (token expired between the pre-check and the request). If the retry
|
|
1772
|
-
* also fails with auth, the corrupt credentials are dropped from disk so
|
|
1773
|
-
* the next invocation does not loop through the same broken session.
|
|
1778
|
+
* If the credentials store is empty or the session has expired, refuses
|
|
1779
|
+
* immediately with `code: 'auth'` (exit 2) and points the user at
|
|
1780
|
+
* `stackbone login`. Auth flow and command flow are kept strictly separate
|
|
1781
|
+
* — protected commands never trigger the device-code dance on the user's
|
|
1782
|
+
* behalf, and a 401 mid-flight propagates as-is so the same `stackbone
|
|
1783
|
+
* login` instruction is the only path back.
|
|
1774
1784
|
*/
|
|
1775
1785
|
var protectedCommand = (handler) => {
|
|
1776
1786
|
return async (ctx) => {
|
|
1777
|
-
const session = await
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
const refreshed = await getCurrentSession(ctx) ?? refuseWithAuthError();
|
|
1785
|
-
try {
|
|
1786
|
-
await handler(ctx, refreshed);
|
|
1787
|
-
} catch (retryErr) {
|
|
1788
|
-
if (isAuthError(retryErr)) {
|
|
1789
|
-
ctx.logger.warn("auth still failing after re-login, clearing the stored session");
|
|
1790
|
-
await clearSession(ctx);
|
|
1791
|
-
}
|
|
1792
|
-
throw retryErr;
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1787
|
+
const session = await getCurrentSession(ctx);
|
|
1788
|
+
if (!session) throw new StackboneCliError({
|
|
1789
|
+
code: "auth",
|
|
1790
|
+
message: "Not authenticated",
|
|
1791
|
+
suggestion: "Run `stackbone login`"
|
|
1792
|
+
});
|
|
1793
|
+
await handler(ctx, session);
|
|
1795
1794
|
};
|
|
1796
1795
|
};
|
|
1797
1796
|
//#endregion
|
|
@@ -2614,7 +2613,7 @@ var buildMigrateStatusCommand = (ctx) => defineCommand({
|
|
|
2614
2613
|
env: ctx.env,
|
|
2615
2614
|
json: ctx.flags.json
|
|
2616
2615
|
});
|
|
2617
|
-
})(
|
|
2616
|
+
})()
|
|
2618
2617
|
});
|
|
2619
2618
|
/**
|
|
2620
2619
|
* Pure handler for `stackbone db migrate add-rag`. Reads `agent.yaml` to
|
|
@@ -2702,7 +2701,7 @@ var buildStudioCommand = (ctx) => defineCommand({
|
|
|
2702
2701
|
env: ctx.env,
|
|
2703
2702
|
json: ctx.flags.json
|
|
2704
2703
|
});
|
|
2705
|
-
})(
|
|
2704
|
+
})()
|
|
2706
2705
|
});
|
|
2707
2706
|
var createDbCommand = (ctx) => defineCommand({
|
|
2708
2707
|
meta: {
|
|
@@ -2747,7 +2746,6 @@ var createDeployCommand = (ctx) => buildStubCommand(ctx, {
|
|
|
2747
2746
|
});
|
|
2748
2747
|
//#endregion
|
|
2749
2748
|
//#region src/dev/boot-banner.ts
|
|
2750
|
-
var STUDIO_CLOUD_BASE_URL = "https://app.stackbone.ai/app";
|
|
2751
2749
|
var ANSI = {
|
|
2752
2750
|
reset: "\x1B[0m",
|
|
2753
2751
|
bold: "\x1B[1m",
|
|
@@ -2757,7 +2755,6 @@ var ANSI = {
|
|
|
2757
2755
|
};
|
|
2758
2756
|
var ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
2759
2757
|
var visibleLength = (s) => s.replace(ANSI_RE, "").length;
|
|
2760
|
-
var buildDeeplink = (tunnelUrl) => `${STUDIO_CLOUD_BASE_URL}?stackbone-dev=${encodeURIComponent(tunnelUrl)}`;
|
|
2761
2758
|
var LABEL_WIDTH = 11;
|
|
2762
2759
|
var renderRow = (row, colors) => {
|
|
2763
2760
|
const marker = colors && row.marker.trim() ? `${ANSI.cyan}${row.marker}${ANSI.reset}` : row.marker;
|
|
@@ -2768,11 +2765,11 @@ var TITLE = "stackbone studio";
|
|
|
2768
2765
|
var formatBootBanner = (input) => {
|
|
2769
2766
|
const colors = input.colors ?? false;
|
|
2770
2767
|
const rows = [];
|
|
2771
|
-
if (input.tunnelUrl) {
|
|
2768
|
+
if (input.deeplink && input.tunnelUrl) {
|
|
2772
2769
|
rows.push({
|
|
2773
2770
|
marker: "▸",
|
|
2774
2771
|
label: "Open Studio",
|
|
2775
|
-
value:
|
|
2772
|
+
value: input.deeplink,
|
|
2776
2773
|
primary: true
|
|
2777
2774
|
});
|
|
2778
2775
|
rows.push({
|
|
@@ -2790,6 +2787,23 @@ var formatBootBanner = (input) => {
|
|
|
2790
2787
|
label: "Local",
|
|
2791
2788
|
value: input.localUrl
|
|
2792
2789
|
});
|
|
2790
|
+
} else if (input.tunnelUrl) {
|
|
2791
|
+
rows.push({
|
|
2792
|
+
marker: "▸",
|
|
2793
|
+
label: "Tunnel",
|
|
2794
|
+
value: input.tunnelUrl,
|
|
2795
|
+
primary: true
|
|
2796
|
+
});
|
|
2797
|
+
rows.push({
|
|
2798
|
+
marker: " ",
|
|
2799
|
+
label: "",
|
|
2800
|
+
value: ""
|
|
2801
|
+
});
|
|
2802
|
+
rows.push({
|
|
2803
|
+
marker: " ",
|
|
2804
|
+
label: "Local",
|
|
2805
|
+
value: input.localUrl
|
|
2806
|
+
});
|
|
2793
2807
|
} else rows.push({
|
|
2794
2808
|
marker: "▸",
|
|
2795
2809
|
label: "Local",
|
|
@@ -2973,6 +2987,7 @@ var fetchActiveConfigValue = async (client) => {
|
|
|
2973
2987
|
//#region src/dev/connections.ts
|
|
2974
2988
|
var STACKBONE_ENV_KEYS = {
|
|
2975
2989
|
contractVersion: "STACKBONE_CONTRACT_VERSION",
|
|
2990
|
+
agentId: "STACKBONE_AGENT_ID",
|
|
2976
2991
|
postgresUrl: "STACKBONE_POSTGRES_URL",
|
|
2977
2992
|
queueUrl: "STACKBONE_QUEUE_URL",
|
|
2978
2993
|
s3Endpoint: "STACKBONE_S3_ENDPOINT",
|
|
@@ -2987,6 +3002,7 @@ var buildAgentEnv = (base, injection, options = {}) => {
|
|
|
2987
3002
|
const env = {
|
|
2988
3003
|
...base,
|
|
2989
3004
|
[STACKBONE_ENV_KEYS.contractVersion]: String(10),
|
|
3005
|
+
[STACKBONE_ENV_KEYS.agentId]: injection.agentId,
|
|
2990
3006
|
[STACKBONE_ENV_KEYS.postgresUrl]: injection.services.postgresUrl,
|
|
2991
3007
|
[STACKBONE_ENV_KEYS.queueUrl]: injection.services.queueUrl,
|
|
2992
3008
|
[STACKBONE_ENV_KEYS.s3Endpoint]: injection.services.s3Endpoint,
|
|
@@ -4142,7 +4158,7 @@ var quoteIdent = (raw) => `"${raw.replace(/"/g, "\"\"")}"`;
|
|
|
4142
4158
|
//#region src/dev/fixtures-store.ts
|
|
4143
4159
|
var defaultFixturesDir = () => resolve(homedir(), ".stackbone", "dev", "fixtures");
|
|
4144
4160
|
var filenameFor = (baseDir, agentSlug) => resolve(baseDir, `${agentSlug}.jsonl`);
|
|
4145
|
-
var isSerializedFixture = (value) => !!value && typeof value === "object" && typeof value.id === "string" && typeof value.name === "string" && typeof value.
|
|
4161
|
+
var isSerializedFixture = (value) => !!value && typeof value === "object" && typeof value.id === "string" && typeof value.name === "string" && typeof value.createdAt === "string";
|
|
4146
4162
|
var readJsonl = async (path) => {
|
|
4147
4163
|
let raw;
|
|
4148
4164
|
try {
|
|
@@ -4169,7 +4185,7 @@ var createFixturesStore = (opts) => {
|
|
|
4169
4185
|
};
|
|
4170
4186
|
return {
|
|
4171
4187
|
async list() {
|
|
4172
|
-
return (await readJsonl(path)).sort((a, b) => a.
|
|
4188
|
+
return (await readJsonl(path)).sort((a, b) => a.createdAt < b.createdAt ? 1 : -1);
|
|
4173
4189
|
},
|
|
4174
4190
|
async create(input) {
|
|
4175
4191
|
await ensureDir();
|
|
@@ -4179,7 +4195,7 @@ var createFixturesStore = (opts) => {
|
|
|
4179
4195
|
name: input.name,
|
|
4180
4196
|
input: input.input,
|
|
4181
4197
|
description: input.description ?? null,
|
|
4182
|
-
|
|
4198
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4183
4199
|
};
|
|
4184
4200
|
await appendFile(path, `${JSON.stringify(fixture)}\n`, "utf8");
|
|
4185
4201
|
return fixture;
|
|
@@ -5426,22 +5442,25 @@ var buildEmulatorApp = (opts) => {
|
|
|
5426
5442
|
app.get("/api/logs/stream", (c) => streamSSE(c, (stream) => {
|
|
5427
5443
|
return runLogTail(stream, parseLogFilters(new URL(c.req.url).searchParams), (emit) => tailJsonlDirectory(logsDir, emit));
|
|
5428
5444
|
}));
|
|
5429
|
-
|
|
5445
|
+
app.get("/api/schema", async (c) => {
|
|
5430
5446
|
try {
|
|
5431
|
-
const resp = await fetch(`${agentBaseUrl}
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5447
|
+
const resp = await fetch(`${agentBaseUrl}/schema`);
|
|
5448
|
+
if (!resp.ok) return c.json({
|
|
5449
|
+
input: null,
|
|
5450
|
+
output: null
|
|
5451
|
+
});
|
|
5452
|
+
const raw = await resp.json().catch(() => null);
|
|
5453
|
+
return c.json({
|
|
5454
|
+
input: extractSchemaField(raw, "input"),
|
|
5455
|
+
output: extractSchemaField(raw, "output")
|
|
5436
5456
|
});
|
|
5437
5457
|
} catch {
|
|
5438
|
-
return
|
|
5439
|
-
|
|
5440
|
-
|
|
5458
|
+
return c.json({
|
|
5459
|
+
input: null,
|
|
5460
|
+
output: null
|
|
5441
5461
|
});
|
|
5442
5462
|
}
|
|
5443
|
-
};
|
|
5444
|
-
app.get("/api/schema", () => proxyAgentJson("/schema"));
|
|
5463
|
+
});
|
|
5445
5464
|
const dbExplorerOptions = { connectionString: opts.injection.services.postgresUrl };
|
|
5446
5465
|
app.get("/api/db/schemas", async (c) => {
|
|
5447
5466
|
const response = await listSchemas(dbExplorerOptions);
|
|
@@ -6044,6 +6063,12 @@ var buildEmulatorApp = (opts) => {
|
|
|
6044
6063
|
}));
|
|
6045
6064
|
return app;
|
|
6046
6065
|
};
|
|
6066
|
+
var extractSchemaField = (raw, field) => {
|
|
6067
|
+
if (!raw) return null;
|
|
6068
|
+
const value = raw[field];
|
|
6069
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return value;
|
|
6070
|
+
return null;
|
|
6071
|
+
};
|
|
6047
6072
|
var eventMatchesRunId = (event, runId) => {
|
|
6048
6073
|
const data = event.data;
|
|
6049
6074
|
if (!data || typeof data["run_id"] === "undefined") return true;
|
|
@@ -7176,6 +7201,7 @@ var startDevSession = async (input) => {
|
|
|
7176
7201
|
const bus = createEventBus();
|
|
7177
7202
|
const agentPort = await pickFreePort();
|
|
7178
7203
|
const injection = {
|
|
7204
|
+
agentId: input.agent.slug,
|
|
7179
7205
|
agentPort,
|
|
7180
7206
|
services: DEFAULT_DEV_SERVICES
|
|
7181
7207
|
};
|
|
@@ -7503,6 +7529,24 @@ var startDevSession = async (input) => {
|
|
|
7503
7529
|
}
|
|
7504
7530
|
};
|
|
7505
7531
|
//#endregion
|
|
7532
|
+
//#region src/services/local-dev-installations.service.ts
|
|
7533
|
+
var parse$1 = (raw, context) => {
|
|
7534
|
+
const parsed = localDevInstallationResponseSchema.safeParse(raw);
|
|
7535
|
+
if (!parsed.success) throw new StackboneCliError({
|
|
7536
|
+
code: "generic",
|
|
7537
|
+
message: `Unexpected response shape from ${context}`,
|
|
7538
|
+
cause: parsed.error
|
|
7539
|
+
});
|
|
7540
|
+
return parsed.data;
|
|
7541
|
+
};
|
|
7542
|
+
var upsertLocalDevInstallation = async (ctx, body) => {
|
|
7543
|
+
return parse$1(await ctx.http.post("/api/v1/installations/local-dev", body), "POST /api/v1/installations/local-dev");
|
|
7544
|
+
};
|
|
7545
|
+
var patchLocalDevInstallation = async (ctx, id, body) => {
|
|
7546
|
+
const path = `/api/v1/installations/local-dev/${encodeURIComponent(id)}`;
|
|
7547
|
+
return parse$1(await ctx.http.patch(path, body), `PATCH ${path}`);
|
|
7548
|
+
};
|
|
7549
|
+
//#endregion
|
|
7506
7550
|
//#region src/commands/dev/print-contract.ts
|
|
7507
7551
|
var defaultWriteStdout = (chunk) => {
|
|
7508
7552
|
process.stdout.write(chunk);
|
|
@@ -7599,15 +7643,37 @@ var buildHandler$2 = (ctx, flags) => protectedCommand(async (innerCtx) => {
|
|
|
7599
7643
|
name: agent.name
|
|
7600
7644
|
},
|
|
7601
7645
|
emulatorPort: flags.port,
|
|
7602
|
-
tunnel:
|
|
7646
|
+
tunnel: true,
|
|
7603
7647
|
listen: flags.listen,
|
|
7604
7648
|
verbose: flags.verbose,
|
|
7605
7649
|
...renderStage ? { onStage: renderStage } : {}
|
|
7606
7650
|
});
|
|
7651
|
+
const activeOrg = await getActiveOrganization(innerCtx);
|
|
7652
|
+
let localDevId = config.localDevInstallationId;
|
|
7653
|
+
let installationOrgSlug = config.organizationSlug ?? activeOrg.slug;
|
|
7654
|
+
if (!localDevId) {
|
|
7655
|
+
const upserted = await upsertLocalDevInstallation(innerCtx, {
|
|
7656
|
+
creatorOrganizationSlug: activeOrg.slug,
|
|
7657
|
+
agentTemplateSlug: agent.slug
|
|
7658
|
+
});
|
|
7659
|
+
localDevId = upserted.id;
|
|
7660
|
+
installationOrgSlug = upserted.organizationSlug;
|
|
7661
|
+
}
|
|
7662
|
+
if (session.publicUrl) {
|
|
7663
|
+
const patched = await patchLocalDevInstallation(innerCtx, localDevId, { localTunnelUrl: session.publicUrl }).catch((err) => {
|
|
7664
|
+
innerCtx.logger.warn({ err: err.message }, "failed to PATCH local-dev tunnel URL (continuing)");
|
|
7665
|
+
return null;
|
|
7666
|
+
});
|
|
7667
|
+
if (patched) installationOrgSlug = patched.organizationSlug;
|
|
7668
|
+
}
|
|
7669
|
+
const studioBaseUrl = innerCtx.apiUrl.includes("localhost") ? "http://localhost:4200/app" : "https://app.stackbone.ai/app";
|
|
7670
|
+
const deeplink = session.publicUrl ? `${studioBaseUrl}/${installationOrgSlug}/installations/${localDevId}?stackbone-dev=${encodeURIComponent(session.publicUrl)}` : null;
|
|
7607
7671
|
if (innerCtx.flags.json) emit(innerCtx.flags, () => "", {
|
|
7608
7672
|
emulator_url: session.emulatorUrl,
|
|
7609
7673
|
agent_url: session.agentUrl,
|
|
7610
7674
|
public_url: session.publicUrl,
|
|
7675
|
+
deeplink,
|
|
7676
|
+
local_dev_installation_id: localDevId,
|
|
7611
7677
|
services: {
|
|
7612
7678
|
postgres_url: session.injection.services.postgresUrl,
|
|
7613
7679
|
queue_url: session.injection.services.queueUrl,
|
|
@@ -7619,6 +7685,7 @@ var buildHandler$2 = (ctx, flags) => protectedCommand(async (innerCtx) => {
|
|
|
7619
7685
|
const inlineLines = formatBootBanner({
|
|
7620
7686
|
tunnelUrl: session.publicUrl,
|
|
7621
7687
|
localUrl: session.emulatorUrl,
|
|
7688
|
+
deeplink,
|
|
7622
7689
|
contractVersion: 10,
|
|
7623
7690
|
capabilityCount: EMULATOR_CAPABILITIES.length,
|
|
7624
7691
|
agentName: agent.name,
|
|
@@ -7630,7 +7697,13 @@ var buildHandler$2 = (ctx, flags) => protectedCommand(async (innerCtx) => {
|
|
|
7630
7697
|
log.step("Press Ctrl-C to stop");
|
|
7631
7698
|
}
|
|
7632
7699
|
const sigintHandler = () => {
|
|
7633
|
-
|
|
7700
|
+
(async () => {
|
|
7701
|
+
if (localDevId) await patchLocalDevInstallation(innerCtx, localDevId, { localTunnelUrl: null }).catch((err) => {
|
|
7702
|
+
innerCtx.logger.debug({ err: err.message }, "failed to null local-dev tunnel URL on exit (safety net will catch up)");
|
|
7703
|
+
return null;
|
|
7704
|
+
});
|
|
7705
|
+
await session.stop();
|
|
7706
|
+
})();
|
|
7634
7707
|
};
|
|
7635
7708
|
process.once("SIGINT", sigintHandler);
|
|
7636
7709
|
process.once("SIGTERM", sigintHandler);
|
|
@@ -7648,12 +7721,6 @@ var createDevCommand = (ctx) => defineCommand({
|
|
|
7648
7721
|
description: `Emulator port. Defaults to ${DEFAULT_EMULATOR_PORT}.`,
|
|
7649
7722
|
default: String(DEFAULT_EMULATOR_PORT)
|
|
7650
7723
|
},
|
|
7651
|
-
tunnel: {
|
|
7652
|
-
type: "boolean",
|
|
7653
|
-
description: "Open a public tunnel to the emulator (default: on).",
|
|
7654
|
-
negativeDescription: "Skip the public tunnel and serve the emulator only on localhost.",
|
|
7655
|
-
default: true
|
|
7656
|
-
},
|
|
7657
7724
|
listen: {
|
|
7658
7725
|
type: "boolean",
|
|
7659
7726
|
description: "Bind the HTTP server to 0.0.0.0 (reachable from your LAN). Default binds to 127.0.0.1 only.",
|
|
@@ -7683,7 +7750,6 @@ var createDevCommand = (ctx) => defineCommand({
|
|
|
7683
7750
|
});
|
|
7684
7751
|
await buildHandler$2(ctx, {
|
|
7685
7752
|
port,
|
|
7686
|
-
tunnel: Boolean(a.tunnel),
|
|
7687
7753
|
listen: Boolean(a.listen),
|
|
7688
7754
|
verbose: Boolean(a.verbose)
|
|
7689
7755
|
});
|
|
@@ -7720,10 +7786,6 @@ several exploratory calls.
|
|
|
7720
7786
|
- \`-y, --yes\`
|
|
7721
7787
|
Skip every confirmation prompt. Required in CI / non-TTY contexts.
|
|
7722
7788
|
|
|
7723
|
-
- \`--no-auto-login\` (or \`STACKBONE_AUTO_LOGIN=0\`)
|
|
7724
|
-
When a protected command finds no valid session, fail fast with exit
|
|
7725
|
-
code 2 instead of triggering the device-code flow inline.
|
|
7726
|
-
|
|
7727
7789
|
## Exit codes
|
|
7728
7790
|
|
|
7729
7791
|
- \`0\` ok
|
|
@@ -8016,7 +8078,6 @@ Branch on these instead of parsing stack traces.
|
|
|
8016
8078
|
|
|
8017
8079
|
- \`STACKBONE_JSON=1\` — same as \`--json\`
|
|
8018
8080
|
- \`STACKBONE_API_URL\` — same as \`--api-url <url>\`
|
|
8019
|
-
- \`STACKBONE_AUTO_LOGIN=0\` — same as \`--no-auto-login\`
|
|
8020
8081
|
- \`STACKBONE_LOG_LEVEL\` — \`debug\`, \`info\`, \`warn\`, \`error\` (logger goes
|
|
8021
8082
|
to stderr, never pollutes the JSON payload on stdout)
|
|
8022
8083
|
|
|
@@ -8161,12 +8222,15 @@ var writeTemplateFiles = async (files, input) => {
|
|
|
8161
8222
|
var writeAgentManifest = async (agentName, input) => {
|
|
8162
8223
|
await writeFileSafe(join(input.root, "agent.yaml"), renderAgentYaml({ name: agentName }), input.force);
|
|
8163
8224
|
};
|
|
8164
|
-
var writeProjectConfig = async (ctx, agent, input) => {
|
|
8225
|
+
var writeProjectConfig = async (ctx, agent, input, extras = {}) => {
|
|
8165
8226
|
const config = {
|
|
8166
8227
|
schemaVersion: 1,
|
|
8167
8228
|
organizationId: agent.organizationId,
|
|
8168
8229
|
agentId: agent.id,
|
|
8169
|
-
controlPlaneUrl: ctx.apiUrl
|
|
8230
|
+
controlPlaneUrl: ctx.apiUrl,
|
|
8231
|
+
...extras.localDevInstallationId !== void 0 ? { localDevInstallationId: extras.localDevInstallationId } : {},
|
|
8232
|
+
...extras.organizationSlug !== void 0 ? { organizationSlug: extras.organizationSlug } : {},
|
|
8233
|
+
...extras.agentSlug !== void 0 ? { agentSlug: extras.agentSlug } : {}
|
|
8170
8234
|
};
|
|
8171
8235
|
await mkdir(join(input.root, ".stackbone"), { recursive: true });
|
|
8172
8236
|
await writeFile(join(input.root, ".stackbone", "project.json"), JSON.stringify(config, null, 2));
|
|
@@ -8500,7 +8564,7 @@ var resolveInputs = async (ctx, flags, namePositional) => {
|
|
|
8500
8564
|
targetDir
|
|
8501
8565
|
};
|
|
8502
8566
|
};
|
|
8503
|
-
var writeProject = async (ctx, resolved, agent, force) => {
|
|
8567
|
+
var writeProject = async (ctx, resolved, agent, force, localDev) => {
|
|
8504
8568
|
const writeInput = {
|
|
8505
8569
|
root: resolved.targetDir,
|
|
8506
8570
|
force
|
|
@@ -8512,7 +8576,11 @@ var writeProject = async (ctx, resolved, agent, force) => {
|
|
|
8512
8576
|
});
|
|
8513
8577
|
await writeTemplateFiles(templateFiles, writeInput);
|
|
8514
8578
|
await writeAgentManifest(resolved.agentName, writeInput);
|
|
8515
|
-
const projectConfig = await writeProjectConfig(ctx, agent, writeInput
|
|
8579
|
+
const projectConfig = await writeProjectConfig(ctx, agent, writeInput, {
|
|
8580
|
+
localDevInstallationId: localDev.id,
|
|
8581
|
+
organizationSlug: localDev.organizationSlug,
|
|
8582
|
+
agentSlug: localDev.agentSlug
|
|
8583
|
+
});
|
|
8516
8584
|
await ensureGitignore(resolved.targetDir);
|
|
8517
8585
|
const skillFiles = await writeSkillFiles(ctx, agent, projectConfig, writeInput);
|
|
8518
8586
|
return { filesWritten: [
|
|
@@ -8535,13 +8603,22 @@ var buildHandler$1 = (ctx, args) => protectedCommand(async (innerCtx) => {
|
|
|
8535
8603
|
const resolved = await resolveInputs(innerCtx, flags, args.name);
|
|
8536
8604
|
await ensureEmptyOrForce(resolved.targetDir, flags.force);
|
|
8537
8605
|
if (!innerCtx.flags.json) log.step(`Creating "${resolved.agentName}" (template: ${resolved.templateId})`);
|
|
8606
|
+
const activeOrg = await getActiveOrganization(innerCtx);
|
|
8538
8607
|
const agent = await createAgent(innerCtx, {
|
|
8539
|
-
organizationSlug:
|
|
8608
|
+
organizationSlug: activeOrg.slug,
|
|
8540
8609
|
name: resolved.agentName,
|
|
8541
8610
|
...resolved.agentSlug !== void 0 ? { slug: resolved.agentSlug } : {},
|
|
8542
8611
|
...resolved.description !== void 0 ? { description: resolved.description } : {}
|
|
8543
8612
|
});
|
|
8544
|
-
const
|
|
8613
|
+
const localDev = await upsertLocalDevInstallation(innerCtx, {
|
|
8614
|
+
creatorOrganizationSlug: activeOrg.slug,
|
|
8615
|
+
agentTemplateSlug: agent.slug
|
|
8616
|
+
});
|
|
8617
|
+
const { filesWritten } = await writeProject(innerCtx, resolved, agent, flags.force, {
|
|
8618
|
+
id: localDev.id,
|
|
8619
|
+
organizationSlug: localDev.organizationSlug,
|
|
8620
|
+
agentSlug: localDev.agentSlug
|
|
8621
|
+
});
|
|
8545
8622
|
emit(innerCtx.flags, () => "", {
|
|
8546
8623
|
agent: {
|
|
8547
8624
|
id: agent.id,
|
|
@@ -8549,6 +8626,10 @@ var buildHandler$1 = (ctx, args) => protectedCommand(async (innerCtx) => {
|
|
|
8549
8626
|
name: agent.name
|
|
8550
8627
|
},
|
|
8551
8628
|
organization_id: agent.organizationId,
|
|
8629
|
+
local_dev_installation: {
|
|
8630
|
+
id: localDev.id,
|
|
8631
|
+
organization_slug: localDev.organizationSlug
|
|
8632
|
+
},
|
|
8552
8633
|
target_dir: resolved.targetDir,
|
|
8553
8634
|
template: resolved.templateId,
|
|
8554
8635
|
files_written: filesWritten
|
|
@@ -8637,11 +8718,20 @@ var buildHandler = (ctx, args) => protectedCommand(async (innerCtx) => {
|
|
|
8637
8718
|
}
|
|
8638
8719
|
chosenSlug = value;
|
|
8639
8720
|
}
|
|
8640
|
-
const
|
|
8721
|
+
const activeOrg = await getActiveOrganization(innerCtx);
|
|
8722
|
+
const agent = await getAgentByOwnerAndSlug(innerCtx, activeOrg.slug, chosenSlug);
|
|
8641
8723
|
if (!innerCtx.flags.json) log.step(`Linking ${targetDir} to ${agent.slug}`);
|
|
8724
|
+
const localDev = await upsertLocalDevInstallation(innerCtx, {
|
|
8725
|
+
creatorOrganizationSlug: activeOrg.slug,
|
|
8726
|
+
agentTemplateSlug: agent.slug
|
|
8727
|
+
});
|
|
8642
8728
|
const projectConfig = await writeProjectConfig(innerCtx, agent, {
|
|
8643
8729
|
root: targetDir,
|
|
8644
8730
|
force: true
|
|
8731
|
+
}, {
|
|
8732
|
+
localDevInstallationId: localDev.id,
|
|
8733
|
+
organizationSlug: localDev.organizationSlug,
|
|
8734
|
+
agentSlug: localDev.agentSlug
|
|
8645
8735
|
});
|
|
8646
8736
|
await ensureGitignore(targetDir);
|
|
8647
8737
|
const skillFiles = await writeSkillFiles(innerCtx, agent, projectConfig, {
|
|
@@ -8655,6 +8745,10 @@ var buildHandler = (ctx, args) => protectedCommand(async (innerCtx) => {
|
|
|
8655
8745
|
name: agent.name
|
|
8656
8746
|
},
|
|
8657
8747
|
organization_id: agent.organizationId,
|
|
8748
|
+
local_dev_installation: {
|
|
8749
|
+
id: localDev.id,
|
|
8750
|
+
organization_slug: localDev.organizationSlug
|
|
8751
|
+
},
|
|
8658
8752
|
target_dir: targetDir,
|
|
8659
8753
|
files_written: [
|
|
8660
8754
|
".stackbone/project.json",
|
|
@@ -8883,11 +8977,6 @@ var buildRootCommand = async () => {
|
|
|
8883
8977
|
"api-url": {
|
|
8884
8978
|
type: "string",
|
|
8885
8979
|
description: "Override the control plane URL."
|
|
8886
|
-
},
|
|
8887
|
-
"no-auto-login": {
|
|
8888
|
-
type: "boolean",
|
|
8889
|
-
description: "Refuse to start the device-code flow when no session exists.",
|
|
8890
|
-
default: false
|
|
8891
8980
|
}
|
|
8892
8981
|
},
|
|
8893
8982
|
subCommands: {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
-- 0004_rename_runs_partial_index.sql — clarify semantics of the is_playground partial index.
|
|
2
|
+
-- ADR: docs/adr/2026-05-14-playground-runs-bill-like-real-invocations.md.
|
|
3
|
+
--
|
|
4
|
+
-- Migration 0003 created the partial index `runs_billable_workspace_started_idx`
|
|
5
|
+
-- on `stackbone_platform.runs (workspace_id, started_at DESC NULLS LAST)
|
|
6
|
+
-- WHERE is_playground = false`. Its name and accompanying comments framed the
|
|
7
|
+
-- predicate as the "billable" set — that framing is wrong.
|
|
8
|
+
--
|
|
9
|
+
-- Playground runs DO bill: invoking from Studio consumes real LLM tokens, real
|
|
10
|
+
-- DB queries and real compute, and the creator's organization is charged for
|
|
11
|
+
-- it like any other invocation (otherwise a malicious client could mark all
|
|
12
|
+
-- production traffic with `X-Stackbone-Playground: 1` and dodge billing). The
|
|
13
|
+
-- predicate `WHERE is_playground = false` actually identifies the rows that
|
|
14
|
+
-- count toward Trust Layer / public marketplace aggregations (p50 latency,
|
|
15
|
+
-- success rate, median cost) — a slow demo run from Studio must not lower
|
|
16
|
+
-- the public stats of an `agent_template`.
|
|
17
|
+
--
|
|
18
|
+
-- This migration renames the index so the next reader gets the semantics
|
|
19
|
+
-- right. The index definition (table, columns, predicate) is unchanged.
|
|
20
|
+
|
|
21
|
+
ALTER INDEX IF EXISTS stackbone_platform.runs_billable_workspace_started_idx
|
|
22
|
+
RENAME TO runs_public_metrics_workspace_started_idx;
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|