@treeseed/cli 0.4.12 → 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/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/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/dev.js +6 -1
- package/dist/cli/handlers/doctor.js +11 -5
- 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/status.js +31 -5
- package/dist/cli/handlers/workflow.js +31 -0
- package/dist/cli/help-ui.js +1 -1
- package/dist/cli/operations-registry.js +123 -9
- package/dist/cli/registry.d.ts +6 -0
- package/dist/cli/registry.js +15 -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
|
@@ -540,6 +540,83 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
|
|
|
540
540
|
executionMode: "handler",
|
|
541
541
|
handlerName: "auth:whoami"
|
|
542
542
|
})],
|
|
543
|
+
["secrets:status", command({
|
|
544
|
+
options: [{ name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }],
|
|
545
|
+
examples: ["treeseed secrets:status"],
|
|
546
|
+
help: {
|
|
547
|
+
longSummary: ["Secrets:status shows whether the local key agent is running, whether the wrapped machine key exists, and whether the in-memory secret session is currently unlocked."],
|
|
548
|
+
whenToUse: ["Use this before secret-backed local commands when you need to confirm whether the machine key is already unlocked."],
|
|
549
|
+
beforeYouRun: ["Decide whether you want the human-readable summary or `--json` for automation."],
|
|
550
|
+
automationNotes: ["This command is read-only and safe for agents to call before deciding whether an unlock step is required."]
|
|
551
|
+
},
|
|
552
|
+
executionMode: "handler",
|
|
553
|
+
handlerName: "secrets:status"
|
|
554
|
+
})],
|
|
555
|
+
["secrets:unlock", command({
|
|
556
|
+
options: [
|
|
557
|
+
{ name: "fromEnv", flags: "--from-env", description: "Unlock from TREESEED_KEY_PASSPHRASE instead of prompting.", kind: "boolean" },
|
|
558
|
+
{ name: "createIfMissing", flags: "--create-if-missing", description: "Create a wrapped machine key when one does not exist yet.", kind: "boolean" },
|
|
559
|
+
{ name: "allowMigration", flags: "--allow-migration", description: "Allow migration from the legacy plaintext machine-key file.", kind: "boolean" },
|
|
560
|
+
{ name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }
|
|
561
|
+
],
|
|
562
|
+
examples: ["treeseed secrets:unlock", "treeseed secrets:unlock --from-env"],
|
|
563
|
+
help: {
|
|
564
|
+
longSummary: ["Secrets:unlock starts or reuses the host-local key agent and unlocks the in-memory machine key from an interactive passphrase prompt or the TREESEED_KEY_PASSPHRASE startup env var."],
|
|
565
|
+
whenToUse: ["Use this before running local dev, config, deployment, or runner commands that need encrypted local secrets."],
|
|
566
|
+
beforeYouRun: ["Use a TTY for the interactive prompt path, or set TREESEED_KEY_PASSPHRASE before using `--from-env` in backend startup automation."],
|
|
567
|
+
automationNotes: ["Backend servers should use `--from-env` only during the explicit startup unlock step, not during arbitrary job execution."]
|
|
568
|
+
},
|
|
569
|
+
executionMode: "handler",
|
|
570
|
+
handlerName: "secrets:unlock"
|
|
571
|
+
})],
|
|
572
|
+
["secrets:lock", command({
|
|
573
|
+
options: [{ name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }],
|
|
574
|
+
examples: ["treeseed secrets:lock"],
|
|
575
|
+
help: {
|
|
576
|
+
longSummary: ["Secrets:lock clears the in-memory machine key from the host-local key agent."],
|
|
577
|
+
whenToUse: ["Use this when you want to end the current local secret session before the idle timeout expires."],
|
|
578
|
+
beforeYouRun: ["No additional setup is required."],
|
|
579
|
+
automationNotes: ["This command is safe to run in automation when a runner host should explicitly clear local secret access."]
|
|
580
|
+
},
|
|
581
|
+
executionMode: "handler",
|
|
582
|
+
handlerName: "secrets:lock"
|
|
583
|
+
})],
|
|
584
|
+
["secrets:migrate-key", command({
|
|
585
|
+
options: [{ name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }],
|
|
586
|
+
examples: ["treeseed secrets:migrate-key"],
|
|
587
|
+
help: {
|
|
588
|
+
longSummary: ["Secrets:migrate-key replaces the legacy plaintext machine-key file with the wrapped passphrase-protected format used by the Treeseed key agent."],
|
|
589
|
+
whenToUse: ["Use this when status or doctor reports that machine-key migration is still required."],
|
|
590
|
+
beforeYouRun: ["Run this in a TTY so you can create and confirm the new wrapping passphrase."],
|
|
591
|
+
automationNotes: ["Prefer this as a one-time operator action rather than an unattended automation step."]
|
|
592
|
+
},
|
|
593
|
+
executionMode: "handler",
|
|
594
|
+
handlerName: "secrets:migrate-key"
|
|
595
|
+
})],
|
|
596
|
+
["secrets:rotate-passphrase", command({
|
|
597
|
+
options: [{ name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }],
|
|
598
|
+
examples: ["treeseed secrets:rotate-passphrase"],
|
|
599
|
+
help: {
|
|
600
|
+
longSummary: ["Secrets:rotate-passphrase re-wraps the existing machine key with a newly entered passphrase without changing the underlying machine key."],
|
|
601
|
+
whenToUse: ["Use this when the local wrapping passphrase should be changed without re-encrypting the stored secret payloads."],
|
|
602
|
+
beforeYouRun: ["Unlock the current secret session first, then run this in a TTY so you can enter and confirm the new passphrase."],
|
|
603
|
+
automationNotes: ["Treat passphrase rotation as an operator-controlled maintenance action, not a normal background job."]
|
|
604
|
+
},
|
|
605
|
+
executionMode: "handler",
|
|
606
|
+
handlerName: "secrets:rotate-passphrase"
|
|
607
|
+
})],
|
|
608
|
+
["secrets:rotate-machine-key", command({
|
|
609
|
+
options: [{ name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }],
|
|
610
|
+
examples: ["treeseed secrets:rotate-machine-key"],
|
|
611
|
+
help: {
|
|
612
|
+
longSummary: ["Secrets:rotate-machine-key generates a new machine key, re-encrypts stored local secrets, and re-wraps the result with the configured passphrase."],
|
|
613
|
+
whenToUse: ["Use this when the underlying machine key itself must be rotated, such as after a local secret-hygiene event."],
|
|
614
|
+
beforeYouRun: ["Unlock the current secret session and make sure TREESEED_KEY_PASSPHRASE is set for the non-interactive re-wrap step."],
|
|
615
|
+
automationNotes: ["This command mutates local encrypted state and should be run intentionally rather than as part of routine startup automation."]
|
|
616
|
+
},
|
|
617
|
+
executionMode: "handler",
|
|
618
|
+
handlerName: "secrets:rotate-machine-key"
|
|
619
|
+
})],
|
|
543
620
|
["template", command({
|
|
544
621
|
usage: "treeseed template [list|show|validate] [id]",
|
|
545
622
|
arguments: [
|
|
@@ -622,15 +699,27 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
|
|
|
622
699
|
["config", command({
|
|
623
700
|
options: [
|
|
624
701
|
{ name: "full", flags: "--full", description: "Open the advanced full editor directly in human interactive mode.", kind: "boolean" },
|
|
702
|
+
{ name: "mouse", flags: "--mouse", description: "Opt into mouse capture for the config UI. Keyboard-first terminal behavior remains the default.", kind: "boolean" },
|
|
625
703
|
{ name: "environment", flags: "--environment <scope>", description: "Select all environments or limit configuration to local, staging, or prod. Defaults to all.", kind: "enum", repeatable: true, values: ["all", "local", "staging", "prod"] },
|
|
626
704
|
{ name: "sync", flags: "--sync <mode>", description: "Sync hosted secrets/variables to GitHub, Cloudflare, Railway, or all providers. Defaults to all.", kind: "enum", values: ["none", "github", "cloudflare", "railway", "all"] },
|
|
705
|
+
{ name: "nonInteractive", flags: "--non-interactive", description: "Apply resolved values without opening the interactive UI. Required for non-TTY automation unless using an operational mode such as --print-env-only.", kind: "boolean" },
|
|
706
|
+
{ name: "installMissingTooling", flags: "--install-missing-tooling", description: "Install missing config verification tooling such as `gh-act` during the run instead of only reporting it.", kind: "boolean" },
|
|
627
707
|
{ name: "printEnv", flags: "--print-env", description: "Print resolved environment values before remote initialization.", kind: "boolean" },
|
|
628
708
|
{ name: "printEnvOnly", flags: "--print-env-only", description: "Print resolved environment values, check provider connections, and exit without prompting or initializing remote resources.", kind: "boolean" },
|
|
629
709
|
{ name: "showSecrets", flags: "--show-secrets", description: "Print full secret values in environment reports instead of masking them.", kind: "boolean" },
|
|
630
710
|
{ name: "rotateMachineKey", flags: "--rotate-machine-key", description: "Regenerate the local home machine key and re-encrypt stored Treeseed secrets and remote auth sessions.", kind: "boolean" },
|
|
711
|
+
{ name: "connectMarket", flags: "--connect-market", description: "Pair the current local repo to a Knowledge Coop project and register the hybrid runner connection.", kind: "boolean" },
|
|
712
|
+
{ name: "marketBaseUrl", flags: "--market-base-url <url>", description: "Knowledge Coop control-plane base URL for --connect-market. Defaults to the active remote host.", kind: "string" },
|
|
713
|
+
{ name: "marketTeamId", flags: "--market-team-id <id>", description: "Team ID to record in the local Knowledge Coop pairing metadata.", kind: "string" },
|
|
714
|
+
{ name: "marketTeamSlug", flags: "--market-team-slug <slug>", description: "Team slug to record in the local Knowledge Coop pairing metadata.", kind: "string" },
|
|
715
|
+
{ name: "marketProjectId", flags: "--market-project-id <id>", description: "Project ID to pair with when using --connect-market.", kind: "string" },
|
|
716
|
+
{ name: "marketProjectSlug", flags: "--market-project-slug <slug>", description: "Project slug to record in the local Knowledge Coop pairing metadata.", kind: "string" },
|
|
717
|
+
{ name: "marketProjectApiBaseUrl", flags: "--market-project-api-base-url <url>", description: "Override the project API base URL recorded on the Knowledge Coop project connection.", kind: "string" },
|
|
718
|
+
{ name: "marketAccessToken", flags: "--market-access-token <token>", description: "Explicit Knowledge Coop access token to use for pairing. Prefer an existing remote session when possible.", kind: "string" },
|
|
719
|
+
{ name: "rotateRunnerToken", flags: "--rotate-runner-token", description: "Rotate the project runner credential while pairing the local hybrid repo.", kind: "boolean" },
|
|
631
720
|
{ name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }
|
|
632
721
|
],
|
|
633
|
-
examples: ["treeseed config", "treeseed config --full", "treeseed config --environment all", "treeseed config --environment local --sync none", "treeseed config --environment staging --print-env-only --show-secrets", "treeseed config --rotate-machine-key"],
|
|
722
|
+
examples: ["treeseed config", "treeseed config --full --mouse", "treeseed config --environment all", "treeseed config --environment local --sync none", "treeseed config --environment local --sync none --non-interactive", "treeseed config --environment staging --print-env-only --show-secrets", "treeseed config --rotate-machine-key", "treeseed config --connect-market --market-project-id kc_proj_123"],
|
|
634
723
|
notes: ["Does not create branch preview deployments. Use `treeseed switch <branch> --preview` for that."],
|
|
635
724
|
help: {
|
|
636
725
|
workflowPosition: "configure runtime",
|
|
@@ -640,10 +729,10 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
|
|
|
640
729
|
],
|
|
641
730
|
whenToUse: [
|
|
642
731
|
"Use this during first-run setup, after new required environment variables are introduced, or when provider-backed configuration drift must be repaired.",
|
|
643
|
-
"Use the startup wizard for onboarding and the full editor when you need complete per-variable control."
|
|
732
|
+
"Use the startup wizard for onboarding and the full editor when you need complete per-variable control. Terminal-native copy, selection, and paste are the default interaction model."
|
|
644
733
|
],
|
|
645
734
|
beforeYouRun: [
|
|
646
|
-
"Decide whether you want human interactive mode or machine-readable `--json` output before invoking the command.",
|
|
735
|
+
"Decide whether you want human interactive mode, explicit `--non-interactive` application, or machine-readable `--json` output before invoking the command.",
|
|
647
736
|
"Choose the environment scope you care about: all, local, staging, or prod.",
|
|
648
737
|
"If you plan to sync hosted state, make sure GitHub, Cloudflare, and Railway authentication is already configured or be ready to log in first."
|
|
649
738
|
],
|
|
@@ -655,21 +744,29 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
|
|
|
655
744
|
examples: [
|
|
656
745
|
example("treeseed config", "Run the startup wizard", "Open the newcomer-friendly configuration wizard in human TTY mode."),
|
|
657
746
|
example("treeseed config --full", "Open the advanced editor directly", "Skip the startup wizard and go straight to the full configuration surface."),
|
|
747
|
+
example("treeseed config --full --mouse", "Opt into mouse capture for the editor", "Keep the keyboard-first defaults unless you explicitly want click and wheel interaction inside the config UI."),
|
|
658
748
|
example("treeseed config --environment local --sync none", "Edit local values without provider sync", "Limit the session to local values and avoid hosted synchronization while iterating locally."),
|
|
749
|
+
example("treeseed config --environment local --sync none --non-interactive", "Apply deterministic local config in automation", "Use the resolved current and suggested values without opening the interactive UI."),
|
|
659
750
|
example("treeseed config --environment staging --print-env-only --show-secrets", "Inspect a resolved environment report", "Print the resolved staging environment with full secret visibility and exit."),
|
|
660
|
-
example("treeseed config --rotate-machine-key", "Rotate the local secret encryption key", "Regenerate the machine key and re-encrypt locally stored Treeseed secrets.")
|
|
751
|
+
example("treeseed config --rotate-machine-key", "Rotate the local secret encryption key", "Regenerate the machine key and re-encrypt locally stored Treeseed secrets."),
|
|
752
|
+
example("treeseed config --connect-market --market-project-id kc_proj_123", "Pair a hybrid repo to Knowledge Coop", "Register the current local repo as the hybrid runner connection for an existing Knowledge Coop project.")
|
|
661
753
|
],
|
|
662
754
|
optionDetails: [
|
|
663
755
|
detail("--full", "Enter the advanced editor directly instead of the startup wizard."),
|
|
756
|
+
detail("--mouse", "Opt into terminal mouse capture for clicking, scrolling, and focus changes inside the config UI."),
|
|
664
757
|
detail("--environment <scope>", "Filter configuration to `all`, `local`, `staging`, or `prod`."),
|
|
665
758
|
detail("--sync <mode>", "Choose which provider surfaces should receive synchronized values after local updates are applied."),
|
|
759
|
+
detail("--non-interactive", "Apply resolved values without opening the interactive editor. Use this for automation when you do not want `--json` output."),
|
|
760
|
+
detail("--install-missing-tooling", "Allow config to install missing verification helpers such as the GitHub `gh-act` extension instead of only reporting them."),
|
|
666
761
|
detail("--print-env", "Print the resolved environment values before remote initialization continues."),
|
|
667
762
|
detail("--print-env-only", "Print the environment report and exit without interactive editing or remote initialization."),
|
|
668
|
-
detail("--rotate-machine-key", "Rotate the local machine key used for encrypted Treeseed secret storage.")
|
|
763
|
+
detail("--rotate-machine-key", "Rotate the local machine key used for encrypted Treeseed secret storage."),
|
|
764
|
+
detail("--connect-market", "Pair the current repo to a Knowledge Coop project and store the resulting market connection metadata locally.")
|
|
669
765
|
],
|
|
670
766
|
automationNotes: [
|
|
671
|
-
"Use `--json` for
|
|
672
|
-
"`--print-env-only
|
|
767
|
+
"Use `--json` for machine-readable automation, or `--non-interactive` when you want deterministic application without interactive UI.",
|
|
768
|
+
"`--print-env-only`, `--rotate-machine-key`, and `--connect-market` are operational paths that bypass the interactive UI.",
|
|
769
|
+
"Config reports missing tooling by default. Use `--install-missing-tooling` when you want the command to attempt installation.",
|
|
673
770
|
"Shared versus scoped environment semantics are resolved inside the SDK; the CLI help should be treated as the operator-facing explanation layer."
|
|
674
771
|
],
|
|
675
772
|
warnings: [
|
|
@@ -850,7 +947,25 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
|
|
|
850
947
|
["lint", command({ examples: ["treeseed lint"], help: { longSummary: ["Lint runs the project linting and related surface checks for the current tenant."], examples: [example("treeseed lint", "Run lint", "Execute the lint checks for the current project."), example("trsd lint", "Use the short alias", "Run the same lint checks through the shorter entrypoint."), example("treeseed lint && treeseed test:unit", "Lint before unit tests", "Use lint as a first local verification step.")] }, executionMode: "adapter" })],
|
|
851
948
|
["test", command({ examples: ["treeseed test"], help: { longSummary: ["Test runs the default Treeseed test surface for the current project."], examples: [example("treeseed test", "Run the default test suite", "Execute the standard project test flow."), example("trsd test", "Use the short alias", "Run the same test surface with the shorter entrypoint."), example("treeseed test && treeseed build", "Verify before building", "Run tests before the build step in a local verification loop.")] }, executionMode: "adapter" })],
|
|
852
949
|
["test:unit", command({ examples: ["treeseed test:unit"], help: { longSummary: ["Test:unit runs workspace unit tests in dependency order."], examples: [example("treeseed test:unit", "Run unit tests", "Execute the package unit test flow."), example("trsd test:unit", "Use the short alias", "Run the same unit tests via the short entrypoint."), example("treeseed test:unit && treeseed check", "Unit tests then validation", "Combine focused tests with broader tenant validation.")] }, executionMode: "adapter" })],
|
|
853
|
-
["preflight", command({
|
|
950
|
+
["preflight", command({
|
|
951
|
+
options: [
|
|
952
|
+
{ name: "launch", flags: "--launch", description: "Validate managed Knowledge Coop launch prerequisites, provider auth, and required live configuration.", kind: "boolean" }
|
|
953
|
+
],
|
|
954
|
+
examples: ["treeseed preflight", "treeseed preflight --launch"],
|
|
955
|
+
help: {
|
|
956
|
+
longSummary: ["Preflight checks local prerequisites and authentication state before heavier workflows run."],
|
|
957
|
+
examples: [
|
|
958
|
+
example("treeseed preflight", "Run the preflight checklist", "Inspect local prerequisites and auth readiness."),
|
|
959
|
+
example("treeseed preflight --launch", "Validate live launch readiness", "Check managed Knowledge Coop launch prerequisites before creating live GitHub, Cloudflare, and Railway resources."),
|
|
960
|
+
example("trsd preflight", "Use the short alias", "Run the same readiness check via the short entrypoint."),
|
|
961
|
+
example("treeseed preflight && treeseed dev", "Validate before starting local runtime", "Confirm readiness before launching the integrated dev surface.")
|
|
962
|
+
]
|
|
963
|
+
},
|
|
964
|
+
executionMode: "adapter",
|
|
965
|
+
buildAdapterInput: (invocation) => ({
|
|
966
|
+
launch: invocation.args.launch === true
|
|
967
|
+
})
|
|
968
|
+
})],
|
|
854
969
|
["auth:check", command({ examples: ["treeseed auth:check"], executionMode: "adapter", buildAdapterInput: () => ({ requireAuth: true }) })],
|
|
855
970
|
["test:e2e", command({ examples: ["treeseed test:e2e"], executionMode: "adapter" })],
|
|
856
971
|
["test:e2e:local", command({ examples: ["treeseed test:e2e:local"], executionMode: "adapter" })],
|
|
@@ -860,7 +975,6 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
|
|
|
860
975
|
["test:release:full", command({ examples: ["treeseed test:release:full", "treeseed release:verify"], executionMode: "adapter" })],
|
|
861
976
|
["release:publish:changed", command({ examples: ["treeseed release:publish:changed"], executionMode: "adapter" })],
|
|
862
977
|
["astro", command({ examples: ["treeseed astro -- --help"], executionMode: "adapter", buildAdapterInput: PASS_THROUGH_ARGS })],
|
|
863
|
-
["sync:devvars", command({ examples: ["treeseed sync:devvars"], executionMode: "adapter" })],
|
|
864
978
|
["mailpit:up", command({ examples: ["treeseed mailpit:up"], executionMode: "adapter" })],
|
|
865
979
|
["mailpit:down", command({ examples: ["treeseed mailpit:down"], executionMode: "adapter" })],
|
|
866
980
|
["mailpit:logs", command({ examples: ["treeseed mailpit:logs"], executionMode: "adapter" })],
|
package/dist/cli/registry.d.ts
CHANGED
|
@@ -22,6 +22,12 @@ export declare const COMMAND_HANDLERS: {
|
|
|
22
22
|
readonly 'auth:login': import("./operations-types.js").TreeseedCommandHandler;
|
|
23
23
|
readonly 'auth:logout': import("./operations-types.js").TreeseedCommandHandler;
|
|
24
24
|
readonly 'auth:whoami': import("./operations-types.js").TreeseedCommandHandler;
|
|
25
|
+
readonly 'secrets:status': import("./operations-types.js").TreeseedCommandHandler;
|
|
26
|
+
readonly 'secrets:unlock': import("./operations-types.js").TreeseedCommandHandler;
|
|
27
|
+
readonly 'secrets:lock': import("./operations-types.js").TreeseedCommandHandler;
|
|
28
|
+
readonly 'secrets:migrate-key': import("./operations-types.js").TreeseedCommandHandler;
|
|
29
|
+
readonly 'secrets:rotate-passphrase': import("./operations-types.js").TreeseedCommandHandler;
|
|
30
|
+
readonly 'secrets:rotate-machine-key': import("./operations-types.js").TreeseedCommandHandler;
|
|
25
31
|
};
|
|
26
32
|
export declare const TRESEED_COMMAND_SPECS: TreeseedCommandSpec[];
|
|
27
33
|
export declare function findCommandSpec(name: string | null | undefined): import("./operations-types.js").TreeseedOperationSpec | null;
|
package/dist/cli/registry.js
CHANGED
|
@@ -18,6 +18,14 @@ import { handleSync } from "./handlers/sync.js";
|
|
|
18
18
|
import { handleAuthLogin } from "./handlers/auth-login.js";
|
|
19
19
|
import { handleAuthLogout } from "./handlers/auth-logout.js";
|
|
20
20
|
import { handleAuthWhoAmI } from "./handlers/auth-whoami.js";
|
|
21
|
+
import {
|
|
22
|
+
handleSecretsLock,
|
|
23
|
+
handleSecretsMigrateKey,
|
|
24
|
+
handleSecretsRotateMachineKey,
|
|
25
|
+
handleSecretsRotatePassphrase,
|
|
26
|
+
handleSecretsStatus,
|
|
27
|
+
handleSecretsUnlock
|
|
28
|
+
} from "./handlers/secrets.js";
|
|
21
29
|
import { handleTasks } from "./handlers/tasks.js";
|
|
22
30
|
import { handleSwitch } from "./handlers/switch.js";
|
|
23
31
|
import { handleStage } from "./handlers/stage.js";
|
|
@@ -46,7 +54,13 @@ const COMMAND_HANDLERS = {
|
|
|
46
54
|
export: handleExport,
|
|
47
55
|
"auth:login": handleAuthLogin,
|
|
48
56
|
"auth:logout": handleAuthLogout,
|
|
49
|
-
"auth:whoami": handleAuthWhoAmI
|
|
57
|
+
"auth:whoami": handleAuthWhoAmI,
|
|
58
|
+
"secrets:status": handleSecretsStatus,
|
|
59
|
+
"secrets:unlock": handleSecretsUnlock,
|
|
60
|
+
"secrets:lock": handleSecretsLock,
|
|
61
|
+
"secrets:migrate-key": handleSecretsMigrateKey,
|
|
62
|
+
"secrets:rotate-passphrase": handleSecretsRotatePassphrase,
|
|
63
|
+
"secrets:rotate-machine-key": handleSecretsRotateMachineKey
|
|
50
64
|
};
|
|
51
65
|
const TRESEED_COMMAND_SPECS = TRESEED_OPERATION_SPECS;
|
|
52
66
|
function findCommandSpec(name) {
|
package/dist/cli/repair.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import {
|
|
4
4
|
createDefaultTreeseedMachineConfig,
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
loadDeployState,
|
|
9
9
|
loadTreeseedMachineConfig,
|
|
10
10
|
resolveTreeseedMachineEnvironmentValues,
|
|
11
|
-
|
|
11
|
+
warnDeprecatedTreeseedLocalEnvFiles,
|
|
12
12
|
writeTreeseedMachineConfig,
|
|
13
13
|
createPersistentDeployTarget,
|
|
14
14
|
ensureGeneratedWranglerConfig
|
|
@@ -17,11 +17,9 @@ function applyTreeseedSafeRepairs(tenantRoot) {
|
|
|
17
17
|
const actions = [];
|
|
18
18
|
ensureTreeseedGitignoreEntries(tenantRoot);
|
|
19
19
|
actions.push({ id: "gitignore", detail: "Ensured Treeseed gitignore entries are present." });
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
copyFileSync(envLocalExamplePath, envLocalPath);
|
|
24
|
-
actions.push({ id: "env-local", detail: "Created .env.local from .env.local.example." });
|
|
20
|
+
const deprecatedFiles = warnDeprecatedTreeseedLocalEnvFiles(tenantRoot);
|
|
21
|
+
if (deprecatedFiles.length > 0) {
|
|
22
|
+
actions.push({ id: "deprecated-local-env", detail: "Detected deprecated .env.local/.dev.vars files that Treeseed now ignores." });
|
|
25
23
|
}
|
|
26
24
|
const deployConfig = loadCliDeployConfig(tenantRoot);
|
|
27
25
|
const { configPath } = getTreeseedMachineConfigPaths(tenantRoot);
|
|
@@ -38,8 +36,6 @@ function applyTreeseedSafeRepairs(tenantRoot) {
|
|
|
38
36
|
actions.push({ id: "machine-key", detail: "Ensured the Treeseed machine key exists." });
|
|
39
37
|
const machineConfig = loadTreeseedMachineConfig(tenantRoot);
|
|
40
38
|
writeTreeseedMachineConfig(tenantRoot, machineConfig);
|
|
41
|
-
writeTreeseedLocalEnvironmentFiles(tenantRoot);
|
|
42
|
-
actions.push({ id: "local-env", detail: "Regenerated .env.local and .dev.vars from the current machine config." });
|
|
43
39
|
const stateRoot = resolve(tenantRoot, ".treeseed", "state", "environments");
|
|
44
40
|
if (existsSync(stateRoot)) {
|
|
45
41
|
for (const scope of ["local", "staging", "prod"]) {
|
|
@@ -53,6 +53,7 @@ export declare function scrollOffsetByDelta(state: ScrollRegionState, delta: num
|
|
|
53
53
|
export declare function scrollOffsetByPage(state: ScrollRegionState, pages: number): number;
|
|
54
54
|
export declare function ensureVisible(index: number, offset: number, viewportSize: number): number;
|
|
55
55
|
export declare function truncateLine(value: string, width: number): string;
|
|
56
|
+
export declare function formatSecretMaskedValue(value: string): string;
|
|
56
57
|
export declare function wrapText(value: string, width: number): string[];
|
|
57
58
|
export declare function containsPoint(rect: UiRect, x: number, y: number): boolean;
|
|
58
59
|
export declare function findClickableRegion(regions: UiClickRegion[], x: number, y: number): UiClickRegion | null;
|
|
@@ -120,6 +121,7 @@ type TextInputFieldProps = {
|
|
|
120
121
|
secret?: boolean;
|
|
121
122
|
placeholder?: string;
|
|
122
123
|
cursorPosition?: number;
|
|
124
|
+
helperText?: string;
|
|
123
125
|
};
|
|
124
126
|
export declare function TextInputField(props: TextInputFieldProps): any;
|
|
125
127
|
type TextAreaFieldProps = {
|
package/dist/cli/ui/framework.js
CHANGED
|
@@ -48,6 +48,25 @@ function truncateLine(value, width) {
|
|
|
48
48
|
}
|
|
49
49
|
return `${value.slice(0, Math.max(0, width - 1))}\u2026`;
|
|
50
50
|
}
|
|
51
|
+
function formatSecretMaskedValue(value) {
|
|
52
|
+
if (!value) {
|
|
53
|
+
return "(unset)";
|
|
54
|
+
}
|
|
55
|
+
const normalized = value.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "").replace(/\n+/g, " ").trim();
|
|
56
|
+
if (!normalized) {
|
|
57
|
+
return "(unset)";
|
|
58
|
+
}
|
|
59
|
+
if (normalized.length === 1) {
|
|
60
|
+
return `${normalized} [1]`;
|
|
61
|
+
}
|
|
62
|
+
const maxRevealedTotal = Math.max(2, Math.floor(normalized.length * 0.25));
|
|
63
|
+
const revealPerSide = Math.min(3, Math.max(1, Math.floor(maxRevealedTotal / 2)));
|
|
64
|
+
const leading = normalized.slice(0, revealPerSide);
|
|
65
|
+
const trailing = normalized.slice(Math.max(revealPerSide, normalized.length - revealPerSide));
|
|
66
|
+
const maskedCount = Math.max(0, normalized.length - leading.length - trailing.length);
|
|
67
|
+
const maskedMiddle = "*".repeat(maskedCount);
|
|
68
|
+
return `${leading}${maskedMiddle}${trailing} [${normalized.length}]`;
|
|
69
|
+
}
|
|
51
70
|
function wrapText(value, width) {
|
|
52
71
|
if (width <= 0) {
|
|
53
72
|
return [""];
|
|
@@ -186,29 +205,40 @@ function FieldCard(props) {
|
|
|
186
205
|
))
|
|
187
206
|
);
|
|
188
207
|
}
|
|
189
|
-
function
|
|
190
|
-
const height = props.height ?? 4;
|
|
208
|
+
function renderTextInputContent(props) {
|
|
191
209
|
const safeCursor = Math.max(0, Math.min(props.cursorPosition ?? props.value.length, props.value.length));
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const beforeCursor = visibleValue.slice(0, safeCursor);
|
|
197
|
-
const afterCursor = visibleValue.slice(safeCursor);
|
|
198
|
-
inputLine = `${beforeCursor}\u2588${afterCursor}`;
|
|
199
|
-
} else if (!inputLine) {
|
|
200
|
-
inputLine = placeholder;
|
|
210
|
+
const placeholder = props.placeholder ?? "";
|
|
211
|
+
const contentWidth = Math.max(1, props.width - 2);
|
|
212
|
+
if (!props.focused && !props.value) {
|
|
213
|
+
return React.createElement(Text, { color: "gray" }, truncateLine(placeholder || " ".repeat(contentWidth), contentWidth));
|
|
201
214
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
215
|
+
const visibleValue = props.secret && props.value.length > 0 ? formatSecretMaskedValue(props.value) : props.value;
|
|
216
|
+
if (!props.focused) {
|
|
217
|
+
return React.createElement(Text, null, truncateLine(visibleValue || placeholder, contentWidth));
|
|
218
|
+
}
|
|
219
|
+
const preservedPrefix = visibleValue.slice(0, safeCursor);
|
|
220
|
+
const visiblePrefix = preservedPrefix.slice(Math.max(0, preservedPrefix.length - Math.max(0, contentWidth - 1)));
|
|
221
|
+
const cursorCell = visibleValue[safeCursor] ?? " ";
|
|
222
|
+
const padding = " ".repeat(Math.max(0, contentWidth - visiblePrefix.length - 1));
|
|
223
|
+
return React.createElement(
|
|
224
|
+
Text,
|
|
225
|
+
null,
|
|
226
|
+
visiblePrefix,
|
|
227
|
+
React.createElement(Text, { color: "black", backgroundColor: "cyan" }, cursorCell),
|
|
228
|
+
padding
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
function TextInputField(props) {
|
|
232
|
+
const height = props.height ?? 4;
|
|
233
|
+
const placeholder = props.placeholder ?? "";
|
|
234
|
+
const helperText = props.helperText ?? (props.value.length > 0 ? props.secret ? "Secret value captured. Paste or type a replacement, or leave this as-is." : "Type a replacement value or leave this as-is." : placeholder);
|
|
235
|
+
return React.createElement(
|
|
236
|
+
Box,
|
|
237
|
+
{ flexDirection: "column", width: props.width, height, borderStyle: "round", borderColor: props.focused ? "cyan" : "blue", overflow: "hidden" },
|
|
238
|
+
React.createElement(Text, { color: "blue", bold: true }, truncateLine(props.label, props.width - 2)),
|
|
239
|
+
renderTextInputContent(props),
|
|
240
|
+
React.createElement(Text, { color: "gray" }, truncateLine(helperText || " ", props.width - 2))
|
|
241
|
+
);
|
|
212
242
|
}
|
|
213
243
|
function TextAreaField(props) {
|
|
214
244
|
return React.createElement(FieldCard, {
|
|
@@ -216,7 +246,7 @@ function TextAreaField(props) {
|
|
|
216
246
|
height: props.height,
|
|
217
247
|
title: props.label,
|
|
218
248
|
focused: props.focused,
|
|
219
|
-
lines: wrapText(props.value || "
|
|
249
|
+
lines: wrapText(props.value || "Value is unset.", Math.max(1, props.width - 2))
|
|
220
250
|
});
|
|
221
251
|
}
|
|
222
252
|
function ActionButton(props) {
|
|
@@ -286,6 +316,7 @@ export {
|
|
|
286
316
|
ensureVisible,
|
|
287
317
|
findClickableRegion,
|
|
288
318
|
findScrollRegion,
|
|
319
|
+
formatSecretMaskedValue,
|
|
289
320
|
popNavigationEntry,
|
|
290
321
|
pushNavigationEntry,
|
|
291
322
|
routeWheelDeltaToScrollRegion,
|
package/dist/cli/ui/mouse.d.ts
CHANGED
|
@@ -8,4 +8,6 @@ export type TerminalMouseEvent = {
|
|
|
8
8
|
ctrl: boolean;
|
|
9
9
|
};
|
|
10
10
|
export declare function parseTerminalMouseInput(input: string): TerminalMouseEvent[];
|
|
11
|
-
export declare function useTerminalMouse(onEvent: (event: TerminalMouseEvent) => void
|
|
11
|
+
export declare function useTerminalMouse(onEvent: (event: TerminalMouseEvent) => void, options?: {
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
}): void;
|
package/dist/cli/ui/mouse.js
CHANGED
|
@@ -41,7 +41,7 @@ function parseTerminalMouseInput(input) {
|
|
|
41
41
|
}
|
|
42
42
|
return events;
|
|
43
43
|
}
|
|
44
|
-
function useTerminalMouse(onEvent) {
|
|
44
|
+
function useTerminalMouse(onEvent, options = {}) {
|
|
45
45
|
const { stdin } = useStdin();
|
|
46
46
|
const { stdout } = useStdout();
|
|
47
47
|
const handlerRef = React.useRef(onEvent);
|
|
@@ -49,7 +49,7 @@ function useTerminalMouse(onEvent) {
|
|
|
49
49
|
handlerRef.current = onEvent;
|
|
50
50
|
}, [onEvent]);
|
|
51
51
|
React.useEffect(() => {
|
|
52
|
-
if (!stdin || !stdout || !process.stdin.isTTY || !process.stdout.isTTY) {
|
|
52
|
+
if (options.enabled === false || !stdin || !stdout || !process.stdin.isTTY || !process.stdout.isTTY) {
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
55
|
stdout.write("\x1B[?1000h\x1B[?1006h");
|
|
@@ -64,7 +64,7 @@ function useTerminalMouse(onEvent) {
|
|
|
64
64
|
stdin.off("data", handleData);
|
|
65
65
|
stdout.write("\x1B[?1000l\x1B[?1006l");
|
|
66
66
|
};
|
|
67
|
-
}, [stdin, stdout]);
|
|
67
|
+
}, [options.enabled, stdin, stdout]);
|
|
68
68
|
}
|
|
69
69
|
export {
|
|
70
70
|
parseTerminalMouseInput,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@treeseed/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Operator-facing Treeseed CLI package.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"repository": {
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"types": "./dist/index.d.ts",
|
|
21
21
|
"files": [
|
|
22
22
|
"README.md",
|
|
23
|
-
"dist"
|
|
23
|
+
"dist",
|
|
24
|
+
"scripts/verify-driver.mjs"
|
|
24
25
|
],
|
|
25
26
|
"publishConfig": {
|
|
26
27
|
"access": "public"
|
|
@@ -34,16 +35,16 @@
|
|
|
34
35
|
"build:dist": "node ./scripts/run-ts.mjs ./scripts/build-dist.ts",
|
|
35
36
|
"prepack": "npm run build:dist",
|
|
36
37
|
"verify:direct": "npm run release:verify",
|
|
37
|
-
"verify:local": "node --input-type=module -e \"process.env.TREESEED_VERIFY_DRIVER='direct'; await import('
|
|
38
|
-
"verify:action": "node --input-type=module -e \"process.env.TREESEED_VERIFY_DRIVER='act'; await import('
|
|
39
|
-
"verify": "node --input-type=module -e \"await import('
|
|
38
|
+
"verify:local": "node --input-type=module -e \"process.env.TREESEED_VERIFY_DRIVER='direct'; await import('./scripts/verify-driver.mjs')\"",
|
|
39
|
+
"verify:action": "node --input-type=module -e \"process.env.TREESEED_VERIFY_DRIVER='act'; await import('./scripts/verify-driver.mjs')\"",
|
|
40
|
+
"verify": "node --input-type=module -e \"await import('./scripts/verify-driver.mjs')\"",
|
|
40
41
|
"release:setup": "npm run setup:ci",
|
|
41
42
|
"release:check-tag": "node ./scripts/run-ts.mjs ./scripts/assert-release-tag-version.ts",
|
|
42
43
|
"release:verify": "node ./scripts/run-ts.mjs ./scripts/release-verify.ts",
|
|
43
44
|
"release:publish": "node ./scripts/run-ts.mjs ./scripts/publish-package.ts"
|
|
44
45
|
},
|
|
45
46
|
"dependencies": {
|
|
46
|
-
"@treeseed/sdk": "^0.
|
|
47
|
+
"@treeseed/sdk": "^0.5.3",
|
|
47
48
|
"ink": "^7.0.0",
|
|
48
49
|
"react": "^19.2.5"
|
|
49
50
|
},
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
function runDirectVerify() {
|
|
6
|
+
const result = spawnSync('npm', ['run', 'verify:direct'], {
|
|
7
|
+
cwd: process.cwd(),
|
|
8
|
+
env: process.env,
|
|
9
|
+
stdio: 'inherit',
|
|
10
|
+
});
|
|
11
|
+
process.exit(result.status ?? 1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const entrypointCheckOnly = process.env.TREESEED_VERIFY_ENTRYPOINT_CHECK === 'true';
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
await import('@treeseed/sdk/scripts/verify-driver');
|
|
18
|
+
if (entrypointCheckOnly) {
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ERR_MODULE_NOT_FOUND') {
|
|
23
|
+
if (entrypointCheckOnly) {
|
|
24
|
+
process.stderr.write('Treeseed cli verify: @treeseed/sdk is required for verify entrypoint resolution.\n');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
if (process.env.TREESEED_VERIFY_DRIVER === 'act') {
|
|
28
|
+
process.stderr.write('Treeseed cli verify: `act` mode requires @treeseed/sdk to be installed.\n');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
runDirectVerify();
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|