@tonbo/cli 0.0.7 → 0.1.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/README.md +83 -31
- package/dist/bin/tonbo.js +949 -301
- package/package.json +8 -8
package/dist/bin/tonbo.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Command } from "commander";
|
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
6
|
|
|
7
7
|
// src/api.ts
|
|
8
|
-
import {
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
9
|
|
|
10
10
|
// src/http.ts
|
|
11
11
|
var HttpError = class extends Error {
|
|
@@ -33,41 +33,51 @@ async function requestJson(fetcher, url, init = {}) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// src/api.ts
|
|
36
|
-
function stableJson(value) {
|
|
37
|
-
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
38
|
-
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
39
|
-
const object = value;
|
|
40
|
-
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableJson(object[key])}`).join(",")}}`;
|
|
41
|
-
}
|
|
42
|
-
function digest(value) {
|
|
43
|
-
return createHash("sha256").update(stableJson(value)).digest("hex");
|
|
44
|
-
}
|
|
45
36
|
var TonboApi = class {
|
|
46
37
|
constructor(fetcher, accountOrigin = "https://tonbo.dev", managementOrigin = "https://api.tonbo.dev") {
|
|
47
38
|
this.fetcher = fetcher;
|
|
48
39
|
this.accountOrigin = accountOrigin;
|
|
49
40
|
this.managementOrigin = managementOrigin;
|
|
50
41
|
}
|
|
42
|
+
machineRequest(oauthToken, path6 = "", method = "GET", body) {
|
|
43
|
+
return requestJson(this.fetcher, `${this.accountOrigin}/api/cli/machines${path6}`, {
|
|
44
|
+
method,
|
|
45
|
+
headers: {
|
|
46
|
+
authorization: `Bearer ${oauthToken}`,
|
|
47
|
+
...body === void 0 ? {} : { "content-type": "application/json" }
|
|
48
|
+
},
|
|
49
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
50
|
+
});
|
|
51
|
+
}
|
|
51
52
|
listProjects(oauthToken) {
|
|
52
53
|
return requestJson(
|
|
53
54
|
this.fetcher,
|
|
54
|
-
`${this.accountOrigin}/api/cli/
|
|
55
|
+
`${this.accountOrigin}/api/cli/agents`,
|
|
55
56
|
{
|
|
56
57
|
headers: { authorization: `Bearer ${oauthToken}` }
|
|
57
58
|
}
|
|
58
59
|
).then((body) => body.projects);
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
+
listOrganizations(oauthToken) {
|
|
61
62
|
return requestJson(
|
|
62
63
|
this.fetcher,
|
|
63
|
-
`${this.accountOrigin}/api/cli/
|
|
64
|
+
`${this.accountOrigin}/api/cli/accounts`,
|
|
65
|
+
{
|
|
66
|
+
headers: { authorization: `Bearer ${oauthToken}` }
|
|
67
|
+
}
|
|
68
|
+
).then((body) => body.organizations);
|
|
69
|
+
}
|
|
70
|
+
createProject(oauthToken, name, organizationId) {
|
|
71
|
+
return requestJson(
|
|
72
|
+
this.fetcher,
|
|
73
|
+
`${this.accountOrigin}/api/cli/agents`,
|
|
64
74
|
{
|
|
65
75
|
method: "POST",
|
|
66
76
|
headers: {
|
|
67
77
|
authorization: `Bearer ${oauthToken}`,
|
|
68
78
|
"content-type": "application/json"
|
|
69
79
|
},
|
|
70
|
-
body: JSON.stringify(
|
|
80
|
+
body: JSON.stringify(organizationId ? { name, orgId: organizationId } : { name })
|
|
71
81
|
}
|
|
72
82
|
).then((body) => body.project);
|
|
73
83
|
}
|
|
@@ -106,7 +116,7 @@ var TonboApi = class {
|
|
|
106
116
|
exchangeManagementToken(oauthToken, projectId) {
|
|
107
117
|
return requestJson(
|
|
108
118
|
this.fetcher,
|
|
109
|
-
`${this.accountOrigin}/api/cli/
|
|
119
|
+
`${this.accountOrigin}/api/cli/agents/${projectId}/token`,
|
|
110
120
|
{
|
|
111
121
|
method: "POST",
|
|
112
122
|
headers: { authorization: `Bearer ${oauthToken}` }
|
|
@@ -115,7 +125,9 @@ var TonboApi = class {
|
|
|
115
125
|
}
|
|
116
126
|
async deploy({
|
|
117
127
|
bundle,
|
|
128
|
+
origin,
|
|
118
129
|
projectId,
|
|
130
|
+
promote,
|
|
119
131
|
spec,
|
|
120
132
|
token
|
|
121
133
|
}) {
|
|
@@ -124,8 +136,7 @@ var TonboApi = class {
|
|
|
124
136
|
sha256: bundle.sha256,
|
|
125
137
|
size_bytes: bundle.size_bytes
|
|
126
138
|
};
|
|
127
|
-
const
|
|
128
|
-
const bundlesPath = `${projectPath}/source-bundles`;
|
|
139
|
+
const bundlesPath = `/v1/agents/${projectId}/source-bundles`;
|
|
129
140
|
const prepared = await this.management("PUT", `${bundlesPath}/${bundle.sha256}`, token, {
|
|
130
141
|
format: descriptor.format,
|
|
131
142
|
size_bytes: descriptor.size_bytes
|
|
@@ -155,28 +166,69 @@ var TonboApi = class {
|
|
|
155
166
|
throw error;
|
|
156
167
|
}
|
|
157
168
|
}
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
169
|
+
let deployment = await this.createDeployment(projectId, { spec, origin }, token);
|
|
170
|
+
const previous = await this.getProduction(projectId, token);
|
|
171
|
+
const previousDeployment = previous ? await this.getDeployment(projectId, previous.deployment_id, token) : null;
|
|
172
|
+
const unchanged = previousDeployment?.spec_sha256 === deployment.spec_sha256;
|
|
173
|
+
let production = previous;
|
|
174
|
+
if (promote) {
|
|
175
|
+
production = await this.putProduction(
|
|
176
|
+
projectId,
|
|
177
|
+
{
|
|
178
|
+
deployment_id: deployment.id,
|
|
179
|
+
desired_state: "running",
|
|
180
|
+
expected_generation: previous ? previous.generation : null
|
|
181
|
+
},
|
|
182
|
+
token
|
|
183
|
+
);
|
|
184
|
+
deployment = await this.getDeployment(projectId, deployment.id, token);
|
|
164
185
|
}
|
|
165
|
-
|
|
166
|
-
|
|
186
|
+
return { deployment, production, unchanged };
|
|
187
|
+
}
|
|
188
|
+
createDeployment(projectId, body, token) {
|
|
189
|
+
return this.management(
|
|
190
|
+
"POST",
|
|
191
|
+
`/v1/agents/${projectId}/deployments`,
|
|
192
|
+
token,
|
|
193
|
+
body
|
|
194
|
+
).then((response) => response.data);
|
|
195
|
+
}
|
|
196
|
+
listDeployments(projectId, token) {
|
|
197
|
+
return this.managementList(`/v1/agents/${projectId}/deployments`, token);
|
|
198
|
+
}
|
|
199
|
+
getDeployment(projectId, deploymentId, token) {
|
|
200
|
+
return this.management(
|
|
167
201
|
"GET",
|
|
168
|
-
|
|
202
|
+
`/v1/agents/${projectId}/deployments/${deploymentId}`,
|
|
169
203
|
token
|
|
170
|
-
).
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
204
|
+
).then((response) => response.data);
|
|
205
|
+
}
|
|
206
|
+
getProduction(projectId, token) {
|
|
207
|
+
return this.management(
|
|
208
|
+
"GET",
|
|
209
|
+
`/v1/agents/${projectId}/production`,
|
|
210
|
+
token
|
|
211
|
+
).then(
|
|
212
|
+
(response) => response.data,
|
|
213
|
+
(error) => {
|
|
214
|
+
if (error.status === 404) return null;
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
putProduction(projectId, body, token) {
|
|
220
|
+
return this.management(
|
|
221
|
+
"PUT",
|
|
222
|
+
`/v1/agents/${projectId}/production`,
|
|
223
|
+
token,
|
|
224
|
+
body
|
|
225
|
+
).then((response) => response.data);
|
|
226
|
+
}
|
|
227
|
+
listRollouts(projectId, token) {
|
|
228
|
+
return this.managementList(
|
|
229
|
+
`/v1/agents/${projectId}/production/rollouts`,
|
|
230
|
+
token
|
|
231
|
+
);
|
|
180
232
|
}
|
|
181
233
|
async run({
|
|
182
234
|
projectId,
|
|
@@ -185,15 +237,19 @@ var TonboApi = class {
|
|
|
185
237
|
token,
|
|
186
238
|
turnId = randomUUID()
|
|
187
239
|
}) {
|
|
188
|
-
const projectPath = `/v1/
|
|
189
|
-
const
|
|
190
|
-
if (
|
|
191
|
-
throw new Error(
|
|
240
|
+
const projectPath = `/v1/agents/${projectId}`;
|
|
241
|
+
const production = await this.getProduction(projectId, token);
|
|
242
|
+
if (!production)
|
|
243
|
+
throw new Error("Agent has no Production Deployment; run `tonbo deploy` first.");
|
|
244
|
+
if (production.observed_state !== "running")
|
|
245
|
+
throw new Error(
|
|
246
|
+
`Production is ${production.observed_state}${production.last_error ? ` (${production.last_error})` : ""}; wait for it to be running.`
|
|
247
|
+
);
|
|
192
248
|
const session = sessionId ? { id: sessionId } : (await this.management(
|
|
193
249
|
"POST",
|
|
194
250
|
`${projectPath}/sessions`,
|
|
195
251
|
token,
|
|
196
|
-
{
|
|
252
|
+
{ deployment_id: production.deployment_id }
|
|
197
253
|
)).data;
|
|
198
254
|
const turn = (await this.management(
|
|
199
255
|
"POST",
|
|
@@ -202,7 +258,7 @@ var TonboApi = class {
|
|
|
202
258
|
{ prompt },
|
|
203
259
|
turnId
|
|
204
260
|
)).data;
|
|
205
|
-
return {
|
|
261
|
+
return { production, session, turn };
|
|
206
262
|
}
|
|
207
263
|
turnEvents({
|
|
208
264
|
after = 0,
|
|
@@ -213,21 +269,21 @@ var TonboApi = class {
|
|
|
213
269
|
}) {
|
|
214
270
|
return this.management(
|
|
215
271
|
"GET",
|
|
216
|
-
`/v1/
|
|
272
|
+
`/v1/agents/${projectId}/sessions/${sessionId}/turns/${turnId}/events?after=${after}`,
|
|
217
273
|
token
|
|
218
274
|
);
|
|
219
275
|
}
|
|
220
276
|
listProjectSecrets(projectId, token) {
|
|
221
277
|
return this.management(
|
|
222
278
|
"GET",
|
|
223
|
-
`/v1/
|
|
279
|
+
`/v1/agents/${projectId}/secrets`,
|
|
224
280
|
token
|
|
225
281
|
).then((body) => body.data);
|
|
226
282
|
}
|
|
227
283
|
setProjectSecret(projectId, name, value, token) {
|
|
228
284
|
return this.management(
|
|
229
285
|
"PUT",
|
|
230
|
-
`/v1/
|
|
286
|
+
`/v1/agents/${projectId}/secrets/${encodeURIComponent(name)}`,
|
|
231
287
|
token,
|
|
232
288
|
{ value }
|
|
233
289
|
);
|
|
@@ -235,12 +291,12 @@ var TonboApi = class {
|
|
|
235
291
|
deleteProjectSecret(projectId, name, token) {
|
|
236
292
|
return this.management(
|
|
237
293
|
"DELETE",
|
|
238
|
-
`/v1/
|
|
294
|
+
`/v1/agents/${projectId}/secrets/${encodeURIComponent(name)}`,
|
|
239
295
|
token
|
|
240
296
|
);
|
|
241
297
|
}
|
|
242
|
-
management(method,
|
|
243
|
-
return requestJson(this.fetcher, `${this.managementOrigin}${
|
|
298
|
+
management(method, path6, token, body, idempotencyKey) {
|
|
299
|
+
return requestJson(this.fetcher, `${this.managementOrigin}${path6}`, {
|
|
244
300
|
method,
|
|
245
301
|
headers: {
|
|
246
302
|
authorization: `Bearer ${token}`,
|
|
@@ -252,14 +308,14 @@ var TonboApi = class {
|
|
|
252
308
|
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
253
309
|
});
|
|
254
310
|
}
|
|
255
|
-
async managementList(
|
|
311
|
+
async managementList(path6, token) {
|
|
256
312
|
const values = [];
|
|
257
313
|
let cursor = null;
|
|
258
314
|
do {
|
|
259
|
-
const separator =
|
|
315
|
+
const separator = path6.includes("?") ? "&" : "?";
|
|
260
316
|
const page = await this.management(
|
|
261
317
|
"GET",
|
|
262
|
-
`${
|
|
318
|
+
`${path6}${separator}limit=100${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`,
|
|
263
319
|
token
|
|
264
320
|
);
|
|
265
321
|
values.push(...page.data);
|
|
@@ -289,10 +345,10 @@ var supportedExecutionTargets = [
|
|
|
289
345
|
{ driver: "command", harness: "pi" }
|
|
290
346
|
];
|
|
291
347
|
var AgentSourceInspectionError = class extends Error {
|
|
292
|
-
constructor(code,
|
|
348
|
+
constructor(code, path6, message, options) {
|
|
293
349
|
super(message, options);
|
|
294
350
|
this.code = code;
|
|
295
|
-
this.path =
|
|
351
|
+
this.path = path6;
|
|
296
352
|
this.name = "AgentSourceInspectionError";
|
|
297
353
|
}
|
|
298
354
|
};
|
|
@@ -395,7 +451,7 @@ function inspectLocalAgentSource(root) {
|
|
|
395
451
|
}
|
|
396
452
|
|
|
397
453
|
// src/auth.ts
|
|
398
|
-
import { createHash
|
|
454
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
399
455
|
import { createServer } from "node:http";
|
|
400
456
|
import { execFile } from "node:child_process";
|
|
401
457
|
import { promisify } from "node:util";
|
|
@@ -585,7 +641,7 @@ var AuthClient = class {
|
|
|
585
641
|
if (config.redirect_uri !== LOOPBACK_REDIRECT)
|
|
586
642
|
throw new Error(`Unsupported OAuth redirect URI: ${config.redirect_uri}`);
|
|
587
643
|
const verifier = base64url(randomBytes(48));
|
|
588
|
-
const challenge =
|
|
644
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
589
645
|
const state = base64url(randomBytes(24));
|
|
590
646
|
const authorization = new URL(config.authorization_endpoint);
|
|
591
647
|
authorization.search = new URLSearchParams({
|
|
@@ -751,12 +807,6 @@ async function runBuildCommand(root, command) {
|
|
|
751
807
|
});
|
|
752
808
|
}
|
|
753
809
|
|
|
754
|
-
// src/declaration.ts
|
|
755
|
-
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
756
|
-
import { lstat as lstat2, open, readFile as readFile2, rename, rm } from "node:fs/promises";
|
|
757
|
-
import path2 from "node:path";
|
|
758
|
-
import { parse, stringify } from "smol-toml";
|
|
759
|
-
|
|
760
810
|
// src/contracts.ts
|
|
761
811
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
762
812
|
|
|
@@ -883,20 +933,17 @@ var kubernetesProfilesSchema = {
|
|
|
883
933
|
};
|
|
884
934
|
var declarationSchema = {
|
|
885
935
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
886
|
-
"$id": "https://contracts.tonbo.dev/agents/tonbo-declaration-
|
|
887
|
-
"title": "Tonbo
|
|
936
|
+
"$id": "https://contracts.tonbo.dev/agents/tonbo-declaration-v2.schema.json",
|
|
937
|
+
"title": "Tonbo Agent declaration v2",
|
|
888
938
|
"type": "object",
|
|
889
939
|
"additionalProperties": false,
|
|
890
940
|
"required": [
|
|
891
941
|
"version",
|
|
892
|
-
"
|
|
942
|
+
"harness"
|
|
893
943
|
],
|
|
894
944
|
"properties": {
|
|
895
945
|
"version": {
|
|
896
|
-
"const":
|
|
897
|
-
},
|
|
898
|
-
"agent": {
|
|
899
|
-
"$ref": "https://contracts.tonbo.dev/agents/pi-agent-v1.schema.json"
|
|
946
|
+
"const": 2
|
|
900
947
|
},
|
|
901
948
|
"inference": {
|
|
902
949
|
"type": "object",
|
|
@@ -936,13 +983,19 @@ var declarationSchema = {
|
|
|
936
983
|
},
|
|
937
984
|
"service": {
|
|
938
985
|
"$ref": "https://contracts.tonbo.dev/agents/project-service-v1.schema.json"
|
|
986
|
+
},
|
|
987
|
+
"agent": {
|
|
988
|
+
"$ref": "https://contracts.tonbo.dev/agents/project-name-v1.json#/$defs/publicHostname"
|
|
989
|
+
},
|
|
990
|
+
"harness": {
|
|
991
|
+
"$ref": "https://contracts.tonbo.dev/agents/pi-agent-v1.schema.json"
|
|
939
992
|
}
|
|
940
993
|
}
|
|
941
994
|
};
|
|
942
|
-
var
|
|
995
|
+
var deploymentSchema = {
|
|
943
996
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
944
|
-
"$id": "https://contracts.tonbo.dev/agents/managed-
|
|
945
|
-
"title": "Managed Project
|
|
997
|
+
"$id": "https://contracts.tonbo.dev/agents/managed-deployment-v1.schema.json",
|
|
998
|
+
"title": "Managed Project Deployment v1",
|
|
946
999
|
"type": "object",
|
|
947
1000
|
"additionalProperties": false,
|
|
948
1001
|
"required": [
|
|
@@ -1020,15 +1073,66 @@ var piSessionContract = {
|
|
|
1020
1073
|
"session-preflight"
|
|
1021
1074
|
]
|
|
1022
1075
|
};
|
|
1076
|
+
var projectNameContract = {
|
|
1077
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
1078
|
+
"$id": "https://contracts.tonbo.dev/agents/project-name-v1.json",
|
|
1079
|
+
"x-tonbo-version": 1,
|
|
1080
|
+
"x-tonbo-public-hostname-apex": "tonbo.sh",
|
|
1081
|
+
"x-tonbo-public-hostname-template": "<project>-<organization>.tonbo.sh",
|
|
1082
|
+
"$defs": {
|
|
1083
|
+
"name": {
|
|
1084
|
+
"type": "string",
|
|
1085
|
+
"minLength": 3,
|
|
1086
|
+
"maxLength": 40,
|
|
1087
|
+
"pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
|
|
1088
|
+
"not": {
|
|
1089
|
+
"enum": [
|
|
1090
|
+
"api",
|
|
1091
|
+
"artifacts",
|
|
1092
|
+
"auth",
|
|
1093
|
+
"inference",
|
|
1094
|
+
"network-health",
|
|
1095
|
+
"sandbox-control",
|
|
1096
|
+
"status",
|
|
1097
|
+
"streams",
|
|
1098
|
+
"www"
|
|
1099
|
+
]
|
|
1100
|
+
}
|
|
1101
|
+
},
|
|
1102
|
+
"publicHostname": {
|
|
1103
|
+
"type": "string",
|
|
1104
|
+
"minLength": 16,
|
|
1105
|
+
"maxLength": 72,
|
|
1106
|
+
"pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)+\\.tonbo\\.sh$",
|
|
1107
|
+
"not": {
|
|
1108
|
+
"enum": [
|
|
1109
|
+
"api.tonbo.sh",
|
|
1110
|
+
"artifacts.tonbo.sh",
|
|
1111
|
+
"auth.tonbo.sh",
|
|
1112
|
+
"inference.tonbo.sh",
|
|
1113
|
+
"network-health.tonbo.sh",
|
|
1114
|
+
"sandbox-control.tonbo.sh",
|
|
1115
|
+
"status.tonbo.sh",
|
|
1116
|
+
"streams.tonbo.sh",
|
|
1117
|
+
"www.tonbo.sh"
|
|
1118
|
+
]
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1023
1123
|
|
|
1024
1124
|
// src/contracts.ts
|
|
1025
1125
|
var ajv = new Ajv2020({ allErrors: true, useDefaults: true });
|
|
1026
1126
|
ajv.addKeyword({ keyword: "x-tonbo-profiles" });
|
|
1127
|
+
ajv.addKeyword({ keyword: "x-tonbo-version" });
|
|
1128
|
+
ajv.addKeyword({ keyword: "x-tonbo-public-hostname-apex" });
|
|
1129
|
+
ajv.addKeyword({ keyword: "x-tonbo-public-hostname-template" });
|
|
1027
1130
|
ajv.addSchema(kubernetesProfilesSchema);
|
|
1028
1131
|
ajv.addSchema(piAgentSchema);
|
|
1132
|
+
ajv.addSchema(projectNameContract);
|
|
1029
1133
|
ajv.addSchema(projectServiceSchema);
|
|
1030
1134
|
var validateDeclaration = ajv.compile(declarationSchema);
|
|
1031
|
-
var
|
|
1135
|
+
var validateDeploymentSpec = ajv.compile(deploymentSchema);
|
|
1032
1136
|
function validationMessage(label, errors) {
|
|
1033
1137
|
const detail = errors?.map((error) => `${error.instancePath || "/"} ${error.message}`).join("; ");
|
|
1034
1138
|
return `${label} is invalid${detail ? `: ${detail}` : "."}`;
|
|
@@ -1040,27 +1144,39 @@ function parseDeclaration(value) {
|
|
|
1040
1144
|
}
|
|
1041
1145
|
return candidate;
|
|
1042
1146
|
}
|
|
1043
|
-
function
|
|
1044
|
-
if (!
|
|
1045
|
-
throw new Error(validationMessage("Managed
|
|
1147
|
+
function assertManagedDeploymentSpec(value) {
|
|
1148
|
+
if (!validateDeploymentSpec(value)) {
|
|
1149
|
+
throw new Error(validationMessage("Managed Deployment spec", validateDeploymentSpec.errors));
|
|
1046
1150
|
}
|
|
1047
1151
|
}
|
|
1048
1152
|
|
|
1049
1153
|
// src/declaration.ts
|
|
1154
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1155
|
+
import { lstat as lstat2, open, readFile as readFile2, rename, rm } from "node:fs/promises";
|
|
1156
|
+
import path2 from "node:path";
|
|
1157
|
+
import { parse, stringify } from "smol-toml";
|
|
1050
1158
|
var DECLARATION_FILENAME = ".tonbo";
|
|
1051
1159
|
var DEFAULT_INFERENCE_MODEL = "claude-sonnet-4-5";
|
|
1052
|
-
function createDeclaration(model = DEFAULT_INFERENCE_MODEL, driver = { kind: "native" }, buildCommand) {
|
|
1160
|
+
function createDeclaration(model = DEFAULT_INFERENCE_MODEL, driver = { kind: "native" }, buildCommand, projectHostname) {
|
|
1053
1161
|
return parseDeclaration({
|
|
1054
|
-
version:
|
|
1055
|
-
|
|
1162
|
+
version: 2,
|
|
1163
|
+
...projectHostname ? { agent: projectHostname } : {},
|
|
1164
|
+
harness: { runtime: "pi", driver },
|
|
1056
1165
|
inference: { model: model.trim() },
|
|
1057
1166
|
...buildCommand ? { build: { command: buildCommand } } : {}
|
|
1058
1167
|
});
|
|
1059
1168
|
}
|
|
1060
1169
|
function renderDeclaration(declaration) {
|
|
1061
|
-
return `# Tonbo
|
|
1170
|
+
return `# Tonbo Agent configuration.
|
|
1171
|
+
# Edit this file directly or run \`tonbo init\` to reconfigure.
|
|
1062
1172
|
${stringify(declaration)}`;
|
|
1063
1173
|
}
|
|
1174
|
+
async function bindDeclarationProject(root, projectHostname) {
|
|
1175
|
+
const declaration = await loadDeclaration(root);
|
|
1176
|
+
const bound = parseDeclaration({ ...declaration, agent: projectHostname });
|
|
1177
|
+
await saveDeclaration(root, bound, true);
|
|
1178
|
+
return bound;
|
|
1179
|
+
}
|
|
1064
1180
|
async function declarationExists(root) {
|
|
1065
1181
|
const filename = path2.join(root, DECLARATION_FILENAME);
|
|
1066
1182
|
try {
|
|
@@ -1120,10 +1236,10 @@ async function loadDeclaration(declarationRoot) {
|
|
|
1120
1236
|
}
|
|
1121
1237
|
return parseDeclaration(parsed);
|
|
1122
1238
|
}
|
|
1123
|
-
function
|
|
1239
|
+
function buildDeploymentSpec(declaration, source) {
|
|
1124
1240
|
const spec = {
|
|
1125
1241
|
version: 1,
|
|
1126
|
-
agent: declaration.
|
|
1242
|
+
agent: declaration.harness,
|
|
1127
1243
|
source: {
|
|
1128
1244
|
format: source.format,
|
|
1129
1245
|
sha256: source.sha256,
|
|
@@ -1132,12 +1248,12 @@ function buildRevision(declaration, source) {
|
|
|
1132
1248
|
inference: declaration.inference,
|
|
1133
1249
|
...declaration.service ? { service: declaration.service } : {}
|
|
1134
1250
|
};
|
|
1135
|
-
|
|
1251
|
+
assertManagedDeploymentSpec(spec);
|
|
1136
1252
|
return spec;
|
|
1137
1253
|
}
|
|
1138
1254
|
|
|
1139
1255
|
// src/source.ts
|
|
1140
|
-
import { createHash as
|
|
1256
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1141
1257
|
import { lstat as lstat3, readFile as readFile3, readdir } from "node:fs/promises";
|
|
1142
1258
|
import path3 from "node:path";
|
|
1143
1259
|
import ignore from "ignore";
|
|
@@ -1199,7 +1315,7 @@ async function validatePackageSource(root, source) {
|
|
|
1199
1315
|
throw error;
|
|
1200
1316
|
}
|
|
1201
1317
|
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
1202
|
-
throw new Error(`Local PI package ${source} must resolve to a real
|
|
1318
|
+
throw new Error(`Local PI package ${source} must resolve to a real agent directory.`);
|
|
1203
1319
|
}
|
|
1204
1320
|
return;
|
|
1205
1321
|
}
|
|
@@ -1339,13 +1455,13 @@ async function buildSourceBundle(root) {
|
|
|
1339
1455
|
bytes,
|
|
1340
1456
|
format: sourceBundleContract.format,
|
|
1341
1457
|
root: resolvedRoot,
|
|
1342
|
-
sha256:
|
|
1458
|
+
sha256: createHash2("sha256").update(bytes).digest("hex"),
|
|
1343
1459
|
size_bytes: bytes.length
|
|
1344
1460
|
};
|
|
1345
1461
|
}
|
|
1346
1462
|
|
|
1347
1463
|
// src/ssh-key.ts
|
|
1348
|
-
import { createHash as
|
|
1464
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1349
1465
|
import { readFile as readFile4 } from "node:fs/promises";
|
|
1350
1466
|
import { homedir } from "node:os";
|
|
1351
1467
|
import { basename, join } from "node:path";
|
|
@@ -1360,13 +1476,13 @@ function parseOpenSshPublicKey(value, label) {
|
|
|
1360
1476
|
throw new Error("SSH public key is not canonical base64.");
|
|
1361
1477
|
return {
|
|
1362
1478
|
algorithm: fields[0],
|
|
1363
|
-
fingerprint: `SHA256:${
|
|
1479
|
+
fingerprint: `SHA256:${createHash3("sha256").update(blob).digest("base64").replace(/=$/, "")}`,
|
|
1364
1480
|
keyBase64: fields[1],
|
|
1365
1481
|
label
|
|
1366
1482
|
};
|
|
1367
1483
|
}
|
|
1368
|
-
async function readSshPublicKey(
|
|
1369
|
-
return parseOpenSshPublicKey(await readFile4(
|
|
1484
|
+
async function readSshPublicKey(path6) {
|
|
1485
|
+
return parseOpenSshPublicKey(await readFile4(path6, "utf8"), basename(path6, ".pub"));
|
|
1370
1486
|
}
|
|
1371
1487
|
async function readDefaultSshPublicKeys(sshDirectory = join(homedir(), ".ssh")) {
|
|
1372
1488
|
const keys = [];
|
|
@@ -1382,140 +1498,362 @@ async function readDefaultSshPublicKeys(sshDirectory = join(homedir(), ".ssh"))
|
|
|
1382
1498
|
return keys;
|
|
1383
1499
|
}
|
|
1384
1500
|
|
|
1385
|
-
// src/
|
|
1386
|
-
|
|
1387
|
-
var
|
|
1388
|
-
|
|
1389
|
-
|
|
1501
|
+
// src/project-name.ts
|
|
1502
|
+
var PROJECT_NAME_RULE = projectNameContract.$defs.name;
|
|
1503
|
+
var PROJECT_NAME_PATTERN = new RegExp(PROJECT_NAME_RULE.pattern);
|
|
1504
|
+
var RESERVED_PROJECT_NAMES = new Set(PROJECT_NAME_RULE.not.enum);
|
|
1505
|
+
function validateProjectName(name) {
|
|
1506
|
+
const valid = name.length >= PROJECT_NAME_RULE.minLength && name.length <= PROJECT_NAME_RULE.maxLength && PROJECT_NAME_PATTERN.test(name) && !RESERVED_PROJECT_NAMES.has(name);
|
|
1507
|
+
if (!valid) {
|
|
1508
|
+
throw new Error(
|
|
1509
|
+
`Agent name must be ${PROJECT_NAME_RULE.minLength}-${PROJECT_NAME_RULE.maxLength} lowercase letters, digits, or single hyphens, start with a letter, and not be reserved.`
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1390
1512
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
});
|
|
1401
|
-
});
|
|
1513
|
+
function projectNameSuggestion(value) {
|
|
1514
|
+
let name = value.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
|
|
1515
|
+
if (!/^[a-z]/.test(name)) name = `agent-${name}`;
|
|
1516
|
+
name = name.slice(0, PROJECT_NAME_RULE.maxLength).replace(/-+$/g, "");
|
|
1517
|
+
if (name.length < PROJECT_NAME_RULE.minLength) {
|
|
1518
|
+
name = `${name || "agent"}-agent`.slice(0, PROJECT_NAME_RULE.maxLength);
|
|
1519
|
+
}
|
|
1520
|
+
if (RESERVED_PROJECT_NAMES.has(name)) name = `${name}-agent`;
|
|
1521
|
+
return name;
|
|
1402
1522
|
}
|
|
1403
1523
|
|
|
1404
1524
|
// src/commands.ts
|
|
1405
|
-
async function resolveProject(deps
|
|
1406
|
-
const oauthToken = await deps.auth.accessToken();
|
|
1407
|
-
if (selector) {
|
|
1408
|
-
const normalized = selector.endsWith(".tonbo.sh") ? selector.slice(0, -".tonbo.sh".length) : selector;
|
|
1409
|
-
return {
|
|
1410
|
-
oauthToken,
|
|
1411
|
-
project: selectProject(await deps.api.listProjects(oauthToken), normalized)
|
|
1412
|
-
};
|
|
1413
|
-
}
|
|
1525
|
+
async function resolveProject(deps) {
|
|
1414
1526
|
const root = await findDeclarationRoot(deps.cwd());
|
|
1415
|
-
const
|
|
1416
|
-
if (!
|
|
1527
|
+
const declaration = await loadDeclaration(root);
|
|
1528
|
+
if (!declaration.agent) {
|
|
1529
|
+
throw new Error("No Agent is configured in .tonbo. Run `tonbo agent use <agent>` first.");
|
|
1530
|
+
}
|
|
1531
|
+
const oauthToken = await deps.auth.accessToken();
|
|
1417
1532
|
return {
|
|
1533
|
+
declaration,
|
|
1418
1534
|
oauthToken,
|
|
1419
|
-
project:
|
|
1420
|
-
|
|
1421
|
-
slug: binding.projectSlug,
|
|
1422
|
-
status: "active"
|
|
1423
|
-
}
|
|
1535
|
+
project: selectProject(await deps.api.listProjects(oauthToken), declaration.agent),
|
|
1536
|
+
root
|
|
1424
1537
|
};
|
|
1425
1538
|
}
|
|
1426
1539
|
function selectProject(projects, selector) {
|
|
1540
|
+
const normalized = selector.toLowerCase();
|
|
1427
1541
|
const matches = projects.filter(
|
|
1428
|
-
(project) => project.id === selector || project.
|
|
1542
|
+
(project) => project.id === selector || project.name === normalized || project.publicHostname === normalized
|
|
1429
1543
|
);
|
|
1430
|
-
if (matches.length === 0) throw new Error(`
|
|
1431
|
-
if (matches.length > 1)
|
|
1432
|
-
|
|
1544
|
+
if (matches.length === 0) throw new Error(`Agent ${selector} was not found in your account.`);
|
|
1545
|
+
if (matches.length > 1)
|
|
1546
|
+
throw new Error(`Agent name ${selector} is ambiguous; use its full hostname or ID.`);
|
|
1547
|
+
if (matches[0].status !== "active") throw new Error(`Agent ${selector} is not active.`);
|
|
1433
1548
|
return matches[0];
|
|
1434
1549
|
}
|
|
1435
1550
|
async function initCommand(deps, options) {
|
|
1436
1551
|
const root = deps.cwd();
|
|
1437
1552
|
const exists = await declarationExists(root);
|
|
1438
|
-
let
|
|
1439
|
-
if (exists
|
|
1440
|
-
|
|
1441
|
-
throw new Error(`${DECLARATION_FILENAME} already exists. Pass --force to replace it.`);
|
|
1442
|
-
const answer = (await deps.prompt(`Replace existing ${DECLARATION_FILENAME}? [y/N] `)).trim().toLowerCase();
|
|
1443
|
-
if (answer !== "y" && answer !== "yes") {
|
|
1444
|
-
deps.output({ message: `Kept existing ${DECLARATION_FILENAME}.` });
|
|
1445
|
-
return;
|
|
1446
|
-
}
|
|
1447
|
-
overwrite = true;
|
|
1553
|
+
let existingDeclaration;
|
|
1554
|
+
if (exists) {
|
|
1555
|
+
existingDeclaration = await loadDeclaration(root).catch(() => void 0);
|
|
1448
1556
|
}
|
|
1557
|
+
const interactive = deps.interactive();
|
|
1558
|
+
const overwrite = options.force === true || exists && interactive;
|
|
1559
|
+
if (exists && !overwrite)
|
|
1560
|
+
throw new Error(`${DECLARATION_FILENAME} already exists. Pass --force to replace it.`);
|
|
1449
1561
|
const inspection = await deps.inspectSource(root);
|
|
1450
|
-
let harness = options.harness;
|
|
1562
|
+
let harness = options.harness ?? (existingDeclaration ? "pi" : void 0);
|
|
1451
1563
|
if (harness !== void 0 && harness !== "pi") {
|
|
1452
1564
|
throw new Error("--harness must name a supported Harness: pi.");
|
|
1453
1565
|
}
|
|
1454
|
-
let driver = options.driver;
|
|
1566
|
+
let driver = options.driver ?? existingDeclaration?.harness.driver.kind;
|
|
1455
1567
|
if (driver !== void 0 && driver !== "native" && driver !== "command") {
|
|
1456
1568
|
throw new Error("--driver must be native or command.");
|
|
1457
1569
|
}
|
|
1458
|
-
if (!harness && !driver && deps.interactive() && inspection.harness.state === "selection_required") {
|
|
1459
|
-
const target = await deps.select("How should Tonbo run this project?", piTargetChoices(false));
|
|
1460
|
-
harness = "pi";
|
|
1461
|
-
driver = target;
|
|
1462
|
-
}
|
|
1463
1570
|
if (!harness) {
|
|
1464
1571
|
if (inspection.harness.state === "identified") harness = inspection.harness.id;
|
|
1465
|
-
else if (!
|
|
1572
|
+
else if (!interactive) {
|
|
1466
1573
|
throw new Error("No Harness-specific configuration found. Pass --harness pi.");
|
|
1467
1574
|
} else {
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
value: "pi"
|
|
1473
|
-
}
|
|
1474
|
-
]);
|
|
1575
|
+
deps.output({
|
|
1576
|
+
message: "No Harness-specific configuration was found in this project."
|
|
1577
|
+
});
|
|
1578
|
+
harness = await selectHarness(deps);
|
|
1475
1579
|
}
|
|
1476
1580
|
}
|
|
1477
|
-
if (harness !== "pi") throw new Error(`Harness ${harness} is not supported.`);
|
|
1478
1581
|
if (!driver) {
|
|
1479
|
-
if (!
|
|
1582
|
+
if (!interactive) {
|
|
1480
1583
|
throw new Error("PI execution mode is required. Pass --driver native or --driver command.");
|
|
1481
1584
|
}
|
|
1482
1585
|
if (inspection.pi.settingsFound) {
|
|
1483
1586
|
const packageSummary = inspection.pi.packageCount === 0 ? "Found PI configuration at .pi/settings.json." : `Found PI configuration at .pi/settings.json with ${inspection.pi.packageCount} package${inspection.pi.packageCount === 1 ? "" : "s"}.`;
|
|
1484
1587
|
deps.output({ message: packageSummary });
|
|
1485
1588
|
}
|
|
1486
|
-
driver =
|
|
1487
|
-
"How should Tonbo start PI?",
|
|
1488
|
-
piTargetChoices(inspection.pi.settingsFound)
|
|
1489
|
-
);
|
|
1589
|
+
driver = "native";
|
|
1490
1590
|
}
|
|
1491
1591
|
if (driver === "native" && (options.agentEntry || options.buildCommand)) {
|
|
1492
1592
|
throw new Error("--agent-entry and --build-command require --driver command.");
|
|
1493
1593
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1594
|
+
const existingEntry = existingDeclaration?.harness.driver.kind === "command" && existingDeclaration.harness.driver.command[0] === "node" ? existingDeclaration.harness.driver.command[1] : void 0;
|
|
1595
|
+
let entry = options.agentEntry?.trim() || existingEntry;
|
|
1596
|
+
if (driver === "command" && !entry && interactive) {
|
|
1496
1597
|
entry = (await deps.prompt("PI SDK entry file [dist/agent.mjs]: ")).trim();
|
|
1497
1598
|
}
|
|
1498
1599
|
entry ||= "dist/agent.mjs";
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1600
|
+
let model = options.model?.trim() || existingDeclaration?.inference.model || DEFAULT_INFERENCE_MODEL;
|
|
1601
|
+
let buildCommand = driver === "command" ? options.buildCommand ?? existingDeclaration?.build?.command ?? ["npm", "run", "build"] : void 0;
|
|
1602
|
+
let projectPlan = existingDeclaration?.agent ? {
|
|
1603
|
+
kind: "existing",
|
|
1604
|
+
publicHostname: existingDeclaration.agent
|
|
1605
|
+
} : { kind: "later" };
|
|
1606
|
+
if (interactive && !existingDeclaration?.agent) {
|
|
1607
|
+
projectPlan = await selectInitProject(deps, root);
|
|
1608
|
+
}
|
|
1609
|
+
if (interactive) {
|
|
1610
|
+
while (true) {
|
|
1611
|
+
deps.output({
|
|
1612
|
+
message: renderInitSummary(projectPlan, harness, driver, model, entry, buildCommand)
|
|
1613
|
+
});
|
|
1614
|
+
const saveLabel = initSaveLabel(exists, projectPlan);
|
|
1615
|
+
const action = await deps.select(`${saveLabel} with this configuration?`, [
|
|
1616
|
+
{
|
|
1617
|
+
name: saveLabel,
|
|
1618
|
+
value: "save"
|
|
1619
|
+
},
|
|
1620
|
+
{
|
|
1621
|
+
description: "Change the agent, Harness, execution mode, model, or SDK entrypoint.",
|
|
1622
|
+
name: "Edit configuration",
|
|
1623
|
+
value: "edit"
|
|
1624
|
+
},
|
|
1625
|
+
{ name: "Cancel", value: "cancel" }
|
|
1626
|
+
]);
|
|
1627
|
+
if (action === "cancel") {
|
|
1628
|
+
deps.output({
|
|
1629
|
+
message: exists ? `Cancelled. Kept existing ${DECLARATION_FILENAME}.` : `Cancelled. Did not create ${DECLARATION_FILENAME}.`
|
|
1630
|
+
});
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
if (action === "save") break;
|
|
1634
|
+
const setting = await deps.select("What would you like to change?", [
|
|
1635
|
+
{
|
|
1636
|
+
description: initProjectSummary(projectPlan),
|
|
1637
|
+
name: "Project",
|
|
1638
|
+
value: "project"
|
|
1639
|
+
},
|
|
1640
|
+
{ description: harnessName(harness), name: "Harness", value: "harness" },
|
|
1641
|
+
{
|
|
1642
|
+
description: driver === "native" ? "PI CLI (`pi`)" : "PI SDK app",
|
|
1643
|
+
name: "Execution mode",
|
|
1644
|
+
value: "driver"
|
|
1645
|
+
},
|
|
1646
|
+
{ description: model, name: "Inference model", value: "model" },
|
|
1647
|
+
...driver === "command" ? [{ description: entry, name: "PI SDK entry file", value: "entry" }] : [],
|
|
1648
|
+
{ name: "Back to review", value: "back" }
|
|
1649
|
+
]);
|
|
1650
|
+
if (setting === "project") projectPlan = await selectInitProject(deps, root);
|
|
1651
|
+
if (setting === "harness") harness = await selectHarness(deps);
|
|
1652
|
+
if (setting === "driver") {
|
|
1653
|
+
driver = await deps.select(
|
|
1654
|
+
"How should Tonbo start PI?",
|
|
1655
|
+
piTargetChoices(inspection.pi.settingsFound)
|
|
1656
|
+
);
|
|
1657
|
+
if (driver === "command") {
|
|
1658
|
+
const answer = (await deps.prompt(`PI SDK entry file [${entry}]: `)).trim();
|
|
1659
|
+
if (answer) entry = answer;
|
|
1660
|
+
buildCommand = options.buildCommand ?? ["npm", "run", "build"];
|
|
1661
|
+
} else {
|
|
1662
|
+
buildCommand = void 0;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
if (setting === "model") {
|
|
1666
|
+
const answer = (await deps.prompt(`Inference model [${model}]: `)).trim();
|
|
1667
|
+
if (answer) model = answer;
|
|
1668
|
+
}
|
|
1669
|
+
if (setting === "entry") {
|
|
1670
|
+
const answer = (await deps.prompt(`PI SDK entry file [${entry}]: `)).trim();
|
|
1671
|
+
if (answer) entry = answer;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
let projectHostname = projectPlan.kind === "existing" ? projectPlan.publicHostname : void 0;
|
|
1676
|
+
let createdProject;
|
|
1677
|
+
if (projectPlan.kind === "create") {
|
|
1678
|
+
const oauthToken = await deps.auth.accessToken();
|
|
1679
|
+
createdProject = await deps.api.createProject(
|
|
1680
|
+
oauthToken,
|
|
1681
|
+
projectPlan.name,
|
|
1682
|
+
projectPlan.organizationId
|
|
1683
|
+
);
|
|
1684
|
+
projectHostname = createdProject.publicHostname;
|
|
1685
|
+
}
|
|
1686
|
+
const configuredDeclaration = createDeclaration(
|
|
1502
1687
|
model,
|
|
1503
1688
|
driver === "native" ? { kind: "native" } : { kind: "command", protocol: "pi-rpc-v1", command: ["node", entry] },
|
|
1504
|
-
buildCommand
|
|
1689
|
+
buildCommand,
|
|
1690
|
+
projectHostname
|
|
1505
1691
|
);
|
|
1506
|
-
|
|
1692
|
+
const declaration = parseDeclaration({
|
|
1693
|
+
...configuredDeclaration,
|
|
1694
|
+
harness: {
|
|
1695
|
+
...configuredDeclaration.harness,
|
|
1696
|
+
...existingDeclaration?.harness.secrets ? { secrets: existingDeclaration.harness.secrets } : {}
|
|
1697
|
+
},
|
|
1698
|
+
...existingDeclaration?.service ? { service: existingDeclaration.service } : {}
|
|
1699
|
+
});
|
|
1700
|
+
try {
|
|
1701
|
+
await saveDeclaration(root, declaration, overwrite);
|
|
1702
|
+
} catch (error) {
|
|
1703
|
+
if (createdProject) {
|
|
1704
|
+
throw new Error(
|
|
1705
|
+
`Created agent ${createdProject.name}, but could not write ${DECLARATION_FILENAME}. Fix the file and run \`tonbo agent use ${createdProject.publicHostname}\`.`,
|
|
1706
|
+
{ cause: error }
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
throw error;
|
|
1710
|
+
}
|
|
1507
1711
|
const driverSummary = driver === "command" ? `PI SDK app
|
|
1508
1712
|
Build: ${buildCommand?.join(" ")}
|
|
1509
1713
|
Entrypoint: node ${entry}` : inspection.pi.packageCount > 0 ? `PI CLI with ${inspection.pi.packageCount} package${inspection.pi.packageCount === 1 ? "" : "s"}` : "PI CLI";
|
|
1714
|
+
const projectSummary = createdProject ? `Agent: ${createdProject.name} (${createdProject.publicHostname})
|
|
1715
|
+
Organization: ${createdProject.organizationName}
|
|
1716
|
+
Application: https://${createdProject.publicHostname}
|
|
1717
|
+
SSH after deploy: ssh ${createdProject.sshDestination}` : projectPlan.kind === "existing" ? `Agent: ${projectPlan.name ? `${projectPlan.name} (${projectPlan.publicHostname})` : projectPlan.publicHostname}${projectPlan.organizationName ? `
|
|
1718
|
+
Organization: ${projectPlan.organizationName}` : ""}${projectPlan.sshDestination ? `
|
|
1719
|
+
SSH after deploy: ssh ${projectPlan.sshDestination}` : ""}` : "Next: tonbo agent create <name>";
|
|
1510
1720
|
deps.output({
|
|
1511
|
-
message: `${exists ? "Updated" : "Created"} ${DECLARATION_FILENAME}.
|
|
1721
|
+
message: `${createdProject ? `Created agent ${createdProject.name} and` : exists ? "Updated" : "Created"} ${DECLARATION_FILENAME}.
|
|
1722
|
+
${projectSummary}
|
|
1512
1723
|
Harness: PI
|
|
1513
|
-
Mode: ${driverSummary}
|
|
1514
|
-
Next: tonbo project create <slug>`,
|
|
1724
|
+
Mode: ${driverSummary}${projectPlan.kind === "later" ? "" : "\nNext: tonbo deploy"}`,
|
|
1515
1725
|
declaration,
|
|
1516
1726
|
path: path4.join(root, DECLARATION_FILENAME)
|
|
1517
1727
|
});
|
|
1518
1728
|
}
|
|
1729
|
+
function initSaveLabel(exists, project) {
|
|
1730
|
+
if (project.kind === "create") {
|
|
1731
|
+
return exists ? `Create agent and replace ${DECLARATION_FILENAME}` : `Create agent and ${DECLARATION_FILENAME}`;
|
|
1732
|
+
}
|
|
1733
|
+
return exists ? `Replace ${DECLARATION_FILENAME}` : `Create ${DECLARATION_FILENAME}`;
|
|
1734
|
+
}
|
|
1735
|
+
async function selectInitProject(deps, root) {
|
|
1736
|
+
for (; ; ) {
|
|
1737
|
+
const action = await deps.select("How should this Agent connect to Tonbo?", [
|
|
1738
|
+
{
|
|
1739
|
+
description: "Create it only after you confirm the complete configuration.",
|
|
1740
|
+
name: "Create a new agent",
|
|
1741
|
+
value: "create"
|
|
1742
|
+
},
|
|
1743
|
+
{
|
|
1744
|
+
description: "Bind this source tree to one agent you can access.",
|
|
1745
|
+
name: "Use an existing agent",
|
|
1746
|
+
value: "existing"
|
|
1747
|
+
},
|
|
1748
|
+
{
|
|
1749
|
+
description: "Write an unbound declaration and configure its agent later.",
|
|
1750
|
+
name: "Set up later",
|
|
1751
|
+
value: "later"
|
|
1752
|
+
}
|
|
1753
|
+
]);
|
|
1754
|
+
if (action === "later") return { kind: "later" };
|
|
1755
|
+
if (action === "create") {
|
|
1756
|
+
const organization = await selectInitOrganization(deps);
|
|
1757
|
+
const defaultName = projectNameSuggestion(path4.basename(root)) || "tonbo-agent";
|
|
1758
|
+
for (; ; ) {
|
|
1759
|
+
const answer = (await deps.prompt(`Agent name [${defaultName}]: `)).trim().toLowerCase();
|
|
1760
|
+
const name = answer || defaultName;
|
|
1761
|
+
try {
|
|
1762
|
+
validateProjectName(name);
|
|
1763
|
+
return {
|
|
1764
|
+
kind: "create",
|
|
1765
|
+
name,
|
|
1766
|
+
organizationId: organization?.id,
|
|
1767
|
+
organizationName: organization?.name
|
|
1768
|
+
};
|
|
1769
|
+
} catch (error) {
|
|
1770
|
+
deps.output({
|
|
1771
|
+
message: error instanceof Error ? error.message : "Agent name is invalid."
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
const oauthToken = await deps.auth.accessToken();
|
|
1777
|
+
const projects = (await deps.api.listProjects(oauthToken)).filter(
|
|
1778
|
+
(project2) => project2.status === "active"
|
|
1779
|
+
);
|
|
1780
|
+
if (projects.length === 0) {
|
|
1781
|
+
deps.output({ message: "No active agents are available. Create a new agent instead." });
|
|
1782
|
+
continue;
|
|
1783
|
+
}
|
|
1784
|
+
const id = await deps.select(
|
|
1785
|
+
"Which agent should this Agent use?",
|
|
1786
|
+
projects.map((project2) => ({
|
|
1787
|
+
name: project2.name,
|
|
1788
|
+
description: `Organization: ${project2.organizationName} \xB7 ${project2.publicHostname} \xB7 ${project2.id}`,
|
|
1789
|
+
value: project2.id
|
|
1790
|
+
}))
|
|
1791
|
+
);
|
|
1792
|
+
const project = selectProject(projects, id);
|
|
1793
|
+
return {
|
|
1794
|
+
kind: "existing",
|
|
1795
|
+
name: project.name,
|
|
1796
|
+
organizationName: project.organizationName,
|
|
1797
|
+
publicHostname: project.publicHostname,
|
|
1798
|
+
sshDestination: project.sshDestination
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
function initProjectSummary(project) {
|
|
1803
|
+
if (project.kind === "create")
|
|
1804
|
+
return `Create ${project.name}${project.organizationName ? ` in ${project.organizationName}` : ""} (permanent address assigned after creation)`;
|
|
1805
|
+
if (project.kind === "existing") {
|
|
1806
|
+
return project.name ? `${project.name}${project.organizationName ? ` in ${project.organizationName}` : ""} (${project.publicHostname})` : `Keep ${project.publicHostname}`;
|
|
1807
|
+
}
|
|
1808
|
+
return "Set up later";
|
|
1809
|
+
}
|
|
1810
|
+
async function selectInitOrganization(deps) {
|
|
1811
|
+
const oauthToken = await deps.auth.accessToken();
|
|
1812
|
+
const organizations = (await deps.api.listOrganizations(oauthToken)).filter(
|
|
1813
|
+
(organization) => organization.role !== "member"
|
|
1814
|
+
);
|
|
1815
|
+
if (organizations.length === 0)
|
|
1816
|
+
throw new Error("No organization lets you create a project. Ask an admin or owner.");
|
|
1817
|
+
if (organizations.length === 1) return organizations[0];
|
|
1818
|
+
const id = await deps.select(
|
|
1819
|
+
"Which organization should own the new agent?",
|
|
1820
|
+
organizations.map((organization) => ({
|
|
1821
|
+
description: organization.role,
|
|
1822
|
+
name: organization.name,
|
|
1823
|
+
value: organization.id
|
|
1824
|
+
}))
|
|
1825
|
+
);
|
|
1826
|
+
return organizations.find((organization) => organization.id === id) ?? organizations[0];
|
|
1827
|
+
}
|
|
1828
|
+
async function selectHarness(deps) {
|
|
1829
|
+
return await deps.select(
|
|
1830
|
+
"Which supported Harness should Tonbo use?",
|
|
1831
|
+
supportedHarnesses.map((supported) => ({
|
|
1832
|
+
description: `Run this agent with the ${supported.command} command.`,
|
|
1833
|
+
name: `${supported.name} (\`${supported.command}\`)`,
|
|
1834
|
+
value: supported.id
|
|
1835
|
+
}))
|
|
1836
|
+
);
|
|
1837
|
+
}
|
|
1838
|
+
function harnessName(harness) {
|
|
1839
|
+
const supported = supportedHarnesses.find((candidate) => candidate.id === harness);
|
|
1840
|
+
return supported?.name ?? harness;
|
|
1841
|
+
}
|
|
1842
|
+
function renderInitSummary(project, harness, driver, model, entry, buildCommand) {
|
|
1843
|
+
const lines = [
|
|
1844
|
+
"",
|
|
1845
|
+
"Tonbo Agent configuration:",
|
|
1846
|
+
` Agent: ${initProjectSummary(project)}`,
|
|
1847
|
+
` Harness: ${harnessName(harness)}`,
|
|
1848
|
+
` Execution: ${driver === "native" ? "PI CLI (`pi`)" : "PI SDK app"}`,
|
|
1849
|
+
` Model: ${model}`
|
|
1850
|
+
];
|
|
1851
|
+
if (driver === "command") {
|
|
1852
|
+
lines.push(` Build: ${buildCommand?.join(" ") ?? "none"}`);
|
|
1853
|
+
lines.push(` Entrypoint: node ${entry}`);
|
|
1854
|
+
}
|
|
1855
|
+
return lines.join("\n");
|
|
1856
|
+
}
|
|
1519
1857
|
function piTargetChoices(settingsFound) {
|
|
1520
1858
|
const choices = {
|
|
1521
1859
|
command: {
|
|
@@ -1579,8 +1917,8 @@ function reportLoginProgress(progress, event) {
|
|
|
1579
1917
|
if (event.status === "started") progress.start(messages.started);
|
|
1580
1918
|
else progress.succeed(messages.completed);
|
|
1581
1919
|
}
|
|
1582
|
-
async function sshKeyAddCommand(deps,
|
|
1583
|
-
const key = await readSshPublicKey(
|
|
1920
|
+
async function sshKeyAddCommand(deps, path6) {
|
|
1921
|
+
const key = await readSshPublicKey(path6);
|
|
1584
1922
|
const oauthToken = await deps.auth.accessToken();
|
|
1585
1923
|
await deps.api.registerSshKey(oauthToken, key);
|
|
1586
1924
|
deps.output({ message: `Registered SSH key ${key.fingerprint}.`, key });
|
|
@@ -1590,166 +1928,361 @@ async function sshKeyRemoveCommand(deps, fingerprint) {
|
|
|
1590
1928
|
const key = await deps.api.revokeSshKey(oauthToken, fingerprint);
|
|
1591
1929
|
deps.output({ message: `Revoked SSH key ${key.fingerprint}.`, key });
|
|
1592
1930
|
}
|
|
1593
|
-
async function projectUseCommand(deps, selector) {
|
|
1931
|
+
async function projectUseCommand(deps, selector, force = false) {
|
|
1594
1932
|
const root = await findDeclarationRoot(deps.cwd());
|
|
1933
|
+
const declaration = await loadDeclaration(root);
|
|
1595
1934
|
const oauthToken = await deps.auth.accessToken();
|
|
1596
|
-
const
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1935
|
+
const projects = await deps.api.listProjects(oauthToken);
|
|
1936
|
+
const project = selectProject(projects, selector);
|
|
1937
|
+
if (declaration.agent === project.publicHostname) {
|
|
1938
|
+
deps.output({ message: `.tonbo already uses agent ${project.publicHostname}.`, project });
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
if (declaration.agent && !force) {
|
|
1942
|
+
const current = projects.find((candidate) => candidate.publicHostname === declaration.agent);
|
|
1943
|
+
const currentLabel = current?.publicHostname ?? declaration.agent;
|
|
1944
|
+
if (!deps.interactive()) {
|
|
1945
|
+
throw new Error(
|
|
1946
|
+
`.tonbo already uses agent ${currentLabel}. Pass --force to rebind it to ${project.publicHostname}.`
|
|
1947
|
+
);
|
|
1948
|
+
}
|
|
1949
|
+
const action = await deps.select(
|
|
1950
|
+
`Rebind .tonbo from ${currentLabel} to ${project.publicHostname}?`,
|
|
1951
|
+
[
|
|
1952
|
+
{ name: `Rebind to ${project.publicHostname}`, value: "rebind" },
|
|
1953
|
+
{ name: "Cancel", value: "cancel" }
|
|
1954
|
+
]
|
|
1955
|
+
);
|
|
1956
|
+
if (action !== "rebind") {
|
|
1957
|
+
deps.output({ message: `Cancelled. .tonbo still uses agent ${currentLabel}.` });
|
|
1958
|
+
return;
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
await bindDeclarationProject(root, project.publicHostname);
|
|
1962
|
+
deps.output({ message: `Bound .tonbo to agent ${project.publicHostname}.`, project });
|
|
1602
1963
|
}
|
|
1603
|
-
async function projectCreateCommand(deps,
|
|
1964
|
+
async function projectCreateCommand(deps, name) {
|
|
1604
1965
|
const root = await findDeclarationRoot(deps.cwd());
|
|
1966
|
+
const declaration = await loadDeclaration(root);
|
|
1967
|
+
if (declaration.agent) {
|
|
1968
|
+
throw new Error(
|
|
1969
|
+
`.tonbo is already bound to agent ${declaration.agent}. Run \`tonbo agent use <agent>\` to review a rebind.`
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1605
1972
|
const oauthToken = await deps.auth.accessToken();
|
|
1606
|
-
|
|
1607
|
-
await deps.
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
})
|
|
1611
|
-
|
|
1973
|
+
validateProjectName(name);
|
|
1974
|
+
const project = await deps.api.createProject(oauthToken, name);
|
|
1975
|
+
try {
|
|
1976
|
+
await bindDeclarationProject(root, project.publicHostname);
|
|
1977
|
+
} catch (error) {
|
|
1978
|
+
throw new Error(
|
|
1979
|
+
`Created agent ${project.name}, but could not bind ${DECLARATION_FILENAME}. Fix the file and run \`tonbo agent use ${project.publicHostname}\`.`,
|
|
1980
|
+
{ cause: error }
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
deps.output({ message: `Created agent ${project.name} and bound it in .tonbo.`, project });
|
|
1612
1984
|
}
|
|
1613
|
-
async function deployCommand(deps,
|
|
1614
|
-
const root = await
|
|
1615
|
-
const declaration = await loadDeclaration(root);
|
|
1985
|
+
async function deployCommand(deps, options = { promote: true }) {
|
|
1986
|
+
const { declaration, oauthToken, project, root } = await resolveProject(deps);
|
|
1616
1987
|
if (declaration.build) await runBuildCommand(root, declaration.build.command);
|
|
1617
1988
|
const source = await buildSourceBundle(root);
|
|
1618
|
-
const
|
|
1619
|
-
|
|
1620
|
-
if (selector) {
|
|
1621
|
-
const project = selectProject(await deps.api.listProjects(oauthToken), selector);
|
|
1622
|
-
binding = { projectId: project.id, projectSlug: project.slug };
|
|
1623
|
-
}
|
|
1624
|
-
if (!binding) throw new Error("No Project selected. Run `tonbo project use <project>` first.");
|
|
1625
|
-
const managementToken = await deps.api.exchangeManagementToken(oauthToken, binding.projectId);
|
|
1989
|
+
const origin = { actor: "cli", git: await deps.gitProvenance(root) };
|
|
1990
|
+
const managementToken = await deps.api.exchangeManagementToken(oauthToken, project.id);
|
|
1626
1991
|
const result = await deps.api.deploy({
|
|
1627
1992
|
bundle: source,
|
|
1628
|
-
|
|
1629
|
-
|
|
1993
|
+
origin,
|
|
1994
|
+
projectId: project.id,
|
|
1995
|
+
promote: options.promote,
|
|
1996
|
+
spec: buildDeploymentSpec(declaration, source),
|
|
1630
1997
|
token: managementToken
|
|
1631
1998
|
});
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1999
|
+
const { deployment } = result;
|
|
2000
|
+
const shortId = shortDeploymentId(deployment.id);
|
|
2001
|
+
const lines = [
|
|
2002
|
+
options.promote ? `Deployed agent ${project.name}.` : `Created Deployment ${deploymentName(deployment)} (${shortId}) for agent ${project.name} without promoting it.`,
|
|
2003
|
+
`Organization: ${project.organizationName}`,
|
|
2004
|
+
`Deployment: ${deploymentName(deployment)} (${shortId})`,
|
|
2005
|
+
`Source: ${deploymentSourceLabel(deployment)}`,
|
|
2006
|
+
`Production: ${options.promote ? productionStateLabel(deployment) : `not promoted; run \`tonbo deployments promote ${shortId}\` to move Production here`}`,
|
|
2007
|
+
`Application: https://${project.publicHostname}`
|
|
2008
|
+
];
|
|
2009
|
+
if (result.unchanged) {
|
|
2010
|
+
lines.push(
|
|
2011
|
+
`Contents are identical to the ${options.promote ? "previous" : "current"} Production Deployment; the running Agent does not change.`
|
|
2012
|
+
);
|
|
2013
|
+
}
|
|
2014
|
+
if (options.promote) {
|
|
2015
|
+
lines.push(
|
|
2016
|
+
"",
|
|
2017
|
+
"Agent process: starts with the first turn and stays warm while the runtime is active.",
|
|
2018
|
+
"",
|
|
2019
|
+
"Start the Agent:",
|
|
2020
|
+
' tonbo run "<prompt>"',
|
|
2021
|
+
"",
|
|
2022
|
+
"Connect with SSH:",
|
|
2023
|
+
` ssh ${project.sshDestination}`
|
|
2024
|
+
);
|
|
2025
|
+
}
|
|
2026
|
+
deps.output({ message: lines.join("\n"), project, ...result });
|
|
1637
2027
|
}
|
|
1638
2028
|
async function runCommand(deps, prompt, options) {
|
|
1639
|
-
const
|
|
1640
|
-
const
|
|
1641
|
-
let binding = await deps.config.getBinding(root);
|
|
1642
|
-
if (options.project) {
|
|
1643
|
-
const project = selectProject(await deps.api.listProjects(oauthToken), options.project);
|
|
1644
|
-
binding = { projectId: project.id, projectSlug: project.slug };
|
|
1645
|
-
}
|
|
1646
|
-
if (!binding) throw new Error("No Project selected. Run `tonbo project use <project>` first.");
|
|
1647
|
-
const managementToken = await deps.api.exchangeManagementToken(oauthToken, binding.projectId);
|
|
2029
|
+
const { oauthToken, project } = await resolveProject(deps);
|
|
2030
|
+
const managementToken = await deps.api.exchangeManagementToken(oauthToken, project.id);
|
|
1648
2031
|
const result = await deps.api.run({
|
|
1649
|
-
projectId:
|
|
2032
|
+
projectId: project.id,
|
|
1650
2033
|
prompt,
|
|
1651
2034
|
sessionId: options.session,
|
|
1652
2035
|
token: managementToken
|
|
1653
2036
|
});
|
|
1654
2037
|
deps.output({
|
|
1655
2038
|
message: result.turn.assistant_text,
|
|
1656
|
-
project
|
|
2039
|
+
project,
|
|
2040
|
+
...result
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
async function projectShowCommand(deps) {
|
|
2044
|
+
const { project, token } = await projectManagement(deps);
|
|
2045
|
+
const production = await deps.api.getProduction(project.id, token);
|
|
2046
|
+
const deployment = production ? await deps.api.getDeployment(project.id, production.deployment_id, token) : null;
|
|
2047
|
+
const lines = [
|
|
2048
|
+
`agent: ${project.name}`,
|
|
2049
|
+
`Organization: ${project.organizationName}`,
|
|
2050
|
+
`application: https://${project.publicHostname}`,
|
|
2051
|
+
`ssh: ssh ${project.sshDestination}`
|
|
2052
|
+
];
|
|
2053
|
+
if (deployment) {
|
|
2054
|
+
lines.push(
|
|
2055
|
+
`production: ${deploymentName(deployment)} (${shortDeploymentId(deployment.id)}) ${productionStateLabel(deployment)}`
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
deps.output({ message: lines.join("\n"), project, production, deployment });
|
|
2059
|
+
}
|
|
2060
|
+
function shortDeploymentId(id) {
|
|
2061
|
+
return id.replace(/-/g, "").slice(0, 8);
|
|
2062
|
+
}
|
|
2063
|
+
function deploymentName(deployment) {
|
|
2064
|
+
const subject = deployment.origin.git?.subject?.trim();
|
|
2065
|
+
return subject ? subject : shortDeploymentId(deployment.id);
|
|
2066
|
+
}
|
|
2067
|
+
function deploymentSourceLabel(deployment) {
|
|
2068
|
+
const git = deployment.origin.git;
|
|
2069
|
+
if (!git) return "tonbo deploy";
|
|
2070
|
+
const sha = git.commit_sha.slice(0, 7);
|
|
2071
|
+
return git.ref ? `${sha} \xB7 ${git.ref}` : sha;
|
|
2072
|
+
}
|
|
2073
|
+
function productionStateLabel(deployment) {
|
|
2074
|
+
const { generation, state } = deployment.production;
|
|
2075
|
+
return generation === null ? state : `${state} (generation ${generation})`;
|
|
2076
|
+
}
|
|
2077
|
+
function selectDeployment(deployments, prefix) {
|
|
2078
|
+
const normalized = prefix.trim().toLowerCase().replace(/-/g, "");
|
|
2079
|
+
if (!/^[0-9a-f]+$/.test(normalized)) {
|
|
2080
|
+
throw new Error(`Deployment id ${prefix} must be a hexadecimal id prefix.`);
|
|
2081
|
+
}
|
|
2082
|
+
const matches = deployments.filter(
|
|
2083
|
+
(deployment) => deployment.id.replace(/-/g, "").startsWith(normalized)
|
|
2084
|
+
);
|
|
2085
|
+
if (matches.length === 0) throw new Error(`Deployment ${prefix} was not found in this project.`);
|
|
2086
|
+
if (matches.length > 1) {
|
|
2087
|
+
throw new Error(
|
|
2088
|
+
`Deployment id ${prefix} is ambiguous; it matches ${matches.map((deployment) => shortDeploymentId(deployment.id)).join(", ")}. Use more characters.`
|
|
2089
|
+
);
|
|
2090
|
+
}
|
|
2091
|
+
return matches[0];
|
|
2092
|
+
}
|
|
2093
|
+
function renderTable(headers, rows) {
|
|
2094
|
+
const widths = headers.map(
|
|
2095
|
+
(header, column) => Math.max(header.length, ...rows.map((row) => row[column].length))
|
|
2096
|
+
);
|
|
2097
|
+
const render = (row) => row.map((cell, column) => column === row.length - 1 ? cell : cell.padEnd(widths[column])).join(" ").trimEnd();
|
|
2098
|
+
return [render(headers), ...rows.map(render)].join("\n");
|
|
2099
|
+
}
|
|
2100
|
+
function formatTimestamp(value) {
|
|
2101
|
+
const date = new Date(value);
|
|
2102
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
2103
|
+
return `${date.toISOString().slice(0, 16)}Z`;
|
|
2104
|
+
}
|
|
2105
|
+
function productionMarker(state) {
|
|
2106
|
+
if (state === "current") return "\u25CF";
|
|
2107
|
+
if (state === "previous") return "\u25CB";
|
|
2108
|
+
return "";
|
|
2109
|
+
}
|
|
2110
|
+
async function deploymentsListCommand(deps) {
|
|
2111
|
+
const { project, token } = await projectManagement(deps);
|
|
2112
|
+
const [deployments, production] = await Promise.all([
|
|
2113
|
+
deps.api.listDeployments(project.id, token),
|
|
2114
|
+
deps.api.getProduction(project.id, token)
|
|
2115
|
+
]);
|
|
2116
|
+
const message = deployments.length ? renderTable(
|
|
2117
|
+
["ID", "NAME", "STATUS", "PRODUCTION", "SOURCE", "CREATED", "BY"],
|
|
2118
|
+
deployments.map((deployment) => [
|
|
2119
|
+
shortDeploymentId(deployment.id),
|
|
2120
|
+
deploymentName(deployment),
|
|
2121
|
+
deployment.production.state,
|
|
2122
|
+
productionMarker(deployment.production.state),
|
|
2123
|
+
deploymentSourceLabel(deployment),
|
|
2124
|
+
formatTimestamp(deployment.created_at),
|
|
2125
|
+
deployment.created_by_user_id ? shortDeploymentId(deployment.created_by_user_id) : ""
|
|
2126
|
+
])
|
|
2127
|
+
) : `No Deployments exist for agent ${project.name}. Run \`tonbo deploy\` to create one.`;
|
|
2128
|
+
deps.output({ message, project, production, deployments });
|
|
2129
|
+
}
|
|
2130
|
+
async function deploymentsShowCommand(deps, prefix) {
|
|
2131
|
+
const { project, token } = await projectManagement(deps);
|
|
2132
|
+
const deployment = selectDeployment(await deps.api.listDeployments(project.id, token), prefix);
|
|
2133
|
+
const rollouts = (await deps.api.listRollouts(project.id, token)).filter(
|
|
2134
|
+
(rollout) => rollout.deployment_id === deployment.id || rollout.previous_deployment_id === deployment.id
|
|
2135
|
+
);
|
|
2136
|
+
deps.output({
|
|
2137
|
+
message: renderDeploymentDetail(deployment, rollouts),
|
|
2138
|
+
project,
|
|
2139
|
+
deployment,
|
|
2140
|
+
rollouts
|
|
2141
|
+
});
|
|
2142
|
+
}
|
|
2143
|
+
function renderDeploymentDetail(deployment, rollouts) {
|
|
2144
|
+
const git = deployment.origin.git;
|
|
2145
|
+
const source = deployment.spec.source;
|
|
2146
|
+
const bundleLabel = typeof source?.sha256 === "string" ? [
|
|
2147
|
+
typeof source.format === "string" ? source.format : "",
|
|
2148
|
+
source.sha256,
|
|
2149
|
+
typeof source.size_bytes === "number" ? `(${source.size_bytes} bytes)` : ""
|
|
2150
|
+
].filter(Boolean).join(" ") : null;
|
|
2151
|
+
const inference = deployment.spec.inference;
|
|
2152
|
+
const rows = [
|
|
2153
|
+
["Deployment", `${deploymentName(deployment)} (${shortDeploymentId(deployment.id)})`],
|
|
2154
|
+
["ID", deployment.id],
|
|
2155
|
+
["Status", productionStateLabel(deployment)],
|
|
2156
|
+
["Source", `${deploymentSourceLabel(deployment)}${git?.dirty ? " (dirty)" : ""}`]
|
|
2157
|
+
];
|
|
2158
|
+
if (git) rows.push(["Commit", git.commit_sha], ["Author", git.author ?? ""]);
|
|
2159
|
+
rows.push(
|
|
2160
|
+
["Created", formatTimestamp(deployment.created_at)],
|
|
2161
|
+
["Created by", deployment.created_by_user_id ?? ""],
|
|
2162
|
+
["Spec sha256", deployment.spec_sha256]
|
|
2163
|
+
);
|
|
2164
|
+
if (typeof inference?.model === "string") rows.push(["Model", inference.model]);
|
|
2165
|
+
if (bundleLabel) rows.push(["Bundle", bundleLabel]);
|
|
2166
|
+
const label = Math.max(...rows.map(([name]) => name.length)) + 1;
|
|
2167
|
+
const lines = rows.map(([name, value]) => `${`${name}:`.padEnd(label)} ${value}`.trimEnd());
|
|
2168
|
+
lines.push("", "Rollouts:");
|
|
2169
|
+
if (rollouts.length === 0) lines.push(" none");
|
|
2170
|
+
for (const rollout of rollouts) {
|
|
2171
|
+
const direction = rollout.deployment_id === deployment.id ? `${rollout.kind} to this Deployment${rollout.previous_deployment_id ? ` from ${shortDeploymentId(rollout.previous_deployment_id)}` : ""}` : `${rollout.kind} away to ${shortDeploymentId(rollout.deployment_id)}`;
|
|
2172
|
+
lines.push(
|
|
2173
|
+
` generation ${rollout.generation} ${formatTimestamp(rollout.created_at)} ${direction}${rollout.created_by_user_id ? ` by ${shortDeploymentId(rollout.created_by_user_id)}` : ""}`
|
|
2174
|
+
);
|
|
2175
|
+
}
|
|
2176
|
+
return lines.join("\n");
|
|
2177
|
+
}
|
|
2178
|
+
async function deploymentsPromoteCommand(deps, prefix) {
|
|
2179
|
+
const { project, token } = await projectManagement(deps);
|
|
2180
|
+
const target = selectDeployment(await deps.api.listDeployments(project.id, token), prefix);
|
|
2181
|
+
const result = await moveProduction(deps, project.id, target, token);
|
|
2182
|
+
deps.output({
|
|
2183
|
+
message: describeProductionMove(project, result, "Promoted"),
|
|
2184
|
+
project,
|
|
2185
|
+
...result
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
async function deploymentsRollbackCommand(deps, prefix) {
|
|
2189
|
+
const { project, token } = await projectManagement(deps);
|
|
2190
|
+
let target;
|
|
2191
|
+
if (prefix !== void 0) {
|
|
2192
|
+
target = selectDeployment(await deps.api.listDeployments(project.id, token), prefix);
|
|
2193
|
+
} else {
|
|
2194
|
+
const rollouts = await deps.api.listRollouts(project.id, token);
|
|
2195
|
+
const previousId = rollouts.find(
|
|
2196
|
+
(rollout) => rollout.previous_deployment_id !== null
|
|
2197
|
+
)?.previous_deployment_id;
|
|
2198
|
+
if (!previousId) {
|
|
2199
|
+
throw new Error(
|
|
2200
|
+
`Agent ${project.name} has no previous Production Deployment to roll back to. Name one with \`tonbo deployments rollback <id>\`.`
|
|
2201
|
+
);
|
|
2202
|
+
}
|
|
2203
|
+
target = await deps.api.getDeployment(project.id, previousId, token);
|
|
2204
|
+
}
|
|
2205
|
+
const result = await moveProduction(deps, project.id, target, token);
|
|
2206
|
+
deps.output({
|
|
2207
|
+
message: describeProductionMove(project, result, "Rolled back"),
|
|
2208
|
+
project,
|
|
1657
2209
|
...result
|
|
1658
2210
|
});
|
|
1659
2211
|
}
|
|
1660
|
-
async function
|
|
1661
|
-
const
|
|
1662
|
-
const
|
|
1663
|
-
|
|
2212
|
+
async function moveProduction(deps, projectId, target, token) {
|
|
2213
|
+
const previous = await deps.api.getProduction(projectId, token);
|
|
2214
|
+
const production = await deps.api.putProduction(
|
|
2215
|
+
projectId,
|
|
2216
|
+
{
|
|
2217
|
+
deployment_id: target.id,
|
|
2218
|
+
desired_state: "running",
|
|
2219
|
+
expected_generation: previous ? previous.generation : null
|
|
2220
|
+
},
|
|
2221
|
+
token
|
|
2222
|
+
);
|
|
2223
|
+
return {
|
|
2224
|
+
deployment: await deps.api.getDeployment(projectId, target.id, token),
|
|
2225
|
+
previous,
|
|
2226
|
+
production
|
|
2227
|
+
};
|
|
1664
2228
|
}
|
|
1665
|
-
|
|
1666
|
-
const {
|
|
2229
|
+
function describeProductionMove(project, move, verb) {
|
|
2230
|
+
const { deployment, previous } = move;
|
|
2231
|
+
const name = `${deploymentName(deployment)} (${shortDeploymentId(deployment.id)})`;
|
|
2232
|
+
const headline = previous?.deployment_id === deployment.id ? `Deployment ${name} is already Production for agent ${project.name}; requested it to be running.` : `${verb} ${name} to Production for agent ${project.name}${previous ? ` (from ${shortDeploymentId(previous.deployment_id)})` : ""}.`;
|
|
2233
|
+
return `${headline}
|
|
2234
|
+
Production: ${productionStateLabel(deployment)}
|
|
2235
|
+
Application: https://${project.publicHostname}`;
|
|
2236
|
+
}
|
|
2237
|
+
async function projectManagement(deps) {
|
|
2238
|
+
const { oauthToken, project } = await resolveProject(deps);
|
|
1667
2239
|
return {
|
|
1668
2240
|
project,
|
|
1669
2241
|
token: await deps.api.exchangeManagementToken(oauthToken, project.id)
|
|
1670
2242
|
};
|
|
1671
2243
|
}
|
|
1672
|
-
async function secretListCommand(deps
|
|
1673
|
-
const { project, token } = await projectManagement(deps
|
|
2244
|
+
async function secretListCommand(deps) {
|
|
2245
|
+
const { project, token } = await projectManagement(deps);
|
|
1674
2246
|
const secrets = await deps.api.listProjectSecrets(project.id, token);
|
|
1675
2247
|
deps.output({
|
|
1676
|
-
message: secrets.length ? secrets.map((secret) => secret.name).join("\n") : "No
|
|
2248
|
+
message: secrets.length ? secrets.map((secret) => secret.name).join("\n") : "No agent secrets are configured.",
|
|
1677
2249
|
project,
|
|
1678
2250
|
secrets
|
|
1679
2251
|
});
|
|
1680
2252
|
}
|
|
1681
2253
|
async function secretSetCommand(deps, name, options) {
|
|
1682
2254
|
const value = await deps.secretValue(name, options.fromEnv);
|
|
1683
|
-
const { project, token } = await projectManagement(deps
|
|
2255
|
+
const { project, token } = await projectManagement(deps);
|
|
1684
2256
|
const secret = await deps.api.setProjectSecret(project.id, name, value, token);
|
|
1685
2257
|
deps.output({
|
|
1686
|
-
message: `Set
|
|
2258
|
+
message: `Set agent secret ${secret.name}. Redeploy to replace the active runtime with this value.`,
|
|
1687
2259
|
project,
|
|
1688
2260
|
secret
|
|
1689
2261
|
});
|
|
1690
2262
|
}
|
|
1691
|
-
async function secretRemoveCommand(deps, name
|
|
1692
|
-
const { project, token } = await projectManagement(deps
|
|
2263
|
+
async function secretRemoveCommand(deps, name) {
|
|
2264
|
+
const { project, token } = await projectManagement(deps);
|
|
1693
2265
|
await deps.api.deleteProjectSecret(project.id, name, token);
|
|
1694
|
-
deps.output({ message: `Removed
|
|
2266
|
+
deps.output({ message: `Removed agent secret ${name}.`, project, name });
|
|
1695
2267
|
}
|
|
1696
2268
|
|
|
1697
|
-
// src/
|
|
1698
|
-
import {
|
|
2269
|
+
// src/credentials.ts
|
|
2270
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
2271
|
+
import { chmod, lstat as lstat4, mkdir, open as open2, readFile as readFile5, rename as rename2, rm as rm2 } from "node:fs/promises";
|
|
1699
2272
|
import os from "node:os";
|
|
1700
2273
|
import path5 from "node:path";
|
|
1701
|
-
var FileConfigStore = class {
|
|
1702
|
-
constructor(filename = defaultConfigPath()) {
|
|
1703
|
-
this.filename = filename;
|
|
1704
|
-
}
|
|
1705
|
-
async getBinding(declarationRoot) {
|
|
1706
|
-
const config = await this.read();
|
|
1707
|
-
return config.bindings[path5.resolve(declarationRoot)] ?? null;
|
|
1708
|
-
}
|
|
1709
|
-
async setBinding(declarationRoot, binding) {
|
|
1710
|
-
const config = await this.read();
|
|
1711
|
-
config.bindings[path5.resolve(declarationRoot)] = binding;
|
|
1712
|
-
await mkdir(path5.dirname(this.filename), { recursive: true, mode: 448 });
|
|
1713
|
-
await writeFile(this.filename, `${JSON.stringify(config, null, 2)}
|
|
1714
|
-
`, {
|
|
1715
|
-
mode: 384
|
|
1716
|
-
});
|
|
1717
|
-
}
|
|
1718
|
-
async read() {
|
|
1719
|
-
try {
|
|
1720
|
-
const value = JSON.parse(await readFile5(this.filename, "utf8"));
|
|
1721
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error();
|
|
1722
|
-
const bindings = value.bindings;
|
|
1723
|
-
if (!bindings || typeof bindings !== "object" || Array.isArray(bindings)) throw new Error();
|
|
1724
|
-
return { bindings };
|
|
1725
|
-
} catch (error) {
|
|
1726
|
-
if (error.code === "ENOENT") return { bindings: {} };
|
|
1727
|
-
throw new Error(`Could not read Tonbo config at ${this.filename}.`, {
|
|
1728
|
-
cause: error
|
|
1729
|
-
});
|
|
1730
|
-
}
|
|
1731
|
-
}
|
|
1732
|
-
};
|
|
1733
|
-
function defaultConfigPath() {
|
|
1734
|
-
return path5.join(defaultConfigDirectory(), "config.json");
|
|
1735
|
-
}
|
|
1736
2274
|
function defaultConfigDirectory() {
|
|
1737
2275
|
const base = process.env.XDG_CONFIG_HOME || path5.join(os.homedir(), ".config");
|
|
1738
2276
|
return path5.join(base, "tonbo");
|
|
1739
2277
|
}
|
|
1740
|
-
|
|
1741
|
-
// src/credentials.ts
|
|
1742
|
-
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
1743
|
-
import { chmod, lstat as lstat4, mkdir as mkdir2, open as open2, readFile as readFile6, rename as rename2, rm as rm2 } from "node:fs/promises";
|
|
1744
|
-
import path6 from "node:path";
|
|
1745
2278
|
var FileCredentialStore = class {
|
|
1746
|
-
constructor(filename =
|
|
2279
|
+
constructor(filename = path5.join(defaultConfigDirectory(), "credentials.json")) {
|
|
1747
2280
|
this.filename = filename;
|
|
1748
2281
|
}
|
|
1749
2282
|
async load() {
|
|
1750
2283
|
try {
|
|
1751
2284
|
await assertPrivateRegularFile(this.filename);
|
|
1752
|
-
const parsed = JSON.parse(await
|
|
2285
|
+
const parsed = JSON.parse(await readFile5(this.filename, "utf8"));
|
|
1753
2286
|
if (!isOAuthTokenSet(parsed)) throw new Error("invalid token set");
|
|
1754
2287
|
return parsed;
|
|
1755
2288
|
} catch (error) {
|
|
@@ -1759,13 +2292,13 @@ var FileCredentialStore = class {
|
|
|
1759
2292
|
}
|
|
1760
2293
|
async save(tokens) {
|
|
1761
2294
|
if (!isOAuthTokenSet(tokens)) throw new Error("Refusing to store a malformed Tonbo token set.");
|
|
1762
|
-
const directory =
|
|
1763
|
-
await
|
|
2295
|
+
const directory = path5.dirname(this.filename);
|
|
2296
|
+
await mkdir(directory, { mode: 448, recursive: true });
|
|
1764
2297
|
await preparePrivateDirectory(directory);
|
|
1765
2298
|
await assertExistingDestinationIsSafe(this.filename);
|
|
1766
|
-
const temporary =
|
|
2299
|
+
const temporary = path5.join(
|
|
1767
2300
|
directory,
|
|
1768
|
-
`.${
|
|
2301
|
+
`.${path5.basename(this.filename)}.${process.pid}.${randomUUID3()}.tmp`
|
|
1769
2302
|
);
|
|
1770
2303
|
let handle = null;
|
|
1771
2304
|
try {
|
|
@@ -1812,6 +2345,60 @@ async function assertPrivateRegularFile(filename) {
|
|
|
1812
2345
|
throw new Error("Tonbo credential file must have mode 0600.");
|
|
1813
2346
|
}
|
|
1814
2347
|
|
|
2348
|
+
// src/git.ts
|
|
2349
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
2350
|
+
var defaultGitRunner = (args, cwd) => new Promise((resolve, reject) => {
|
|
2351
|
+
execFile2(
|
|
2352
|
+
"git",
|
|
2353
|
+
args,
|
|
2354
|
+
{
|
|
2355
|
+
cwd,
|
|
2356
|
+
encoding: "utf8",
|
|
2357
|
+
// Never take the index lock for a read: a concurrent editor or IDE
|
|
2358
|
+
// must not turn provenance collection into a failure.
|
|
2359
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
|
|
2360
|
+
windowsHide: true
|
|
2361
|
+
},
|
|
2362
|
+
(error, stdout) => error ? reject(new Error(`git ${args.join(" ")} failed.`, { cause: error })) : resolve(stdout)
|
|
2363
|
+
);
|
|
2364
|
+
});
|
|
2365
|
+
var COMMIT_SHA = /^[0-9a-f]{40}$/;
|
|
2366
|
+
var REF_MAX_LENGTH = 255;
|
|
2367
|
+
var SUBJECT_MAX_LENGTH = 512;
|
|
2368
|
+
var AUTHOR_MAX_LENGTH = 255;
|
|
2369
|
+
async function collectGitProvenance(root, run = defaultGitRunner) {
|
|
2370
|
+
const commitSha = await read(run, ["rev-parse", "HEAD"], root);
|
|
2371
|
+
if (!commitSha || !COMMIT_SHA.test(commitSha)) return null;
|
|
2372
|
+
const [ref, subject, author, status] = await Promise.all([
|
|
2373
|
+
read(run, ["rev-parse", "--abbrev-ref", "HEAD"], root),
|
|
2374
|
+
read(run, ["log", "-1", "--format=%s"], root),
|
|
2375
|
+
read(run, ["log", "-1", "--format=%an"], root),
|
|
2376
|
+
// Only the deployed tree matters: changes elsewhere in the repository do
|
|
2377
|
+
// not alter the uploaded bundle.
|
|
2378
|
+
read(run, ["status", "--porcelain", "--", "."], root)
|
|
2379
|
+
]);
|
|
2380
|
+
return {
|
|
2381
|
+
commit_sha: commitSha,
|
|
2382
|
+
ref: ref === "HEAD" ? null : clamp(ref, REF_MAX_LENGTH),
|
|
2383
|
+
subject: clamp(subject, SUBJECT_MAX_LENGTH),
|
|
2384
|
+
author: clamp(author, AUTHOR_MAX_LENGTH),
|
|
2385
|
+
// When the status cannot be read the tree cannot be shown to match the
|
|
2386
|
+
// commit, so the Deployment is recorded as dirty rather than clean.
|
|
2387
|
+
dirty: status === null || status.length > 0
|
|
2388
|
+
};
|
|
2389
|
+
}
|
|
2390
|
+
async function read(run, args, cwd) {
|
|
2391
|
+
try {
|
|
2392
|
+
return (await run(args, cwd)).trim();
|
|
2393
|
+
} catch {
|
|
2394
|
+
return null;
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
function clamp(value, maxLength) {
|
|
2398
|
+
if (!value) return null;
|
|
2399
|
+
return value.length > maxLength ? value.slice(0, maxLength) : value;
|
|
2400
|
+
}
|
|
2401
|
+
|
|
1815
2402
|
// src/progress.ts
|
|
1816
2403
|
var FRAMES = ["|", "/", "-", "\\"];
|
|
1817
2404
|
var TerminalProgress = class {
|
|
@@ -1909,9 +2496,9 @@ function createDependencies(json = false) {
|
|
|
1909
2496
|
return {
|
|
1910
2497
|
api: new TonboApi(fetch, accountOrigin, managementOrigin),
|
|
1911
2498
|
auth: new AuthClient(new FileCredentialStore(), fetch, accountOrigin),
|
|
1912
|
-
config: new FileConfigStore(),
|
|
1913
2499
|
cwd: () => process.cwd(),
|
|
1914
2500
|
defaultSshPublicKeys: readDefaultSshPublicKeys,
|
|
2501
|
+
gitProvenance: collectGitProvenance,
|
|
1915
2502
|
interactive: () => !json && process.stdin.isTTY === true && process.stderr.isTTY === true,
|
|
1916
2503
|
inspectSource: inspectLocalAgentSource,
|
|
1917
2504
|
output: (value) => {
|
|
@@ -1921,54 +2508,115 @@ function createDependencies(json = false) {
|
|
|
1921
2508
|
progress: json ? silentProgress : new TerminalProgress(process.stderr),
|
|
1922
2509
|
prompt: terminalPrompt,
|
|
1923
2510
|
select: terminalSelect,
|
|
1924
|
-
secretValue:
|
|
2511
|
+
secretValue: (name, fromEnvironment) => {
|
|
1925
2512
|
const environmentName = fromEnvironment ?? name;
|
|
1926
2513
|
const value = process.env[environmentName];
|
|
1927
2514
|
if (!value)
|
|
1928
2515
|
throw new Error(
|
|
1929
2516
|
`Environment variable ${environmentName} is empty. Set it before running tonbo secret set.`
|
|
1930
2517
|
);
|
|
1931
|
-
return value;
|
|
2518
|
+
return Promise.resolve(value);
|
|
1932
2519
|
}
|
|
1933
2520
|
};
|
|
1934
2521
|
}
|
|
2522
|
+
function jsonOutput(program) {
|
|
2523
|
+
return program.opts().json === true;
|
|
2524
|
+
}
|
|
1935
2525
|
function createProgram(dependencies = createDependencies) {
|
|
1936
|
-
const program = new Command().name("tonbo").description("Deploy a persistent
|
|
2526
|
+
const program = new Command().name("tonbo").description("Deploy a persistent Agent Agent to Tonbo.").version(packageVersion.version).option("--json", "print machine-readable JSON");
|
|
1937
2527
|
program.command("init").description("interactively create a Tonbo Agent declaration in this directory").option("--harness <harness>", "Agent Harness (currently pi)").option("--model <model>", "inference model").option("--driver <driver>", "PI driver: native or command").option("--agent-entry <file>", "Node entry file for the command driver").option("--build-command <argv...>", "build command argv for the command driver").option("--force", `replace an existing ${DECLARATION_FILENAME}`).action(
|
|
1938
|
-
async (options) => initCommand(dependencies(program
|
|
2528
|
+
async (options) => initCommand(dependencies(jsonOutput(program)), options)
|
|
1939
2529
|
);
|
|
1940
|
-
program.command("login").description("sign in through the browser and store the session in the user config").action(async () => loginCommand(dependencies(program
|
|
1941
|
-
const
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
2530
|
+
program.command("login").description("sign in through the browser and store the session in the user config").action(async () => loginCommand(dependencies(jsonOutput(program))));
|
|
2531
|
+
const machine = program.command("machine").description("manage independent account-owned Machines");
|
|
2532
|
+
machine.command("list").option("--account <account>", "account UUID").action(async (options) => {
|
|
2533
|
+
const deps = dependencies(jsonOutput(program));
|
|
2534
|
+
deps.output(
|
|
2535
|
+
await deps.api.machineRequest(
|
|
2536
|
+
await deps.auth.accessToken(),
|
|
2537
|
+
options.account ? `?account=${encodeURIComponent(options.account)}` : ""
|
|
2538
|
+
)
|
|
2539
|
+
);
|
|
2540
|
+
});
|
|
2541
|
+
machine.command("allocate").requiredOption("--account <account>", "owning account UUID").requiredOption("--region <region>", "compute region (currently us-east-1)").action(async (options) => {
|
|
2542
|
+
const deps = dependencies(jsonOutput(program));
|
|
2543
|
+
deps.output(
|
|
2544
|
+
await deps.api.machineRequest(await deps.auth.accessToken(), "", "POST", {
|
|
2545
|
+
accountId: options.account,
|
|
2546
|
+
region: options.region
|
|
2547
|
+
})
|
|
2548
|
+
);
|
|
2549
|
+
});
|
|
2550
|
+
for (const operation of ["show", "metrics", "release"]) {
|
|
2551
|
+
machine.command(`${operation} <machine>`).action(async (id) => {
|
|
2552
|
+
const deps = dependencies(jsonOutput(program));
|
|
2553
|
+
const result = await deps.api.machineRequest(
|
|
2554
|
+
await deps.auth.accessToken(),
|
|
2555
|
+
`/${encodeURIComponent(id)}`,
|
|
2556
|
+
operation === "release" ? "DELETE" : "GET"
|
|
2557
|
+
);
|
|
2558
|
+
deps.output(operation === "metrics" ? result.metrics : result);
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
machine.command("bind <machine>").requiredOption("--agent <agent>", "Agent UUID").action(async (id, options) => {
|
|
2562
|
+
const deps = dependencies(jsonOutput(program));
|
|
2563
|
+
deps.output(
|
|
2564
|
+
await deps.api.machineRequest(
|
|
2565
|
+
await deps.auth.accessToken(),
|
|
2566
|
+
`/${encodeURIComponent(id)}/binding`,
|
|
2567
|
+
"POST",
|
|
2568
|
+
{ agentId: options.agent }
|
|
2569
|
+
)
|
|
2570
|
+
);
|
|
2571
|
+
});
|
|
2572
|
+
machine.command("unbind <machine>").description("detach Agent and reset system disk; retain Agent Workspace and Sessions").action(async (id) => {
|
|
2573
|
+
const deps = dependencies(jsonOutput(program));
|
|
2574
|
+
const token = await deps.auth.accessToken();
|
|
2575
|
+
const path6 = `/${encodeURIComponent(id)}`;
|
|
2576
|
+
const current = await deps.api.machineRequest(token, path6);
|
|
2577
|
+
if (!current.binding) throw new Error("Machine is not bound to an Agent.");
|
|
2578
|
+
deps.output(
|
|
2579
|
+
await deps.api.machineRequest(token, `${path6}/binding`, "DELETE", {
|
|
2580
|
+
agentId: current.binding.agent_id,
|
|
2581
|
+
bindingId: current.binding.id,
|
|
2582
|
+
generation: current.binding.generation
|
|
2583
|
+
})
|
|
2584
|
+
);
|
|
2585
|
+
});
|
|
2586
|
+
const project = program.command("agent").description("manage the agent bound to this Agent directory");
|
|
2587
|
+
project.command("create <name>").description("create and bind a agent to this Agent directory").action(async (name) => projectCreateCommand(dependencies(jsonOutput(program)), name));
|
|
2588
|
+
const sshKey = program.command("ssh-key").description("manage public keys used by native agent SSH");
|
|
2589
|
+
sshKey.command("add <public-key>").description("register an OpenSSH public key with the current Tonbo account").action(async (path6) => sshKeyAddCommand(dependencies(jsonOutput(program)), path6));
|
|
1947
2590
|
sshKey.command("remove <fingerprint>").description("revoke an SSH public key from the current Tonbo account").action(
|
|
1948
|
-
async (fingerprint) => sshKeyRemoveCommand(dependencies(program
|
|
2591
|
+
async (fingerprint) => sshKeyRemoveCommand(dependencies(jsonOutput(program)), fingerprint)
|
|
1949
2592
|
);
|
|
1950
|
-
project.command("use <
|
|
1951
|
-
async (selector) => projectUseCommand(dependencies(program
|
|
2593
|
+
project.command("use <agent>").description("write a agent ID into this Agent's .tonbo declaration").option("--force", "replace an existing agent binding without confirmation").action(
|
|
2594
|
+
async (selector, options) => projectUseCommand(dependencies(jsonOutput(program)), selector, options.force === true)
|
|
1952
2595
|
);
|
|
1953
|
-
|
|
1954
|
-
|
|
2596
|
+
project.command("show").description("show the agent bound to this Agent directory and its connection addresses").action(async () => projectShowCommand(dependencies(jsonOutput(program))));
|
|
2597
|
+
program.command("deploy").description("upload this directory as a new Deployment and promote it to Production").option("--no-promote", "create the Deployment without moving Production").action(
|
|
2598
|
+
async (options) => deployCommand(dependencies(jsonOutput(program)), { promote: options.promote })
|
|
1955
2599
|
);
|
|
1956
|
-
program.command("
|
|
1957
|
-
|
|
2600
|
+
const deployments = program.command("deployments").description("list and move between the immutable Deployments of the bound agent");
|
|
2601
|
+
deployments.command("list", { isDefault: true }).description("list Deployments, newest first").action(async () => deploymentsListCommand(dependencies(jsonOutput(program))));
|
|
2602
|
+
deployments.command("show <deployment>").description("show one Deployment and its Rollouts by id prefix").action(
|
|
2603
|
+
async (prefix) => deploymentsShowCommand(dependencies(jsonOutput(program)), prefix)
|
|
1958
2604
|
);
|
|
1959
|
-
|
|
1960
|
-
async (
|
|
2605
|
+
deployments.command("promote <deployment>").description("move Production to a Deployment by id prefix").action(
|
|
2606
|
+
async (prefix) => deploymentsPromoteCommand(dependencies(jsonOutput(program)), prefix)
|
|
1961
2607
|
);
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
async (options) => secretListCommand(dependencies(program.opts().json), options.project)
|
|
2608
|
+
deployments.command("rollback [deployment]").description("move Production back to the previous Production Deployment, or to a named one").action(
|
|
2609
|
+
async (prefix) => deploymentsRollbackCommand(dependencies(jsonOutput(program)), prefix)
|
|
1965
2610
|
);
|
|
1966
|
-
|
|
1967
|
-
async (
|
|
2611
|
+
program.command("run <prompt>").description("run one prompt in a durable agent session").option("--session <session>", "resume an existing Agent Session UUID").action(
|
|
2612
|
+
async (prompt, options) => runCommand(dependencies(jsonOutput(program)), prompt, options)
|
|
1968
2613
|
);
|
|
1969
|
-
secret.command("
|
|
1970
|
-
|
|
2614
|
+
const secret = program.command("secret").description("manage encrypted environment secrets for the agent service");
|
|
2615
|
+
secret.command("list").action(async () => secretListCommand(dependencies(jsonOutput(program))));
|
|
2616
|
+
secret.command("set <name>").description("set a secret from an environment variable (the same name by default)").option("--from-env <name>", "read the value from another environment variable").action(
|
|
2617
|
+
async (name, options) => secretSetCommand(dependencies(jsonOutput(program)), name, options)
|
|
1971
2618
|
);
|
|
2619
|
+
secret.command("remove <name>").action(async (name) => secretRemoveCommand(dependencies(jsonOutput(program)), name));
|
|
1972
2620
|
return program;
|
|
1973
2621
|
}
|
|
1974
2622
|
|