@treeseed/cli 0.10.17 → 0.10.20
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/config-ui.d.ts +3 -0
- package/dist/cli/handlers/config-ui.js +6 -0
- package/dist/cli/handlers/config.js +2 -1
- package/dist/cli/handlers/init.js +35 -4
- package/dist/cli/handlers/projects.js +133 -1
- package/dist/cli/handlers/template.js +90 -0
- package/dist/cli/operations-registry.js +23 -11
- package/package.json +2 -2
|
@@ -21,6 +21,9 @@ type ConfigEntry = {
|
|
|
21
21
|
purposes: string[];
|
|
22
22
|
storage: 'shared' | 'scoped';
|
|
23
23
|
validation?: ConfigValidation;
|
|
24
|
+
sourceRequirement?: string;
|
|
25
|
+
sourceHostType?: string | null;
|
|
26
|
+
sourceProvider?: string | null;
|
|
24
27
|
scope: Exclude<ConfigScope, 'all'>;
|
|
25
28
|
sharedScopes: Array<Exclude<ConfigScope, 'all'>>;
|
|
26
29
|
required: boolean;
|
|
@@ -336,6 +336,9 @@ function buildStartupDetailLines(step, draftValue) {
|
|
|
336
336
|
`Applies to: ${step.scopes.join(", ")}`,
|
|
337
337
|
`Required in: ${step.requiredScopes.join(", ")}`,
|
|
338
338
|
`Storage: ${step.entry.storage}`,
|
|
339
|
+
...step.entry.sourceRequirement ? [
|
|
340
|
+
`Host source: ${step.entry.sourceRequirement}${step.entry.sourceProvider ? ` (${step.entry.sourceProvider})` : ""}${step.entry.sourceHostType ? ` / ${step.entry.sourceHostType}` : ""}`
|
|
341
|
+
] : [],
|
|
339
342
|
"",
|
|
340
343
|
`Current value: ${formatDisplayValue(step, step.currentValue, "(unset)")}`,
|
|
341
344
|
`Suggested value: ${formatDisplayValue(step, step.suggestedValue, "(none)")}`,
|
|
@@ -356,6 +359,9 @@ function buildFullDetailLines(page, draftValue) {
|
|
|
356
359
|
`Scope: ${page.scopes.join(", ")}`,
|
|
357
360
|
`Storage: ${page.entry.storage} | ${page.required ? "required" : "optional"}`,
|
|
358
361
|
`Group: ${page.entry.group}`,
|
|
362
|
+
...page.entry.sourceRequirement ? [
|
|
363
|
+
`Host source: ${page.entry.sourceRequirement}${page.entry.sourceProvider ? ` (${page.entry.sourceProvider})` : ""}${page.entry.sourceHostType ? ` / ${page.entry.sourceHostType}` : ""}`
|
|
364
|
+
] : [],
|
|
359
365
|
"",
|
|
360
366
|
`Current: ${formatDisplayValue(page, page.currentValue, "(unset)")}`,
|
|
361
367
|
`Suggested: ${formatDisplayValue(page, page.suggestedValue, "(none)")}`,
|
|
@@ -32,7 +32,8 @@ function formatPrintEnvReports(payload) {
|
|
|
32
32
|
lines.push(`Resolved environment values for ${report.scope}`);
|
|
33
33
|
lines.push(payload.secretsRevealed ? "Secrets are shown." : "Secret values are masked.");
|
|
34
34
|
for (const entry of report.environment?.entries ?? []) {
|
|
35
|
-
|
|
35
|
+
const hostSource = entry.sourceRequirement ? ` requirement=${entry.sourceRequirement}${entry.sourceProvider ? ` provider=${entry.sourceProvider}` : ""}${entry.sourceHostType ? ` hostType=${entry.sourceHostType}` : ""}` : "";
|
|
36
|
+
lines.push(`${entry.id}=${entry.displayValue} (${entry.source}${hostSource})`);
|
|
36
37
|
}
|
|
37
38
|
lines.push("");
|
|
38
39
|
lines.push(`Provider connection checks for ${report.scope}`);
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { TREESEED_DEFAULT_STARTER_TEMPLATE_ID } from "@treeseed/sdk";
|
|
1
2
|
import { TreeseedOperationsSdk } from "@treeseed/sdk/operations";
|
|
2
3
|
import { guidedResult } from "./utils.js";
|
|
3
4
|
const operations = new TreeseedOperationsSdk();
|
|
5
|
+
function normalizeRepeatable(value) {
|
|
6
|
+
return Array.isArray(value) ? value.map(String) : typeof value === "string" ? [value] : [];
|
|
7
|
+
}
|
|
4
8
|
const handleInit = async (invocation, context) => {
|
|
5
9
|
const directory = invocation.positionals[0];
|
|
6
10
|
const result = await operations.execute({
|
|
@@ -13,7 +17,8 @@ const handleInit = async (invocation, context) => {
|
|
|
13
17
|
siteUrl: invocation.args.siteUrl,
|
|
14
18
|
contactEmail: invocation.args.contactEmail,
|
|
15
19
|
repo: invocation.args.repo,
|
|
16
|
-
discord: invocation.args.discord
|
|
20
|
+
discord: invocation.args.discord,
|
|
21
|
+
hostBindingSpecs: normalizeRepeatable(invocation.args.host)
|
|
17
22
|
}
|
|
18
23
|
}, {
|
|
19
24
|
cwd: context.cwd,
|
|
@@ -31,13 +36,35 @@ const handleInit = async (invocation, context) => {
|
|
|
31
36
|
report: result.payload
|
|
32
37
|
};
|
|
33
38
|
}
|
|
39
|
+
const payload = result.payload;
|
|
40
|
+
const hostSummaries = Array.isArray(payload?.hostBindingSummaries) ? payload.hostBindingSummaries : [];
|
|
41
|
+
const configWrites = Array.isArray(payload?.hostBindingConfig?.configWrites) ? payload.hostBindingConfig.configWrites : [];
|
|
42
|
+
const environmentWrites = Array.isArray(payload?.hostBindingConfig?.environmentWrites) ? payload.hostBindingConfig.environmentWrites : [];
|
|
34
43
|
return guidedResult({
|
|
35
44
|
command: "init",
|
|
36
45
|
summary: "Treeseed init completed successfully.",
|
|
37
|
-
facts: [
|
|
46
|
+
facts: [
|
|
47
|
+
{ label: "Directory", value: directory ?? "(current directory)" },
|
|
48
|
+
{ label: "Template", value: String(payload?.template ?? TREESEED_DEFAULT_STARTER_TEMPLATE_ID) },
|
|
49
|
+
{ label: "Host bindings", value: hostSummaries.length > 0 ? hostSummaries.length : "(none)" }
|
|
50
|
+
],
|
|
51
|
+
sections: [
|
|
52
|
+
...hostSummaries.length > 0 ? [{
|
|
53
|
+
title: "Host Bindings",
|
|
54
|
+
lines: hostSummaries.map((summary) => `${summary.requirementKey}: ${summary.mode}${summary.provider ? ` ${summary.provider}` : ""}${summary.alias ? ` (${summary.alias})` : ""}`)
|
|
55
|
+
}] : [],
|
|
56
|
+
...configWrites.length > 0 ? [{
|
|
57
|
+
title: "Config Writes",
|
|
58
|
+
lines: configWrites.map((write) => `${write.target} ${write.path} <- ${write.provider ?? "template"}`)
|
|
59
|
+
}] : [],
|
|
60
|
+
...environmentWrites.length > 0 ? [{
|
|
61
|
+
title: "Environment Entries",
|
|
62
|
+
lines: environmentWrites.map((entry) => `${entry.env}: ${entry.sensitivity} from ${entry.requirementKey}${entry.sourceProvider ? ` (${entry.sourceProvider})` : ""}`)
|
|
63
|
+
}] : []
|
|
64
|
+
],
|
|
38
65
|
nextSteps: [
|
|
39
66
|
`cd ${directory}`,
|
|
40
|
-
|
|
67
|
+
`treeseed template show ${String(payload?.template ?? TREESEED_DEFAULT_STARTER_TEMPLATE_ID)}`,
|
|
41
68
|
"treeseed sync --check",
|
|
42
69
|
"treeseed doctor",
|
|
43
70
|
"treeseed config --environment local",
|
|
@@ -45,7 +72,11 @@ const handleInit = async (invocation, context) => {
|
|
|
45
72
|
],
|
|
46
73
|
report: {
|
|
47
74
|
directory: directory ?? null,
|
|
48
|
-
template:
|
|
75
|
+
template: payload?.template ?? null,
|
|
76
|
+
hostBindings: payload?.hostBindings ?? {},
|
|
77
|
+
hostBindingPlans: payload?.hostBindingPlans ?? null,
|
|
78
|
+
hostBindingSummaries: hostSummaries,
|
|
79
|
+
hostBindingConfig: payload?.hostBindingConfig ?? null
|
|
49
80
|
}
|
|
50
81
|
});
|
|
51
82
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MarketApiError } from "@treeseed/sdk/market-client";
|
|
2
|
+
import { parseProjectLaunchHostBindingSpecs } from "@treeseed/sdk";
|
|
2
3
|
import { fail, guidedResult } from "./utils.js";
|
|
3
4
|
import { createMarketClientForInvocation } from "./market-utils.js";
|
|
4
5
|
const DEPLOYMENT_TERMINAL_STATUSES = /* @__PURE__ */ new Set(["succeeded", "failed", "cancelled", "timed_out"]);
|
|
@@ -39,8 +40,10 @@ function projectUsage(action) {
|
|
|
39
40
|
return "Usage: treeseed projects deployments <project-id>";
|
|
40
41
|
case "deployment":
|
|
41
42
|
return "Usage: treeseed projects deployment <project-id> <deployment-id>";
|
|
43
|
+
case "hosts":
|
|
44
|
+
return "Usage: treeseed projects hosts [audit|replace|resync|rotate] <project-id> [--host <requirement=provider:host-id|managed>]";
|
|
42
45
|
default:
|
|
43
|
-
return "Usage: treeseed projects [list|access|deploy|publish|monitor|deployments|deployment]";
|
|
46
|
+
return "Usage: treeseed projects [list|access|hosts|deploy|publish|monitor|deployments|deployment]";
|
|
44
47
|
}
|
|
45
48
|
}
|
|
46
49
|
function authFailure(error) {
|
|
@@ -106,6 +109,55 @@ function deploymentLine(deployment) {
|
|
|
106
109
|
deploymentUrl(deployment)
|
|
107
110
|
].filter(Boolean).join(" ");
|
|
108
111
|
}
|
|
112
|
+
function normalizeRepeatable(value) {
|
|
113
|
+
if (Array.isArray(value)) return value.map(String).filter((entry) => entry.trim());
|
|
114
|
+
return typeof value === "string" && value.trim() ? [value.trim()] : [];
|
|
115
|
+
}
|
|
116
|
+
function hostBindingLine(entry) {
|
|
117
|
+
const binding = entry?.binding ?? {};
|
|
118
|
+
const audit = entry?.audit ?? {};
|
|
119
|
+
return [
|
|
120
|
+
entry.requirementKey,
|
|
121
|
+
entry.required ? "required" : "optional",
|
|
122
|
+
entry.type,
|
|
123
|
+
binding.provider ?? "(none)",
|
|
124
|
+
binding.hostId ?? binding.managedHostKey ?? "(not selected)",
|
|
125
|
+
audit.status ?? "ok"
|
|
126
|
+
].filter(Boolean).join(" ");
|
|
127
|
+
}
|
|
128
|
+
function operationFacts(projectId, response) {
|
|
129
|
+
const operation = response.operation ?? null;
|
|
130
|
+
return [
|
|
131
|
+
{ label: "Project", value: projectId },
|
|
132
|
+
{ label: "Operation", value: operation?.id ?? null },
|
|
133
|
+
{ label: "Status", value: operation?.status ?? null },
|
|
134
|
+
{ label: "Poll", value: operation?.pollUrl ?? null }
|
|
135
|
+
].filter((fact) => fact.value != null && fact.value !== "");
|
|
136
|
+
}
|
|
137
|
+
function hostBindingForMarket(parsed, requirementKey) {
|
|
138
|
+
const summary = parsed.summaries.find((entry) => entry.requirementKey === requirementKey) ?? parsed.omitted.find((entry) => entry.requirementKey === requirementKey);
|
|
139
|
+
const binding = parsed.hostBindings[requirementKey];
|
|
140
|
+
if (summary?.mode === "none") {
|
|
141
|
+
return {
|
|
142
|
+
requirementKey,
|
|
143
|
+
requirementKind: "host",
|
|
144
|
+
type: summary.type,
|
|
145
|
+
provider: summary.provider ?? "",
|
|
146
|
+
hostId: null,
|
|
147
|
+
managedHostKey: null,
|
|
148
|
+
mode: "none",
|
|
149
|
+
selectedBy: "user"
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (!binding || !summary) return null;
|
|
153
|
+
return {
|
|
154
|
+
...binding,
|
|
155
|
+
hostId: summary.mode === "team_owned" ? summary.alias : null,
|
|
156
|
+
managedHostKey: summary.mode === "treeseed_managed" ? summary.alias === "managed" ? binding.managedHostKey : summary.alias : null,
|
|
157
|
+
displayName: summary.displayName,
|
|
158
|
+
environmentScopes: binding.environmentScopes?.filter((scope) => scope !== "local") ?? ["staging", "prod"]
|
|
159
|
+
};
|
|
160
|
+
}
|
|
109
161
|
function waitExitCode(status) {
|
|
110
162
|
if (status === "succeeded") return 0;
|
|
111
163
|
if (status === "timed_out") return 4;
|
|
@@ -226,6 +278,86 @@ const handleProjects = async (invocation, context) => {
|
|
|
226
278
|
report: { marketId: profile.id, access: redact(response.payload) }
|
|
227
279
|
});
|
|
228
280
|
}
|
|
281
|
+
if (action === "hosts") {
|
|
282
|
+
const subaction = ["audit", "replace", "resync", "rotate"].includes(String(invocation.positionals[1])) ? String(invocation.positionals[1]) : "list";
|
|
283
|
+
const projectId = subaction === "list" ? invocation.positionals[1] : invocation.positionals[2];
|
|
284
|
+
if (!projectId) return fail(projectUsage(action));
|
|
285
|
+
if (subaction === "list") {
|
|
286
|
+
const response2 = await client.projectHosts(projectId);
|
|
287
|
+
const view2 = response2.payload.view ?? {};
|
|
288
|
+
return guidedResult({
|
|
289
|
+
command: "projects",
|
|
290
|
+
summary: "Treeseed project host bindings",
|
|
291
|
+
facts: [
|
|
292
|
+
{ label: "Project", value: projectId },
|
|
293
|
+
{ label: "Status", value: view2.summary?.status ?? "ok" },
|
|
294
|
+
{ label: "Requirements", value: view2.summary?.total ?? 0 }
|
|
295
|
+
],
|
|
296
|
+
sections: [{
|
|
297
|
+
title: "Host requirements",
|
|
298
|
+
lines: (view2.requirements ?? []).map(hostBindingLine)
|
|
299
|
+
}],
|
|
300
|
+
report: { marketId: profile.id, projectId, hosts: redact(response2.payload) }
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
if (subaction === "audit") {
|
|
304
|
+
const response2 = await client.auditProjectHosts(projectId, {
|
|
305
|
+
idempotencyKey: stringArg(invocation, "idempotencyKey")
|
|
306
|
+
});
|
|
307
|
+
const view2 = response2.payload.view ?? {};
|
|
308
|
+
return guidedResult({
|
|
309
|
+
command: "projects",
|
|
310
|
+
summary: "Treeseed project host audit",
|
|
311
|
+
facts: [
|
|
312
|
+
{ label: "Project", value: projectId },
|
|
313
|
+
{ label: "Status", value: view2.summary?.status ?? "ok" },
|
|
314
|
+
{ label: "Warnings", value: view2.summary?.warnings ?? 0 },
|
|
315
|
+
{ label: "Blocked", value: view2.summary?.blocked ?? 0 }
|
|
316
|
+
],
|
|
317
|
+
sections: [{
|
|
318
|
+
title: "Diagnostics",
|
|
319
|
+
lines: (view2.diagnostics ?? []).map((entry) => `${entry.status} ${entry.requirementKey ?? ""} ${entry.message}`)
|
|
320
|
+
}],
|
|
321
|
+
report: { marketId: profile.id, projectId, audit: redact(response2.payload) }
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
const hostSpecs = normalizeRepeatable(invocation.args.host);
|
|
325
|
+
const hostSnapshot = await client.projectHosts(projectId);
|
|
326
|
+
const launchRequirements = hostSnapshot.payload.launchRequirements ?? null;
|
|
327
|
+
let requirementKey = stringArg(invocation, "requirement");
|
|
328
|
+
let hostBinding = null;
|
|
329
|
+
if (subaction === "replace") {
|
|
330
|
+
if (hostSpecs.length !== 1) return fail("Host replacement requires exactly one --host <requirement=provider:host-id|managed> spec.");
|
|
331
|
+
try {
|
|
332
|
+
const parsed = parseProjectLaunchHostBindingSpecs({ specs: hostSpecs, launchRequirements });
|
|
333
|
+
requirementKey = requirementKey ?? parsed.summaries[0]?.requirementKey ?? parsed.omitted[0]?.requirementKey ?? null;
|
|
334
|
+
if (!requirementKey) return fail("Host replacement could not determine a launch requirement key.");
|
|
335
|
+
hostBinding = hostBindingForMarket(parsed, requirementKey);
|
|
336
|
+
if (!hostBinding) return fail("Host replacement could not normalize the selected host binding.");
|
|
337
|
+
} catch (error) {
|
|
338
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (!requirementKey) return fail(`${subaction} requires --requirement <key>.`);
|
|
342
|
+
const body = {
|
|
343
|
+
...hostBinding ? { hostBinding } : {},
|
|
344
|
+
...stringArg(invocation, "sensitivePassphrase") ? { sensitivePassphrase: stringArg(invocation, "sensitivePassphrase") } : {},
|
|
345
|
+
...stringArg(invocation, "idempotencyKey") ? { idempotencyKey: stringArg(invocation, "idempotencyKey") } : {}
|
|
346
|
+
};
|
|
347
|
+
const response = subaction === "replace" ? await client.replaceProjectHost(projectId, requirementKey, body) : subaction === "resync" ? await client.resyncProjectHost(projectId, requirementKey, body) : await client.rotateProjectHost(projectId, requirementKey, body);
|
|
348
|
+
const view = response.payload.view ?? {};
|
|
349
|
+
return guidedResult({
|
|
350
|
+
command: "projects",
|
|
351
|
+
summary: `Treeseed project host ${subaction} queued`,
|
|
352
|
+
facts: operationFacts(projectId, response),
|
|
353
|
+
sections: [{
|
|
354
|
+
title: "Host requirements",
|
|
355
|
+
lines: (view.requirements ?? []).map(hostBindingLine)
|
|
356
|
+
}],
|
|
357
|
+
nextSteps: response.operation?.id ? [`trsd projects hosts ${projectId}`, `trsd operations ${response.operation.id}`] : [`trsd projects hosts ${projectId}`],
|
|
358
|
+
report: { marketId: profile.id, projectId, response: redact(response) }
|
|
359
|
+
});
|
|
360
|
+
}
|
|
229
361
|
if (action === "connect") {
|
|
230
362
|
return fail("Use treeseed config --connect-market --market-project-id <project-id> for project pairing.");
|
|
231
363
|
}
|
|
@@ -9,6 +9,50 @@ import {
|
|
|
9
9
|
import { guidedResult } from "./utils.js";
|
|
10
10
|
import { marketAuthRoot, marketSelector } from "./market-utils.js";
|
|
11
11
|
const operations = new TreeseedOperationsSdk();
|
|
12
|
+
function requirementStatus(required) {
|
|
13
|
+
return required === true ? "required" : "optional";
|
|
14
|
+
}
|
|
15
|
+
function providerList(requirement) {
|
|
16
|
+
return Array.isArray(requirement.compatibleProviders) && requirement.compatibleProviders.length > 0 ? requirement.compatibleProviders.join(", ") : "any provider";
|
|
17
|
+
}
|
|
18
|
+
function renderLaunchRequirementSections(template) {
|
|
19
|
+
const launchRequirements = template.launchRequirements;
|
|
20
|
+
if (!launchRequirements) {
|
|
21
|
+
return [{ title: "Launch Requirements", lines: ["No launch requirements declared."] }];
|
|
22
|
+
}
|
|
23
|
+
const hosts = Array.isArray(launchRequirements.hosts) ? launchRequirements.hosts : [];
|
|
24
|
+
const resources = Array.isArray(launchRequirements.resources) ? launchRequirements.resources : [];
|
|
25
|
+
const secrets = Array.isArray(launchRequirements.secrets) ? launchRequirements.secrets : [];
|
|
26
|
+
const configurableRequirements = [...hosts, ...resources];
|
|
27
|
+
const configWrites = configurableRequirements.flatMap((requirement) => Array.isArray(requirement.configWrites) ? requirement.configWrites.map((write) => `${requirement.key}: ${write.target}.${write.path} <- ${write.valueFrom}${write.writeWhen ? ` (${write.writeWhen})` : ""}`) : []);
|
|
28
|
+
const environmentWrites = configurableRequirements.flatMap((requirement) => Array.isArray(requirement.environmentWrites) ? requirement.environmentWrites.map((write) => `${requirement.key}: ${write.env} -> ${(write.targets ?? []).join(", ") || "config"} [${(write.scopes ?? []).join(", ") || "template scopes"}]`) : []);
|
|
29
|
+
return [
|
|
30
|
+
{
|
|
31
|
+
title: "Required Hosts",
|
|
32
|
+
lines: hosts.filter((host) => host.required === true).map((host) => `${host.key}: ${host.type} via ${providerList(host)} - ${host.purpose ?? host.displayName}`)
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
title: "Optional Hosts",
|
|
36
|
+
lines: hosts.filter((host) => host.required !== true).map((host) => `${host.key}: ${host.type} via ${providerList(host)} - ${host.purpose ?? host.displayName}`)
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
title: "Resources",
|
|
40
|
+
lines: resources.length > 0 ? resources.map((resource) => `${resource.key}: ${resource.type} ${requirementStatus(resource.required)} via ${providerList(resource)}`) : ["No resource lifecycle requirements in this phase."]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
title: "Secrets",
|
|
44
|
+
lines: secrets.length > 0 ? secrets.map((secret) => `${secret.key}: ${secret.env} ${requirementStatus(secret.required)} -> ${(secret.targets ?? []).join(", ")}`) : ["No standalone secret requirements declared."]
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
title: "Config Writes",
|
|
48
|
+
lines: configWrites.length > 0 ? configWrites : ["No config writes declared."]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
title: "Environment Targets",
|
|
52
|
+
lines: environmentWrites.length > 0 ? environmentWrites : ["No host-derived environment targets declared."]
|
|
53
|
+
}
|
|
54
|
+
].filter((section) => section.lines.length > 0);
|
|
55
|
+
}
|
|
12
56
|
const handleTemplate = async (invocation, context) => {
|
|
13
57
|
if (invocation.positionals[0] === "search" || invocation.positionals[0] === "install" || typeof invocation.args.market === "string") {
|
|
14
58
|
const action = invocation.positionals[0] ?? "search";
|
|
@@ -78,6 +122,52 @@ const handleTemplate = async (invocation, context) => {
|
|
|
78
122
|
outputFormat: context.outputFormat,
|
|
79
123
|
transport: "cli"
|
|
80
124
|
});
|
|
125
|
+
if (context.outputFormat === "json" || !result.ok) {
|
|
126
|
+
return {
|
|
127
|
+
exitCode: result.exitCode ?? (result.ok ? 0 : 1),
|
|
128
|
+
stdout: result.stdout,
|
|
129
|
+
stderr: result.stderr,
|
|
130
|
+
report: result.payload
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const payload = result.payload;
|
|
134
|
+
if (payload?.action === "show" && payload.template) {
|
|
135
|
+
const template = payload.template;
|
|
136
|
+
return guidedResult({
|
|
137
|
+
command: "template",
|
|
138
|
+
summary: `Template ${template.id} is ready to scaffold.`,
|
|
139
|
+
facts: [
|
|
140
|
+
{ label: "Name", value: template.displayName ?? template.id },
|
|
141
|
+
{ label: "Status", value: template.status ?? "(unknown)" },
|
|
142
|
+
{ label: "Version", value: template.templateVersion ?? "(unversioned)" },
|
|
143
|
+
{ label: "Fulfillment", value: template.fulfillmentMode ?? "(unknown)" }
|
|
144
|
+
],
|
|
145
|
+
sections: renderLaunchRequirementSections(template),
|
|
146
|
+
report: payload
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (payload?.action === "list" && Array.isArray(payload.templates)) {
|
|
150
|
+
return guidedResult({
|
|
151
|
+
command: "template",
|
|
152
|
+
summary: "Treeseed starter templates",
|
|
153
|
+
sections: [{
|
|
154
|
+
title: "Templates",
|
|
155
|
+
lines: payload.templates.map((template) => `${template.id} ${template.displayName ?? template.id} ${template.status ?? "unknown"}`)
|
|
156
|
+
}],
|
|
157
|
+
report: payload
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (payload?.action === "validate") {
|
|
161
|
+
return guidedResult({
|
|
162
|
+
command: "template",
|
|
163
|
+
summary: "Template validation completed.",
|
|
164
|
+
sections: [{
|
|
165
|
+
title: "Validated",
|
|
166
|
+
lines: Array.isArray(payload.validated) ? payload.validated.map(String) : []
|
|
167
|
+
}],
|
|
168
|
+
report: payload
|
|
169
|
+
});
|
|
170
|
+
}
|
|
81
171
|
return {
|
|
82
172
|
exitCode: result.exitCode ?? (result.ok ? 0 : 1),
|
|
83
173
|
stdout: result.stdout,
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
findTreeseedOperation as findSdkOperation,
|
|
3
3
|
TRESEED_OPERATION_SPECS as SDK_OPERATION_SPECS
|
|
4
4
|
} from "@treeseed/sdk/operations";
|
|
5
|
+
import { TREESEED_DEFAULT_STARTER_TEMPLATE_ID } from "@treeseed/sdk";
|
|
5
6
|
function command(overlay) {
|
|
6
7
|
return overlay;
|
|
7
8
|
}
|
|
@@ -848,14 +849,14 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
|
|
|
848
849
|
{ name: "version", flags: "--version <version>", description: "Artifact version for market template install.", kind: "string" },
|
|
849
850
|
{ name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }
|
|
850
851
|
],
|
|
851
|
-
examples: ["treeseed template", "treeseed template list",
|
|
852
|
+
examples: ["treeseed template", "treeseed template list", `treeseed template show ${TREESEED_DEFAULT_STARTER_TEMPLATE_ID}`, "treeseed template validate"],
|
|
852
853
|
help: {
|
|
853
854
|
longSummary: [
|
|
854
855
|
"Template exposes local starter catalog actions and market-backed search/install actions. Market search/install uses an integrated catalog from central and configured specialized markets, with every result labeled by source market."
|
|
855
856
|
],
|
|
856
857
|
examples: [
|
|
857
858
|
example("treeseed template", "Default to the catalog list", "Show the available starters without specifying an action."),
|
|
858
|
-
example(
|
|
859
|
+
example(`treeseed template show ${TREESEED_DEFAULT_STARTER_TEMPLATE_ID}`, "Inspect a single starter", "View the details of one starter template."),
|
|
859
860
|
example("treeseed template validate", "Validate the current template set", "Run template validation to confirm the catalog is internally consistent.")
|
|
860
861
|
]
|
|
861
862
|
},
|
|
@@ -881,15 +882,19 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
|
|
|
881
882
|
["init", command({
|
|
882
883
|
arguments: [{ name: "directory", description: "Target directory for the new tenant.", required: true }],
|
|
883
884
|
options: [
|
|
884
|
-
{ name: "template", flags: "--template <starter-id>", description:
|
|
885
|
+
{ name: "template", flags: "--template <starter-id>", description: `Select the starter template id to generate. Defaults to ${TREESEED_DEFAULT_STARTER_TEMPLATE_ID}.`, kind: "string" },
|
|
885
886
|
{ name: "name", flags: "--name <site-name>", description: "Override the generated site name.", kind: "string" },
|
|
886
887
|
{ name: "slug", flags: "--slug <slug>", description: "Override the generated package and tenant slug.", kind: "string" },
|
|
887
888
|
{ name: "siteUrl", flags: "--site-url <url>", description: "Set the initial public site URL.", kind: "string" },
|
|
888
889
|
{ name: "contactEmail", flags: "--contact-email <email>", description: "Set the site contact address.", kind: "string" },
|
|
889
890
|
{ name: "repo", flags: "--repo <url>", description: "Set the repository URL.", kind: "string" },
|
|
890
|
-
{ name: "discord", flags: "--discord <url>", description: "Set the Discord/community URL.", kind: "string" }
|
|
891
|
+
{ name: "discord", flags: "--discord <url>", description: "Set the Discord/community URL.", kind: "string" },
|
|
892
|
+
{ name: "host", flags: "--host <requirement=provider:alias>", description: "Bind a template launch requirement locally. Repeat for multiple requirements, or use requirement=none for optional hosts.", kind: "string", repeatable: true }
|
|
893
|
+
],
|
|
894
|
+
examples: [
|
|
895
|
+
`treeseed init docs-site --template ${TREESEED_DEFAULT_STARTER_TEMPLATE_ID} --name "Docs Site" --site-url https://docs.example.com`,
|
|
896
|
+
`treeseed init docs-site --template ${TREESEED_DEFAULT_STARTER_TEMPLATE_ID} --host sourceRepository=github:acme --host publicWeb=cloudflare:managed`
|
|
891
897
|
],
|
|
892
|
-
examples: ['treeseed init docs-site --template starter-basic --name "Docs Site" --site-url https://docs.example.com'],
|
|
893
898
|
notes: ["Runs outside an existing repo or from any branch."],
|
|
894
899
|
help: {
|
|
895
900
|
workflowPosition: "create workspace",
|
|
@@ -909,7 +914,8 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
|
|
|
909
914
|
"Seeds the project metadata fields requested through the CLI flags."
|
|
910
915
|
],
|
|
911
916
|
examples: [
|
|
912
|
-
example(
|
|
917
|
+
example(`treeseed init docs-site --template ${TREESEED_DEFAULT_STARTER_TEMPLATE_ID} --name "Docs Site" --site-url https://docs.example.com`, "Create a starter site", "Scaffold a new tenant using the default starter and explicit branding metadata."),
|
|
918
|
+
example(`treeseed init docs-site --template ${TREESEED_DEFAULT_STARTER_TEMPLATE_ID} --host sourceRepository=github:acme --host publicWeb=cloudflare:managed`, "Bind launch hosts locally", "Apply host-derived starter config during scaffold without calling Market inventory APIs."),
|
|
913
919
|
example("treeseed init workbench --slug workbench --contact-email ops@example.com", "Control project identity fields", "Initialize a tenant while overriding slug and contact metadata at creation time."),
|
|
914
920
|
example("treeseed init docs-site --repo https://github.com/example/docs-site --discord https://discord.gg/example", "Seed community and repository metadata", "Attach repository and community URLs during project initialization.")
|
|
915
921
|
],
|
|
@@ -1498,11 +1504,11 @@ const CLI_ONLY_OPERATION_SPECS = [
|
|
|
1498
1504
|
name: "projects",
|
|
1499
1505
|
aliases: [],
|
|
1500
1506
|
group: "Utilities",
|
|
1501
|
-
summary: "Inspect projects and
|
|
1502
|
-
description: "List market projects, inspect access, and queue
|
|
1507
|
+
summary: "Inspect projects, project hosts, and web deployment operations from the selected market.",
|
|
1508
|
+
description: "List market projects, inspect access and host bindings, and queue project host or web deployment operations through the Market API.",
|
|
1503
1509
|
provider: "default",
|
|
1504
1510
|
related: ["market", "teams", "config"],
|
|
1505
|
-
usage: "treeseed projects [list|access|deploy|publish|monitor|deployments|deployment]",
|
|
1511
|
+
usage: "treeseed projects [list|access|hosts|deploy|publish|monitor|deployments|deployment]",
|
|
1506
1512
|
arguments: [
|
|
1507
1513
|
{ name: "action", description: "Projects action.", required: false },
|
|
1508
1514
|
{ name: "project-id", description: "Project id for deployment and access actions.", required: false },
|
|
@@ -1518,6 +1524,9 @@ const CLI_ONLY_OPERATION_SPECS = [
|
|
|
1518
1524
|
{ name: "dryRun", flags: "--dry-run", description: "Queue a dry-run deployment request when supported.", kind: "boolean" },
|
|
1519
1525
|
{ name: "reason", flags: "--reason <text>", description: "Presentation-safe reason stored on the deployment request.", kind: "string" },
|
|
1520
1526
|
{ name: "idempotencyKey", flags: "--idempotency-key <key>", description: "Deterministic idempotency key for the deployment request.", kind: "string" },
|
|
1527
|
+
{ name: "requirement", flags: "--requirement <key>", description: "Launch host requirement key for project host resync or rotate operations.", kind: "string" },
|
|
1528
|
+
{ name: "host", flags: "--host <requirement=provider:host-id>", description: "Replacement host binding for `projects hosts replace`. Use requirement=provider:managed for a managed host or requirement=none for optional hosts.", kind: "string", repeatable: true },
|
|
1529
|
+
{ name: "sensitivePassphrase", flags: "--sensitive-passphrase <passphrase>", description: "One-time unlock passphrase for team-owned host credentials. Avoid this in shell history; prefer interactive UI when possible.", kind: "string" },
|
|
1521
1530
|
{ name: "yes", flags: "--yes", description: "Required confirmation for production deploy and publish actions.", kind: "boolean" },
|
|
1522
1531
|
{ name: "limit", flags: "--limit <count>", description: "Maximum number of deployments to list.", kind: "string" },
|
|
1523
1532
|
{ name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }
|
|
@@ -1525,14 +1534,17 @@ const CLI_ONLY_OPERATION_SPECS = [
|
|
|
1525
1534
|
examples: [
|
|
1526
1535
|
"treeseed projects list",
|
|
1527
1536
|
"treeseed projects access project_123",
|
|
1537
|
+
"treeseed projects hosts project_123",
|
|
1538
|
+
"treeseed projects hosts audit project_123",
|
|
1539
|
+
"treeseed projects hosts replace project_123 --host publicWeb=cloudflare:web-host-123",
|
|
1528
1540
|
"treeseed projects deploy project_123 --environment staging --wait",
|
|
1529
1541
|
"treeseed projects publish project_123 --environment prod --yes",
|
|
1530
1542
|
"treeseed projects deployments project_123 --json",
|
|
1531
1543
|
"treeseed projects deployment project_123 dep_123"
|
|
1532
1544
|
],
|
|
1533
1545
|
help: {
|
|
1534
|
-
longSummary: ["Projects reads project, access, and deployment state from the selected market API using the SDK market client."],
|
|
1535
|
-
whenToUse: ["Use this to inspect projects, queue staging or production web deployment operations, and inspect the same
|
|
1546
|
+
longSummary: ["Projects reads project, host binding, access, and deployment state from the selected market API using the SDK market client."],
|
|
1547
|
+
whenToUse: ["Use this to inspect projects, audit or replace launch host bindings, queue staging or production web deployment operations, and inspect the same state shown in the Market UI."],
|
|
1536
1548
|
beforeYouRun: ["Authenticate to the market with `treeseed auth:login --market <selector>` and know the project id before queueing deployment work."],
|
|
1537
1549
|
automationNotes: ["Use `--json` to capture project lists, deployment records, events, and wait results for automation."],
|
|
1538
1550
|
warnings: ["Production deploy and publish require `--yes`; without it the CLI exits before calling the API."]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@treeseed/cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.20",
|
|
4
4
|
"description": "Operator-facing Treeseed CLI package.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"repository": {
|
|
@@ -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.26",
|
|
49
49
|
"ink": "^7.0.0",
|
|
50
50
|
"react": "^19.2.5"
|
|
51
51
|
},
|