@stackbone/cli 0.3.0 → 0.3.2

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/main.js CHANGED
@@ -501,6 +501,25 @@ var init_approvals = __esmMin((() => {
501
501
  //#endregion
502
502
  //#region ../../libs/shared/validators/src/lib/agents/contract.ts
503
503
  /**
504
+ * Whether a box speaking protocol `version` can be registered as a deployment.
505
+ * Pure and total: the caller passes the number the handshake reported.
506
+ */
507
+ function supportsDeploymentRegistration(version) {
508
+ return version >= 15;
509
+ }
510
+ /**
511
+ * The one sentence both doors print when a box is below the floor.
512
+ *
513
+ * Shared, not copied, because the wizard and `stackbone link` refuse by the same
514
+ * rule and an operator who tries the other door must not read a different story.
515
+ * For the same reason it names no door: there is no "register it from the
516
+ * terminal instead" any more, and the only way out it can honestly offer is the
517
+ * one that works everywhere — run a current image.
518
+ */
519
+ function deploymentRegistrationRefusal(version) {
520
+ return `This box speaks protocol version ${version}. Registering a deployment needs version 15 or later, because an older box refuses the signed call that proves its secret. Update the image the box runs to a current one and try again.`;
521
+ }
522
+ /**
504
523
  * Validates that the negotiated `contract.version` is at or above the
505
524
  * version the SDK still executes against. Pure, table-driven, never throws.
506
525
  *
@@ -604,7 +623,7 @@ var init_deployments = __esmMin((() => {
604
623
  agentSlug: agentSlugSchema,
605
624
  url: z.url(),
606
625
  hmacSecret: z.string().min(1),
607
- imageTag: z.string().min(1).max(200)
626
+ imageTag: z.string().min(1).max(200).optional()
608
627
  });
609
628
  z.object({ url: z.url() });
610
629
  deploymentResponseSchema = z.object({
@@ -3461,7 +3480,7 @@ function createContractStore(resolved = resolveConfig({})) {
3461
3480
  };
3462
3481
  }
3463
3482
  async function performHandshake(baseUrl, fetchImpl) {
3464
- const url = `${baseUrl}${CONTRACT_PATH}`;
3483
+ const url = `${baseUrl}${CONTRACT_PATH$1}`;
3465
3484
  let response;
3466
3485
  try {
3467
3486
  response = await fetchImpl(url, { method: "GET" });
@@ -3547,12 +3566,12 @@ function maybeLogDebug(debugLogged, key, contract) {
3547
3566
  debugLogged.add(key);
3548
3567
  console.warn(`[stackbone] contract handshake ok: baseUrl=${key} version=${contract.version} capabilities=${contract.capabilities.length}`);
3549
3568
  }
3550
- var CONTRACT_PATH;
3569
+ var CONTRACT_PATH$1;
3551
3570
  var init_contract_handshake = __esmMin((() => {
3552
3571
  init_src$13();
3553
3572
  init_config$1();
3554
3573
  init_result$1();
3555
- CONTRACT_PATH = "/api/contract";
3574
+ CONTRACT_PATH$1 = "/api/contract";
3556
3575
  }));
3557
3576
  //#endregion
3558
3577
  //#region ../../libs/sdk/src/contract/capability-registry.ts
@@ -10576,7 +10595,7 @@ var createBoxAuthGate = (options) => {
10576
10595
  //#endregion
10577
10596
  //#region ../../libs/runtime/workspace-runtime/src/lib/server/contract.ts
10578
10597
  init_src$13();
10579
- var STACKBONE_CLI_BUILD_VERSION = "0.3.0";
10598
+ var STACKBONE_CLI_BUILD_VERSION = "0.3.2";
10580
10599
  /**
10581
10600
  * Capabilities the local emulator advertises on `GET /api/contract`, derived
10582
10601
  * from the route registry above so the advertised set and the mounted routes
@@ -10597,7 +10616,7 @@ var EMULATOR_CAPABILITIES = Object.keys({
10597
10616
  "browser.provider": "/api/browser-provider"
10598
10617
  });
10599
10618
  var buildContractResponse = () => contractResponseSchema.parse({
10600
- version: 14,
10619
+ version: 15,
10601
10620
  minSupported: 1,
10602
10621
  capabilities: [...EMULATOR_CAPABILITIES],
10603
10622
  build: {
@@ -40738,8 +40757,7 @@ var redisConnection = (redisUrl) => {
40738
40757
  const db = path === "" ? void 0 : Number(path);
40739
40758
  if (db !== void 0 && !Number.isInteger(db)) throw new Error(`Invalid Redis database in URL path: '${url.pathname}' (expected an integer)`);
40740
40759
  return {
40741
- host: url.hostname,
40742
- port: url.port ? Number(url.port) : 6379,
40760
+ url: redisUrl,
40743
40761
  ...db === void 0 ? {} : { db },
40744
40762
  maxRetriesPerRequest: null
40745
40763
  };
@@ -41160,7 +41178,7 @@ var init_connections = __esmMin((() => {
41160
41178
  buildAgentEnv = (base, injection, options) => {
41161
41179
  const env = {
41162
41180
  ...base,
41163
- [STACKBONE_ENV_KEYS.contractVersion]: String(14),
41181
+ [STACKBONE_ENV_KEYS.contractVersion]: String(15),
41164
41182
  [STACKBONE_ENV_KEYS.agentId]: injection.agentId,
41165
41183
  [STACKBONE_ENV_KEYS.postgresUrl]: injection.services.postgresUrl,
41166
41184
  [STACKBONE_ENV_KEYS.s3Endpoint]: injection.services.s3Endpoint,
@@ -45043,7 +45061,8 @@ var buildEmulatorApp = (opts, hooks = {}) => {
45043
45061
  "x-stackbone-session",
45044
45062
  "x-stackbone-conversation",
45045
45063
  "x-api-key",
45046
- "anthropic-version"
45064
+ "anthropic-version",
45065
+ STACKBONE_SIGNATURE_HEADER
45047
45066
  ],
45048
45067
  exposeHeaders: ["X-Stackbone-Run-Id"]
45049
45068
  }));
@@ -49159,7 +49178,9 @@ STACKBONE_SECRET_KEY=${secretKey}
49159
49178
  # agent creates its own schema on first boot. It must have the pgvector extension
49160
49179
  # available — see the README.
49161
49180
  # DATABASE_URL=postgres://user:password@host:5432/dbname?sslmode=require
49162
- # WORKFLOW_REDIS_URL=redis://host:6379
49181
+ # WORKFLOW_REDIS_URL=redis://default:password@host:6379
49182
+ # Managed Redis always wants a password, and some require TLS — use rediss:// for
49183
+ # those. Percent-encode a password containing / + = or @, or the URL will not parse.
49163
49184
 
49164
49185
  # ---- Optional ----------------------------------------------------------------
49165
49186
  # Set if your database role is not allowed to create schemas; you then apply the
@@ -49244,6 +49265,14 @@ and apply these by hand first, in filename order:
49244
49265
 
49245
49266
  Both live inside the image; \`docker run --rm ${input.imageTag} ls <path>\` lists them.
49246
49267
 
49268
+ Redis needs no preparation, only a reachable URL. Managed instances hand out a
49269
+ password, so the URL carries one — \`redis://default:password@host:6379\`. Use
49270
+ \`rediss://\` where the provider requires TLS (Upstash, ElastiCache with encryption
49271
+ in transit), and percent-encode a password containing \`/\`, \`+\`, \`=\` or \`@\`. The
49272
+ agent PINGs Redis before it starts and stops with the reason if it cannot: a
49273
+ rejected password fails right away, and a Redis still starting up is retried for
49274
+ 30 seconds first.
49275
+
49247
49276
  ## Running it somewhere other than Compose
49248
49277
 
49249
49278
  The agent serves on port **${RUNTIME_PORT}** by default, and binds whatever
@@ -53652,6 +53681,175 @@ var assertSdkCompatible = async (ctx, opts) => {
53652
53681
  return result;
53653
53682
  };
53654
53683
  //#endregion
53684
+ //#region src/services/project.service.ts
53685
+ var GITIGNORE_MARKER_BEGIN$1 = "# >>> stackbone >>>";
53686
+ var GITIGNORE_BLOCK = [
53687
+ GITIGNORE_MARKER_BEGIN$1,
53688
+ ".stackbone/",
53689
+ "node_modules/",
53690
+ "dist/",
53691
+ "# <<< stackbone <<<",
53692
+ ""
53693
+ ].join("\n");
53694
+ var exists = async (path) => {
53695
+ try {
53696
+ await stat(path);
53697
+ return true;
53698
+ } catch (err) {
53699
+ if (isENOENT(err)) return false;
53700
+ throw err;
53701
+ }
53702
+ };
53703
+ var writeProjectConfig = async (ctx, agent, input, extras = {}) => {
53704
+ const config = {
53705
+ schemaVersion: 1,
53706
+ organizationId: agent.organizationId,
53707
+ agentId: agent.id,
53708
+ controlPlaneUrl: ctx.apiUrl,
53709
+ ...extras.localDevInstallationId !== void 0 ? { localDevInstallationId: extras.localDevInstallationId } : {},
53710
+ ...extras.organizationSlug !== void 0 ? { organizationSlug: extras.organizationSlug } : {},
53711
+ ...extras.agentSlug !== void 0 ? { agentSlug: extras.agentSlug } : {},
53712
+ ...extras.starter !== void 0 ? { starter: extras.starter } : {}
53713
+ };
53714
+ await mkdir(join(input.root, ".stackbone"), { recursive: true });
53715
+ await writeFile(join(input.root, ".stackbone", "project.json"), JSON.stringify(config, null, 2));
53716
+ return config;
53717
+ };
53718
+ var ensureGitignore = async (root) => {
53719
+ const path = join(root, ".gitignore");
53720
+ let current = "";
53721
+ try {
53722
+ current = await readFile(path, "utf8");
53723
+ } catch (err) {
53724
+ if (!isENOENT(err)) throw err;
53725
+ }
53726
+ if (current.includes(GITIGNORE_MARKER_BEGIN$1)) return;
53727
+ await writeFile(path, current.length > 0 && !current.endsWith("\n") ? `${current}\n${GITIGNORE_BLOCK}` : `${current}${GITIGNORE_BLOCK}`);
53728
+ };
53729
+ var detectExistingProjectFiles = async (root, paths) => {
53730
+ const checks = await Promise.all(paths.map((p) => exists(join(root, p))));
53731
+ return paths.filter((_, i) => checks[i]);
53732
+ };
53733
+ //#endregion
53734
+ //#region src/services/register-agent.ts
53735
+ var registerAgentEager = async (ctx, input) => {
53736
+ const slug = input.slug ?? input.name;
53737
+ let agent;
53738
+ let created = false;
53739
+ try {
53740
+ agent = await getAgentByOwnerAndSlug(ctx, input.organizationSlug, slug);
53741
+ } catch (err) {
53742
+ if (!(isStackboneCliError(err) && err.code === "not_found")) throw err;
53743
+ agent = await createAgent(ctx, {
53744
+ organizationSlug: input.organizationSlug,
53745
+ name: input.name,
53746
+ slug,
53747
+ ...input.description !== void 0 ? { description: input.description } : {}
53748
+ });
53749
+ created = true;
53750
+ }
53751
+ const localDev = await upsertLocalDevInstallation(ctx, {
53752
+ creatorOrganizationSlug: input.organizationSlug,
53753
+ agentTemplateSlug: agent.slug
53754
+ });
53755
+ return {
53756
+ agent,
53757
+ localDev,
53758
+ created
53759
+ };
53760
+ };
53761
+ //#endregion
53762
+ //#region src/commands/dev/link-on-demand.ts
53763
+ /** Sentinel for the "none of these" row in the agent picker. */
53764
+ var CREATE_NEW = "\0create-new";
53765
+ /**
53766
+ * A control-plane slug from a folder name: lowercase, non-alphanumerics folded
53767
+ * to single dashes, no leading or trailing dash. A folder called `Astex Email
53768
+ * Agent` should not fail the create call for a reason nobody typed.
53769
+ */
53770
+ var slugifyProjectName = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
53771
+ var promptForAgentInteractively = async (agents) => {
53772
+ if (agents.length === 0) return null;
53773
+ const value = await select({
53774
+ message: "This project is not linked to an agent yet. Which one is it?",
53775
+ options: [...agents.map((a) => ({
53776
+ value: a.slug,
53777
+ label: a.name,
53778
+ hint: a.slug
53779
+ })), {
53780
+ value: CREATE_NEW,
53781
+ label: "Create a new agent",
53782
+ hint: "registers one for this project"
53783
+ }]
53784
+ });
53785
+ if (isCancel(value)) throw new StackboneCliError({
53786
+ code: "no_project",
53787
+ message: "No agent selected, so this directory is still unlinked.",
53788
+ suggestion: "Run `stackbone dev` again and pick an agent, or create one in Studio."
53789
+ });
53790
+ return value === CREATE_NEW ? null : String(value);
53791
+ };
53792
+ var promptForNewNameInteractively = async (suggested) => {
53793
+ const value = await text$1({
53794
+ message: "Name for the new agent",
53795
+ initialValue: suggested,
53796
+ validate: (v) => slugifyProjectName(v ?? "").length === 0 ? "Pick a name with letters or numbers in it." : void 0
53797
+ });
53798
+ if (isCancel(value)) throw new StackboneCliError({
53799
+ code: "no_project",
53800
+ message: "No name given, so no agent was created.",
53801
+ suggestion: "Run `stackbone dev` again to pick or create one."
53802
+ });
53803
+ return String(value);
53804
+ };
53805
+ /**
53806
+ * The message a non-interactive run gets. Kept apart from the interactive path
53807
+ * because it is the one a CI log will carry, and the old text sent the reader to
53808
+ * two commands that both do the wrong thing here.
53809
+ */
53810
+ var unlinkedProjectError = () => new StackboneCliError({
53811
+ code: "no_project",
53812
+ message: "No project linked in this directory, and nothing here can ask which agent it is.",
53813
+ 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."
53814
+ });
53815
+ /**
53816
+ * Link this directory to an agent, asking which one, and return the config the
53817
+ * boot needs. Throws {@link unlinkedProjectError} when nobody can be asked.
53818
+ */
53819
+ var linkProjectOnDemand = async (ctx, projectRoot, deps = {}) => {
53820
+ if (!canPromptInteractively(ctx)) throw unlinkedProjectError();
53821
+ const list = deps.listAgents ?? listAgents;
53822
+ const activeOrg = deps.getActiveOrganization ?? getActiveOrganization;
53823
+ const register = deps.registerAgentEager ?? registerAgentEager;
53824
+ const askAgent = deps.promptForAgent ?? promptForAgentInteractively;
53825
+ const askName = deps.promptForNewName ?? promptForNewNameInteractively;
53826
+ const org = await activeOrg(ctx);
53827
+ const name = await askAgent((await list(ctx)).map((a) => ({
53828
+ slug: a.slug,
53829
+ name: a.name ?? a.slug
53830
+ }))) ?? slugifyProjectName(await askName(basename(projectRoot)));
53831
+ if (name.length === 0) throw new StackboneCliError({
53832
+ code: "generic",
53833
+ message: "That name has no letters or numbers in it, so it cannot become an agent slug.",
53834
+ suggestion: "Run `stackbone dev` again and give the agent a name."
53835
+ });
53836
+ const registration = await register(ctx, {
53837
+ organizationSlug: org.slug,
53838
+ name
53839
+ });
53840
+ const config = await writeProjectConfig(ctx, registration.agent, {
53841
+ root: projectRoot,
53842
+ force: false
53843
+ }, {
53844
+ localDevInstallationId: registration.localDev.id,
53845
+ organizationSlug: registration.localDev.organizationSlug,
53846
+ agentSlug: registration.localDev.agentSlug
53847
+ });
53848
+ await ensureGitignore(projectRoot);
53849
+ log.success(registration.created ? `Registered agent "${registration.agent.slug}" and linked this directory to it.` : `Linked this directory to agent "${registration.agent.slug}".`);
53850
+ return config;
53851
+ };
53852
+ //#endregion
53655
53853
  //#region src/commands/dev/print-contract.ts
53656
53854
  var defaultWriteStdout = (chunk) => {
53657
53855
  process.stdout.write(chunk);
@@ -53766,12 +53964,7 @@ var createReloadRenderer = () => {
53766
53964
  };
53767
53965
  };
53768
53966
  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
- });
53967
+ const config = await innerCtx.projectConfig.read() ?? await linkProjectOnDemand(innerCtx, resolve(innerCtx.cwd));
53775
53968
  if (!innerCtx.flags.json) intro(formatBrandHeader({
53776
53969
  version: STACKBONE_CLI_BUILD_VERSION,
53777
53970
  command: "dev",
@@ -53896,7 +54089,7 @@ var buildHandler$2 = (ctx, flags) => protectedCommand(async (innerCtx) => {
53896
54089
  tunnelUrl: session.publicUrl,
53897
54090
  localUrl: session.emulatorUrl,
53898
54091
  deeplink,
53899
- contractVersion: 14,
54092
+ contractVersion: 15,
53900
54093
  capabilityCount: EMULATOR_CAPABILITIES.length,
53901
54094
  agentName: agent.name,
53902
54095
  colors: process.stdout.isTTY === true
@@ -54785,6 +54978,18 @@ it. The emulator API listens on \`http://127.0.0.1:4242\` by default. The tunnel
54785
54978
  mandatory: it fills the \`local_tunnel_url\` field on the \`installation\` row and
54786
54979
  yields the Studio deeplink.
54787
54980
 
54981
+ **A project that was handed to you links itself here.** \`.stackbone/\` is
54982
+ gitignored, so a project cloned from someone else's repository arrives with no
54983
+ link. \`dev\` lists the agents your organization owns, lets you pick one or create
54984
+ a new one named after the folder, writes \`.stackbone/project.json\` and carries on
54985
+ booting. Neither of the alternatives fits this case: \`stackbone init\` registers a
54986
+ _new_ agent and scaffolds its own \`package.json\` over the project you just
54987
+ received, and \`stackbone link\` is for registering a box you already deployed, so
54988
+ it requires its URL, HMAC secret and image tag.
54989
+
54990
+ Non-interactive runs (\`--json\`, \`--yes\`, CI, no terminal) still fail rather than
54991
+ guessing, and say what to do instead.
54992
+
54788
54993
  The project is a **workspace**: every \`deep-agents/<name>/index.ts\`, plus
54789
54994
  every \`workflows/<name>.workflow.ts\`, discovered by convention (with an
54790
54995
  explicit \`stackbone.config.ts\` override for workflows). Boot:
@@ -56446,10 +56651,12 @@ them.
56446
56651
  the CLI must run from WSL2, Docker Desktop with WSL2 integration enabled
56447
56652
  (or Docker Engine inside the Ubuntu distro) covers it.
56448
56653
  - **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>\`.
56654
+ \`deep-agents/\` folder, a \`workflows/\` folder and supporting files). Add
56655
+ pieces over time with \`stackbone add agent <name>\`,
56656
+ \`stackbone add workflow <name>\` or \`stackbone add workflow-agent <name>\`.
56657
+ A project someone handed you needs none of that: it arrives unlinked, and
56658
+ \`dev\` asks which agent it is. See
56659
+ [A project that arrived without a link](#a-project-that-arrived-without-a-link).
56453
56660
  - **At least one agent or workflow:** \`stackbone dev\` discovers your workspace
56454
56661
  by convention, any \`deep-agents/<name>/index.ts\` (or a
56455
56662
  \`workflows/<name>.workflow.ts\`) is enough to activate the runtime. A
@@ -56461,6 +56668,27 @@ them.
56461
56668
  \`STACKBONE_FRPC_BIN\` to point at a system install when your environment
56462
56669
  forbids untrusted binaries.
56463
56670
 
56671
+ ## A project that arrived without a link
56672
+
56673
+ A project you cloned from someone else's repository has no link to an agent.
56674
+ \`.stackbone/\` is gitignored, so the link never travels with the code.
56675
+
56676
+ \`stackbone dev\` does not stop for that. It lists the agents your organization
56677
+ owns, lets you pick one or create a new one named after the folder, writes
56678
+ \`.stackbone/project.json\`, adds it to \`.gitignore\` and carries on booting.
56679
+
56680
+ Neither of the other commands fits this case:
56681
+
56682
+ - \`stackbone init\` registers a **new** agent and writes its own \`package.json\`,
56683
+ \`tsconfig.json\` and scaffold over the project you were just handed.
56684
+ - \`stackbone link\` registers a box you have already deployed, so it requires
56685
+ that box's URL, HMAC secret and image tag.
56686
+
56687
+ A run that cannot ask fails instead of guessing, and says what to do instead.
56688
+ That covers any run without a terminal, a \`CI\` environment variable, \`--yes\` or
56689
+ \`--json\`. See
56690
+ [This directory is not linked to an agent](/docs/cli/troubleshooting#this-directory-is-not-linked-to-an-agent).
56691
+
56464
56692
  ## Synopsis
56465
56693
 
56466
56694
  \`\`\`sh
@@ -57118,6 +57346,26 @@ Stages you do not use are skipped: no workflows means no \`workflows\` or
57118
57346
 
57119
57347
  ## \`stackbone dev\` failures
57120
57348
 
57349
+ ### This directory is not linked to an agent
57350
+
57351
+ **Symptom:** \`stackbone dev\` exits before any stage runs with \`no_project\` and
57352
+ "No project linked in this directory, and nothing here can ask which agent it
57353
+ is."
57354
+
57355
+ **Cause:** the project has no \`.stackbone/project.json\` and this run cannot
57356
+ prompt for one. \`.stackbone/\` is gitignored, so a project cloned from someone
57357
+ else's repository always arrives unlinked, and a run without a terminal (or one
57358
+ with a \`CI\` environment variable, \`--yes\` or \`--json\`) has nobody to ask.
57359
+
57360
+ **Fix:** run \`stackbone dev\` once from a terminal and pick the agent. It writes
57361
+ the link, and every later run reuses it, scripted or not. To link without a
57362
+ prompt, write \`.stackbone/project.json\` yourself; \`stackbone agents get <slug>\`
57363
+ prints the agent id it needs.
57364
+
57365
+ Reaching for \`stackbone init\` here is the common mistake: it registers a new
57366
+ agent and scaffolds over the project you were handed. Full contract:
57367
+ [A project that arrived without a link](/docs/cli/local-development#a-project-that-arrived-without-a-link).
57368
+
57121
57369
  ### Docker not running, or the datastore is unhealthy
57122
57370
 
57123
57371
  **Symptom:** the \`docker\` stage fails, or \`platform-migrations\` /
@@ -62648,84 +62896,6 @@ var createHitlCommand = defineGroup({
62648
62896
  }
62649
62897
  });
62650
62898
  //#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
62899
  //#region src/services/skills.service.ts
62730
62900
  var execFileAsync = promisify(execFile);
62731
62901
  /**
@@ -63221,6 +63391,84 @@ var verifyDeploymentSecret = async (args) => {
63221
63391
  return readIdentityVerification(response);
63222
63392
  };
63223
63393
  //#endregion
63394
+ //#region src/services/handshake.service.ts
63395
+ init_src$13();
63396
+ /** Public, credential-free handshake every deployed box serves. */
63397
+ var CONTRACT_PATH = "/api/contract";
63398
+ /**
63399
+ * Something is at that address, but it is not a Stackbone box: a gateway's error
63400
+ * page, a company site, a load balancer in front of the wrong service. Kept
63401
+ * distinct from "nothing answered" because the remedy is different — one is an
63402
+ * address to correct, the other a machine to go and look at.
63403
+ */
63404
+ var notAHandshake = (url, status, cause) => new StackboneCliError({
63405
+ code: "generic",
63406
+ message: `${url} answered with HTTP ${status}, but not with a Stackbone box handshake`,
63407
+ suggestion: "Check --url points at the box itself: a company site, or a load balancer in front of the wrong service, answers just like this.",
63408
+ cause
63409
+ });
63410
+ /**
63411
+ * Asks a box which protocol version it speaks, before it is registered.
63412
+ *
63413
+ * Uses a DIRECT `fetch` — `ctx.http` refuses absolute URLs on purpose (it only
63414
+ * ever talks to the control plane), and this is a cross-host call to the
63415
+ * customer's own box. The handshake is public and takes no credential, which is
63416
+ * what makes it usable here: at this point in `link` nothing has been registered,
63417
+ * so there is no identity to present anyway.
63418
+ */
63419
+ var readBoxContract = async (args) => {
63420
+ const fetchImpl = args.fetchImpl ?? fetch;
63421
+ let contractUrl;
63422
+ try {
63423
+ contractUrl = new URL(CONTRACT_PATH, args.url).toString();
63424
+ } catch (cause) {
63425
+ throw new StackboneCliError({
63426
+ code: "generic",
63427
+ message: `--url is not a valid URL: ${args.url}`,
63428
+ suggestion: "Pass the full public URL of the deployed agent, e.g. https://agent.example.com",
63429
+ cause
63430
+ });
63431
+ }
63432
+ let response;
63433
+ try {
63434
+ response = await fetchImpl(contractUrl, { method: "GET" });
63435
+ } catch (cause) {
63436
+ throw new StackboneCliError({
63437
+ code: "generic",
63438
+ message: `Nothing answered at ${args.url}, so there is no way to read which protocol version that box speaks`,
63439
+ suggestion: "Check --url is the public address of the running container and that this machine can reach it.",
63440
+ cause
63441
+ });
63442
+ }
63443
+ let body;
63444
+ try {
63445
+ body = await response.json();
63446
+ } catch (cause) {
63447
+ throw notAHandshake(args.url, response.status, cause);
63448
+ }
63449
+ const parsed = contractResponseSchema.safeParse(body);
63450
+ if (!response.ok || !parsed.success) throw notAHandshake(args.url, response.status, parsed.success ? body : parsed.error);
63451
+ return parsed.data;
63452
+ };
63453
+ /**
63454
+ * Refuses a box this product cannot register, whatever door it came through.
63455
+ *
63456
+ * Pure and total: it reads the version the handshake reported and nothing else.
63457
+ * The floor is `MIN_SUPPORTED_DEPLOYMENT_REGISTRATION_CONTRACT_VERSION`, NOT the
63458
+ * SDK-wide `MIN_SUPPORTED_CONTRACT_VERSION` — that one is 1, the handshake schema
63459
+ * already refuses anything below 1, and a guard built on it could never fire.
63460
+ * The sentence comes from the shared source the browser wizard prints too, so an
63461
+ * operator who tries the other door reads the same story about the same box.
63462
+ */
63463
+ var assertBoxCanBeRegistered = (contract) => {
63464
+ if (supportsDeploymentRegistration(contract.version)) return;
63465
+ throw new StackboneCliError({
63466
+ code: "generic",
63467
+ message: deploymentRegistrationRefusal(contract.version),
63468
+ suggestion: "Re-run `stackbone link` once the box is running a current image."
63469
+ });
63470
+ };
63471
+ //#endregion
63224
63472
  //#region src/commands/link/link.command.ts
63225
63473
  var requireLinkField = (value, flag, description) => {
63226
63474
  const trimmed = (value ?? "").trim();
@@ -63307,6 +63555,8 @@ var runLink = async (ctx, args) => {
63307
63555
  const activeOrg = await getActiveOrganization(ctx);
63308
63556
  const agent = await getAgentByOwnerAndSlug(ctx, activeOrg.slug, chosenSlug);
63309
63557
  if (!ctx.flags.json) log.step(`Linking ${targetDir} to ${agent.slug}`);
63558
+ if (!ctx.flags.json) log.step("Reading the box protocol version");
63559
+ assertBoxCanBeRegistered(await readBoxContract({ url }));
63310
63560
  if (!ctx.flags.json) log.step("Verifying the --secret against the deployed container");
63311
63561
  const identityVerification = await verifyDeploymentSecret({
63312
63562
  url,
@@ -65605,7 +65855,7 @@ if (shouldReexecUnderWinpty({
65605
65855
  if (isVersionFlag(rawArgs)) {
65606
65856
  process.stdout.write(`${formatVersionOutput({
65607
65857
  cliVersion: STACKBONE_CLI_BUILD_VERSION,
65608
- contractVersion: 14
65858
+ contractVersion: 15
65609
65859
  })}\n`);
65610
65860
  process.exit(0);
65611
65861
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackbone/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "bin": {