Skip to content

fix(config): port working jtc config set/show from JuliaPkgTemplatesCLI#6

Merged
ultimatile merged 15 commits intomainfrom
fix/config-set-options
Apr 26, 2026
Merged

fix(config): port working jtc config set/show from JuliaPkgTemplatesCLI#6
ultimatile merged 15 commits intomainfrom
fix/config-set-options

Conversation

@ultimatile
Copy link
Copy Markdown
Owner

Summary

jtc config set was declared but never wired up — the ArgParse subcommand had no options and ConfigCommand.execute looked at the wrong key (%SUBCOMMAND% vs ArgParse's %COMMAND%), so any jtc config set --author X returned unrecognized option. This PR ports the option surface and dispatch from the Python source (../JuliaPkgTemplatesCLI) so config set, config show, and the create flow that consumes those defaults all work end-to-end.

Changes

  • config set / config show CLI surface: add --author (repeatable), --user, --mail, --license, --julia-version, --mise-filename-base, exclusive --with-mise/--no-mise, --config-file, plus per-plugin --git/--tests/etc. (skipping License to avoid colliding with --license).
  • ConfigCommand.execute: read ArgParse's %COMMAND%, descend into nested sub-args, map options into the [default] section, and resolve plugin keys to PkgTemplates canonical names. Quoted strings stay literal (commas no longer auto-promote when the value was explicitly quoted).
  • ConfigManager.{get_config_path,load_config,save_config}: optional custom path so --config-file works.
  • CreateCommand.execute now consumes the new persisted shapes:
    • Vector authors flow through to PkgTemplates as-is.
    • default.license_type is promoted into the License plugin section when no CLI license is supplied.
    • --mail (now also a create flag) appends <mail> to each author entry that doesn't already carry an email.
    • --with-mise / --no-mise get normalized to with_mise so an explicit CLI flag overrides a saved default.
  • PackageGenerator.instantiate_plugins: reflect on plugin field types and wrap stored strings into PkgTemplates.Secret for fields like TagBot.token / TagBot.ssh / TagBot.gpg. Plain String fields (e.g. License.name) are left untouched.
  • Type preservation: drop the Float64 coercion that turned version=1.10 into 1.1 and broke ProjectFile's VersionNumber conversion (matches the Python port's int-only coercion via isdigit).

Out of scope

Test plan

  • Pkg.test() passes (692/692)
  • jtc config set --author A --author B --user u --mail m --license MIT writes the expected TOML and jtc create produces Project.toml with authors = ["A <m>", "B <m>"] and a MIT LICENSE
  • jtc config set --tagbot token=MY_TOKEN followed by jtc create succeeds (Secret wrap)
  • jtc config set --projectfile version=1.10 keeps the value as a string
  • config set --no-mise then create --with-mise actually generates .mise.toml
  • --git 'name="Doe, Jane"' saves Git.name as a single string, not a list
  • jtc config show --config-file <path> and jtc config set --config-file <path> round-trip through a custom location

The config set subcommand declared no arguments, so jtc config set --author ...
returned "unrecognized option". ConfigCommand.execute also looked up
%SUBCOMMAND% (ArgParse uses %COMMAND%) and never read the nested subcommand
args, so even reachable paths defaulted to show.

Port the option surface from the Python source (JuliaPkgTemplatesCLI):
- Add --author (repeatable), --user, --mail, --license, --julia-version,
  --mise-filename-base, --config-file, and exclusive --with-mise/--no-mise
  to config set
- Add --config-file to config show
- Generalize add_dynamic_plugin_options! so config set also gets per-plugin
  options (skipping License to avoid colliding with --license, using
  nargs='?' so --git "ignore=... ssh=true" works)
- Teach ConfigManager.{load,save,get_config_path} to honor a custom path
- Rework ConfigCommand.execute to read %COMMAND%, descend into the nested
  args dict, map CLI options into the [default] section, and resolve
  plugin keys to their PkgTemplates canonical names (booleans, numerics,
  bracket arrays, and comma-split strings round-trip into TOML)
`config set --author A --author B` and `config set --license MIT` persist
values that the create flow ignored: a Vector author was wrapped in another
list (PkgTemplates rejects Vector{Vector{String}}), and license_type was
never promoted to the License plugin so the package shipped with no license.

Pass through Vector authors as-is, and lift `default.license_type` into
plugin_options["License"]["name"] when no CLI license is supplied. Run the
transforms before the dry-run branch so the plan reflects what create will
actually pass to PkgTemplates.
…-mise override

Two follow-ups exposed by the new config set surface:

- parse_plugin_option_value coerced "1.2"/"1.10" to Float64, so a persisted
  ProjectFile.version made create fail with "Cannot convert Float64 to
  VersionNumber" and silently dropped trailing zeros. Match the Python port
  (int-only coercion via isdigit) and let decimals stay as strings.
- create read the mise toggle from merged_options["with_mise"] but the CLI
  flag landed under "with-mise"/"no-mise", so `create --with-mise` could not
  override a config that had `with_mise = false`. Normalize the dashed flags
  into the underscore key before the dry-run/check, so explicit CLI input
  always wins over the saved default.
- Add --mail to create and use it during package generation: when authors
  do not already carry an email, append "<mail>" so the generated
  Project.toml records "Name <mail>". Previously --mail was advertised by
  config set but silently dropped on create.
- Stop dropping dot-notation keys in _apply_set_args. Direct callers that
  pass mixed shapes like author="A" together with "formatter.style"="blue"
  used to lose the dotted entry once the CLI-style branch was taken;
  handle the dotted case alongside the canonical-plugin branch so legacy
  flat updates round-trip.
…gin fields

PkgTemplates plugins like TagBot declare `token`, `ssh`, and `gpg` as
`Secret`/`Union{Nothing, Secret}`, and the constructor refuses to convert
plain strings. After config set persists `--tagbot token=...`, create then
forwarded the raw String and failed with MethodError.

Reflect on the plugin's field types and wrap strings in PkgTemplates.Secret
when (and only when) the field requires it. Skip fields that don't exist on
the plugin or that already accept a String, so plain options like
License.name keep flowing through unchanged.
parse_plugin_option_value stripped surrounding quotes and then split on
commas, so a literal value such as `--git 'name="Doe, Jane"'` was rewritten
to ["Doe", "Jane"] and broke String-typed fields like Git.name on create.
Track whether the original value was quoted and only apply comma-to-array
promotion when the user did not explicitly opt into a string.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes the jtc config command wiring so config set/config show have the intended option surface, correctly dispatch via ArgParse’s %COMMAND%, persist defaults (including plugin defaults) to TOML, and ensure create consumes the new persisted shapes end-to-end.

Changes:

  • Added/ported ArgParse options for config show/config set (including --config-file, mise toggles, and dynamic per-plugin options).
  • Reworked ConfigCommand.execute to correctly dispatch, parse nested subcommand args, map basic keys, and normalize plugin defaults into canonical PkgTemplates plugin sections.
  • Updated config loading/saving to accept an optional custom path, and updated create to consume vector authors, apply --mail, promote license_type, and normalize mise overrides; plugin instantiation now wraps Secret-typed fields.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/cli.jl Adds CLI surface for config show/config set, mise toggles, and dynamic plugin options for config persistence.
src/config_command.jl Fixes ArgParse dispatch (%COMMAND%), implements config-set mapping/parsing (including plugin KV parsing) and stable TOML formatting.
src/config_manager.jl Adds optional custom config path support to get/load/save for --config-file.
src/create_command.jl Updates create flow to consume new persisted shapes (authors vector, mail appending, license promotion, mise override normalization).
src/package_generator.jl Wraps stored strings into PkgTemplates.Secret when plugin field types require it.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/config_manager.jl
Comment thread src/config_manager.jl
Comment thread src/config_command.jl Outdated
Comment thread src/config_manager.jl Outdated
Copilot review on the config-set port flagged that "1"/"0" lived in the
boolean synonym lists, so a value like `--git indent=1` was persisted as
`indent = true` instead of the integer 1. Drop them so numeric plugin
options round-trip correctly; "true"/"yes"/"false"/"no" still cover
boolean intent.

Also bring the get_config_path/load_config/save_config docstrings in line
with the new optional `custom_path` parameter so the `--config-file`
behavior is discoverable from the API docs.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/cli.jl
Comment thread src/config_command.jl Outdated
Two follow-ups from Copilot's second review:

- add_dynamic_plugin_options! flattened the create vs config-set paths so
  both used `nargs='?'` for non-argumentless plugins. CreateCommand.parse_plugin_options
  only reads plugin keys when the parsed value is a Vector, so any future
  non-argumentless plugin would be silently dropped on create. Split the
  branches: create keeps the legacy `:append_arg`/'*' shape, config set
  keeps the optional KEY=VALUE form.
- ConfigCommand.execute called _plugin_canonical_names() inside the
  uses_cli_shape `any(...)` generator, re-running plugin discovery for
  every key in sub_args. Lift it into a local so it runs once.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/config_command.jl Outdated
Comment thread src/config_command.jl Outdated
… branches

Two cleanup follow-ups from Copilot review:

- _BASIC_KEYS was declared at module top level but never referenced. Remove it.
- update_config had separate `value isa AbstractString && contains(key, '.')`
  and `elseif contains(key, '.')` branches that ran identical logic. Collapse
  to a single `contains(key, '.')` branch.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/package_generator.jl
Comment thread src/cli.jl
Comment thread src/create_command.jl
Comment thread src/config_manager.jl
Comment thread src/config_command.jl
Promote each actionable finding from the codex/Copilot review cycle into a
contract test that targets the implicit specification, not just the
specific input that broke. Adds 73 tests across four files (692 → 765).

- parse_plugin_option_value: bool/int/decimal/bracket/quoted/unquoted
  semantics, including the "1"/"0" stay-integer rule and the "Doe, Jane"
  stay-string-when-quoted rule.
- ConfigCommand.execute: ArgParse-shaped nested args round-trip, repeated
  --author becoming Vector{String}, --no-mise normalising to with_mise=false
  (no leaked dashed keys), lowercase plugin keys mapping to canonical
  PkgTemplates names, quoted plugin string values staying strings, mixed
  flat dict (CLI key + dotted key) round-trip.
- CreateCommand.execute consume contract via dry-run: Vector author flows
  as authors=Vector{String}, --mail appended only when author lacks <email>,
  license_type promoted into License plugin (CLI License keeps precedence),
  --with-mise / --no-mise CLI override the saved with_mise default.
- PackageGenerator.instantiate_plugins: Secret-typed fields wrap stored
  strings, Union{Nothing,Secret} also wraps, String-typed fields are not
  wrapped, License kwarg-only `name` is not over-coerced.
- ConfigManager: get_config_path/load_config/save_config honour
  custom_path, create parent dirs as needed, and leave the XDG default
  untouched.
@ultimatile ultimatile requested a review from Copilot April 25, 2026 23:13
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/create_command.jl
Comment thread src/config_command.jl Outdated
A direct caller passing only `config-file` to redirect the destination
would have it written into `[default]` by `update_config`. Add it to the
ignored-keys list alongside the meta-keys so the override never becomes
a saved default.

Adds a contract test exercising the legacy flat-dict path with
`config-file` plus a dotted plugin key, and tightens the comment on the
existing license-precedence test to record that it currently exercises
execute()'s guard via the recognised "--License" key shape — the same
guard via the bare ArgParse "license" key is blocked on issue #7.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/config_command.jl Outdated
…lity

The plugin-flag handling listed Bool branches (true/false) as if some
plugins were registered with :store_true, but config-set plugin options
all use nargs='?' / constant='' / default=nothing, so value is only
nothing / "" / String. Drop the dead Bool branches and rewrite the
comment to describe the actual ArgParse contract.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/config_command.jl
…ch create

CreateCommand only treats nested Dicts whose key starts with an uppercase
letter as plugin options, but update_config and _apply_set_args persisted
dotted-section keys (`formatter.style`) under the literal lowercase
`formatter`. Plugin defaults written via dot notation therefore never
reached create.

Resolve the section target case-insensitively: prefer an existing section
(so manually edited lowercase TOML keeps being updated in place), and
otherwise fall back to the canonical PkgTemplates name (`Formatter`) so
new entries land under the form CreateCommand looks for. Update the
existing dot-notation contract tests to assert the canonical form, and
add a contract test asserting `[default.Formatter]` reaches create's
plugin_options end-to-end.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/config_command.jl Outdated
Comment thread src/config_command.jl
_resolve_section_target rebuilt the plugin-name lookup on every call,
re-running PkgTemplates.concretes(Plugin) for each dotted key in a
single config set. Compute the mapping once per update_config /
_apply_set_args call and thread it through, so callers passing many
dotted keys pay plugin discovery exactly once.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Three legal shapes are produced by ArgParse for `--<plugin>` under
`config set` (nargs='?', constant="", default=nothing): unspecified
(nothing), bare flag (""), and a KEY=VALUE bundle. The bundle case had
coverage; the other two relied on implicit behaviour. Add a contract
test so:

- nothing  → no section is created (skip)
- ""       → section appears empty (enable with defaults; create picks it up)
@ultimatile ultimatile merged commit e133ef8 into main Apr 26, 2026
2 checks passed
@ultimatile ultimatile deleted the fix/config-set-options branch April 26, 2026 10:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants