forgedev 1.2.0 → 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/README.md +57 -10
- package/bin/chainproof.js +126 -0
- package/bin/devforge.js +1 -1
- package/package.json +25 -7
- package/src/chainproof-bridge.js +330 -0
- package/src/ci-mode.js +85 -0
- package/src/claude-configurator.js +171 -78
- package/src/cli.js +30 -7
- package/src/composer.js +242 -214
- package/src/doctor-checks-chainproof.js +106 -0
- package/src/doctor-checks.js +39 -20
- package/src/doctor-prompts.js +9 -9
- package/src/doctor.js +37 -4
- package/src/guided.js +3 -3
- package/src/index.js +31 -10
- package/src/init-mode.js +76 -12
- package/src/menu.js +178 -0
- package/src/prompts.js +5 -12
- package/src/recommender.js +163 -30
- package/src/scanner.js +57 -2
- package/src/uat-generator.js +204 -189
- package/src/update-check.js +9 -4
- package/src/update.js +57 -13
- package/src/utils.js +162 -5
- package/templates/ai/guardrails-py/backend/app/ai/__init__.py +29 -0
- package/templates/ai/guardrails-py/backend/app/ai/audit_log.py +133 -0
- package/templates/ai/guardrails-py/backend/app/ai/client.py.template +323 -0
- package/templates/ai/guardrails-py/backend/app/ai/health.py.template +157 -0
- package/templates/ai/guardrails-py/backend/app/ai/input_guard.py +98 -0
- package/templates/ai/guardrails-ts/src/lib/ai/audit-log.ts.template +164 -0
- package/templates/ai/guardrails-ts/src/lib/ai/client.ts.template +403 -0
- package/templates/ai/guardrails-ts/src/lib/ai/health.ts.template +165 -0
- package/templates/ai/guardrails-ts/src/lib/ai/index.ts.template +17 -0
- package/templates/ai/guardrails-ts/src/lib/ai/input-guard.ts.template +124 -0
- package/templates/auth/nextauth/src/lib/auth.ts.template +12 -7
- package/templates/backend/express/Dockerfile.template +18 -0
- package/templates/backend/express/package.json.template +33 -0
- package/templates/backend/express/src/index.ts.template +34 -0
- package/templates/backend/express/src/routes/health.ts.template +27 -0
- package/templates/backend/express/tsconfig.json +17 -0
- package/templates/backend/fastapi/backend/Dockerfile.template +5 -0
- package/templates/backend/fastapi/backend/app/api/health.py.template +1 -1
- package/templates/backend/fastapi/backend/app/core/config.py.template +1 -1
- package/templates/backend/fastapi/backend/app/core/errors.py +1 -1
- package/templates/backend/fastapi/backend/app/main.py.template +3 -1
- package/templates/backend/fastapi/backend/requirements.txt.template +2 -0
- package/templates/backend/hono/Dockerfile.template +18 -0
- package/templates/backend/hono/package.json.template +31 -0
- package/templates/backend/hono/src/index.ts.template +32 -0
- package/templates/backend/hono/src/routes/health.ts.template +27 -0
- package/templates/backend/hono/tsconfig.json +18 -0
- package/templates/base/.gitignore.template +3 -0
- package/templates/base/docs/uat/UAT_TEMPLATE.md.template +1 -1
- package/templates/chainproof/base/.chainproof/config.json.template +11 -0
- package/templates/chainproof/base/.chainproof/mcp-server.mjs +310 -0
- package/templates/chainproof/base/.mcp.json +9 -0
- package/templates/chainproof/fastapi/.chainproof/middleware.json.template +14 -0
- package/templates/chainproof/nextjs/.chainproof/hooks.json.template +19 -0
- package/templates/chainproof/polyglot/.chainproof/config.json.template +21 -0
- package/templates/claude-code/agents/architect.md +25 -11
- package/templates/claude-code/agents/build-error-resolver.md +19 -5
- package/templates/claude-code/agents/chief-of-staff.md +42 -8
- package/templates/claude-code/agents/code-quality-reviewer.md +14 -0
- package/templates/claude-code/agents/database-reviewer.md +15 -1
- package/templates/claude-code/agents/deep-reviewer.md +191 -0
- package/templates/claude-code/agents/doc-updater.md +19 -5
- package/templates/claude-code/agents/docs-lookup.md +19 -5
- package/templates/claude-code/agents/e2e-runner.md +26 -12
- package/templates/claude-code/agents/enforcement-gate.md +102 -0
- package/templates/claude-code/agents/frontend-builder.md +188 -0
- package/templates/claude-code/agents/harness-optimizer.md +36 -1
- package/templates/claude-code/agents/loop-operator.md +27 -13
- package/templates/claude-code/agents/planner.md +21 -7
- package/templates/claude-code/agents/product-strategist.md +24 -10
- package/templates/claude-code/agents/production-readiness.md +14 -0
- package/templates/claude-code/agents/prompt-auditor.md +115 -0
- package/templates/claude-code/agents/refactor-cleaner.md +22 -8
- package/templates/claude-code/agents/security-reviewer.md +14 -0
- package/templates/claude-code/agents/spec-validator.md +15 -1
- package/templates/claude-code/agents/tdd-guide.md +21 -7
- package/templates/claude-code/agents/uat-validator.md +14 -0
- package/templates/claude-code/claude-md/base.md +14 -7
- package/templates/claude-code/claude-md/fastapi.md +8 -8
- package/templates/claude-code/claude-md/fullstack.md +6 -6
- package/templates/claude-code/claude-md/hono.md +18 -0
- package/templates/claude-code/claude-md/nextjs.md +5 -5
- package/templates/claude-code/claude-md/remix.md +18 -0
- package/templates/claude-code/commands/audit-security.md +14 -0
- package/templates/claude-code/commands/audit-spec.md +14 -0
- package/templates/claude-code/commands/audit-wiring.md +14 -0
- package/templates/claude-code/commands/build-fix.md +28 -0
- package/templates/claude-code/commands/build-ui.md +59 -0
- package/templates/claude-code/commands/code-review.md +53 -31
- package/templates/claude-code/commands/fix-loop.md +211 -0
- package/templates/claude-code/commands/full-audit.md +36 -8
- package/templates/claude-code/commands/generate-prd.md +1 -1
- package/templates/claude-code/commands/generate-sdd.md +74 -0
- package/templates/claude-code/commands/generate-uat.md +107 -35
- package/templates/claude-code/commands/help.md +68 -0
- package/templates/claude-code/commands/live-uat.md +268 -0
- package/templates/claude-code/commands/optimize-claude-md.md +15 -1
- package/templates/claude-code/commands/plan.md +3 -3
- package/templates/claude-code/commands/pre-pr.md +57 -19
- package/templates/claude-code/commands/product-strategist.md +21 -0
- package/templates/claude-code/commands/resume-session.md +10 -10
- package/templates/claude-code/commands/run-uat.md +59 -2
- package/templates/claude-code/commands/save-session.md +10 -10
- package/templates/claude-code/commands/simplify.md +36 -0
- package/templates/claude-code/commands/tdd.md +17 -18
- package/templates/claude-code/commands/verify-all.md +24 -0
- package/templates/claude-code/commands/verify-intent.md +55 -0
- package/templates/claude-code/commands/workflows.md +52 -40
- package/templates/claude-code/hooks/polyglot.json +10 -1
- package/templates/claude-code/hooks/python.json +10 -1
- package/templates/claude-code/hooks/scripts/autofix-polyglot.mjs +2 -2
- package/templates/claude-code/hooks/scripts/autofix-python.mjs +1 -1
- package/templates/claude-code/hooks/scripts/autofix-typescript.mjs +1 -1
- package/templates/claude-code/hooks/scripts/code-hygiene.mjs +293 -0
- package/templates/claude-code/hooks/scripts/pre-commit-gate.mjs +207 -0
- package/templates/claude-code/hooks/typescript.json +10 -1
- package/templates/claude-code/skills/ai-prompts/SKILL.md +119 -41
- package/templates/claude-code/skills/git-workflow/SKILL.md +5 -5
- package/templates/claude-code/skills/nextjs/SKILL.md +1 -1
- package/templates/claude-code/skills/playwright/SKILL.md +5 -5
- package/templates/claude-code/skills/security-api/SKILL.md +1 -1
- package/templates/claude-code/skills/security-web/SKILL.md +1 -1
- package/templates/claude-code/skills/testing-patterns/SKILL.md +9 -9
- package/templates/database/prisma-postgres/{.env.example → .env.example.template} +1 -0
- package/templates/database/sqlalchemy-postgres/{.env.example → .env.example.template} +1 -0
- package/templates/docs-portal/fastapi/backend/app/portal/__pycache__/docs_reader.cpython-314.pyc +0 -0
- package/templates/docs-portal/fastapi/backend/app/portal/docs_reader.py +201 -0
- package/templates/docs-portal/fastapi/backend/app/portal/html_renderer.py +229 -0
- package/templates/docs-portal/fastapi/backend/app/portal/router.py.template +35 -0
- package/templates/docs-portal/nextjs/src/app/portal/[category]/[slug]/page.tsx +81 -0
- package/templates/docs-portal/nextjs/src/app/portal/[category]/page.tsx +65 -0
- package/templates/docs-portal/nextjs/src/app/portal/layout.tsx.template +54 -0
- package/templates/docs-portal/nextjs/src/app/portal/page.tsx +85 -0
- package/templates/docs-portal/nextjs/src/components/portal/markdown-renderer.tsx +101 -0
- package/templates/docs-portal/nextjs/src/components/portal/mobile-portal-nav.tsx +81 -0
- package/templates/docs-portal/nextjs/src/components/portal/portal-nav.tsx +86 -0
- package/templates/docs-portal/nextjs/src/lib/docs.ts +139 -0
- package/templates/frontend/nextjs/package.json.template +3 -1
- package/templates/frontend/react/index.html.template +12 -0
- package/templates/frontend/react/package.json.template +34 -0
- package/templates/frontend/react/src/App.tsx.template +10 -0
- package/templates/frontend/react/src/index.css +1 -0
- package/templates/frontend/react/src/main.tsx +10 -0
- package/templates/frontend/react/tsconfig.json +17 -0
- package/templates/frontend/react/vite.config.ts.template +15 -0
- package/templates/frontend/react/vitest.config.ts +9 -0
- package/templates/frontend/remix/app/root.tsx.template +31 -0
- package/templates/frontend/remix/app/routes/_index.tsx.template +19 -0
- package/templates/frontend/remix/app/routes/api.health.ts.template +10 -0
- package/templates/frontend/remix/app/tailwind.css +1 -0
- package/templates/frontend/remix/package.json.template +39 -0
- package/templates/frontend/remix/tsconfig.json +18 -0
- package/templates/frontend/remix/vite.config.ts.template +7 -0
- package/templates/infra/github-actions/.github/workflows/ci.yml.template +3 -0
- package/templates/infra/k8s/k8s/deployment.yml.template +70 -0
- package/templates/infra/k8s/k8s/hpa.yml.template +24 -0
- package/templates/infra/k8s/k8s/ingress.yml.template +26 -0
- package/templates/infra/k8s/k8s/kustomization.yml.template +13 -0
- package/templates/infra/k8s/k8s/namespace.yml.template +4 -0
- package/templates/infra/k8s/k8s/networkpolicy.yml.template +41 -0
- package/templates/infra/k8s/k8s/secrets.yml.template +10 -0
- package/templates/infra/k8s/k8s/service.yml.template +15 -0
- package/templates/testing/load/k6/README.md.template +48 -0
- package/templates/testing/load/k6/load-test.js.template +57 -0
- package/docs/00-README.md +0 -310
- package/docs/01-universal-prompt-library.md +0 -1049
- package/docs/02-claude-code-mastery-playbook.md +0 -283
- package/docs/03-multi-agent-verification.md +0 -565
- package/docs/04-errata-and-verification-checklist.md +0 -284
- package/docs/05-universal-scaffolder-vision.md +0 -452
- package/docs/06-confidence-assessment-and-repo-prompt.md +0 -407
- package/docs/errata.md +0 -58
- package/docs/multi-agent-verification.md +0 -66
- package/docs/playbook.md +0 -95
- package/docs/prompt-library.md +0 -160
- package/docs/uat/UAT_CHECKLIST.csv +0 -9
- package/docs/uat/UAT_TEMPLATE.md +0 -163
- package/templates/claude-code/commands/done.md +0 -19
- /package/{docs/plans/.gitkeep → templates/docs-portal/fastapi/backend/app/portal/__init__.py} +0 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChainProof bridge - pure Node.js trust chain operations.
|
|
3
|
+
* Uses node:crypto for Ed25519 + SHA-256, no Python dependency needed.
|
|
4
|
+
* All operations are file-based, reading and writing to .chainproof/.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { createHash, generateKeyPairSync, sign, verify, randomUUID } from 'node:crypto';
|
|
10
|
+
|
|
11
|
+
// --- Crypto primitives (interoperable with chainproof/core/crypto.py) ---
|
|
12
|
+
|
|
13
|
+
export function hashContent(content) {
|
|
14
|
+
return createHash('sha256').update(content, 'utf-8').digest('hex');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildSignablePayload(entry) {
|
|
18
|
+
// Length-prefixed using UTF-8 byte length for cross-language interop.
|
|
19
|
+
// JS .length and Python len() diverge on emoji; Buffer.byteLength is stable.
|
|
20
|
+
const fields = [
|
|
21
|
+
entry.prevHash,
|
|
22
|
+
entry.contentHash,
|
|
23
|
+
entry.timestamp,
|
|
24
|
+
entry.entryType,
|
|
25
|
+
entry.sessionId,
|
|
26
|
+
];
|
|
27
|
+
return fields.map((f) => {
|
|
28
|
+
const s = String(f ?? '');
|
|
29
|
+
return `${Buffer.byteLength(s, 'utf-8')}:${s}`;
|
|
30
|
+
}).join(':');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function signEntry(payload, privateKeyPem) {
|
|
34
|
+
const signature = sign(null, Buffer.from(payload, 'utf-8'), privateKeyPem);
|
|
35
|
+
return signature.toString('base64');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function verifySignature(payload, signatureB64, publicKeyPem) {
|
|
39
|
+
try {
|
|
40
|
+
return verify(
|
|
41
|
+
null,
|
|
42
|
+
Buffer.from(payload, 'utf-8'),
|
|
43
|
+
publicKeyPem,
|
|
44
|
+
Buffer.from(signatureB64, 'base64')
|
|
45
|
+
);
|
|
46
|
+
} catch {
|
|
47
|
+
// Invalid key or malformed signature
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function computeChainHash(prevHash, contentHash) {
|
|
53
|
+
return createHash('sha256')
|
|
54
|
+
.update(prevHash + contentHash, 'utf-8')
|
|
55
|
+
.digest('hex');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function generateKeypair() {
|
|
59
|
+
const { publicKey, privateKey } = generateKeyPairSync('ed25519', {
|
|
60
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
61
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
62
|
+
});
|
|
63
|
+
return { publicKey, privateKey };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// --- High-level chain operations ---
|
|
67
|
+
|
|
68
|
+
const GENESIS_HASH = '0'.repeat(64);
|
|
69
|
+
|
|
70
|
+
export function initChainproof(projectDir) {
|
|
71
|
+
const cpDir = path.join(projectDir, '.chainproof');
|
|
72
|
+
const keysDir = path.join(cpDir, 'keys');
|
|
73
|
+
|
|
74
|
+
fs.mkdirSync(cpDir, { recursive: true });
|
|
75
|
+
fs.mkdirSync(keysDir, { recursive: true });
|
|
76
|
+
|
|
77
|
+
// Config — merge with any template-composed config (from templates/chainproof/)
|
|
78
|
+
const configPath = path.join(cpDir, 'config.json');
|
|
79
|
+
const projectName = path.basename(projectDir);
|
|
80
|
+
const defaults = {
|
|
81
|
+
version: '1.0.0',
|
|
82
|
+
projectName,
|
|
83
|
+
hashAlgorithm: 'sha256',
|
|
84
|
+
signatureAlgorithm: 'ed25519',
|
|
85
|
+
createdAt: new Date().toISOString(),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (fs.existsSync(configPath)) {
|
|
89
|
+
// Template already composed a config — merge our defaults under it
|
|
90
|
+
try {
|
|
91
|
+
const existing = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
92
|
+
const merged = { ...defaults, ...existing, createdAt: existing.createdAt || defaults.createdAt };
|
|
93
|
+
fs.writeFileSync(configPath, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
|
|
94
|
+
} catch {
|
|
95
|
+
// Corrupted config, overwrite with defaults
|
|
96
|
+
fs.writeFileSync(configPath, JSON.stringify(defaults, null, 2) + '\n', 'utf-8');
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
fs.writeFileSync(configPath, JSON.stringify(defaults, null, 2) + '\n', 'utf-8');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Chain — only create if missing
|
|
103
|
+
const chainPath = path.join(cpDir, 'chain.json');
|
|
104
|
+
if (!fs.existsSync(chainPath)) {
|
|
105
|
+
fs.writeFileSync(
|
|
106
|
+
chainPath,
|
|
107
|
+
JSON.stringify({ entries: [], currentHash: GENESIS_HASH }, null, 2) + '\n',
|
|
108
|
+
'utf-8'
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Artifacts — only create if missing
|
|
113
|
+
const artifactsPath = path.join(cpDir, 'artifacts.json');
|
|
114
|
+
if (!fs.existsSync(artifactsPath)) {
|
|
115
|
+
fs.writeFileSync(
|
|
116
|
+
artifactsPath,
|
|
117
|
+
JSON.stringify({ artifacts: [] }, null, 2) + '\n',
|
|
118
|
+
'utf-8'
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Generate keypair only if missing
|
|
123
|
+
const privateKeyPath = path.join(keysDir, 'private.pem');
|
|
124
|
+
const publicKeyPath = path.join(keysDir, 'public.pem');
|
|
125
|
+
if (!fs.existsSync(privateKeyPath) || !fs.existsSync(publicKeyPath)) {
|
|
126
|
+
const { publicKey, privateKey } = generateKeypair();
|
|
127
|
+
fs.writeFileSync(privateKeyPath, privateKey, { encoding: 'utf-8', mode: 0o600 });
|
|
128
|
+
fs.writeFileSync(publicKeyPath, publicKey, 'utf-8');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Gitignore for keys
|
|
132
|
+
const gitignorePath = path.join(cpDir, '.gitignore');
|
|
133
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
134
|
+
fs.writeFileSync(gitignorePath, 'keys/\n', 'utf-8');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function recordDecision(projectDir, entry) {
|
|
139
|
+
if (!entry || typeof entry.content !== 'string') {
|
|
140
|
+
throw new Error('entry.content must be a non-empty string');
|
|
141
|
+
}
|
|
142
|
+
if (entry.content.length > 1_048_576) {
|
|
143
|
+
throw new Error('entry.content exceeds 1MB limit');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const cpDir = path.join(projectDir, '.chainproof');
|
|
147
|
+
const chainPath = path.join(cpDir, 'chain.json');
|
|
148
|
+
|
|
149
|
+
let chain;
|
|
150
|
+
try {
|
|
151
|
+
chain = JSON.parse(fs.readFileSync(chainPath, 'utf-8'));
|
|
152
|
+
} catch (err) {
|
|
153
|
+
throw new Error(`Failed to read chain.json: ${err.message}`, { cause: err });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const contentHash = hashContent(entry.content);
|
|
157
|
+
const prevHash = chain.currentHash;
|
|
158
|
+
const chainHash = computeChainHash(prevHash, contentHash);
|
|
159
|
+
const timestamp = new Date().toISOString();
|
|
160
|
+
const entryType = entry.entryType || 'decision';
|
|
161
|
+
const sessionId = entry.sessionId || 'default';
|
|
162
|
+
|
|
163
|
+
// Build the full entry before signing so all metadata is covered
|
|
164
|
+
const nllEntry = {
|
|
165
|
+
id: randomUUID(),
|
|
166
|
+
timestamp,
|
|
167
|
+
entryType,
|
|
168
|
+
content: entry.content,
|
|
169
|
+
contentHash,
|
|
170
|
+
prevHash,
|
|
171
|
+
chainHash,
|
|
172
|
+
signature: null,
|
|
173
|
+
sessionId,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Sign the full canonical payload (not just content)
|
|
177
|
+
const privateKeyPath = path.join(cpDir, 'keys', 'private.pem');
|
|
178
|
+
if (fs.existsSync(privateKeyPath)) {
|
|
179
|
+
const privateKey = fs.readFileSync(privateKeyPath, 'utf-8');
|
|
180
|
+
const payload = buildSignablePayload(nllEntry);
|
|
181
|
+
nllEntry.signature = signEntry(payload, privateKey);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
chain.entries.push(nllEntry);
|
|
185
|
+
chain.currentHash = chainHash;
|
|
186
|
+
|
|
187
|
+
// Atomic write: write to temp file first, then rename
|
|
188
|
+
const tmpPath = chainPath + '.tmp';
|
|
189
|
+
fs.writeFileSync(tmpPath, JSON.stringify(chain, null, 2) + '\n', 'utf-8');
|
|
190
|
+
fs.renameSync(tmpPath, chainPath);
|
|
191
|
+
|
|
192
|
+
return nllEntry;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function recordCodeArtifact(projectDir, artifact) {
|
|
196
|
+
const cpDir = path.join(projectDir, '.chainproof');
|
|
197
|
+
const artifactsPath = path.join(cpDir, 'artifacts.json');
|
|
198
|
+
|
|
199
|
+
let data;
|
|
200
|
+
try {
|
|
201
|
+
data = JSON.parse(fs.readFileSync(artifactsPath, 'utf-8'));
|
|
202
|
+
} catch (err) {
|
|
203
|
+
throw new Error(`Failed to read artifacts.json: ${err.message}`, { cause: err });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const record = {
|
|
207
|
+
id: randomUUID(),
|
|
208
|
+
timestamp: new Date().toISOString(),
|
|
209
|
+
filePath: artifact.filePath,
|
|
210
|
+
contentHash: artifact.contentHash || hashContent(artifact.content || ''),
|
|
211
|
+
language: artifact.language || null,
|
|
212
|
+
generator: artifact.generator || null,
|
|
213
|
+
promptHash: artifact.promptHash || null,
|
|
214
|
+
nllEntryId: artifact.nllEntryId || null,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
data.artifacts.push(record);
|
|
218
|
+
|
|
219
|
+
// Atomic write: write to temp file first, then rename
|
|
220
|
+
const tmpPath = artifactsPath + '.tmp';
|
|
221
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
222
|
+
fs.renameSync(tmpPath, artifactsPath);
|
|
223
|
+
|
|
224
|
+
return record;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function verifyChain(projectDir) {
|
|
228
|
+
const cpDir = path.join(projectDir, '.chainproof');
|
|
229
|
+
const chainPath = path.join(cpDir, 'chain.json');
|
|
230
|
+
|
|
231
|
+
if (!fs.existsSync(chainPath)) {
|
|
232
|
+
return { valid: false, errors: ['chain.json not found'] };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const chain = JSON.parse(fs.readFileSync(chainPath, 'utf-8'));
|
|
236
|
+
const errors = [];
|
|
237
|
+
let expectedHash = GENESIS_HASH;
|
|
238
|
+
|
|
239
|
+
for (let i = 0; i < chain.entries.length; i++) {
|
|
240
|
+
const entry = chain.entries[i];
|
|
241
|
+
|
|
242
|
+
// Verify prev_hash links correctly
|
|
243
|
+
if (entry.prevHash !== expectedHash) {
|
|
244
|
+
errors.push(
|
|
245
|
+
`Entry ${i}: prevHash mismatch (expected ${expectedHash.slice(0, 8)}..., got ${entry.prevHash.slice(0, 8)}...)`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Verify content hash
|
|
250
|
+
const actualContentHash = hashContent(entry.content);
|
|
251
|
+
if (entry.contentHash !== actualContentHash) {
|
|
252
|
+
errors.push(`Entry ${i}: contentHash mismatch (content was tampered)`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Verify chain hash
|
|
256
|
+
const actualChainHash = computeChainHash(entry.prevHash, entry.contentHash);
|
|
257
|
+
if (entry.chainHash !== actualChainHash) {
|
|
258
|
+
errors.push(`Entry ${i}: chainHash mismatch`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Verify signature covers full payload (not just content)
|
|
262
|
+
if (entry.signature) {
|
|
263
|
+
const publicKeyPath = path.join(cpDir, 'keys', 'public.pem');
|
|
264
|
+
if (fs.existsSync(publicKeyPath)) {
|
|
265
|
+
const publicKey = fs.readFileSync(publicKeyPath, 'utf-8');
|
|
266
|
+
const payload = buildSignablePayload(entry);
|
|
267
|
+
if (!verifySignature(payload, entry.signature, publicKey)) {
|
|
268
|
+
errors.push(`Entry ${i}: invalid signature`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
expectedHash = entry.chainHash;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Verify current hash matches last entry
|
|
277
|
+
if (chain.entries.length > 0 && chain.currentHash !== expectedHash) {
|
|
278
|
+
errors.push('currentHash does not match last entry chainHash');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return { valid: errors.length === 0, errors };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function getChainStatus(projectDir) {
|
|
285
|
+
const cpDir = path.join(projectDir, '.chainproof');
|
|
286
|
+
const chainPath = path.join(cpDir, 'chain.json');
|
|
287
|
+
const configPath = path.join(cpDir, 'config.json');
|
|
288
|
+
|
|
289
|
+
if (!fs.existsSync(cpDir)) {
|
|
290
|
+
return { initialized: false };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let chain;
|
|
294
|
+
try {
|
|
295
|
+
chain = JSON.parse(fs.readFileSync(chainPath, 'utf-8'));
|
|
296
|
+
} catch (err) {
|
|
297
|
+
throw new Error(`Failed to read chain.json: ${err.message}`, { cause: err });
|
|
298
|
+
}
|
|
299
|
+
let config = {};
|
|
300
|
+
if (fs.existsSync(configPath)) {
|
|
301
|
+
try {
|
|
302
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
303
|
+
} catch (err) {
|
|
304
|
+
throw new Error(`Failed to read config.json: ${err.message}`, { cause: err });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const artifactsPath = path.join(cpDir, 'artifacts.json');
|
|
309
|
+
let artifacts = { artifacts: [] };
|
|
310
|
+
if (fs.existsSync(artifactsPath)) {
|
|
311
|
+
try {
|
|
312
|
+
artifacts = JSON.parse(fs.readFileSync(artifactsPath, 'utf-8'));
|
|
313
|
+
} catch (err) {
|
|
314
|
+
throw new Error(`Failed to read artifacts.json: ${err.message}`, { cause: err });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const unsignedCount = chain.entries.filter((e) => !e.signature).length;
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
initialized: true,
|
|
322
|
+
projectName: config.projectName || path.basename(projectDir),
|
|
323
|
+
entryCount: chain.entries.length,
|
|
324
|
+
artifactCount: artifacts.artifacts.length,
|
|
325
|
+
currentHash: chain.currentHash,
|
|
326
|
+
unsignedEntries: unsignedCount,
|
|
327
|
+
createdAt: config.createdAt || null,
|
|
328
|
+
lastEntry: chain.entries.length > 0 ? chain.entries[chain.entries.length - 1].timestamp : null,
|
|
329
|
+
};
|
|
330
|
+
}
|
package/src/ci-mode.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { log } from './utils.js';
|
|
5
|
+
import { scanProject, detectStack } from './scanner.js';
|
|
6
|
+
import { runAllChecks } from './doctor-checks.js';
|
|
7
|
+
import { generateReport } from './doctor-prompts.js';
|
|
8
|
+
|
|
9
|
+
export async function runCI(projectDir) {
|
|
10
|
+
const resolvedDir = path.resolve(projectDir);
|
|
11
|
+
|
|
12
|
+
if (!fs.existsSync(resolvedDir)) {
|
|
13
|
+
log.error(`Directory not found: ${resolvedDir}`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
console.log('');
|
|
18
|
+
log.info(' DevForge CI Automated project health check');
|
|
19
|
+
console.log('');
|
|
20
|
+
|
|
21
|
+
// Scan project
|
|
22
|
+
const scan = scanProject(resolvedDir);
|
|
23
|
+
const stack = detectStack(resolvedDir, scan);
|
|
24
|
+
log.dim(` Stack: ${stack || 'unknown'}`);
|
|
25
|
+
|
|
26
|
+
// Run all checks
|
|
27
|
+
const issues = runAllChecks(resolvedDir, scan);
|
|
28
|
+
|
|
29
|
+
const critical = issues.filter(i => i.severity === 'critical');
|
|
30
|
+
const warnings = issues.filter(i => i.severity === 'warning');
|
|
31
|
+
const info = issues.filter(i => i.severity === 'info');
|
|
32
|
+
|
|
33
|
+
// Output results
|
|
34
|
+
console.log('');
|
|
35
|
+
if (critical.length > 0) {
|
|
36
|
+
log.error(` CRITICAL: ${critical.length} issue${critical.length > 1 ? 's' : ''}`);
|
|
37
|
+
for (const issue of critical) {
|
|
38
|
+
console.error(` - ${issue.title}`);
|
|
39
|
+
if (issue.files?.length) {
|
|
40
|
+
for (const detail of issue.files.slice(0, 5)) {
|
|
41
|
+
console.error(chalk.dim(` ${detail}`));
|
|
42
|
+
}
|
|
43
|
+
if (issue.files.length > 5) {
|
|
44
|
+
console.error(chalk.dim(` ... and ${issue.files.length - 5} more`));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (warnings.length > 0) {
|
|
51
|
+
log.warn(` WARNINGS: ${warnings.length} issue${warnings.length > 1 ? 's' : ''}`);
|
|
52
|
+
for (const issue of warnings) {
|
|
53
|
+
console.error(` - ${issue.title}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (info.length > 0) {
|
|
58
|
+
log.dim(` INFO: ${info.length} suggestion${info.length > 1 ? 's' : ''}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (critical.length === 0 && warnings.length === 0) {
|
|
62
|
+
log.success(' All checks passed!');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Save report if docs/ exists
|
|
66
|
+
const docsDir = path.join(resolvedDir, 'docs');
|
|
67
|
+
if (fs.existsSync(docsDir)) {
|
|
68
|
+
const reportPath = path.join(docsDir, 'ci-report.md');
|
|
69
|
+
const projectName = path.basename(resolvedDir);
|
|
70
|
+
const report = generateReport(issues, projectName);
|
|
71
|
+
fs.writeFileSync(reportPath, report, 'utf-8');
|
|
72
|
+
log.dim(` Report saved to docs/ci-report.md`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log('');
|
|
76
|
+
|
|
77
|
+
// Summary line for CI parsers
|
|
78
|
+
const total = issues.length;
|
|
79
|
+
console.log(`DevForge CI: ${critical.length} critical, ${warnings.length} warnings, ${info.length} info (${total} total)`);
|
|
80
|
+
|
|
81
|
+
// Exit with error code if critical issues found
|
|
82
|
+
if (critical.length > 0) {
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
}
|