karajan-code 2.7.3 → 2.8.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 +4 -4
- package/package.json +24 -4
- package/scripts/diet-baseline.mjs +170 -0
- package/scripts/regen-arch-stats.sh +71 -0
- package/src/agents/availability.js +15 -9
- package/src/agents/claude-agent.js +143 -45
- package/src/agents/model-registry.js +1 -1
- package/src/audit/agent-readiness.js +240 -0
- package/src/bootstrap.js +26 -1
- package/src/brain/decision-tracker.js +3 -1
- package/src/brain/decisor.js +1 -1
- package/src/canvas/parse-md.js +238 -0
- package/src/canvas/schema.js +101 -0
- package/src/canvas/to-flat.js +85 -0
- package/src/canvas/validate.js +163 -0
- package/src/checks/binaries.js +1 -1
- package/src/checks/ports.js +97 -27
- package/src/checks/runner.js +26 -10
- package/src/checks/sonar.js +1 -1
- package/src/checks/system.js +1 -1
- package/src/checks/tokens.js +77 -24
- package/src/cli/_shared.js +30 -0
- package/src/cli/register-meta.js +204 -0
- package/src/cli/register-pipeline.js +212 -0
- package/src/cli/register-plan.js +152 -0
- package/src/cli/register-roles-skills.js +38 -0
- package/src/cli/register-sonar.js +28 -0
- package/src/cli.js +83 -468
- package/src/commands/agents.js +18 -6
- package/src/commands/architect.js +29 -2
- package/src/commands/audit.js +27 -3
- package/src/commands/board.js +160 -30
- package/src/commands/clean.js +83 -0
- package/src/commands/code.js +7 -2
- package/src/commands/discover.js +7 -2
- package/src/commands/init.js +2 -2
- package/src/commands/plan/_shared.js +40 -0
- package/src/commands/plan/add-hu.js +21 -0
- package/src/commands/plan/delete.js +9 -0
- package/src/commands/plan/generate.js +284 -0
- package/src/commands/plan/index.js +14 -0
- package/src/commands/plan/list.js +21 -0
- package/src/commands/plan/ready.js +46 -0
- package/src/commands/plan/remove-hu.js +17 -0
- package/src/commands/plan/show.js +81 -0
- package/src/commands/plan/validate.js +20 -0
- package/src/commands/plan.js +14 -291
- package/src/commands/researcher.js +7 -2
- package/src/commands/resume.js +24 -1
- package/src/commands/review.js +7 -2
- package/src/commands/run.js +52 -1
- package/src/commands/status.js +1 -1
- package/src/commands/sync.js +75 -0
- package/src/commands/triage.js +7 -2
- package/src/commands/undo.js +1 -1
- package/src/config/defaults.js +213 -0
- package/src/config/loader.js +137 -0
- package/src/config/overrides.js +199 -0
- package/src/config/role-resolver.js +159 -0
- package/src/config/test-harness.js +79 -0
- package/src/config.js +30 -609
- package/src/git/automation.js +2 -2
- package/src/guards/output-guard.js +0 -2
- package/src/hu/acceptance-runner.js +52 -10
- package/src/hu/store.js +11 -0
- package/src/mcp/handlers/direct-handlers.js +2 -5
- package/src/mcp/handlers/management-handlers.js +48 -4
- package/src/mcp/handlers/run-handler.js +9 -8
- package/src/mcp/preflight.js +32 -13
- package/src/mcp/server-handlers.js +4 -3
- package/src/mcp/suggest-handler.js +3 -2
- package/src/mcp/tools.js +15 -0
- package/src/orchestrator/agent-fallback.js +1 -1
- package/src/orchestrator/ci-integration.js +10 -10
- package/src/orchestrator/config-init.js +25 -7
- package/src/orchestrator/direct-actions.js +6 -2
- package/src/orchestrator/drivers/error-recovery.js +287 -0
- package/src/orchestrator/drivers/init-context.js +222 -0
- package/src/orchestrator/drivers/iteration-loop.js +311 -0
- package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +48 -0
- package/src/orchestrator/drivers/iteration-phases/guards.js +84 -0
- package/src/orchestrator/drivers/iteration-phases/handle-approved.js +29 -0
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +76 -0
- package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +60 -0
- package/src/orchestrator/drivers/post-loop.js +370 -0
- package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +116 -0
- package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +47 -0
- package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +84 -0
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +268 -0
- package/src/orchestrator/drivers/pre-loop.js +434 -0
- package/src/orchestrator/drivers/run-hu-batch.js +367 -0
- package/src/orchestrator/flow-control.js +4 -3
- package/src/orchestrator/flow-runner.js +121 -2019
- package/src/orchestrator/fs-leak-detector.js +156 -0
- package/src/orchestrator/hu-sub-pipeline.js +197 -9
- package/src/orchestrator/integrations.js +88 -0
- package/src/orchestrator/post-loop-stages.js +35 -8
- package/src/orchestrator/preflight-checks.js +30 -3
- package/src/orchestrator/repair/repair-detector.js +117 -0
- package/src/orchestrator/repair/repair-runner.js +142 -0
- package/src/orchestrator/reviewer-fallback.js +1 -1
- package/src/orchestrator/solomon-escalation.js +3 -2
- package/src/orchestrator/solomon-rules.js +7 -3
- package/src/orchestrator/stages/architect-stage.js +12 -11
- package/src/orchestrator/stages/coder-stage.js +56 -7
- package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +127 -0
- package/src/orchestrator/stages/hu-reviewer-stage.js +6 -79
- package/src/orchestrator/stages/planner-stage.js +1 -1
- package/src/orchestrator/stages/research-stage.js +1 -1
- package/src/orchestrator/stages/reviewer-stage.js +7 -7
- package/src/orchestrator/stages/sonar-stage.js +94 -10
- package/src/orchestrator/stages/stage-classes.js +86 -0
- package/src/orchestrator/stages/stage-executor.js +1 -1
- package/src/orchestrator/stages/triage-stage.js +1 -1
- package/src/orchestrator/standby.js +3 -2
- package/src/orchestrator/verification-gate.js +20 -12
- package/src/plan/adr-generator.js +170 -0
- package/src/plan/adr-loader.js +60 -0
- package/src/plan/plan-executor.js +19 -1
- package/src/plan/plan-hu-ops.js +170 -1
- package/src/plan/plan-reviewer.js +199 -0
- package/src/plan/plan-schema.js +69 -1
- package/src/plan/plan-store.js +40 -3
- package/src/plan/spec-detector.js +61 -0
- package/src/plan/tests-synthesizer.js +116 -0
- package/src/planning-game/client.js +3 -14
- package/src/planning-game/decomposition.js +1 -1
- package/src/planning-game/pipeline-adapter.js +36 -5
- package/src/prompts/coder.js +172 -1
- package/src/prompts/hu-reviewer.js +0 -1
- package/src/prompts/planner.js +81 -4
- package/src/review/diff-generator.js +35 -22
- package/src/review/snapshot-diff.js +1 -1
- package/src/roles/coder-role.js +25 -3
- package/src/roles/planner-role.js +42 -1
- package/src/roles/repairer-role.js +181 -0
- package/src/roles/tester-role.js +70 -4
- package/src/session/mutators.js +304 -0
- package/src/session/runtime-overrides.js +59 -0
- package/src/{session-store.js → session/store.js} +8 -2
- package/src/skills/semantic-detector.js +10 -5
- package/src/skills/skill-detector.js +1 -1
- package/src/sonar/cloud-scanner.js +0 -1
- package/src/sonar/credentials.js +14 -6
- package/src/sonar/discovery.js +123 -0
- package/src/sonar/manager.js +18 -0
- package/src/sync/detect-drift.js +113 -0
- package/src/tracker-bootstrap.js +44 -0
- package/src/utils/agent-detect.js +15 -11
- package/src/utils/board-prompt-bridge.js +109 -0
- package/src/utils/cli-progress.js +161 -0
- package/src/utils/cwd-confirm.js +39 -0
- package/src/utils/derive-project-name-from-cwd.js +119 -0
- package/src/utils/garbage-collector.js +396 -0
- package/src/utils/git.js +16 -4
- package/src/utils/locale.js +1 -1
- package/src/utils/paths.js +21 -1
- package/src/utils/process.js +1 -1
- package/src/utils/prompt-project-name.js +35 -0
- package/src/utils/run-context.js +75 -0
- package/src/utils/run-log.js +12 -0
- package/src/utils/task-file.js +1 -1
- package/src/utils/telemetry.js +13 -1
- package/src/webperf/devtools-detect.js +1 -1
- package/templates/coder-rules.md +13 -0
- package/templates/roles/coder.md +34 -0
- package/templates/roles/repairer.md +94 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI progress reporter — prints live agent activity to the terminal while
|
|
3
|
+
* an agent (planner, coder, reviewer, auditor, …) is thinking.
|
|
4
|
+
*
|
|
5
|
+
* Context: commands like `kj plan --task-file spec.md` used to call
|
|
6
|
+
* `agent.runTask(...)` without an `onOutput` callback. The CLI then hung
|
|
7
|
+
* for 30 s – 3 min with zero feedback until a line like "Plan saved: …"
|
|
8
|
+
* finally appeared. Users assumed the command had deadlocked.
|
|
9
|
+
*
|
|
10
|
+
* This helper gives every CLI command the same live feedback the `kj run`
|
|
11
|
+
* orchestrator already emits: a header with role + start, a stream of
|
|
12
|
+
* tool calls / short text lines as the agent works, and a footer with
|
|
13
|
+
* outcome + elapsed time. It wraps `onOutput` so every command can pass
|
|
14
|
+
* `createCliProgressReporter({ role: "planner" }).onOutput` straight
|
|
15
|
+
* into the agent's runTask / reviewTask call.
|
|
16
|
+
*
|
|
17
|
+
* Output shape:
|
|
18
|
+
*
|
|
19
|
+
* [planner] starting...
|
|
20
|
+
* ▸ Read SPEC.md
|
|
21
|
+
* ▸ Grep "architecture"
|
|
22
|
+
* · Analyzing sections 1–5…
|
|
23
|
+
* ▸ Write plan.json
|
|
24
|
+
* [planner] done (42.8s)
|
|
25
|
+
*
|
|
26
|
+
* Design notes:
|
|
27
|
+
* - Writes to `process.stderr` by default (so stdout is reserved for
|
|
28
|
+
* structured output like `--json` mode).
|
|
29
|
+
* - `onOutput` is shaped as `{ stream, line, kind }` to match the
|
|
30
|
+
* contract Claude's `createStreamJsonFilter` emits — so passing
|
|
31
|
+
* this reporter's `onOutput` into a runTask{…, onOutput} call Just
|
|
32
|
+
* Works with Claude, Codex, Gemini, Aider and OpenCode (each one's
|
|
33
|
+
* agent wrapper already normalizes its CLI's stream format into
|
|
34
|
+
* that shape).
|
|
35
|
+
* - `kind` is optional; when absent the default bullet is used.
|
|
36
|
+
* - `quiet: true` turns the reporter into a no-op (useful when the
|
|
37
|
+
* command is running under `--json` and should not pollute stdout).
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
const ICONS = {
|
|
41
|
+
tool: "▸", // ▸ right-pointing small triangle
|
|
42
|
+
text: "·", // · middle dot
|
|
43
|
+
thinking: "…", // …
|
|
44
|
+
heartbeat: "⠿", // braille dots — distinct from real activity icons
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Rotating idle messages so the user knows the process is alive without
|
|
49
|
+
* the same string repeating every minute. Picked at random each tick;
|
|
50
|
+
* the previous one is held back so two ticks in a row never duplicate.
|
|
51
|
+
*/
|
|
52
|
+
const HEARTBEAT_LINES = [
|
|
53
|
+
"still working…",
|
|
54
|
+
"thinking through it…",
|
|
55
|
+
"wading through tokens…",
|
|
56
|
+
"asking the model nicely…",
|
|
57
|
+
"crunching the spec…",
|
|
58
|
+
"burning down the prompt…",
|
|
59
|
+
"kneading the context window…",
|
|
60
|
+
"still here, give me a sec…",
|
|
61
|
+
"the LLM is doing LLM things…",
|
|
62
|
+
"drafting, redrafting…",
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Default cadence for the idle heartbeat. ~45s is short enough to feel
|
|
67
|
+
* responsive on a quiet planner run (Claude can think for 1-2 min on a
|
|
68
|
+
* complex spec) without spamming every few seconds.
|
|
69
|
+
*/
|
|
70
|
+
const DEFAULT_HEARTBEAT_MS = 45_000;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @typedef {Object} CliProgressReporter
|
|
74
|
+
* @property {(event: { stream?: string, line: string, kind?: string }) => void} onOutput
|
|
75
|
+
* @property {(outcome?: string) => void} finish
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {Object} [opts]
|
|
80
|
+
* @param {string} [opts.role] - label shown in the header/footer ("planner", "auditor", …).
|
|
81
|
+
* Default: "agent".
|
|
82
|
+
* @param {NodeJS.WriteStream} [opts.stream] - output destination. Default: process.stderr.
|
|
83
|
+
* @param {boolean} [opts.quiet] - when true, nothing is printed (useful for --json mode).
|
|
84
|
+
* @param {number} [opts.heartbeatMs] - silent interval before printing an
|
|
85
|
+
* "alive" line. Defaults to 45 s. Set to 0 to disable the heartbeat
|
|
86
|
+
* (useful in tests).
|
|
87
|
+
* @returns {CliProgressReporter}
|
|
88
|
+
*/
|
|
89
|
+
export function createCliProgressReporter(opts = {}) {
|
|
90
|
+
const {
|
|
91
|
+
role = "agent",
|
|
92
|
+
stream = process.stderr,
|
|
93
|
+
quiet = false,
|
|
94
|
+
heartbeatMs = DEFAULT_HEARTBEAT_MS,
|
|
95
|
+
} = opts;
|
|
96
|
+
|
|
97
|
+
if (quiet) {
|
|
98
|
+
return { onOutput: () => {}, finish: () => {} };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const startedAt = Date.now();
|
|
102
|
+
stream.write(`[${role}] starting...\n`);
|
|
103
|
+
|
|
104
|
+
// Idle heartbeat: a periodic timer that prints a varied "still alive"
|
|
105
|
+
// line whenever the agent goes quiet for longer than `heartbeatMs`.
|
|
106
|
+
// Reset on every real onOutput so heavy chatter doesn't trigger the
|
|
107
|
+
// duplicate. Always elapses to a different message than last time so
|
|
108
|
+
// two consecutive ticks never read the same.
|
|
109
|
+
let lastActivityAt = Date.now();
|
|
110
|
+
let lastHeartbeatIdx = -1;
|
|
111
|
+
let heartbeatTimer = null;
|
|
112
|
+
|
|
113
|
+
function pickHeartbeatLine() {
|
|
114
|
+
let idx;
|
|
115
|
+
do {
|
|
116
|
+
idx = Math.floor(Math.random() * HEARTBEAT_LINES.length);
|
|
117
|
+
} while (idx === lastHeartbeatIdx && HEARTBEAT_LINES.length > 1);
|
|
118
|
+
lastHeartbeatIdx = idx;
|
|
119
|
+
return HEARTBEAT_LINES[idx];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function tick() {
|
|
123
|
+
const idleFor = Date.now() - lastActivityAt;
|
|
124
|
+
if (idleFor < heartbeatMs) return; // real activity reset us
|
|
125
|
+
const elapsed = Math.round((Date.now() - startedAt) / 1000);
|
|
126
|
+
stream.write(` ${ICONS.heartbeat} ${pickHeartbeatLine()} (${elapsed}s)\n`);
|
|
127
|
+
lastActivityAt = Date.now(); // collapse next ticks
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (heartbeatMs > 0) {
|
|
131
|
+
heartbeatTimer = setInterval(tick, heartbeatMs);
|
|
132
|
+
if (typeof heartbeatTimer.unref === "function") heartbeatTimer.unref();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const onOutput = (event) => {
|
|
136
|
+
if (!event || typeof event !== "object") return;
|
|
137
|
+
const raw = typeof event.line === "string" ? event.line : "";
|
|
138
|
+
if (!raw) return;
|
|
139
|
+
// Strip trailing newline — we re-add our own.
|
|
140
|
+
const line = raw.replace(/\s+$/, "");
|
|
141
|
+
if (!line) return;
|
|
142
|
+
lastActivityAt = Date.now();
|
|
143
|
+
const icon = ICONS[event.kind] || ICONS.text;
|
|
144
|
+
stream.write(` ${icon} ${line}\n`);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @param {string} [outcome] default "done". Use "failed", "stopped", "timeout" etc.
|
|
149
|
+
*/
|
|
150
|
+
const finish = (outcome = "done") => {
|
|
151
|
+
if (heartbeatTimer) {
|
|
152
|
+
clearInterval(heartbeatTimer);
|
|
153
|
+
heartbeatTimer = null;
|
|
154
|
+
}
|
|
155
|
+
const ms = Date.now() - startedAt;
|
|
156
|
+
const s = (ms / 1000).toFixed(1);
|
|
157
|
+
stream.write(`[${role}] ${outcome} (${s}s)\n`);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
return { onOutput, finish };
|
|
161
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PR-F: ask the user "Run on $(pwd)?" before launching `kj run` from
|
|
3
|
+
* a terminal. Same defensive UX as `git push --force-with-lease`
|
|
4
|
+
* surfacing the branch — the user wants to know what they're aiming
|
|
5
|
+
* at before the rocket fires.
|
|
6
|
+
*
|
|
7
|
+
* The prompt is plain readline (no extra deps) and accepts any of:
|
|
8
|
+
* y, yes, s, sí, ENTER → true
|
|
9
|
+
* n, no, ENTER+Esc → false
|
|
10
|
+
*
|
|
11
|
+
* Skipped automatically by the caller when stdin/stdout are not a
|
|
12
|
+
* TTY, when --yes is set, or when --json is set. So this module is
|
|
13
|
+
* never called in CI / non-interactive contexts.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import readline from "node:readline";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} projectDir absolute path of the cwd / projectDir
|
|
20
|
+
* @returns {Promise<boolean>} true to continue, false to abort
|
|
21
|
+
*/
|
|
22
|
+
export async function confirmCwd(projectDir) {
|
|
23
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
24
|
+
// No emoji — keep terminal-safe even on basic Windows shells.
|
|
25
|
+
const question = `Karajan va a ejecutarse sobre:\n ${projectDir}\n\n¿Continuar? [Y/n] `;
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
rl.question(question, (answer) => {
|
|
28
|
+
rl.close();
|
|
29
|
+
const trimmed = String(answer || "").trim().toLowerCase();
|
|
30
|
+
// Empty answer = ENTER = accept the default = yes.
|
|
31
|
+
if (trimmed === "" || trimmed === "y" || trimmed === "yes" || trimmed === "s" || trimmed === "si" || trimmed === "sí") {
|
|
32
|
+
resolve(true);
|
|
33
|
+
} else {
|
|
34
|
+
resolve(false);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
rl.on("SIGINT", () => { rl.close(); resolve(false); });
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive a sensible default project name from the working directory,
|
|
3
|
+
* trying several signals in order:
|
|
4
|
+
*
|
|
5
|
+
* 1. `package.json` "name" field (most authoritative — that's what
|
|
6
|
+
* the user already chose for their npm/pnpm project).
|
|
7
|
+
* 2. Git remote URL basename (strip protocol, user, `.git` suffix —
|
|
8
|
+
* e.g. `git@github.com:foo/weather-dashboard.git` → `weather-dashboard`).
|
|
9
|
+
* 3. The cwd directory basename.
|
|
10
|
+
*
|
|
11
|
+
* Any of these is then prettified: kebab/snake → spaces, words
|
|
12
|
+
* capitalised. So `weather-dashboard` becomes `Weather Dashboard`.
|
|
13
|
+
*
|
|
14
|
+
* Returns null when no signal yields anything useful — caller can then
|
|
15
|
+
* fall back to its old "derive from task text" heuristic. This keeps
|
|
16
|
+
* the function pure / sync-friendly even though git/fs lookups are
|
|
17
|
+
* sync — they're cheap on a single-shot CLI startup.
|
|
18
|
+
*
|
|
19
|
+
* Why this matters: pre-fix, running `kj plan generate --task-file
|
|
20
|
+
* SPEC.md` inside `~/ws_demo/weather-dashboard` (an initialised git
|
|
21
|
+
* repo) suggested a project name made from the words of the SPEC's
|
|
22
|
+
* first sentence. That ignored both the directory the user was
|
|
23
|
+
* standing in and the repo they had just `git init`-ed. The right
|
|
24
|
+
* default is "the project I'm in", not "the topic of the document
|
|
25
|
+
* I just wrote".
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import fs from "node:fs";
|
|
29
|
+
import path from "node:path";
|
|
30
|
+
import { execFileSync } from "node:child_process";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Turn `weather-dashboard` / `weather_dashboard` / `WeatherDashboard`
|
|
34
|
+
* into `Weather Dashboard`. Preserves single-token names as-is when
|
|
35
|
+
* already title-cased.
|
|
36
|
+
*/
|
|
37
|
+
function prettifyName(raw) {
|
|
38
|
+
if (!raw || typeof raw !== "string") return null;
|
|
39
|
+
const trimmed = raw.trim();
|
|
40
|
+
if (!trimmed) return null;
|
|
41
|
+
// Insert space at camelCase boundaries, then split on -/_/whitespace.
|
|
42
|
+
const spaced = trimmed
|
|
43
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
44
|
+
.replace(/[-_]+/g, " ")
|
|
45
|
+
.replace(/\s+/g, " ")
|
|
46
|
+
.trim();
|
|
47
|
+
if (!spaced) return null;
|
|
48
|
+
return spaced
|
|
49
|
+
.split(" ")
|
|
50
|
+
.map((w) => (w.length ? w[0].toUpperCase() + w.slice(1) : w))
|
|
51
|
+
.join(" ");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readPackageJsonName(cwd) {
|
|
55
|
+
try {
|
|
56
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
57
|
+
if (!fs.existsSync(pkgPath)) return null;
|
|
58
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
59
|
+
if (typeof pkg.name === "string" && pkg.name.trim()) {
|
|
60
|
+
// Strip npm scope if present: @org/name → name
|
|
61
|
+
const unscoped = pkg.name.includes("/") ? pkg.name.split("/").pop() : pkg.name;
|
|
62
|
+
return unscoped;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readGitRemoteBasename(cwd) {
|
|
71
|
+
try {
|
|
72
|
+
// -C points git at the cwd without us cd'ing the process.
|
|
73
|
+
// Audit follow-up: was using execSync with template-string interpolation
|
|
74
|
+
// of `cwd`. Switched to execFileSync with arg arrays so no interpolation
|
|
75
|
+
// reaches /bin/sh.
|
|
76
|
+
const out = execFileSync("git", ["-C", String(cwd), "config", "--get", "remote.origin.url"], {
|
|
77
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
78
|
+
encoding: "utf8",
|
|
79
|
+
timeout: 1500,
|
|
80
|
+
}).trim();
|
|
81
|
+
if (!out) return null;
|
|
82
|
+
// Strip optional protocol + user, .git suffix, trailing slash.
|
|
83
|
+
// Examples:
|
|
84
|
+
// git@github.com:foo/weather-dashboard.git
|
|
85
|
+
// https://github.com/foo/weather-dashboard
|
|
86
|
+
// git@gitlab.com:team/weather-dashboard
|
|
87
|
+
const last = out.replace(/\.git\/?$/, "").split(/[/:]/).pop();
|
|
88
|
+
return last || null;
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readCwdBasename(cwd) {
|
|
95
|
+
const base = path.basename(cwd || process.cwd());
|
|
96
|
+
if (!base || base === "/" || base === ".") return null;
|
|
97
|
+
return base;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Try every signal in priority order and return the first prettified
|
|
102
|
+
* non-empty result. Returns null when nothing useful is available.
|
|
103
|
+
*
|
|
104
|
+
* @param {string} [cwd] — defaults to process.cwd()
|
|
105
|
+
* @returns {string|null}
|
|
106
|
+
*/
|
|
107
|
+
export function deriveProjectNameFromCwd(cwd = process.cwd()) {
|
|
108
|
+
const fromPkg = prettifyName(readPackageJsonName(cwd));
|
|
109
|
+
if (fromPkg) return fromPkg;
|
|
110
|
+
const fromGit = prettifyName(readGitRemoteBasename(cwd));
|
|
111
|
+
if (fromGit) return fromGit;
|
|
112
|
+
const fromCwd = prettifyName(readCwdBasename(cwd));
|
|
113
|
+
if (fromCwd) return fromCwd;
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Exposed for unit tests so they can drive the individual signals
|
|
118
|
+
// without real filesystem / git side-effects.
|
|
119
|
+
export const __test__ = { prettifyName, readPackageJsonName, readGitRemoteBasename, readCwdBasename };
|