@treeseed/cli 0.4.11 → 0.5.1
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 +35 -8
- package/dist/cli/handlers/auth-login.js +58 -46
- package/dist/cli/handlers/auth-logout.js +23 -11
- package/dist/cli/handlers/auth-whoami.js +27 -16
- package/dist/cli/handlers/close.js +9 -4
- package/dist/cli/handlers/config-ui.d.ts +65 -9
- package/dist/cli/handlers/config-ui.js +561 -175
- package/dist/cli/handlers/config.js +164 -10
- package/dist/cli/handlers/destroy.js +3 -2
- package/dist/cli/handlers/dev.js +6 -1
- package/dist/cli/handlers/doctor.js +82 -47
- package/dist/cli/handlers/recover.d.ts +2 -0
- package/dist/cli/handlers/recover.js +25 -0
- package/dist/cli/handlers/release.js +14 -4
- package/dist/cli/handlers/resume.d.ts +2 -0
- package/dist/cli/handlers/resume.js +23 -0
- package/dist/cli/handlers/save.js +9 -3
- package/dist/cli/handlers/secret-prompts.d.ts +2 -0
- package/dist/cli/handlers/secret-prompts.js +54 -0
- package/dist/cli/handlers/secrets.d.ts +7 -0
- package/dist/cli/handlers/secrets.js +161 -0
- package/dist/cli/handlers/stage.js +10 -4
- package/dist/cli/handlers/status.js +37 -5
- package/dist/cli/handlers/switch.js +11 -4
- package/dist/cli/handlers/tasks.js +1 -0
- package/dist/cli/handlers/utils.js +15 -8
- package/dist/cli/handlers/workflow.js +74 -3
- package/dist/cli/help-ui.js +1 -1
- package/dist/cli/operations-registry.js +200 -21
- package/dist/cli/registry.d.ts +8 -0
- package/dist/cli/registry.js +19 -1
- package/dist/cli/repair.js +5 -9
- package/dist/cli/ui/framework.d.ts +2 -0
- package/dist/cli/ui/framework.js +53 -22
- package/dist/cli/ui/mouse.d.ts +3 -1
- package/dist/cli/ui/mouse.js +3 -3
- package/package.json +7 -6
- package/scripts/verify-driver.mjs +34 -0
- package/dist/cli/workflow-state.d.ts +0 -58
- package/dist/cli/workflow-state.js +0 -195
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
|
+
applyTreeseedConfigValues,
|
|
2
3
|
applyTreeseedSafeRepairs,
|
|
3
4
|
collectTreeseedConfigContext,
|
|
5
|
+
ensureTreeseedActVerificationTooling,
|
|
6
|
+
ensureTreeseedSecretSessionForConfig,
|
|
4
7
|
findNearestTreeseedRoot
|
|
5
8
|
} from "@treeseed/sdk/workflow-support";
|
|
6
9
|
import { fail, guidedResult } from "./utils.js";
|
|
7
10
|
import { buildCliConfigPages, runCliConfigEditor } from "./config-ui.js";
|
|
11
|
+
import { promptForNewPassphrase, promptHidden } from "./secret-prompts.js";
|
|
8
12
|
import { createWorkflowSdk, renderWorkflowNextSteps, workflowErrorResult } from "./workflow.js";
|
|
9
13
|
function normalizeConfigScopes(value) {
|
|
10
14
|
const requested = Array.isArray(value) ? value.map(String) : typeof value === "string" ? [value] : ["all"];
|
|
@@ -31,11 +35,85 @@ function formatPrintEnvReports(payload) {
|
|
|
31
35
|
}
|
|
32
36
|
return lines.filter((line, index, all) => !(line === "" && all[index - 1] === ""));
|
|
33
37
|
}
|
|
38
|
+
function resolveMouseEnabled(value, env) {
|
|
39
|
+
if (value === true) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
const envValue = env.TREESEED_UI_MOUSE;
|
|
43
|
+
return envValue === "1" || envValue === "true";
|
|
44
|
+
}
|
|
45
|
+
function configReadinessLabel(readiness) {
|
|
46
|
+
if (!readiness) {
|
|
47
|
+
return "pending";
|
|
48
|
+
}
|
|
49
|
+
if (typeof readiness.phase === "string" && readiness.phase.length > 0) {
|
|
50
|
+
return readiness.phase;
|
|
51
|
+
}
|
|
52
|
+
if (readiness.deployable) {
|
|
53
|
+
return "deployable";
|
|
54
|
+
}
|
|
55
|
+
if (readiness.provisioned) {
|
|
56
|
+
return "provisioned";
|
|
57
|
+
}
|
|
58
|
+
if (readiness.configured) {
|
|
59
|
+
return "config_complete";
|
|
60
|
+
}
|
|
61
|
+
return "pending";
|
|
62
|
+
}
|
|
63
|
+
function describeSecretBootstrap(secretSession) {
|
|
64
|
+
if (!secretSession?.status) {
|
|
65
|
+
return "(unknown)";
|
|
66
|
+
}
|
|
67
|
+
if (secretSession.createdWrappedKey) {
|
|
68
|
+
return secretSession.unlockSource === "env" ? "created via env passphrase" : "created via interactive passphrase";
|
|
69
|
+
}
|
|
70
|
+
if (secretSession.migratedWrappedKey) {
|
|
71
|
+
return secretSession.unlockSource === "env" ? "migrated via env passphrase" : "migrated via interactive passphrase";
|
|
72
|
+
}
|
|
73
|
+
if (secretSession.unlockSource === "existing-session") {
|
|
74
|
+
return secretSession.status.unlocked ? "already unlocked" : "locked";
|
|
75
|
+
}
|
|
76
|
+
return secretSession.unlockSource === "env" ? "unlocked via env passphrase" : "unlocked interactively";
|
|
77
|
+
}
|
|
78
|
+
function describeInteractiveSecretBootstrap(secretSession) {
|
|
79
|
+
if (!secretSession?.status) {
|
|
80
|
+
return void 0;
|
|
81
|
+
}
|
|
82
|
+
if (secretSession.createdWrappedKey) {
|
|
83
|
+
return "Wrapped machine key created and unlocked.";
|
|
84
|
+
}
|
|
85
|
+
if (secretSession.migratedWrappedKey) {
|
|
86
|
+
return "Legacy machine key wrapped and unlocked.";
|
|
87
|
+
}
|
|
88
|
+
if (secretSession.unlockSource === "interactive") {
|
|
89
|
+
return "Wrapped machine key unlocked.";
|
|
90
|
+
}
|
|
91
|
+
if (secretSession.unlockSource === "env") {
|
|
92
|
+
return "Wrapped machine key unlocked from TREESEED_KEY_PASSPHRASE.";
|
|
93
|
+
}
|
|
94
|
+
return void 0;
|
|
95
|
+
}
|
|
96
|
+
function describeSharedStorageMigrations(migrations) {
|
|
97
|
+
if (!Array.isArray(migrations) || migrations.length === 0) {
|
|
98
|
+
return void 0;
|
|
99
|
+
}
|
|
100
|
+
return migrations.map((migration) => {
|
|
101
|
+
const label = typeof migration.label === "string" && migration.label.length > 0 ? migration.label : migration.entryId;
|
|
102
|
+
const promotedFrom = typeof migration.promotedFrom === "string" ? migration.promotedFrom : "staging";
|
|
103
|
+
const scopes = Array.isArray(migration.consolidatedScopes) && migration.consolidatedScopes.length > 0 ? migration.consolidatedScopes.join("/") : promotedFrom;
|
|
104
|
+
return migration.hadConflicts ? `${label} consolidated from ${scopes} using ${promotedFrom}` : `${label} promoted from ${promotedFrom}`;
|
|
105
|
+
}).join("; ");
|
|
106
|
+
}
|
|
34
107
|
function renderConfigResult(commandName, result) {
|
|
35
108
|
const payload = result.payload;
|
|
36
109
|
const toolHealth = payload.toolHealth;
|
|
110
|
+
const configContext = payload.context;
|
|
111
|
+
const configReadiness = configContext?.configReadinessByScope?.local ?? {};
|
|
37
112
|
const readinessByScope = payload.result?.readinessByScope ?? {};
|
|
38
|
-
const
|
|
113
|
+
const secretSession = payload.secretSession;
|
|
114
|
+
const sharedStorageMigrations = payload.result?.sharedStorageMigrations;
|
|
115
|
+
const summary = payload.mode === "print-env-only" ? "Treeseed config environment report completed." : payload.mode === "rotate-machine-key" ? "Treeseed machine key rotated successfully." : payload.mode === "connect-market" ? "Knowledge Coop pairing completed successfully." : "Treeseed config completed successfully.";
|
|
116
|
+
const market = payload.market;
|
|
39
117
|
return guidedResult({
|
|
40
118
|
command: commandName,
|
|
41
119
|
summary,
|
|
@@ -46,13 +124,28 @@ function renderConfigResult(commandName, result) {
|
|
|
46
124
|
{ label: "Safe repairs", value: Array.isArray(payload.repairs) ? payload.repairs.length : 0 },
|
|
47
125
|
{ label: "Machine config", value: payload.configPath },
|
|
48
126
|
{ label: "Machine key", value: payload.keyPath },
|
|
49
|
-
{ label: "
|
|
50
|
-
{ label: "
|
|
51
|
-
{ label: "
|
|
127
|
+
{ label: "Secrets session", value: describeSecretBootstrap(secretSession) },
|
|
128
|
+
{ label: "Shared consolidations", value: describeSharedStorageMigrations(sharedStorageMigrations) },
|
|
129
|
+
{ label: "Local readiness", value: configReadinessLabel(readinessByScope.local) },
|
|
130
|
+
{ label: "Staging readiness", value: configReadinessLabel(readinessByScope.staging) },
|
|
131
|
+
{ label: "Prod readiness", value: configReadinessLabel(readinessByScope.prod) },
|
|
132
|
+
{ label: "GitHub token/config", value: configReadiness.github?.configured ? "configured" : "missing" },
|
|
133
|
+
{ label: "Cloudflare token/config", value: configReadiness.cloudflare?.configured ? "configured" : "missing" },
|
|
134
|
+
{ label: "Railway token/config", value: configReadiness.railway?.configured ? "configured" : "missing" },
|
|
52
135
|
{ label: "GitHub CLI", value: toolHealth?.githubCli?.available ? "ready" : "missing" },
|
|
136
|
+
{ label: "Wrangler CLI", value: toolHealth?.wranglerCli?.available ? "ready" : "missing" },
|
|
137
|
+
{ label: "Railway CLI", value: toolHealth?.railwayCli?.available ? "ready" : "missing" },
|
|
53
138
|
{ label: "gh act", value: toolHealth?.ghActExtension?.available ? "ready" : "missing" },
|
|
54
139
|
{ label: "Docker", value: toolHealth?.dockerDaemon?.available ? "ready" : "missing" },
|
|
55
|
-
{ label: "ACT verify", value: toolHealth?.actVerificationReady ? "ready" : "not ready" }
|
|
140
|
+
{ label: "ACT verify", value: toolHealth?.actVerificationReady ? "ready" : "not ready" },
|
|
141
|
+
...market ? [
|
|
142
|
+
{ label: "Market base URL", value: market.baseUrl ?? "(none)" },
|
|
143
|
+
{ label: "Market team", value: market.teamSlug ?? market.teamId ?? "(none)" },
|
|
144
|
+
{ label: "Market project", value: market.projectSlug ?? market.projectId ?? "(none)" },
|
|
145
|
+
{ label: "Hub mode", value: market.hubMode ?? "(unknown)" },
|
|
146
|
+
{ label: "Runtime mode", value: market.runtimeMode ?? "(unknown)" },
|
|
147
|
+
{ label: "Runtime ready", value: market.runtimeReady ? "yes" : "no" }
|
|
148
|
+
] : []
|
|
56
149
|
],
|
|
57
150
|
nextSteps: renderWorkflowNextSteps(result),
|
|
58
151
|
report: payload
|
|
@@ -66,35 +159,86 @@ const handleConfig = async (invocation, context) => {
|
|
|
66
159
|
});
|
|
67
160
|
const scopes = normalizeConfigScopes(invocation.args.environment);
|
|
68
161
|
const sync = invocation.args.sync;
|
|
69
|
-
const interactive = context.outputFormat !== "json" && process.stdin.isTTY && process.stdout.isTTY;
|
|
70
|
-
|
|
162
|
+
const interactive = context.outputFormat !== "json" && context.interactiveUi !== false && process.stdin.isTTY && process.stdout.isTTY;
|
|
163
|
+
const nonInteractive = invocation.args.nonInteractive === true || context.outputFormat === "json";
|
|
164
|
+
const operationalMode = invocation.args.printEnvOnly === true || invocation.args.rotateMachineKey === true || invocation.args.connectMarket === true;
|
|
165
|
+
if (!interactive && !nonInteractive && !operationalMode) {
|
|
166
|
+
return fail("Treeseed config requires a TTY for the interactive editor. Re-run in a terminal, or use --non-interactive, --json, --print-env-only, --rotate-machine-key, or --connect-market.");
|
|
167
|
+
}
|
|
168
|
+
if (interactive && !nonInteractive && !operationalMode) {
|
|
71
169
|
const tenantRoot = findNearestTreeseedRoot(context.cwd) ?? context.cwd;
|
|
72
170
|
if (!tenantRoot) {
|
|
73
171
|
return fail("Treeseed config requires a Treeseed project. Run the command from inside a tenant or initialize one first.");
|
|
74
172
|
}
|
|
75
173
|
applyTreeseedSafeRepairs(tenantRoot);
|
|
174
|
+
const toolAvailability = ensureTreeseedActVerificationTooling({
|
|
175
|
+
tenantRoot,
|
|
176
|
+
installIfMissing: invocation.args.installMissingTooling === true,
|
|
177
|
+
env: context.env,
|
|
178
|
+
write: context.write
|
|
179
|
+
});
|
|
180
|
+
const secretSession = await ensureTreeseedSecretSessionForConfig({
|
|
181
|
+
tenantRoot,
|
|
182
|
+
interactive: true,
|
|
183
|
+
env: context.env,
|
|
184
|
+
promptForPassphrase: () => promptHidden("Treeseed passphrase: "),
|
|
185
|
+
promptForNewPassphrase
|
|
186
|
+
});
|
|
76
187
|
const configContext = collectTreeseedConfigContext({
|
|
77
188
|
tenantRoot,
|
|
78
189
|
scopes,
|
|
79
190
|
env: context.env
|
|
80
191
|
});
|
|
192
|
+
const initialViewMode = (() => {
|
|
193
|
+
if (invocation.args.full === true) {
|
|
194
|
+
return "full";
|
|
195
|
+
}
|
|
196
|
+
return buildCliConfigPages(configContext, "local", {}, "startup").length > 0 ? "startup" : "full";
|
|
197
|
+
})();
|
|
81
198
|
const editorResult = await runCliConfigEditor(configContext, {
|
|
82
|
-
initialViewMode
|
|
199
|
+
initialViewMode,
|
|
200
|
+
mouseEnabled: resolveMouseEnabled(invocation.args.mouse, context.env),
|
|
201
|
+
initialStatusMessage: describeInteractiveSecretBootstrap(secretSession),
|
|
202
|
+
toolAvailability,
|
|
203
|
+
secretSession,
|
|
204
|
+
onCommit: async (update) => {
|
|
205
|
+
applyTreeseedConfigValues({
|
|
206
|
+
tenantRoot,
|
|
207
|
+
updates: [{
|
|
208
|
+
scope: update.scope,
|
|
209
|
+
entryId: update.entryId,
|
|
210
|
+
value: update.value,
|
|
211
|
+
reused: false
|
|
212
|
+
}]
|
|
213
|
+
});
|
|
214
|
+
return collectTreeseedConfigContext({
|
|
215
|
+
tenantRoot,
|
|
216
|
+
scopes,
|
|
217
|
+
env: context.env
|
|
218
|
+
});
|
|
219
|
+
}
|
|
83
220
|
});
|
|
84
221
|
if (editorResult === null) {
|
|
85
222
|
return fail("Treeseed config canceled.");
|
|
86
223
|
}
|
|
87
|
-
const
|
|
224
|
+
const refreshedContext = collectTreeseedConfigContext({
|
|
225
|
+
tenantRoot,
|
|
226
|
+
scopes,
|
|
227
|
+
env: context.env
|
|
228
|
+
});
|
|
229
|
+
const updates = refreshedContext.scopes.flatMap((scope) => buildCliConfigPages(refreshedContext, scope, editorResult.overrides, "full")).filter((page, index, allPages) => allPages.findIndex((candidate) => candidate.key === page.key) === index).map((page) => ({
|
|
88
230
|
scope: page.scope,
|
|
89
231
|
entryId: page.entry.id,
|
|
90
232
|
value: page.finalValue,
|
|
91
233
|
reused: !(page.key in editorResult.overrides)
|
|
92
234
|
}));
|
|
235
|
+
context.write("Applying config updates, validating environments, and syncing managed providers...", "stdout");
|
|
93
236
|
const result2 = await workflow.config({
|
|
94
237
|
environment: scopes,
|
|
95
238
|
sync,
|
|
96
239
|
printEnv: invocation.args.printEnv === true,
|
|
97
240
|
showSecrets: invocation.args.showSecrets === true,
|
|
241
|
+
installMissingTooling: invocation.args.installMissingTooling === true,
|
|
98
242
|
nonInteractive: true,
|
|
99
243
|
updates
|
|
100
244
|
});
|
|
@@ -107,7 +251,17 @@ const handleConfig = async (invocation, context) => {
|
|
|
107
251
|
printEnvOnly: invocation.args.printEnvOnly === true,
|
|
108
252
|
showSecrets: invocation.args.showSecrets === true,
|
|
109
253
|
rotateMachineKey: invocation.args.rotateMachineKey === true,
|
|
110
|
-
|
|
254
|
+
connectMarket: invocation.args.connectMarket === true,
|
|
255
|
+
marketBaseUrl: typeof invocation.args.marketBaseUrl === "string" ? invocation.args.marketBaseUrl : void 0,
|
|
256
|
+
marketTeamId: typeof invocation.args.marketTeamId === "string" ? invocation.args.marketTeamId : void 0,
|
|
257
|
+
marketTeamSlug: typeof invocation.args.marketTeamSlug === "string" ? invocation.args.marketTeamSlug : void 0,
|
|
258
|
+
marketProjectId: typeof invocation.args.marketProjectId === "string" ? invocation.args.marketProjectId : void 0,
|
|
259
|
+
marketProjectSlug: typeof invocation.args.marketProjectSlug === "string" ? invocation.args.marketProjectSlug : void 0,
|
|
260
|
+
marketProjectApiBaseUrl: typeof invocation.args.marketProjectApiBaseUrl === "string" ? invocation.args.marketProjectApiBaseUrl : void 0,
|
|
261
|
+
marketAccessToken: typeof invocation.args.marketAccessToken === "string" ? invocation.args.marketAccessToken : void 0,
|
|
262
|
+
rotateRunnerToken: invocation.args.rotateRunnerToken === true,
|
|
263
|
+
installMissingTooling: invocation.args.installMissingTooling === true,
|
|
264
|
+
nonInteractive
|
|
111
265
|
});
|
|
112
266
|
if (context.outputFormat !== "json" && result.payload.mode === "print-env-only") {
|
|
113
267
|
return {
|
|
@@ -24,6 +24,7 @@ const handleDestroy = async (invocation, context) => {
|
|
|
24
24
|
}
|
|
25
25
|
}).destroy({
|
|
26
26
|
environment: String(invocation.args.environment),
|
|
27
|
+
plan: invocation.args.plan === true || invocation.args.dryRun === true,
|
|
27
28
|
dryRun: invocation.args.dryRun === true,
|
|
28
29
|
force: invocation.args.force === true,
|
|
29
30
|
removeBuildArtifacts: invocation.args.removeBuildArtifacts === true
|
|
@@ -31,14 +32,14 @@ const handleDestroy = async (invocation, context) => {
|
|
|
31
32
|
const payload = result.payload;
|
|
32
33
|
return guidedResult({
|
|
33
34
|
command: invocation.commandName || "destroy",
|
|
34
|
-
summary:
|
|
35
|
+
summary: result.executionMode === "plan" ? "Treeseed destroy plan ready." : "Treeseed destroy completed successfully.",
|
|
35
36
|
facts: [
|
|
36
37
|
{ label: "Environment", value: payload.scope },
|
|
37
38
|
{ label: "Dry run", value: payload.dryRun ? "yes" : "no" },
|
|
38
39
|
{ label: "Removed build artifacts", value: payload.removeBuildArtifacts ? "yes" : "no" }
|
|
39
40
|
],
|
|
40
41
|
nextSteps: renderWorkflowNextSteps(result),
|
|
41
|
-
report:
|
|
42
|
+
report: result
|
|
42
43
|
});
|
|
43
44
|
} catch (error) {
|
|
44
45
|
return workflowErrorResult(error);
|
package/dist/cli/handlers/dev.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { resolveTreeseedLaunchEnvironment } from "@treeseed/sdk/workflow-support";
|
|
4
5
|
import { workflowErrorResult } from "./workflow.js";
|
|
5
6
|
const require2 = createRequire(import.meta.url);
|
|
6
7
|
function resolveCoreDevEntrypoint(cwd) {
|
|
@@ -46,7 +47,11 @@ const handleDev = async (invocation, context) => {
|
|
|
46
47
|
const args = watch ? [...resolved.args, "--watch"] : resolved.args;
|
|
47
48
|
const result = context.spawn(resolved.command, args, {
|
|
48
49
|
cwd: context.cwd,
|
|
49
|
-
env:
|
|
50
|
+
env: resolveTreeseedLaunchEnvironment({
|
|
51
|
+
tenantRoot: context.cwd,
|
|
52
|
+
scope: "local",
|
|
53
|
+
baseEnv: { ...process.env, ...context.env ?? {} }
|
|
54
|
+
}),
|
|
50
55
|
stdio: "inherit"
|
|
51
56
|
});
|
|
52
57
|
return {
|
|
@@ -1,54 +1,89 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
1
3
|
import { collectCliPreflight } from "@treeseed/sdk/workflow-support";
|
|
2
4
|
import { guidedResult } from "./utils.js";
|
|
3
|
-
import { resolveTreeseedWorkflowState } from "../workflow-state.js";
|
|
4
5
|
import { applyTreeseedSafeRepairs } from "../repair.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
6
|
+
import { createWorkflowSdk, workflowErrorResult } from "./workflow.js";
|
|
7
|
+
const handleDoctor = async (invocation, context) => {
|
|
8
|
+
try {
|
|
9
|
+
const status = await createWorkflowSdk(context).status();
|
|
10
|
+
const state = status.payload;
|
|
11
|
+
const performedFixes = invocation.args.fix === true && state.deployConfigPresent ? applyTreeseedSafeRepairs(state.cwd) : [];
|
|
12
|
+
const preflight = collectCliPreflight({ cwd: context.cwd, requireAuth: false });
|
|
13
|
+
const railwayManagedServicesEnabled = Object.values(state.managedServices).some((service) => service.enabled);
|
|
14
|
+
const mustFixNow = [];
|
|
15
|
+
const optional = [];
|
|
16
|
+
if (!state.workspaceRoot) mustFixNow.push("Run Treeseed inside a Treeseed workspace so package commands and workflow state can resolve correctly.");
|
|
17
|
+
if (!state.repoRoot) mustFixNow.push("Initialize or clone the git repository before using save, close, stage, or release flows.");
|
|
18
|
+
if (!state.deployConfigPresent) mustFixNow.push("Create or restore treeseed.site.yaml so the tenant contract can be loaded.");
|
|
19
|
+
if (preflight.missingCommands.includes("git")) mustFixNow.push("Install Git.");
|
|
20
|
+
if (preflight.missingCommands.includes("npm")) mustFixNow.push("Install npm 10 or newer.");
|
|
21
|
+
if (!state.files.machineConfig) mustFixNow.push("Run `treeseed config --environment local` to create the local machine config.");
|
|
22
|
+
if (!state.secrets.wrappedKeyPresent) mustFixNow.push("Run `treeseed secrets:unlock` to create the wrapped machine key used for local secret storage.");
|
|
23
|
+
if (state.secrets.migrationRequired) mustFixNow.push("Run `treeseed secrets:migrate-key` to replace the legacy plaintext machine key with the wrapped format.");
|
|
24
|
+
if (state.packageSync.blockers.length > 0) mustFixNow.push(...state.packageSync.blockers);
|
|
25
|
+
if (state.workflowControl.lock.active && state.workflowControl.lock.runId) {
|
|
26
|
+
mustFixNow.push(`Active workflow lock detected for run ${state.workflowControl.lock.runId}. Use \`treeseed recover\` before starting another mutating command.`);
|
|
27
|
+
}
|
|
28
|
+
if (state.workflowControl.interruptedRuns.length > 0) {
|
|
29
|
+
mustFixNow.push(`Interrupted workflow runs detected. Resume the latest run with \`treeseed resume ${state.workflowControl.interruptedRuns[0].runId}\` or inspect \`treeseed recover\`.`);
|
|
30
|
+
}
|
|
31
|
+
if (state.packageSync.completeCheckout) {
|
|
32
|
+
for (const repo of state.packageSync.repos) {
|
|
33
|
+
const publishWorkflowPath = resolve(state.cwd, repo.path, ".github", "workflows", "publish.yml");
|
|
34
|
+
if (!existsSync(publishWorkflowPath)) {
|
|
35
|
+
mustFixNow.push(`${repo.name} is missing .github/workflows/publish.yml required for recursive release.`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for (const workflowName of ["verify.yml", "deploy.yml"]) {
|
|
40
|
+
const workflowPath = resolve(state.cwd, ".github", "workflows", workflowName);
|
|
41
|
+
if (!existsSync(workflowPath)) {
|
|
42
|
+
mustFixNow.push(`Missing root workflow contract .github/workflows/${workflowName}.`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!state.auth.gh) optional.push("Configure GitHub token/config (`GH_TOKEN`) for GitHub CLI automation and Copilot-backed workflows.");
|
|
46
|
+
if (!state.auth.wrangler) optional.push("Configure Cloudflare token/config (`CLOUDFLARE_API_TOKEN`) before staging, preview, or production deployment work.");
|
|
47
|
+
if (!state.auth.railway && railwayManagedServicesEnabled) {
|
|
48
|
+
optional.push("Configure Railway token/config (`RAILWAY_API_TOKEN`) before deploying the managed Railway services.");
|
|
49
|
+
}
|
|
50
|
+
if (!state.auth.remoteApi && state.managedServices.api.enabled) {
|
|
51
|
+
optional.push("Run `treeseed auth:login` so the CLI can use the configured remote API.");
|
|
52
|
+
}
|
|
53
|
+
if (state.secrets.wrappedKeyPresent && !state.secrets.keyAgentUnlocked) {
|
|
54
|
+
optional.push("Run `treeseed secrets:unlock` before starting secret-backed dev, deploy, or runner commands.");
|
|
55
|
+
}
|
|
56
|
+
if (!state.secrets.keyAgentRunning) {
|
|
57
|
+
optional.push("The Treeseed key-agent is not running yet. It will start automatically when you unlock the secret session.");
|
|
58
|
+
}
|
|
59
|
+
if (!state.auth.copilot) optional.push("Configure `GH_TOKEN` if you rely on local Copilot-assisted workflows.");
|
|
60
|
+
return guidedResult({
|
|
61
|
+
command: "doctor",
|
|
62
|
+
summary: mustFixNow.length === 0 ? "Treeseed doctor found no blocking issues." : "Treeseed doctor found issues that need attention.",
|
|
63
|
+
facts: [
|
|
64
|
+
{ label: "Must fix now", value: mustFixNow.length },
|
|
65
|
+
{ label: "Optional follow-up", value: optional.length },
|
|
66
|
+
{ label: "Safe fixes applied", value: performedFixes.length },
|
|
67
|
+
{ label: "Branch", value: state.branchName ?? "(none)" },
|
|
68
|
+
{ label: "Workspace root", value: state.workspaceRoot ? "yes" : "no" }
|
|
69
|
+
],
|
|
70
|
+
nextSteps: [
|
|
71
|
+
...mustFixNow.map((item) => item),
|
|
72
|
+
...mustFixNow.length === 0 ? optional : optional.map((item) => `Optional: ${item}`)
|
|
73
|
+
],
|
|
74
|
+
report: {
|
|
75
|
+
...status,
|
|
76
|
+
state,
|
|
77
|
+
preflight,
|
|
78
|
+
performedFixes,
|
|
79
|
+
mustFixNow,
|
|
80
|
+
optional
|
|
81
|
+
},
|
|
82
|
+
exitCode: mustFixNow.length === 0 ? 0 : 1
|
|
83
|
+
});
|
|
84
|
+
} catch (error) {
|
|
85
|
+
return workflowErrorResult(error);
|
|
24
86
|
}
|
|
25
|
-
if (!state.auth.remoteApi && state.managedServices.api.enabled) {
|
|
26
|
-
optional.push("Run `treeseed auth:login` so the CLI can use the configured remote API.");
|
|
27
|
-
}
|
|
28
|
-
if (!state.auth.copilot) optional.push("Configure `GH_TOKEN` if you rely on local Copilot-assisted workflows.");
|
|
29
|
-
return guidedResult({
|
|
30
|
-
command: "doctor",
|
|
31
|
-
summary: mustFixNow.length === 0 ? "Treeseed doctor found no blocking issues." : "Treeseed doctor found issues that need attention.",
|
|
32
|
-
facts: [
|
|
33
|
-
{ label: "Must fix now", value: mustFixNow.length },
|
|
34
|
-
{ label: "Optional follow-up", value: optional.length },
|
|
35
|
-
{ label: "Safe fixes applied", value: performedFixes.length },
|
|
36
|
-
{ label: "Branch", value: state.branchName ?? "(none)" },
|
|
37
|
-
{ label: "Workspace root", value: state.workspaceRoot ? "yes" : "no" }
|
|
38
|
-
],
|
|
39
|
-
nextSteps: [
|
|
40
|
-
...mustFixNow.map((item) => item),
|
|
41
|
-
...mustFixNow.length === 0 ? optional : optional.map((item) => `Optional: ${item}`)
|
|
42
|
-
],
|
|
43
|
-
report: {
|
|
44
|
-
state,
|
|
45
|
-
preflight,
|
|
46
|
-
performedFixes,
|
|
47
|
-
mustFixNow,
|
|
48
|
-
optional
|
|
49
|
-
},
|
|
50
|
-
exitCode: mustFixNow.length === 0 ? 0 : 1
|
|
51
|
-
});
|
|
52
87
|
};
|
|
53
88
|
export {
|
|
54
89
|
handleDoctor
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { guidedResult } from "./utils.js";
|
|
2
|
+
import { createWorkflowSdk, renderWorkflowNextSteps, workflowErrorResult } from "./workflow.js";
|
|
3
|
+
const handleRecover = async (_invocation, context) => {
|
|
4
|
+
try {
|
|
5
|
+
const result = await createWorkflowSdk(context).recover({});
|
|
6
|
+
const payload = result.payload;
|
|
7
|
+
return guidedResult({
|
|
8
|
+
command: "recover",
|
|
9
|
+
summary: payload.interruptedRuns.length > 0 || payload.lock.active ? "Treeseed recover found workflow state that may need attention." : "Treeseed recover found no active locks or interrupted runs.",
|
|
10
|
+
facts: [
|
|
11
|
+
{ label: "Active lock", value: payload.lock.active ? "yes" : "no" },
|
|
12
|
+
{ label: "Stale lock", value: payload.lock.stale ? "yes" : "no" },
|
|
13
|
+
{ label: "Interrupted runs", value: payload.interruptedRuns.length },
|
|
14
|
+
{ label: "Recorded runs", value: payload.runCount }
|
|
15
|
+
],
|
|
16
|
+
nextSteps: renderWorkflowNextSteps(result),
|
|
17
|
+
report: result
|
|
18
|
+
});
|
|
19
|
+
} catch (error) {
|
|
20
|
+
return workflowErrorResult(error);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
export {
|
|
24
|
+
handleRecover
|
|
25
|
+
};
|
|
@@ -3,23 +3,33 @@ import { createWorkflowSdk, renderWorkflowNextSteps, workflowErrorResult } from
|
|
|
3
3
|
const handleRelease = async (invocation, context) => {
|
|
4
4
|
try {
|
|
5
5
|
const bump = ["major", "minor", "patch"].find((candidate) => invocation.args[candidate] === true) ?? "patch";
|
|
6
|
-
const result = await createWorkflowSdk(context).release({
|
|
6
|
+
const result = await createWorkflowSdk(context).release({
|
|
7
|
+
bump,
|
|
8
|
+
plan: invocation.args.plan === true || invocation.args.dryRun === true,
|
|
9
|
+
dryRun: invocation.args.dryRun === true
|
|
10
|
+
});
|
|
7
11
|
const payload = result.payload;
|
|
12
|
+
const completedPublishes = payload.publishWait.filter((entry) => entry.status === "completed").length;
|
|
8
13
|
return guidedResult({
|
|
9
14
|
command: invocation.commandName || "release",
|
|
10
|
-
summary: "Treeseed release completed successfully.",
|
|
15
|
+
summary: result.executionMode === "plan" ? "Treeseed release plan ready." : "Treeseed release completed successfully.",
|
|
11
16
|
facts: [
|
|
17
|
+
{ label: "Mode", value: payload.mode },
|
|
12
18
|
{ label: "Staging branch", value: payload.stagingBranch },
|
|
13
19
|
{ label: "Production branch", value: payload.productionBranch },
|
|
20
|
+
{ label: "Merge strategy", value: payload.mergeStrategy },
|
|
14
21
|
{ label: "Release level", value: payload.level },
|
|
15
22
|
{ label: "Root version", value: payload.rootVersion },
|
|
16
23
|
{ label: "Release tag", value: payload.releaseTag },
|
|
17
24
|
{ label: "Released commit", value: payload.releasedCommit.slice(0, 12) },
|
|
18
|
-
{ label: "
|
|
25
|
+
{ label: "Changed packages", value: String(payload.packageSelection.changed.length) },
|
|
26
|
+
{ label: "Dependent packages", value: String(payload.packageSelection.dependents.length) },
|
|
27
|
+
{ label: "Released packages", value: String(payload.touchedPackages.length) },
|
|
28
|
+
{ label: "Publish waits", value: String(completedPublishes) },
|
|
19
29
|
{ label: "Final branch", value: payload.finalBranch }
|
|
20
30
|
],
|
|
21
31
|
nextSteps: renderWorkflowNextSteps(result),
|
|
22
|
-
report:
|
|
32
|
+
report: result
|
|
23
33
|
});
|
|
24
34
|
} catch (error) {
|
|
25
35
|
return workflowErrorResult(error);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { guidedResult } from "./utils.js";
|
|
2
|
+
import { createWorkflowSdk, renderWorkflowNextSteps, workflowErrorResult } from "./workflow.js";
|
|
3
|
+
const handleResume = async (invocation, context) => {
|
|
4
|
+
try {
|
|
5
|
+
const runId = invocation.positionals[0] ?? "";
|
|
6
|
+
const result = await createWorkflowSdk(context).resume({ runId });
|
|
7
|
+
return guidedResult({
|
|
8
|
+
command: invocation.commandName || "resume",
|
|
9
|
+
summary: `Resumed workflow run ${runId}.`,
|
|
10
|
+
facts: [
|
|
11
|
+
{ label: "Run", value: result.runId ?? runId },
|
|
12
|
+
{ label: "Command", value: result.command }
|
|
13
|
+
],
|
|
14
|
+
nextSteps: renderWorkflowNextSteps(result),
|
|
15
|
+
report: result
|
|
16
|
+
});
|
|
17
|
+
} catch (error) {
|
|
18
|
+
return workflowErrorResult(error);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
export {
|
|
22
|
+
handleResume
|
|
23
|
+
};
|
|
@@ -5,22 +5,28 @@ const handleSave = async (invocation, context) => {
|
|
|
5
5
|
const result = await createWorkflowSdk(context).save({
|
|
6
6
|
message: invocation.positionals.join(" ").trim(),
|
|
7
7
|
hotfix: invocation.args.hotfix === true,
|
|
8
|
-
preview: invocation.args.preview === true
|
|
8
|
+
preview: invocation.args.preview === true,
|
|
9
|
+
plan: invocation.args.plan === true || invocation.args.dryRun === true,
|
|
10
|
+
dryRun: invocation.args.dryRun === true
|
|
9
11
|
});
|
|
10
12
|
const payload = result.payload;
|
|
13
|
+
const savedRepos = (payload.repos ?? []).filter((repo) => repo.committed || repo.pushed).map((repo) => `${repo.name}@${String(repo.commitSha ?? "").slice(0, 12)}`).join(", ");
|
|
11
14
|
return guidedResult({
|
|
12
15
|
command: invocation.commandName || "save",
|
|
13
|
-
summary: payload.noChanges ? "Treeseed save found no new changes and confirmed branch sync." : "Treeseed save completed successfully.",
|
|
16
|
+
summary: result.executionMode === "plan" ? "Treeseed save plan ready." : payload.noChanges ? "Treeseed save found no new changes and confirmed branch sync." : "Treeseed save completed successfully.",
|
|
14
17
|
facts: [
|
|
18
|
+
{ label: "Mode", value: payload.mode },
|
|
15
19
|
{ label: "Branch", value: payload.branch },
|
|
16
20
|
{ label: "Environment scope", value: payload.scope },
|
|
17
21
|
{ label: "Hotfix", value: payload.hotfix ? "yes" : "no" },
|
|
18
22
|
{ label: "Commit", value: payload.commitSha.slice(0, 12) },
|
|
19
23
|
{ label: "Commit created", value: payload.commitCreated ? "yes" : "no" },
|
|
24
|
+
{ label: "Workspace repos", value: savedRepos || ((payload.repos ?? []).length > 0 ? "none saved" : "not applicable") },
|
|
25
|
+
{ label: "Market pushed", value: payload.rootRepo?.pushed ? "yes" : "no" },
|
|
20
26
|
{ label: "Preview action", value: payload.previewAction?.status ?? "skipped" }
|
|
21
27
|
],
|
|
22
28
|
nextSteps: renderWorkflowNextSteps(result),
|
|
23
|
-
report:
|
|
29
|
+
report: result
|
|
24
30
|
});
|
|
25
31
|
} catch (error) {
|
|
26
32
|
return workflowErrorResult(error);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
function promptHidden(question) {
|
|
2
|
+
return new Promise((resolvePromise) => {
|
|
3
|
+
const stdin = process.stdin;
|
|
4
|
+
const stdout = process.stdout;
|
|
5
|
+
let value = "";
|
|
6
|
+
function cleanup() {
|
|
7
|
+
stdin.removeListener("data", onData);
|
|
8
|
+
if (stdin.isTTY) {
|
|
9
|
+
stdin.setRawMode(false);
|
|
10
|
+
}
|
|
11
|
+
stdout.write("\n");
|
|
12
|
+
}
|
|
13
|
+
function onData(chunk) {
|
|
14
|
+
const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk;
|
|
15
|
+
for (const char of text) {
|
|
16
|
+
if (char === "\n" || char === "\r") {
|
|
17
|
+
cleanup();
|
|
18
|
+
resolvePromise(value);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (char === "") {
|
|
22
|
+
cleanup();
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
if (char === "\x7F") {
|
|
26
|
+
value = value.slice(0, -1);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
value += char;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
stdout.write(question);
|
|
33
|
+
if (stdin.isTTY) {
|
|
34
|
+
stdin.setRawMode(true);
|
|
35
|
+
}
|
|
36
|
+
stdin.resume();
|
|
37
|
+
stdin.on("data", onData);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
async function promptForNewPassphrase() {
|
|
41
|
+
const passphrase = (await promptHidden("New Treeseed passphrase: ")).trim();
|
|
42
|
+
if (!passphrase) {
|
|
43
|
+
throw new Error("A non-empty passphrase is required.");
|
|
44
|
+
}
|
|
45
|
+
const confirmation = (await promptHidden("Confirm passphrase: ")).trim();
|
|
46
|
+
if (passphrase !== confirmation) {
|
|
47
|
+
throw new Error("The passphrase confirmation did not match.");
|
|
48
|
+
}
|
|
49
|
+
return passphrase;
|
|
50
|
+
}
|
|
51
|
+
export {
|
|
52
|
+
promptForNewPassphrase,
|
|
53
|
+
promptHidden
|
|
54
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { TreeseedCommandHandler } from '../types.js';
|
|
2
|
+
export declare const handleSecretsStatus: TreeseedCommandHandler;
|
|
3
|
+
export declare const handleSecretsUnlock: TreeseedCommandHandler;
|
|
4
|
+
export declare const handleSecretsLock: TreeseedCommandHandler;
|
|
5
|
+
export declare const handleSecretsMigrateKey: TreeseedCommandHandler;
|
|
6
|
+
export declare const handleSecretsRotatePassphrase: TreeseedCommandHandler;
|
|
7
|
+
export declare const handleSecretsRotateMachineKey: TreeseedCommandHandler;
|