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 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();
@@ -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
+ }