@treeseed/cli 0.10.21 → 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,257 @@
1
+ import {
2
+ applyTreeseedHostingGraph,
3
+ compileTreeseedHostingGraph,
4
+ planTreeseedHostingGraph,
5
+ serializeHostingApplyResult,
6
+ serializeHostingPlan,
7
+ serializeHostingUnit
8
+ } from "@treeseed/sdk/hosting";
9
+ import {
10
+ collectTreeseedConfigSeedValues,
11
+ collectTreeseedLiveHostedServiceChecks as collectLiveChecks,
12
+ deleteRailwayProject,
13
+ listRailwayProjects,
14
+ resolveRailwayWorkspaceContext
15
+ } from "@treeseed/sdk/workflow-support";
16
+ import { guidedResult } from "./utils.js";
17
+ import { workflowErrorResult } from "./workflow.js";
18
+ function environmentFor(value) {
19
+ return value === "prod" || value === "production" ? "prod" : value === "staging" ? "staging" : "local";
20
+ }
21
+ function subcommandFor(value) {
22
+ const subcommand = typeof value === "string" && value.trim() ? value.trim() : "status";
23
+ if (!["plan", "apply", "verify", "status", "destroy"].includes(subcommand)) {
24
+ throw new Error(`Unknown hosting subcommand "${subcommand}". Use plan, apply, verify, status, or destroy.`);
25
+ }
26
+ return subcommand;
27
+ }
28
+ function listArg(value) {
29
+ const raw = Array.isArray(value) ? value : value === void 0 || value === null || value === "" ? [] : [value];
30
+ return [...new Set(raw.flatMap((entry) => String(entry).split(",")).map((entry) => entry.trim()).filter(Boolean))];
31
+ }
32
+ function renderUnitLine(entry) {
33
+ return `${entry.unit.id}: ${entry.unit.serviceType} -> ${entry.unit.hostId} (${entry.unit.placement})${entry.plan?.action ? ` ${entry.plan.action}` : ""}${entry.verification ? ` verified=${entry.verification.verified ? "yes" : "no"}` : ""}`;
34
+ }
35
+ const handleHosting = async (invocation, context) => {
36
+ try {
37
+ const subcommand = subcommandFor(invocation.positionals[0]);
38
+ const environment = environmentFor(invocation.args.environment);
39
+ const dryRun = invocation.args.dryRun !== false && invocation.args.execute !== true;
40
+ const appId = typeof invocation.args.app === "string" && invocation.args.app.trim() ? invocation.args.app.trim() : void 0;
41
+ const filter = {
42
+ serviceIds: listArg(invocation.args.service),
43
+ placements: listArg(invocation.args.placement),
44
+ hosts: listArg(invocation.args.host)
45
+ };
46
+ const filterInput = filter.serviceIds.length || filter.placements.length || filter.hosts.length ? { filter } : {};
47
+ if (subcommand === "status") {
48
+ const graph = compileTreeseedHostingGraph({ tenantRoot: context.cwd, environment, appId, ...filterInput });
49
+ const units = graph.units.map((unit) => serializeHostingUnit(unit));
50
+ return guidedResult({
51
+ command: "hosting status",
52
+ summary: `Hosting graph status for ${environment}`,
53
+ facts: [
54
+ { label: "Environment", value: environment },
55
+ { label: "Application", value: appId ?? "(workspace)" },
56
+ { label: "Units", value: String(units.length) },
57
+ { label: "Hosts", value: Object.keys(graph.hosts).join(", ") },
58
+ { label: "Service types", value: Object.keys(graph.serviceTypes).join(", ") }
59
+ ],
60
+ sections: [
61
+ {
62
+ title: "Placements",
63
+ lines: graph.placements.map((placement) => `${placement.label}: ${placement.serviceIds.join(", ")} on ${placement.hostIds.join(", ")}`)
64
+ },
65
+ {
66
+ title: "Units",
67
+ lines: units.map((unit) => renderUnitLine({ unit }))
68
+ }
69
+ ],
70
+ report: {
71
+ command: "hosting status",
72
+ environment,
73
+ graph: {
74
+ environment,
75
+ appId: appId ?? null,
76
+ applications: graph.applications?.map((app) => ({
77
+ id: app.id,
78
+ relativeRoot: app.relativeRoot,
79
+ roles: app.roles
80
+ })) ?? [],
81
+ placements: graph.placements,
82
+ units,
83
+ warnings: graph.warnings
84
+ }
85
+ }
86
+ });
87
+ }
88
+ if (subcommand === "destroy") {
89
+ if (!appId) {
90
+ throw new Error("hosting destroy requires --app so the teardown boundary is explicit.");
91
+ }
92
+ const graph = compileTreeseedHostingGraph({ tenantRoot: context.cwd, environment, appId, ...filterInput });
93
+ const selectedProjectNames = graph.units.filter((unit) => unit.host.id === "railway").map((unit) => unit.projectGroup?.environments?.[environment]?.projectName).filter((value) => typeof value === "string" && value.trim().length > 0);
94
+ const railwayProjectNames = [...new Set(selectedProjectNames)];
95
+ const seedEnv = {
96
+ ...context.env,
97
+ ...collectTreeseedConfigSeedValues(context.cwd, environment, context.env)
98
+ };
99
+ const projects = railwayProjectNames.length === 0 ? [] : await listRailwayProjects({
100
+ workspaceId: (await resolveRailwayWorkspaceContext({ env: seedEnv })).id,
101
+ env: seedEnv
102
+ });
103
+ const results = [];
104
+ for (const projectName of railwayProjectNames) {
105
+ const matchingProjects = projects.filter((entry) => entry.name === projectName || entry.id === projectName);
106
+ if (matchingProjects.length === 0) {
107
+ results.push({
108
+ projectName,
109
+ projectId: null,
110
+ action: dryRun ? "destroy" : "noop",
111
+ result: { status: dryRun ? "planned" : "missing", projectName }
112
+ });
113
+ continue;
114
+ }
115
+ for (const project of matchingProjects) {
116
+ const result2 = dryRun ? { status: "planned", projectName, projectId: project.id } : await deleteRailwayProject({ projectId: project.id, env: seedEnv });
117
+ results.push({
118
+ projectName,
119
+ projectId: project.id,
120
+ action: dryRun ? "destroy" : result2.status === "missing" ? "noop" : "destroy",
121
+ result: result2
122
+ });
123
+ }
124
+ }
125
+ return guidedResult({
126
+ command: "hosting destroy",
127
+ summary: `${dryRun ? "Planned" : "Destroyed"} hosting resources for ${appId} in ${environment}`,
128
+ facts: [
129
+ { label: "Environment", value: environment },
130
+ { label: "Application", value: appId },
131
+ { label: "Dry run", value: dryRun ? "yes" : "no" },
132
+ { label: "Railway projects", value: railwayProjectNames.length ? railwayProjectNames.join(", ") : "(none)" }
133
+ ],
134
+ sections: [
135
+ {
136
+ title: "Railway Projects",
137
+ lines: results.length ? results.map((entry) => `${entry.projectName}: ${entry.action}`) : ["No Railway project groups selected."]
138
+ }
139
+ ],
140
+ report: {
141
+ command: "hosting destroy",
142
+ environment,
143
+ appId,
144
+ dryRun,
145
+ projects: results
146
+ }
147
+ });
148
+ }
149
+ if (subcommand === "plan" || subcommand === "verify") {
150
+ const plan = await planTreeseedHostingGraph({ tenantRoot: context.cwd, environment, appId, dryRun: true, ...filterInput });
151
+ const report2 = serializeHostingPlan(plan);
152
+ const liveHostedServices2 = subcommand === "verify" && invocation.args.live === true && environment !== "local" ? await collectLiveChecks({
153
+ tenantRoot: context.cwd,
154
+ target: environment,
155
+ appId,
156
+ strict: true,
157
+ requireLiveRailway: !appId || appId === "api",
158
+ requireLiveHttp: true,
159
+ env: {
160
+ ...context.env,
161
+ ...collectTreeseedConfigSeedValues(context.cwd, environment, context.env)
162
+ }
163
+ }) : null;
164
+ const liveFailures2 = liveHostedServices2 ? [
165
+ ...liveHostedServices2.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.issues.join("; ") || "failed"}`),
166
+ ...liveHostedServices2.liveObservation.issues
167
+ ] : [];
168
+ return guidedResult({
169
+ command: `hosting ${subcommand}`,
170
+ summary: `${subcommand === "verify" ? "Verified" : "Planned"} hosting graph for ${environment}`,
171
+ facts: [
172
+ { label: "Environment", value: environment },
173
+ { label: "Application", value: appId ?? "(workspace)" },
174
+ { label: "Dry run", value: "yes" },
175
+ { label: "Units", value: String(report2.units.length) },
176
+ { label: "Verified units", value: String(report2.units.filter((entry) => entry.verification.verified).length) }
177
+ ],
178
+ sections: [
179
+ {
180
+ title: "Placements",
181
+ lines: report2.placements.map((placement) => `${placement.label}: ${placement.serviceIds.join(", ")} on ${placement.hostIds.join(", ")}`)
182
+ },
183
+ {
184
+ title: "Plan",
185
+ lines: report2.units.map((entry) => renderUnitLine(entry))
186
+ }
187
+ ],
188
+ report: liveHostedServices2 ? { ...report2, hostedServices: liveHostedServices2 } : report2,
189
+ exitCode: liveFailures2.length > 0 ? 1 : 0,
190
+ stderr: liveFailures2
191
+ });
192
+ }
193
+ const result = await applyTreeseedHostingGraph({ tenantRoot: context.cwd, environment, appId, dryRun, ...filterInput });
194
+ const report = serializeHostingApplyResult(result);
195
+ const liveHostedServices = !dryRun && environment !== "local" ? await collectLiveChecks({
196
+ tenantRoot: context.cwd,
197
+ target: environment,
198
+ appId,
199
+ strict: true,
200
+ requireLiveRailway: !appId || appId === "api",
201
+ requireLiveHttp: true,
202
+ env: {
203
+ ...context.env,
204
+ ...collectTreeseedConfigSeedValues(context.cwd, environment, context.env)
205
+ }
206
+ }) : null;
207
+ const liveFailures = liveHostedServices ? [
208
+ ...liveHostedServices.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.issues.join("; ") || "failed"}`),
209
+ ...liveHostedServices.liveObservation.issues
210
+ ] : [];
211
+ const finalReport = liveHostedServices ? {
212
+ ...report,
213
+ liveVerification: {
214
+ ok: liveFailures.length === 0,
215
+ source: "live-hosted-service-checks",
216
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
217
+ issues: liveFailures,
218
+ checks: liveHostedServices.checks
219
+ },
220
+ hostedServices: liveHostedServices,
221
+ ok: report.ok === true && liveFailures.length === 0
222
+ } : report;
223
+ return guidedResult({
224
+ command: "hosting apply",
225
+ summary: `${dryRun ? "Dry-run applied" : "Applied"} hosting graph for ${environment}`,
226
+ facts: [
227
+ { label: "Environment", value: environment },
228
+ { label: "Application", value: appId ?? "(workspace)" },
229
+ { label: "Dry run", value: dryRun ? "yes" : "no" },
230
+ { label: "Units", value: String(finalReport.results.length) },
231
+ { label: "Verified units", value: String(finalReport.results.filter((entry) => entry.verification.verified).length) },
232
+ { label: "Selected systems", value: finalReport.selectedSystems?.join(", ") || "(none)" },
233
+ { label: "Railway reconcile", value: finalReport.transport?.railway?.reconcile ?? "(not selected)" },
234
+ { label: "Railway deploy", value: finalReport.transport?.railway?.deploy ?? "(not selected)" },
235
+ { label: "Live verification", value: finalReport.liveVerification?.ok === false ? "failed" : "passed" }
236
+ ],
237
+ sections: [
238
+ {
239
+ title: "Results",
240
+ lines: finalReport.results.map((entry) => renderUnitLine({
241
+ unit: entry.unit,
242
+ plan: entry.plan,
243
+ verification: entry.verification
244
+ }))
245
+ }
246
+ ],
247
+ report: finalReport,
248
+ exitCode: !dryRun && (liveFailures.length > 0 || finalReport.ok === false) ? 1 : 0,
249
+ stderr: liveFailures
250
+ });
251
+ } catch (error) {
252
+ return workflowErrorResult(error);
253
+ }
254
+ };
255
+ export {
256
+ handleHosting
257
+ };
@@ -0,0 +1,2 @@
1
+ import type { TreeseedCommandHandler } from '../types.js';
2
+ export declare const handleOperations: TreeseedCommandHandler;
@@ -0,0 +1,46 @@
1
+ import { runTreeseedOperationsRunnerSmoke } from "@treeseed/sdk/workflow-support";
2
+ import { guidedResult } from "./utils.js";
3
+ import { workflowErrorResult } from "./workflow.js";
4
+ function environmentFor(value) {
5
+ const raw = typeof value === "string" && value.trim() ? value.trim() : "staging";
6
+ return raw === "prod" || raw === "production" ? "prod" : "staging";
7
+ }
8
+ const handleOperations = async (invocation, context) => {
9
+ try {
10
+ const action = typeof invocation.positionals[0] === "string" ? invocation.positionals[0] : "smoke";
11
+ if (action !== "smoke") {
12
+ throw new Error(`Unsupported operations action "${action}". Use smoke.`);
13
+ }
14
+ const service = typeof invocation.args.service === "string" ? invocation.args.service : "operationsRunner";
15
+ if (service !== "operationsRunner") {
16
+ throw new Error(`Unsupported operations smoke service "${service}". Use operationsRunner.`);
17
+ }
18
+ const environment = environmentFor(invocation.args.environment);
19
+ const report = await runTreeseedOperationsRunnerSmoke({
20
+ tenantRoot: context.cwd,
21
+ environment,
22
+ env: context.env
23
+ });
24
+ return guidedResult({
25
+ command: "operations smoke",
26
+ summary: report.ok ? "Treeseed operations runner smoke passed." : "Treeseed operations runner smoke failed.",
27
+ facts: [
28
+ { label: "Environment", value: environment },
29
+ { label: "Base URL", value: report.baseUrl },
30
+ { label: "Operation", value: report.operationId ?? "(none)" },
31
+ { label: "Final status", value: report.finalStatus ?? "(none)" },
32
+ { label: "Runner", value: report.runnerId ?? "(none)" }
33
+ ],
34
+ nextSteps: report.ok ? [] : [
35
+ `npx trsd hosting verify --environment ${environment} --service operationsRunner --live --json`
36
+ ],
37
+ report,
38
+ exitCode: report.ok ? 0 : 1
39
+ });
40
+ } catch (error) {
41
+ return workflowErrorResult(error);
42
+ }
43
+ };
44
+ export {
45
+ handleOperations
46
+ };
@@ -0,0 +1,10 @@
1
+ import type { TreeseedCommandHandler, TreeseedParsedInvocation } from '../types.js';
2
+ export declare function runPackageImageCommand(invocation: TreeseedParsedInvocation, context: Parameters<TreeseedCommandHandler>[1], options?: {
3
+ packageId?: string | null;
4
+ commandName?: string;
5
+ }): Promise<import("../operations-types.js").TreeseedCommandResult | {
6
+ exitCode: number;
7
+ stdout: string[];
8
+ stderr: never[];
9
+ report: Record<string, unknown>;
10
+ }>;
@@ -0,0 +1,132 @@
1
+ import {
2
+ createGitHubApiClient,
3
+ dispatchGitHubWorkflowRun,
4
+ ensureGitHubActionsEnvironment,
5
+ getLatestGitHubWorkflowRun,
6
+ listGitHubEnvironmentSecretNames,
7
+ listGitHubEnvironmentVariableNames,
8
+ planTreeseedPackageDevelopmentImage,
9
+ resolveGitHubCredentialForRepository,
10
+ resolveTreeseedLaunchEnvironment,
11
+ upsertGitHubEnvironmentSecret,
12
+ upsertGitHubEnvironmentVariable
13
+ } from "@treeseed/sdk/workflow-support";
14
+ import { fail, guidedResult } from "./utils.js";
15
+ function stringArg(invocation, key) {
16
+ const value = invocation.args[key];
17
+ return typeof value === "string" && value.trim() ? value.trim() : null;
18
+ }
19
+ function boolArg(invocation, key) {
20
+ return invocation.args[key] === true;
21
+ }
22
+ function jsonResult(invocation, context, report) {
23
+ if (context.outputFormat === "json" || boolArg(invocation, "json")) {
24
+ return { exitCode: 0, stdout: [JSON.stringify(report, null, 2)], stderr: [], report };
25
+ }
26
+ return null;
27
+ }
28
+ async function runPackageImageCommand(invocation, context, options = {}) {
29
+ const packageId = options.packageId ?? stringArg(invocation, "package") ?? stringArg(invocation, "packageId");
30
+ if (!packageId) {
31
+ return fail("Missing --package. Use `trsd package image --package <package-id>`.");
32
+ }
33
+ const branch = stringArg(invocation, "branch") ?? "staging";
34
+ const workflow = stringArg(invocation, "workflow") ?? null;
35
+ const execute = boolArg(invocation, "execute");
36
+ const syncConfig = boolArg(invocation, "syncConfig");
37
+ const planOnly = boolArg(invocation, "plan") || !execute;
38
+ const imagePlan = planTreeseedPackageDevelopmentImage(context.cwd, packageId, { branch });
39
+ const selectedWorkflow = workflow ?? imagePlan.workflow;
40
+ const configEnv = resolveTreeseedLaunchEnvironment({
41
+ tenantRoot: context.cwd,
42
+ scope: "staging",
43
+ baseEnv: context.env
44
+ });
45
+ const dockerHub = {
46
+ usernameConfigured: Boolean(String(configEnv.DOCKERHUB_USERNAME ?? "").trim()),
47
+ tokenConfigured: Boolean(String(configEnv.DOCKERHUB_TOKEN ?? "").trim()),
48
+ requiredSecrets: ["DOCKERHUB_TOKEN"],
49
+ requiredVariables: ["DOCKERHUB_USERNAME"]
50
+ };
51
+ const credential = resolveGitHubCredentialForRepository(imagePlan.repository, { values: configEnv, env: context.env });
52
+ const githubClientEnv = credential.token ? { ...configEnv, GH_TOKEN: credential.token, GITHUB_TOKEN: credential.token } : configEnv;
53
+ const report = {
54
+ ok: true,
55
+ action: planOnly ? "plan" : "dispatch",
56
+ package: imagePlan.package,
57
+ repository: imagePlan.repository,
58
+ workflow: selectedWorkflow,
59
+ branch: imagePlan.branch,
60
+ refs: imagePlan.refs,
61
+ credential: {
62
+ repository: credential.repository,
63
+ envName: credential.envName,
64
+ configured: credential.configured,
65
+ source: credential.source,
66
+ fallbackUsed: credential.fallbackUsed
67
+ },
68
+ dockerHub,
69
+ hosting: imagePlan.hosting
70
+ };
71
+ if (syncConfig) {
72
+ const client = createGitHubApiClient({ env: githubClientEnv });
73
+ const environment = imagePlan.hosting?.environment ?? "staging";
74
+ await ensureGitHubActionsEnvironment(imagePlan.repository, environment, {
75
+ client,
76
+ branchName: imagePlan.branch
77
+ });
78
+ const [secretNames, variableNames] = await Promise.all([
79
+ listGitHubEnvironmentSecretNames(imagePlan.repository, environment, { client }),
80
+ listGitHubEnvironmentVariableNames(imagePlan.repository, environment, { client })
81
+ ]);
82
+ const synced = {
83
+ environment,
84
+ secrets: [],
85
+ variables: []
86
+ };
87
+ if (configEnv.DOCKERHUB_TOKEN) {
88
+ await upsertGitHubEnvironmentSecret(imagePlan.repository, environment, "DOCKERHUB_TOKEN", configEnv.DOCKERHUB_TOKEN, { client });
89
+ synced.secrets.push({ name: "DOCKERHUB_TOKEN", existed: secretNames.has("DOCKERHUB_TOKEN") });
90
+ }
91
+ if (configEnv.DOCKERHUB_USERNAME) {
92
+ await upsertGitHubEnvironmentVariable(imagePlan.repository, environment, "DOCKERHUB_USERNAME", configEnv.DOCKERHUB_USERNAME, { client });
93
+ synced.variables.push({ name: "DOCKERHUB_USERNAME", existed: variableNames.has("DOCKERHUB_USERNAME") });
94
+ }
95
+ report.syncedConfig = synced;
96
+ }
97
+ if (execute) {
98
+ const client = createGitHubApiClient({ env: githubClientEnv });
99
+ report.dispatch = await dispatchGitHubWorkflowRun(imagePlan.repository, {
100
+ client,
101
+ workflow: selectedWorkflow,
102
+ branch: imagePlan.branch
103
+ });
104
+ report.latestWorkflowRun = await getLatestGitHubWorkflowRun(imagePlan.repository, {
105
+ client,
106
+ workflow: selectedWorkflow,
107
+ branch: imagePlan.branch
108
+ });
109
+ }
110
+ const json = jsonResult(invocation, context, report);
111
+ if (json) return json;
112
+ return guidedResult({
113
+ command: options.commandName ?? "package image",
114
+ summary: execute ? "Package development image workflow dispatched." : "Package development image plan ready.",
115
+ facts: [
116
+ { label: "Package", value: `${imagePlan.package.id} (${imagePlan.package.path})` },
117
+ { label: "Workflow", value: `${selectedWorkflow} @ ${imagePlan.branch}` },
118
+ { label: "Image", value: imagePlan.refs.imageRef },
119
+ { label: "Moving tag", value: imagePlan.refs.movingImageRef ?? "disabled" },
120
+ { label: "GitHub credential", value: credential.configured ? `${credential.envName}${credential.fallbackUsed ? " (fallback)" : ""}` : `${credential.envName} missing` },
121
+ { label: "Docker Hub config", value: dockerHub.usernameConfigured && dockerHub.tokenConfigured ? "configured" : "missing" },
122
+ { label: "Hosting override", value: imagePlan.hosting ? `${imagePlan.hosting.overrideEnvVar}=${imagePlan.refs.imageRef}` : "not declared" }
123
+ ],
124
+ sections: [
125
+ { title: "Next", lines: imagePlan.hosting ? [imagePlan.hosting.command] : [] }
126
+ ],
127
+ report
128
+ });
129
+ }
130
+ export {
131
+ runPackageImageCommand
132
+ };
@@ -0,0 +1,2 @@
1
+ import type { TreeseedCommandHandler } from '../types.js';
2
+ export declare const handlePackage: TreeseedCommandHandler;
@@ -0,0 +1,14 @@
1
+ import { runPackageImageCommand } from "./package-image.js";
2
+ import { fail } from "./utils.js";
3
+ const handlePackage = async (invocation, context) => {
4
+ const action = invocation.positionals[0] ?? "status";
5
+ try {
6
+ if (action === "image") return runPackageImageCommand(invocation, context, { commandName: "package image" });
7
+ return fail("Unknown package action. Use image.");
8
+ } catch (error) {
9
+ return fail(error instanceof Error ? error.message : String(error));
10
+ }
11
+ };
12
+ export {
13
+ handlePackage
14
+ };
@@ -1,4 +1,4 @@
1
- import { MarketApiError } from "@treeseed/sdk/market-client";
1
+ import { MarketClientError } from "@treeseed/sdk/market-client";
2
2
  import { parseProjectLaunchHostBindingSpecs } from "@treeseed/sdk";
3
3
  import { fail, guidedResult } from "./utils.js";
4
4
  import { createMarketClientForInvocation } from "./market-utils.js";
@@ -47,7 +47,7 @@ function projectUsage(action) {
47
47
  }
48
48
  }
49
49
  function authFailure(error) {
50
- if (error instanceof MarketApiError && [401, 403].includes(error.status)) {
50
+ if (error instanceof MarketClientError && [401, 403].includes(error.status)) {
51
51
  return fail(error.message, 2);
52
52
  }
53
53
  const message = error instanceof Error ? error.message : String(error);
@@ -57,7 +57,7 @@ function authFailure(error) {
57
57
  return null;
58
58
  }
59
59
  function deploymentApiExitCode(error) {
60
- if (error instanceof MarketApiError) {
60
+ if (error instanceof MarketClientError) {
61
61
  if ([401, 403].includes(error.status)) return 2;
62
62
  const payload = error.payload;
63
63
  const code = payload?.error?.code ?? payload?.code;
@@ -0,0 +1,2 @@
1
+ import type { TreeseedCommandHandler } from '../types.js';
2
+ export declare const handleReady: TreeseedCommandHandler;
@@ -0,0 +1,84 @@
1
+ import {
2
+ collectTreeseedDeploymentReadiness,
3
+ collectTreeseedLiveHostedServiceChecks,
4
+ collectTreeseedHostedServiceChecks,
5
+ formatTreeseedReadinessReport,
6
+ runTreeseedOperationsRunnerSmoke
7
+ } from "@treeseed/sdk/workflow-support";
8
+ import { guidedResult } from "./utils.js";
9
+ import { createWorkflowSdk, workflowErrorResult } from "./workflow.js";
10
+ function environmentFor(value) {
11
+ const raw = typeof value === "string" && value.trim() ? value.trim() : "staging";
12
+ if (raw === "prod" || raw === "production") return "prod";
13
+ if (raw === "local") return "local";
14
+ return "staging";
15
+ }
16
+ function failedCount(section) {
17
+ return Number(section?.summary?.failed ?? 0);
18
+ }
19
+ const handleReady = async (invocation, context) => {
20
+ try {
21
+ const environment = environmentFor(invocation.positionals[0] ?? invocation.args.environment);
22
+ const live = invocation.args.live === true || environment !== "local";
23
+ const strict = invocation.args.strict === true || environment !== "local";
24
+ const status = await createWorkflowSdk(context).status();
25
+ const readiness = collectTreeseedDeploymentReadiness({ tenantRoot: context.cwd, environment });
26
+ const hostedServices = live ? await collectTreeseedLiveHostedServiceChecks({
27
+ tenantRoot: context.cwd,
28
+ target: environment,
29
+ strict,
30
+ requireLiveRailway: strict,
31
+ requireLiveHttp: strict,
32
+ env: context.env
33
+ }) : collectTreeseedHostedServiceChecks({ tenantRoot: context.cwd, target: environment });
34
+ const runnerSmoke = live && environment !== "local" && readiness.ok ? await runTreeseedOperationsRunnerSmoke({
35
+ tenantRoot: context.cwd,
36
+ environment,
37
+ env: context.env
38
+ }) : null;
39
+ const ok = readiness.ok && failedCount(hostedServices) === 0 && (runnerSmoke ? runnerSmoke.ok : true);
40
+ const nextActions = [
41
+ ...readiness.checks.filter((check) => check.status === "failed" && check.remediation).map((check) => check.remediation),
42
+ ...runnerSmoke && !runnerSmoke.ok ? [
43
+ `npx trsd operations smoke --environment ${environment} --service operationsRunner --json`,
44
+ `npx trsd hosting verify --environment ${environment} --service operationsRunner --live --json`
45
+ ] : []
46
+ ];
47
+ const report = {
48
+ ok,
49
+ environment,
50
+ live,
51
+ strict,
52
+ workflow: status.payload,
53
+ deploymentReadiness: readiness,
54
+ hostedServices,
55
+ runnerSmoke,
56
+ nextActions
57
+ };
58
+ return guidedResult({
59
+ command: "ready",
60
+ summary: ok ? `Treeseed ${environment} readiness passed.` : `Treeseed ${environment} readiness failed.`,
61
+ facts: [
62
+ { label: "Environment", value: environment },
63
+ { label: "Live", value: live ? "yes" : "no" },
64
+ { label: "Readiness failed", value: readiness.summary.failed },
65
+ { label: "Hosted checks failed", value: hostedServices.summary.failed },
66
+ { label: "Runner smoke", value: runnerSmoke ? runnerSmoke.ok ? "passed" : "failed" : "skipped" }
67
+ ],
68
+ sections: [
69
+ {
70
+ title: "Deployment readiness",
71
+ lines: formatTreeseedReadinessReport(readiness).split("\n")
72
+ }
73
+ ],
74
+ nextSteps: nextActions,
75
+ report,
76
+ exitCode: ok ? 0 : 1
77
+ });
78
+ } catch (error) {
79
+ return workflowErrorResult(error);
80
+ }
81
+ };
82
+ export {
83
+ handleReady
84
+ };
@@ -0,0 +1,2 @@
1
+ import type { TreeseedCommandHandler } from '../types.js';
2
+ export declare const handleReconcile: TreeseedCommandHandler;