@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.
- package/README.md +89 -116
- package/dist/cli/handlers/audit.js +28 -3
- package/dist/cli/handlers/capacity.js +2 -2
- package/dist/cli/handlers/destroy.js +6 -1
- package/dist/cli/handlers/dev.js +16 -2
- package/dist/cli/handlers/doctor.js +13 -3
- package/dist/cli/handlers/hosting.d.ts +2 -0
- package/dist/cli/handlers/hosting.js +257 -0
- package/dist/cli/handlers/operations.d.ts +2 -0
- package/dist/cli/handlers/operations.js +46 -0
- package/dist/cli/handlers/package-image.d.ts +10 -0
- package/dist/cli/handlers/package-image.js +132 -0
- package/dist/cli/handlers/package.d.ts +2 -0
- package/dist/cli/handlers/package.js +14 -0
- package/dist/cli/handlers/projects.js +3 -3
- package/dist/cli/handlers/ready.d.ts +2 -0
- package/dist/cli/handlers/ready.js +84 -0
- package/dist/cli/handlers/reconcile.d.ts +2 -0
- package/dist/cli/handlers/reconcile.js +157 -0
- package/dist/cli/handlers/release.js +12 -3
- package/dist/cli/handlers/save.js +12 -2
- package/dist/cli/handlers/seed.js +2 -2
- package/dist/cli/handlers/stage.js +8 -2
- package/dist/cli/handlers/status.js +30 -1
- package/dist/cli/handlers/tool-wrapper.js +71 -3
- package/dist/cli/handlers/treedx.d.ts +2 -0
- package/dist/cli/handlers/treedx.js +310 -0
- package/dist/cli/handlers/workflow.d.ts +40 -0
- package/dist/cli/handlers/workflow.js +41 -0
- package/dist/cli/operations-registry.js +300 -34
- package/dist/cli/registry.d.ts +6 -0
- package/dist/cli/registry.js +12 -0
- package/dist/cli/runtime.js +17 -2
- package/package.json +3 -3
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import {
|
|
2
|
+
runTreeseedLiveReconcileTests
|
|
3
|
+
} from "@treeseed/sdk/reconcile";
|
|
4
|
+
import { collectTreeseedConfigSeedValues } from "@treeseed/sdk/workflow-support";
|
|
5
|
+
import { guidedResult } from "./utils.js";
|
|
6
|
+
import { workflowErrorResult } from "./workflow.js";
|
|
7
|
+
function environmentFor(value) {
|
|
8
|
+
return value === "prod" || value === "production" ? "prod" : value === "staging" ? "staging" : "local";
|
|
9
|
+
}
|
|
10
|
+
function modeFor(value) {
|
|
11
|
+
const raw = typeof value === "string" && value.trim() ? value.trim() : "smoke";
|
|
12
|
+
if (raw === "smoke" || raw === "acceptance" || raw === "cleanup") return raw;
|
|
13
|
+
throw new Error(`Unknown live reconciliation test mode "${raw}". Use smoke, acceptance, or cleanup.`);
|
|
14
|
+
}
|
|
15
|
+
function providersFor(value) {
|
|
16
|
+
const raw = typeof value === "string" && value.trim() ? value.trim() : "all";
|
|
17
|
+
if (raw === "all") return ["railway", "cloudflare", "github", "local"];
|
|
18
|
+
const providers = raw.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
19
|
+
const allowed = /* @__PURE__ */ new Set(["railway", "cloudflare", "github", "local"]);
|
|
20
|
+
for (const provider of providers) {
|
|
21
|
+
if (!allowed.has(provider)) {
|
|
22
|
+
throw new Error(`Unknown live reconciliation test provider "${provider}". Use railway, cloudflare, github, local, or all.`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return [...new Set(providers)];
|
|
26
|
+
}
|
|
27
|
+
function yesRequested(value) {
|
|
28
|
+
return value === true || value === "true" || value === "yes" || value === "1";
|
|
29
|
+
}
|
|
30
|
+
function formatDuration(ms) {
|
|
31
|
+
const value = typeof ms === "number" && Number.isFinite(ms) ? ms : 0;
|
|
32
|
+
if (value < 1e3) return `${Math.max(0, Math.round(value))}ms`;
|
|
33
|
+
if (value < 6e4) return `${(value / 1e3).toFixed(value < 1e4 ? 1 : 0)}s`;
|
|
34
|
+
const minutes = Math.floor(value / 6e4);
|
|
35
|
+
const seconds = Math.round(value % 6e4 / 1e3);
|
|
36
|
+
return `${minutes}m ${seconds}s`;
|
|
37
|
+
}
|
|
38
|
+
function pad(value, width) {
|
|
39
|
+
return value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
|
|
40
|
+
}
|
|
41
|
+
function truncate(value, width) {
|
|
42
|
+
if (value.length <= width) return value;
|
|
43
|
+
if (width <= 1) return value.slice(0, width);
|
|
44
|
+
return `${value.slice(0, width - 1)}\u2026`;
|
|
45
|
+
}
|
|
46
|
+
function renderScenarioRows(result) {
|
|
47
|
+
const rows = result.providers.flatMap((entry) => entry.scenarioResults.map((scenario) => ({
|
|
48
|
+
provider: entry.provider,
|
|
49
|
+
service: scenario.capability,
|
|
50
|
+
status: scenario.ok ? "ok" : "blocked",
|
|
51
|
+
lifecycle: scenario.ok && scenario.phase === "create" ? "create+verify" : scenario.phase,
|
|
52
|
+
action: scenario.action,
|
|
53
|
+
duration: formatDuration(scenario.durationMs),
|
|
54
|
+
reason: scenario.reason
|
|
55
|
+
})));
|
|
56
|
+
const width = typeof process.stdout?.columns === "number" ? process.stdout.columns : 120;
|
|
57
|
+
if (width < 96) {
|
|
58
|
+
return rows.map((row) => `- ${row.provider}:${row.service} ${row.status}; lifecycle=${row.lifecycle}; action=${row.action}; duration=${row.duration}; ${row.reason}`);
|
|
59
|
+
}
|
|
60
|
+
const providerWidth = Math.max("provider".length, ...rows.map((row) => row.provider.length));
|
|
61
|
+
const serviceWidth = Math.max("service type".length, ...rows.map((row) => row.service.length));
|
|
62
|
+
const statusWidth = Math.max("status".length, ...rows.map((row) => row.status.length));
|
|
63
|
+
const lifecycleWidth = Math.max("lifecycle".length, ...rows.map((row) => row.lifecycle.length));
|
|
64
|
+
const actionWidth = Math.max("action".length, ...rows.map((row) => row.action.length));
|
|
65
|
+
const durationWidth = Math.max("time".length, ...rows.map((row) => row.duration.length));
|
|
66
|
+
const fixedWidth = providerWidth + serviceWidth + statusWidth + lifecycleWidth + actionWidth + durationWidth + 18;
|
|
67
|
+
const reasonWidth = Math.max(24, width - fixedWidth);
|
|
68
|
+
const header = [
|
|
69
|
+
pad("provider", providerWidth),
|
|
70
|
+
pad("service type", serviceWidth),
|
|
71
|
+
pad("status", statusWidth),
|
|
72
|
+
pad("lifecycle", lifecycleWidth),
|
|
73
|
+
pad("action", actionWidth),
|
|
74
|
+
pad("time", durationWidth),
|
|
75
|
+
"reason"
|
|
76
|
+
].join(" ");
|
|
77
|
+
const divider = [
|
|
78
|
+
"-".repeat(providerWidth),
|
|
79
|
+
"-".repeat(serviceWidth),
|
|
80
|
+
"-".repeat(statusWidth),
|
|
81
|
+
"-".repeat(lifecycleWidth),
|
|
82
|
+
"-".repeat(actionWidth),
|
|
83
|
+
"-".repeat(durationWidth),
|
|
84
|
+
"-".repeat(Math.min(reasonWidth, 40))
|
|
85
|
+
].join(" ");
|
|
86
|
+
return [
|
|
87
|
+
header,
|
|
88
|
+
divider,
|
|
89
|
+
...rows.map((row) => [
|
|
90
|
+
pad(row.provider, providerWidth),
|
|
91
|
+
pad(row.service, serviceWidth),
|
|
92
|
+
pad(row.status, statusWidth),
|
|
93
|
+
pad(row.lifecycle, lifecycleWidth),
|
|
94
|
+
pad(row.action, actionWidth),
|
|
95
|
+
pad(row.duration, durationWidth),
|
|
96
|
+
truncate(row.reason, reasonWidth)
|
|
97
|
+
].join(" "))
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
const handleReconcile = async (invocation, context) => {
|
|
101
|
+
try {
|
|
102
|
+
const subcommand = typeof invocation.positionals[0] === "string" && invocation.positionals[0].trim() ? invocation.positionals[0].trim() : "status";
|
|
103
|
+
if (subcommand !== "test-live") {
|
|
104
|
+
throw new Error(`Unknown reconcile subcommand "${subcommand}". Use test-live.`);
|
|
105
|
+
}
|
|
106
|
+
const environment = environmentFor(invocation.args.environment);
|
|
107
|
+
const providers = providersFor(invocation.args.provider);
|
|
108
|
+
const mode = modeFor(invocation.args.mode);
|
|
109
|
+
if ((mode === "acceptance" || mode === "cleanup") && !yesRequested(invocation.args.yes)) {
|
|
110
|
+
throw new Error(`Live reconciliation ${mode} mode creates or deletes real provider resources. Re-run with --yes to confirm.`);
|
|
111
|
+
}
|
|
112
|
+
const resolvedEnv = {
|
|
113
|
+
...context.env,
|
|
114
|
+
...collectTreeseedConfigSeedValues(context.cwd, environment, context.env)
|
|
115
|
+
};
|
|
116
|
+
const shouldStreamProgress = mode !== "smoke";
|
|
117
|
+
const result = await runTreeseedLiveReconcileTests({
|
|
118
|
+
cwd: context.cwd,
|
|
119
|
+
environment,
|
|
120
|
+
providers,
|
|
121
|
+
mode,
|
|
122
|
+
env: resolvedEnv,
|
|
123
|
+
onProgress: shouldStreamProgress ? (event) => {
|
|
124
|
+
const elapsed = typeof event.elapsedMs === "number" ? ` (${formatDuration(event.elapsedMs)})` : "";
|
|
125
|
+
context.write(`[reconcile] ${event.message}${elapsed}`, "stderr");
|
|
126
|
+
} : void 0
|
|
127
|
+
});
|
|
128
|
+
const blocked = result.providers.flatMap((entry) => entry.report.blockedDrift.map((drift) => `${entry.provider}: ${drift.reason}`));
|
|
129
|
+
return guidedResult({
|
|
130
|
+
command: "reconcile test-live",
|
|
131
|
+
summary: result.ok ? `Live reconciliation ${mode} tests passed for ${providers.join(", ")}.` : `Live reconciliation ${mode} tests found blocking drift for ${providers.join(", ")}.`,
|
|
132
|
+
facts: [
|
|
133
|
+
{ label: "Mode", value: mode },
|
|
134
|
+
{ label: "Environment", value: environment },
|
|
135
|
+
{ label: "Run ID", value: result.runId },
|
|
136
|
+
{ label: "Resource prefix", value: result.resourcePrefix },
|
|
137
|
+
{ label: "Providers", value: providers.join(", ") },
|
|
138
|
+
{ label: "OK", value: result.ok ? "yes" : "no" }
|
|
139
|
+
],
|
|
140
|
+
sections: [{
|
|
141
|
+
title: "Providers",
|
|
142
|
+
lines: result.providers.map((entry) => `${entry.provider}: ${entry.ok ? "ok" : `${entry.report.blockedDrift.length} blocked`} (${entry.coverage.passed}/${entry.coverage.total})`)
|
|
143
|
+
}, {
|
|
144
|
+
title: "Service Type Results",
|
|
145
|
+
lines: renderScenarioRows(result)
|
|
146
|
+
}],
|
|
147
|
+
report: result,
|
|
148
|
+
exitCode: result.ok ? 0 : 1,
|
|
149
|
+
stderr: blocked
|
|
150
|
+
});
|
|
151
|
+
} catch (error) {
|
|
152
|
+
return workflowErrorResult(error);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
export {
|
|
156
|
+
handleReconcile
|
|
157
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { guidedResult } from "./utils.js";
|
|
2
|
-
import { createWorkflowSdk, renderWorkflowNextSteps, workflowErrorResult } from "./workflow.js";
|
|
2
|
+
import { createWorkflowSdk, hostingGraphSections, renderWorkflowNextSteps, resolveWorkflowHostingGraph, workflowErrorResult } from "./workflow.js";
|
|
3
3
|
function formatReleasePlanSections(payload) {
|
|
4
4
|
const sections = [];
|
|
5
5
|
const selection = payload.packageSelection ?? {};
|
|
@@ -75,6 +75,7 @@ const handleRelease = async (invocation, context) => {
|
|
|
75
75
|
worktreeMode: typeof invocation.args.worktreeMode === "string" ? invocation.args.worktreeMode : void 0,
|
|
76
76
|
ciMode: typeof invocation.args.ciMode === "string" ? invocation.args.ciMode : void 0,
|
|
77
77
|
workspaceLinks: typeof invocation.args.workspaceLinks === "string" ? invocation.args.workspaceLinks : void 0,
|
|
78
|
+
verifyDeployedResources: invocation.args.verifyDeployedResources === true,
|
|
78
79
|
fresh: invocation.args.fresh === true,
|
|
79
80
|
plan: invocation.args.plan === true || invocation.args.dryRun === true,
|
|
80
81
|
dryRun: invocation.args.dryRun === true
|
|
@@ -84,6 +85,7 @@ const handleRelease = async (invocation, context) => {
|
|
|
84
85
|
const completedPublishes = publishWait.filter((entry) => entry.status === "completed").length;
|
|
85
86
|
const plannedPublishes = payload.plannedPublishWaits?.length ?? 0;
|
|
86
87
|
const releasedCommit = typeof payload.releasedCommit === "string" && payload.releasedCommit.length > 0 ? payload.releasedCommit.slice(0, 12) : result.executionMode === "plan" ? "planned" : "not available";
|
|
88
|
+
const hostingGraph = resolveWorkflowHostingGraph(context, "prod", payload.applicationSelection);
|
|
87
89
|
return guidedResult({
|
|
88
90
|
command: invocation.commandName || "release",
|
|
89
91
|
summary: result.executionMode === "plan" ? "Treeseed release plan ready." : "Treeseed release completed successfully.",
|
|
@@ -102,14 +104,21 @@ const handleRelease = async (invocation, context) => {
|
|
|
102
104
|
{ label: result.executionMode === "plan" ? "Packages planned" : "Released packages", value: String((payload.touchedPackages ?? payload.packageSelection.selected).length) },
|
|
103
105
|
{ label: "Publish waits", value: result.executionMode === "plan" ? String(plannedPublishes) : String(completedPublishes) },
|
|
104
106
|
{ label: "CI mode", value: payload.ciMode ?? "auto" },
|
|
107
|
+
{ label: "Selected apps", value: payload.applicationSelection?.selected?.join(", ") || "all" },
|
|
105
108
|
{ label: "Fresh release", value: payload.fresh === true ? "yes" : "no" },
|
|
106
109
|
{ label: "Workflow gates", value: String(payload.workflowGates?.length ?? 0) },
|
|
107
110
|
{ label: "Worktree path", value: payload.worktreePath ?? "(in-place)" },
|
|
108
111
|
{ label: "Final branch", value: payload.finalBranch ?? (result.executionMode === "plan" ? payload.stagingBranch : "(unknown)") }
|
|
109
112
|
],
|
|
110
|
-
sections: result.executionMode === "plan" ?
|
|
113
|
+
sections: result.executionMode === "plan" ? [
|
|
114
|
+
...hostingGraphSections(hostingGraph),
|
|
115
|
+
...formatReleasePlanSections(payload)
|
|
116
|
+
] : [],
|
|
111
117
|
nextSteps: renderWorkflowNextSteps(result),
|
|
112
|
-
report:
|
|
118
|
+
report: {
|
|
119
|
+
...result,
|
|
120
|
+
hostingGraph
|
|
121
|
+
}
|
|
113
122
|
});
|
|
114
123
|
} catch (error) {
|
|
115
124
|
return workflowErrorResult(error);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { guidedResult } from "./utils.js";
|
|
2
|
-
import { createWorkflowSdk, renderWorkflowNextSteps, workflowErrorResult } from "./workflow.js";
|
|
2
|
+
import { createWorkflowSdk, hostingGraphSections, renderWorkflowNextSteps, resolveWorkflowHostingGraph, workflowErrorResult } from "./workflow.js";
|
|
3
3
|
function formatRepoPlanSummary(repo) {
|
|
4
4
|
const branch = repo.currentBranch && repo.targetBranch && repo.currentBranch !== repo.targetBranch ? `${repo.currentBranch} -> ${repo.targetBranch}` : repo.targetBranch ?? repo.currentBranch ?? "unknown";
|
|
5
5
|
const version = repo.plannedVersion ? `, version ${repo.currentVersion ?? "?"} -> ${repo.plannedVersion}` : repo.currentVersion ? `, version ${repo.currentVersion}` : "";
|
|
@@ -52,8 +52,10 @@ const handleSave = async (invocation, context) => {
|
|
|
52
52
|
hotfix: invocation.args.hotfix === true,
|
|
53
53
|
preview: invocation.args.preview === true,
|
|
54
54
|
worktreeMode: typeof invocation.args.worktreeMode === "string" ? invocation.args.worktreeMode : void 0,
|
|
55
|
+
lane: typeof invocation.args.lane === "string" ? invocation.args.lane : void 0,
|
|
55
56
|
ciMode: typeof invocation.args.ciMode === "string" ? invocation.args.ciMode : void 0,
|
|
56
57
|
verifyMode: typeof invocation.args.verifyMode === "string" ? invocation.args.verifyMode : void 0,
|
|
58
|
+
releaseCandidate: typeof invocation.args.releaseCandidate === "string" ? invocation.args.releaseCandidate : void 0,
|
|
57
59
|
workspaceLinks: typeof invocation.args.workspaceLinks === "string" ? invocation.args.workspaceLinks : void 0,
|
|
58
60
|
plan: invocation.args.plan === true || invocation.args.dryRun === true,
|
|
59
61
|
dryRun: invocation.args.dryRun === true
|
|
@@ -62,6 +64,7 @@ const handleSave = async (invocation, context) => {
|
|
|
62
64
|
const commitSha = typeof payload.commitSha === "string" && payload.commitSha.length > 0 ? payload.commitSha.slice(0, 12) : "not applicable";
|
|
63
65
|
const savedRepos = (payload.repos ?? []).filter((repo) => repo.committed || repo.pushed).map((repo) => `${repo.name}@${String(repo.commitSha ?? "").slice(0, 12)}`).join(", ");
|
|
64
66
|
const plannedRepos = result.executionMode === "plan" ? (payload.repositoryPlan?.repos ?? payload.repos ?? []).map((repo) => repo.name).join(", ") : "";
|
|
67
|
+
const hostingGraph = resolveWorkflowHostingGraph(context, payload.scope === "prod" ? "prod" : payload.scope === "staging" ? "staging" : "local", payload.applicationSelection);
|
|
65
68
|
return guidedResult({
|
|
66
69
|
command: invocation.commandName || "save",
|
|
67
70
|
summary: result.executionMode === "plan" ? "Treeseed save plan ready." : payload.noChanges ? "Treeseed save found no new changes and confirmed branch sync." : "Treeseed save completed successfully.",
|
|
@@ -82,16 +85,23 @@ const handleSave = async (invocation, context) => {
|
|
|
82
85
|
},
|
|
83
86
|
{ label: "Market pushed", value: payload.rootRepo?.pushed ? "yes" : "no" },
|
|
84
87
|
{ label: "Preview action", value: payload.previewAction?.status ?? "skipped" },
|
|
88
|
+
{ label: "Lane", value: payload.lane ?? "fast" },
|
|
85
89
|
{ label: "CI mode", value: payload.ciMode ?? "auto" },
|
|
90
|
+
{ label: "Release candidate", value: payload.releaseCandidateMode ?? "n/a" },
|
|
91
|
+
{ label: "Selected apps", value: payload.applicationSelection?.selected?.join(", ") || "all" },
|
|
86
92
|
{ label: "Workflow gates", value: String(payload.workflowGates?.length ?? 0) },
|
|
87
93
|
{ label: "Worktree path", value: payload.worktreePath ?? "(in-place)" }
|
|
88
94
|
],
|
|
89
95
|
sections: result.executionMode === "plan" ? [
|
|
96
|
+
...hostingGraphSections(hostingGraph),
|
|
90
97
|
...payload.plannedSteps?.length ? [{ title: "Dependency mode transitions", lines: payload.plannedSteps.filter((step) => /workspace-(?:link|unlink)/u.test(String(step.id ?? ""))).map((step) => `- ${step.description ?? step.id}`) }] : [],
|
|
91
98
|
...formatSavePlanSections(payload.repositoryPlan)
|
|
92
99
|
] : [],
|
|
93
100
|
nextSteps: renderWorkflowNextSteps(result),
|
|
94
|
-
report:
|
|
101
|
+
report: {
|
|
102
|
+
...result,
|
|
103
|
+
hostingGraph
|
|
104
|
+
}
|
|
95
105
|
});
|
|
96
106
|
} catch (error) {
|
|
97
107
|
return workflowErrorResult(error);
|
|
@@ -3,7 +3,7 @@ import { dirname, resolve } from "node:path";
|
|
|
3
3
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { formatSeedDiagnostics, formatSeedPlan, loadAndPlanSeed } from "@treeseed/sdk/seeds";
|
|
5
5
|
import { persistCapacityProviderConnectionToTreeseedConfig } from "@treeseed/sdk/capacity-provider";
|
|
6
|
-
import {
|
|
6
|
+
import { MarketClientError } from "@treeseed/sdk/market-client";
|
|
7
7
|
import { createMarketClientForInvocation, marketAuthRoot, marketSelector } from "./market-utils.js";
|
|
8
8
|
import { MarketClient, resolveMarketProfile, resolveMarketSession, setMarketSession } from "@treeseed/sdk/market-client";
|
|
9
9
|
async function loadLocalSeedModule(projectRoot) {
|
|
@@ -145,7 +145,7 @@ function storeLocalCapacityProviderConnection(input) {
|
|
|
145
145
|
});
|
|
146
146
|
}
|
|
147
147
|
function remoteSeedError(error, command) {
|
|
148
|
-
if (error instanceof
|
|
148
|
+
if (error instanceof MarketClientError) {
|
|
149
149
|
const payload = error.payload && typeof error.payload === "object" ? error.payload : { error: error.message };
|
|
150
150
|
const blocked = error.status === 409 || payload.result?.blocked === true;
|
|
151
151
|
const auth = error.status === 401 || error.status === 403;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { guidedResult } from "./utils.js";
|
|
2
|
-
import { createWorkflowSdk, renderWorkflowNextSteps, workflowErrorResult } from "./workflow.js";
|
|
2
|
+
import { createWorkflowSdk, hostingGraphSections, renderWorkflowNextSteps, resolveWorkflowHostingGraph, workflowErrorResult } from "./workflow.js";
|
|
3
3
|
const handleStage = async (invocation, context) => {
|
|
4
4
|
try {
|
|
5
5
|
const result = await createWorkflowSdk(context).stage({
|
|
@@ -12,6 +12,7 @@ const handleStage = async (invocation, context) => {
|
|
|
12
12
|
});
|
|
13
13
|
const payload = result.payload;
|
|
14
14
|
const mergedPackages = payload.repos.filter((repo) => repo.merged).length;
|
|
15
|
+
const hostingGraph = resolveWorkflowHostingGraph(context, "staging", payload.applicationSelection);
|
|
15
16
|
return guidedResult({
|
|
16
17
|
command: invocation.commandName || "stage",
|
|
17
18
|
summary: result.executionMode === "plan" ? "Treeseed stage plan ready." : "Treeseed stage completed successfully.",
|
|
@@ -24,14 +25,19 @@ const handleStage = async (invocation, context) => {
|
|
|
24
25
|
{ label: "Deprecated tag", value: payload.rootRepo.tagName ?? payload.deprecatedTag?.tagName ?? "(planned)" },
|
|
25
26
|
{ label: "Package merges", value: String(mergedPackages) },
|
|
26
27
|
{ label: "Staging wait", value: payload.stagingWait?.status ?? (result.executionMode === "plan" ? "planned" : "unknown") },
|
|
28
|
+
{ label: "Selected apps", value: payload.applicationSelection?.selected?.join(", ") || "all" },
|
|
27
29
|
{ label: "Workflow gates", value: String(payload.workflowGates?.length ?? 0) },
|
|
28
30
|
{ label: "Preview cleanup", value: payload.previewCleanup?.performed ? "performed" : result.executionMode === "plan" ? "planned" : "not needed" },
|
|
29
31
|
{ label: "Worktree cleanup", value: payload.worktreeCleanup?.removed ? "removed" : "not needed" },
|
|
30
32
|
{ label: "Worktree path", value: payload.worktreePath ?? "(in-place)" },
|
|
31
33
|
{ label: "Final branch", value: payload.finalBranch ?? payload.mergeTarget }
|
|
32
34
|
],
|
|
35
|
+
sections: result.executionMode === "plan" ? hostingGraphSections(hostingGraph) : [],
|
|
33
36
|
nextSteps: renderWorkflowNextSteps(result),
|
|
34
|
-
report:
|
|
37
|
+
report: {
|
|
38
|
+
...result,
|
|
39
|
+
hostingGraph
|
|
40
|
+
}
|
|
35
41
|
});
|
|
36
42
|
} catch (error) {
|
|
37
43
|
return workflowErrorResult(error);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { compileTreeseedHostingGraph, serializeHostingUnit } from "@treeseed/sdk/hosting";
|
|
1
2
|
import { guidedResult } from "./utils.js";
|
|
2
3
|
import { createWorkflowSdk, renderWorkflowNextSteps, workflowErrorResult } from "./workflow.js";
|
|
3
4
|
import { renderTreeseedStatusInk } from "./status-ui.js";
|
|
@@ -45,6 +46,7 @@ function environmentLines(state, scope) {
|
|
|
45
46
|
];
|
|
46
47
|
}
|
|
47
48
|
function statusFacts(state, live) {
|
|
49
|
+
const packageKinds = Array.isArray(state.packageSync?.packages) ? [...new Set(state.packageSync.packages.map((pkg) => pkg.kind).filter(Boolean))].join(", ") : "";
|
|
48
50
|
return [
|
|
49
51
|
{ label: "Mode", value: live ? "saved state + live provider checks" : "saved state" },
|
|
50
52
|
{ label: "Workspace root", value: state.workspaceRoot ? "yes" : "no" },
|
|
@@ -54,6 +56,7 @@ function statusFacts(state, live) {
|
|
|
54
56
|
{ label: "Mapped environment", value: state.environment },
|
|
55
57
|
{ label: "Dirty worktree", value: state.dirtyWorktree ? "yes" : "no" },
|
|
56
58
|
{ label: "Package mode", value: state.packageSync.mode },
|
|
59
|
+
{ label: "Package adapters", value: packageKinds || "(none)" },
|
|
57
60
|
{ label: "Dependency mode", value: state.packageSync.dependencyMode ?? "(unknown)" },
|
|
58
61
|
{ label: "Full package checkout", value: state.packageSync.completeCheckout ? "yes" : "no" },
|
|
59
62
|
{ label: "Package branch aligned", value: state.packageSync.aligned ? "yes" : "no" },
|
|
@@ -84,11 +87,29 @@ const handleStatus = async (invocation, context) => {
|
|
|
84
87
|
const history = invocation.args.history === "all" ? "all" : "recent";
|
|
85
88
|
const result = await createWorkflowSdk(context).status({ live, history });
|
|
86
89
|
const state = result.payload;
|
|
90
|
+
let hostingGraph = null;
|
|
91
|
+
try {
|
|
92
|
+
const graph = compileTreeseedHostingGraph({
|
|
93
|
+
tenantRoot: context.cwd,
|
|
94
|
+
environment: state.environment === "prod" || state.environment === "production" ? "prod" : state.environment === "staging" ? "staging" : "local"
|
|
95
|
+
});
|
|
96
|
+
hostingGraph = {
|
|
97
|
+
environment: graph.environment,
|
|
98
|
+
placements: graph.placements,
|
|
99
|
+
units: graph.units.map((unit) => serializeHostingUnit(unit))
|
|
100
|
+
};
|
|
101
|
+
state.hostingGraph = hostingGraph;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
state.hostingGraph = {
|
|
104
|
+
error: error instanceof Error ? error.message : String(error)
|
|
105
|
+
};
|
|
106
|
+
}
|
|
87
107
|
const nextSteps = renderWorkflowNextSteps(result);
|
|
88
108
|
const report = {
|
|
89
109
|
...result,
|
|
90
110
|
state,
|
|
91
|
-
live
|
|
111
|
+
live,
|
|
112
|
+
hostingGraph
|
|
92
113
|
};
|
|
93
114
|
if (await renderTreeseedStatusInk(state, context)) {
|
|
94
115
|
return {
|
|
@@ -106,6 +127,14 @@ const handleStatus = async (invocation, context) => {
|
|
|
106
127
|
title: scope.label,
|
|
107
128
|
lines: environmentLines(state, scope.id)
|
|
108
129
|
})),
|
|
130
|
+
{
|
|
131
|
+
title: "Hosting graph",
|
|
132
|
+
lines: hostingGraph?.placements ? hostingGraph.placements.map((placement) => `${placement.label}: ${placement.serviceIds.join(", ")} on ${placement.hostIds.join(", ")}`) : [state.hostingGraph?.error ?? "No hosting graph available."]
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
title: "Package adapters",
|
|
136
|
+
lines: Array.isArray(state.packageSync?.packages) && state.packageSync.packages.length > 0 ? state.packageSync.packages.map((pkg) => `${pkg.id}: ${pkg.kind}${pkg.version ? ` @ ${pkg.version}` : ""}${pkg.publishTarget ? ` -> ${pkg.publishTarget}` : ""}`) : ["No package adapters discovered."]
|
|
137
|
+
},
|
|
109
138
|
{
|
|
110
139
|
title: "Managed services",
|
|
111
140
|
lines: Object.entries(state.managedServices ?? {}).map(([name, service]) => `${name}: ${service.enabled ? service.initialized ? "deployed" : "not deployed" : "disabled"}${service.lastDeployedUrl ? ` (${service.lastDeployedUrl})` : ""}`)
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createTreeseedManagedToolEnv,
|
|
3
|
+
listRailwayProjects,
|
|
3
4
|
resolveTreeseedLaunchEnvironment,
|
|
4
5
|
resolveTreeseedToolCommand
|
|
5
6
|
} from "@treeseed/sdk/workflow-support";
|
|
7
|
+
import { loadTreeseedDeployConfigFromPath } from "@treeseed/sdk/platform/deploy-config";
|
|
6
8
|
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
7
9
|
import { tmpdir } from "node:os";
|
|
8
10
|
import { join } from "node:path";
|
|
@@ -96,11 +98,73 @@ function railwayProjectIdFromDeployState(cwd, scope) {
|
|
|
96
98
|
return null;
|
|
97
99
|
}
|
|
98
100
|
}
|
|
101
|
+
function railwayProjectNameFromManifest(cwd) {
|
|
102
|
+
const candidates = [
|
|
103
|
+
join(cwd, "treeseed.site.yaml"),
|
|
104
|
+
join(cwd, "packages", "api", "treeseed.site.yaml"),
|
|
105
|
+
join(cwd, "..", "..", "treeseed.site.yaml")
|
|
106
|
+
];
|
|
107
|
+
for (const candidate of candidates) {
|
|
108
|
+
if (!existsSync(candidate)) continue;
|
|
109
|
+
try {
|
|
110
|
+
const config = loadTreeseedDeployConfigFromPath(candidate);
|
|
111
|
+
const services = config.services ?? {};
|
|
112
|
+
const apiName = services.api?.railway?.projectName;
|
|
113
|
+
const runnerName = services.operationsRunner?.railway?.projectName;
|
|
114
|
+
if (typeof apiName === "string" && apiName.trim()) return apiName.trim();
|
|
115
|
+
if (typeof runnerName === "string" && runnerName.trim()) return runnerName.trim();
|
|
116
|
+
if (config.hosting?.kind === "treeseed_control_plane" && config.slug) return config.slug;
|
|
117
|
+
} catch {
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
async function resolveLiveRailwayProjectId({
|
|
123
|
+
cwd,
|
|
124
|
+
env,
|
|
125
|
+
fallbackProjectId
|
|
126
|
+
}) {
|
|
127
|
+
const projectName = typeof env.TREESEED_RAILWAY_PROJECT_NAME === "string" && env.TREESEED_RAILWAY_PROJECT_NAME.trim() ? env.TREESEED_RAILWAY_PROJECT_NAME.trim() : railwayProjectNameFromManifest(cwd);
|
|
128
|
+
if (!projectName) {
|
|
129
|
+
return fallbackProjectId;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const projects = await listRailwayProjects({ env });
|
|
133
|
+
const live = projects.find((project) => project.deletedAt === null && project.name === projectName);
|
|
134
|
+
return live?.id ?? projectName;
|
|
135
|
+
} catch {
|
|
136
|
+
return projectName;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
99
139
|
function railwayCommandUsesProjectFiles(args) {
|
|
100
140
|
const command = args[0] ?? "";
|
|
101
141
|
return ["up", "dev", "develop", "run", "local", "shell"].includes(command);
|
|
102
142
|
}
|
|
103
|
-
|
|
143
|
+
function railwayCommandNeedsProjectContext(args) {
|
|
144
|
+
const command = args[0] ?? "";
|
|
145
|
+
if (!command) return false;
|
|
146
|
+
if (args.includes("--help") || args.includes("-h")) return false;
|
|
147
|
+
return ![
|
|
148
|
+
"account",
|
|
149
|
+
"completion",
|
|
150
|
+
"environment",
|
|
151
|
+
"environments",
|
|
152
|
+
"help",
|
|
153
|
+
"init",
|
|
154
|
+
"link",
|
|
155
|
+
"login",
|
|
156
|
+
"logout",
|
|
157
|
+
"open",
|
|
158
|
+
"project",
|
|
159
|
+
"projects",
|
|
160
|
+
"team",
|
|
161
|
+
"teams",
|
|
162
|
+
"whoami",
|
|
163
|
+
"workspace",
|
|
164
|
+
"workspaces"
|
|
165
|
+
].includes(command);
|
|
166
|
+
}
|
|
167
|
+
const handleToolWrapper = async (invocation, context) => {
|
|
104
168
|
let isolatedRailwayCwd = null;
|
|
105
169
|
try {
|
|
106
170
|
const toolName = wrappedToolName(invocation.commandName);
|
|
@@ -133,9 +197,13 @@ const handleToolWrapper = (invocation, context) => {
|
|
|
133
197
|
};
|
|
134
198
|
}
|
|
135
199
|
const targetArgs = invocation.positionals;
|
|
136
|
-
if (toolName === "railway" && scope !== "local" && targetArgs
|
|
200
|
+
if (toolName === "railway" && scope !== "local" && railwayCommandNeedsProjectContext(targetArgs)) {
|
|
137
201
|
const environmentName = railwayEnvironmentName(scope);
|
|
138
|
-
const projectId =
|
|
202
|
+
const projectId = await resolveLiveRailwayProjectId({
|
|
203
|
+
cwd: context.cwd,
|
|
204
|
+
env: managedEnv,
|
|
205
|
+
fallbackProjectId: managedEnv.TREESEED_RAILWAY_PROJECT_ID || railwayProjectIdFromDeployState(context.cwd, scope)
|
|
206
|
+
});
|
|
139
207
|
const railwayCwd = railwayCommandUsesProjectFiles(targetArgs) ? context.cwd : isolatedRailwayCwd = mkdtempSync(join(tmpdir(), `treeseed-railway-${scope}-`));
|
|
140
208
|
const railwayEnv = isolatedRailwayCwd ? {
|
|
141
209
|
...managedEnv,
|