@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 +69 -0
- package/main.js +521 -161
- package/migrations/0009_rename_runs_partial_index.sql +22 -0
- package/package.json +1 -1
- package/stackbone-cli-0.1.0-alpha.3.tgz +0 -0
- package/stackbone-cli-0.1.0-alpha.1.tgz +0 -0
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
|
|
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
|
-
|
|
1363
|
-
|
|
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({
|
|
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.
|
|
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
|
-
*
|
|
1760
|
-
*
|
|
1761
|
-
*
|
|
1762
|
-
*
|
|
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
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
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
|
|
2745
|
-
|
|
2746
|
-
|
|
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(
|
|
2750
|
-
lines.push("
|
|
2751
|
-
lines.push(
|
|
2752
|
-
|
|
2753
|
-
|
|
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,
|
|
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)
|
|
3494
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
5445
|
+
app.get("/api/schema", async (c) => {
|
|
5339
5446
|
try {
|
|
5340
|
-
const resp = await fetch(`${agentBaseUrl}
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
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
|
|
5348
|
-
|
|
5349
|
-
|
|
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
|
-
|
|
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)
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
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
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
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
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
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)
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
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:
|
|
7344
|
-
listen: flags.listen
|
|
7345
|
-
|
|
7346
|
-
|
|
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
|
-
|
|
7358
|
-
|
|
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
|
-
|
|
7365
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7417
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
Binary file
|
|
Binary file
|