@treeseed/cli 0.10.5 → 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.
|
@@ -4,6 +4,7 @@ import { resolve } from "node:path";
|
|
|
4
4
|
import { resolveCapacityProviderLaunchEnvironment } from "@treeseed/sdk/capacity-provider";
|
|
5
5
|
import { resolveMarketProfile } from "@treeseed/sdk/market-client";
|
|
6
6
|
import { findNearestTreeseedRoot, findNearestTreeseedWorkspaceRoot } from "@treeseed/sdk/workflow-support";
|
|
7
|
+
import { createMarketClientForInvocation } from "./market-utils.js";
|
|
7
8
|
import { fail, guidedResult } from "./utils.js";
|
|
8
9
|
const ENTRYPOINT_RELATIVE_PATH = ["dist", "provider", "entrypoint.js"];
|
|
9
10
|
const COMPOSE_FILE_NAME = "compose.capacity-provider.yml";
|
|
@@ -11,6 +12,7 @@ const DEFAULT_PROJECT_NAME = "treeseed-capacity-provider";
|
|
|
11
12
|
const DEFAULT_HOST_DATA_DIR = ".treeseed/local-capacity-provider/data";
|
|
12
13
|
const PROVIDER_LIFECYCLE_ACTIONS = /* @__PURE__ */ new Set(["build", "up", "down", "restart", "logs", "status", "test-local"]);
|
|
13
14
|
const PROVIDER_ENTRYPOINT_ACTIONS = /* @__PURE__ */ new Set(["doctor", "register", "plan"]);
|
|
15
|
+
const MARKET_CAPACITY_ACTIONS = /* @__PURE__ */ new Set(["migrate"]);
|
|
14
16
|
function stringArg(invocation, name) {
|
|
15
17
|
const value = invocation.args[name];
|
|
16
18
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
@@ -18,6 +20,24 @@ function stringArg(invocation, name) {
|
|
|
18
20
|
function boolArg(invocation, name) {
|
|
19
21
|
return invocation.args[name] === true;
|
|
20
22
|
}
|
|
23
|
+
function numberArg(invocation, name) {
|
|
24
|
+
const value = invocation.args[name];
|
|
25
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
26
|
+
if (typeof value === "string" && value.trim().length > 0 && Number.isFinite(Number(value))) return Number(value);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
function formatNumber(value, digits = 2) {
|
|
30
|
+
if (value === null || value === void 0 || value === "") return "n/a";
|
|
31
|
+
const numeric = Number(value);
|
|
32
|
+
if (!Number.isFinite(numeric)) return String(value);
|
|
33
|
+
return numeric.toLocaleString("en-US", { maximumFractionDigits: digits });
|
|
34
|
+
}
|
|
35
|
+
function recordValue(record, key) {
|
|
36
|
+
return record && typeof record === "object" && key in record ? record[key] : void 0;
|
|
37
|
+
}
|
|
38
|
+
function marketRequest(client, path, options = {}) {
|
|
39
|
+
return client.request(path, options);
|
|
40
|
+
}
|
|
21
41
|
function readPackageName(packageRoot) {
|
|
22
42
|
const packageJsonPath = resolve(packageRoot, "package.json");
|
|
23
43
|
if (!existsSync(packageJsonPath)) return null;
|
|
@@ -108,6 +128,212 @@ function composeCommandArgs(composeFilePath, projectName, action) {
|
|
|
108
128
|
return base;
|
|
109
129
|
}
|
|
110
130
|
}
|
|
131
|
+
function nativeBudgetSummaryLines(report) {
|
|
132
|
+
const budgets = recordValue(report, "budgets");
|
|
133
|
+
const nativeCapacity = recordValue(budgets, "nativeCapacity") ?? recordValue(budgets, "native_capacity");
|
|
134
|
+
const executionProviders = recordValue(nativeCapacity, "executionProviders") ?? recordValue(nativeCapacity, "execution_providers");
|
|
135
|
+
if (!Array.isArray(executionProviders)) return [];
|
|
136
|
+
return executionProviders.flatMap((provider) => {
|
|
137
|
+
const name = recordValue(provider, "name") ?? recordValue(provider, "id") ?? "execution provider";
|
|
138
|
+
const kind = recordValue(provider, "kind") ?? "custom";
|
|
139
|
+
const nativeUnit = recordValue(provider, "nativeUnit") ?? recordValue(provider, "native_unit") ?? "native unit";
|
|
140
|
+
const workers = recordValue(provider, "maxConcurrentWorkers") ?? recordValue(provider, "max_concurrent_workers");
|
|
141
|
+
const limits = recordValue(provider, "nativeLimits") ?? recordValue(provider, "native_limits");
|
|
142
|
+
const lines = [`${name}: ${kind}, ${nativeUnit}${workers ? `, workers ${workers}` : ""}`];
|
|
143
|
+
if (Array.isArray(limits)) {
|
|
144
|
+
for (const limit of limits) {
|
|
145
|
+
lines.push(` ${recordValue(limit, "scope") ?? recordValue(limit, "limitScope") ?? "limit"}: ${formatNumber(recordValue(limit, "limitAmount") ?? recordValue(limit, "limit_amount"))} ${recordValue(limit, "nativeUnit") ?? nativeUnit}, reserve ${formatNumber(recordValue(limit, "reserveBufferPercent") ?? recordValue(limit, "reserve_buffer_percent"))}%`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return lines;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function derivedCapacityLines(plan) {
|
|
152
|
+
const derivedCapacity = recordValue(plan, "derivedCapacity");
|
|
153
|
+
const entries = recordValue(derivedCapacity, "entries");
|
|
154
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
155
|
+
return ["No derived native capacity entries are available yet."];
|
|
156
|
+
}
|
|
157
|
+
return entries.map((entry) => [
|
|
158
|
+
`${recordValue(entry, "executionProviderKind") ?? "provider"}:${recordValue(entry, "nativeUnit") ?? "native"}`,
|
|
159
|
+
`limit ${formatNumber(recordValue(entry, "configuredNativeLimit"))}`,
|
|
160
|
+
`observed ${formatNumber(recordValue(entry, "observedNativeRemaining"))}`,
|
|
161
|
+
`reserved ${formatNumber(recordValue(entry, "activeReservedNativeAmount"))}`,
|
|
162
|
+
`reserve ${formatNumber(recordValue(entry, "reserveBufferPercent"))}%`,
|
|
163
|
+
`conversion ${formatNumber(recordValue(entry, "nativeUnitsPerCredit"))} native/credit`,
|
|
164
|
+
`derived ${formatNumber(recordValue(entry, "derivedAvailableCredits"))} credits`,
|
|
165
|
+
`confidence ${recordValue(entry, "confidence") ?? "unknown"}`
|
|
166
|
+
].join(" | "));
|
|
167
|
+
}
|
|
168
|
+
function grantAllocationLines(plan) {
|
|
169
|
+
const grants = recordValue(plan, "grants");
|
|
170
|
+
if (!Array.isArray(grants) || grants.length === 0) return [];
|
|
171
|
+
return grants.map((grant) => [
|
|
172
|
+
`${recordValue(grant, "grantScope") ?? "grant"} ${recordValue(grant, "environment") ?? "all"}`,
|
|
173
|
+
`allocation ${formatNumber(recordValue(grant, "portfolioAllocationPercent"))}%`,
|
|
174
|
+
`reserve pool ${formatNumber(recordValue(grant, "reservePoolPercent"))}%`,
|
|
175
|
+
`max daily project credits ${formatNumber(recordValue(grant, "maxDailyProjectCredits"))}`,
|
|
176
|
+
`overflow ${recordValue(grant, "overflowPolicy") ?? "soft_grant"}`,
|
|
177
|
+
`emergency ${recordValue(grant, "emergencyOverride") === true ? "on" : "off"}`
|
|
178
|
+
].join(" | "));
|
|
179
|
+
}
|
|
180
|
+
async function runProjectCapacityPlan(invocation, context) {
|
|
181
|
+
const projectId = stringArg(invocation, "project");
|
|
182
|
+
if (!projectId) return fail("Missing --project. Use `trsd capacity plan --project <project-id> --environment local`.");
|
|
183
|
+
const { profile, client } = createMarketClientForInvocation(invocation, context, { requireAuth: true });
|
|
184
|
+
const environment = environmentSelector(invocation);
|
|
185
|
+
const response = await marketRequest(
|
|
186
|
+
client,
|
|
187
|
+
`/v1/projects/${encodeURIComponent(projectId)}/capacity-plan?environment=${encodeURIComponent(environment)}`,
|
|
188
|
+
{ requireAuth: true }
|
|
189
|
+
);
|
|
190
|
+
const plan = response.payload;
|
|
191
|
+
return guidedResult({
|
|
192
|
+
command: "capacity plan",
|
|
193
|
+
summary: `Capacity plan for project ${projectId} in ${environment}.`,
|
|
194
|
+
facts: [
|
|
195
|
+
{ label: "Market", value: `${profile.id} (${profile.baseUrl})` },
|
|
196
|
+
{ label: "Project", value: projectId },
|
|
197
|
+
{ label: "Environment", value: environment },
|
|
198
|
+
{ label: "Derived credits", value: formatNumber(recordValue(recordValue(plan, "derivedCapacity"), "totalDerivedAvailableCredits")) }
|
|
199
|
+
],
|
|
200
|
+
sections: [
|
|
201
|
+
{ title: "Native projection", lines: derivedCapacityLines(plan) },
|
|
202
|
+
{ title: "Allocation grants", lines: grantAllocationLines(plan) }
|
|
203
|
+
],
|
|
204
|
+
report: { action: "plan", projectId, environment, market: { id: profile.id, baseUrl: profile.baseUrl }, plan }
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function providerMatcher(selector) {
|
|
208
|
+
return (provider) => {
|
|
209
|
+
const id = String(recordValue(provider, "id") ?? "");
|
|
210
|
+
const name = String(recordValue(provider, "name") ?? "");
|
|
211
|
+
return id === selector || name === selector;
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function migrationMissingFields(invocation) {
|
|
215
|
+
const missing = [];
|
|
216
|
+
if (!stringArg(invocation, "team")) missing.push("--team");
|
|
217
|
+
if (!stringArg(invocation, "provider")) missing.push("--provider");
|
|
218
|
+
if (!stringArg(invocation, "kind")) missing.push("--kind");
|
|
219
|
+
if (!stringArg(invocation, "nativeUnit")) missing.push("--native-unit");
|
|
220
|
+
if (numberArg(invocation, "limit") === null) missing.push("--limit");
|
|
221
|
+
return missing;
|
|
222
|
+
}
|
|
223
|
+
async function runMigrateToDerived(invocation, context) {
|
|
224
|
+
if (!boolArg(invocation, "toDerived")) {
|
|
225
|
+
return fail("Missing --to-derived. Phase 8 supports `trsd capacity migrate --to-derived`.");
|
|
226
|
+
}
|
|
227
|
+
const missing = migrationMissingFields(invocation);
|
|
228
|
+
const example = "trsd capacity migrate --to-derived --team team_123 --provider provider_123 --kind codex_subscription --native-unit wall_minute --limit 480 --scope daily --reset-cadence daily --quota-visibility opaque --reserve-buffer-percent 20 --max-concurrent-workers 4 --project project_123 --portfolio-allocation-percent 100 --dry-run";
|
|
229
|
+
if (missing.length > 0) {
|
|
230
|
+
return fail(`Missing native capacity facts: ${missing.join(", ")}.
|
|
231
|
+
Example: ${example}`);
|
|
232
|
+
}
|
|
233
|
+
const teamId = stringArg(invocation, "team");
|
|
234
|
+
const providerSelectorValue = stringArg(invocation, "provider");
|
|
235
|
+
const kind = stringArg(invocation, "kind");
|
|
236
|
+
const nativeUnit = stringArg(invocation, "nativeUnit");
|
|
237
|
+
const limitAmount = numberArg(invocation, "limit");
|
|
238
|
+
const scope = stringArg(invocation, "scope") ?? "daily";
|
|
239
|
+
const resetCadence = stringArg(invocation, "resetCadence") ?? "daily";
|
|
240
|
+
const quotaVisibility = stringArg(invocation, "quotaVisibility") ?? "opaque";
|
|
241
|
+
const reserveBufferPercent = numberArg(invocation, "reserveBufferPercent") ?? 20;
|
|
242
|
+
const maxConcurrentWorkers = Math.max(1, Math.floor(numberArg(invocation, "maxConcurrentWorkers") ?? 1));
|
|
243
|
+
const environment = environmentSelector(invocation);
|
|
244
|
+
const projectId = stringArg(invocation, "project");
|
|
245
|
+
const allocationPercent = numberArg(invocation, "portfolioAllocationPercent");
|
|
246
|
+
const dryRun = boolArg(invocation, "dryRun");
|
|
247
|
+
const { profile, client } = createMarketClientForInvocation(invocation, context, { requireAuth: !dryRun });
|
|
248
|
+
const providerList = dryRun ? { payload: [{ id: providerSelectorValue, name: providerSelectorValue }] } : await marketRequest(
|
|
249
|
+
client,
|
|
250
|
+
`/v1/teams/${encodeURIComponent(teamId)}/capacity-providers`,
|
|
251
|
+
{ requireAuth: true }
|
|
252
|
+
);
|
|
253
|
+
const provider = providerList.payload.find(providerMatcher(providerSelectorValue));
|
|
254
|
+
if (!provider) return fail(`Capacity provider "${providerSelectorValue}" was not found in team ${teamId}.`);
|
|
255
|
+
const providerId = String(recordValue(provider, "id"));
|
|
256
|
+
const executionProvider = {
|
|
257
|
+
name: `${kind.replace(/_/gu, " ")} ${nativeUnit}`,
|
|
258
|
+
kind,
|
|
259
|
+
nativeUnit,
|
|
260
|
+
quotaVisibility,
|
|
261
|
+
maxConcurrentWorkers,
|
|
262
|
+
resetCadence,
|
|
263
|
+
nativeLimits: [{
|
|
264
|
+
scope,
|
|
265
|
+
nativeUnit,
|
|
266
|
+
limitAmount,
|
|
267
|
+
reserveBufferPercent,
|
|
268
|
+
resetCadence,
|
|
269
|
+
confidence: "estimated",
|
|
270
|
+
source: "operator_migration"
|
|
271
|
+
}],
|
|
272
|
+
metadata: {
|
|
273
|
+
source: "trsd capacity migrate --to-derived",
|
|
274
|
+
staticCreditBudgetsPreservedAs: "hybrid_fallback_cap"
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
const grant = allocationPercent === null ? null : {
|
|
278
|
+
capacityProviderId: providerId,
|
|
279
|
+
teamId,
|
|
280
|
+
projectId,
|
|
281
|
+
environment,
|
|
282
|
+
grantScope: projectId ? "project" : "team",
|
|
283
|
+
portfolioAllocationPercent: allocationPercent,
|
|
284
|
+
overflowPolicy: "soft_grant",
|
|
285
|
+
metadata: {
|
|
286
|
+
source: "trsd capacity migrate --to-derived"
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
if (!dryRun) {
|
|
290
|
+
await marketRequest(client, `/v1/teams/${encodeURIComponent(teamId)}/capacity-providers/${encodeURIComponent(providerId)}`, {
|
|
291
|
+
method: "PATCH",
|
|
292
|
+
body: {
|
|
293
|
+
name: String(recordValue(provider, "name") ?? providerId),
|
|
294
|
+
creditBudgetMode: "hybrid"
|
|
295
|
+
},
|
|
296
|
+
requireAuth: true
|
|
297
|
+
});
|
|
298
|
+
await marketRequest(client, `/v1/teams/${encodeURIComponent(teamId)}/capacity-providers/${encodeURIComponent(providerId)}/execution-providers`, {
|
|
299
|
+
method: "POST",
|
|
300
|
+
body: executionProvider,
|
|
301
|
+
requireAuth: true
|
|
302
|
+
});
|
|
303
|
+
if (grant) {
|
|
304
|
+
await marketRequest(client, `/v1/teams/${encodeURIComponent(teamId)}/capacity-grants`, {
|
|
305
|
+
method: "POST",
|
|
306
|
+
body: grant,
|
|
307
|
+
requireAuth: true
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return guidedResult({
|
|
312
|
+
command: "capacity migrate",
|
|
313
|
+
summary: dryRun ? "Dry run: derived native capacity migration plan." : "Derived native capacity migration applied.",
|
|
314
|
+
facts: [
|
|
315
|
+
{ label: "Market", value: `${profile.id} (${profile.baseUrl})` },
|
|
316
|
+
{ label: "Team", value: teamId },
|
|
317
|
+
{ label: "Provider", value: providerId },
|
|
318
|
+
{ label: "Native limit", value: `${formatNumber(limitAmount)} ${nativeUnit} / ${scope}` },
|
|
319
|
+
{ label: "Reserve buffer", value: `${formatNumber(reserveBufferPercent)}%` },
|
|
320
|
+
{ label: "Allocation percent", value: allocationPercent === null ? null : `${formatNumber(allocationPercent)}%` },
|
|
321
|
+
{ label: "Dry run", value: dryRun }
|
|
322
|
+
],
|
|
323
|
+
sections: [
|
|
324
|
+
{ title: "Execution provider", lines: [`${executionProvider.name}: ${kind}, ${nativeUnit}, ${maxConcurrentWorkers} workers, ${quotaVisibility} quota visibility`] },
|
|
325
|
+
...grant ? [{ title: "Allocation grant", lines: [`${grant.grantScope} ${projectId ?? teamId} in ${environment}: ${formatNumber(allocationPercent)}%`] }] : []
|
|
326
|
+
],
|
|
327
|
+
report: {
|
|
328
|
+
action: "migrate",
|
|
329
|
+
dryRun,
|
|
330
|
+
teamId,
|
|
331
|
+
providerId,
|
|
332
|
+
executionProvider,
|
|
333
|
+
grant
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
}
|
|
111
337
|
function lifecycleActionRequiresConnection(action) {
|
|
112
338
|
return action === "up" || action === "restart";
|
|
113
339
|
}
|
|
@@ -256,6 +482,7 @@ function invokeProviderEntrypoint(action, invocation, context) {
|
|
|
256
482
|
],
|
|
257
483
|
sections: [
|
|
258
484
|
{ title: "Output", lines: stdout ? stdout.split(/\r?\n/u) : [] },
|
|
485
|
+
{ title: "Native budget file", lines: nativeBudgetSummaryLines(report) },
|
|
259
486
|
{ title: "Errors", lines: stderr ? stderr.split(/\r?\n/u) : [] }
|
|
260
487
|
],
|
|
261
488
|
exitCode: result.status ?? 1,
|
|
@@ -268,6 +495,21 @@ function invokeProviderEntrypoint(action, invocation, context) {
|
|
|
268
495
|
}
|
|
269
496
|
const handleCapacity = (invocation, context) => {
|
|
270
497
|
const action = invocation.positionals[0] ?? "doctor";
|
|
498
|
+
if (action === "plan" && stringArg(invocation, "project")) {
|
|
499
|
+
try {
|
|
500
|
+
return runProjectCapacityPlan(invocation, context);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (MARKET_CAPACITY_ACTIONS.has(action)) {
|
|
506
|
+
try {
|
|
507
|
+
if (action === "migrate") return runMigrateToDerived(invocation, context);
|
|
508
|
+
return fail(`Unknown capacity action "${action}".`);
|
|
509
|
+
} catch (error) {
|
|
510
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
511
|
+
}
|
|
512
|
+
}
|
|
271
513
|
if (PROVIDER_LIFECYCLE_ACTIONS.has(action)) {
|
|
272
514
|
try {
|
|
273
515
|
return runLifecycleAction(action, invocation, context);
|
|
@@ -282,7 +524,7 @@ const handleCapacity = (invocation, context) => {
|
|
|
282
524
|
return fail(error instanceof Error ? error.message : String(error));
|
|
283
525
|
}
|
|
284
526
|
}
|
|
285
|
-
return fail(`Unknown capacity action "${action}". Use doctor, register, plan, build, up, down, restart, logs, status, or test-local.`);
|
|
527
|
+
return fail(`Unknown capacity action "${action}". Use doctor, register, plan, migrate, build, up, down, restart, logs, status, or test-local.`);
|
|
286
528
|
};
|
|
287
529
|
export {
|
|
288
530
|
handleCapacity
|
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,
|
|
@@ -1523,16 +1545,28 @@ const CLI_ONLY_OPERATION_SPECS = [
|
|
|
1523
1545
|
name: "capacity",
|
|
1524
1546
|
aliases: [],
|
|
1525
1547
|
group: "Utilities",
|
|
1526
|
-
summary: "
|
|
1527
|
-
description: "
|
|
1548
|
+
summary: "Inspect capacity plans and operate the package-owned capacity provider runtime.",
|
|
1549
|
+
description: "Read Market capacity plans, migrate static providers to derived native capacity, and run provider diagnostics through the built @treeseed/agent entrypoint.",
|
|
1528
1550
|
provider: "default",
|
|
1529
1551
|
related: ["teams", "projects", "agents"],
|
|
1530
|
-
usage: "treeseed capacity [doctor|register|plan|up|down|restart|logs|status|build|test-local]",
|
|
1552
|
+
usage: "treeseed capacity [doctor|register|plan|migrate|up|down|restart|logs|status|build|test-local]",
|
|
1531
1553
|
arguments: [{ name: "action", description: "Capacity action.", required: false }],
|
|
1532
1554
|
options: [
|
|
1533
1555
|
{ name: "market", flags: "--market <id-or-url>", description: "Select a configured market id or direct market API URL.", kind: "string" },
|
|
1534
1556
|
{ name: "provider", flags: "--provider <provider-id>", description: "Provider id or local provider selector for diagnostics.", kind: "string" },
|
|
1557
|
+
{ name: "team", flags: "--team <team-id>", description: "Team id for Market capacity migration.", kind: "string" },
|
|
1558
|
+
{ name: "project", flags: "--project <project-id>", description: "Project id for Market capacity plan or allocation migration.", kind: "string" },
|
|
1535
1559
|
{ name: "environment", flags: "--environment <scope>", description: "Treeseed config scope used when resolving encrypted provider launch values.", kind: "enum", values: ["local", "staging", "prod"] },
|
|
1560
|
+
{ name: "toDerived", flags: "--to-derived", description: "Migrate a static provider to derived native capacity facts.", kind: "boolean" },
|
|
1561
|
+
{ name: "kind", flags: "--kind <provider-kind>", description: "Execution provider kind such as codex_subscription or openrouter.", kind: "string" },
|
|
1562
|
+
{ name: "nativeUnit", flags: "--native-unit <unit>", description: "Native unit humans can forecast, such as wall_minute, usd, or billable_token.", kind: "string" },
|
|
1563
|
+
{ name: "limit", flags: "--limit <amount>", description: "Native limit amount for the selected scope.", kind: "string" },
|
|
1564
|
+
{ name: "scope", flags: "--scope <scope>", description: "Native limit scope, usually daily or monthly.", kind: "string" },
|
|
1565
|
+
{ name: "resetCadence", flags: "--reset-cadence <cadence>", description: "Native limit reset cadence.", kind: "string" },
|
|
1566
|
+
{ name: "quotaVisibility", flags: "--quota-visibility <mode>", description: "Whether quota remaining is visible, sampled, or opaque.", kind: "string" },
|
|
1567
|
+
{ name: "reserveBufferPercent", flags: "--reserve-buffer-percent <percent>", description: "Native reserve buffer percentage.", kind: "string" },
|
|
1568
|
+
{ name: "maxConcurrentWorkers", flags: "--max-concurrent-workers <count>", description: "Maximum workers this execution provider can run concurrently.", kind: "string" },
|
|
1569
|
+
{ name: "portfolioAllocationPercent", flags: "--portfolio-allocation-percent <percent>", description: "Optional project/team allocation percent to create during migration.", kind: "string" },
|
|
1536
1570
|
{ name: "dataDir", flags: "--data-dir <path>", description: "Host data directory mounted into the provider container at /data.", kind: "string" },
|
|
1537
1571
|
{ name: "agentPackageRoot", flags: "--agent-package-root <path>", description: "Path to a built @treeseed/agent package root.", kind: "string" },
|
|
1538
1572
|
{ name: "diagnostic", flags: "--diagnostic", description: "Start lifecycle commands without live Market registration or provider credentials.", kind: "boolean" },
|
|
@@ -1541,8 +1575,10 @@ const CLI_ONLY_OPERATION_SPECS = [
|
|
|
1541
1575
|
],
|
|
1542
1576
|
examples: [
|
|
1543
1577
|
"treeseed capacity doctor --market local --provider local",
|
|
1578
|
+
"treeseed capacity plan --market local --project project_123 --environment local",
|
|
1544
1579
|
"treeseed capacity register --market local --provider local --dry-run --json",
|
|
1545
1580
|
"treeseed capacity plan --market local --provider local --dry-run --json",
|
|
1581
|
+
"treeseed capacity migrate --to-derived --market local --team team_123 --provider provider_123 --kind codex_subscription --native-unit wall_minute --limit 480 --scope daily --dry-run",
|
|
1546
1582
|
"treeseed capacity build",
|
|
1547
1583
|
"treeseed capacity up --market local --provider local",
|
|
1548
1584
|
"treeseed capacity up --market local --provider local --diagnostic",
|
|
@@ -1552,8 +1588,8 @@ const CLI_ONLY_OPERATION_SPECS = [
|
|
|
1552
1588
|
"treeseed capacity test-local"
|
|
1553
1589
|
],
|
|
1554
1590
|
help: {
|
|
1555
|
-
longSummary: ["Capacity invokes the package-owned @treeseed/agent provider runtime for local diagnostics
|
|
1556
|
-
whenToUse: ["Use this when validating a self-hosted capacity provider install, checking provider runtime output, or running the local provider stack."],
|
|
1591
|
+
longSummary: ["Capacity reads Market project capacity plans, migrates provider setup toward derived native facts, and invokes the package-owned @treeseed/agent provider runtime for local diagnostics and Docker/Compose lifecycle commands."],
|
|
1592
|
+
whenToUse: ["Use this when inspecting native-to-credit projection, validating a self-hosted capacity provider install, checking provider runtime output, or running the local provider stack."],
|
|
1557
1593
|
beforeYouRun: ["Build @treeseed/agent first, or pass --agent-package-root to a built package. Use `trsd config` for encrypted provider values; capacity commands do not write plaintext env files."],
|
|
1558
1594
|
automationNotes: ["Use `--json` for stable provider runtime output. `up` runs live local provider mode by default; pass `--diagnostic` or use `test-local` for diagnostics without provider secrets."]
|
|
1559
1595
|
},
|
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
|
},
|