@pugi/cli 0.1.0-beta.23 → 0.1.0-beta.25
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/dist/core/auto-update/channels.js +122 -0
- package/dist/core/auto-update/checker.js +241 -0
- package/dist/core/auto-update/state.js +235 -0
- package/dist/core/engine/compaction-hook.js +154 -0
- package/dist/core/engine/native-pugi.js +67 -3
- package/dist/core/engine/tool-bridge.js +123 -3
- package/dist/core/hooks/events.js +44 -0
- package/dist/core/hooks/index.js +15 -0
- package/dist/core/hooks/registry.js +213 -0
- package/dist/core/hooks/runner.js +236 -0
- package/dist/core/init/scaffold.js +195 -0
- package/dist/core/lsp/cache.js +105 -0
- package/dist/core/lsp/language-detect.js +66 -0
- package/dist/core/lsp/post-edit-diagnostics.js +171 -0
- package/dist/core/repl/codebase-survey.js +308 -0
- package/dist/core/repl/init-interview.js +457 -0
- package/dist/core/repl/onboarding-state.js +297 -0
- package/dist/core/repl/session.js +84 -0
- package/dist/core/repl/slash-commands.js +25 -0
- package/dist/core/repo-map/build.js +125 -0
- package/dist/core/repo-map/cache.js +185 -0
- package/dist/core/repo-map/extractor.js +254 -0
- package/dist/core/repo-map/formatter.js +145 -0
- package/dist/core/repo-map/scanner.js +211 -0
- package/dist/core/session.js +44 -0
- package/dist/core/settings.js +9 -0
- package/dist/runtime/cli.js +170 -0
- package/dist/runtime/commands/hooks.js +184 -0
- package/dist/runtime/commands/lsp.js +25 -23
- package/dist/runtime/commands/repo-map.js +95 -0
- package/dist/runtime/commands/update.js +289 -0
- package/dist/runtime/version.js +1 -1
- package/dist/tui/repl-splash-mascot.js +19 -7
- package/package.json +3 -3
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-edit diagnostics — Leak L15.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code's leak intel surfaced this pattern: after a `FileEdit` /
|
|
5
|
+
* `Write` tool call lands, an LSP diagnostic pass runs on the touched
|
|
6
|
+
* file and the result is appended to the tool envelope before the
|
|
7
|
+
* model sees it. The model then self-corrects in the same turn —
|
|
8
|
+
* "TS2304: Cannot find name 'undef'" comes back, the model fixes the
|
|
9
|
+
* typo in the next tool call, no operator round-trip needed.
|
|
10
|
+
*
|
|
11
|
+
* This module is the Pugi side of that pattern:
|
|
12
|
+
*
|
|
13
|
+
* 1. The tool-bridge calls `runPostEditDiagnostics(path, ctx)` after
|
|
14
|
+
* a successful `edit` / `write` / `multi_edit`.
|
|
15
|
+
* 2. We infer the language from the extension (`language-detect`).
|
|
16
|
+
* Unsupported extension → `{ skip: true }` and the bridge appends
|
|
17
|
+
* nothing.
|
|
18
|
+
* 3. We borrow (or lazily start) the per-language cached client
|
|
19
|
+
* from `cache.ts`. A spawn failure → `{ skip: true }` and the
|
|
20
|
+
* envelope stays clean. Silence on failure is intentional: an
|
|
21
|
+
* operator who has not installed `typescript-language-server`
|
|
22
|
+
* should not see an LSP nag on every edit.
|
|
23
|
+
* 4. We pull diagnostics with a hard 5s ceiling. A timeout logs a
|
|
24
|
+
* warning on stderr (gated on `PUGI_LSP_DEBUG=1`) and skips —
|
|
25
|
+
* the envelope is never blocked on LSP.
|
|
26
|
+
* 5. We format the surviving diagnostics into a readable tail
|
|
27
|
+
* mirroring the leak format:
|
|
28
|
+
*
|
|
29
|
+
* LSP DIAGNOSTICS (typescript):
|
|
30
|
+
* foo.ts:42:5 error TS2304: Cannot find name 'undef'.
|
|
31
|
+
* foo.ts:51:1 warn TS6133: 'unused' is declared.
|
|
32
|
+
*
|
|
33
|
+
* The bridge concatenates this tail onto its existing `wrote ...` /
|
|
34
|
+
* `edited ...` body with a single newline separator. When there are
|
|
35
|
+
* zero diagnostics we return `{ skip: true }` so the existing body
|
|
36
|
+
* is unchanged — the "no news is good news" path stays terse.
|
|
37
|
+
*
|
|
38
|
+
* Brand voice: ASCII only, no emoji, no banned words.
|
|
39
|
+
*/
|
|
40
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
41
|
+
import { getOrStartLspClient } from './cache.js';
|
|
42
|
+
import { languageForFile } from './language-detect.js';
|
|
43
|
+
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
44
|
+
/**
|
|
45
|
+
* Hard cap on how many diagnostics we surface to the model. A file
|
|
46
|
+
* with 200 errors after a broken bulk edit would otherwise blow the
|
|
47
|
+
* context window; the model can re-run `pugi lsp diagnostics` if
|
|
48
|
+
* it needs the full list.
|
|
49
|
+
*/
|
|
50
|
+
const MAX_DIAGNOSTICS = 25;
|
|
51
|
+
export async function runPostEditDiagnostics(filePath, opts) {
|
|
52
|
+
const lang = languageForFile(filePath);
|
|
53
|
+
if (!lang) {
|
|
54
|
+
return { skip: true, reason: 'unsupported_language' };
|
|
55
|
+
}
|
|
56
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
57
|
+
const clientResult = await loadClient(lang, opts);
|
|
58
|
+
if (!clientResult.ok) {
|
|
59
|
+
return { skip: true, reason: mapStartFailure(clientResult.reason) };
|
|
60
|
+
}
|
|
61
|
+
// Run diagnostics with a hard timeout. The underlying LspClient has
|
|
62
|
+
// its own per-request timeout (5s default) but a slow handshake
|
|
63
|
+
// can blow past it; we belt-and-suspenders here so the agent loop
|
|
64
|
+
// never blocks on LSP.
|
|
65
|
+
const relPath = toWorkspaceRelative(filePath, opts.cwd);
|
|
66
|
+
const diagnosticsPromise = clientResult.client.diagnostics(relPath);
|
|
67
|
+
let timer;
|
|
68
|
+
const timeoutPromise = new Promise((resolveFn) => {
|
|
69
|
+
timer = setTimeout(() => resolveFn({ timedOut: true }), timeoutMs);
|
|
70
|
+
timer.unref();
|
|
71
|
+
});
|
|
72
|
+
const race = await Promise.race([
|
|
73
|
+
diagnosticsPromise.then((value) => ({ timedOut: false, value })),
|
|
74
|
+
timeoutPromise,
|
|
75
|
+
]);
|
|
76
|
+
if (timer)
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
if (race.timedOut) {
|
|
79
|
+
const writeFn = opts.debugWrite ?? ((line) => {
|
|
80
|
+
if (process.env.PUGI_LSP_DEBUG === '1')
|
|
81
|
+
process.stderr.write(`${line}\n`);
|
|
82
|
+
});
|
|
83
|
+
writeFn(`[pugi-lsp] post-edit diagnostics for ${relPath} timed out after ${timeoutMs}ms (lang=${lang})`);
|
|
84
|
+
return { skip: true, reason: 'timeout' };
|
|
85
|
+
}
|
|
86
|
+
const diag = race.value;
|
|
87
|
+
if (!diag.ok) {
|
|
88
|
+
return { skip: true, reason: 'lsp_error' };
|
|
89
|
+
}
|
|
90
|
+
if (diag.value.length === 0) {
|
|
91
|
+
return { skip: true, reason: 'no_diagnostics' };
|
|
92
|
+
}
|
|
93
|
+
const tail = formatDiagnosticsTail(relPath, lang, diag.value);
|
|
94
|
+
return { skip: false, tail, count: diag.value.length, language: lang };
|
|
95
|
+
}
|
|
96
|
+
async function loadClient(lang, opts) {
|
|
97
|
+
if (opts.clientLoader) {
|
|
98
|
+
return opts.clientLoader(lang);
|
|
99
|
+
}
|
|
100
|
+
const { cwd, timeoutMs, clientLoader: _ignoredA, debugWrite: _ignoredB, ...rest } = opts;
|
|
101
|
+
const result = await getOrStartLspClient(lang, { cwd, ...rest });
|
|
102
|
+
if (!result.ok) {
|
|
103
|
+
return { ok: false, reason: result.reason, detail: result.detail };
|
|
104
|
+
}
|
|
105
|
+
return { ok: true, client: result.client };
|
|
106
|
+
}
|
|
107
|
+
function mapStartFailure(reason) {
|
|
108
|
+
if (reason === 'lsp_unavailable' || reason === 'language_unsupported')
|
|
109
|
+
return 'lsp_unavailable';
|
|
110
|
+
if (reason === 'lsp_disabled')
|
|
111
|
+
return 'lsp_disabled';
|
|
112
|
+
return 'lsp_error';
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Convert an absolute or workspace-relative path into the form the
|
|
116
|
+
* LSP client expects — same shape as `runtime/commands/lsp.ts` uses.
|
|
117
|
+
*/
|
|
118
|
+
function toWorkspaceRelative(filePath, cwd) {
|
|
119
|
+
if (!isAbsolute(filePath))
|
|
120
|
+
return filePath;
|
|
121
|
+
const rel = relative(cwd, resolve(cwd, filePath));
|
|
122
|
+
return rel || filePath;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Format diagnostics into the leak-shaped envelope tail. Pure function
|
|
126
|
+
* exported for unit tests to assert the line format independent of
|
|
127
|
+
* any LSP plumbing.
|
|
128
|
+
*/
|
|
129
|
+
export function formatDiagnosticsTail(relPath, lang, diagnostics) {
|
|
130
|
+
const visible = diagnostics.slice(0, MAX_DIAGNOSTICS);
|
|
131
|
+
const truncated = diagnostics.length > visible.length;
|
|
132
|
+
const lines = [`LSP DIAGNOSTICS (${LANGUAGE_LABELS[lang]}):`];
|
|
133
|
+
for (const diag of visible) {
|
|
134
|
+
const line = diag.range.start.line + 1; // LSP is zero-based; humans expect 1-based.
|
|
135
|
+
const col = diag.range.start.character + 1;
|
|
136
|
+
const severity = SEVERITY_LABELS[diag.severityLabel];
|
|
137
|
+
const code = diag.code !== undefined && diag.code !== '' ? ` ${diag.code}` : '';
|
|
138
|
+
const source = diag.source ? `${diag.source}` : '';
|
|
139
|
+
const head = source ? `${severity}${code} (${source}):` : `${severity}${code}:`;
|
|
140
|
+
lines.push(` ${relPath}:${line}:${col} ${head} ${diag.message}`);
|
|
141
|
+
}
|
|
142
|
+
if (truncated) {
|
|
143
|
+
lines.push(` ... ${diagnostics.length - visible.length} more diagnostic(s) — re-run pugi lsp diagnostics ${relPath} for the full list`);
|
|
144
|
+
}
|
|
145
|
+
return lines.join('\n');
|
|
146
|
+
}
|
|
147
|
+
const LANGUAGE_LABELS = {
|
|
148
|
+
ts: 'typescript',
|
|
149
|
+
js: 'javascript',
|
|
150
|
+
py: 'python',
|
|
151
|
+
go: 'go',
|
|
152
|
+
rust: 'rust',
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Map LSP severity label → the short token the leak envelope uses.
|
|
156
|
+
* "warn" is shorter than "warning" and matches Claude Code's leak
|
|
157
|
+
* verbatim; the rest mirror LSP terminology.
|
|
158
|
+
*/
|
|
159
|
+
const SEVERITY_LABELS = {
|
|
160
|
+
error: 'error',
|
|
161
|
+
warning: 'warn ',
|
|
162
|
+
info: 'info ',
|
|
163
|
+
hint: 'hint ',
|
|
164
|
+
};
|
|
165
|
+
/** Test-only surface so specs can poke the pure helpers without LSP. */
|
|
166
|
+
export const __test__ = {
|
|
167
|
+
formatDiagnosticsTail,
|
|
168
|
+
MAX_DIAGNOSTICS,
|
|
169
|
+
DEFAULT_TIMEOUT_MS,
|
|
170
|
+
};
|
|
171
|
+
//# sourceMappingURL=post-edit-diagnostics.js.map
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codebase survey for the `/init` interview - Phase 2.
|
|
3
|
+
*
|
|
4
|
+
* Inspired by Claude Code's /init Phase 2 (the upstream spawns a
|
|
5
|
+
* Task-tool subagent to read manifest files + CI config + existing
|
|
6
|
+
* agent rules and produce a structured "what this repo is" digest).
|
|
7
|
+
* Independent implementation: Pugi's subagent infra is admin-api
|
|
8
|
+
* scheduled and not available in the local REPL boot path, so the
|
|
9
|
+
* survey runs as a direct filesystem scan instead. The shape of the
|
|
10
|
+
* output matches the upstream pattern - manifest, languages, build /
|
|
11
|
+
* test / lint commands, existing AI-tool configs - so the downstream
|
|
12
|
+
* Phase 3 / Phase 4 logic stays portable.
|
|
13
|
+
*
|
|
14
|
+
* # Design notes
|
|
15
|
+
*
|
|
16
|
+
* - Pure fs reads. No spawn, no network, no LLM call. Safe to run on
|
|
17
|
+
* every `/init` invocation without rate-limit concern.
|
|
18
|
+
* - Bounded: every read caps at 16 KB to defend against an enormous
|
|
19
|
+
* manifest pinning memory. Real package.json / pyproject.toml are
|
|
20
|
+
* well under that.
|
|
21
|
+
* - Defensive: a missing or unreadable file maps to `undefined` in the
|
|
22
|
+
* returned record. Phase 3 treats unknowns as "ask the operator".
|
|
23
|
+
* - Manifest grammar is closed: package.json (Node) and pyproject.toml
|
|
24
|
+
* (Python) are recognised explicitly because Pugi customers ship one
|
|
25
|
+
* of those nine times out of ten. Cargo.toml / go.mod / pom.xml are
|
|
26
|
+
* detected by filename only - we surface "rust"/"go"/"java" as the
|
|
27
|
+
* language hint but do not parse them, because the Phase 3 question
|
|
28
|
+
* set asks for build commands directly when the manifest is opaque.
|
|
29
|
+
*
|
|
30
|
+
* # What we collect
|
|
31
|
+
*
|
|
32
|
+
* 1. `manifest`: which manifest file was found (closed enum).
|
|
33
|
+
* 2. `languages`: deduped list inferred from manifest + file extension
|
|
34
|
+
* heuristics under the workspace root (one-level deep scan).
|
|
35
|
+
* 3. `packageManager`: pnpm / npm / yarn (Node only) or `unknown`.
|
|
36
|
+
* 4. `buildCommand` / `testCommand` / `lintCommand`: parsed out of
|
|
37
|
+
* `package.json` scripts when a Node manifest is present.
|
|
38
|
+
* 5. `aiToolConfigs`: a record of which sibling-agent config files
|
|
39
|
+
* already exist (CLAUDE.md, AGENTS.md, .cursorrules,
|
|
40
|
+
* .github/copilot-instructions.md, .windsurfrules, .clinerules,
|
|
41
|
+
* .mcp.json). Phase 4 mines these for "important parts" without
|
|
42
|
+
* duplicating them into PUGI.md.
|
|
43
|
+
* 6. `hasReadme`, `hasGit`, `hasCi`: simple booleans for the
|
|
44
|
+
* gap-question logic.
|
|
45
|
+
* 7. `hasExistingPugiMd`: true when re-running `/init` against a
|
|
46
|
+
* workspace that already produced PUGI.md.
|
|
47
|
+
*
|
|
48
|
+
* # Why not spawn a Pugi subagent
|
|
49
|
+
*
|
|
50
|
+
* The Pugi subagent dispatcher (apps/pugi-cli/src/core/subagents/)
|
|
51
|
+
* speaks to admin-api over the SSE transport. Running the codebase
|
|
52
|
+
* survey through that path would (a) burn a tenant token quota on
|
|
53
|
+
* every `/init`, (b) require an online connection, and (c) round-trip
|
|
54
|
+
* structured data through the persona prompt - which is the wrong
|
|
55
|
+
* tool for "list which files exist". A direct fs scan is faster,
|
|
56
|
+
* deterministic, and works offline. The upstream Task-tool decision
|
|
57
|
+
* makes sense in a hosted product where every operation is metered;
|
|
58
|
+
* Pugi runs locally so we keep the survey local too.
|
|
59
|
+
*/
|
|
60
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
61
|
+
import { join } from 'node:path';
|
|
62
|
+
/**
|
|
63
|
+
* Maximum bytes the survey reads from any single file. Real manifests
|
|
64
|
+
* are tiny (<8 KB); this cap defends against a hostile or accidental
|
|
65
|
+
* gigabyte JSON pinning the REPL boot.
|
|
66
|
+
*/
|
|
67
|
+
const MAX_READ_BYTES = 16 * 1024;
|
|
68
|
+
/**
|
|
69
|
+
* Top-level directory scan budget. The survey looks at the workspace
|
|
70
|
+
* root + at most this many entries when inferring languages from file
|
|
71
|
+
* extensions. Deep walks are not needed - the manifest is the source
|
|
72
|
+
* of truth for the active stack.
|
|
73
|
+
*/
|
|
74
|
+
const MAX_TOP_LEVEL_ENTRIES = 200;
|
|
75
|
+
/**
|
|
76
|
+
* Run a codebase survey against `workspaceRoot`. Pure: returns a
|
|
77
|
+
* snapshot record; the caller decides how to render it.
|
|
78
|
+
*/
|
|
79
|
+
export function surveyCodebase(workspaceRoot) {
|
|
80
|
+
const errors = [];
|
|
81
|
+
const manifest = detectManifest(workspaceRoot);
|
|
82
|
+
const packageJson = manifest === 'package.json'
|
|
83
|
+
? safeReadJson(join(workspaceRoot, 'package.json'), errors)
|
|
84
|
+
: undefined;
|
|
85
|
+
const packageManager = inferPackageManager(workspaceRoot, packageJson);
|
|
86
|
+
const languages = inferLanguages(workspaceRoot, manifest, errors);
|
|
87
|
+
const scripts = (packageJson && typeof packageJson === 'object' && packageJson !== null
|
|
88
|
+
? packageJson.scripts
|
|
89
|
+
: undefined) ?? {};
|
|
90
|
+
const buildCommand = pickScript(scripts, ['build', 'compile']);
|
|
91
|
+
const testCommand = pickScript(scripts, ['test', 'tests']);
|
|
92
|
+
const lintCommand = pickScript(scripts, ['lint', 'check']);
|
|
93
|
+
const formatCommand = pickScript(scripts, ['format', 'fmt', 'prettier']);
|
|
94
|
+
const aiToolConfigs = scanAiToolConfigs(workspaceRoot);
|
|
95
|
+
return {
|
|
96
|
+
workspaceRoot,
|
|
97
|
+
manifest,
|
|
98
|
+
packageManager,
|
|
99
|
+
languages,
|
|
100
|
+
buildCommand,
|
|
101
|
+
testCommand,
|
|
102
|
+
lintCommand,
|
|
103
|
+
formatCommand,
|
|
104
|
+
aiToolConfigs,
|
|
105
|
+
hasReadme: existsSafe(join(workspaceRoot, 'README.md')) ||
|
|
106
|
+
existsSafe(join(workspaceRoot, 'readme.md')),
|
|
107
|
+
hasGit: existsSafe(join(workspaceRoot, '.git')),
|
|
108
|
+
hasCi: detectCi(workspaceRoot),
|
|
109
|
+
hasExistingPugiMd: aiToolConfigs['PUGI.md'],
|
|
110
|
+
readErrors: errors,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/* ------------------------------------------------------------------ */
|
|
114
|
+
/* Manifest detection */
|
|
115
|
+
/* ------------------------------------------------------------------ */
|
|
116
|
+
const MANIFEST_PROBE_ORDER = Object.freeze([
|
|
117
|
+
'package.json',
|
|
118
|
+
'pyproject.toml',
|
|
119
|
+
'Cargo.toml',
|
|
120
|
+
'go.mod',
|
|
121
|
+
'pom.xml',
|
|
122
|
+
'Gemfile',
|
|
123
|
+
'composer.json',
|
|
124
|
+
]);
|
|
125
|
+
function detectManifest(root) {
|
|
126
|
+
for (const candidate of MANIFEST_PROBE_ORDER) {
|
|
127
|
+
if (existsSafe(join(root, candidate)))
|
|
128
|
+
return candidate;
|
|
129
|
+
}
|
|
130
|
+
return 'unknown';
|
|
131
|
+
}
|
|
132
|
+
function inferPackageManager(root, packageJson) {
|
|
133
|
+
// package.json `packageManager` field wins when present (corepack convention).
|
|
134
|
+
if (packageJson &&
|
|
135
|
+
typeof packageJson === 'object' &&
|
|
136
|
+
packageJson !== null &&
|
|
137
|
+
'packageManager' in packageJson) {
|
|
138
|
+
const declared = packageJson.packageManager;
|
|
139
|
+
if (typeof declared === 'string') {
|
|
140
|
+
if (declared.startsWith('pnpm@'))
|
|
141
|
+
return 'pnpm';
|
|
142
|
+
if (declared.startsWith('yarn@'))
|
|
143
|
+
return 'yarn';
|
|
144
|
+
if (declared.startsWith('npm@'))
|
|
145
|
+
return 'npm';
|
|
146
|
+
if (declared.startsWith('bun@'))
|
|
147
|
+
return 'bun';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Lockfile fallback.
|
|
151
|
+
if (existsSafe(join(root, 'pnpm-lock.yaml')))
|
|
152
|
+
return 'pnpm';
|
|
153
|
+
if (existsSafe(join(root, 'yarn.lock')))
|
|
154
|
+
return 'yarn';
|
|
155
|
+
if (existsSafe(join(root, 'bun.lockb')) || existsSafe(join(root, 'bun.lock')))
|
|
156
|
+
return 'bun';
|
|
157
|
+
if (existsSafe(join(root, 'package-lock.json')))
|
|
158
|
+
return 'npm';
|
|
159
|
+
return 'unknown';
|
|
160
|
+
}
|
|
161
|
+
/* ------------------------------------------------------------------ */
|
|
162
|
+
/* Language inference */
|
|
163
|
+
/* ------------------------------------------------------------------ */
|
|
164
|
+
const EXT_TO_LANG = Object.freeze({
|
|
165
|
+
'.ts': 'typescript',
|
|
166
|
+
'.tsx': 'typescript',
|
|
167
|
+
'.js': 'javascript',
|
|
168
|
+
'.jsx': 'javascript',
|
|
169
|
+
'.mjs': 'javascript',
|
|
170
|
+
'.cjs': 'javascript',
|
|
171
|
+
'.py': 'python',
|
|
172
|
+
'.rs': 'rust',
|
|
173
|
+
'.go': 'go',
|
|
174
|
+
'.java': 'java',
|
|
175
|
+
'.kt': 'kotlin',
|
|
176
|
+
'.swift': 'swift',
|
|
177
|
+
'.rb': 'ruby',
|
|
178
|
+
'.php': 'php',
|
|
179
|
+
'.cs': 'csharp',
|
|
180
|
+
'.cpp': 'cpp',
|
|
181
|
+
'.c': 'c',
|
|
182
|
+
});
|
|
183
|
+
const MANIFEST_TO_LANG = Object.freeze({
|
|
184
|
+
'package.json': ['javascript'],
|
|
185
|
+
'pyproject.toml': ['python'],
|
|
186
|
+
'Cargo.toml': ['rust'],
|
|
187
|
+
'go.mod': ['go'],
|
|
188
|
+
'pom.xml': ['java'],
|
|
189
|
+
'Gemfile': ['ruby'],
|
|
190
|
+
'composer.json': ['php'],
|
|
191
|
+
'unknown': [],
|
|
192
|
+
});
|
|
193
|
+
function inferLanguages(root, manifest, errors) {
|
|
194
|
+
const collected = new Set(MANIFEST_TO_LANG[manifest]);
|
|
195
|
+
// Top-level extension scan, bounded.
|
|
196
|
+
try {
|
|
197
|
+
const entries = readdirSync(root);
|
|
198
|
+
let scanned = 0;
|
|
199
|
+
for (const entry of entries) {
|
|
200
|
+
if (scanned >= MAX_TOP_LEVEL_ENTRIES)
|
|
201
|
+
break;
|
|
202
|
+
scanned += 1;
|
|
203
|
+
// Skip dotfiles + common dependency dirs - they pollute the
|
|
204
|
+
// language inference with build/cache content.
|
|
205
|
+
if (entry.startsWith('.') || entry === 'node_modules' || entry === 'dist')
|
|
206
|
+
continue;
|
|
207
|
+
const dot = entry.lastIndexOf('.');
|
|
208
|
+
if (dot <= 0)
|
|
209
|
+
continue;
|
|
210
|
+
const ext = entry.slice(dot);
|
|
211
|
+
const lang = EXT_TO_LANG[ext];
|
|
212
|
+
if (lang)
|
|
213
|
+
collected.add(lang);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
errors.push(`readdir ${root}: ${normalizeError(error)}`);
|
|
218
|
+
}
|
|
219
|
+
// `typescript` implies `javascript` runtime; keep both so the
|
|
220
|
+
// interview can ask "compiled-with vs run-with" if needed.
|
|
221
|
+
return Array.from(collected).sort();
|
|
222
|
+
}
|
|
223
|
+
/* ------------------------------------------------------------------ */
|
|
224
|
+
/* Script picker */
|
|
225
|
+
/* ------------------------------------------------------------------ */
|
|
226
|
+
function pickScript(scripts, candidates) {
|
|
227
|
+
for (const key of candidates) {
|
|
228
|
+
const value = scripts[key];
|
|
229
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
230
|
+
// Surface the npm-style invocation so Phase 4 can quote it
|
|
231
|
+
// verbatim. The package manager name is filled in by the caller
|
|
232
|
+
// once it has resolved `packageManager`.
|
|
233
|
+
return key;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
/* ------------------------------------------------------------------ */
|
|
239
|
+
/* AI tool config scan */
|
|
240
|
+
/* ------------------------------------------------------------------ */
|
|
241
|
+
const AI_TOOL_CONFIG_PATHS = Object.freeze([
|
|
242
|
+
'CLAUDE.md',
|
|
243
|
+
'CLAUDE.local.md',
|
|
244
|
+
'AGENTS.md',
|
|
245
|
+
'.cursorrules',
|
|
246
|
+
'.cursor/rules',
|
|
247
|
+
'.github/copilot-instructions.md',
|
|
248
|
+
'.windsurfrules',
|
|
249
|
+
'.clinerules',
|
|
250
|
+
'.mcp.json',
|
|
251
|
+
'PUGI.md',
|
|
252
|
+
'PUGI.local.md',
|
|
253
|
+
]);
|
|
254
|
+
function scanAiToolConfigs(root) {
|
|
255
|
+
const result = {};
|
|
256
|
+
for (const rel of AI_TOOL_CONFIG_PATHS) {
|
|
257
|
+
result[rel] = existsSafe(join(root, rel));
|
|
258
|
+
}
|
|
259
|
+
return Object.freeze(result);
|
|
260
|
+
}
|
|
261
|
+
/* ------------------------------------------------------------------ */
|
|
262
|
+
/* CI detection */
|
|
263
|
+
/* ------------------------------------------------------------------ */
|
|
264
|
+
const CI_PROBE_PATHS = Object.freeze([
|
|
265
|
+
'.github/workflows',
|
|
266
|
+
'.gitlab-ci.yml',
|
|
267
|
+
'.circleci/config.yml',
|
|
268
|
+
'azure-pipelines.yml',
|
|
269
|
+
'.travis.yml',
|
|
270
|
+
'.buildkite',
|
|
271
|
+
]);
|
|
272
|
+
function detectCi(root) {
|
|
273
|
+
return CI_PROBE_PATHS.some((rel) => existsSafe(join(root, rel)));
|
|
274
|
+
}
|
|
275
|
+
/* ------------------------------------------------------------------ */
|
|
276
|
+
/* Safe IO helpers */
|
|
277
|
+
/* ------------------------------------------------------------------ */
|
|
278
|
+
function existsSafe(path) {
|
|
279
|
+
try {
|
|
280
|
+
return existsSync(path);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function safeReadJson(path, errors) {
|
|
287
|
+
try {
|
|
288
|
+
const stats = statSync(path);
|
|
289
|
+
if (!stats.isFile())
|
|
290
|
+
return undefined;
|
|
291
|
+
if (stats.size > MAX_READ_BYTES) {
|
|
292
|
+
errors.push(`oversize ${path}: ${stats.size} bytes`);
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
const raw = readFileSync(path, 'utf8');
|
|
296
|
+
return JSON.parse(raw);
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
errors.push(`read ${path}: ${normalizeError(error)}`);
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function normalizeError(error) {
|
|
304
|
+
if (error instanceof Error)
|
|
305
|
+
return error.message;
|
|
306
|
+
return String(error);
|
|
307
|
+
}
|
|
308
|
+
//# sourceMappingURL=codebase-survey.js.map
|