codedeck 0.7.0 → 0.7.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 (121) hide show
  1. package/README.md +49 -34
  2. package/dist/cli/commands/claims.js +3 -2
  3. package/dist/cli/commands/claims.js.map +1 -1
  4. package/dist/cli/commands/doctor.js +8 -40
  5. package/dist/cli/commands/doctor.js.map +1 -1
  6. package/dist/cli/commands/logs.js +2 -2
  7. package/dist/cli/commands/logs.js.map +1 -1
  8. package/dist/cli/commands/open.js +8 -9
  9. package/dist/cli/commands/open.js.map +1 -1
  10. package/dist/cli/commands/ps.js +6 -6
  11. package/dist/cli/commands/ps.js.map +1 -1
  12. package/dist/cli/commands/review.js +36 -58
  13. package/dist/cli/commands/review.js.map +1 -1
  14. package/dist/cli/commands/run.js +27 -27
  15. package/dist/cli/commands/run.js.map +1 -1
  16. package/dist/cli/commands/setup.js +96 -314
  17. package/dist/cli/commands/setup.js.map +1 -1
  18. package/dist/cli/commands/show.js +2 -1
  19. package/dist/cli/commands/show.js.map +1 -1
  20. package/dist/cli/commands/ui.js +61 -0
  21. package/dist/cli/commands/ui.js.map +1 -0
  22. package/dist/cli/commands/usage.js +65 -42
  23. package/dist/cli/commands/usage.js.map +1 -1
  24. package/dist/cli/commands/wait.js +3 -2
  25. package/dist/cli/commands/wait.js.map +1 -1
  26. package/dist/cli/index.js +55 -39
  27. package/dist/cli/index.js.map +1 -1
  28. package/dist/cli/web-launch.js +126 -0
  29. package/dist/cli/web-launch.js.map +1 -0
  30. package/dist/config/config.js +0 -70
  31. package/dist/config/config.js.map +1 -1
  32. package/dist/config/paths.js +1 -0
  33. package/dist/config/paths.js.map +1 -1
  34. package/dist/config/setup.js +248 -0
  35. package/dist/config/setup.js.map +1 -0
  36. package/dist/config/web-host.js +31 -0
  37. package/dist/config/web-host.js.map +1 -0
  38. package/dist/config/web-port.js +24 -0
  39. package/dist/config/web-port.js.map +1 -0
  40. package/dist/core/claude-transcript.js +87 -0
  41. package/dist/core/claude-transcript.js.map +1 -1
  42. package/dist/core/errors.js +17 -0
  43. package/dist/core/errors.js.map +1 -1
  44. package/dist/core/models.js +4 -1
  45. package/dist/core/models.js.map +1 -1
  46. package/dist/core/pricing.js +73 -50
  47. package/dist/core/pricing.js.map +1 -1
  48. package/dist/core/roles.js +6 -5
  49. package/dist/core/roles.js.map +1 -1
  50. package/dist/core/run-skills.js +114 -0
  51. package/dist/core/run-skills.js.map +1 -0
  52. package/dist/core/run-usage.js +4 -1
  53. package/dist/core/run-usage.js.map +1 -1
  54. package/dist/core/session.js +6 -16
  55. package/dist/core/session.js.map +1 -1
  56. package/dist/core/usage-query.js +41 -0
  57. package/dist/core/usage-query.js.map +1 -0
  58. package/dist/daemon/build-id.js +30 -0
  59. package/dist/daemon/build-id.js.map +1 -0
  60. package/dist/daemon/daemon.js +1116 -767
  61. package/dist/daemon/daemon.js.map +1 -1
  62. package/dist/daemon/instance-lock.js +48 -0
  63. package/dist/daemon/instance-lock.js.map +1 -0
  64. package/dist/daemon/web-supervisor.js +251 -0
  65. package/dist/daemon/web-supervisor.js.map +1 -0
  66. package/dist/drivers/claude/driver.js +1 -0
  67. package/dist/drivers/claude/driver.js.map +1 -1
  68. package/dist/drivers/codex/driver.js +14 -3
  69. package/dist/drivers/codex/driver.js.map +1 -1
  70. package/dist/drivers/codex/host-skills.js +57 -0
  71. package/dist/drivers/codex/host-skills.js.map +1 -0
  72. package/dist/drivers/opencode/driver.js +6 -2
  73. package/dist/drivers/opencode/driver.js.map +1 -1
  74. package/dist/drivers/session-runtime.js +2 -0
  75. package/dist/drivers/session-runtime.js.map +1 -1
  76. package/dist/drivers/terminal.js +15 -0
  77. package/dist/drivers/terminal.js.map +1 -1
  78. package/dist/git/diff.js +29 -4
  79. package/dist/git/diff.js.map +1 -1
  80. package/dist/open/contract.js.map +1 -1
  81. package/dist/open/pty.js +330 -5
  82. package/dist/open/pty.js.map +1 -1
  83. package/dist/plugin/agents/general.md +33 -6
  84. package/dist/plugin/hooks/pane-ticker.ts +102 -0
  85. package/dist/plugin/hooks/register.tsx +33 -8
  86. package/dist/plugin/mods/agents/pane.ts +259 -136
  87. package/dist/plugin/mods/agents/types.ts +19 -3
  88. package/dist/plugin/statusline.sh +22 -34
  89. package/dist/store/database.js +5 -1
  90. package/dist/store/database.js.map +1 -1
  91. package/dist/store/sessions.js +55 -12
  92. package/dist/store/sessions.js.map +1 -1
  93. package/dist/utils/process.js +2 -16
  94. package/dist/utils/process.js.map +1 -1
  95. package/dist/web/brand.js +59 -0
  96. package/dist/web/brand.js.map +1 -0
  97. package/dist/web/child.js +67 -0
  98. package/dist/web/child.js.map +1 -0
  99. package/dist/web/home-page.js +48 -0
  100. package/dist/web/home-page.js.map +1 -0
  101. package/dist/web/review-page.js +12 -5
  102. package/dist/web/review-page.js.map +1 -1
  103. package/dist/web/security.js +170 -0
  104. package/dist/web/security.js.map +1 -0
  105. package/dist/web/server.js +198 -0
  106. package/dist/web/server.js.map +1 -0
  107. package/dist/web/setup-icons.js +37 -0
  108. package/dist/web/setup-icons.js.map +1 -0
  109. package/dist/web/setup-page.js +1795 -0
  110. package/dist/web/setup-page.js.map +1 -0
  111. package/dist/web/setup-routes.js +475 -0
  112. package/dist/web/setup-routes.js.map +1 -0
  113. package/dist/web/usage-page.js +609 -0
  114. package/dist/web/usage-page.js.map +1 -0
  115. package/dist/web/usage-routes.js +88 -0
  116. package/dist/web/usage-routes.js.map +1 -0
  117. package/dist/web/web-token.js +56 -0
  118. package/dist/web/web-token.js.map +1 -0
  119. package/package.json +1 -1
  120. package/dist/plugin/prompts/_partials/commit.md +0 -145
  121. package/dist/plugin/prompts/_partials/pr-writer.md +0 -186
@@ -9,7 +9,11 @@ import { getCliName } from "../cli-name.js";
9
9
  import { getRegistry } from "../../drivers/registry.js";
10
10
  import { ROLES } from "../../core/roles.js";
11
11
  import { getPaths } from "../../config/paths.js";
12
- import { BALANCED_PRESET, baseConfig, DISPATCHER_PRESET, EXPLORER_PRESET, getProfileSnapshot, ORCHESTRATOR_PRESETS, createSetupConfigStore, DEFAULT_CONFIG, extractProfileSnapshot, isOrchestratorMode, loadConfig, orchestratorModeLabel, parseProfileName, resolveEffectiveConfig, resolveOrchestratorMode, saveConfig, serializeConfig, } from "../../config/config.js";
12
+ import { buildSetupPlan, diffConfig, SetupUsageError, validateBindings, } from "../../config/setup.js";
13
+ import { BALANCED_PRESET, DISPATCHER_PRESET, EXPLORER_PRESET, ORCHESTRATOR_PRESETS, createSetupConfigStore, DEFAULT_CONFIG, isOrchestratorMode, loadConfig, orchestratorModeLabel, resolveOrchestratorMode, saveConfig, serializeConfig, } from "../../config/config.js";
14
+ import { parseOptionalWebPort } from "../../web/server.js";
15
+ import { launchWebPage } from "../web-launch.js";
16
+ export { diffConfig, SetupUsageError };
13
17
  const ORCHESTRATOR_SCREEN_ROLE = "orchestrator-mode";
14
18
  const ORCHESTRATOR_PICKER_GROUP = "orchestrator";
15
19
  const PARALLELISM_NONE = "none";
@@ -320,16 +324,8 @@ export function needsModelSetup(config, isTTY = isInteractiveTerminal()) {
320
324
  return false;
321
325
  // `agents` is what setup writes now. A config carrying only the older
322
326
  // per-harness `models` has never answered the per-role question, so it still
323
- // counts as unset. The active profile answers too: its agents are the ones
324
- // a launch would use.
325
- if (config == null)
326
- return true;
327
- try {
328
- return resolveEffectiveConfig(config).agents == null;
329
- }
330
- catch {
331
- return config.agents == null;
332
- }
327
+ // counts as unset.
328
+ return config?.agents == null;
333
329
  }
334
330
  /**
335
331
  * The catalog of every installed harness, flattened into one list.
@@ -476,7 +472,7 @@ function watchResize(listener) {
476
472
  }
477
473
  export async function runModelSetupWizard(options = {}) {
478
474
  const loaded = options.config ?? loadConfig();
479
- const { profile, config } = resolveSetupTarget(loaded, options.profile);
475
+ const config = loaded;
480
476
  if (!(options.isTTY ?? isInteractiveTerminal()))
481
477
  return config;
482
478
  const output = options.output ?? process.stdout;
@@ -571,22 +567,22 @@ export async function runModelSetupWizard(options = {}) {
571
567
  return config;
572
568
  }
573
569
  }
574
- const updatedConfig = { ...config, agents };
570
+ const selections = { agents };
575
571
  if (orchestrator !== undefined)
576
- updatedConfig.orchestrator = orchestrator;
572
+ selections.orchestrator = orchestrator;
577
573
  const sandboxSelection = answeredResults.find((result) => result.role === SANDBOX_SCREEN_ROLE);
578
574
  if (sandboxSelection?.kind === "picked") {
579
575
  if (sandboxSelection.id === SANDBOX_OFF || sandboxSelection.id === SANDBOX_ON) {
580
- updatedConfig.defaultSandbox = sandboxSelection.id;
576
+ selections.sandbox = sandboxSelection.id;
581
577
  }
582
578
  }
583
579
  const autocompactSelection = answeredResults.find((result) => result.role === AUTOCOMPACT_SCREEN_ROLE);
584
580
  if (autocompactSelection?.kind === "picked") {
585
581
  if (autocompactSelection.id === AUTOCOMPACT_ON) {
586
- updatedConfig.autocompact = { ...config.autocompact, enabled: true };
582
+ selections.autocompact = { enabled: true };
587
583
  }
588
584
  else if (autocompactSelection.id === AUTOCOMPACT_OFF && config.autocompact !== undefined) {
589
- updatedConfig.autocompact = { ...config.autocompact, enabled: false };
585
+ selections.autocompact = { enabled: false };
590
586
  }
591
587
  }
592
588
  // Per-role effort answers arrive as followups of their role screen, in the
@@ -601,16 +597,10 @@ export async function runModelSetupWizard(options = {}) {
601
597
  agents[target] = { ...agents[target], effort: result.id };
602
598
  }
603
599
  }
604
- updatedConfig.agents = { ...agents };
605
- const toSave = profile === undefined
606
- ? updatedConfig
607
- : {
608
- ...loaded,
609
- profiles: { ...loaded.profiles, [profile]: extractProfileSnapshot(updatedConfig) },
610
- };
600
+ const { proposedConfig: updatedConfig } = buildSetupPlan(loaded, selections);
611
601
  let saved = false;
612
602
  try {
613
- (options.save ?? saveConfig)(toSave);
603
+ (options.save ?? saveConfig)(updatedConfig);
614
604
  saved = true;
615
605
  }
616
606
  catch (error) {
@@ -631,32 +621,6 @@ export async function runModelSetupWizard(options = {}) {
631
621
  output.write(`\n saved: ${summary}${modeSummary}\n\n`);
632
622
  return updatedConfig;
633
623
  }
634
- export class SetupUsageError extends Error {
635
- constructor(message) {
636
- super(message);
637
- this.name = "SetupUsageError";
638
- }
639
- }
640
- function resolveSetupTarget(loaded, explicitProfile) {
641
- const isExplicit = explicitProfile !== undefined;
642
- const profile = isExplicit
643
- ? parseProfileName(explicitProfile)
644
- : loaded.activeProfile === undefined || loaded.activeProfile.trim() === ""
645
- ? undefined
646
- : parseProfileName(loaded.activeProfile);
647
- const snapshot = profile === undefined ? undefined : getProfileSnapshot(loaded, profile);
648
- if (!isExplicit && profile !== undefined && snapshot === undefined) {
649
- throw new SetupUsageError(`Active profile "${profile}" does not exist. Choose an existing profile with "codedeck profile use <name>" or pass --profile <name>.`);
650
- }
651
- return {
652
- ...(profile === undefined ? {} : { profile }),
653
- config: profile === undefined
654
- ? loaded
655
- : snapshot === undefined
656
- ? { ...baseConfig(loaded), agents: {} }
657
- : { ...baseConfig(loaded), ...snapshot },
658
- };
659
- }
660
624
  function invalidBindMessage(value) {
661
625
  return `Invalid --bind "${value}": expected role=harness:model[:effort] (role: general|orchestrator|reviewer|auditor; harness: claude|codex|opencode|omp; model: non-empty and without whitespace, control characters or '='; effort: ${REASONING_EFFORTS.join("|")})`;
662
626
  }
@@ -703,34 +667,34 @@ export function parseSetupArgs(args) {
703
667
  let refresh = false;
704
668
  let nonInteractive = false;
705
669
  let dryRun = false;
706
- let profile;
670
+ let tui = false;
671
+ let noOpen = false;
672
+ let port;
707
673
  for (let index = 0; index < args.length; index += 1) {
708
674
  const arg = args[index];
709
675
  if (arg === "--refresh") {
710
676
  refresh = true;
711
677
  continue;
712
678
  }
713
- if (arg === "--profile") {
679
+ if (arg === "--tui") {
680
+ tui = true;
681
+ continue;
682
+ }
683
+ if (arg === "--no-open") {
684
+ noOpen = true;
685
+ continue;
686
+ }
687
+ if (arg === "--port") {
714
688
  const value = args[index + 1];
715
689
  if (value === undefined || isSetupFlag(value)) {
716
- return parseError('Option "--profile" expects a name.', json);
690
+ return parseError('Option "--port" expects a port.', json);
717
691
  }
692
+ port = value;
718
693
  index += 1;
719
- try {
720
- profile = parseProfileName(value);
721
- }
722
- catch (error) {
723
- return parseError(error instanceof Error ? error.message : String(error), json);
724
- }
725
694
  continue;
726
695
  }
727
- if (arg.startsWith("--profile=")) {
728
- try {
729
- profile = parseProfileName(arg.slice("--profile=".length));
730
- }
731
- catch (error) {
732
- return parseError(error instanceof Error ? error.message : String(error), json);
733
- }
696
+ if (arg.startsWith("--port=")) {
697
+ port = arg.slice("--port=".length);
734
698
  continue;
735
699
  }
736
700
  if (arg === "--non-interactive") {
@@ -771,6 +735,12 @@ export function parseSetupArgs(args) {
771
735
  return parseError(`Unknown option "${arg}".`, json);
772
736
  return parseError(`Unexpected argument "${arg}".`, json);
773
737
  }
738
+ const batch = nonInteractive || json || dryRun || binds.length > 0;
739
+ if (tui && batch)
740
+ return parseError('Option "--tui" cannot be used with batch setup flags.', json);
741
+ if (port !== undefined && (json || dryRun || nonInteractive)) {
742
+ return parseError('Option "--port" cannot be used with --json, --dry-run, or --non-interactive.', json);
743
+ }
774
744
  return {
775
745
  ok: true,
776
746
  options: {
@@ -778,9 +748,11 @@ export function parseSetupArgs(args) {
778
748
  nonInteractive,
779
749
  json,
780
750
  dryRun,
751
+ tui,
752
+ noOpen,
753
+ ...(port === undefined ? {} : { port }),
781
754
  binds,
782
- batch: nonInteractive || json || dryRun || binds.length > 0,
783
- ...(profile === undefined ? {} : { profile }),
755
+ batch,
784
756
  },
785
757
  };
786
758
  }
@@ -815,202 +787,6 @@ function emptyEnvelope(message, code, path) {
815
787
  resultado: { status: "error", code, saved: false, message },
816
788
  };
817
789
  }
818
- function jsonObject(value) {
819
- return typeof value === "object" && value !== null && !Array.isArray(value);
820
- }
821
- function jsonValue(value) {
822
- if (value === undefined)
823
- return null;
824
- if (Array.isArray(value))
825
- return value.map(jsonValue);
826
- if (jsonObject(value)) {
827
- return Object.fromEntries(Object.keys(value).sort((left, right) => left.localeCompare(right)).map((key) => [key, jsonValue(value[key])]));
828
- }
829
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) {
830
- return value;
831
- }
832
- return null;
833
- }
834
- function jsonEqual(left, right) {
835
- return JSON.stringify(jsonValue(left)) === JSON.stringify(jsonValue(right));
836
- }
837
- function pointerPart(value) {
838
- return value.replaceAll("~", "~0").replaceAll("/", "~1");
839
- }
840
- function diffAt(output, pathValue, beforePresent, before, afterPresent, after, expandObjectChildren = false) {
841
- const beforeObject = beforePresent && jsonObject(before) ? before : undefined;
842
- const afterObject = afterPresent && jsonObject(after) ? after : undefined;
843
- if (beforeObject !== undefined && afterObject !== undefined) {
844
- const keys = [...new Set([...Object.keys(beforeObject), ...Object.keys(afterObject)])].sort((left, right) => left.localeCompare(right));
845
- if (keys.length === 0)
846
- return;
847
- for (const key of keys) {
848
- diffAt(output, `${pathValue}/${pointerPart(key)}`, Object.hasOwn(beforeObject, key), beforeObject[key], Object.hasOwn(afterObject, key), afterObject[key]);
849
- }
850
- return;
851
- }
852
- if (!beforePresent && afterObject !== undefined) {
853
- if (expandObjectChildren) {
854
- const keys = Object.keys(afterObject).sort((left, right) => left.localeCompare(right));
855
- if (keys.length === 0) {
856
- output.push({
857
- path: pathValue,
858
- beforePresent: false,
859
- before: null,
860
- afterPresent: true,
861
- after: jsonValue(afterObject),
862
- });
863
- return;
864
- }
865
- for (const key of keys) {
866
- const value = afterObject[key];
867
- const childPath = `${pathValue}/${pointerPart(key)}`;
868
- if (jsonObject(value)) {
869
- output.push({
870
- path: childPath,
871
- beforePresent: false,
872
- before: null,
873
- afterPresent: true,
874
- after: jsonValue(value),
875
- });
876
- }
877
- else {
878
- diffAt(output, childPath, false, undefined, true, value);
879
- }
880
- }
881
- return;
882
- }
883
- output.push({
884
- path: pathValue,
885
- beforePresent: false,
886
- before: null,
887
- afterPresent: true,
888
- after: jsonValue(afterObject),
889
- });
890
- return;
891
- }
892
- if (beforeObject !== undefined && !afterPresent) {
893
- if (expandObjectChildren) {
894
- const keys = Object.keys(beforeObject).sort((left, right) => left.localeCompare(right));
895
- if (keys.length === 0) {
896
- output.push({
897
- path: pathValue,
898
- beforePresent: true,
899
- before: jsonValue(beforeObject),
900
- afterPresent: false,
901
- after: null,
902
- });
903
- return;
904
- }
905
- for (const key of keys) {
906
- const value = beforeObject[key];
907
- const childPath = `${pathValue}/${pointerPart(key)}`;
908
- if (jsonObject(value)) {
909
- output.push({
910
- path: childPath,
911
- beforePresent: true,
912
- before: jsonValue(value),
913
- afterPresent: false,
914
- after: null,
915
- });
916
- }
917
- else {
918
- diffAt(output, childPath, true, value, false, undefined);
919
- }
920
- }
921
- return;
922
- }
923
- output.push({
924
- path: pathValue,
925
- beforePresent: true,
926
- before: jsonValue(beforeObject),
927
- afterPresent: false,
928
- after: null,
929
- });
930
- return;
931
- }
932
- if (beforePresent === afterPresent && (!beforePresent || jsonEqual(before, after)))
933
- return;
934
- output.push({
935
- path: pathValue,
936
- beforePresent,
937
- before: jsonValue(before),
938
- afterPresent,
939
- after: jsonValue(after),
940
- });
941
- }
942
- export function diffConfig(before, after) {
943
- const output = [];
944
- const beforeObject = jsonObject(before) ? before : {};
945
- const afterObject = jsonObject(after) ? after : {};
946
- const keys = [...new Set([...Object.keys(beforeObject), ...Object.keys(afterObject)])].sort((left, right) => left.localeCompare(right));
947
- for (const key of keys) {
948
- diffAt(output, `/${pointerPart(key)}`, Object.hasOwn(beforeObject, key), beforeObject[key], Object.hasOwn(afterObject, key), afterObject[key], true);
949
- }
950
- return output.sort((left, right) => left.path.localeCompare(right.path));
951
- }
952
- function catalogContains(catalog, model) {
953
- return catalog.providers.some((provider) => provider.models.some((candidate) => candidate.id === model || (candidate.aliases !== undefined && candidate.aliases.some((alias) => alias === model))));
954
- }
955
- function validateBindings(bindings, catalog) {
956
- const byAgent = new Map(catalog.models.map((item) => [item.agent, item]));
957
- const entries = bindings.map(({ role, binding }) => {
958
- const found = byAgent.get(binding.harness);
959
- if (found?.available === false) {
960
- return {
961
- role,
962
- harness: binding.harness,
963
- model: binding.model,
964
- status: "harness-unavailable",
965
- message: `Cannot apply binding for role "${role}": harness "${binding.harness}" is unavailable.`,
966
- };
967
- }
968
- if (catalog.status === "unavailable" || found === undefined) {
969
- return {
970
- role,
971
- harness: binding.harness,
972
- model: binding.model,
973
- status: "unverified",
974
- message: `Cannot validate model "${binding.model}" for harness "${binding.harness}": catalog unavailable.`,
975
- };
976
- }
977
- if (catalog.status === "offline" && !catalogContains(found, binding.model)) {
978
- return {
979
- role,
980
- harness: binding.harness,
981
- model: binding.model,
982
- status: "unverified",
983
- message: `Cannot validate model "${binding.model}" for harness "${binding.harness}": the catalog is stale. Re-run with --refresh.`,
984
- };
985
- }
986
- if (!catalogContains(found, binding.model)) {
987
- return {
988
- role,
989
- harness: binding.harness,
990
- model: binding.model,
991
- status: "unknown-model",
992
- message: `Model "${binding.model}" is not in the ${binding.harness} catalog for role "${role}".`,
993
- };
994
- }
995
- return { role, harness: binding.harness, model: binding.model, status: "accepted", message: "" };
996
- });
997
- const failed = entries.find((entry) => entry.status !== "accepted");
998
- const code = failed === undefined
999
- ? undefined
1000
- : failed.status === "harness-unavailable"
1001
- ? 11
1002
- : failed.status === "unknown-model"
1003
- ? 12
1004
- : 13;
1005
- let message = null;
1006
- if (catalog.status === "offline") {
1007
- message = failed?.status === "unverified" ? failed.message : "Catalog is stale; using offline cache.";
1008
- }
1009
- else if (catalog.status === "unavailable") {
1010
- message = failed?.message ?? "Catalog unavailable.";
1011
- }
1012
- return { entries, code, message };
1013
- }
1014
790
  function catalogValidation(catalog, message) {
1015
791
  if (catalog === undefined)
1016
792
  return notNeededCatalog();
@@ -1063,38 +839,23 @@ export async function runSetupBatch(options, dependencies = {}) {
1063
839
  return errorResult(read, null, notNeededCatalog(), [], [], read.readError ? 15 : 14, message);
1064
840
  }
1065
841
  const current = { ...DEFAULT_CONFIG, ...(read.config ?? {}) };
1066
- let profile;
1067
- let target;
1068
- try {
1069
- const resolved = resolveSetupTarget(current, options.profile);
1070
- profile = resolved.profile;
1071
- target = resolved.config;
1072
- }
1073
- catch (error) {
1074
- const message = error instanceof Error ? error.message : String(error);
1075
- writeLine(stderr, message);
1076
- return errorResult(read, null, notNeededCatalog(), [], [], 14, message);
1077
- }
1078
842
  const lastByRole = new Map();
1079
843
  options.binds.forEach((binding, index) => lastByRole.set(binding.role, index));
1080
844
  const winning = options.binds.filter((binding, index) => lastByRole.get(binding.role) === index);
1081
- const existingAgents = jsonObject(target.agents) ? target.agents : {};
1082
- const updatedAgents = {
1083
- ...existingAgents,
1084
- ...Object.fromEntries(winning.map(({ role, binding }) => [role, binding])),
1085
- };
1086
- const proposed = options.binds.length === 0
1087
- ? { ...current }
1088
- : profile === undefined
1089
- ? { ...current, agents: updatedAgents }
1090
- : {
1091
- ...current,
1092
- profiles: {
1093
- ...current.profiles,
1094
- [profile]: { ...extractProfileSnapshot(target), agents: updatedAgents },
1095
- },
1096
- };
1097
- const changes = diffConfig(current, proposed);
845
+ let proposed;
846
+ let changes;
847
+ if (winning.length === 0) {
848
+ proposed = { ...current };
849
+ changes = diffConfig(current, proposed);
850
+ }
851
+ else {
852
+ const agents = {};
853
+ for (const { role, binding } of winning)
854
+ agents[role] = binding;
855
+ const plan = buildSetupPlan(current, { agents });
856
+ proposed = plan.proposedConfig;
857
+ changes = plan.diff;
858
+ }
1098
859
  let catalog;
1099
860
  let bindingValidations = [];
1100
861
  let catalogMessage = null;
@@ -1189,9 +950,8 @@ export async function runSetupBatch(options, dependencies = {}) {
1189
950
  };
1190
951
  }
1191
952
  const message = "Configuration saved.";
1192
- const summarized = profile === undefined ? proposed : (proposed.profiles?.[profile] ?? proposed);
1193
953
  if (!options.json) {
1194
- writeLine(stderr, profile === undefined ? `saved: ${configSummary(summarized)}` : `saved profile "${profile}": ${configSummary(summarized)}`);
954
+ writeLine(stderr, `saved: ${configSummary(proposed)}`);
1195
955
  }
1196
956
  return {
1197
957
  code: 0,
@@ -1202,14 +962,18 @@ function commandTokens(opts, command) {
1202
962
  const tokens = [];
1203
963
  if (opts.refresh)
1204
964
  tokens.push("--refresh");
965
+ if (opts.tui)
966
+ tokens.push("--tui");
967
+ if (opts.open === false)
968
+ tokens.push("--no-open");
969
+ if (typeof opts.port === "string")
970
+ tokens.push("--port", opts.port);
1205
971
  if (opts.nonInteractive)
1206
972
  tokens.push("--non-interactive");
1207
973
  if (opts.json)
1208
974
  tokens.push("--json");
1209
975
  if (opts.dryRun)
1210
976
  tokens.push("--dry-run");
1211
- if (typeof opts.profile === "string")
1212
- tokens.push("--profile", opts.profile);
1213
977
  const binds = Array.isArray(opts.bind) ? opts.bind : opts.bind === undefined ? [] : [opts.bind];
1214
978
  for (const bind of binds) {
1215
979
  tokens.push("--bind");
@@ -1231,28 +995,44 @@ export async function executeSetupAction(args, dependencies = {}) {
1231
995
  }
1232
996
  const tty = dependencies.isTTY ?? Boolean(input.isTTY && stdout.isTTY);
1233
997
  if (!parsed.options.batch) {
1234
- if (!tty) {
1235
- const message = `${getCliName()} setup needs a terminal on both stdin and stdout.`;
1236
- writeError(message);
1237
- return { code: 1 };
998
+ if (parsed.options.tui) {
999
+ if (!tty) {
1000
+ writeError(`${getCliName()} setup needs a terminal on both stdin and stdout.`);
1001
+ return { code: 1 };
1002
+ }
1003
+ try {
1004
+ await (dependencies.runWizard ?? runModelSetupWizard)({
1005
+ registry: dependencies.registry,
1006
+ input: dependencies.input,
1007
+ output: dependencies.stdout,
1008
+ refresh: parsed.options.refresh,
1009
+ discoverModels: dependencies.wizardDiscoverModels,
1010
+ save: dependencies.saveConfig,
1011
+ isTTY: tty,
1012
+ });
1013
+ }
1014
+ catch (error) {
1015
+ writeError(error instanceof Error ? error.message : String(error));
1016
+ return { code: 1 };
1017
+ }
1018
+ return { code: 0 };
1238
1019
  }
1020
+ let port;
1239
1021
  try {
1240
- await runModelSetupWizard({
1241
- registry: dependencies.registry,
1242
- input: dependencies.input,
1243
- output: dependencies.stdout,
1244
- refresh: parsed.options.refresh,
1245
- discoverModels: dependencies.wizardDiscoverModels,
1246
- save: dependencies.saveConfig,
1247
- isTTY: tty,
1248
- ...(parsed.options.profile === undefined ? {} : { profile: parsed.options.profile }),
1249
- });
1022
+ port = parseOptionalWebPort(parsed.options.port);
1250
1023
  }
1251
1024
  catch (error) {
1252
1025
  writeError(error instanceof Error ? error.message : String(error));
1253
1026
  return { code: 1 };
1254
1027
  }
1255
- return { code: 0 };
1028
+ const code = await (dependencies.launch ?? launchWebPage)({
1029
+ path: "/setup",
1030
+ query: parsed.options.refresh ? { refresh: "1" } : {},
1031
+ title: "CodeDeck setup",
1032
+ port,
1033
+ open: !parsed.options.noOpen,
1034
+ }, { error: writeError });
1035
+ return { code: code === 0 ? 0 : 1 };
1256
1036
  }
1257
1037
  const result = await runSetupBatch(parsed.options, dependencies);
1258
1038
  if (parsed.options.json)
@@ -1266,10 +1046,12 @@ export function registerSetupCommand(program, dependencies = {}) {
1266
1046
  .allowUnknownOption(true)
1267
1047
  .allowExcessArguments(true)
1268
1048
  .option("--refresh", "ignore the cached catalog and rediscover")
1049
+ .option("--tui", "use the frozen terminal setup wizard")
1050
+ .option("--port <n>", "port for a new console (default: web.port from config, else 7777)")
1051
+ .option("--no-open", "print the setup URL without opening a browser")
1269
1052
  .option("--non-interactive", "run setup without the picker")
1270
1053
  .option("--json", "output one machine-readable envelope")
1271
1054
  .option("--dry-run", "show the proposed config without writing it")
1272
- .option("--profile <name>", "edit a saved profile instead of the active setup")
1273
1055
  .option("--bind [binding]", "bind a role to a harness and model", (value, previous = []) => [...previous, value], [])
1274
1056
  .action(async (opts, command) => {
1275
1057
  const result = await executeSetupAction(commandTokens(opts, command), dependencies);