great-cto 3.8.0 → 3.9.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.
@@ -2,7 +2,7 @@
2
2
  "name": "great_cto",
3
3
  "id": "great_cto",
4
4
  "description": "Engineering process for solo founders and teams up to 50 engineers. Agents do architecture, code review, QA, and security. You make two decisions per feature.",
5
- "version": "3.8.0",
5
+ "version": "3.9.0",
6
6
  "author": {
7
7
  "name": "Great CTO",
8
8
  "url": "https://github.com/avelikiy/great_cto"
@@ -416,6 +416,31 @@ function resolveProjectInfo(slugOrPath) {
416
416
  const matches = reg.projects.filter(p => p.slug === slugOrPath);
417
417
  const found = pickBestBySlug(matches);
418
418
  if (found) return { cwd: found.path, resolved: 'slug' };
419
+
420
+ // The identifier the board SHOWS and the identifier it RESOLVES were two
421
+ // different things.
422
+ //
423
+ // `listProjects()` derives a display slug at :184 —
424
+ // `project` / `name` / basename(dir) — while resolution matched only
425
+ // `p.slug` as stored in the registry. A project listed as `<private-project>`
426
+ // (basename of its directory) had a different slug on disk, so asking for the
427
+ // name the UI itself printed resolved to nothing and fell back to the server's
428
+ // own project: one project's session logs served under another's name. The
429
+ // fallback header said so, which is why this was a wrong answer rather than a
430
+ // silent one — but a caller that asks by the name it was given should not have
431
+ // to read a header to learn it was ignored.
432
+ //
433
+ // So the derived identifier is accepted too, and only after the stored one
434
+ // fails. Basename matching is last because it is the weakest claim: two
435
+ // directories can share a name, and pickBestBySlug already knows how to
436
+ // choose between candidates.
437
+ const byDerived = reg.projects.filter((p) => {
438
+ if (!p?.path) return false;
439
+ return path.basename(p.path) === slugOrPath;
440
+ });
441
+ const derived = pickBestBySlug(byDerived);
442
+ if (derived) return { cwd: derived.path, resolved: 'slug' };
443
+
419
444
  return { cwd: process.cwd(), resolved: 'fallback', requested: slugOrPath };
420
445
  }
421
446
 
package/dist/installer.js CHANGED
@@ -1,9 +1,11 @@
1
1
  // Install the great_cto plugin into ~/.claude/plugins/cache/local/great_cto/<version>/.
2
2
  // Uses git clone. Falls back to tarball fetch if git is unavailable.
3
3
  import { spawnSync, execFileSync } from "node:child_process";
4
+ import { cpSync } from "node:fs";
4
5
  import { existsSync, mkdirSync, rmSync, readFileSync, readdirSync } from "node:fs";
5
6
  import { homedir } from "node:os";
6
- import { join } from "node:path";
7
+ import { dirname, join } from "node:path";
8
+ import { fileURLToPath } from "node:url";
7
9
  import { log, success, warn, dim } from "./ui.js";
8
10
  const REPO_URL = "https://github.com/avelikiy/great_cto.git";
9
11
  export function hasGit() {
@@ -113,14 +115,81 @@ export function install(opts = {}) {
113
115
  throw new Error(`git clone failed: ${stderr}`);
114
116
  }
115
117
  }
116
- // Sanity check: did we get a plugin?
117
- const manifest = join(pluginDir, ".claude-plugin", "plugin.json");
118
- if (!existsSync(manifest)) {
119
- throw new Error(`Install appeared to succeed but ${manifest} is missing. Repo layout may have changed.`);
118
+ // A clone carries sources; the plugin runs on the build. Supply it from this
119
+ // CLI's own dist before checking whether the result can run.
120
+ const supplied = supplyBuiltDist(pluginDir);
121
+ if (supplied != null)
122
+ log(dim(` supplied ${supplied} built file(s) into packages/cli/dist/`));
123
+ // Sanity check: can it RUN, not merely "did files arrive".
124
+ const missing = missingRuntimeParts(pluginDir);
125
+ if (missing.length) {
126
+ throw new Error(`Install appeared to succeed but the plugin cannot run — missing:\n` +
127
+ missing.map((m) => ` - ${m}`).join("\n") +
128
+ `\nThis is an install bug, not a configuration problem. Please report it with this list.`);
120
129
  }
121
130
  success(`plugin installed at ${pluginDir}`);
122
131
  return { installed: true, pluginDir, version, alreadyInstalled: false };
123
132
  }
133
+ /**
134
+ * The plugin needs BUILT JavaScript, and a git clone does not contain any.
135
+ *
136
+ * `packages/cli/dist/` is a build artefact. Five of its thirty-two files are in
137
+ * git by accident; the rest, including `archetypes.js`, are not. So a cloned
138
+ * plugin gets 5 of 32, and `scripts/lib/gate-plan.mjs` — which the board's
139
+ * project reader imports — dies on
140
+ *
141
+ * ERR_MODULE_NOT_FOUND … packages/cli/dist/archetypes.js
142
+ *
143
+ * The board therefore did not start for anyone installing this the documented
144
+ * way. It started for the author, whose plugin cache is populated by
145
+ * `install-local.sh` from a working tree with a full local build, and it started
146
+ * from the npm tarball, which ships all 32. It failed on exactly one path: the
147
+ * one a new user takes.
148
+ *
149
+ * The build is not fetched or rebuilt — it is already here. This CLI IS the
150
+ * published package, so the version being installed and the version doing the
151
+ * installing are the same artefacts. Copying them across is both the cheapest
152
+ * source and the only one that cannot drift.
153
+ *
154
+ * @returns how many files were supplied, or null when this CLI has no dist of
155
+ * its own to give (running from source in the monorepo, where the clone is
156
+ * not what gets used anyway).
157
+ */
158
+ function supplyBuiltDist(pluginDir) {
159
+ const here = dirname(fileURLToPath(import.meta.url)); // …/dist
160
+ const target = join(pluginDir, "packages", "cli", "dist");
161
+ try {
162
+ if (!existsSync(join(here, "archetypes.js")))
163
+ return null;
164
+ mkdirSync(target, { recursive: true });
165
+ cpSync(here, target, { recursive: true });
166
+ return readdirSync(target).length;
167
+ }
168
+ catch {
169
+ return null;
170
+ }
171
+ }
172
+ /**
173
+ * Can the plugin actually run, or did we merely receive files?
174
+ *
175
+ * The check this replaces asked whether `.claude-plugin/plugin.json` exists —
176
+ * "did we get a plugin?" — which a clone always satisfies while the board is
177
+ * still unable to start. A sanity check that a broken install passes is not a
178
+ * sanity check.
179
+ *
180
+ * @returns a list of what is missing; empty means runnable.
181
+ */
182
+ export function missingRuntimeParts(pluginDir) {
183
+ const required = [
184
+ [".claude-plugin/plugin.json", "the plugin manifest"],
185
+ ["packages/board/server.mjs", "the board server"],
186
+ ["packages/cli/dist/archetypes.js", "the built archetype table the board imports"],
187
+ ["scripts/lib/gate-plan.mjs", "the gate planner"],
188
+ ];
189
+ return required
190
+ .filter(([rel]) => !existsSync(join(pluginDir, rel)))
191
+ .map(([rel, what]) => `${rel} (${what})`);
192
+ }
124
193
  function readPluginVersion(pluginDir) {
125
194
  try {
126
195
  const manifest = join(pluginDir, ".claude-plugin", "plugin.json");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "3.8.0",
3
+ "version": "3.9.0",
4
4
  "description": "One command install for the great_cto Claude Code plugin. Auto-detects your stack, picks the right archetype, bootstraps PROJECT.md.",
5
5
  "keywords": [
6
6
  "claude-code",