chati-dev 4.2.1 → 4.3.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 +80 -53
- package/bin/chati.js +150 -5
- package/framework/agents/build/dev.md +509 -76
- package/framework/agents/deploy/devops.md +40 -25
- package/framework/agents/discover/brief.md +156 -22
- package/framework/agents/discover/brownfield-wu.md +24 -14
- package/framework/agents/discover/greenfield-wu.md +100 -14
- package/framework/agents/plan/architect-data-engineer.md +6 -6
- package/framework/agents/plan/architect-system.md +46 -12
- package/framework/agents/plan/architect.md +40 -20
- package/framework/agents/plan/detail.md +36 -24
- package/framework/agents/plan/phases.md +36 -26
- package/framework/agents/plan/tasks.md +114 -33
- package/framework/agents/plan/ux-brand-architect.md +240 -8
- package/framework/agents/plan/ux-component-engineer.md +28 -13
- package/framework/agents/plan/ux-researcher.md +7 -6
- package/framework/agents/plan/ux.md +55 -22
- package/framework/agents/quality/qa-implementation.md +143 -74
- package/framework/agents/quality/qa-planning.md +115 -42
- package/framework/agents/quality/qa-visual.md +439 -0
- package/framework/agents/shared/visualizer.md +128 -0
- package/framework/config.yaml +8 -7
- package/framework/constitution.md +127 -43
- package/framework/context/governance.md +12 -7
- package/framework/context/quality.md +6 -5
- package/framework/context/root.md +6 -6
- package/framework/data/entity-registry.yaml +377 -4
- package/framework/data/model-limits.json +19 -0
- package/framework/domains/agents/qa-visual.yaml +74 -0
- package/framework/domains/constitution.yaml +46 -2
- package/framework/domains/workflows/greenfield-fullstack.yaml +2 -2
- package/framework/hooks/advance-trigger.js +131 -0
- package/framework/hooks/brief-validator.js +83 -0
- package/framework/hooks/constitution-guard.js +91 -36
- package/framework/hooks/license-guard.js +129 -190
- package/framework/hooks/mode-governance.js +68 -16
- package/framework/hooks/model-governance.js +19 -8
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/post-dev.js +207 -0
- package/framework/hooks/prism-engine.js +288 -105
- package/framework/hooks/read-protection.js +37 -9
- package/framework/hooks/reasoning-escalator.js +371 -0
- package/framework/hooks/reference-trigger.js +117 -0
- package/framework/hooks/session-digest.js +94 -20
- package/framework/hooks/settings.json +32 -1
- package/framework/hooks/style-guard.js +52 -13
- package/framework/hooks/team-quality-gate.js +58 -25
- package/framework/hooks/undercover-guard.js +34 -13
- package/framework/i18n/en.yaml +3 -3
- package/framework/i18n/es.yaml +3 -3
- package/framework/i18n/fr.yaml +3 -3
- package/framework/i18n/pt.yaml +3 -3
- package/framework/intelligence/context-engine.md +4 -5
- package/framework/intelligence/decision-engine.md +1 -1
- package/framework/intelligence/hooks-performance.md +3 -3
- package/framework/migrations/v1.0-to-v1.1.yaml +1 -1
- package/framework/migrations/v1.4-to-v2.0.yaml +11 -11
- package/framework/migrations/v4.0-to-v4.1.yaml +2 -2
- package/framework/migrations/v4.2-to-v4.3.yaml +29 -0
- package/framework/orchestrator/chati-router.js +387 -0
- package/framework/orchestrator/chati-update.md +40 -40
- package/framework/orchestrator/chati.md +318 -115
- package/framework/scaffold/motion-premium/README.md +89 -0
- package/framework/scaffold/motion-premium/app/globals.css.template +400 -0
- package/framework/scaffold/motion-premium/app/layout.tsx.template +110 -0
- package/framework/scaffold/motion-premium/components/animation/BackgroundCrossfadeProvider.tsx.template +170 -0
- package/framework/scaffold/motion-premium/components/animation/LenisProvider.tsx.template +49 -0
- package/framework/scaffold/motion-premium/components/animation/PageTransitionWrapper.tsx.template +83 -0
- package/framework/scaffold/motion-premium/components/animation/Preloader.tsx.template +171 -0
- package/framework/scaffold/motion-premium/components/ui/Container.tsx.template +69 -0
- package/framework/scaffold/motion-premium/components/ui/PageSection.tsx.template +74 -0
- package/framework/scaffold/motion-premium/lib/animations/gsap.ts.template +112 -0
- package/framework/scaffold/motion-premium/lib/animations/refreshCoordinator.ts.template +75 -0
- package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +119 -0
- package/framework/scaffold/motion-premium/lib/animations/useGsapContext.ts.template +92 -0
- package/framework/scaffold/motion-premium/lib/animations/useScrollSnapStepper.ts.template +265 -0
- package/framework/scaffold/motion-premium/lib/animations/useSmoothScroll.ts.template +67 -0
- package/framework/scaffold/motion-premium/lib/brand.ts.template +43 -0
- package/framework/scaffold/motion-premium/scaffold.yaml +174 -0
- package/framework/scaffold/motion-premium-3d/README.md +80 -0
- package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoCanvas.tsx.template +81 -0
- package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoClient.tsx.template +75 -0
- package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/page.tsx.template +26 -0
- package/framework/scaffold/motion-premium-3d/components/3d/CameraRig.tsx.template +100 -0
- package/framework/scaffold/motion-premium-3d/components/3d/CanvasProvider.tsx.template +85 -0
- package/framework/scaffold/motion-premium-3d/components/3d/InvalidateOnScroll.tsx.template +51 -0
- package/framework/scaffold/motion-premium-3d/components/3d/MeshCrossfade.tsx.template +79 -0
- package/framework/scaffold/motion-premium-3d/components/3d/ScrollCrossfade.tsx.template +88 -0
- package/framework/scaffold/motion-premium-3d/components/3d/ScrollScene.tsx.template +121 -0
- package/framework/scaffold/motion-premium-3d/components/webgl/SceneFallback.tsx.template +65 -0
- package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +68 -0
- package/framework/scaffold/motion-premium-3d/lib/webgl/detect.ts.template +69 -0
- package/framework/scaffold/motion-premium-3d/scaffold.yaml +133 -0
- package/framework/schemas/session.schema.json +109 -21
- package/framework/scripts/reference-capture.js +430 -0
- package/framework/scripts/visual-qa.js +674 -0
- package/framework/tasks/orchestrator-handoff.md +1 -1
- package/framework/tasks/orchestrator-resume.md +1 -1
- package/framework/tasks/orchestrator-route.md +1 -1
- package/framework/tasks/orchestrator-status.md +3 -3
- package/framework/tasks/qa-planning-gate-define.md +1 -1
- package/framework/templates/brandbook-html-tmpl.md +1 -1
- package/framework/templates/brandbook-tmpl.yaml +1 -1
- package/framework/templates/component-spec-tmpl.yaml +1 -1
- package/framework/templates/design-token-tmpl.yaml +1 -1
- package/framework/templates/icon-system-tmpl.yaml +1 -1
- package/framework/templates/team-planning-tasks.yaml +6 -5
- package/framework/workflows/brownfield-discovery.yaml +2 -2
- package/framework/workflows/brownfield-fullstack.yaml +15 -11
- package/framework/workflows/brownfield-service.yaml +14 -10
- package/framework/workflows/brownfield-ui.yaml +15 -11
- package/framework/workflows/greenfield-fullstack.yaml +16 -13
- package/framework/workflows/quick-flow.yaml +3 -3
- package/framework/workflows/standard-flow.yaml +12 -9
- package/package.json +10 -5
- package/src/autonomy/autonomous-gate.js +1 -0
- package/src/autonomy/build-state.js +1 -2
- package/src/autonomy/progress-reporter.js +1 -1
- package/src/config/agent-customizer.js +11 -3
- package/src/config/claude-settings-generator.js +226 -0
- package/src/config/context-file-generator.js +41 -21
- package/src/config/framework-adapter.js +1 -0
- package/src/config/gemini-hooks-generator.js +76 -6
- package/src/config/mcp-configs.js +1 -0
- package/src/context/layers/l1-global.js +2 -1
- package/src/dashboard/data-reader.js +4 -3
- package/src/dashboard/layout.js +2 -1
- package/src/decision/analyzer.js +6 -30
- package/src/decision/engine.js +4 -28
- package/src/decision/registry-healer.js +3 -2
- package/src/decision/registry-updater.js +23 -14
- package/src/extensions/loader.js +2 -8
- package/src/gates/g1-planning-complete.js +2 -1
- package/src/gates/g2-qa-planning.js +2 -1
- package/src/gates/g3-implementation.js +2 -1
- package/src/gates/g4-qa-implementation.js +3 -2
- package/src/gates/g5-deploy-ready.js +2 -1
- package/src/health/engine.js +4 -3
- package/src/installer/core.js +557 -81
- package/src/installer/preflight.js +131 -0
- package/src/installer/provider-overlay.js +3 -3
- package/src/installer/scaffold-applier.js +358 -0
- package/src/installer/templates.js +52 -31
- package/src/installer/validator.js +17 -12
- package/src/intelligence/registry-manager.js +22 -29
- package/src/intelligence/timeline.js +11 -6
- package/src/license/commands.js +1 -1
- package/src/license/wait.js +102 -0
- package/src/memory/agent-memory.js +81 -0
- package/src/memory/dream.js +32 -1
- package/src/merger/replace-merger.js +28 -15
- package/src/orchestrator/agent-selector.js +2 -1
- package/src/orchestrator/cli.js +1908 -80
- package/src/orchestrator/doctor.js +270 -0
- package/src/orchestrator/handoff-engine.js +4 -3
- package/src/orchestrator/index.js +2 -0
- package/src/orchestrator/pipeline-manager.js +306 -15
- package/src/orchestrator/session-manager.js +331 -6
- package/src/tasks/handoff.js +3 -2
- package/src/telemetry/config.js +4 -3
- package/src/telemetry/schema.js +1 -0
- package/src/terminal/collector.js +3 -2
- package/src/terminal/index.js +1 -2
- package/src/terminal/isolation.js +52 -18
- package/src/terminal/prompt-builder.js +42 -25
- package/src/terminal/run-parallel.js +1 -1
- package/src/terminal/run-team.js +3 -3
- package/src/terminal/team-task-list.js +43 -4
- package/src/upgrade/backup.js +3 -2
- package/src/upgrade/checker.js +3 -2
- package/src/upgrade/migrator.js +65 -7
- package/src/upgrade/tracked-files-detector.js +86 -0
- package/src/upgrade/user-messages.js +94 -0
- package/src/utils/config-parser.js +2 -1
- package/src/utils/feature-flags.js +2 -1
- package/src/utils/flatten-entities.js +69 -0
- package/src/utils/framework-dir.js +16 -0
- package/src/utils/model-id.js +85 -0
- package/src/utils/provider-limits.js +84 -23
- package/src/utils/schema-validator.js +1 -1
- package/src/wizard/i18n.js +5 -4
- package/src/wizard/index.js +14 -0
- package/assets/logo - c/303/263pia.png +0 -0
- package/assets/logo.svg +0 -42
- package/assets/logo2.png +0 -0
- package/assets/social-preview.png +0 -0
- package/scripts/bundle-framework.js +0 -69
- package/scripts/changelog-generator.js +0 -222
- package/scripts/codebase-mapper.js +0 -728
- package/scripts/commit-message-generator.js +0 -167
- package/scripts/coverage-analyzer.js +0 -260
- package/scripts/dependency-analyzer.js +0 -280
- package/scripts/doctor/checks/agents.js +0 -77
- package/scripts/doctor/checks/constitution.js +0 -41
- package/scripts/doctor/checks/domain-alignment.js +0 -58
- package/scripts/doctor/checks/prism-layers.js +0 -84
- package/scripts/doctor/checks/registry.js +0 -55
- package/scripts/doctor/checks/schemas.js +0 -61
- package/scripts/doctor/fixes/reference-fix.js +0 -100
- package/scripts/doctor/fixes/registry-fix.js +0 -56
- package/scripts/doctor/index.js +0 -212
- package/scripts/framework-analyzer.js +0 -308
- package/scripts/generate-constitution-domain.js +0 -253
- package/scripts/generate-signing-key.js +0 -33
- package/scripts/health-check.js +0 -481
- package/scripts/ide-sync.js +0 -326
- package/scripts/performance-analyzer.js +0 -325
- package/scripts/plan-tracker.js +0 -278
- package/scripts/populate-entity-registry.js +0 -481
- package/scripts/pr-review.js +0 -317
- package/scripts/rollback-manager.js +0 -310
- package/scripts/semantic-lint.js +0 -328
- package/scripts/sign-manifest.js +0 -53
- package/scripts/stuck-detector.js +0 -343
- package/scripts/test-quality-assessment.js +0 -257
- package/scripts/validate-agents.js +0 -368
- package/scripts/validate-package.js +0 -367
- package/scripts/validate-tasks.js +0 -465
- package/src/autonomy/worktree-manager.js +0 -250
- package/src/intelligence/decision-engine.js +0 -256
- package/src/intelligence/document-sharder.js +0 -221
- package/src/intelligence/elicitation.js +0 -265
package/src/installer/core.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync, copyFileSync, existsSync, readFileSync, readdirSync } from 'fs';
|
|
2
|
-
import { join, dirname } from 'path';
|
|
1
|
+
import { mkdirSync, writeFileSync, copyFileSync, existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, dirname, basename } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
+
import { createRequire } from 'module';
|
|
4
5
|
import { IDE_CONFIGS, IDE_TO_PROVIDER } from '../config/ide-configs.js';
|
|
5
6
|
import { generateClaudeMCPConfig } from '../config/mcp-configs.js';
|
|
6
7
|
import { generateSessionYaml, generateConfigYaml, generateClaudeMd, generateClaudeLocalMd, generateCodexSkill, generateGeminiRouter, generateGeminiSessionLock, generateAgentsOverrideMd, generateCodexConstitutionGuardRules, generateCodexReadProtectionRules } from './templates.js';
|
|
7
8
|
import { generateContextFiles } from '../config/context-file-generator.js';
|
|
8
9
|
import { adaptFrameworkFile, ADAPTABLE_FILES } from '../config/framework-adapter.js';
|
|
10
|
+
import { generateClaudeSettings } from '../config/claude-settings-generator.js';
|
|
9
11
|
import { generateProviderOverlays } from './provider-overlay.js';
|
|
10
12
|
import { verifyManifest } from './manifest.js';
|
|
11
13
|
|
|
@@ -21,7 +23,15 @@ const FRAMEWORK_SOURCE = existsSync(BUNDLED_SOURCE) ? BUNDLED_SOURCE : MONOREPO_
|
|
|
21
23
|
* Install Chati.dev framework into target directory
|
|
22
24
|
*/
|
|
23
25
|
export async function installFramework(config) {
|
|
24
|
-
const { targetDir, projectType, language, selectedIDEs, selectedMCPs, projectName,
|
|
26
|
+
const { targetDir, projectType, language, selectedIDEs, selectedMCPs, projectName, llmProvider, allProviders } = config;
|
|
27
|
+
// Always derive version from package.json (single source of truth)
|
|
28
|
+
let { version } = config;
|
|
29
|
+
if (!version) {
|
|
30
|
+
try {
|
|
31
|
+
const pkgPath = join(__dirname, '..', '..', 'package.json');
|
|
32
|
+
version = JSON.parse(readFileSync(pkgPath, 'utf-8')).version;
|
|
33
|
+
} catch { version = 'unknown'; }
|
|
34
|
+
}
|
|
25
35
|
|
|
26
36
|
// 0. Verify framework signature (supply chain protection)
|
|
27
37
|
const manifestPath = join(FRAMEWORK_SOURCE, 'manifest.json');
|
|
@@ -39,17 +49,36 @@ export async function installFramework(config) {
|
|
|
39
49
|
|
|
40
50
|
// 1. Create .chati/ session directory
|
|
41
51
|
createDir(join(targetDir, '.chati'));
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
const sessionPath = join(targetDir, '.chati', 'session.yaml');
|
|
53
|
+
// Preserve existing session.yaml on reinstall/recovery (don't destroy pipeline state).
|
|
54
|
+
// Only write fresh session if one does not exist.
|
|
55
|
+
if (!existsSync(sessionPath)) {
|
|
56
|
+
writeFileSync(
|
|
57
|
+
sessionPath,
|
|
58
|
+
generateSessionYaml({ projectName, projectType, language, selectedIDEs, selectedMCPs, llmProvider, allProviders }),
|
|
59
|
+
'utf-8'
|
|
60
|
+
);
|
|
61
|
+
}
|
|
47
62
|
|
|
48
|
-
// 2. Create chati.dev/ framework directory (
|
|
49
|
-
|
|
63
|
+
// 2. Create .chati.dev/ framework directory (hidden — dot prefix)
|
|
64
|
+
// The source is chati.dev/ in the monorepo; installed as .chati.dev/ in user projects.
|
|
65
|
+
// Text replacement during copy handles all internal path references.
|
|
66
|
+
const frameworkDir = join(targetDir, '.chati.dev');
|
|
50
67
|
createDir(frameworkDir);
|
|
51
68
|
|
|
52
|
-
//
|
|
69
|
+
// 2b. Create artifacts/ at PROJECT ROOT (visible to user — this is their output)
|
|
70
|
+
const artifactsDir = join(targetDir, 'artifacts');
|
|
71
|
+
const artifactDirs = [
|
|
72
|
+
'0-WU', '1-Brief', '2-PRD',
|
|
73
|
+
'3-Architecture', '4-UX', '5-Phases',
|
|
74
|
+
'6-Tasks', '7-QA-Planning', '8-QA-Visual', '9-QA-Implementation', '10-Deploy',
|
|
75
|
+
'handoffs', 'decisions',
|
|
76
|
+
];
|
|
77
|
+
for (const dir of artifactDirs) {
|
|
78
|
+
createDir(join(artifactsDir, dir));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Copy framework structure (everything EXCEPT artifacts, which live at root)
|
|
53
82
|
const frameworkDirs = [
|
|
54
83
|
'orchestrator',
|
|
55
84
|
'agents/discover', 'agents/plan', 'agents/quality', 'agents/build', 'agents/deploy',
|
|
@@ -57,10 +86,6 @@ export async function installFramework(config) {
|
|
|
57
86
|
'schemas', 'frameworks', 'intelligence', 'patterns',
|
|
58
87
|
'hooks', 'domains', 'domains/agents', 'domains/workflows',
|
|
59
88
|
'i18n', 'migrations', 'data', 'context',
|
|
60
|
-
'artifacts/0-WU', 'artifacts/1-Brief', 'artifacts/2-PRD',
|
|
61
|
-
'artifacts/3-Architecture', 'artifacts/4-UX', 'artifacts/5-Phases',
|
|
62
|
-
'artifacts/6-Tasks', 'artifacts/7-QA-Planning', 'artifacts/9-QA-Implementation', 'artifacts/10-Deploy',
|
|
63
|
-
'artifacts/handoffs', 'artifacts/decisions',
|
|
64
89
|
];
|
|
65
90
|
|
|
66
91
|
for (const dir of frameworkDirs) {
|
|
@@ -81,6 +106,7 @@ export async function installFramework(config) {
|
|
|
81
106
|
'tasks/durable', 'tasks/daily',
|
|
82
107
|
'qa-planning/durable', 'qa-planning/daily',
|
|
83
108
|
'qa-implementation/durable', 'qa-implementation/daily',
|
|
109
|
+
'qa-visual/durable', 'qa-visual/daily',
|
|
84
110
|
'dev/durable', 'dev/daily',
|
|
85
111
|
'devops/durable', 'devops/daily',
|
|
86
112
|
];
|
|
@@ -96,6 +122,31 @@ export async function installFramework(config) {
|
|
|
96
122
|
generateProviderOverlays(targetDir, FRAMEWORK_SOURCE, llmProvider || 'claude', allProviders);
|
|
97
123
|
}
|
|
98
124
|
|
|
125
|
+
// Bundle the CLI source into the framework dir so chati-router.js can run
|
|
126
|
+
// commands locally without depending on `npx chati-dev` (which fetches from
|
|
127
|
+
// the npm registry — version drift causes inconsistent behavior).
|
|
128
|
+
// Result: every install is self-contained; no network/cache dependency at runtime.
|
|
129
|
+
copyCliSource(
|
|
130
|
+
join(__dirname, '..'), // packages/chati-dev/src/
|
|
131
|
+
join(frameworkDir, '_cli') // .chati.dev/_cli/
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// The bundled CLI imports ../../framework/hooks/model-governance.js relative
|
|
135
|
+
// to _cli/terminal/prompt-builder.js. Starting at .chati.dev/_cli/terminal/,
|
|
136
|
+
// `../../` resolves to .chati.dev/ — so the hooks copy must land at
|
|
137
|
+
// .chati.dev/framework/hooks/ to satisfy the import path.
|
|
138
|
+
copyDirVerbatim(
|
|
139
|
+
join(__dirname, '..', '..', 'framework', 'hooks'),
|
|
140
|
+
join(frameworkDir, 'framework', 'hooks')
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Also bundle production deps so the CLI can import js-yaml/chalk/etc. at runtime
|
|
144
|
+
// without relying on a node_modules tree in the user's project.
|
|
145
|
+
copyCliDependencies(
|
|
146
|
+
join(__dirname, '..', '..'), // packages/chati-dev/
|
|
147
|
+
join(frameworkDir, '_cli', 'node_modules') // .chati.dev/_cli/node_modules/
|
|
148
|
+
);
|
|
149
|
+
|
|
99
150
|
// Write config.yaml
|
|
100
151
|
writeFileSync(
|
|
101
152
|
join(frameworkDir, 'config.yaml'),
|
|
@@ -109,8 +160,8 @@ export async function installFramework(config) {
|
|
|
109
160
|
const ideProvider = IDE_TO_PROVIDER[ideKey];
|
|
110
161
|
const isSecondary = ideProvider && ideProvider !== primary && allProviders && allProviders.length > 1;
|
|
111
162
|
const orchestratorPath = isSecondary
|
|
112
|
-
?
|
|
113
|
-
: 'chati.dev/orchestrator/chati.md';
|
|
163
|
+
? `.chati.dev/.adapted/${ideProvider}/orchestrator/chati.md`
|
|
164
|
+
: '.chati.dev/orchestrator/chati.md';
|
|
114
165
|
await configureIDE(targetDir, ideKey, selectedMCPs, { orchestratorPath, isSecondary, providerName: ideProvider });
|
|
115
166
|
}
|
|
116
167
|
|
|
@@ -128,11 +179,22 @@ export async function installFramework(config) {
|
|
|
128
179
|
for (const file of contextFiles) {
|
|
129
180
|
const src = join(FRAMEWORK_SOURCE, 'context', file);
|
|
130
181
|
if (existsSync(src)) {
|
|
131
|
-
|
|
182
|
+
// Apply path replacement so rules reference .chati.dev/ not chati.dev/
|
|
183
|
+
let content = readFileSync(src, 'utf-8');
|
|
184
|
+
content = applyPathReplacement(content);
|
|
185
|
+
writeFileSync(join(claudeRulesDir, file), content, 'utf-8');
|
|
132
186
|
}
|
|
133
187
|
}
|
|
134
188
|
|
|
135
|
-
|
|
189
|
+
// Preserve existing CLAUDE.md on reinstall — user or magic-docs may have
|
|
190
|
+
// edited it. Section-aware merge if exists; fresh write if not.
|
|
191
|
+
const claudeMdPath = join(targetDir, 'CLAUDE.md');
|
|
192
|
+
if (existsSync(claudeMdPath)) {
|
|
193
|
+
const existingClaudeMd = readFileSync(claudeMdPath, 'utf-8');
|
|
194
|
+
writeFileSync(claudeMdPath, mergeClaudeMd(existingClaudeMd, baseContent), 'utf-8');
|
|
195
|
+
} else {
|
|
196
|
+
writeFileSync(claudeMdPath, baseContent, 'utf-8');
|
|
197
|
+
}
|
|
136
198
|
writeFileSync(join(targetDir, 'CLAUDE.local.md'), generateClaudeLocalMd(), 'utf-8');
|
|
137
199
|
}
|
|
138
200
|
|
|
@@ -171,6 +233,8 @@ const FRAMEWORK_DIRS_TO_COPY = [
|
|
|
171
233
|
'context',
|
|
172
234
|
'tasks',
|
|
173
235
|
'presets',
|
|
236
|
+
'scripts',
|
|
237
|
+
'scaffold',
|
|
174
238
|
];
|
|
175
239
|
|
|
176
240
|
/**
|
|
@@ -181,8 +245,39 @@ const FRAMEWORK_ROOT_FILES = [
|
|
|
181
245
|
];
|
|
182
246
|
|
|
183
247
|
/**
|
|
184
|
-
*
|
|
185
|
-
*
|
|
248
|
+
* Text file extensions that get path replacement during copy.
|
|
249
|
+
* Binary files (.png, .woff2, etc.) are copied without modification.
|
|
250
|
+
*/
|
|
251
|
+
const TEXT_EXTENSIONS = new Set(['.md', '.js', '.yaml', '.yml', '.json', '.html', '.toml', '.txt', '.rules']);
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Apply installed-path replacement to file content.
|
|
255
|
+
*
|
|
256
|
+
* Source uses `chati.dev/` everywhere. Installed project uses:
|
|
257
|
+
* - `.chati.dev/` for framework files (hidden)
|
|
258
|
+
* - `artifacts/` at project root for user-visible output
|
|
259
|
+
*
|
|
260
|
+
* Order matters: replace `chati.dev/artifacts/` FIRST (→ `artifacts/`),
|
|
261
|
+
* then `chati.dev/` (→ `.chati.dev/`). Reversing would produce `.chati.dev/artifacts/`.
|
|
262
|
+
*/
|
|
263
|
+
function applyPathReplacement(content) {
|
|
264
|
+
return content
|
|
265
|
+
// Step 1: artifacts at root (must be before generic replace)
|
|
266
|
+
.replace(/chati\.dev\/artifacts\//g, 'artifacts/')
|
|
267
|
+
// Step 2: path strings with slash — but NOT if already prefixed with dot
|
|
268
|
+
// (negative lookbehind for any char that would create `.chati.dev` already)
|
|
269
|
+
.replace(/(?<!\.)chati\.dev\//g, '.chati.dev/')
|
|
270
|
+
// Step 3: quoted dir name in JS join() calls (WITHOUT slash)
|
|
271
|
+
// Only match 'chati.dev' (the DIRECTORY, with dot) not 'chati-dev' (the NPM PACKAGE, with dash)
|
|
272
|
+
// Also: avoid double-prefixing if already '.chati.dev' in source
|
|
273
|
+
.replace(/(?<!\.)'chati\.dev'/g, "'.chati.dev'")
|
|
274
|
+
.replace(/(?<!\.)"chati\.dev"/g, '".chati.dev"');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Recursively copy a directory from source to destination, applying:
|
|
279
|
+
* 1. Path replacement (chati.dev/ → .chati.dev/, artifacts/ at root)
|
|
280
|
+
* 2. Provider adaptation for non-Claude providers
|
|
186
281
|
*/
|
|
187
282
|
function copyDirRecursive(srcDir, destDir, provider, relBase = '') {
|
|
188
283
|
if (!existsSync(srcDir)) return;
|
|
@@ -195,14 +290,24 @@ function copyDirRecursive(srcDir, destDir, provider, relBase = '') {
|
|
|
195
290
|
const relPath = relBase ? join(relBase, entry.name) : entry.name;
|
|
196
291
|
|
|
197
292
|
if (entry.isDirectory()) {
|
|
293
|
+
// Skip artifacts/ — those live at project root, not inside .chati.dev/
|
|
294
|
+
if (entry.name === 'artifacts') continue;
|
|
198
295
|
copyDirRecursive(srcPath, destPath, provider, relPath);
|
|
199
296
|
} else if (entry.isFile()) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
297
|
+
const ext = entry.name.includes('.') ? '.' + entry.name.split('.').pop() : '';
|
|
298
|
+
const isText = TEXT_EXTENSIONS.has(ext);
|
|
299
|
+
|
|
300
|
+
if (isText) {
|
|
301
|
+
let content = readFileSync(srcPath, 'utf-8');
|
|
302
|
+
// Always apply path replacement for installed location
|
|
303
|
+
content = applyPathReplacement(content);
|
|
304
|
+
// Provider adaptation on top (for non-Claude)
|
|
305
|
+
if (provider !== 'claude' && ADAPTABLE_FILES.has(relPath)) {
|
|
306
|
+
content = adaptFrameworkFile(content, relPath, provider);
|
|
307
|
+
}
|
|
308
|
+
writeFileSync(destPath, content, 'utf-8');
|
|
204
309
|
} else {
|
|
205
|
-
//
|
|
310
|
+
// Binary file: direct copy
|
|
206
311
|
copyFileSync(srcPath, destPath);
|
|
207
312
|
}
|
|
208
313
|
}
|
|
@@ -221,18 +326,18 @@ function copyDirRecursive(srcDir, destDir, provider, relBase = '') {
|
|
|
221
326
|
export function copyFrameworkFiles(destDir, provider = 'claude') {
|
|
222
327
|
if (!existsSync(FRAMEWORK_SOURCE)) return;
|
|
223
328
|
|
|
224
|
-
// Copy loose root files
|
|
329
|
+
// Copy loose root files (with path replacement)
|
|
225
330
|
for (const file of FRAMEWORK_ROOT_FILES) {
|
|
226
331
|
const src = join(FRAMEWORK_SOURCE, file);
|
|
227
332
|
if (!existsSync(src)) continue;
|
|
228
333
|
const dest = join(destDir, file);
|
|
229
334
|
createDir(dirname(dest));
|
|
335
|
+
let content = readFileSync(src, 'utf-8');
|
|
336
|
+
content = applyPathReplacement(content);
|
|
230
337
|
if (provider !== 'claude' && ADAPTABLE_FILES.has(file)) {
|
|
231
|
-
|
|
232
|
-
writeFileSync(dest, adaptFrameworkFile(content, file, provider), 'utf-8');
|
|
233
|
-
} else {
|
|
234
|
-
copyFileSync(src, dest);
|
|
338
|
+
content = adaptFrameworkFile(content, file, provider);
|
|
235
339
|
}
|
|
340
|
+
writeFileSync(dest, content, 'utf-8');
|
|
236
341
|
}
|
|
237
342
|
|
|
238
343
|
// Copy each framework directory recursively
|
|
@@ -256,40 +361,47 @@ async function configureIDE(targetDir, ideKey, selectedMCPs, options = {}) {
|
|
|
256
361
|
|
|
257
362
|
if (ideKey === 'claude-code') {
|
|
258
363
|
// Thin router
|
|
259
|
-
const routerContent = `# /chati —
|
|
260
|
-
|
|
261
|
-
## CRITICAL — Language Override
|
|
364
|
+
const routerContent = `# /chati — Deterministic Router
|
|
262
365
|
|
|
263
|
-
|
|
264
|
-
ALL responses MUST be in this language. This overrides any global IDE language setting.
|
|
366
|
+
## STEP 0 — Run the deterministic pipeline router (MUST be first)
|
|
265
367
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
| \`es\` | Espanol |
|
|
271
|
-
| \`fr\` | Francais |
|
|
368
|
+
Run via Bash:
|
|
369
|
+
\`\`\`
|
|
370
|
+
node .chati.dev/orchestrator/chati-router.js
|
|
371
|
+
\`\`\`
|
|
272
372
|
|
|
273
|
-
|
|
373
|
+
Parse the JSON output. This single command already handled: license validation, integrity check, session state, pipeline routing, and team registration (if needed). You do NOT need to run any of these separately.
|
|
274
374
|
|
|
275
|
-
|
|
375
|
+
### If \`ok\` is false:
|
|
276
376
|
|
|
277
|
-
|
|
377
|
+
| \`error\` value | What to tell the user |
|
|
378
|
+
|---|---|
|
|
379
|
+
| \`license_invalid\` | Display \`license.reason\` as a friendly message. STOP. |
|
|
380
|
+
| \`license_guard_missing\` | "License enforcement is missing. Reinstall with: npx chati-dev init". STOP. |
|
|
381
|
+
| \`integrity_failed\` | "License enforcement was tampered with. Reinstall with: npx chati-dev init". STOP. |
|
|
382
|
+
| anything else | Display error. STOP. |
|
|
278
383
|
|
|
279
|
-
|
|
384
|
+
### If \`ok\` is true:
|
|
280
385
|
|
|
281
|
-
|
|
386
|
+
1. Use \`session.language\` for ALL responses (pt = Português, en = English, es = Español, fr = Français).
|
|
387
|
+
2. Read the full orchestrator at \`${orchestratorPath || '.chati.dev/orchestrator/chati.md'}\`.
|
|
388
|
+
3. The \`action\` field tells you what to do. Go to the matching "Action:" section in the orchestrator.
|
|
389
|
+
4. **If \`action\` is \`spawn_team\`**: the team is ALREADY registered (Step 1 already ran as code). The \`team\` object contains \`team_id\`, \`task_list_path\`, \`mailbox_path\`, \`members\`. Go directly to "Action: Spawn Team" Step 2 and use these values in the agent prompts. DO NOT run \`orchestrate spawn-team\` again — it already ran.
|
|
390
|
+
5. **If \`action\` is \`setup\`**: this is a new project. Follow "Action: Setup".
|
|
391
|
+
6. For any other action: follow the matching section in the orchestrator.
|
|
282
392
|
|
|
283
|
-
**
|
|
284
|
-
- \`.chati/session.yaml\` (session state — includes language)
|
|
285
|
-
- \`CLAUDE.local.md\` (runtime state — session lock, current agent)
|
|
286
|
-
- \`chati.dev/artifacts/handoffs/\` (latest handoff)
|
|
287
|
-
- \`chati.dev/config.yaml\` (version info)
|
|
393
|
+
**The \`pipeline\` object contains the full JSON from \`orchestrate next\`.** Use its fields (agent_file, spawn_command, handoff_status, etc.) as documented in the orchestrator.
|
|
288
394
|
|
|
289
395
|
**User input:** $ARGUMENTS
|
|
290
396
|
`;
|
|
291
397
|
writeFileSync(join(targetDir, '.claude', 'commands', 'chati.md'), routerContent, 'utf-8');
|
|
292
398
|
|
|
399
|
+
// .claude/settings.json — wires the 10 chati.dev hooks into Claude Code's
|
|
400
|
+
// hook system and applies a baseline permission policy. THIS IS THE FILE
|
|
401
|
+
// CLAUDE CODE READS — without it, every hook in chati.dev/hooks/ is dormant.
|
|
402
|
+
// (Pre-v4.2.2 bug: this file was never written, hooks never ran.)
|
|
403
|
+
writeClaudeSettingsWithMerge(join(targetDir, '.claude', 'settings.json'));
|
|
404
|
+
|
|
293
405
|
// MCP config
|
|
294
406
|
if (selectedMCPs.length > 0) {
|
|
295
407
|
const mcpConfig = generateClaudeMCPConfig(selectedMCPs);
|
|
@@ -307,10 +419,35 @@ Pass through all context: session state, handoffs, artifacts, and user input.
|
|
|
307
419
|
// Session lock override file (equivalent to CLAUDE.local.md)
|
|
308
420
|
writeFileSync(join(targetDir, 'AGENTS.override.md'), generateAgentsOverrideMd(), 'utf-8');
|
|
309
421
|
|
|
310
|
-
// Starlark execution policies (
|
|
422
|
+
// Starlark execution policies (Codex sandboxed rule engine)
|
|
311
423
|
createDir(join(targetDir, '.codex', 'rules'));
|
|
312
424
|
writeFileSync(join(targetDir, '.codex', 'rules', 'constitution-guard.rules'), generateCodexConstitutionGuardRules(), 'utf-8');
|
|
313
425
|
writeFileSync(join(targetDir, '.codex', 'rules', 'read-protection.rules'), generateCodexReadProtectionRules(), 'utf-8');
|
|
426
|
+
|
|
427
|
+
// .codex/hooks.json — license-guard wired to UserPromptSubmit (per-turn enforcement).
|
|
428
|
+
// Codex hooks.json is experimental (requires [features] codex_hooks = true in
|
|
429
|
+
// codex config). Without it, license enforcement only happens at slash command
|
|
430
|
+
// entry — a long-running terminal session would not be re-validated.
|
|
431
|
+
writeFileSync(
|
|
432
|
+
join(targetDir, '.codex', 'hooks.json'),
|
|
433
|
+
JSON.stringify({
|
|
434
|
+
$comment: 'chati.dev v4.2.2 — experimental Codex hooks. Requires [features] codex_hooks = true.',
|
|
435
|
+
hooks: {
|
|
436
|
+
UserPromptSubmit: [
|
|
437
|
+
{
|
|
438
|
+
matcher: '.*',
|
|
439
|
+
hooks: [
|
|
440
|
+
{
|
|
441
|
+
type: 'command',
|
|
442
|
+
command: 'node .chati.dev/hooks/license-guard.js',
|
|
443
|
+
},
|
|
444
|
+
],
|
|
445
|
+
},
|
|
446
|
+
],
|
|
447
|
+
},
|
|
448
|
+
}, null, 2) + '\n',
|
|
449
|
+
'utf-8'
|
|
450
|
+
);
|
|
314
451
|
} else if (ideKey === 'gemini-cli') {
|
|
315
452
|
// Gemini CLI: TOML command file (native format for /chati command)
|
|
316
453
|
writeFileSync(join(targetDir, '.gemini', 'commands', 'chati.toml'), generateGeminiRouter({ orchestratorPath }), 'utf-8');
|
|
@@ -357,32 +494,31 @@ function generateProviderInstructions(providerName) {
|
|
|
357
494
|
# This file configures ${providerName} to work with Chati.dev
|
|
358
495
|
|
|
359
496
|
## System Location
|
|
360
|
-
All system content is in the
|
|
497
|
+
All system content is in the \`.chati.dev/\` directory (hidden). User artifacts are in \`artifacts/\`.
|
|
361
498
|
|
|
362
499
|
## Session State
|
|
363
500
|
Runtime session state is in \`.chati/session.yaml\` (IDE-agnostic).
|
|
364
501
|
|
|
365
502
|
## Getting Started
|
|
366
|
-
The orchestrator is at
|
|
367
|
-
Read it to understand routing, session management, and agent activation.
|
|
503
|
+
The orchestrator is at \`.chati.dev/orchestrator/chati.md\`.
|
|
368
504
|
|
|
369
505
|
## Constitution
|
|
370
|
-
Governance rules are in
|
|
506
|
+
Governance rules are in \`.chati.dev/constitution.md\`.
|
|
371
507
|
|
|
372
508
|
## Pipeline
|
|
373
509
|
\`\`\`
|
|
374
510
|
DISCOVER: WU -> Brief
|
|
375
|
-
PLAN: Detail
|
|
376
|
-
BUILD: Dev -> QA-
|
|
511
|
+
PLAN: [Planning Team: Detail + Architect + UX + QA-Planning] -> Phases -> Tasks
|
|
512
|
+
BUILD: [Build Team: Dev + QA-Implementation] -> QA-Visual
|
|
377
513
|
DEPLOY: DevOps
|
|
378
514
|
\`\`\`
|
|
379
515
|
|
|
380
516
|
## Agents
|
|
381
|
-
- DISCOVER: chati.dev/agents/discover/
|
|
382
|
-
- PLAN: chati.dev/agents/plan/
|
|
383
|
-
- Quality: chati.dev/agents/quality/
|
|
384
|
-
- BUILD: chati.dev/agents/build/
|
|
385
|
-
- DEPLOY: chati.dev/agents/deploy/
|
|
517
|
+
- DISCOVER: .chati.dev/agents/discover/
|
|
518
|
+
- PLAN: .chati.dev/agents/plan/
|
|
519
|
+
- Quality: .chati.dev/agents/quality/
|
|
520
|
+
- BUILD: .chati.dev/agents/build/
|
|
521
|
+
- DEPLOY: .chati.dev/agents/deploy/
|
|
386
522
|
`;
|
|
387
523
|
}
|
|
388
524
|
|
|
@@ -390,36 +526,165 @@ DEPLOY: DevOps
|
|
|
390
526
|
* Append chati.dev runtime entries to .gitignore.
|
|
391
527
|
* Session lock files are runtime-only and should never be committed.
|
|
392
528
|
*/
|
|
393
|
-
function updateGitignore(targetDir, selectedIDEs) {
|
|
394
|
-
const
|
|
529
|
+
export function updateGitignore(targetDir, selectedIDEs) {
|
|
530
|
+
const START_MARKER = '# BEGIN Chati.dev — do not edit between markers';
|
|
531
|
+
const END_MARKER = '# END Chati.dev';
|
|
532
|
+
// Legacy marker from pre-4.3.0 installs — detected for migration
|
|
533
|
+
const LEGACY_MARKER = '# Chati.dev runtime files';
|
|
534
|
+
|
|
535
|
+
const block = [
|
|
536
|
+
START_MARKER,
|
|
537
|
+
'# Framework files — generated by chati-dev, not part of your project.',
|
|
538
|
+
'# Do NOT remove these entries. If a framework file is already in git history,',
|
|
539
|
+
'# run: git rm -r --cached <path> after upgrading.',
|
|
540
|
+
'',
|
|
541
|
+
'# Framework (all code, prompts, hooks, orchestrator, data)',
|
|
542
|
+
'.chati.dev/',
|
|
395
543
|
'',
|
|
396
|
-
'#
|
|
544
|
+
'# Runtime state (session, teams, timeline, transient memories)',
|
|
545
|
+
'.chati/session.yaml',
|
|
546
|
+
'.chati/teams/',
|
|
547
|
+
'.chati/timeline.json',
|
|
397
548
|
'.chati/memories/*/session/',
|
|
549
|
+
'# NOTE: durable/ and daily/ memories ARE committed — they contain project knowledge',
|
|
550
|
+
'',
|
|
551
|
+
'# Claude Code integration (framework portions only)',
|
|
552
|
+
'.claude/commands/chati.md',
|
|
553
|
+
'.claude/rules/chati/',
|
|
554
|
+
'.claude/settings.local.json',
|
|
555
|
+
'# NOTE: .claude/settings.json IS committed — it is shared team config (hooks, MCPs)',
|
|
556
|
+
'',
|
|
557
|
+
'# Session locks (runtime, per-machine)',
|
|
558
|
+
'CLAUDE.local.md',
|
|
559
|
+
'.gemini/session-lock.md',
|
|
560
|
+
'AGENTS.override.md',
|
|
398
561
|
];
|
|
399
562
|
|
|
400
|
-
if (selectedIDEs.includes('claude-code')) {
|
|
401
|
-
entries.push('CLAUDE.local.md');
|
|
402
|
-
}
|
|
403
563
|
if (selectedIDEs.includes('gemini-cli')) {
|
|
404
|
-
|
|
564
|
+
block.push(
|
|
565
|
+
'',
|
|
566
|
+
'# Gemini provider framework files',
|
|
567
|
+
'.gemini/commands/',
|
|
568
|
+
'.gemini/context/',
|
|
569
|
+
'.gemini/hooks/',
|
|
570
|
+
'.gemini/settings.json',
|
|
571
|
+
);
|
|
405
572
|
}
|
|
573
|
+
|
|
406
574
|
if (selectedIDEs.includes('codex-cli')) {
|
|
407
|
-
|
|
575
|
+
block.push(
|
|
576
|
+
'',
|
|
577
|
+
'# Codex provider framework files',
|
|
578
|
+
'.codex/',
|
|
579
|
+
'.agents/skills/chati/',
|
|
580
|
+
);
|
|
408
581
|
}
|
|
409
582
|
|
|
410
|
-
|
|
583
|
+
block.push('', END_MARKER);
|
|
411
584
|
|
|
585
|
+
const newBlock = block.join('\n');
|
|
412
586
|
const gitignorePath = join(targetDir, '.gitignore');
|
|
413
|
-
const marker = '# Chati.dev runtime files';
|
|
414
587
|
|
|
415
|
-
if (existsSync(gitignorePath)) {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
588
|
+
if (!existsSync(gitignorePath)) {
|
|
589
|
+
writeFileSync(gitignorePath, newBlock + '\n', 'utf-8');
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
let existing = readFileSync(gitignorePath, 'utf-8');
|
|
594
|
+
|
|
595
|
+
// Strip new-style block if present (idempotency)
|
|
596
|
+
const newStartIdx = existing.indexOf(START_MARKER);
|
|
597
|
+
const newEndIdx = existing.indexOf(END_MARKER);
|
|
598
|
+
if (newStartIdx !== -1 && newEndIdx !== -1) {
|
|
599
|
+
existing = existing.slice(0, newStartIdx) + existing.slice(newEndIdx + END_MARKER.length);
|
|
600
|
+
existing = existing.replace(/\n{3,}/g, '\n\n').trimEnd();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Strip legacy block if present (pre-4.3.0)
|
|
604
|
+
if (existing.includes(LEGACY_MARKER)) {
|
|
605
|
+
const lines = existing.split('\n');
|
|
606
|
+
const legacyIdx = lines.findIndex(l => l.includes(LEGACY_MARKER));
|
|
607
|
+
if (legacyIdx !== -1) {
|
|
608
|
+
// Remove legacy marker line + the short known-legacy entries that followed
|
|
609
|
+
const legacyEntries = new Set([
|
|
610
|
+
'.chati/memories/*/session/',
|
|
611
|
+
'CLAUDE.local.md',
|
|
612
|
+
'.gemini/session-lock.md',
|
|
613
|
+
'AGENTS.override.md',
|
|
614
|
+
'',
|
|
615
|
+
]);
|
|
616
|
+
let endIdx = legacyIdx + 1;
|
|
617
|
+
while (endIdx < lines.length && legacyEntries.has(lines[endIdx].trim())) endIdx++;
|
|
618
|
+
lines.splice(legacyIdx, endIdx - legacyIdx);
|
|
619
|
+
existing = lines.join('\n').trimEnd();
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
writeFileSync(gitignorePath, existing.trimEnd() + '\n\n' + newBlock + '\n', 'utf-8');
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Section-aware merge for CLAUDE.md: preserves user-authored sections and
|
|
628
|
+
* framework-managed sections (those with markers). Framework sections are
|
|
629
|
+
* re-rendered from the fresh template; everything else is kept as-is.
|
|
630
|
+
*/
|
|
631
|
+
function mergeClaudeMd(existing, fresh) {
|
|
632
|
+
// Framework-managed sections — these come from the install template AND from
|
|
633
|
+
// magic-docs runtime updates. Replace these from fresh template; preserve all others.
|
|
634
|
+
const frameworkSections = [
|
|
635
|
+
// Install template (templates.js generateClaudeMd)
|
|
636
|
+
'## Project Context',
|
|
637
|
+
'## Chati.dev',
|
|
638
|
+
// Magic-docs runtime sections (when project starts)
|
|
639
|
+
'## Current State',
|
|
640
|
+
'## Session Lock',
|
|
641
|
+
'## Recent Decisions',
|
|
642
|
+
'## Framework',
|
|
643
|
+
'## How to Resume',
|
|
644
|
+
];
|
|
645
|
+
|
|
646
|
+
// If existing has no framework sections at all, user has a custom CLAUDE.md;
|
|
647
|
+
// just append the framework template separator + fresh template below.
|
|
648
|
+
const hasAnyFwSection = frameworkSections.some(h => existing.includes(h));
|
|
649
|
+
if (!hasAnyFwSection) {
|
|
650
|
+
return existing.trimEnd() + '\n\n<!-- Chati.dev framework context (auto-managed) -->\n\n' + fresh;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Otherwise, do section-level replacement — for each framework section in fresh,
|
|
654
|
+
// replace the corresponding section in existing (or append if missing).
|
|
655
|
+
let merged = existing;
|
|
656
|
+
for (const header of frameworkSections) {
|
|
657
|
+
const freshSection = extractSection(fresh, header);
|
|
658
|
+
if (!freshSection) continue;
|
|
659
|
+
if (merged.includes(header)) {
|
|
660
|
+
merged = replaceSection(merged, header, freshSection);
|
|
661
|
+
} else {
|
|
662
|
+
merged = merged.trimEnd() + '\n\n' + freshSection;
|
|
663
|
+
}
|
|
422
664
|
}
|
|
665
|
+
return merged;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function extractSection(content, header) {
|
|
669
|
+
const lines = content.split('\n');
|
|
670
|
+
const startIdx = lines.findIndex(l => l.trim() === header);
|
|
671
|
+
if (startIdx === -1) return null;
|
|
672
|
+
let endIdx = startIdx + 1;
|
|
673
|
+
while (endIdx < lines.length && !lines[endIdx].match(/^##\s/)) endIdx++;
|
|
674
|
+
return lines.slice(startIdx, endIdx).join('\n');
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function replaceSection(content, header, newSection) {
|
|
678
|
+
const lines = content.split('\n');
|
|
679
|
+
const startIdx = lines.findIndex(l => l.trim() === header);
|
|
680
|
+
if (startIdx === -1) return content;
|
|
681
|
+
let endIdx = startIdx + 1;
|
|
682
|
+
while (endIdx < lines.length && !lines[endIdx].match(/^##\s/)) endIdx++;
|
|
683
|
+
return [
|
|
684
|
+
...lines.slice(0, startIdx),
|
|
685
|
+
newSection,
|
|
686
|
+
...lines.slice(endIdx),
|
|
687
|
+
].join('\n');
|
|
423
688
|
}
|
|
424
689
|
|
|
425
690
|
/**
|
|
@@ -430,3 +695,214 @@ function createDir(dir) {
|
|
|
430
695
|
mkdirSync(dir, { recursive: true });
|
|
431
696
|
}
|
|
432
697
|
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Recursively copy CLI source (excluding tests, node_modules, hidden files).
|
|
701
|
+
* Used to bundle the CLI source into the user's .chati.dev/_cli/ folder so
|
|
702
|
+
* the framework runs self-contained (no `npx chati-dev` dependency).
|
|
703
|
+
*/
|
|
704
|
+
function copyCliSource(srcDir, destDir) {
|
|
705
|
+
if (!existsSync(srcDir)) return;
|
|
706
|
+
createDir(destDir);
|
|
707
|
+
const entries = readdirSync(srcDir);
|
|
708
|
+
for (const entry of entries) {
|
|
709
|
+
if (entry.startsWith('.')) continue;
|
|
710
|
+
if (entry === 'node_modules' || entry === 'test' || entry === 'tests') continue;
|
|
711
|
+
const srcPath = join(srcDir, entry);
|
|
712
|
+
const destPath = join(destDir, entry);
|
|
713
|
+
let stat;
|
|
714
|
+
try { stat = statSync(srcPath); } catch { continue; }
|
|
715
|
+
if (stat.isDirectory()) {
|
|
716
|
+
copyCliSource(srcPath, destPath);
|
|
717
|
+
} else {
|
|
718
|
+
copyFileSync(srcPath, destPath);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Recursively copy a directory verbatim (used for bundling node_modules trees).
|
|
725
|
+
*/
|
|
726
|
+
function copyDirVerbatim(srcDir, destDir) {
|
|
727
|
+
if (!existsSync(srcDir)) return;
|
|
728
|
+
createDir(destDir);
|
|
729
|
+
for (const entry of readdirSync(srcDir)) {
|
|
730
|
+
const srcPath = join(srcDir, entry);
|
|
731
|
+
const destPath = join(destDir, entry);
|
|
732
|
+
let stat;
|
|
733
|
+
try { stat = statSync(srcPath); } catch { continue; }
|
|
734
|
+
if (stat.isDirectory()) {
|
|
735
|
+
copyDirVerbatim(srcPath, destPath);
|
|
736
|
+
} else {
|
|
737
|
+
try { copyFileSync(srcPath, destPath); } catch { /* skip unreadable */ }
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Bundle chati-dev's production dependencies into .chati.dev/_cli/node_modules/
|
|
744
|
+
* so the bundled CLI can import js-yaml, chalk, etc. without relying on a
|
|
745
|
+
* project-level node_modules tree.
|
|
746
|
+
*
|
|
747
|
+
* Uses createRequire to resolve each dep from the installed chati-dev package,
|
|
748
|
+
* which works whether the package is hoisted or nested in the user's
|
|
749
|
+
* node_modules (npx cache, npm install, etc.).
|
|
750
|
+
*/
|
|
751
|
+
function copyCliDependencies(pkgDir, destNodeModules) {
|
|
752
|
+
let pkg;
|
|
753
|
+
try {
|
|
754
|
+
pkg = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf-8'));
|
|
755
|
+
} catch { return; }
|
|
756
|
+
const deps = [
|
|
757
|
+
...Object.keys(pkg.dependencies || {}),
|
|
758
|
+
...Object.keys(pkg.optionalDependencies || {}),
|
|
759
|
+
];
|
|
760
|
+
if (deps.length === 0) return;
|
|
761
|
+
|
|
762
|
+
// Resolve each dep from the package.json location (works for hoisted + nested layouts).
|
|
763
|
+
// eslint-disable-next-line no-undef
|
|
764
|
+
const require = createRequire(join(pkgDir, 'package.json'));
|
|
765
|
+
createDir(destNodeModules);
|
|
766
|
+
|
|
767
|
+
const visited = new Set();
|
|
768
|
+
const queue = [...deps];
|
|
769
|
+
|
|
770
|
+
while (queue.length > 0) {
|
|
771
|
+
const depName = queue.shift();
|
|
772
|
+
if (visited.has(depName)) continue;
|
|
773
|
+
visited.add(depName);
|
|
774
|
+
|
|
775
|
+
let depDir;
|
|
776
|
+
try {
|
|
777
|
+
// Try direct package.json resolve first (fast path)
|
|
778
|
+
depDir = dirname(require.resolve(`${depName}/package.json`));
|
|
779
|
+
} catch {
|
|
780
|
+
// Fallback: resolve the main entry then walk up to find package root.
|
|
781
|
+
// Needed for packages with strict "exports" (chalk, ora, etc.).
|
|
782
|
+
try {
|
|
783
|
+
let p = require.resolve(depName);
|
|
784
|
+
while (p && p !== dirname(p)) {
|
|
785
|
+
p = dirname(p);
|
|
786
|
+
if (existsSync(join(p, 'package.json')) && basename(p) === depName.split('/').pop()) {
|
|
787
|
+
depDir = p;
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
} catch { continue; }
|
|
792
|
+
if (!depDir) continue;
|
|
793
|
+
}
|
|
794
|
+
const depPkgJsonPath = join(depDir, 'package.json');
|
|
795
|
+
copyDirVerbatim(depDir, join(destNodeModules, depName));
|
|
796
|
+
|
|
797
|
+
// Include transitive deps
|
|
798
|
+
try {
|
|
799
|
+
const depPkg = JSON.parse(readFileSync(depPkgJsonPath, 'utf-8'));
|
|
800
|
+
for (const t of Object.keys(depPkg.dependencies || {})) {
|
|
801
|
+
if (!visited.has(t)) queue.push(t);
|
|
802
|
+
}
|
|
803
|
+
} catch { /* continue */ }
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Write .claude/settings.json with safe merge against existing user content.
|
|
809
|
+
*
|
|
810
|
+
* If the file does not exist: write fresh chati settings.
|
|
811
|
+
* If the file exists and is valid JSON: merge — chati hooks/permissions are
|
|
812
|
+
* UNIONED into existing values rather than replacing them. User customizations
|
|
813
|
+
* (env, statusLine, plugins, language, alwaysThinkingEnabled, etc.) are preserved.
|
|
814
|
+
* If the file exists but is malformed: leave it alone, log a warning. We will
|
|
815
|
+
* not silently destroy user data.
|
|
816
|
+
*/
|
|
817
|
+
function writeClaudeSettingsWithMerge(settingsPath) {
|
|
818
|
+
createDir(dirname(settingsPath));
|
|
819
|
+
const fresh = JSON.parse(generateClaudeSettings());
|
|
820
|
+
|
|
821
|
+
if (!existsSync(settingsPath)) {
|
|
822
|
+
writeFileSync(settingsPath, JSON.stringify(fresh, null, 2) + '\n', 'utf-8');
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
let existing;
|
|
827
|
+
try {
|
|
828
|
+
existing = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
829
|
+
} catch (err) {
|
|
830
|
+
process.stderr.write(`[chati] WARNING: .claude/settings.json exists but is not valid JSON (${err.message}). Skipping merge — chati hooks will NOT be wired. Fix the file and re-run install.\n`);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const merged = mergeClaudeSettings(existing, fresh);
|
|
835
|
+
writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Merge two Claude Code settings.json objects.
|
|
840
|
+
*
|
|
841
|
+
* - permissions.allow / permissions.deny: array union (de-duplicated)
|
|
842
|
+
* - permissions.defaultMode: keep existing if set, else use fresh
|
|
843
|
+
* - hooks: per-event union — chati hook commands are appended to existing
|
|
844
|
+
* matcher groups; user's hooks for the same events are preserved
|
|
845
|
+
* - $schema: prefer fresh (always our canonical URL)
|
|
846
|
+
* - all other top-level keys (env, statusLine, plugins, language, etc.):
|
|
847
|
+
* keep existing untouched
|
|
848
|
+
*/
|
|
849
|
+
export function mergeClaudeSettings(existing, fresh) {
|
|
850
|
+
const merged = { ...existing };
|
|
851
|
+
|
|
852
|
+
// $schema — chati's canonical URL wins
|
|
853
|
+
if (fresh.$schema) merged.$schema = fresh.$schema;
|
|
854
|
+
|
|
855
|
+
// permissions
|
|
856
|
+
const existingPerms = existing.permissions || {};
|
|
857
|
+
const freshPerms = fresh.permissions || {};
|
|
858
|
+
merged.permissions = {
|
|
859
|
+
...existingPerms,
|
|
860
|
+
allow: unionArrays(existingPerms.allow, freshPerms.allow),
|
|
861
|
+
deny: unionArrays(existingPerms.deny, freshPerms.deny),
|
|
862
|
+
defaultMode: existingPerms.defaultMode || freshPerms.defaultMode || 'default',
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
// hooks — per-event merge
|
|
866
|
+
const existingHooks = existing.hooks || {};
|
|
867
|
+
const freshHooks = fresh.hooks || {};
|
|
868
|
+
merged.hooks = { ...existingHooks };
|
|
869
|
+
|
|
870
|
+
for (const [eventName, freshGroups] of Object.entries(freshHooks)) {
|
|
871
|
+
const existingGroups = existingHooks[eventName] || [];
|
|
872
|
+
// Append fresh groups; we don't dedup at the group level (different
|
|
873
|
+
// matchers are different groups). Within a single fresh group, the
|
|
874
|
+
// hook commands are unique to chati so duplication risk is minimal.
|
|
875
|
+
// To be safe, dedup by command string across the merged event.
|
|
876
|
+
const seen = new Set();
|
|
877
|
+
const collected = [];
|
|
878
|
+
for (const group of [...existingGroups, ...freshGroups]) {
|
|
879
|
+
const dedupedHooks = (group.hooks || []).filter(h => {
|
|
880
|
+
const key = `${group.matcher || ''}|${h.command}`;
|
|
881
|
+
if (seen.has(key)) return false;
|
|
882
|
+
seen.add(key);
|
|
883
|
+
return true;
|
|
884
|
+
});
|
|
885
|
+
if (dedupedHooks.length > 0) {
|
|
886
|
+
collected.push({
|
|
887
|
+
...(group.matcher !== undefined ? { matcher: group.matcher } : {}),
|
|
888
|
+
hooks: dedupedHooks,
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
merged.hooks[eventName] = collected;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
return merged;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function unionArrays(a, b) {
|
|
899
|
+
const out = [];
|
|
900
|
+
const seen = new Set();
|
|
901
|
+
for (const item of [...(a || []), ...(b || [])]) {
|
|
902
|
+
if (!seen.has(item)) {
|
|
903
|
+
seen.add(item);
|
|
904
|
+
out.push(item);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return out;
|
|
908
|
+
}
|