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.
Files changed (183) hide show
  1. package/README.md +57 -10
  2. package/bin/chainproof.js +126 -0
  3. package/bin/devforge.js +1 -1
  4. package/package.json +25 -7
  5. package/src/chainproof-bridge.js +330 -0
  6. package/src/ci-mode.js +85 -0
  7. package/src/claude-configurator.js +171 -78
  8. package/src/cli.js +30 -7
  9. package/src/composer.js +242 -214
  10. package/src/doctor-checks-chainproof.js +106 -0
  11. package/src/doctor-checks.js +39 -20
  12. package/src/doctor-prompts.js +9 -9
  13. package/src/doctor.js +37 -4
  14. package/src/guided.js +3 -3
  15. package/src/index.js +31 -10
  16. package/src/init-mode.js +76 -12
  17. package/src/menu.js +178 -0
  18. package/src/prompts.js +5 -12
  19. package/src/recommender.js +163 -30
  20. package/src/scanner.js +57 -2
  21. package/src/uat-generator.js +204 -189
  22. package/src/update-check.js +9 -4
  23. package/src/update.js +57 -13
  24. package/src/utils.js +162 -5
  25. package/templates/ai/guardrails-py/backend/app/ai/__init__.py +29 -0
  26. package/templates/ai/guardrails-py/backend/app/ai/audit_log.py +133 -0
  27. package/templates/ai/guardrails-py/backend/app/ai/client.py.template +323 -0
  28. package/templates/ai/guardrails-py/backend/app/ai/health.py.template +157 -0
  29. package/templates/ai/guardrails-py/backend/app/ai/input_guard.py +98 -0
  30. package/templates/ai/guardrails-ts/src/lib/ai/audit-log.ts.template +164 -0
  31. package/templates/ai/guardrails-ts/src/lib/ai/client.ts.template +403 -0
  32. package/templates/ai/guardrails-ts/src/lib/ai/health.ts.template +165 -0
  33. package/templates/ai/guardrails-ts/src/lib/ai/index.ts.template +17 -0
  34. package/templates/ai/guardrails-ts/src/lib/ai/input-guard.ts.template +124 -0
  35. package/templates/auth/nextauth/src/lib/auth.ts.template +12 -7
  36. package/templates/backend/express/Dockerfile.template +18 -0
  37. package/templates/backend/express/package.json.template +33 -0
  38. package/templates/backend/express/src/index.ts.template +34 -0
  39. package/templates/backend/express/src/routes/health.ts.template +27 -0
  40. package/templates/backend/express/tsconfig.json +17 -0
  41. package/templates/backend/fastapi/backend/Dockerfile.template +5 -0
  42. package/templates/backend/fastapi/backend/app/api/health.py.template +1 -1
  43. package/templates/backend/fastapi/backend/app/core/config.py.template +1 -1
  44. package/templates/backend/fastapi/backend/app/core/errors.py +1 -1
  45. package/templates/backend/fastapi/backend/app/main.py.template +3 -1
  46. package/templates/backend/fastapi/backend/requirements.txt.template +2 -0
  47. package/templates/backend/hono/Dockerfile.template +18 -0
  48. package/templates/backend/hono/package.json.template +31 -0
  49. package/templates/backend/hono/src/index.ts.template +32 -0
  50. package/templates/backend/hono/src/routes/health.ts.template +27 -0
  51. package/templates/backend/hono/tsconfig.json +18 -0
  52. package/templates/base/.gitignore.template +3 -0
  53. package/templates/base/docs/uat/UAT_TEMPLATE.md.template +1 -1
  54. package/templates/chainproof/base/.chainproof/config.json.template +11 -0
  55. package/templates/chainproof/base/.chainproof/mcp-server.mjs +310 -0
  56. package/templates/chainproof/base/.mcp.json +9 -0
  57. package/templates/chainproof/fastapi/.chainproof/middleware.json.template +14 -0
  58. package/templates/chainproof/nextjs/.chainproof/hooks.json.template +19 -0
  59. package/templates/chainproof/polyglot/.chainproof/config.json.template +21 -0
  60. package/templates/claude-code/agents/architect.md +25 -11
  61. package/templates/claude-code/agents/build-error-resolver.md +19 -5
  62. package/templates/claude-code/agents/chief-of-staff.md +42 -8
  63. package/templates/claude-code/agents/code-quality-reviewer.md +14 -0
  64. package/templates/claude-code/agents/database-reviewer.md +15 -1
  65. package/templates/claude-code/agents/deep-reviewer.md +191 -0
  66. package/templates/claude-code/agents/doc-updater.md +19 -5
  67. package/templates/claude-code/agents/docs-lookup.md +19 -5
  68. package/templates/claude-code/agents/e2e-runner.md +26 -12
  69. package/templates/claude-code/agents/enforcement-gate.md +102 -0
  70. package/templates/claude-code/agents/frontend-builder.md +188 -0
  71. package/templates/claude-code/agents/harness-optimizer.md +36 -1
  72. package/templates/claude-code/agents/loop-operator.md +27 -13
  73. package/templates/claude-code/agents/planner.md +21 -7
  74. package/templates/claude-code/agents/product-strategist.md +24 -10
  75. package/templates/claude-code/agents/production-readiness.md +14 -0
  76. package/templates/claude-code/agents/prompt-auditor.md +115 -0
  77. package/templates/claude-code/agents/refactor-cleaner.md +22 -8
  78. package/templates/claude-code/agents/security-reviewer.md +14 -0
  79. package/templates/claude-code/agents/spec-validator.md +15 -1
  80. package/templates/claude-code/agents/tdd-guide.md +21 -7
  81. package/templates/claude-code/agents/uat-validator.md +14 -0
  82. package/templates/claude-code/claude-md/base.md +14 -7
  83. package/templates/claude-code/claude-md/fastapi.md +8 -8
  84. package/templates/claude-code/claude-md/fullstack.md +6 -6
  85. package/templates/claude-code/claude-md/hono.md +18 -0
  86. package/templates/claude-code/claude-md/nextjs.md +5 -5
  87. package/templates/claude-code/claude-md/remix.md +18 -0
  88. package/templates/claude-code/commands/audit-security.md +14 -0
  89. package/templates/claude-code/commands/audit-spec.md +14 -0
  90. package/templates/claude-code/commands/audit-wiring.md +14 -0
  91. package/templates/claude-code/commands/build-fix.md +28 -0
  92. package/templates/claude-code/commands/build-ui.md +59 -0
  93. package/templates/claude-code/commands/code-review.md +53 -31
  94. package/templates/claude-code/commands/fix-loop.md +211 -0
  95. package/templates/claude-code/commands/full-audit.md +36 -8
  96. package/templates/claude-code/commands/generate-prd.md +1 -1
  97. package/templates/claude-code/commands/generate-sdd.md +74 -0
  98. package/templates/claude-code/commands/generate-uat.md +107 -35
  99. package/templates/claude-code/commands/help.md +68 -0
  100. package/templates/claude-code/commands/live-uat.md +268 -0
  101. package/templates/claude-code/commands/optimize-claude-md.md +15 -1
  102. package/templates/claude-code/commands/plan.md +3 -3
  103. package/templates/claude-code/commands/pre-pr.md +57 -19
  104. package/templates/claude-code/commands/product-strategist.md +21 -0
  105. package/templates/claude-code/commands/resume-session.md +10 -10
  106. package/templates/claude-code/commands/run-uat.md +59 -2
  107. package/templates/claude-code/commands/save-session.md +10 -10
  108. package/templates/claude-code/commands/simplify.md +36 -0
  109. package/templates/claude-code/commands/tdd.md +17 -18
  110. package/templates/claude-code/commands/verify-all.md +24 -0
  111. package/templates/claude-code/commands/verify-intent.md +55 -0
  112. package/templates/claude-code/commands/workflows.md +52 -40
  113. package/templates/claude-code/hooks/polyglot.json +10 -1
  114. package/templates/claude-code/hooks/python.json +10 -1
  115. package/templates/claude-code/hooks/scripts/autofix-polyglot.mjs +2 -2
  116. package/templates/claude-code/hooks/scripts/autofix-python.mjs +1 -1
  117. package/templates/claude-code/hooks/scripts/autofix-typescript.mjs +1 -1
  118. package/templates/claude-code/hooks/scripts/code-hygiene.mjs +293 -0
  119. package/templates/claude-code/hooks/scripts/pre-commit-gate.mjs +207 -0
  120. package/templates/claude-code/hooks/typescript.json +10 -1
  121. package/templates/claude-code/skills/ai-prompts/SKILL.md +119 -41
  122. package/templates/claude-code/skills/git-workflow/SKILL.md +5 -5
  123. package/templates/claude-code/skills/nextjs/SKILL.md +1 -1
  124. package/templates/claude-code/skills/playwright/SKILL.md +5 -5
  125. package/templates/claude-code/skills/security-api/SKILL.md +1 -1
  126. package/templates/claude-code/skills/security-web/SKILL.md +1 -1
  127. package/templates/claude-code/skills/testing-patterns/SKILL.md +9 -9
  128. package/templates/database/prisma-postgres/{.env.example → .env.example.template} +1 -0
  129. package/templates/database/sqlalchemy-postgres/{.env.example → .env.example.template} +1 -0
  130. package/templates/docs-portal/fastapi/backend/app/portal/__pycache__/docs_reader.cpython-314.pyc +0 -0
  131. package/templates/docs-portal/fastapi/backend/app/portal/docs_reader.py +201 -0
  132. package/templates/docs-portal/fastapi/backend/app/portal/html_renderer.py +229 -0
  133. package/templates/docs-portal/fastapi/backend/app/portal/router.py.template +35 -0
  134. package/templates/docs-portal/nextjs/src/app/portal/[category]/[slug]/page.tsx +81 -0
  135. package/templates/docs-portal/nextjs/src/app/portal/[category]/page.tsx +65 -0
  136. package/templates/docs-portal/nextjs/src/app/portal/layout.tsx.template +54 -0
  137. package/templates/docs-portal/nextjs/src/app/portal/page.tsx +85 -0
  138. package/templates/docs-portal/nextjs/src/components/portal/markdown-renderer.tsx +101 -0
  139. package/templates/docs-portal/nextjs/src/components/portal/mobile-portal-nav.tsx +81 -0
  140. package/templates/docs-portal/nextjs/src/components/portal/portal-nav.tsx +86 -0
  141. package/templates/docs-portal/nextjs/src/lib/docs.ts +139 -0
  142. package/templates/frontend/nextjs/package.json.template +3 -1
  143. package/templates/frontend/react/index.html.template +12 -0
  144. package/templates/frontend/react/package.json.template +34 -0
  145. package/templates/frontend/react/src/App.tsx.template +10 -0
  146. package/templates/frontend/react/src/index.css +1 -0
  147. package/templates/frontend/react/src/main.tsx +10 -0
  148. package/templates/frontend/react/tsconfig.json +17 -0
  149. package/templates/frontend/react/vite.config.ts.template +15 -0
  150. package/templates/frontend/react/vitest.config.ts +9 -0
  151. package/templates/frontend/remix/app/root.tsx.template +31 -0
  152. package/templates/frontend/remix/app/routes/_index.tsx.template +19 -0
  153. package/templates/frontend/remix/app/routes/api.health.ts.template +10 -0
  154. package/templates/frontend/remix/app/tailwind.css +1 -0
  155. package/templates/frontend/remix/package.json.template +39 -0
  156. package/templates/frontend/remix/tsconfig.json +18 -0
  157. package/templates/frontend/remix/vite.config.ts.template +7 -0
  158. package/templates/infra/github-actions/.github/workflows/ci.yml.template +3 -0
  159. package/templates/infra/k8s/k8s/deployment.yml.template +70 -0
  160. package/templates/infra/k8s/k8s/hpa.yml.template +24 -0
  161. package/templates/infra/k8s/k8s/ingress.yml.template +26 -0
  162. package/templates/infra/k8s/k8s/kustomization.yml.template +13 -0
  163. package/templates/infra/k8s/k8s/namespace.yml.template +4 -0
  164. package/templates/infra/k8s/k8s/networkpolicy.yml.template +41 -0
  165. package/templates/infra/k8s/k8s/secrets.yml.template +10 -0
  166. package/templates/infra/k8s/k8s/service.yml.template +15 -0
  167. package/templates/testing/load/k6/README.md.template +48 -0
  168. package/templates/testing/load/k6/load-test.js.template +57 -0
  169. package/docs/00-README.md +0 -310
  170. package/docs/01-universal-prompt-library.md +0 -1049
  171. package/docs/02-claude-code-mastery-playbook.md +0 -283
  172. package/docs/03-multi-agent-verification.md +0 -565
  173. package/docs/04-errata-and-verification-checklist.md +0 -284
  174. package/docs/05-universal-scaffolder-vision.md +0 -452
  175. package/docs/06-confidence-assessment-and-repo-prompt.md +0 -407
  176. package/docs/errata.md +0 -58
  177. package/docs/multi-agent-verification.md +0 -66
  178. package/docs/playbook.md +0 -95
  179. package/docs/prompt-library.md +0 -160
  180. package/docs/uat/UAT_CHECKLIST.csv +0 -9
  181. package/docs/uat/UAT_TEMPLATE.md +0 -163
  182. package/templates/claude-code/commands/done.md +0 -19
  183. /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
+ }