Formalize the apps/plugins/packages dependency-direction rule
- Dominant language
- TypeScript
- Stars
- 16
- Forks
- 6
- Avg merge
- 1h 51m
- Merged PRs (30d)
- 167
Description
## The rule
From internal discussion, the intended layering for this monorepo:
```
/apps can rely on /packages
/plugins can rely on /packages
/packages can rely on other /packages (but only 1 way, never circular)
/apps can not rely on /plugins (plugins are self contained, and are loaded using
a mechanism, not direct dependency)
/plugins can not rely on /apps, if it needs to then that part has to be moved
into a /packages module
```
Nothing in the repo currently documents this explicitly (not in `AGENTS.md`, `CONTRIBUTING.md`, or `DEVELOPMENT.md`). This issue is to (a) record the rule somewhere real, and (b) track where the actual repo layout does/doesn't hold up against it, since it was never checked mechanically before.
## Current state (audited 2026-08-24)
The repo is two-tier today: `src/` (the `game-ci` CLI — the app) + `plugins/*` (two workspace packages: `@game-ci/orchestrator`, `@game-ci/unity-engine-core`). **There is no `/packages` tier at all.**
Checked against each rule literally:
- **`/apps` can rely on `/packages`** — N/A, no packages exist.
- **`/plugins` can rely on `/packages`** — N/A, same.
- **`/packages` can rely on other `/packages` (one-way)** — N/A, no packages tier.
- **`/apps` can not rely on `/plugins`** — was **violated**: `src/cli.ts` statically imported `orchestratorPlugin` directly from `plugins/orchestrator/src/cli-plugin/index.ts` (a relative path into a plugin's private internals) to make `orchestrate` work without requiring `--plugin @game-ci/orchestrator-plugin`. Fixed in #121 — orchestrator is now loaded through `PluginLoader.load('@game-ci/orchestrator/cli-plugin')` (its public package export), same mechanism as any other plugin; it's just always in the default load list. Two things had to be fixed to make that actually work, not just compile — see #121's description for the full writeup: (1) the root `package.json` never declared a dependency on `@game-ci/orchestrator` at all, so there was no workspace symlink to resolve the package name through; (2) `@game-ci/orchestrator`'s `exports` map only pointed at compiled `dist/`, which would have introduced a new, currently-unwired build-order dependency (orchestrator's own `tsc` build would need to run before the root CLI's `bun build`) — solved with a `"bun"` export condition pointing at TypeScript source directly, so bundling stays byte-identical to before (1756 modules, 10.52MB, verified with zero pre-built orchestrator `dist/` present).
- **`/plugins` can not rely on `/apps`** — **held**. Neither `orchestrator` nor `unity-engine-core` imports anything from `src/`.
## Open question: is "plugin" even the right classification for these two?
`@game-ci/orchestrator` and `@game-ci/unity-engine-core` don't depend on `src/` or on each other, so they technically satisfy the plugin rules as written. But they don't share a `/packages` tier for common needs either — each independently vendors its own copies of overlapping dependencies (`@actions/core`, `@actions/exec`, `nanoid`, `semver`, `yaml`, etc.) rather than depending on one shared module. That's not a rule violation (there's no cross-plugin reliance happening to route through `/packages`), but it's exactly the situation the `/packages` tier exists to solve once it's needed.
Separately: `@game-ci/orchestrator` ships its own standalone `bin` entry (`"game-ci": "./dist/cli.js"`) and can also be loaded as a fully out-of-process executable plugin via the `executable:`/CLI-protocol path (`CliProtocolPlugin`) — i.e. it has real independent-app characteristics, not just "extends the host app" characteristics. `@game-ci/unity-engine-core` has no `bin` entry and no standalone identity — it's closer to a pure library. Worth deciding explicitly whether "plugin" here means "a thing dynamically loaded into `game-ci` to extend it" (a *consumption mode*, which a package can have alongside also being runnable standalone) or a stricter category that excludes anything with independent app identity. Current lean: the two aren't mutually exclusive — a single workspace package can be both "an app" (has its own entrypoint, runs standalone) and "a plugin" (also exposes a `GameCIPlugin`-shaped export the host can dynamically load) — but this should be a deliberate call, not an accident of how things were built.
## Follow-ups this issue should track
- [ ] Document the apps/plugins/packages rule somewhere real (`AGENTS.md` or a new `ARCHITECTURE.md`)
- [ ] Decide the open classification question above and record the decision
- [ ] If/when orchestrator and unity-engine-core end up needing to share code, factor it into a real `/packages` tier rather than duplicating it further
- [ ] Consider a lightweight CI check (e.g. an import-boundary lint rule) that would have caught the `src/cli.ts` violation automatically, so this class of drift doesn't require a manual audit to notice again
Contributor guide
Research direction
Read AGENTS.md, CONTRIBUTING.md, and DEVELOPMENT.md to choose where the dependency-direction rule belongs, then review src/cli.ts and the workspace layout alongside issue #121. Document the rule and the classification decision, record the current audit and follow-ups, and determine whether a lightweight CI boundary check can be specified without expanding the scope.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- cli, documentation, tooling
- Issue type
- Documentation
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100