@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.
@@ -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
- lines.push(`${entry.id}=${entry.displayValue} (${entry.source})`);
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: [{ label: "Directory", value: directory ?? "(current directory)" }],
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
- "treeseed template show starter-basic",
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: result.payload?.template ?? null
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", "treeseed template show starter-basic", "treeseed template validate"],
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("treeseed template show starter-basic", "Inspect a single starter", "View the details of one starter template."),
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: "Select the starter template id to generate. Defaults to starter-basic.", kind: "string" },
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('treeseed init docs-site --template starter-basic --name "Docs Site" --site-url https://docs.example.com', "Create a starter site", "Scaffold a new tenant using the basic starter and explicit branding metadata."),
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 run web deployment operations from the selected market.",
1502
- description: "List market projects, inspect access, and queue or inspect project web deployment operations through the Market API.",
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 deployment state shown in the Market UI."],
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.17",
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.23",
48
+ "@treeseed/sdk": "github:treeseed-ai/sdk#0.10.26",
49
49
  "ink": "^7.0.0",
50
50
  "react": "^19.2.5"
51
51
  },