@stackbone/cli 0.3.0 → 0.3.1

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,58 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.1] - 2026-08-06
11
+
12
+ ### Added
13
+
14
+ - **`stackbone dev` links a project that arrived without a link.** `.stackbone/`
15
+ is gitignored — by the very block `init` and `link` write — so a project
16
+ cloned from someone else's repository has none, and there was no clean way to
17
+ get one: `dev` refused and pointed at `stackbone init`, which registers a
18
+ _new_ agent and scaffolds its own `package.json` over the project you were
19
+ handed, or at `stackbone link`, which registers an already-deployed box and so
20
+ requires its URL, HMAC secret and image tag. Neither describes "I was given
21
+ this project and want to run it".
22
+
23
+ Now `dev` lists the agents your organization owns, lets you pick one or create
24
+ a new one named after the folder, writes the link, gitignores it, and carries
25
+ on booting. Non-interactive runs (`--json`, `--yes`, CI, no terminal) still
26
+ fail rather than guessing, with a message naming the options that apply.
27
+
28
+ ## [0.3.0] - 2026-08-06
29
+
30
+ ### Added
31
+
32
+ - **`stackbone package` turns a project into a folder someone else can run.**
33
+ It compiles the workspace and writes a two-line Dockerfile over the published
34
+ Stackbone runtime base, a `docker-compose.yml` that also brings up a Postgres
35
+ and a Redis, an `.env` carrying a freshly generated `STACKBONE_SECRET_KEY`,
36
+ and a README written for whoever receives it. On the far end it is two
37
+ commands: `docker compose up -d`, then a health check. Building the image is
38
+ one `COPY` over a base that already carries the server and its dependencies,
39
+ so it takes seconds and needs neither the Stackbone monorepo nor an install.
40
+
41
+ The command always compiles rather than packaging whatever is sitting in
42
+ `dist/`: the folder gets run by someone else, days later, and there is nobody
43
+ left to notice it was built from yesterday's code.
44
+
45
+ `--offline` also saves the image into the folder (around 150 MB) for a
46
+ network that cannot reach a registry. That variant emits no build
47
+ instructions, since the image arrives by loading the archive.
48
+
49
+ Two things the generated files say that nothing said before: **the database
50
+ needs the `pgvector` extension** — without it the first boot applies most of
51
+ the schema and then stops, which reads like a bug in the agent rather than a
52
+ missing extension — and the agent serves on **8080** unless `PORT` says
53
+ otherwise, with `/live` as the health check a managed host should use.
54
+
55
+ ### Changed
56
+
57
+ - **`stackbone build` no longer has to be followed by hand-written deployment
58
+ files.** It still emits just the workspace bundle, for anyone who already
59
+ owns their image and their deployment; `stackbone package` is now the route
60
+ to take when you do not.
61
+
10
62
  ## [0.2.10] - 2026-08-02
11
63
 
12
64
  ### Added
package/main.js CHANGED
@@ -10576,7 +10576,7 @@ var createBoxAuthGate = (options) => {
10576
10576
  //#endregion
10577
10577
  //#region ../../libs/runtime/workspace-runtime/src/lib/server/contract.ts
10578
10578
  init_src$13();
10579
- var STACKBONE_CLI_BUILD_VERSION = "0.3.0";
10579
+ var STACKBONE_CLI_BUILD_VERSION = "0.3.1";
10580
10580
  /**
10581
10581
  * Capabilities the local emulator advertises on `GET /api/contract`, derived
10582
10582
  * from the route registry above so the advertised set and the mounted routes
@@ -53652,6 +53652,175 @@ var assertSdkCompatible = async (ctx, opts) => {
53652
53652
  return result;
53653
53653
  };
53654
53654
  //#endregion
53655
+ //#region src/services/project.service.ts
53656
+ var GITIGNORE_MARKER_BEGIN$1 = "# >>> stackbone >>>";
53657
+ var GITIGNORE_BLOCK = [
53658
+ GITIGNORE_MARKER_BEGIN$1,
53659
+ ".stackbone/",
53660
+ "node_modules/",
53661
+ "dist/",
53662
+ "# <<< stackbone <<<",
53663
+ ""
53664
+ ].join("\n");
53665
+ var exists = async (path) => {
53666
+ try {
53667
+ await stat(path);
53668
+ return true;
53669
+ } catch (err) {
53670
+ if (isENOENT(err)) return false;
53671
+ throw err;
53672
+ }
53673
+ };
53674
+ var writeProjectConfig = async (ctx, agent, input, extras = {}) => {
53675
+ const config = {
53676
+ schemaVersion: 1,
53677
+ organizationId: agent.organizationId,
53678
+ agentId: agent.id,
53679
+ controlPlaneUrl: ctx.apiUrl,
53680
+ ...extras.localDevInstallationId !== void 0 ? { localDevInstallationId: extras.localDevInstallationId } : {},
53681
+ ...extras.organizationSlug !== void 0 ? { organizationSlug: extras.organizationSlug } : {},
53682
+ ...extras.agentSlug !== void 0 ? { agentSlug: extras.agentSlug } : {},
53683
+ ...extras.starter !== void 0 ? { starter: extras.starter } : {}
53684
+ };
53685
+ await mkdir(join(input.root, ".stackbone"), { recursive: true });
53686
+ await writeFile(join(input.root, ".stackbone", "project.json"), JSON.stringify(config, null, 2));
53687
+ return config;
53688
+ };
53689
+ var ensureGitignore = async (root) => {
53690
+ const path = join(root, ".gitignore");
53691
+ let current = "";
53692
+ try {
53693
+ current = await readFile(path, "utf8");
53694
+ } catch (err) {
53695
+ if (!isENOENT(err)) throw err;
53696
+ }
53697
+ if (current.includes(GITIGNORE_MARKER_BEGIN$1)) return;
53698
+ await writeFile(path, current.length > 0 && !current.endsWith("\n") ? `${current}\n${GITIGNORE_BLOCK}` : `${current}${GITIGNORE_BLOCK}`);
53699
+ };
53700
+ var detectExistingProjectFiles = async (root, paths) => {
53701
+ const checks = await Promise.all(paths.map((p) => exists(join(root, p))));
53702
+ return paths.filter((_, i) => checks[i]);
53703
+ };
53704
+ //#endregion
53705
+ //#region src/services/register-agent.ts
53706
+ var registerAgentEager = async (ctx, input) => {
53707
+ const slug = input.slug ?? input.name;
53708
+ let agent;
53709
+ let created = false;
53710
+ try {
53711
+ agent = await getAgentByOwnerAndSlug(ctx, input.organizationSlug, slug);
53712
+ } catch (err) {
53713
+ if (!(isStackboneCliError(err) && err.code === "not_found")) throw err;
53714
+ agent = await createAgent(ctx, {
53715
+ organizationSlug: input.organizationSlug,
53716
+ name: input.name,
53717
+ slug,
53718
+ ...input.description !== void 0 ? { description: input.description } : {}
53719
+ });
53720
+ created = true;
53721
+ }
53722
+ const localDev = await upsertLocalDevInstallation(ctx, {
53723
+ creatorOrganizationSlug: input.organizationSlug,
53724
+ agentTemplateSlug: agent.slug
53725
+ });
53726
+ return {
53727
+ agent,
53728
+ localDev,
53729
+ created
53730
+ };
53731
+ };
53732
+ //#endregion
53733
+ //#region src/commands/dev/link-on-demand.ts
53734
+ /** Sentinel for the "none of these" row in the agent picker. */
53735
+ var CREATE_NEW = "\0create-new";
53736
+ /**
53737
+ * A control-plane slug from a folder name: lowercase, non-alphanumerics folded
53738
+ * to single dashes, no leading or trailing dash. A folder called `Astex Email
53739
+ * Agent` should not fail the create call for a reason nobody typed.
53740
+ */
53741
+ var slugifyProjectName = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
53742
+ var promptForAgentInteractively = async (agents) => {
53743
+ if (agents.length === 0) return null;
53744
+ const value = await select({
53745
+ message: "This project is not linked to an agent yet. Which one is it?",
53746
+ options: [...agents.map((a) => ({
53747
+ value: a.slug,
53748
+ label: a.name,
53749
+ hint: a.slug
53750
+ })), {
53751
+ value: CREATE_NEW,
53752
+ label: "Create a new agent",
53753
+ hint: "registers one for this project"
53754
+ }]
53755
+ });
53756
+ if (isCancel(value)) throw new StackboneCliError({
53757
+ code: "no_project",
53758
+ message: "No agent selected, so this directory is still unlinked.",
53759
+ suggestion: "Run `stackbone dev` again and pick an agent, or create one in Studio."
53760
+ });
53761
+ return value === CREATE_NEW ? null : String(value);
53762
+ };
53763
+ var promptForNewNameInteractively = async (suggested) => {
53764
+ const value = await text$1({
53765
+ message: "Name for the new agent",
53766
+ initialValue: suggested,
53767
+ validate: (v) => slugifyProjectName(v ?? "").length === 0 ? "Pick a name with letters or numbers in it." : void 0
53768
+ });
53769
+ if (isCancel(value)) throw new StackboneCliError({
53770
+ code: "no_project",
53771
+ message: "No name given, so no agent was created.",
53772
+ suggestion: "Run `stackbone dev` again to pick or create one."
53773
+ });
53774
+ return String(value);
53775
+ };
53776
+ /**
53777
+ * The message a non-interactive run gets. Kept apart from the interactive path
53778
+ * because it is the one a CI log will carry, and the old text sent the reader to
53779
+ * two commands that both do the wrong thing here.
53780
+ */
53781
+ var unlinkedProjectError = () => new StackboneCliError({
53782
+ code: "no_project",
53783
+ message: "No project linked in this directory, and nothing here can ask which agent it is.",
53784
+ suggestion: "Run `stackbone dev` in a terminal to pick an agent, or write .stackbone/project.json yourself (`stackbone agents get <slug>` prints the id). `stackbone init` scaffolds a NEW project over this one — not what you want for a project you were handed."
53785
+ });
53786
+ /**
53787
+ * Link this directory to an agent, asking which one, and return the config the
53788
+ * boot needs. Throws {@link unlinkedProjectError} when nobody can be asked.
53789
+ */
53790
+ var linkProjectOnDemand = async (ctx, projectRoot, deps = {}) => {
53791
+ if (!canPromptInteractively(ctx)) throw unlinkedProjectError();
53792
+ const list = deps.listAgents ?? listAgents;
53793
+ const activeOrg = deps.getActiveOrganization ?? getActiveOrganization;
53794
+ const register = deps.registerAgentEager ?? registerAgentEager;
53795
+ const askAgent = deps.promptForAgent ?? promptForAgentInteractively;
53796
+ const askName = deps.promptForNewName ?? promptForNewNameInteractively;
53797
+ const org = await activeOrg(ctx);
53798
+ const name = await askAgent((await list(ctx)).map((a) => ({
53799
+ slug: a.slug,
53800
+ name: a.name ?? a.slug
53801
+ }))) ?? slugifyProjectName(await askName(basename(projectRoot)));
53802
+ if (name.length === 0) throw new StackboneCliError({
53803
+ code: "generic",
53804
+ message: "That name has no letters or numbers in it, so it cannot become an agent slug.",
53805
+ suggestion: "Run `stackbone dev` again and give the agent a name."
53806
+ });
53807
+ const registration = await register(ctx, {
53808
+ organizationSlug: org.slug,
53809
+ name
53810
+ });
53811
+ const config = await writeProjectConfig(ctx, registration.agent, {
53812
+ root: projectRoot,
53813
+ force: false
53814
+ }, {
53815
+ localDevInstallationId: registration.localDev.id,
53816
+ organizationSlug: registration.localDev.organizationSlug,
53817
+ agentSlug: registration.localDev.agentSlug
53818
+ });
53819
+ await ensureGitignore(projectRoot);
53820
+ log.success(registration.created ? `Registered agent "${registration.agent.slug}" and linked this directory to it.` : `Linked this directory to agent "${registration.agent.slug}".`);
53821
+ return config;
53822
+ };
53823
+ //#endregion
53655
53824
  //#region src/commands/dev/print-contract.ts
53656
53825
  var defaultWriteStdout = (chunk) => {
53657
53826
  process.stdout.write(chunk);
@@ -53766,12 +53935,7 @@ var createReloadRenderer = () => {
53766
53935
  };
53767
53936
  };
53768
53937
  var buildHandler$2 = (ctx, flags) => protectedCommand(async (innerCtx) => {
53769
- const config = await innerCtx.projectConfig.read();
53770
- if (!config) throw new StackboneCliError({
53771
- code: "no_project",
53772
- message: "No project linked in this directory",
53773
- suggestion: "Run `stackbone init` or `stackbone link` first"
53774
- });
53938
+ const config = await innerCtx.projectConfig.read() ?? await linkProjectOnDemand(innerCtx, resolve(innerCtx.cwd));
53775
53939
  if (!innerCtx.flags.json) intro(formatBrandHeader({
53776
53940
  version: STACKBONE_CLI_BUILD_VERSION,
53777
53941
  command: "dev",
@@ -54785,6 +54949,18 @@ it. The emulator API listens on \`http://127.0.0.1:4242\` by default. The tunnel
54785
54949
  mandatory: it fills the \`local_tunnel_url\` field on the \`installation\` row and
54786
54950
  yields the Studio deeplink.
54787
54951
 
54952
+ **A project that was handed to you links itself here.** \`.stackbone/\` is
54953
+ gitignored, so a project cloned from someone else's repository arrives with no
54954
+ link. \`dev\` lists the agents your organization owns, lets you pick one or create
54955
+ a new one named after the folder, writes \`.stackbone/project.json\` and carries on
54956
+ booting. Neither of the alternatives fits this case: \`stackbone init\` registers a
54957
+ _new_ agent and scaffolds its own \`package.json\` over the project you just
54958
+ received, and \`stackbone link\` is for registering a box you already deployed, so
54959
+ it requires its URL, HMAC secret and image tag.
54960
+
54961
+ Non-interactive runs (\`--json\`, \`--yes\`, CI, no terminal) still fail rather than
54962
+ guessing, and say what to do instead.
54963
+
54788
54964
  The project is a **workspace**: every \`deep-agents/<name>/index.ts\`, plus
54789
54965
  every \`workflows/<name>.workflow.ts\`, discovered by convention (with an
54790
54966
  explicit \`stackbone.config.ts\` override for workflows). Boot:
@@ -56446,10 +56622,12 @@ them.
56446
56622
  the CLI must run from WSL2, Docker Desktop with WSL2 integration enabled
56447
56623
  (or Docker Engine inside the Ubuntu distro) covers it.
56448
56624
  - **A project:** run \`stackbone init\` to scaffold a workspace shell (a
56449
- \`deep-agents/\` folder, a \`workflows/\` folder and supporting files), or
56450
- \`stackbone link\` to connect an existing directory. Add pieces over time with
56451
- \`stackbone add agent <name>\`, \`stackbone add workflow <name>\` or
56452
- \`stackbone add workflow-agent <name>\`.
56625
+ \`deep-agents/\` folder, a \`workflows/\` folder and supporting files). Add
56626
+ pieces over time with \`stackbone add agent <name>\`,
56627
+ \`stackbone add workflow <name>\` or \`stackbone add workflow-agent <name>\`.
56628
+ A project someone handed you needs none of that: it arrives unlinked, and
56629
+ \`dev\` asks which agent it is. See
56630
+ [A project that arrived without a link](#a-project-that-arrived-without-a-link).
56453
56631
  - **At least one agent or workflow:** \`stackbone dev\` discovers your workspace
56454
56632
  by convention, any \`deep-agents/<name>/index.ts\` (or a
56455
56633
  \`workflows/<name>.workflow.ts\`) is enough to activate the runtime. A
@@ -56461,6 +56639,27 @@ them.
56461
56639
  \`STACKBONE_FRPC_BIN\` to point at a system install when your environment
56462
56640
  forbids untrusted binaries.
56463
56641
 
56642
+ ## A project that arrived without a link
56643
+
56644
+ A project you cloned from someone else's repository has no link to an agent.
56645
+ \`.stackbone/\` is gitignored, so the link never travels with the code.
56646
+
56647
+ \`stackbone dev\` does not stop for that. It lists the agents your organization
56648
+ owns, lets you pick one or create a new one named after the folder, writes
56649
+ \`.stackbone/project.json\`, adds it to \`.gitignore\` and carries on booting.
56650
+
56651
+ Neither of the other commands fits this case:
56652
+
56653
+ - \`stackbone init\` registers a **new** agent and writes its own \`package.json\`,
56654
+ \`tsconfig.json\` and scaffold over the project you were just handed.
56655
+ - \`stackbone link\` registers a box you have already deployed, so it requires
56656
+ that box's URL, HMAC secret and image tag.
56657
+
56658
+ A run that cannot ask fails instead of guessing, and says what to do instead.
56659
+ That covers any run without a terminal, a \`CI\` environment variable, \`--yes\` or
56660
+ \`--json\`. See
56661
+ [This directory is not linked to an agent](/docs/cli/troubleshooting#this-directory-is-not-linked-to-an-agent).
56662
+
56464
56663
  ## Synopsis
56465
56664
 
56466
56665
  \`\`\`sh
@@ -57118,6 +57317,26 @@ Stages you do not use are skipped: no workflows means no \`workflows\` or
57118
57317
 
57119
57318
  ## \`stackbone dev\` failures
57120
57319
 
57320
+ ### This directory is not linked to an agent
57321
+
57322
+ **Symptom:** \`stackbone dev\` exits before any stage runs with \`no_project\` and
57323
+ "No project linked in this directory, and nothing here can ask which agent it
57324
+ is."
57325
+
57326
+ **Cause:** the project has no \`.stackbone/project.json\` and this run cannot
57327
+ prompt for one. \`.stackbone/\` is gitignored, so a project cloned from someone
57328
+ else's repository always arrives unlinked, and a run without a terminal (or one
57329
+ with a \`CI\` environment variable, \`--yes\` or \`--json\`) has nobody to ask.
57330
+
57331
+ **Fix:** run \`stackbone dev\` once from a terminal and pick the agent. It writes
57332
+ the link, and every later run reuses it, scripted or not. To link without a
57333
+ prompt, write \`.stackbone/project.json\` yourself; \`stackbone agents get <slug>\`
57334
+ prints the agent id it needs.
57335
+
57336
+ Reaching for \`stackbone init\` here is the common mistake: it registers a new
57337
+ agent and scaffolds over the project you were handed. Full contract:
57338
+ [A project that arrived without a link](/docs/cli/local-development#a-project-that-arrived-without-a-link).
57339
+
57121
57340
  ### Docker not running, or the datastore is unhealthy
57122
57341
 
57123
57342
  **Symptom:** the \`docker\` stage fails, or \`platform-migrations\` /
@@ -62648,84 +62867,6 @@ var createHitlCommand = defineGroup({
62648
62867
  }
62649
62868
  });
62650
62869
  //#endregion
62651
- //#region src/services/register-agent.ts
62652
- var registerAgentEager = async (ctx, input) => {
62653
- const slug = input.slug ?? input.name;
62654
- let agent;
62655
- let created = false;
62656
- try {
62657
- agent = await getAgentByOwnerAndSlug(ctx, input.organizationSlug, slug);
62658
- } catch (err) {
62659
- if (!(isStackboneCliError(err) && err.code === "not_found")) throw err;
62660
- agent = await createAgent(ctx, {
62661
- organizationSlug: input.organizationSlug,
62662
- name: input.name,
62663
- slug,
62664
- ...input.description !== void 0 ? { description: input.description } : {}
62665
- });
62666
- created = true;
62667
- }
62668
- const localDev = await upsertLocalDevInstallation(ctx, {
62669
- creatorOrganizationSlug: input.organizationSlug,
62670
- agentTemplateSlug: agent.slug
62671
- });
62672
- return {
62673
- agent,
62674
- localDev,
62675
- created
62676
- };
62677
- };
62678
- //#endregion
62679
- //#region src/services/project.service.ts
62680
- var GITIGNORE_MARKER_BEGIN$1 = "# >>> stackbone >>>";
62681
- var GITIGNORE_BLOCK = [
62682
- GITIGNORE_MARKER_BEGIN$1,
62683
- ".stackbone/",
62684
- "node_modules/",
62685
- "dist/",
62686
- "# <<< stackbone <<<",
62687
- ""
62688
- ].join("\n");
62689
- var exists = async (path) => {
62690
- try {
62691
- await stat(path);
62692
- return true;
62693
- } catch (err) {
62694
- if (isENOENT(err)) return false;
62695
- throw err;
62696
- }
62697
- };
62698
- var writeProjectConfig = async (ctx, agent, input, extras = {}) => {
62699
- const config = {
62700
- schemaVersion: 1,
62701
- organizationId: agent.organizationId,
62702
- agentId: agent.id,
62703
- controlPlaneUrl: ctx.apiUrl,
62704
- ...extras.localDevInstallationId !== void 0 ? { localDevInstallationId: extras.localDevInstallationId } : {},
62705
- ...extras.organizationSlug !== void 0 ? { organizationSlug: extras.organizationSlug } : {},
62706
- ...extras.agentSlug !== void 0 ? { agentSlug: extras.agentSlug } : {},
62707
- ...extras.starter !== void 0 ? { starter: extras.starter } : {}
62708
- };
62709
- await mkdir(join(input.root, ".stackbone"), { recursive: true });
62710
- await writeFile(join(input.root, ".stackbone", "project.json"), JSON.stringify(config, null, 2));
62711
- return config;
62712
- };
62713
- var ensureGitignore = async (root) => {
62714
- const path = join(root, ".gitignore");
62715
- let current = "";
62716
- try {
62717
- current = await readFile(path, "utf8");
62718
- } catch (err) {
62719
- if (!isENOENT(err)) throw err;
62720
- }
62721
- if (current.includes(GITIGNORE_MARKER_BEGIN$1)) return;
62722
- await writeFile(path, current.length > 0 && !current.endsWith("\n") ? `${current}\n${GITIGNORE_BLOCK}` : `${current}${GITIGNORE_BLOCK}`);
62723
- };
62724
- var detectExistingProjectFiles = async (root, paths) => {
62725
- const checks = await Promise.all(paths.map((p) => exists(join(root, p))));
62726
- return paths.filter((_, i) => checks[i]);
62727
- };
62728
- //#endregion
62729
62870
  //#region src/services/skills.service.ts
62730
62871
  var execFileAsync = promisify(execFile);
62731
62872
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackbone/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "bin": {
Binary file
Binary file