@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.
Files changed (40) hide show
  1. package/README.md +35 -8
  2. package/dist/cli/handlers/auth-login.js +58 -46
  3. package/dist/cli/handlers/auth-logout.js +23 -11
  4. package/dist/cli/handlers/auth-whoami.js +27 -16
  5. package/dist/cli/handlers/close.js +9 -4
  6. package/dist/cli/handlers/config-ui.d.ts +65 -9
  7. package/dist/cli/handlers/config-ui.js +561 -175
  8. package/dist/cli/handlers/config.js +164 -10
  9. package/dist/cli/handlers/destroy.js +3 -2
  10. package/dist/cli/handlers/dev.js +6 -1
  11. package/dist/cli/handlers/doctor.js +82 -47
  12. package/dist/cli/handlers/recover.d.ts +2 -0
  13. package/dist/cli/handlers/recover.js +25 -0
  14. package/dist/cli/handlers/release.js +14 -4
  15. package/dist/cli/handlers/resume.d.ts +2 -0
  16. package/dist/cli/handlers/resume.js +23 -0
  17. package/dist/cli/handlers/save.js +9 -3
  18. package/dist/cli/handlers/secret-prompts.d.ts +2 -0
  19. package/dist/cli/handlers/secret-prompts.js +54 -0
  20. package/dist/cli/handlers/secrets.d.ts +7 -0
  21. package/dist/cli/handlers/secrets.js +161 -0
  22. package/dist/cli/handlers/stage.js +10 -4
  23. package/dist/cli/handlers/status.js +37 -5
  24. package/dist/cli/handlers/switch.js +11 -4
  25. package/dist/cli/handlers/tasks.js +1 -0
  26. package/dist/cli/handlers/utils.js +15 -8
  27. package/dist/cli/handlers/workflow.js +74 -3
  28. package/dist/cli/help-ui.js +1 -1
  29. package/dist/cli/operations-registry.js +200 -21
  30. package/dist/cli/registry.d.ts +8 -0
  31. package/dist/cli/registry.js +19 -1
  32. package/dist/cli/repair.js +5 -9
  33. package/dist/cli/ui/framework.d.ts +2 -0
  34. package/dist/cli/ui/framework.js +53 -22
  35. package/dist/cli/ui/mouse.d.ts +3 -1
  36. package/dist/cli/ui/mouse.js +3 -3
  37. package/package.json +7 -6
  38. package/scripts/verify-driver.mjs +34 -0
  39. package/dist/cli/workflow-state.d.ts +0 -58
  40. package/dist/cli/workflow-state.js +0 -195
package/README.md CHANGED
@@ -21,11 +21,14 @@ npm install @treeseed/cli @treeseed/core @treeseed/sdk
21
21
 
22
22
  Workflow guarantees:
23
23
 
24
- - `treeseed init`, `treeseed config`, and `treeseed release` resolve the project from nested directories and do not rely on the currently checked-out task branch.
25
- - `treeseed switch` requires a clean worktree before leaving the current branch and creates new task branches from the latest `staging`.
26
- - `treeseed save` is the canonical checkpoint command: it syncs the current branch with origin, succeeds even when no new changes exist, and can create or refresh preview deployments with `--preview`.
27
- - `treeseed stage` and `treeseed close` auto-save meaningful uncommitted task-branch changes before merge or cleanup, then leave the repository on `staging`.
28
- - `treeseed release` completes on `staging` after promoting `staging` into `main` and pushing the release tag.
24
+ - `treeseed` is the only supported project-management surface for market and any checked-out `packages/sdk`, `packages/core`, and `packages/cli` repos.
25
+ - `treeseed switch` requires clean worktrees, mirrors the task branch into checked-out package repos, and only pushes the market branch on branch creation.
26
+ - `treeseed save` is the canonical recursive checkpoint command: it verifies, commits, and pushes dirty package repos in dependency order before saving the market repo.
27
+ - `treeseed stage` squash-merges task branches into `staging` across package repos first, refreshes market submodule pointers to package `staging` heads, then stages the market repo.
28
+ - `treeseed close` recursively archives and deletes matching task branches across market and checked-out package repos.
29
+ - `treeseed release` only bumps, tags, and publishes changed packages plus internal dependents, then syncs market production to package `main` heads.
30
+ - Every mutating workflow command supports `--plan`; `--dry-run` is only an alias where it still exists for compatibility.
31
+ - Interrupted workflow runs are journaled under `.treeseed/workflow`; use `treeseed recover` to inspect them and `treeseed resume <run-id>` to continue a resumable run.
29
32
 
30
33
  After installation, the published binary is available as:
31
34
 
@@ -42,11 +45,13 @@ The main workflow commands exposed by the current CLI are:
42
45
  - `treeseed tasks [--json]`
43
46
  - `treeseed switch <branch-name> [--preview]`
44
47
  - `treeseed dev`
45
- - `treeseed save [--preview] "<commit message>"`
48
+ - `treeseed save [--preview] [--plan] "<commit message>"`
46
49
  - `treeseed stage "<resolution message>"`
47
50
  - `treeseed close "<close reason>"`
48
- - `treeseed release --major|--minor|--patch`
49
- - `treeseed destroy --environment <local|staging|prod>`
51
+ - `treeseed release --major|--minor|--patch [--plan]`
52
+ - `treeseed resume <run-id>`
53
+ - `treeseed recover`
54
+ - `treeseed destroy --environment <local|staging|prod> [--plan]`
50
55
 
51
56
  Support utilities such as `treeseed rollback`, `treeseed doctor`, `treeseed auth:*`, `treeseed template`, `treeseed sync`, `treeseed lint`, `treeseed test`, `treeseed build`, service helpers, and `treeseed agents ...` remain available.
52
57
 
@@ -57,14 +62,36 @@ Use `treeseed help` for the full command list and `treeseed help <command>` for
57
62
  ```bash
58
63
  treeseed status
59
64
  treeseed config
65
+ treeseed switch feature/search-improvements --plan
60
66
  treeseed switch feature/search-improvements --preview
61
67
  treeseed dev
62
68
  treeseed save --preview "feat: add search filters"
63
69
  treeseed stage "feat: add search filters"
64
70
  treeseed release --patch
71
+ treeseed recover
65
72
  treeseed status --json
66
73
  ```
67
74
 
75
+ ## Agent-Safe Workflow
76
+
77
+ Use planning mode before any destructive or multi-repo mutation:
78
+
79
+ ```bash
80
+ treeseed switch feature/search-improvements --plan --json
81
+ treeseed save --plan "feat: add search filters" --json
82
+ treeseed stage --plan "feat: add search filters" --json
83
+ treeseed release --patch --plan --json
84
+ ```
85
+
86
+ If a workflow stops partway through, inspect the journaled state and resume from the recorded run:
87
+
88
+ ```bash
89
+ treeseed recover
90
+ treeseed resume <run-id>
91
+ ```
92
+
93
+ In a full checked-out workspace, `treeseed tasks`, `treeseed status`, and `treeseed doctor` also report package-branch drift, dirty embedded repos, active workflow locks, and interrupted runs.
94
+
68
95
  ## Maintainer Workflow
69
96
 
70
97
  All package maintenance commands are npm-based and run from the `cli/` package root. This package verifies the published command surface, parser/help behavior, and packaged artifact shape.
@@ -1,66 +1,78 @@
1
1
  import { RemoteTreeseedAuthClient, RemoteTreeseedClient } from "@treeseed/sdk/remote";
2
2
  import {
3
3
  resolveTreeseedRemoteConfig,
4
- setTreeseedRemoteSession
4
+ setTreeseedRemoteSession,
5
+ TreeseedKeyAgentError
5
6
  } from "@treeseed/sdk/workflow-support";
6
7
  import { guidedResult } from "./utils.js";
7
8
  function sleep(ms) {
8
9
  return new Promise((resolve) => setTimeout(resolve, ms));
9
10
  }
10
11
  const handleAuthLogin = async (invocation, context) => {
11
- const tenantRoot = context.cwd;
12
- const remoteConfig = resolveTreeseedRemoteConfig(tenantRoot, context.env);
13
- const hostId = typeof invocation.args.host === "string" ? invocation.args.host : remoteConfig.activeHostId;
14
- const client = new RemoteTreeseedAuthClient(new RemoteTreeseedClient({
15
- ...remoteConfig,
16
- activeHostId: hostId
17
- }));
18
- const started = await client.startDeviceFlow({
19
- clientName: "treeseed-cli",
20
- scopes: ["auth:me", "sdk", "operations"]
21
- });
22
- if (context.outputFormat !== "json") {
23
- context.write(`Open ${started.verificationUriComplete}`, "stdout");
24
- context.write(`User code: ${started.userCode}`, "stdout");
25
- context.write("Waiting for approval...", "stdout");
26
- }
27
- const deadline = Date.parse(started.expiresAt);
28
- while (Date.now() < deadline) {
29
- const response = await client.pollDeviceFlow({ deviceCode: started.deviceCode });
30
- if (response.ok && response.status === "approved") {
31
- setTreeseedRemoteSession(tenantRoot, {
32
- hostId,
33
- accessToken: response.accessToken,
34
- refreshToken: response.refreshToken,
35
- expiresAt: response.expiresAt,
36
- principal: response.principal
37
- });
38
- return guidedResult({
39
- command: "auth:login",
40
- summary: "Treeseed API login completed successfully.",
41
- facts: [
42
- { label: "Host", value: hostId },
43
- { label: "Principal", value: response.principal.displayName ?? response.principal.id },
44
- { label: "Scopes", value: response.principal.scopes.join(", ") }
45
- ],
46
- report: {
12
+ try {
13
+ const tenantRoot = context.cwd;
14
+ const remoteConfig = resolveTreeseedRemoteConfig(tenantRoot, context.env);
15
+ const hostId = typeof invocation.args.host === "string" ? invocation.args.host : remoteConfig.activeHostId;
16
+ const client = new RemoteTreeseedAuthClient(new RemoteTreeseedClient({
17
+ ...remoteConfig,
18
+ activeHostId: hostId
19
+ }));
20
+ const started = await client.startDeviceFlow({
21
+ clientName: "treeseed-cli",
22
+ scopes: ["auth:me", "sdk", "operations"]
23
+ });
24
+ if (context.outputFormat !== "json") {
25
+ context.write(`Open ${started.verificationUriComplete}`, "stdout");
26
+ context.write(`User code: ${started.userCode}`, "stdout");
27
+ context.write("Waiting for approval...", "stdout");
28
+ }
29
+ const deadline = Date.parse(started.expiresAt);
30
+ while (Date.now() < deadline) {
31
+ const response = await client.pollDeviceFlow({ deviceCode: started.deviceCode });
32
+ if (response.ok && response.status === "approved") {
33
+ setTreeseedRemoteSession(tenantRoot, {
47
34
  hostId,
35
+ accessToken: response.accessToken,
36
+ refreshToken: response.refreshToken,
37
+ expiresAt: response.expiresAt,
48
38
  principal: response.principal
49
- }
50
- });
39
+ });
40
+ return guidedResult({
41
+ command: "auth:login",
42
+ summary: "Treeseed API login completed successfully.",
43
+ facts: [
44
+ { label: "Host", value: hostId },
45
+ { label: "Principal", value: response.principal.displayName ?? response.principal.id },
46
+ { label: "Scopes", value: response.principal.scopes.join(", ") }
47
+ ],
48
+ report: {
49
+ hostId,
50
+ principal: response.principal
51
+ }
52
+ });
53
+ }
54
+ if (!response.ok && response.status !== "already_used") {
55
+ return {
56
+ exitCode: 1,
57
+ stderr: [response.error]
58
+ };
59
+ }
60
+ await sleep(started.intervalSeconds * 1e3);
51
61
  }
52
- if (!response.ok && response.status !== "already_used") {
62
+ return {
63
+ exitCode: 1,
64
+ stderr: ["Treeseed API login expired before approval completed."]
65
+ };
66
+ } catch (error) {
67
+ if (error instanceof TreeseedKeyAgentError) {
53
68
  return {
54
69
  exitCode: 1,
55
- stderr: [response.error]
70
+ stderr: [error.message],
71
+ report: { command: "auth:login", ok: false, code: error.code, details: error.details ?? null }
56
72
  };
57
73
  }
58
- await sleep(started.intervalSeconds * 1e3);
74
+ throw error;
59
75
  }
60
- return {
61
- exitCode: 1,
62
- stderr: ["Treeseed API login expired before approval completed."]
63
- };
64
76
  };
65
77
  export {
66
78
  handleAuthLogin
@@ -1,19 +1,31 @@
1
1
  import {
2
2
  clearTreeseedRemoteSession,
3
- resolveTreeseedRemoteConfig
3
+ resolveTreeseedRemoteConfig,
4
+ TreeseedKeyAgentError
4
5
  } from "@treeseed/sdk/workflow-support";
5
6
  import { guidedResult } from "./utils.js";
6
7
  const handleAuthLogout = async (invocation, context) => {
7
- const tenantRoot = context.cwd;
8
- const remoteConfig = resolveTreeseedRemoteConfig(tenantRoot, context.env);
9
- const hostId = typeof invocation.args.host === "string" ? invocation.args.host : remoteConfig.activeHostId;
10
- clearTreeseedRemoteSession(tenantRoot, hostId);
11
- return guidedResult({
12
- command: "auth:logout",
13
- summary: "Cleared the local Treeseed API session.",
14
- facts: [{ label: "Host", value: hostId }],
15
- report: { hostId }
16
- });
8
+ try {
9
+ const tenantRoot = context.cwd;
10
+ const remoteConfig = resolveTreeseedRemoteConfig(tenantRoot, context.env);
11
+ const hostId = typeof invocation.args.host === "string" ? invocation.args.host : remoteConfig.activeHostId;
12
+ clearTreeseedRemoteSession(tenantRoot, hostId);
13
+ return guidedResult({
14
+ command: "auth:logout",
15
+ summary: "Cleared the local Treeseed API session.",
16
+ facts: [{ label: "Host", value: hostId }],
17
+ report: { hostId }
18
+ });
19
+ } catch (error) {
20
+ if (error instanceof TreeseedKeyAgentError) {
21
+ return {
22
+ exitCode: 1,
23
+ stderr: [error.message],
24
+ report: { command: "auth:logout", ok: false, code: error.code, details: error.details ?? null }
25
+ };
26
+ }
27
+ throw error;
28
+ }
17
29
  };
18
30
  export {
19
31
  handleAuthLogout
@@ -1,23 +1,34 @@
1
1
  import { RemoteTreeseedAuthClient, RemoteTreeseedClient } from "@treeseed/sdk/remote";
2
- import { resolveTreeseedRemoteConfig } from "@treeseed/sdk/workflow-support";
2
+ import { resolveTreeseedRemoteConfig, TreeseedKeyAgentError } from "@treeseed/sdk/workflow-support";
3
3
  import { guidedResult } from "./utils.js";
4
4
  const handleAuthWhoAmI = async (_invocation, context) => {
5
- const remoteConfig = resolveTreeseedRemoteConfig(context.cwd, context.env);
6
- const client = new RemoteTreeseedAuthClient(new RemoteTreeseedClient(remoteConfig));
7
- const response = await client.whoAmI();
8
- return guidedResult({
9
- command: "auth:whoami",
10
- summary: "Treeseed API identity",
11
- facts: [
12
- { label: "Host", value: remoteConfig.activeHostId },
13
- { label: "Principal", value: response.payload.displayName ?? response.payload.id },
14
- { label: "Scopes", value: response.payload.scopes.join(", ") }
15
- ],
16
- report: {
17
- hostId: remoteConfig.activeHostId,
18
- principal: response.payload
5
+ try {
6
+ const remoteConfig = resolveTreeseedRemoteConfig(context.cwd, context.env);
7
+ const client = new RemoteTreeseedAuthClient(new RemoteTreeseedClient(remoteConfig));
8
+ const response = await client.whoAmI();
9
+ return guidedResult({
10
+ command: "auth:whoami",
11
+ summary: "Treeseed API identity",
12
+ facts: [
13
+ { label: "Host", value: remoteConfig.activeHostId },
14
+ { label: "Principal", value: response.payload.displayName ?? response.payload.id },
15
+ { label: "Scopes", value: response.payload.scopes.join(", ") }
16
+ ],
17
+ report: {
18
+ hostId: remoteConfig.activeHostId,
19
+ principal: response.payload
20
+ }
21
+ });
22
+ } catch (error) {
23
+ if (error instanceof TreeseedKeyAgentError) {
24
+ return {
25
+ exitCode: 1,
26
+ stderr: [error.message],
27
+ report: { command: "auth:whoami", ok: false, code: error.code, details: error.details ?? null }
28
+ };
19
29
  }
20
- });
30
+ throw error;
31
+ }
21
32
  };
22
33
  export {
23
34
  handleAuthWhoAmI
@@ -3,21 +3,26 @@ import { createWorkflowSdk, renderWorkflowNextSteps, workflowErrorResult } from
3
3
  const handleClose = async (invocation, context) => {
4
4
  try {
5
5
  const result = await createWorkflowSdk(context).close({
6
- message: invocation.positionals.join(" ").trim()
6
+ message: invocation.positionals.join(" ").trim(),
7
+ plan: invocation.args.plan === true || invocation.args.dryRun === true,
8
+ dryRun: invocation.args.dryRun === true
7
9
  });
8
10
  const payload = result.payload;
11
+ const deletedPackages = payload.repos.filter((repo) => repo.deletedLocal || repo.deletedRemote).length;
9
12
  return guidedResult({
10
13
  command: invocation.commandName || "close",
11
- summary: "Treeseed close completed successfully.",
14
+ summary: result.executionMode === "plan" ? "Treeseed close plan ready." : "Treeseed close completed successfully.",
12
15
  facts: [
16
+ { label: "Mode", value: payload.mode },
13
17
  { label: "Closed branch", value: payload.branchName },
14
18
  { label: "Auto-saved", value: payload.autoSaved ? "yes" : "no" },
15
- { label: "Deprecated tag", value: payload.deprecatedTag.tagName },
19
+ { label: "Deprecated tag", value: payload.rootRepo.tagName ?? payload.deprecatedTag.tagName },
20
+ { label: "Package branches cleaned", value: String(deletedPackages) },
16
21
  { label: "Preview cleanup", value: payload.previewCleanup.performed ? "performed" : "not needed" },
17
22
  { label: "Final branch", value: payload.finalBranch }
18
23
  ],
19
24
  nextSteps: renderWorkflowNextSteps(result),
20
- report: payload
25
+ report: result
21
26
  });
22
27
  } catch (error) {
23
28
  return workflowErrorResult(error);
@@ -1,16 +1,26 @@
1
1
  import { type UiViewportLayout } from '../ui/framework.js';
2
- type ConfigScope = 'all' | 'local' | 'staging' | 'prod';
2
+ type ConfigScope = 'local' | 'staging' | 'prod';
3
3
  export type ConfigViewMode = 'startup' | 'full';
4
+ type ConfigValidation = {
5
+ kind: 'string' | 'nonempty' | 'boolean' | 'number' | 'url' | 'email';
6
+ } | {
7
+ kind: 'enum';
8
+ values: string[];
9
+ };
4
10
  type ConfigEntry = {
5
11
  id: string;
6
12
  label: string;
7
13
  group: string;
14
+ cluster: string;
15
+ startupProfile: 'core' | 'optional' | 'advanced';
16
+ requirement: 'required' | 'conditional' | 'optional';
8
17
  description: string;
9
18
  howToGet: string;
10
19
  sensitivity: 'secret' | 'plain' | 'derived';
11
20
  targets: string[];
12
21
  purposes: string[];
13
22
  storage: 'shared' | 'scoped';
23
+ validation?: ConfigValidation;
14
24
  scope: Exclude<ConfigScope, 'all'>;
15
25
  sharedScopes: Array<Exclude<ConfigScope, 'all'>>;
16
26
  required: boolean;
@@ -25,23 +35,28 @@ type ConfigContextSnapshot = {
25
35
  };
26
36
  scopes: Array<Exclude<ConfigScope, 'all'>>;
27
37
  entriesByScope: Record<Exclude<ConfigScope, 'all'>, ConfigEntry[]>;
28
- authStatusByScope: Record<Exclude<ConfigScope, 'all'>, {
29
- gh: {
30
- authenticated: boolean;
38
+ configReadinessByScope: Record<Exclude<ConfigScope, 'all'>, {
39
+ github: {
40
+ configured: boolean;
31
41
  };
32
- wrangler: {
33
- authenticated: boolean;
42
+ cloudflare: {
43
+ configured: boolean;
34
44
  };
35
45
  railway: {
36
- authenticated: boolean;
46
+ configured: boolean;
47
+ };
48
+ localDevelopment: {
49
+ configured: boolean;
37
50
  };
38
51
  }>;
39
52
  };
40
53
  export type ConfigPage = {
54
+ kind: 'entry';
41
55
  key: string;
42
56
  entry: ConfigEntry;
43
- scope: Exclude<ConfigScope, 'all'>;
44
- scopes: Array<Exclude<ConfigScope, 'all'>>;
57
+ scope: ConfigScope;
58
+ scopes: ConfigScope[];
59
+ requiredScopes: ConfigScope[];
45
60
  required: boolean;
46
61
  currentValue: string;
47
62
  suggestedValue: string;
@@ -56,6 +71,15 @@ export type ConfigEditorResult = {
56
71
  overrides: Record<string, string>;
57
72
  viewMode: ConfigViewMode;
58
73
  };
74
+ type ConfigCommitUpdate = {
75
+ scope: Exclude<ConfigScope, 'all'>;
76
+ entryId: string;
77
+ value: string;
78
+ };
79
+ export type ConfigInputState = {
80
+ value: string;
81
+ cursor: number;
82
+ };
59
83
  export type ConfigViewportLayout = UiViewportLayout & {
60
84
  sidebarWidth: number;
61
85
  contentWidth: number;
@@ -64,9 +88,41 @@ export type ConfigViewportLayout = UiViewportLayout & {
64
88
  inputHeight: number;
65
89
  actionRowHeight: number;
66
90
  };
91
+ export declare function filterCliConfigPages(pages: ConfigPage[], query: string): ConfigPage[];
67
92
  export declare function computeConfigViewportLayout(rows: number, columns: number): ConfigViewportLayout;
68
93
  export declare function buildCliConfigPages(context: ConfigContextSnapshot, selectedFilter: ConfigScope, overrides?: Record<string, string>, viewMode?: ConfigViewMode): ConfigPage[];
94
+ export declare function normalizeConfigInputChunk(input: string): string;
95
+ export declare function applyConfigInputInsertion(state: ConfigInputState, input: string): ConfigInputState;
96
+ export declare function readLinuxClipboardText(): string | null;
69
97
  export declare function runCliConfigEditor(context: ConfigContextSnapshot, options?: {
70
98
  initialViewMode?: ConfigViewMode;
99
+ mouseEnabled?: boolean;
100
+ initialStatusMessage?: string;
101
+ toolAvailability?: {
102
+ githubCli?: {
103
+ available: boolean;
104
+ };
105
+ wranglerCli?: {
106
+ available: boolean;
107
+ };
108
+ railwayCli?: {
109
+ available: boolean;
110
+ };
111
+ ghActExtension?: {
112
+ available: boolean;
113
+ };
114
+ dockerDaemon?: {
115
+ available: boolean;
116
+ };
117
+ };
118
+ secretSession?: {
119
+ status?: {
120
+ unlocked?: boolean;
121
+ };
122
+ createdWrappedKey?: boolean;
123
+ migratedWrappedKey?: boolean;
124
+ unlockSource?: string;
125
+ };
126
+ onCommit?: (update: ConfigCommitUpdate) => Promise<ConfigContextSnapshot> | ConfigContextSnapshot;
71
127
  }): Promise<ConfigEditorResult | null>;
72
128
  export {};