@treeseed/cli 0.10.22 → 0.11.0

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.
@@ -0,0 +1,310 @@
1
+ import { createMarketClientForInvocation } from "./market-utils.js";
2
+ import { runPackageImageCommand } from "./package-image.js";
3
+ import { fail, guidedResult } from "./utils.js";
4
+ function stringArg(invocation, key) {
5
+ const value = invocation.args[key];
6
+ return typeof value === "string" && value.trim() ? value.trim() : null;
7
+ }
8
+ function boolArg(invocation, key) {
9
+ return invocation.args[key] === true;
10
+ }
11
+ function marketRequest(client, path, options = {}) {
12
+ return client.request(path, options);
13
+ }
14
+ function teamId(invocation) {
15
+ return stringArg(invocation, "team");
16
+ }
17
+ function projectId(invocation) {
18
+ return stringArg(invocation, "project");
19
+ }
20
+ function environmentArg(invocation) {
21
+ const value = stringArg(invocation, "environment") ?? "staging";
22
+ return value === "prod" ? "prod" : "staging";
23
+ }
24
+ function recordValue(record, key) {
25
+ return record && typeof record === "object" && key in record ? record[key] : void 0;
26
+ }
27
+ function jsonResult(invocation, context, report) {
28
+ if (context.outputFormat === "json" || boolArg(invocation, "json")) {
29
+ return { exitCode: 0, stdout: [JSON.stringify(report, null, 2)], stderr: [], report };
30
+ }
31
+ return null;
32
+ }
33
+ async function status(invocation, context) {
34
+ const team = teamId(invocation);
35
+ if (!team) return fail("Missing --team. Use `trsd db status --team <team-id>`.");
36
+ const { profile, client } = createMarketClientForInvocation(invocation, context, { requireAuth: true });
37
+ const response = await marketRequest(
38
+ client,
39
+ `/v1/teams/${encodeURIComponent(team)}/treedx`,
40
+ { requireAuth: true }
41
+ );
42
+ const payload = response.payload;
43
+ const json = jsonResult(invocation, context, { ok: true, market: profile.id, team, payload });
44
+ if (json) return json;
45
+ const instance = recordValue(payload, "instance");
46
+ return guidedResult({
47
+ command: "db status",
48
+ summary: instance ? "TreeDX binding is configured." : "No TreeDX binding is configured.",
49
+ facts: [
50
+ { label: "Market", value: `${profile.id} (${profile.baseUrl})` },
51
+ { label: "Team", value: team },
52
+ { label: "Instance", value: String(recordValue(instance, "name") ?? "none") },
53
+ { label: "Status", value: String(recordValue(instance, "status") ?? "not configured") },
54
+ { label: "Provider", value: String(recordValue(instance, "provider") ?? "n/a") }
55
+ ],
56
+ sections: [
57
+ { title: "Mirrors", lines: (recordValue(payload, "mirrors") ?? []).map((mirror) => `${mirror.name}: ${mirror.status} ${mirror.targetUrl ?? mirror.targetKind ?? ""}`) },
58
+ { title: "Shares", lines: (recordValue(payload, "shares") ?? []).map((share) => `${share.scope}: ${share.status} ${share.libraryId ?? share.projectId ?? ""}`) }
59
+ ],
60
+ report: { ok: true, market: profile.id, team, payload }
61
+ });
62
+ }
63
+ async function provision(invocation, context) {
64
+ const team = teamId(invocation);
65
+ if (!team) return fail("Missing --team. Use `trsd db provision --team <team-id>`.");
66
+ const { profile, client } = createMarketClientForInvocation(invocation, context, { requireAuth: true });
67
+ const body = {
68
+ publicRead: boolArg(invocation, "public"),
69
+ baseUrl: stringArg(invocation, "url"),
70
+ imageRef: stringArg(invocation, "image") ?? void 0
71
+ };
72
+ const response = await marketRequest(
73
+ client,
74
+ `/v1/teams/${encodeURIComponent(team)}/treedx/provision`,
75
+ { method: "POST", body, requireAuth: true }
76
+ );
77
+ const json = jsonResult(invocation, context, { ok: true, market: profile.id, team, payload: response.payload });
78
+ if (json) return json;
79
+ return guidedResult({
80
+ command: "db provision",
81
+ summary: body.publicRead ? "Attached the team to the public TreeSeed federation." : "Queued a private managed TreeDX binding.",
82
+ facts: [
83
+ { label: "Market", value: `${profile.id} (${profile.baseUrl})` },
84
+ { label: "Team", value: team },
85
+ { label: "Mode", value: body.publicRead ? "public federation" : "private managed Railway service" }
86
+ ],
87
+ report: { ok: true, market: profile.id, team, payload: response.payload }
88
+ });
89
+ }
90
+ async function connect(invocation, context) {
91
+ const team = teamId(invocation);
92
+ const url = stringArg(invocation, "url");
93
+ if (!team || !url) return fail("Missing --team or --url. Use `trsd db connect --team <team-id> --url <base-url>`.");
94
+ const { profile, client } = createMarketClientForInvocation(invocation, context, { requireAuth: true });
95
+ const response = await marketRequest(
96
+ client,
97
+ `/v1/teams/${encodeURIComponent(team)}/treedx`,
98
+ {
99
+ method: "PUT",
100
+ body: {
101
+ kind: boolArg(invocation, "selfHosted") ? "self_hosted" : "managed_private",
102
+ provider: boolArg(invocation, "selfHosted") ? "self_hosted" : "railway",
103
+ baseUrl: url,
104
+ registryUrl: stringArg(invocation, "registryUrl") ?? url,
105
+ status: "active",
106
+ publicRead: boolArg(invocation, "public")
107
+ },
108
+ requireAuth: true
109
+ }
110
+ );
111
+ const json = jsonResult(invocation, context, { ok: true, market: profile.id, team, payload: response.payload });
112
+ if (json) return json;
113
+ return guidedResult({
114
+ command: "db connect",
115
+ summary: "Connected a TreeDX primary binding.",
116
+ facts: [
117
+ { label: "Market", value: `${profile.id} (${profile.baseUrl})` },
118
+ { label: "Team", value: team },
119
+ { label: "TreeDX URL", value: url }
120
+ ],
121
+ report: { ok: true, market: profile.id, team, payload: response.payload }
122
+ });
123
+ }
124
+ async function mirrors(invocation, context) {
125
+ const team = teamId(invocation);
126
+ if (!team) return fail("Missing --team. Use `trsd db mirrors --team <team-id>`.");
127
+ const { profile, client } = createMarketClientForInvocation(invocation, context, { requireAuth: true });
128
+ const create = stringArg(invocation, "name") || stringArg(invocation, "targetUrl");
129
+ const response = await marketRequest(
130
+ client,
131
+ `/v1/teams/${encodeURIComponent(team)}/treedx/mirrors`,
132
+ create ? {
133
+ method: "POST",
134
+ body: {
135
+ name: stringArg(invocation, "name") ?? "TreeDX mirror",
136
+ targetUrl: stringArg(invocation, "targetUrl"),
137
+ targetKind: stringArg(invocation, "targetKind") ?? "git",
138
+ direction: stringArg(invocation, "direction") ?? "bidirectional"
139
+ },
140
+ requireAuth: true
141
+ } : { requireAuth: true }
142
+ );
143
+ const payload = response.payload;
144
+ const json = jsonResult(invocation, context, { ok: true, market: profile.id, team, payload });
145
+ if (json) return json;
146
+ const lines = Array.isArray(payload) ? payload.map((mirror) => `${mirror.name}: ${mirror.status} ${mirror.targetUrl ?? mirror.targetKind ?? ""}`) : [`${payload.name}: ${payload.status}`];
147
+ return guidedResult({
148
+ command: "db mirrors",
149
+ summary: create ? "Created TreeDX mirror record." : "TreeDX mirrors listed.",
150
+ facts: [{ label: "Market", value: `${profile.id} (${profile.baseUrl})` }, { label: "Team", value: team }],
151
+ sections: [{ title: "Mirrors", lines }],
152
+ report: { ok: true, market: profile.id, team, payload }
153
+ });
154
+ }
155
+ async function shares(invocation, context) {
156
+ const team = teamId(invocation);
157
+ if (!team) return fail("Missing --team. Use `trsd db shares --team <team-id>`.");
158
+ const { profile, client } = createMarketClientForInvocation(invocation, context, { requireAuth: true });
159
+ const create = stringArg(invocation, "scope") || stringArg(invocation, "library") || boolArg(invocation, "public");
160
+ const response = await marketRequest(
161
+ client,
162
+ `/v1/teams/${encodeURIComponent(team)}/treedx/shares`,
163
+ create ? {
164
+ method: "POST",
165
+ body: {
166
+ scope: stringArg(invocation, "scope") ?? (boolArg(invocation, "public") ? "public_federation" : "team"),
167
+ libraryId: stringArg(invocation, "library"),
168
+ projectId: projectId(invocation),
169
+ targetTeamId: stringArg(invocation, "targetTeam"),
170
+ publicRead: boolArg(invocation, "public")
171
+ },
172
+ requireAuth: true
173
+ } : { requireAuth: true }
174
+ );
175
+ const payload = response.payload;
176
+ const json = jsonResult(invocation, context, { ok: true, market: profile.id, team, payload });
177
+ if (json) return json;
178
+ const lines = Array.isArray(payload) ? payload.map((share) => `${share.scope}: ${share.status} ${share.libraryId ?? share.projectId ?? ""}`) : [`${payload.scope}: ${payload.status}`];
179
+ return guidedResult({
180
+ command: "db shares",
181
+ summary: create ? "Created TreeDX share record." : "TreeDX shares listed.",
182
+ facts: [{ label: "Market", value: `${profile.id} (${profile.baseUrl})` }, { label: "Team", value: team }],
183
+ sections: [{ title: "Shares", lines }],
184
+ report: { ok: true, market: profile.id, team, payload }
185
+ });
186
+ }
187
+ async function library(invocation, context) {
188
+ const project = projectId(invocation);
189
+ if (!project) return fail("Missing --project. Use `trsd db library --project <project-id>`.");
190
+ const { profile, client } = createMarketClientForInvocation(invocation, context, { requireAuth: true });
191
+ const bind = stringArg(invocation, "library") || stringArg(invocation, "instance");
192
+ const response = await marketRequest(
193
+ client,
194
+ `/v1/projects/${encodeURIComponent(project)}/treedx-library`,
195
+ bind ? {
196
+ method: "POST",
197
+ body: {
198
+ instanceId: stringArg(invocation, "instance"),
199
+ libraryId: stringArg(invocation, "library"),
200
+ repositoryId: stringArg(invocation, "repository"),
201
+ contentRepositoryUrl: stringArg(invocation, "contentRepositoryUrl")
202
+ },
203
+ requireAuth: true
204
+ } : { requireAuth: true }
205
+ );
206
+ const json = jsonResult(invocation, context, { ok: true, market: profile.id, project, payload: response.payload });
207
+ if (json) return json;
208
+ return guidedResult({
209
+ command: "db library",
210
+ summary: bind ? "Project TreeDX library binding saved." : "Project TreeDX library binding loaded.",
211
+ facts: [
212
+ { label: "Market", value: `${profile.id} (${profile.baseUrl})` },
213
+ { label: "Project", value: project },
214
+ { label: "Library", value: String(recordValue(response.payload, "libraryId") ?? "not bound") }
215
+ ],
216
+ report: { ok: true, market: profile.id, project, payload: response.payload }
217
+ });
218
+ }
219
+ async function topology(invocation, context) {
220
+ const project = projectId(invocation);
221
+ if (!project) return fail("Missing --project. Use `trsd db topology --project <project-id>`.");
222
+ const { profile, client } = createMarketClientForInvocation(invocation, context, { requireAuth: true });
223
+ const response = await marketRequest(
224
+ client,
225
+ `/v1/projects/${encodeURIComponent(project)}/repository-topology`,
226
+ { requireAuth: true }
227
+ );
228
+ const payload = response.payload;
229
+ const json = jsonResult(invocation, context, { ok: true, market: profile.id, project, payload });
230
+ if (json) return json;
231
+ const content = recordValue(payload, "contentRepository");
232
+ const site = recordValue(payload, "siteRepository");
233
+ const parent = recordValue(payload, "projectRepository");
234
+ return guidedResult({
235
+ command: "db topology",
236
+ summary: "Project repository topology loaded.",
237
+ facts: [
238
+ { label: "Market", value: `${profile.id} (${profile.baseUrl})` },
239
+ { label: "Project", value: project }
240
+ ],
241
+ sections: [
242
+ { title: "Content", lines: [`${recordValue(content, "accessMode")} ${JSON.stringify(recordValue(content, "treeDx") ?? {})}`] },
243
+ { title: "Site", lines: [`${recordValue(site, "accessMode")} ${recordValue(site, "url") ?? recordValue(site, "name") ?? ""} -> ${recordValue(site, "volumePath") ?? recordValue(site, "checkoutPath") ?? ""}`] },
244
+ { title: "Project", lines: parent ? [`${recordValue(parent, "accessMode")} ${recordValue(parent, "url") ?? recordValue(parent, "name") ?? ""} -> ${recordValue(parent, "volumePath") ?? recordValue(parent, "checkoutPath") ?? ""}`] : ["No parent project repository configured."] }
245
+ ],
246
+ report: { ok: true, market: profile.id, project, payload }
247
+ });
248
+ }
249
+ async function publish(invocation, context) {
250
+ const project = projectId(invocation);
251
+ if (!project) return fail("Missing --project. Use `trsd db publish --project <project-id>`.");
252
+ const environment = environmentArg(invocation);
253
+ if (environment === "prod" && !boolArg(invocation, "yes")) return fail("Production content publish requires --yes and was not queued.");
254
+ const { profile, client } = createMarketClientForInvocation(invocation, context, { requireAuth: true });
255
+ const response = await marketRequest(
256
+ client,
257
+ `/v1/projects/${encodeURIComponent(project)}/deployments/web`,
258
+ {
259
+ method: "POST",
260
+ body: {
261
+ environment,
262
+ action: "publish_content",
263
+ source: "cli",
264
+ reason: stringArg(invocation, "reason") ?? "TreeDX content publish",
265
+ confirmProduction: environment === "prod",
266
+ dryRun: boolArg(invocation, "dryRun")
267
+ },
268
+ requireAuth: true
269
+ }
270
+ );
271
+ const json = jsonResult(invocation, context, { ok: true, market: profile.id, project, payload: response.payload });
272
+ if (json) return json;
273
+ const deployment = recordValue(response.payload, "deployment");
274
+ const operation = recordValue(response.payload, "operation");
275
+ return guidedResult({
276
+ command: "db publish",
277
+ summary: "TreeDX content publish queued.",
278
+ facts: [
279
+ { label: "Market", value: `${profile.id} (${profile.baseUrl})` },
280
+ { label: "Project", value: project },
281
+ { label: "Environment", value: environment },
282
+ { label: "Deployment", value: String(recordValue(deployment, "id") ?? "queued") },
283
+ { label: "Operation", value: String(recordValue(operation, "id") ?? "queued") }
284
+ ],
285
+ report: { ok: true, market: profile.id, project, payload: response.payload }
286
+ });
287
+ }
288
+ async function image(invocation, context) {
289
+ return runPackageImageCommand(invocation, context, { packageId: "treedx", commandName: "db image" });
290
+ }
291
+ const handleTreeDx = async (invocation, context) => {
292
+ const action = invocation.positionals[0] ?? "status";
293
+ try {
294
+ if (action === "status") return status(invocation, context);
295
+ if (action === "provision") return provision(invocation, context);
296
+ if (action === "connect") return connect(invocation, context);
297
+ if (action === "mirrors") return mirrors(invocation, context);
298
+ if (action === "shares") return shares(invocation, context);
299
+ if (action === "library") return library(invocation, context);
300
+ if (action === "topology") return topology(invocation, context);
301
+ if (action === "publish") return publish(invocation, context);
302
+ if (action === "image") return image(invocation, context);
303
+ return fail("Unknown db action. Use status, provision, connect, mirrors, shares, library, topology, publish, or image.");
304
+ } catch (error) {
305
+ return fail(error instanceof Error ? error.message : String(error));
306
+ }
307
+ };
308
+ export {
309
+ handleTreeDx
310
+ };
@@ -1,6 +1,46 @@
1
1
  import { TreeseedWorkflowSdk, type TreeseedWorkflowContext, type TreeseedWorkflowNextStep, type TreeseedWorkflowResult } from '@treeseed/sdk/workflow';
2
+ import { type TreeseedHostingEnvironment } from '@treeseed/sdk/hosting';
2
3
  import type { TreeseedCommandContext, TreeseedCommandResult } from '../types.js';
3
4
  export declare function createWorkflowSdk(context: TreeseedCommandContext, overrides?: Partial<TreeseedWorkflowContext>): TreeseedWorkflowSdk;
4
5
  export declare function workflowErrorResult(error: unknown): TreeseedCommandResult;
5
6
  export declare function renderWorkflowNextStep(step: TreeseedWorkflowNextStep): string;
6
7
  export declare function renderWorkflowNextSteps(result: TreeseedWorkflowResult): string[];
8
+ export declare function resolveWorkflowHostingGraph(context: TreeseedCommandContext, environment: TreeseedHostingEnvironment, applicationSelection?: {
9
+ selected?: string[];
10
+ }): {
11
+ environment: TreeseedHostingEnvironment;
12
+ selectedApplications: string[];
13
+ placements: import("@treeseed/sdk/hosting").TreeseedHostingPlacementSummary[];
14
+ units: {
15
+ id: string;
16
+ label: string;
17
+ serviceType: string;
18
+ placement: import("@treeseed/sdk/hosting").TreeseedServicePlacement;
19
+ hostId: string;
20
+ environment: TreeseedHostingEnvironment;
21
+ projectGroupId: string | null;
22
+ requiredCapabilities: import("@treeseed/sdk/hosting").TreeseedHostCapability[];
23
+ secretRefs: string[];
24
+ variableRefs: string[];
25
+ application: {
26
+ id: string;
27
+ relativeRoot: string;
28
+ roles: string[];
29
+ } | null;
30
+ config: unknown;
31
+ metadata: unknown;
32
+ }[];
33
+ warnings: string[];
34
+ error?: undefined;
35
+ } | {
36
+ environment: TreeseedHostingEnvironment;
37
+ selectedApplications: never[];
38
+ error: string;
39
+ placements: never[];
40
+ units: never[];
41
+ warnings: never[];
42
+ };
43
+ export declare function hostingGraphSections(hostingGraph: ReturnType<typeof resolveWorkflowHostingGraph>): {
44
+ title: string;
45
+ lines: string[];
46
+ }[];
@@ -1,5 +1,6 @@
1
1
  import { TreeseedWorkflowError, TreeseedWorkflowSdk } from "@treeseed/sdk/workflow";
2
2
  import { TreeseedKeyAgentError } from "@treeseed/sdk/workflow-support";
3
+ import { compileTreeseedHostingGraph, serializeHostingUnit } from "@treeseed/sdk/hosting";
3
4
  function createWorkflowSdk(context, overrides = {}) {
4
5
  return new TreeseedWorkflowSdk({
5
6
  cwd: context.cwd,
@@ -136,9 +137,49 @@ function renderWorkflowNextSteps(result) {
136
137
  return step.reason ? `${command} # ${step.reason}` : command;
137
138
  });
138
139
  }
140
+ function resolveWorkflowHostingGraph(context, environment, applicationSelection) {
141
+ try {
142
+ const selectedApps = Array.isArray(applicationSelection?.selected) ? applicationSelection.selected.filter((app) => typeof app === "string") : [];
143
+ const graph = compileTreeseedHostingGraph({
144
+ tenantRoot: context.cwd,
145
+ environment,
146
+ appId: selectedApps.length === 1 ? selectedApps[0] : void 0
147
+ });
148
+ return {
149
+ environment: graph.environment,
150
+ selectedApplications: selectedApps,
151
+ placements: graph.placements,
152
+ units: graph.units.map((unit) => serializeHostingUnit(unit)),
153
+ warnings: graph.warnings
154
+ };
155
+ } catch (error) {
156
+ return {
157
+ environment,
158
+ selectedApplications: [],
159
+ error: error instanceof Error ? error.message : String(error),
160
+ placements: [],
161
+ units: [],
162
+ warnings: []
163
+ };
164
+ }
165
+ }
166
+ function hostingGraphSections(hostingGraph) {
167
+ if (hostingGraph.error) {
168
+ return [{
169
+ title: "Hosting graph",
170
+ lines: [hostingGraph.error]
171
+ }];
172
+ }
173
+ return [{
174
+ title: "Hosting graph",
175
+ lines: hostingGraph.placements.length > 0 ? hostingGraph.placements.map((placement) => `${placement.label}: ${placement.serviceIds.join(", ")} on ${placement.hostIds.join(", ")}`) : ["No hosting placements resolved."]
176
+ }];
177
+ }
139
178
  export {
140
179
  createWorkflowSdk,
180
+ hostingGraphSections,
141
181
  renderWorkflowNextStep,
142
182
  renderWorkflowNextSteps,
183
+ resolveWorkflowHostingGraph,
143
184
  workflowErrorResult
144
185
  };