@stackbone/cli 0.1.0-alpha.1 → 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,75 @@ 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
+
55
+ ## [0.1.0-alpha.2] - 2026-05-12
56
+
57
+ ### Added
58
+
59
+ - Per-stage spinners during `stackbone dev` (docker, platform/creator/rag
60
+ migrations, agent, emulator, tunnel) and a redesigned rounded-box Studio
61
+ banner that surfaces the actionable deeplink first. New global flag
62
+ `--verbose` (also `STACKBONE_VERBOSE=1`) restores the raw firehose of
63
+ docker compose stdout and pino logs for debugging.
64
+
65
+ ### Changed
66
+
67
+ - `stackbone dev` Studio banner now prints
68
+ `app.stackbone.ai/app?stackbone-dev=<tunnel>` instead of the retired
69
+ `/studio?baseUrl=…` deeplink. The new URL is consumed in one shot by the
70
+ `stackboneDevBootstrapGuard` mounted on `/app`, which flips Studio to
71
+ local mode against the tunnel and opens the panel on Runs.
72
+
73
+ ### Fixed
74
+
75
+ - `stackbone dev` boot banner no longer emits a leading newline or
76
+ duplicate `│` rail rows around the inlined Studio banner when running
77
+ under clack's left rail in human mode.
78
+
10
79
  ## [0.1.0-alpha.1] - 2026-05-12
11
80
 
12
81
  ### Changed
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,13 +1292,16 @@ 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
1270
1300
  //#region src/infra/logger.ts
1271
1301
  var createLogger = (opts) => {
1272
- const level = opts.level ?? process.env["STACKBONE_LOG_LEVEL"] ?? "info";
1302
+ const envLevel = process.env["STACKBONE_LOG_LEVEL"];
1303
+ const defaultLevel = opts.json || opts.verbose ? "info" : "warn";
1304
+ const level = opts.level ?? envLevel ?? defaultLevel;
1273
1305
  if (opts.json) return pino({ level }, pino.destination(2));
1274
1306
  const stream = pretty({
1275
1307
  destination: 2,
@@ -1359,8 +1391,8 @@ var flagValue = (argv, flag) => {
1359
1391
  var parseFlags = (argv, env) => ({
1360
1392
  json: detectJsonMode(argv, env),
1361
1393
  yes: flagPresent(argv, "-y") || flagPresent(argv, "--yes"),
1362
- autoLogin: env["STACKBONE_AUTO_LOGIN"] !== "0" && !flagPresent(argv, "--no-auto-login"),
1363
- apiUrlOverride: flagValue(argv, "--api-url") ?? env["STACKBONE_API_URL"] ?? null
1394
+ apiUrlOverride: flagValue(argv, "--api-url") ?? env["STACKBONE_API_URL"] ?? null,
1395
+ verbose: flagPresent(argv, "--verbose") || env["STACKBONE_VERBOSE"] === "1"
1364
1396
  });
1365
1397
  /**
1366
1398
  * Builds the single `CliContext` value passed to every command.
@@ -1378,7 +1410,10 @@ var parseFlags = (argv, env) => ({
1378
1410
  */
1379
1411
  var createCliContext = async (opts) => {
1380
1412
  const flags = parseFlags(opts.argv, opts.env);
1381
- const logger = createLogger({ json: flags.json });
1413
+ const logger = createLogger({
1414
+ json: flags.json,
1415
+ verbose: flags.verbose
1416
+ });
1382
1417
  const projectConfig = createProjectConfigAdapter(opts.cwd);
1383
1418
  const globalConfig = createGlobalConfigAdapter();
1384
1419
  const tokenStore = createTokenStore();
@@ -1416,7 +1451,7 @@ var isVersionFlag = (rawArgs) => rawArgs.length === 1 && VERSION_FLAGS.has(rawAr
1416
1451
  var formatVersionOutput = (input) => `stackbone-cli ${input.cliVersion}\ncontract ${input.contractVersion}`;
1417
1452
  //#endregion
1418
1453
  //#region src/dev/contract.ts
1419
- var STACKBONE_CLI_BUILD_VERSION = "0.1.0-alpha.1";
1454
+ var STACKBONE_CLI_BUILD_VERSION = "0.1.0-alpha.3";
1420
1455
  /**
1421
1456
  * Capabilities the local emulator advertises on `GET /api/contract`. Typed as
1422
1457
  * `readonly Capability[]` so the compiler refuses any string outside the
@@ -1737,55 +1772,25 @@ var clearSession = async (ctx) => {
1737
1772
  };
1738
1773
  //#endregion
1739
1774
  //#region src/commands/_shared/protected.ts
1740
- var isAuthError = (err) => isStackboneCliError(err) && err.code === "auth";
1741
- var refuseWithAuthError = () => {
1742
- throw new StackboneCliError({
1743
- code: "auth",
1744
- message: "Not authenticated",
1745
- suggestion: "Run `stackbone login`"
1746
- });
1747
- };
1748
- var ensureSession = async (ctx) => {
1749
- const existing = await getCurrentSession(ctx);
1750
- if (existing) return existing;
1751
- if (!ctx.flags.autoLogin) refuseWithAuthError();
1752
- await runLoginFlow(ctx);
1753
- return await getCurrentSession(ctx) ?? refuseWithAuthError();
1754
- };
1755
1775
  /**
1756
1776
  * Wraps a command handler so it always receives a non-null `Session`.
1757
1777
  *
1758
- * If the credentials store is empty or the session expired:
1759
- * - With auto-login enabled (default): runs the device-code flow inline,
1760
- * reloads the session, and runs the handler.
1761
- * - With auto-login disabled (`STACKBONE_AUTO_LOGIN=0` or `--no-auto-login`):
1762
- * refuses immediately with `code: 'auth'` and exit 2.
1763
- *
1764
- * Retries once if the handler itself throws an auth error mid-flight
1765
- * (token expired between the pre-check and the request). If the retry
1766
- * also fails with auth, the corrupt credentials are dropped from disk so
1767
- * 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.
1768
1784
  */
1769
1785
  var protectedCommand = (handler) => {
1770
1786
  return async (ctx) => {
1771
- const session = await ensureSession(ctx);
1772
- try {
1773
- await handler(ctx, session);
1774
- } catch (err) {
1775
- if (!isAuthError(err) || !ctx.flags.autoLogin) throw err;
1776
- ctx.logger.debug("auth error mid-flight, retrying after re-login");
1777
- await runLoginFlow(ctx);
1778
- const refreshed = await getCurrentSession(ctx) ?? refuseWithAuthError();
1779
- try {
1780
- await handler(ctx, refreshed);
1781
- } catch (retryErr) {
1782
- if (isAuthError(retryErr)) {
1783
- ctx.logger.warn("auth still failing after re-login, clearing the stored session");
1784
- await clearSession(ctx);
1785
- }
1786
- throw retryErr;
1787
- }
1788
- }
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);
1789
1794
  };
1790
1795
  };
1791
1796
  //#endregion
@@ -2608,7 +2613,7 @@ var buildMigrateStatusCommand = (ctx) => defineCommand({
2608
2613
  env: ctx.env,
2609
2614
  json: ctx.flags.json
2610
2615
  });
2611
- })({})
2616
+ })()
2612
2617
  });
2613
2618
  /**
2614
2619
  * Pure handler for `stackbone db migrate add-rag`. Reads `agent.yaml` to
@@ -2696,7 +2701,7 @@ var buildStudioCommand = (ctx) => defineCommand({
2696
2701
  env: ctx.env,
2697
2702
  json: ctx.flags.json
2698
2703
  });
2699
- })({})
2704
+ })()
2700
2705
  });
2701
2706
  var createDbCommand = (ctx) => defineCommand({
2702
2707
  meta: {
@@ -2741,22 +2746,108 @@ var createDeployCommand = (ctx) => buildStubCommand(ctx, {
2741
2746
  });
2742
2747
  //#endregion
2743
2748
  //#region src/dev/boot-banner.ts
2744
- var STUDIO_CLOUD_BASE_URL = "https://app.stackbone.ai/studio";
2745
- var BORDER = "".repeat(72);
2746
- var buildDeeplink = (tunnelUrl) => `${STUDIO_CLOUD_BASE_URL}?baseUrl=${encodeURIComponent(tunnelUrl)}`;
2749
+ var ANSI = {
2750
+ reset: "\x1B[0m",
2751
+ bold: "\x1B[1m",
2752
+ dim: "\x1B[2m",
2753
+ cyan: "\x1B[36m",
2754
+ underline: "\x1B[4m"
2755
+ };
2756
+ var ANSI_RE = /\x1b\[[0-9;]*m/g;
2757
+ var visibleLength = (s) => s.replace(ANSI_RE, "").length;
2758
+ var LABEL_WIDTH = 11;
2759
+ var renderRow = (row, colors) => {
2760
+ const marker = colors && row.marker.trim() ? `${ANSI.cyan}${row.marker}${ANSI.reset}` : row.marker;
2761
+ const labelText = row.label.padEnd(LABEL_WIDTH, " ");
2762
+ return `${marker} ${colors ? `${ANSI.dim}${labelText}${ANSI.reset}` : labelText} ${colors && row.primary ? `${ANSI.bold}${ANSI.cyan}${row.value}${ANSI.reset}` : row.value}`;
2763
+ };
2764
+ var TITLE = "stackbone studio";
2747
2765
  var formatBootBanner = (input) => {
2766
+ const colors = input.colors ?? false;
2767
+ const rows = [];
2768
+ if (input.deeplink && input.tunnelUrl) {
2769
+ rows.push({
2770
+ marker: "▸",
2771
+ label: "Open Studio",
2772
+ value: input.deeplink,
2773
+ primary: true
2774
+ });
2775
+ rows.push({
2776
+ marker: " ",
2777
+ label: "",
2778
+ value: ""
2779
+ });
2780
+ rows.push({
2781
+ marker: " ",
2782
+ label: "Tunnel",
2783
+ value: input.tunnelUrl
2784
+ });
2785
+ rows.push({
2786
+ marker: " ",
2787
+ label: "Local",
2788
+ value: input.localUrl
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
+ });
2807
+ } else rows.push({
2808
+ marker: "▸",
2809
+ label: "Local",
2810
+ value: input.localUrl,
2811
+ primary: true
2812
+ });
2813
+ rows.push({
2814
+ marker: " ",
2815
+ label: "",
2816
+ value: ""
2817
+ });
2818
+ rows.push({
2819
+ marker: " ",
2820
+ label: "Agent",
2821
+ value: input.agentName
2822
+ });
2823
+ rows.push({
2824
+ marker: " ",
2825
+ label: "Protocol",
2826
+ value: `v${input.contractVersion} · ${input.capabilityCount} capabilities`
2827
+ });
2828
+ const renderedRows = rows.map((row) => {
2829
+ if (row.label === "" && row.value === "") return "";
2830
+ return renderRow(row, colors);
2831
+ });
2832
+ const padding = 2;
2833
+ const longestRow = renderedRows.reduce((max, line) => Math.max(max, visibleLength(line)), 0);
2834
+ const innerWidth = Math.max(longestRow, 22);
2835
+ const totalInner = innerWidth + padding * 2;
2836
+ const titleSegment = ` ${TITLE} `;
2837
+ const titleSegmentColored = colors ? ` ${ANSI.bold}${TITLE}${ANSI.reset} ` : titleSegment;
2838
+ const dashesAfter = totalInner - 1 - titleSegment.length;
2839
+ const top = `╭─${titleSegmentColored}${"─".repeat(Math.max(dashesAfter, 1))}╮`;
2840
+ const bottom = `╰${"─".repeat(totalInner)}╯`;
2841
+ const padRow = (line) => {
2842
+ const trailing = innerWidth - visibleLength(line);
2843
+ return `│ ${line}${" ".repeat(Math.max(trailing, 0))} │`;
2844
+ };
2748
2845
  const lines = [];
2749
- lines.push(BORDER);
2750
- lines.push(" Stackbone Studio");
2751
- lines.push("");
2752
- if (input.tunnelUrl) {
2753
- lines.push(` Tunnel: ${input.tunnelUrl}`);
2754
- lines.push(` Studio: ${buildDeeplink(input.tunnelUrl)}`);
2755
- lines.push(` Local: ${input.localUrl} (sin tunnel)`);
2756
- } else lines.push(` Local: ${input.localUrl}`);
2757
- lines.push(` Contract version ${input.contractVersion} (capabilities: ${input.capabilityCount})`);
2758
- lines.push("");
2759
- lines.push(BORDER);
2846
+ lines.push(top);
2847
+ lines.push(padRow(""));
2848
+ for (const line of renderedRows) lines.push(padRow(line));
2849
+ lines.push(padRow(""));
2850
+ lines.push(bottom);
2760
2851
  return lines.join("\n");
2761
2852
  };
2762
2853
  //#endregion
@@ -2896,6 +2987,7 @@ var fetchActiveConfigValue = async (client) => {
2896
2987
  //#region src/dev/connections.ts
2897
2988
  var STACKBONE_ENV_KEYS = {
2898
2989
  contractVersion: "STACKBONE_CONTRACT_VERSION",
2990
+ agentId: "STACKBONE_AGENT_ID",
2899
2991
  postgresUrl: "STACKBONE_POSTGRES_URL",
2900
2992
  queueUrl: "STACKBONE_QUEUE_URL",
2901
2993
  s3Endpoint: "STACKBONE_S3_ENDPOINT",
@@ -2910,6 +3002,7 @@ var buildAgentEnv = (base, injection, options = {}) => {
2910
3002
  const env = {
2911
3003
  ...base,
2912
3004
  [STACKBONE_ENV_KEYS.contractVersion]: String(10),
3005
+ [STACKBONE_ENV_KEYS.agentId]: injection.agentId,
2913
3006
  [STACKBONE_ENV_KEYS.postgresUrl]: injection.services.postgresUrl,
2914
3007
  [STACKBONE_ENV_KEYS.queueUrl]: injection.services.queueUrl,
2915
3008
  [STACKBONE_ENV_KEYS.s3Endpoint]: injection.services.s3Endpoint,
@@ -3477,21 +3570,34 @@ volumes:
3477
3570
  //#endregion
3478
3571
  //#region src/dev/compose.ts
3479
3572
  var DOCKER_BIN = process.env["STACKBONE_DOCKER_BIN"] ?? "docker";
3480
- var runDocker = async (args, cwd, signal) => {
3573
+ var runDocker = async (args, cwd, opts = {}) => {
3481
3574
  await new Promise((resolve, reject) => {
3575
+ const captured = [];
3482
3576
  const child = spawn(DOCKER_BIN, args, {
3483
3577
  cwd,
3484
- stdio: [
3578
+ stdio: opts.verbose ? [
3485
3579
  "ignore",
3486
3580
  "inherit",
3487
3581
  "inherit"
3582
+ ] : [
3583
+ "ignore",
3584
+ "pipe",
3585
+ "pipe"
3488
3586
  ],
3489
- signal
3587
+ signal: opts.signal
3490
3588
  });
3589
+ if (!opts.verbose) {
3590
+ child.stdout?.on("data", (chunk) => captured.push(chunk));
3591
+ child.stderr?.on("data", (chunk) => captured.push(chunk));
3592
+ }
3491
3593
  child.once("error", reject);
3492
3594
  child.once("close", (code) => {
3493
- if (code === 0) resolve();
3494
- else reject(new StackboneCliError({
3595
+ if (code === 0) {
3596
+ resolve();
3597
+ return;
3598
+ }
3599
+ if (!opts.verbose && captured.length > 0) process.stderr.write(Buffer.concat(captured));
3600
+ reject(new StackboneCliError({
3495
3601
  code: "generic",
3496
3602
  message: `\`docker ${args.join(" ")}\` exited with code ${code}`,
3497
3603
  suggestion: "Check Docker Desktop is running and that the embedded compose file in `.stackbone/dev/` is valid"
@@ -3519,12 +3625,13 @@ var writeComposeFiles = async (runtime) => {
3519
3625
  await writeFile(join(runtime.workdir, "Dockerfile.postgres"), POSTGRES_DOCKERFILE);
3520
3626
  };
3521
3627
  var composeUp = async (runtime) => {
3628
+ const verbose = runtime.verbose ?? false;
3522
3629
  await runDocker([
3523
3630
  "compose",
3524
3631
  "up",
3525
3632
  "-d",
3526
3633
  "--wait"
3527
- ], runtime.workdir);
3634
+ ], runtime.workdir, { verbose });
3528
3635
  await runDocker([
3529
3636
  "compose",
3530
3637
  "--profile",
@@ -3532,11 +3639,11 @@ var composeUp = async (runtime) => {
3532
3639
  "run",
3533
3640
  "--rm",
3534
3641
  "minio-bucket-init"
3535
- ], runtime.workdir);
3642
+ ], runtime.workdir, { verbose });
3536
3643
  };
3537
3644
  var composeDown = async (runtime) => {
3538
3645
  try {
3539
- await runDocker(["compose", "down"], runtime.workdir);
3646
+ await runDocker(["compose", "down"], runtime.workdir, { verbose: runtime.verbose ?? false });
3540
3647
  } catch {}
3541
3648
  };
3542
3649
  var buildComposeRuntime = (projectRoot, options) => {
@@ -4051,7 +4158,7 @@ var quoteIdent = (raw) => `"${raw.replace(/"/g, "\"\"")}"`;
4051
4158
  //#region src/dev/fixtures-store.ts
4052
4159
  var defaultFixturesDir = () => resolve(homedir(), ".stackbone", "dev", "fixtures");
4053
4160
  var filenameFor = (baseDir, agentSlug) => resolve(baseDir, `${agentSlug}.jsonl`);
4054
- var isSerializedFixture = (value) => !!value && typeof value === "object" && typeof value.id === "string" && typeof value.name === "string" && typeof value.created_at === "string";
4161
+ var isSerializedFixture = (value) => !!value && typeof value === "object" && typeof value.id === "string" && typeof value.name === "string" && typeof value.createdAt === "string";
4055
4162
  var readJsonl = async (path) => {
4056
4163
  let raw;
4057
4164
  try {
@@ -4078,7 +4185,7 @@ var createFixturesStore = (opts) => {
4078
4185
  };
4079
4186
  return {
4080
4187
  async list() {
4081
- return (await readJsonl(path)).sort((a, b) => a.created_at < b.created_at ? 1 : -1);
4188
+ return (await readJsonl(path)).sort((a, b) => a.createdAt < b.createdAt ? 1 : -1);
4082
4189
  },
4083
4190
  async create(input) {
4084
4191
  await ensureDir();
@@ -4088,7 +4195,7 @@ var createFixturesStore = (opts) => {
4088
4195
  name: input.name,
4089
4196
  input: input.input,
4090
4197
  description: input.description ?? null,
4091
- created_at: (/* @__PURE__ */ new Date()).toISOString()
4198
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
4092
4199
  };
4093
4200
  await appendFile(path, `${JSON.stringify(fixture)}\n`, "utf8");
4094
4201
  return fixture;
@@ -5335,22 +5442,25 @@ var buildEmulatorApp = (opts) => {
5335
5442
  app.get("/api/logs/stream", (c) => streamSSE(c, (stream) => {
5336
5443
  return runLogTail(stream, parseLogFilters(new URL(c.req.url).searchParams), (emit) => tailJsonlDirectory(logsDir, emit));
5337
5444
  }));
5338
- const proxyAgentJson = async (path) => {
5445
+ app.get("/api/schema", async (c) => {
5339
5446
  try {
5340
- const resp = await fetch(`${agentBaseUrl}${path}`);
5341
- const body = await resp.text();
5342
- return new Response(body, {
5343
- status: resp.status,
5344
- headers: { "content-type": "application/json" }
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")
5345
5456
  });
5346
5457
  } catch {
5347
- return new Response(JSON.stringify({ error: "agent_unreachable" }), {
5348
- status: 502,
5349
- headers: { "content-type": "application/json" }
5458
+ return c.json({
5459
+ input: null,
5460
+ output: null
5350
5461
  });
5351
5462
  }
5352
- };
5353
- app.get("/api/schema", () => proxyAgentJson("/schema"));
5463
+ });
5354
5464
  const dbExplorerOptions = { connectionString: opts.injection.services.postgresUrl };
5355
5465
  app.get("/api/db/schemas", async (c) => {
5356
5466
  const response = await listSchemas(dbExplorerOptions);
@@ -5953,6 +6063,12 @@ var buildEmulatorApp = (opts) => {
5953
6063
  }));
5954
6064
  return app;
5955
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
+ };
5956
6072
  var eventMatchesRunId = (event, runId) => {
5957
6073
  const data = event.data;
5958
6074
  if (!data || typeof data["run_id"] === "undefined") return true;
@@ -7080,10 +7196,12 @@ var autoMigrationName = () => {
7080
7196
  return `auto_${(/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/T/, "_").slice(0, 15)}`;
7081
7197
  };
7082
7198
  var startDevSession = async (input) => {
7199
+ const onStage = input.onStage ?? (() => void 0);
7083
7200
  await ensureDockerAvailable();
7084
7201
  const bus = createEventBus();
7085
7202
  const agentPort = await pickFreePort();
7086
7203
  const injection = {
7204
+ agentId: input.agent.slug,
7087
7205
  agentPort,
7088
7206
  services: DEFAULT_DEV_SERVICES
7089
7207
  };
@@ -7099,13 +7217,40 @@ var startDevSession = async (input) => {
7099
7217
  minioRootPassword: "stackbone-secret",
7100
7218
  minioDefaultBucket: "stackbone-dev"
7101
7219
  });
7220
+ if (input.verbose) compose.verbose = true;
7102
7221
  await writeComposeFiles(compose);
7103
- await composeUp(compose);
7222
+ onStage({
7223
+ stage: "docker",
7224
+ status: "start"
7225
+ });
7226
+ try {
7227
+ await composeUp(compose);
7228
+ onStage({
7229
+ stage: "docker",
7230
+ status: "success",
7231
+ detail: "postgres + minio ready"
7232
+ });
7233
+ } catch (err) {
7234
+ onStage({
7235
+ stage: "docker",
7236
+ status: "fail",
7237
+ detail: err instanceof Error ? err.message : String(err)
7238
+ });
7239
+ throw err;
7240
+ }
7104
7241
  const migrationsDir = resolve(dirname(fileURLToPath(import.meta.url)), "migrations");
7242
+ onStage({
7243
+ stage: "platform-migrations",
7244
+ status: "start"
7245
+ });
7246
+ let platformApplied = 0;
7247
+ let platformSkipped = 0;
7105
7248
  try {
7106
7249
  await applyMigrations(injection.services.postgresUrl, {
7107
7250
  migrationsDir,
7108
7251
  onMigration: (event) => {
7252
+ if (event.action === "applied") platformApplied += 1;
7253
+ else platformSkipped += 1;
7109
7254
  input.ctx.logger.info({
7110
7255
  migration: event.version,
7111
7256
  action: event.action,
@@ -7113,7 +7258,17 @@ var startDevSession = async (input) => {
7113
7258
  }, `stackbone_platform schema: ${event.action} ${String(event.version).padStart(4, "0")} in ${event.durationMs}ms`);
7114
7259
  }
7115
7260
  });
7261
+ onStage({
7262
+ stage: "platform-migrations",
7263
+ status: "success",
7264
+ detail: platformApplied > 0 ? `${platformApplied} applied, ${platformSkipped} skipped` : `${platformSkipped} already applied`
7265
+ });
7116
7266
  } catch (err) {
7267
+ onStage({
7268
+ stage: "platform-migrations",
7269
+ status: "fail",
7270
+ detail: err instanceof Error ? err.message : String(err)
7271
+ });
7117
7272
  await composeDown(compose);
7118
7273
  throw new StackboneCliError({
7119
7274
  code: "generic",
@@ -7122,39 +7277,67 @@ var startDevSession = async (input) => {
7122
7277
  cause: err
7123
7278
  });
7124
7279
  }
7280
+ onStage({
7281
+ stage: "creator-migrations",
7282
+ status: "start"
7283
+ });
7125
7284
  try {
7126
7285
  await runPreBootMigrate({
7127
7286
  agentRoot: input.projectRoot,
7128
7287
  connectionString: injection.services.postgresUrl,
7129
7288
  logger: input.ctx.logger
7130
7289
  });
7290
+ onStage({
7291
+ stage: "creator-migrations",
7292
+ status: "success"
7293
+ });
7131
7294
  } catch (err) {
7295
+ onStage({
7296
+ stage: "creator-migrations",
7297
+ status: "fail",
7298
+ detail: err instanceof Error ? err.message : String(err)
7299
+ });
7132
7300
  await composeDown(compose);
7133
7301
  throw err;
7134
7302
  }
7135
7303
  const preBootRagManifest = readWatcherManifest(input.projectRoot);
7136
- if (preBootRagManifest !== null) try {
7137
- await runPreBootRag({
7138
- agentRoot: input.projectRoot,
7139
- migrationsDir: preBootRagManifest.migrations,
7140
- autoMigrate: preBootRagManifest.ragAutoMigrate,
7141
- logger: input.ctx.logger,
7142
- addRag,
7143
- runMigrateUp: () => migrateUp({
7144
- connectionString: injection.services.postgresUrl,
7145
- migrationsDir: preBootRagManifest.migrations,
7146
- onMigration: (event) => {
7147
- input.ctx.logger.info({
7148
- migration: event.tag,
7149
- action: event.action,
7150
- durationMs: event.durationMs
7151
- }, `RAG schema: ${event.action} ${event.tag}`);
7152
- }
7153
- })
7304
+ if (preBootRagManifest !== null) {
7305
+ onStage({
7306
+ stage: "rag-migrations",
7307
+ status: "start"
7154
7308
  });
7155
- } catch (err) {
7156
- await composeDown(compose);
7157
- throw err;
7309
+ try {
7310
+ await runPreBootRag({
7311
+ agentRoot: input.projectRoot,
7312
+ migrationsDir: preBootRagManifest.migrations,
7313
+ autoMigrate: preBootRagManifest.ragAutoMigrate,
7314
+ logger: input.ctx.logger,
7315
+ addRag,
7316
+ runMigrateUp: () => migrateUp({
7317
+ connectionString: injection.services.postgresUrl,
7318
+ migrationsDir: preBootRagManifest.migrations,
7319
+ onMigration: (event) => {
7320
+ input.ctx.logger.info({
7321
+ migration: event.tag,
7322
+ action: event.action,
7323
+ durationMs: event.durationMs
7324
+ }, `RAG schema: ${event.action} ${event.tag}`);
7325
+ }
7326
+ })
7327
+ });
7328
+ onStage({
7329
+ stage: "rag-migrations",
7330
+ status: "success"
7331
+ });
7332
+ } catch (err) {
7333
+ onStage({
7334
+ stage: "rag-migrations",
7335
+ status: "fail",
7336
+ detail: err instanceof Error ? err.message : String(err)
7337
+ });
7338
+ await composeDown(compose);
7339
+ throw err;
7340
+ }
7158
7341
  }
7159
7342
  const invocations = createInvocationsStore({ connectionString: injection.services.postgresUrl });
7160
7343
  const approvals = createApprovalsStore({
@@ -7199,6 +7382,10 @@ var startDevSession = async (input) => {
7199
7382
  });
7200
7383
  };
7201
7384
  try {
7385
+ onStage({
7386
+ stage: "agent",
7387
+ status: "start"
7388
+ });
7202
7389
  agent = startAgentProcess({
7203
7390
  cwd: input.projectRoot,
7204
7391
  env: input.ctx.env,
@@ -7207,6 +7394,11 @@ var startDevSession = async (input) => {
7207
7394
  injection,
7208
7395
  agentConfig: agentConfigEnv
7209
7396
  });
7397
+ onStage({
7398
+ stage: "agent",
7399
+ status: "success",
7400
+ detail: `http://127.0.0.1:${agentPort}`
7401
+ });
7210
7402
  configWatcher = await startAgentConfigWatcher({
7211
7403
  connectionString: injection.services.postgresUrl,
7212
7404
  logger: input.ctx.logger,
@@ -7261,25 +7453,63 @@ var startDevSession = async (input) => {
7261
7453
  envValue: input.ctx.env["STACKBONE_CORS_ALLOW_ORIGINS"],
7262
7454
  fileValue: fileConfig?.studio?.corsOrigins
7263
7455
  });
7264
- emulator = await startEmulatorServer({
7265
- port: input.emulatorPort,
7266
- bus,
7267
- invocations,
7268
- approvals,
7269
- injection,
7270
- agent: input.agent,
7271
- cors: corsAllowlist,
7272
- listen: input.listen
7456
+ onStage({
7457
+ stage: "emulator",
7458
+ status: "start"
7273
7459
  });
7460
+ try {
7461
+ emulator = await startEmulatorServer({
7462
+ port: input.emulatorPort,
7463
+ bus,
7464
+ invocations,
7465
+ approvals,
7466
+ injection,
7467
+ agent: input.agent,
7468
+ cors: corsAllowlist,
7469
+ listen: input.listen
7470
+ });
7471
+ onStage({
7472
+ stage: "emulator",
7473
+ status: "success",
7474
+ detail: emulator.url
7475
+ });
7476
+ } catch (err) {
7477
+ onStage({
7478
+ stage: "emulator",
7479
+ status: "fail",
7480
+ detail: err instanceof Error ? err.message : String(err)
7481
+ });
7482
+ throw err;
7483
+ }
7274
7484
  if (input.listen) input.ctx.logger.warn({
7275
7485
  boundTo: "0.0.0.0",
7276
7486
  port: input.emulatorPort
7277
7487
  }, `WARNING: emulator bound to 0.0.0.0:${input.emulatorPort} — reachable from your LAN`);
7278
- if (input.tunnel) tunnel = await openDevTunnel({
7279
- targetUrl: emulator.url,
7280
- ctx: input.ctx,
7281
- projectRoot: input.projectRoot
7282
- });
7488
+ if (input.tunnel) {
7489
+ onStage({
7490
+ stage: "tunnel",
7491
+ status: "start"
7492
+ });
7493
+ try {
7494
+ tunnel = await openDevTunnel({
7495
+ targetUrl: emulator.url,
7496
+ ctx: input.ctx,
7497
+ projectRoot: input.projectRoot
7498
+ });
7499
+ onStage({
7500
+ stage: "tunnel",
7501
+ status: "success",
7502
+ detail: tunnel.publicUrl
7503
+ });
7504
+ } catch (err) {
7505
+ onStage({
7506
+ stage: "tunnel",
7507
+ status: "fail",
7508
+ detail: err instanceof Error ? err.message : String(err)
7509
+ });
7510
+ throw err;
7511
+ }
7512
+ }
7283
7513
  return {
7284
7514
  emulatorUrl: emulator.url,
7285
7515
  agentUrl: `http://127.0.0.1:${agentPort}`,
@@ -7299,6 +7529,24 @@ var startDevSession = async (input) => {
7299
7529
  }
7300
7530
  };
7301
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
7302
7550
  //#region src/commands/dev/print-contract.ts
7303
7551
  var defaultWriteStdout = (chunk) => {
7304
7552
  process.stdout.write(chunk);
@@ -7317,6 +7565,60 @@ var printContract = (deps = {}) => {
7317
7565
  };
7318
7566
  //#endregion
7319
7567
  //#region src/commands/dev/dev.command.ts
7568
+ var STAGE_LABELS = {
7569
+ docker: {
7570
+ start: "Starting docker services",
7571
+ done: "Docker services ready"
7572
+ },
7573
+ "platform-migrations": {
7574
+ start: "Applying platform migrations",
7575
+ done: "Platform migrations applied"
7576
+ },
7577
+ "creator-migrations": {
7578
+ start: "Applying agent migrations",
7579
+ done: "Agent migrations applied"
7580
+ },
7581
+ "rag-migrations": {
7582
+ start: "Preparing RAG schema",
7583
+ done: "RAG schema ready"
7584
+ },
7585
+ agent: {
7586
+ start: "Starting agent process",
7587
+ done: "Agent process ready"
7588
+ },
7589
+ emulator: {
7590
+ start: "Starting emulator",
7591
+ done: "Emulator listening"
7592
+ },
7593
+ tunnel: {
7594
+ start: "Opening tunnel",
7595
+ done: "Tunnel ready"
7596
+ }
7597
+ };
7598
+ /**
7599
+ * Wires `onStage` events from the orchestrator to a single rolling clack
7600
+ * spinner. Only one stage is in flight at a time (the orchestrator is
7601
+ * sequential), so we keep one handle and recycle it across stages.
7602
+ */
7603
+ var createStageRenderer = () => {
7604
+ let active = null;
7605
+ let activeStage = null;
7606
+ return (event) => {
7607
+ const label = STAGE_LABELS[event.stage];
7608
+ if (event.status === "start") {
7609
+ active = spinner();
7610
+ activeStage = event.stage;
7611
+ active.start(label.start);
7612
+ return;
7613
+ }
7614
+ if (active === null || activeStage !== event.stage) return;
7615
+ const detail = event.detail ? ` (${event.detail})` : "";
7616
+ if (event.status === "success") active.stop(`${label.done}${detail}`);
7617
+ else active.error(`${label.start} failed${detail}`);
7618
+ active = null;
7619
+ activeStage = null;
7620
+ };
7621
+ };
7320
7622
  var buildHandler$2 = (ctx, flags) => protectedCommand(async (innerCtx) => {
7321
7623
  const config = await innerCtx.projectConfig.read();
7322
7624
  if (!config) throw new StackboneCliError({
@@ -7332,6 +7634,7 @@ var buildHandler$2 = (ctx, flags) => protectedCommand(async (innerCtx) => {
7332
7634
  message: "The agent linked to this directory no longer exists in the workspace",
7333
7635
  suggestion: "Run `stackbone link --force --agent <slug>` to re-link to a different agent"
7334
7636
  });
7637
+ const renderStage = innerCtx.flags.json ? null : createStageRenderer();
7335
7638
  const session = await startDevSession({
7336
7639
  ctx: innerCtx,
7337
7640
  projectRoot,
@@ -7340,13 +7643,37 @@ var buildHandler$2 = (ctx, flags) => protectedCommand(async (innerCtx) => {
7340
7643
  name: agent.name
7341
7644
  },
7342
7645
  emulatorPort: flags.port,
7343
- tunnel: flags.tunnel,
7344
- listen: flags.listen
7345
- });
7346
- emit(innerCtx.flags, () => "", {
7646
+ tunnel: true,
7647
+ listen: flags.listen,
7648
+ verbose: flags.verbose,
7649
+ ...renderStage ? { onStage: renderStage } : {}
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;
7671
+ if (innerCtx.flags.json) emit(innerCtx.flags, () => "", {
7347
7672
  emulator_url: session.emulatorUrl,
7348
7673
  agent_url: session.agentUrl,
7349
7674
  public_url: session.publicUrl,
7675
+ deeplink,
7676
+ local_dev_installation_id: localDevId,
7350
7677
  services: {
7351
7678
  postgres_url: session.injection.services.postgresUrl,
7352
7679
  queue_url: session.injection.services.queueUrl,
@@ -7354,18 +7681,29 @@ var buildHandler$2 = (ctx, flags) => protectedCommand(async (innerCtx) => {
7354
7681
  s3_bucket: session.injection.services.s3Bucket
7355
7682
  }
7356
7683
  });
7357
- if (!innerCtx.flags.json) {
7358
- log.step(`Agent: ${session.agentUrl}`);
7359
- console.log(`\n${formatBootBanner({
7684
+ else {
7685
+ const inlineLines = formatBootBanner({
7360
7686
  tunnelUrl: session.publicUrl,
7361
7687
  localUrl: session.emulatorUrl,
7688
+ deeplink,
7362
7689
  contractVersion: 10,
7363
- capabilityCount: EMULATOR_CAPABILITIES.length
7364
- })}\n`);
7365
- log.step("Press Ctrl-C to stop.");
7690
+ capabilityCount: EMULATOR_CAPABILITIES.length,
7691
+ agentName: agent.name,
7692
+ colors: process.stdout.isTTY === true
7693
+ }).split("\n").slice(1, -1).map((line) => line.replace(/^│ {2}/, "").replace(/ {2}│$/, "").trimEnd());
7694
+ while (inlineLines.length > 0 && inlineLines[0] === "") inlineLines.shift();
7695
+ while (inlineLines.length > 0 && inlineLines[inlineLines.length - 1] === "") inlineLines.pop();
7696
+ log.message(inlineLines.join("\n"));
7697
+ log.step("Press Ctrl-C to stop");
7366
7698
  }
7367
7699
  const sigintHandler = () => {
7368
- session.stop();
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
+ })();
7369
7707
  };
7370
7708
  process.once("SIGINT", sigintHandler);
7371
7709
  process.once("SIGTERM", sigintHandler);
@@ -7383,12 +7721,6 @@ var createDevCommand = (ctx) => defineCommand({
7383
7721
  description: `Emulator port. Defaults to ${DEFAULT_EMULATOR_PORT}.`,
7384
7722
  default: String(DEFAULT_EMULATOR_PORT)
7385
7723
  },
7386
- tunnel: {
7387
- type: "boolean",
7388
- description: "Open a public tunnel to the emulator (default: on).",
7389
- negativeDescription: "Skip the public tunnel and serve the emulator only on localhost.",
7390
- default: true
7391
- },
7392
7724
  listen: {
7393
7725
  type: "boolean",
7394
7726
  description: "Bind the HTTP server to 0.0.0.0 (reachable from your LAN). Default binds to 127.0.0.1 only.",
@@ -7398,6 +7730,11 @@ var createDevCommand = (ctx) => defineCommand({
7398
7730
  type: "boolean",
7399
7731
  description: "Print the JSON contract this CLI advertises (the same payload the emulator serves at /api/contract) and exit without booting the dev session.",
7400
7732
  default: false
7733
+ },
7734
+ verbose: {
7735
+ type: "boolean",
7736
+ description: "Stream every log line and docker compose output. Default UI uses per-stage spinners; --verbose switches back to the raw firehose.",
7737
+ default: false
7401
7738
  }
7402
7739
  },
7403
7740
  run: ({ args }) => safeRun(ctx.flags, async (a) => {
@@ -7413,8 +7750,8 @@ var createDevCommand = (ctx) => defineCommand({
7413
7750
  });
7414
7751
  await buildHandler$2(ctx, {
7415
7752
  port,
7416
- tunnel: Boolean(a.tunnel),
7417
- listen: Boolean(a.listen)
7753
+ listen: Boolean(a.listen),
7754
+ verbose: Boolean(a.verbose)
7418
7755
  });
7419
7756
  })(args)
7420
7757
  });
@@ -7449,10 +7786,6 @@ several exploratory calls.
7449
7786
  - \`-y, --yes\`
7450
7787
  Skip every confirmation prompt. Required in CI / non-TTY contexts.
7451
7788
 
7452
- - \`--no-auto-login\` (or \`STACKBONE_AUTO_LOGIN=0\`)
7453
- When a protected command finds no valid session, fail fast with exit
7454
- code 2 instead of triggering the device-code flow inline.
7455
-
7456
7789
  ## Exit codes
7457
7790
 
7458
7791
  - \`0\` ok
@@ -7745,7 +8078,6 @@ Branch on these instead of parsing stack traces.
7745
8078
 
7746
8079
  - \`STACKBONE_JSON=1\` — same as \`--json\`
7747
8080
  - \`STACKBONE_API_URL\` — same as \`--api-url <url>\`
7748
- - \`STACKBONE_AUTO_LOGIN=0\` — same as \`--no-auto-login\`
7749
8081
  - \`STACKBONE_LOG_LEVEL\` — \`debug\`, \`info\`, \`warn\`, \`error\` (logger goes
7750
8082
  to stderr, never pollutes the JSON payload on stdout)
7751
8083
 
@@ -7890,12 +8222,15 @@ var writeTemplateFiles = async (files, input) => {
7890
8222
  var writeAgentManifest = async (agentName, input) => {
7891
8223
  await writeFileSafe(join(input.root, "agent.yaml"), renderAgentYaml({ name: agentName }), input.force);
7892
8224
  };
7893
- var writeProjectConfig = async (ctx, agent, input) => {
8225
+ var writeProjectConfig = async (ctx, agent, input, extras = {}) => {
7894
8226
  const config = {
7895
8227
  schemaVersion: 1,
7896
8228
  organizationId: agent.organizationId,
7897
8229
  agentId: agent.id,
7898
- 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 } : {}
7899
8234
  };
7900
8235
  await mkdir(join(input.root, ".stackbone"), { recursive: true });
7901
8236
  await writeFile(join(input.root, ".stackbone", "project.json"), JSON.stringify(config, null, 2));
@@ -8229,7 +8564,7 @@ var resolveInputs = async (ctx, flags, namePositional) => {
8229
8564
  targetDir
8230
8565
  };
8231
8566
  };
8232
- var writeProject = async (ctx, resolved, agent, force) => {
8567
+ var writeProject = async (ctx, resolved, agent, force, localDev) => {
8233
8568
  const writeInput = {
8234
8569
  root: resolved.targetDir,
8235
8570
  force
@@ -8241,7 +8576,11 @@ var writeProject = async (ctx, resolved, agent, force) => {
8241
8576
  });
8242
8577
  await writeTemplateFiles(templateFiles, writeInput);
8243
8578
  await writeAgentManifest(resolved.agentName, writeInput);
8244
- 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
+ });
8245
8584
  await ensureGitignore(resolved.targetDir);
8246
8585
  const skillFiles = await writeSkillFiles(ctx, agent, projectConfig, writeInput);
8247
8586
  return { filesWritten: [
@@ -8264,13 +8603,22 @@ var buildHandler$1 = (ctx, args) => protectedCommand(async (innerCtx) => {
8264
8603
  const resolved = await resolveInputs(innerCtx, flags, args.name);
8265
8604
  await ensureEmptyOrForce(resolved.targetDir, flags.force);
8266
8605
  if (!innerCtx.flags.json) log.step(`Creating "${resolved.agentName}" (template: ${resolved.templateId})`);
8606
+ const activeOrg = await getActiveOrganization(innerCtx);
8267
8607
  const agent = await createAgent(innerCtx, {
8268
- organizationSlug: (await getActiveOrganization(innerCtx)).slug,
8608
+ organizationSlug: activeOrg.slug,
8269
8609
  name: resolved.agentName,
8270
8610
  ...resolved.agentSlug !== void 0 ? { slug: resolved.agentSlug } : {},
8271
8611
  ...resolved.description !== void 0 ? { description: resolved.description } : {}
8272
8612
  });
8273
- const { filesWritten } = await writeProject(innerCtx, resolved, agent, flags.force);
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
+ });
8274
8622
  emit(innerCtx.flags, () => "", {
8275
8623
  agent: {
8276
8624
  id: agent.id,
@@ -8278,6 +8626,10 @@ var buildHandler$1 = (ctx, args) => protectedCommand(async (innerCtx) => {
8278
8626
  name: agent.name
8279
8627
  },
8280
8628
  organization_id: agent.organizationId,
8629
+ local_dev_installation: {
8630
+ id: localDev.id,
8631
+ organization_slug: localDev.organizationSlug
8632
+ },
8281
8633
  target_dir: resolved.targetDir,
8282
8634
  template: resolved.templateId,
8283
8635
  files_written: filesWritten
@@ -8366,11 +8718,20 @@ var buildHandler = (ctx, args) => protectedCommand(async (innerCtx) => {
8366
8718
  }
8367
8719
  chosenSlug = value;
8368
8720
  }
8369
- const agent = await getAgentByOwnerAndSlug(innerCtx, (await getActiveOrganization(innerCtx)).slug, chosenSlug);
8721
+ const activeOrg = await getActiveOrganization(innerCtx);
8722
+ const agent = await getAgentByOwnerAndSlug(innerCtx, activeOrg.slug, chosenSlug);
8370
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
+ });
8371
8728
  const projectConfig = await writeProjectConfig(innerCtx, agent, {
8372
8729
  root: targetDir,
8373
8730
  force: true
8731
+ }, {
8732
+ localDevInstallationId: localDev.id,
8733
+ organizationSlug: localDev.organizationSlug,
8734
+ agentSlug: localDev.agentSlug
8374
8735
  });
8375
8736
  await ensureGitignore(targetDir);
8376
8737
  const skillFiles = await writeSkillFiles(innerCtx, agent, projectConfig, {
@@ -8384,6 +8745,10 @@ var buildHandler = (ctx, args) => protectedCommand(async (innerCtx) => {
8384
8745
  name: agent.name
8385
8746
  },
8386
8747
  organization_id: agent.organizationId,
8748
+ local_dev_installation: {
8749
+ id: localDev.id,
8750
+ organization_slug: localDev.organizationSlug
8751
+ },
8387
8752
  target_dir: targetDir,
8388
8753
  files_written: [
8389
8754
  ".stackbone/project.json",
@@ -8612,11 +8977,6 @@ var buildRootCommand = async () => {
8612
8977
  "api-url": {
8613
8978
  type: "string",
8614
8979
  description: "Override the control plane URL."
8615
- },
8616
- "no-auto-login": {
8617
- type: "boolean",
8618
- description: "Refuse to start the device-code flow when no session exists.",
8619
- default: false
8620
8980
  }
8621
8981
  },
8622
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackbone/cli",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0-alpha.3",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "bin": {
Binary file
Binary file