feat(adopt): bulk/filtered adoption, --with-dynamic ruleset capture, unknown-field warnings - #58
Merged
Merged
Conversation
Derive a per-resource-type allowlist from the registry's own managedFields (registry.knownFields) — never hand-copied — and warn (not throw) in the config DSL when a declaration carries a field the registry doesn't manage for that type, e.g. campus's vestigial `shortName` instead of the real `shorty`. The field still passes through unchanged; this only surfaces the mistake instead of leaving it silently un-diffed forever. File:line locations are #52's job, not built here.
…ing them (#51) `ct adopt`'s subcommands (grants, and the new group added in this PR) each redeclare -s/--state and -e/--env for their own --help text. Commander does not merge a same-named parent+subcommand option into either level's plain .opts() — both come up empty for it — so the subcommand silently fell back to the default state path / no env, ignoring the flag the user passed. Read merged options via command.optsWithGlobals() instead, which walks the whole command chain correctly. Adds a regression test proving --state is actually honoured by resolving a scoped grant's dataId via a non-default state file.
…pture (#51) Add `ct adopt group` as a dedicated subcommand (mirroring adopt-grants.ts) covering: - Multiple positional ids: `ct adopt group <id> <id> <id>`. - `--type <groupTypeIdOrKey>`: adopt every group of a group type, resolved as a numeric id or a logical key against the live /group/grouptypes catalog (client.getAll for listing, per #50). - `--children-of <idOrKey>`: recursively adopt a group's full hierarchy subtree via /groups/{id}/children, parent-before-child, excluding the root; guards against a cyclic hierarchy with a visited-id set. - `--with-dynamic`: for each adopted group that IS a dynamic group (a 404 on the ruleset GET means "not dynamic" — skipped silently), capture its normalized ruleset (engine/dynamic.js's normalizeRuleset, the same normalizer plan/apply already use) to rulesets/<key>.json and emit the `dynamic: { status, ruleset: { ref } }` block in the printed config snippet, so `ct plan` is a no-op once pasted (covered by an end-to-end test using buildPlan against the same mocked client). Output stays per-resource (state entries + config snippets), plus one grouped `// group` config block for bulk selections — configSnippet's own per-line FORMAT is untouched (#52 reworks that later). Selection filters compose with each other via mutual exclusivity (exactly one of: ids / --type / --children-of); duplicate resolved ids are deduped; --key is rejected whenever more than one group resolves.
2000game
force-pushed
the
feat/bulk-adopt-51
branch
from
July 9, 2026 12:13
1755ce7 to
0b65f57
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements #51 — three related asks:
1. Bulk/filtered group adoption —
ct adopt groupis now a dedicated subcommand supporting:ct adopt group <id> <id> <id>— multiple positional ids in one invocation.ct adopt group --type <groupTypeIdOrKey>— adopt every group of a group type (numeric id or logical key resolved against the live/group/grouptypescatalog). Usesclient.getAllfor listing (bug(get): list commands return only ChurchTools first page; raw errors hide status/body #50).ct adopt group --children-of <idOrKey>— recursively adopt a group's full hierarchy subtree via/groups/{id}/children, parent-before-child, excluding the root; guarded against a cyclic hierarchy with a visited-id set.--type/--children-of); duplicate resolved ids are deduped;--keyis rejected once more than one group resolves.// group-headed config block for pasting.configSnippet's own per-line FORMAT is untouched (feat(dx): human-authorable configs — idiomatic adopt output, dynamic sugar, located errors, machine-only state, blueprints by default #52 reworks that later).2.
--with-dynamicruleset capture — for each adopted group (single or bulk) that IS a dynamic group (a 404 on the ruleset GET means "not dynamic" — skipped silently), captures its normalized ruleset (reusingengine/dynamic.js'snormalizeRuleset, the same normalizer plan/apply already use) torulesets/<key>.jsonand emits thedynamic: { status, ruleset: { ref } }block in the printed config snippet — soct planis a no-op once pasted. Covered end-to-end viabuildPlanagainst the same mocked client (see acceptance test below).3. Unknown-declaration-field warning — the config DSL (
src/config/context.ts) now warns (via the existingwarn()UI helper, never throws) when a declaration carries a field the registry doesn't manage for that type, e.g. campus's vestigialshortNameinstead of the realshorty. The allowlist is derived from the registry's ownmanagedFields(a newknownFields()export) — never hand-copied, so it can't drift. File:line locations are #52's job, not built here.Bonus fix: while building the
groupsubcommand I found that Commander does not merge a same-named parent+subcommand option into either level's plain.opts()—ct adopt's parent already declares-s/--state/-e/--env, and any subcommand redeclaring them (bothgrantsand the newgroup) silently had those flags dropped. Fixed by readingcommand.optsWithGlobals()instead, with a regression test onadopt grantsproving--stateis now actually honoured.Test plan
npm test— 435 passed, 4 skipped (live-only), 0 failednpm run typecheck— cleannpm run lint— cleantests/adopt-group-command.test.ts(20 tests): multi-id list,--type(numeric + logical key),--children-of(nested subtree, cycle guard, state-key resolution, empty subtree),--with-dynamic(capture, skip-silently for non-dynamic, dry-run previews without writing, mixed bulk selection), and an end-to-end acceptance test reconstructing the pasted config from the printed snippet and assertingbuildPlanreturns an all-no-op plan.tests/registry.test.ts/tests/context.test.ts:knownFields()allowlist derivation + unknown-field warning (including the exactshortNamevestigial-field scenario from the issue).tests/adopt-grants-command.test.ts: regression test for theoptsWithGlobals()fix.CtClient.Closes #51