Compare commits
37
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6c8bf7105 | ||
|
|
9b5e862d8a | ||
|
|
a509509192 | ||
|
|
9b82f3eecd | ||
|
|
c7b25b63c9 | ||
|
|
c8674a5059 | ||
|
|
77a829aff0 | ||
|
|
137af86de3 | ||
|
|
13c3f56aa5 | ||
|
|
67c28d4b08 | ||
|
|
2ce60a86ab | ||
|
|
1c359729a8 | ||
|
|
545c9841b2 | ||
|
|
5b50f32f4b | ||
|
|
0c0062d955 | ||
|
|
edd874945e | ||
|
|
ab3007cf48 | ||
|
|
1355cbe3cd | ||
|
|
453da10b61 | ||
|
|
bfd3093776 | ||
|
|
847038d1e0 | ||
|
|
9d23c4455e | ||
|
|
e02b6cfed2 | ||
|
|
c979f47621 | ||
|
|
89de58ff24 | ||
|
|
3fa0d74ad8 | ||
|
|
3a01d28506 | ||
|
|
01220cebd4 | ||
|
|
a2b9a9f5c4 | ||
|
|
29d26b0918 | ||
|
|
0a76bd5e54 | ||
|
|
77dab28c9a | ||
|
|
3fab891383 | ||
|
|
c94dfdcb30 | ||
|
|
0cc6909057 | ||
|
|
18b6a7e390 | ||
|
|
5f75de4eb1 |
No files matched your search
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"PostToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "Edit|Write|apply_patch",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "[ ! -f 'C:\\Users\\j.lechner\\.agents\\skills\\impeccable\\scripts\\impeccable' ] || 'C:\\Users\\j.lechner\\.agents\\skills\\impeccable\\scripts\\impeccable' hook",
|
||||||
|
"commandWindows": "if exist \"C:\\\\Users\\\\j.lechner\\\\.agents\\\\skills\\\\impeccable\\\\scripts\\\\impeccable.cmd\" (\"C:\\\\Users\\\\j.lechner\\\\.agents\\\\skills\\\\impeccable\\\\scripts\\\\impeccable.cmd\" hook & exit /b)",
|
||||||
|
"timeout": 5,
|
||||||
|
"statusMessage": "Checking UI changes"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Stop": [
|
||||||
|
{
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "[ ! -f 'C:\\Users\\j.lechner\\.agents\\skills\\impeccable\\scripts\\impeccable' ] || 'C:\\Users\\j.lechner\\.agents\\skills\\impeccable\\scripts\\impeccable' hook",
|
||||||
|
"commandWindows": "if exist \"C:\\\\Users\\\\j.lechner\\\\.agents\\\\skills\\\\impeccable\\\\scripts\\\\impeccable.cmd\" (\"C:\\\\Users\\\\j.lechner\\\\.agents\\\\skills\\\\impeccable\\\\scripts\\\\impeccable.cmd\" hook & exit /b)",
|
||||||
|
"timeout": 30,
|
||||||
|
"statusMessage": "Design deep pass"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
-6
@@ -17,20 +17,33 @@ jobs:
|
|||||||
- name: Bash syntax
|
- name: Bash syntax
|
||||||
run: find scripts shared/hooks -name '*.sh' -print0 | xargs -0 -n1 bash -n
|
run: find scripts shared/hooks -name '*.sh' -print0 | xargs -0 -n1 bash -n
|
||||||
- name: Build
|
- name: Build
|
||||||
run: REQUIRE_CODEX_SCHEMA=true ./scripts/build.sh
|
run: REQUIRE_CODEX_SCHEMA=true ./scripts/build.sh --summary
|
||||||
|
- name: Prompt budget
|
||||||
|
run: ./scripts/prompt-budget.sh --summary
|
||||||
|
- name: Repository context cache
|
||||||
|
run: ./scripts/repository-context.sh --summary
|
||||||
- name: Doctor
|
- name: Doctor
|
||||||
run: ./scripts/doctor.sh
|
run: ./scripts/doctor.sh --summary
|
||||||
- name: Platform package tests
|
- name: Platform package tests
|
||||||
run: ./scripts/test-platform-packages.sh
|
run: ./scripts/test-platform-packages.sh --summary
|
||||||
- name: Managed manifest tests
|
- name: Managed manifest tests
|
||||||
run: ./scripts/test-managed-install.sh
|
run: ./scripts/test-managed-install.sh --summary
|
||||||
- name: Build validation tests
|
- name: Build validation tests
|
||||||
run: ./scripts/test-build-validation.sh
|
run: ./scripts/test-build-validation.sh --summary
|
||||||
|
- name: Install guard tests
|
||||||
|
run: ./scripts/test-install-guards.sh --summary
|
||||||
|
- name: Plugin selection tests
|
||||||
|
run: ./scripts/test-plugin-selection.sh --summary
|
||||||
- name: Install dry run
|
- name: Install dry run
|
||||||
run: |
|
run: |
|
||||||
export HOME="$RUNNER_TEMP/isolated-home"
|
export HOME="$RUNNER_TEMP/isolated-home"
|
||||||
mkdir -p "$HOME"
|
mkdir -p "$HOME"
|
||||||
./scripts/install.sh --dry-run --client both --platform linux
|
./scripts/install.sh --summary --dry-run --client both --platform linux
|
||||||
|
- name: Update dry run
|
||||||
|
run: |
|
||||||
|
export HOME="$RUNNER_TEMP/isolated-home"
|
||||||
|
mkdir -p "$HOME"
|
||||||
|
./scripts/update.sh --summary --dry-run --client both --platform linux
|
||||||
|
|
||||||
# windows:
|
# windows:
|
||||||
# runs-on: windows-latest
|
# runs-on: windows-latest
|
||||||
@@ -65,6 +78,15 @@ jobs:
|
|||||||
# - name: Build validation tests
|
# - name: Build validation tests
|
||||||
# shell: powershell
|
# shell: powershell
|
||||||
# run: .\scripts\test-build-validation.ps1
|
# run: .\scripts\test-build-validation.ps1
|
||||||
|
# - name: Install guard tests
|
||||||
|
# shell: powershell
|
||||||
|
# run: .\scripts\test-install-guards.ps1
|
||||||
|
# - name: Plugin selection tests
|
||||||
|
# shell: powershell
|
||||||
|
# run: .\scripts\test-plugin-selection.ps1
|
||||||
# - name: Install dry run
|
# - name: Install dry run
|
||||||
# shell: powershell
|
# shell: powershell
|
||||||
# run: .\scripts\install.ps1 -DryRun -Client Both -Platform Windows
|
# run: .\scripts\install.ps1 -DryRun -Client Both -Platform Windows
|
||||||
|
# - name: Update dry run
|
||||||
|
# shell: powershell
|
||||||
|
# run: .\scripts\update.ps1 -DryRun -Client Both -Platform Windows
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
generated/
|
generated/
|
||||||
|
.superpowers/
|
||||||
*.log
|
*.log
|
||||||
*.tmp
|
*.tmp
|
||||||
*.bak
|
*.bak
|
||||||
backups/
|
backups/
|
||||||
|
.ai-session/
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
auth.json
|
auth.json
|
||||||
|
|||||||
@@ -25,9 +25,11 @@ into platform-specific packages that can be inspected before they are installed.
|
|||||||
or deleting them.
|
or deleting them.
|
||||||
- Dry-run support (no writes, no backups, no plugin changes) and timestamped, per-run backup directories.
|
- Dry-run support (no writes, no backups, no plugin changes) and timestamped, per-run backup directories.
|
||||||
- Shared safety, notification, validation, and status-line hooks.
|
- Shared safety, notification, validation, and status-line hooks.
|
||||||
- User-level plugin installation for Ponytail, i-have-adhd, Superpowers, Context7, Caveman, Humanizer, Impeccable,
|
- Separate install and update scripts: install refuses to run against an already-installed destination, update refuses
|
||||||
and Anthropic Frontend Design; updates only with
|
to run against one that is not installed yet.
|
||||||
`-UpdatePlugins`/`--update-plugins`.
|
- User-level plugin installation for Ponytail, i-have-adhd, Superpowers, Context7, Caveman, Humanizer, Impeccable, and
|
||||||
|
Anthropic Frontend Design, with an interactive per-plugin toggle during both install and update; update pre-checks
|
||||||
|
plugins that are already installed, so unchecking one uninstalls it and checking one installs or updates it.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -40,7 +42,7 @@ shared definitions -> client adapters -> generated packages -> user installation
|
|||||||
| `shared/` | Client-independent rules, agents, skills, hooks, status lines, and global instructions. |
|
| `shared/` | Client-independent rules, agents, skills, hooks, status lines, and global instructions. |
|
||||||
| `adapters/` | Codex and Claude Code formats, settings, templates, and the plugin manifest. |
|
| `adapters/` | Codex and Claude Code formats, settings, templates, and the plugin manifest. |
|
||||||
| `generated/` | Reproducible Windows and Linux packages. This directory is ignored by Git. |
|
| `generated/` | Reproducible Windows and Linux packages. This directory is ignored by Git. |
|
||||||
| `scripts/` | Build, installation, diagnosis, and platform validation entry points. |
|
| `scripts/` | Build, installation, update, diagnosis, and platform validation entry points. |
|
||||||
| `docs/` | Focused documentation about the configuration design. |
|
| `docs/` | Focused documentation about the configuration design. |
|
||||||
|
|
||||||
The build creates:
|
The build creates:
|
||||||
@@ -82,9 +84,10 @@ Every repository change must pass the build before completion. Building only wri
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
The installer runs a fresh build and then asks for the target platform and client, unless they are passed as
|
The installer asks for the target platform and client, unless they are passed as parameters, then refuses to continue
|
||||||
parameters. Selecting Linux or Windows chooses the generated package format; installation always targets the current
|
if any selected destination is already installed (`Already installed, use the update script.`). Selecting Linux or
|
||||||
user's home directory.
|
Windows chooses the generated package format; installation always targets the current user's home directory. This
|
||||||
|
check is skipped for a dry run, so previewing always works regardless of prior install state.
|
||||||
|
|
||||||
Preview an installation without changing user files, backups, or plugins:
|
Preview an installation without changing user files, backups, or plugins:
|
||||||
|
|
||||||
@@ -131,11 +134,30 @@ and left in place with a warning instead. A file the installer never tracked is
|
|||||||
same name is now part of the package. Backups land in a single timestamped directory per run under
|
same name is now part of the package. Backups land in a single timestamped directory per run under
|
||||||
`backups/<timestamp>/` inside each destination.
|
`backups/<timestamp>/` inside each destination.
|
||||||
|
|
||||||
Selecting Codex also installs shared skills to `.agents/skills`. A normal installation only installs plugins that are
|
Selecting Codex also installs shared skills to `.agents/skills`. Before plugins are installed, an interactive list lets
|
||||||
missing; add `-UpdatePlugins`/`--update-plugins` to also update and enable plugins that are already installed.
|
you toggle each configured plugin on or off (all checked by default); unchecked plugins are skipped. This step is also
|
||||||
|
skipped for a dry run.
|
||||||
|
|
||||||
Run the same installation command whenever the repository configuration should be updated. There is no separate update
|
## Update
|
||||||
script.
|
|
||||||
|
Once a destination is installed, use the update script (not the installer) to refresh its files and plugins. It refuses
|
||||||
|
to continue if any selected destination is not installed yet (`Not installed, use the install script.`), also skipped
|
||||||
|
for a dry run.
|
||||||
|
|
||||||
|
Powershell: (Windows)
|
||||||
|
```powershell
|
||||||
|
.\scripts\update.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Bash: (Linux)
|
||||||
|
```bash
|
||||||
|
./scripts/update.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
It accepts the same `-DryRun`/`--dry-run`, `-Client`/`--client`, and `-Platform`/`--platform` parameters as the
|
||||||
|
installer, re-syncs every managed file the same way, and shows the same plugin toggle list — but pre-checks plugins
|
||||||
|
that are already installed. Checking a plugin that is not installed installs it; checking one that is installed
|
||||||
|
updates it; unchecking an installed plugin uninstalls it.
|
||||||
|
|
||||||
## Doctor
|
## Doctor
|
||||||
|
|
||||||
@@ -151,7 +173,8 @@ Bash: (Linux)
|
|||||||
./scripts/doctor.sh
|
./scripts/doctor.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
The doctor reports concise `PASS`, `WARN`, and `FAIL` results without reading or printing credentials.
|
Use `-Summary` (PowerShell) or `--summary` (Bash) with build, doctor, install, update, and test scripts for compact output.
|
||||||
|
Omit the flag for detailed output. Warnings and failure diagnostics remain visible in both modes.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@@ -159,8 +182,8 @@ Codex receives global instructions, rules, skills, agent TOML files, hooks, and
|
|||||||
workspace-scoped and use automatic review for eligible escalation requests. Every rule ships as a plain file under
|
workspace-scoped and use automatic review for eligible escalation requests. Every rule ships as a plain file under
|
||||||
`rules/`, and `AGENTS.md` tells Codex to load the matching one by path.
|
`rules/`, and `AGENTS.md` tells Codex to load the matching one by path.
|
||||||
|
|
||||||
Claude Code receives the equivalent global instructions, agents, hooks, and `settings.json`. Only `general.md` ships as
|
Claude Code receives the equivalent global instructions, agents, hooks, and `settings.json`. General rules are embedded
|
||||||
an always-applied rule file (its content is also embedded directly in `CLAUDE.md`); every technology- or
|
once in `CLAUDE.md` (and in Codex's `AGENTS.md`); every technology- or
|
||||||
situation-specific rule (`adapters/claude/rule-skills.tsv`) is generated as a skill under `skills/rules/` instead, so
|
situation-specific rule (`adapters/claude/rule-skills.tsv`) is generated as a skill under `skills/rules/` instead, so
|
||||||
only its name and description are permanently visible and the full rule text loads only when Claude invokes it. Its
|
only its name and description are permanently visible and the full rule text loads only when Claude invokes it. Its
|
||||||
generated settings use the supported automatic permission mode where available.
|
generated settings use the supported automatic permission mode where available.
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
"command": "__STATUSLINE_COMMAND__ \"__AI_CONFIG_ROOT__/statusline/__STATUSLINE_SCRIPT__\""
|
"command": "__STATUSLINE_COMMAND__ \"__AI_CONFIG_ROOT__/statusline/__STATUSLINE_SCRIPT__\""
|
||||||
},
|
},
|
||||||
"hooks": {
|
"hooks": {
|
||||||
|
"PreCompact": [{"hooks": [{"type": "command", "command": "__HOOK_COMMAND__ \"__AI_CONFIG_ROOT__/hooks/scripts/__COMPACT_SCRIPT__\"", "timeout": 4}]}],
|
||||||
|
"SessionStart": [{"hooks": [{"type": "command", "command": "__HOOK_COMMAND__ \"__AI_CONFIG_ROOT__/hooks/scripts/__SESSION_POINTER_SCRIPT__\"", "timeout": 4}]}],
|
||||||
"Stop": [
|
"Stop": [
|
||||||
{
|
{
|
||||||
"hooks": [
|
"hooks": [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
rule_file skill_name trigger
|
rule_file skill_name trigger
|
||||||
angular.md rules-angular Angular component, template, or RxJS/signal work
|
angular rules-angular Angular component, template, or RxJS/signal work
|
||||||
csharp.md rules-csharp C# or .NET implementation, review, or refactoring work
|
csharp.md rules-csharp C# or .NET implementation, review, or refactoring work
|
||||||
decision-rule.md rules-decision-rule evaluating requirements, ambiguous behavior, or technical choices that need a decision
|
decision-rule.md rules-decision-rule unresolved requirements, ambiguous behavior, or a meaningful technical choice
|
||||||
definition-of-done.md rules-definition-of-done validating that a task is complete or ready for review
|
definition-of-done.md rules-definition-of-done validating that a task is complete or ready for review
|
||||||
git.md rules-git Git operations such as commits, branches, or merges
|
git.md rules-git Git operations such as commits, branches, or merges
|
||||||
microsoft.md rules-microsoft Microsoft 365, Azure DevOps, or Teams operations
|
microsoft.md rules-microsoft Microsoft 365, Azure DevOps, or Teams operations
|
||||||
@@ -16,5 +16,5 @@ security-network.md rules-security-network network, URL fetch, proxy, outbound r
|
|||||||
security-crypto.md rules-security-crypto cryptography, secrets, credentials, key, or token work
|
security-crypto.md rules-security-crypto cryptography, secrets, credentials, key, or token work
|
||||||
security-supply-chain.md rules-security-supply-chain dependency, package, plugin, build, deployment, or CI supply-chain work
|
security-supply-chain.md rules-security-supply-chain dependency, package, plugin, build, deployment, or CI supply-chain work
|
||||||
typescript.md rules-typescript TypeScript or modern ECMAScript implementation work
|
typescript.md rules-typescript TypeScript or modern ECMAScript implementation work
|
||||||
ui-ux.md rules-ui-ux UI or UX design and interaction decisions
|
ui-ux rules-ui-ux UI or UX design and interaction decisions
|
||||||
wpf.md rules-wpf WPF, XAML, or MVVM desktop UI work
|
wpf rules-wpf WPF, XAML, or MVVM desktop UI work
|
||||||
|
@@ -0,0 +1 @@
|
|||||||
|
{"Claude permanent context":5030,"Codex permanent context":4828}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Claude 5030
|
||||||
|
Codex 4828
|
||||||
|
+6
-2
@@ -2,9 +2,13 @@
|
|||||||
|
|
||||||
The user-level plugin manifest is `adapters/plugins.tsv`. It maps each logical plugin to the marketplace selector understood by Claude Code and Codex.
|
The user-level plugin manifest is `adapters/plugins.tsv`. It maps each logical plugin to the marketplace selector understood by Claude Code and Codex.
|
||||||
|
|
||||||
The current baseline includes Ponytail, i-have-adhd, Superpowers, Context7, Caveman, Humanizer, Impeccable, and Anthropic Frontend Design. The install script operates at user scope and is idempotent: a normal run only installs missing plugins (a disabled but already-installed Claude plugin is re-enabled). Pass `-UpdatePlugins`/`--update-plugins` to also update plugins that are already installed, so a configuration update never silently updates third-party plugin code.
|
The current baseline includes Ponytail, i-have-adhd, Superpowers, Context7, Caveman, Humanizer, Impeccable, and Anthropic Frontend Design. Both the install and update scripts operate at user scope and show an interactive list to toggle each configured plugin on or off before applying anything (skipped for a dry run).
|
||||||
|
|
||||||
Codex receives Caveman through its native plugin adapter. Humanizer and Anthropic Frontend Design are installed through the cross-agent Skills CLI; Impeccable uses its provider-aware Codex installer. All four are therefore available to both clients, although Codex does not expose all four as marketplace plugins.
|
`install.ps1`/`install.sh` starts every plugin checked by default; unchecking one skips it. It refuses to run against an already-installed destination (`Already installed, use the update script.`).
|
||||||
|
|
||||||
|
`update.ps1`/`update.sh` starts each plugin checked to match what is already installed (a disabled but installed Claude plugin still counts as installed and is re-enabled), so a plain confirm re-syncs and updates the existing set without silently touching anything else. Checking a plugin that is not installed installs it; unchecking an installed plugin uninstalls it (`claude plugin uninstall`/`codex plugin remove`). It refuses to run against a destination that is not installed yet (`Not installed, use the install script.`).
|
||||||
|
|
||||||
|
Codex receives Caveman through its native plugin adapter. Humanizer and Anthropic Frontend Design are installed through the cross-agent Skills CLI; Impeccable uses its provider-aware Codex installer. All four are therefore available to both clients, although Codex does not expose all four as marketplace plugins. Because those three have no install/uninstall state on Codex, they are always ensured on every Codex run and are left out of the toggle list when only Codex is selected (they still appear, toggleable, for Claude or Both).
|
||||||
|
|
||||||
Plugin installation requires the corresponding client CLI, network access, and any authentication required by the plugin. Context7 may open an OAuth flow. Codex or plugin hooks may require a trust review in `/hooks`. The scripts never install plugins during a build or doctor run and never install them when `--dry-run` or `-DryRun` is used.
|
Plugin installation requires the corresponding client CLI, network access, and any authentication required by the plugin. Context7 may open an OAuth flow. Codex or plugin hooks may require a trust review in `/hooks`. The scripts never install plugins during a build or doctor run and never install them when `--dry-run` or `-DryRun` is used.
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,149 @@
|
|||||||
|
# Token-Efficiency Improvements — Design
|
||||||
|
|
||||||
|
Status: approved by user (chat), 2026-09-16. Implementation via writing-plans.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Reduce unnecessary token consumption in both Claude Code and Codex — script
|
||||||
|
output copied into agent context, rule/skill content loaded when not needed,
|
||||||
|
duplicated instructions loaded on every task, and delegation that duplicates
|
||||||
|
context — without weakening correctness, security, or verification quality.
|
||||||
|
|
||||||
|
## 1. Quiet output mode for scripts
|
||||||
|
|
||||||
|
Add `-Summary` (PowerShell) / `--summary` (Bash) to `build`, `doctor`,
|
||||||
|
`install`, `update`, and the `test-*` scripts.
|
||||||
|
|
||||||
|
- Purely an output-verbosity switch. Orthogonal to `-Client`/`-Platform`
|
||||||
|
(interactivity is unaffected — a quiet run without `-Client`/`-Platform`
|
||||||
|
still prompts) and to `-DryRun` (dry-run keeps its existing defaults;
|
||||||
|
`-Summary -DryRun` just makes the preview more compact).
|
||||||
|
- On success: suppress the low-value per-item lines (`SOURCE`, `CREATE`,
|
||||||
|
`UPDATE`, `UNCHANGED`, per-plugin `PASS ... ensured/already installed`,
|
||||||
|
per-doctor-check `PASS`) and print one summary line instead:
|
||||||
|
- `Build: PASS | 4 packages`
|
||||||
|
- `Doctor: PASS | <n> checks`
|
||||||
|
- `Tests: PASS | <n>/<n>`
|
||||||
|
- `Install: PASS | <client>, <platform>` / `Update: PASS | <client>, <platform>`
|
||||||
|
- Never suppress: `WARN`, `FAIL`, thrown errors, interactive prompts, or the
|
||||||
|
plugin toggle list. Warnings/errors always print in full regardless of the
|
||||||
|
flag.
|
||||||
|
- Implementation: each script gains a `$Summary`/`summary` bool threaded into
|
||||||
|
the existing `Write-Output`/`printf` call sites in `scripts/lib/manifest.*`
|
||||||
|
and `scripts/lib/plugins.*` (guard the per-item lines), plus a tally +
|
||||||
|
one-line footer at the end of `build.*`, `doctor.*`, `install.*`,
|
||||||
|
`update.*`, and each `test-*.*`.
|
||||||
|
|
||||||
|
## 2. Progressive disclosure for large rules
|
||||||
|
|
||||||
|
Split only the three rules that are 2-3x larger than the rest and have
|
||||||
|
clearly separable, independently-invokable subtopics:
|
||||||
|
|
||||||
|
- `angular.md` (297 lines) → components, templates, signals, rxjs,
|
||||||
|
dependency-injection-and-services, forms-and-routing, testing
|
||||||
|
- `wpf.md` (375 lines) → mvvm-and-binding, commands-and-async,
|
||||||
|
collections-and-virtualization, xaml-and-layout, resources-and-lifecycle,
|
||||||
|
testing-and-quality
|
||||||
|
- `ui-ux.md` (306 lines) → navigation-and-actions, forms, content-and-feedback,
|
||||||
|
visual-and-accessibility, tables-search-dialogs, destructive-and-error-prevention
|
||||||
|
|
||||||
|
`git.md`, `typescript.md`, `csharp.md`, `refactoring.md`, `microsoft.md`
|
||||||
|
(116-167 lines) stay single-file — splitting them adds a read hop without
|
||||||
|
materially cutting tokens.
|
||||||
|
|
||||||
|
Structure per split rule: `shared/rules/<name>/index.md` (small: universal/
|
||||||
|
high-value rules + a short table telling the agent which `references/*.md`
|
||||||
|
to open for which subtopic) + `shared/rules/<name>/references/*.md`.
|
||||||
|
|
||||||
|
- Claude: `skills/rules/<skill_name>/SKILL.md` = generated frontmatter (from
|
||||||
|
`rule-skills.tsv`, unchanged mechanism) + `index.md` body; `references/`
|
||||||
|
copied alongside into the same skill directory (already documented Anthropic
|
||||||
|
pattern; `Copy-Directory` already ships arbitrary sibling files, so no new
|
||||||
|
copy mechanism needed — only the generation loop needs to read `index.md`
|
||||||
|
instead of a flat file when `rule_file` in the manifest names a directory).
|
||||||
|
- Codex: `rules/<name>/index.md` + `rules/<name>/references/*.md`; the path
|
||||||
|
in `AGENTS.md`'s rule-loading heredoc (`scripts/build.ps1`/`build.sh`)
|
||||||
|
changes from `rules/<name>.md` to `rules/<name>/index.md` for these three.
|
||||||
|
- `adapters/claude/rule-skills.tsv` stays the single source of truth for
|
||||||
|
trigger wording — no new duplication.
|
||||||
|
- Build-script change: detect whether `shared/rules/<rule_file>` resolves to
|
||||||
|
a file or a directory; directory ⇒ read `<dir>/index.md` as the body and
|
||||||
|
copy the whole `<dir>/` (including `references/`) instead of a single file.
|
||||||
|
|
||||||
|
## 3+4. Narrower triggers and canonical rule locations (same fix)
|
||||||
|
|
||||||
|
Root cause found in the current build: `general.md` is embedded in full into
|
||||||
|
`CLAUDE.md` (`build.ps1`/`build.sh`, unconditional, every task) but for Codex
|
||||||
|
is only *referenced by path* ("Always load `rules/general.md`"—Codex must
|
||||||
|
actively read it). `shared/global-instructions.md` separately re-paraphrases
|
||||||
|
Priority, Documentation/Comments, and decision-making content inline — so
|
||||||
|
`decision-rule.md`'s substance is already permanently loaded for both
|
||||||
|
clients today, never actually lazy, defeating its own skill/file-based
|
||||||
|
lazy-loading.
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
|
||||||
|
- Embed `general.md` in full into `AGENTS.md` too (mirror what `CLAUDE.md`
|
||||||
|
already does). Remove the "Always load `rules/general.md`" instruction —
|
||||||
|
no longer needed. This is strictly safer, not riskier, for Codex even
|
||||||
|
though subagent AGENTS.md inheritance is undocumented/unconfirmed
|
||||||
|
(researched): today, if a Codex subagent doesn't inherit AGENTS.md, it
|
||||||
|
already can't see the *instruction* to go read `general.md` either, so
|
||||||
|
embedding changes nothing for that case, and strictly helps the case where
|
||||||
|
it does inherit (guaranteed-present vs. requires-a-read-action).
|
||||||
|
- Trim `shared/global-instructions.md` to just the rule-loading block and the
|
||||||
|
(rewritten, see §5) orchestration paragraph. Delete the restated
|
||||||
|
Priority/Documentation/Comments/decision-making prose — `general.md` (now
|
||||||
|
embedded for both clients, always) is the single canonical source.
|
||||||
|
- `general.md` gains one short pointer line to the decision-rule skill/file
|
||||||
|
for actual ambiguity, replacing the removed inline paragraph — keeps
|
||||||
|
decision-making guidance genuinely lazy (loads only when ambiguity is
|
||||||
|
actually present), which is exactly the item-3 goal for `decision-rule.md`.
|
||||||
|
- `definition-of-done.md`'s two bullets that restate the documentation rule
|
||||||
|
(lines 19-20) collapse to one line referencing `general.md`'s Documentation
|
||||||
|
section instead of repeating it.
|
||||||
|
- `shared/agents/*/instructions.md` (the 3-line per-role files) stay
|
||||||
|
unchanged. Researched: Claude subagents fully inherit CLAUDE.md (redundant
|
||||||
|
there), but Codex subagent AGENTS.md inheritance is unconfirmed/
|
||||||
|
undocumented — these files feed both clients' agent definitions from one
|
||||||
|
shared source, and for Codex the one-line "no comments" reminder inside
|
||||||
|
`developer_instructions` is the only guaranteed-present copy if inheritance
|
||||||
|
turns out not to hold. Cost is 3 lines; not worth the risk to trim.
|
||||||
|
- `security.md`'s broad trigger ("any software development task") is left
|
||||||
|
as-is — it's already the documented "short universal baseline" design
|
||||||
|
(`docs/configuration.md`), 11 lines, and narrowing it would just relocate
|
||||||
|
the same content behind a less reliable trigger. The actual "effectively
|
||||||
|
always loaded" problem was the *duplicated* Priority/decision content in
|
||||||
|
`global-instructions.md`, fixed above.
|
||||||
|
|
||||||
|
## 5. Token-aware subagent orchestration guidance
|
||||||
|
|
||||||
|
Replace the "Use one implementer for small, contained tasks. Add researcher
|
||||||
|
only for material uncertainty..." paragraph in `global-instructions.md` with:
|
||||||
|
|
||||||
|
> Delegate only when the expected improvement in correctness, independence,
|
||||||
|
> specialization, or parallelism outweighs the duplicated context and tool
|
||||||
|
> cost. Do not delegate simple repository exploration the main agent can do
|
||||||
|
> directly. Do not create a researcher for information the main agent can
|
||||||
|
> obtain cheaply itself. Use one implementer for small and medium contained
|
||||||
|
> tasks. Run reviewer and verifier together only when their responsibilities
|
||||||
|
> are materially different for the current task. Avoid multiple agents
|
||||||
|
> independently reading the same large set of files. Use parallel agents
|
||||||
|
> only for genuinely independent work. Do not let subagents create further
|
||||||
|
> subagents.
|
||||||
|
|
||||||
|
`config.toml`'s `max_concurrent_threads_per_session = 5` / Claude's five-
|
||||||
|
operation concurrency stay untouched — only the decision guidance changes,
|
||||||
|
not the ceiling.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- `scripts/build.ps1`/`.sh`, `scripts/doctor.ps1`/`.sh` pass.
|
||||||
|
- `scripts/test-platform-packages.*` (agent/skill/AGENTS.md/CLAUDE.md content
|
||||||
|
assertions) updated for: embedded `general.md` in `AGENTS.md`, the split
|
||||||
|
rule skill/reference structure, trimmed `global-instructions.md`.
|
||||||
|
- New/updated assertions for `-Summary`/`--summary` output shape on at least
|
||||||
|
one script per language (build + doctor), covering both the summary line
|
||||||
|
and that `WARN`/`FAIL` still surface unsummarized.
|
||||||
|
- Manual read-through of the three split rules to confirm no content was
|
||||||
|
dropped, only relocated.
|
||||||
+29
-20
@@ -1,5 +1,5 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param()
|
param([switch]$Summary)
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$root = Split-Path $PSScriptRoot -Parent
|
$root = Split-Path $PSScriptRoot -Parent
|
||||||
$shared = Join-Path $root 'shared'
|
$shared = Join-Path $root 'shared'
|
||||||
@@ -79,7 +79,7 @@ function Test-CodexSchema($configPath) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& (Join-Path $root 'shared/hooks/scripts/Test-SessionConfig.ps1') -RepositoryRoot $root
|
& (Join-Path $root 'shared/hooks/scripts/Test-SessionConfig.ps1') -RepositoryRoot $root | Where-Object { -not $Summary -or $_ -ne 'PASS session configuration' }
|
||||||
|
|
||||||
$sourceAgentDirs = @(Get-ChildItem (Join-Path $shared 'agents') -Directory | Sort-Object Name)
|
$sourceAgentDirs = @(Get-ChildItem (Join-Path $shared 'agents') -Directory | Sort-Object Name)
|
||||||
if ($sourceAgentDirs.Count -eq 0) { throw 'No agents found under shared/agents.' }
|
if ($sourceAgentDirs.Count -eq 0) { throw 'No agents found under shared/agents.' }
|
||||||
@@ -90,10 +90,11 @@ foreach ($agentDir in $sourceAgentDirs) {
|
|||||||
$agentNames += $name
|
$agentNames += $name
|
||||||
}
|
}
|
||||||
|
|
||||||
# Codex keeps the original rule-loading text unchanged: every rule ships as a plain
|
# Codex: general.md is embedded directly into AGENTS.md, the same way Claude embeds
|
||||||
# file and AGENTS.md tells Codex to load the matching one by path.
|
# it into CLAUDE.md, instead of only being referenced by path — guaranteed present
|
||||||
|
# either way, and safe even if a subagent's AGENTS.md inheritance is not guaranteed.
|
||||||
$codexRuleLoading = @'
|
$codexRuleLoading = @'
|
||||||
Always load and apply `rules/general.md` before starting any task.
|
General rules are embedded below.
|
||||||
|
|
||||||
When programming, always load and apply `rules/security.md`. This includes implementing, modifying, debugging, reviewing, testing, and configuring software, scripts, hooks, infrastructure, and integrations.
|
When programming, always load and apply `rules/security.md`. This includes implementing, modifying, debugging, reviewing, testing, and configuring software, scripts, hooks, infrastructure, and integrations.
|
||||||
|
|
||||||
@@ -109,16 +110,16 @@ Load rule files when their subject applies:
|
|||||||
- rules/security-network.md for network access, URLs, TLS, proxies, or SSRF.
|
- rules/security-network.md for network access, URLs, TLS, proxies, or SSRF.
|
||||||
- rules/security-crypto.md for cryptography, secrets, credentials, keys, or tokens.
|
- rules/security-crypto.md for cryptography, secrets, credentials, keys, or tokens.
|
||||||
- rules/security-supply-chain.md for dependencies, packages, plugins, builds, deployments, or CI.
|
- rules/security-supply-chain.md for dependencies, packages, plugins, builds, deployments, or CI.
|
||||||
- rules/angular.md for Angular work.
|
- rules/angular/index.md for Angular work.
|
||||||
- rules/typescript.md for TypeScript work.
|
- rules/typescript.md for TypeScript work.
|
||||||
- rules/csharp.md for C# or .NET work.
|
- rules/csharp.md for C# or .NET work.
|
||||||
- rules/wpf.md for WPF work.
|
- rules/wpf/index.md for WPF work.
|
||||||
- rules/ui-ux.md for UI or UX decisions.
|
- rules/ui-ux/index.md for UI or UX decisions.
|
||||||
- rules/microsoft.md for Microsoft 365, Azure DevOps, or Teams work.
|
- rules/microsoft.md for Microsoft 365, Azure DevOps, or Teams work.
|
||||||
- rules/git.md for Git operations.
|
- rules/git.md for Git operations.
|
||||||
- rules/refactoring.md for refactoring work.
|
- rules/refactoring.md for refactoring work.
|
||||||
- rules/definition-of-done.md when validating completion.
|
- rules/definition-of-done.md when validating completion.
|
||||||
- rules/decision-rule.md when requirements, behavior, or technical choices need to be evaluated.
|
- rules/decision-rule.md when requirements are unresolved, behavior is ambiguous, or a meaningful technical choice remains.
|
||||||
'@
|
'@
|
||||||
|
|
||||||
foreach ($platform in @('windows','linux')) {
|
foreach ($platform in @('windows','linux')) {
|
||||||
@@ -131,28 +132,34 @@ foreach ($platform in @('windows','linux')) {
|
|||||||
Copy-Directory (Join-Path $shared 'hooks') (Join-Path $output "claude-$platform/hooks")
|
Copy-Directory (Join-Path $shared 'hooks') (Join-Path $output "claude-$platform/hooks")
|
||||||
Copy-Directory (Join-Path $shared 'statusline') (Join-Path $output "claude-$platform/statusline")
|
Copy-Directory (Join-Path $shared 'statusline') (Join-Path $output "claude-$platform/statusline")
|
||||||
|
|
||||||
# Claude: general.md stays a plain, always-applied rule file. Every technology- or
|
|
||||||
# situation-specific rule becomes a skill instead, so only its name and description
|
|
||||||
# sit permanently in context; the full text loads only when the skill is invoked.
|
|
||||||
$claudeRulesDir = Join-Path $output "claude-$platform/rules"
|
$claudeRulesDir = Join-Path $output "claude-$platform/rules"
|
||||||
New-Item $claudeRulesDir -ItemType Directory -Force | Out-Null
|
New-Item $claudeRulesDir -ItemType Directory -Force | Out-Null
|
||||||
Copy-Item (Join-Path $shared 'rules/general.md') (Join-Path $claudeRulesDir 'general.md') -Force
|
|
||||||
|
|
||||||
$claudeRuleSkillLines = @()
|
$claudeRuleSkillLines = @()
|
||||||
foreach ($entry in $ruleSkillEntries) {
|
foreach ($entry in $ruleSkillEntries) {
|
||||||
|
$ruleSourcePath = Join-Path $shared "rules/$($entry.rule_file)"
|
||||||
|
$isDirectory = Test-Path -LiteralPath $ruleSourcePath -PathType Container
|
||||||
|
$ruleBodyPath = if ($isDirectory) { Join-Path $ruleSourcePath 'index.md' } else { $ruleSourcePath }
|
||||||
$skillDir = Join-Path $output "claude-$platform/skills/rules/$($entry.skill_name)"
|
$skillDir = Join-Path $output "claude-$platform/skills/rules/$($entry.skill_name)"
|
||||||
New-Item $skillDir -ItemType Directory -Force | Out-Null
|
New-Item $skillDir -ItemType Directory -Force | Out-Null
|
||||||
$ruleContent = Get-Content (Join-Path $shared "rules/$($entry.rule_file)") -Raw
|
$ruleContent = Get-Content $ruleBodyPath -Raw
|
||||||
$skillBody = "---`r`nname: $($entry.skill_name)`r`ndescription: Use for $($entry.trigger).`r`n---`r`n`r`n$ruleContent"
|
$skillBody = "---`r`nname: $($entry.skill_name)`r`ndescription: $(Quote-Toml "Use for $($entry.trigger).")`r`n---`r`n`r`n$ruleContent"
|
||||||
Set-Content (Join-Path $skillDir 'SKILL.md') $skillBody -Encoding UTF8
|
Set-Content (Join-Path $skillDir 'SKILL.md') $skillBody -Encoding UTF8
|
||||||
|
if ($isDirectory) {
|
||||||
|
$referencesSource = Join-Path $ruleSourcePath 'references'
|
||||||
|
if (Test-Path -LiteralPath $referencesSource) { Copy-Directory $referencesSource (Join-Path $skillDir 'references') }
|
||||||
|
}
|
||||||
$claudeRuleSkillLines += "- $($entry.skill_name) for $($entry.trigger)."
|
$claudeRuleSkillLines += "- $($entry.skill_name) for $($entry.trigger)."
|
||||||
}
|
}
|
||||||
$claudeRuleLoading = "Always apply ``rules/general.md`` before starting any task, embedded below.`r`n`r`nDetect the languages, frameworks, tools, and change areas from the repository and the requested work. Invoke every matching rule skill before editing. Invoke all matching rule skills when multiple technologies apply.`r`n`r`nInvoke rule skills when their subject applies:`r`n`r`n" + ($claudeRuleSkillLines -join "`r`n")
|
$claudeRuleLoading = "General rules are embedded below.`r`n`r`nDetect the languages, frameworks, tools, and change areas from the repository and the requested work. Invoke every matching rule skill before editing. Invoke all matching rule skills when multiple technologies apply.`r`n`r`nInvoke rule skills when their subject applies:`r`n`r`n" + ($claudeRuleSkillLines -join "`r`n")
|
||||||
|
|
||||||
$sharedTemplate = Get-Content (Join-Path $shared 'global-instructions.md') -Raw
|
$sharedTemplate = Get-Content (Join-Path $shared 'global-instructions.md') -Raw
|
||||||
Set-Content (Join-Path $output "codex-$platform/AGENTS.md") ($sharedTemplate.Replace('__RULE_LOADING__', $codexRuleLoading)) -Encoding UTF8
|
|
||||||
|
|
||||||
$generalContent = Get-Content (Join-Path $shared 'rules/general.md') -Raw
|
$generalContent = Get-Content (Join-Path $shared 'rules/general.md') -Raw
|
||||||
|
|
||||||
|
$agentsContent = $sharedTemplate.Replace('__RULE_LOADING__', $codexRuleLoading)
|
||||||
|
$agentsContent = $agentsContent.TrimEnd() + "`r`n`r`n---`r`n`r`n$generalContent"
|
||||||
|
Set-Content (Join-Path $output "codex-$platform/AGENTS.md") $agentsContent -Encoding UTF8
|
||||||
|
|
||||||
$claudeContent = $sharedTemplate.Replace('__RULE_LOADING__', $claudeRuleLoading)
|
$claudeContent = $sharedTemplate.Replace('__RULE_LOADING__', $claudeRuleLoading)
|
||||||
$claudeContent = $claudeContent.TrimEnd() + "`r`n`r`n---`r`n`r`n$generalContent"
|
$claudeContent = $claudeContent.TrimEnd() + "`r`n`r`n---`r`n`r`n$generalContent"
|
||||||
Set-Content (Join-Path $output "claude-$platform/CLAUDE.md") $claudeContent -Encoding UTF8
|
Set-Content (Join-Path $output "claude-$platform/CLAUDE.md") $claudeContent -Encoding UTF8
|
||||||
@@ -183,7 +190,9 @@ foreach ($platform in @('windows','linux')) {
|
|||||||
$script = if ($platform -eq 'windows') { 'flashbang.ps1' } else { 'flashbang.sh' }
|
$script = if ($platform -eq 'windows') { 'flashbang.ps1' } else { 'flashbang.sh' }
|
||||||
$statusLineScript = if ($platform -eq 'windows') { 'statusline.ps1' } else { 'statusline.sh' }
|
$statusLineScript = if ($platform -eq 'windows') { 'statusline.ps1' } else { 'statusline.sh' }
|
||||||
$content = Get-Content -LiteralPath $path -Raw
|
$content = Get-Content -LiteralPath $path -Raw
|
||||||
$content = $content.Replace('__HOOK_COMMAND__', $command).Replace('__WINDOWS_HOOK_COMMAND__', $command).Replace('__HOOK_SCRIPT__', $script).Replace('__WINDOWS_HOOK_SCRIPT__', $script).Replace('__STATUSLINE_COMMAND__', $command).Replace('__STATUSLINE_SCRIPT__', $statusLineScript)
|
$compactScript = if ($platform -eq 'windows') { 'Record-Compact.ps1' } else { 'record-compact.sh' }
|
||||||
|
$pointerScript = if ($platform -eq 'windows') { 'Show-SessionStatePointer.ps1' } else { 'show-session-state-pointer.sh' }
|
||||||
|
$content = $content.Replace('__HOOK_COMMAND__', $command).Replace('__WINDOWS_HOOK_COMMAND__', $command).Replace('__HOOK_SCRIPT__', $script).Replace('__WINDOWS_HOOK_SCRIPT__', $script).Replace('__COMPACT_SCRIPT__', $compactScript).Replace('__SESSION_POINTER_SCRIPT__', $pointerScript).Replace('__STATUSLINE_COMMAND__', $command).Replace('__STATUSLINE_SCRIPT__', $statusLineScript)
|
||||||
Set-Content -LiteralPath $path -Value $content -Encoding UTF8
|
Set-Content -LiteralPath $path -Value $content -Encoding UTF8
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,5 +212,5 @@ $leftoverPlaceholders = @(Get-ChildItem $output -File -Recurse | ForEach-Object
|
|||||||
})
|
})
|
||||||
if ($leftoverPlaceholders.Count) { throw "Unresolved template placeholders in: $($leftoverPlaceholders -join ', ')" }
|
if ($leftoverPlaceholders.Count) { throw "Unresolved template placeholders in: $($leftoverPlaceholders -join ', ')" }
|
||||||
|
|
||||||
Write-Output 'PASS build: codex-windows, claude-windows, codex-linux, claude-linux'
|
if ($Summary) { Write-Output 'Build: PASS | 4 packages' } else { Write-Output 'PASS build: codex-windows, claude-windows, codex-linux, claude-linux' }
|
||||||
exit 0
|
exit 0
|
||||||
+42
-19
@@ -1,5 +1,12 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
summary=false
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--summary) summary=true; shift ;;
|
||||||
|
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
shared="$root/shared"
|
shared="$root/shared"
|
||||||
@@ -27,11 +34,12 @@ rule_skill_files=()
|
|||||||
rule_skill_names=()
|
rule_skill_names=()
|
||||||
rule_skill_triggers=()
|
rule_skill_triggers=()
|
||||||
while IFS=$'\t' read -r rule_file skill_name trigger; do
|
while IFS=$'\t' read -r rule_file skill_name trigger; do
|
||||||
|
trigger=${trigger%$'\r'}
|
||||||
[ "$rule_file" != rule_file ] || continue
|
[ "$rule_file" != rule_file ] || continue
|
||||||
[ -n "$rule_file" ] || continue
|
[ -n "$rule_file" ] || continue
|
||||||
[ -n "$skill_name" ] || { printf 'Missing skill_name in rule-skill manifest row for %s.\n' "$rule_file" >&2; exit 1; }
|
[ -n "$skill_name" ] || { printf 'Missing skill_name in rule-skill manifest row for %s.\n' "$rule_file" >&2; exit 1; }
|
||||||
[ -n "$trigger" ] || { printf 'Missing trigger in rule-skill manifest row for %s.\n' "$rule_file" >&2; exit 1; }
|
[ -n "$trigger" ] || { printf 'Missing trigger in rule-skill manifest row for %s.\n' "$rule_file" >&2; exit 1; }
|
||||||
[ -f "$shared/rules/$rule_file" ] || { printf 'Rule-skill manifest references a missing rule file: %s\n' "$rule_file" >&2; exit 1; }
|
[ -f "$shared/rules/$rule_file" ] || [ -d "$shared/rules/$rule_file" ] || { printf 'Rule-skill manifest references a missing rule file: %s\n' "$rule_file" >&2; exit 1; }
|
||||||
[ -z "${rule_skill_seen[$skill_name]+x}" ] || { printf 'Duplicate rule-skill name in manifest: %s\n' "$skill_name" >&2; exit 1; }
|
[ -z "${rule_skill_seen[$skill_name]+x}" ] || { printf 'Duplicate rule-skill name in manifest: %s\n' "$skill_name" >&2; exit 1; }
|
||||||
rule_skill_seen[$skill_name]=1
|
rule_skill_seen[$skill_name]=1
|
||||||
rule_skill_files+=("$rule_file")
|
rule_skill_files+=("$rule_file")
|
||||||
@@ -62,7 +70,12 @@ quote_toml() {
|
|||||||
printf '"%s"' "$value"
|
printf '"%s"' "$value"
|
||||||
}
|
}
|
||||||
|
|
||||||
bash "$shared/hooks/scripts/test-session-config.sh" "$root"
|
session_output=$(bash "$shared/hooks/scripts/test-session-config.sh" "$root") || { printf '%s\n' "$session_output" >&2; exit 1; }
|
||||||
|
if [ "$summary" = true ]; then
|
||||||
|
printf '%s\n' "$session_output" | sed '/^PASS session configuration$/d'
|
||||||
|
else
|
||||||
|
printf '%s\n' "$session_output"
|
||||||
|
fi
|
||||||
rm -rf -- "$output"
|
rm -rf -- "$output"
|
||||||
|
|
||||||
agent_dirs=("$shared"/agents/*/)
|
agent_dirs=("$shared"/agents/*/)
|
||||||
@@ -73,10 +86,11 @@ for agent_dir in "${agent_dirs[@]}"; do
|
|||||||
agent_seen[$name]=1
|
agent_seen[$name]=1
|
||||||
done
|
done
|
||||||
|
|
||||||
# Codex keeps the original rule-loading text unchanged: every rule ships as a plain
|
# Codex: general.md is embedded directly into AGENTS.md, the same way Claude embeds
|
||||||
# file and AGENTS.md tells Codex to load the matching one by path.
|
# it into CLAUDE.md, instead of only being referenced by path — guaranteed present
|
||||||
|
# either way, and safe even if a subagent's AGENTS.md inheritance is not guaranteed.
|
||||||
read -r -d '' codex_rule_loading <<'BLOCK' || true
|
read -r -d '' codex_rule_loading <<'BLOCK' || true
|
||||||
Always load and apply `rules/general.md` before starting any task.
|
General rules are embedded below.
|
||||||
|
|
||||||
When programming, always load and apply `rules/security.md`. This includes implementing, modifying, debugging, reviewing, testing, and configuring software, scripts, hooks, infrastructure, and integrations.
|
When programming, always load and apply `rules/security.md`. This includes implementing, modifying, debugging, reviewing, testing, and configuring software, scripts, hooks, infrastructure, and integrations.
|
||||||
|
|
||||||
@@ -92,16 +106,16 @@ Load rule files when their subject applies:
|
|||||||
- rules/security-network.md for network access, URLs, TLS, proxies, or SSRF.
|
- rules/security-network.md for network access, URLs, TLS, proxies, or SSRF.
|
||||||
- rules/security-crypto.md for cryptography, secrets, credentials, keys, or tokens.
|
- rules/security-crypto.md for cryptography, secrets, credentials, keys, or tokens.
|
||||||
- rules/security-supply-chain.md for dependencies, packages, plugins, builds, deployments, or CI.
|
- rules/security-supply-chain.md for dependencies, packages, plugins, builds, deployments, or CI.
|
||||||
- rules/angular.md for Angular work.
|
- rules/angular/index.md for Angular work.
|
||||||
- rules/typescript.md for TypeScript work.
|
- rules/typescript.md for TypeScript work.
|
||||||
- rules/csharp.md for C# or .NET work.
|
- rules/csharp.md for C# or .NET work.
|
||||||
- rules/wpf.md for WPF work.
|
- rules/wpf/index.md for WPF work.
|
||||||
- rules/ui-ux.md for UI or UX decisions.
|
- rules/ui-ux/index.md for UI or UX decisions.
|
||||||
- rules/microsoft.md for Microsoft 365, Azure DevOps, or Teams work.
|
- rules/microsoft.md for Microsoft 365, Azure DevOps, or Teams work.
|
||||||
- rules/git.md for Git operations.
|
- rules/git.md for Git operations.
|
||||||
- rules/refactoring.md for refactoring work.
|
- rules/refactoring.md for refactoring work.
|
||||||
- rules/definition-of-done.md when validating completion.
|
- rules/definition-of-done.md when validating completion.
|
||||||
- rules/decision-rule.md when requirements, behavior, or technical choices need to be evaluated.
|
- rules/decision-rule.md when requirements are unresolved, behavior is ambiguous, or a meaningful technical choice remains.
|
||||||
BLOCK
|
BLOCK
|
||||||
|
|
||||||
for platform in windows linux; do
|
for platform in windows linux; do
|
||||||
@@ -113,24 +127,26 @@ copy_directory "$shared/hooks" "$output/codex-$platform/hooks"
|
|||||||
copy_directory "$shared/hooks" "$output/claude-$platform/hooks"
|
copy_directory "$shared/hooks" "$output/claude-$platform/hooks"
|
||||||
copy_directory "$shared/statusline" "$output/claude-$platform/statusline"
|
copy_directory "$shared/statusline" "$output/claude-$platform/statusline"
|
||||||
|
|
||||||
# Claude: general.md stays a plain, always-applied rule file. Every technology- or
|
|
||||||
# situation-specific rule becomes a skill instead, so only its name and description
|
|
||||||
# sit permanently in context; the full text loads only when the skill is invoked.
|
|
||||||
mkdir -p "$output/claude-$platform/rules"
|
mkdir -p "$output/claude-$platform/rules"
|
||||||
cp "$shared/rules/general.md" "$output/claude-$platform/rules/general.md"
|
|
||||||
|
|
||||||
claude_rule_loading_list=""
|
claude_rule_loading_list=""
|
||||||
for i in "${!rule_skill_names[@]}"; do
|
for i in "${!rule_skill_names[@]}"; do
|
||||||
rule_file=${rule_skill_files[$i]}
|
rule_file=${rule_skill_files[$i]}
|
||||||
skill_name=${rule_skill_names[$i]}
|
skill_name=${rule_skill_names[$i]}
|
||||||
trigger=${rule_skill_triggers[$i]}
|
trigger=${rule_skill_triggers[$i]}
|
||||||
|
rule_source="$shared/rules/$rule_file"
|
||||||
|
rule_body="$rule_source"
|
||||||
|
[ -d "$rule_source" ] && rule_body="$rule_source/index.md"
|
||||||
skill_dir="$output/claude-$platform/skills/rules/$skill_name"
|
skill_dir="$output/claude-$platform/skills/rules/$skill_name"
|
||||||
mkdir -p "$skill_dir"
|
mkdir -p "$skill_dir"
|
||||||
{ printf -- '---\nname: %s\ndescription: Use for %s.\n---\n\n' "$skill_name" "$trigger"; cat "$shared/rules/$rule_file"; } > "$skill_dir/SKILL.md"
|
{ printf -- '---\nname: %s\ndescription: %s\n---\n\n' "$skill_name" "$(quote_toml "Use for $trigger.")"; cat "$rule_body"; } > "$skill_dir/SKILL.md"
|
||||||
|
if [ -d "$rule_source/references" ]; then
|
||||||
|
copy_directory "$rule_source/references" "$skill_dir/references"
|
||||||
|
fi
|
||||||
claude_rule_loading_list="${claude_rule_loading_list}- ${skill_name} for ${trigger}.
|
claude_rule_loading_list="${claude_rule_loading_list}- ${skill_name} for ${trigger}.
|
||||||
"
|
"
|
||||||
done
|
done
|
||||||
claude_rule_loading="Always apply \`rules/general.md\` before starting any task, embedded below.
|
claude_rule_loading="General rules are embedded below.
|
||||||
|
|
||||||
Detect the languages, frameworks, tools, and change areas from the repository and the requested work. Invoke every matching rule skill before editing. Invoke all matching rule skills when multiple technologies apply.
|
Detect the languages, frameworks, tools, and change areas from the repository and the requested work. Invoke every matching rule skill before editing. Invoke all matching rule skills when multiple technologies apply.
|
||||||
|
|
||||||
@@ -139,10 +155,11 @@ Invoke rule skills when their subject applies:
|
|||||||
${claude_rule_loading_list%$'\n'}"
|
${claude_rule_loading_list%$'\n'}"
|
||||||
|
|
||||||
shared_template=$(<"$shared/global-instructions.md")
|
shared_template=$(<"$shared/global-instructions.md")
|
||||||
agents_content=${shared_template//__RULE_LOADING__/$codex_rule_loading}
|
|
||||||
printf '%s\n' "$agents_content" > "$output/codex-$platform/AGENTS.md"
|
|
||||||
|
|
||||||
general_content=$(<"$shared/rules/general.md")
|
general_content=$(<"$shared/rules/general.md")
|
||||||
|
|
||||||
|
agents_content=${shared_template//__RULE_LOADING__/$codex_rule_loading}
|
||||||
|
printf '%s\n\n---\n\n%s\n' "${agents_content%$'\n'}" "$general_content" > "$output/codex-$platform/AGENTS.md"
|
||||||
|
|
||||||
claude_content=${shared_template//__RULE_LOADING__/$claude_rule_loading}
|
claude_content=${shared_template//__RULE_LOADING__/$claude_rule_loading}
|
||||||
printf '%s\n\n---\n\n%s\n' "${claude_content%$'\n'}" "$general_content" > "$output/claude-$platform/CLAUDE.md"
|
printf '%s\n\n---\n\n%s\n' "${claude_content%$'\n'}" "$general_content" > "$output/claude-$platform/CLAUDE.md"
|
||||||
|
|
||||||
@@ -171,10 +188,14 @@ for client in codex claude; do
|
|||||||
command=bash
|
command=bash
|
||||||
script=flashbang.sh
|
script=flashbang.sh
|
||||||
statusline_script=statusline.sh
|
statusline_script=statusline.sh
|
||||||
|
compact_script=record-compact.sh
|
||||||
|
pointer_script=show-session-state-pointer.sh
|
||||||
if [ "$platform" = windows ]; then
|
if [ "$platform" = windows ]; then
|
||||||
command='powershell -NoProfile -ExecutionPolicy Bypass -File'
|
command='powershell -NoProfile -ExecutionPolicy Bypass -File'
|
||||||
script=flashbang.ps1
|
script=flashbang.ps1
|
||||||
statusline_script=statusline.ps1
|
statusline_script=statusline.ps1
|
||||||
|
compact_script=Record-Compact.ps1
|
||||||
|
pointer_script=Show-SessionStatePointer.ps1
|
||||||
fi
|
fi
|
||||||
path="$output/$client-$platform/$file"
|
path="$output/$client-$platform/$file"
|
||||||
content=$(<"$path")
|
content=$(<"$path")
|
||||||
@@ -182,6 +203,8 @@ for client in codex claude; do
|
|||||||
content=${content//__WINDOWS_HOOK_COMMAND__/$command}
|
content=${content//__WINDOWS_HOOK_COMMAND__/$command}
|
||||||
content=${content//__HOOK_SCRIPT__/$script}
|
content=${content//__HOOK_SCRIPT__/$script}
|
||||||
content=${content//__WINDOWS_HOOK_SCRIPT__/$script}
|
content=${content//__WINDOWS_HOOK_SCRIPT__/$script}
|
||||||
|
content=${content//__COMPACT_SCRIPT__/$compact_script}
|
||||||
|
content=${content//__SESSION_POINTER_SCRIPT__/$pointer_script}
|
||||||
content=${content//__STATUSLINE_COMMAND__/$command}
|
content=${content//__STATUSLINE_COMMAND__/$command}
|
||||||
content=${content//__STATUSLINE_SCRIPT__/$statusline_script}
|
content=${content//__STATUSLINE_SCRIPT__/$statusline_script}
|
||||||
printf '%s\n' "$content" > "$path"
|
printf '%s\n' "$content" > "$path"
|
||||||
@@ -213,4 +236,4 @@ if [ -n "$leftover_tokens" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf 'PASS build: codex-windows, claude-windows, codex-linux, claude-linux\n'
|
if [ "$summary" = true ]; then printf 'Build: PASS | 4 packages\n'; else printf 'PASS build: codex-windows, claude-windows, codex-linux, claude-linux\n'; fi
|
||||||
+25
-6
@@ -1,11 +1,21 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param()
|
param([switch]$Summary)
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$root = Split-Path $PSScriptRoot -Parent
|
$root = Split-Path $PSScriptRoot -Parent
|
||||||
. (Join-Path $PSScriptRoot 'lib/manifest.ps1')
|
. (Join-Path $PSScriptRoot 'lib/manifest.ps1')
|
||||||
$homePath = [Environment]::GetFolderPath('UserProfile')
|
$homePath = [Environment]::GetFolderPath('UserProfile')
|
||||||
$fail = $false
|
$script:fail = $false
|
||||||
function Result($state, $message) { Write-Output ("$state $message"); if ($state -eq 'FAIL') { $script:fail = $true } }
|
$script:checkCount = 0
|
||||||
|
$script:passBuffer = @()
|
||||||
|
function Result($state, $message) {
|
||||||
|
$script:checkCount++
|
||||||
|
if ($state -eq 'FAIL') { $script:fail = $true }
|
||||||
|
if ($Summary -and $state -eq 'PASS') {
|
||||||
|
$script:passBuffer += "$state $message"
|
||||||
|
} else {
|
||||||
|
Write-Output ("$state $message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($tool in @('codex','claude')) { if (Get-Command $tool -ErrorAction SilentlyContinue) { $version = & $tool --version 2>&1 | Select-Object -First 1; Result 'PASS' "$tool available ($version)" } else { Result 'WARN' "$tool unavailable" } }
|
foreach ($tool in @('codex','claude')) { if (Get-Command $tool -ErrorAction SilentlyContinue) { $version = & $tool --version 2>&1 | Select-Object -First 1; Result 'PASS' "$tool available ($version)" } else { Result 'WARN' "$tool unavailable" } }
|
||||||
if ($PSVersionTable.PSVersion.Major -ge 6 -and -not $IsWindows) { if (Get-Command jq -ErrorAction SilentlyContinue) { Result 'PASS' 'jq available for Claude status line' } else { Result 'WARN' 'jq unavailable; Claude status line is disabled' } }
|
if ($PSVersionTable.PSVersion.Major -ge 6 -and -not $IsWindows) { if (Get-Command jq -ErrorAction SilentlyContinue) { Result 'PASS' 'jq available for Claude status line' } else { Result 'WARN' 'jq unavailable; Claude status line is disabled' } }
|
||||||
@@ -42,7 +52,7 @@ if (Test-Path -LiteralPath $settingsPath) {
|
|||||||
}
|
}
|
||||||
$configTomlPath = Join-Path $homePath '.codex/config.toml'
|
$configTomlPath = Join-Path $homePath '.codex/config.toml'
|
||||||
if (Test-Path -LiteralPath $configTomlPath) {
|
if (Test-Path -LiteralPath $configTomlPath) {
|
||||||
$parseResult = & python -c 'import sys,tomllib,pathlib; tomllib.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8-sig"))' $configTomlPath 2>&1
|
$parseResult = & python -c "import sys,tomllib,pathlib; tomllib.loads(pathlib.Path(sys.argv[1]).read_text(encoding='utf-8-sig'))" $configTomlPath 2>&1
|
||||||
if ($LASTEXITCODE -eq 0) { Result 'PASS' 'installed .codex/config.toml parses as TOML' } else { Result 'FAIL' "installed .codex/config.toml is invalid TOML: $parseResult" }
|
if ($LASTEXITCODE -eq 0) { Result 'PASS' 'installed .codex/config.toml parses as TOML' } else { Result 'FAIL' "installed .codex/config.toml is invalid TOML: $parseResult" }
|
||||||
if (Get-Command codex -ErrorAction SilentlyContinue) {
|
if (Get-Command codex -ErrorAction SilentlyContinue) {
|
||||||
$previousCodexHome = $env:CODEX_HOME
|
$previousCodexHome = $env:CODEX_HOME
|
||||||
@@ -77,6 +87,15 @@ foreach ($client in @('codex','claude')) {
|
|||||||
if ($registered) { Result 'PASS' "$client Stop hook is registered" } else { Result 'WARN' "$client ships hook utilities, but no Stop hook is registered" }
|
if ($registered) { Result 'PASS' "$client Stop hook is registered" } else { Result 'WARN' "$client ships hook utilities, but no Stop hook is registered" }
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($fail) { exit 1 }
|
if ($Summary) {
|
||||||
Write-Output 'PASS doctor'
|
if ($script:fail) {
|
||||||
|
$script:passBuffer | ForEach-Object { Write-Output $_ }
|
||||||
|
Write-Output "Doctor: FAIL | $script:checkCount checks"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Output "Doctor: PASS | $script:checkCount checks"
|
||||||
|
} else {
|
||||||
|
if ($script:fail) { exit 1 }
|
||||||
|
Write-Output 'PASS doctor'
|
||||||
|
}
|
||||||
exit 0
|
exit 0
|
||||||
+27
-3
@@ -1,14 +1,29 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
summary=false
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--summary) summary=true; shift ;;
|
||||||
|
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
. "$root/scripts/lib/manifest.sh"
|
. "$root/scripts/lib/manifest.sh"
|
||||||
home_path=${HOME:?HOME is required}
|
home_path=${HOME:?HOME is required}
|
||||||
failed=false
|
failed=false
|
||||||
|
check_count=0
|
||||||
|
pass_buffer=()
|
||||||
|
|
||||||
result() {
|
result() {
|
||||||
printf '%s %s\n' "$1" "$2"
|
check_count=$((check_count + 1))
|
||||||
[ "$1" != FAIL ] || failed=true
|
[ "$1" != FAIL ] || failed=true
|
||||||
|
if [ "$summary" = true ] && [ "$1" = PASS ]; then
|
||||||
|
pass_buffer+=("$1 $2")
|
||||||
|
else
|
||||||
|
printf '%s %s\n' "$1" "$2"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
for tool in codex claude; do
|
for tool in codex claude; do
|
||||||
@@ -85,5 +100,14 @@ for path in shared/rules shared/skills shared/agents shared/hooks/scripts; do
|
|||||||
[ -e "$root/$path" ] && result PASS "source $path" || result FAIL "missing $path"
|
[ -e "$root/$path" ] && result PASS "source $path" || result FAIL "missing $path"
|
||||||
done
|
done
|
||||||
|
|
||||||
"$failed" && exit 1
|
if [ "$summary" = true ]; then
|
||||||
printf 'PASS doctor\n'
|
if [ "$failed" = true ]; then
|
||||||
|
for line in "${pass_buffer[@]}"; do printf '%s\n' "$line"; done
|
||||||
|
printf 'Doctor: FAIL | %s checks\n' "$check_count"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf 'Doctor: PASS | %s checks\n' "$check_count"
|
||||||
|
else
|
||||||
|
[ "$failed" = false ] || exit 1
|
||||||
|
printf 'PASS doctor\n'
|
||||||
|
fi
|
||||||
+18
-29
@@ -3,51 +3,40 @@ param(
|
|||||||
[switch]$DryRun,
|
[switch]$DryRun,
|
||||||
[ValidateSet('Codex','Claude','Both')][string]$Client,
|
[ValidateSet('Codex','Claude','Both')][string]$Client,
|
||||||
[ValidateSet('Windows','Linux')][string]$Platform,
|
[ValidateSet('Windows','Linux')][string]$Platform,
|
||||||
[switch]$UpdatePlugins
|
[switch]$Summary
|
||||||
)
|
)
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$root = Split-Path $PSScriptRoot -Parent
|
$root = Split-Path $PSScriptRoot -Parent
|
||||||
. (Join-Path $PSScriptRoot 'lib/plugins.ps1')
|
. (Join-Path $PSScriptRoot 'lib/plugins.ps1')
|
||||||
. (Join-Path $PSScriptRoot 'lib/manifest.ps1')
|
. (Join-Path $PSScriptRoot 'lib/manifest.ps1')
|
||||||
|
. (Join-Path $PSScriptRoot 'lib/install-targets.ps1')
|
||||||
|
|
||||||
|
$Platform = Read-InstallPlatform -Platform $Platform
|
||||||
|
$platform = $Platform.ToLowerInvariant()
|
||||||
|
$Client = Read-InstallClient -Client $Client
|
||||||
|
|
||||||
|
$homePath = [Environment]::GetFolderPath('UserProfile')
|
||||||
|
if (-not $DryRun -and (Test-AnyManifestPresent (Get-InstallDestinations -HomePath $homePath -Client $Client))) {
|
||||||
|
throw 'Already installed, use the update script.'
|
||||||
|
}
|
||||||
|
|
||||||
$buildScript = Join-Path $PSScriptRoot 'build.ps1'
|
$buildScript = Join-Path $PSScriptRoot 'build.ps1'
|
||||||
& $buildScript
|
& $buildScript -Summary:$Summary
|
||||||
if (-not $?) { throw 'Build failed. Installation was not started.' }
|
if (-not $?) { throw 'Build failed. Installation was not started.' }
|
||||||
$generated = Join-Path $root 'generated'
|
$generated = Join-Path $root 'generated'
|
||||||
if (-not (Test-Path $generated)) { throw 'Generated output is missing after a successful build.' }
|
if (-not (Test-Path $generated)) { throw 'Generated output is missing after a successful build.' }
|
||||||
|
|
||||||
if (-not $Platform) {
|
|
||||||
Write-Output 'Select target platform:'
|
|
||||||
Write-Output '1) Windows'
|
|
||||||
Write-Output '2) Linux'
|
|
||||||
do { $platformSelection = Read-Host 'Selection [1-2]' } while ($platformSelection -notin @('1','2'))
|
|
||||||
$Platform = @{'1'='Windows'; '2'='Linux'}[$platformSelection]
|
|
||||||
}
|
|
||||||
$platform = $Platform.ToLowerInvariant()
|
|
||||||
|
|
||||||
if (-not $Client) {
|
|
||||||
Write-Output 'Select installation target:'
|
|
||||||
Write-Output '1) Codex'
|
|
||||||
Write-Output '2) Claude'
|
|
||||||
Write-Output '3) Both'
|
|
||||||
do { $selection = Read-Host 'Selection [1-3]' } while ($selection -notin @('1','2','3'))
|
|
||||||
$Client = @{'1'='Codex'; '2'='Claude'; '3'='Both'}[$selection]
|
|
||||||
}
|
|
||||||
|
|
||||||
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||||
$homePath = [Environment]::GetFolderPath('UserProfile')
|
|
||||||
$shellCommand = 'pwsh -NoProfile -ExecutionPolicy Bypass -File'
|
$shellCommand = 'pwsh -NoProfile -ExecutionPolicy Bypass -File'
|
||||||
$windowsShellCommand = 'powershell -NoProfile -ExecutionPolicy Bypass -File'
|
$windowsShellCommand = 'powershell -NoProfile -ExecutionPolicy Bypass -File'
|
||||||
if ([Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT) { $shellCommand = $windowsShellCommand }
|
if ([Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT) { $shellCommand = $windowsShellCommand }
|
||||||
$targets = @()
|
|
||||||
if ($Client -in @('Codex','Both')) { $targets += @{ Source=(Join-Path $generated "codex-$platform"); Destination=(Join-Path $homePath '.codex') } }
|
|
||||||
if ($Client -in @('Codex','Both')) { $targets += @{ Source=(Join-Path $generated "codex-$platform/skills"); Destination=(Join-Path $homePath '.agents/skills') } }
|
|
||||||
if ($Client -in @('Claude','Both')) { $targets += @{ Source=(Join-Path $generated "claude-$platform"); Destination=(Join-Path $homePath '.claude') } }
|
|
||||||
|
|
||||||
foreach ($item in $targets) {
|
foreach ($item in (Get-InstallTargets -Generated $generated -HomePath $homePath -Platform $platform -Client $Client)) {
|
||||||
Sync-ManagedDestination -Source $item.Source -Destination $item.Destination -Stamp $stamp `
|
Sync-ManagedDestination -Source $item.Source -Destination $item.Destination -Stamp $stamp `
|
||||||
-AiConfigRoot $item.Destination.Replace('\','/') -ShellCommand $shellCommand -WindowsShellCommand $windowsShellCommand -DryRun:$DryRun
|
-AiConfigRoot $item.Destination.Replace('\','/') -ShellCommand $shellCommand -WindowsShellCommand $windowsShellCommand -DryRun:$DryRun -Summary:$Summary
|
||||||
}
|
}
|
||||||
|
|
||||||
Install-ConfiguredPlugins -RepositoryRoot $root -Client $Client -DryRun:$DryRun -Update:$UpdatePlugins
|
$plugins = Select-ConfiguredPlugins -RepositoryRoot $root -Client $Client -Mode 'Install' -DryRun:$DryRun
|
||||||
Write-Output ($(if ($DryRun) { 'PASS install dry-run' } else { 'PASS install' }))
|
Install-ConfiguredPlugins -RepositoryRoot $root -Client $Client -DryRun:$DryRun -Summary:$Summary -Entries $plugins.Selected
|
||||||
|
if ($Summary) { Write-Output "Install: PASS | $Client, $Platform$(if ($DryRun) { ', dry-run' })" } else { Write-Output ($(if ($DryRun) { 'PASS install dry-run' } else { 'PASS install' })) }
|
||||||
exit 0
|
exit 0
|
||||||
+31
-48
@@ -4,11 +4,11 @@ set -euo pipefail
|
|||||||
dry_run=false
|
dry_run=false
|
||||||
client=""
|
client=""
|
||||||
platform=""
|
platform=""
|
||||||
update_plugins=false
|
summary=false
|
||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--dry-run) dry_run=true; shift ;;
|
--dry-run) dry_run=true; shift ;;
|
||||||
--update-plugins) update_plugins=true; shift ;;
|
--summary) summary=true; shift ;;
|
||||||
--client) client=${2:?--client requires a value}; shift 2 ;;
|
--client) client=${2:?--client requires a value}; shift 2 ;;
|
||||||
--platform) platform=${2:?--platform requires a value}; shift 2 ;;
|
--platform) platform=${2:?--platform requires a value}; shift 2 ;;
|
||||||
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
|
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
|
||||||
@@ -18,60 +18,43 @@ done
|
|||||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
. "$root/scripts/lib/plugins.sh"
|
. "$root/scripts/lib/plugins.sh"
|
||||||
. "$root/scripts/lib/manifest.sh"
|
. "$root/scripts/lib/manifest.sh"
|
||||||
bash "$root/scripts/build.sh"
|
. "$root/scripts/lib/install-targets.sh"
|
||||||
|
|
||||||
|
platform=$(read_install_platform "$platform")
|
||||||
|
client=$(read_install_client "$client")
|
||||||
|
|
||||||
|
home_path=${HOME:?HOME is required}
|
||||||
|
if [ "$dry_run" = false ]; then
|
||||||
|
mapfile -t destinations < <(get_install_destinations "$home_path" "$client")
|
||||||
|
if any_manifest_present "${destinations[@]}"; then
|
||||||
|
printf 'Already installed, use the update script.\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
build_args=()
|
||||||
|
[ "$summary" = false ] || build_args+=(--summary)
|
||||||
|
bash "$root/scripts/build.sh" "${build_args[@]}"
|
||||||
generated="$root/generated"
|
generated="$root/generated"
|
||||||
[ -d "$generated" ] || { printf 'Generated output is missing after a successful build.\n' >&2; exit 1; }
|
[ -d "$generated" ] || { printf 'Generated output is missing after a successful build.\n' >&2; exit 1; }
|
||||||
|
|
||||||
if [ -z "$platform" ]; then
|
|
||||||
printf 'Select target platform:\n1) Windows\n2) Linux\n'
|
|
||||||
while :; do
|
|
||||||
read -r -p 'Selection [1-2] ' platform_selection
|
|
||||||
case "$platform_selection" in 1) platform=windows; break;; 2) platform=linux; break;; esac
|
|
||||||
done
|
|
||||||
else
|
|
||||||
case "$platform" in
|
|
||||||
windows|Windows) platform=windows ;;
|
|
||||||
linux|Linux) platform=linux ;;
|
|
||||||
*) printf 'Unknown --platform value: %s (expected windows or linux)\n' "$platform" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$client" ]; then
|
|
||||||
printf 'Select installation target:\n1) Codex\n2) Claude\n3) Both\n'
|
|
||||||
while :; do
|
|
||||||
read -r -p 'Selection [1-3] ' selection
|
|
||||||
case "$selection" in 1) client=codex; break;; 2) client=claude; break;; 3) client=both; break;; esac
|
|
||||||
done
|
|
||||||
else
|
|
||||||
case "$client" in
|
|
||||||
codex|Codex) client=codex ;;
|
|
||||||
claude|Claude) client=claude ;;
|
|
||||||
both|Both) client=both ;;
|
|
||||||
*) printf 'Unknown --client value: %s (expected codex, claude, or both)\n' "$client" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
stamp=$(date +%Y%m%d-%H%M%S)
|
stamp=$(date +%Y%m%d-%H%M%S)
|
||||||
home_path=${HOME:?HOME is required}
|
|
||||||
shell_command=bash
|
shell_command=bash
|
||||||
windows_shell_command='powershell -NoProfile -ExecutionPolicy Bypass -File'
|
windows_shell_command='powershell -NoProfile -ExecutionPolicy Bypass -File'
|
||||||
|
|
||||||
targets=()
|
while IFS='|' read -r source destination; do
|
||||||
case "$client" in
|
|
||||||
codex|both) targets+=("$generated/codex-$platform|$home_path/.codex" "$generated/codex-$platform/skills|$home_path/.agents/skills");;
|
|
||||||
esac
|
|
||||||
case "$client" in
|
|
||||||
claude|both) targets+=("$generated/claude-$platform|$home_path/.claude");;
|
|
||||||
esac
|
|
||||||
|
|
||||||
for target_pair in "${targets[@]}"; do
|
|
||||||
source=${target_pair%%|*}
|
|
||||||
destination=${target_pair#*|}
|
|
||||||
ai_config_root=$destination
|
ai_config_root=$destination
|
||||||
command -v cygpath >/dev/null && ai_config_root=$(cygpath -m "$destination")
|
command -v cygpath >/dev/null && ai_config_root=$(cygpath -m "$destination")
|
||||||
sync_managed_destination "$source" "$destination" "$stamp" "$ai_config_root" "$shell_command" "$windows_shell_command" "$dry_run"
|
sync_managed_destination "$source" "$destination" "$stamp" "$ai_config_root" "$shell_command" "$windows_shell_command" "$dry_run" "$summary"
|
||||||
done
|
done < <(get_install_targets "$generated" "$home_path" "$platform" "$client")
|
||||||
|
|
||||||
install_configured_plugins "$root" "$client" "$dry_run" "$update_plugins"
|
selected_plugins=()
|
||||||
|
deselected_plugins=()
|
||||||
|
select_configured_plugins "$root" "$client" Install "$dry_run" selected_plugins deselected_plugins
|
||||||
|
install_configured_plugins "$root" "$client" "$dry_run" false selected_plugins "$summary"
|
||||||
|
|
||||||
if "$dry_run"; then printf 'PASS install dry-run\n'; else printf 'PASS install\n'; fi
|
if [ "$summary" = true ]; then
|
||||||
|
suffix=
|
||||||
|
[ "$dry_run" = false ] || suffix=', dry-run'
|
||||||
|
printf 'Install: PASS | %s, %s%s\n' "$client" "$platform" "$suffix"
|
||||||
|
elif "$dry_run"; then printf 'PASS install dry-run\n'; else printf 'PASS install\n'; fi
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
function Read-InstallPlatform {
|
||||||
|
param([string]$Platform)
|
||||||
|
if ($Platform) { return $Platform }
|
||||||
|
return Read-InteractiveSingle -Title 'Select shell' -Names @('PowerShell','Bash') -Values @('Windows','Linux')
|
||||||
|
}
|
||||||
|
function Read-InstallClient {
|
||||||
|
param([string]$Client)
|
||||||
|
if ($Client) { return $Client }
|
||||||
|
return Read-InteractiveSingle -Title 'Select client' -Names @('Codex','Claude','Both') -Values @('Codex','Claude','Both')
|
||||||
|
}
|
||||||
|
function Clear-InteractiveScreen { if (-not [Console]::IsInputRedirected -and $env:CI -ne 'true' -and $env:AI_CONFIG_NO_INTERACTIVE -ne '1') { Clear-Host } }
|
||||||
|
function Read-InteractiveSingle {
|
||||||
|
param([string]$Title,[string[]]$Names,[string[]]$Values)
|
||||||
|
if ([Console]::IsInputRedirected -or $env:CI -eq 'true' -or $env:AI_CONFIG_NO_INTERACTIVE -eq '1') {
|
||||||
|
Write-Host $Title; for($i=0;$i -lt $Names.Count;$i++){ Write-Host ('{0}) {1}' -f ($i+1),$Names[$i]) }
|
||||||
|
do { $answer=Read-Host 'Selection' } while($answer -notmatch ('^[1-{0}]$' -f $Names.Count)); return $Values[[int]$answer-1]
|
||||||
|
}
|
||||||
|
$index=0
|
||||||
|
while($true) {
|
||||||
|
Clear-InteractiveScreen; Write-Host $Title
|
||||||
|
for($i=0;$i -lt $Names.Count;$i++){ $prefix=if($i -eq $index){'>'}else{' '}; if($i -eq $index -and $env:NO_COLOR -ne '1'){Write-Host ('{0} {1}' -f $prefix,$Names[$i]) -ForegroundColor Cyan}else{Write-Host ('{0} {1}' -f $prefix,$Names[$i])} }
|
||||||
|
Write-Host 'Up/Down Navigate Enter Confirm'; $key=[Console]::ReadKey($true)
|
||||||
|
if($key.Key -eq 'Up'){ $index=($index+$Names.Count-1)%$Names.Count } elseif($key.Key -eq 'Down'){ $index=($index+1)%$Names.Count } elseif($key.Key -eq 'Enter'){ Clear-InteractiveScreen; return $Values[$index] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function Get-InstallDestinations {
|
||||||
|
param([Parameter(Mandatory=$true)][string]$HomePath,[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client)
|
||||||
|
$destinations=@(); if($Client -in @('Codex','Both')){$destinations+=Join-Path $HomePath '.codex'}; if($Client -in @('Claude','Both')){$destinations+=Join-Path $HomePath '.claude'}; return $destinations
|
||||||
|
}
|
||||||
|
function Get-InstallTargets {
|
||||||
|
param([Parameter(Mandatory=$true)][string]$Generated,[Parameter(Mandatory=$true)][string]$HomePath,[Parameter(Mandatory=$true)][string]$Platform,[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client)
|
||||||
|
$targets=@(); if($Client -in @('Codex','Both')){$targets+=@{Source=(Join-Path $Generated "codex-$Platform");Destination=(Join-Path $HomePath '.codex')};$targets+=@{Source=(Join-Path $Generated "codex-$Platform/skills");Destination=(Join-Path $HomePath '.agents/skills')}}; if($Client -in @('Claude','Both')){$targets+=@{Source=(Join-Path $Generated "claude-$Platform");Destination=(Join-Path $HomePath '.claude')};}; return $targets
|
||||||
|
}
|
||||||
|
function Test-AnyManifestPresent { param([Parameter(Mandatory=$true)][string[]]$Destinations); return [bool](@($Destinations|Where-Object{Test-Path(Get-ManagedManifestPath $_)}).Count) }
|
||||||
|
function Test-AllManifestsPresent { param([Parameter(Mandatory=$true)][string[]]$Destinations); return [bool](@($Destinations|Where-Object{-not(Test-Path(Get-ManagedManifestPath $_))}).Count -eq 0) }
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
read_install_platform() {
|
||||||
|
local platform=$1
|
||||||
|
if [ -n "$platform" ]; then
|
||||||
|
case "$platform" in
|
||||||
|
windows|Windows) printf 'windows\n'; return 0 ;;
|
||||||
|
linux|Linux) printf 'linux\n'; return 0 ;;
|
||||||
|
*) printf 'Unknown --platform value: %s (expected windows or linux)\n' "$platform" >&2; return 1 ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
read_single_selection 'Select shell' 'PowerShell' 'windows' 'Bash' 'linux'
|
||||||
|
}
|
||||||
|
|
||||||
|
read_install_client() {
|
||||||
|
local client=$1
|
||||||
|
if [ -n "$client" ]; then
|
||||||
|
case "$client" in
|
||||||
|
codex|Codex) printf 'codex\n'; return 0 ;;
|
||||||
|
claude|Claude) printf 'claude\n'; return 0 ;;
|
||||||
|
both|Both) printf 'both\n'; return 0 ;;
|
||||||
|
*) printf 'Unknown --client value: %s (expected codex, claude, or both)\n' "$client" >&2; return 1 ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
read_single_selection 'Select client' 'Codex' 'codex' 'Claude' 'claude' 'Both' 'both'
|
||||||
|
}
|
||||||
|
|
||||||
|
clear_interactive() { [ -t 0 ] && [ "${CI:-}" != true ] && [ "${AI_CONFIG_NO_INTERACTIVE:-}" != 1 ] && printf '\033[2J\033[H'; }
|
||||||
|
read_single_selection() {
|
||||||
|
local title=$1; shift; local labels=() values=() label value index=0 key
|
||||||
|
while [ $# -gt 0 ]; do labels+=("$1"); values+=("$2"); shift 2; done
|
||||||
|
if [ ! -t 0 ] || [ "${CI:-}" = true ] || [ "${AI_CONFIG_NO_INTERACTIVE:-}" = 1 ]; then printf '%s\n' "$title" >&2; for ((index=0;index<${#labels[@]};index++)); do printf '%d) %s\n' "$((index+1))" "${labels[$index]}" >&2; done; while read -r -p 'Selection ' value; do [[ "$value" =~ ^[1-9][0-9]*$ ]] && [ "$value" -le "${#labels[@]}" ] && { printf '%s\n' "${values[$((value-1))]}"; return; }; done; return 1; fi
|
||||||
|
local selected=0; while :; do clear_interactive; printf '%s\n' "$title"; for ((index=0;index<${#labels[@]};index++)); do [ "$index" -eq "$selected" ] && printf '> %s\n' "${labels[$index]}" || printf ' %s\n' "${labels[$index]}"; done; printf '↑/↓ Navigate Enter Confirm\n'; IFS= read -rsn1 key || return 1; case "$key" in $'\x1b') read -rsn2 key; case "$key" in '[A') selected=$(( (selected+${#labels[@]}-1)%${#labels[@]} ));; '[B') selected=$(( (selected+1)%${#labels[@]} ));; esac;; '') clear_interactive; printf '%s\n' "${values[$selected]}"; return;; esac; done
|
||||||
|
}
|
||||||
|
|
||||||
|
# get_install_destinations <home> <client> -- prints one destination per line
|
||||||
|
get_install_destinations() {
|
||||||
|
local home_path=$1 client=$2
|
||||||
|
case "$client" in codex|both) printf '%s/.codex\n' "$home_path" ;; esac
|
||||||
|
case "$client" in claude|both) printf '%s/.claude\n' "$home_path" ;; esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# get_install_targets <generated> <home> <platform> <client> -- prints "source|dest" per line
|
||||||
|
get_install_targets() {
|
||||||
|
local generated=$1 home_path=$2 platform=$3 client=$4
|
||||||
|
case "$client" in
|
||||||
|
codex|both)
|
||||||
|
printf '%s/codex-%s|%s/.codex\n' "$generated" "$platform" "$home_path"
|
||||||
|
printf '%s/codex-%s/skills|%s/.agents/skills\n' "$generated" "$platform" "$home_path"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
case "$client" in claude|both) printf '%s/claude-%s|%s/.claude\n' "$generated" "$platform" "$home_path" ;; esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# any_manifest_present <destination>... -- true (0) if at least one has a manifest
|
||||||
|
any_manifest_present() {
|
||||||
|
local destination
|
||||||
|
for destination in "$@"; do
|
||||||
|
[ -f "$(manifest_path "$destination")" ] && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# all_manifests_present <destination>... -- true (0) only if every destination has a manifest
|
||||||
|
all_manifests_present() {
|
||||||
|
local destination
|
||||||
|
for destination in "$@"; do
|
||||||
|
[ -f "$(manifest_path "$destination")" ] || return 1
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
+16
-10
@@ -56,13 +56,15 @@ function Sync-ManagedDestination {
|
|||||||
[string]$AiConfigRoot,
|
[string]$AiConfigRoot,
|
||||||
[string]$ShellCommand,
|
[string]$ShellCommand,
|
||||||
[string]$WindowsShellCommand,
|
[string]$WindowsShellCommand,
|
||||||
[switch]$DryRun
|
[switch]$DryRun,
|
||||||
|
[switch]$Summary
|
||||||
)
|
)
|
||||||
Write-Output "SOURCE $Source -> $Destination"
|
if (-not $Summary) { Write-Output "SOURCE $Source -> $Destination" }
|
||||||
$manifestPath = Get-ManagedManifestPath $Destination
|
$manifestPath = Get-ManagedManifestPath $Destination
|
||||||
$oldManifest = Read-ManagedManifest $manifestPath
|
$oldManifest = Read-ManagedManifest $manifestPath
|
||||||
$newManifest = @{}
|
$newManifest = @{}
|
||||||
$backupRoot = Join-Path $Destination "backups/$Stamp"
|
$backupRoot = Join-Path $Destination "backups/$Stamp"
|
||||||
|
$tally = @{ Created = 0; Updated = 0; Unchanged = 0; Removed = 0; Warned = 0 }
|
||||||
|
|
||||||
Get-ChildItem $Source -File -Recurse | ForEach-Object {
|
Get-ChildItem $Source -File -Recurse | ForEach-Object {
|
||||||
$relative = $_.FullName.Substring($Source.Length).TrimStart([char[]]@('\','/')).Replace('\','/')
|
$relative = $_.FullName.Substring($Source.Length).TrimStart([char[]]@('\','/')).Replace('\','/')
|
||||||
@@ -79,38 +81,42 @@ function Sync-ManagedDestination {
|
|||||||
$newManifest[$relative] = $newHash
|
$newManifest[$relative] = $newHash
|
||||||
|
|
||||||
$exists = Test-Path -LiteralPath $target
|
$exists = Test-Path -LiteralPath $target
|
||||||
if ($exists -and (Get-Sha256Hash $target) -eq $newHash) { Write-Output "UNCHANGED $target"; return }
|
if ($exists -and (Get-Sha256Hash $target) -eq $newHash) { $tally.Unchanged++; if (-not $Summary) { Write-Output "UNCHANGED $target" }; return }
|
||||||
|
|
||||||
$action = if ($exists) { 'UPDATE' } else { 'CREATE' }
|
$action = if ($exists) { 'UPDATE' } else { 'CREATE' }
|
||||||
if ($DryRun) { Write-Output "DRYRUN $action $target"; return }
|
if ($DryRun) { if (-not $Summary) { Write-Output "DRYRUN $action $target" }; if ($action -eq 'CREATE') { $tally.Created++ } else { $tally.Updated++ }; return }
|
||||||
if ($exists) {
|
if ($exists) {
|
||||||
$wasManaged = $oldManifest.ContainsKey($relative)
|
$wasManaged = $oldManifest.ContainsKey($relative)
|
||||||
$backup = Join-Path $backupRoot $relative
|
$backup = Join-Path $backupRoot $relative
|
||||||
New-Item (Split-Path $backup -Parent) -ItemType Directory -Force | Out-Null
|
New-Item (Split-Path $backup -Parent) -ItemType Directory -Force | Out-Null
|
||||||
Copy-Item -LiteralPath $target $backup -Force
|
Copy-Item -LiteralPath $target $backup -Force
|
||||||
Write-Output "BACKUP $target -> $backup"
|
if (-not $Summary) { Write-Output "BACKUP $target -> $backup" }
|
||||||
if (-not $wasManaged) { Write-Output "WARN $target existed before this installation but was not tracked by a previous run; it was backed up before being overwritten." }
|
if (-not $wasManaged) { Write-Output "WARN $target existed before this installation but was not tracked by a previous run; it was backed up before being overwritten."; $tally.Warned++ }
|
||||||
}
|
}
|
||||||
New-Item (Split-Path $target -Parent) -ItemType Directory -Force | Out-Null
|
New-Item (Split-Path $target -Parent) -ItemType Directory -Force | Out-Null
|
||||||
[System.IO.File]::WriteAllBytes($target, $newBytes)
|
[System.IO.File]::WriteAllBytes($target, $newBytes)
|
||||||
Write-Output "$action $target"
|
if (-not $Summary) { Write-Output "$action $target" }
|
||||||
|
if ($action -eq 'CREATE') { $tally.Created++ } else { $tally.Updated++ }
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($relative in @($oldManifest.Keys | Where-Object { -not $newManifest.ContainsKey($_) })) {
|
foreach ($relative in @($oldManifest.Keys | Where-Object { -not $newManifest.ContainsKey($_) })) {
|
||||||
$target = Join-Path $Destination $relative
|
$target = Join-Path $Destination $relative
|
||||||
if (-not (Test-Path -LiteralPath $target)) { continue }
|
if (-not (Test-Path -LiteralPath $target)) { continue }
|
||||||
if ($DryRun) { Write-Output "DRYRUN REMOVE $target"; continue }
|
if ($DryRun) { if (-not $Summary) { Write-Output "DRYRUN REMOVE $target" }; $tally.Removed++; continue }
|
||||||
$backup = Join-Path $backupRoot $relative
|
$backup = Join-Path $backupRoot $relative
|
||||||
New-Item (Split-Path $backup -Parent) -ItemType Directory -Force | Out-Null
|
New-Item (Split-Path $backup -Parent) -ItemType Directory -Force | Out-Null
|
||||||
Copy-Item -LiteralPath $target $backup -Force
|
Copy-Item -LiteralPath $target $backup -Force
|
||||||
Write-Output "BACKUP $target -> $backup"
|
if (-not $Summary) { Write-Output "BACKUP $target -> $backup" }
|
||||||
if ((Get-Sha256Hash $target) -eq $oldManifest[$relative]) {
|
if ((Get-Sha256Hash $target) -eq $oldManifest[$relative]) {
|
||||||
Remove-Item -LiteralPath $target -Force
|
Remove-Item -LiteralPath $target -Force
|
||||||
Write-Output "REMOVE $target"
|
if (-not $Summary) { Write-Output "REMOVE $target" }
|
||||||
|
$tally.Removed++
|
||||||
} else {
|
} else {
|
||||||
Write-Output "WARN $target was managed by a previous installation and has changed locally; it was backed up but left in place instead of being removed."
|
Write-Output "WARN $target was managed by a previous installation and has changed locally; it was backed up but left in place instead of being removed."
|
||||||
|
$tally.Warned++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not $DryRun) { Write-ManagedManifest $manifestPath $newManifest }
|
if (-not $DryRun) { Write-ManagedManifest $manifestPath $newManifest }
|
||||||
|
if ($Summary) { Write-Output ("SYNC {0}: {1} created, {2} updated, {3} unchanged, {4} removed, {5} warnings" -f $Destination, $tally.Created, $tally.Updated, $tally.Unchanged, $tally.Removed, $tally.Warned) }
|
||||||
}
|
}
|
||||||
+27
-10
@@ -49,16 +49,17 @@ write_managed_manifest() {
|
|||||||
} > "$path"
|
} > "$path"
|
||||||
}
|
}
|
||||||
|
|
||||||
# sync_managed_destination <source-dir> <destination-dir> <stamp> <ai-config-root> <shell-command> <windows-shell-command> <dry-run: true|false>
|
# sync_managed_destination <source-dir> <destination-dir> <stamp> <ai-config-root> <shell-command> <windows-shell-command> <dry-run: true|false> [summary: true|false]
|
||||||
sync_managed_destination() {
|
sync_managed_destination() {
|
||||||
local source=$1 destination=$2 stamp=$3 ai_config_root=$4 shell_command=$5 windows_shell_command=$6 dry_run=$7
|
local source=$1 destination=$2 stamp=$3 ai_config_root=$4 shell_command=$5 windows_shell_command=$6 dry_run=$7 summary=${8:-false}
|
||||||
printf 'SOURCE %s -> %s\n' "$source" "$destination"
|
[ "$summary" = true ] || printf 'SOURCE %s -> %s\n' "$source" "$destination"
|
||||||
local manifest
|
local manifest
|
||||||
manifest=$(manifest_path "$destination")
|
manifest=$(manifest_path "$destination")
|
||||||
declare -A old_manifest
|
declare -A old_manifest
|
||||||
read_managed_manifest "$manifest" old_manifest
|
read_managed_manifest "$manifest" old_manifest
|
||||||
declare -A new_manifest
|
declare -A new_manifest
|
||||||
local backup_root="$destination/backups/$stamp"
|
local backup_root="$destination/backups/$stamp"
|
||||||
|
local created=0 updated=0 unchanged=0 removed=0 warned=0
|
||||||
|
|
||||||
while IFS= read -r -d '' source_file; do
|
while IFS= read -r -d '' source_file; do
|
||||||
local relative target content needs_sub new_hash exists current_hash action backup
|
local relative target content needs_sub new_hash exists current_hash action backup
|
||||||
@@ -84,7 +85,8 @@ sync_managed_destination() {
|
|||||||
if [ "$exists" = true ]; then
|
if [ "$exists" = true ]; then
|
||||||
current_hash=$(sha256_of_file "$target")
|
current_hash=$(sha256_of_file "$target")
|
||||||
if [ "$current_hash" = "$new_hash" ]; then
|
if [ "$current_hash" = "$new_hash" ]; then
|
||||||
printf 'UNCHANGED %s\n' "$target"
|
unchanged=$((unchanged + 1))
|
||||||
|
[ "$summary" = true ] || printf 'UNCHANGED %s\n' "$target"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
action=UPDATE
|
action=UPDATE
|
||||||
@@ -92,15 +94,20 @@ sync_managed_destination() {
|
|||||||
action=CREATE
|
action=CREATE
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$dry_run" = true ]; then printf 'DRYRUN %s %s\n' "$action" "$target"; continue; fi
|
if [ "$dry_run" = true ]; then
|
||||||
|
[ "$summary" = true ] || printf 'DRYRUN %s %s\n' "$action" "$target"
|
||||||
|
[ "$action" = CREATE ] && created=$((created + 1)) || updated=$((updated + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$exists" = true ]; then
|
if [ "$exists" = true ]; then
|
||||||
backup="$backup_root/$relative"
|
backup="$backup_root/$relative"
|
||||||
mkdir -p "$(dirname -- "$backup")"
|
mkdir -p "$(dirname -- "$backup")"
|
||||||
cp -- "$target" "$backup"
|
cp -- "$target" "$backup"
|
||||||
printf 'BACKUP %s -> %s\n' "$target" "$backup"
|
[ "$summary" = true ] || printf 'BACKUP %s -> %s\n' "$target" "$backup"
|
||||||
if [ -z "${old_manifest[$relative]+x}" ]; then
|
if [ -z "${old_manifest[$relative]+x}" ]; then
|
||||||
printf 'WARN %s existed before this installation but was not tracked by a previous run; it was backed up before being overwritten.\n' "$target"
|
printf 'WARN %s existed before this installation but was not tracked by a previous run; it was backed up before being overwritten.\n' "$target"
|
||||||
|
warned=$((warned + 1))
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
mkdir -p "$(dirname -- "$target")"
|
mkdir -p "$(dirname -- "$target")"
|
||||||
@@ -109,7 +116,8 @@ sync_managed_destination() {
|
|||||||
else
|
else
|
||||||
cp -- "$source_file" "$target"
|
cp -- "$source_file" "$target"
|
||||||
fi
|
fi
|
||||||
printf '%s %s\n' "$action" "$target"
|
[ "$summary" = true ] || printf '%s %s\n' "$action" "$target"
|
||||||
|
[ "$action" = CREATE ] && created=$((created + 1)) || updated=$((updated + 1))
|
||||||
done < <(find "$source" -type f -print0)
|
done < <(find "$source" -type f -print0)
|
||||||
|
|
||||||
local relative target current_hash backup
|
local relative target current_hash backup
|
||||||
@@ -117,19 +125,28 @@ sync_managed_destination() {
|
|||||||
[ -z "${new_manifest[$relative]+x}" ] || continue
|
[ -z "${new_manifest[$relative]+x}" ] || continue
|
||||||
target="$destination/$relative"
|
target="$destination/$relative"
|
||||||
[ -f "$target" ] || continue
|
[ -f "$target" ] || continue
|
||||||
if [ "$dry_run" = true ]; then printf 'DRYRUN REMOVE %s\n' "$target"; continue; fi
|
if [ "$dry_run" = true ]; then
|
||||||
|
[ "$summary" = true ] || printf 'DRYRUN REMOVE %s\n' "$target"
|
||||||
|
removed=$((removed + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
backup="$backup_root/$relative"
|
backup="$backup_root/$relative"
|
||||||
mkdir -p "$(dirname -- "$backup")"
|
mkdir -p "$(dirname -- "$backup")"
|
||||||
cp -- "$target" "$backup"
|
cp -- "$target" "$backup"
|
||||||
printf 'BACKUP %s -> %s\n' "$target" "$backup"
|
[ "$summary" = true ] || printf 'BACKUP %s -> %s\n' "$target" "$backup"
|
||||||
current_hash=$(sha256_of_file "$target")
|
current_hash=$(sha256_of_file "$target")
|
||||||
if [ "$current_hash" = "${old_manifest[$relative]}" ]; then
|
if [ "$current_hash" = "${old_manifest[$relative]}" ]; then
|
||||||
rm -f -- "$target"
|
rm -f -- "$target"
|
||||||
printf 'REMOVE %s\n' "$target"
|
[ "$summary" = true ] || printf 'REMOVE %s\n' "$target"
|
||||||
|
removed=$((removed + 1))
|
||||||
else
|
else
|
||||||
printf 'WARN %s was managed by a previous installation and has changed locally; it was backed up but left in place instead of being removed.\n' "$target"
|
printf 'WARN %s was managed by a previous installation and has changed locally; it was backed up but left in place instead of being removed.\n' "$target"
|
||||||
|
warned=$((warned + 1))
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ "$dry_run" != true ]; then write_managed_manifest "$manifest" new_manifest; fi
|
if [ "$dry_run" != true ]; then write_managed_manifest "$manifest" new_manifest; fi
|
||||||
|
if [ "$summary" = true ]; then
|
||||||
|
printf 'SYNC %s: %s created, %s updated, %s unchanged, %s removed, %s warnings\n' "$destination" "$created" "$updated" "$unchanged" "$removed" "$warned"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
+132
-21
@@ -2,12 +2,25 @@ function Invoke-PluginCommand {
|
|||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$true)][string]$Command,
|
[Parameter(Mandatory=$true)][string]$Command,
|
||||||
[Parameter(Mandatory=$true)][string[]]$Arguments,
|
[Parameter(Mandatory=$true)][string[]]$Arguments,
|
||||||
[switch]$DryRun
|
[switch]$DryRun,
|
||||||
|
[switch]$Summary
|
||||||
)
|
)
|
||||||
$display = "$Command $($Arguments -join ' ')"
|
$display = "$Command $($Arguments -join ' ')"
|
||||||
if ($DryRun) { Write-Output "DRYRUN $display"; return }
|
if ($DryRun) { if (-not $Summary) { Write-Output "DRYRUN $display" }; return }
|
||||||
& $Command @Arguments
|
$resolvedCommand = Get-Command $Command -ErrorAction Stop
|
||||||
|
if (-not $Summary) {
|
||||||
|
& $resolvedCommand @Arguments
|
||||||
if ($LASTEXITCODE -ne 0) { throw "Plugin command failed: $display" }
|
if ($LASTEXITCODE -ne 0) { throw "Plugin command failed: $display" }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$previousPreference = $ErrorActionPreference
|
||||||
|
try {
|
||||||
|
$ErrorActionPreference = 'Continue'
|
||||||
|
$output = @(& $resolvedCommand @Arguments 2>&1)
|
||||||
|
$exitCode = $LASTEXITCODE
|
||||||
|
} finally { $ErrorActionPreference = $previousPreference }
|
||||||
|
if (-not $Summary -or $env:AI_CONFIG_VERBOSE -eq '1') { $output | Write-Output } elseif ($exitCode -eq 0) { $output | Where-Object { "$_" -match '(?i)warn|error|fail|deprecat' } }
|
||||||
|
if ($exitCode -ne 0) { throw "Plugin command failed: $display" }
|
||||||
}
|
}
|
||||||
|
|
||||||
function Get-ConfiguredPluginEntries {
|
function Get-ConfiguredPluginEntries {
|
||||||
@@ -37,10 +50,11 @@ function Ensure-CodexMarketplace {
|
|||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$true)][string]$Source,
|
[Parameter(Mandatory=$true)][string]$Source,
|
||||||
[Parameter(Mandatory=$true)][string]$MarketplaceName,
|
[Parameter(Mandatory=$true)][string]$MarketplaceName,
|
||||||
[switch]$DryRun
|
[switch]$DryRun,
|
||||||
|
[switch]$Summary
|
||||||
)
|
)
|
||||||
if ($DryRun) {
|
if ($DryRun) {
|
||||||
Write-Output "DRYRUN codex plugin marketplace add $Source"
|
if (-not $Summary) { Write-Output "DRYRUN codex plugin marketplace add $Source" }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
$previousErrorActionPreference = $ErrorActionPreference
|
$previousErrorActionPreference = $ErrorActionPreference
|
||||||
@@ -54,31 +68,119 @@ function Ensure-CodexMarketplace {
|
|||||||
} finally {
|
} finally {
|
||||||
$ErrorActionPreference = $previousErrorActionPreference
|
$ErrorActionPreference = $previousErrorActionPreference
|
||||||
}
|
}
|
||||||
$output | ForEach-Object { Write-Output $_ }
|
if (-not $Summary -or $env:AI_CONFIG_VERBOSE -eq '1') { $output | Write-Output } elseif ($exitCode -eq 0) { $output | Where-Object { "$_" -match '(?i)warn|error|fail|deprecat' } }
|
||||||
if ($exitCode -eq 0) { return }
|
if ($exitCode -eq 0) { return }
|
||||||
|
|
||||||
$text = $output -join [Environment]::NewLine
|
$text = $output -join [Environment]::NewLine
|
||||||
if ($text -match 'marketplace .* already added from a different source') {
|
if ($text -match 'marketplace .* already added from a different source') {
|
||||||
Write-Output "WARN Codex marketplace '$MarketplaceName' exists from another source; replacing it with: $Source"
|
Write-Output "WARN Codex marketplace '$MarketplaceName' exists from another source; replacing it with: $Source"
|
||||||
Invoke-PluginCommand -Command 'codex' -Arguments @('plugin','marketplace','remove',$MarketplaceName)
|
Invoke-PluginCommand -Command 'codex' -Arguments @('plugin','marketplace','remove',$MarketplaceName) -Summary:$Summary
|
||||||
Invoke-PluginCommand -Command 'codex' -Arguments @('plugin','marketplace','add',$Source)
|
Invoke-PluginCommand -Command 'codex' -Arguments @('plugin','marketplace','add',$Source) -Summary:$Summary
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
throw "Plugin command failed: codex plugin marketplace add $Source"
|
throw "Plugin command failed: codex plugin marketplace add $Source"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Read-PluginToggleSelection {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][string[]]$Names,
|
||||||
|
[Parameter(Mandatory=$true)][bool[]]$Checked
|
||||||
|
)
|
||||||
|
$state = @($Checked)
|
||||||
|
if ([Console]::IsInputRedirected -or $env:CI -eq 'true' -or $env:AI_CONFIG_NO_INTERACTIVE -eq '1') { while($true){ Write-Host 'Select plugins (enter a number to toggle, "done" to confirm):'; for($i=0;$i -lt $Names.Count;$i++){Write-Host (" {0}) [{1}] {2}" -f ($i+1),$(if($state[$i]){'x'}else{' '}),$Names[$i])}; $answer=Read-Host 'Toggle number or "done"'; if([string]::IsNullOrWhiteSpace($answer)-or $answer -eq 'done'){return $state}; $index=0;if([int]::TryParse($answer,[ref]$index)-and $index -ge 1 -and $index -le $Names.Count){$state[$index-1]= -not $state[$index-1]} } }
|
||||||
|
$index=0; while($true){ Clear-Host; Write-Host 'Select plugins'; for($i=0;$i -lt $Names.Count;$i++){ $focus=if($i -eq $index){'>'}else{' '}; $mark=if($state[$i]){'x'}else{' '}; Write-Host "$focus [$mark] $($Names[$i])" }; Write-Host '↑/↓ Navigate Space Toggle Enter Confirm'; $key=[Console]::ReadKey($true); if($key.Key -eq 'Up'){$index=($index+$Names.Count-1)%$Names.Count}elseif($key.Key -eq 'Down'){$index=($index+1)%$Names.Count}elseif($key.Key -eq 'Spacebar'){$state[$index]= -not $state[$index]}elseif($key.Key -eq 'Enter'){Clear-Host;return $state} }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Update-CodexMarketplace {
|
||||||
|
param([Parameter(Mandatory=$true)][string]$MarketplaceName,[Parameter(Mandatory=$true)][string]$Source,[switch]$DryRun,[switch]$Summary)
|
||||||
|
try { Invoke-PluginCommand -Command 'codex' -Arguments @('plugin','marketplace','upgrade',$MarketplaceName) -DryRun:$DryRun -Summary:$Summary }
|
||||||
|
catch { Ensure-CodexMarketplace -Source $Source -MarketplaceName $MarketplaceName -DryRun:$DryRun -Summary:$Summary }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Select-ConfiguredPlugins {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Lets the user toggle each configured plugin on or off. Install mode starts
|
||||||
|
with everything checked; update mode starts checked to match what the
|
||||||
|
reference client (Claude, or Codex when only Codex is targeted) already
|
||||||
|
has installed, so unchecking a currently installed plugin uninstalls it.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][string]$RepositoryRoot,
|
||||||
|
[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client,
|
||||||
|
[ValidateSet('Install','Update')][Parameter(Mandatory=$true)][string]$Mode,
|
||||||
|
[switch]$DryRun
|
||||||
|
)
|
||||||
|
$entries = @(Get-ConfiguredPluginEntries -RepositoryRoot $RepositoryRoot)
|
||||||
|
$toggleable = @($entries | Where-Object { $Client -in @('Claude','Both') -or $_.codex_method -eq 'plugin' })
|
||||||
|
$fixed = @($entries | Where-Object { $toggleable -notcontains $_ })
|
||||||
|
if ($DryRun -or $toggleable.Count -eq 0) { return @{ Selected = $entries; Deselected = @() } }
|
||||||
|
|
||||||
|
$referenceClient = if ($Client -eq 'Codex') { 'Codex' } else { 'Claude' }
|
||||||
|
$installed = if ($Mode -eq 'Update') { @(Get-InstalledPlugins -Client $referenceClient) } else { @() }
|
||||||
|
$idField = if ($referenceClient -eq 'Claude') { 'id' } else { 'pluginId' }
|
||||||
|
$checked = @($toggleable | ForEach-Object {
|
||||||
|
if ($Mode -eq 'Install') { $true; return }
|
||||||
|
$selector = if ($referenceClient -eq 'Claude') { $_.claude_plugin } else { $_.codex_plugin }
|
||||||
|
[bool](@($installed | Where-Object { $_.$idField -eq $selector }).Count)
|
||||||
|
})
|
||||||
|
$names = @($toggleable | ForEach-Object { $_.name })
|
||||||
|
$checked = Read-PluginToggleSelection -Names $names -Checked $checked
|
||||||
|
|
||||||
|
$selected = @()
|
||||||
|
$deselected = @()
|
||||||
|
for ($i = 0; $i -lt $toggleable.Count; $i++) {
|
||||||
|
if ($checked[$i]) { $selected += $toggleable[$i] } else { $deselected += $toggleable[$i] }
|
||||||
|
}
|
||||||
|
return @{ Selected = ($selected + $fixed); Deselected = $deselected }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Uninstall-DeselectedPlugins {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][AllowEmptyCollection()][object[]]$Entries,
|
||||||
|
[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client,
|
||||||
|
[switch]$DryRun,
|
||||||
|
[switch]$Summary
|
||||||
|
)
|
||||||
|
if ($Entries.Count -eq 0 -and $PSBoundParameters.ContainsKey('Entries')) { return }
|
||||||
|
$clients = if ($Client -eq 'Both') { @('Claude','Codex') } else { @($Client) }
|
||||||
|
foreach ($selectedClient in $clients) {
|
||||||
|
$installed = if ($DryRun) { @() } else { @(Get-InstalledPlugins -Client $selectedClient) }
|
||||||
|
$idField = if ($selectedClient -eq 'Claude') { 'id' } else { 'pluginId' }
|
||||||
|
$removedCount = 0
|
||||||
|
foreach ($entry in $Entries) {
|
||||||
|
$plugin = if ($selectedClient -eq 'Claude') { $entry.claude_plugin } else { $entry.codex_plugin }
|
||||||
|
if ([string]::IsNullOrWhiteSpace($plugin)) { continue }
|
||||||
|
$isInstalled = [bool](@($installed | Where-Object { $_.$idField -eq $plugin }).Count)
|
||||||
|
if (-not $isInstalled) { continue }
|
||||||
|
if ($selectedClient -eq 'Claude') {
|
||||||
|
Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','uninstall',$plugin,'--scope','user') -DryRun:$DryRun -Summary:$Summary
|
||||||
|
} else {
|
||||||
|
Invoke-PluginCommand -Command 'codex' -Arguments @('plugin','remove',$plugin) -DryRun:$DryRun -Summary:$Summary
|
||||||
|
}
|
||||||
|
if (-not $Summary) { Write-Output "PASS $selectedClient plugin removed: $plugin" }
|
||||||
|
$removedCount++
|
||||||
|
}
|
||||||
|
if ($Summary -and $removedCount -gt 0) { Write-Output "PLUGINS ${selectedClient}: $removedCount removed" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function Install-ConfiguredPlugins {
|
function Install-ConfiguredPlugins {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$true)][string]$RepositoryRoot,
|
[Parameter(Mandatory=$true)][string]$RepositoryRoot,
|
||||||
[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client,
|
[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client,
|
||||||
[switch]$DryRun,
|
[switch]$DryRun,
|
||||||
[switch]$Update
|
[switch]$Update,
|
||||||
|
[switch]$Summary,
|
||||||
|
[object[]]$Entries
|
||||||
)
|
)
|
||||||
$entries = @(Get-ConfiguredPluginEntries -RepositoryRoot $RepositoryRoot)
|
$entries = if ($PSBoundParameters.ContainsKey('Entries')) { $Entries } else { @(Get-ConfiguredPluginEntries -RepositoryRoot $RepositoryRoot) }
|
||||||
|
if ($Entries.Count -eq 0 -and $PSBoundParameters.ContainsKey('Entries')) { return }
|
||||||
$clients = if ($Client -eq 'Both') { @('Claude','Codex') } else { @($Client) }
|
$clients = if ($Client -eq 'Both') { @('Claude','Codex') } else { @($Client) }
|
||||||
|
|
||||||
foreach ($selectedClient in $clients) {
|
foreach ($selectedClient in $clients) {
|
||||||
$installed = if ($DryRun) { @() } else { @(Get-InstalledPlugins -Client $selectedClient) }
|
$installed = if ($DryRun) { @() } else { @(Get-InstalledPlugins -Client $selectedClient) }
|
||||||
|
$tally = @{ Ensured = 0; AlreadyInstalled = 0; Updated = 0 }
|
||||||
foreach ($entry in $entries) {
|
foreach ($entry in $entries) {
|
||||||
$marketplace = if ($selectedClient -eq 'Claude') { $entry.claude_marketplace.Trim() } else { $entry.codex_marketplace.Trim() }
|
$marketplace = if ($selectedClient -eq 'Claude') { $entry.claude_marketplace.Trim() } else { $entry.codex_marketplace.Trim() }
|
||||||
if ($marketplace -eq '-') { $marketplace = '' }
|
if ($marketplace -eq '-') { $marketplace = '' }
|
||||||
@@ -94,40 +196,49 @@ function Install-ConfiguredPlugins {
|
|||||||
} else {
|
} else {
|
||||||
throw "Unknown Codex install method '$($entry.codex_method)' for $($entry.name)."
|
throw "Unknown Codex install method '$($entry.codex_method)' for $($entry.name)."
|
||||||
}
|
}
|
||||||
Invoke-PluginCommand -Command 'npx' -Arguments $arguments -DryRun:$DryRun
|
Invoke-PluginCommand -Command 'npx' -Arguments $arguments -DryRun:$DryRun -Summary:$Summary
|
||||||
Write-Output "PASS Codex skill ensured: $($entry.name)"
|
if (-not $Summary) { Write-Output "PASS Codex skill ensured: $($entry.name)" }
|
||||||
|
$tally.Ensured++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
$installedItem = if ($selectedClient -eq 'Claude') { $installed | Where-Object { $_.id -eq $plugin } | Select-Object -First 1 } else { $installed | Where-Object { $_.pluginId -eq $plugin } | Select-Object -First 1 }
|
$installedItem = if ($selectedClient -eq 'Claude') { $installed | Where-Object { $_.id -eq $plugin } | Select-Object -First 1 } else { $installed | Where-Object { $_.pluginId -eq $plugin } | Select-Object -First 1 }
|
||||||
if ($Update -and $selectedClient -eq 'Claude' -and $null -ne $installedItem) {
|
if ($Update -and $selectedClient -eq 'Claude' -and $null -ne $installedItem) {
|
||||||
Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','update',$plugin) -DryRun:$DryRun
|
Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','update',$plugin) -DryRun:$DryRun -Summary:$Summary
|
||||||
if (-not $installedItem.enabled) {
|
if (-not $installedItem.enabled) {
|
||||||
Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','enable',$plugin) -DryRun:$DryRun
|
Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','enable',$plugin) -DryRun:$DryRun -Summary:$Summary
|
||||||
}
|
}
|
||||||
|
$tally.Updated++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (-not $Update -and $null -ne $installedItem) {
|
if (-not $Update -and $null -ne $installedItem) {
|
||||||
if ($selectedClient -eq 'Claude' -and -not $installedItem.enabled) {
|
if ($selectedClient -eq 'Claude' -and -not $installedItem.enabled) {
|
||||||
Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','enable',$plugin) -DryRun:$DryRun
|
Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','enable',$plugin) -DryRun:$DryRun -Summary:$Summary
|
||||||
}
|
}
|
||||||
Write-Output "PASS $selectedClient plugin already installed: $plugin"
|
if (-not $Summary) { Write-Output "PASS $selectedClient plugin already installed: $plugin" }
|
||||||
|
$tally.AlreadyInstalled++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($selectedClient -eq 'Claude') {
|
if ($selectedClient -eq 'Claude') {
|
||||||
if (-not [string]::IsNullOrWhiteSpace($marketplace)) {
|
if (-not [string]::IsNullOrWhiteSpace($marketplace)) {
|
||||||
Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','marketplace','add',$marketplace) -DryRun:$DryRun
|
Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','marketplace','add',$marketplace) -DryRun:$DryRun -Summary:$Summary
|
||||||
}
|
}
|
||||||
Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','install',$plugin,'--scope','user') -DryRun:$DryRun
|
Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','install',$plugin,'--scope','user') -DryRun:$DryRun -Summary:$Summary
|
||||||
} else {
|
} else {
|
||||||
|
if ($Update -and -not [string]::IsNullOrWhiteSpace($marketplace) -and $marketplace -ne 'openai-curated-remote') {
|
||||||
|
Update-CodexMarketplace -MarketplaceName (($plugin -split '@')[-1]) -Source $marketplace -DryRun:$DryRun -Summary:$Summary
|
||||||
|
}
|
||||||
if (-not [string]::IsNullOrWhiteSpace($marketplace) -and $marketplace -ne 'openai-curated-remote') {
|
if (-not [string]::IsNullOrWhiteSpace($marketplace) -and $marketplace -ne 'openai-curated-remote') {
|
||||||
$marketplaceName = ($plugin -split '@')[-1]
|
$marketplaceName = ($plugin -split '@')[-1]
|
||||||
Ensure-CodexMarketplace -Source $marketplace -MarketplaceName $marketplaceName -DryRun:$DryRun
|
Ensure-CodexMarketplace -Source $marketplace -MarketplaceName $marketplaceName -DryRun:$DryRun -Summary:$Summary
|
||||||
}
|
}
|
||||||
Invoke-PluginCommand -Command 'codex' -Arguments @('plugin','add',$plugin) -DryRun:$DryRun
|
Invoke-PluginCommand -Command 'codex' -Arguments @('plugin','add',$plugin) -DryRun:$DryRun -Summary:$Summary
|
||||||
}
|
}
|
||||||
Write-Output "PASS $selectedClient plugin ensured: $plugin"
|
if (-not $Summary) { Write-Output "PASS $selectedClient plugin ensured: $plugin" }
|
||||||
|
if ($Update -and $null -ne $installedItem) { $tally.Updated++ } else { $tally.Ensured++ }
|
||||||
}
|
}
|
||||||
|
if ($Summary) { Write-Output ("PLUGINS {0}: {1} ensured, {2} already installed, {3} updated" -f $selectedClient, $tally.Ensured, $tally.AlreadyInstalled, $tally.Updated) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+171
-10
@@ -14,16 +14,34 @@ run_plugin_command() {
|
|||||||
local dry_run=$1
|
local dry_run=$1
|
||||||
shift
|
shift
|
||||||
if [ "$dry_run" = true ]; then
|
if [ "$dry_run" = true ]; then
|
||||||
printf 'DRYRUN %s\n' "$*"
|
[ "${summary:-false}" = true ] || printf 'DRYRUN %s\n' "$*"
|
||||||
|
return 0
|
||||||
|
elif [ "${summary:-false}" = true ]; then
|
||||||
|
local output status
|
||||||
|
if output=$("$@"); then
|
||||||
|
printf '%s\n' "$output" | grep -Ei 'warn|error|fail|deprecat' || true
|
||||||
|
else
|
||||||
|
status=$?
|
||||||
|
[ "${AI_CONFIG_VERBOSE:-}" = 1 ] && printf '%s\n' "$output" >&2
|
||||||
|
printf 'Plugin command failed: %s\n' "$*" >&2
|
||||||
|
return "$status"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
"$@"
|
"$@"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update_codex_marketplace() {
|
||||||
|
local dry_run=$1 marketplace_name=$2 source=$3
|
||||||
|
if ! run_plugin_command "$dry_run" codex plugin marketplace upgrade "$marketplace_name"; then
|
||||||
|
ensure_codex_marketplace "$dry_run" "$source" "$marketplace_name"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
ensure_codex_marketplace() {
|
ensure_codex_marketplace() {
|
||||||
local dry_run=$1 source=$2 marketplace_name=$3 output
|
local dry_run=$1 source=$2 marketplace_name=$3 output
|
||||||
if [ "$dry_run" = true ]; then
|
if [ "$dry_run" = true ]; then
|
||||||
printf 'DRYRUN codex plugin marketplace add %s\n' "$source"
|
[ "${summary:-false}" = true ] || printf 'DRYRUN codex plugin marketplace add %s\n' "$source"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
if output=$(codex plugin marketplace add "$source" 2>&1); then
|
if output=$(codex plugin marketplace add "$source" 2>&1); then
|
||||||
@@ -33,18 +51,150 @@ ensure_codex_marketplace() {
|
|||||||
printf '%s\n' "$output" >&2
|
printf '%s\n' "$output" >&2
|
||||||
if printf '%s\n' "$output" | grep -Eiq 'marketplace .* already added from a different source'; then
|
if printf '%s\n' "$output" | grep -Eiq 'marketplace .* already added from a different source'; then
|
||||||
printf "WARN Codex marketplace '%s' exists from another source; replacing it with: %s\n" "$marketplace_name" "$source"
|
printf "WARN Codex marketplace '%s' exists from another source; replacing it with: %s\n" "$marketplace_name" "$source"
|
||||||
codex plugin marketplace remove "$marketplace_name"
|
run_plugin_command false codex plugin marketplace remove "$marketplace_name"
|
||||||
codex plugin marketplace add "$source"
|
run_plugin_command false codex plugin marketplace add "$source"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get_plugin_names() {
|
||||||
|
local root=$1 name rest
|
||||||
|
while IFS=$'\t' read -r name rest; do
|
||||||
|
[ "$name" = name ] && continue
|
||||||
|
[ -n "$name" ] || continue
|
||||||
|
printf '%s\n' "$name"
|
||||||
|
done < "$root/adapters/plugins.tsv"
|
||||||
|
}
|
||||||
|
|
||||||
|
# plugin_field <root> <name> <field> -- field is one of the plugins.tsv column names
|
||||||
|
plugin_field() {
|
||||||
|
local root=$1 target=$2 field=$3
|
||||||
|
local name claude_marketplace claude_plugin codex_marketplace codex_plugin codex_method codex_source codex_skill
|
||||||
|
while IFS=$'\t' read -r name claude_marketplace claude_plugin codex_marketplace codex_plugin codex_method codex_source codex_skill; do
|
||||||
|
[ "$name" = "$target" ] || continue
|
||||||
|
printf '%s\n' "${!field}"
|
||||||
|
return 0
|
||||||
|
done < "$root/adapters/plugins.tsv"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin_toggleable() {
|
||||||
|
local root=$1 name=$2 client=$3
|
||||||
|
case "$client" in claude|both) return 0;; esac
|
||||||
|
[ "$(plugin_field "$root" "$name" codex_method)" = plugin ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# read_plugin_toggle_selection <names-array-name> <checked-array-name>
|
||||||
|
read_plugin_toggle_selection() {
|
||||||
|
local -n names_ref=$1
|
||||||
|
local -n checked_ref=$2
|
||||||
|
local answer i mark index=0 key
|
||||||
|
if [ -t 0 ] && [ "${CI:-}" != true ] && [ "${AI_CONFIG_NO_INTERACTIVE:-}" != 1 ]; then
|
||||||
|
while :; do clear_interactive; printf 'Select plugins\n'; for ((i=0;i<${#names_ref[@]};i++)); do mark=' '; [ "${checked_ref[$i]}" = true ] && mark=x; [ "$i" -eq "$index" ] && printf '> [%s] %s\n' "$mark" "${names_ref[$i]}" || printf ' [%s] %s\n' "$mark" "${names_ref[$i]}"; done; printf '↑/↓ Navigate Space Toggle Enter Confirm\n'; IFS= read -rsn1 key || return 1; case "$key" in $'\x1b') read -rsn2 key; case "$key" in '[A') index=$(( (index+${#names_ref[@]}-1)%${#names_ref[@]} ));; '[B') index=$(( (index+1)%${#names_ref[@]} ));; esac;; ' ') [ "${checked_ref[$index]}" = true ] && checked_ref[$index]=false || checked_ref[$index]=true;; '') clear_interactive; return 0;; esac; done
|
||||||
|
fi
|
||||||
|
while :; do
|
||||||
|
printf 'Select plugins (enter a number to toggle, "done" to confirm):\n'
|
||||||
|
for ((i = 0; i < ${#names_ref[@]}; i++)); do
|
||||||
|
mark=' '
|
||||||
|
[ "${checked_ref[$i]}" = true ] && mark=x
|
||||||
|
printf ' %d) [%s] %s\n' "$((i + 1))" "$mark" "${names_ref[$i]}"
|
||||||
|
done
|
||||||
|
read -r -p 'Toggle number or "done" ' answer || { printf 'Plugin selection cancelled: input closed.\n' >&2; return 1; }
|
||||||
|
{ [ -z "$answer" ] || [ "$answer" = done ]; } && return 0
|
||||||
|
if [[ "$answer" =~ ^[0-9]+$ ]] && [ "$answer" -ge 1 ] && [ "$answer" -le "${#names_ref[@]}" ]; then
|
||||||
|
i=$((10#$answer - 1))
|
||||||
|
if [ "${checked_ref[$i]}" = true ]; then checked_ref[$i]=false; else checked_ref[$i]=true; fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# select_configured_plugins <root> <client> <mode: Install|Update> <dry_run> <selected-array-name> <deselected-array-name>
|
||||||
|
select_configured_plugins() {
|
||||||
|
local root=$1 client=$2 mode=$3 dry_run=$4
|
||||||
|
local -n selected_ref=$5
|
||||||
|
local -n deselected_ref=$6
|
||||||
|
selected_ref=()
|
||||||
|
deselected_ref=()
|
||||||
|
|
||||||
|
local name all_names=() toggle_names=() fixed_names=()
|
||||||
|
while IFS= read -r name; do
|
||||||
|
all_names+=("$name")
|
||||||
|
if plugin_toggleable "$root" "$name" "$client"; then toggle_names+=("$name"); else fixed_names+=("$name"); fi
|
||||||
|
done < <(get_plugin_names "$root")
|
||||||
|
|
||||||
|
if [ "$dry_run" = true ] || [ ${#toggle_names[@]} -eq 0 ]; then
|
||||||
|
selected_ref=("${all_names[@]}")
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local reference_client=claude
|
||||||
|
[ "$client" = codex ] && reference_client=codex
|
||||||
|
|
||||||
|
local checked=() selector
|
||||||
|
for name in "${toggle_names[@]}"; do
|
||||||
|
if [ "$mode" = Install ]; then
|
||||||
|
checked+=(true)
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [ "$reference_client" = claude ]; then selector=$(plugin_field "$root" "$name" claude_plugin); else selector=$(plugin_field "$root" "$name" codex_plugin); fi
|
||||||
|
if plugin_installed "$reference_client" "$selector"; then checked+=(true); else checked+=(false); fi
|
||||||
|
done
|
||||||
|
|
||||||
|
read_plugin_toggle_selection toggle_names checked
|
||||||
|
|
||||||
|
local i
|
||||||
|
for ((i = 0; i < ${#toggle_names[@]}; i++)); do
|
||||||
|
if [ "${checked[$i]}" = true ]; then selected_ref+=("${toggle_names[$i]}"); else deselected_ref+=("${toggle_names[$i]}"); fi
|
||||||
|
done
|
||||||
|
selected_ref+=("${fixed_names[@]}")
|
||||||
|
}
|
||||||
|
|
||||||
|
# uninstall_deselected_plugins <root> <client> <dry_run> <deselected-array-name> [summary]
|
||||||
|
uninstall_deselected_plugins() {
|
||||||
|
local root=$1 client_selection=$2 dry_run=$3
|
||||||
|
local -n names_ref=$4
|
||||||
|
local summary=${5:-false}
|
||||||
|
local clients=()
|
||||||
|
case "$client_selection" in
|
||||||
|
codex) clients=(codex);;
|
||||||
|
claude) clients=(claude);;
|
||||||
|
both) clients=(claude codex);;
|
||||||
|
esac
|
||||||
|
local client name plugin removed
|
||||||
|
for client in "${clients[@]}"; do
|
||||||
|
removed=0
|
||||||
|
for name in "${names_ref[@]}"; do
|
||||||
|
if [ "$client" = claude ]; then plugin=$(plugin_field "$root" "$name" claude_plugin); else plugin=$(plugin_field "$root" "$name" codex_plugin); fi
|
||||||
|
[ -n "$plugin" ] || continue
|
||||||
|
if [ "$dry_run" = false ] && plugin_installed "$client" "$plugin"; then
|
||||||
|
if [ "$client" = claude ]; then
|
||||||
|
run_plugin_command "$dry_run" claude plugin uninstall "$plugin" --scope user
|
||||||
|
else
|
||||||
|
run_plugin_command "$dry_run" codex plugin remove "$plugin"
|
||||||
|
fi
|
||||||
|
[ "$summary" = true ] || printf 'PASS %s plugin removed: %s\n' "$client" "$plugin"
|
||||||
|
removed=$((removed + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$summary" = true ] && [ "$removed" -gt 0 ]; then printf 'PLUGINS %s: %s removed\n' "$client" "$removed"; fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# install_configured_plugins <root> <client> <dry_run> <update> [selected-names-array-name] [summary]
|
||||||
install_configured_plugins() {
|
install_configured_plugins() {
|
||||||
local root=$1 client_selection=$2 dry_run=$3 update=$4
|
local root=$1 client_selection=$2 dry_run=$3 update=$4 selected_name=${5:-} summary=${6:-false}
|
||||||
local manifest="$root/adapters/plugins.tsv"
|
local manifest="$root/adapters/plugins.tsv"
|
||||||
[ -f "$manifest" ] || { printf 'Plugin manifest is missing: %s\n' "$manifest" >&2; return 1; }
|
[ -f "$manifest" ] || { printf 'Plugin manifest is missing: %s\n' "$manifest" >&2; return 1; }
|
||||||
|
|
||||||
|
local -A selected_set=()
|
||||||
|
if [ -n "$selected_name" ]; then
|
||||||
|
local -n selected_names_ref=$selected_name
|
||||||
|
local sn
|
||||||
|
for sn in "${selected_names_ref[@]}"; do selected_set[$sn]=1; done
|
||||||
|
fi
|
||||||
|
local ensured=0 already_installed=0 updated_count=0
|
||||||
|
|
||||||
local clients=()
|
local clients=()
|
||||||
case "$client_selection" in
|
case "$client_selection" in
|
||||||
codex) clients=(codex);;
|
codex) clients=(codex);;
|
||||||
@@ -54,12 +204,14 @@ install_configured_plugins() {
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
local client name claude_marketplace claude_plugin codex_marketplace codex_plugin codex_method codex_source codex_skill marketplace plugin
|
local client name claude_marketplace claude_plugin codex_marketplace codex_plugin codex_method codex_source codex_skill marketplace plugin
|
||||||
|
for client in "${clients[@]}"; do
|
||||||
|
ensured=0 already_installed=0 updated_count=0
|
||||||
while IFS=$'\t' read -r name claude_marketplace claude_plugin codex_marketplace codex_plugin codex_method codex_source codex_skill; do
|
while IFS=$'\t' read -r name claude_marketplace claude_plugin codex_marketplace codex_plugin codex_method codex_source codex_skill; do
|
||||||
[ "$name" = name ] && continue
|
[ "$name" = name ] && continue
|
||||||
[ -n "$name" ] || continue
|
[ -n "$name" ] || continue
|
||||||
|
[ -n "$selected_name" ] && [ -z "${selected_set[$name]+x}" ] && continue
|
||||||
[ "$claude_marketplace" = - ] && claude_marketplace=
|
[ "$claude_marketplace" = - ] && claude_marketplace=
|
||||||
[ "$codex_marketplace" = - ] && codex_marketplace=
|
[ "$codex_marketplace" = - ] && codex_marketplace=
|
||||||
for client in "${clients[@]}"; do
|
|
||||||
if [ "$client" = claude ]; then marketplace=$claude_marketplace; plugin=$claude_plugin; else marketplace=$codex_marketplace; plugin=$codex_plugin; fi
|
if [ "$client" = claude ]; then marketplace=$claude_marketplace; plugin=$claude_plugin; else marketplace=$codex_marketplace; plugin=$codex_plugin; fi
|
||||||
[ -n "$plugin" ] || { printf 'Plugin selector missing for %s: %s\n' "$client" "$name" >&2; return 1; }
|
[ -n "$plugin" ] || { printf 'Plugin selector missing for %s: %s\n' "$client" "$name" >&2; return 1; }
|
||||||
if [ "$client" = codex ] && [ "$codex_method" != plugin ]; then
|
if [ "$client" = codex ] && [ "$codex_method" != plugin ]; then
|
||||||
@@ -76,17 +228,22 @@ install_configured_plugins() {
|
|||||||
;;
|
;;
|
||||||
*) printf 'Unknown Codex install method for %s: %s\n' "$client" "$name" >&2; return 1;;
|
*) printf 'Unknown Codex install method for %s: %s\n' "$client" "$name" >&2; return 1;;
|
||||||
esac
|
esac
|
||||||
printf 'PASS Codex skill ensured: %s\n' "$name"
|
[ "$summary" = true ] || printf 'PASS Codex skill ensured: %s\n' "$name"
|
||||||
|
ensured=$((ensured + 1))
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
if [ "$dry_run" = false ] && plugin_installed "$client" "$plugin"; then
|
if [ "$dry_run" = false ] && plugin_installed "$client" "$plugin"; then
|
||||||
if [ "$update" = true ] && [ "$client" = claude ]; then
|
if [ "$update" = true ] && [ "$client" = claude ]; then
|
||||||
run_plugin_command "$dry_run" claude plugin update "$plugin"
|
run_plugin_command "$dry_run" claude plugin update "$plugin"
|
||||||
run_plugin_command "$dry_run" claude plugin enable "$plugin"
|
run_plugin_command "$dry_run" claude plugin enable "$plugin"
|
||||||
|
updated_count=$((updated_count + 1))
|
||||||
elif [ "$update" = true ] && [ "$client" = codex ]; then
|
elif [ "$update" = true ] && [ "$client" = codex ]; then
|
||||||
|
[ -z "$marketplace" ] || [ "$marketplace" = openai-curated-remote ] || update_codex_marketplace "$dry_run" "${plugin##*@}" "$marketplace"
|
||||||
run_plugin_command "$dry_run" codex plugin add "$plugin"
|
run_plugin_command "$dry_run" codex plugin add "$plugin"
|
||||||
|
updated_count=$((updated_count + 1))
|
||||||
else
|
else
|
||||||
printf 'PASS %s plugin already installed: %s\n' "$client" "$plugin"
|
[ "$summary" = true ] || printf 'PASS %s plugin already installed: %s\n' "$client" "$plugin"
|
||||||
|
already_installed=$((already_installed + 1))
|
||||||
fi
|
fi
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
@@ -100,7 +257,11 @@ install_configured_plugins() {
|
|||||||
fi
|
fi
|
||||||
run_plugin_command "$dry_run" codex plugin add "$plugin"
|
run_plugin_command "$dry_run" codex plugin add "$plugin"
|
||||||
fi
|
fi
|
||||||
printf 'PASS %s plugin ensured: %s\n' "$client" "$plugin"
|
[ "$summary" = true ] || printf 'PASS %s plugin ensured: %s\n' "$client" "$plugin"
|
||||||
done
|
ensured=$((ensured + 1))
|
||||||
done < "$manifest"
|
done < "$manifest"
|
||||||
|
if [ "$summary" = true ]; then
|
||||||
|
printf 'PLUGINS %s: %s ensured, %s already installed, %s updated\n' "$client" "$ensured" "$already_installed" "$updated_count"
|
||||||
|
fi
|
||||||
|
done
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param([switch]$Summary)
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$root = Split-Path $PSScriptRoot -Parent
|
||||||
|
$generated = Join-Path $root 'generated'
|
||||||
|
if (-not (Test-Path (Join-Path $generated 'claude-linux/CLAUDE.md'))) { throw 'Generated output is missing; run build first.' }
|
||||||
|
function Measure-Tree([string]$path) {
|
||||||
|
if (-not (Test-Path $path)) { return 0 }
|
||||||
|
return [int64]((Get-ChildItem $path -File -Recurse | ForEach-Object { $_.Length } | Measure-Object -Sum).Sum)
|
||||||
|
}
|
||||||
|
function Tokens([int64]$bytes) { return [int][Math]::Ceiling($bytes / 4.0) }
|
||||||
|
$claude = (Get-Item (Join-Path $generated 'claude-linux/CLAUDE.md')).Length
|
||||||
|
$codex = (Get-Item (Join-Path $generated 'codex-linux/AGENTS.md')).Length
|
||||||
|
$skills = Measure-Tree (Join-Path $generated 'claude-linux/skills')
|
||||||
|
$triggers = (Get-Item (Join-Path $root 'adapters/claude/rule-skills.tsv')).Length
|
||||||
|
$lazyItem = Get-ChildItem (Join-Path $root 'shared/rules') -File -Recurse | Sort-Object Length -Descending | Select-Object -First 1
|
||||||
|
$lazy = if ($lazyItem) { [int64]$lazyItem.Length } else { 0 }
|
||||||
|
$baseline = Join-Path $root 'adapters/prompt-budget-baseline.json'
|
||||||
|
$old = if (Test-Path $baseline) { Get-Content $baseline -Raw | ConvertFrom-Json } else { $null }
|
||||||
|
$rows = @([pscustomobject]@{Name='Claude permanent context';Bytes=$claude;Tokens=(Tokens $claude)},[pscustomobject]@{Name='Codex permanent context';Bytes=$codex;Tokens=(Tokens $codex)},[pscustomobject]@{Name='Claude skill metadata';Bytes=$skills;Tokens=(Tokens $skills)},[pscustomobject]@{Name='Rule trigger metadata';Bytes=$triggers;Tokens=(Tokens $triggers)},[pscustomobject]@{Name='Largest lazy rule';Bytes=$lazy;Tokens=(Tokens $lazy)})
|
||||||
|
$fail = $false
|
||||||
|
if ($old) { foreach ($row in $rows | Select-Object -First 2) { $previous = [double]$old.($row.Name); if ($previous -gt 0 -and $row.Bytes -gt $previous * 1.15) { $fail = $true } } }
|
||||||
|
if ($Summary) { Write-Output 'Prompt Budget'; $rows | ForEach-Object { Write-Output ('{0,-28} {1,8} bytes {2,6} tokens' -f $_.Name,$_.Bytes,$_.Tokens) } } else { $rows | ForEach-Object { Write-Output ('{0}: {1} bytes / {2} tokens' -f $_.Name,$_.Bytes,$_.Tokens) } }
|
||||||
|
if ($fail) { Write-Error 'Prompt budget: permanent context increased by more than 15 percent.'; exit 1 }
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
summary=false
|
||||||
|
for arg in "$@"; do case "$arg" in --summary) summary=true;; *) printf 'Unknown argument: %s\n' "$arg" >&2; exit 1;; esac; done
|
||||||
|
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
generated="$root/generated"
|
||||||
|
[ -f "$generated/claude-linux/CLAUDE.md" ] || { printf 'Generated output is missing; run build first.\n' >&2; exit 1; }
|
||||||
|
bytes() { wc -c < "$1" | tr -d ' '; }
|
||||||
|
tree_bytes() { find "$1" -type f -printf '%s\n' 2>/dev/null | awk '{s+=$1} END {print s+0}'; }
|
||||||
|
tokens() { awk -v b="$1" 'BEGIN { printf "%d", (b + 3) / 4 }'; }
|
||||||
|
claude=$(bytes "$generated/claude-linux/CLAUDE.md"); codex=$(bytes "$generated/codex-linux/AGENTS.md"); skills=$(tree_bytes "$generated/claude-linux/skills"); triggers=$(bytes "$root/adapters/claude/rule-skills.tsv"); lazy=$(find "$root/shared/rules" -type f -printf '%s %p\n' | sort -nr | awk 'NR==1 {print $1+0}'); fail=false
|
||||||
|
if [ -f "$root/adapters/prompt-budget-baseline.tsv" ]; then while IFS=$'\t' read -r name old; do current=$([ "$name" = Claude ] && printf '%s' "$claude" || printf '%s' "$codex"); awk -v n="$current" -v o="$old" 'BEGIN { exit !(o > 0 && n > o * 1.15) }' && fail=true; done < "$root/adapters/prompt-budget-baseline.tsv"; fi
|
||||||
|
if [ "$summary" = true ]; then printf 'Prompt Budget\nClaude permanent context %8s bytes %6s tokens\nCodex permanent context %8s bytes %6s tokens\nClaude skill metadata %8s bytes %6s tokens\nRule trigger metadata %8s bytes %6s tokens\nLargest lazy rule %8s bytes %6s tokens\n' "$claude" "$(tokens "$claude")" "$codex" "$(tokens "$codex")" "$skills" "$(tokens "$skills")" "$triggers" "$(tokens "$triggers")" "$lazy" "$(tokens "$lazy")"; else printf 'Prompt budget: Claude %s bytes, Codex %s bytes, skills %s bytes, triggers %s bytes, largest lazy rule %s bytes\n' "$claude" "$codex" "$skills" "$triggers" "$lazy"; fi
|
||||||
|
$fail && { printf 'Prompt budget: permanent context increased by more than 15 percent.\n' >&2; exit 1; }
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param([switch]$Force,[switch]$Summary)
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$root = Split-Path $PSScriptRoot -Parent
|
||||||
|
$cacheDir = Join-Path $root '.ai-session'
|
||||||
|
$cachePath = Join-Path $cacheDir 'repository-context.json'
|
||||||
|
$safeRoot = $root.Replace('\','/')
|
||||||
|
$head = (& git -c "safe.directory=$safeRoot" -C $root rev-parse HEAD 2>$null).Trim()
|
||||||
|
$inputs = @('AI-Instructions.md','adapters/plugins.tsv','adapters/claude/rule-skills.tsv','adapters/codex/config/config.toml','adapters/claude/config/settings.json')
|
||||||
|
$inputs += @(Get-ChildItem $root -File -Recurse | Where-Object { $_.FullName -notmatch '\\(generated|\.git|node_modules|bin|obj|vendor|\.ai-session)\\' -and $_.Extension -in @('.sln','.csproj','.fsproj','.ts','.tsx','.json','.toml','.yaml','.yml','.cs','.fs','.py','.sh','.ps1') } | ForEach-Object { $_.FullName.Substring($root.Length + 1) } | Sort-Object)
|
||||||
|
$keyText = $head + "`n" + (($inputs | Sort-Object -Unique | ForEach-Object { $_ + ':' + (Get-FileHash (Join-Path $root $_) -Algorithm SHA256).Hash }) -join "`n")
|
||||||
|
$key = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($keyText)).TrimEnd('=')
|
||||||
|
if (-not $Force -and (Test-Path $cachePath)) { try { $existing = Get-Content $cachePath -Raw | ConvertFrom-Json; if ($existing.invalidationKey -eq $key) { if ($Summary) { Write-Output 'Repository context: unchanged' }; exit 0 } } catch {} }
|
||||||
|
$extensions = @(Get-ChildItem $root -File -Recurse | Where-Object { $_.FullName -notmatch '\\(generated|\.git|node_modules|bin|obj|vendor|\.ai-session)\\' } | Group-Object Extension | Sort-Object Count -Descending)
|
||||||
|
$languages = @(); foreach ($item in $extensions) { switch ($item.Name.ToLowerInvariant()) { '.cs' { $languages += 'csharp' }; '.ts' { $languages += 'typescript' }; '.tsx' { $languages += 'typescript' }; '.py' { $languages += 'python' }; '.ps1' { $languages += 'powershell' }; '.sh' { $languages += 'bash' } } }
|
||||||
|
$languages = @($languages | Sort-Object -Unique)
|
||||||
|
$solutions = @(Get-ChildItem $root -Filter *.sln -File -Recurse | Where-Object { $_.FullName -notmatch '\\(generated|\.git)\\' } | ForEach-Object { $_.BaseName } | Sort-Object -Unique)
|
||||||
|
$tests = @(Get-ChildItem $root -File -Recurse | Where-Object { $_.Name -match '(?i)test' -and $_.FullName -notmatch '\\(generated|\.git|node_modules|bin|obj)\\' } | ForEach-Object { $_.BaseName } | Sort-Object -Unique)
|
||||||
|
$packageManager = if (Test-Path (Join-Path $root 'pnpm-lock.yaml')) { 'pnpm' } elseif (Test-Path (Join-Path $root 'package-lock.json')) { 'npm' } elseif (Test-Path (Join-Path $root 'yarn.lock')) { 'yarn' } else { $null }
|
||||||
|
$data = [ordered]@{ invalidationKey=$key; head=$head; languages=$languages; frameworks=@(); solutions=$solutions; testProjects=$tests; packageManager=$packageManager; buildCommands=[ordered]@{}; generatedAt=(Get-Date).ToUniversalTime().ToString('o') }
|
||||||
|
New-Item $cacheDir -ItemType Directory -Force | Out-Null
|
||||||
|
$data | ConvertTo-Json -Depth 5 | Set-Content $cachePath -Encoding UTF8
|
||||||
|
if ($Summary) { Write-Output ('Repository context: refreshed | {0} languages | {1} solutions | {2} tests' -f $languages.Count,$solutions.Count,$tests.Count) } else { Write-Output $cachePath }
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
force=false; summary=false
|
||||||
|
for arg in "$@"; do case "$arg" in --force) force=true;; --summary) summary=true;; *) printf 'Unknown argument: %s\n' "$arg" >&2; exit 1;; esac; done
|
||||||
|
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd); cache_dir="$root/.ai-session"; cache_path="$cache_dir/repository-context.json"
|
||||||
|
head=$(git -c "safe.directory=$root" -C "$root" rev-parse HEAD 2>/dev/null || true); key_input=$head
|
||||||
|
while IFS= read -r file; do key_input+="\n$file:$(sha256sum "$file" | awk '{print $1}')"; done < <(find "$root" -type f \( -name AI-Instructions.md -o -path '*/adapters/*' \) ! -path '*/generated/*' ! -path '*/.git/*' ! -path '*/.ai-session/*' | sort)
|
||||||
|
key=$(printf '%s' "$key_input" | sha256sum | awk '{print $1}')
|
||||||
|
if [ "$force" = false ] && [ -f "$cache_path" ] && grep -Fq "\"invalidationKey\": \"$key\"" "$cache_path"; then [ "$summary" = true ] && printf 'Repository context: unchanged\n'; exit 0; fi
|
||||||
|
mapfile -t languages < <(find "$root" -type f ! -path '*/generated/*' ! -path '*/.git/*' ! -path '*/node_modules/*' ! -path '*/.ai-session/*' | awk -F. 'NF>1 {print $NF}' | sort -u | awk 'BEGIN{a["cs"]="csharp";a["ts"]="typescript";a["tsx"]="typescript";a["py"]="python";a["ps1"]="powershell";a["sh"]="bash"} a[$1]!=""{print a[$1]}' | sort -u)
|
||||||
|
solutions=$(find "$root" -type f -name '*.sln' ! -path '*/generated/*' -printf '"%f",' | sed 's/,$//'); tests=$(find "$root" -type f ! -path '*/generated/*' ! -path '*/.git/*' -iname '*test*' -printf '"%f",' | sed 's/,$//'); manager=null; [ -f "$root/pnpm-lock.yaml" ] && manager='"pnpm"'; [ -f "$root/package-lock.json" ] && manager='"npm"'; [ -f "$root/yarn.lock" ] && manager='"yarn"'
|
||||||
|
mkdir -p "$cache_dir"; printf '{\n "invalidationKey": "%s",\n "head": "%s",\n "languages": [%s],\n "frameworks": [],\n "solutions": [%s],\n "testProjects": [%s],\n "packageManager": %s,\n "buildCommands": {}\n}\n' "$key" "$head" "$(printf '"%s",' "${languages[@]}" | sed 's/,$//')" "$solutions" "$tests" "$manager" > "$cache_path"
|
||||||
|
if [ "$summary" = true ]; then printf 'Repository context: refreshed | %s languages | %s solutions\n' "${#languages[@]}" "$(find "$root" -type f -name '*.sln' ! -path '*/generated/*' | wc -l)"; else printf '%s\n' "$cache_path"; fi
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param([string]$Goal,[string[]]$ChangedFiles=@(),[string[]]$Verified=@(),[string[]]$Pending=@(),[string[]]$ImportantFindings=@(),[switch]$Pointer)
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$root = Split-Path $PSScriptRoot -Parent; $dir = Join-Path $root '.ai-session'; $path = Join-Path $dir 'state.json'
|
||||||
|
if ($Pointer) { if (Test-Path $path) { Write-Output 'Task state is available in .ai-session/state.json; read it only if needed.' }; exit 0 }
|
||||||
|
function Limit([string[]]$items) { @($items | Where-Object { $_ } | Select-Object -First 12 | ForEach-Object { if ($_.Length -gt 180) { $_.Substring(0,180) } else { $_ } }) }
|
||||||
|
New-Item $dir -ItemType Directory -Force | Out-Null
|
||||||
|
[ordered]@{ goal=if($Goal){$Goal.Substring(0,[Math]::Min(240,$Goal.Length))}else{''}; changedFiles=Limit $ChangedFiles; verified=Limit $Verified; pending=Limit $Pending; importantFindings=Limit $ImportantFindings; updatedAt=(Get-Date).ToUniversalTime().ToString('o') } | ConvertTo-Json -Depth 4 | Set-Content $path -Encoding UTF8
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd); dir="$root/.ai-session"; path="$dir/state.json"
|
||||||
|
if [ "${1:-}" = --pointer ]; then [ -f "$path" ] && printf 'Task state is available in .ai-session/state.json; read it only if needed.\n'; exit 0; fi
|
||||||
|
goal=${AI_TASK_GOAL:-}; goal=${goal:0:240}; goal_json=$(printf '%s' "$goal" | sed 's/[\\]/\\\\/g; s/"/\\"/g'); mkdir -p "$dir"
|
||||||
|
printf '{"goal":"%s","changedFiles":[],"verified":[],"pending":[],"importantFindings":[],"updatedAt":"%s"}\n' "$goal_json" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$path"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param([ValidateSet('record','summary','rotate')][string]$Action='record',[string]$Event,[string]$Agent,[string]$Role,[int]$DurationMs,[int]$ToolCalls,[string]$CommandClass,[int64]$Bytes,[int]$Count)
|
||||||
|
$ErrorActionPreference = 'Stop'; if ($env:AI_CONFIG_TELEMETRY -eq '0') { exit 0 }
|
||||||
|
$root = Split-Path $PSScriptRoot -Parent; $dir = Join-Path $root '.ai-session'; $path = Join-Path $dir 'telemetry.jsonl'; New-Item $dir -ItemType Directory -Force | Out-Null
|
||||||
|
if ($Action -eq 'rotate') { if (Test-Path $path) { Move-Item $path ($path + '.1') -Force }; exit 0 }
|
||||||
|
if ($Action -eq 'summary') { if (-not (Test-Path $path)) { Write-Output 'Telemetry: no events'; exit 0 }; $events=@(Get-Content $path | ForEach-Object { try { $_ | ConvertFrom-Json } catch {} }); $agents=@($events | Where-Object event -eq 'subagent-stop'); Write-Output ('Telemetry: {0} events | {1} subagents' -f $events.Count,$agents.Count); exit 0 }
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Event)) { throw 'Event is required.' }
|
||||||
|
$record=[ordered]@{event=$Event;timestamp=(Get-Date).ToUniversalTime().ToString('o')}; foreach($pair in @(@('agent',$Agent),@('role',$Role),@('durationMs',$DurationMs),@('toolCalls',$ToolCalls),@('commandClass',$CommandClass),@('bytes',$Bytes),@('count',$Count))){if($pair[1]){$record[$pair[0]]=$pair[1]}}; ($record|ConvertTo-Json -Compress)|Add-Content $path; if((Get-Item $path).Length -gt 1048576){Move-Item $path ($path+'.1') -Force}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
[ "${AI_CONFIG_TELEMETRY:-1}" = 0 ] && exit 0
|
||||||
|
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd); dir="$root/.ai-session"; path="$dir/telemetry.jsonl"; mkdir -p "$dir"
|
||||||
|
case "${1:-record}" in
|
||||||
|
rotate) [ -f "$path" ] && mv -f "$path" "$path.1"; exit 0;;
|
||||||
|
summary) [ -f "$path" ] || { printf 'Telemetry: no events\n'; exit 0; }; count=$(wc -l < "$path" | tr -d ' '); agents=$(grep -c '"event":"subagent-stop"' "$path" || true); printf 'Telemetry: %s events | %s subagents\n' "$count" "$agents"; exit 0;;
|
||||||
|
record) event=${2:?event required};; *) printf 'Unknown action: %s\n' "$1" >&2; exit 1;;
|
||||||
|
esac
|
||||||
|
printf '{"event":"%s","timestamp":"%s"}\n' "$event" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$path"; [ "$(wc -c < "$path")" -le 1048576 ] || mv -f "$path" "$path.1"
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param()
|
param([switch]$Summary)
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$root = Split-Path $PSScriptRoot -Parent
|
$root = Split-Path $PSScriptRoot -Parent
|
||||||
$work = Join-Path ([System.IO.Path]::GetTempPath()) ("ai-config-build-validation-" + [Guid]::NewGuid())
|
$work = Join-Path ([System.IO.Path]::GetTempPath()) ("ai-config-build-validation-" + [Guid]::NewGuid())
|
||||||
@@ -48,5 +48,5 @@ Test-BuildFails 'leftover template placeholder' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Remove-Item $work -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item $work -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
Write-Output 'PASS build validation: invalid JSON, duplicate agent, duplicate plugin, and leftover placeholders are all rejected'
|
if ($Summary) { Write-Output 'Tests: PASS | build validation' } else { Write-Output 'PASS build validation: invalid JSON, duplicate agent, duplicate plugin, and leftover placeholders are all rejected' }
|
||||||
exit 0
|
exit 0
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
summary=false
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--summary) summary=true; shift ;;
|
||||||
|
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
work=$(mktemp -d)
|
work=$(mktemp -d)
|
||||||
@@ -45,4 +52,4 @@ mutate_leftover_placeholder() {
|
|||||||
}
|
}
|
||||||
test_build_fails 'leftover template placeholder' mutate_leftover_placeholder
|
test_build_fails 'leftover template placeholder' mutate_leftover_placeholder
|
||||||
|
|
||||||
printf 'PASS build validation: invalid JSON, duplicate agent, duplicate plugin, and leftover placeholders are all rejected\n'
|
if [ "$summary" = true ]; then printf 'Tests: PASS | build validation\n'; else printf 'PASS build validation: invalid JSON, duplicate agent, duplicate plugin, and leftover placeholders are all rejected\n'; fi
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param([switch]$Summary)
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$root = Split-Path $PSScriptRoot -Parent
|
||||||
|
. (Join-Path $PSScriptRoot 'lib/manifest.ps1')
|
||||||
|
. (Join-Path $PSScriptRoot 'lib/install-targets.ps1')
|
||||||
|
|
||||||
|
$work = Join-Path ([System.IO.Path]::GetTempPath()) ("ai-config-install-guard-test-" + [Guid]::NewGuid())
|
||||||
|
try {
|
||||||
|
$installed = Join-Path $work 'installed'
|
||||||
|
$missing = Join-Path $work 'missing'
|
||||||
|
New-Item $installed -ItemType Directory -Force | Out-Null
|
||||||
|
New-Item (Get-ManagedManifestPath $installed) -ItemType File -Force | Out-Null
|
||||||
|
|
||||||
|
if (-not (Test-AnyManifestPresent @($installed, $missing))) { throw 'Expected an installed destination to be detected.' }
|
||||||
|
if (Test-AnyManifestPresent @($missing)) { throw 'A destination without a manifest must not count as installed.' }
|
||||||
|
if (Test-AllManifestsPresent @($installed, $missing)) { throw 'Update guard must fail when any selected destination is not installed.' }
|
||||||
|
if (-not (Test-AllManifestsPresent @($installed))) { throw 'Update guard must pass when every selected destination is installed.' }
|
||||||
|
|
||||||
|
if ($Summary) { Write-Output 'Tests: PASS | install guards' } else { Write-Output 'PASS install guards: already-installed and not-installed detection' }
|
||||||
|
} finally {
|
||||||
|
Remove-Item $work -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
summary=false
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--summary) summary=true; shift ;;
|
||||||
|
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
. "$root/scripts/lib/manifest.sh"
|
||||||
|
. "$root/scripts/lib/install-targets.sh"
|
||||||
|
|
||||||
|
work=$(mktemp -d)
|
||||||
|
trap 'rm -rf -- "$work"' EXIT
|
||||||
|
|
||||||
|
installed="$work/installed"
|
||||||
|
missing="$work/missing"
|
||||||
|
mkdir -p "$installed"
|
||||||
|
: > "$(manifest_path "$installed")"
|
||||||
|
|
||||||
|
any_manifest_present "$installed" "$missing" || { printf 'Expected an installed destination to be detected.\n' >&2; exit 1; }
|
||||||
|
! any_manifest_present "$missing" || { printf 'A destination without a manifest must not count as installed.\n' >&2; exit 1; }
|
||||||
|
! all_manifests_present "$installed" "$missing" || { printf 'Update guard must fail when any selected destination is not installed.\n' >&2; exit 1; }
|
||||||
|
all_manifests_present "$installed" || { printf 'Update guard must pass when every selected destination is installed.\n' >&2; exit 1; }
|
||||||
|
|
||||||
|
if [ "$summary" = true ]; then printf 'Tests: PASS | install guards\n'; else printf 'PASS install guards: already-installed and not-installed detection\n'; fi
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param()
|
param([switch]$Summary)
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$root = Split-Path $PSScriptRoot -Parent
|
$root = Split-Path $PSScriptRoot -Parent
|
||||||
. (Join-Path $PSScriptRoot 'lib/manifest.ps1')
|
. (Join-Path $PSScriptRoot 'lib/manifest.ps1')
|
||||||
@@ -44,7 +44,11 @@ try {
|
|||||||
if (Compare-Object $beforeDry $afterDry) { throw 'A dry run must not change the destination at all.' }
|
if (Compare-Object $beforeDry $afterDry) { throw 'A dry run must not change the destination at all.' }
|
||||||
if (@($outDry | Where-Object { $_ -match 'DRYRUN REMOVE.*a\.md' }).Count -ne 1) { throw "Dry run should report the planned removal without performing it: $($outDry -join '; ')" }
|
if (@($outDry | Where-Object { $_ -match 'DRYRUN REMOVE.*a\.md' }).Count -ne 1) { throw "Dry run should report the planned removal without performing it: $($outDry -join '; ')" }
|
||||||
|
|
||||||
Write-Output 'PASS managed manifest: idempotent, stale removal, modified-file protection, foreign files preserved, dry run side-effect free'
|
$outSummary = @(Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'summary' -Summary)
|
||||||
|
if (@($outSummary | Where-Object { $_ -like 'CREATE*' -or $_ -like 'UPDATE*' -or $_ -like 'UNCHANGED*' -or $_ -like 'BACKUP*' }).Count -ne 0) { throw "Summary mode must not print per-file lines: $($outSummary -join '; ')" }
|
||||||
|
if (@($outSummary | Where-Object { $_ -match '^SYNC .* unchanged' }).Count -ne 1) { throw "Summary mode must print exactly one SYNC tally line: $($outSummary -join '; ')" }
|
||||||
|
|
||||||
|
if ($Summary) { Write-Output 'Tests: PASS | managed install' } else { Write-Output 'PASS managed manifest: idempotent, stale removal, modified-file protection, foreign files preserved, dry run side-effect free' }
|
||||||
} finally {
|
} finally {
|
||||||
Remove-Item $work -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item $work -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
summary=false
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--summary) summary=true; shift ;;
|
||||||
|
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
. "$root/scripts/lib/manifest.sh"
|
. "$root/scripts/lib/manifest.sh"
|
||||||
@@ -44,4 +51,14 @@ after_dry=$(find "$destination" -type f | sort)
|
|||||||
[ "$before_dry" = "$after_dry" ] || { printf 'A dry run must not change the destination at all.\n' >&2; exit 1; }
|
[ "$before_dry" = "$after_dry" ] || { printf 'A dry run must not change the destination at all.\n' >&2; exit 1; }
|
||||||
printf '%s\n' "$out_dry" | grep -q '^DRYRUN REMOVE .*a\.md$' || { printf 'Dry run should report the planned removal without performing it:\n%s\n' "$out_dry" >&2; exit 1; }
|
printf '%s\n' "$out_dry" | grep -q '^DRYRUN REMOVE .*a\.md$' || { printf 'Dry run should report the planned removal without performing it:\n%s\n' "$out_dry" >&2; exit 1; }
|
||||||
|
|
||||||
printf 'PASS managed manifest: idempotent, stale removal, modified-file protection, foreign files preserved, dry run side-effect free\n'
|
out_summary=$(sync_managed_destination "$source_dir" "$destination" "summary" "" "" "" false true)
|
||||||
|
if printf '%s\n' "$out_summary" | grep -Eq '^(CREATE|UPDATE|UNCHANGED|BACKUP) '; then
|
||||||
|
printf 'Summary mode must not print per-file lines: %s\n' "$out_summary" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "$(printf '%s\n' "$out_summary" | grep -c '^SYNC .* unchanged')" -ne 1 ]; then
|
||||||
|
printf 'Summary mode must print exactly one SYNC tally line: %s\n' "$out_summary" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$summary" = true ]; then printf 'Tests: PASS | managed install\n'; else printf 'PASS managed manifest: idempotent, stale removal, modified-file protection, foreign files preserved, dry run side-effect free\n'; fi
|
||||||
@@ -1,12 +1,19 @@
|
|||||||
$ErrorActionPreference = 'Stop'
|
[CmdletBinding()]
|
||||||
|
param([switch]$Summary)
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
$root = Split-Path $PSScriptRoot -Parent
|
$root = Split-Path $PSScriptRoot -Parent
|
||||||
$powerShellHook = Get-Content -LiteralPath (Join-Path $root 'shared/hooks/flashbang.ps1') -Raw
|
$powerShellHook = Get-Content -LiteralPath (Join-Path $root 'shared/hooks/flashbang.ps1') -Raw
|
||||||
$bashHook = Get-Content -LiteralPath (Join-Path $root 'shared/hooks/flashbang.sh') -Raw
|
$bashHook = Get-Content -LiteralPath (Join-Path $root 'shared/hooks/flashbang.sh') -Raw
|
||||||
if ($powerShellHook -notmatch '\$HoldMs\s*=\s*250' -or $powerShellHook -notmatch '\$FadeMs\s*=\s*250' -or ([regex]::Matches($bashHook, 'default=250').Count -lt 2)) { throw 'Flashbang defaults must total 500 milliseconds on Windows and Linux.' }
|
if ($powerShellHook -notmatch '\$HoldMs\s*=\s*250' -or $powerShellHook -notmatch '\$FadeMs\s*=\s*250' -or ([regex]::Matches($bashHook, 'default=250').Count -lt 2)) { throw 'Flashbang defaults must total 500 milliseconds on Windows and Linux.' }
|
||||||
$statusInput = '{"model":{"display_name":"Test Model"},"effort":{"level":"high"},"workspace":{"current_dir":"' + $root.Replace('\','\\') + '","repo":{"name":"TestRepo"}},"context_window":{"context_window_size":200000,"used_percentage":8.5,"total_input_tokens":15500,"total_output_tokens":1200}}'
|
$statusInput = '{"model":{"display_name":"Test Model"},"effort":{"level":"high"},"workspace":{"current_dir":"' + $root.Replace('\','\\') + '","repo":{"name":"TestRepo"}},"context_window":{"context_window_size":200000,"used_percentage":8.5,"total_input_tokens":15500,"total_output_tokens":1200}}'
|
||||||
$branch = & git -C $root branch --show-current
|
$branch = & git -C $root branch --show-current
|
||||||
$statusOutput = $statusInput | & (Join-Path $root 'shared/statusline/statusline.ps1')
|
$buildSummaryOutput = & (Join-Path $root 'scripts/build.ps1') -Summary
|
||||||
if ($statusOutput -ne "Model: Test Model | Effort: high | Repo: TestRepo | Branch: $branch | Max Context: 200000 | Used Context: 9% | Used Tokens: 16700") { throw 'PowerShell status line output is incorrect.' }
|
if (@($buildSummaryOutput | Where-Object { $_ -eq 'Build: PASS | 4 packages' }).Count -ne 1) { throw "build.ps1 -Summary must still print the PASS line: $($buildSummaryOutput -join '; ')" }
|
||||||
|
$statusOutput = ($statusInput | & (Join-Path $root 'shared/statusline/statusline.ps1')) -join "`n"
|
||||||
|
$plainStatusOutput = [regex]::Replace($statusOutput, [char]27 + '\[[0-9;]*m', '')
|
||||||
|
$expectedStatusOutput = "Test Model $([char]0x00B7) high`nTestRepo on $branch $([char]0x00B7) 9% ctx (16.7k tok)"
|
||||||
|
if ($buildSummaryOutput.Count -ne 1) { throw 'Build summary must contain only one success line.' }
|
||||||
|
if ($plainStatusOutput -ne $expectedStatusOutput) { throw 'PowerShell status line output is incorrect.' }
|
||||||
$sourceAgentCount = @(Get-ChildItem (Join-Path $root 'shared/agents') -Directory).Count
|
$sourceAgentCount = @(Get-ChildItem (Join-Path $root 'shared/agents') -Directory).Count
|
||||||
$sourceSkillCount = @(Get-ChildItem (Join-Path $root 'shared/skills') -Filter 'SKILL.md' -Recurse).Count
|
$sourceSkillCount = @(Get-ChildItem (Join-Path $root 'shared/skills') -Filter 'SKILL.md' -Recurse).Count
|
||||||
$ruleSkillCount = @(Import-Csv -LiteralPath (Join-Path $root 'adapters/claude/rule-skills.tsv') -Delimiter ([char]9)).Count
|
$ruleSkillCount = @(Import-Csv -LiteralPath (Join-Path $root 'adapters/claude/rule-skills.tsv') -Delimiter ([char]9)).Count
|
||||||
@@ -34,6 +41,15 @@ foreach ($platform in @('windows','linux')) {
|
|||||||
} elseif (-not (Test-Path -LiteralPath (Join-Path $package "statusline/statusline.$(if ($platform -eq 'windows') { 'ps1' } else { 'sh' })")) -or $settings.statusLine.command -notmatch "statusline\.$(if ($platform -eq 'windows') { 'ps1' } else { 'sh' })") {
|
} elseif (-not (Test-Path -LiteralPath (Join-Path $package "statusline/statusline.$(if ($platform -eq 'windows') { 'ps1' } else { 'sh' })")) -or $settings.statusLine.command -notmatch "statusline\.$(if ($platform -eq 'windows') { 'ps1' } else { 'sh' })") {
|
||||||
throw "Claude status line is incorrect in $package"
|
throw "Claude status line is incorrect in $package"
|
||||||
}
|
}
|
||||||
|
$docFile = if ($client -eq 'codex') { 'AGENTS.md' } else { 'CLAUDE.md' }
|
||||||
|
$docContent = Get-Content -LiteralPath (Join-Path $package $docFile) -Raw
|
||||||
|
if (([regex]::Matches($docContent, [regex]::Escape('Apply instructions in this order'))).Count -ne 1) { throw "$client-$platform must embed the priority rule exactly once" }
|
||||||
|
if ($client -eq 'codex' -and $docContent -match 'Always load and apply `rules/general\.md`') { throw "$package must not still instruct loading general.md by path" }
|
||||||
|
if ($client -eq 'claude' -and (Test-Path (Join-Path $package 'rules/general.md'))) { throw 'Claude must not automatically load a second copy of general rules.' }
|
||||||
|
foreach ($splitRule in @('angular','wpf','ui-ux')) {
|
||||||
|
$referencesDir = if ($client -eq 'codex') { Join-Path $package "rules/$splitRule/references" } else { Join-Path $package "skills/rules/rules-$splitRule/references" }
|
||||||
|
if (-not (Test-Path $referencesDir) -or (Get-ChildItem $referencesDir -Filter '*.md').Count -eq 0) { throw "$package is missing reference files for $splitRule" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
foreach ($platformSelection in @('1','2')) {
|
foreach ($platformSelection in @('1','2')) {
|
||||||
@@ -42,12 +58,21 @@ foreach ($platformSelection in @('1','2')) {
|
|||||||
$script:answers.Enqueue($platformSelection)
|
$script:answers.Enqueue($platformSelection)
|
||||||
$script:answers.Enqueue($clientSelection)
|
$script:answers.Enqueue($clientSelection)
|
||||||
function Read-Host { param($Prompt) $answers.Dequeue() }
|
function Read-Host { param($Prompt) $answers.Dequeue() }
|
||||||
$output = & "$PSScriptRoot/install.ps1" -DryRun
|
$output = & "$PSScriptRoot/install.ps1" -DryRun 6>$null
|
||||||
if ($output -notcontains 'PASS install dry-run') { throw 'Dry-run did not finish.' }
|
if ($output -notcontains 'PASS install dry-run') { throw 'Dry-run did not finish.' }
|
||||||
$platform = if ($platformSelection -eq '1') { 'windows' } else { 'linux' }
|
$platform = if ($platformSelection -eq '1') { 'windows' } else { 'linux' }
|
||||||
if (($output -join "`n") -notmatch "-$platform") { throw 'Incorrect selected package.' }
|
if (($output -join "`n") -notmatch "-$platform") { throw 'Incorrect selected package.' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Write-Output 'PASS four platform packages and six PowerShell selections'
|
$doctorSummaryOutput = & (Join-Path $root 'scripts/doctor.ps1') -Summary
|
||||||
|
if (@($doctorSummaryOutput | Where-Object { $_ -match '^PASS ' }).Count -ne 0) { throw 'Doctor summary mode must not print individual PASS lines.' }
|
||||||
|
if (@($doctorSummaryOutput | Where-Object { $_ -match '^Doctor: PASS \| \d+ checks$' }).Count -ne 1) { throw "Doctor summary mode must print one 'Doctor: PASS | N checks' line: $($doctorSummaryOutput -join '; ')" }
|
||||||
|
foreach ($entryPoint in @('install','update')) {
|
||||||
|
$preview = @(& "$PSScriptRoot/$entryPoint.ps1" -Summary -DryRun -Client Both -Platform Windows)
|
||||||
|
if ($preview -match '^(SOURCE|CREATE|UPDATE|UNCHANGED|BACKUP|DRYRUN|PASS) ') { throw "$entryPoint summary leaked per-item output." }
|
||||||
|
if (@($preview | Where-Object { $_ -match ('^' + $entryPoint + ': PASS') }).Count -ne 1) { throw "$entryPoint summary is missing." }
|
||||||
|
}
|
||||||
|
$doctorSummaryOutput | Where-Object { $_ -match '^WARN ' }
|
||||||
|
if ($Summary) { Write-Output 'Tests: PASS | platform packages' } else { Write-Output 'PASS four platform packages and six PowerShell selections' }
|
||||||
exit 0
|
exit 0
|
||||||
|
|
||||||
@@ -1,12 +1,27 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
summary=false
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--summary) summary=true; shift ;;
|
||||||
|
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
grep -Eq '\$HoldMs[[:space:]]*=[[:space:]]*250' "$root/shared/hooks/flashbang.ps1"
|
grep -Eq '\$HoldMs[[:space:]]*=[[:space:]]*250' "$root/shared/hooks/flashbang.ps1"
|
||||||
grep -Eq '\$FadeMs[[:space:]]*=[[:space:]]*250' "$root/shared/hooks/flashbang.ps1"
|
grep -Eq '\$FadeMs[[:space:]]*=[[:space:]]*250' "$root/shared/hooks/flashbang.ps1"
|
||||||
[ "$(grep -c 'default=250' "$root/shared/hooks/flashbang.sh")" -ge 2 ]
|
[ "$(grep -c 'default=250' "$root/shared/hooks/flashbang.sh")" -ge 2 ]
|
||||||
branch=$(git -C "$root" branch --show-current)
|
branch=$(git -C "$root" branch --show-current)
|
||||||
|
build_summary_output=$(bash "$root/scripts/build.sh" --summary)
|
||||||
|
if ! printf '%s\n' "$build_summary_output" | grep -qx 'Build: PASS | 4 packages'; then
|
||||||
|
printf 'build.sh --summary must still print the PASS line: %s\n' "$build_summary_output" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
[ "$build_summary_output" = 'Build: PASS | 4 packages' ]
|
||||||
status_output=$(printf '{"model":{"display_name":"Test Model"},"effort":{"level":"high"},"workspace":{"current_dir":"%s","repo":{"name":"TestRepo"}},"context_window":{"context_window_size":200000,"used_percentage":8.5,"total_input_tokens":15500,"total_output_tokens":1200}}' "$root" | bash "$root/shared/statusline/statusline.sh")
|
status_output=$(printf '{"model":{"display_name":"Test Model"},"effort":{"level":"high"},"workspace":{"current_dir":"%s","repo":{"name":"TestRepo"}},"context_window":{"context_window_size":200000,"used_percentage":8.5,"total_input_tokens":15500,"total_output_tokens":1200}}' "$root" | bash "$root/shared/statusline/statusline.sh")
|
||||||
[ "$status_output" = "Model: Test Model | Effort: high | Repo: TestRepo | Branch: $branch | Max Context: 200000 | Used Context: 9% | Used Tokens: 16700" ]
|
plain_status_output=$(printf '%s' "$status_output" | sed -E $'s/\x1b\\[[0-9;]*m//g')
|
||||||
|
expected_status_output=$'Test Model \xc2\xb7 high\nTestRepo on '"$branch"$' \xc2\xb7 9% ctx (16.7k tok)'
|
||||||
|
[ "$plain_status_output" = "$expected_status_output" ]
|
||||||
source_agent_count=$(find "$root/shared/agents" -mindepth 1 -maxdepth 1 -type d | wc -l)
|
source_agent_count=$(find "$root/shared/agents" -mindepth 1 -maxdepth 1 -type d | wc -l)
|
||||||
source_skill_count=$(find "$root/shared/skills" -name SKILL.md | wc -l)
|
source_skill_count=$(find "$root/shared/skills" -name SKILL.md | wc -l)
|
||||||
rule_skill_count=$(($(wc -l < "$root/adapters/claude/rule-skills.tsv") - 1))
|
rule_skill_count=$(($(wc -l < "$root/adapters/claude/rule-skills.tsv") - 1))
|
||||||
@@ -43,16 +58,59 @@ for platform in windows linux; do
|
|||||||
grep -q "statusline.$statusline_extension" "$package/$file"
|
grep -q "statusline.$statusline_extension" "$package/$file"
|
||||||
fi
|
fi
|
||||||
if grep -Eq '__HOOK_|__WINDOWS_HOOK_' "$package/$file"; then exit 1; fi
|
if grep -Eq '__HOOK_|__WINDOWS_HOOK_' "$package/$file"; then exit 1; fi
|
||||||
|
doc_file=AGENTS.md
|
||||||
|
[ "$client" != claude ] || doc_file=CLAUDE.md
|
||||||
|
doc_content=$(cat "$package/$doc_file")
|
||||||
|
occurrences=$(printf '%s' "$doc_content" | grep -o 'Apply instructions in this order' | wc -l)
|
||||||
|
[ "$occurrences" -eq 1 ] || { printf '%s-%s must embed the priority rule exactly once\n' "$client" "$platform" >&2; exit 1; }
|
||||||
|
if [ "$client" = codex ] && printf '%s' "$doc_content" | grep -q 'Always load and apply `rules/general\.md`'; then
|
||||||
|
printf '%s must not still instruct loading general.md by path\n' "$package" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "$client" = claude ] && [ -f "$package/rules/general.md" ]; then
|
||||||
|
printf 'Claude must not automatically load a second copy of general rules.\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
for split_rule in angular wpf ui-ux; do
|
||||||
|
if [ "$client" = codex ]; then
|
||||||
|
references_dir="$package/rules/$split_rule/references"
|
||||||
|
else
|
||||||
|
references_dir="$package/skills/rules/rules-$split_rule/references"
|
||||||
|
fi
|
||||||
|
if [ ! -d "$references_dir" ] || [ -z "$(find "$references_dir" -maxdepth 1 -name '*.md' -print -quit)" ]; then
|
||||||
|
printf '%s is missing reference files for %s\n' "$package" "$split_rule" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
for platform_selection in 1 2; do
|
for platform_selection in 1 2; do
|
||||||
for client_selection in 1 2 3; do
|
for client_selection in 1 2 3; do
|
||||||
output=$(printf '%s\n%s\n' "$platform_selection" "$client_selection" | bash "$root/scripts/install.sh" --dry-run)
|
output=$(printf '%s\n%s\n' "$platform_selection" "$client_selection" | bash "$root/scripts/install.sh" --dry-run 2>&1) || { printf '%s\n' "$output" >&2; exit 1; }
|
||||||
|
printf '%s\n' "$output" | grep -E 'WARN|FAIL|WARNING|ERROR' || true
|
||||||
[[ "$output" == *'PASS install dry-run'* ]]
|
[[ "$output" == *'PASS install dry-run'* ]]
|
||||||
platform=windows
|
platform=windows
|
||||||
[ "$platform_selection" != 2 ] || platform=linux
|
[ "$platform_selection" != 2 ] || platform=linux
|
||||||
[[ "$output" == *"-$platform"* ]]
|
[[ "$output" == *"-$platform"* ]]
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
printf 'PASS four platform packages and six Bash selections\n'
|
doctor_summary_output=$(bash "$root/scripts/doctor.sh" --summary)
|
||||||
|
if printf '%s\n' "$doctor_summary_output" | grep -q '^PASS '; then
|
||||||
|
printf 'Doctor summary mode must not print individual PASS lines.\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! printf '%s\n' "$doctor_summary_output" | grep -Eq '^Doctor: PASS \| [0-9]+ checks$'; then
|
||||||
|
printf "Doctor summary mode must print one 'Doctor: PASS | N checks' line: %s\n" "$doctor_summary_output" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
for entry_point in install update; do
|
||||||
|
preview=$(bash "$root/scripts/$entry_point.sh" --summary --dry-run --client both --platform linux)
|
||||||
|
if printf '%s\n' "$preview" | grep -Eq '^(SOURCE|CREATE|UPDATE|UNCHANGED|BACKUP|DRYRUN|PASS) '; then
|
||||||
|
printf '%s summary leaked per-item output.\n' "$entry_point" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf '%s\n' "$preview" | grep -Eiq "^$entry_point: PASS"
|
||||||
|
done
|
||||||
|
printf '%s\n' "$doctor_summary_output" | grep '^WARN ' || true
|
||||||
|
if [ "$summary" = true ]; then printf 'Tests: PASS | platform packages\n'; else printf 'PASS four platform packages and six Bash selections\n'; fi
|
||||||
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param([switch]$Summary)
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
function Write-Host { param($Object) if (-not $Summary) { Microsoft.PowerShell.Utility\Write-Host $Object } }
|
||||||
|
. (Join-Path $PSScriptRoot 'lib/plugins.ps1')
|
||||||
|
|
||||||
|
$script:answers = [System.Collections.Generic.Queue[string]]::new()
|
||||||
|
$script:answers.Enqueue('1')
|
||||||
|
$script:answers.Enqueue('2')
|
||||||
|
$script:answers.Enqueue('done')
|
||||||
|
function Read-Host { param($Prompt) $answers.Dequeue() }
|
||||||
|
|
||||||
|
$result = Read-PluginToggleSelection -Names @('A','B','C') -Checked @($true, $false, $true)
|
||||||
|
if ($result[0] -ne $false) { throw 'Toggling checked entry 1 must uncheck it.' }
|
||||||
|
if ($result[1] -ne $true) { throw 'Toggling unchecked entry 2 must check it.' }
|
||||||
|
if ($result[2] -ne $true) { throw 'Entry 3 was never toggled and must stay unchanged.' }
|
||||||
|
|
||||||
|
$script:answers.Enqueue('')
|
||||||
|
$confirmed = @(Read-PluginToggleSelection -Names @('A') -Checked @($true))
|
||||||
|
if ($confirmed.Count -ne 1 -or -not $confirmed[0]) { throw 'Enter must confirm without changing the selection.' }
|
||||||
|
function Test-PluginOutput {
|
||||||
|
param([string]$Mode)
|
||||||
|
Write-Output 'download progress'
|
||||||
|
Write-Output 'WARN test warning'
|
||||||
|
$global:LASTEXITCODE = if ($Mode -eq 'fail') { 7 } else { 0 }
|
||||||
|
}
|
||||||
|
$compact = @(Invoke-PluginCommand -Command Test-PluginOutput -Arguments @('success') -Summary)
|
||||||
|
if ($compact.Count -ne 1 -or $compact[0] -ne 'WARN test warning') { throw 'Plugin summary must retain warnings and suppress progress.' }
|
||||||
|
$detailed = @(Invoke-PluginCommand -Command Test-PluginOutput -Arguments @('success'))
|
||||||
|
if ($detailed.Count -ne 2 -or $detailed[0] -ne 'download progress') { throw 'Detailed output must retain progress.' }
|
||||||
|
$diagnostics = [System.Collections.Generic.List[string]]::new()
|
||||||
|
$failed = $false
|
||||||
|
$env:AI_CONFIG_VERBOSE = '1'
|
||||||
|
try { Invoke-PluginCommand -Command Test-PluginOutput -Arguments @('fail') -Summary | ForEach-Object { $diagnostics.Add("$_") } }
|
||||||
|
catch { $failed = $true }
|
||||||
|
$env:AI_CONFIG_VERBOSE = $null
|
||||||
|
if (-not $failed -or $diagnostics -notcontains 'download progress' -or $diagnostics -notcontains 'WARN test warning') { throw 'Plugin failure must retain full diagnostics and fail.' }
|
||||||
|
$global:LASTEXITCODE = 0
|
||||||
|
function Get-InstalledPlugins { [pscustomobject]@{ pluginId = 'test@market' } }
|
||||||
|
$script:commands = @()
|
||||||
|
function Invoke-PluginCommand { param($Command, $Arguments, [switch]$DryRun, [switch]$Summary) $script:commands += "$Command $($Arguments -join ' ')" }
|
||||||
|
$entry = [pscustomobject]@{ name = 'Test'; codex_method = 'plugin'; codex_marketplace = '-'; codex_plugin = 'test@market' }
|
||||||
|
$updated = @(Install-ConfiguredPlugins -RepositoryRoot (Split-Path $PSScriptRoot -Parent) -Client Codex -Update -Summary -Entries @($entry))
|
||||||
|
if ($commands.Count -ne 1 -or $commands[0] -ne 'codex plugin add test@market' -or $updated[0] -ne 'PLUGINS Codex: 0 ensured, 0 already installed, 1 updated') { throw 'Updating an installed Codex plugin must report one update.' }
|
||||||
|
function Get-InstalledPlugins { throw 'Empty selection must not query installed plugins.' }
|
||||||
|
function Invoke-PluginCommand { throw 'Empty selection must not run plugin commands.' }
|
||||||
|
Install-ConfiguredPlugins -RepositoryRoot (Split-Path $PSScriptRoot -Parent) -Client Both -Entries @()
|
||||||
|
Uninstall-DeselectedPlugins -Client Both -Entries @()
|
||||||
|
|
||||||
|
if ($Summary) { Write-Output 'Tests: PASS | plugin selection' } else { Write-Output 'PASS plugin toggle selection: toggle and confirm' }
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
summary=false
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--summary) summary=true; shift ;;
|
||||||
|
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
. "$root/scripts/lib/plugins.sh"
|
||||||
|
|
||||||
|
names=(A B C)
|
||||||
|
checked=(true false true)
|
||||||
|
toggle_log=$(mktemp)
|
||||||
|
trap 'rm -f -- "$toggle_log"' EXIT
|
||||||
|
read_plugin_toggle_selection names checked <<< $'1\n2\ndone' > "$toggle_log"
|
||||||
|
[ "$summary" = true ] || cat "$toggle_log"
|
||||||
|
|
||||||
|
[ "${checked[0]}" = false ] || { printf 'Toggling checked entry 1 must uncheck it.\n' >&2; exit 1; }
|
||||||
|
[ "${checked[1]}" = true ] || { printf 'Toggling unchecked entry 2 must check it.\n' >&2; exit 1; }
|
||||||
|
[ "${checked[2]}" = true ] || { printf 'Entry 3 was never toggled and must stay unchanged.\n' >&2; exit 1; }
|
||||||
|
|
||||||
|
read_plugin_toggle_selection names checked <<< '' > "$toggle_log"
|
||||||
|
[ "${checked[0]}" = false ] && [ "${checked[1]}" = true ]
|
||||||
|
test_plugin_output() {
|
||||||
|
printf 'download progress\nWARN test warning\n'
|
||||||
|
[ "$1" != fail ]
|
||||||
|
}
|
||||||
|
compact=$(summary=true run_plugin_command false test_plugin_output success)
|
||||||
|
[ "$compact" = 'WARN test warning' ] || { printf 'Plugin summary lost warnings or leaked progress.\n' >&2; exit 1; }
|
||||||
|
if diagnostics=$(summary=true run_plugin_command false test_plugin_output fail 2>&1); then
|
||||||
|
printf 'Plugin failure must fail.\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
[[ "$diagnostics" == *'download progress'* ]] && [[ "$diagnostics" == *'WARN test warning'* ]]
|
||||||
|
selected=()
|
||||||
|
plugin_installed() { printf 'Empty selection must not query installed plugins.\n' >&2; exit 1; }
|
||||||
|
run_plugin_command() { printf 'Empty selection must not run commands.\n' >&2; exit 1; }
|
||||||
|
install_configured_plugins "$root" both false false selected false
|
||||||
|
uninstall_deselected_plugins "$root" both false selected false
|
||||||
|
if read_plugin_toggle_selection names checked </dev/null > "$toggle_log" 2>&1; then
|
||||||
|
printf 'Closed input must fail instead of looping.\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$summary" = true ]; then printf 'Tests: PASS | plugin selection\n'; else printf 'PASS plugin toggle selection: toggle and confirm\n'; fi
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[switch]$DryRun,
|
||||||
|
[ValidateSet('Codex','Claude','Both')][string]$Client,
|
||||||
|
[ValidateSet('Windows','Linux')][string]$Platform,
|
||||||
|
[switch]$Summary
|
||||||
|
)
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$root = Split-Path $PSScriptRoot -Parent
|
||||||
|
. (Join-Path $PSScriptRoot 'lib/plugins.ps1')
|
||||||
|
. (Join-Path $PSScriptRoot 'lib/manifest.ps1')
|
||||||
|
. (Join-Path $PSScriptRoot 'lib/install-targets.ps1')
|
||||||
|
|
||||||
|
$Platform = Read-InstallPlatform -Platform $Platform
|
||||||
|
$platform = $Platform.ToLowerInvariant()
|
||||||
|
$Client = Read-InstallClient -Client $Client
|
||||||
|
|
||||||
|
$homePath = [Environment]::GetFolderPath('UserProfile')
|
||||||
|
if (-not $DryRun -and -not (Test-AllManifestsPresent (Get-InstallDestinations -HomePath $homePath -Client $Client))) {
|
||||||
|
throw 'Not installed, use the install script.'
|
||||||
|
}
|
||||||
|
|
||||||
|
$buildScript = Join-Path $PSScriptRoot 'build.ps1'
|
||||||
|
& $buildScript -Summary:$Summary
|
||||||
|
if (-not $?) { throw 'Build failed. Update was not started.' }
|
||||||
|
$generated = Join-Path $root 'generated'
|
||||||
|
if (-not (Test-Path $generated)) { throw 'Generated output is missing after a successful build.' }
|
||||||
|
|
||||||
|
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||||
|
$shellCommand = 'pwsh -NoProfile -ExecutionPolicy Bypass -File'
|
||||||
|
$windowsShellCommand = 'powershell -NoProfile -ExecutionPolicy Bypass -File'
|
||||||
|
if ([Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT) { $shellCommand = $windowsShellCommand }
|
||||||
|
|
||||||
|
foreach ($item in (Get-InstallTargets -Generated $generated -HomePath $homePath -Platform $platform -Client $Client)) {
|
||||||
|
Sync-ManagedDestination -Source $item.Source -Destination $item.Destination -Stamp $stamp `
|
||||||
|
-AiConfigRoot $item.Destination.Replace('\','/') -ShellCommand $shellCommand -WindowsShellCommand $windowsShellCommand -DryRun:$DryRun -Summary:$Summary
|
||||||
|
}
|
||||||
|
|
||||||
|
$plugins = Select-ConfiguredPlugins -RepositoryRoot $root -Client $Client -Mode 'Update' -DryRun:$DryRun
|
||||||
|
Install-ConfiguredPlugins -RepositoryRoot $root -Client $Client -DryRun:$DryRun -Update -Summary:$Summary -Entries $plugins.Selected
|
||||||
|
Uninstall-DeselectedPlugins -Entries $plugins.Deselected -Client $Client -DryRun:$DryRun -Summary:$Summary
|
||||||
|
if ($Summary) { Write-Output "Update: PASS | $Client, $Platform$(if ($DryRun) { ', dry-run' })" } else { Write-Output ($(if ($DryRun) { 'PASS update dry-run' } else { 'PASS update' })) }
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
dry_run=false
|
||||||
|
client=""
|
||||||
|
platform=""
|
||||||
|
summary=false
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--dry-run) dry_run=true; shift ;;
|
||||||
|
--summary) summary=true; shift ;;
|
||||||
|
--client) client=${2:?--client requires a value}; shift 2 ;;
|
||||||
|
--platform) platform=${2:?--platform requires a value}; shift 2 ;;
|
||||||
|
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
. "$root/scripts/lib/plugins.sh"
|
||||||
|
. "$root/scripts/lib/manifest.sh"
|
||||||
|
. "$root/scripts/lib/install-targets.sh"
|
||||||
|
|
||||||
|
platform=$(read_install_platform "$platform")
|
||||||
|
client=$(read_install_client "$client")
|
||||||
|
|
||||||
|
home_path=${HOME:?HOME is required}
|
||||||
|
if [ "$dry_run" = false ]; then
|
||||||
|
mapfile -t destinations < <(get_install_destinations "$home_path" "$client")
|
||||||
|
if ! all_manifests_present "${destinations[@]}"; then
|
||||||
|
printf 'Not installed, use the install script.\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
build_args=()
|
||||||
|
[ "$summary" = false ] || build_args+=(--summary)
|
||||||
|
bash "$root/scripts/build.sh" "${build_args[@]}"
|
||||||
|
generated="$root/generated"
|
||||||
|
[ -d "$generated" ] || { printf 'Generated output is missing after a successful build.\n' >&2; exit 1; }
|
||||||
|
|
||||||
|
stamp=$(date +%Y%m%d-%H%M%S)
|
||||||
|
shell_command=bash
|
||||||
|
windows_shell_command='powershell -NoProfile -ExecutionPolicy Bypass -File'
|
||||||
|
|
||||||
|
while IFS='|' read -r source destination; do
|
||||||
|
ai_config_root=$destination
|
||||||
|
command -v cygpath >/dev/null && ai_config_root=$(cygpath -m "$destination")
|
||||||
|
sync_managed_destination "$source" "$destination" "$stamp" "$ai_config_root" "$shell_command" "$windows_shell_command" "$dry_run" "$summary"
|
||||||
|
done < <(get_install_targets "$generated" "$home_path" "$platform" "$client")
|
||||||
|
|
||||||
|
selected_plugins=()
|
||||||
|
deselected_plugins=()
|
||||||
|
select_configured_plugins "$root" "$client" Update "$dry_run" selected_plugins deselected_plugins
|
||||||
|
install_configured_plugins "$root" "$client" "$dry_run" true selected_plugins "$summary"
|
||||||
|
uninstall_deselected_plugins "$root" "$client" "$dry_run" deselected_plugins "$summary"
|
||||||
|
|
||||||
|
if [ "$summary" = true ]; then
|
||||||
|
suffix=
|
||||||
|
[ "$dry_run" = false ] || suffix=', dry-run'
|
||||||
|
printf 'Update: PASS | %s, %s%s\n' "$client" "$platform" "$suffix"
|
||||||
|
elif "$dry_run"; then printf 'PASS update dry-run\n'; else printf 'PASS update\n'; fi
|
||||||
@@ -58,12 +58,23 @@ def validate_claude(path: Path) -> None:
|
|||||||
fail(f"hooks.{event} contains an unsupported command hook")
|
fail(f"hooks.{event} contains an unsupported command hook")
|
||||||
|
|
||||||
|
|
||||||
|
def validate_rule_skills(package: Path) -> None:
|
||||||
|
for path in (package / "skills" / "rules").glob("*/SKILL.md"):
|
||||||
|
lines = path.read_text(encoding="utf-8-sig").splitlines()
|
||||||
|
if len(lines) < 4 or lines[0] != "---" or lines[3] != "---" or not lines[1].startswith("name: ") or not lines[2].startswith("description: "):
|
||||||
|
fail(f"{path}: invalid generated skill frontmatter")
|
||||||
|
description = json.loads(lines[2].removeprefix("description: "))
|
||||||
|
if not isinstance(description, str) or not description.strip():
|
||||||
|
fail(f"{path}: skill description must be a quoted, nonempty string")
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
if len(sys.argv) != 3:
|
if len(sys.argv) != 3:
|
||||||
print("usage: validate-config.py <codex-toml> <claude-json>", file=sys.stderr)
|
print("usage: validate-config.py <codex-toml> <claude-json>", file=sys.stderr)
|
||||||
return 2
|
return 2
|
||||||
tomllib.loads(Path(sys.argv[1]).read_text(encoding="utf-8-sig"))
|
tomllib.loads(Path(sys.argv[1]).read_text(encoding="utf-8-sig"))
|
||||||
validate_claude(Path(sys.argv[2]))
|
validate_claude(Path(sys.argv[2]))
|
||||||
|
validate_rule_skills(Path(sys.argv[2]).parent)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,4 @@ This instruction set is generated into the client-specific `AGENTS.md` and `CLAU
|
|||||||
|
|
||||||
__RULE_LOADING__
|
__RULE_LOADING__
|
||||||
|
|
||||||
Apply instructions in this order: direct current user instructions; security and data-loss protections; applicable project instructions; these global rules; existing project conventions; agent preferences.
|
Delegate only when the expected improvement in correctness, independence, specialization, or parallelism outweighs the duplicated context and tool cost. Do not delegate simple repository exploration the main agent can do directly. Do not create a researcher for information the main agent can obtain cheaply itself. Use one implementer for small and medium contained tasks. Run reviewer and verifier together only when their responsibilities are materially different for the current task. Avoid multiple agents independently reading the same large set of files. Use parallel agents only for genuinely independent work. Do not let subagents create further subagents.
|
||||||
|
|
||||||
Make evidence-based, reversible technical decisions independently. Ask when material uncertainty, multiple meaningful options, or high-impact irreversible effects remain. Ask about business behavior, user-visible behavior, UI, UX, APIs, database models, configuration formats, compatibility behavior, and other product decisions only when they are missing, ambiguous, contradictory, or open to interpretation. Implement explicitly specified behavior without asking again.
|
|
||||||
|
|
||||||
Never modify existing documentation automatically. Ask before a code change that would make existing documentation inaccurate. Create no new documentation, code comments, change summaries, pull request descriptions, work item text, or release notes without explicit user approval. If a comment appears necessary, ask first.
|
|
||||||
|
|
||||||
Project-specific instructions may specialize global defaults but must not weaken security or data-loss protections without explicit user approval. Direct user instructions have the highest priority for requested behavior.
|
|
||||||
|
|
||||||
Use one implementer for small, contained tasks. Add researcher only for material uncertainty, architect only for architecture or boundary decisions, reviewer only for independent quality review, and verifier only for explicit acceptance criteria, tests, or generated artifacts. Do not start reviewer and verifier by default or let subagents create further subagents. Parallelize only genuinely independent work.
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param()
|
||||||
|
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||||
|
$stateDir = Join-Path $root '.ai-session'
|
||||||
|
New-Item $stateDir -ItemType Directory -Force | Out-Null
|
||||||
|
('{"event":"compact","timestamp":"' + (Get-Date).ToUniversalTime().ToString('o') + '"}') | Add-Content (Join-Path $stateDir 'telemetry.jsonl')
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param()
|
||||||
|
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||||
|
if (Test-Path (Join-Path $root '.ai-session/state.json')) { Write-Output 'Task state is available in .ai-session/state.json; read it only if needed.' }
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||||
|
mkdir -p "$root/.ai-session"
|
||||||
|
printf '{"event":"compact","timestamp":"%s"}\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$root/.ai-session/telemetry.jsonl"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||||
|
[ -f "$root/.ai-session/state.json" ] && printf 'Task state is available in .ai-session/state.json; read it only if needed.\n'
|
||||||
@@ -1,297 +0,0 @@
|
|||||||
# Angular Code Guidelines
|
|
||||||
|
|
||||||
* Use modern Angular patterns and current framework conventions.
|
|
||||||
* Prefer standalone components, directives and pipes for new code.
|
|
||||||
* Avoid introducing `NgModule` unless the existing project architecture requires it.
|
|
||||||
* Keep components small and focused.
|
|
||||||
* Keep templates simple.
|
|
||||||
* Move complex logic out of templates.
|
|
||||||
* Keep business logic out of components where practical.
|
|
||||||
* Prefer services, facades or dedicated domain logic for non-trivial behavior.
|
|
||||||
* Avoid god components and god services.
|
|
||||||
* Keep dependencies explicit.
|
|
||||||
* Prefer composition over inheritance.
|
|
||||||
* Avoid unnecessary abstractions.
|
|
||||||
* Avoid unnecessary wrapper components.
|
|
||||||
* Follow the existing project architecture unless there is a strong reason to change it.
|
|
||||||
|
|
||||||
## Components
|
|
||||||
|
|
||||||
* Use `ChangeDetectionStrategy.OnPush` unless there is a concrete reason not to.
|
|
||||||
* Prefer signal-based state for local component state.
|
|
||||||
* Prefer `computed` for derived state.
|
|
||||||
* Prefer `effect` only for actual side effects.
|
|
||||||
* Do not use `effect` as a replacement for normal data flow.
|
|
||||||
* Keep component state minimal.
|
|
||||||
* Avoid duplicating derived state.
|
|
||||||
* Prefer immutable state updates.
|
|
||||||
* Avoid direct mutation of shared state.
|
|
||||||
* Keep inputs and outputs strongly typed.
|
|
||||||
* Prefer signal-based inputs and outputs when consistent with the project.
|
|
||||||
* Avoid unnecessary two-way binding.
|
|
||||||
* Avoid unnecessary component-to-component coupling.
|
|
||||||
* Prefer explicit data flow from parent to child and explicit events upward.
|
|
||||||
* Do not expose internal component state unnecessarily.
|
|
||||||
* Use lifecycle hooks only when required.
|
|
||||||
* Avoid complex initialization logic inside constructors.
|
|
||||||
|
|
||||||
## Templates
|
|
||||||
|
|
||||||
* Keep expressions short and readable.
|
|
||||||
* Do not execute expensive methods from templates.
|
|
||||||
* Avoid function calls in templates when their result can be derived or cached.
|
|
||||||
* Avoid complex conditions directly in markup.
|
|
||||||
* Prefer derived state in TypeScript over repeated template expressions.
|
|
||||||
* Use Angular control flow syntax such as `@if`, `@for` and `@switch` for new code.
|
|
||||||
* Always provide stable tracking for repeated collections.
|
|
||||||
* Avoid deeply nested template structures.
|
|
||||||
* Split large templates into focused components when it improves maintainability.
|
|
||||||
* Avoid unnecessary DOM elements.
|
|
||||||
* Use semantic HTML.
|
|
||||||
* Prefer native HTML behavior before custom implementations.
|
|
||||||
* Keep accessibility in mind when choosing elements and interactions.
|
|
||||||
|
|
||||||
## Signals and State
|
|
||||||
|
|
||||||
* Prefer signals for local and synchronous reactive state.
|
|
||||||
* Prefer `computed` for derived values.
|
|
||||||
* Keep signals private when external mutation is not intended.
|
|
||||||
* Expose readonly state where practical.
|
|
||||||
* Avoid storing values that can be derived from existing state.
|
|
||||||
* Avoid circular signal dependencies.
|
|
||||||
* Avoid side effects inside `computed`.
|
|
||||||
* Keep effects small and deterministic.
|
|
||||||
* Clean up resources created by effects.
|
|
||||||
* Do not use global state unless multiple unrelated parts of the application genuinely need it.
|
|
||||||
* Do not introduce a state-management library without a clear requirement.
|
|
||||||
|
|
||||||
## RxJS
|
|
||||||
|
|
||||||
* Use RxJS for asynchronous streams, events and complex reactive flows.
|
|
||||||
* Do not use RxJS when simple synchronous signals are sufficient.
|
|
||||||
* Prefer declarative observable pipelines.
|
|
||||||
* Avoid nested `subscribe`.
|
|
||||||
* Avoid manual subscriptions where `async`, signals or framework helpers are sufficient.
|
|
||||||
* Prefer `takeUntilDestroyed` or equivalent Angular lifecycle integration for manual subscriptions.
|
|
||||||
* Always consider subscription lifetime.
|
|
||||||
* Avoid memory leaks from unmanaged subscriptions.
|
|
||||||
* Use the correct flattening operator for the required semantics.
|
|
||||||
* Do not use `switchMap`, `mergeMap`, `concatMap` or `exhaustMap` interchangeably.
|
|
||||||
* Avoid unnecessary `Subject` usage.
|
|
||||||
* Prefer exposing observables instead of exposing mutable subjects.
|
|
||||||
* Prefer `BehaviorSubject` only when a current value is actually required.
|
|
||||||
* Avoid excessive observable chains for simple state.
|
|
||||||
* Avoid converting repeatedly between signals and observables without need.
|
|
||||||
|
|
||||||
## Dependency Injection
|
|
||||||
|
|
||||||
* Prefer Angular dependency injection for application services.
|
|
||||||
* Keep injected dependencies minimal.
|
|
||||||
* Avoid service locator patterns.
|
|
||||||
* Prefer `inject()` where it improves readability and is consistent with the project.
|
|
||||||
* Do not inject services that are not used.
|
|
||||||
* Scope services intentionally.
|
|
||||||
* Prefer application-wide singletons only for truly shared stateless or coordinated state.
|
|
||||||
* Avoid storing request-specific or component-specific mutable state in root services.
|
|
||||||
* Use injection tokens for configuration or abstractions where appropriate.
|
|
||||||
* Do not create interfaces only to satisfy dependency injection when no abstraction is needed.
|
|
||||||
|
|
||||||
## Services
|
|
||||||
|
|
||||||
* Keep services focused on a clear responsibility.
|
|
||||||
* Avoid large catch-all services.
|
|
||||||
* Separate API access, domain logic and UI state where practical.
|
|
||||||
* Avoid leaking transport models throughout the application.
|
|
||||||
* Do not expose mutable internal state directly.
|
|
||||||
* Prefer readonly APIs for state consumers.
|
|
||||||
* Avoid hidden side effects.
|
|
||||||
* Keep service methods predictable.
|
|
||||||
* Do not use services merely to move poorly structured component code elsewhere.
|
|
||||||
|
|
||||||
## HTTP and APIs
|
|
||||||
|
|
||||||
* Keep API access centralized and strongly typed.
|
|
||||||
* Prefer dedicated API clients or data-access services.
|
|
||||||
* Do not use `any` for API responses.
|
|
||||||
* Treat all server responses as untrusted at runtime.
|
|
||||||
* Validate external data when the contract is security- or correctness-critical.
|
|
||||||
* Keep DTOs separate from domain models when their responsibilities differ.
|
|
||||||
* Avoid duplicate API calls.
|
|
||||||
* Cache only when the invalidation strategy is clear.
|
|
||||||
* Avoid manual URL string construction when typed helpers or centralized endpoints are available.
|
|
||||||
* Use interceptors only for cross-cutting concerns.
|
|
||||||
* Do not put unrelated business logic into HTTP interceptors.
|
|
||||||
* Handle cancellation where appropriate.
|
|
||||||
* Handle expected error states explicitly.
|
|
||||||
|
|
||||||
## Forms
|
|
||||||
|
|
||||||
* Prefer reactive forms for non-trivial forms.
|
|
||||||
* Keep forms strongly typed.
|
|
||||||
* Avoid untyped forms.
|
|
||||||
* Keep validation rules centralized and reusable where practical.
|
|
||||||
* Validate on both client and server where required.
|
|
||||||
* Never treat client-side validation as a security boundary.
|
|
||||||
* Keep form state separate from domain state when useful.
|
|
||||||
* Avoid large components that combine layout, validation, persistence and business logic.
|
|
||||||
* Display validation errors consistently.
|
|
||||||
* Preserve user input when recoverable errors occur.
|
|
||||||
|
|
||||||
## Routing
|
|
||||||
|
|
||||||
* Keep route definitions explicit and predictable.
|
|
||||||
* Prefer lazy loading for larger feature areas.
|
|
||||||
* Avoid loading large feature bundles eagerly without need.
|
|
||||||
* Use route guards only for navigation behavior, not as a security boundary.
|
|
||||||
* Authorization must still be enforced on the backend.
|
|
||||||
* Keep resolvers focused.
|
|
||||||
* Avoid expensive work during navigation unless required.
|
|
||||||
* Keep route parameters strongly typed at application boundaries where practical.
|
|
||||||
* Do not rely on client-side routing state for sensitive authorization decisions.
|
|
||||||
|
|
||||||
## Performance
|
|
||||||
|
|
||||||
* Use `OnPush`.
|
|
||||||
* Use stable tracking in loops.
|
|
||||||
* Avoid unnecessary change detection triggers.
|
|
||||||
* Avoid repeated expensive template calculations.
|
|
||||||
* Avoid unnecessary subscriptions.
|
|
||||||
* Avoid unnecessary DOM rendering.
|
|
||||||
* Lazy-load large features and heavy dependencies where appropriate.
|
|
||||||
* Avoid large initial bundles.
|
|
||||||
* Avoid importing full libraries when only a small part is required.
|
|
||||||
* Avoid unnecessary deep cloning.
|
|
||||||
* Avoid unnecessary object and array recreation in hot paths.
|
|
||||||
* Keep state updates targeted.
|
|
||||||
* Avoid unnecessary global state updates.
|
|
||||||
* Measure bundle size and runtime performance before micro-optimizing.
|
|
||||||
* Prefer architectural improvements over template-level micro-optimizations.
|
|
||||||
|
|
||||||
## Security
|
|
||||||
|
|
||||||
* Treat all external data as untrusted.
|
|
||||||
* Never bypass Angular sanitization without a verified reason.
|
|
||||||
* Avoid direct DOM manipulation.
|
|
||||||
* Avoid `innerHTML` with untrusted content.
|
|
||||||
* Avoid `bypassSecurityTrust*` unless explicitly required and the input is fully controlled.
|
|
||||||
* Never expose secrets in frontend code.
|
|
||||||
* Never store sensitive credentials in the Angular application.
|
|
||||||
* Do not rely on frontend checks for authorization.
|
|
||||||
* Enforce authentication and authorization on the backend.
|
|
||||||
* Avoid leaking sensitive information through logs or error messages.
|
|
||||||
* Use secure cookie or token handling according to the application's authentication architecture.
|
|
||||||
* Avoid storing sensitive tokens in insecure browser storage without a deliberate security decision.
|
|
||||||
* Do not disable security controls for convenience.
|
|
||||||
|
|
||||||
## DOM and Browser APIs
|
|
||||||
|
|
||||||
* Prefer Angular APIs and declarative templates over direct DOM manipulation.
|
|
||||||
* Use `Renderer2` only when direct rendering abstraction is actually needed.
|
|
||||||
* Avoid accessing globals directly when it harms testability or SSR compatibility.
|
|
||||||
* Consider SSR and hydration compatibility for browser-specific code.
|
|
||||||
* Guard access to `window`, `document`, `localStorage` and similar browser-only APIs when SSR is supported.
|
|
||||||
* Clean up event listeners, observers and timers.
|
|
||||||
* Avoid layout thrashing in performance-sensitive code.
|
|
||||||
|
|
||||||
## Accessibility
|
|
||||||
|
|
||||||
* Use semantic HTML.
|
|
||||||
* Preserve keyboard navigation.
|
|
||||||
* Ensure interactive elements are actually interactive elements.
|
|
||||||
* Do not replace buttons with clickable `div` elements.
|
|
||||||
* Provide labels for form controls.
|
|
||||||
* Maintain visible focus behavior.
|
|
||||||
* Use ARIA only when native HTML semantics are insufficient.
|
|
||||||
* Avoid unnecessary custom widgets.
|
|
||||||
* Keep accessible names and states synchronized with UI state.
|
|
||||||
|
|
||||||
## Styling
|
|
||||||
|
|
||||||
* Keep styles scoped and predictable.
|
|
||||||
* Avoid unnecessary global CSS.
|
|
||||||
* Follow the project's styling strategy consistently.
|
|
||||||
* Avoid excessive specificity.
|
|
||||||
* Avoid `!important` unless there is a concrete reason.
|
|
||||||
* Prefer reusable design tokens and shared styles over duplicated magic values.
|
|
||||||
* Keep component styles focused on component concerns.
|
|
||||||
* Avoid styling based on fragile DOM structure.
|
|
||||||
* Do not mix multiple styling approaches without need.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
* Test behavior, not implementation details.
|
|
||||||
* Prefer focused component tests.
|
|
||||||
* Test critical user interactions.
|
|
||||||
* Test services and domain logic independently where useful.
|
|
||||||
* Avoid brittle tests coupled to internal structure.
|
|
||||||
* Avoid excessive mocking.
|
|
||||||
* Mock external boundaries rather than internal implementation details.
|
|
||||||
* Keep tests deterministic.
|
|
||||||
* Avoid time-dependent tests without controlled clocks.
|
|
||||||
* Avoid network-dependent tests without proper isolation.
|
|
||||||
* Keep test setup minimal and readable.
|
|
||||||
* Update tests when refactoring changes structure but not behavior.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
* Keep UI, application, domain and data-access concerns separated where practical.
|
|
||||||
* Organize code by feature rather than by technical file type when it improves cohesion.
|
|
||||||
* Avoid circular dependencies.
|
|
||||||
* Avoid cross-feature imports without a clear contract.
|
|
||||||
* Keep shared code genuinely shared.
|
|
||||||
* Do not move feature-specific logic into generic shared modules.
|
|
||||||
* Avoid dumping unrelated helpers into global utility files.
|
|
||||||
* Keep public APIs of feature areas small.
|
|
||||||
* Avoid exposing internal implementation details across feature boundaries.
|
|
||||||
* Prefer local ownership of state and logic.
|
|
||||||
|
|
||||||
## Code Quality
|
|
||||||
|
|
||||||
* Use strict TypeScript settings.
|
|
||||||
* Avoid `any`.
|
|
||||||
* Avoid unnecessary type assertions.
|
|
||||||
* Avoid non-null assertions unless safety is guaranteed.
|
|
||||||
* Prefer modern, readable syntax.
|
|
||||||
* Use descriptive names.
|
|
||||||
* Avoid vague names such as `data`, `item`, `value` or `temp` when better names exist.
|
|
||||||
* Remove dead code.
|
|
||||||
* Remove unused imports and dependencies.
|
|
||||||
* Remove commented-out code instead of keeping it in source files.
|
|
||||||
* Do not add comments unless the user explicitly requests them.
|
|
||||||
* Treat Angular, TypeScript and lint warnings seriously.
|
|
||||||
* Do not suppress diagnostics without a concrete reason.
|
|
||||||
* Prefer correctness, security and maintainability over cleverness.
|
|
||||||
|
|
||||||
## Dependency Rules
|
|
||||||
|
|
||||||
* Prefer Angular and browser platform capabilities before adding third-party libraries.
|
|
||||||
* Do not add dependencies without a clear benefit.
|
|
||||||
* Avoid libraries that duplicate existing framework functionality.
|
|
||||||
* Avoid abandoned or poorly maintained packages.
|
|
||||||
* Keep Angular package versions aligned.
|
|
||||||
* Do not mix incompatible Angular package versions.
|
|
||||||
* Respect the existing lockfile and package manager.
|
|
||||||
* Avoid unrelated dependency upgrades.
|
|
||||||
|
|
||||||
## Performance Priority
|
|
||||||
|
|
||||||
Prioritize Angular performance work in this order:
|
|
||||||
|
|
||||||
* Application architecture
|
|
||||||
* Network and API behavior
|
|
||||||
* Bundle size and lazy loading
|
|
||||||
* State architecture
|
|
||||||
* Change detection
|
|
||||||
* Rendering and DOM complexity
|
|
||||||
* RxJS and subscription behavior
|
|
||||||
* Memory allocations
|
|
||||||
* CPU micro-optimizations
|
|
||||||
|
|
||||||
Never optimize based purely on assumptions.
|
|
||||||
|
|
||||||
## Safety Rule
|
|
||||||
|
|
||||||
* Angular frontend code is never a security boundary.
|
|
||||||
* Client-side validation, guards, hidden controls and disabled UI elements do not replace backend enforcement.
|
|
||||||
* Do not bypass Angular's built-in security mechanisms without a verified and explicit reason.
|
|
||||||
* Prefer safe framework defaults over custom low-level behavior.
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Angular Code Guidelines
|
||||||
|
|
||||||
|
* Use modern Angular patterns and current framework conventions.
|
||||||
|
* Prefer standalone components, directives and pipes for new code.
|
||||||
|
* Avoid introducing `NgModule` unless the existing project architecture requires it.
|
||||||
|
* Keep components small and focused.
|
||||||
|
* Keep templates simple.
|
||||||
|
* Move complex logic out of templates.
|
||||||
|
* Keep business logic out of components where practical.
|
||||||
|
* Prefer services, facades or dedicated domain logic for non-trivial behavior.
|
||||||
|
* Avoid god components and god services.
|
||||||
|
* Keep dependencies explicit.
|
||||||
|
* Prefer composition over inheritance.
|
||||||
|
* Avoid unnecessary abstractions.
|
||||||
|
* Avoid unnecessary wrapper components.
|
||||||
|
* Follow the existing project architecture unless there is a strong reason to change it.
|
||||||
|
|
||||||
|
## Safety Rule
|
||||||
|
|
||||||
|
* Angular frontend code is never a security boundary.
|
||||||
|
* Client-side validation, guards, hidden controls and disabled UI elements do not replace backend enforcement.
|
||||||
|
* Do not bypass Angular's built-in security mechanisms without a verified and explicit reason.
|
||||||
|
* Prefer safe framework defaults over custom low-level behavior.
|
||||||
|
|
||||||
|
## Detailed guidance
|
||||||
|
|
||||||
|
Read the matching reference file before working in that area:
|
||||||
|
|
||||||
|
| Read this reference for... | ...this kind of work |
|
||||||
|
| --- | --- |
|
||||||
|
| `references/components-and-state.md` | Component structure, signal-based state |
|
||||||
|
| `references/templates-and-styling.md` | Template markup, CSS/styling |
|
||||||
|
| `references/rxjs.md` | RxJS observables and subscriptions |
|
||||||
|
| `references/services-and-http.md` | Dependency injection, services, HTTP/API access |
|
||||||
|
| `references/forms-and-routing.md` | Reactive forms, routing |
|
||||||
|
| `references/security-and-accessibility.md` | Security, DOM/browser APIs, accessibility |
|
||||||
|
| `references/performance.md` | Performance work and its priority order |
|
||||||
|
| `references/testing-and-architecture.md` | Testing, architecture, code quality, dependency rules |
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
## Components
|
||||||
|
|
||||||
|
* Use `ChangeDetectionStrategy.OnPush` unless there is a concrete reason not to.
|
||||||
|
* Prefer signal-based state for local component state.
|
||||||
|
* Prefer `computed` for derived state.
|
||||||
|
* Prefer `effect` only for actual side effects.
|
||||||
|
* Do not use `effect` as a replacement for normal data flow.
|
||||||
|
* Keep component state minimal.
|
||||||
|
* Avoid duplicating derived state.
|
||||||
|
* Prefer immutable state updates.
|
||||||
|
* Avoid direct mutation of shared state.
|
||||||
|
* Keep inputs and outputs strongly typed.
|
||||||
|
* Prefer signal-based inputs and outputs when consistent with the project.
|
||||||
|
* Avoid unnecessary two-way binding.
|
||||||
|
* Avoid unnecessary component-to-component coupling.
|
||||||
|
* Prefer explicit data flow from parent to child and explicit events upward.
|
||||||
|
* Do not expose internal component state unnecessarily.
|
||||||
|
* Use lifecycle hooks only when required.
|
||||||
|
* Avoid complex initialization logic inside constructors.
|
||||||
|
|
||||||
|
## Signals and State
|
||||||
|
|
||||||
|
* Prefer signals for local and synchronous reactive state.
|
||||||
|
* Prefer `computed` for derived values.
|
||||||
|
* Keep signals private when external mutation is not intended.
|
||||||
|
* Expose readonly state where practical.
|
||||||
|
* Avoid storing values that can be derived from existing state.
|
||||||
|
* Avoid circular signal dependencies.
|
||||||
|
* Avoid side effects inside `computed`.
|
||||||
|
* Keep effects small and deterministic.
|
||||||
|
* Clean up resources created by effects.
|
||||||
|
* Do not use global state unless multiple unrelated parts of the application genuinely need it.
|
||||||
|
* Do not introduce a state-management library without a clear requirement.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
## Forms
|
||||||
|
|
||||||
|
* Prefer reactive forms for non-trivial forms.
|
||||||
|
* Keep forms strongly typed.
|
||||||
|
* Avoid untyped forms.
|
||||||
|
* Keep validation rules centralized and reusable where practical.
|
||||||
|
* Validate on both client and server where required.
|
||||||
|
* Never treat client-side validation as a security boundary.
|
||||||
|
* Keep form state separate from domain state when useful.
|
||||||
|
* Avoid large components that combine layout, validation, persistence and business logic.
|
||||||
|
* Display validation errors consistently.
|
||||||
|
* Preserve user input when recoverable errors occur.
|
||||||
|
|
||||||
|
## Routing
|
||||||
|
|
||||||
|
* Keep route definitions explicit and predictable.
|
||||||
|
* Prefer lazy loading for larger feature areas.
|
||||||
|
* Avoid loading large feature bundles eagerly without need.
|
||||||
|
* Use route guards only for navigation behavior, not as a security boundary.
|
||||||
|
* Authorization must still be enforced on the backend.
|
||||||
|
* Keep resolvers focused.
|
||||||
|
* Avoid expensive work during navigation unless required.
|
||||||
|
* Keep route parameters strongly typed at application boundaries where practical.
|
||||||
|
* Do not rely on client-side routing state for sensitive authorization decisions.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
## Performance
|
||||||
|
|
||||||
|
* Use `OnPush`.
|
||||||
|
* Use stable tracking in loops.
|
||||||
|
* Avoid unnecessary change detection triggers.
|
||||||
|
* Avoid repeated expensive template calculations.
|
||||||
|
* Avoid unnecessary subscriptions.
|
||||||
|
* Avoid unnecessary DOM rendering.
|
||||||
|
* Lazy-load large features and heavy dependencies where appropriate.
|
||||||
|
* Avoid large initial bundles.
|
||||||
|
* Avoid importing full libraries when only a small part is required.
|
||||||
|
* Avoid unnecessary deep cloning.
|
||||||
|
* Avoid unnecessary object and array recreation in hot paths.
|
||||||
|
* Keep state updates targeted.
|
||||||
|
* Avoid unnecessary global state updates.
|
||||||
|
* Measure bundle size and runtime performance before micro-optimizing.
|
||||||
|
* Prefer architectural improvements over template-level micro-optimizations.
|
||||||
|
|
||||||
|
## Performance Priority
|
||||||
|
|
||||||
|
Prioritize Angular performance work in this order:
|
||||||
|
|
||||||
|
* Application architecture
|
||||||
|
* Network and API behavior
|
||||||
|
* Bundle size and lazy loading
|
||||||
|
* State architecture
|
||||||
|
* Change detection
|
||||||
|
* Rendering and DOM complexity
|
||||||
|
* RxJS and subscription behavior
|
||||||
|
* Memory allocations
|
||||||
|
* CPU micro-optimizations
|
||||||
|
|
||||||
|
Never optimize based purely on assumptions.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
## RxJS
|
||||||
|
|
||||||
|
* Use RxJS for asynchronous streams, events and complex reactive flows.
|
||||||
|
* Do not use RxJS when simple synchronous signals are sufficient.
|
||||||
|
* Prefer declarative observable pipelines.
|
||||||
|
* Avoid nested `subscribe`.
|
||||||
|
* Avoid manual subscriptions where `async`, signals or framework helpers are sufficient.
|
||||||
|
* Prefer `takeUntilDestroyed` or equivalent Angular lifecycle integration for manual subscriptions.
|
||||||
|
* Always consider subscription lifetime.
|
||||||
|
* Avoid memory leaks from unmanaged subscriptions.
|
||||||
|
* Use the correct flattening operator for the required semantics.
|
||||||
|
* Do not use `switchMap`, `mergeMap`, `concatMap` or `exhaustMap` interchangeably.
|
||||||
|
* Avoid unnecessary `Subject` usage.
|
||||||
|
* Prefer exposing observables instead of exposing mutable subjects.
|
||||||
|
* Prefer `BehaviorSubject` only when a current value is actually required.
|
||||||
|
* Avoid excessive observable chains for simple state.
|
||||||
|
* Avoid converting repeatedly between signals and observables without need.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
## Security
|
||||||
|
|
||||||
|
* Treat all external data as untrusted.
|
||||||
|
* Never bypass Angular sanitization without a verified reason.
|
||||||
|
* Avoid direct DOM manipulation.
|
||||||
|
* Avoid `innerHTML` with untrusted content.
|
||||||
|
* Avoid `bypassSecurityTrust*` unless explicitly required and the input is fully controlled.
|
||||||
|
* Never expose secrets in frontend code.
|
||||||
|
* Never store sensitive credentials in the Angular application.
|
||||||
|
* Do not rely on frontend checks for authorization.
|
||||||
|
* Enforce authentication and authorization on the backend.
|
||||||
|
* Avoid leaking sensitive information through logs or error messages.
|
||||||
|
* Use secure cookie or token handling according to the application's authentication architecture.
|
||||||
|
* Avoid storing sensitive tokens in insecure browser storage without a deliberate security decision.
|
||||||
|
* Do not disable security controls for convenience.
|
||||||
|
|
||||||
|
## DOM and Browser APIs
|
||||||
|
|
||||||
|
* Prefer Angular APIs and declarative templates over direct DOM manipulation.
|
||||||
|
* Use `Renderer2` only when direct rendering abstraction is actually needed.
|
||||||
|
* Avoid accessing globals directly when it harms testability or SSR compatibility.
|
||||||
|
* Consider SSR and hydration compatibility for browser-specific code.
|
||||||
|
* Guard access to `window`, `document`, `localStorage` and similar browser-only APIs when SSR is supported.
|
||||||
|
* Clean up event listeners, observers and timers.
|
||||||
|
* Avoid layout thrashing in performance-sensitive code.
|
||||||
|
|
||||||
|
## Accessibility
|
||||||
|
|
||||||
|
* Use semantic HTML.
|
||||||
|
* Preserve keyboard navigation.
|
||||||
|
* Ensure interactive elements are actually interactive elements.
|
||||||
|
* Do not replace buttons with clickable `div` elements.
|
||||||
|
* Provide labels for form controls.
|
||||||
|
* Maintain visible focus behavior.
|
||||||
|
* Use ARIA only when native HTML semantics are insufficient.
|
||||||
|
* Avoid unnecessary custom widgets.
|
||||||
|
* Keep accessible names and states synchronized with UI state.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
## Dependency Injection
|
||||||
|
|
||||||
|
* Prefer Angular dependency injection for application services.
|
||||||
|
* Keep injected dependencies minimal.
|
||||||
|
* Avoid service locator patterns.
|
||||||
|
* Prefer `inject()` where it improves readability and is consistent with the project.
|
||||||
|
* Do not inject services that are not used.
|
||||||
|
* Scope services intentionally.
|
||||||
|
* Prefer application-wide singletons only for truly shared stateless or coordinated state.
|
||||||
|
* Avoid storing request-specific or component-specific mutable state in root services.
|
||||||
|
* Use injection tokens for configuration or abstractions where appropriate.
|
||||||
|
* Do not create interfaces only to satisfy dependency injection when no abstraction is needed.
|
||||||
|
|
||||||
|
## Services
|
||||||
|
|
||||||
|
* Keep services focused on a clear responsibility.
|
||||||
|
* Avoid large catch-all services.
|
||||||
|
* Separate API access, domain logic and UI state where practical.
|
||||||
|
* Avoid leaking transport models throughout the application.
|
||||||
|
* Do not expose mutable internal state directly.
|
||||||
|
* Prefer readonly APIs for state consumers.
|
||||||
|
* Avoid hidden side effects.
|
||||||
|
* Keep service methods predictable.
|
||||||
|
* Do not use services merely to move poorly structured component code elsewhere.
|
||||||
|
|
||||||
|
## HTTP and APIs
|
||||||
|
|
||||||
|
* Keep API access centralized and strongly typed.
|
||||||
|
* Prefer dedicated API clients or data-access services.
|
||||||
|
* Do not use `any` for API responses.
|
||||||
|
* Treat all server responses as untrusted at runtime.
|
||||||
|
* Validate external data when the contract is security- or correctness-critical.
|
||||||
|
* Keep DTOs separate from domain models when their responsibilities differ.
|
||||||
|
* Avoid duplicate API calls.
|
||||||
|
* Cache only when the invalidation strategy is clear.
|
||||||
|
* Avoid manual URL string construction when typed helpers or centralized endpoints are available.
|
||||||
|
* Use interceptors only for cross-cutting concerns.
|
||||||
|
* Do not put unrelated business logic into HTTP interceptors.
|
||||||
|
* Handle cancellation where appropriate.
|
||||||
|
* Handle expected error states explicitly.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
## Templates
|
||||||
|
|
||||||
|
* Keep expressions short and readable.
|
||||||
|
* Do not execute expensive methods from templates.
|
||||||
|
* Avoid function calls in templates when their result can be derived or cached.
|
||||||
|
* Avoid complex conditions directly in markup.
|
||||||
|
* Prefer derived state in TypeScript over repeated template expressions.
|
||||||
|
* Use Angular control flow syntax such as `@if`, `@for` and `@switch` for new code.
|
||||||
|
* Always provide stable tracking for repeated collections.
|
||||||
|
* Avoid deeply nested template structures.
|
||||||
|
* Split large templates into focused components when it improves maintainability.
|
||||||
|
* Avoid unnecessary DOM elements.
|
||||||
|
* Use semantic HTML.
|
||||||
|
* Prefer native HTML behavior before custom implementations.
|
||||||
|
* Keep accessibility in mind when choosing elements and interactions.
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
* Keep styles scoped and predictable.
|
||||||
|
* Avoid unnecessary global CSS.
|
||||||
|
* Follow the project's styling strategy consistently.
|
||||||
|
* Avoid excessive specificity.
|
||||||
|
* Avoid `!important` unless there is a concrete reason.
|
||||||
|
* Prefer reusable design tokens and shared styles over duplicated magic values.
|
||||||
|
* Keep component styles focused on component concerns.
|
||||||
|
* Avoid styling based on fragile DOM structure.
|
||||||
|
* Do not mix multiple styling approaches without need.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
## Testing
|
||||||
|
|
||||||
|
* Test behavior, not implementation details.
|
||||||
|
* Prefer focused component tests.
|
||||||
|
* Test critical user interactions.
|
||||||
|
* Test services and domain logic independently where useful.
|
||||||
|
* Avoid brittle tests coupled to internal structure.
|
||||||
|
* Avoid excessive mocking.
|
||||||
|
* Mock external boundaries rather than internal implementation details.
|
||||||
|
* Keep tests deterministic.
|
||||||
|
* Avoid time-dependent tests without controlled clocks.
|
||||||
|
* Avoid network-dependent tests without proper isolation.
|
||||||
|
* Keep test setup minimal and readable.
|
||||||
|
* Update tests when refactoring changes structure but not behavior.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
* Keep UI, application, domain and data-access concerns separated where practical.
|
||||||
|
* Organize code by feature rather than by technical file type when it improves cohesion.
|
||||||
|
* Avoid circular dependencies.
|
||||||
|
* Avoid cross-feature imports without a clear contract.
|
||||||
|
* Keep shared code genuinely shared.
|
||||||
|
* Do not move feature-specific logic into generic shared modules.
|
||||||
|
* Avoid dumping unrelated helpers into global utility files.
|
||||||
|
* Keep public APIs of feature areas small.
|
||||||
|
* Avoid exposing internal implementation details across feature boundaries.
|
||||||
|
* Prefer local ownership of state and logic.
|
||||||
|
|
||||||
|
## Code Quality
|
||||||
|
|
||||||
|
* Use strict TypeScript settings.
|
||||||
|
* Avoid `any`.
|
||||||
|
* Avoid unnecessary type assertions.
|
||||||
|
* Avoid non-null assertions unless safety is guaranteed.
|
||||||
|
* Prefer modern, readable syntax.
|
||||||
|
* Use descriptive names.
|
||||||
|
* Avoid vague names such as `data`, `item`, `value` or `temp` when better names exist.
|
||||||
|
* Remove dead code.
|
||||||
|
* Remove unused imports and dependencies.
|
||||||
|
* Remove commented-out code instead of keeping it in source files.
|
||||||
|
* Do not add comments unless the user explicitly requests them.
|
||||||
|
* Treat Angular, TypeScript and lint warnings seriously.
|
||||||
|
* Do not suppress diagnostics without a concrete reason.
|
||||||
|
* Prefer correctness, security and maintainability over cleverness.
|
||||||
|
|
||||||
|
## Dependency Rules
|
||||||
|
|
||||||
|
* Prefer Angular and browser platform capabilities before adding third-party libraries.
|
||||||
|
* Do not add dependencies without a clear benefit.
|
||||||
|
* Avoid libraries that duplicate existing framework functionality.
|
||||||
|
* Avoid abandoned or poorly maintained packages.
|
||||||
|
* Keep Angular package versions aligned.
|
||||||
|
* Do not mix incompatible Angular package versions.
|
||||||
|
* Respect the existing lockfile and package manager.
|
||||||
|
* Avoid unrelated dependency upgrades.
|
||||||
@@ -16,8 +16,7 @@ A task is only considered done when all applicable requirements below are fulfil
|
|||||||
* No temporary code, debug output, commented-out code, placeholders, or TODOs remain unless explicitly intended.
|
* No temporary code, debug output, commented-out code, placeholders, or TODOs remain unless explicitly intended.
|
||||||
* Error handling and edge cases are handled appropriately.
|
* Error handling and edge cases are handled appropriately.
|
||||||
* The implementation is readable, maintainable, and production-ready.
|
* The implementation is readable, maintainable, and production-ready.
|
||||||
* Existing documentation was not changed automatically. If the code makes it inaccurate, the user was asked before proceeding.
|
* Documentation and comments follow `general.md`'s Documentation and Comments rules (not changed automatically, none created without explicit instruction).
|
||||||
* No new documentation, repository comments, pull request text, or work-item text was created without explicit user instruction.
|
|
||||||
* Any delegated research, review, or verification is complete and its relevant findings are resolved or reported.
|
* Any delegated research, review, or verification is complete and its relevant findings are resolved or reported.
|
||||||
|
|
||||||
## Work Items
|
## Work Items
|
||||||
|
|||||||
+12
-2
@@ -13,19 +13,29 @@ Apply instructions in this order:
|
|||||||
|
|
||||||
Project instructions may specialize global defaults, but must not weaken security or data-loss protections without explicit user approval.
|
Project instructions may specialize global defaults, but must not weaken security or data-loss protections without explicit user approval.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
* Make evidence-based, reversible technical decisions independently; ask only when material uncertainty, multiple meaningful options, or high-impact/hard-to-reverse effects remain.
|
||||||
|
* Ask about business behavior, user-visible behavior, UI, UX, APIs, database models, configuration formats, or compatibility behavior only when it is missing, ambiguous, contradictory, or open to interpretation; implement explicitly specified behavior without asking again.
|
||||||
|
* For deeper guidance on ambiguous requirements or a meaningful technical choice, consult `decision-rule.md` (Codex: read `rules/decision-rule.md`; Claude: invoke the `rules-decision-rule` skill).
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
* Never modify existing documentation automatically.
|
* Never modify existing documentation automatically.
|
||||||
* If a code change would make existing documentation inaccurate or outdated, ask the user before changing the code or documentation.
|
|
||||||
* Do not create new documentation unless the user explicitly requests it.
|
* Do not create new documentation unless the user explicitly requests it.
|
||||||
* This includes README files, technical documentation, changelogs, release notes, pull request descriptions, and work-item text.
|
* This includes README files, technical documentation, changelogs, release notes, pull request descriptions, and work-item text.
|
||||||
|
* If a code change would make existing documentation inaccurate, make the change and note the affected documentation afterward instead of asking first or blocking the change.
|
||||||
|
|
||||||
## Comments
|
## Comments
|
||||||
|
|
||||||
* Do not add new code comments without the user's explicit approval.
|
* Do not add new code comments without the user's explicit approval.
|
||||||
* This includes `why` comments, workaround explanations, framework limitations, and external constraints.
|
* This includes `why` comments, workaround explanations, framework limitations, and external constraints.
|
||||||
* If a comment appears technically useful or necessary, ask the user before adding it.
|
|
||||||
* Prefer self-explanatory code and clear names.
|
* Prefer self-explanatory code and clear names.
|
||||||
|
* Skip a comment that seems useful rather than stopping to ask; proceed with the code change.
|
||||||
|
|
||||||
|
## Tool output
|
||||||
|
|
||||||
|
* Prefer a script's documented summary mode for routine agent runs; keep warnings and failure diagnostics visible. Use detailed output when investigating a failure or when requested.
|
||||||
|
|
||||||
## Language
|
## Language
|
||||||
|
|
||||||
|
|||||||
@@ -1,306 +0,0 @@
|
|||||||
# UI & UX Guidelines
|
|
||||||
|
|
||||||
* Design for clarity first.
|
|
||||||
* Prefer simple, modern and predictable interfaces.
|
|
||||||
* Optimize for fast understanding and low cognitive load.
|
|
||||||
* Every screen should have a clear primary purpose.
|
|
||||||
* Every important action should be easy to discover.
|
|
||||||
* Prefer familiar interaction patterns over novel ones.
|
|
||||||
* Do not require users to learn unnecessary custom behavior.
|
|
||||||
* Keep layouts visually calm and structured.
|
|
||||||
* Use consistent spacing, typography, colors and interaction patterns.
|
|
||||||
* Avoid visual noise.
|
|
||||||
* Avoid decorative elements that do not improve usability.
|
|
||||||
* Avoid unnecessary complexity in navigation and workflows.
|
|
||||||
* Minimize the number of steps required to complete common tasks.
|
|
||||||
* Keep related information and actions close together.
|
|
||||||
* Group content logically.
|
|
||||||
* Use clear visual hierarchy.
|
|
||||||
* Make primary actions visually distinct from secondary actions.
|
|
||||||
* Avoid giving multiple actions equal visual priority when one is clearly more important.
|
|
||||||
* Use whitespace intentionally.
|
|
||||||
* Prefer readable line lengths and sufficient spacing.
|
|
||||||
* Avoid dense walls of content.
|
|
||||||
* Avoid oversized empty areas that reduce information efficiency.
|
|
||||||
* Keep important information visible without unnecessary scrolling where practical.
|
|
||||||
|
|
||||||
## Navigation
|
|
||||||
|
|
||||||
* Navigation must be predictable and consistent.
|
|
||||||
* Users should always understand where they are.
|
|
||||||
* Users should always understand how to go back.
|
|
||||||
* Avoid hidden navigation when visible navigation is practical.
|
|
||||||
* Keep navigation depth shallow where possible.
|
|
||||||
* Use clear and descriptive navigation labels.
|
|
||||||
* Do not use ambiguous icons without labels when meaning is not obvious.
|
|
||||||
* Preserve navigation state when users return to a previous view where practical.
|
|
||||||
* Avoid resetting filters, search state or scroll position unnecessarily.
|
|
||||||
* Avoid unexpected redirects.
|
|
||||||
* Do not change navigation behavior between similar screens without a clear reason.
|
|
||||||
|
|
||||||
## Actions
|
|
||||||
|
|
||||||
* Use clear action labels.
|
|
||||||
* Prefer verbs that describe the actual outcome.
|
|
||||||
* Avoid vague labels such as `OK`, `Submit`, `Proceed` or `Execute` when a more specific label is possible.
|
|
||||||
* Make destructive actions clearly distinguishable.
|
|
||||||
* Keep dangerous actions away from common actions.
|
|
||||||
* Require confirmation for destructive or difficult-to-reverse actions.
|
|
||||||
* Do not require unnecessary confirmation for safe, reversible actions.
|
|
||||||
* Disable unavailable actions when the reason is obvious.
|
|
||||||
* Explain why an action is unavailable when it is not obvious.
|
|
||||||
* Avoid actions that appear clickable but are not.
|
|
||||||
* Provide immediate visual feedback after user actions.
|
|
||||||
* Prevent accidental duplicate submissions.
|
|
||||||
* Make loading, success and failure states visible.
|
|
||||||
|
|
||||||
## Forms
|
|
||||||
|
|
||||||
* Keep forms as short as practical.
|
|
||||||
* Ask only for information that is actually required.
|
|
||||||
* Group related fields together.
|
|
||||||
* Use clear field labels.
|
|
||||||
* Do not rely on placeholders as the only label.
|
|
||||||
* Use appropriate input controls for the expected value.
|
|
||||||
* Provide useful defaults where safe and predictable.
|
|
||||||
* Preserve user input when validation or network errors occur.
|
|
||||||
* Validate fields at useful moments without interrupting normal input.
|
|
||||||
* Show validation errors close to the affected field.
|
|
||||||
* Explain how to fix invalid input.
|
|
||||||
* Avoid technical validation messages.
|
|
||||||
* Clearly mark optional fields where useful.
|
|
||||||
* Avoid asking the same information multiple times.
|
|
||||||
* Use autocomplete and autofill where appropriate.
|
|
||||||
* Make keyboard navigation logical.
|
|
||||||
* Use sensible tab order.
|
|
||||||
* Support Enter and Escape behavior where users expect it.
|
|
||||||
|
|
||||||
## Content and Language
|
|
||||||
|
|
||||||
* Use plain, concise language.
|
|
||||||
* Prefer user terminology over internal technical terminology.
|
|
||||||
* Avoid jargon unless the target users are expected to understand it.
|
|
||||||
* Use consistent naming for the same concepts.
|
|
||||||
* Do not rename concepts between screens.
|
|
||||||
* Keep button labels, headings and descriptions concise.
|
|
||||||
* Put the most important information first.
|
|
||||||
* Avoid unnecessary explanatory text.
|
|
||||||
* Add explanation only where users may reasonably be confused.
|
|
||||||
* Error messages should explain what happened and what the user can do next.
|
|
||||||
* Avoid blaming the user.
|
|
||||||
* Avoid exposing stack traces, exception names or internal implementation details.
|
|
||||||
|
|
||||||
## Feedback and System State
|
|
||||||
|
|
||||||
* Always communicate important system state.
|
|
||||||
* Show loading states for operations that are not immediate.
|
|
||||||
* Avoid indefinite loading indicators without context.
|
|
||||||
* Use progress indicators for longer operations where progress can be measured.
|
|
||||||
* Clearly distinguish loading, empty, error and success states.
|
|
||||||
* Do not leave blank screens where an empty state is expected.
|
|
||||||
* Empty states should explain what the user can do next.
|
|
||||||
* Show success feedback when the result is not otherwise obvious.
|
|
||||||
* Keep transient notifications concise.
|
|
||||||
* Do not hide important errors in temporary notifications.
|
|
||||||
* Preserve actionable errors until the user has had a chance to understand them.
|
|
||||||
* Do not silently fail.
|
|
||||||
* Do not silently discard user input.
|
|
||||||
|
|
||||||
## Responsiveness
|
|
||||||
|
|
||||||
* The interface should feel responsive at all times.
|
|
||||||
* Provide immediate feedback after interaction.
|
|
||||||
* Avoid blocking the entire interface when only one section is loading.
|
|
||||||
* Prefer localized loading states.
|
|
||||||
* Use optimistic UI only when failures can be handled safely.
|
|
||||||
* Avoid layout jumps during loading.
|
|
||||||
* Reserve space for content where practical.
|
|
||||||
* Keep animations short and functional.
|
|
||||||
* Do not use animations that delay task completion.
|
|
||||||
* Respect reduced-motion preferences.
|
|
||||||
* Avoid unnecessary transitions and visual effects.
|
|
||||||
|
|
||||||
## Visual Design
|
|
||||||
|
|
||||||
* Prefer modern, clean and restrained visual design.
|
|
||||||
* Use a consistent design system.
|
|
||||||
* Keep typography hierarchy clear.
|
|
||||||
* Maintain sufficient contrast.
|
|
||||||
* Use color intentionally.
|
|
||||||
* Do not rely on color alone to communicate state.
|
|
||||||
* Keep the number of accent colors limited.
|
|
||||||
* Avoid excessive gradients, shadows and decorative effects.
|
|
||||||
* Use rounded corners, borders and elevation consistently.
|
|
||||||
* Keep icons stylistically consistent.
|
|
||||||
* Use familiar icons for familiar actions.
|
|
||||||
* Pair unclear icons with text labels.
|
|
||||||
* Avoid tiny click targets.
|
|
||||||
* Keep interactive targets large enough for comfortable use.
|
|
||||||
* Maintain consistent alignment.
|
|
||||||
* Avoid arbitrary spacing values.
|
|
||||||
* Prefer reusable spacing and sizing tokens.
|
|
||||||
|
|
||||||
## Accessibility
|
|
||||||
|
|
||||||
* Accessibility is part of the default design, not an optional enhancement.
|
|
||||||
* Use semantic controls and elements.
|
|
||||||
* Ensure full keyboard usability.
|
|
||||||
* Maintain visible focus indicators.
|
|
||||||
* Use sufficient color contrast.
|
|
||||||
* Do not rely solely on hover interactions.
|
|
||||||
* Do not rely solely on color to indicate status.
|
|
||||||
* Provide accessible names for controls.
|
|
||||||
* Keep focus order logical.
|
|
||||||
* Move focus intentionally after dialogs, navigation or major state changes.
|
|
||||||
* Support screen readers where applicable.
|
|
||||||
* Use ARIA only when native semantics are insufficient.
|
|
||||||
* Respect reduced-motion and system accessibility preferences.
|
|
||||||
* Avoid flashing or distracting content.
|
|
||||||
|
|
||||||
## Responsive Design
|
|
||||||
|
|
||||||
* Design for the available viewport instead of fixed screen sizes.
|
|
||||||
* Prioritize important content on smaller screens.
|
|
||||||
* Avoid horizontal scrolling unless the content genuinely requires it.
|
|
||||||
* Keep controls usable on touch devices.
|
|
||||||
* Do not simply shrink desktop layouts.
|
|
||||||
* Adapt navigation and layout intentionally for smaller screens.
|
|
||||||
* Preserve core functionality across supported screen sizes.
|
|
||||||
* Avoid hiding important actions solely because space is limited.
|
|
||||||
* Test common breakpoints and extreme content lengths.
|
|
||||||
|
|
||||||
## Tables and Data-Dense Views
|
|
||||||
|
|
||||||
* Use tables only for genuinely tabular data.
|
|
||||||
* Keep column names concise.
|
|
||||||
* Align numeric data consistently.
|
|
||||||
* Make sorting and filtering discoverable.
|
|
||||||
* Preserve filter and sort state where useful.
|
|
||||||
* Do not overload tables with too many actions.
|
|
||||||
* Prefer a clear primary row action and secondary contextual actions.
|
|
||||||
* Keep important columns visible.
|
|
||||||
* Allow horizontal scrolling only when necessary.
|
|
||||||
* Use pagination, virtualization or incremental loading for large data sets.
|
|
||||||
* Provide clear empty states.
|
|
||||||
* Avoid showing unnecessary columns by default.
|
|
||||||
* Use sensible formatting for dates, numbers and units.
|
|
||||||
|
|
||||||
## Search and Filtering
|
|
||||||
|
|
||||||
* Search should behave predictably.
|
|
||||||
* Make active filters visible.
|
|
||||||
* Make filters easy to remove.
|
|
||||||
* Provide a clear way to reset filters.
|
|
||||||
* Preserve search and filter state when navigating back where practical.
|
|
||||||
* Avoid requiring users to configure many filters before seeing results.
|
|
||||||
* Use sensible defaults.
|
|
||||||
* Clearly distinguish no results from loading or errors.
|
|
||||||
* Suggest recovery options when no results are found.
|
|
||||||
|
|
||||||
## Dialogs and Modals
|
|
||||||
|
|
||||||
* Use dialogs only when interruption is justified.
|
|
||||||
* Avoid stacking dialogs.
|
|
||||||
* Keep dialogs focused on one decision or task.
|
|
||||||
* Use clear titles.
|
|
||||||
* Keep primary and secondary actions obvious.
|
|
||||||
* Make cancellation easy.
|
|
||||||
* Support Escape where appropriate.
|
|
||||||
* Do not use dialogs for information that belongs inline.
|
|
||||||
* Avoid large workflows inside modal dialogs.
|
|
||||||
* Do not close dialogs unexpectedly while users are entering data.
|
|
||||||
|
|
||||||
## Destructive Actions
|
|
||||||
|
|
||||||
* Clearly label destructive actions.
|
|
||||||
* Use confirmations for irreversible or high-impact operations.
|
|
||||||
* Explain the consequence before confirmation.
|
|
||||||
* Prefer reversible actions when possible.
|
|
||||||
* Offer undo where practical.
|
|
||||||
* Do not use generic confirmation text for destructive operations.
|
|
||||||
* Make the safe action visually distinct.
|
|
||||||
* Do not preselect destructive choices.
|
|
||||||
|
|
||||||
## Error Prevention
|
|
||||||
|
|
||||||
* Prevent invalid actions before they occur where practical.
|
|
||||||
* Use constraints and suitable controls instead of relying only on validation errors.
|
|
||||||
* Provide sensible defaults.
|
|
||||||
* Confirm only high-impact decisions.
|
|
||||||
* Warn users before losing unsaved changes.
|
|
||||||
* Do not clear entered data unexpectedly.
|
|
||||||
* Avoid ambiguous state transitions.
|
|
||||||
* Make dependencies between fields or actions visible.
|
|
||||||
|
|
||||||
## Consistency
|
|
||||||
|
|
||||||
* Similar problems should have similar UI solutions.
|
|
||||||
* Similar actions should use the same wording and placement.
|
|
||||||
* Do not introduce custom patterns when an existing pattern already solves the problem.
|
|
||||||
* Reuse components and design tokens.
|
|
||||||
* Keep interaction behavior consistent across the application.
|
|
||||||
* Preserve platform conventions unless there is a strong reason not to.
|
|
||||||
|
|
||||||
## User Control
|
|
||||||
|
|
||||||
* Keep users in control of important actions.
|
|
||||||
* Do not perform surprising destructive actions automatically.
|
|
||||||
* Do not change user data without a clear action or established behavior.
|
|
||||||
* Make automatic behavior visible when it materially affects the user.
|
|
||||||
* Allow users to cancel long-running operations where practical.
|
|
||||||
* Allow recovery from mistakes where possible.
|
|
||||||
* Avoid dark patterns.
|
|
||||||
* Never manipulate users into actions they did not intend.
|
|
||||||
|
|
||||||
## Progressive Disclosure
|
|
||||||
|
|
||||||
* Show the most important options first.
|
|
||||||
* Hide advanced complexity until it is needed.
|
|
||||||
* Do not overwhelm users with every possible setting at once.
|
|
||||||
* Keep advanced options discoverable.
|
|
||||||
* Avoid forcing expert workflows on normal users.
|
|
||||||
* Avoid hiding frequently used functionality behind excessive menus.
|
|
||||||
|
|
||||||
## Performance Perception
|
|
||||||
|
|
||||||
* Optimize perceived performance as well as actual performance.
|
|
||||||
* Show useful content as early as possible.
|
|
||||||
* Avoid blocking initial rendering on non-critical data.
|
|
||||||
* Use skeletons only when they improve comprehension.
|
|
||||||
* Avoid fake progress.
|
|
||||||
* Do not use loading animations to mask avoidable slowness.
|
|
||||||
* Keep interactions responsive even while background work continues.
|
|
||||||
|
|
||||||
## Defaults
|
|
||||||
|
|
||||||
* Choose safe and sensible defaults.
|
|
||||||
* Defaults should match the most common expected user intent.
|
|
||||||
* Avoid defaults with destructive consequences.
|
|
||||||
* Preserve user preferences when appropriate.
|
|
||||||
* Do not force repeated configuration for common workflows.
|
|
||||||
|
|
||||||
## Design Decision Rule
|
|
||||||
|
|
||||||
* Every UI element should have a clear purpose.
|
|
||||||
* Every additional interaction adds cognitive cost.
|
|
||||||
* Prefer removing complexity over explaining unnecessary complexity.
|
|
||||||
* Prefer familiar patterns over clever ones.
|
|
||||||
* Prefer fewer clear choices over many ambiguous choices.
|
|
||||||
* Prefer predictable behavior over surprising automation.
|
|
||||||
* If a design decision introduces ambiguity, unnecessary complexity, or multiple equally valid UX directions, ask the user instead of deciding autonomously.
|
|
||||||
|
|
||||||
## Priority Order
|
|
||||||
|
|
||||||
Prioritize UI/UX decisions in this order:
|
|
||||||
|
|
||||||
* Correctness
|
|
||||||
* Clarity
|
|
||||||
* Safety
|
|
||||||
* Accessibility
|
|
||||||
* User control
|
|
||||||
* Task efficiency
|
|
||||||
* Consistency
|
|
||||||
* Responsiveness
|
|
||||||
* Visual polish
|
|
||||||
|
|
||||||
Visual polish must never reduce usability.
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# UI & UX Guidelines
|
||||||
|
|
||||||
|
* Design for clarity first.
|
||||||
|
* Prefer simple, modern and predictable interfaces.
|
||||||
|
* Optimize for fast understanding and low cognitive load.
|
||||||
|
* Every screen should have a clear primary purpose.
|
||||||
|
* Every important action should be easy to discover.
|
||||||
|
* Prefer familiar interaction patterns over novel ones.
|
||||||
|
* Do not require users to learn unnecessary custom behavior.
|
||||||
|
* Keep layouts visually calm and structured.
|
||||||
|
* Use consistent spacing, typography, colors and interaction patterns.
|
||||||
|
* Avoid visual noise.
|
||||||
|
* Avoid decorative elements that do not improve usability.
|
||||||
|
* Avoid unnecessary complexity in navigation and workflows.
|
||||||
|
* Minimize the number of steps required to complete common tasks.
|
||||||
|
* Keep related information and actions close together.
|
||||||
|
* Group content logically.
|
||||||
|
* Use clear visual hierarchy.
|
||||||
|
* Make primary actions visually distinct from secondary actions.
|
||||||
|
* Avoid giving multiple actions equal visual priority when one is clearly more important.
|
||||||
|
* Use whitespace intentionally.
|
||||||
|
* Prefer readable line lengths and sufficient spacing.
|
||||||
|
* Avoid dense walls of content.
|
||||||
|
* Avoid oversized empty areas that reduce information efficiency.
|
||||||
|
* Keep important information visible without unnecessary scrolling where practical.
|
||||||
|
|
||||||
|
## Design Decision Rule
|
||||||
|
|
||||||
|
* Every UI element should have a clear purpose.
|
||||||
|
* Every additional interaction adds cognitive cost.
|
||||||
|
* Prefer removing complexity over explaining unnecessary complexity.
|
||||||
|
* Prefer familiar patterns over clever ones.
|
||||||
|
* Prefer fewer clear choices over many ambiguous choices.
|
||||||
|
* Prefer predictable behavior over surprising automation.
|
||||||
|
* If a design decision introduces ambiguity, unnecessary complexity, or multiple equally valid UX directions, ask the user instead of deciding autonomously.
|
||||||
|
|
||||||
|
## Priority Order
|
||||||
|
|
||||||
|
Prioritize UI/UX decisions in this order:
|
||||||
|
|
||||||
|
* Correctness
|
||||||
|
* Clarity
|
||||||
|
* Safety
|
||||||
|
* Accessibility
|
||||||
|
* User control
|
||||||
|
* Task efficiency
|
||||||
|
* Consistency
|
||||||
|
* Responsiveness
|
||||||
|
* Visual polish
|
||||||
|
|
||||||
|
Visual polish must never reduce usability.
|
||||||
|
|
||||||
|
## Detailed guidance
|
||||||
|
|
||||||
|
Read the matching reference file before working in that area:
|
||||||
|
|
||||||
|
| Read this reference for... | ...this kind of work |
|
||||||
|
| --- | --- |
|
||||||
|
| `references/navigation-and-actions.md` | Navigation structure, action labeling and placement |
|
||||||
|
| `references/forms.md` | Form design and validation UX |
|
||||||
|
| `references/content-and-feedback.md` | Copy, loading/empty/error states, responsiveness |
|
||||||
|
| `references/visual-and-accessibility.md` | Visual design, accessibility, responsive layout |
|
||||||
|
| `references/tables-search-dialogs.md` | Data-dense views, search/filtering, dialogs/modals |
|
||||||
|
| `references/destructive-and-error-prevention.md` | Destructive actions, error prevention, consistency, user control, progressive disclosure, perceived performance, defaults |
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
## Content and Language
|
||||||
|
|
||||||
|
* Use plain, concise language.
|
||||||
|
* Prefer user terminology over internal technical terminology.
|
||||||
|
* Avoid jargon unless the target users are expected to understand it.
|
||||||
|
* Use consistent naming for the same concepts.
|
||||||
|
* Do not rename concepts between screens.
|
||||||
|
* Keep button labels, headings and descriptions concise.
|
||||||
|
* Put the most important information first.
|
||||||
|
* Avoid unnecessary explanatory text.
|
||||||
|
* Add explanation only where users may reasonably be confused.
|
||||||
|
* Error messages should explain what happened and what the user can do next.
|
||||||
|
* Avoid blaming the user.
|
||||||
|
* Avoid exposing stack traces, exception names or internal implementation details.
|
||||||
|
|
||||||
|
## Feedback and System State
|
||||||
|
|
||||||
|
* Always communicate important system state.
|
||||||
|
* Show loading states for operations that are not immediate.
|
||||||
|
* Avoid indefinite loading indicators without context.
|
||||||
|
* Use progress indicators for longer operations where progress can be measured.
|
||||||
|
* Clearly distinguish loading, empty, error and success states.
|
||||||
|
* Do not leave blank screens where an empty state is expected.
|
||||||
|
* Empty states should explain what the user can do next.
|
||||||
|
* Show success feedback when the result is not otherwise obvious.
|
||||||
|
* Keep transient notifications concise.
|
||||||
|
* Do not hide important errors in temporary notifications.
|
||||||
|
* Preserve actionable errors until the user has had a chance to understand them.
|
||||||
|
* Do not silently fail.
|
||||||
|
* Do not silently discard user input.
|
||||||
|
|
||||||
|
## Responsiveness
|
||||||
|
|
||||||
|
* The interface should feel responsive at all times.
|
||||||
|
* Provide immediate feedback after interaction.
|
||||||
|
* Avoid blocking the entire interface when only one section is loading.
|
||||||
|
* Prefer localized loading states.
|
||||||
|
* Use optimistic UI only when failures can be handled safely.
|
||||||
|
* Avoid layout jumps during loading.
|
||||||
|
* Reserve space for content where practical.
|
||||||
|
* Keep animations short and functional.
|
||||||
|
* Do not use animations that delay task completion.
|
||||||
|
* Respect reduced-motion preferences.
|
||||||
|
* Avoid unnecessary transitions and visual effects.
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
## Destructive Actions
|
||||||
|
|
||||||
|
* Clearly label destructive actions.
|
||||||
|
* Use confirmations for irreversible or high-impact operations.
|
||||||
|
* Explain the consequence before confirmation.
|
||||||
|
* Prefer reversible actions when possible.
|
||||||
|
* Offer undo where practical.
|
||||||
|
* Do not use generic confirmation text for destructive operations.
|
||||||
|
* Make the safe action visually distinct.
|
||||||
|
* Do not preselect destructive choices.
|
||||||
|
|
||||||
|
## Error Prevention
|
||||||
|
|
||||||
|
* Prevent invalid actions before they occur where practical.
|
||||||
|
* Use constraints and suitable controls instead of relying only on validation errors.
|
||||||
|
* Provide sensible defaults.
|
||||||
|
* Confirm only high-impact decisions.
|
||||||
|
* Warn users before losing unsaved changes.
|
||||||
|
* Do not clear entered data unexpectedly.
|
||||||
|
* Avoid ambiguous state transitions.
|
||||||
|
* Make dependencies between fields or actions visible.
|
||||||
|
|
||||||
|
## Consistency
|
||||||
|
|
||||||
|
* Similar problems should have similar UI solutions.
|
||||||
|
* Similar actions should use the same wording and placement.
|
||||||
|
* Do not introduce custom patterns when an existing pattern already solves the problem.
|
||||||
|
* Reuse components and design tokens.
|
||||||
|
* Keep interaction behavior consistent across the application.
|
||||||
|
* Preserve platform conventions unless there is a strong reason not to.
|
||||||
|
|
||||||
|
## User Control
|
||||||
|
|
||||||
|
* Keep users in control of important actions.
|
||||||
|
* Do not perform surprising destructive actions automatically.
|
||||||
|
* Do not change user data without a clear action or established behavior.
|
||||||
|
* Make automatic behavior visible when it materially affects the user.
|
||||||
|
* Allow users to cancel long-running operations where practical.
|
||||||
|
* Allow recovery from mistakes where possible.
|
||||||
|
* Avoid dark patterns.
|
||||||
|
* Never manipulate users into actions they did not intend.
|
||||||
|
|
||||||
|
## Progressive Disclosure
|
||||||
|
|
||||||
|
* Show the most important options first.
|
||||||
|
* Hide advanced complexity until it is needed.
|
||||||
|
* Do not overwhelm users with every possible setting at once.
|
||||||
|
* Keep advanced options discoverable.
|
||||||
|
* Avoid forcing expert workflows on normal users.
|
||||||
|
* Avoid hiding frequently used functionality behind excessive menus.
|
||||||
|
|
||||||
|
## Performance Perception
|
||||||
|
|
||||||
|
* Optimize perceived performance as well as actual performance.
|
||||||
|
* Show useful content as early as possible.
|
||||||
|
* Avoid blocking initial rendering on non-critical data.
|
||||||
|
* Use skeletons only when they improve comprehension.
|
||||||
|
* Avoid fake progress.
|
||||||
|
* Do not use loading animations to mask avoidable slowness.
|
||||||
|
* Keep interactions responsive even while background work continues.
|
||||||
|
|
||||||
|
## Defaults
|
||||||
|
|
||||||
|
* Choose safe and sensible defaults.
|
||||||
|
* Defaults should match the most common expected user intent.
|
||||||
|
* Avoid defaults with destructive consequences.
|
||||||
|
* Preserve user preferences when appropriate.
|
||||||
|
* Do not force repeated configuration for common workflows.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
## Forms
|
||||||
|
|
||||||
|
* Keep forms as short as practical.
|
||||||
|
* Ask only for information that is actually required.
|
||||||
|
* Group related fields together.
|
||||||
|
* Use clear field labels.
|
||||||
|
* Do not rely on placeholders as the only label.
|
||||||
|
* Use appropriate input controls for the expected value.
|
||||||
|
* Provide useful defaults where safe and predictable.
|
||||||
|
* Preserve user input when validation or network errors occur.
|
||||||
|
* Validate fields at useful moments without interrupting normal input.
|
||||||
|
* Show validation errors close to the affected field.
|
||||||
|
* Explain how to fix invalid input.
|
||||||
|
* Avoid technical validation messages.
|
||||||
|
* Clearly mark optional fields where useful.
|
||||||
|
* Avoid asking the same information multiple times.
|
||||||
|
* Use autocomplete and autofill where appropriate.
|
||||||
|
* Make keyboard navigation logical.
|
||||||
|
* Use sensible tab order.
|
||||||
|
* Support Enter and Escape behavior where users expect it.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
## Navigation
|
||||||
|
|
||||||
|
* Navigation must be predictable and consistent.
|
||||||
|
* Users should always understand where they are.
|
||||||
|
* Users should always understand how to go back.
|
||||||
|
* Avoid hidden navigation when visible navigation is practical.
|
||||||
|
* Keep navigation depth shallow where possible.
|
||||||
|
* Use clear and descriptive navigation labels.
|
||||||
|
* Do not use ambiguous icons without labels when meaning is not obvious.
|
||||||
|
* Preserve navigation state when users return to a previous view where practical.
|
||||||
|
* Avoid resetting filters, search state or scroll position unnecessarily.
|
||||||
|
* Avoid unexpected redirects.
|
||||||
|
* Do not change navigation behavior between similar screens without a clear reason.
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
* Use clear action labels.
|
||||||
|
* Prefer verbs that describe the actual outcome.
|
||||||
|
* Avoid vague labels such as `OK`, `Submit`, `Proceed` or `Execute` when a more specific label is possible.
|
||||||
|
* Make destructive actions clearly distinguishable.
|
||||||
|
* Keep dangerous actions away from common actions.
|
||||||
|
* Require confirmation for destructive or difficult-to-reverse actions.
|
||||||
|
* Do not require unnecessary confirmation for safe, reversible actions.
|
||||||
|
* Disable unavailable actions when the reason is obvious.
|
||||||
|
* Explain why an action is unavailable when it is not obvious.
|
||||||
|
* Avoid actions that appear clickable but are not.
|
||||||
|
* Provide immediate visual feedback after user actions.
|
||||||
|
* Prevent accidental duplicate submissions.
|
||||||
|
* Make loading, success and failure states visible.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
## Tables and Data-Dense Views
|
||||||
|
|
||||||
|
* Use tables only for genuinely tabular data.
|
||||||
|
* Keep column names concise.
|
||||||
|
* Align numeric data consistently.
|
||||||
|
* Make sorting and filtering discoverable.
|
||||||
|
* Preserve filter and sort state where useful.
|
||||||
|
* Do not overload tables with too many actions.
|
||||||
|
* Prefer a clear primary row action and secondary contextual actions.
|
||||||
|
* Keep important columns visible.
|
||||||
|
* Allow horizontal scrolling only when necessary.
|
||||||
|
* Use pagination, virtualization or incremental loading for large data sets.
|
||||||
|
* Provide clear empty states.
|
||||||
|
* Avoid showing unnecessary columns by default.
|
||||||
|
* Use sensible formatting for dates, numbers and units.
|
||||||
|
|
||||||
|
## Search and Filtering
|
||||||
|
|
||||||
|
* Search should behave predictably.
|
||||||
|
* Make active filters visible.
|
||||||
|
* Make filters easy to remove.
|
||||||
|
* Provide a clear way to reset filters.
|
||||||
|
* Preserve search and filter state when navigating back where practical.
|
||||||
|
* Avoid requiring users to configure many filters before seeing results.
|
||||||
|
* Use sensible defaults.
|
||||||
|
* Clearly distinguish no results from loading or errors.
|
||||||
|
* Suggest recovery options when no results are found.
|
||||||
|
|
||||||
|
## Dialogs and Modals
|
||||||
|
|
||||||
|
* Use dialogs only when interruption is justified.
|
||||||
|
* Avoid stacking dialogs.
|
||||||
|
* Keep dialogs focused on one decision or task.
|
||||||
|
* Use clear titles.
|
||||||
|
* Keep primary and secondary actions obvious.
|
||||||
|
* Make cancellation easy.
|
||||||
|
* Support Escape where appropriate.
|
||||||
|
* Do not use dialogs for information that belongs inline.
|
||||||
|
* Avoid large workflows inside modal dialogs.
|
||||||
|
* Do not close dialogs unexpectedly while users are entering data.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
## Visual Design
|
||||||
|
|
||||||
|
* Prefer modern, clean and restrained visual design.
|
||||||
|
* Use a consistent design system.
|
||||||
|
* Keep typography hierarchy clear.
|
||||||
|
* Maintain sufficient contrast.
|
||||||
|
* Use color intentionally.
|
||||||
|
* Do not rely on color alone to communicate state.
|
||||||
|
* Keep the number of accent colors limited.
|
||||||
|
* Avoid excessive gradients, shadows and decorative effects.
|
||||||
|
* Use rounded corners, borders and elevation consistently.
|
||||||
|
* Keep icons stylistically consistent.
|
||||||
|
* Use familiar icons for familiar actions.
|
||||||
|
* Pair unclear icons with text labels.
|
||||||
|
* Avoid tiny click targets.
|
||||||
|
* Keep interactive targets large enough for comfortable use.
|
||||||
|
* Maintain consistent alignment.
|
||||||
|
* Avoid arbitrary spacing values.
|
||||||
|
* Prefer reusable spacing and sizing tokens.
|
||||||
|
|
||||||
|
## Accessibility
|
||||||
|
|
||||||
|
* Accessibility is part of the default design, not an optional enhancement.
|
||||||
|
* Use semantic controls and elements.
|
||||||
|
* Ensure full keyboard usability.
|
||||||
|
* Maintain visible focus indicators.
|
||||||
|
* Use sufficient color contrast.
|
||||||
|
* Do not rely solely on hover interactions.
|
||||||
|
* Do not rely solely on color to indicate status.
|
||||||
|
* Provide accessible names for controls.
|
||||||
|
* Keep focus order logical.
|
||||||
|
* Move focus intentionally after dialogs, navigation or major state changes.
|
||||||
|
* Support screen readers where applicable.
|
||||||
|
* Use ARIA only when native semantics are insufficient.
|
||||||
|
* Respect reduced-motion and system accessibility preferences.
|
||||||
|
* Avoid flashing or distracting content.
|
||||||
|
|
||||||
|
## Responsive Design
|
||||||
|
|
||||||
|
* Design for the available viewport instead of fixed screen sizes.
|
||||||
|
* Prioritize important content on smaller screens.
|
||||||
|
* Avoid horizontal scrolling unless the content genuinely requires it.
|
||||||
|
* Keep controls usable on touch devices.
|
||||||
|
* Do not simply shrink desktop layouts.
|
||||||
|
* Adapt navigation and layout intentionally for smaller screens.
|
||||||
|
* Preserve core functionality across supported screen sizes.
|
||||||
|
* Avoid hiding important actions solely because space is limited.
|
||||||
|
* Test common breakpoints and extreme content lengths.
|
||||||
@@ -1,375 +0,0 @@
|
|||||||
# WPF Code Guidelines
|
|
||||||
|
|
||||||
* Use modern C# and current .NET/WPF conventions.
|
|
||||||
* Keep UI, application logic and domain logic clearly separated.
|
|
||||||
* Prefer MVVM for non-trivial views.
|
|
||||||
* Keep code-behind minimal.
|
|
||||||
* Use code-behind only for view-specific behavior that does not belong in the ViewModel.
|
|
||||||
* Avoid business logic in views.
|
|
||||||
* Avoid direct service access from controls.
|
|
||||||
* Keep dependencies explicit and testable.
|
|
||||||
* Prefer composition over inheritance.
|
|
||||||
* Avoid unnecessary abstractions and framework wrappers.
|
|
||||||
* Follow the existing project architecture unless there is a strong reason to change it.
|
|
||||||
|
|
||||||
## MVVM
|
|
||||||
|
|
||||||
* Keep ViewModels focused on presentation state and commands.
|
|
||||||
* Do not put UI element references into ViewModels.
|
|
||||||
* Do not access `Window`, `UserControl`, `DispatcherObject` or visual tree elements from ViewModels.
|
|
||||||
* Avoid direct navigation, dialogs or message boxes from ViewModels without an abstraction when testability matters.
|
|
||||||
* Prefer explicit application services for navigation, dialogs and external interactions.
|
|
||||||
* Keep domain logic outside ViewModels.
|
|
||||||
* Do not use ViewModels as general-purpose service containers.
|
|
||||||
* Keep ViewModel dependencies minimal.
|
|
||||||
* Prefer immutable or readonly state where practical.
|
|
||||||
* Avoid duplicated derived state.
|
|
||||||
* Keep state transitions predictable.
|
|
||||||
|
|
||||||
## Data Binding
|
|
||||||
|
|
||||||
* Prefer data binding over manual UI synchronization.
|
|
||||||
* Use strongly typed properties.
|
|
||||||
* Use appropriate binding modes.
|
|
||||||
* Prefer `OneWay` when two-way updates are not required.
|
|
||||||
* Use `TwoWay` only when the UI must modify the source.
|
|
||||||
* Avoid unnecessary `UpdateSourceTrigger=PropertyChanged` for expensive operations.
|
|
||||||
* Keep binding paths simple and stable.
|
|
||||||
* Avoid deeply nested binding paths.
|
|
||||||
* Do not hide broken bindings.
|
|
||||||
* Treat binding errors as real defects.
|
|
||||||
* Use `FallbackValue` and `TargetNullValue` only when they reflect intended behavior.
|
|
||||||
* Avoid excessive converters.
|
|
||||||
* Prefer exposing correctly shaped state from the ViewModel over complex converter logic.
|
|
||||||
* Avoid `MultiBinding` when simpler state modeling is clearer.
|
|
||||||
* Do not put business logic into converters.
|
|
||||||
|
|
||||||
## Property Change Notifications
|
|
||||||
|
|
||||||
* Implement property change notifications consistently.
|
|
||||||
* Avoid raising `PropertyChanged` unnecessarily.
|
|
||||||
* Raise notifications only when values actually change.
|
|
||||||
* Keep dependent property notifications explicit.
|
|
||||||
* Avoid hidden notification chains that are difficult to reason about.
|
|
||||||
* Prefer reusable base implementations only when they reduce duplication without obscuring behavior.
|
|
||||||
* Do not use reflection-based notification mechanisms unless there is a concrete benefit.
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
* Prefer commands over click handlers for ViewModel-driven actions.
|
|
||||||
* Keep command execution focused.
|
|
||||||
* Avoid putting large workflows directly inside command implementations.
|
|
||||||
* Keep `CanExecute` logic cheap.
|
|
||||||
* Refresh command availability only when relevant state changes.
|
|
||||||
* Avoid command implementations with hidden side effects.
|
|
||||||
* Handle async commands safely.
|
|
||||||
* Prevent accidental duplicate execution when an async action must not run concurrently.
|
|
||||||
* Never use `async void` except for true event handlers.
|
|
||||||
|
|
||||||
## Async and Threading
|
|
||||||
|
|
||||||
* Keep async operations asynchronous throughout the call chain.
|
|
||||||
* Avoid `.Result`, `.Wait()` and other synchronous blocking of async work.
|
|
||||||
* Do not block the UI thread.
|
|
||||||
* Run I/O asynchronously.
|
|
||||||
* Move expensive CPU work off the UI thread only when necessary.
|
|
||||||
* Marshal UI updates back to the UI thread when required.
|
|
||||||
* Keep Dispatcher usage minimal and explicit.
|
|
||||||
* Do not use the Dispatcher to hide incorrect threading design.
|
|
||||||
* Support `CancellationToken` for cancellable long-running operations.
|
|
||||||
* Cancel obsolete work when views or selections change.
|
|
||||||
* Prevent race conditions caused by overlapping async operations.
|
|
||||||
* Handle exceptions from background operations explicitly.
|
|
||||||
|
|
||||||
## UI Thread
|
|
||||||
|
|
||||||
* Treat the UI thread as a scarce resource.
|
|
||||||
* Avoid expensive loops, parsing, serialization, database access or network calls on the UI thread.
|
|
||||||
* Avoid excessive Dispatcher invocations.
|
|
||||||
* Batch UI updates where practical.
|
|
||||||
* Do not update UI-bound collections excessively in tight loops.
|
|
||||||
* Avoid synchronous waits on background work.
|
|
||||||
* Keep rendering-related callbacks lightweight.
|
|
||||||
|
|
||||||
## Collections
|
|
||||||
|
|
||||||
* Use `ObservableCollection<T>` only when collection change notifications are required.
|
|
||||||
* Do not use `ObservableCollection<T>` as a general-purpose collection.
|
|
||||||
* Use `List<T>` or other suitable collections for non-observable internal data.
|
|
||||||
* Avoid replacing entire observable collections when incremental updates are sufficient.
|
|
||||||
* Avoid thousands of individual collection notifications when a bulk update is more appropriate.
|
|
||||||
* Keep collection mutations on the correct thread.
|
|
||||||
* Avoid exposing mutable internal collections unnecessarily.
|
|
||||||
* Prefer read-only views where consumers should not modify data.
|
|
||||||
|
|
||||||
## XAML
|
|
||||||
|
|
||||||
* Keep XAML readable and focused.
|
|
||||||
* Avoid excessively large view files.
|
|
||||||
* Split complex views into focused controls when it improves maintainability.
|
|
||||||
* Use resources for repeated values and styles.
|
|
||||||
* Avoid magic numbers in XAML.
|
|
||||||
* Keep styles and templates consistent.
|
|
||||||
* Avoid excessive inline styling.
|
|
||||||
* Prefer reusable resources over duplicated markup.
|
|
||||||
* Avoid deeply nested panels.
|
|
||||||
* Keep visual trees as shallow as practical.
|
|
||||||
* Use semantic control choices.
|
|
||||||
* Avoid unnecessary containers.
|
|
||||||
* Do not overuse triggers when simpler state representation is possible.
|
|
||||||
* Keep templates understandable.
|
|
||||||
* Avoid XAML tricks that make behavior hard to discover.
|
|
||||||
|
|
||||||
## Layout
|
|
||||||
|
|
||||||
* Prefer `Grid` for structured layouts.
|
|
||||||
* Use `StackPanel` only where its layout behavior is actually appropriate.
|
|
||||||
* Avoid unnecessary nested panels.
|
|
||||||
* Avoid fixed sizes unless the design requires them.
|
|
||||||
* Prefer responsive sizing with `Auto` and `*`.
|
|
||||||
* Avoid layout configurations that cause excessive measure/arrange passes.
|
|
||||||
* Be careful with large visual trees inside scrolling containers.
|
|
||||||
* Avoid disabling virtualization accidentally.
|
|
||||||
|
|
||||||
## Virtualization
|
|
||||||
|
|
||||||
* Keep UI virtualization enabled for large collections.
|
|
||||||
* Do not use container layouts that disable virtualization without a reason.
|
|
||||||
* Avoid wrapping large virtualized item lists in additional scrolling containers.
|
|
||||||
* Use virtualizing panels where appropriate.
|
|
||||||
* Verify virtualization when displaying large data sets.
|
|
||||||
* Avoid rendering thousands of controls simultaneously.
|
|
||||||
* Prefer paging, incremental loading or virtualization for very large data sets.
|
|
||||||
|
|
||||||
## Resources and Styles
|
|
||||||
|
|
||||||
* Use shared resources for repeated styles, brushes, templates and dimensions.
|
|
||||||
* Keep resource dictionaries focused.
|
|
||||||
* Avoid giant global resource dictionaries.
|
|
||||||
* Prefer feature-level resources when styles are not truly global.
|
|
||||||
* Use `StaticResource` by default.
|
|
||||||
* Use `DynamicResource` only when runtime resource replacement is required.
|
|
||||||
* Avoid excessive dynamic resource lookups.
|
|
||||||
* Keep theme resources separated from application logic.
|
|
||||||
* Avoid hardcoded colors and dimensions when design tokens or resources already exist.
|
|
||||||
|
|
||||||
## Dependency Properties
|
|
||||||
|
|
||||||
* Use dependency properties only when WPF property-system behavior is required.
|
|
||||||
* Do not use dependency properties as a replacement for normal CLR properties.
|
|
||||||
* Keep metadata and callbacks lightweight.
|
|
||||||
* Avoid expensive work in property-changed callbacks.
|
|
||||||
* Avoid callbacks with hidden side effects.
|
|
||||||
* Validate or coerce values only when the control contract requires it.
|
|
||||||
* Do not expose mutable default values in dependency property metadata.
|
|
||||||
|
|
||||||
## Attached Properties and Behaviors
|
|
||||||
|
|
||||||
* Use attached properties and behaviors for reusable view-specific behavior.
|
|
||||||
* Do not use them to hide business logic.
|
|
||||||
* Keep behaviors focused and predictable.
|
|
||||||
* Clean up event handlers when behaviors are detached.
|
|
||||||
* Avoid global attached-property state.
|
|
||||||
* Prefer explicit behavior over complex XAML hacks.
|
|
||||||
|
|
||||||
## Events
|
|
||||||
|
|
||||||
* Avoid unnecessary event subscriptions.
|
|
||||||
* Unsubscribe from long-lived publishers when required.
|
|
||||||
* Prevent memory leaks from event handlers.
|
|
||||||
* Prefer weak events where publisher lifetime significantly exceeds subscriber lifetime.
|
|
||||||
* Keep event handlers small.
|
|
||||||
* Do not use events as hidden global communication channels.
|
|
||||||
* Avoid excessive event chaining.
|
|
||||||
|
|
||||||
## Memory Management
|
|
||||||
|
|
||||||
* Watch for memory leaks caused by:
|
|
||||||
|
|
||||||
* Event subscriptions
|
|
||||||
* Timers
|
|
||||||
* Static references
|
|
||||||
* Long-lived services
|
|
||||||
* Cached views
|
|
||||||
* Closures
|
|
||||||
* Collection views
|
|
||||||
* Binding-related references
|
|
||||||
* Dispose resources correctly.
|
|
||||||
* Stop timers and background work when no longer needed.
|
|
||||||
* Avoid retaining closed windows or unloaded views.
|
|
||||||
* Do not keep unnecessary references to visual elements.
|
|
||||||
* Profile memory when long-running applications continuously grow.
|
|
||||||
|
|
||||||
## Windows and Dialogs
|
|
||||||
|
|
||||||
* Keep window lifecycle explicit.
|
|
||||||
* Avoid creating duplicate windows unintentionally.
|
|
||||||
* Do not keep hidden windows alive without a reason.
|
|
||||||
* Separate dialog logic from business logic.
|
|
||||||
* Prefer dialog services when ViewModels need to request user interaction.
|
|
||||||
* Keep ownership relationships explicit.
|
|
||||||
* Avoid application shutdown behavior that depends on accidental window lifetime.
|
|
||||||
|
|
||||||
## Navigation
|
|
||||||
|
|
||||||
* Keep navigation state explicit.
|
|
||||||
* Avoid coupling ViewModels directly to concrete views.
|
|
||||||
* Avoid service locator patterns for view resolution.
|
|
||||||
* Keep navigation history intentional.
|
|
||||||
* Dispose or release navigation targets when they are no longer required.
|
|
||||||
* Do not retain entire navigation graphs unnecessarily.
|
|
||||||
|
|
||||||
## Validation
|
|
||||||
|
|
||||||
* Validate user input at the appropriate boundary.
|
|
||||||
* Keep validation logic out of XAML when it becomes complex.
|
|
||||||
* Prefer reusable validation rules or ViewModel validation for non-trivial cases.
|
|
||||||
* Display validation errors consistently.
|
|
||||||
* Do not rely on UI validation as a security boundary.
|
|
||||||
* Validate again at backend or domain boundaries when required.
|
|
||||||
* Preserve user input on recoverable errors.
|
|
||||||
|
|
||||||
## Performance
|
|
||||||
|
|
||||||
* Keep the UI thread responsive.
|
|
||||||
* Minimize visual tree depth.
|
|
||||||
* Keep virtualization enabled.
|
|
||||||
* Avoid unnecessary bindings.
|
|
||||||
* Avoid unnecessary converters.
|
|
||||||
* Avoid unnecessary resource lookups.
|
|
||||||
* Avoid unnecessary property change notifications.
|
|
||||||
* Avoid frequent full collection refreshes.
|
|
||||||
* Avoid loading large datasets eagerly.
|
|
||||||
* Avoid loading large images at full resolution when unnecessary.
|
|
||||||
* Freeze `Freezable` objects where practical.
|
|
||||||
* Reuse immutable resources where appropriate.
|
|
||||||
* Avoid unnecessary animations.
|
|
||||||
* Avoid high-frequency timers on the UI thread.
|
|
||||||
* Measure startup time, UI responsiveness, memory usage and rendering performance before micro-optimizing.
|
|
||||||
|
|
||||||
## Images and Media
|
|
||||||
|
|
||||||
* Load images at an appropriate resolution.
|
|
||||||
* Avoid decoding images significantly larger than their displayed size.
|
|
||||||
* Release streams and file handles correctly.
|
|
||||||
* Avoid locking image files unintentionally.
|
|
||||||
* Cache images only when there is a clear memory strategy.
|
|
||||||
* Avoid keeping large bitmaps alive unnecessarily.
|
|
||||||
* Prefer frozen image resources when they are immutable.
|
|
||||||
|
|
||||||
## Security
|
|
||||||
|
|
||||||
* Treat all external input as untrusted.
|
|
||||||
* Never expose secrets, credentials or tokens in UI code.
|
|
||||||
* Do not store secrets in configuration files committed to source control.
|
|
||||||
* Avoid leaking sensitive information through logs, dialogs or exception messages.
|
|
||||||
* Validate file paths and external resources.
|
|
||||||
* Do not execute external processes or files without explicit validation.
|
|
||||||
* Avoid insecure deserialization.
|
|
||||||
* Do not weaken TLS, authentication or certificate validation.
|
|
||||||
* Prefer secure defaults.
|
|
||||||
* Never disable security controls for convenience.
|
|
||||||
|
|
||||||
## File and Process Access
|
|
||||||
|
|
||||||
* Validate file paths before use.
|
|
||||||
* Avoid assuming files or directories exist.
|
|
||||||
* Handle permissions and locked files gracefully.
|
|
||||||
* Avoid arbitrary shell execution.
|
|
||||||
* Quote and validate process arguments safely.
|
|
||||||
* Do not concatenate untrusted input into shell commands.
|
|
||||||
* Avoid elevated privileges unless explicitly required.
|
|
||||||
* Clean up temporary files when appropriate.
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
* Handle expected errors explicitly.
|
|
||||||
* Do not swallow exceptions.
|
|
||||||
* Preserve useful exception context.
|
|
||||||
* Avoid showing raw technical exceptions directly to end users.
|
|
||||||
* Log enough information for diagnosis without exposing sensitive data.
|
|
||||||
* Keep user-facing error messages clear and actionable.
|
|
||||||
* Avoid using exceptions for normal control flow.
|
|
||||||
* Ensure background exceptions are observed.
|
|
||||||
|
|
||||||
## Logging
|
|
||||||
|
|
||||||
* Use structured logging.
|
|
||||||
* Avoid excessive logging on the UI thread.
|
|
||||||
* Do not log every property change or binding update.
|
|
||||||
* Never log secrets, tokens, credentials or sensitive user data.
|
|
||||||
* Include relevant context for operational failures.
|
|
||||||
* Avoid logging the same exception repeatedly at multiple layers.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
* Keep presentation, application, domain and infrastructure concerns separated.
|
|
||||||
* Avoid direct database access from views or ViewModels.
|
|
||||||
* Avoid direct HTTP access from controls.
|
|
||||||
* Keep external integrations behind focused services.
|
|
||||||
* Avoid circular dependencies.
|
|
||||||
* Keep feature boundaries clear.
|
|
||||||
* Do not create shared utility classes for unrelated functionality.
|
|
||||||
* Keep shared code genuinely reusable.
|
|
||||||
* Minimize global state.
|
|
||||||
* Avoid static service access.
|
|
||||||
|
|
||||||
## Dependency Injection
|
|
||||||
|
|
||||||
* Use dependency injection where lifecycle management, substitution or testability benefits from it.
|
|
||||||
* Avoid service locator patterns.
|
|
||||||
* Keep constructor dependencies minimal.
|
|
||||||
* Do not inject dependencies that are not used.
|
|
||||||
* Choose service lifetimes intentionally.
|
|
||||||
* Do not make stateful services global without a concrete reason.
|
|
||||||
* Avoid introducing interfaces solely for dependency injection when no abstraction is needed.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
* Keep ViewModels testable without a UI thread where practical.
|
|
||||||
* Test behavior rather than implementation details.
|
|
||||||
* Test domain and application logic independently from WPF.
|
|
||||||
* Avoid brittle tests coupled to XAML structure.
|
|
||||||
* Keep UI automation tests focused on critical flows.
|
|
||||||
* Mock external boundaries rather than internal details.
|
|
||||||
* Keep tests deterministic.
|
|
||||||
* Avoid real network, database or filesystem dependencies in unit tests unless explicitly intended.
|
|
||||||
|
|
||||||
## Code Quality
|
|
||||||
|
|
||||||
* Use nullable reference types.
|
|
||||||
* Prefer modern, readable C# syntax.
|
|
||||||
* Avoid unnecessary interfaces and abstractions.
|
|
||||||
* Prefer concrete types where no abstraction is required.
|
|
||||||
* Use descriptive names.
|
|
||||||
* Remove dead code.
|
|
||||||
* Remove commented-out code.
|
|
||||||
* Do not add comments unless the user explicitly requests them.
|
|
||||||
* Treat compiler, analyzer and binding warnings seriously.
|
|
||||||
* Do not suppress warnings without a concrete reason.
|
|
||||||
* Prefer correctness, responsiveness, security and maintainability over cleverness.
|
|
||||||
|
|
||||||
## Performance Priority
|
|
||||||
|
|
||||||
Prioritize WPF performance work in this order:
|
|
||||||
|
|
||||||
* UI architecture
|
|
||||||
* UI thread blocking
|
|
||||||
* Data loading and I/O
|
|
||||||
* Virtualization
|
|
||||||
* Visual tree complexity
|
|
||||||
* Binding and notification frequency
|
|
||||||
* Rendering and images
|
|
||||||
* Memory retention and leaks
|
|
||||||
* Allocations
|
|
||||||
* CPU micro-optimizations
|
|
||||||
|
|
||||||
Never optimize based purely on assumptions.
|
|
||||||
|
|
||||||
## Safety Rule
|
|
||||||
|
|
||||||
* The UI must remain responsive.
|
|
||||||
* Do not block the UI thread with I/O or expensive CPU work.
|
|
||||||
* Do not bypass validation or security controls for convenience.
|
|
||||||
* Do not introduce hidden cross-thread access.
|
|
||||||
* Prefer predictable, explicit and testable UI behavior.
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# WPF Code Guidelines
|
||||||
|
|
||||||
|
* Use modern C# and current .NET/WPF conventions.
|
||||||
|
* Keep UI, application logic and domain logic clearly separated.
|
||||||
|
* Prefer MVVM for non-trivial views.
|
||||||
|
* Keep code-behind minimal.
|
||||||
|
* Use code-behind only for view-specific behavior that does not belong in the ViewModel.
|
||||||
|
* Avoid business logic in views.
|
||||||
|
* Avoid direct service access from controls.
|
||||||
|
* Keep dependencies explicit and testable.
|
||||||
|
* Prefer composition over inheritance.
|
||||||
|
* Avoid unnecessary abstractions and framework wrappers.
|
||||||
|
* Follow the existing project architecture unless there is a strong reason to change it.
|
||||||
|
|
||||||
|
## Safety Rule
|
||||||
|
|
||||||
|
* The UI must remain responsive.
|
||||||
|
* Do not block the UI thread with I/O or expensive CPU work.
|
||||||
|
* Do not bypass validation or security controls for convenience.
|
||||||
|
* Do not introduce hidden cross-thread access.
|
||||||
|
* Prefer predictable, explicit and testable UI behavior.
|
||||||
|
|
||||||
|
## Detailed guidance
|
||||||
|
|
||||||
|
Read the matching reference file before working in that area:
|
||||||
|
|
||||||
|
| Read this reference for... | ...this kind of work |
|
||||||
|
| --- | --- |
|
||||||
|
| `references/mvvm-and-binding.md` | MVVM, data binding, property change notifications, dependency/attached properties |
|
||||||
|
| `references/commands-and-async.md` | Commands, async/threading, UI-thread work, events |
|
||||||
|
| `references/collections-and-xaml.md` | Collections, virtualization, layout, XAML |
|
||||||
|
| `references/resources-and-lifecycle.md` | Resources/styles, windows/dialogs, navigation, memory, images/media |
|
||||||
|
| `references/security-and-errors.md` | Security, file/process access, error handling, logging, validation |
|
||||||
|
| `references/architecture-and-quality.md` | Architecture, DI, testing, code quality, performance |
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
## Architecture
|
||||||
|
|
||||||
|
* Keep presentation, application, domain and infrastructure concerns separated.
|
||||||
|
* Avoid direct database access from views or ViewModels.
|
||||||
|
* Avoid direct HTTP access from controls.
|
||||||
|
* Keep external integrations behind focused services.
|
||||||
|
* Avoid circular dependencies.
|
||||||
|
* Keep feature boundaries clear.
|
||||||
|
* Do not create shared utility classes for unrelated functionality.
|
||||||
|
* Keep shared code genuinely reusable.
|
||||||
|
* Minimize global state.
|
||||||
|
* Avoid static service access.
|
||||||
|
|
||||||
|
## Dependency Injection
|
||||||
|
|
||||||
|
* Use dependency injection where lifecycle management, substitution or testability benefits from it.
|
||||||
|
* Avoid service locator patterns.
|
||||||
|
* Keep constructor dependencies minimal.
|
||||||
|
* Do not inject dependencies that are not used.
|
||||||
|
* Choose service lifetimes intentionally.
|
||||||
|
* Do not make stateful services global without a concrete reason.
|
||||||
|
* Avoid introducing interfaces solely for dependency injection when no abstraction is needed.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
* Keep ViewModels testable without a UI thread where practical.
|
||||||
|
* Test behavior rather than implementation details.
|
||||||
|
* Test domain and application logic independently from WPF.
|
||||||
|
* Avoid brittle tests coupled to XAML structure.
|
||||||
|
* Keep UI automation tests focused on critical flows.
|
||||||
|
* Mock external boundaries rather than internal details.
|
||||||
|
* Keep tests deterministic.
|
||||||
|
* Avoid real network, database or filesystem dependencies in unit tests unless explicitly intended.
|
||||||
|
|
||||||
|
## Code Quality
|
||||||
|
|
||||||
|
* Use nullable reference types.
|
||||||
|
* Prefer modern, readable C# syntax.
|
||||||
|
* Avoid unnecessary interfaces and abstractions.
|
||||||
|
* Prefer concrete types where no abstraction is required.
|
||||||
|
* Use descriptive names.
|
||||||
|
* Remove dead code.
|
||||||
|
* Remove commented-out code.
|
||||||
|
* Do not add comments unless the user explicitly requests them.
|
||||||
|
* Treat compiler, analyzer and binding warnings seriously.
|
||||||
|
* Do not suppress warnings without a concrete reason.
|
||||||
|
* Prefer correctness, responsiveness, security and maintainability over cleverness.
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
* Keep the UI thread responsive.
|
||||||
|
* Minimize visual tree depth.
|
||||||
|
* Keep virtualization enabled.
|
||||||
|
* Avoid unnecessary bindings.
|
||||||
|
* Avoid unnecessary converters.
|
||||||
|
* Avoid unnecessary resource lookups.
|
||||||
|
* Avoid unnecessary property change notifications.
|
||||||
|
* Avoid frequent full collection refreshes.
|
||||||
|
* Avoid loading large datasets eagerly.
|
||||||
|
* Avoid loading large images at full resolution when unnecessary.
|
||||||
|
* Freeze `Freezable` objects where practical.
|
||||||
|
* Reuse immutable resources where appropriate.
|
||||||
|
* Avoid unnecessary animations.
|
||||||
|
* Avoid high-frequency timers on the UI thread.
|
||||||
|
* Measure startup time, UI responsiveness, memory usage and rendering performance before micro-optimizing.
|
||||||
|
|
||||||
|
## Performance Priority
|
||||||
|
|
||||||
|
Prioritize WPF performance work in this order:
|
||||||
|
|
||||||
|
* UI architecture
|
||||||
|
* UI thread blocking
|
||||||
|
* Data loading and I/O
|
||||||
|
* Virtualization
|
||||||
|
* Visual tree complexity
|
||||||
|
* Binding and notification frequency
|
||||||
|
* Rendering and images
|
||||||
|
* Memory retention and leaks
|
||||||
|
* Allocations
|
||||||
|
* CPU micro-optimizations
|
||||||
|
|
||||||
|
Never optimize based purely on assumptions.
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
## Collections
|
||||||
|
|
||||||
|
* Use `ObservableCollection<T>` only when collection change notifications are required.
|
||||||
|
* Do not use `ObservableCollection<T>` as a general-purpose collection.
|
||||||
|
* Use `List<T>` or other suitable collections for non-observable internal data.
|
||||||
|
* Avoid replacing entire observable collections when incremental updates are sufficient.
|
||||||
|
* Avoid thousands of individual collection notifications when a bulk update is more appropriate.
|
||||||
|
* Keep collection mutations on the correct thread.
|
||||||
|
* Avoid exposing mutable internal collections unnecessarily.
|
||||||
|
* Prefer read-only views where consumers should not modify data.
|
||||||
|
|
||||||
|
## XAML
|
||||||
|
|
||||||
|
* Keep XAML readable and focused.
|
||||||
|
* Avoid excessively large view files.
|
||||||
|
* Split complex views into focused controls when it improves maintainability.
|
||||||
|
* Use resources for repeated values and styles.
|
||||||
|
* Avoid magic numbers in XAML.
|
||||||
|
* Keep styles and templates consistent.
|
||||||
|
* Avoid excessive inline styling.
|
||||||
|
* Prefer reusable resources over duplicated markup.
|
||||||
|
* Avoid deeply nested panels.
|
||||||
|
* Keep visual trees as shallow as practical.
|
||||||
|
* Use semantic control choices.
|
||||||
|
* Avoid unnecessary containers.
|
||||||
|
* Do not overuse triggers when simpler state representation is possible.
|
||||||
|
* Keep templates understandable.
|
||||||
|
* Avoid XAML tricks that make behavior hard to discover.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
* Prefer `Grid` for structured layouts.
|
||||||
|
* Use `StackPanel` only where its layout behavior is actually appropriate.
|
||||||
|
* Avoid unnecessary nested panels.
|
||||||
|
* Avoid fixed sizes unless the design requires them.
|
||||||
|
* Prefer responsive sizing with `Auto` and `*`.
|
||||||
|
* Avoid layout configurations that cause excessive measure/arrange passes.
|
||||||
|
* Be careful with large visual trees inside scrolling containers.
|
||||||
|
* Avoid disabling virtualization accidentally.
|
||||||
|
|
||||||
|
## Virtualization
|
||||||
|
|
||||||
|
* Keep UI virtualization enabled for large collections.
|
||||||
|
* Do not use container layouts that disable virtualization without a reason.
|
||||||
|
* Avoid wrapping large virtualized item lists in additional scrolling containers.
|
||||||
|
* Use virtualizing panels where appropriate.
|
||||||
|
* Verify virtualization when displaying large data sets.
|
||||||
|
* Avoid rendering thousands of controls simultaneously.
|
||||||
|
* Prefer paging, incremental loading or virtualization for very large data sets.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
## Commands
|
||||||
|
|
||||||
|
* Prefer commands over click handlers for ViewModel-driven actions.
|
||||||
|
* Keep command execution focused.
|
||||||
|
* Avoid putting large workflows directly inside command implementations.
|
||||||
|
* Keep `CanExecute` logic cheap.
|
||||||
|
* Refresh command availability only when relevant state changes.
|
||||||
|
* Avoid command implementations with hidden side effects.
|
||||||
|
* Handle async commands safely.
|
||||||
|
* Prevent accidental duplicate execution when an async action must not run concurrently.
|
||||||
|
* Never use `async void` except for true event handlers.
|
||||||
|
|
||||||
|
## Async and Threading
|
||||||
|
|
||||||
|
* Keep async operations asynchronous throughout the call chain.
|
||||||
|
* Avoid `.Result`, `.Wait()` and other synchronous blocking of async work.
|
||||||
|
* Do not block the UI thread.
|
||||||
|
* Run I/O asynchronously.
|
||||||
|
* Move expensive CPU work off the UI thread only when necessary.
|
||||||
|
* Marshal UI updates back to the UI thread when required.
|
||||||
|
* Keep Dispatcher usage minimal and explicit.
|
||||||
|
* Do not use the Dispatcher to hide incorrect threading design.
|
||||||
|
* Support `CancellationToken` for cancellable long-running operations.
|
||||||
|
* Cancel obsolete work when views or selections change.
|
||||||
|
* Prevent race conditions caused by overlapping async operations.
|
||||||
|
* Handle exceptions from background operations explicitly.
|
||||||
|
|
||||||
|
## UI Thread
|
||||||
|
|
||||||
|
* Treat the UI thread as a scarce resource.
|
||||||
|
* Avoid expensive loops, parsing, serialization, database access or network calls on the UI thread.
|
||||||
|
* Avoid excessive Dispatcher invocations.
|
||||||
|
* Batch UI updates where practical.
|
||||||
|
* Do not update UI-bound collections excessively in tight loops.
|
||||||
|
* Avoid synchronous waits on background work.
|
||||||
|
* Keep rendering-related callbacks lightweight.
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
* Avoid unnecessary event subscriptions.
|
||||||
|
* Unsubscribe from long-lived publishers when required.
|
||||||
|
* Prevent memory leaks from event handlers.
|
||||||
|
* Prefer weak events where publisher lifetime significantly exceeds subscriber lifetime.
|
||||||
|
* Keep event handlers small.
|
||||||
|
* Do not use events as hidden global communication channels.
|
||||||
|
* Avoid excessive event chaining.
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
## MVVM
|
||||||
|
|
||||||
|
* Keep ViewModels focused on presentation state and commands.
|
||||||
|
* Do not put UI element references into ViewModels.
|
||||||
|
* Do not access `Window`, `UserControl`, `DispatcherObject` or visual tree elements from ViewModels.
|
||||||
|
* Avoid direct navigation, dialogs or message boxes from ViewModels without an abstraction when testability matters.
|
||||||
|
* Prefer explicit application services for navigation, dialogs and external interactions.
|
||||||
|
* Keep domain logic outside ViewModels.
|
||||||
|
* Do not use ViewModels as general-purpose service containers.
|
||||||
|
* Keep ViewModel dependencies minimal.
|
||||||
|
* Prefer immutable or readonly state where practical.
|
||||||
|
* Avoid duplicated derived state.
|
||||||
|
* Keep state transitions predictable.
|
||||||
|
|
||||||
|
## Data Binding
|
||||||
|
|
||||||
|
* Prefer data binding over manual UI synchronization.
|
||||||
|
* Use strongly typed properties.
|
||||||
|
* Use appropriate binding modes.
|
||||||
|
* Prefer `OneWay` when two-way updates are not required.
|
||||||
|
* Use `TwoWay` only when the UI must modify the source.
|
||||||
|
* Avoid unnecessary `UpdateSourceTrigger=PropertyChanged` for expensive operations.
|
||||||
|
* Keep binding paths simple and stable.
|
||||||
|
* Avoid deeply nested binding paths.
|
||||||
|
* Do not hide broken bindings.
|
||||||
|
* Treat binding errors as real defects.
|
||||||
|
* Use `FallbackValue` and `TargetNullValue` only when they reflect intended behavior.
|
||||||
|
* Avoid excessive converters.
|
||||||
|
* Prefer exposing correctly shaped state from the ViewModel over complex converter logic.
|
||||||
|
* Avoid `MultiBinding` when simpler state modeling is clearer.
|
||||||
|
* Do not put business logic into converters.
|
||||||
|
|
||||||
|
## Property Change Notifications
|
||||||
|
|
||||||
|
* Implement property change notifications consistently.
|
||||||
|
* Avoid raising `PropertyChanged` unnecessarily.
|
||||||
|
* Raise notifications only when values actually change.
|
||||||
|
* Keep dependent property notifications explicit.
|
||||||
|
* Avoid hidden notification chains that are difficult to reason about.
|
||||||
|
* Prefer reusable base implementations only when they reduce duplication without obscuring behavior.
|
||||||
|
* Do not use reflection-based notification mechanisms unless there is a concrete benefit.
|
||||||
|
|
||||||
|
## Dependency Properties
|
||||||
|
|
||||||
|
* Use dependency properties only when WPF property-system behavior is required.
|
||||||
|
* Do not use dependency properties as a replacement for normal CLR properties.
|
||||||
|
* Keep metadata and callbacks lightweight.
|
||||||
|
* Avoid expensive work in property-changed callbacks.
|
||||||
|
* Avoid callbacks with hidden side effects.
|
||||||
|
* Validate or coerce values only when the control contract requires it.
|
||||||
|
* Do not expose mutable default values in dependency property metadata.
|
||||||
|
|
||||||
|
## Attached Properties and Behaviors
|
||||||
|
|
||||||
|
* Use attached properties and behaviors for reusable view-specific behavior.
|
||||||
|
* Do not use them to hide business logic.
|
||||||
|
* Keep behaviors focused and predictable.
|
||||||
|
* Clean up event handlers when behaviors are detached.
|
||||||
|
* Avoid global attached-property state.
|
||||||
|
* Prefer explicit behavior over complex XAML hacks.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
## Resources and Styles
|
||||||
|
|
||||||
|
* Use shared resources for repeated styles, brushes, templates and dimensions.
|
||||||
|
* Keep resource dictionaries focused.
|
||||||
|
* Avoid giant global resource dictionaries.
|
||||||
|
* Prefer feature-level resources when styles are not truly global.
|
||||||
|
* Use `StaticResource` by default.
|
||||||
|
* Use `DynamicResource` only when runtime resource replacement is required.
|
||||||
|
* Avoid excessive dynamic resource lookups.
|
||||||
|
* Keep theme resources separated from application logic.
|
||||||
|
* Avoid hardcoded colors and dimensions when design tokens or resources already exist.
|
||||||
|
|
||||||
|
## Windows and Dialogs
|
||||||
|
|
||||||
|
* Keep window lifecycle explicit.
|
||||||
|
* Avoid creating duplicate windows unintentionally.
|
||||||
|
* Do not keep hidden windows alive without a reason.
|
||||||
|
* Separate dialog logic from business logic.
|
||||||
|
* Prefer dialog services when ViewModels need to request user interaction.
|
||||||
|
* Keep ownership relationships explicit.
|
||||||
|
* Avoid application shutdown behavior that depends on accidental window lifetime.
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
|
||||||
|
* Keep navigation state explicit.
|
||||||
|
* Avoid coupling ViewModels directly to concrete views.
|
||||||
|
* Avoid service locator patterns for view resolution.
|
||||||
|
* Keep navigation history intentional.
|
||||||
|
* Dispose or release navigation targets when they are no longer required.
|
||||||
|
* Do not retain entire navigation graphs unnecessarily.
|
||||||
|
|
||||||
|
## Memory Management
|
||||||
|
|
||||||
|
* Watch for memory leaks caused by:
|
||||||
|
|
||||||
|
* Event subscriptions
|
||||||
|
* Timers
|
||||||
|
* Static references
|
||||||
|
* Long-lived services
|
||||||
|
* Cached views
|
||||||
|
* Closures
|
||||||
|
* Collection views
|
||||||
|
* Binding-related references
|
||||||
|
* Dispose resources correctly.
|
||||||
|
* Stop timers and background work when no longer needed.
|
||||||
|
* Avoid retaining closed windows or unloaded views.
|
||||||
|
* Do not keep unnecessary references to visual elements.
|
||||||
|
* Profile memory when long-running applications continuously grow.
|
||||||
|
|
||||||
|
## Images and Media
|
||||||
|
|
||||||
|
* Load images at an appropriate resolution.
|
||||||
|
* Avoid decoding images significantly larger than their displayed size.
|
||||||
|
* Release streams and file handles correctly.
|
||||||
|
* Avoid locking image files unintentionally.
|
||||||
|
* Cache images only when there is a clear memory strategy.
|
||||||
|
* Avoid keeping large bitmaps alive unnecessarily.
|
||||||
|
* Prefer frozen image resources when they are immutable.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
## Security
|
||||||
|
|
||||||
|
* Treat all external input as untrusted.
|
||||||
|
* Never expose secrets, credentials or tokens in UI code.
|
||||||
|
* Do not store secrets in configuration files committed to source control.
|
||||||
|
* Avoid leaking sensitive information through logs, dialogs or exception messages.
|
||||||
|
* Validate file paths and external resources.
|
||||||
|
* Do not execute external processes or files without explicit validation.
|
||||||
|
* Avoid insecure deserialization.
|
||||||
|
* Do not weaken TLS, authentication or certificate validation.
|
||||||
|
* Prefer secure defaults.
|
||||||
|
* Never disable security controls for convenience.
|
||||||
|
|
||||||
|
## File and Process Access
|
||||||
|
|
||||||
|
* Validate file paths before use.
|
||||||
|
* Avoid assuming files or directories exist.
|
||||||
|
* Handle permissions and locked files gracefully.
|
||||||
|
* Avoid arbitrary shell execution.
|
||||||
|
* Quote and validate process arguments safely.
|
||||||
|
* Do not concatenate untrusted input into shell commands.
|
||||||
|
* Avoid elevated privileges unless explicitly required.
|
||||||
|
* Clean up temporary files when appropriate.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
* Handle expected errors explicitly.
|
||||||
|
* Do not swallow exceptions.
|
||||||
|
* Preserve useful exception context.
|
||||||
|
* Avoid showing raw technical exceptions directly to end users.
|
||||||
|
* Log enough information for diagnosis without exposing sensitive data.
|
||||||
|
* Keep user-facing error messages clear and actionable.
|
||||||
|
* Avoid using exceptions for normal control flow.
|
||||||
|
* Ensure background exceptions are observed.
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
* Use structured logging.
|
||||||
|
* Avoid excessive logging on the UI thread.
|
||||||
|
* Do not log every property change or binding update.
|
||||||
|
* Never log secrets, tokens, credentials or sensitive user data.
|
||||||
|
* Include relevant context for operational failures.
|
||||||
|
* Avoid logging the same exception repeatedly at multiple layers.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
* Validate user input at the appropriate boundary.
|
||||||
|
* Keep validation logic out of XAML when it becomes complex.
|
||||||
|
* Prefer reusable validation rules or ViewModel validation for non-trivial cases.
|
||||||
|
* Display validation errors consistently.
|
||||||
|
* Do not rely on UI validation as a security boundary.
|
||||||
|
* Validate again at backend or domain boundaries when required.
|
||||||
|
* Preserve user input on recoverable errors.
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
$invariantCulture = [System.Globalization.CultureInfo]::InvariantCulture
|
||||||
$data = ($input | Out-String) | ConvertFrom-Json
|
$data = ($input | Out-String) | ConvertFrom-Json
|
||||||
$model = if ($data.model.display_name) { $data.model.display_name } else { '-' }
|
$model = if ($data.model.display_name) { $data.model.display_name } else { '-' }
|
||||||
$effort = if ($data.effort.level) { $data.effort.level } else { '-' }
|
$effort = if ($data.effort.level) { $data.effort.level } else { '-' }
|
||||||
@@ -5,7 +7,22 @@ $directory = if ($data.workspace.current_dir) { $data.workspace.current_dir } el
|
|||||||
$repo = if ($data.workspace.repo.name) { $data.workspace.repo.name } elseif ($directory) { Split-Path $directory -Leaf } else { '-' }
|
$repo = if ($data.workspace.repo.name) { $data.workspace.repo.name } elseif ($directory) { Split-Path $directory -Leaf } else { '-' }
|
||||||
$branch = if ($directory -and (Get-Command git -ErrorAction SilentlyContinue)) { (& git -C $directory branch --show-current 2>$null | Select-Object -First 1) } else { $null }
|
$branch = if ($directory -and (Get-Command git -ErrorAction SilentlyContinue)) { (& git -C $directory branch --show-current 2>$null | Select-Object -First 1) } else { $null }
|
||||||
if (-not $branch) { $branch = '-' }
|
if (-not $branch) { $branch = '-' }
|
||||||
$maxContext = if ($data.context_window.context_window_size) { [long]$data.context_window.context_window_size } else { 0 }
|
$usedPercentage = if ($null -ne $data.context_window.used_percentage) { [double]$data.context_window.used_percentage } else { $null }
|
||||||
$usedContext = if ($null -ne $data.context_window.used_percentage) { "$([math]::Round([double]$data.context_window.used_percentage, [MidpointRounding]::AwayFromZero))%" } else { '-' }
|
$usedContext = if ($null -ne $usedPercentage) { "$([math]::Round($usedPercentage, [MidpointRounding]::AwayFromZero).ToString($invariantCulture))%" } else { '-' }
|
||||||
$usedTokens = [long]$data.context_window.total_input_tokens + [long]$data.context_window.total_output_tokens
|
$usedTokens = [long]$data.context_window.total_input_tokens + [long]$data.context_window.total_output_tokens
|
||||||
Write-Output "Model: $model | Effort: $effort | Repo: $repo | Branch: $branch | Max Context: $maxContext | Used Context: $usedContext | Used Tokens: $usedTokens"
|
$usedTokensDisplay = if ($usedTokens -ge 1000) { ($usedTokens / 1000).ToString('0.0', $invariantCulture) + 'k' } else { $usedTokens.ToString($invariantCulture) }
|
||||||
|
|
||||||
|
$esc = [char]27
|
||||||
|
$reset = "$esc[0m"
|
||||||
|
$dim = "$esc[90m"
|
||||||
|
$cyan = "$esc[36m"
|
||||||
|
$magenta = "$esc[35m"
|
||||||
|
$blue = "$esc[34m"
|
||||||
|
$green = "$esc[32m"
|
||||||
|
$contextColor = if ($null -eq $usedPercentage) { $dim } elseif ($usedPercentage -ge 85) { "$esc[31m" } elseif ($usedPercentage -ge 60) { "$esc[33m" } else { $green }
|
||||||
|
$middot = [char]0x00B7
|
||||||
|
$sep = "$dim$middot$reset"
|
||||||
|
|
||||||
|
$line1 = "$cyan$model$reset $sep $magenta$effort$reset"
|
||||||
|
$line2 = "$blue$repo$reset$dim on$reset $green$branch$reset $sep $contextColor$usedContext ctx$reset $dim($usedTokensDisplay tok)$reset"
|
||||||
|
Write-Output "$line1`n$line2"
|
||||||
@@ -4,19 +4,51 @@ set -euo pipefail
|
|||||||
command -v jq >/dev/null 2>&1 || exit 0
|
command -v jq >/dev/null 2>&1 || exit 0
|
||||||
input=
|
input=
|
||||||
while IFS= read -r line || [ -n "$line" ]; do input+="$line"; done
|
while IFS= read -r line || [ -n "$line" ]; do input+="$line"; done
|
||||||
IFS=$'\t' read -r model effort repo directory max_context used_context used_tokens < <(
|
IFS=$'\t' read -r model effort repo directory used_percentage used_tokens < <(
|
||||||
printf '%s' "$input" | jq -r '[
|
printf '%s' "$input" | jq -r '[
|
||||||
(.model.display_name // "-"),
|
(.model.display_name // "-"),
|
||||||
(.effort.level // "-"),
|
(.effort.level // "-"),
|
||||||
(.workspace.repo.name // "-"),
|
(.workspace.repo.name // "-"),
|
||||||
(.workspace.current_dir // .cwd // "."),
|
(.workspace.current_dir // .cwd // "."),
|
||||||
(.context_window.context_window_size // 0),
|
(if .context_window.used_percentage == null then "-" else (.context_window.used_percentage | round) end),
|
||||||
(if .context_window.used_percentage == null then "-" else ((.context_window.used_percentage | round | tostring) + "%") end),
|
|
||||||
((.context_window.total_input_tokens // 0) + (.context_window.total_output_tokens // 0))
|
((.context_window.total_input_tokens // 0) + (.context_window.total_output_tokens // 0))
|
||||||
] | @tsv'
|
] | @tsv'
|
||||||
)
|
)
|
||||||
|
used_percentage=${used_percentage%$'\r'}
|
||||||
|
used_tokens=${used_tokens%$'\r'}
|
||||||
[ "$repo" != - ] || repo=$(basename -- "$directory")
|
[ "$repo" != - ] || repo=$(basename -- "$directory")
|
||||||
[ -n "$repo" ] || repo=-
|
[ -n "$repo" ] || repo=-
|
||||||
branch=$(git -C "$directory" branch --show-current 2>/dev/null || true)
|
branch=$(git -C "$directory" branch --show-current 2>/dev/null || true)
|
||||||
[ -n "$branch" ] || branch=-
|
[ -n "$branch" ] || branch=-
|
||||||
printf 'Model: %s | Effort: %s | Repo: %s | Branch: %s | Max Context: %s | Used Context: %s | Used Tokens: %s\n' "$model" "$effort" "$repo" "$branch" "$max_context" "$used_context" "$used_tokens"
|
|
||||||
|
if [ "$used_percentage" != - ]; then
|
||||||
|
used_context="${used_percentage}%"
|
||||||
|
else
|
||||||
|
used_context=-
|
||||||
|
fi
|
||||||
|
if [ "$used_tokens" -ge 1000 ]; then
|
||||||
|
used_tokens_display=$(awk -v t="$used_tokens" 'BEGIN { printf "%.1fk", t / 1000 }')
|
||||||
|
else
|
||||||
|
used_tokens_display=$used_tokens
|
||||||
|
fi
|
||||||
|
|
||||||
|
esc=$'\033'
|
||||||
|
reset="$esc[0m"
|
||||||
|
dim="$esc[90m"
|
||||||
|
cyan="$esc[36m"
|
||||||
|
magenta="$esc[35m"
|
||||||
|
blue="$esc[34m"
|
||||||
|
green="$esc[32m"
|
||||||
|
if [ "$used_percentage" = - ]; then
|
||||||
|
context_color=$dim
|
||||||
|
elif [ "$used_percentage" -ge 85 ]; then
|
||||||
|
context_color="$esc[31m"
|
||||||
|
elif [ "$used_percentage" -ge 60 ]; then
|
||||||
|
context_color="$esc[33m"
|
||||||
|
else
|
||||||
|
context_color=$green
|
||||||
|
fi
|
||||||
|
sep="${dim}·${reset}"
|
||||||
|
|
||||||
|
printf "${cyan}%s${reset} ${sep} ${magenta}%s${reset}\n${blue}%s${reset}${dim} on${reset} ${green}%s${reset} ${sep} ${context_color}%s ctx${reset} ${dim}(%s tok)${reset}\n" \
|
||||||
|
"$model" "$effort" "$repo" "$branch" "$used_context" "$used_tokens_display"
|
||||||
Reference in new issue
Block a user