Skip to content

feat: Phase 2 — ct adopt + JSON state file - #10

Merged
2000game merged 3 commits into
mainfrom
feat/phase-2-adopt-state
Jul 7, 2026
Merged

2000game merged 3 commits into
mainfrom
feat/phase-2-adopt-state

Conversation

@2000game

@2000game 2000game commented Jul 7, 2026 •

Copy link
Copy Markdown
Member

Closes the first desired-state capability (#4): bring existing ChurchTools structure under management, selectively, into a committable JSON state file. Also fixes a symlink-entrypoint bug that made the global ct silent.

Phase 2

  • State file (src/state/state.ts) — { version, host, resources } keyed by logical key, storing CT id + a managed-field snapshot (diff baseline). Idempotent upsert: re-adopting the same (type,id) updates in place, re-keys on key change, rejects a key taken by a different resource. All id comparisons null-checked — CT ids can be 0 (Mainz is id: 0).
  • Resource registry (src/resources/registry.ts) — campus, group, group-type with item paths from the Phase 0 matrix, key derivation, field snapshots, and TS-as-code config-snippet rendering.
  • ct adopt <type> <id> [--key] [--state] [--dry-run] + ct state list. --dry-run previews the generated config entry without writing; a host guard refuses to mix instances.

Bug fix (was affecting main)

The entrypoint guard compared import.meta.url (realpath) to file:// + argv[1] (symlink path), so the global/brew ct symlink ran but printed nothing (exit 0, 0 bytes). Fixed with a tested isMainModule() that realpaths both sides.

Tests — 39

Phase 2 (state upsert/idempotency/id 0/rekey/collision, registry, adopt write/dry-run/unknown-type) + entrypoint symlink cases. typecheck · lint · build green.

Live-verified ✅

Against the instance (CT 3.134.0): ct adopt campus 0 → state written; state list shows it; re-adopt = "Updated", single entry. Global ct --help and ct auth status confirmed working after the fix.

Refs #4

2000game added 2 commits July 7, 2026 13:21
Selective, opt-in management: bring one existing ChurchTools resource
under management and record it in a committable JSON state file. Nothing
outside the state file is visible to the tool.

- src/state/state.ts: State schema (version, host, resources keyed by
  logical key), load/save, and an idempotent upsert (re-adopting the same
  type+id updates in place, re-keys on key change, rejects key collisions).
  All id comparisons are null-checked — CT ids can be 0 (Mainz campus).
- src/resources/registry.ts: adoptable-type registry (campus, group,
  group-type) with item paths from the Phase 0 matrix, key derivation,
  managed-field snapshots, and TS-as-code config-snippet rendering.
- ct adopt <type> <id> [--key] [--state] [--dry-run]: fetch by id, snapshot,
  upsert into state; --dry-run previews the generated config entry without
  writing. Host guard refuses to mix instances.
- ct state list: show the managed set.
- 17 new tests (state upsert/idempotency/id-0/rekey/collision, registry
  slug/paths/snippet, adopt write/idempotency/dry-run/unknown-type) = 34.

Refs #4
The entrypoint guard compared import.meta.url (the module realpath) against
file://+argv[1] (the symlink path). Invoked through the npm-link / Homebrew
`ct` symlink these never matched, so `ct --help` exited 0 printing nothing.
Resolve both sides to their realpath via a tested isMainModule() helper.

Refs #4
Correctness:
- adopt: reject non-integer ids strictly (/^\d+$/) — Number.parseInt
  accepted "0x10"→0, "3abc"→3, "2.9"→2, silently adopting the wrong id.
- state: validate the state file's shape (top-level object, version, host,
  resources) and surface friendly errors instead of cryptic TypeErrors on a
  hand-edited/committed file (e.g. missing "resources", top-level null,
  invalid JSON).
- state: move the host-mismatch guard into loadState so every command
  inherits it — `ct state list` previously listed another instance's
  resources with no warning.
- adopt: load + validate the state file (host guard included) BEFORE
  authedSession()/GET, so a mismatched instance never triggers a live
  authenticated request against the wrong host.
- registry: strip NFKD combining marks in slug() so German names slug to
  their base letters ("Zürich"→"zurich") instead of gaining underscores.
- registry: snapshot group ids from a nested `information` object OR the top
  level, so the baseline isn't silently empty if the API shape differs.
- isMain: resolve each path independently in the realpath fallback so an
  unresolvable argv1 still compares against the module's realpath.

Cleanup:
- adopt: drop the redundant second resolveConfig() (single source now).
- registry: derive itemPath from a single collection-path literal per entry.

Refs #4
@2000game
2000game merged commit 9c477b2 into main Jul 7, 2026
1 check passed
@2000game
2000game deleted the feat/phase-2-adopt-state branch July 8, 2026 09:56
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.

1 participant