kortext 2.2.3 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +175 -65
- package/agents/architect.md +85 -0
- package/agents/compliance-expert.md +36 -75
- package/agents/copywriter.md +36 -92
- package/agents/db-admin.md +43 -91
- package/agents/designer.md +40 -77
- package/agents/devops-engineer.md +37 -113
- package/agents/growth-expert.md +45 -88
- package/agents/product-manager.md +38 -83
- package/agents/qa-engineer.md +51 -99
- package/agents/security-engineer.md +47 -92
- package/bin/kortext.js +2 -0
- package/dist/app.js +829 -0
- package/dist/cli-spawn.js +137 -0
- package/dist/daemon.js +40 -0
- package/dist/db.js +78 -0
- package/dist/design-preview.js +533 -0
- package/dist/docs.js +701 -0
- package/dist/engines.js +225 -0
- package/dist/index.js +109 -0
- package/dist/pick-directory.js +38 -0
- package/dist/projects.js +189 -0
- package/dist/readiness.js +288 -0
- package/dist/runner.js +771 -0
- package/dist/update.js +55 -0
- package/docs/CHANGELOG.md +11 -0
- package/package.json +53 -13
- package/templates/AGENTS.md +62 -0
- package/templates/docs/API.md +67 -0
- package/templates/docs/ARCHITECTURE.md +31 -0
- package/templates/docs/BRIEF.md +27 -0
- package/templates/docs/CONTENT.md +66 -0
- package/templates/docs/DATABASE.md +63 -0
- package/templates/docs/DESIGN.md +124 -0
- package/templates/docs/ENGINEERING.md +66 -0
- package/templates/docs/ENVIRONMENT.md +102 -0
- package/templates/docs/GROWTH.md +44 -0
- package/templates/docs/LEGAL.md +48 -0
- package/templates/docs/PRODUCT.md +43 -0
- package/templates/docs/SECURITY.md +65 -0
- package/templates/docs/STACK.md +57 -0
- package/templates/docs/STRUCTURE.md +73 -0
- package/templates/docs/TEST.md +62 -0
- package/ui/dist/assets/abnfDiagram-VCTEODGH-Bm7Q2BML.js +1 -0
- package/ui/dist/assets/arc-BV9hs0Hu.js +1 -0
- package/ui/dist/assets/architecture-7GRP2DOG-RckR3gBI.js +1 -0
- package/ui/dist/assets/architectureDiagram-5GKGNRK7-CHiJ6JFT.js +36 -0
- package/ui/dist/assets/array-BifhSqXX.js +1 -0
- package/ui/dist/assets/blockDiagram-I7D4REHJ-qCbaKVhd.js +129 -0
- package/ui/dist/assets/c4Diagram-7LVT6UL2-DidCDJrv.js +38 -0
- package/ui/dist/assets/channel-CNUQ11ne.js +1 -0
- package/ui/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/ui/dist/assets/chunk-4HAMMTFA-CDGFjfVp.js +62 -0
- package/ui/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/ui/dist/assets/chunk-75Z2AOVW-CbNT4sDh.js +2 -0
- package/ui/dist/assets/chunk-DU6HZSFF-Cq2BVv26.js +127 -0
- package/ui/dist/assets/chunk-F27PBJKO-CE365m0v.js +1 -0
- package/ui/dist/assets/chunk-FOHPRMQF-DHwB1DNv.js +161 -0
- package/ui/dist/assets/chunk-GMAD6QVW-DxWL1VC-.js +72 -0
- package/ui/dist/assets/chunk-GVQU2GXP-aARV8YbK.js +1 -0
- package/ui/dist/assets/chunk-IMKFNOWR-CC790Kt2.js +231 -0
- package/ui/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/ui/dist/assets/chunk-L3NEJ4N5-BfE4VE8J.js +1 -0
- package/ui/dist/assets/chunk-OSK3NFVY-BImQbSWH.js +10 -0
- package/ui/dist/assets/chunk-P2QGCYS3-CN3XD313.js +1 -0
- package/ui/dist/assets/chunk-POPQ4Y6H-Dqb6jOrX.js +1 -0
- package/ui/dist/assets/chunk-PWAF6VOD-DfXX6rBk.js +1 -0
- package/ui/dist/assets/chunk-SHT3W25Y-DATyyqPK.js +168 -0
- package/ui/dist/assets/chunk-SVP7TREG-r1chwRmg.js +88 -0
- package/ui/dist/assets/chunk-TICWLB2K-DL4atHUC.js +206 -0
- package/ui/dist/assets/chunk-XXDRQBXY-DXrM6jh7.js +1 -0
- package/ui/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/ui/dist/assets/classDiagram-ZZMXUADV-BpQxfIQ7.js +1 -0
- package/ui/dist/assets/classDiagram-v2-VYDZK3BY-BpQxfIQ7.js +1 -0
- package/ui/dist/assets/cose-bilkent-JH36ORCC-Bhe-ZBlS.js +1 -0
- package/ui/dist/assets/cynefin-OW5HDTMX-D1koLd-G.js +1 -0
- package/ui/dist/assets/cynefinDiagram-5FMLGOSQ-BMcw40hV.js +62 -0
- package/ui/dist/assets/cytoscape.esm-CGd-uY3x.js +321 -0
- package/ui/dist/assets/dagre-GXQ25YYZ-DBun-Rs2.js +4 -0
- package/ui/dist/assets/dagre-PrKaheQc.js +1 -0
- package/ui/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/ui/dist/assets/diagram-S7CK7UJ4-C1fUIGvo.js +30 -0
- package/ui/dist/assets/diagram-UQ7AKVKN-B5FDfkvB.js +41 -0
- package/ui/dist/assets/diagram-VSXAHHWV-CCIpOCNC.js +3 -0
- package/ui/dist/assets/diagram-VX7I27RA-DV0q2xo_.js +24 -0
- package/ui/dist/assets/diagram-Z3DM3KII-B3LCdNgI.js +24 -0
- package/ui/dist/assets/dist-EcW76Zvr.js +1 -0
- package/ui/dist/assets/ebnfDiagram-PWID7BFC-De_oRbYe.js +1 -0
- package/ui/dist/assets/erDiagram-RLTQ6QDP-B9NikMr1.js +99 -0
- package/ui/dist/assets/eventmodeling-NTZA5JFV-DZ4t581k.js +1 -0
- package/ui/dist/assets/flowDiagram-HODETNUW-DuTOJp1C.js +1 -0
- package/ui/dist/assets/ganttDiagram-EL5Y4UJY-BVTqqlPw.js +292 -0
- package/ui/dist/assets/gitGraph-4MIJSDKK-xRFaEJdZ.js +1 -0
- package/ui/dist/assets/gitGraphDiagram-WWUBYQGX-CK5_GCmX.js +106 -0
- package/ui/dist/assets/graphlib-DS17s2tU.js +1 -0
- package/ui/dist/assets/index-CjkpNftW.js +46 -0
- package/ui/dist/assets/index-DVIDTWin.css +1 -0
- package/ui/dist/assets/info-A6RAGUB7-Dg7z-CTI.js +1 -0
- package/ui/dist/assets/infoDiagram-27XIBGKW-YaqsOXh0.js +2 -0
- package/ui/dist/assets/init-C-OQMol4.js +1 -0
- package/ui/dist/assets/ishikawaDiagram-5VMMS53U-C-wtxZPa.js +70 -0
- package/ui/dist/assets/journeyDiagram-3NMN7TZE-DsXSXf16.js +139 -0
- package/ui/dist/assets/kanban-definition-UXKFOSKX-Ct59fuWk.js +89 -0
- package/ui/dist/assets/katex-CXMH3UgJ.js +257 -0
- package/ui/dist/assets/line-BUVw9Mhs.js +1 -0
- package/ui/dist/assets/linear-D8ncLO0X.js +1 -0
- package/ui/dist/assets/mermaid-parser.core-BDC4bG-O.js +7 -0
- package/ui/dist/assets/mermaid.core-DZY3WwzX.js +44 -0
- package/ui/dist/assets/mindmap-definition-YA3MSWOX-DtiuF6Ce.js +96 -0
- package/ui/dist/assets/ordinal-BDEzSJ7C.js +1 -0
- package/ui/dist/assets/packet-AYTQ26CC-BX2-w9rD.js +1 -0
- package/ui/dist/assets/path-fybaL0A-.js +1 -0
- package/ui/dist/assets/pegDiagram-XKGWAZYB-CotwuIHB.js +1 -0
- package/ui/dist/assets/pie-WAS4IAKB-Hth8ZVpm.js +1 -0
- package/ui/dist/assets/pieDiagram-E7YTZNPT-D9-INs8B.js +39 -0
- package/ui/dist/assets/quadrantDiagram-AXDQQJYC-CMtBXZGV.js +7 -0
- package/ui/dist/assets/radar-RG4KPBEZ-6UuwQPsw.js +1 -0
- package/ui/dist/assets/railroad-74A4TZTK-BF5TZgj0.js +1 -0
- package/ui/dist/assets/railroad-abnf-HS5TGJTU-3KYovtYT.js +1 -0
- package/ui/dist/assets/railroad-ebnf-LZEXJU2U-DeP4WedE.js +1 -0
- package/ui/dist/assets/railroad-peg-WCYAUIDC-i42N1-yk.js +1 -0
- package/ui/dist/assets/railroadDiagram-O6MQD6OU-BS6zkU3Z.js +1 -0
- package/ui/dist/assets/requirementDiagram-BXWQKSXE-J4IOB-kP.js +84 -0
- package/ui/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/ui/dist/assets/sankeyDiagram-P5KCCOFB-jYTCuYzQ.js +40 -0
- package/ui/dist/assets/sequenceDiagram-WJ2MYXX4-Cd2r6PW-.js +162 -0
- package/ui/dist/assets/sizeCapture-INFHLROL-B0uUizjq.js +1 -0
- package/ui/dist/assets/src-7TsfZXRO.js +1 -0
- package/ui/dist/assets/stateDiagram-D77RDMKH-DXOdtOKe.js +1 -0
- package/ui/dist/assets/stateDiagram-v2-MP3YSRHH-9h7sIf3R.js +1 -0
- package/ui/dist/assets/swimlanes-42K2YHIH-BvJGmGs1.js +1 -0
- package/ui/dist/assets/swimlanesDiagram-VR7AAH4N-QA4gPi6a.js +8 -0
- package/ui/dist/assets/timeline-definition-24CTP7MA-CAaD6-yd.js +120 -0
- package/ui/dist/assets/treeView-Q6P3EWNA-4gDEXIyJ.js +1 -0
- package/ui/dist/assets/treemap-WGGIJYW6-CGel5TwW.js +1 -0
- package/ui/dist/assets/vennDiagram-4TSXK5OY-DpxsKboL.js +34 -0
- package/ui/dist/assets/wardley-WFR3VGLG-Ck38v5ns.js +1 -0
- package/ui/dist/assets/wardleyDiagram-VM6X3IG4-Ba0grrqs.js +78 -0
- package/ui/dist/assets/xychartDiagram-S5SC5T6Z-DH_gC96K.js +7 -0
- package/ui/dist/index.html +13 -0
- package/ui/dist/kortext-logo-dark.svg +41 -0
- package/ui/dist/kortext-logo.svg +41 -0
- package/workflows/existing-project-analysis.md +87 -0
- package/workflows/new-project-analysis.md +98 -0
- package/workflows/planning-pipeline.md +61 -0
- package/AGENTS.md +0 -28
- package/agents/backend-developer.md +0 -132
- package/agents/delivery-manager.md +0 -121
- package/agents/engineering-manager.md +0 -208
- package/agents/frontend-developer.md +0 -174
- package/agents/operation-manager.md +0 -122
- package/hooks/audit-logger.sh +0 -25
- package/hooks/auto-locker.sh +0 -74
- package/hooks/auto-unlocker.sh +0 -17
- package/hooks/backlog-sync-guard.sh +0 -12
- package/hooks/branch-guard.sh +0 -32
- package/hooks/commit-msg-guard.sh +0 -43
- package/hooks/git-pre-commit.sh +0 -76
- package/hooks/git-pre-push.sh +0 -17
- package/hooks/handover-guard.sh +0 -33
- package/hooks/kortext-init.sh +0 -194
- package/hooks/kortext-lib.sh +0 -151
- package/hooks/lint-guard.sh +0 -50
- package/hooks/secret-scanner.sh +0 -89
- package/hooks/size-guard.sh +0 -48
- package/hooks/snapshot-guard.sh +0 -39
- package/hooks/write-guard.sh +0 -77
- package/rules/behavior.md +0 -252
- package/rules/branching.md +0 -82
- package/rules/commands.md +0 -112
- package/rules/emergency.md +0 -160
- package/rules/mcp.md +0 -47
- package/rules/models.md +0 -93
- package/scripts/kortext-backlog-add.py +0 -123
- package/scripts/kortext-backlog-health.py +0 -112
- package/scripts/kortext-backlog-sync.py +0 -117
- package/scripts/kortext-bulk-plan.py +0 -63
- package/scripts/kortext-cli.py +0 -376
- package/scripts/kortext-consistency-check.py +0 -74
- package/scripts/kortext-context-check.py +0 -146
- package/scripts/kortext-handover.py +0 -107
- package/scripts/kortext-item-check.py +0 -61
- package/scripts/kortext-item-start.py +0 -70
- package/scripts/kortext-item-transition.py +0 -78
- package/scripts/kortext-lock.py +0 -174
- package/scripts/kortext-session-start.py +0 -127
- package/scripts/lock_kortext.sh +0 -34
- package/settings/.claude-settings.template.json +0 -48
- package/settings/CHANGELOG.md +0 -176
- package/settings/INTEGRATION-MAP.md +0 -276
- package/settings/README.md +0 -255
- package/settings/USER-GUIDE.md +0 -502
- package/settings/VERSION +0 -1
- package/settings/config.md +0 -8
- package/settings/runtime-adapters.md +0 -104
- package/skills/backend-developer/.gitkeep +0 -0
- package/skills/compliance-expert/.gitkeep +0 -0
- package/skills/copywriter/.gitkeep +0 -0
- package/skills/db-admin/.gitkeep +0 -0
- package/skills/delivery-manager/.gitkeep +0 -0
- package/skills/designer/.gitkeep +0 -0
- package/skills/devops-engineer/.gitkeep +0 -0
- package/skills/engineering-manager/.gitkeep +0 -0
- package/skills/frontend-developer/.gitkeep +0 -0
- package/skills/growth-expert/.gitkeep +0 -0
- package/skills/operation-manager/.gitkeep +0 -0
- package/skills/product-manager/.gitkeep +0 -0
- package/skills/qa-engineer/.gitkeep +0 -0
- package/skills/security-engineer/.gitkeep +0 -0
- package/workflows/00-kortext-setup.md +0 -53
- package/workflows/01a-analysis-pipeline.md +0 -117
- package/workflows/01b-onboarding-pipeline.md +0 -110
- package/workflows/02-planning-pipeline.md +0 -146
- package/workflows/02b-spike-workflow.md +0 -120
- package/workflows/03-environment-setup.md +0 -99
- package/workflows/04-development-cycle.md +0 -203
- package/workflows/05-test-cycle.md +0 -123
- package/workflows/06-deployment-cycle.md +0 -224
- package/workflows/07-rollback-pipeline.md +0 -203
- package/workflows/08-hotfix-pipeline.md +0 -196
- package/workflows/09-maintenance-cycle.md +0 -133
- package/workspace/archive/.gitkeep +0 -0
- package/workspace/backups/.gitkeep +0 -0
- package/workspace/memory/backlog/.gitkeep +0 -0
- package/workspace/memory/backlog/BXX-[bug-name].md +0 -2
- package/workspace/memory/backlog/DXX-[debt-name].md +0 -2
- package/workspace/memory/backlog/README.md +0 -31
- package/workspace/memory/backlog/TXX-[task-name].md +0 -2
- package/workspace/memory/backlog/debt-dashboard.md +0 -3
- package/workspace/memory/backlog/epic-dashboard.md +0 -10
- package/workspace/memory/backlog/version-dashboard.md +0 -7
- package/workspace/memory/context/.gitkeep +0 -0
- package/workspace/memory/context/README.md +0 -35
- package/workspace/memory/context/[agent-name]-active.md +0 -2
- package/workspace/memory/decisions.md +0 -17
- package/workspace/memory/handover.md +0 -40
- package/workspace/memory/learned.md +0 -21
- package/workspace/references/access.md +0 -47
- package/workspace/references/api-reference.md +0 -59
- package/workspace/references/blueprint.md +0 -29
- package/workspace/references/content-strategy.md +0 -39
- package/workspace/references/db-schema.md +0 -51
- package/workspace/references/design-system.md +0 -107
- package/workspace/references/dictionary.md +0 -42
- package/workspace/references/file-system.md +0 -42
- package/workspace/references/growth-strategy.md +0 -46
- package/workspace/references/legal-strategy.md +0 -35
- package/workspace/references/required-skills.md +0 -11
- package/workspace/references/security-rules.md +0 -34
- package/workspace/references/tech-stack.md +0 -49
- package/workspace/references/test-strategy.md +0 -48
- package/workspace/reports/analysis-reports.md +0 -40
- package/workspace/reports/content-reports.md +0 -29
- package/workspace/reports/delivery-reports.md +0 -26
- package/workspace/reports/growth-reports.md +0 -9
- package/workspace/reports/legal-reports.md +0 -29
- package/workspace/reports/product-requirements.md +0 -28
- package/workspace/reports/release-notes.md +0 -20
- package/workspace/reports/security-reports.md +0 -27
- package/workspace/reports/status-reports.md +0 -48
- package/workspace/reports/tech-requirements.md +0 -30
- package/workspace/reports/test-reports.md +0 -61
- package/workspace/templates/BXX-[bug-name].md +0 -34
- package/workspace/templates/DXX-[debt-name].md +0 -35
- package/workspace/templates/EXX-[epic-name].md +0 -48
- package/workspace/templates/HXX-[hotfix-name].md +0 -73
- package/workspace/templates/SXX-[spike-name].md +0 -51
- package/workspace/templates/TXX-[task-name].md +0 -34
- package/workspace/templates/[agent-name]-active.md +0 -4
- package/workspace/templates/[original-name]_[YYYY-MM-DD_HHMMSS].md +0 -5
- package/workspace/templates/adr-XXX.md +0 -66
- package/workspace/templates/debt-dashboard.md +0 -10
- package/workspace/templates/epic-dashboard.md +0 -10
- package/workspace/templates/version-dashboard.md +0 -10
package/dist/engines.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
// The first four ran a real project here. The rest are prepared from each
|
|
6
|
+
// CLI's own documentation (2026-09) so the panel offers them the day they are
|
|
7
|
+
// installed; the first run on a real machine settles the flags.
|
|
8
|
+
export const ENGINES = [
|
|
9
|
+
{
|
|
10
|
+
id: 'claude',
|
|
11
|
+
binary: 'claude',
|
|
12
|
+
// --print: headless (no REPL); skip-permissions: auto-approve tool use;
|
|
13
|
+
// stdin carries the step prompt.
|
|
14
|
+
args: ['--print', '--dangerously-skip-permissions'],
|
|
15
|
+
modelFlag: '--model',
|
|
16
|
+
// Aliases the CLI resolves to its latest of each tier.
|
|
17
|
+
models: ['fable', 'opus', 'sonnet', 'haiku'],
|
|
18
|
+
installHint: 'npm install -g @anthropic-ai/claude-code',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'codex',
|
|
22
|
+
binary: 'codex',
|
|
23
|
+
// exec = non-interactive; workspace-write so it can create the output
|
|
24
|
+
// file; skip-git-repo-check: project may not be a git repo (yet).
|
|
25
|
+
args: ['exec', '--sandbox', 'workspace-write', '--skip-git-repo-check'],
|
|
26
|
+
modelFlag: '-m',
|
|
27
|
+
models: ['gpt-6-astra', 'gpt-5.6-terra', 'gpt-5.6-luna', 'gpt-5.6-sol', 'gpt-5.4-mini'],
|
|
28
|
+
installHint: 'npm install -g @openai/codex',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'antigravity',
|
|
32
|
+
binary: 'agy',
|
|
33
|
+
// Antigravity's CLI. --print takes the prompt itself, so it rides as an
|
|
34
|
+
// argument; the default print timeout is 5 minutes, too short for a document.
|
|
35
|
+
args: ['--dangerously-skip-permissions', '--print-timeout', '30m'],
|
|
36
|
+
promptFlag: '--print',
|
|
37
|
+
// Without it the workspace is ~/.gemini/antigravity-cli and the agent
|
|
38
|
+
// hunts for the repository with find and ls.
|
|
39
|
+
cwdFlag: '--add-dir',
|
|
40
|
+
modelFlag: '--model',
|
|
41
|
+
// `agy models` lists more; these are the tiers.
|
|
42
|
+
models: [
|
|
43
|
+
'gemini-3.8-flash-high',
|
|
44
|
+
'gemini-3.1-pro-high',
|
|
45
|
+
'claude-opus-4-6-thinking',
|
|
46
|
+
'claude-sonnet-4-6',
|
|
47
|
+
'gpt-oss-120b-medium',
|
|
48
|
+
],
|
|
49
|
+
installHint: 'install Antigravity, then run: agy install',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 'gemini',
|
|
53
|
+
binary: 'gemini',
|
|
54
|
+
args: ['--yolo'],
|
|
55
|
+
modelFlag: '-m',
|
|
56
|
+
models: ['gemini-2.5-pro', 'gemini-2.5-flash'],
|
|
57
|
+
installHint: 'npm install -g @google/gemini-cli',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'cursor',
|
|
61
|
+
// Cursor's agent CLI installs as `agent`. -p prints instead of opening
|
|
62
|
+
// the TUI; --force lets it write files in that mode.
|
|
63
|
+
binary: 'agent',
|
|
64
|
+
args: ['--force'],
|
|
65
|
+
promptFlag: '-p',
|
|
66
|
+
modelFlag: '--model',
|
|
67
|
+
models: ['auto', 'composer-2', 'sonnet-4.6', 'opus-4.6', 'gpt-5.4'],
|
|
68
|
+
installHint: 'curl https://cursor.com/install -fsS | bash',
|
|
69
|
+
untested: true,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'copilot',
|
|
73
|
+
// -p runs one prompt and exits; -s drops the decoration; --yolo is
|
|
74
|
+
// --allow-all-tools plus paths and URLs; --no-ask-user stops it pausing
|
|
75
|
+
// for a human who is not there.
|
|
76
|
+
binary: 'copilot',
|
|
77
|
+
args: ['-s', '--yolo', '--no-ask-user'],
|
|
78
|
+
promptFlag: '-p',
|
|
79
|
+
modelFlag: '--model',
|
|
80
|
+
models: ['gpt-5.4', 'gpt-5.3-codex', 'claude-sonnet-4.6', 'claude-haiku-4.5'],
|
|
81
|
+
installHint: 'npm install -g @github/copilot',
|
|
82
|
+
untested: true,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 'opencode',
|
|
86
|
+
// `opencode run <message>`; --auto approves what is not explicitly denied.
|
|
87
|
+
binary: 'opencode',
|
|
88
|
+
args: ['run', '--auto'],
|
|
89
|
+
promptFlag: '',
|
|
90
|
+
modelFlag: '-m',
|
|
91
|
+
// provider/model.
|
|
92
|
+
models: ['anthropic/claude-sonnet-4-6', 'openai/gpt-5.4', 'google/gemini-3.1-pro'],
|
|
93
|
+
installHint: 'npm install -g opencode-ai',
|
|
94
|
+
untested: true,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: 'amp',
|
|
98
|
+
// -x reads the prompt from stdin and exits after one turn. No model flag:
|
|
99
|
+
// Amp picks its own.
|
|
100
|
+
binary: 'amp',
|
|
101
|
+
args: ['--dangerously-allow-all', '-x'],
|
|
102
|
+
models: [],
|
|
103
|
+
installHint: 'npm install -g @sourcegraph/amp',
|
|
104
|
+
untested: true,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: 'droid',
|
|
108
|
+
// `droid exec` reads stdin; --auto low is file edits without shell.
|
|
109
|
+
binary: 'droid',
|
|
110
|
+
args: ['exec', '--auto', 'low'],
|
|
111
|
+
modelFlag: '-m',
|
|
112
|
+
models: ['claude-sonnet-4-6', 'claude-opus-4-6', 'gpt-5.3-codex'],
|
|
113
|
+
installHint: 'curl -fsSL https://app.factory.ai/cli | sh',
|
|
114
|
+
untested: true,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: 'goose',
|
|
118
|
+
// `goose run -i -` reads stdin; approval and model come from the
|
|
119
|
+
// environment, not flags.
|
|
120
|
+
binary: 'goose',
|
|
121
|
+
args: ['run', '-i', '-'],
|
|
122
|
+
env: { GOOSE_MODE: 'auto' },
|
|
123
|
+
modelEnv: 'GOOSE_MODEL',
|
|
124
|
+
models: [],
|
|
125
|
+
installHint: 'brew install block-goose-cli',
|
|
126
|
+
untested: true,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: 'qwen',
|
|
130
|
+
// Qwen Code is a gemini-cli fork: same --yolo, same stdin prompt.
|
|
131
|
+
binary: 'qwen',
|
|
132
|
+
args: ['--yolo'],
|
|
133
|
+
modelFlag: '-m',
|
|
134
|
+
models: ['qwen3-coder-plus', 'qwen3-coder-flash'],
|
|
135
|
+
installHint: 'npm install -g @qwen-code/qwen-code',
|
|
136
|
+
untested: true,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: 'cline',
|
|
140
|
+
// The prompt is the last argument; -y skips approvals and exits when the
|
|
141
|
+
// turn ends; provider/model ids.
|
|
142
|
+
binary: 'cline',
|
|
143
|
+
args: ['-y'],
|
|
144
|
+
promptFlag: '',
|
|
145
|
+
modelFlag: '-m',
|
|
146
|
+
models: ['anthropic/claude-sonnet-4.6', 'anthropic/claude-opus-4-6', 'google/gemini-3-pro'],
|
|
147
|
+
installHint: 'npm install -g cline',
|
|
148
|
+
untested: true,
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
/** The CLI's arguments with the project's model, when one is set. */
|
|
152
|
+
export function engineArgs(engine, project) {
|
|
153
|
+
const model = (project.model ?? '').trim();
|
|
154
|
+
return [
|
|
155
|
+
...engine.args,
|
|
156
|
+
...(engine.cwdFlag && project.repo_path ? [engine.cwdFlag, project.repo_path] : []),
|
|
157
|
+
...(model && engine.modelFlag ? [engine.modelFlag, model] : []),
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
/** The CLI's environment: the server's, the spec's, and the model when it
|
|
161
|
+
* travels as a variable. */
|
|
162
|
+
export function engineEnv(engine, project) {
|
|
163
|
+
const model = (project.model ?? '').trim();
|
|
164
|
+
if (!engine.env && !(model && engine.modelEnv))
|
|
165
|
+
return undefined;
|
|
166
|
+
return {
|
|
167
|
+
...process.env,
|
|
168
|
+
...engine.env,
|
|
169
|
+
...(model && engine.modelEnv ? { [engine.modelEnv]: model } : {}),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Check PATH with which on POSIX and where on Windows.
|
|
174
|
+
* Windows support is experimental and has not been runtime-tested.
|
|
175
|
+
*/
|
|
176
|
+
/** Where the CLI is, or null: the bare name when PATH has it, else the one
|
|
177
|
+
* place a server started from an app rather than a shell tends to miss —
|
|
178
|
+
* ~/.local/bin, where agy lands. */
|
|
179
|
+
export function binaryPath(binary) {
|
|
180
|
+
const lookup = process.platform === 'win32' ? 'where' : 'which';
|
|
181
|
+
if (spawnSync(lookup, [binary], { stdio: 'ignore' }).status === 0)
|
|
182
|
+
return binary;
|
|
183
|
+
const local = join(homedir(), '.local', 'bin', binary);
|
|
184
|
+
return existsSync(local) ? local : null;
|
|
185
|
+
}
|
|
186
|
+
export function onPath(binary) {
|
|
187
|
+
return binaryPath(binary) !== null;
|
|
188
|
+
}
|
|
189
|
+
// Cache blocking PATH lookups briefly to avoid spawning a lookup on every panel poll.
|
|
190
|
+
const DETECT_TTL_MS = 5000;
|
|
191
|
+
let detected = null;
|
|
192
|
+
export function detectEngines() {
|
|
193
|
+
if (detected && Date.now() - detected.at < DETECT_TTL_MS)
|
|
194
|
+
return detected.engines;
|
|
195
|
+
const engines = ENGINES.map((e) => {
|
|
196
|
+
const path = binaryPath(e.binary);
|
|
197
|
+
return { ...e, binary: path ?? e.binary, available: path !== null };
|
|
198
|
+
});
|
|
199
|
+
detected = { at: Date.now(), engines };
|
|
200
|
+
return engines;
|
|
201
|
+
}
|
|
202
|
+
/** Forget the cache — the user just told us the installed set may have changed. */
|
|
203
|
+
export function forgetDetectedEngines() {
|
|
204
|
+
detected = null;
|
|
205
|
+
}
|
|
206
|
+
export function getSetting(db, key) {
|
|
207
|
+
const row = db.prepare('SELECT value FROM settings WHERE key = ?').get(key);
|
|
208
|
+
return row?.value ?? null;
|
|
209
|
+
}
|
|
210
|
+
export function setSetting(db, key, value) {
|
|
211
|
+
db.prepare('INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value').run(key, value);
|
|
212
|
+
}
|
|
213
|
+
/** Prefer the project CLI when installed; otherwise use the global selection or first available CLI. */
|
|
214
|
+
export function engineFor(db, project) {
|
|
215
|
+
const detected = detectEngines();
|
|
216
|
+
return detected.find((e) => e.id === project.engine && e.available) ?? selectedEngine(db);
|
|
217
|
+
}
|
|
218
|
+
// Selected engine: explicit setting if still installed, else first available.
|
|
219
|
+
export function selectedEngine(db) {
|
|
220
|
+
const detected = detectEngines();
|
|
221
|
+
const chosen = getSetting(db, 'engine');
|
|
222
|
+
return (detected.find((e) => e.id === chosen && e.available) ??
|
|
223
|
+
detected.find((e) => e.available) ??
|
|
224
|
+
null);
|
|
225
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { parseArgs } from 'node:util';
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import { defaultDbPath, openDb } from './db.js';
|
|
8
|
+
import { buildApp } from './app.js';
|
|
9
|
+
import { abortAllRuns } from './runner.js';
|
|
10
|
+
import { respawnDetached, serverUp, waitForServer } from './daemon.js';
|
|
11
|
+
const { values } = parseArgs({
|
|
12
|
+
options: {
|
|
13
|
+
port: { type: 'string' },
|
|
14
|
+
db: { type: 'string' },
|
|
15
|
+
'no-open': { type: 'boolean' },
|
|
16
|
+
'no-detach': { type: 'boolean' },
|
|
17
|
+
stop: { type: 'boolean' },
|
|
18
|
+
help: { type: 'boolean', short: 'h' },
|
|
19
|
+
version: { type: 'boolean', short: 'v' },
|
|
20
|
+
},
|
|
21
|
+
allowPositionals: true,
|
|
22
|
+
});
|
|
23
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
// package root works from both server/ (tsx dev) and dist/ (built)
|
|
25
|
+
const pkgRoot = join(here, '..');
|
|
26
|
+
if (values.version) {
|
|
27
|
+
const pkg = JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf8'));
|
|
28
|
+
console.log(pkg.version);
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
if (values.help) {
|
|
32
|
+
console.log(`kortext — project brain for AI-driven development
|
|
33
|
+
|
|
34
|
+
Usage: kortext [--port 3441] [--db ~/.kortext/kortext.db] [--no-open] [--no-detach]
|
|
35
|
+
kortext --stop
|
|
36
|
+
kortext --version
|
|
37
|
+
|
|
38
|
+
Starts the server in the background and opens the panel in your browser: the
|
|
39
|
+
terminal window can be closed, and the panel's ⏻ button stops the server.
|
|
40
|
+
--no-detach keeps it in this terminal, where Ctrl+C stops it.
|
|
41
|
+
--stop stops a server running in the background, like the panel's ⏻ button.
|
|
42
|
+
Data lives in a global SQLite database — one database, multiple projects.`);
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
const PORT = Number(values.port ?? process.env.PORT ?? 3441);
|
|
46
|
+
const DB_PATH = values.db ?? defaultDbPath();
|
|
47
|
+
// Use the panel shutdown endpoint so CLI and UI enforce the same busy check.
|
|
48
|
+
if (values.stop) {
|
|
49
|
+
const res = await fetch(`http://127.0.0.1:${PORT}/api/quit`, { method: 'POST' }).catch(() => null);
|
|
50
|
+
if (!res) {
|
|
51
|
+
console.log(`nothing is running on port ${PORT}`);
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
const { error } = (await res.json().catch(() => ({})));
|
|
56
|
+
console.error(error ?? `could not stop kortext (HTTP ${res.status})`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
console.log('kortext stopped');
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
// Detach with KORTEXT_CHILD set so closing the terminal leaves the server running.
|
|
63
|
+
// Development uses --no-detach to keep the watcher in the foreground.
|
|
64
|
+
if (!process.env.KORTEXT_CHILD && !values['no-detach']) {
|
|
65
|
+
const url = `http://localhost:${PORT}`;
|
|
66
|
+
const logPath = `${DB_PATH}.log`;
|
|
67
|
+
// Reuse an existing Kortext server on this port.
|
|
68
|
+
const already = await serverUp(PORT);
|
|
69
|
+
if (!already) {
|
|
70
|
+
// Only the parent opens the browser, after the server passes its health check.
|
|
71
|
+
respawnDetached(fileURLToPath(import.meta.url), [...process.argv.slice(2), '--no-open'], logPath);
|
|
72
|
+
}
|
|
73
|
+
if (!(already || (await waitForServer(PORT)))) {
|
|
74
|
+
console.error(`kortext did not start. See ${logPath}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
console.log(`${already ? 'kortext is already running' : 'kortext panel'}: ${url}`);
|
|
78
|
+
console.log('You can close this window. Stop it with the ⏻ button in the panel.');
|
|
79
|
+
if (!values['no-open'])
|
|
80
|
+
openBrowser(url);
|
|
81
|
+
process.exit(0);
|
|
82
|
+
}
|
|
83
|
+
const db = openDb(DB_PATH);
|
|
84
|
+
const app = buildApp(db, pkgRoot, DB_PATH);
|
|
85
|
+
// Bind only loopback addresses: the API exposes local paths and file mutations without authentication.
|
|
86
|
+
// Listen on IPv4 and, where available, IPv6 because localhost can resolve to either.
|
|
87
|
+
app.listen(PORT, '127.0.0.1', () => {
|
|
88
|
+
const url = `http://localhost:${PORT}`;
|
|
89
|
+
console.log(`kortext panel: ${url}`);
|
|
90
|
+
console.log(`db: ${DB_PATH}`);
|
|
91
|
+
if (!values['no-open'])
|
|
92
|
+
openBrowser(url);
|
|
93
|
+
});
|
|
94
|
+
app.listen(PORT, '::1').on('error', () => {
|
|
95
|
+
/* no IPv6 loopback on this machine; the IPv4 listener above is the panel */
|
|
96
|
+
});
|
|
97
|
+
// tsx watch, kortext --stop and a terminal Ctrl-C all arrive as a signal. Kill
|
|
98
|
+
// the CLIs first — SIGTERM, then SIGKILL a second later — or they outlive us.
|
|
99
|
+
for (const sig of ['SIGTERM', 'SIGINT']) {
|
|
100
|
+
process.on(sig, () => {
|
|
101
|
+
abortAllRuns();
|
|
102
|
+
setTimeout(() => process.exit(0), 1500).unref();
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function openBrowser(url) {
|
|
106
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
107
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
108
|
+
spawn(cmd, args, { stdio: 'ignore', detached: true }).unref();
|
|
109
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* Parse the output of the macOS folder chooser (osascript). Returns the chosen
|
|
4
|
+
* absolute POSIX path on success (trailing slash stripped), or null when the
|
|
5
|
+
* user cancelled (non-zero exit) or the output was empty.
|
|
6
|
+
*/
|
|
7
|
+
export function parsePickedPath(stdout, exitCode) {
|
|
8
|
+
if (exitCode !== 0)
|
|
9
|
+
return null;
|
|
10
|
+
const p = stdout.trim();
|
|
11
|
+
if (!p)
|
|
12
|
+
return null;
|
|
13
|
+
return p.replace(/\/+$/, '');
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Open a native folder picker and resolve to the chosen absolute path, or null
|
|
17
|
+
* (cancelled / unsupported platform). macOS-only for now via `osascript`; other
|
|
18
|
+
* platforms resolve to null so the UI falls back to a typed path. The spawn is
|
|
19
|
+
* injectable so the parsing logic stays unit-testable.
|
|
20
|
+
*/
|
|
21
|
+
export function pickDirectoryNative(platform = process.platform, spawnFn = spawn) {
|
|
22
|
+
if (platform !== 'darwin')
|
|
23
|
+
return Promise.resolve(null);
|
|
24
|
+
return new Promise((resolveResult) => {
|
|
25
|
+
const proc = spawnFn('osascript', [
|
|
26
|
+
'-e',
|
|
27
|
+
'POSIX path of (choose folder with prompt "Select the project folder for Kortext")',
|
|
28
|
+
]);
|
|
29
|
+
let out = '';
|
|
30
|
+
proc.stdout?.on('data', (d) => {
|
|
31
|
+
out += String(d);
|
|
32
|
+
});
|
|
33
|
+
proc.on('close', (code) => {
|
|
34
|
+
resolveResult(parsePickedPath(out, code ?? 1));
|
|
35
|
+
});
|
|
36
|
+
proc.on('error', () => resolveResult(null));
|
|
37
|
+
});
|
|
38
|
+
}
|
package/dist/projects.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
|
+
// Live workspace inside a registered repo:
|
|
4
|
+
// AGENTS.md (repo root — the agent's entry contract)
|
|
5
|
+
// .kortext/*.md (every document, one flat shelf, in the order they are written)
|
|
6
|
+
// Workflows and personas are NOT copied — kortext itself drives the engine
|
|
7
|
+
// with them during the analysis; after the handshake the docs are the contract.
|
|
8
|
+
export const BRIEF_REL = join('.kortext', 'BRIEF.md');
|
|
9
|
+
// Resolve existing ancestors too, so a new folder below a symlink has one identity.
|
|
10
|
+
function canonicalRepo(path) {
|
|
11
|
+
const absolute = resolve(path);
|
|
12
|
+
return existsSync(absolute)
|
|
13
|
+
? realpathSync.native(absolute)
|
|
14
|
+
: join(canonicalRepo(dirname(absolute)), basename(absolute));
|
|
15
|
+
}
|
|
16
|
+
export function scaffoldProject(repoPath, pkgRoot, opts = {}) {
|
|
17
|
+
const kx = join(repoPath, '.kortext');
|
|
18
|
+
mkdirSync(kx, { recursive: true });
|
|
19
|
+
const templates = join(pkgRoot, 'templates');
|
|
20
|
+
installContract(repoPath, templates);
|
|
21
|
+
// Initialize the brief separately so an empty template always starts as draft.
|
|
22
|
+
copyDirIfMissing(join(templates, 'docs'), kx, new Set(['BRIEF.md']));
|
|
23
|
+
if (opts.skipBrief)
|
|
24
|
+
return;
|
|
25
|
+
const brief = join(repoPath, BRIEF_REL);
|
|
26
|
+
if (!existsSync(brief)) {
|
|
27
|
+
const template = join(templates, 'docs', 'BRIEF.md');
|
|
28
|
+
if (existsSync(template)) {
|
|
29
|
+
copyFileSync(template, brief);
|
|
30
|
+
forceStatus(brief, 'draft');
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
writeFileSync(brief, '---\nstatus: draft\n---\n\n# Brief\n', 'utf8');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// The handover contract
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Keep the contract in a marked AGENTS.md block to preserve user content.
|
|
41
|
+
// Add a pointer to an existing CLAUDE.md rather than duplicating the contract.
|
|
42
|
+
const BLOCK_START = '<!-- kortext:start -->';
|
|
43
|
+
const BLOCK_END = '<!-- kortext:end -->';
|
|
44
|
+
const POINTER = '<!-- kortext --> Read AGENTS.md and the .kortext/ docs before any work.';
|
|
45
|
+
export function writeContractBlock(path, body) {
|
|
46
|
+
const block = `${BLOCK_START}\n${body.trim()}\n${BLOCK_END}`;
|
|
47
|
+
if (!existsSync(path)) {
|
|
48
|
+
writeFileSync(path, `${block}\n`, 'utf8');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const current = readFileSync(path, 'utf8');
|
|
52
|
+
// The panel re-scaffolds on every poll; an unchanged block is not rewritten.
|
|
53
|
+
if (current.includes(block))
|
|
54
|
+
return;
|
|
55
|
+
const start = current.indexOf(BLOCK_START);
|
|
56
|
+
const end = current.indexOf(BLOCK_END);
|
|
57
|
+
if (start !== -1 && end > start) {
|
|
58
|
+
writeFileSync(path, current.slice(0, start) + block + current.slice(end + BLOCK_END.length), 'utf8');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
writeFileSync(path, `${current.replace(/\s*$/, '')}\n\n${block}\n`, 'utf8');
|
|
62
|
+
}
|
|
63
|
+
/** Takes back only kortext's block. A file that held anything else stays. */
|
|
64
|
+
export function removeContractBlock(path) {
|
|
65
|
+
if (!existsSync(path))
|
|
66
|
+
return;
|
|
67
|
+
const current = readFileSync(path, 'utf8');
|
|
68
|
+
const start = current.indexOf(BLOCK_START);
|
|
69
|
+
const end = current.indexOf(BLOCK_END);
|
|
70
|
+
if (start === -1 || end < start)
|
|
71
|
+
return; // not ours — never written by kortext
|
|
72
|
+
const rest = (current.slice(0, start) + current.slice(end + BLOCK_END.length)).trim();
|
|
73
|
+
if (rest === '')
|
|
74
|
+
rmSync(path, { force: true });
|
|
75
|
+
else
|
|
76
|
+
writeFileSync(path, `${rest}\n`, 'utf8');
|
|
77
|
+
}
|
|
78
|
+
function writePointer(path) {
|
|
79
|
+
if (!existsSync(path))
|
|
80
|
+
return; // kortext never invents another tool's memory file
|
|
81
|
+
const current = readFileSync(path, 'utf8');
|
|
82
|
+
if (current.includes('<!-- kortext -->'))
|
|
83
|
+
return;
|
|
84
|
+
writeFileSync(path, `${current.replace(/\s*$/, '')}\n\n${POINTER}\n`, 'utf8');
|
|
85
|
+
}
|
|
86
|
+
function removePointer(path) {
|
|
87
|
+
if (!existsSync(path))
|
|
88
|
+
return;
|
|
89
|
+
const kept = readFileSync(path, 'utf8')
|
|
90
|
+
.split('\n')
|
|
91
|
+
.filter((l) => !l.includes('<!-- kortext -->'));
|
|
92
|
+
writeFileSync(path, `${kept.join('\n').replace(/\s*$/, '')}\n`, 'utf8');
|
|
93
|
+
}
|
|
94
|
+
function installContract(repoPath, templates) {
|
|
95
|
+
const template = join(templates, 'AGENTS.md');
|
|
96
|
+
if (!existsSync(template))
|
|
97
|
+
return;
|
|
98
|
+
writeContractBlock(join(repoPath, 'AGENTS.md'), readFileSync(template, 'utf8'));
|
|
99
|
+
writePointer(join(repoPath, 'CLAUDE.md'));
|
|
100
|
+
}
|
|
101
|
+
/** Remove the Kortext contract block and CLAUDE.md pointer, preserving user content. */
|
|
102
|
+
export function uninstallContract(repoPath) {
|
|
103
|
+
removeContractBlock(join(repoPath, 'AGENTS.md'));
|
|
104
|
+
removePointer(join(repoPath, 'CLAUDE.md'));
|
|
105
|
+
}
|
|
106
|
+
function copyIfMissing(from, to) {
|
|
107
|
+
if (existsSync(from) && !existsSync(to))
|
|
108
|
+
copyFileSync(from, to);
|
|
109
|
+
}
|
|
110
|
+
function copyDirIfMissing(fromDir, toDir, skip = new Set()) {
|
|
111
|
+
if (!existsSync(fromDir))
|
|
112
|
+
return;
|
|
113
|
+
for (const entry of readdirSync(fromDir, { withFileTypes: true })) {
|
|
114
|
+
if (entry.isFile() && !skip.has(entry.name)) {
|
|
115
|
+
copyIfMissing(join(fromDir, entry.name), join(toDir, entry.name));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// A freshly scaffolded brief always starts as draft, whatever the template says.
|
|
120
|
+
function forceStatus(path, status) {
|
|
121
|
+
const body = readFileSync(path, 'utf8');
|
|
122
|
+
if (/^status:/m.test(body)) {
|
|
123
|
+
writeFileSync(path, body.replace(/^status:.*$/m, `status: ${status}`), 'utf8');
|
|
124
|
+
}
|
|
125
|
+
else if (body.startsWith('---\n')) {
|
|
126
|
+
writeFileSync(path, body.replace('---\n', `---\nstatus: ${status}\n`), 'utf8');
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
writeFileSync(path, `---\nstatus: ${status}\n---\n\n${body}`, 'utf8');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export function listProjects(db) {
|
|
133
|
+
return db.prepare('SELECT * FROM projects ORDER BY created_at DESC').all();
|
|
134
|
+
}
|
|
135
|
+
// Derive an uppercase project code from the name when no code is supplied.
|
|
136
|
+
export function deriveCode(name) {
|
|
137
|
+
// Strip digits so derived codes pass the same validation as user-supplied codes.
|
|
138
|
+
const cleaned = name
|
|
139
|
+
.toUpperCase()
|
|
140
|
+
.replace(/[ÇĞİIÖŞÜ]/g, (c) => 'CGIIOSU'['ÇĞİIÖŞÜ'.indexOf(c)] ?? c)
|
|
141
|
+
.replace(/[^A-Z]/g, '');
|
|
142
|
+
return (cleaned.slice(0, 5) || 'PROJ').padEnd(2, 'X');
|
|
143
|
+
}
|
|
144
|
+
export function createProject(db, input, pkgRoot) {
|
|
145
|
+
const name = input.name.trim();
|
|
146
|
+
// Compare real filesystem identities, including older registry entries that
|
|
147
|
+
// stored an alias, before another project can own and reset these documents.
|
|
148
|
+
const repoPath = input.repoPath.trim() ? canonicalRepo(input.repoPath.trim()) : '';
|
|
149
|
+
const kind = input.kind === 'existing' ? 'existing' : 'new';
|
|
150
|
+
const code = (input.code ?? '').trim().toUpperCase() || deriveCode(name);
|
|
151
|
+
if (!name)
|
|
152
|
+
throw new Error('name is required');
|
|
153
|
+
if (!repoPath)
|
|
154
|
+
throw new Error('repoPath is required');
|
|
155
|
+
if (!/^[A-Z]{2,8}$/.test(code)) {
|
|
156
|
+
throw new Error(`code must be 2-8 letters, A-Z (got: ${code})`);
|
|
157
|
+
}
|
|
158
|
+
const codeTaken = db.prepare('SELECT name FROM projects WHERE code = ?').get(code);
|
|
159
|
+
if (codeTaken) {
|
|
160
|
+
throw new Error(`The code ${code} already belongs to "${codeTaken.name}" — task ids carry it, so two projects cannot share one. Pick another, or remove that project first.`);
|
|
161
|
+
}
|
|
162
|
+
const taken = listProjects(db).find((p) => canonicalRepo(p.repo_path) === repoPath);
|
|
163
|
+
if (taken) {
|
|
164
|
+
throw new Error(taken.archived
|
|
165
|
+
? `This folder is already the archived project "${taken.name}" — unarchive it instead of adding it again.`
|
|
166
|
+
: `This folder is already the project "${taken.name}".`);
|
|
167
|
+
}
|
|
168
|
+
mkdirSync(repoPath, { recursive: true });
|
|
169
|
+
// existing projects take no brief — the ground truth is the code itself
|
|
170
|
+
scaffoldProject(repoPath, pkgRoot, { skipBrief: kind === 'existing' });
|
|
171
|
+
const brief = input.brief?.trim();
|
|
172
|
+
if (brief) {
|
|
173
|
+
// Treat a submitted brief as approved; readiness is checked on Start and may demote it to draft.
|
|
174
|
+
writeFileSync(join(repoPath, BRIEF_REL), `---\nstatus: approved\nauthor: +prime\napprover: +prime\n---\n\n${brief}\n`, 'utf8');
|
|
175
|
+
}
|
|
176
|
+
const row = db
|
|
177
|
+
.prepare('INSERT INTO projects (name, repo_path, kind, code, doc_lang, engine) VALUES (?, ?, ?, ?, ?, ?) RETURNING *')
|
|
178
|
+
.get(name, repoPath, kind, code, (input.docLang ?? '').trim(), (input.engine ?? '').trim());
|
|
179
|
+
return row;
|
|
180
|
+
}
|
|
181
|
+
// Archive without deleting the registry row or project files.
|
|
182
|
+
export function setArchived(db, id, archived) {
|
|
183
|
+
return (db.prepare('UPDATE projects SET archived = ? WHERE id = ?').run(archived ? 1 : 0, id).changes >
|
|
184
|
+
0);
|
|
185
|
+
}
|
|
186
|
+
// Unregister only — never touches files in the repo.
|
|
187
|
+
export function removeProject(db, id) {
|
|
188
|
+
return db.prepare('DELETE FROM projects WHERE id = ?').run(id).changes > 0;
|
|
189
|
+
}
|