swift-code-reviewer-skill 1.2.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md CHANGED
@@ -1,26 +1,68 @@
1
1
  ---
2
2
  name: swift-code-reviewer
3
- description: "Multi-layer code review agent for Swift and SwiftUI projects. Analyzes PRs, diffs, and files across six dimensions: Swift 6+ concurrency safety, SwiftUI state management and modern APIs, performance (view updates, ForEach identity, lazy loading), security (force unwraps, Keychain, input validation), architecture compliance (MVVM/MVI/TCA, dependency injection), and project-specific standards from .claude/CLAUDE.md. Outputs structured reports with Critical/High/Medium/Low severity, positive feedback, and prioritized action items with file:line references. Use when the user says review this PR, review my code, review my changes, check this file, code review, audit this codebase, check code quality, review uncommitted changes, review all ViewModels, or mentions reviewing .swift files, navigation, sheets, theming, or async patterns."
3
+ description: "Perform thorough code reviews for Swift/SwiftUI code, including spec adherence (PR description + linked issues), code quality, architecture, performance, security, Swift 6+ best practices, project standards from .claude/CLAUDE.md, and meta-feedback on recurring patterns that suggest gaps in the agent's instructions. Use when reviewing PRs/MRs (especially AI-generated ones), performing quality audits, validating against original spec, or providing structured feedback with severity levels and improvement suggestions for both the code and the agent loop that produced it."
4
4
  ---
5
5
 
6
6
  # Swift/SwiftUI Code Review Skill
7
7
 
8
8
  Multi-layer review covering Swift 6+ concurrency, SwiftUI patterns, performance, security, architecture, and project-specific standards. Reads `.claude/CLAUDE.md` and outputs Critical/High/Medium/Low severity findings with `file:line` references and before/after code examples.
9
9
 
10
+ ## When to Use This Skill
11
+
12
+ - "Review this PR"
13
+ - "Review my code" / "Review my changes" / "Review uncommitted changes"
14
+ - "Code review for [component]"
15
+ - "Audit this codebase" / "Check code quality"
16
+ - "Review against .claude/CLAUDE.md" / "Check if this follows our coding standards"
17
+ - "Architecture review" / "Performance audit" / "Security review"
18
+ - "Review this PR against the spec"
19
+ - "Did the agent miss anything from issue #123?"
20
+ - "What rules am I missing in CLAUDE.md based on this PR?"
21
+ - "Review this AI-generated PR"
22
+
10
23
  ## Workflow
11
24
 
12
25
  ### Phase 1 — Context Gathering
13
26
 
14
- 1. Try to load `.claude/CLAUDE.md`.
27
+ 1. **Read the Spec**
28
+ - For PRs: `gh pr view <num> --json title,body,closingIssuesReferences,labels`
29
+ - For linked issues: `gh issue view <num> --json title,body,labels`
30
+ - For MRs: `glab mr view <num>` and `glab issue view <num>`
31
+ - Extract:
32
+ - Stated goal / problem being solved
33
+ - Explicit acceptance criteria (look for checkboxes, "should", "must", "Given/When/Then")
34
+ - Edge cases or non-goals mentioned
35
+ - Out-of-scope items
36
+ - If no PR/issue context is available, note this and fall back to inferring intent from the diff.
37
+ 2. Try to load `.claude/CLAUDE.md`.
15
38
  - **If missing**: add a note to the report — _"No project standards file found — review uses default Apple guidelines"_ — then continue.
16
- 2. Obtain the changeset: `git diff`, `git diff --cached`, or `gh pr diff <n>`.
39
+ 3. Obtain the changeset: `git diff`, `git diff --cached`, or `gh pr diff <n>`.
17
40
  - **If diff is empty**: stop and ask the user to specify files, a PR number, or a directory.
18
- 3. Read each changed file plus key related files (imports, protocols it conforms to, corresponding test file if present).
41
+ 4. Read each changed file plus key related files (imports, protocols it conforms to, corresponding test file if present).
19
42
 
20
43
  ### Phase 2 — Analysis
21
44
 
22
45
  For each category, load the reference file before writing findings:
23
46
 
47
+ #### 0. Spec Adherence
48
+
49
+ Reference: `references/spec-adherence.md`
50
+
51
+ - **Requirement Coverage**
52
+ - Does each acceptance criterion map to a concrete code change?
53
+ - Are edge cases mentioned in the spec handled?
54
+ - Are tests covering the scenarios described?
55
+ - **Scope Discipline**
56
+ - Flag changes outside the stated scope (scope creep)
57
+ - Flag unrelated refactors bundled into the PR
58
+ - **Missing Work**
59
+ - TODOs, `fatalError("not implemented")`, empty function bodies
60
+ - Stubbed mocks that should be real implementations
61
+ - Acceptance criteria with no corresponding diff
62
+ - **Intent Drift**
63
+ - Code solves a *similar* but different problem than stated
64
+ - Naming/structure suggests a different mental model than the spec
65
+
24
66
  1. **Swift Quality** — concurrency, error handling, optionals, naming → `references/swift-quality-checklist.md`; for concurrency findings also read `skills/swift-concurrency/references/sendable.md` and `actors.md`
25
67
  2. **SwiftUI Patterns** — property wrappers, state management, deprecated APIs → `references/swiftui-review-checklist.md`; for wrapper selection read `skills/swiftui-expert-skill/references/state-management.md`
26
68
  3. **Performance** — view body cost, ForEach identity, lazy loading, retain cycles → `references/performance-review.md`
@@ -31,6 +73,28 @@ For each category, load the reference file before writing findings:
31
73
  For test file findings, consult `skills/swift-testing/references/test-organization.md`.
32
74
  For navigation/routing findings, consult `skills/swiftui-ui-patterns/references/navigationstack.md`.
33
75
 
76
+ ### Phase 2.5 — Pattern Detection (for Agent Loop Feedback)
77
+
78
+ **Objective**: Identify recurring issues that point to gaps in the agent's
79
+ instructions, not just the code.
80
+
81
+ After collecting per-file findings, aggregate them:
82
+
83
+ 1. Group findings by rule (e.g., "force-unwrap", "deprecated NavigationView",
84
+ "missing @MainActor on UI mutation").
85
+ 2. Mark any rule that fires **≥2 times across the diff** as a recurring pattern.
86
+ 3. For each recurring pattern, draft a one-line rule suitable for
87
+ `.claude/CLAUDE.md` or an agent system prompt — written as a directive,
88
+ not a description.
89
+ 4. If the same recurring pattern appeared in past reviews (check git log of
90
+ `.claude/CLAUDE.md`), escalate priority — the existing rule isn't strong
91
+ enough or isn't being read.
92
+
93
+ Threshold rationale: one occurrence is a slip; two is a pattern; three+ means
94
+ the agent's instructions are silent on this and need an explicit rule.
95
+
96
+ Reference: `references/agent-loop-feedback.md`.
97
+
34
98
  ### Phase 3 — Report
35
99
 
36
100
  Group findings by file → sort by severity within each file → write prioritized action items.
@@ -102,6 +166,22 @@ Also migrate from `ObservableObject`/`@Published` to `@Observable` (iOS 17+) —
102
166
  ## Summary
103
167
  Files: N | Critical: N | High: N | Medium: N | Low: N
104
168
 
169
+ ## Spec Adherence
170
+
171
+ **Source**: PR #123 / Issue #456
172
+
173
+ | Requirement | Status | Location |
174
+ |-------------|--------|----------|
175
+ | User can log in with email | ✅ Implemented | LoginView.swift:23 |
176
+ | Show error on invalid credentials | ⚠️ Partial — missing 401 case | LoginViewModel.swift:67 |
177
+ | Persist session in Keychain | ❌ Not implemented | — |
178
+ | Rate limit retries | ❌ Not implemented | — |
179
+
180
+ **Scope creep**: 1 unrelated change (UserSettings.swift refactor) — recommend
181
+ splitting into a separate PR.
182
+
183
+ ---
184
+
105
185
  ## <Filename.swift>
106
186
 
107
187
  [Severity] **<Category>** (line N)
@@ -115,6 +195,33 @@ Fix: <explanation + corrected snippet>
115
195
  - [Must fix] ...
116
196
  - [Should fix] ...
117
197
  - [Consider] ...
198
+
199
+ ---
200
+
201
+ ## Agent Loop Feedback
202
+
203
+ Recurring patterns suggest the following rules are missing or under-emphasized
204
+ in `.claude/CLAUDE.md`:
205
+
206
+ ### Pattern: Force-unwraps (4 occurrences)
207
+ **Files**: LoginView.swift:89, NetworkService.swift:34, UserRepo.swift:12,78
208
+
209
+ **Suggested rule**:
210
+ > Never use `!`, `try!`, or `as!`. Use `guard let` with explicit early return,
211
+ > typed throws, or `as?` with handling. Force-unwraps are crashes waiting to happen.
212
+
213
+ ### Pattern: Deprecated NavigationView (2 occurrences)
214
+ **Files**: ProfileView.swift:15, SettingsView.swift:22
215
+
216
+ **Suggested rule**:
217
+ > Use `NavigationStack` exclusively. `NavigationView` is deprecated as of iOS 16.
218
+
219
+ ### Pattern: Business logic in View body (3 occurrences)
220
+ **Files**: LoginView.swift:45, ProfileView.swift:78, FeedView.swift:34
221
+
222
+ **Suggested rule**:
223
+ > Views must not contain business logic, network calls, or data transformations.
224
+ > Move all such work into the @Observable view model.
118
225
  ```
119
226
 
120
227
  Full templates and severity classification: `references/feedback-templates.md`.
@@ -149,10 +256,21 @@ git diff HEAD~1 # last commit
149
256
  git diff -- path/to/file.swift
150
257
  ```
151
258
 
259
+ ## Limitations
260
+
261
+ - Spec adherence checks require an accessible PR description or linked issue.
262
+ When reviewing local changes with no PR context, mark spec adherence as
263
+ "not assessed" rather than guessing intent.
264
+ - Agent loop feedback assumes the code was AI-generated or AI-assisted. For
265
+ fully human-written code, recurring patterns are still useful but should be
266
+ framed as team coding standards rather than agent instructions.
267
+
152
268
  ## Reference Files
153
269
 
154
270
  - `references/review-workflow.md` — detailed process, diff parsing, git commands
155
271
  - `references/feedback-templates.md` — output templates, severity classification
272
+ - `references/spec-adherence.md` — parsing PR/issue specs, requirement coverage tables, scope creep classification
273
+ - `references/agent-loop-feedback.md` — recurring-pattern threshold, directive phrasing, suggested-rule template
156
274
  - `references/swift-quality-checklist.md` — Swift 6+, concurrency, optionals, naming
157
275
  - `references/swiftui-review-checklist.md` — property wrappers, state, modern APIs
158
276
  - `references/performance-review.md` — view optimization, ForEach, resource management
package/bin/install.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ 'use strict';
2
3
 
3
4
  const fs = require('fs');
4
5
  const path = require('path');
@@ -6,7 +7,6 @@ const os = require('os');
6
7
 
7
8
  const SKILL_NAME = 'swift-code-reviewer-skill';
8
9
 
9
- // Colors for terminal output
10
10
  const colors = {
11
11
  reset: '\x1b[0m',
12
12
  bright: '\x1b[1m',
@@ -14,7 +14,7 @@ const colors = {
14
14
  yellow: '\x1b[33m',
15
15
  blue: '\x1b[34m',
16
16
  red: '\x1b[31m',
17
- cyan: '\x1b[36m'
17
+ cyan: '\x1b[36m',
18
18
  };
19
19
 
20
20
  function log(message, color = colors.reset) {
@@ -23,14 +23,9 @@ function log(message, color = colors.reset) {
23
23
 
24
24
  function copyRecursive(src, dest) {
25
25
  const stats = fs.statSync(src);
26
-
27
26
  if (stats.isDirectory()) {
28
- if (!fs.existsSync(dest)) {
29
- fs.mkdirSync(dest, { recursive: true });
30
- }
31
-
32
- const entries = fs.readdirSync(src);
33
- for (const entry of entries) {
27
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
28
+ for (const entry of fs.readdirSync(src)) {
34
29
  copyRecursive(path.join(src, entry), path.join(dest, entry));
35
30
  }
36
31
  } else {
@@ -39,18 +34,20 @@ function copyRecursive(src, dest) {
39
34
  }
40
35
 
41
36
  function findPackageRoot() {
42
- let packageRoot = __dirname;
43
- while (!fs.existsSync(path.join(packageRoot, 'SKILL.md'))) {
44
- const parent = path.dirname(packageRoot);
45
- if (parent === packageRoot) {
37
+ let dir = __dirname;
38
+ while (!fs.existsSync(path.join(dir, 'SKILL.md'))) {
39
+ const parent = path.dirname(dir);
40
+ if (parent === dir) {
46
41
  log(' Error: Could not find SKILL.md in package', colors.red);
47
42
  process.exit(1);
48
43
  }
49
- packageRoot = parent;
44
+ dir = parent;
50
45
  }
51
- return packageRoot;
46
+ return dir;
52
47
  }
53
48
 
49
+ // ---------- global install (default command) ----------
50
+
54
51
  function install() {
55
52
  log('\n Swift Code Reviewer Skill Installer', colors.cyan + colors.bright);
56
53
  log(' ====================================\n', colors.cyan);
@@ -76,113 +73,70 @@ function install() {
76
73
 
77
74
  fs.mkdirSync(targetDir, { recursive: true });
78
75
 
79
- const filesToCopy = [
80
- 'SKILL.md',
81
- 'README.md',
82
- 'LICENSE',
83
- 'CONTRIBUTING.md',
84
- 'CHANGELOG.md'
85
- ];
86
-
87
- const dirsToCopy = ['references', 'skills', 'templates'];
76
+ const filesToCopy = ['SKILL.md', 'README.md', 'LICENSE', 'CONTRIBUTING.md', 'CHANGELOG.md'];
77
+ const dirsToCopy = ['core', 'references', 'skills', 'templates'];
88
78
 
89
79
  for (const file of filesToCopy) {
90
80
  const src = path.join(packageRoot, file);
91
- const dest = path.join(targetDir, file);
92
-
93
81
  if (fs.existsSync(src)) {
94
- fs.copyFileSync(src, dest);
82
+ fs.copyFileSync(src, path.join(targetDir, file));
95
83
  log(` Copied: ${file}`, colors.green);
96
84
  }
97
85
  }
98
86
 
99
87
  for (const dir of dirsToCopy) {
100
88
  const src = path.join(packageRoot, dir);
101
- const dest = path.join(targetDir, dir);
102
-
103
89
  if (fs.existsSync(src)) {
104
- copyRecursive(src, dest);
90
+ copyRecursive(src, path.join(targetDir, dir));
105
91
  log(` Copied: ${dir}/`, colors.green);
106
92
  }
107
93
  }
108
94
 
109
95
  log('\n Installation complete!', colors.green + colors.bright);
110
96
  log('\n The skill is now available in Claude Code.', colors.reset);
111
- log(' To add the review agent and /review command to a project, run:', colors.reset);
97
+ log(' To scaffold the review agent into a project, run:', colors.reset);
112
98
  log('\n npx swift-code-reviewer-skill init\n', colors.cyan);
113
- log(' Or use it directly by asking Claude to:', colors.reset);
114
- log('\n - "Review this PR"', colors.cyan);
115
- log(' - "Review LoginView.swift"', colors.cyan);
116
- log(' - "Review my uncommitted changes"', colors.cyan);
117
- log(' - "Check if this follows our coding standards"\n', colors.cyan);
118
-
119
99
  log(` Skill location: ${targetDir}`, colors.blue);
120
100
  log(' Documentation: https://github.com/Viniciuscarvalho/swift-code-reviewer-skill\n', colors.blue);
121
101
  }
122
102
 
123
- function init() {
103
+ // ---------- init (project scaffolding) ----------
104
+
105
+ async function init(flags) {
124
106
  log('\n Swift Code Reviewer — Project Setup', colors.cyan + colors.bright);
125
107
  log(' =====================================\n', colors.cyan);
126
108
 
127
109
  const packageRoot = findPackageRoot();
128
110
  const cwd = process.cwd();
129
111
 
130
- // Verify we're in a git repo (likely a real project)
131
112
  if (!fs.existsSync(path.join(cwd, '.git'))) {
132
113
  log(' Warning: Not a git repository. Running anyway.\n', colors.yellow);
133
114
  }
134
115
 
135
- const claudeDir = path.join(cwd, '.claude');
136
- const agentsDir = path.join(claudeDir, 'agents');
137
- const commandsDir = path.join(claudeDir, 'commands');
116
+ const { selectAgents } = require('./lib/prompt');
117
+ const { AGENT_INSTALLERS } = require('./lib/agents');
138
118
 
139
- const templateAgentSrc = path.join(packageRoot, 'templates', 'agents', 'swift-code-reviewer.md');
140
- const templateCommandSrc = path.join(packageRoot, 'templates', 'commands', 'review.md');
119
+ const agents = await selectAgents({
120
+ allFlag: flags.all,
121
+ agentFlag: flags.agent,
122
+ isTTY: Boolean(process.stdout.isTTY),
123
+ });
141
124
 
142
- // Check templates exist
143
- if (!fs.existsSync(templateAgentSrc)) {
144
- log(' Error: Agent template not found. Reinstall the skill with:', colors.red);
145
- log(' npx swift-code-reviewer-skill\n', colors.cyan);
146
- process.exit(1);
125
+ if (agents.length === 0) {
126
+ log(' No agents selected. Nothing to do.\n', colors.yellow);
127
+ return;
147
128
  }
148
129
 
149
- fs.mkdirSync(agentsDir, { recursive: true });
150
- fs.mkdirSync(commandsDir, { recursive: true });
151
-
152
- let created = 0;
153
- let skipped = 0;
154
-
155
- // Copy agent
156
- const agentDest = path.join(agentsDir, 'swift-code-reviewer.md');
157
- if (fs.existsSync(agentDest)) {
158
- log(' Skipped: .claude/agents/swift-code-reviewer.md (already exists)', colors.yellow);
159
- skipped++;
160
- } else {
161
- fs.copyFileSync(templateAgentSrc, agentDest);
162
- log(' Created: .claude/agents/swift-code-reviewer.md', colors.green);
163
- created++;
130
+ const opts = { dryRun: flags.dryRun, force: flags.force };
131
+ for (const agent of agents) {
132
+ AGENT_INSTALLERS[agent](packageRoot, cwd, opts);
164
133
  }
165
134
 
166
- // Copy command
167
- const commandDest = path.join(commandsDir, 'review.md');
168
- if (fs.existsSync(commandDest)) {
169
- log(' Skipped: .claude/commands/review.md (already exists)', colors.yellow);
170
- skipped++;
171
- } else {
172
- fs.copyFileSync(templateCommandSrc, commandDest);
173
- log(' Created: .claude/commands/review.md', colors.green);
174
- created++;
175
- }
176
-
177
- log(`\n Done! ${created} file(s) created, ${skipped} skipped.`, colors.green + colors.bright);
178
-
179
- if (created > 0) {
180
- log('\n Usage:', colors.reset);
181
- log(' /review — run a full code review before pushing', colors.cyan);
182
- log(' @swift-code-reviewer — invoke the agent directly\n', colors.cyan);
183
- }
135
+ log('\n Done!\n', colors.green + colors.bright);
184
136
  }
185
137
 
138
+ // ---------- uninstall ----------
139
+
186
140
  function uninstall() {
187
141
  log('\n Uninstalling Swift Code Reviewer Skill', colors.yellow + colors.bright);
188
142
  log(' ======================================\n', colors.yellow);
@@ -199,29 +153,56 @@ function uninstall() {
199
153
  }
200
154
  }
201
155
 
156
+ // ---------- help ----------
157
+
202
158
  function showHelp() {
203
159
  log('\n Swift Code Reviewer Skill', colors.cyan + colors.bright);
204
160
  log(' =========================\n', colors.cyan);
205
- log(' Usage: npx swift-code-reviewer-skill [command]\n', colors.reset);
161
+ log(' Usage: npx swift-code-reviewer-skill [command] [options]\n', colors.reset);
206
162
  log(' Commands:', colors.bright);
207
163
  log(' (none) Install the skill to ~/.claude/skills/', colors.reset);
208
- log(' init Scaffold agent + /review command into current project', colors.reset);
164
+ log(' init Scaffold review agent into the current project', colors.reset);
209
165
  log(' uninstall Remove the skill from ~/.claude/skills/', colors.reset);
210
166
  log(' help Show this help message\n', colors.reset);
167
+ log(' Options for init:', colors.bright);
168
+ log(' --agent <name[,name]> Target specific agent(s): claude, codex, gemini, kiro', colors.reset);
169
+ log(' --all Install for all supported agents', colors.reset);
170
+ log(' --force Overwrite existing files', colors.reset);
171
+ log(' --dry-run Preview writes without touching the filesystem\n', colors.reset);
211
172
  log(' Examples:', colors.bright);
212
- log(' npx swift-code-reviewer-skill # install skill globally', colors.cyan);
213
- log(' npx swift-code-reviewer-skill init # add agent + command to project', colors.cyan);
214
- log(' npx swift-code-reviewer-skill uninstall # remove skill\n', colors.cyan);
173
+ log(' npx swift-code-reviewer-skill # install skill globally (Claude)', colors.cyan);
174
+ log(' npx swift-code-reviewer-skill init # interactive agent picker', colors.cyan);
175
+ log(' npx swift-code-reviewer-skill init --all # all agents at once', colors.cyan);
176
+ log(' npx swift-code-reviewer-skill init --agent codex,gemini', colors.cyan);
177
+ log(' npx swift-code-reviewer-skill init --dry-run', colors.cyan);
178
+ log(' npx swift-code-reviewer-skill uninstall\n', colors.cyan);
215
179
  }
216
180
 
217
- // Parse command line arguments
181
+ // ---------- arg parsing ----------
182
+
183
+ function parseFlags(argv) {
184
+ const flags = { all: false, agent: null, dryRun: false, force: false };
185
+ for (let i = 0; i < argv.length; i++) {
186
+ const arg = argv[i];
187
+ if (arg === '--all') flags.all = true;
188
+ else if (arg === '--dry-run') flags.dryRun = true;
189
+ else if (arg === '--force') flags.force = true;
190
+ else if (arg === '--agent' && argv[i + 1]) { flags.agent = argv[++i]; }
191
+ else if (arg.startsWith('--agent=')) { flags.agent = arg.slice('--agent='.length); }
192
+ }
193
+ return flags;
194
+ }
195
+
196
+ // ---------- entry point ----------
197
+
218
198
  const args = process.argv.slice(2);
219
- const command = args[0];
199
+ const command = args.find((a) => !a.startsWith('-')) || '';
200
+ const flags = parseFlags(args.filter((a) => a.startsWith('-') || args.indexOf(a) > 0));
220
201
 
221
202
  switch (command) {
222
203
  case 'init':
223
204
  case 'setup':
224
- init();
205
+ init(flags).catch((err) => { console.error(err); process.exit(1); });
225
206
  break;
226
207
  case 'uninstall':
227
208
  case 'remove':
@@ -0,0 +1,134 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const colors = {
7
+ reset: '\x1b[0m',
8
+ bright: '\x1b[1m',
9
+ green: '\x1b[32m',
10
+ yellow: '\x1b[33m',
11
+ blue: '\x1b[34m',
12
+ cyan: '\x1b[36m',
13
+ };
14
+
15
+ function log(msg, color = colors.reset) {
16
+ console.log(`${color}${msg}${colors.reset}`);
17
+ }
18
+
19
+ function writeFile(dest, src, label, dryRun) {
20
+ const exists = fs.existsSync(dest);
21
+ if (dryRun) {
22
+ log(` [dry-run] ${exists ? 'Update' : 'Create'}: ${label}`, colors.blue);
23
+ return;
24
+ }
25
+ if (exists) {
26
+ log(` Updated: ${label}`, colors.yellow);
27
+ } else {
28
+ log(` Created: ${label}`, colors.green);
29
+ }
30
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
31
+ fs.copyFileSync(src, dest);
32
+ }
33
+
34
+ function skipFile(label, dryRun) {
35
+ if (dryRun) {
36
+ log(` [dry-run] Skip (exists): ${label}`, colors.yellow);
37
+ } else {
38
+ log(` Skipped: ${label} (already exists)`, colors.yellow);
39
+ }
40
+ }
41
+
42
+ function installClaude(packageRoot, cwd, { dryRun = false, force = false } = {}) {
43
+ log('\n Installing for Claude Code…', colors.cyan);
44
+
45
+ const agentSrc = path.join(packageRoot, 'templates', 'agents', 'claude', 'swift-code-reviewer.md');
46
+ const cmdSrc = path.join(packageRoot, 'templates', 'commands', 'claude', 'review.md');
47
+
48
+ const agentDest = path.join(cwd, '.claude', 'agents', 'swift-code-reviewer.md');
49
+ const cmdDest = path.join(cwd, '.claude', 'commands', 'review.md');
50
+
51
+ if (!force && fs.existsSync(agentDest)) {
52
+ skipFile('.claude/agents/swift-code-reviewer.md', dryRun);
53
+ } else {
54
+ writeFile(agentDest, agentSrc, '.claude/agents/swift-code-reviewer.md', dryRun);
55
+ }
56
+
57
+ if (!force && fs.existsSync(cmdDest)) {
58
+ skipFile('.claude/commands/review.md', dryRun);
59
+ } else {
60
+ writeFile(cmdDest, cmdSrc, '.claude/commands/review.md', dryRun);
61
+ }
62
+
63
+ log('\n Claude Code hints:', colors.reset);
64
+ log(' /review — run a full Swift code review', colors.cyan);
65
+ log(' @swift-code-reviewer — invoke the agent directly\n', colors.cyan);
66
+ }
67
+
68
+ function installCodex(packageRoot, cwd, { dryRun = false, force = false } = {}) {
69
+ log('\n Installing for OpenAI Codex CLI…', colors.cyan);
70
+
71
+ const agentSrc = path.join(packageRoot, 'templates', 'agents', 'codex', 'swift-code-reviewer.md');
72
+ const agentDest = path.join(cwd, 'swift-code-reviewer.md');
73
+
74
+ if (!force && fs.existsSync(agentDest)) {
75
+ skipFile('swift-code-reviewer.md', dryRun);
76
+ } else {
77
+ writeFile(agentDest, agentSrc, 'swift-code-reviewer.md', dryRun);
78
+ }
79
+
80
+ log('\n Codex post-install — add this to your AGENTS.md:\n', colors.reset);
81
+ log(' ## Swift code review', colors.cyan);
82
+ log(' See swift-code-reviewer.md for the full review guide.\n', colors.cyan);
83
+ log(' Note: Codex concatenates AGENTS.md into the system prompt and does not', colors.yellow);
84
+ log(' auto-resolve @-path mentions. Paste the snippet above manually.\n', colors.yellow);
85
+ }
86
+
87
+ function installGemini(packageRoot, cwd, { dryRun = false, force = false } = {}) {
88
+ log('\n Installing for Google Gemini CLI…', colors.cyan);
89
+
90
+ const agentSrc = path.join(packageRoot, 'templates', 'agents', 'gemini', 'swift-code-reviewer.md');
91
+ const cmdSrc = path.join(packageRoot, 'templates', 'commands', 'gemini', 'review.toml');
92
+
93
+ const agentDest = path.join(cwd, 'swift-code-reviewer.md');
94
+ const cmdDest = path.join(cwd, '.gemini', 'commands', 'review.toml');
95
+
96
+ if (!force && fs.existsSync(agentDest)) {
97
+ skipFile('swift-code-reviewer.md', dryRun);
98
+ } else {
99
+ writeFile(agentDest, agentSrc, 'swift-code-reviewer.md', dryRun);
100
+ }
101
+
102
+ if (!force && fs.existsSync(cmdDest)) {
103
+ skipFile('.gemini/commands/review.toml', dryRun);
104
+ } else {
105
+ writeFile(cmdDest, cmdSrc, '.gemini/commands/review.toml', dryRun);
106
+ }
107
+
108
+ log('\n Gemini CLI hints:', colors.reset);
109
+ log(' /review — run a full Swift code review (via review.toml)', colors.cyan);
110
+ log(' The command uses @./swift-code-reviewer.md to load the guide.\n', colors.cyan);
111
+ }
112
+
113
+ function installKiro(packageRoot, cwd, { dryRun = false, force = false } = {}) {
114
+ log('\n Installing for Kiro…', colors.cyan);
115
+
116
+ const agentSrc = path.join(packageRoot, 'templates', 'agents', 'kiro', 'swift-code-reviewer.md');
117
+ const agentDest = path.join(cwd, '.kiro', 'steering', 'swift-code-reviewer.md');
118
+
119
+ if (!force && fs.existsSync(agentDest)) {
120
+ skipFile('.kiro/steering/swift-code-reviewer.md', dryRun);
121
+ } else {
122
+ writeFile(agentDest, agentSrc, '.kiro/steering/swift-code-reviewer.md', dryRun);
123
+ }
124
+
125
+ log('\n Kiro hints:', colors.reset);
126
+ log(' Steering auto-activates for any *.swift file (fileMatch inclusion).', colors.cyan);
127
+ log(' Note: workspace-scoped steering only — global steering has a known', colors.yellow);
128
+ log(' bug (https://github.com/kirodotdev/Kiro/issues/6171).\n', colors.yellow);
129
+ }
130
+
131
+ const AGENT_INSTALLERS = { claude: installClaude, codex: installCodex, gemini: installGemini, kiro: installKiro };
132
+ const VALID_AGENTS = Object.keys(AGENT_INSTALLERS);
133
+
134
+ module.exports = { installClaude, installCodex, installGemini, installKiro, AGENT_INSTALLERS, VALID_AGENTS };
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ const { VALID_AGENTS } = require('./agents');
4
+
5
+ /**
6
+ * Resolve which agents to install based on CLI flags and TTY state.
7
+ *
8
+ * Priority:
9
+ * 1. --all flag → all agents
10
+ * 2. --agent <name[,name]> flag → named agents
11
+ * 3. TTY available → interactive @inquirer/prompts checkbox
12
+ * 4. Non-TTY without flags → ['claude'] (CI-safe fallback)
13
+ */
14
+ async function selectAgents({ allFlag, agentFlag, isTTY } = {}) {
15
+ if (allFlag) return [...VALID_AGENTS];
16
+
17
+ if (agentFlag) {
18
+ const requested = agentFlag.split(',').map((s) => s.trim().toLowerCase());
19
+ const invalid = requested.filter((a) => !VALID_AGENTS.includes(a));
20
+ if (invalid.length > 0) {
21
+ console.error(` Unknown agent(s): ${invalid.join(', ')}`);
22
+ console.error(` Valid options: ${VALID_AGENTS.join(', ')}`);
23
+ process.exit(1);
24
+ }
25
+ return requested;
26
+ }
27
+
28
+ if (!isTTY) {
29
+ return ['claude'];
30
+ }
31
+
32
+ const { checkbox } = require('@inquirer/prompts');
33
+ const choices = VALID_AGENTS.map((name) => ({
34
+ name: agentLabel(name),
35
+ value: name,
36
+ checked: name === 'claude',
37
+ }));
38
+
39
+ const selected = await checkbox({
40
+ message: 'Which agent(s) should the review guide be installed for?',
41
+ choices,
42
+ validate: (val) => val.length > 0 || 'Select at least one agent.',
43
+ });
44
+
45
+ return selected;
46
+ }
47
+
48
+ function agentLabel(name) {
49
+ const labels = {
50
+ claude: 'Claude Code (.claude/agents/ + .claude/commands/)',
51
+ codex: 'OpenAI Codex CLI (swift-code-reviewer.md at repo root)',
52
+ gemini: 'Google Gemini CLI (swift-code-reviewer.md + .gemini/commands/review.toml)',
53
+ kiro: 'Kiro (.kiro/steering/swift-code-reviewer.md, fileMatch: **/*.swift)',
54
+ };
55
+ return labels[name] || name;
56
+ }
57
+
58
+ module.exports = { selectAgents };