@treeseed/cli 0.10.6 → 0.10.7
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/dist/cli/handlers/dev.js +2 -2
- package/dist/cli/handlers/projects.js +391 -36
- package/dist/cli/operations-registry.js +39 -17
- package/package.json +3 -3
package/dist/cli/handlers/dev.js
CHANGED
|
@@ -44,11 +44,11 @@ function resolveCoreDevEntrypoint(cwd) {
|
|
|
44
44
|
const handleDev = async (invocation, context) => {
|
|
45
45
|
try {
|
|
46
46
|
if (invocation.commandName !== "dev") {
|
|
47
|
-
return fail("`trsd dev`
|
|
47
|
+
return fail("`trsd dev` starts the Market web/API/dev-runner runtime. Use `trsd capacity ...` for capacity provider lifecycle commands.");
|
|
48
48
|
}
|
|
49
49
|
const removedOptions = ["surface", "surfaces", "withWorker"].filter((name) => invocation.args[name] !== void 0);
|
|
50
50
|
if (removedOptions.length > 0) {
|
|
51
|
-
return fail(`\`trsd dev\` no longer accepts ${removedOptions.map((name) => `--${name.replace(/[A-Z]/gu, (char) => `-${char.toLowerCase()}`)}`).join(", ")}. It always starts fixed Market web/API surfaces; use \`trsd capacity ...\` for providers.`);
|
|
51
|
+
return fail(`\`trsd dev\` no longer accepts ${removedOptions.map((name) => `--${name.replace(/[A-Z]/gu, (char) => `-${char.toLowerCase()}`)}`).join(", ")}. It always starts fixed Market web/API/dev-runner surfaces; use \`trsd capacity ...\` for providers.`);
|
|
52
52
|
}
|
|
53
53
|
const feedback = typeof invocation.args.feedback === "string" ? invocation.args.feedback : void 0;
|
|
54
54
|
const watch = feedback !== "off";
|
|
@@ -1,47 +1,402 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { MarketApiError } from "@treeseed/sdk/market-client";
|
|
2
|
+
import { fail, guidedResult } from "./utils.js";
|
|
2
3
|
import { createMarketClientForInvocation } from "./market-utils.js";
|
|
4
|
+
const DEPLOYMENT_TERMINAL_STATUSES = /* @__PURE__ */ new Set(["succeeded", "failed", "cancelled", "timed_out"]);
|
|
5
|
+
const FORBIDDEN_OUTPUT_FIELDS = /* @__PURE__ */ new Set([
|
|
6
|
+
"capacityProviderId",
|
|
7
|
+
"laneId",
|
|
8
|
+
"grantId",
|
|
9
|
+
"workerPoolId",
|
|
10
|
+
"runtimeHostId",
|
|
11
|
+
"railwayServiceId",
|
|
12
|
+
"runnerToken"
|
|
13
|
+
]);
|
|
14
|
+
const ACTIONS = {
|
|
15
|
+
deploy: "deploy_web",
|
|
16
|
+
publish: "publish_content",
|
|
17
|
+
monitor: "monitor"
|
|
18
|
+
};
|
|
19
|
+
function stringArg(invocation, key) {
|
|
20
|
+
const value = invocation.args[key];
|
|
21
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
22
|
+
}
|
|
23
|
+
function boolArg(invocation, key) {
|
|
24
|
+
return invocation.args[key] === true;
|
|
25
|
+
}
|
|
26
|
+
function environmentArg(invocation) {
|
|
27
|
+
const value = stringArg(invocation, "environment") ?? "staging";
|
|
28
|
+
return value === "prod" ? "prod" : "staging";
|
|
29
|
+
}
|
|
30
|
+
function projectUsage(action) {
|
|
31
|
+
switch (action) {
|
|
32
|
+
case "deploy":
|
|
33
|
+
return "Usage: treeseed projects deploy <project-id> --environment staging|prod";
|
|
34
|
+
case "publish":
|
|
35
|
+
return "Usage: treeseed projects publish <project-id> --environment staging|prod";
|
|
36
|
+
case "monitor":
|
|
37
|
+
return "Usage: treeseed projects monitor <project-id> --environment staging|prod";
|
|
38
|
+
case "deployments":
|
|
39
|
+
return "Usage: treeseed projects deployments <project-id>";
|
|
40
|
+
case "deployment":
|
|
41
|
+
return "Usage: treeseed projects deployment <project-id> <deployment-id>";
|
|
42
|
+
default:
|
|
43
|
+
return "Usage: treeseed projects [list|access|deploy|publish|monitor|deployments|deployment]";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function authFailure(error) {
|
|
47
|
+
if (error instanceof MarketApiError && [401, 403].includes(error.status)) {
|
|
48
|
+
return fail(error.message, 2);
|
|
49
|
+
}
|
|
50
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
51
|
+
if (/not logged in|unauthori[sz]ed|forbidden/iu.test(message)) {
|
|
52
|
+
return fail(message, 2);
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
function deploymentApiExitCode(error) {
|
|
57
|
+
if (error instanceof MarketApiError) {
|
|
58
|
+
if ([401, 403].includes(error.status)) return 2;
|
|
59
|
+
const payload = error.payload;
|
|
60
|
+
const code = payload?.error?.code ?? payload?.code;
|
|
61
|
+
if (code === "operation_not_retryable" || code === "operation_not_cancellable") return 1;
|
|
62
|
+
}
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
function redact(value) {
|
|
66
|
+
if (Array.isArray(value)) return value.map((item) => redact(item));
|
|
67
|
+
if (!value || typeof value !== "object") return value;
|
|
68
|
+
return Object.fromEntries(Object.entries(value).filter(([key]) => !FORBIDDEN_OUTPUT_FIELDS.has(key)).filter(([key]) => !/(?:secret|token|password|apiKey|privateKey)/iu.test(key)).map(([key, entry]) => [key, redact(entry)]));
|
|
69
|
+
}
|
|
70
|
+
function text(value, fallback = "") {
|
|
71
|
+
return typeof value === "string" && value.trim() ? value.trim() : fallback;
|
|
72
|
+
}
|
|
73
|
+
function actionLabel(action) {
|
|
74
|
+
switch (action) {
|
|
75
|
+
case "deploy_web":
|
|
76
|
+
return "deploy_web";
|
|
77
|
+
case "publish_content":
|
|
78
|
+
return "publish_content";
|
|
79
|
+
case "monitor":
|
|
80
|
+
return "monitor";
|
|
81
|
+
default:
|
|
82
|
+
return text(action, "deployment");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function deploymentUrl(deployment) {
|
|
86
|
+
return text(deployment?.target?.url, text(deployment?.target?.previewUrl, ""));
|
|
87
|
+
}
|
|
88
|
+
function workflowUrl(deployment) {
|
|
89
|
+
return text(deployment?.externalWorkflow?.url, text(deployment?.externalWorkflow?.htmlUrl, text(deployment?.externalWorkflow?.runUrl, "")));
|
|
90
|
+
}
|
|
91
|
+
function inspectCommand(projectId, deploymentId) {
|
|
92
|
+
return `trsd projects deployment ${projectId} ${deploymentId}`;
|
|
93
|
+
}
|
|
94
|
+
function retryCommand(projectId, deploymentId) {
|
|
95
|
+
return `trsd projects deployment retry ${projectId} ${deploymentId}`;
|
|
96
|
+
}
|
|
97
|
+
function deploymentLine(deployment) {
|
|
98
|
+
return [
|
|
99
|
+
deployment.id,
|
|
100
|
+
deployment.environment,
|
|
101
|
+
actionLabel(deployment.action),
|
|
102
|
+
deployment.status,
|
|
103
|
+
deployment.monitor?.status ? `monitor=${deployment.monitor.status}` : "",
|
|
104
|
+
deployment.completedAt ?? deployment.finishedAt ?? deployment.updatedAt ?? "",
|
|
105
|
+
workflowUrl(deployment),
|
|
106
|
+
deploymentUrl(deployment)
|
|
107
|
+
].filter(Boolean).join(" ");
|
|
108
|
+
}
|
|
109
|
+
function waitExitCode(status) {
|
|
110
|
+
if (status === "succeeded") return 0;
|
|
111
|
+
if (status === "timed_out") return 4;
|
|
112
|
+
if (status === "cancelled") return 5;
|
|
113
|
+
return 3;
|
|
114
|
+
}
|
|
115
|
+
function monitorExitCode(deployment, fallback) {
|
|
116
|
+
if (deployment?.monitor?.status === "failed") return 3;
|
|
117
|
+
if (["healthy", "degraded", "unknown"].includes(String(deployment?.monitor?.status))) return 0;
|
|
118
|
+
return fallback;
|
|
119
|
+
}
|
|
120
|
+
function delay(ms) {
|
|
121
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
122
|
+
}
|
|
123
|
+
async function waitForDeployment(input) {
|
|
124
|
+
const started = Date.now();
|
|
125
|
+
let current = (await input.client.projectDeployment(input.projectId, input.deploymentId)).payload;
|
|
126
|
+
while (!DEPLOYMENT_TERMINAL_STATUSES.has(String(current.status))) {
|
|
127
|
+
if (Date.now() - started >= input.timeoutSeconds * 1e3) {
|
|
128
|
+
return {
|
|
129
|
+
exitCode: 4,
|
|
130
|
+
deployment: current,
|
|
131
|
+
timedOut: true
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
await delay(input.pollIntervalMs);
|
|
135
|
+
current = (await input.client.projectDeployment(input.projectId, input.deploymentId)).payload;
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
exitCode: waitExitCode(String(current.status)),
|
|
139
|
+
deployment: current,
|
|
140
|
+
timedOut: false
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function timeoutSeconds(invocation) {
|
|
144
|
+
const value = Number(stringArg(invocation, "timeoutSeconds") ?? 300);
|
|
145
|
+
return Number.isFinite(value) && value > 0 ? value : 300;
|
|
146
|
+
}
|
|
147
|
+
function pollIntervalMs(invocation) {
|
|
148
|
+
const value = Number(stringArg(invocation, "pollIntervalMs") ?? 1e3);
|
|
149
|
+
return Number.isFinite(value) && value > 0 ? value : 1e3;
|
|
150
|
+
}
|
|
151
|
+
function deploymentRequestBody(invocation, action, environment) {
|
|
152
|
+
const body = {
|
|
153
|
+
environment,
|
|
154
|
+
action,
|
|
155
|
+
source: "cli"
|
|
156
|
+
};
|
|
157
|
+
const reason = stringArg(invocation, "reason");
|
|
158
|
+
const idempotencyKey = stringArg(invocation, "idempotencyKey");
|
|
159
|
+
if (boolArg(invocation, "dryRun")) body.dryRun = true;
|
|
160
|
+
if (reason) body.reason = reason;
|
|
161
|
+
if (idempotencyKey) body.idempotencyKey = idempotencyKey;
|
|
162
|
+
if (environment === "prod" && action !== "monitor") body.confirmProduction = true;
|
|
163
|
+
return body;
|
|
164
|
+
}
|
|
165
|
+
function monitorFacts(deployment) {
|
|
166
|
+
const monitor = deployment?.monitor && typeof deployment.monitor === "object" ? deployment.monitor : null;
|
|
167
|
+
if (!monitor) return [];
|
|
168
|
+
return [
|
|
169
|
+
{ label: "Monitor", value: monitor.status ?? null },
|
|
170
|
+
{ label: "Checked", value: monitor.checkedAt ?? null },
|
|
171
|
+
{ label: "Checks", value: Array.isArray(monitor.checks) ? `${monitor.checks.filter((check) => check.status === "passed").length} passed, ${monitor.checks.filter((check) => check.status === "warning").length} warnings, ${monitor.checks.filter((check) => check.status === "failed").length} failed` : null }
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
function monitorSection(deployment) {
|
|
175
|
+
const checks = Array.isArray(deployment?.monitor?.checks) ? deployment.monitor.checks : [];
|
|
176
|
+
if (checks.length === 0) return [];
|
|
177
|
+
return [{
|
|
178
|
+
title: "Monitor checks",
|
|
179
|
+
lines: checks.map((check) => [
|
|
180
|
+
check.status ?? "skipped",
|
|
181
|
+
check.key ?? check.label ?? "check",
|
|
182
|
+
check.summary ?? "",
|
|
183
|
+
check.inspectCommand ?? check.url ?? ""
|
|
184
|
+
].filter(Boolean).join(" "))
|
|
185
|
+
}];
|
|
186
|
+
}
|
|
3
187
|
const handleProjects = async (invocation, context) => {
|
|
4
188
|
const action = invocation.positionals[0] ?? "list";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return
|
|
10
|
-
command: "projects",
|
|
11
|
-
summary: "Treeseed market projects",
|
|
12
|
-
sections: [{
|
|
13
|
-
title: "Projects",
|
|
14
|
-
lines: response.payload.map((project) => `${project.id} ${project.name ?? project.slug} team=${project.teamId}`)
|
|
15
|
-
}],
|
|
16
|
-
report: { marketId: profile.id, teamId, projects: response.payload }
|
|
17
|
-
});
|
|
189
|
+
let market;
|
|
190
|
+
try {
|
|
191
|
+
market = createMarketClientForInvocation(invocation, context, { requireAuth: true });
|
|
192
|
+
} catch (error) {
|
|
193
|
+
return authFailure(error) ?? fail(error instanceof Error ? error.message : String(error), 1);
|
|
18
194
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (
|
|
22
|
-
|
|
195
|
+
const { profile, client } = market;
|
|
196
|
+
try {
|
|
197
|
+
if (action === "list") {
|
|
198
|
+
const teamId = typeof invocation.args.team === "string" ? invocation.args.team : null;
|
|
199
|
+
const response = await client.projects(teamId);
|
|
200
|
+
return guidedResult({
|
|
201
|
+
command: "projects",
|
|
202
|
+
summary: "Treeseed market projects",
|
|
203
|
+
sections: [{
|
|
204
|
+
title: "Projects",
|
|
205
|
+
lines: response.payload.map((project) => `${project.id} ${project.name ?? project.slug} team=${project.teamId}`)
|
|
206
|
+
}],
|
|
207
|
+
report: { marketId: profile.id, teamId, projects: redact(response.payload) }
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if (action === "access") {
|
|
211
|
+
const projectId = invocation.positionals[1];
|
|
212
|
+
if (!projectId) return fail(projectUsage(action));
|
|
213
|
+
const response = await client.projectAccess(projectId);
|
|
214
|
+
return guidedResult({
|
|
215
|
+
command: "projects",
|
|
216
|
+
summary: "Treeseed market project access",
|
|
217
|
+
facts: [
|
|
218
|
+
{ label: "Project", value: response.payload.projectId },
|
|
219
|
+
{ label: "Staging admin", value: response.payload.team.summary.canAdminStaging },
|
|
220
|
+
{ label: "Production admin", value: response.payload.team.summary.canAdminProduction }
|
|
221
|
+
],
|
|
222
|
+
sections: [{
|
|
223
|
+
title: "Environments",
|
|
224
|
+
lines: response.payload.environments.map((entry) => `${entry.environment}: ${entry.role}`)
|
|
225
|
+
}],
|
|
226
|
+
report: { marketId: profile.id, access: redact(response.payload) }
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
if (action === "connect") {
|
|
230
|
+
return fail("Use treeseed config --connect-market --market-project-id <project-id> for project pairing.");
|
|
231
|
+
}
|
|
232
|
+
if (action in ACTIONS) {
|
|
233
|
+
const projectId = invocation.positionals[1];
|
|
234
|
+
if (!projectId) return fail(projectUsage(action));
|
|
235
|
+
const environment = environmentArg(invocation);
|
|
236
|
+
const deploymentAction = ACTIONS[action];
|
|
237
|
+
if (environment === "prod" && deploymentAction !== "monitor" && !boolArg(invocation, "yes")) {
|
|
238
|
+
return fail(`Production ${action} requires --yes and was not queued.`);
|
|
239
|
+
}
|
|
240
|
+
const response = await client.createProjectWebDeployment(projectId, deploymentRequestBody(invocation, deploymentAction, environment));
|
|
241
|
+
let deployment = response.deployment;
|
|
242
|
+
let waitResult = null;
|
|
243
|
+
if (boolArg(invocation, "wait")) {
|
|
244
|
+
waitResult = await waitForDeployment({
|
|
245
|
+
client,
|
|
246
|
+
projectId,
|
|
247
|
+
deploymentId: deployment.id,
|
|
248
|
+
timeoutSeconds: timeoutSeconds(invocation),
|
|
249
|
+
pollIntervalMs: pollIntervalMs(invocation)
|
|
250
|
+
});
|
|
251
|
+
deployment = waitResult.deployment;
|
|
252
|
+
}
|
|
253
|
+
const exitCode = monitorExitCode(deployment, waitResult?.exitCode ?? 0);
|
|
254
|
+
const summary = waitResult ? waitResult.timedOut ? "Treeseed project deployment wait timed out" : deployment.status === "succeeded" ? "Treeseed project deployment completed" : `Treeseed project deployment ${deployment.status}` : "Treeseed project deployment queued";
|
|
255
|
+
const nextSteps = [
|
|
256
|
+
inspectCommand(projectId, deployment.id),
|
|
257
|
+
...["failed", "timed_out", "cancelled"].includes(deployment.status) ? [retryCommand(projectId, deployment.id)] : []
|
|
258
|
+
];
|
|
259
|
+
return guidedResult({
|
|
260
|
+
command: "projects",
|
|
261
|
+
summary,
|
|
262
|
+
exitCode,
|
|
263
|
+
facts: [
|
|
264
|
+
{ label: "Project", value: projectId },
|
|
265
|
+
{ label: "Environment", value: deployment.environment },
|
|
266
|
+
{ label: "Action", value: deployment.action },
|
|
267
|
+
{ label: "Deployment", value: deployment.id },
|
|
268
|
+
{ label: "Operation", value: deployment.platformOperationId ?? response.operation?.id ?? null },
|
|
269
|
+
{ label: "Status", value: deployment.status },
|
|
270
|
+
{ label: "URL", value: deploymentUrl(deployment) || null },
|
|
271
|
+
{ label: "Workflow", value: workflowUrl(deployment) || null },
|
|
272
|
+
...monitorFacts(deployment)
|
|
273
|
+
],
|
|
274
|
+
sections: monitorSection(deployment),
|
|
275
|
+
nextSteps,
|
|
276
|
+
report: {
|
|
277
|
+
marketId: profile.id,
|
|
278
|
+
projectId,
|
|
279
|
+
deployment: redact(deployment),
|
|
280
|
+
operation: redact(response.operation),
|
|
281
|
+
pollUrl: response.pollUrl,
|
|
282
|
+
eventsUrl: response.eventsUrl,
|
|
283
|
+
stateUrl: response.stateUrl,
|
|
284
|
+
wait: waitResult ? { timedOut: waitResult.timedOut, exitCode } : null
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
if (action === "deployments") {
|
|
289
|
+
const projectId = invocation.positionals[1];
|
|
290
|
+
if (!projectId) return fail(projectUsage(action));
|
|
291
|
+
const response = await client.projectDeployments(projectId, {
|
|
292
|
+
environment: stringArg(invocation, "environment"),
|
|
293
|
+
limit: stringArg(invocation, "limit")
|
|
294
|
+
});
|
|
295
|
+
return guidedResult({
|
|
296
|
+
command: "projects",
|
|
297
|
+
summary: "Treeseed project deployments",
|
|
298
|
+
sections: [{
|
|
299
|
+
title: "Deployments",
|
|
300
|
+
lines: response.payload.map(deploymentLine)
|
|
301
|
+
}],
|
|
302
|
+
report: { marketId: profile.id, projectId, deployments: redact(response.payload) }
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
if (action === "deployment") {
|
|
306
|
+
const subaction = invocation.positionals[1];
|
|
307
|
+
const projectId = ["retry", "resume", "cancel"].includes(String(subaction)) ? invocation.positionals[2] : invocation.positionals[1];
|
|
308
|
+
const deploymentId = ["retry", "resume", "cancel"].includes(String(subaction)) ? invocation.positionals[3] : invocation.positionals[2];
|
|
309
|
+
if (!projectId || !deploymentId) return fail(projectUsage(action));
|
|
310
|
+
if (subaction === "retry") {
|
|
311
|
+
const response = await client.retryProjectDeployment(projectId, deploymentId, {
|
|
312
|
+
...stringArg(invocation, "idempotencyKey") ? { idempotencyKey: stringArg(invocation, "idempotencyKey") } : {}
|
|
313
|
+
});
|
|
314
|
+
return guidedResult({
|
|
315
|
+
command: "projects",
|
|
316
|
+
summary: "Treeseed project deployment retry queued",
|
|
317
|
+
facts: [
|
|
318
|
+
{ label: "Original deployment", value: response.originalDeployment.id },
|
|
319
|
+
{ label: "Retry deployment", value: response.retryDeployment.id },
|
|
320
|
+
{ label: "Operation", value: response.operation?.id ?? response.retryDeployment.platformOperationId },
|
|
321
|
+
{ label: "Status", value: response.retryDeployment.status }
|
|
322
|
+
],
|
|
323
|
+
nextSteps: [inspectCommand(projectId, response.retryDeployment.id)],
|
|
324
|
+
report: { marketId: profile.id, projectId, originalDeployment: redact(response.originalDeployment), retryDeployment: redact(response.retryDeployment), operation: redact(response.operation) }
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
if (subaction === "resume") {
|
|
328
|
+
try {
|
|
329
|
+
const response = await client.resumeProjectDeployment(projectId, deploymentId);
|
|
330
|
+
return guidedResult({
|
|
331
|
+
command: "projects",
|
|
332
|
+
summary: "Treeseed project deployment resume queued",
|
|
333
|
+
report: { marketId: profile.id, projectId, response: redact(response) }
|
|
334
|
+
});
|
|
335
|
+
} catch (error) {
|
|
336
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
337
|
+
return guidedResult({
|
|
338
|
+
command: "projects",
|
|
339
|
+
summary: message,
|
|
340
|
+
exitCode: deploymentApiExitCode(error),
|
|
341
|
+
stderr: [message],
|
|
342
|
+
report: { marketId: profile.id, projectId, deploymentId, ok: false, error: message }
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (subaction === "cancel") {
|
|
347
|
+
const response = await client.cancelProjectDeployment(projectId, deploymentId);
|
|
348
|
+
const exitCode = response.deployment.status === "cancelled" ? 5 : 0;
|
|
349
|
+
return guidedResult({
|
|
350
|
+
command: "projects",
|
|
351
|
+
summary: response.deployment.status === "cancelled" ? "Treeseed project deployment cancelled" : "Treeseed project deployment cancellation requested",
|
|
352
|
+
exitCode,
|
|
353
|
+
facts: [
|
|
354
|
+
{ label: "Deployment", value: response.deployment.id },
|
|
355
|
+
{ label: "Status", value: response.deployment.status },
|
|
356
|
+
{ label: "Cancellation", value: response.cancellation }
|
|
357
|
+
],
|
|
358
|
+
report: { marketId: profile.id, projectId, deployment: redact(response.deployment), cancellation: response.cancellation }
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
const [deploymentResponse, eventsResponse] = await Promise.all([
|
|
362
|
+
client.projectDeployment(projectId, deploymentId),
|
|
363
|
+
client.projectDeploymentEvents(projectId, deploymentId)
|
|
364
|
+
]);
|
|
365
|
+
const deployment = deploymentResponse.payload;
|
|
366
|
+
return guidedResult({
|
|
367
|
+
command: "projects",
|
|
368
|
+
summary: "Treeseed project deployment",
|
|
369
|
+
facts: [
|
|
370
|
+
{ label: "Project", value: projectId },
|
|
371
|
+
{ label: "Deployment", value: deployment.id },
|
|
372
|
+
{ label: "Environment", value: deployment.environment },
|
|
373
|
+
{ label: "Action", value: deployment.action },
|
|
374
|
+
{ label: "Status", value: deployment.status },
|
|
375
|
+
{ label: "URL", value: deploymentUrl(deployment) || null },
|
|
376
|
+
{ label: "Workflow", value: workflowUrl(deployment) || null },
|
|
377
|
+
...monitorFacts(deployment)
|
|
378
|
+
],
|
|
379
|
+
sections: [{
|
|
380
|
+
title: "Events",
|
|
381
|
+
lines: eventsResponse.payload.map((event) => `${event.sequence} ${event.kind} ${event.status ?? ""} ${event.message}`)
|
|
382
|
+
}, ...monitorSection(deployment)],
|
|
383
|
+
nextSteps: ["failed", "timed_out", "cancelled"].includes(deployment.status) ? [retryCommand(projectId, deployment.id)] : [],
|
|
384
|
+
report: { marketId: profile.id, projectId, deployment: redact(deployment), events: redact(eventsResponse.payload) }
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
return fail(`Unknown projects action: ${action}`);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
const auth = authFailure(error);
|
|
390
|
+
if (auth) return auth;
|
|
391
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
23
392
|
return guidedResult({
|
|
24
393
|
command: "projects",
|
|
25
|
-
summary:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
{ label: "Production admin", value: response.payload.team.summary.canAdminProduction }
|
|
30
|
-
],
|
|
31
|
-
sections: [{
|
|
32
|
-
title: "Environments",
|
|
33
|
-
lines: response.payload.environments.map((entry) => `${entry.environment}: ${entry.role}`)
|
|
34
|
-
}],
|
|
35
|
-
report: { marketId: profile.id, access: response.payload }
|
|
394
|
+
summary: message,
|
|
395
|
+
exitCode: deploymentApiExitCode(error),
|
|
396
|
+
stderr: [message],
|
|
397
|
+
report: { marketId: profile.id, ok: false, error: message }
|
|
36
398
|
});
|
|
37
399
|
}
|
|
38
|
-
if (action === "connect") {
|
|
39
|
-
return {
|
|
40
|
-
exitCode: 1,
|
|
41
|
-
stderr: ["Use treeseed config --connect-market --market-project-id <project-id> for project pairing."]
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
return { exitCode: 1, stderr: [`Unknown projects action: ${action}`] };
|
|
45
400
|
};
|
|
46
401
|
export {
|
|
47
402
|
handleProjects
|
|
@@ -1168,29 +1168,30 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
|
|
|
1168
1168
|
examples: ["treeseed dev", "treeseed dev --reset", "treeseed dev --reset --plan --json", "treeseed dev --web-runtime local --plan --json", "treeseed dev --port 4322"],
|
|
1169
1169
|
help: {
|
|
1170
1170
|
longSummary: [
|
|
1171
|
-
"Dev starts the local Treeseed Market web/API
|
|
1171
|
+
"Dev starts the local Treeseed Market web/API/runtime services as a foreground supervisor.",
|
|
1172
1172
|
"Capacity provider lifecycle is package-owned and runs through `treeseed capacity ...`, not through `treeseed dev`."
|
|
1173
1173
|
],
|
|
1174
1174
|
beforeYouRun: [
|
|
1175
1175
|
"Run from the tenant or workspace root you want to develop.",
|
|
1176
|
-
"
|
|
1177
|
-
"Use `--
|
|
1176
|
+
"From the Market repo root, dev automatically starts the local Market API, managed local PostgreSQL, and Market operations runner alongside the web UI.",
|
|
1177
|
+
"Use `--plan --json` when you want to inspect fixed web/API/runner commands, setup steps, readiness checks, watched paths, and restart policy without starting services.",
|
|
1178
|
+
"Use `--reset` when you want a fresh local D1 database, Market PostgreSQL state, Mailpit inbox, generated worker bundle, and Wrangler temp output without deleting configuration.",
|
|
1178
1179
|
"Dev prints the local web URL after readiness; it does not open a browser unless you pass `--open on` or `--open auto`.",
|
|
1179
1180
|
"Keep the foreground process running while you test. Press Ctrl+C to stop the supervised stack and free the local ports."
|
|
1180
1181
|
],
|
|
1181
1182
|
examples: [
|
|
1182
|
-
example("treeseed dev", "Start local Market development", "Run web
|
|
1183
|
-
example("treeseed dev --reset", "Start from a fresh local runtime", "Clear disposable local dev state, rerun setup and
|
|
1183
|
+
example("treeseed dev", "Start local Market development", "Run web, API, managed PostgreSQL setup, and the Market operations runner locally."),
|
|
1184
|
+
example("treeseed dev --reset", "Start from a fresh local runtime", "Clear disposable local dev state, rerun setup and database migrations, then start the dev supervisor."),
|
|
1184
1185
|
example("treeseed dev --reset --plan --json", "Inspect reset actions", "Emit the reset, setup, readiness, command, and watch plan without deleting local state or starting services."),
|
|
1185
1186
|
example("treeseed dev --plan --json", "Inspect the runtime plan", "Emit a structured plan with setup steps, commands, ports, URLs, readiness checks, and watch entries."),
|
|
1186
|
-
example("treeseed dev --web-runtime local --plan --json", "Inspect local web runtime", "Plan fixed web/API startup using the local Astro web runtime."),
|
|
1187
|
-
example("treeseed dev --port 4322", "Change the web port", "Start the fixed web/API runtime with the Astro UI on a specific port."),
|
|
1187
|
+
example("treeseed dev --web-runtime local --plan --json", "Inspect local web runtime", "Plan fixed web/API/runner startup using the local Astro web runtime."),
|
|
1188
|
+
example("treeseed dev --port 4322", "Change the web port", "Start the fixed web/API/runner runtime with the Astro UI on a specific port."),
|
|
1188
1189
|
example("treeseed dev --open on", "Open the browser explicitly", "Start the integrated runtime and launch the local web URL after readiness."),
|
|
1189
1190
|
example("trsd dev", "Use the short alias", "Start the same local runtime through the shorter entrypoint."),
|
|
1190
1191
|
example("treeseed dev --json", "Stream dev events", "Emit newline-delimited events while the long-running dev process supervises local services.")
|
|
1191
1192
|
],
|
|
1192
1193
|
outcomes: [
|
|
1193
|
-
"Starts fixed local web/API surfaces, waits for readiness, prints the local URL, and then remains attached as the live supervisor.",
|
|
1194
|
+
"Starts fixed local web/API/runner surfaces, waits for readiness, prints the local URL, and then remains attached as the live supervisor.",
|
|
1194
1195
|
"Restarts required crashed surfaces with capped exponential backoff and keeps setup/readiness failures alive for retry.",
|
|
1195
1196
|
"Stops watchers first and then terminates service process groups when the foreground command exits."
|
|
1196
1197
|
]
|
|
@@ -1495,23 +1496,44 @@ const CLI_ONLY_OPERATION_SPECS = [
|
|
|
1495
1496
|
name: "projects",
|
|
1496
1497
|
aliases: [],
|
|
1497
1498
|
group: "Utilities",
|
|
1498
|
-
summary: "Inspect projects and
|
|
1499
|
-
description: "List market projects and inspect
|
|
1499
|
+
summary: "Inspect projects and run web deployment operations from the selected market.",
|
|
1500
|
+
description: "List market projects, inspect access, and queue or inspect project web deployment operations through the Market API.",
|
|
1500
1501
|
provider: "default",
|
|
1501
1502
|
related: ["market", "teams", "config"],
|
|
1502
|
-
usage: "treeseed projects [list|access|
|
|
1503
|
-
arguments: [
|
|
1503
|
+
usage: "treeseed projects [list|access|deploy|publish|monitor|deployments|deployment]",
|
|
1504
|
+
arguments: [
|
|
1505
|
+
{ name: "action", description: "Projects action.", required: false },
|
|
1506
|
+
{ name: "project-id", description: "Project id for deployment and access actions.", required: false },
|
|
1507
|
+
{ name: "deployment-id", description: "Deployment id for deployment detail, retry, resume, or cancel.", required: false }
|
|
1508
|
+
],
|
|
1504
1509
|
options: [
|
|
1505
1510
|
{ name: "market", flags: "--market <id-or-url>", description: "Select a configured market id or direct market API URL.", kind: "string" },
|
|
1506
1511
|
{ name: "team", flags: "--team <team-id>", description: "Limit project list to a team.", kind: "string" },
|
|
1512
|
+
{ name: "environment", flags: "--environment <environment>", description: "Deployment environment for project web actions.", kind: "enum", values: ["staging", "prod"] },
|
|
1513
|
+
{ name: "wait", flags: "--wait", description: "Poll the queued deployment until it reaches a terminal state.", kind: "boolean" },
|
|
1514
|
+
{ name: "timeoutSeconds", flags: "--timeout-seconds <seconds>", description: "Maximum seconds to wait before returning timeout.", kind: "string" },
|
|
1515
|
+
{ name: "pollIntervalMs", flags: "--poll-interval-ms <milliseconds>", description: "Polling interval for --wait.", kind: "string" },
|
|
1516
|
+
{ name: "dryRun", flags: "--dry-run", description: "Queue a dry-run deployment request when supported.", kind: "boolean" },
|
|
1517
|
+
{ name: "reason", flags: "--reason <text>", description: "Presentation-safe reason stored on the deployment request.", kind: "string" },
|
|
1518
|
+
{ name: "idempotencyKey", flags: "--idempotency-key <key>", description: "Deterministic idempotency key for the deployment request.", kind: "string" },
|
|
1519
|
+
{ name: "yes", flags: "--yes", description: "Required confirmation for production deploy and publish actions.", kind: "boolean" },
|
|
1520
|
+
{ name: "limit", flags: "--limit <count>", description: "Maximum number of deployments to list.", kind: "string" },
|
|
1507
1521
|
{ name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }
|
|
1508
1522
|
],
|
|
1509
|
-
examples: [
|
|
1523
|
+
examples: [
|
|
1524
|
+
"treeseed projects list",
|
|
1525
|
+
"treeseed projects access project_123",
|
|
1526
|
+
"treeseed projects deploy project_123 --environment staging --wait",
|
|
1527
|
+
"treeseed projects publish project_123 --environment prod --yes",
|
|
1528
|
+
"treeseed projects deployments project_123 --json",
|
|
1529
|
+
"treeseed projects deployment project_123 dep_123"
|
|
1530
|
+
],
|
|
1510
1531
|
help: {
|
|
1511
|
-
longSummary: ["Projects reads project and
|
|
1512
|
-
whenToUse: ["Use this to
|
|
1513
|
-
beforeYouRun: ["Authenticate to the market and know the project id
|
|
1514
|
-
automationNotes: ["Use `--json` to capture project lists and
|
|
1532
|
+
longSummary: ["Projects reads project, access, and deployment state from the selected market API using the SDK market client."],
|
|
1533
|
+
whenToUse: ["Use this to inspect projects, queue staging or production web deployment operations, and inspect the same deployment state shown in the Market UI."],
|
|
1534
|
+
beforeYouRun: ["Authenticate to the market with `treeseed auth:login --market <selector>` and know the project id before queueing deployment work."],
|
|
1535
|
+
automationNotes: ["Use `--json` to capture project lists, deployment records, events, and wait results for automation."],
|
|
1536
|
+
warnings: ["Production deploy and publish require `--yes`; without it the CLI exits before calling the API."]
|
|
1515
1537
|
},
|
|
1516
1538
|
helpVisible: true,
|
|
1517
1539
|
helpFeatured: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@treeseed/cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.7",
|
|
4
4
|
"description": "Operator-facing Treeseed CLI package.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"repository": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"setup:ci": "npm ci",
|
|
32
32
|
"build": "npm run build:dist",
|
|
33
33
|
"lint": "npm run build:dist",
|
|
34
|
-
"test": "npm run build:dist && node --test --test-concurrency=1 ./scripts/treeseed-help.test.mjs ./scripts/seed.test.mjs ./scripts/wrapper-package.test.mjs",
|
|
34
|
+
"test": "npm run build:dist && node --test --test-concurrency=1 ./scripts/treeseed-help.test.mjs ./scripts/seed.test.mjs ./scripts/projects-deploy.test.mjs ./scripts/wrapper-package.test.mjs",
|
|
35
35
|
"build:dist": "node ./scripts/run-ts.mjs ./scripts/build-dist.ts",
|
|
36
36
|
"prepare": "node ./scripts/prepare.mjs",
|
|
37
37
|
"prepack": "npm run build:dist",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"release:publish": "node ./scripts/run-ts.mjs ./scripts/publish-package.ts"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@treeseed/sdk": "github:treeseed-ai/sdk#0.10.
|
|
48
|
+
"@treeseed/sdk": "github:treeseed-ai/sdk#0.10.13",
|
|
49
49
|
"ink": "^7.0.0",
|
|
50
50
|
"react": "^19.2.5"
|
|
51
51
|
},
|