@mstar-harness/cli 1.0.6 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/mstar-harness.js +63 -341
  2. package/package.json +1 -1
@@ -2351,7 +2351,6 @@ import { fileURLToPath } from "url";
2351
2351
  // ../../node_modules/@inquirer/core/dist/lib/key.js
2352
2352
  var isUpKey = (key, keybindings = []) => key.name === "up" || keybindings.includes("vim") && key.name === "k" || keybindings.includes("emacs") && key.ctrl && key.name === "p";
2353
2353
  var isDownKey = (key, keybindings = []) => key.name === "down" || keybindings.includes("vim") && key.name === "j" || keybindings.includes("emacs") && key.ctrl && key.name === "n";
2354
- var isSpaceKey = (key) => key.name === "space";
2355
2354
  var isBackspaceKey = (key) => key.name === "backspace";
2356
2355
  var isNumberKey = (key) => "1234567890".includes(key.name);
2357
2356
  var isEnterKey = (key) => key.name === "enter" || key.name === "return";
@@ -3783,23 +3782,17 @@ class Separator {
3783
3782
  return Boolean(choice && typeof choice === "object" && "type" in choice && choice.type === "separator");
3784
3783
  }
3785
3784
  }
3786
- // ../../node_modules/@inquirer/checkbox/dist/index.js
3785
+ // ../../node_modules/@inquirer/select/dist/index.js
3787
3786
  import { styleText as styleText3 } from "node:util";
3788
- var checkboxTheme = {
3789
- icon: {
3790
- checked: styleText3("green", dist_default.circleFilled),
3791
- unchecked: dist_default.circle,
3792
- cursor: dist_default.pointer,
3793
- disabledChecked: styleText3("green", dist_default.circleDouble),
3794
- disabledUnchecked: "-"
3795
- },
3787
+ var selectTheme = {
3788
+ icon: { cursor: dist_default.pointer },
3796
3789
  style: {
3797
3790
  disabled: (text) => styleText3("dim", text),
3798
- renderSelectedChoices: (selectedChoices) => selectedChoices.map((choice) => choice.short).join(", "),
3799
3791
  description: (text) => styleText3("cyan", text),
3800
3792
  keysHelpTip: (keys) => keys.map(([key, action]) => `${styleText3("bold", key)} ${styleText3("dim", action)}`).join(styleText3("dim", " • "))
3801
3793
  },
3802
- i18n: { disabledError: "This option is disabled and cannot be toggled." },
3794
+ i18n: { disabledError: "This option is disabled and cannot be selected." },
3795
+ indexMode: "hidden",
3803
3796
  keybindings: []
3804
3797
  };
3805
3798
  function isSelectable(item) {
@@ -3808,192 +3801,7 @@ function isSelectable(item) {
3808
3801
  function isNavigable(item) {
3809
3802
  return !Separator.isSeparator(item);
3810
3803
  }
3811
- function isChecked(item) {
3812
- return !Separator.isSeparator(item) && item.checked;
3813
- }
3814
- function toggle(item) {
3815
- return isSelectable(item) ? { ...item, checked: !item.checked } : item;
3816
- }
3817
- function check(checked) {
3818
- return function(item) {
3819
- return isSelectable(item) ? { ...item, checked } : item;
3820
- };
3821
- }
3822
3804
  function normalizeChoices(choices) {
3823
- return choices.map((choice) => {
3824
- if (Separator.isSeparator(choice))
3825
- return choice;
3826
- if (typeof choice !== "object" || choice === null || !("value" in choice)) {
3827
- const name2 = String(choice);
3828
- return {
3829
- value: choice,
3830
- name: name2,
3831
- short: name2,
3832
- checkedName: name2,
3833
- disabled: false,
3834
- checked: false
3835
- };
3836
- }
3837
- const name = choice.name ?? String(choice.value);
3838
- const normalizedChoice = {
3839
- value: choice.value,
3840
- name,
3841
- short: choice.short ?? name,
3842
- checkedName: choice.checkedName ?? name,
3843
- disabled: choice.disabled ?? false,
3844
- checked: choice.checked ?? false
3845
- };
3846
- if (choice.description) {
3847
- normalizedChoice.description = choice.description;
3848
- }
3849
- return normalizedChoice;
3850
- });
3851
- }
3852
- var dist_default4 = createPrompt((config, done) => {
3853
- const { pageSize = 7, loop = true, required, validate = () => true } = config;
3854
- const shortcuts = { all: "a", invert: "i", ...config.shortcuts };
3855
- const theme = makeTheme(checkboxTheme, config.theme);
3856
- const { keybindings } = theme;
3857
- const [status, setStatus] = useState("idle");
3858
- const prefix = usePrefix({ status, theme });
3859
- const [items, setItems] = useState(normalizeChoices(config.choices));
3860
- const bounds = useMemo(() => {
3861
- const first = items.findIndex(isNavigable);
3862
- const last = items.findLastIndex(isNavigable);
3863
- if (first === -1) {
3864
- throw new ValidationError("[checkbox prompt] No selectable choices. All choices are disabled.");
3865
- }
3866
- return { first, last };
3867
- }, [items]);
3868
- const [active, setActive] = useState(bounds.first);
3869
- const [errorMsg, setError] = useState();
3870
- useKeypress(async (key) => {
3871
- if (isEnterKey(key)) {
3872
- const selection = items.filter(isChecked);
3873
- const isValid = await validate([...selection]);
3874
- if (required && !selection.length) {
3875
- setError("At least one choice must be selected");
3876
- } else if (isValid === true) {
3877
- setStatus("done");
3878
- done(selection.map((choice) => choice.value));
3879
- } else {
3880
- setError(isValid || "You must select a valid value");
3881
- }
3882
- } else if (isUpKey(key, keybindings) || isDownKey(key, keybindings)) {
3883
- if (errorMsg) {
3884
- setError(undefined);
3885
- }
3886
- if (loop || isUpKey(key, keybindings) && active !== bounds.first || isDownKey(key, keybindings) && active !== bounds.last) {
3887
- const offset = isUpKey(key, keybindings) ? -1 : 1;
3888
- let next = active;
3889
- do {
3890
- next = (next + offset + items.length) % items.length;
3891
- } while (!isNavigable(items[next]));
3892
- setActive(next);
3893
- }
3894
- } else if (isSpaceKey(key)) {
3895
- const activeItem = items[active];
3896
- if (activeItem && !Separator.isSeparator(activeItem)) {
3897
- if (activeItem.disabled) {
3898
- setError(theme.i18n.disabledError);
3899
- } else {
3900
- setError(undefined);
3901
- setItems(items.map((choice, i) => i === active ? toggle(choice) : choice));
3902
- }
3903
- }
3904
- } else if (key.name === shortcuts.all) {
3905
- const selectAll = items.some((choice) => isSelectable(choice) && !choice.checked);
3906
- setItems(items.map(check(selectAll)));
3907
- } else if (key.name === shortcuts.invert) {
3908
- setItems(items.map(toggle));
3909
- } else if (isNumberKey(key)) {
3910
- const selectedIndex = Number(key.name) - 1;
3911
- let selectableIndex = -1;
3912
- const position = items.findIndex((item) => {
3913
- if (Separator.isSeparator(item))
3914
- return false;
3915
- selectableIndex++;
3916
- return selectableIndex === selectedIndex;
3917
- });
3918
- const selectedItem = items[position];
3919
- if (selectedItem && isSelectable(selectedItem)) {
3920
- setActive(position);
3921
- setItems(items.map((choice, i) => i === position ? toggle(choice) : choice));
3922
- }
3923
- }
3924
- });
3925
- const message = theme.style.message(config.message, status);
3926
- let description;
3927
- const page = usePagination({
3928
- items,
3929
- active,
3930
- renderItem({ item, isActive }) {
3931
- if (Separator.isSeparator(item)) {
3932
- return ` ${item.separator}`;
3933
- }
3934
- const cursor = isActive ? theme.icon.cursor : " ";
3935
- if (item.disabled) {
3936
- const disabledLabel = typeof item.disabled === "string" ? item.disabled : "(disabled)";
3937
- const checkbox2 = item.checked ? theme.icon.disabledChecked : theme.icon.disabledUnchecked;
3938
- return theme.style.disabled(`${cursor}${checkbox2} ${item.name} ${disabledLabel}`);
3939
- }
3940
- if (isActive) {
3941
- description = item.description;
3942
- }
3943
- const checkbox = item.checked ? theme.icon.checked : theme.icon.unchecked;
3944
- const name = item.checked ? item.checkedName : item.name;
3945
- const color = isActive ? theme.style.highlight : (x) => x;
3946
- return color(`${cursor}${checkbox} ${name}`);
3947
- },
3948
- pageSize,
3949
- loop
3950
- });
3951
- if (status === "done") {
3952
- const selection = items.filter(isChecked);
3953
- const answer = theme.style.answer(theme.style.renderSelectedChoices(selection, items));
3954
- return [prefix, message, answer].filter(Boolean).join(" ");
3955
- }
3956
- const keys = [
3957
- ["↑↓", "navigate"],
3958
- ["space", "select"]
3959
- ];
3960
- if (shortcuts.all)
3961
- keys.push([shortcuts.all, "all"]);
3962
- if (shortcuts.invert)
3963
- keys.push([shortcuts.invert, "invert"]);
3964
- keys.push(["⏎", "submit"]);
3965
- const helpLine = theme.style.keysHelpTip(keys);
3966
- const lines = [
3967
- [prefix, message].filter(Boolean).join(" "),
3968
- page,
3969
- " ",
3970
- description ? theme.style.description(description) : "",
3971
- errorMsg ? theme.style.error(errorMsg) : "",
3972
- helpLine
3973
- ].filter(Boolean).join(`
3974
- `).trimEnd();
3975
- return `${lines}${cursorHide}`;
3976
- });
3977
- // ../../node_modules/@inquirer/select/dist/index.js
3978
- import { styleText as styleText4 } from "node:util";
3979
- var selectTheme = {
3980
- icon: { cursor: dist_default.pointer },
3981
- style: {
3982
- disabled: (text) => styleText4("dim", text),
3983
- description: (text) => styleText4("cyan", text),
3984
- keysHelpTip: (keys) => keys.map(([key, action]) => `${styleText4("bold", key)} ${styleText4("dim", action)}`).join(styleText4("dim", " • "))
3985
- },
3986
- i18n: { disabledError: "This option is disabled and cannot be selected." },
3987
- indexMode: "hidden",
3988
- keybindings: []
3989
- };
3990
- function isSelectable2(item) {
3991
- return !Separator.isSeparator(item) && !item.disabled;
3992
- }
3993
- function isNavigable2(item) {
3994
- return !Separator.isSeparator(item);
3995
- }
3996
- function normalizeChoices2(choices) {
3997
3805
  return choices.map((choice) => {
3998
3806
  if (Separator.isSeparator(choice))
3999
3807
  return choice;
@@ -4019,7 +3827,7 @@ function normalizeChoices2(choices) {
4019
3827
  return normalizedChoice;
4020
3828
  });
4021
3829
  }
4022
- var dist_default5 = createPrompt((config, done) => {
3830
+ var dist_default4 = createPrompt((config, done) => {
4023
3831
  const { loop = true, pageSize = 7 } = config;
4024
3832
  const theme = makeTheme(selectTheme, config.theme);
4025
3833
  const { keybindings } = theme;
@@ -4027,10 +3835,10 @@ var dist_default5 = createPrompt((config, done) => {
4027
3835
  const prefix = usePrefix({ status, theme });
4028
3836
  const searchTimeoutRef = useRef();
4029
3837
  const searchEnabled = !keybindings.includes("vim");
4030
- const items = useMemo(() => normalizeChoices2(config.choices), [config.choices]);
3838
+ const items = useMemo(() => normalizeChoices(config.choices), [config.choices]);
4031
3839
  const bounds = useMemo(() => {
4032
- const first = items.findIndex(isNavigable2);
4033
- const last = items.findLastIndex(isNavigable2);
3840
+ const first = items.findIndex(isNavigable);
3841
+ const last = items.findLastIndex(isNavigable);
4034
3842
  if (first === -1) {
4035
3843
  throw new ValidationError("[select prompt] No selectable choices. All choices are disabled.");
4036
3844
  }
@@ -4039,7 +3847,7 @@ var dist_default5 = createPrompt((config, done) => {
4039
3847
  const defaultItemIndex = useMemo(() => {
4040
3848
  if (!("default" in config))
4041
3849
  return -1;
4042
- return items.findIndex((item) => isSelectable2(item) && item.value === config.default);
3850
+ return items.findIndex((item) => isSelectable(item) && item.value === config.default);
4043
3851
  }, [config.default, items]);
4044
3852
  const [active, setActive] = useState(defaultItemIndex === -1 ? bounds.first : defaultItemIndex);
4045
3853
  const selectedChoice = items[active];
@@ -4066,7 +3874,7 @@ var dist_default5 = createPrompt((config, done) => {
4066
3874
  let next = active;
4067
3875
  do {
4068
3876
  next = (next + offset + items.length) % items.length;
4069
- } while (!isNavigable2(items[next]));
3877
+ } while (!isNavigable(items[next]));
4070
3878
  setActive(next);
4071
3879
  }
4072
3880
  } else if (isNumberKey(key) && !Number.isNaN(Number(rl.line))) {
@@ -4079,7 +3887,7 @@ var dist_default5 = createPrompt((config, done) => {
4079
3887
  return selectableIndex === selectedIndex;
4080
3888
  });
4081
3889
  const item = items[position];
4082
- if (item != null && isSelectable2(item)) {
3890
+ if (item != null && isSelectable(item)) {
4083
3891
  setActive(position);
4084
3892
  }
4085
3893
  searchTimeoutRef.current = setTimeout(() => {
@@ -4090,7 +3898,7 @@ var dist_default5 = createPrompt((config, done) => {
4090
3898
  } else if (searchEnabled) {
4091
3899
  const searchTerm = rl.line.toLowerCase();
4092
3900
  const matchIndex = items.findIndex((item) => {
4093
- if (Separator.isSeparator(item) || !isSelectable2(item))
3901
+ if (Separator.isSeparator(item) || !isSelectable(item))
4094
3902
  return false;
4095
3903
  return item.name.toLowerCase().startsWith(searchTerm);
4096
3904
  });
@@ -4189,31 +3997,6 @@ var ROLE_GROUPS = {
4189
3997
  };
4190
3998
 
4191
3999
  // src/assignment.ts
4192
- function parseSelection(rawValues, models, maxCount, singleOnly = false) {
4193
- const picked = (rawValues || []).map((item) => item.trim()).filter(Boolean);
4194
- if (!picked.length)
4195
- return { ok: false, reason: "empty" };
4196
- if (singleOnly && picked.length !== 1)
4197
- return { ok: false, reason: "single_required" };
4198
- if (picked.length > maxCount)
4199
- return { ok: false, reason: "too_many" };
4200
- const invalid = picked.filter((id) => !models.includes(id));
4201
- if (invalid.length)
4202
- return { ok: false, reason: "invalid", invalid };
4203
- return { ok: true, value: picked };
4204
- }
4205
- function requireValidatedSelection(label, rawValues, models, maxCount, singleOnly = false) {
4206
- const parsed = parseSelection(rawValues, models, maxCount, singleOnly);
4207
- if (parsed.ok)
4208
- return parsed.value;
4209
- if (parsed.reason === "empty")
4210
- throw new Error(`Missing option for ${label}.`);
4211
- if (parsed.reason === "single_required")
4212
- throw new Error(`${label} requires exactly one model.`);
4213
- if (parsed.reason === "too_many")
4214
- throw new Error(`${label} allows at most ${maxCount} model(s).`);
4215
- throw new Error(`${label} contains invalid model(s): ${parsed.invalid.join(", ")}`);
4216
- }
4217
4000
  function pickByOrder(roleIds, selectedModels) {
4218
4001
  const assignments = {};
4219
4002
  for (let i = 0;i < roleIds.length; i += 1) {
@@ -4248,10 +4031,6 @@ import path4 from "node:path";
4248
4031
  // src/utils.ts
4249
4032
  import fs from "node:fs";
4250
4033
  import path2 from "node:path";
4251
- function normalizeModelList(raw) {
4252
- return raw.split(`
4253
- `).map((line) => line.trim()).filter(Boolean);
4254
- }
4255
4034
  function ensureObject(value) {
4256
4035
  if (value && typeof value === "object" && !Array.isArray(value))
4257
4036
  return value;
@@ -4732,7 +4511,6 @@ var cursorAdapter = {
4732
4511
  // src/adapters/opencode.ts
4733
4512
  import os4 from "node:os";
4734
4513
  import path6 from "node:path";
4735
- import { execFileSync as execFileSync2 } from "node:child_process";
4736
4514
  var OPENCODE_CONFIG_SCHEMA = "https://opencode.ai/config.json";
4737
4515
  var MSTAR_OPENCODE_PLUGIN = "@mstar-harness/opencode@latest";
4738
4516
  function isLegacyMorningStarGitPlugin(plugin) {
@@ -4750,13 +4528,6 @@ function isMstarHarnessOpencodePlugin(plugin) {
4750
4528
  function isAnyMstarHarnessOpencodeSlot(plugin) {
4751
4529
  return isLegacyMorningStarGitPlugin(plugin) || isMstarHarnessOpencodePlugin(plugin);
4752
4530
  }
4753
- function getOpencodeModels() {
4754
- const raw = execFileSync2("opencode", ["models"], { encoding: "utf8" });
4755
- const models = normalizeModelList(raw);
4756
- if (!models.length)
4757
- throw new Error("`opencode models` returned no model entries.");
4758
- return models;
4759
- }
4760
4531
  function resolveOpencodeConfigPath(scope, outputPath) {
4761
4532
  if (outputPath && outputPath.trim()) {
4762
4533
  const raw = outputPath.trim();
@@ -4792,6 +4563,8 @@ function updatePluginList(config) {
4792
4563
  return next;
4793
4564
  }
4794
4565
  function applyAssignments(config, assignments) {
4566
+ if (!Object.keys(assignments).length)
4567
+ return config;
4795
4568
  const next = ensureObject(config);
4796
4569
  const agent = ensureObject(next.agent);
4797
4570
  next.agent = agent;
@@ -4809,38 +4582,36 @@ function validateSetup(config) {
4809
4582
  }
4810
4583
  const plugins = Array.isArray(config.plugin) ? config.plugin : [];
4811
4584
  const hasMstarOpencode = plugins.some((item) => typeof item === "string" && isAnyMstarHarnessOpencodeSlot(item.trim()));
4812
- if (!hasMstarOpencode)
4585
+ if (!hasMstarOpencode) {
4813
4586
  errors2.push("Missing @mstar-harness/opencode plugin entry in `plugin` (or legacy morning-star git plugin).");
4814
- const agent = ensureObject(config.agent);
4815
- for (const roleId of ALL_ROLES) {
4816
- const role = ensureObject(agent[roleId]);
4817
- if (typeof role.model !== "string" || !role.model.trim()) {
4818
- errors2.push(`Missing model for role: ${roleId}`);
4819
- }
4820
4587
  }
4821
4588
  return errors2;
4822
4589
  }
4823
4590
  function getDoctorWarnings(config) {
4591
+ const warnings = [];
4824
4592
  const plugins = Array.isArray(config.plugin) ? config.plugin : [];
4825
4593
  const strings = plugins.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
4826
4594
  const hasNpm = strings.some(isMstarHarnessOpencodePlugin);
4827
4595
  const hasLegacy = strings.some(isLegacyMorningStarGitPlugin);
4828
4596
  if (hasLegacy && !hasNpm) {
4829
- return [
4830
- "Plugin list uses legacy `morning-star@git+…` for this harness; run `mstar-harness init --target opencode` (or `--yes` with model flags) to rewrite to `@mstar-harness/opencode@latest`."
4831
- ];
4597
+ warnings.push("Plugin list uses legacy `morning-star@git+…` for this harness; run `mstar-harness init --target opencode` to rewrite to `@mstar-harness/opencode@latest`.");
4832
4598
  }
4833
4599
  if (hasLegacy && hasNpm) {
4834
- return [
4835
- "Both legacy `morning-star@git+…` and `@mstar-harness/opencode` appear in `plugin`; run `init` again to dedupe and keep a single npm plugin line."
4836
- ];
4600
+ warnings.push("Both legacy `morning-star@git+…` and `@mstar-harness/opencode` appear in `plugin`; run `init` again to dedupe and keep a single npm plugin line.");
4601
+ }
4602
+ const agent = ensureObject(config.agent);
4603
+ const missingModels = ALL_ROLES.filter((roleId) => {
4604
+ const role = ensureObject(agent[roleId]);
4605
+ return typeof role.model !== "string" || !role.model.trim();
4606
+ });
4607
+ if (missingModels.length) {
4608
+ warnings.push(`${missingModels.length} role(s) have no explicit agent.<role>.model — OpenCode default model will be used (recommended for fastest setup).`);
4837
4609
  }
4838
- return [];
4610
+ return warnings;
4839
4611
  }
4840
4612
  var opencodeAdapter = {
4841
4613
  target: "opencode",
4842
4614
  mode: "config",
4843
- getAvailableModels: () => getOpencodeModels(),
4844
4615
  resolveConfigPath: (scope, outputPath) => resolveOpencodeConfigPath(scope, outputPath),
4845
4616
  mutateConfigForInit: (config, assignments) => {
4846
4617
  const withSchema = ensureConfigSchema(config);
@@ -4852,6 +4623,7 @@ var opencodeAdapter = {
4852
4623
  printPostSetupSummary: () => {
4853
4624
  console.log(`Schema: ${OPENCODE_CONFIG_SCHEMA} (ensured)`);
4854
4625
  console.log(`Plugin: ${MSTAR_OPENCODE_PLUGIN} (ensured; legacy git morning-star entries removed)`);
4626
+ console.log("Role models: left to OpenCode defaults (set agent.<role>.model in opencode.json only if you want overrides)");
4855
4627
  }
4856
4628
  };
4857
4629
 
@@ -4914,83 +4686,32 @@ function logStep(message) {
4914
4686
  console.log(import_picocolors.default.cyan(message));
4915
4687
  }
4916
4688
  async function pickTargetInteractive() {
4917
- return dist_default5({
4689
+ return dist_default4({
4918
4690
  message: "Select install target",
4919
4691
  choices: SUPPORTED_TARGETS.map((target) => ({ name: target, value: target }))
4920
4692
  });
4921
4693
  }
4922
- var ASCII_ICONS = {
4923
- checked: "(*)",
4924
- unchecked: "( )",
4925
- cursor: ">"
4926
- };
4927
- async function pickModelsInteractive(params) {
4928
- const choices = params.models.map((model) => ({ name: model, value: model }));
4929
- if (params.single) {
4930
- const picked = await dist_default5({
4931
- message: `${params.title} (${params.hint})`,
4932
- choices,
4933
- theme: { icon: { cursor: ASCII_ICONS.cursor } }
4934
- });
4935
- return [picked];
4936
- }
4937
- return dist_default4({
4938
- message: `${params.title} (${params.hint})`,
4939
- choices,
4940
- theme: { icon: ASCII_ICONS },
4941
- validate: (picked) => {
4942
- if (picked.length < 1)
4943
- return "Pick at least one model.";
4944
- if (picked.length > params.maxCount)
4945
- return `Pick at most ${params.maxCount} models.`;
4946
- return true;
4947
- }
4948
- });
4694
+ function hasExplicitModelFlags(options) {
4695
+ return Boolean(options.pmModel || options.strategicModels || options.devModels || options.qcModels || options.otherModels);
4949
4696
  }
4950
- async function resolveSelections(options, models) {
4951
- if (options.yes) {
4952
- return {
4953
- pm: requireValidatedSelection("pm-model", options.pmModel ? [options.pmModel] : undefined, models, 1, true),
4954
- strategic: requireValidatedSelection("strategic-models", parseCsv(options.strategicModels), models, 3),
4955
- dev: requireValidatedSelection("dev-models", parseCsv(options.devModels), models, 3),
4956
- qc: requireValidatedSelection("qc-models", parseCsv(options.qcModels), models, 3),
4957
- others: requireValidatedSelection("other-models", parseCsv(options.otherModels), models, 3)
4958
- };
4959
- }
4960
- logStep("Step 4/7 - Configure models by role group");
4961
- return {
4962
- pm: await pickModelsInteractive({
4963
- title: "1) project-manager",
4964
- hint: "orchestrator role, prefer strong agentic model",
4965
- models,
4966
- maxCount: 1,
4967
- single: true
4968
- }),
4969
- strategic: await pickModelsInteractive({
4970
- title: "2) architect / product-manager / prompt-engineer",
4971
- hint: "decision-heavy roles, prefer high intelligence",
4972
- models,
4973
- maxCount: 3
4974
- }),
4975
- dev: await pickModelsInteractive({
4976
- title: "3) fullstack-dev / fullstack-dev-2 / frontend-dev",
4977
- hint: "prefer coding-focused models",
4978
- models,
4979
- maxCount: 3
4980
- }),
4981
- qc: await pickModelsInteractive({
4982
- title: "4) qc-specialist / qc-specialist-2 / qc-specialist-3",
4983
- hint: "prefer three distinct models",
4984
- models,
4985
- maxCount: 3
4986
- }),
4987
- others: await pickModelsInteractive({
4988
- title: "5) other roles",
4989
- hint: "up to 3 models, random assignment",
4990
- models,
4991
- maxCount: 3
4992
- })
4697
+ function resolveExplicitModelAssignments(options) {
4698
+ const allow = (label, values, max, required) => {
4699
+ if (!values?.length) {
4700
+ if (required)
4701
+ throw new Error(`${label} is required when any --*-model flag is set.`);
4702
+ return [];
4703
+ }
4704
+ if (values.length > max)
4705
+ throw new Error(`${label}: pick at most ${max} model(s).`);
4706
+ return values;
4993
4707
  };
4708
+ return buildModelAssignments({
4709
+ pm: allow("pm-model", options.pmModel ? [options.pmModel] : undefined, 1, true),
4710
+ strategic: allow("strategic-models", parseCsv(options.strategicModels), 3, true),
4711
+ dev: allow("dev-models", parseCsv(options.devModels), 3, true),
4712
+ qc: allow("qc-models", parseCsv(options.qcModels), 3, true),
4713
+ others: allow("other-models", parseCsv(options.otherModels), 3, true)
4714
+ });
4994
4715
  }
4995
4716
  async function runInit2(options) {
4996
4717
  const target = options.target || (options.yes ? "opencode" : await pickTargetInteractive());
@@ -5013,14 +4734,14 @@ async function runInit2(options) {
5013
4734
  }
5014
4735
  return;
5015
4736
  }
5016
- logStep(`Step 3/7 - Fetch available models from ${adapter.target}`);
5017
- const models = adapter.getAvailableModels?.();
5018
- if (!models)
5019
- throw new Error(`Adapter ${target} does not implement model discovery.`);
5020
- const selections = await resolveSelections(options, models);
5021
- logStep("Step 5/7 - Build role model assignments");
5022
- const assignments = buildModelAssignments(selections);
5023
- logStep("Step 6/7 - Update config");
4737
+ const useExplicitModels = hasExplicitModelFlags(options);
4738
+ const assignments = useExplicitModels ? resolveExplicitModelAssignments(options) : {};
4739
+ if (useExplicitModels) {
4740
+ logStep("Step 3/4 - Apply explicit role model overrides from CLI flags");
4741
+ } else {
4742
+ logStep("Step 3/4 - Fast setup (schema + plugin; OpenCode default models)");
4743
+ }
4744
+ logStep("Step 4/4 - Update config");
5024
4745
  const configPath = adapter.resolveConfigPath?.(scope, options.output);
5025
4746
  if (!configPath)
5026
4747
  throw new Error(`Adapter ${target} does not implement config path resolution.`);
@@ -5028,7 +4749,6 @@ async function runInit2(options) {
5028
4749
  const updated = adapter.mutateConfigForInit?.(current, assignments);
5029
4750
  if (!updated)
5030
4751
  throw new Error(`Adapter ${target} does not implement init mutation.`);
5031
- logStep("Step 7/7 - Self-check");
5032
4752
  const checkErrors = adapter.validateConfig?.(updated) || [];
5033
4753
  if (checkErrors.length) {
5034
4754
  throw new Error(`Configuration verification failed:
@@ -5049,9 +4769,11 @@ async function runInit2(options) {
5049
4769
  console.log(`Config file: ${configPath}`);
5050
4770
  if (adapter.printPostSetupSummary)
5051
4771
  adapter.printPostSetupSummary(updated);
5052
- console.log("Assigned roles:");
5053
- for (const [roleId, modelId] of Object.entries(assignments)) {
5054
- console.log(` - ${roleId}: ${modelId}`);
4772
+ if (Object.keys(assignments).length) {
4773
+ console.log("Assigned roles:");
4774
+ for (const [roleId, modelId] of Object.entries(assignments)) {
4775
+ console.log(` - ${roleId}: ${modelId}`);
4776
+ }
5055
4777
  }
5056
4778
  }
5057
4779
  function runDoctor2(options) {
@@ -5098,7 +4820,7 @@ function runDoctor2(options) {
5098
4820
  process.exitCode = 1;
5099
4821
  }
5100
4822
  program2.name("mstar-harness").description("Morning Star harness CLI for target-based agent bootstrap").version(packageVersion);
5101
- program2.command("init").description("Interactive/non-interactive setup for target agent bootstrap").option("-y, --yes", "Non-interactive mode").option("--target <target>", "Install target", "opencode").option("--scope <scope>", "Config scope: global|project (default: project)").option("--output <path>", "Config file path override, relative to project root").option("--dry-run", "Preview result without writing config").option("--pm-model <model>", "Model for project-manager").option("--strategic-models <a,b,c>", "Models for architect/product-manager/prompt-engineer").option("--dev-models <a,b,c>", "Models for fullstack-dev/fullstack-dev-2/frontend-dev").option("--qc-models <a,b,c>", "Models for qc trio").option("--other-models <a,b,c>", "Models for random assignment to remaining roles").action(async (options) => {
4823
+ program2.command("init").description("Interactive/non-interactive setup for target agent bootstrap").option("-y, --yes", "Non-interactive mode").option("--target <target>", "Install target", "opencode").option("--scope <scope>", "Config scope: global|project (default: project)").option("--output <path>", "Config file path override, relative to project root").option("--dry-run", "Preview result without writing config").option("--pm-model <model>", "Optional: model for project-manager (advanced override)").option("--strategic-models <a,b,c>", "Optional: models for architect/product-manager/prompt-engineer").option("--dev-models <a,b,c>", "Optional: models for fullstack-dev/fullstack-dev-2/frontend-dev").option("--qc-models <a,b,c>", "Optional: models for qc trio").option("--other-models <a,b,c>", "Optional: models for remaining roles").action(async (options) => {
5102
4824
  await runInit2(options);
5103
4825
  });
5104
4826
  program2.command("doctor").description("Validate Morning Star setup for a target agent config").option("--target <target>", "Target agent for doctor checks", "opencode").option("--scope <scope>", "Config scope: global|project", "project").option("--output <path>", "Config file path override, relative to project root").action((options) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mstar-harness/cli",
3
- "version": "1.0.6",
3
+ "version": "1.2.0",
4
4
  "description": "Morning Star harness installer CLI (OpenCode, Cursor, Codex).",
5
5
  "license": "MIT",
6
6
  "repository": {