@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/CHANGELOG.md +85 -0
- package/main.js +351 -101
- package/package.json +1 -1
- package/runtime-deps/@stackbone/workflow-world/index.js +70 -70
- package/stackbone-cli-0.3.2.tgz +0 -0
- package/stackbone-cli-0.3.0.tgz +0 -0
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.
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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)
|
|
56450
|
-
|
|
56451
|
-
\`stackbone add
|
|
56452
|
-
|
|
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:
|
|
65858
|
+
contractVersion: 15
|
|
65609
65859
|
})}\n`);
|
|
65610
65860
|
process.exit(0);
|
|
65611
65861
|
}
|