great-cto 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -0
- package/dist/archetypes.js +234 -0
- package/dist/bootstrap.js +75 -0
- package/dist/detect.js +314 -0
- package/dist/installer.js +132 -0
- package/dist/main.js +240 -0
- package/dist/settings.js +56 -0
- package/dist/ui.js +55 -0
- package/index.mjs +19 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# great-cto
|
|
2
|
+
|
|
3
|
+
> One command install for the [great_cto](https://github.com/avelikiy/great_cto) Claude Code plugin.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx great-cto init
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
That's it. The CLI detects your stack, picks the right archetype, clones the plugin, enables it, and writes a pre-filled `PROJECT.md`.
|
|
10
|
+
|
|
11
|
+
## What it does
|
|
12
|
+
|
|
13
|
+
1. **Scans** your project for stack signals — `package.json`, `requirements.txt`, `Cargo.toml`, `go.mod`, `Chart.yaml`, `*.tf`, `hardhat.config.*`, etc.
|
|
14
|
+
2. **Picks** the matching great_cto archetype:
|
|
15
|
+
- `web-service` · `mobile-app` · `ai-system` · `commerce` · `web3`
|
|
16
|
+
- `data-platform` · `infra` · `library` · `iot-embedded` · `regulated` · `greenfield`
|
|
17
|
+
3. **Installs** the plugin into `~/.claude/plugins/cache/local/great_cto/<latest>/`
|
|
18
|
+
4. **Enables** it in `~/.claude/settings.json` (atomic merge — other keys preserved, backup taken)
|
|
19
|
+
5. **Bootstraps** `.great_cto/PROJECT.md` pre-filled with archetype, stack, suggested compliance frameworks
|
|
20
|
+
|
|
21
|
+
After install, restart Claude Code and run `/inbox` or `/audit`.
|
|
22
|
+
|
|
23
|
+
## Examples
|
|
24
|
+
|
|
25
|
+
### Commerce detection (Stripe + Next.js)
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
[1/5] scanning /path/to/saas
|
|
29
|
+
stack: next.js, nodejs, prisma, react, stripe, supabase, typescript
|
|
30
|
+
languages: javascript, typescript
|
|
31
|
+
tests: yes CI: yes
|
|
32
|
+
[2/5] picking archetype
|
|
33
|
+
archetype: commerce (confidence: medium)
|
|
34
|
+
rationale: payments SDK detected: Stripe — PCI-DSS gate mandatory
|
|
35
|
+
suggested compliance: gdpr, pci-dss
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### AI system (MCP + Anthropic SDK)
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
archetype: ai-system (confidence: high)
|
|
42
|
+
rationale: AI/LLM tooling detected (MCP SDK, Anthropic SDK, LangChain) —
|
|
43
|
+
security gate mandatory for prompt injection + output sanitization
|
|
44
|
+
suggested compliance: eu-ai-act
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Infra repo (Terraform + Helm)
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
archetype: infra (confidence: high)
|
|
51
|
+
rationale: infrastructure-as-code detected: Terraform, Helm
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Options
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
npx great-cto init [options]
|
|
58
|
+
|
|
59
|
+
-y, --yes Skip confirmation prompts (non-interactive)
|
|
60
|
+
--dry-run Show what would be done without doing it
|
|
61
|
+
--force Reinstall even if already present
|
|
62
|
+
--archetype NAME Override detected archetype
|
|
63
|
+
--version-tag VER Pin to specific great_cto version (default: latest)
|
|
64
|
+
--dir PATH Run against a different directory (default: cwd)
|
|
65
|
+
-h, --help Show help
|
|
66
|
+
-v, --version Show CLI version
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Safety
|
|
70
|
+
|
|
71
|
+
- **Atomic `settings.json` merge**: a timestamped `.bak` file is written before any change. Other `enabledPlugins` entries and unrelated keys are preserved.
|
|
72
|
+
- **Dry-run by default**: run `--dry-run` first to see exactly what will happen.
|
|
73
|
+
- **Idempotent**: running twice does nothing the second time (unless `--force`).
|
|
74
|
+
- **Never overwrites `PROJECT.md`**: if you already have one, the CLI leaves it untouched.
|
|
75
|
+
|
|
76
|
+
## Requirements
|
|
77
|
+
|
|
78
|
+
- Node.js ≥ 22.6.0
|
|
79
|
+
- Git (to clone the plugin repo)
|
|
80
|
+
- [Claude Code](https://claude.com/claude-code)
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT — same as the plugin.
|
|
85
|
+
|
|
86
|
+
## Links
|
|
87
|
+
|
|
88
|
+
- Plugin: [github.com/avelikiy/great_cto](https://github.com/avelikiy/great_cto)
|
|
89
|
+
- Issues: [github.com/avelikiy/great_cto/issues](https://github.com/avelikiy/great_cto/issues)
|
|
90
|
+
- Author: [velykyi](https://www.linkedin.com/in/velykyi/)
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// Archetype decision: detected stack → archetype recommendation.
|
|
2
|
+
// Mirrors great_cto's 10 archetypes in skills/great_cto/ARCHETYPES.md.
|
|
3
|
+
// Rules are evaluated; highest score wins.
|
|
4
|
+
const RULES = [
|
|
5
|
+
// ── commerce ─────────────────────────────────────
|
|
6
|
+
{
|
|
7
|
+
archetype: "commerce",
|
|
8
|
+
score: (d) => {
|
|
9
|
+
let s = 0;
|
|
10
|
+
if (d.stack.includes("stripe"))
|
|
11
|
+
s += 5;
|
|
12
|
+
if (d.stack.includes("shopify"))
|
|
13
|
+
s += 5;
|
|
14
|
+
if (d.stack.includes("braintree"))
|
|
15
|
+
s += 5;
|
|
16
|
+
return s;
|
|
17
|
+
},
|
|
18
|
+
reason: (d) => {
|
|
19
|
+
const payments = [];
|
|
20
|
+
if (d.stack.includes("stripe"))
|
|
21
|
+
payments.push("Stripe");
|
|
22
|
+
if (d.stack.includes("shopify"))
|
|
23
|
+
payments.push("Shopify");
|
|
24
|
+
if (d.stack.includes("braintree"))
|
|
25
|
+
payments.push("Braintree");
|
|
26
|
+
return `payments SDK detected: ${payments.join(", ")} — PCI-DSS gate mandatory`;
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
// ── web3 ─────────────────────────────────────────
|
|
30
|
+
{
|
|
31
|
+
archetype: "web3",
|
|
32
|
+
score: (d) => {
|
|
33
|
+
let s = 0;
|
|
34
|
+
if (d.stack.includes("solidity"))
|
|
35
|
+
s += 6;
|
|
36
|
+
if (d.stack.includes("web3"))
|
|
37
|
+
s += 4;
|
|
38
|
+
return s;
|
|
39
|
+
},
|
|
40
|
+
reason: (_d) => "Solidity / smart-contract tooling detected — formal verification gate",
|
|
41
|
+
},
|
|
42
|
+
// ── iot-embedded ─────────────────────────────────
|
|
43
|
+
{
|
|
44
|
+
archetype: "iot-embedded",
|
|
45
|
+
score: (d) => (d.stack.includes("embedded") ? 6 : 0),
|
|
46
|
+
reason: (_d) => "platformio.ini / sdkconfig detected — embedded firmware archetype",
|
|
47
|
+
},
|
|
48
|
+
// ── ai-system ────────────────────────────────────
|
|
49
|
+
{
|
|
50
|
+
archetype: "ai-system",
|
|
51
|
+
score: (d) => {
|
|
52
|
+
let s = 0;
|
|
53
|
+
if (d.stack.includes("anthropic-sdk"))
|
|
54
|
+
s += 4;
|
|
55
|
+
if (d.stack.includes("openai-sdk"))
|
|
56
|
+
s += 3;
|
|
57
|
+
if (d.stack.includes("langchain"))
|
|
58
|
+
s += 4;
|
|
59
|
+
if (d.stack.includes("llamaindex"))
|
|
60
|
+
s += 4;
|
|
61
|
+
if (d.stack.includes("mcp"))
|
|
62
|
+
s += 5;
|
|
63
|
+
if (d.stack.includes("ml"))
|
|
64
|
+
s += 3;
|
|
65
|
+
return s;
|
|
66
|
+
},
|
|
67
|
+
reason: (d) => {
|
|
68
|
+
const bits = [];
|
|
69
|
+
if (d.stack.includes("mcp"))
|
|
70
|
+
bits.push("MCP SDK");
|
|
71
|
+
if (d.stack.includes("anthropic-sdk"))
|
|
72
|
+
bits.push("Anthropic SDK");
|
|
73
|
+
if (d.stack.includes("openai-sdk"))
|
|
74
|
+
bits.push("OpenAI SDK");
|
|
75
|
+
if (d.stack.includes("langchain"))
|
|
76
|
+
bits.push("LangChain");
|
|
77
|
+
if (d.stack.includes("llamaindex"))
|
|
78
|
+
bits.push("LlamaIndex");
|
|
79
|
+
if (d.stack.includes("ml"))
|
|
80
|
+
bits.push("ML stack");
|
|
81
|
+
return `AI/LLM tooling detected (${bits.join(", ")}) — security gate mandatory for prompt injection + output sanitization`;
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
// ── mobile-app ───────────────────────────────────
|
|
85
|
+
{
|
|
86
|
+
archetype: "mobile-app",
|
|
87
|
+
score: (d) => {
|
|
88
|
+
let s = 0;
|
|
89
|
+
if (d.stack.includes("react-native"))
|
|
90
|
+
s += 5;
|
|
91
|
+
if (d.stack.includes("expo"))
|
|
92
|
+
s += 5;
|
|
93
|
+
if (d.stack.includes("ios"))
|
|
94
|
+
s += 5;
|
|
95
|
+
if (d.stack.includes("swift"))
|
|
96
|
+
s += 3;
|
|
97
|
+
return s;
|
|
98
|
+
},
|
|
99
|
+
reason: (d) => {
|
|
100
|
+
const bits = [];
|
|
101
|
+
if (d.stack.includes("react-native"))
|
|
102
|
+
bits.push("React Native");
|
|
103
|
+
if (d.stack.includes("expo"))
|
|
104
|
+
bits.push("Expo");
|
|
105
|
+
if (d.stack.includes("ios"))
|
|
106
|
+
bits.push("iOS project");
|
|
107
|
+
return `mobile framework detected: ${bits.join(", ")}`;
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
// ── data-platform ────────────────────────────────
|
|
111
|
+
{
|
|
112
|
+
archetype: "data-platform",
|
|
113
|
+
score: (d) => (d.stack.includes("data-pipeline") ? 4 : 0),
|
|
114
|
+
reason: (_d) => "data pipeline tooling detected (pandas/airflow/prefect)",
|
|
115
|
+
},
|
|
116
|
+
// ── infra ────────────────────────────────────────
|
|
117
|
+
{
|
|
118
|
+
archetype: "infra",
|
|
119
|
+
score: (d) => {
|
|
120
|
+
const hasTerraform = d.stack.includes("terraform");
|
|
121
|
+
const hasHelm = d.stack.includes("helm");
|
|
122
|
+
const hasK8s = d.stack.includes("kubernetes");
|
|
123
|
+
// Require at least one explicit infra signal
|
|
124
|
+
if (!hasTerraform && !hasHelm && !hasK8s)
|
|
125
|
+
return 0;
|
|
126
|
+
let s = 0;
|
|
127
|
+
if (hasTerraform)
|
|
128
|
+
s += 4;
|
|
129
|
+
if (hasHelm)
|
|
130
|
+
s += 4;
|
|
131
|
+
if (hasK8s)
|
|
132
|
+
s += 4;
|
|
133
|
+
// Pure-infra repo (no app code) gets a small bonus
|
|
134
|
+
if (!d.stack.includes("nodejs") && !d.stack.includes("python") && !d.stack.includes("go"))
|
|
135
|
+
s += 2;
|
|
136
|
+
return s;
|
|
137
|
+
},
|
|
138
|
+
reason: (d) => {
|
|
139
|
+
const bits = [];
|
|
140
|
+
if (d.stack.includes("terraform"))
|
|
141
|
+
bits.push("Terraform");
|
|
142
|
+
if (d.stack.includes("helm"))
|
|
143
|
+
bits.push("Helm");
|
|
144
|
+
if (d.stack.includes("kubernetes"))
|
|
145
|
+
bits.push("Kustomize/K8s");
|
|
146
|
+
return `infrastructure-as-code detected: ${bits.join(", ")}`;
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
// ── web-service (default for web frameworks) ─────
|
|
150
|
+
{
|
|
151
|
+
archetype: "web-service",
|
|
152
|
+
score: (d) => {
|
|
153
|
+
let s = 0;
|
|
154
|
+
const webFrameworks = [
|
|
155
|
+
"next.js", "react", "vue", "angular", "svelte", "astro",
|
|
156
|
+
"express", "fastify", "nestjs", "hono",
|
|
157
|
+
"django", "fastapi", "flask",
|
|
158
|
+
];
|
|
159
|
+
for (const fw of webFrameworks)
|
|
160
|
+
if (d.stack.includes(fw))
|
|
161
|
+
s += 1;
|
|
162
|
+
if (s > 0)
|
|
163
|
+
s += 2; // baseline bonus for any web framework
|
|
164
|
+
return s;
|
|
165
|
+
},
|
|
166
|
+
reason: (d) => {
|
|
167
|
+
const fw = d.stack.find((t) => ["next.js", "react", "vue", "angular", "svelte", "astro", "express", "fastify", "nestjs", "hono", "django", "fastapi", "flask"].includes(t));
|
|
168
|
+
return `web framework detected: ${fw ?? "unknown"}`;
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
// ── library (no app framework, just code) ────────
|
|
172
|
+
{
|
|
173
|
+
archetype: "library",
|
|
174
|
+
score: (d) => {
|
|
175
|
+
const hasApp = d.stack.some((t) => ["next.js", "django", "fastapi", "express", "fastify", "nestjs", "react-native", "expo", "terraform"].includes(t));
|
|
176
|
+
if (hasApp)
|
|
177
|
+
return 0;
|
|
178
|
+
// Plain Node or Python or Go or Rust with no web/mobile/infra → likely a library
|
|
179
|
+
if (d.stack.includes("nodejs") || d.stack.includes("python") || d.stack.includes("go") || d.stack.includes("rust")) {
|
|
180
|
+
return 2;
|
|
181
|
+
}
|
|
182
|
+
return 0;
|
|
183
|
+
},
|
|
184
|
+
reason: (_d) => "no web/mobile/infra framework detected — looks like a library/SDK",
|
|
185
|
+
},
|
|
186
|
+
];
|
|
187
|
+
export function pickArchetype(d) {
|
|
188
|
+
const scored = RULES
|
|
189
|
+
.map((r) => ({ archetype: r.archetype, score: r.score(d), reason: r.reason(d) }))
|
|
190
|
+
.filter((r) => r.score > 0)
|
|
191
|
+
.sort((a, b) => b.score - a.score);
|
|
192
|
+
if (scored.length === 0) {
|
|
193
|
+
return {
|
|
194
|
+
primary: "greenfield",
|
|
195
|
+
confidence: "low",
|
|
196
|
+
rationale: "no strong signals detected — treating as greenfield project",
|
|
197
|
+
alternatives: [],
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const top = scored[0];
|
|
201
|
+
const nextBest = scored[1]?.score ?? 0;
|
|
202
|
+
const gap = top.score - nextBest;
|
|
203
|
+
const confidence = top.score >= 5 && gap >= 2 ? "high" :
|
|
204
|
+
top.score >= 3 ? "medium" : "low";
|
|
205
|
+
return {
|
|
206
|
+
primary: top.archetype,
|
|
207
|
+
confidence,
|
|
208
|
+
rationale: top.reason,
|
|
209
|
+
alternatives: scored.slice(1, 4).map((r) => r.archetype),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
// Compliance hints — auto-suggested based on stack.
|
|
213
|
+
export function suggestCompliance(d, archetype) {
|
|
214
|
+
const c = new Set();
|
|
215
|
+
if (archetype === "commerce") {
|
|
216
|
+
c.add("pci-dss");
|
|
217
|
+
c.add("gdpr");
|
|
218
|
+
}
|
|
219
|
+
if (archetype === "ai-system") {
|
|
220
|
+
c.add("eu-ai-act");
|
|
221
|
+
}
|
|
222
|
+
if (archetype === "web3") {
|
|
223
|
+
c.add("soc2");
|
|
224
|
+
}
|
|
225
|
+
if (archetype === "iot-embedded") {
|
|
226
|
+
c.add("iso27001");
|
|
227
|
+
}
|
|
228
|
+
if (d.stack.includes("stripe"))
|
|
229
|
+
c.add("pci-dss");
|
|
230
|
+
// Reasonable default for any web service storing user data
|
|
231
|
+
if (archetype === "web-service")
|
|
232
|
+
c.add("gdpr");
|
|
233
|
+
return Array.from(c).sort();
|
|
234
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Generate .great_cto/PROJECT.md pre-filled from detected stack + archetype.
|
|
2
|
+
// Safe: will NOT overwrite an existing PROJECT.md.
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { dim, success, warn } from "./ui.js";
|
|
6
|
+
export function bootstrap(dir, detection, archetype, compliance) {
|
|
7
|
+
const greatCtoDir = join(dir, ".great_cto");
|
|
8
|
+
const projectMd = join(greatCtoDir, "PROJECT.md");
|
|
9
|
+
if (existsSync(projectMd)) {
|
|
10
|
+
warn(`.great_cto/PROJECT.md already exists — not overwriting.`);
|
|
11
|
+
return { projectMdPath: projectMd, created: false, skippedReason: "already exists" };
|
|
12
|
+
}
|
|
13
|
+
mkdirSync(greatCtoDir, { recursive: true });
|
|
14
|
+
const title = inferProjectTitle(dir);
|
|
15
|
+
const stackLine = detection.stack.length > 0 ? detection.stack.join(", ") : "to be defined";
|
|
16
|
+
const complianceLine = compliance.length > 0 ? compliance.join(", ") : "none";
|
|
17
|
+
const teamSize = 1; // MVP default — user edits later
|
|
18
|
+
const approvalLevel = "gates-only"; // default per README
|
|
19
|
+
const content = `# ${title}
|
|
20
|
+
|
|
21
|
+
> Auto-generated by \`great-cto init\` on ${new Date().toISOString().slice(0, 10)}.
|
|
22
|
+
> Edit freely — this file is yours. Re-run \`npx great-cto init\` won't overwrite it.
|
|
23
|
+
|
|
24
|
+
## Project
|
|
25
|
+
|
|
26
|
+
primary: ${archetype}
|
|
27
|
+
stack: ${stackLine}
|
|
28
|
+
languages: ${detection.languages.join(", ") || "to be defined"}
|
|
29
|
+
package-manager: ${detection.packageManager ?? "none"}
|
|
30
|
+
|
|
31
|
+
## Team
|
|
32
|
+
|
|
33
|
+
size: ${teamSize}
|
|
34
|
+
mode: solo
|
|
35
|
+
approval-level: ${approvalLevel}
|
|
36
|
+
|
|
37
|
+
## Compliance
|
|
38
|
+
|
|
39
|
+
frameworks: [${complianceLine}]
|
|
40
|
+
|
|
41
|
+
## Goals
|
|
42
|
+
|
|
43
|
+
- <add your primary goal here>
|
|
44
|
+
- <add your second goal here>
|
|
45
|
+
|
|
46
|
+
## Context
|
|
47
|
+
|
|
48
|
+
- Tests present: ${detection.hasTests ? "yes" : "no"}
|
|
49
|
+
- CI configured: ${detection.hasCI ? "yes" : "no"}
|
|
50
|
+
- Detected signals: ${JSON.stringify(detection.signals, null, 2).replace(/\n/g, "\n ")}
|
|
51
|
+
|
|
52
|
+
## Notes
|
|
53
|
+
|
|
54
|
+
Generated from stack auto-detection. For a full audit (gap analysis, tech
|
|
55
|
+
debt scan, architectural review) run \`/audit\` in Claude Code after install.
|
|
56
|
+
`;
|
|
57
|
+
writeFileSync(projectMd, content, "utf-8");
|
|
58
|
+
success(`created .great_cto/PROJECT.md ${dim(`(archetype: ${archetype})`)}`);
|
|
59
|
+
return { projectMdPath: projectMd, created: true, skippedReason: null };
|
|
60
|
+
}
|
|
61
|
+
function inferProjectTitle(dir) {
|
|
62
|
+
// Prefer package.json name, then directory basename.
|
|
63
|
+
try {
|
|
64
|
+
const pkgPath = join(dir, "package.json");
|
|
65
|
+
if (existsSync(pkgPath)) {
|
|
66
|
+
const { readFileSync } = require("node:fs");
|
|
67
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
68
|
+
if (pkg.name)
|
|
69
|
+
return pkg.name;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch { /* ignore */ }
|
|
73
|
+
const parts = dir.split(/[\/\\]/);
|
|
74
|
+
return parts[parts.length - 1] ?? "project";
|
|
75
|
+
}
|
package/dist/detect.js
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// Stack detection: scan cwd for technology signals.
|
|
2
|
+
// Zero-dependency — pure file reads + JSON parse.
|
|
3
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
export function detect(dir) {
|
|
6
|
+
const signals = {};
|
|
7
|
+
const stack = new Set();
|
|
8
|
+
const languages = new Set();
|
|
9
|
+
function sig(name, file) {
|
|
10
|
+
if (!signals[name])
|
|
11
|
+
signals[name] = [];
|
|
12
|
+
signals[name].push(file);
|
|
13
|
+
}
|
|
14
|
+
// ── package.json (Node/TS) ────────────────────────────────
|
|
15
|
+
const pkgPath = join(dir, "package.json");
|
|
16
|
+
let pkg = {};
|
|
17
|
+
if (existsSync(pkgPath)) {
|
|
18
|
+
sig("node", "package.json");
|
|
19
|
+
stack.add("nodejs");
|
|
20
|
+
languages.add("javascript");
|
|
21
|
+
try {
|
|
22
|
+
pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
23
|
+
const allDeps = {
|
|
24
|
+
...(pkg.dependencies ?? {}),
|
|
25
|
+
...(pkg.devDependencies ?? {}),
|
|
26
|
+
...(pkg.peerDependencies ?? {}),
|
|
27
|
+
};
|
|
28
|
+
const has = (name) => name in allDeps;
|
|
29
|
+
if (has("typescript") || existsSync(join(dir, "tsconfig.json"))) {
|
|
30
|
+
stack.add("typescript");
|
|
31
|
+
languages.add("typescript");
|
|
32
|
+
}
|
|
33
|
+
// Frameworks
|
|
34
|
+
if (has("next")) {
|
|
35
|
+
stack.add("next.js");
|
|
36
|
+
sig("framework-next", "package.json");
|
|
37
|
+
}
|
|
38
|
+
if (has("react") || has("react-dom"))
|
|
39
|
+
stack.add("react");
|
|
40
|
+
if (has("vue") || has("nuxt"))
|
|
41
|
+
stack.add("vue");
|
|
42
|
+
if (has("@angular/core"))
|
|
43
|
+
stack.add("angular");
|
|
44
|
+
if (has("svelte") || has("@sveltejs/kit"))
|
|
45
|
+
stack.add("svelte");
|
|
46
|
+
if (has("astro"))
|
|
47
|
+
stack.add("astro");
|
|
48
|
+
if (has("express"))
|
|
49
|
+
stack.add("express");
|
|
50
|
+
if (has("fastify"))
|
|
51
|
+
stack.add("fastify");
|
|
52
|
+
if (has("@nestjs/core"))
|
|
53
|
+
stack.add("nestjs");
|
|
54
|
+
if (has("hono"))
|
|
55
|
+
stack.add("hono");
|
|
56
|
+
// Mobile
|
|
57
|
+
if (has("react-native")) {
|
|
58
|
+
stack.add("react-native");
|
|
59
|
+
sig("mobile", "package.json");
|
|
60
|
+
}
|
|
61
|
+
if (has("expo"))
|
|
62
|
+
stack.add("expo");
|
|
63
|
+
// AI / agents
|
|
64
|
+
if (has("openai")) {
|
|
65
|
+
stack.add("openai-sdk");
|
|
66
|
+
sig("ai", "openai");
|
|
67
|
+
}
|
|
68
|
+
if (has("@anthropic-ai/sdk")) {
|
|
69
|
+
stack.add("anthropic-sdk");
|
|
70
|
+
sig("ai", "anthropic-sdk");
|
|
71
|
+
}
|
|
72
|
+
if (has("langchain") || has("@langchain/core")) {
|
|
73
|
+
stack.add("langchain");
|
|
74
|
+
sig("ai", "langchain");
|
|
75
|
+
}
|
|
76
|
+
if (has("llamaindex")) {
|
|
77
|
+
stack.add("llamaindex");
|
|
78
|
+
sig("ai", "llamaindex");
|
|
79
|
+
}
|
|
80
|
+
if (has("@modelcontextprotocol/sdk")) {
|
|
81
|
+
stack.add("mcp");
|
|
82
|
+
sig("ai", "mcp");
|
|
83
|
+
}
|
|
84
|
+
// Payments / commerce
|
|
85
|
+
if (has("stripe") || has("@stripe/stripe-js")) {
|
|
86
|
+
stack.add("stripe");
|
|
87
|
+
sig("commerce", "stripe");
|
|
88
|
+
}
|
|
89
|
+
if (has("@shopify/shopify-api")) {
|
|
90
|
+
stack.add("shopify");
|
|
91
|
+
sig("commerce", "shopify");
|
|
92
|
+
}
|
|
93
|
+
if (has("braintree")) {
|
|
94
|
+
stack.add("braintree");
|
|
95
|
+
sig("commerce", "braintree");
|
|
96
|
+
}
|
|
97
|
+
// Auth
|
|
98
|
+
if (has("next-auth") || has("@auth/core"))
|
|
99
|
+
stack.add("auth");
|
|
100
|
+
if (has("@clerk/nextjs") || has("@clerk/clerk-sdk-node"))
|
|
101
|
+
stack.add("clerk");
|
|
102
|
+
if (has("@supabase/supabase-js"))
|
|
103
|
+
stack.add("supabase");
|
|
104
|
+
// Databases / ORMs
|
|
105
|
+
if (has("prisma") || has("@prisma/client"))
|
|
106
|
+
stack.add("prisma");
|
|
107
|
+
if (has("drizzle-orm"))
|
|
108
|
+
stack.add("drizzle");
|
|
109
|
+
if (has("typeorm"))
|
|
110
|
+
stack.add("typeorm");
|
|
111
|
+
if (has("mongodb") || has("mongoose"))
|
|
112
|
+
stack.add("mongodb");
|
|
113
|
+
if (has("pg") || has("postgres"))
|
|
114
|
+
stack.add("postgres");
|
|
115
|
+
if (has("mysql") || has("mysql2"))
|
|
116
|
+
stack.add("mysql");
|
|
117
|
+
if (has("redis") || has("ioredis"))
|
|
118
|
+
stack.add("redis");
|
|
119
|
+
// Testing
|
|
120
|
+
if (has("jest") || has("vitest") || has("mocha") || has("@playwright/test") || has("playwright")) {
|
|
121
|
+
sig("tests", "package.json");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch { /* ignore malformed */ }
|
|
125
|
+
}
|
|
126
|
+
// ── Python ────────────────────────────────────────────────
|
|
127
|
+
if (existsSync(join(dir, "requirements.txt")) ||
|
|
128
|
+
existsSync(join(dir, "pyproject.toml")) ||
|
|
129
|
+
existsSync(join(dir, "setup.py"))) {
|
|
130
|
+
sig("python", "pyproject/requirements/setup");
|
|
131
|
+
stack.add("python");
|
|
132
|
+
languages.add("python");
|
|
133
|
+
try {
|
|
134
|
+
const reqs = existsSync(join(dir, "requirements.txt"))
|
|
135
|
+
? readFileSync(join(dir, "requirements.txt"), "utf-8")
|
|
136
|
+
: "";
|
|
137
|
+
const pyproject = existsSync(join(dir, "pyproject.toml"))
|
|
138
|
+
? readFileSync(join(dir, "pyproject.toml"), "utf-8")
|
|
139
|
+
: "";
|
|
140
|
+
const all = reqs + "\n" + pyproject;
|
|
141
|
+
const ihas = (s) => all.toLowerCase().includes(s);
|
|
142
|
+
if (ihas("django")) {
|
|
143
|
+
stack.add("django");
|
|
144
|
+
sig("framework-django", "python");
|
|
145
|
+
}
|
|
146
|
+
if (ihas("fastapi")) {
|
|
147
|
+
stack.add("fastapi");
|
|
148
|
+
sig("framework-fastapi", "python");
|
|
149
|
+
}
|
|
150
|
+
if (ihas("flask"))
|
|
151
|
+
stack.add("flask");
|
|
152
|
+
if (ihas("openai"))
|
|
153
|
+
stack.add("openai-sdk");
|
|
154
|
+
if (ihas("anthropic"))
|
|
155
|
+
stack.add("anthropic-sdk");
|
|
156
|
+
if (ihas("langchain")) {
|
|
157
|
+
stack.add("langchain");
|
|
158
|
+
sig("ai", "langchain");
|
|
159
|
+
}
|
|
160
|
+
if (ihas("llama-index") || ihas("llamaindex"))
|
|
161
|
+
stack.add("llamaindex");
|
|
162
|
+
if (ihas("torch") || ihas("tensorflow") || ihas("scikit-learn")) {
|
|
163
|
+
stack.add("ml");
|
|
164
|
+
sig("ml", "python");
|
|
165
|
+
}
|
|
166
|
+
if (ihas("pandas") || ihas("dask") || ihas("airflow") || ihas("prefect")) {
|
|
167
|
+
stack.add("data-pipeline");
|
|
168
|
+
sig("data", "python");
|
|
169
|
+
}
|
|
170
|
+
if (ihas("stripe")) {
|
|
171
|
+
stack.add("stripe");
|
|
172
|
+
sig("commerce", "stripe");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch { /* ignore */ }
|
|
176
|
+
}
|
|
177
|
+
// ── Go ────────────────────────────────────────────────────
|
|
178
|
+
if (existsSync(join(dir, "go.mod"))) {
|
|
179
|
+
sig("go", "go.mod");
|
|
180
|
+
stack.add("go");
|
|
181
|
+
languages.add("go");
|
|
182
|
+
try {
|
|
183
|
+
const gomod = readFileSync(join(dir, "go.mod"), "utf-8");
|
|
184
|
+
if (gomod.includes("stripe-go"))
|
|
185
|
+
stack.add("stripe");
|
|
186
|
+
if (gomod.includes("openai-go"))
|
|
187
|
+
stack.add("openai-sdk");
|
|
188
|
+
}
|
|
189
|
+
catch { /* ignore */ }
|
|
190
|
+
}
|
|
191
|
+
// ── Rust ──────────────────────────────────────────────────
|
|
192
|
+
if (existsSync(join(dir, "Cargo.toml"))) {
|
|
193
|
+
sig("rust", "Cargo.toml");
|
|
194
|
+
stack.add("rust");
|
|
195
|
+
languages.add("rust");
|
|
196
|
+
try {
|
|
197
|
+
const cargo = readFileSync(join(dir, "Cargo.toml"), "utf-8");
|
|
198
|
+
if (cargo.includes("actix-web") || cargo.includes("axum") || cargo.includes("rocket")) {
|
|
199
|
+
sig("web-rust", "Cargo.toml");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch { /* ignore */ }
|
|
203
|
+
}
|
|
204
|
+
// ── Java / Kotlin ─────────────────────────────────────────
|
|
205
|
+
if (existsSync(join(dir, "pom.xml"))) {
|
|
206
|
+
sig("java", "pom.xml");
|
|
207
|
+
stack.add("java");
|
|
208
|
+
languages.add("java");
|
|
209
|
+
}
|
|
210
|
+
if (existsSync(join(dir, "build.gradle")) || existsSync(join(dir, "build.gradle.kts"))) {
|
|
211
|
+
sig("gradle", "build.gradle");
|
|
212
|
+
stack.add("gradle");
|
|
213
|
+
if (existsSync(join(dir, "build.gradle.kts")))
|
|
214
|
+
languages.add("kotlin");
|
|
215
|
+
else
|
|
216
|
+
languages.add("java");
|
|
217
|
+
}
|
|
218
|
+
// ── Swift / iOS ───────────────────────────────────────────
|
|
219
|
+
if (existsSync(join(dir, "Package.swift"))) {
|
|
220
|
+
sig("swift", "Package.swift");
|
|
221
|
+
stack.add("swift");
|
|
222
|
+
languages.add("swift");
|
|
223
|
+
}
|
|
224
|
+
if (safeGlob(dir, /\.xcodeproj$/)) {
|
|
225
|
+
sig("ios", "xcodeproj");
|
|
226
|
+
stack.add("ios");
|
|
227
|
+
}
|
|
228
|
+
// ── Infra ─────────────────────────────────────────────────
|
|
229
|
+
if (safeGlob(dir, /\.tf$/)) {
|
|
230
|
+
sig("infra", "terraform");
|
|
231
|
+
stack.add("terraform");
|
|
232
|
+
}
|
|
233
|
+
if (existsSync(join(dir, "Chart.yaml")) || existsSync(join(dir, "values.yaml"))) {
|
|
234
|
+
sig("infra", "helm");
|
|
235
|
+
stack.add("helm");
|
|
236
|
+
}
|
|
237
|
+
if (safeGlob(dir, /kustomization\.ya?ml$/)) {
|
|
238
|
+
sig("infra", "kustomize");
|
|
239
|
+
stack.add("kubernetes");
|
|
240
|
+
}
|
|
241
|
+
if (existsSync(join(dir, "Dockerfile")) || existsSync(join(dir, "docker-compose.yml"))) {
|
|
242
|
+
sig("docker", "Dockerfile");
|
|
243
|
+
stack.add("docker");
|
|
244
|
+
}
|
|
245
|
+
// ── Smart contracts ──────────────────────────────────────
|
|
246
|
+
if (existsSync(join(dir, "hardhat.config.js")) ||
|
|
247
|
+
existsSync(join(dir, "hardhat.config.ts")) ||
|
|
248
|
+
existsSync(join(dir, "foundry.toml"))) {
|
|
249
|
+
sig("web3", "smart-contract");
|
|
250
|
+
stack.add("web3");
|
|
251
|
+
stack.add("solidity");
|
|
252
|
+
languages.add("solidity");
|
|
253
|
+
}
|
|
254
|
+
if (safeGlob(dir, /\.sol$/)) {
|
|
255
|
+
sig("web3", "solidity-files");
|
|
256
|
+
stack.add("solidity");
|
|
257
|
+
}
|
|
258
|
+
// ── Embedded ─────────────────────────────────────────────
|
|
259
|
+
if (existsSync(join(dir, "platformio.ini")) ||
|
|
260
|
+
existsSync(join(dir, "sdkconfig")) ||
|
|
261
|
+
existsSync(join(dir, "Kconfig"))) {
|
|
262
|
+
sig("embedded", "platformio/sdk");
|
|
263
|
+
stack.add("embedded");
|
|
264
|
+
}
|
|
265
|
+
// ── Package manager ──────────────────────────────────────
|
|
266
|
+
let packageManager = null;
|
|
267
|
+
if (existsSync(join(dir, "pnpm-lock.yaml")))
|
|
268
|
+
packageManager = "pnpm";
|
|
269
|
+
else if (existsSync(join(dir, "yarn.lock")))
|
|
270
|
+
packageManager = "yarn";
|
|
271
|
+
else if (existsSync(join(dir, "bun.lockb")) || existsSync(join(dir, "bun.lock")))
|
|
272
|
+
packageManager = "bun";
|
|
273
|
+
else if (existsSync(join(dir, "package-lock.json")))
|
|
274
|
+
packageManager = "npm";
|
|
275
|
+
// ── CI / tests ───────────────────────────────────────────
|
|
276
|
+
const hasTests = !!signals["tests"] ||
|
|
277
|
+
existsSync(join(dir, "pytest.ini")) ||
|
|
278
|
+
existsSync(join(dir, "tox.ini")) ||
|
|
279
|
+
safeGlob(dir, /^(tests?|spec|__tests__)$/, "dir");
|
|
280
|
+
const hasCI = existsSync(join(dir, ".github", "workflows")) ||
|
|
281
|
+
existsSync(join(dir, ".gitlab-ci.yml")) ||
|
|
282
|
+
existsSync(join(dir, ".circleci")) ||
|
|
283
|
+
existsSync(join(dir, "azure-pipelines.yml"));
|
|
284
|
+
const hasExistingGreatCto = existsSync(join(dir, ".great_cto", "PROJECT.md")) ||
|
|
285
|
+
existsSync(join(dir, ".great_cto", "SKILL.md"));
|
|
286
|
+
return {
|
|
287
|
+
stack: Array.from(stack).sort(),
|
|
288
|
+
languages: Array.from(languages).sort(),
|
|
289
|
+
signals,
|
|
290
|
+
packageManager,
|
|
291
|
+
hasTests,
|
|
292
|
+
hasCI,
|
|
293
|
+
hasExistingGreatCto,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
// ── helpers ──────────────────────────────────────────────────
|
|
297
|
+
function safeGlob(dir, pattern, kind = "file") {
|
|
298
|
+
try {
|
|
299
|
+
const entries = readdirSync(dir);
|
|
300
|
+
for (const e of entries) {
|
|
301
|
+
const p = join(dir, e);
|
|
302
|
+
try {
|
|
303
|
+
const st = statSync(p);
|
|
304
|
+
if (kind === "file" && st.isFile() && pattern.test(e))
|
|
305
|
+
return true;
|
|
306
|
+
if (kind === "dir" && st.isDirectory() && pattern.test(e))
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
catch { /* unreadable entry */ }
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
catch { /* unreadable dir */ }
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Install the great_cto plugin into ~/.claude/plugins/cache/local/great_cto/<version>/.
|
|
2
|
+
// Uses git clone. Falls back to tarball fetch if git is unavailable.
|
|
3
|
+
import { spawnSync, execFileSync } from "node:child_process";
|
|
4
|
+
import { existsSync, mkdirSync, rmSync, readFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { log, success, warn, dim } from "./ui.js";
|
|
8
|
+
const REPO_URL = "https://github.com/avelikiy/great_cto.git";
|
|
9
|
+
export function hasGit() {
|
|
10
|
+
try {
|
|
11
|
+
execFileSync("git", ["--version"], { stdio: "pipe", timeout: 5_000 });
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function detectLatestVersion() {
|
|
19
|
+
try {
|
|
20
|
+
const out = execFileSync("git", ["ls-remote", "--tags", REPO_URL], {
|
|
21
|
+
encoding: "utf-8",
|
|
22
|
+
timeout: 15_000,
|
|
23
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
24
|
+
});
|
|
25
|
+
const tags = out
|
|
26
|
+
.split("\n")
|
|
27
|
+
.map((line) => line.match(/refs\/tags\/v?([0-9]+\.[0-9]+\.[0-9]+)(?!\^)/)?.[1])
|
|
28
|
+
.filter((t) => !!t)
|
|
29
|
+
.sort((a, b) => cmpSemver(b, a));
|
|
30
|
+
return tags[0] ?? null;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function cmpSemver(a, b) {
|
|
37
|
+
const pa = a.split(".").map(Number);
|
|
38
|
+
const pb = b.split(".").map(Number);
|
|
39
|
+
for (let i = 0; i < 3; i++) {
|
|
40
|
+
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
41
|
+
if (diff !== 0)
|
|
42
|
+
return diff;
|
|
43
|
+
}
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
export function getPluginBaseDir() {
|
|
47
|
+
return join(homedir(), ".claude", "plugins", "cache", "local", "great_cto");
|
|
48
|
+
}
|
|
49
|
+
export function findInstalledVersions() {
|
|
50
|
+
const base = getPluginBaseDir();
|
|
51
|
+
if (!existsSync(base))
|
|
52
|
+
return [];
|
|
53
|
+
try {
|
|
54
|
+
const { readdirSync } = require("node:fs");
|
|
55
|
+
return readdirSync(base).filter((name) => /^[0-9]+\.[0-9]+\.[0-9]+$/.test(name)).sort(cmpSemver);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function install(opts = {}) {
|
|
62
|
+
if (!hasGit()) {
|
|
63
|
+
throw new Error("git is required to install great_cto. Install git first: https://git-scm.com/downloads");
|
|
64
|
+
}
|
|
65
|
+
// Resolve version
|
|
66
|
+
let version = opts.version;
|
|
67
|
+
if (!version) {
|
|
68
|
+
const latest = detectLatestVersion();
|
|
69
|
+
if (!latest) {
|
|
70
|
+
warn("Could not detect latest version from GitHub tags — falling back to main branch.");
|
|
71
|
+
version = "main";
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
version = latest;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const pluginDir = join(getPluginBaseDir(), version);
|
|
78
|
+
if (existsSync(pluginDir) && !opts.force) {
|
|
79
|
+
// Verify it has plugin.json (otherwise corrupted install)
|
|
80
|
+
const manifest = join(pluginDir, ".claude-plugin", "plugin.json");
|
|
81
|
+
if (existsSync(manifest)) {
|
|
82
|
+
return { installed: false, pluginDir, version, alreadyInstalled: true };
|
|
83
|
+
}
|
|
84
|
+
// Corrupted → remove and reinstall
|
|
85
|
+
warn(`Previous install at ${pluginDir} looks corrupted — reinstalling.`);
|
|
86
|
+
rmSync(pluginDir, { recursive: true, force: true });
|
|
87
|
+
}
|
|
88
|
+
mkdirSync(join(getPluginBaseDir()), { recursive: true });
|
|
89
|
+
log(dim(` cloning ${REPO_URL} into ${pluginDir}`));
|
|
90
|
+
const ref = /^[0-9]+\.[0-9]+\.[0-9]+$/.test(version) ? `v${version}` : version;
|
|
91
|
+
const result = spawnSync("git", ["clone", "--depth=1", "--branch", ref, REPO_URL, pluginDir], {
|
|
92
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
93
|
+
timeout: 120_000,
|
|
94
|
+
});
|
|
95
|
+
if (result.status !== 0) {
|
|
96
|
+
const stderr = result.stderr?.toString() ?? "";
|
|
97
|
+
// If branch/tag doesn't exist, try plain clone of main
|
|
98
|
+
if (stderr.includes("not found") || stderr.includes("Remote branch")) {
|
|
99
|
+
warn(`Tag ${ref} not found — cloning default branch.`);
|
|
100
|
+
rmSync(pluginDir, { recursive: true, force: true });
|
|
101
|
+
const r2 = spawnSync("git", ["clone", "--depth=1", REPO_URL, pluginDir], {
|
|
102
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
103
|
+
timeout: 120_000,
|
|
104
|
+
});
|
|
105
|
+
if (r2.status !== 0) {
|
|
106
|
+
throw new Error(`git clone failed: ${r2.stderr?.toString() ?? "unknown error"}`);
|
|
107
|
+
}
|
|
108
|
+
// Re-read version from actual plugin.json
|
|
109
|
+
version = readPluginVersion(pluginDir) ?? "main";
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
throw new Error(`git clone failed: ${stderr}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Sanity check: did we get a plugin?
|
|
116
|
+
const manifest = join(pluginDir, ".claude-plugin", "plugin.json");
|
|
117
|
+
if (!existsSync(manifest)) {
|
|
118
|
+
throw new Error(`Install appeared to succeed but ${manifest} is missing. Repo layout may have changed.`);
|
|
119
|
+
}
|
|
120
|
+
success(`plugin installed at ${pluginDir}`);
|
|
121
|
+
return { installed: true, pluginDir, version, alreadyInstalled: false };
|
|
122
|
+
}
|
|
123
|
+
function readPluginVersion(pluginDir) {
|
|
124
|
+
try {
|
|
125
|
+
const manifest = join(pluginDir, ".claude-plugin", "plugin.json");
|
|
126
|
+
const pkg = JSON.parse(readFileSync(manifest, "utf-8"));
|
|
127
|
+
return pkg.version ?? null;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// CLI entry: parse args, run the init flow.
|
|
2
|
+
//
|
|
3
|
+
// Flow:
|
|
4
|
+
// 1. banner
|
|
5
|
+
// 2. detect stack in cwd
|
|
6
|
+
// 3. pick archetype + compliance
|
|
7
|
+
// 4. confirm with user (unless -y)
|
|
8
|
+
// 5. install plugin (git clone)
|
|
9
|
+
// 6. enable in ~/.claude/settings.json
|
|
10
|
+
// 7. bootstrap .great_cto/PROJECT.md
|
|
11
|
+
// 8. print next steps
|
|
12
|
+
import { resolve } from "node:path";
|
|
13
|
+
import { banner, bold, cyan, dim, error, green, log, step, warn, yellow, confirm } from "./ui.js";
|
|
14
|
+
import { detect } from "./detect.js";
|
|
15
|
+
import { pickArchetype, suggestCompliance } from "./archetypes.js";
|
|
16
|
+
import { install, findInstalledVersions } from "./installer.js";
|
|
17
|
+
import { enableGreatCto } from "./settings.js";
|
|
18
|
+
import { bootstrap } from "./bootstrap.js";
|
|
19
|
+
function parseArgs(argv) {
|
|
20
|
+
const args = {
|
|
21
|
+
command: "init",
|
|
22
|
+
dir: process.cwd(),
|
|
23
|
+
yes: false,
|
|
24
|
+
dryRun: false,
|
|
25
|
+
force: false,
|
|
26
|
+
archetype: null,
|
|
27
|
+
version: null,
|
|
28
|
+
};
|
|
29
|
+
const rest = [];
|
|
30
|
+
for (let i = 0; i < argv.length; i++) {
|
|
31
|
+
const a = argv[i];
|
|
32
|
+
if (a === "-h" || a === "--help")
|
|
33
|
+
args.command = "help";
|
|
34
|
+
else if (a === "-v" || a === "--version")
|
|
35
|
+
args.command = "version";
|
|
36
|
+
else if (a === "-y" || a === "--yes")
|
|
37
|
+
args.yes = true;
|
|
38
|
+
else if (a === "--dry-run")
|
|
39
|
+
args.dryRun = true;
|
|
40
|
+
else if (a === "--force")
|
|
41
|
+
args.force = true;
|
|
42
|
+
else if (a === "--archetype")
|
|
43
|
+
args.archetype = argv[++i] ?? null;
|
|
44
|
+
else if (a === "--version-tag")
|
|
45
|
+
args.version = argv[++i] ?? null;
|
|
46
|
+
else if (a.startsWith("--dir="))
|
|
47
|
+
args.dir = a.slice("--dir=".length);
|
|
48
|
+
else if (a === "--dir")
|
|
49
|
+
args.dir = argv[++i] ?? args.dir;
|
|
50
|
+
else if (a === "init" || a === "help" || a === "version") {
|
|
51
|
+
args.command = a;
|
|
52
|
+
}
|
|
53
|
+
else
|
|
54
|
+
rest.push(a);
|
|
55
|
+
}
|
|
56
|
+
args.dir = resolve(args.dir);
|
|
57
|
+
return args;
|
|
58
|
+
}
|
|
59
|
+
function printHelp() {
|
|
60
|
+
log(`${bold("great-cto")} — one-command install for the great_cto Claude Code plugin
|
|
61
|
+
|
|
62
|
+
${bold("Usage:")}
|
|
63
|
+
npx great-cto init [options]
|
|
64
|
+
npx great-cto help
|
|
65
|
+
npx great-cto version
|
|
66
|
+
|
|
67
|
+
${bold("Options:")}
|
|
68
|
+
-y, --yes Skip confirmation prompts (non-interactive)
|
|
69
|
+
--dry-run Show what would be done without doing it
|
|
70
|
+
--force Reinstall even if already present
|
|
71
|
+
--archetype NAME Override detected archetype
|
|
72
|
+
(${cyan("web-service|mobile-app|ai-system|commerce|web3|")}
|
|
73
|
+
${cyan("data-platform|infra|library|iot-embedded|regulated")})
|
|
74
|
+
--version-tag VER Pin to specific great_cto version (default: latest)
|
|
75
|
+
--dir PATH Run against a different directory (default: cwd)
|
|
76
|
+
-h, --help Show this help
|
|
77
|
+
-v, --version Show great-cto CLI version
|
|
78
|
+
|
|
79
|
+
${bold("What it does:")}
|
|
80
|
+
1. Scans your project for stack signals (package.json, Cargo.toml, go.mod, etc.)
|
|
81
|
+
2. Picks the matching great_cto archetype (web-service, commerce, ai-system, ...)
|
|
82
|
+
3. Clones the plugin into ~/.claude/plugins/cache/local/great_cto/<version>/
|
|
83
|
+
4. Enables the plugin in ~/.claude/settings.json
|
|
84
|
+
5. Creates .great_cto/PROJECT.md pre-filled with archetype + detected stack
|
|
85
|
+
|
|
86
|
+
${bold("Next steps after install:")}
|
|
87
|
+
Restart Claude Code. Then run ${cyan("/inbox")} to see what needs attention,
|
|
88
|
+
or ${cyan("/audit")} for a full analysis of an existing codebase.
|
|
89
|
+
|
|
90
|
+
${bold("Links:")}
|
|
91
|
+
github.com/avelikiy/great_cto
|
|
92
|
+
`);
|
|
93
|
+
}
|
|
94
|
+
async function runInit(args) {
|
|
95
|
+
banner();
|
|
96
|
+
// ── 1. detect ────────────────────────────────────────────
|
|
97
|
+
step(1, 5, `scanning ${args.dir}`);
|
|
98
|
+
const detection = detect(args.dir);
|
|
99
|
+
if (detection.hasExistingGreatCto) {
|
|
100
|
+
warn(".great_cto/ already exists in this directory.");
|
|
101
|
+
warn("If you're re-initializing, back it up first or run with --force.");
|
|
102
|
+
if (!args.yes && !args.force) {
|
|
103
|
+
const ok = await confirm("Continue anyway?", false);
|
|
104
|
+
if (!ok) {
|
|
105
|
+
log("Aborted.");
|
|
106
|
+
return 1;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
log(` ${dim("stack:")} ${detection.stack.length > 0 ? detection.stack.join(", ") : dim("(no strong signals)")}`);
|
|
111
|
+
log(` ${dim("languages:")} ${detection.languages.join(", ") || dim("(none)")}`);
|
|
112
|
+
if (detection.packageManager)
|
|
113
|
+
log(` ${dim("package manager:")} ${detection.packageManager}`);
|
|
114
|
+
log(` ${dim("tests:")} ${detection.hasTests ? green("yes") : yellow("no")} ${dim("CI:")} ${detection.hasCI ? green("yes") : yellow("no")}`);
|
|
115
|
+
// ── 2. pick archetype ────────────────────────────────────
|
|
116
|
+
step(2, 5, "picking archetype");
|
|
117
|
+
let archetype;
|
|
118
|
+
let rationale;
|
|
119
|
+
let alternatives;
|
|
120
|
+
let confidence;
|
|
121
|
+
if (args.archetype) {
|
|
122
|
+
archetype = args.archetype;
|
|
123
|
+
rationale = "overridden via --archetype";
|
|
124
|
+
alternatives = [];
|
|
125
|
+
confidence = "user-specified";
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
const pick = pickArchetype(detection);
|
|
129
|
+
archetype = pick.primary;
|
|
130
|
+
rationale = pick.rationale;
|
|
131
|
+
alternatives = pick.alternatives;
|
|
132
|
+
confidence = pick.confidence;
|
|
133
|
+
}
|
|
134
|
+
const compliance = suggestCompliance(detection, archetype);
|
|
135
|
+
log(` ${dim("archetype:")} ${cyan(archetype)} ${dim(`(confidence: ${confidence})`)}`);
|
|
136
|
+
log(` ${dim("rationale:")} ${rationale}`);
|
|
137
|
+
if (alternatives.length > 0) {
|
|
138
|
+
log(` ${dim("alternatives:")} ${alternatives.join(", ")}`);
|
|
139
|
+
}
|
|
140
|
+
log(` ${dim("suggested compliance:")} ${compliance.length > 0 ? compliance.join(", ") : "none"}`);
|
|
141
|
+
// Confirmation
|
|
142
|
+
if (!args.yes) {
|
|
143
|
+
log("");
|
|
144
|
+
const ok = await confirm(bold("Install great_cto plugin and bootstrap this project?"), true);
|
|
145
|
+
if (!ok) {
|
|
146
|
+
log("Aborted.");
|
|
147
|
+
return 1;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (args.dryRun) {
|
|
151
|
+
log("");
|
|
152
|
+
log(yellow("dry-run: no changes made."));
|
|
153
|
+
log(` would install plugin into ~/.claude/plugins/cache/local/great_cto/<version>/`);
|
|
154
|
+
log(` would enable great_cto@local in ~/.claude/settings.json`);
|
|
155
|
+
log(` would create .great_cto/PROJECT.md with archetype=${archetype}`);
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
// ── 3. install plugin ────────────────────────────────────
|
|
159
|
+
step(3, 5, "installing plugin");
|
|
160
|
+
const existing = findInstalledVersions();
|
|
161
|
+
if (existing.length > 0 && !args.version && !args.force) {
|
|
162
|
+
log(` ${dim("already-installed versions:")} ${existing.join(", ")}`);
|
|
163
|
+
}
|
|
164
|
+
const installResult = install({
|
|
165
|
+
version: args.version ?? undefined,
|
|
166
|
+
force: args.force,
|
|
167
|
+
});
|
|
168
|
+
if (installResult.alreadyInstalled) {
|
|
169
|
+
log(` ${dim("version")} ${installResult.version} ${dim("already installed at")} ${installResult.pluginDir}`);
|
|
170
|
+
log(` ${dim("(use --force to reinstall)")}`);
|
|
171
|
+
}
|
|
172
|
+
// ── 4. enable in settings ────────────────────────────────
|
|
173
|
+
step(4, 5, "enabling plugin in ~/.claude/settings.json");
|
|
174
|
+
const enableResult = enableGreatCto();
|
|
175
|
+
if (enableResult.alreadyEnabled) {
|
|
176
|
+
log(` ${dim("already enabled in")} ${enableResult.settingsPath}`);
|
|
177
|
+
}
|
|
178
|
+
// ── 5. bootstrap ─────────────────────────────────────────
|
|
179
|
+
step(5, 5, "bootstrapping .great_cto/PROJECT.md");
|
|
180
|
+
const bs = bootstrap(args.dir, detection, archetype, compliance);
|
|
181
|
+
if (!bs.created) {
|
|
182
|
+
log(` ${dim("PROJECT.md already exists at")} ${bs.projectMdPath} ${dim("— kept as-is")}`);
|
|
183
|
+
}
|
|
184
|
+
// ── done ─────────────────────────────────────────────────
|
|
185
|
+
log("");
|
|
186
|
+
log(green(bold("✓ great_cto is ready.")));
|
|
187
|
+
log("");
|
|
188
|
+
log(bold("Next steps:"));
|
|
189
|
+
log(` 1. ${dim("Restart Claude Code to pick up the plugin.")}`);
|
|
190
|
+
log(` 2. ${dim("Edit")} ${cyan(".great_cto/PROJECT.md")} ${dim("to refine goals and compliance.")}`);
|
|
191
|
+
log(` 3. ${dim("In Claude Code, run:")} ${cyan("/inbox")} ${dim("— see what needs attention.")}`);
|
|
192
|
+
log(` 4. ${dim("For existing repos:")} ${cyan("/audit")} ${dim("— gap analysis + prioritized task backlog.")}`);
|
|
193
|
+
log(` 5. ${dim("For new features:")} ${cyan('/start "describe what you\'re building"')}`);
|
|
194
|
+
log("");
|
|
195
|
+
log(dim("Docs: https://github.com/avelikiy/great_cto"));
|
|
196
|
+
log("");
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
199
|
+
async function main() {
|
|
200
|
+
const args = parseArgs(process.argv.slice(2));
|
|
201
|
+
if (args.command === "help") {
|
|
202
|
+
printHelp();
|
|
203
|
+
process.exit(0);
|
|
204
|
+
}
|
|
205
|
+
if (args.command === "version") {
|
|
206
|
+
// Version resolved in index.mjs or from package.json at runtime
|
|
207
|
+
try {
|
|
208
|
+
const { readFileSync } = await import("node:fs");
|
|
209
|
+
const { dirname, join } = await import("node:path");
|
|
210
|
+
const { fileURLToPath } = await import("node:url");
|
|
211
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
212
|
+
// dist or src; package.json is two levels up
|
|
213
|
+
for (const base of [here, join(here, ".."), join(here, "..", "..")]) {
|
|
214
|
+
const p = join(base, "package.json");
|
|
215
|
+
try {
|
|
216
|
+
const pkg = JSON.parse(readFileSync(p, "utf-8"));
|
|
217
|
+
if (pkg.name === "great-cto" && pkg.version) {
|
|
218
|
+
log(pkg.version);
|
|
219
|
+
process.exit(0);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch { /* keep searching */ }
|
|
223
|
+
}
|
|
224
|
+
log("0.0.0");
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
log("0.0.0");
|
|
228
|
+
}
|
|
229
|
+
process.exit(0);
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
const code = await runInit(args);
|
|
233
|
+
process.exit(code);
|
|
234
|
+
}
|
|
235
|
+
catch (e) {
|
|
236
|
+
error(e.message);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
await main();
|
package/dist/settings.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Atomic merge of enabledPlugins into ~/.claude/settings.json.
|
|
2
|
+
// Preserves all other keys. Backup-aware.
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join, dirname } from "node:path";
|
|
6
|
+
import { dim, success, warn } from "./ui.js";
|
|
7
|
+
export function getSettingsPath() {
|
|
8
|
+
return join(homedir(), ".claude", "settings.json");
|
|
9
|
+
}
|
|
10
|
+
export function enableGreatCto() {
|
|
11
|
+
const path = getSettingsPath();
|
|
12
|
+
const pluginKey = "great_cto@local";
|
|
13
|
+
const backupPath = existsSync(path) ? `${path}.bak-${Date.now()}` : null;
|
|
14
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
15
|
+
// Read existing
|
|
16
|
+
let existing = {};
|
|
17
|
+
if (existsSync(path)) {
|
|
18
|
+
try {
|
|
19
|
+
const raw = readFileSync(path, "utf-8");
|
|
20
|
+
if (raw.trim())
|
|
21
|
+
existing = JSON.parse(raw);
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
warn(`${path} exists but is not valid JSON — leaving it alone.`);
|
|
25
|
+
warn(`Create or fix it manually and add: { "enabledPlugins": { "${pluginKey}": true } }`);
|
|
26
|
+
return { settingsPath: path, enabled: false, alreadyEnabled: false, backupPath: null };
|
|
27
|
+
}
|
|
28
|
+
if (backupPath)
|
|
29
|
+
copyFileSync(path, backupPath);
|
|
30
|
+
}
|
|
31
|
+
// Check if already enabled
|
|
32
|
+
const currentEnabled = existing["enabledPlugins"];
|
|
33
|
+
if (currentEnabled &&
|
|
34
|
+
typeof currentEnabled === "object" &&
|
|
35
|
+
currentEnabled[pluginKey] === true) {
|
|
36
|
+
return { settingsPath: path, enabled: false, alreadyEnabled: true, backupPath: null };
|
|
37
|
+
}
|
|
38
|
+
// Merge
|
|
39
|
+
const enabledPlugins = currentEnabled && typeof currentEnabled === "object"
|
|
40
|
+
? { ...currentEnabled }
|
|
41
|
+
: {};
|
|
42
|
+
enabledPlugins[pluginKey] = true;
|
|
43
|
+
existing["enabledPlugins"] = enabledPlugins;
|
|
44
|
+
// Atomic write: write to temp, rename
|
|
45
|
+
const tmp = `${path}.tmp-${Date.now()}`;
|
|
46
|
+
writeFileSync(tmp, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
47
|
+
const { renameSync } = require("node:fs");
|
|
48
|
+
renameSync(tmp, path);
|
|
49
|
+
if (backupPath) {
|
|
50
|
+
success(`enabled ${pluginKey} in ~/.claude/settings.json ${dim(`(backup: ${backupPath})`)}`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
success(`created ~/.claude/settings.json with ${pluginKey} enabled`);
|
|
54
|
+
}
|
|
55
|
+
return { settingsPath: path, enabled: true, alreadyEnabled: false, backupPath };
|
|
56
|
+
}
|
package/dist/ui.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Minimal terminal UI: colors, logging, prompts. Zero deps.
|
|
2
|
+
const isTTY = process.stdout.isTTY && process.env.NO_COLOR !== "1";
|
|
3
|
+
function wrap(code) {
|
|
4
|
+
return (s) => (isTTY ? `\x1b[${code}m${s}\x1b[0m` : s);
|
|
5
|
+
}
|
|
6
|
+
export const bold = wrap("1");
|
|
7
|
+
export const dim = wrap("2");
|
|
8
|
+
export const red = wrap("31");
|
|
9
|
+
export const green = wrap("32");
|
|
10
|
+
export const yellow = wrap("33");
|
|
11
|
+
export const blue = wrap("34");
|
|
12
|
+
export const magenta = wrap("35");
|
|
13
|
+
export const cyan = wrap("36");
|
|
14
|
+
export const gray = wrap("90");
|
|
15
|
+
export function log(msg = "") {
|
|
16
|
+
process.stdout.write(msg + "\n");
|
|
17
|
+
}
|
|
18
|
+
export function error(msg) {
|
|
19
|
+
process.stderr.write(red("error: ") + msg + "\n");
|
|
20
|
+
}
|
|
21
|
+
export function warn(msg) {
|
|
22
|
+
process.stderr.write(yellow("warning: ") + msg + "\n");
|
|
23
|
+
}
|
|
24
|
+
export function step(n, total, msg) {
|
|
25
|
+
log(cyan(`[${n}/${total}]`) + " " + msg);
|
|
26
|
+
}
|
|
27
|
+
export function success(msg) {
|
|
28
|
+
log(green("✓") + " " + msg);
|
|
29
|
+
}
|
|
30
|
+
export function banner() {
|
|
31
|
+
if (!isTTY) {
|
|
32
|
+
log("great-cto");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
log("");
|
|
36
|
+
log(bold(cyan(" great_cto")) + dim(" — SDLC pipeline plugin for Claude Code"));
|
|
37
|
+
log(dim(" https://github.com/avelikiy/great_cto"));
|
|
38
|
+
log("");
|
|
39
|
+
}
|
|
40
|
+
export async function confirm(question, defaultYes = true) {
|
|
41
|
+
if (!process.stdin.isTTY)
|
|
42
|
+
return defaultYes;
|
|
43
|
+
const hint = defaultYes ? dim("[Y/n]") : dim("[y/N]");
|
|
44
|
+
process.stdout.write(question + " " + hint + " ");
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
process.stdin.setEncoding("utf-8");
|
|
47
|
+
process.stdin.once("data", (data) => {
|
|
48
|
+
const ans = String(data).trim().toLowerCase();
|
|
49
|
+
if (ans === "")
|
|
50
|
+
resolve(defaultYes);
|
|
51
|
+
else
|
|
52
|
+
resolve(ans === "y" || ans === "yes");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
package/index.mjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Entry point. Loads compiled src/main.js or runs src/main.ts directly in dev.
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const compiled = join(__dirname, "dist", "main.js");
|
|
9
|
+
const source = join(__dirname, "src", "main.ts");
|
|
10
|
+
|
|
11
|
+
if (existsSync(compiled)) {
|
|
12
|
+
await import(compiled);
|
|
13
|
+
} else if (existsSync(source)) {
|
|
14
|
+
// Node 22 supports --experimental-strip-types via import for .ts
|
|
15
|
+
await import(source);
|
|
16
|
+
} else {
|
|
17
|
+
console.error("great-cto: no entry point found. Did you run `npm run build`?");
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "great-cto",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "One command install for the great_cto Claude Code plugin. Auto-detects your stack, picks the right archetype, bootstraps PROJECT.md.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude-code",
|
|
7
|
+
"plugin",
|
|
8
|
+
"sdlc",
|
|
9
|
+
"agent",
|
|
10
|
+
"ai",
|
|
11
|
+
"cli",
|
|
12
|
+
"installer",
|
|
13
|
+
"cto",
|
|
14
|
+
"great-cto"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/avelikiy/great_cto",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Oleksandr Velykyi",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/avelikiy/great_cto.git",
|
|
22
|
+
"directory": "packages/cli"
|
|
23
|
+
},
|
|
24
|
+
"bin": {
|
|
25
|
+
"great-cto": "index.mjs"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"index.mjs",
|
|
29
|
+
"dist/",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
32
|
+
"type": "module",
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc",
|
|
35
|
+
"test": "node --test 'tests/*.test.ts'",
|
|
36
|
+
"prepublishOnly": "npm run build"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "22.15.3",
|
|
40
|
+
"typescript": "5.8.3"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=22.6.0"
|
|
44
|
+
}
|
|
45
|
+
}
|