isthmus-cli 0.5.0 → 0.7.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.ko.md +170 -45
- package/README.md +178 -36
- package/Skills/isthmus/SKILL.md +104 -5
- package/compatibility.json +10 -0
- package/dist/cli/check-command.d.ts +2 -2
- package/dist/cli/check-command.js +22 -9
- package/dist/cli/check-command.js.map +1 -1
- package/dist/cli/command-support.d.ts +20 -2
- package/dist/cli/command-support.js +38 -9
- package/dist/cli/command-support.js.map +1 -1
- package/dist/cli/diff-command.js +4 -3
- package/dist/cli/diff-command.js.map +1 -1
- package/dist/cli/doctor-command.d.ts +11 -0
- package/dist/cli/doctor-command.js +128 -0
- package/dist/cli/doctor-command.js.map +1 -0
- package/dist/cli/extract-js-command.d.ts +33 -0
- package/dist/cli/extract-js-command.js +233 -0
- package/dist/cli/extract-js-command.js.map +1 -0
- package/dist/cli/graph-command.js +13 -6
- package/dist/cli/graph-command.js.map +1 -1
- package/dist/cli/impact-command.d.ts +5 -0
- package/dist/cli/impact-command.js +111 -0
- package/dist/cli/impact-command.js.map +1 -0
- package/dist/cli/init-command.d.ts +11 -0
- package/dist/cli/init-command.js +115 -0
- package/dist/cli/init-command.js.map +1 -0
- package/dist/cli/main.js +67 -1
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/mcp-server.d.ts +16 -0
- package/dist/cli/mcp-server.js +534 -0
- package/dist/cli/mcp-server.js.map +1 -0
- package/dist/cli/preflight-command.d.ts +5 -0
- package/dist/cli/preflight-command.js +83 -0
- package/dist/cli/preflight-command.js.map +1 -0
- package/dist/cli/query-command.js +13 -6
- package/dist/cli/query-command.js.map +1 -1
- package/dist/cli/retentions-command.js +14 -7
- package/dist/cli/retentions-command.js.map +1 -1
- package/dist/cli/runtime-command.d.ts +5 -0
- package/dist/cli/runtime-command.js +42 -0
- package/dist/cli/runtime-command.js.map +1 -0
- package/dist/cli/runtime-json-reader.d.ts +12 -0
- package/dist/cli/runtime-json-reader.js +37 -0
- package/dist/cli/runtime-json-reader.js.map +1 -0
- package/dist/cli/serve-command.d.ts +13 -0
- package/dist/cli/serve-command.js +43 -0
- package/dist/cli/serve-command.js.map +1 -0
- package/dist/exchange/capture-config.d.ts +18 -0
- package/dist/exchange/capture-config.js +100 -0
- package/dist/exchange/capture-config.js.map +1 -0
- package/dist/exchange/impact-selection.d.ts +13 -0
- package/dist/exchange/impact-selection.js +35 -0
- package/dist/exchange/impact-selection.js.map +1 -0
- package/dist/exchange/kartograph-impact.d.ts +4 -0
- package/dist/exchange/kartograph-impact.js +169 -0
- package/dist/exchange/kartograph-impact.js.map +1 -0
- package/dist/exchange/messages.d.ts +36 -0
- package/dist/exchange/messages.js +96 -0
- package/dist/exchange/messages.js.map +1 -0
- package/dist/exchange/parse.d.ts +69 -0
- package/dist/exchange/parse.js +127 -20
- package/dist/exchange/parse.js.map +1 -1
- package/dist/exchange/preflight-context.d.ts +71 -0
- package/dist/exchange/preflight-context.js +317 -0
- package/dist/exchange/preflight-context.js.map +1 -0
- package/dist/exchange/producer-impact.d.ts +17 -0
- package/dist/exchange/producer-impact.js +226 -0
- package/dist/exchange/producer-impact.js.map +1 -0
- package/dist/exchange/runtime.d.ts +69 -0
- package/dist/exchange/runtime.js +160 -0
- package/dist/exchange/runtime.js.map +1 -0
- package/dist/extract/js-document.d.ts +22 -0
- package/dist/extract/js-document.js +230 -0
- package/dist/extract/js-document.js.map +1 -0
- package/dist/extract/js-scan.d.ts +93 -0
- package/dist/extract/js-scan.js +1205 -0
- package/dist/extract/js-scan.js.map +1 -0
- package/dist/extract/lexer.d.ts +26 -0
- package/dist/extract/lexer.js +316 -0
- package/dist/extract/lexer.js.map +1 -0
- package/dist/join/join.d.ts +66 -1
- package/dist/join/join.js +171 -1
- package/dist/join/join.js.map +1 -1
- package/dist/join/message-address.d.ts +7 -0
- package/dist/join/message-address.js +37 -0
- package/dist/join/message-address.js.map +1 -0
- package/dist/join/messages.d.ts +26 -0
- package/dist/join/messages.js +87 -0
- package/dist/join/messages.js.map +1 -0
- package/dist/report/check-report.d.ts +20 -3
- package/dist/report/check-report.js +240 -8
- package/dist/report/check-report.js.map +1 -1
- package/dist/report/codequality.d.ts +45 -0
- package/dist/report/codequality.js +74 -0
- package/dist/report/codequality.js.map +1 -0
- package/dist/report/diff.d.ts +25 -3
- package/dist/report/diff.js +91 -10
- package/dist/report/diff.js.map +1 -1
- package/dist/report/graph.d.ts +10 -4
- package/dist/report/graph.js +60 -6
- package/dist/report/graph.js.map +1 -1
- package/dist/report/impact.d.ts +62 -0
- package/dist/report/impact.js +168 -0
- package/dist/report/impact.js.map +1 -0
- package/dist/report/preflight-runtime.d.ts +42 -0
- package/dist/report/preflight-runtime.js +193 -0
- package/dist/report/preflight-runtime.js.map +1 -0
- package/dist/report/preflight-view.d.ts +146 -0
- package/dist/report/preflight-view.js +216 -0
- package/dist/report/preflight-view.js.map +1 -0
- package/dist/report/preflight.d.ts +104 -0
- package/dist/report/preflight.js +331 -0
- package/dist/report/preflight.js.map +1 -0
- package/dist/report/query.d.ts +11 -4
- package/dist/report/query.js +84 -17
- package/dist/report/query.js.map +1 -1
- package/dist/report/retentions.d.ts +17 -5
- package/dist/report/retentions.js +85 -8
- package/dist/report/retentions.js.map +1 -1
- package/dist/report/rules.d.ts +16 -0
- package/dist/report/rules.js +30 -0
- package/dist/report/rules.js.map +1 -0
- package/dist/report/runtime-impact.d.ts +38 -0
- package/dist/report/runtime-impact.js +94 -0
- package/dist/report/runtime-impact.js.map +1 -0
- package/dist/report/runtime.d.ts +54 -0
- package/dist/report/runtime.js +137 -0
- package/dist/report/runtime.js.map +1 -0
- package/dist/report/sarif.d.ts +2 -8
- package/dist/report/sarif.js +2 -10
- package/dist/report/sarif.js.map +1 -1
- package/dist/report/sorted-json.d.ts +1 -1
- package/dist/report/sorted-json.js +2 -2
- package/dist/report/sorted-json.js.map +1 -1
- package/docs/BRIDGE-EVENTS.md +87 -0
- package/docs/BRIDGE-MESSAGES.md +117 -0
- package/docs/GRAPH-EXCHANGE.md +382 -0
- package/docs/IMPACT.md +96 -0
- package/docs/MCP.md +62 -0
- package/docs/PREFLIGHT.md +311 -0
- package/docs/RUNTIME.md +172 -0
- package/docs/TOOLCHAIN.md +101 -0
- package/package.json +14 -2
- package/scripts/build-preflight-toolchain.mjs +186 -0
- package/scripts/capture-preflight.mjs +380 -0
- package/scripts/run-child.mjs +17 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
2
|
+
import { CaptureConfigValidationError, validateCaptureConfig } from "../exchange/capture-config.js";
|
|
3
|
+
import { isJsonObject, isSafeNonEmptyString } from "../exchange/parse.js";
|
|
4
|
+
import { encodeSortedJson } from "../report/sorted-json.js";
|
|
5
|
+
import { inputFailure, isJsonParseFailure, MAX_INPUT_TEXT_LENGTH, } from "./command-support.js";
|
|
6
|
+
/**
|
|
7
|
+
* capture 설정 JSON을 검증하고 참조한 실행 파일이 실제로 있는지 확인한다.
|
|
8
|
+
*
|
|
9
|
+
* producer를 실행하거나 버전을 묻지 않는다 — 제품은 JSON만 읽는다. 여기서
|
|
10
|
+
* 확인하는 것은 `scripts/capture-preflight.mjs`가 실행할 명령의 첫 토큰이
|
|
11
|
+
* 존재하는 파일(또는 PATH 위의 실행 파일)인지뿐이다.
|
|
12
|
+
*/
|
|
13
|
+
export async function runDoctorCommand(arguments_, readTextFile, fileSystem, cwd, pathValue = '') {
|
|
14
|
+
if (arguments_[0] !== 'doctor')
|
|
15
|
+
return usageError();
|
|
16
|
+
const rest = arguments_.slice(1);
|
|
17
|
+
if (rest.length !== 1 || rest[0].startsWith('--'))
|
|
18
|
+
return usageError();
|
|
19
|
+
const configPath = rest[0];
|
|
20
|
+
let text;
|
|
21
|
+
try {
|
|
22
|
+
text = await readTextFile(configPath);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return inputFailure('Unable to read the capture config; check that the file exists and is readable.\n');
|
|
26
|
+
}
|
|
27
|
+
if (text.length > MAX_INPUT_TEXT_LENGTH) {
|
|
28
|
+
return inputFailure('The capture config exceeds the input size limits.\n');
|
|
29
|
+
}
|
|
30
|
+
let parsed;
|
|
31
|
+
try {
|
|
32
|
+
parsed = JSON.parse(text);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (isJsonParseFailure(error)) {
|
|
36
|
+
return inputFailure('The capture config is not valid JSON; regenerate it with isthmus init.\n');
|
|
37
|
+
}
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
if (!isJsonObject(parsed) || !isSafeNonEmptyString(parsed.project)) {
|
|
41
|
+
return inputFailure('The capture config must be a JSON object with a project path.\n');
|
|
42
|
+
}
|
|
43
|
+
let project;
|
|
44
|
+
try {
|
|
45
|
+
project = await fileSystem.realPath(parsed.project);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return inputFailure('The capture config project path does not exist; point it at the app root.\n');
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
validateCaptureConfig(parsed, project);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (error instanceof CaptureConfigValidationError) {
|
|
55
|
+
return inputFailure(`The capture config violates the workflow contract: ${error.message}\n`);
|
|
56
|
+
}
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
const pathEntries = pathValue.split(':').filter((entry) => entry.length > 0);
|
|
60
|
+
const checks = [];
|
|
61
|
+
const names = [
|
|
62
|
+
...producerNames(parsed),
|
|
63
|
+
...objectCommands(parsed.messages, parsed),
|
|
64
|
+
...objectCommands(parsed.events, parsed),
|
|
65
|
+
...parsed.prepare.map((command, index) => ({
|
|
66
|
+
name: `prepare-${index + 1}`,
|
|
67
|
+
command,
|
|
68
|
+
})),
|
|
69
|
+
];
|
|
70
|
+
for (const { name, command } of names) {
|
|
71
|
+
const resolved = await resolveExecutable(command[0], project, fileSystem, pathEntries);
|
|
72
|
+
checks.push({
|
|
73
|
+
name,
|
|
74
|
+
status: resolved === undefined ? 'missing' : 'ok',
|
|
75
|
+
command,
|
|
76
|
+
...(resolved === undefined ? {} : { resolved }),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
const report = {
|
|
80
|
+
format: 'isthmus-doctor',
|
|
81
|
+
version: 1,
|
|
82
|
+
status: checks.every(({ status }) => status === 'ok') ? 'ok' : 'incomplete',
|
|
83
|
+
project,
|
|
84
|
+
checks,
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
standardOutput: encodeSortedJson(report),
|
|
88
|
+
standardError: '',
|
|
89
|
+
exitCode: report.status === 'ok' ? 0 : 1,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** 설정에 선언된 기본 producer 명령과 이름을 순서대로 모은다. */
|
|
93
|
+
function producerNames(config) {
|
|
94
|
+
return [
|
|
95
|
+
{ name: 'dartograph', command: config.dartograph },
|
|
96
|
+
...['cartograph', 'kartograph']
|
|
97
|
+
.filter((name) => config[name] !== undefined)
|
|
98
|
+
.map((name) => ({ name, command: config[name] })),
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
/** messages·events의 사용자 지정 명령만 검사한다. `true`는 기본 명령 재사용이다. */
|
|
102
|
+
function objectCommands(value, config) {
|
|
103
|
+
if (value === undefined || value === true || !isJsonObject(value))
|
|
104
|
+
return [];
|
|
105
|
+
return Object.entries(value)
|
|
106
|
+
.filter(([name, command]) => config[name] !== undefined && Array.isArray(command))
|
|
107
|
+
.map(([name, command]) => ({ name, command: command }));
|
|
108
|
+
}
|
|
109
|
+
/** 첫 토큰이 존재하는 파일이거나 PATH 위의 실행 파일인지 확인한다. */
|
|
110
|
+
async function resolveExecutable(token, project, fileSystem, pathEntries) {
|
|
111
|
+
if (token.includes('/') || token.includes('\\')) {
|
|
112
|
+
const candidate = isAbsolute(token) ? token : resolve(project, token);
|
|
113
|
+
return (await fileSystem.statPath(candidate)) === 'file' ? candidate : undefined;
|
|
114
|
+
}
|
|
115
|
+
for (const entry of pathEntries) {
|
|
116
|
+
const candidate = join(entry, token);
|
|
117
|
+
if ((await fileSystem.statPath(candidate)) === 'file')
|
|
118
|
+
return candidate;
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
/** 잘못된 CLI 호출을 경로 없는 사용법과 코드 64로 바꾼다. */
|
|
123
|
+
function usageError() {
|
|
124
|
+
return { standardOutput: '', standardError: `${doctorUsage}\n`, exitCode: 64 };
|
|
125
|
+
}
|
|
126
|
+
/** doctor 명령의 한 줄 사용법이다. */
|
|
127
|
+
export const doctorUsage = 'Usage: isthmus doctor <capture.json>';
|
|
128
|
+
//# sourceMappingURL=doctor-command.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-command.js","sourceRoot":"","sources":["../../src/cli/doctor-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { isAbsolute, join, resolve } from 'node:path';\n\nimport { CaptureConfigValidationError, validateCaptureConfig } from '../exchange/capture-config.ts';\nimport { isJsonObject, isSafeNonEmptyString } from '../exchange/parse.ts';\nimport { encodeSortedJson } from '../report/sorted-json.ts';\nimport {\n inputFailure,\n isJsonParseFailure,\n MAX_INPUT_TEXT_LENGTH,\n type CaptureFileSystem,\n type CommandResult,\n type ReadTextFile,\n} from './command-support.ts';\n\n/** doctor가 확인한 명령 하나의 결과다. */\ninterface DoctorCheck {\n readonly name: string;\n readonly status: 'ok' | 'missing';\n readonly command: readonly string[];\n readonly resolved?: string;\n}\n\n/** doctor가 내는 기계 판독 보고서다. */\ninterface DoctorReport {\n readonly format: 'isthmus-doctor';\n readonly version: 1;\n readonly status: 'ok' | 'incomplete';\n readonly project: string;\n readonly checks: readonly DoctorCheck[];\n}\n\n/**\n * capture 설정 JSON을 검증하고 참조한 실행 파일이 실제로 있는지 확인한다.\n *\n * producer를 실행하거나 버전을 묻지 않는다 — 제품은 JSON만 읽는다. 여기서\n * 확인하는 것은 `scripts/capture-preflight.mjs`가 실행할 명령의 첫 토큰이\n * 존재하는 파일(또는 PATH 위의 실행 파일)인지뿐이다.\n */\nexport async function runDoctorCommand(\n arguments_: readonly string[],\n readTextFile: ReadTextFile,\n fileSystem: CaptureFileSystem,\n cwd: string,\n pathValue: string = '',\n): Promise<CommandResult> {\n if (arguments_[0] !== 'doctor') return usageError();\n const rest = arguments_.slice(1);\n if (rest.length !== 1 || rest[0]!.startsWith('--')) return usageError();\n const configPath = rest[0]!;\n\n let text: string;\n try {\n text = await readTextFile(configPath);\n } catch {\n return inputFailure(\n 'Unable to read the capture config; check that the file exists and is readable.\\n',\n );\n }\n if (text.length > MAX_INPUT_TEXT_LENGTH) {\n return inputFailure('The capture config exceeds the input size limits.\\n');\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch (error) {\n if (isJsonParseFailure(error)) {\n return inputFailure(\n 'The capture config is not valid JSON; regenerate it with isthmus init.\\n',\n );\n }\n throw error;\n }\n if (!isJsonObject(parsed) || !isSafeNonEmptyString(parsed.project)) {\n return inputFailure('The capture config must be a JSON object with a project path.\\n');\n }\n let project: string;\n try {\n project = await fileSystem.realPath(parsed.project);\n } catch {\n return inputFailure(\n 'The capture config project path does not exist; point it at the app root.\\n',\n );\n }\n try {\n validateCaptureConfig(parsed, project);\n } catch (error) {\n if (error instanceof CaptureConfigValidationError) {\n return inputFailure(`The capture config violates the workflow contract: ${error.message}\\n`);\n }\n throw error;\n }\n\n const pathEntries = pathValue.split(':').filter((entry) => entry.length > 0);\n const checks: DoctorCheck[] = [];\n const names = [\n ...producerNames(parsed),\n ...objectCommands(parsed.messages, parsed),\n ...objectCommands(parsed.events, parsed),\n ...(parsed.prepare as readonly (readonly string[])[]).map((command, index) => ({\n name: `prepare-${index + 1}`,\n command,\n })),\n ];\n for (const { name, command } of names) {\n const resolved = await resolveExecutable(command[0]!, project, fileSystem, pathEntries);\n checks.push({\n name,\n status: resolved === undefined ? 'missing' : 'ok',\n command,\n ...(resolved === undefined ? {} : { resolved }),\n });\n }\n const report: DoctorReport = {\n format: 'isthmus-doctor',\n version: 1,\n status: checks.every(({ status }) => status === 'ok') ? 'ok' : 'incomplete',\n project,\n checks,\n };\n return {\n standardOutput: encodeSortedJson(report),\n standardError: '',\n exitCode: report.status === 'ok' ? 0 : 1,\n };\n}\n\n/** 설정에 선언된 기본 producer 명령과 이름을 순서대로 모은다. */\nfunction producerNames(\n config: Record<string, unknown>,\n): ReadonlyArray<{ name: string; command: readonly string[] }> {\n return [\n { name: 'dartograph', command: config.dartograph as readonly string[] },\n ...(['cartograph', 'kartograph'] as const)\n .filter((name) => config[name] !== undefined)\n .map((name) => ({ name, command: config[name] as readonly string[] })),\n ];\n}\n\n/** messages·events의 사용자 지정 명령만 검사한다. `true`는 기본 명령 재사용이다. */\nfunction objectCommands(\n value: unknown,\n config: Record<string, unknown>,\n): ReadonlyArray<{ name: string; command: readonly string[] }> {\n if (value === undefined || value === true || !isJsonObject(value)) return [];\n return Object.entries(value)\n .filter(([name, command]) => config[name] !== undefined && Array.isArray(command))\n .map(([name, command]) => ({ name, command: command as readonly string[] }));\n}\n\n/** 첫 토큰이 존재하는 파일이거나 PATH 위의 실행 파일인지 확인한다. */\nasync function resolveExecutable(\n token: string,\n project: string,\n fileSystem: CaptureFileSystem,\n pathEntries: readonly string[],\n): Promise<string | undefined> {\n if (token.includes('/') || token.includes('\\\\')) {\n const candidate = isAbsolute(token) ? token : resolve(project, token);\n return (await fileSystem.statPath(candidate)) === 'file' ? candidate : undefined;\n }\n for (const entry of pathEntries) {\n const candidate = join(entry, token);\n if ((await fileSystem.statPath(candidate)) === 'file') return candidate;\n }\n return undefined;\n}\n\n/** 잘못된 CLI 호출을 경로 없는 사용법과 코드 64로 바꾼다. */\nfunction usageError(): CommandResult {\n return { standardOutput: '', standardError: `${doctorUsage}\\n`, exitCode: 64 };\n}\n\n/** doctor 명령의 한 줄 사용법이다. */\nexport const doctorUsage =\n 'Usage: isthmus doctor <capture.json>';"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `isthmus extract-js` — React Native 호출 측 bridge-facts 생산자다.
|
|
3
|
+
*
|
|
4
|
+
* JS/TS 파일(또는 그 파일을 담은 디렉터리)을 토큰 스캔해 정적 모듈·컴포넌트
|
|
5
|
+
* 이름과 바인딩 해석이 끝난 메서드 호출을 bridge-facts v1 문서로 낸다.
|
|
6
|
+
* 해석이 멈춘 곳은 limitations로만 보고한다 — 문서를 거짓으로 완전하게
|
|
7
|
+
* 만들지 않는다.
|
|
8
|
+
*/
|
|
9
|
+
import { type Clock, type CommandResult, type ReadTextFile } from './command-support.ts';
|
|
10
|
+
/** 디렉터리 항목 — 워커가 경로 종류를 다시 질의하지 않게 타입을 함께 준다. */
|
|
11
|
+
export interface JsDirectoryEntry {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly isFile: boolean;
|
|
14
|
+
readonly isDirectory: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** extract-js가 필요로 하는 최소 파일시스템 표면이다. */
|
|
17
|
+
export interface ExtractJsFileSystem {
|
|
18
|
+
/** 경로 종류를 돌려준다 — 없으면 'missing'. */
|
|
19
|
+
readonly statPath: (path: string) => Promise<'file' | 'directory' | 'missing'>;
|
|
20
|
+
/** 디렉터리의 바로 아래 항목들을 나열한다. */
|
|
21
|
+
readonly listDirectory: (path: string) => Promise<readonly JsDirectoryEntry[]>;
|
|
22
|
+
/** 심볼릭 링크·`..`를 풀어 절대 경로로 만든다. */
|
|
23
|
+
readonly realPath: (path: string) => Promise<string>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* extract-js 인자를 실행해 프로세스에 독립적인 결과를 반환한다.
|
|
27
|
+
*
|
|
28
|
+
* 성공 시 정렬된 bridge-facts JSON을 stdout에 쓴다. 입력 실패는 원인 종류와
|
|
29
|
+
* 해결 방향을 담은 코드 2, 잘못된 호출은 사용법과 코드 64다.
|
|
30
|
+
*/
|
|
31
|
+
export declare function runExtractJsCommand(arguments_: readonly string[], fileSystem: ExtractJsFileSystem, readSource: ReadTextFile, now?: Clock, version?: string): Promise<CommandResult>;
|
|
32
|
+
/** extract-js 명령의 한 줄 사용법이다. */
|
|
33
|
+
export declare const extractJsUsage = "Usage: isthmus extract-js <file-or-dir> [more...] [--project <dir>]";
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `isthmus extract-js` — React Native 호출 측 bridge-facts 생산자다.
|
|
3
|
+
*
|
|
4
|
+
* JS/TS 파일(또는 그 파일을 담은 디렉터리)을 토큰 스캔해 정적 모듈·컴포넌트
|
|
5
|
+
* 이름과 바인딩 해석이 끝난 메서드 호출을 bridge-facts v1 문서로 낸다.
|
|
6
|
+
* 해석이 멈춘 곳은 limitations로만 보고한다 — 문서를 거짓으로 완전하게
|
|
7
|
+
* 만들지 않는다.
|
|
8
|
+
*/
|
|
9
|
+
import { sep } from 'node:path';
|
|
10
|
+
import { createJsFactsDocument, JS_SOURCE_EXTENSIONS, } from "../extract/js-document.js";
|
|
11
|
+
import { encodeSortedJson } from "../report/sorted-json.js";
|
|
12
|
+
import { inputFailure, MAX_INPUT_TEXT_LENGTH, MAX_TOTAL_INPUT_TEXT_LENGTH, } from "./command-support.js";
|
|
13
|
+
import { parseCommandArguments } from "./parse-arguments.js";
|
|
14
|
+
/** 스캔 대상 확장자 집합이다. */
|
|
15
|
+
const jsExtensions = new Set(JS_SOURCE_EXTENSIONS);
|
|
16
|
+
/** 워크에서 건너뛸 디렉터리다 — 종속성·빌드 산출물·VCS 내부다. */
|
|
17
|
+
const SKIPPED_DIRECTORIES = new Set([
|
|
18
|
+
'node_modules', 'dist', 'build', 'coverage', 'out',
|
|
19
|
+
'.git', '.svn', '.hg', '.expo', '.next', '.turbo',
|
|
20
|
+
]);
|
|
21
|
+
/** 한 번의 호출이 받을 수 있는 입력 경로 수 상한이다. */
|
|
22
|
+
const MAX_INPUT_PATHS = 256;
|
|
23
|
+
/** 스캔 대상 파일 수 상한이다. */
|
|
24
|
+
const MAX_SCANNED_FILES = 10_000;
|
|
25
|
+
/**
|
|
26
|
+
* extract-js 인자를 실행해 프로세스에 독립적인 결과를 반환한다.
|
|
27
|
+
*
|
|
28
|
+
* 성공 시 정렬된 bridge-facts JSON을 stdout에 쓴다. 입력 실패는 원인 종류와
|
|
29
|
+
* 해결 방향을 담은 코드 2, 잘못된 호출은 사용법과 코드 64다.
|
|
30
|
+
*/
|
|
31
|
+
export async function runExtractJsCommand(arguments_, fileSystem, readSource, now = () => new Date(), version) {
|
|
32
|
+
if (arguments_[0] !== 'extract-js')
|
|
33
|
+
return usageError();
|
|
34
|
+
const options = parseCommandArguments(arguments_.slice(1), ['--project'], []);
|
|
35
|
+
if (options === undefined)
|
|
36
|
+
return usageError();
|
|
37
|
+
const { positionals, valueFlags } = options;
|
|
38
|
+
if (positionals.length === 0 || positionals.length > MAX_INPUT_PATHS) {
|
|
39
|
+
return usageError();
|
|
40
|
+
}
|
|
41
|
+
const roots = await Promise.all(positionals.map((input) => safeRealPath(fileSystem, input)));
|
|
42
|
+
const missingIndex = roots.findIndex((path) => path === undefined);
|
|
43
|
+
if (missingIndex !== -1) {
|
|
44
|
+
return inputFailure(`Unable to read extract-js input ${missingIndex + 1}; `
|
|
45
|
+
+ 'check that the file or directory exists and is readable.\n');
|
|
46
|
+
}
|
|
47
|
+
const projectFlag = valueFlags.get('--project');
|
|
48
|
+
let projectRoot;
|
|
49
|
+
if (projectFlag !== undefined) {
|
|
50
|
+
const root = await safeRealPath(fileSystem, projectFlag);
|
|
51
|
+
if (root === undefined || await fileSystem.statPath(root) !== 'directory') {
|
|
52
|
+
return inputFailure('extract-js --project does not name a readable directory; '
|
|
53
|
+
+ 'pass the project root that contains the inputs.\n');
|
|
54
|
+
}
|
|
55
|
+
projectRoot = normalizeSeparators(root);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
projectRoot = await commonAncestor(roots, fileSystem);
|
|
59
|
+
}
|
|
60
|
+
const discovered = [];
|
|
61
|
+
for (const [index, absolute] of roots.entries()) {
|
|
62
|
+
const kind = await fileSystem.statPath(absolute);
|
|
63
|
+
if (kind === 'missing') {
|
|
64
|
+
return inputFailure(`Unable to read extract-js input ${index + 1}; `
|
|
65
|
+
+ 'check that the file or directory exists and is readable.\n');
|
|
66
|
+
}
|
|
67
|
+
let candidates;
|
|
68
|
+
try {
|
|
69
|
+
candidates = kind === 'directory'
|
|
70
|
+
? await walkDirectory(fileSystem, absolute, MAX_SCANNED_FILES - discovered.length)
|
|
71
|
+
: [absolute];
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return inputFailure(`Unable to list extract-js input ${index + 1}; `
|
|
75
|
+
+ 'check that the directory is readable.\n');
|
|
76
|
+
}
|
|
77
|
+
for (const candidate of candidates) {
|
|
78
|
+
if (!isJsSource(candidate)) {
|
|
79
|
+
if (kind === 'file') {
|
|
80
|
+
return inputFailure(`extract-js input ${index + 1} is not a JS/TS source file; `
|
|
81
|
+
+ 'pass .js/.ts/.jsx/.tsx sources or a directory containing them.\n');
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const relative = relativize(candidate, projectRoot);
|
|
86
|
+
if (relative === undefined || relative === '') {
|
|
87
|
+
return inputFailure(`extract-js input ${index + 1} escapes the project root; `
|
|
88
|
+
+ 'pass --project that contains every input.\n');
|
|
89
|
+
}
|
|
90
|
+
discovered.push({ path: relative, text: '' });
|
|
91
|
+
if (discovered.length > MAX_SCANNED_FILES) {
|
|
92
|
+
return inputFailure(`extract-js input set exceeds ${MAX_SCANNED_FILES} files; `
|
|
93
|
+
+ 'narrow the scanned directories.\n');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// 같은 파일이 여러 입력에서 발견돼도 한 번만 스캔한다.
|
|
98
|
+
const ordered = [...new Map(discovered.map((f) => [f.path, f])).values()]
|
|
99
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
100
|
+
let totalLength = 0;
|
|
101
|
+
for (const file of ordered) {
|
|
102
|
+
let text;
|
|
103
|
+
try {
|
|
104
|
+
text = await readSource(`${projectRoot}/${file.path}`);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return inputFailure(`Unable to read extract-js source ${file.path}; `
|
|
108
|
+
+ 'check that the file exists and is readable.\n');
|
|
109
|
+
}
|
|
110
|
+
totalLength += text.length;
|
|
111
|
+
if (text.length > MAX_INPUT_TEXT_LENGTH ||
|
|
112
|
+
totalLength > MAX_TOTAL_INPUT_TEXT_LENGTH) {
|
|
113
|
+
return inputFailure('extract-js input set exceeds the source size limits; '
|
|
114
|
+
+ 'narrow the scanned directories.\n');
|
|
115
|
+
}
|
|
116
|
+
file.text = text;
|
|
117
|
+
}
|
|
118
|
+
if (version === undefined) {
|
|
119
|
+
return {
|
|
120
|
+
standardOutput: '',
|
|
121
|
+
standardError: 'Unable to read package metadata.\n',
|
|
122
|
+
exitCode: 2,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const document = createJsFactsDocument(ordered, version, now().toISOString(), projectRoot);
|
|
126
|
+
return { standardOutput: encodeSortedJson(document), standardError: '', exitCode: 0 };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* 디렉터리 아래 JS/TS 파일을 재귀로 모은다. 실패는 빈 목록이 아니라 던진다.
|
|
130
|
+
*
|
|
131
|
+
* `budget`은 아직 받을 수 있는 파일 수다 — 거대한 트리 전체를 열거한 뒤에야
|
|
132
|
+
* 상한 오류를 내지 않도록 예산을 넘으면 즉시 멈추고 호출자가 상한을 보고한다.
|
|
133
|
+
*/
|
|
134
|
+
async function walkDirectory(fileSystem, directory, budget) {
|
|
135
|
+
const found = [];
|
|
136
|
+
const pending = [directory];
|
|
137
|
+
while (pending.length > 0 && found.length <= budget) {
|
|
138
|
+
const current = pending.pop();
|
|
139
|
+
let entries;
|
|
140
|
+
try {
|
|
141
|
+
entries = await fileSystem.listDirectory(current);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
throw new Error(`listDirectory failed: ${current}`, { cause: error });
|
|
145
|
+
}
|
|
146
|
+
for (const entry of [...entries]
|
|
147
|
+
.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
148
|
+
const child = `${current}/${entry.name}`;
|
|
149
|
+
if (entry.isDirectory) {
|
|
150
|
+
if (SKIPPED_DIRECTORIES.has(entry.name) || entry.name.startsWith('.')) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
pending.push(child);
|
|
154
|
+
}
|
|
155
|
+
else if (entry.isFile && isJsSource(entry.name)) {
|
|
156
|
+
found.push(child);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return found;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* `--project` 없이 입력들의 공통 조상 디렉터리를 찾는다.
|
|
164
|
+
*
|
|
165
|
+
* 파일 입력은 디렉터리로 접고, 모든 입력을 포함하는 가장 깊은 공통
|
|
166
|
+
* 디렉터리를 고른다. 프로젝트 밖 경로가 facts의 상대 경로를 깨지 않게 한다.
|
|
167
|
+
*/
|
|
168
|
+
async function commonAncestor(absolutePaths, fileSystem) {
|
|
169
|
+
const roots = [];
|
|
170
|
+
for (const absolute of absolutePaths) {
|
|
171
|
+
const normalized = normalizeSeparators(absolute);
|
|
172
|
+
const isFile = await fileSystem.statPath(absolute) === 'file';
|
|
173
|
+
roots.push(isFile ? normalized.slice(0, normalized.lastIndexOf('/')) : normalized);
|
|
174
|
+
}
|
|
175
|
+
let common = roots[0] ?? '/';
|
|
176
|
+
for (const candidate of roots.slice(1)) {
|
|
177
|
+
while (candidate !== common && !candidate.startsWith(`${common}/`)) {
|
|
178
|
+
const parent = common.slice(0, common.lastIndexOf('/'));
|
|
179
|
+
if (parent === '' || parent === common) {
|
|
180
|
+
common = '/';
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
common = parent;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return common;
|
|
187
|
+
}
|
|
188
|
+
/** 절대 경로를 프로젝트 상대 POSIX 경로로 바꾼다. 루트 밖이면 undefined다. */
|
|
189
|
+
function relativize(absolute, projectRoot) {
|
|
190
|
+
const normalized = normalizeSeparators(absolute);
|
|
191
|
+
const root = normalizeSeparators(projectRoot);
|
|
192
|
+
if (root === '/')
|
|
193
|
+
return normalized.replace(/^\/+/, '');
|
|
194
|
+
if (normalized === root)
|
|
195
|
+
return '';
|
|
196
|
+
if (!normalized.startsWith(`${root}/`))
|
|
197
|
+
return undefined;
|
|
198
|
+
return normalized.slice(root.length + 1);
|
|
199
|
+
}
|
|
200
|
+
/** 스캔 대상 확장자 여부다. */
|
|
201
|
+
function isJsSource(path) {
|
|
202
|
+
const dot = path.lastIndexOf('.');
|
|
203
|
+
return dot !== -1 && jsExtensions.has(path.slice(dot + 1));
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* OS 경로 구분자를 계약이 요구하는 `/`로 통일한다.
|
|
207
|
+
*
|
|
208
|
+
* `\`는 POSIX에서 유효한 파일명 문자이므로 구분자가 `\`인 플랫폼에서만
|
|
209
|
+
* 바꾼다 — 무조건 치환하면 `a\b.ts` 같은 합법 파일명이 깨진다.
|
|
210
|
+
*/
|
|
211
|
+
function normalizeSeparators(path) {
|
|
212
|
+
return sep === '\\' ? path.replace(/\\/g, '/') : path;
|
|
213
|
+
}
|
|
214
|
+
/** realpath가 던지는 입력을 경로 없는 undefined로 정규화한다. */
|
|
215
|
+
async function safeRealPath(fileSystem, path) {
|
|
216
|
+
try {
|
|
217
|
+
return await fileSystem.realPath(path);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/** 잘못된 CLI 호출을 경로 없는 사용법과 코드 64로 바꾼다. */
|
|
224
|
+
function usageError() {
|
|
225
|
+
return {
|
|
226
|
+
standardOutput: '',
|
|
227
|
+
standardError: `${extractJsUsage}\n`,
|
|
228
|
+
exitCode: 64,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/** extract-js 명령의 한 줄 사용법이다. */
|
|
232
|
+
export const extractJsUsage = 'Usage: isthmus extract-js <file-or-dir> [more...] [--project <dir>]';
|
|
233
|
+
//# sourceMappingURL=extract-js-command.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract-js-command.js","sourceRoot":"","sources":["../../src/cli/extract-js-command.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `isthmus extract-js` — React Native 호출 측 bridge-facts 생산자다.\n *\n * JS/TS 파일(또는 그 파일을 담은 디렉터리)을 토큰 스캔해 정적 모듈·컴포넌트\n * 이름과 바인딩 해석이 끝난 메서드 호출을 bridge-facts v1 문서로 낸다.\n * 해석이 멈춘 곳은 limitations로만 보고한다 — 문서를 거짓으로 완전하게\n * 만들지 않는다.\n */\n\nimport { sep } from 'node:path';\nimport {\n createJsFactsDocument,\n JS_SOURCE_EXTENSIONS,\n} from '../extract/js-document.ts';\nimport { encodeSortedJson } from '../report/sorted-json.ts';\nimport {\n inputFailure,\n MAX_INPUT_TEXT_LENGTH,\n MAX_TOTAL_INPUT_TEXT_LENGTH,\n type Clock,\n type CommandResult,\n type ReadTextFile,\n} from './command-support.ts';\nimport { parseCommandArguments } from './parse-arguments.ts';\n\n/** 디렉터리 항목 — 워커가 경로 종류를 다시 질의하지 않게 타입을 함께 준다. */\nexport interface JsDirectoryEntry {\n readonly name: string;\n readonly isFile: boolean;\n readonly isDirectory: boolean;\n}\n\n/** extract-js가 필요로 하는 최소 파일시스템 표면이다. */\nexport interface ExtractJsFileSystem {\n /** 경로 종류를 돌려준다 — 없으면 'missing'. */\n readonly statPath: (path: string) => Promise<'file' | 'directory' | 'missing'>;\n /** 디렉터리의 바로 아래 항목들을 나열한다. */\n readonly listDirectory: (path: string) => Promise<readonly JsDirectoryEntry[]>;\n /** 심볼릭 링크·`..`를 풀어 절대 경로로 만든다. */\n readonly realPath: (path: string) => Promise<string>;\n}\n\n/** 스캔 대상 확장자 집합이다. */\nconst jsExtensions = new Set<string>(JS_SOURCE_EXTENSIONS);\n\n/** 워크에서 건너뛸 디렉터리다 — 종속성·빌드 산출물·VCS 내부다. */\nconst SKIPPED_DIRECTORIES = new Set([\n 'node_modules', 'dist', 'build', 'coverage', 'out',\n '.git', '.svn', '.hg', '.expo', '.next', '.turbo',\n]);\n\n/** 한 번의 호출이 받을 수 있는 입력 경로 수 상한이다. */\nconst MAX_INPUT_PATHS = 256;\n/** 스캔 대상 파일 수 상한이다. */\nconst MAX_SCANNED_FILES = 10_000;\n\n/**\n * extract-js 인자를 실행해 프로세스에 독립적인 결과를 반환한다.\n *\n * 성공 시 정렬된 bridge-facts JSON을 stdout에 쓴다. 입력 실패는 원인 종류와\n * 해결 방향을 담은 코드 2, 잘못된 호출은 사용법과 코드 64다.\n */\nexport async function runExtractJsCommand(\n arguments_: readonly string[],\n fileSystem: ExtractJsFileSystem,\n readSource: ReadTextFile,\n now: Clock = () => new Date(),\n version?: string,\n): Promise<CommandResult> {\n if (arguments_[0] !== 'extract-js') return usageError();\n const options = parseCommandArguments(arguments_.slice(1), ['--project'], []);\n if (options === undefined) return usageError();\n const { positionals, valueFlags } = options;\n if (positionals.length === 0 || positionals.length > MAX_INPUT_PATHS) {\n return usageError();\n }\n\n const roots = await Promise.all(\n positionals.map((input) => safeRealPath(fileSystem, input)),\n );\n const missingIndex = roots.findIndex((path) => path === undefined);\n if (missingIndex !== -1) {\n return inputFailure(\n `Unable to read extract-js input ${missingIndex + 1}; `\n + 'check that the file or directory exists and is readable.\\n',\n );\n }\n\n const projectFlag = valueFlags.get('--project');\n let projectRoot: string;\n if (projectFlag !== undefined) {\n const root = await safeRealPath(fileSystem, projectFlag);\n if (root === undefined || await fileSystem.statPath(root) !== 'directory') {\n return inputFailure(\n 'extract-js --project does not name a readable directory; '\n + 'pass the project root that contains the inputs.\\n',\n );\n }\n projectRoot = normalizeSeparators(root);\n } else {\n projectRoot = await commonAncestor(roots as string[], fileSystem);\n }\n\n const discovered: { path: string; text: string }[] = [];\n for (const [index, absolute] of (roots as string[]).entries()) {\n const kind = await fileSystem.statPath(absolute);\n if (kind === 'missing') {\n return inputFailure(\n `Unable to read extract-js input ${index + 1}; `\n + 'check that the file or directory exists and is readable.\\n',\n );\n }\n let candidates: string[];\n try {\n candidates = kind === 'directory'\n ? await walkDirectory(\n fileSystem, absolute, MAX_SCANNED_FILES - discovered.length)\n : [absolute];\n } catch {\n return inputFailure(\n `Unable to list extract-js input ${index + 1}; `\n + 'check that the directory is readable.\\n',\n );\n }\n for (const candidate of candidates) {\n if (!isJsSource(candidate)) {\n if (kind === 'file') {\n return inputFailure(\n `extract-js input ${index + 1} is not a JS/TS source file; `\n + 'pass .js/.ts/.jsx/.tsx sources or a directory containing them.\\n',\n );\n }\n continue;\n }\n const relative = relativize(candidate, projectRoot);\n if (relative === undefined || relative === '') {\n return inputFailure(\n `extract-js input ${index + 1} escapes the project root; `\n + 'pass --project that contains every input.\\n',\n );\n }\n discovered.push({ path: relative, text: '' });\n if (discovered.length > MAX_SCANNED_FILES) {\n return inputFailure(\n `extract-js input set exceeds ${MAX_SCANNED_FILES} files; `\n + 'narrow the scanned directories.\\n',\n );\n }\n }\n }\n\n // 같은 파일이 여러 입력에서 발견돼도 한 번만 스캔한다.\n const ordered = [...new Map(discovered.map((f) => [f.path, f])).values()]\n .sort((left, right) => left.path.localeCompare(right.path));\n let totalLength = 0;\n for (const file of ordered) {\n let text: string;\n try {\n text = await readSource(`${projectRoot}/${file.path}`);\n } catch {\n return inputFailure(\n `Unable to read extract-js source ${file.path}; `\n + 'check that the file exists and is readable.\\n',\n );\n }\n totalLength += text.length;\n if (\n text.length > MAX_INPUT_TEXT_LENGTH ||\n totalLength > MAX_TOTAL_INPUT_TEXT_LENGTH\n ) {\n return inputFailure(\n 'extract-js input set exceeds the source size limits; '\n + 'narrow the scanned directories.\\n',\n );\n }\n file.text = text;\n }\n\n if (version === undefined) {\n return {\n standardOutput: '',\n standardError: 'Unable to read package metadata.\\n',\n exitCode: 2,\n };\n }\n const document = createJsFactsDocument(\n ordered, version, now().toISOString(), projectRoot,\n );\n return { standardOutput: encodeSortedJson(document), standardError: '', exitCode: 0 };\n}\n\n/**\n * 디렉터리 아래 JS/TS 파일을 재귀로 모은다. 실패는 빈 목록이 아니라 던진다.\n *\n * `budget`은 아직 받을 수 있는 파일 수다 — 거대한 트리 전체를 열거한 뒤에야\n * 상한 오류를 내지 않도록 예산을 넘으면 즉시 멈추고 호출자가 상한을 보고한다.\n */\nasync function walkDirectory(\n fileSystem: ExtractJsFileSystem,\n directory: string,\n budget: number,\n): Promise<string[]> {\n const found: string[] = [];\n const pending = [directory];\n while (pending.length > 0 && found.length <= budget) {\n const current = pending.pop()!;\n let entries: readonly JsDirectoryEntry[];\n try {\n entries = await fileSystem.listDirectory(current);\n } catch (error) {\n throw new Error(`listDirectory failed: ${current}`, { cause: error });\n }\n for (const entry of [...entries]\n .sort((left, right) => left.name.localeCompare(right.name))) {\n const child = `${current}/${entry.name}`;\n if (entry.isDirectory) {\n if (SKIPPED_DIRECTORIES.has(entry.name) || entry.name.startsWith('.')) {\n continue;\n }\n pending.push(child);\n } else if (entry.isFile && isJsSource(entry.name)) {\n found.push(child);\n }\n }\n }\n return found;\n}\n\n/**\n * `--project` 없이 입력들의 공통 조상 디렉터리를 찾는다.\n *\n * 파일 입력은 디렉터리로 접고, 모든 입력을 포함하는 가장 깊은 공통\n * 디렉터리를 고른다. 프로젝트 밖 경로가 facts의 상대 경로를 깨지 않게 한다.\n */\nasync function commonAncestor(\n absolutePaths: readonly string[],\n fileSystem: ExtractJsFileSystem,\n): Promise<string> {\n const roots: string[] = [];\n for (const absolute of absolutePaths) {\n const normalized = normalizeSeparators(absolute);\n const isFile = await fileSystem.statPath(absolute) === 'file';\n roots.push(isFile ? normalized.slice(0, normalized.lastIndexOf('/')) : normalized);\n }\n let common = roots[0] ?? '/';\n for (const candidate of roots.slice(1)) {\n while (candidate !== common && !candidate.startsWith(`${common}/`)) {\n const parent = common.slice(0, common.lastIndexOf('/'));\n if (parent === '' || parent === common) {\n common = '/';\n break;\n }\n common = parent;\n }\n }\n return common;\n}\n\n/** 절대 경로를 프로젝트 상대 POSIX 경로로 바꾼다. 루트 밖이면 undefined다. */\nfunction relativize(absolute: string, projectRoot: string): string | undefined {\n const normalized = normalizeSeparators(absolute);\n const root = normalizeSeparators(projectRoot);\n if (root === '/') return normalized.replace(/^\\/+/, '');\n if (normalized === root) return '';\n if (!normalized.startsWith(`${root}/`)) return undefined;\n return normalized.slice(root.length + 1);\n}\n\n/** 스캔 대상 확장자 여부다. */\nfunction isJsSource(path: string): boolean {\n const dot = path.lastIndexOf('.');\n return dot !== -1 && jsExtensions.has(path.slice(dot + 1));\n}\n\n/**\n * OS 경로 구분자를 계약이 요구하는 `/`로 통일한다.\n *\n * `\\`는 POSIX에서 유효한 파일명 문자이므로 구분자가 `\\`인 플랫폼에서만\n * 바꾼다 — 무조건 치환하면 `a\\b.ts` 같은 합법 파일명이 깨진다.\n */\nfunction normalizeSeparators(path: string): string {\n return sep === '\\\\' ? path.replace(/\\\\/g, '/') : path;\n}\n\n/** realpath가 던지는 입력을 경로 없는 undefined로 정규화한다. */\nasync function safeRealPath(\n fileSystem: ExtractJsFileSystem,\n path: string,\n): Promise<string | undefined> {\n try {\n return await fileSystem.realPath(path);\n } catch {\n return undefined;\n }\n}\n\n/** 잘못된 CLI 호출을 경로 없는 사용법과 코드 64로 바꾼다. */\nfunction usageError(): CommandResult {\n return {\n standardOutput: '',\n standardError: `${extractJsUsage}\\n`,\n exitCode: 64,\n };\n}\n\n/** extract-js 명령의 한 줄 사용법이다. */\nexport const extractJsUsage =\n 'Usage: isthmus extract-js <file-or-dir> [more...] [--project <dir>]';\n"]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { isBridgeJoinDeferred, joinBridgeDocuments, MAX_DOCUMENTS_PER_JOIN, } from "../join/join.js";
|
|
1
|
+
import { emptyBridgeJoinResult, isBridgeJoinDeferred, joinBridgeDocuments, MAX_DOCUMENTS_PER_JOIN, } from "../join/join.js";
|
|
2
|
+
import { joinMessageBridges } from "../join/messages.js";
|
|
2
3
|
import { BridgeGraphLimitError, BridgeGraphValidationError, createBridgeGraph, renderBridgeGraph, } from "../report/graph.js";
|
|
3
|
-
import { bridgeJoinDeferredError, inputFailureResult, internalError,
|
|
4
|
+
import { bridgeJoinDeferredError, inputFailureResult, internalError, readBridgeInputs, } from "./command-support.js";
|
|
4
5
|
import { parseCommandArguments } from "./parse-arguments.js";
|
|
5
6
|
/** graph 인자를 실행해 경계 그래프와 종료 코드를 반환한다. */
|
|
6
7
|
export async function runGraphCommand(arguments_, readTextFile) {
|
|
@@ -8,12 +9,18 @@ export async function runGraphCommand(arguments_, readTextFile) {
|
|
|
8
9
|
if (options === undefined)
|
|
9
10
|
return graphUsageError();
|
|
10
11
|
try {
|
|
11
|
-
const
|
|
12
|
-
const
|
|
12
|
+
const { bridges, messages } = await readBridgeInputs(options.inputPaths, readTextFile);
|
|
13
|
+
const project = bridges[0]?.project ?? messages[0].project;
|
|
14
|
+
const joined = bridges.length > 0
|
|
15
|
+
? joinBridgeDocuments(bridges)
|
|
16
|
+
: emptyBridgeJoinResult();
|
|
13
17
|
if (isBridgeJoinDeferred(joined)) {
|
|
14
|
-
return bridgeJoinDeferredError(joined.observedFacts,
|
|
18
|
+
return bridgeJoinDeferredError(joined.observedFacts, bridges.length);
|
|
15
19
|
}
|
|
16
|
-
const
|
|
20
|
+
const messageJoin = messages.length > 0
|
|
21
|
+
? joinMessageBridges(messages, project)
|
|
22
|
+
: undefined;
|
|
23
|
+
const graph = createBridgeGraph(joined, messageJoin);
|
|
17
24
|
return {
|
|
18
25
|
standardOutput: renderBridgeGraph(graph, options.format),
|
|
19
26
|
standardError: '',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-command.js","sourceRoot":"","sources":["../../src/cli/graph-command.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,qBAAqB,EACrB,0BAA0B,EAC1B,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,uBAAuB,EACvB,kBAAkB,EAClB,aAAa,EACb,
|
|
1
|
+
{"version":3,"file":"graph-command.js","sourceRoot":"","sources":["../../src/cli/graph-command.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n emptyBridgeJoinResult,\n isBridgeJoinDeferred,\n joinBridgeDocuments,\n MAX_DOCUMENTS_PER_JOIN,\n} from '../join/join.ts';\nimport { joinMessageBridges } from '../join/messages.ts';\nimport {\n BridgeGraphLimitError,\n BridgeGraphValidationError,\n createBridgeGraph,\n renderBridgeGraph,\n} from '../report/graph.ts';\nimport {\n bridgeJoinDeferredError,\n inputFailureResult,\n internalError,\n readBridgeInputs,\n type CommandResult,\n type ReadTextFile,\n} from './command-support.ts';\nimport { parseCommandArguments } from './parse-arguments.ts';\n\n/** graph 인자를 실행해 경계 그래프와 종료 코드를 반환한다. */\nexport async function runGraphCommand(\n arguments_: readonly string[],\n readTextFile: ReadTextFile,\n): Promise<CommandResult> {\n const options = parseGraphArguments(arguments_);\n if (options === undefined) return graphUsageError();\n try {\n const { bridges, messages } = await readBridgeInputs(options.inputPaths, readTextFile);\n const project = bridges[0]?.project ?? messages[0]!.project;\n const joined = bridges.length > 0\n ? joinBridgeDocuments(bridges)\n : emptyBridgeJoinResult();\n if (isBridgeJoinDeferred(joined)) {\n return bridgeJoinDeferredError(joined.observedFacts, bridges.length);\n }\n const messageJoin = messages.length > 0\n ? joinMessageBridges(messages, project)\n : undefined;\n const graph = createBridgeGraph(joined, messageJoin);\n return {\n standardOutput: renderBridgeGraph(graph, options.format),\n standardError: '',\n exitCode: 0,\n };\n } catch (error) {\n if (\n error instanceof BridgeGraphLimitError ||\n error instanceof BridgeGraphValidationError\n ) {\n return {\n standardOutput: '',\n standardError: `${error.message}\\n`,\n exitCode: 2,\n };\n }\n return inputFailureResult(error) ?? internalError();\n }\n}\n\n/** graph의 입력 파일과 선택 출력 형식을 검증한다. 플래그는 어디에 와도 된다. */\nfunction parseGraphArguments(arguments_: readonly string[]): GraphOptions | undefined {\n if (arguments_[0] !== 'graph') return undefined;\n const parsed = parseCommandArguments(arguments_.slice(1), ['--format'], []);\n if (parsed === undefined) return undefined;\n const format = parsed.valueFlags.get('--format');\n if (format !== undefined && !isGraphFormat(format)) return undefined;\n const inputPaths = [...parsed.positionals];\n if (\n inputPaths.length < 2 ||\n inputPaths.length > MAX_DOCUMENTS_PER_JOIN\n ) {\n return undefined;\n }\n return { inputPaths, format: format ?? 'json' };\n}\n\n/** 지원하는 그래프 형식인지 확인한다. */\nfunction isGraphFormat(value: string): value is GraphFormat {\n return value === 'json' || value === 'dot' || value === 'mermaid';\n}\n\n/** graph 사용 오류를 경로 없는 코드 64로 바꾼다. */\nfunction graphUsageError(): CommandResult {\n return {\n standardOutput: '',\n standardError: `${graphUsage}\\n`,\n exitCode: 64,\n };\n}\n\n/** 검증된 graph 명령 옵션이다. */\ninterface GraphOptions {\n readonly inputPaths: readonly string[];\n readonly format: GraphFormat;\n}\n\ntype GraphFormat = 'json' | 'dot' | 'mermaid';\n\n/** graph 명령의 한 줄 사용법이다. */\nexport const graphUsage =\n 'Usage: isthmus graph <bridge-facts.json> <bridge-facts.json> '\n + '[more...] [--format json|dot|mermaid]';\n"]}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { CommandResult, ReadTextFile } from './command-support.ts';
|
|
2
|
+
/** 파일·심볼·CI 변경 목록을 브리지 영향 보고서로 바꾸는 CLI 경계다. */
|
|
3
|
+
export declare function runImpactCommand(arguments_: readonly string[], readTextFile: ReadTextFile): Promise<CommandResult>;
|
|
4
|
+
/** impact 명령의 독립적인 선택 방식과 CI 실패 옵션이다. */
|
|
5
|
+
export declare const impactUsage: string;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { ImpactSelectionValidationError, parseImpactSelection } from "../exchange/impact-selection.js";
|
|
2
|
+
import { isSafeNonEmptyString } from "../exchange/parse.js";
|
|
3
|
+
import { parseBridgeRuntime, RuntimeValidationError } from "../exchange/runtime.js";
|
|
4
|
+
import { MAX_DOCUMENTS_PER_JOIN } from "../join/join.js";
|
|
5
|
+
import { createBridgeImpact, encodeBridgeImpact, hasImpactBlockers } from "../report/impact.js";
|
|
6
|
+
import { inputFailure, inputFailureResult, internalError, isJsonParseFailure, MAX_INPUT_TEXT_LENGTH, readBridgeDocuments, } from "./command-support.js";
|
|
7
|
+
import { parseCommandArguments } from "./parse-arguments.js";
|
|
8
|
+
/** 파일·심볼·CI 변경 목록을 브리지 영향 보고서로 바꾸는 CLI 경계다. */
|
|
9
|
+
export async function runImpactCommand(arguments_, readTextFile) {
|
|
10
|
+
const parsed = parseCommandArguments(arguments_.slice(1), ['--file', '--symbol', '--changes', '--runtime', '--revision'], ['--strict', '--compact']);
|
|
11
|
+
if (parsed === undefined || ['--file', '--symbol', '--changes'].filter((flag) => parsed.valueFlags.has(flag)).length !== 1 ||
|
|
12
|
+
parsed.positionals.length < 2 || parsed.positionals.length > MAX_DOCUMENTS_PER_JOIN) {
|
|
13
|
+
return usageError();
|
|
14
|
+
}
|
|
15
|
+
const runtimePath = parsed.valueFlags.get('--runtime');
|
|
16
|
+
const revision = parsed.valueFlags.get('--revision');
|
|
17
|
+
if ((runtimePath === undefined) !== (revision === undefined) ||
|
|
18
|
+
(revision !== undefined && !isSafeNonEmptyString(revision)))
|
|
19
|
+
return usageError();
|
|
20
|
+
try {
|
|
21
|
+
let selection;
|
|
22
|
+
let supplementalTextLength = 0;
|
|
23
|
+
const changesPath = parsed.valueFlags.get('--changes');
|
|
24
|
+
if (changesPath !== undefined) {
|
|
25
|
+
const loaded = await readSupplemental(changesPath, readTextFile, 'changes', parseImpactSelection);
|
|
26
|
+
if ('exitCode' in loaded)
|
|
27
|
+
return loaded;
|
|
28
|
+
selection = loaded.document;
|
|
29
|
+
supplementalTextLength += loaded.textLength;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const file = parsed.valueFlags.get('--file');
|
|
33
|
+
const symbol = parsed.valueFlags.get('--symbol');
|
|
34
|
+
try {
|
|
35
|
+
selection = parseImpactSelection({ format: 'isthmus-changes', version: 1,
|
|
36
|
+
files: file === undefined ? [] : [file], symbols: symbol === undefined ? [] : [symbol] });
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error instanceof ImpactSelectionValidationError)
|
|
40
|
+
return usageError();
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
let runtime;
|
|
45
|
+
if (runtimePath !== undefined && revision !== undefined) {
|
|
46
|
+
const loaded = await readSupplemental(runtimePath, readTextFile, 'runtime', parseBridgeRuntime);
|
|
47
|
+
if ('exitCode' in loaded)
|
|
48
|
+
return loaded;
|
|
49
|
+
supplementalTextLength += loaded.textLength;
|
|
50
|
+
runtime = { document: loaded.document, revision };
|
|
51
|
+
}
|
|
52
|
+
const documents = await readBridgeDocuments(parsed.positionals, readTextFile, supplementalTextLength);
|
|
53
|
+
const report = createBridgeImpact(documents, selection, runtime);
|
|
54
|
+
const blocked = parsed.booleanFlags.has('--strict') && hasImpactBlockers(report);
|
|
55
|
+
return {
|
|
56
|
+
standardOutput: encodeBridgeImpact(report, parsed.booleanFlags.has('--compact')),
|
|
57
|
+
standardError: blocked
|
|
58
|
+
? 'Change preflight requires review: related errors, analysis gaps, or unobserved selectors remain.\n'
|
|
59
|
+
: report.status === 'unobserved'
|
|
60
|
+
? 'No selected bridge facts were observed; inspect source coverage and use a pre-change snapshot for deleted code.\n'
|
|
61
|
+
: '',
|
|
62
|
+
exitCode: blocked ? 1 : 0,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (error instanceof RuntimeValidationError)
|
|
67
|
+
return inputFailure(`${error.message}\n`);
|
|
68
|
+
return inputFailureResult(error) ?? internalError();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** 변경 파일도 브리지 입력과 같은 읽기·구문·계약 실패 경계를 지킨다. */
|
|
72
|
+
async function readSupplemental(path, readTextFile, kind, parse) {
|
|
73
|
+
const label = kind === 'changes' ? 'Changes' : 'Runtime';
|
|
74
|
+
const format = kind === 'changes' ? 'isthmus-changes' : 'bridge-runtime';
|
|
75
|
+
let text;
|
|
76
|
+
try {
|
|
77
|
+
text = await readTextFile(path);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return inputFailure(`Unable to read ${kind} input; check that the file exists and is readable.\n`);
|
|
81
|
+
}
|
|
82
|
+
if (text.length > MAX_INPUT_TEXT_LENGTH)
|
|
83
|
+
return inputFailure(`${label} input exceeds the input size limit.\n`);
|
|
84
|
+
let value;
|
|
85
|
+
try {
|
|
86
|
+
value = JSON.parse(text);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (isJsonParseFailure(error))
|
|
90
|
+
return inputFailure(`${label} input is not valid JSON.\n`);
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
return { document: parse(value), textLength: text.length };
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (error instanceof ImpactSelectionValidationError || error instanceof RuntimeValidationError) {
|
|
98
|
+
return inputFailure(`${label} input violates the ${format} contract: ${error.message}\n`);
|
|
99
|
+
}
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** 잘못된 인수는 입력을 읽기 전에 코드 64로 거부한다. */
|
|
104
|
+
function usageError() {
|
|
105
|
+
return { standardOutput: '', standardError: `${impactUsage}\n`, exitCode: 64 };
|
|
106
|
+
}
|
|
107
|
+
/** impact 명령의 독립적인 선택 방식과 CI 실패 옵션이다. */
|
|
108
|
+
export const impactUsage = 'Usage: isthmus impact (--file <relative-path> | --symbol <name-or-usr> '
|
|
109
|
+
+ '| --changes <changes.json>) <bridge-facts.json> <bridge-facts.json> [more...] '
|
|
110
|
+
+ '[--runtime <runtime.json> --revision <revision>] [--strict] [--compact]';
|
|
111
|
+
//# sourceMappingURL=impact-command.js.map
|