@codapult/guard 0.1.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/LICENSE +21 -0
- package/README.md +290 -0
- package/dist/adapters/agents/agent-integration.d.ts +9 -0
- package/dist/adapters/agents/agent-integration.js +62 -0
- package/dist/adapters/command.d.ts +21 -0
- package/dist/adapters/command.js +60 -0
- package/dist/adapters/project-checks.d.ts +37 -0
- package/dist/adapters/project-checks.js +149 -0
- package/dist/cli/commands/guard.d.ts +55 -0
- package/dist/cli/commands/guard.js +510 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +75 -0
- package/dist/cli/ui.d.ts +6 -0
- package/dist/cli/ui.js +7 -0
- package/dist/commands/guard.d.ts +2 -0
- package/dist/commands/guard.js +2 -0
- package/dist/core/analysis/doctor.d.ts +12 -0
- package/dist/core/analysis/doctor.js +85 -0
- package/dist/core/analysis/packs.d.ts +21 -0
- package/dist/core/analysis/packs.js +251 -0
- package/dist/core/config.d.ts +6 -0
- package/dist/core/config.js +6 -0
- package/dist/core/discovery/discovery.d.ts +134 -0
- package/dist/core/discovery/discovery.js +816 -0
- package/dist/core/guard.d.ts +189 -0
- package/dist/core/guard.js +936 -0
- package/dist/core/history/history.d.ts +24 -0
- package/dist/core/history/history.js +65 -0
- package/dist/core/output/sarif.d.ts +35 -0
- package/dist/core/output/sarif.js +29 -0
- package/dist/core/policy/schemas.d.ts +272 -0
- package/dist/core/policy/schemas.js +72 -0
- package/dist/core/verification/verify.d.ts +29 -0
- package/dist/core/verification/verify.js +77 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +9 -0
- package/dist/mcp/prompts.d.ts +2 -0
- package/dist/mcp/prompts.js +54 -0
- package/dist/mcp/resources.d.ts +2 -0
- package/dist/mcp/resources.js +64 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +17 -0
- package/dist/mcp/tools/guard.d.ts +2 -0
- package/dist/mcp/tools/guard.js +375 -0
- package/package.json +117 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Codapult
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# @codapult/guard
|
|
2
|
+
|
|
3
|
+
[](https://github.com/codapult/codapult-guard/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/codapult/codapult-guard/actions/workflows/guard-fixtures.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](package.json)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
|
|
9
|
+
## Architecture guardrails for AI coding agents
|
|
10
|
+
|
|
11
|
+
`@codapult/guard` is a local-first, model-agnostic architecture guard for JavaScript and
|
|
12
|
+
TypeScript projects.
|
|
13
|
+
|
|
14
|
+
It learns what already exists, records the project’s architectural memory, and protects future
|
|
15
|
+
changes from introducing regressions.
|
|
16
|
+
|
|
17
|
+
> **Guard does not tell every project to use the same architecture.**
|
|
18
|
+
> It discovers the architecture that is already there, then lets the team decide what becomes policy.
|
|
19
|
+
|
|
20
|
+
The core is universal. The strongest first-class scenarios are Next.js SaaS and AI-assisted
|
|
21
|
+
development, including server/client boundaries, routes, persistence, authentication, billing,
|
|
22
|
+
background jobs, environment configuration, and AI integrations.
|
|
23
|
+
|
|
24
|
+
## Part of the Codapult ecosystem
|
|
25
|
+
|
|
26
|
+
Guard is an independent open-source project from [Codapult](https://codapult.dev). It does not
|
|
27
|
+
require Codapult and can be installed in any JavaScript or TypeScript repository.
|
|
28
|
+
|
|
29
|
+
The relationship is complementary:
|
|
30
|
+
|
|
31
|
+
| Project | Role |
|
|
32
|
+
| --------------------- | ---------------------------------------------------------------------------------------- |
|
|
33
|
+
| **`@codapult/guard`** | Universal architecture guardrails, project memory, contracts, MCP, and AI-agent context. |
|
|
34
|
+
| **`@codapult/cli`** | Codapult SaaS project CLI that includes Guard through a thin adapter. |
|
|
35
|
+
| **Codapult** | Full-source Next.js SaaS foundation with conventions Guard can discover and protect. |
|
|
36
|
+
|
|
37
|
+
Use standalone Guard for any compatible project. Use the Codapult CLI when working on a Codapult
|
|
38
|
+
SaaS project and you want project management, database, plugins, deployment, MCP, and Guard in one
|
|
39
|
+
CLI.
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
Existing project
|
|
43
|
+
│
|
|
44
|
+
▼
|
|
45
|
+
deterministic discovery
|
|
46
|
+
AST · files · imports · Git
|
|
47
|
+
│
|
|
48
|
+
▼
|
|
49
|
+
project architecture
|
|
50
|
+
capabilities · graph · impact paths
|
|
51
|
+
│
|
|
52
|
+
▼
|
|
53
|
+
approved project policy
|
|
54
|
+
rules · contracts · conventions · baseline
|
|
55
|
+
│
|
|
56
|
+
▼
|
|
57
|
+
change verification
|
|
58
|
+
check · review packet · verify · CI
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Why Guard exists
|
|
62
|
+
|
|
63
|
+
AI agents can produce syntactically valid code that still violates the architecture of a real
|
|
64
|
+
project: a client component imports server-only code, a new action bypasses the established auth
|
|
65
|
+
boundary, a route writes to the database directly, or a change duplicates a service that already
|
|
66
|
+
exists.
|
|
67
|
+
|
|
68
|
+
Existing tools remain essential, but they solve different problems:
|
|
69
|
+
|
|
70
|
+
| Tool category | Primary question | Guard’s relationship |
|
|
71
|
+
| -------------------------- | -------------------------------------------------- | --------------------------------------------------------------------- |
|
|
72
|
+
| TypeScript | Is the code type-correct? | Uses the project’s typecheck as an optional gate. |
|
|
73
|
+
| ESLint / Biome | Does code follow language and style rules? | Does not duplicate their lint rules. |
|
|
74
|
+
| Tests | Does behavior match executable expectations? | Runs configured checks when enabled; does not replace tests. |
|
|
75
|
+
| SAST / dependency scanners | Is there a known security or dependency risk? | Can connect adapters; focuses on architecture and change impact. |
|
|
76
|
+
| PR review services | What semantic concerns should a reviewer consider? | Produces a bounded, redacted review packet for the selected AI host. |
|
|
77
|
+
| **Guard** | Is the project becoming architecturally worse? | Maintains project-specific architectural memory and regression gates. |
|
|
78
|
+
|
|
79
|
+
## Install
|
|
80
|
+
|
|
81
|
+
Requirements: Node.js `>=20.19` and a JavaScript or TypeScript project.
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pnpm add -D @codapult/guard
|
|
85
|
+
# or
|
|
86
|
+
npm install --save-dev @codapult/guard
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The package exposes the `codapult-guard` binary:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pnpm exec codapult-guard init
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The npm package is scoped as `@codapult/guard`; the executable intentionally remains
|
|
96
|
+
`codapult-guard` for discoverability and consistency with the standalone product name.
|
|
97
|
+
|
|
98
|
+
## 60-second quick start
|
|
99
|
+
|
|
100
|
+
Run from the project root:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# 1. Build the initial project model and baseline
|
|
104
|
+
pnpm exec codapult-guard init
|
|
105
|
+
|
|
106
|
+
# 2. Inspect only new and changed architecture findings
|
|
107
|
+
pnpm exec codapult-guard check --changed
|
|
108
|
+
|
|
109
|
+
# 3. Prepare deterministic context for an AI review
|
|
110
|
+
pnpm exec codapult-guard review --requirement docs/acceptance.md
|
|
111
|
+
|
|
112
|
+
# 4. Run the configured completion gate
|
|
113
|
+
pnpm exec codapult-guard verify --json
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`init` is protected and refuses to overwrite an existing baseline. Use `init --force` only when
|
|
117
|
+
deliberately replacing the project memory. Use `analyze` to refresh discovered facts without
|
|
118
|
+
resetting the baseline.
|
|
119
|
+
|
|
120
|
+
## The operating model
|
|
121
|
+
|
|
122
|
+
Guard separates facts, policy, verification, and decision:
|
|
123
|
+
|
|
124
|
+
| Layer | Contains | How it is produced |
|
|
125
|
+
| ---------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------- |
|
|
126
|
+
| **Facts** | AST, files, imports, dependencies, routes, calls, capabilities, graph, Git history | Deterministic local discovery |
|
|
127
|
+
| **Policy** | Rules, contracts, conventions, baseline | Observed proposals plus explicit team approval |
|
|
128
|
+
| **Verification** | Changed-file checks, impact analysis, contracts, project checks, review packet | Local CLI, MCP, CI, and existing project tools |
|
|
129
|
+
| **Decision** | Pass, fail, warning, needs-review, not-configured | Developer or AI host using Guard evidence |
|
|
130
|
+
|
|
131
|
+
Guard does not assume a fixed `UI → actions → services → repositories → database` architecture.
|
|
132
|
+
It can discover that shape when the project exhibits it, but observed patterns become enforceable
|
|
133
|
+
only after explicit approval.
|
|
134
|
+
|
|
135
|
+
## What Guard discovers
|
|
136
|
+
|
|
137
|
+
The model is framework-aware without being framework-dependent:
|
|
138
|
+
|
|
139
|
+
| Area | Examples of discovered evidence |
|
|
140
|
+
| ----------------------- | ---------------------------------------------------------------------------------- |
|
|
141
|
+
| JavaScript / TypeScript | AST modules, declarations, calls, directives, aliases, re-exports, dynamic imports |
|
|
142
|
+
| Next.js / React | App Router routes, route methods, Server Actions, client/server boundaries |
|
|
143
|
+
| API boundaries | Route handlers, API modules, entrypoints, impact paths |
|
|
144
|
+
| Data | ORM packages, schemas, migrations, repositories, persistence boundaries |
|
|
145
|
+
| Capabilities | Auth, payments, email, AI/RAG, queues, storage, cache, search, analytics, content |
|
|
146
|
+
| Operations | Environment references, configs, deployment, observability, webhooks |
|
|
147
|
+
| Repository shape | Workspaces, package scripts, dependencies, cycles, import hotspots, Git history |
|
|
148
|
+
| Testing | Test files, test scripts, workspace checks, changed files |
|
|
149
|
+
|
|
150
|
+
Capabilities are evidence, not requirements. A Vite app, Express service, Hono project, Node
|
|
151
|
+
package, monorepo, or Next.js SaaS can all use the same Guard core.
|
|
152
|
+
|
|
153
|
+
## The normal loop
|
|
154
|
+
|
|
155
|
+
```text
|
|
156
|
+
init once → edit → check --changed → review → verify → commit / merge
|
|
157
|
+
↘ analyze after structural changes
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
1. `init` builds project memory and establishes the initial baseline.
|
|
161
|
+
2. `check --changed` is the fast deterministic architecture gate.
|
|
162
|
+
3. `review` prepares a bounded and redacted diff packet for an AI host; it does not call an LLM.
|
|
163
|
+
4. `verify` runs Guard policy, configured project checks, adapters, runtime diagnostics, and
|
|
164
|
+
contract validation.
|
|
165
|
+
5. `audit` inspects the complete current state, including findings accepted by the baseline.
|
|
166
|
+
|
|
167
|
+
Guard state is stored in `.codapult/guard/`:
|
|
168
|
+
|
|
169
|
+
```text
|
|
170
|
+
.codapult/guard/
|
|
171
|
+
├── project.json discovered project model
|
|
172
|
+
├── architecture.json observed architecture and capabilities
|
|
173
|
+
├── conventions.json recurring project conventions
|
|
174
|
+
├── rules.json active and proposed deterministic rules
|
|
175
|
+
├── contracts.json project-specific boundaries and required calls
|
|
176
|
+
├── proposals.json evidence and approval history
|
|
177
|
+
├── baseline.json accepted pre-existing findings
|
|
178
|
+
├── agent.json AI-host completion-gate configuration
|
|
179
|
+
└── history/ project snapshots for comparison
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Commit policy and baseline files when the team wants shared guardrails. Treat cache artifacts as
|
|
183
|
+
disposable according to the project’s policy, and never commit secrets.
|
|
184
|
+
|
|
185
|
+
## AI agents and MCP
|
|
186
|
+
|
|
187
|
+
Guard is deliberately model-agnostic. It does not send source code to a remote LLM and does not
|
|
188
|
+
edit source files by itself. The AI host owns the model call, permissions, repair loop, and final
|
|
189
|
+
decision.
|
|
190
|
+
|
|
191
|
+
Start the standalone MCP server over stdio:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
pnpm exec codapult-guard mcp-server
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Recommended agent loop:
|
|
198
|
+
|
|
199
|
+
```text
|
|
200
|
+
task finished
|
|
201
|
+
→ codapult_guard_context
|
|
202
|
+
→ codapult_guard_review(requirement, diff)
|
|
203
|
+
→ codapult_guard_verify
|
|
204
|
+
→ repair reported failures
|
|
205
|
+
→ repeat until pass or bounded iteration limit
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
For Cursor, Claude Code, Codex, Gemini CLI, GitHub Copilot, and generic hosts, see
|
|
209
|
+
[`docs/integrations/`](docs/integrations/).
|
|
210
|
+
|
|
211
|
+
## CI
|
|
212
|
+
|
|
213
|
+
Copy the consumer workflow into a project that has installed and initialized Guard:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
cp node_modules/@codapult/guard/docs/guard-ci.yml .github/workflows/guard.yml
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Or copy it from [`docs/guard-ci.yml`](docs/guard-ci.yml). It runs project verification and exports
|
|
220
|
+
Guard findings as SARIF for GitHub code scanning.
|
|
221
|
+
|
|
222
|
+
The package repository separately runs its own unit, typecheck, build, and packaging checks in
|
|
223
|
+
[`.github/workflows/ci.yml`](.github/workflows/ci.yml), while the fixture workflow tests Guard on
|
|
224
|
+
real project shapes.
|
|
225
|
+
|
|
226
|
+
## CLI surface
|
|
227
|
+
|
|
228
|
+
| Command | Purpose |
|
|
229
|
+
| -------------------------- | --------------------------------------------------------------- |
|
|
230
|
+
| `init` | Create project memory and the initial baseline. |
|
|
231
|
+
| `analyze` | Refresh facts without changing policy or baseline. |
|
|
232
|
+
| `propose` | Generate evidence-based rule and contract proposals. |
|
|
233
|
+
| `check --changed` | Enforce active policy on changed and untracked files. |
|
|
234
|
+
| `audit` | Scan the complete current project, including baseline findings. |
|
|
235
|
+
| `review` | Create a bounded semantic-review packet for an AI host. |
|
|
236
|
+
| `verify` | Run the configured completion gate. |
|
|
237
|
+
| `doctor` | Diagnose invalid or missing Guard artifacts. |
|
|
238
|
+
| `history` / `history-diff` | Inspect project model evolution. |
|
|
239
|
+
| `rules` / `contracts` | Approve or reject proposed policy. |
|
|
240
|
+
| `baseline` | Review or intentionally accept existing findings. |
|
|
241
|
+
|
|
242
|
+
Run `pnpm exec codapult-guard <command> --help` for command-specific options.
|
|
243
|
+
|
|
244
|
+
## Security and data handling
|
|
245
|
+
|
|
246
|
+
- Deterministic discovery and checks run locally.
|
|
247
|
+
- Review packets are bounded and redact common secrets before they are returned to an AI host.
|
|
248
|
+
- Guard does not invoke an LLM or require a provider API key.
|
|
249
|
+
- Project commands and external adapters run only when enabled by the project configuration.
|
|
250
|
+
- Existing findings can be baselined, but new regressions remain visible.
|
|
251
|
+
|
|
252
|
+
## Verification and proof
|
|
253
|
+
|
|
254
|
+
The repository validates the product through multiple layers:
|
|
255
|
+
|
|
256
|
+
- unit and integration tests for discovery, policy, baseline, verification, MCP, and output;
|
|
257
|
+
- AST adversarial cases for aliases, re-exports, dynamic imports, and route boundaries;
|
|
258
|
+
- golden end-to-end flow from violation to repair;
|
|
259
|
+
- mutation regression checks across real Next.js, React/Vite, Node, Hono, monorepo, and Express
|
|
260
|
+
repositories;
|
|
261
|
+
- npm pack checks to ensure the published artifact contains only the intended build output.
|
|
262
|
+
|
|
263
|
+
Run the maintainer checks locally:
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
pnpm test
|
|
267
|
+
pnpm test:guard:golden
|
|
268
|
+
pnpm test:guard:fixtures
|
|
269
|
+
pnpm test:guard:fixtures:regression
|
|
270
|
+
pnpm release:check
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Documentation
|
|
274
|
+
|
|
275
|
+
- [Complete Guard guide](docs/guard.md)
|
|
276
|
+
- [AI-agent integration](docs/guard-agent-integration.md)
|
|
277
|
+
- [Host integrations](docs/integrations/)
|
|
278
|
+
- [CI consumer workflow](docs/guard-ci.yml)
|
|
279
|
+
- [Fixture matrix](docs/guard-fixtures.md)
|
|
280
|
+
- [Release process](docs/releasing.md)
|
|
281
|
+
|
|
282
|
+
## Project status
|
|
283
|
+
|
|
284
|
+
`@codapult/guard` starts at `0.1.0` as a public alpha. The deterministic core is usable, tested,
|
|
285
|
+
and intended for real projects, while policy schema and integration surfaces may still evolve
|
|
286
|
+
before `1.0.0`.
|
|
287
|
+
|
|
288
|
+
## License
|
|
289
|
+
|
|
290
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type GuardAgentTarget = 'generic' | 'codex' | 'cursor' | 'claude' | 'copilot' | 'gemini';
|
|
2
|
+
export interface GuardAgentInstallResult {
|
|
3
|
+
target: GuardAgentTarget;
|
|
4
|
+
path: string;
|
|
5
|
+
action: 'created' | 'updated';
|
|
6
|
+
}
|
|
7
|
+
export declare function installGuardAgentInstructions(root: string, target: GuardAgentTarget): GuardAgentInstallResult;
|
|
8
|
+
export declare function installGuardAgentTargets(root: string, targetsToInstall: GuardAgentTarget[]): GuardAgentInstallResult[];
|
|
9
|
+
export declare const guardAgentTargets: GuardAgentTarget[];
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
const START_MARKER = '<!-- codapult-guard:start -->';
|
|
4
|
+
const END_MARKER = '<!-- codapult-guard:end -->';
|
|
5
|
+
const instruction = `${START_MARKER}
|
|
6
|
+
## Codapult Guard completion gate
|
|
7
|
+
|
|
8
|
+
After completing a coding task, read the project Guard context, review the requirement and diff,
|
|
9
|
+
then run Guard verification. Use the host's Guard MCP tools when available:
|
|
10
|
+
|
|
11
|
+
1. Call \`codapult_guard_context\`.
|
|
12
|
+
2. Call \`codapult_guard_review\` with the requirement and changed diff.
|
|
13
|
+
3. Call \`codapult_guard_verify\`.
|
|
14
|
+
4. If an error is reported, fix it and repeat steps 2–3 up to \`completionGate.maxIterations\`.
|
|
15
|
+
5. Report warnings and unresolved requirements; never hide them or activate proposals silently.
|
|
16
|
+
|
|
17
|
+
Guard does not edit source files or invoke an LLM. The host agent owns the repair loop. CI remains
|
|
18
|
+
the independent final gate.
|
|
19
|
+
${END_MARKER}`;
|
|
20
|
+
const targets = {
|
|
21
|
+
generic: { path: 'AGENTS.md' },
|
|
22
|
+
codex: { path: 'AGENTS.md' },
|
|
23
|
+
cursor: {
|
|
24
|
+
path: '.cursor/rules/codapult-guard.mdc',
|
|
25
|
+
prefix: '---\ndescription: Codapult Guard completion workflow\nalwaysApply: false\n---\n\n# Codapult Guard\n\n',
|
|
26
|
+
},
|
|
27
|
+
claude: { path: 'CLAUDE.md' },
|
|
28
|
+
copilot: { path: '.github/copilot-instructions.md' },
|
|
29
|
+
gemini: { path: 'GEMINI.md' },
|
|
30
|
+
};
|
|
31
|
+
function renderContent(target) {
|
|
32
|
+
return `${targets[target].prefix ?? ''}${instruction}\n`;
|
|
33
|
+
}
|
|
34
|
+
export function installGuardAgentInstructions(root, target) {
|
|
35
|
+
const relativePath = targets[target].path;
|
|
36
|
+
const path = resolve(root, relativePath);
|
|
37
|
+
const nextBlock = instruction;
|
|
38
|
+
const existing = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
39
|
+
const start = existing.indexOf(START_MARKER);
|
|
40
|
+
const end = existing.indexOf(END_MARKER);
|
|
41
|
+
const content = start >= 0 && end >= start
|
|
42
|
+
? `${existing.slice(0, start)}${nextBlock}${existing.slice(end + END_MARKER.length)}`
|
|
43
|
+
: existing.length > 0
|
|
44
|
+
? `${existing.trimEnd()}\n\n${renderContent(target)}`
|
|
45
|
+
: renderContent(target);
|
|
46
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
47
|
+
writeFileSync(path, content, 'utf8');
|
|
48
|
+
return { target, path: relativePath, action: existing ? 'updated' : 'created' };
|
|
49
|
+
}
|
|
50
|
+
export function installGuardAgentTargets(root, targetsToInstall) {
|
|
51
|
+
const seenPaths = new Set();
|
|
52
|
+
return targetsToInstall
|
|
53
|
+
.filter((target) => {
|
|
54
|
+
const path = targets[target].path;
|
|
55
|
+
if (seenPaths.has(path))
|
|
56
|
+
return false;
|
|
57
|
+
seenPaths.add(path);
|
|
58
|
+
return true;
|
|
59
|
+
})
|
|
60
|
+
.map((target) => installGuardAgentInstructions(root, target));
|
|
61
|
+
}
|
|
62
|
+
export const guardAgentTargets = Object.keys(targets);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface CommandResult {
|
|
2
|
+
command: string;
|
|
3
|
+
status: 'passed' | 'failed' | 'not-configured';
|
|
4
|
+
passed: boolean;
|
|
5
|
+
exitCode: number;
|
|
6
|
+
stdout: string;
|
|
7
|
+
stderr: string;
|
|
8
|
+
timedOut?: boolean;
|
|
9
|
+
truncated?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function runProjectCommand(command: string, cwd: string, options?: {
|
|
12
|
+
timeout?: number;
|
|
13
|
+
env?: NodeJS.ProcessEnv;
|
|
14
|
+
}): CommandResult;
|
|
15
|
+
export declare function commandResponse(result: CommandResult): {
|
|
16
|
+
content: {
|
|
17
|
+
type: 'text';
|
|
18
|
+
text: string;
|
|
19
|
+
}[];
|
|
20
|
+
isError: boolean;
|
|
21
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
const MAX_OUTPUT_CHARS = 20_000;
|
|
3
|
+
const MAX_BUFFER_BYTES = 2_000_000;
|
|
4
|
+
function redactOutput(value) {
|
|
5
|
+
return value
|
|
6
|
+
.replace(/-----BEGIN [A-Z ]+-----[\s\S]*?-----END [A-Z ]+-----/g, '[REDACTED PRIVATE KEY]')
|
|
7
|
+
.replace(/\b(?:sk_(?:live|test)_|pk_(?:live|test)_|AKIA|gh[pousr]_|github_pat_)[A-Za-z0-9_-]+/g, '[REDACTED TOKEN]')
|
|
8
|
+
.replace(/((?:api[_-]?key|secret|token|password|authorization|database[_-]?url)\s*[:=]\s*["']?)[^\s"'`,}]+/gi, '$1[REDACTED]');
|
|
9
|
+
}
|
|
10
|
+
function captureOutput(value) {
|
|
11
|
+
const redacted = redactOutput(value);
|
|
12
|
+
return redacted.length > MAX_OUTPUT_CHARS
|
|
13
|
+
? { value: `${redacted.slice(0, MAX_OUTPUT_CHARS)}\n[output truncated]`, truncated: true }
|
|
14
|
+
: { value: redacted, truncated: false };
|
|
15
|
+
}
|
|
16
|
+
export function runProjectCommand(command, cwd, options = {}) {
|
|
17
|
+
try {
|
|
18
|
+
const stdout = execSync(command, {
|
|
19
|
+
cwd,
|
|
20
|
+
env: { ...process.env, ...options.env },
|
|
21
|
+
stdio: 'pipe',
|
|
22
|
+
timeout: options.timeout ?? 120_000,
|
|
23
|
+
maxBuffer: MAX_BUFFER_BYTES,
|
|
24
|
+
}).toString();
|
|
25
|
+
const captured = captureOutput(stdout);
|
|
26
|
+
return {
|
|
27
|
+
command,
|
|
28
|
+
status: 'passed',
|
|
29
|
+
passed: true,
|
|
30
|
+
exitCode: 0,
|
|
31
|
+
stdout: captured.value,
|
|
32
|
+
stderr: '',
|
|
33
|
+
truncated: captured.truncated,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
const execError = error;
|
|
38
|
+
const exitCode = execError.status ?? 1;
|
|
39
|
+
const passed = exitCode === 0;
|
|
40
|
+
const stdout = captureOutput(execError.stdout?.toString() ?? '');
|
|
41
|
+
const stderr = captureOutput(execError.stderr?.toString() ?? '');
|
|
42
|
+
const timedOut = execError.code === 'ETIMEDOUT' || execError.signal === 'SIGTERM';
|
|
43
|
+
return {
|
|
44
|
+
command,
|
|
45
|
+
status: passed ? 'passed' : 'failed',
|
|
46
|
+
passed,
|
|
47
|
+
exitCode,
|
|
48
|
+
stdout: stdout.value,
|
|
49
|
+
stderr: timedOut ? `${stderr.value}\n[command timed out]` : stderr.value,
|
|
50
|
+
timedOut,
|
|
51
|
+
truncated: stdout.truncated || stderr.truncated,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function commandResponse(result) {
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
58
|
+
isError: !result.passed,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type CommandResult } from './command.js';
|
|
2
|
+
export type ProjectCheck = 'lint' | 'typecheck' | 'test' | 'build';
|
|
3
|
+
export type GuardAdapter = 'dependency-graph' | 'security' | 'dependency-hygiene';
|
|
4
|
+
export type ExternalToolMode = 'auto' | 'on' | 'off';
|
|
5
|
+
export type ProjectCheckResults = Partial<Record<ProjectCheck, CommandResult>>;
|
|
6
|
+
export interface ProjectRuntimeDiagnostics {
|
|
7
|
+
node: string;
|
|
8
|
+
packageManager: string;
|
|
9
|
+
declaredPackageManager?: string;
|
|
10
|
+
declaredNode?: string;
|
|
11
|
+
compatible: boolean;
|
|
12
|
+
issues: string[];
|
|
13
|
+
}
|
|
14
|
+
export declare function inspectProjectRuntime(root: string): ProjectRuntimeDiagnostics;
|
|
15
|
+
export declare function runProjectChecks(root: string, checks: ProjectCheck[], options?: {
|
|
16
|
+
timeout?: number;
|
|
17
|
+
}): ProjectCheckResults;
|
|
18
|
+
/**
|
|
19
|
+
* Runs checks for workspace packages only when the root package does not own
|
|
20
|
+
* the corresponding check. Root orchestration remains the source of truth.
|
|
21
|
+
*/
|
|
22
|
+
export declare function runWorkspaceProjectChecks(root: string, packages: {
|
|
23
|
+
path: string;
|
|
24
|
+
scripts: Record<string, string>;
|
|
25
|
+
}[], checks: ProjectCheck[], options?: {
|
|
26
|
+
timeout?: number;
|
|
27
|
+
rootResults?: ProjectCheckResults;
|
|
28
|
+
}): Record<string, ProjectCheckResults>;
|
|
29
|
+
/** Runs only explicitly configured project scripts; Guard does not recreate these tools. */
|
|
30
|
+
export declare function runProjectAdapters(root: string, options?: {
|
|
31
|
+
mode?: Exclude<ExternalToolMode, 'off'>;
|
|
32
|
+
timeout?: number;
|
|
33
|
+
tooling?: Partial<Record<GuardAdapter, {
|
|
34
|
+
script: string;
|
|
35
|
+
enabled: boolean;
|
|
36
|
+
}>>;
|
|
37
|
+
}): Partial<Record<GuardAdapter, CommandResult>>;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { runProjectCommand } from './command.js';
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
const { satisfies } = require('semver');
|
|
7
|
+
function notConfigured(check) {
|
|
8
|
+
return {
|
|
9
|
+
command: '',
|
|
10
|
+
status: 'not-configured',
|
|
11
|
+
passed: false,
|
|
12
|
+
exitCode: 0,
|
|
13
|
+
stdout: '',
|
|
14
|
+
stderr: `${check} script is not configured`,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function readPackageMetadata(root) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8'));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function packageManager(root, declared) {
|
|
26
|
+
const name = declared?.split('@', 1)[0];
|
|
27
|
+
if (name === 'pnpm' || name === 'yarn' || name === 'npm' || name === 'bun')
|
|
28
|
+
return name;
|
|
29
|
+
for (const [lockfile, manager] of [
|
|
30
|
+
['pnpm-lock.yaml', 'pnpm'],
|
|
31
|
+
['yarn.lock', 'yarn'],
|
|
32
|
+
['package-lock.json', 'npm'],
|
|
33
|
+
['bun.lockb', 'bun'],
|
|
34
|
+
['bun.lock', 'bun'],
|
|
35
|
+
]) {
|
|
36
|
+
if (existsSync(resolve(root, lockfile)))
|
|
37
|
+
return manager;
|
|
38
|
+
}
|
|
39
|
+
return 'pnpm';
|
|
40
|
+
}
|
|
41
|
+
export function inspectProjectRuntime(root) {
|
|
42
|
+
const metadata = readPackageMetadata(root);
|
|
43
|
+
const manager = packageManager(root, metadata.packageManager);
|
|
44
|
+
const issues = [];
|
|
45
|
+
const declaredNode = metadata.engines?.node;
|
|
46
|
+
let nodeCompatible = true;
|
|
47
|
+
if (declaredNode) {
|
|
48
|
+
try {
|
|
49
|
+
nodeCompatible = satisfies(process.versions.node, declaredNode);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
nodeCompatible = false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (declaredNode && !nodeCompatible) {
|
|
56
|
+
issues.push(`Node ${process.versions.node} does not satisfy engines.node "${declaredNode}".`);
|
|
57
|
+
}
|
|
58
|
+
const declaredManager = metadata.packageManager?.split('@', 1)[0];
|
|
59
|
+
if (declaredManager && declaredManager !== manager) {
|
|
60
|
+
issues.push(`Detected package manager ${manager}, but packageManager declares ${declaredManager}.`);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
node: process.versions.node,
|
|
64
|
+
packageManager: manager,
|
|
65
|
+
...(metadata.packageManager ? { declaredPackageManager: metadata.packageManager } : {}),
|
|
66
|
+
...(declaredNode ? { declaredNode } : {}),
|
|
67
|
+
compatible: issues.length === 0,
|
|
68
|
+
issues,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function commandFor(root, manager, check, scripts) {
|
|
72
|
+
const run = (script, suffix = '') => manager === 'npm' ? `npm run ${script}${suffix}` : `${manager} run ${script}${suffix}`;
|
|
73
|
+
if (check === 'typecheck' && !scripts.typecheck && !scripts['type-check']) {
|
|
74
|
+
if (!existsSync(resolve(root, 'tsconfig.json')))
|
|
75
|
+
return undefined;
|
|
76
|
+
return manager === 'npm' ? 'npx --no-install tsc --noEmit' : `${manager} exec tsc --noEmit`;
|
|
77
|
+
}
|
|
78
|
+
if (check === 'typecheck')
|
|
79
|
+
return run(scripts.typecheck ? 'typecheck' : 'type-check');
|
|
80
|
+
if (!scripts[check])
|
|
81
|
+
return undefined;
|
|
82
|
+
return run(check);
|
|
83
|
+
}
|
|
84
|
+
export function runProjectChecks(root, checks, options = {}) {
|
|
85
|
+
const metadata = readPackageMetadata(root);
|
|
86
|
+
const manager = packageManager(root, metadata.packageManager);
|
|
87
|
+
const scripts = metadata.scripts ?? {};
|
|
88
|
+
const results = {};
|
|
89
|
+
for (const check of checks) {
|
|
90
|
+
const command = commandFor(root, manager, check, scripts);
|
|
91
|
+
results[check] = command
|
|
92
|
+
? runProjectCommand(command, root, {
|
|
93
|
+
timeout: options.timeout ?? (check === 'build' ? 300_000 : 120_000),
|
|
94
|
+
})
|
|
95
|
+
: notConfigured(check);
|
|
96
|
+
}
|
|
97
|
+
return results;
|
|
98
|
+
}
|
|
99
|
+
function hasCheck(scripts, check) {
|
|
100
|
+
return check === 'typecheck'
|
|
101
|
+
? Boolean(scripts.typecheck || scripts['type-check'])
|
|
102
|
+
: Boolean(scripts[check]);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Runs checks for workspace packages only when the root package does not own
|
|
106
|
+
* the corresponding check. Root orchestration remains the source of truth.
|
|
107
|
+
*/
|
|
108
|
+
export function runWorkspaceProjectChecks(root, packages, checks, options = {}) {
|
|
109
|
+
const rootResults = options.rootResults ?? runProjectChecks(root, checks, options);
|
|
110
|
+
const missingAtRoot = new Set(checks.filter((check) => rootResults[check]?.status === 'not-configured'));
|
|
111
|
+
return Object.fromEntries(packages
|
|
112
|
+
.filter((workspace) => checks.some((check) => missingAtRoot.has(check) && hasCheck(workspace.scripts, check)))
|
|
113
|
+
.map((workspace) => [
|
|
114
|
+
workspace.path,
|
|
115
|
+
runProjectChecks(resolve(root, workspace.path), [...missingAtRoot], options),
|
|
116
|
+
]));
|
|
117
|
+
}
|
|
118
|
+
const adapterScriptPatterns = {
|
|
119
|
+
'dependency-graph': /(?:dep(?:endency)?[-:]?(?:check|graph|cruise)|madge|architecture)/i,
|
|
120
|
+
security: /(?:security|semgrep|gitleaks|snyk|trivy|audit)/i,
|
|
121
|
+
'dependency-hygiene': /(?:knip|dep(?:endency)?[-:]?(?:unused|hygiene))/i,
|
|
122
|
+
};
|
|
123
|
+
/** Runs only explicitly configured project scripts; Guard does not recreate these tools. */
|
|
124
|
+
export function runProjectAdapters(root, options = {}) {
|
|
125
|
+
const metadata = readPackageMetadata(root);
|
|
126
|
+
const manager = packageManager(root, metadata.packageManager);
|
|
127
|
+
const scripts = metadata.scripts ?? {};
|
|
128
|
+
const results = {};
|
|
129
|
+
for (const [adapter, pattern] of Object.entries(adapterScriptPatterns)) {
|
|
130
|
+
const configured = options.tooling?.[adapter];
|
|
131
|
+
const script = configured?.enabled
|
|
132
|
+
? configured.script
|
|
133
|
+
: configured?.enabled === false
|
|
134
|
+
? undefined
|
|
135
|
+
: Object.keys(scripts).find((name) => pattern.test(name));
|
|
136
|
+
if (!script && options.mode !== 'on') {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (!script) {
|
|
140
|
+
results[adapter] = notConfigured(adapter);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const command = manager === 'npm' ? `npm run ${script}` : `${manager} run ${script}`;
|
|
144
|
+
results[adapter] = runProjectCommand(command, root, {
|
|
145
|
+
timeout: options.timeout ?? 120_000,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return results;
|
|
149
|
+
}
|