@sentry/warden 0.0.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/.agents/skills/find-bugs/SKILL.md +75 -0
- package/.agents/skills/vercel-react-best-practices/AGENTS.md +2934 -0
- package/.agents/skills/vercel-react-best-practices/SKILL.md +136 -0
- package/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
- package/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
- package/.claude/settings.json +57 -0
- package/.claude/settings.local.json +88 -0
- package/.claude/skills/agent-prompt/SKILL.md +54 -0
- package/.claude/skills/agent-prompt/references/agentic-patterns.md +94 -0
- package/.claude/skills/agent-prompt/references/anti-patterns.md +140 -0
- package/.claude/skills/agent-prompt/references/context-design.md +124 -0
- package/.claude/skills/agent-prompt/references/core-principles.md +75 -0
- package/.claude/skills/agent-prompt/references/model-guidance.md +118 -0
- package/.claude/skills/agent-prompt/references/output-formats.md +98 -0
- package/.claude/skills/agent-prompt/references/skill-structure.md +115 -0
- package/.claude/skills/agent-prompt/references/system-prompts.md +115 -0
- package/.claude/skills/notseer/SKILL.md +131 -0
- package/.claude/skills/skill-writer/SKILL.md +140 -0
- package/.claude/skills/testing-guidelines/SKILL.md +132 -0
- package/.claude/skills/warden-skill/SKILL.md +250 -0
- package/.claude/skills/warden-skill/references/config-schema.md +133 -0
- package/.dex/config.toml +2 -0
- package/.github/workflows/ci.yml +33 -0
- package/.github/workflows/release.yml +54 -0
- package/.github/workflows/warden.yml +40 -0
- package/AGENTS.md +89 -0
- package/CONTRIBUTING.md +60 -0
- package/LICENSE +105 -0
- package/README.md +43 -0
- package/SPEC.md +263 -0
- package/action.yml +87 -0
- package/assets/favicon.png +0 -0
- package/assets/warden-icon-bw.svg +5 -0
- package/assets/warden-icon-purple.png +0 -0
- package/assets/warden-icon-purple.svg +5 -0
- package/docs/.claude/settings.local.json +11 -0
- package/docs/astro.config.mjs +43 -0
- package/docs/package.json +19 -0
- package/docs/pnpm-lock.yaml +4000 -0
- package/docs/public/favicon.svg +5 -0
- package/docs/src/components/Code.astro +141 -0
- package/docs/src/components/PackageManagerTabs.astro +183 -0
- package/docs/src/components/Terminal.astro +212 -0
- package/docs/src/layouts/Base.astro +380 -0
- package/docs/src/pages/cli.astro +167 -0
- package/docs/src/pages/config.astro +394 -0
- package/docs/src/pages/guide.astro +449 -0
- package/docs/src/pages/index.astro +490 -0
- package/docs/src/styles/global.css +551 -0
- package/docs/tsconfig.json +3 -0
- package/docs/vercel.json +5 -0
- package/eslint.config.js +33 -0
- package/package.json +73 -0
- package/src/action/index.ts +1 -0
- package/src/action/main.ts +868 -0
- package/src/cli/args.test.ts +477 -0
- package/src/cli/args.ts +415 -0
- package/src/cli/commands/add.ts +447 -0
- package/src/cli/commands/init.test.ts +136 -0
- package/src/cli/commands/init.ts +132 -0
- package/src/cli/commands/setup-app/browser.ts +38 -0
- package/src/cli/commands/setup-app/credentials.ts +45 -0
- package/src/cli/commands/setup-app/manifest.ts +48 -0
- package/src/cli/commands/setup-app/server.ts +172 -0
- package/src/cli/commands/setup-app.ts +156 -0
- package/src/cli/commands/sync.ts +114 -0
- package/src/cli/context.ts +131 -0
- package/src/cli/files.test.ts +155 -0
- package/src/cli/files.ts +89 -0
- package/src/cli/fix.test.ts +310 -0
- package/src/cli/fix.ts +387 -0
- package/src/cli/git.test.ts +119 -0
- package/src/cli/git.ts +318 -0
- package/src/cli/index.ts +14 -0
- package/src/cli/main.ts +672 -0
- package/src/cli/output/box.ts +235 -0
- package/src/cli/output/formatters.test.ts +187 -0
- package/src/cli/output/formatters.ts +269 -0
- package/src/cli/output/icons.ts +13 -0
- package/src/cli/output/index.ts +44 -0
- package/src/cli/output/ink-runner.tsx +337 -0
- package/src/cli/output/jsonl.test.ts +347 -0
- package/src/cli/output/jsonl.ts +126 -0
- package/src/cli/output/reporter.ts +435 -0
- package/src/cli/output/tasks.ts +374 -0
- package/src/cli/output/tty.test.ts +117 -0
- package/src/cli/output/tty.ts +60 -0
- package/src/cli/output/verbosity.test.ts +40 -0
- package/src/cli/output/verbosity.ts +31 -0
- package/src/cli/terminal.test.ts +148 -0
- package/src/cli/terminal.ts +301 -0
- package/src/config/index.ts +3 -0
- package/src/config/loader.test.ts +313 -0
- package/src/config/loader.ts +103 -0
- package/src/config/schema.ts +168 -0
- package/src/config/writer.test.ts +119 -0
- package/src/config/writer.ts +84 -0
- package/src/diff/classify.test.ts +162 -0
- package/src/diff/classify.ts +92 -0
- package/src/diff/coalesce.test.ts +208 -0
- package/src/diff/coalesce.ts +133 -0
- package/src/diff/context.test.ts +226 -0
- package/src/diff/context.ts +201 -0
- package/src/diff/index.ts +4 -0
- package/src/diff/parser.test.ts +212 -0
- package/src/diff/parser.ts +149 -0
- package/src/event/context.ts +132 -0
- package/src/event/index.ts +2 -0
- package/src/event/schedule-context.ts +101 -0
- package/src/examples/examples.integration.test.ts +66 -0
- package/src/examples/index.test.ts +101 -0
- package/src/examples/index.ts +122 -0
- package/src/examples/setup.ts +25 -0
- package/src/index.ts +115 -0
- package/src/output/dedup.test.ts +419 -0
- package/src/output/dedup.ts +607 -0
- package/src/output/github-checks.test.ts +300 -0
- package/src/output/github-checks.ts +476 -0
- package/src/output/github-issues.ts +329 -0
- package/src/output/index.ts +5 -0
- package/src/output/issue-renderer.ts +197 -0
- package/src/output/renderer.test.ts +727 -0
- package/src/output/renderer.ts +217 -0
- package/src/output/stale.test.ts +375 -0
- package/src/output/stale.ts +155 -0
- package/src/output/types.ts +34 -0
- package/src/sdk/index.ts +1 -0
- package/src/sdk/runner.test.ts +806 -0
- package/src/sdk/runner.ts +1232 -0
- package/src/skills/index.ts +36 -0
- package/src/skills/loader.test.ts +300 -0
- package/src/skills/loader.ts +423 -0
- package/src/skills/remote.test.ts +704 -0
- package/src/skills/remote.ts +604 -0
- package/src/triggers/matcher.test.ts +277 -0
- package/src/triggers/matcher.ts +152 -0
- package/src/types/index.ts +194 -0
- package/src/utils/async.ts +18 -0
- package/src/utils/index.test.ts +84 -0
- package/src/utils/index.ts +50 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +8 -0
- package/vitest.integration.config.ts +11 -0
- package/warden.toml +19 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import select from '@inquirer/select';
|
|
5
|
+
import { getRepoRoot } from '../git.js';
|
|
6
|
+
import { loadWardenConfig, appendTrigger } from '../../config/index.js';
|
|
7
|
+
import type { Trigger } from '../../config/schema.js';
|
|
8
|
+
import { discoverAllSkills, type DiscoveredSkill } from '../../skills/loader.js';
|
|
9
|
+
import {
|
|
10
|
+
fetchRemote,
|
|
11
|
+
discoverRemoteSkills,
|
|
12
|
+
parseRemoteRef,
|
|
13
|
+
type DiscoveredRemoteSkill,
|
|
14
|
+
} from '../../skills/remote.js';
|
|
15
|
+
import type { Reporter } from '../output/reporter.js';
|
|
16
|
+
import type { CLIOptions } from '../args.js';
|
|
17
|
+
import { ICON_CHECK } from '../output/icons.js';
|
|
18
|
+
|
|
19
|
+
/** Custom theme for select prompts - white for selected, gray for unselected */
|
|
20
|
+
const selectTheme = {
|
|
21
|
+
prefix: {
|
|
22
|
+
idle: '',
|
|
23
|
+
done: '',
|
|
24
|
+
},
|
|
25
|
+
icon: {
|
|
26
|
+
cursor: chalk.white('›'),
|
|
27
|
+
},
|
|
28
|
+
style: {
|
|
29
|
+
message: () => '', // We print heading separately
|
|
30
|
+
highlight: (text: string) => chalk.white(text),
|
|
31
|
+
disabled: (text: string) => chalk.dim(text),
|
|
32
|
+
description: (text: string) => chalk.white(text),
|
|
33
|
+
keysHelpTip: (keys: [key: string, action: string][]) => {
|
|
34
|
+
const keyStr = keys.map(([key, action]) => `${key} ${action}`).join(', ');
|
|
35
|
+
return `\n${chalk.dim(keyStr)}`;
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Render the list of available local skills.
|
|
42
|
+
*/
|
|
43
|
+
function renderSkillList(
|
|
44
|
+
skills: Map<string, DiscoveredSkill>,
|
|
45
|
+
configuredSkills: Set<string>,
|
|
46
|
+
reporter: Reporter
|
|
47
|
+
): void {
|
|
48
|
+
reporter.bold('Available Skills');
|
|
49
|
+
reporter.blank();
|
|
50
|
+
|
|
51
|
+
// Sort skills alphabetically by name
|
|
52
|
+
const sortedSkills = Array.from(skills.entries()).sort((a, b) => a[0].localeCompare(b[0]));
|
|
53
|
+
|
|
54
|
+
for (const [name, discovered] of sortedSkills) {
|
|
55
|
+
const isConfigured = configuredSkills.has(name);
|
|
56
|
+
const dirTag = chalk.dim(`[${discovered.directory}]`);
|
|
57
|
+
const configuredTag = isConfigured ? chalk.dim(' (already configured)') : '';
|
|
58
|
+
|
|
59
|
+
if (reporter.mode.isTTY) {
|
|
60
|
+
const icon = isConfigured ? chalk.dim(ICON_CHECK) : ' ';
|
|
61
|
+
reporter.text(` ${icon} ${chalk.bold(name)} ${dirTag}${configuredTag}`);
|
|
62
|
+
reporter.text(` ${chalk.dim(discovered.skill.description)}`);
|
|
63
|
+
} else {
|
|
64
|
+
const status = isConfigured ? '[configured]' : '';
|
|
65
|
+
reporter.text(`${name} ${status} [${discovered.directory}]`);
|
|
66
|
+
reporter.text(` ${discovered.skill.description}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Render the list of available remote skills.
|
|
73
|
+
*/
|
|
74
|
+
function renderRemoteSkillList(
|
|
75
|
+
skills: DiscoveredRemoteSkill[],
|
|
76
|
+
configuredSkills: Set<string>,
|
|
77
|
+
remote: string,
|
|
78
|
+
reporter: Reporter
|
|
79
|
+
): void {
|
|
80
|
+
reporter.bold(`Available Skills from ${remote}`);
|
|
81
|
+
reporter.blank();
|
|
82
|
+
|
|
83
|
+
// Sort skills alphabetically by name
|
|
84
|
+
const sortedSkills = [...skills].sort((a, b) => a.name.localeCompare(b.name));
|
|
85
|
+
|
|
86
|
+
for (const skill of sortedSkills) {
|
|
87
|
+
const isConfigured = configuredSkills.has(skill.name);
|
|
88
|
+
const configuredTag = isConfigured ? chalk.dim(' (already configured)') : '';
|
|
89
|
+
|
|
90
|
+
if (reporter.mode.isTTY) {
|
|
91
|
+
const icon = isConfigured ? chalk.dim(ICON_CHECK) : ' ';
|
|
92
|
+
reporter.text(` ${icon} ${chalk.bold(skill.name)}${configuredTag}`);
|
|
93
|
+
reporter.text(` ${chalk.dim(skill.description)}`);
|
|
94
|
+
} else {
|
|
95
|
+
const status = isConfigured ? '[configured]' : '';
|
|
96
|
+
reporter.text(`${skill.name} ${status}`);
|
|
97
|
+
reporter.text(` ${skill.description}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Prompt user to select a local skill interactively.
|
|
104
|
+
*/
|
|
105
|
+
async function promptSkillSelection(
|
|
106
|
+
skills: Map<string, DiscoveredSkill>,
|
|
107
|
+
configuredSkills: Set<string>,
|
|
108
|
+
reporter: Reporter,
|
|
109
|
+
): Promise<string | null> {
|
|
110
|
+
// Filter out already configured skills for selection
|
|
111
|
+
const availableSkills = Array.from(skills.entries())
|
|
112
|
+
.filter(([name]) => !configuredSkills.has(name))
|
|
113
|
+
.sort((a, b) => a[0].localeCompare(b[0]));
|
|
114
|
+
|
|
115
|
+
if (availableSkills.length === 0) {
|
|
116
|
+
reporter.warning('All available skills are already configured.');
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const choices = availableSkills.map(([name, discovered]) => {
|
|
121
|
+
return {
|
|
122
|
+
name: `${name} ${chalk.dim(`[${discovered.directory}]`)}`,
|
|
123
|
+
value: name,
|
|
124
|
+
description: discovered.skill.description,
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
reporter.bold('ADD SKILL');
|
|
129
|
+
reporter.blank();
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const answer = await select({
|
|
133
|
+
message: '',
|
|
134
|
+
choices,
|
|
135
|
+
theme: selectTheme,
|
|
136
|
+
});
|
|
137
|
+
// Clear the inquirer "done" line
|
|
138
|
+
process.stderr.write('\x1b[1A\x1b[2K');
|
|
139
|
+
return answer;
|
|
140
|
+
} catch {
|
|
141
|
+
// User cancelled (Ctrl+C or escape)
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Prompt user to select a remote skill interactively.
|
|
148
|
+
*/
|
|
149
|
+
async function promptRemoteSkillSelection(
|
|
150
|
+
skills: DiscoveredRemoteSkill[],
|
|
151
|
+
configuredSkills: Set<string>,
|
|
152
|
+
reporter: Reporter,
|
|
153
|
+
): Promise<string | null> {
|
|
154
|
+
// Filter out already configured skills for selection
|
|
155
|
+
const availableSkills = skills
|
|
156
|
+
.filter((s) => !configuredSkills.has(s.name))
|
|
157
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
158
|
+
|
|
159
|
+
if (availableSkills.length === 0) {
|
|
160
|
+
reporter.warning('All available skills are already configured.');
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const choices = availableSkills.map((skill) => {
|
|
165
|
+
return {
|
|
166
|
+
name: skill.name,
|
|
167
|
+
value: skill.name,
|
|
168
|
+
description: skill.description,
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
reporter.bold('ADD REMOTE SKILL');
|
|
173
|
+
reporter.blank();
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const answer = await select({
|
|
177
|
+
message: '',
|
|
178
|
+
choices,
|
|
179
|
+
theme: selectTheme,
|
|
180
|
+
});
|
|
181
|
+
// Clear the inquirer "done" line
|
|
182
|
+
process.stderr.write('\x1b[1A\x1b[2K');
|
|
183
|
+
return answer;
|
|
184
|
+
} catch {
|
|
185
|
+
// User cancelled (Ctrl+C or escape)
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Create a default trigger for a local skill.
|
|
192
|
+
*/
|
|
193
|
+
function createDefaultTrigger(skillName: string): Trigger {
|
|
194
|
+
return {
|
|
195
|
+
name: skillName,
|
|
196
|
+
event: 'pull_request',
|
|
197
|
+
actions: ['opened', 'synchronize', 'reopened'],
|
|
198
|
+
skill: skillName,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Create a trigger for a remote skill.
|
|
204
|
+
*/
|
|
205
|
+
function createRemoteTrigger(skillName: string, remote: string): Trigger {
|
|
206
|
+
return {
|
|
207
|
+
name: skillName,
|
|
208
|
+
event: 'pull_request',
|
|
209
|
+
actions: ['opened', 'synchronize', 'reopened'],
|
|
210
|
+
skill: skillName,
|
|
211
|
+
remote,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Run the add command for remote skills.
|
|
217
|
+
*/
|
|
218
|
+
async function runAddRemote(
|
|
219
|
+
options: CLIOptions,
|
|
220
|
+
reporter: Reporter,
|
|
221
|
+
configPath: string,
|
|
222
|
+
configuredSkills: Set<string>
|
|
223
|
+
): Promise<number> {
|
|
224
|
+
const remote = options.remote ?? '';
|
|
225
|
+
if (!remote) {
|
|
226
|
+
reporter.error('Remote repository is required');
|
|
227
|
+
return 1;
|
|
228
|
+
}
|
|
229
|
+
const cwd = process.cwd();
|
|
230
|
+
|
|
231
|
+
// Validate remote ref format
|
|
232
|
+
try {
|
|
233
|
+
parseRemoteRef(remote);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
236
|
+
reporter.error(message);
|
|
237
|
+
return 1;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Fetch remote repository
|
|
241
|
+
reporter.step(`Fetching skills from ${remote}...`);
|
|
242
|
+
try {
|
|
243
|
+
await fetchRemote(remote, {
|
|
244
|
+
onProgress: (msg) => reporter.debug(msg),
|
|
245
|
+
});
|
|
246
|
+
} catch (err) {
|
|
247
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
248
|
+
reporter.error(`Failed to fetch remote: ${message}`);
|
|
249
|
+
return 1;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Discover skills in the remote
|
|
253
|
+
const remoteSkills = await discoverRemoteSkills(remote);
|
|
254
|
+
|
|
255
|
+
if (remoteSkills.length === 0) {
|
|
256
|
+
reporter.error(`No skills found in remote: ${remote}`);
|
|
257
|
+
return 1;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
reporter.success(`Found ${remoteSkills.length} skill${remoteSkills.length === 1 ? '' : 's'}`);
|
|
261
|
+
|
|
262
|
+
// Handle --list: display remote skills and exit
|
|
263
|
+
if (options.list) {
|
|
264
|
+
reporter.blank();
|
|
265
|
+
renderRemoteSkillList(remoteSkills, configuredSkills, remote, reporter);
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Get skill to add (from --skill or interactive prompt)
|
|
270
|
+
let skillName: string | null;
|
|
271
|
+
|
|
272
|
+
if (options.skill) {
|
|
273
|
+
skillName = options.skill;
|
|
274
|
+
} else if (reporter.mode.isTTY) {
|
|
275
|
+
reporter.blank();
|
|
276
|
+
skillName = await promptRemoteSkillSelection(remoteSkills, configuredSkills, reporter);
|
|
277
|
+
if (!skillName) {
|
|
278
|
+
return 0; // User quit or no skills available
|
|
279
|
+
}
|
|
280
|
+
} else {
|
|
281
|
+
reporter.error('Skill name required when not running interactively.');
|
|
282
|
+
reporter.tip(`Use: warden add --remote ${remote} --skill <name>`);
|
|
283
|
+
return 1;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Validate skill exists in remote
|
|
287
|
+
const skill = remoteSkills.find((s) => s.name === skillName);
|
|
288
|
+
if (!skill) {
|
|
289
|
+
reporter.error(`Skill '${skillName}' not found in remote: ${remote}`);
|
|
290
|
+
reporter.blank();
|
|
291
|
+
reporter.tip('Available skills:');
|
|
292
|
+
for (const s of remoteSkills) {
|
|
293
|
+
reporter.text(` - ${s.name}`);
|
|
294
|
+
}
|
|
295
|
+
return 1;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Check for duplicate trigger
|
|
299
|
+
if (configuredSkills.has(skillName)) {
|
|
300
|
+
reporter.warning(`Trigger '${skillName}' already exists in warden.toml`);
|
|
301
|
+
reporter.skipped(relative(cwd, configPath), 'trigger already configured');
|
|
302
|
+
return 0;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Append trigger to warden.toml
|
|
306
|
+
const trigger = createRemoteTrigger(skillName, remote);
|
|
307
|
+
try {
|
|
308
|
+
appendTrigger(configPath, trigger);
|
|
309
|
+
reporter.success(`Added trigger '${skillName}' to ${relative(cwd, configPath)}`);
|
|
310
|
+
} catch (err) {
|
|
311
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
312
|
+
reporter.error(`Failed to update warden.toml: ${message}`);
|
|
313
|
+
return 1;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Show success message
|
|
317
|
+
reporter.blank();
|
|
318
|
+
reporter.text(`The trigger will run on pull requests using skill from ${chalk.cyan(remote)}.`);
|
|
319
|
+
reporter.text(`Edit ${chalk.cyan('warden.toml')} to customize filters and output options.`);
|
|
320
|
+
|
|
321
|
+
return 0;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Run the add command.
|
|
326
|
+
*/
|
|
327
|
+
export async function runAdd(options: CLIOptions, reporter: Reporter): Promise<number> {
|
|
328
|
+
const cwd = process.cwd();
|
|
329
|
+
|
|
330
|
+
// 1. Check git repo
|
|
331
|
+
let repoRoot: string;
|
|
332
|
+
try {
|
|
333
|
+
repoRoot = getRepoRoot(cwd);
|
|
334
|
+
} catch {
|
|
335
|
+
reporter.error('Not a git repository. Run this command from a git repository.');
|
|
336
|
+
return 1;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// 2. Check warden.toml exists (deferred for --list without --remote)
|
|
340
|
+
const configPath = join(repoRoot, 'warden.toml');
|
|
341
|
+
const hasConfig = existsSync(configPath);
|
|
342
|
+
|
|
343
|
+
// 3. Load existing config if available
|
|
344
|
+
let configuredSkills = new Set<string>();
|
|
345
|
+
if (hasConfig) {
|
|
346
|
+
try {
|
|
347
|
+
const config = loadWardenConfig(repoRoot);
|
|
348
|
+
configuredSkills = new Set(config.triggers.map((t) => t.name));
|
|
349
|
+
} catch (err) {
|
|
350
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
351
|
+
reporter.error(`Failed to load warden.toml: ${message}`);
|
|
352
|
+
return 1;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// 4. Handle remote skills with --remote flag
|
|
357
|
+
if (options.remote) {
|
|
358
|
+
// For remote skills, require warden.toml (unless --list)
|
|
359
|
+
if (!hasConfig && !options.list) {
|
|
360
|
+
reporter.error('warden.toml not found.');
|
|
361
|
+
reporter.tip('Run `warden init` first to create the configuration file.');
|
|
362
|
+
return 1;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return runAddRemote(options, reporter, configPath, configuredSkills);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// 5. Discover local skills
|
|
369
|
+
const skills = await discoverAllSkills(repoRoot, {
|
|
370
|
+
onWarning: (message) => reporter.warning(message),
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
if (skills.size === 0) {
|
|
374
|
+
reporter.error('No skills found.');
|
|
375
|
+
reporter.tip('Add skills to .warden/skills/, .agents/skills/, or .claude/skills/');
|
|
376
|
+
reporter.tip('Or use --remote to add remote skills: warden add --remote owner/repo --skill name');
|
|
377
|
+
return 1;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// 6. Handle --list: display skills and exit (works without warden.toml)
|
|
381
|
+
if (options.list) {
|
|
382
|
+
renderSkillList(skills, configuredSkills, reporter);
|
|
383
|
+
return 0;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// 7. For adding skills, require warden.toml
|
|
387
|
+
if (!hasConfig) {
|
|
388
|
+
reporter.error('warden.toml not found.');
|
|
389
|
+
reporter.tip('Run `warden init` first to create the configuration file.');
|
|
390
|
+
return 1;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// 8. Get skill to add (from arg or interactive prompt)
|
|
394
|
+
let skillName: string | null;
|
|
395
|
+
|
|
396
|
+
if (options.skill) {
|
|
397
|
+
// Non-interactive: skill specified as argument
|
|
398
|
+
skillName = options.skill;
|
|
399
|
+
} else if (reporter.mode.isTTY) {
|
|
400
|
+
// Interactive mode
|
|
401
|
+
skillName = await promptSkillSelection(skills, configuredSkills, reporter);
|
|
402
|
+
if (!skillName) {
|
|
403
|
+
return 0; // User quit or no skills available
|
|
404
|
+
}
|
|
405
|
+
} else {
|
|
406
|
+
// Non-TTY and no skill specified
|
|
407
|
+
reporter.error('Skill name required when not running interactively.');
|
|
408
|
+
reporter.tip('Use: warden add <skill-name> or warden add --list');
|
|
409
|
+
return 1;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// 9. Validate skill exists
|
|
413
|
+
if (!skills.has(skillName)) {
|
|
414
|
+
reporter.error(`Skill not found: ${skillName}`);
|
|
415
|
+
reporter.blank();
|
|
416
|
+
reporter.tip('Available skills:');
|
|
417
|
+
for (const name of skills.keys()) {
|
|
418
|
+
reporter.text(` - ${name}`);
|
|
419
|
+
}
|
|
420
|
+
return 1;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// 10. Check for duplicate trigger
|
|
424
|
+
if (configuredSkills.has(skillName)) {
|
|
425
|
+
reporter.warning(`Trigger '${skillName}' already exists in warden.toml`);
|
|
426
|
+
reporter.skipped(relative(cwd, configPath), 'trigger already configured');
|
|
427
|
+
return 0;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// 11. Append trigger to warden.toml
|
|
431
|
+
const trigger = createDefaultTrigger(skillName);
|
|
432
|
+
try {
|
|
433
|
+
appendTrigger(configPath, trigger);
|
|
434
|
+
reporter.success(`Added trigger '${skillName}' to ${relative(cwd, configPath)}`);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
437
|
+
reporter.error(`Failed to update warden.toml: ${message}`);
|
|
438
|
+
return 1;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// 12. Show success message with next steps
|
|
442
|
+
reporter.blank();
|
|
443
|
+
reporter.text(`The trigger will run on pull requests.`);
|
|
444
|
+
reporter.text(`Edit ${chalk.cyan('warden.toml')} to customize filters and output options.`);
|
|
445
|
+
|
|
446
|
+
return 0;
|
|
447
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
import { runInit } from './init.js';
|
|
7
|
+
import { Reporter } from '../output/reporter.js';
|
|
8
|
+
import { detectOutputMode } from '../output/tty.js';
|
|
9
|
+
import { Verbosity } from '../output/verbosity.js';
|
|
10
|
+
import type { CLIOptions } from '../args.js';
|
|
11
|
+
|
|
12
|
+
function createMockReporter(): Reporter {
|
|
13
|
+
return new Reporter(detectOutputMode(false), Verbosity.Normal);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createOptions(overrides: Partial<CLIOptions> = {}): CLIOptions {
|
|
17
|
+
return {
|
|
18
|
+
json: false,
|
|
19
|
+
help: false,
|
|
20
|
+
quiet: false,
|
|
21
|
+
verbose: 0,
|
|
22
|
+
fix: false,
|
|
23
|
+
force: false,
|
|
24
|
+
list: false,
|
|
25
|
+
git: false,
|
|
26
|
+
offline: false,
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('init command', () => {
|
|
32
|
+
let tempDir: string;
|
|
33
|
+
let originalCwd: string;
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
tempDir = join(tmpdir(), `warden-init-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
37
|
+
mkdirSync(tempDir, { recursive: true });
|
|
38
|
+
originalCwd = process.cwd();
|
|
39
|
+
process.chdir(tempDir);
|
|
40
|
+
// Initialize git repo for tests
|
|
41
|
+
execSync('git init', { cwd: tempDir, stdio: 'ignore' });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
process.chdir(originalCwd);
|
|
46
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('file creation', () => {
|
|
50
|
+
it('creates warden.toml and workflow in fresh repo', async () => {
|
|
51
|
+
const reporter = createMockReporter();
|
|
52
|
+
const exitCode = await runInit(createOptions(), reporter);
|
|
53
|
+
|
|
54
|
+
expect(exitCode).toBe(0);
|
|
55
|
+
expect(existsSync(join(tempDir, 'warden.toml'))).toBe(true);
|
|
56
|
+
expect(existsSync(join(tempDir, '.github', 'workflows', 'warden.yml'))).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('creates warden.toml with correct content', async () => {
|
|
60
|
+
const reporter = createMockReporter();
|
|
61
|
+
await runInit(createOptions(), reporter);
|
|
62
|
+
|
|
63
|
+
const content = readFileSync(join(tempDir, 'warden.toml'), 'utf-8');
|
|
64
|
+
expect(content).toContain('version = 1');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('creates workflow with correct content', async () => {
|
|
68
|
+
const reporter = createMockReporter();
|
|
69
|
+
await runInit(createOptions(), reporter);
|
|
70
|
+
|
|
71
|
+
const content = readFileSync(join(tempDir, '.github', 'workflows', 'warden.yml'), 'utf-8');
|
|
72
|
+
expect(content).toContain('name: Warden');
|
|
73
|
+
expect(content).toContain('pull_request');
|
|
74
|
+
expect(content).toContain('permissions:');
|
|
75
|
+
expect(content).toContain('pull-requests: write');
|
|
76
|
+
expect(content).toContain('checks: write');
|
|
77
|
+
expect(content).toContain('WARDEN_ANTHROPIC_API_KEY');
|
|
78
|
+
expect(content).toContain('getsentry/warden@v1');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('existing files', () => {
|
|
83
|
+
it('skips existing files without --force', async () => {
|
|
84
|
+
// Create existing files
|
|
85
|
+
writeFileSync(join(tempDir, 'warden.toml'), 'existing content');
|
|
86
|
+
mkdirSync(join(tempDir, '.github', 'workflows'), { recursive: true });
|
|
87
|
+
writeFileSync(join(tempDir, '.github', 'workflows', 'warden.yml'), 'existing workflow');
|
|
88
|
+
|
|
89
|
+
const reporter = createMockReporter();
|
|
90
|
+
await runInit(createOptions(), reporter);
|
|
91
|
+
|
|
92
|
+
// Files should not be overwritten
|
|
93
|
+
expect(readFileSync(join(tempDir, 'warden.toml'), 'utf-8')).toBe('existing content');
|
|
94
|
+
expect(readFileSync(join(tempDir, '.github', 'workflows', 'warden.yml'), 'utf-8')).toBe('existing workflow');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('overwrites existing files with --force', async () => {
|
|
98
|
+
// Create existing files
|
|
99
|
+
writeFileSync(join(tempDir, 'warden.toml'), 'existing content');
|
|
100
|
+
mkdirSync(join(tempDir, '.github', 'workflows'), { recursive: true });
|
|
101
|
+
writeFileSync(join(tempDir, '.github', 'workflows', 'warden.yml'), 'existing workflow');
|
|
102
|
+
|
|
103
|
+
const reporter = createMockReporter();
|
|
104
|
+
await runInit(createOptions({ force: true }), reporter);
|
|
105
|
+
|
|
106
|
+
// Files should be overwritten with new content
|
|
107
|
+
const tomlContent = readFileSync(join(tempDir, 'warden.toml'), 'utf-8');
|
|
108
|
+
expect(tomlContent).toContain('version = 1');
|
|
109
|
+
expect(tomlContent).not.toBe('existing content');
|
|
110
|
+
|
|
111
|
+
const workflowContent = readFileSync(join(tempDir, '.github', 'workflows', 'warden.yml'), 'utf-8');
|
|
112
|
+
expect(workflowContent).toContain('name: Warden');
|
|
113
|
+
expect(workflowContent).not.toBe('existing workflow');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('error handling', () => {
|
|
118
|
+
it('fails when not in a git repository', async () => {
|
|
119
|
+
// Create a non-git directory
|
|
120
|
+
const nonGitDir = join(tmpdir(), `warden-non-git-${Date.now()}`);
|
|
121
|
+
mkdirSync(nonGitDir, { recursive: true });
|
|
122
|
+
process.chdir(nonGitDir);
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const reporter = createMockReporter();
|
|
126
|
+
const exitCode = await runInit(createOptions(), reporter);
|
|
127
|
+
|
|
128
|
+
expect(exitCode).toBe(1);
|
|
129
|
+
expect(existsSync(join(nonGitDir, 'warden.toml'))).toBe(false);
|
|
130
|
+
} finally {
|
|
131
|
+
process.chdir(originalCwd);
|
|
132
|
+
rmSync(nonGitDir, { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { getRepoRoot, getGitHubRepoUrl } from '../git.js';
|
|
5
|
+
import type { Reporter } from '../output/reporter.js';
|
|
6
|
+
import type { CLIOptions } from '../args.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Template for warden.toml configuration file.
|
|
10
|
+
*/
|
|
11
|
+
function generateWardenToml(): string {
|
|
12
|
+
return `version = 1
|
|
13
|
+
`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Template for GitHub Actions workflow file.
|
|
18
|
+
*/
|
|
19
|
+
function generateWorkflowYaml(): string {
|
|
20
|
+
return `name: Warden
|
|
21
|
+
|
|
22
|
+
on:
|
|
23
|
+
pull_request:
|
|
24
|
+
types: [opened, synchronize, reopened]
|
|
25
|
+
|
|
26
|
+
permissions:
|
|
27
|
+
contents: read
|
|
28
|
+
pull-requests: write
|
|
29
|
+
checks: write
|
|
30
|
+
|
|
31
|
+
jobs:
|
|
32
|
+
review:
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v4
|
|
36
|
+
- uses: getsentry/warden@v1
|
|
37
|
+
with:
|
|
38
|
+
anthropic-api-key: \${{ secrets.WARDEN_ANTHROPIC_API_KEY }}
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check for existing warden configuration files.
|
|
44
|
+
*/
|
|
45
|
+
function checkExistingFiles(repoRoot: string): {
|
|
46
|
+
hasWardenToml: boolean;
|
|
47
|
+
hasWorkflow: boolean;
|
|
48
|
+
} {
|
|
49
|
+
const wardenTomlPath = join(repoRoot, 'warden.toml');
|
|
50
|
+
const workflowPath = join(repoRoot, '.github', 'workflows', 'warden.yml');
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
hasWardenToml: existsSync(wardenTomlPath),
|
|
54
|
+
hasWorkflow: existsSync(workflowPath),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface InitOptions {
|
|
59
|
+
force: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Run the init command to scaffold warden configuration.
|
|
64
|
+
*/
|
|
65
|
+
export async function runInit(options: CLIOptions, reporter: Reporter): Promise<number> {
|
|
66
|
+
const cwd = process.cwd();
|
|
67
|
+
|
|
68
|
+
// Find repo root
|
|
69
|
+
let repoRoot: string;
|
|
70
|
+
try {
|
|
71
|
+
repoRoot = getRepoRoot(cwd);
|
|
72
|
+
} catch {
|
|
73
|
+
reporter.error('Not a git repository. Run this command from a git repository.');
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check for existing files
|
|
78
|
+
const existing = checkExistingFiles(repoRoot);
|
|
79
|
+
|
|
80
|
+
let filesCreated = 0;
|
|
81
|
+
|
|
82
|
+
// Create warden.toml
|
|
83
|
+
const wardenTomlPath = join(repoRoot, 'warden.toml');
|
|
84
|
+
if (existing.hasWardenToml && !options.force) {
|
|
85
|
+
reporter.skipped(relative(cwd, wardenTomlPath), 'already exists');
|
|
86
|
+
} else {
|
|
87
|
+
const content = generateWardenToml();
|
|
88
|
+
writeFileSync(wardenTomlPath, content, 'utf-8');
|
|
89
|
+
reporter.created(relative(cwd, wardenTomlPath));
|
|
90
|
+
filesCreated++;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Create .github/workflows directory if needed
|
|
94
|
+
const workflowDir = join(repoRoot, '.github', 'workflows');
|
|
95
|
+
if (!existsSync(workflowDir)) {
|
|
96
|
+
mkdirSync(workflowDir, { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Create workflow file
|
|
100
|
+
const workflowPath = join(workflowDir, 'warden.yml');
|
|
101
|
+
if (existing.hasWorkflow && !options.force) {
|
|
102
|
+
reporter.skipped(relative(cwd, workflowPath), 'already exists');
|
|
103
|
+
} else {
|
|
104
|
+
const content = generateWorkflowYaml();
|
|
105
|
+
writeFileSync(workflowPath, content, 'utf-8');
|
|
106
|
+
reporter.created(relative(cwd, workflowPath));
|
|
107
|
+
filesCreated++;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (filesCreated === 0) {
|
|
111
|
+
reporter.blank();
|
|
112
|
+
reporter.tip('All configuration files already exist. Use --force to overwrite.');
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Print next steps
|
|
117
|
+
reporter.blank();
|
|
118
|
+
reporter.bold('Next steps:');
|
|
119
|
+
reporter.text(` 1. Add a skill: ${chalk.cyan('warden add <skill-name>')}`);
|
|
120
|
+
reporter.text(` 2. Set ${chalk.cyan('WARDEN_ANTHROPIC_API_KEY')} in .env.local`);
|
|
121
|
+
reporter.text(` 3. Add ${chalk.cyan('WARDEN_ANTHROPIC_API_KEY')} to repository secrets`);
|
|
122
|
+
|
|
123
|
+
// Show GitHub secrets URL if available
|
|
124
|
+
const githubUrl = getGitHubRepoUrl(repoRoot);
|
|
125
|
+
if (githubUrl) {
|
|
126
|
+
reporter.text(` ${chalk.dim(githubUrl + '/settings/secrets/actions')}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
reporter.text(' 4. Commit and open a PR to test');
|
|
130
|
+
|
|
131
|
+
return 0;
|
|
132
|
+
}
|