isthmus-cli 0.6.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 +135 -31
- package/README.md +134 -16
- package/Skills/isthmus/SKILL.md +17 -7
- 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 +19 -1
- package/dist/cli/command-support.js +37 -8
- 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/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 +52 -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/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/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/messages.d.ts +9 -24
- package/dist/exchange/messages.js +25 -55
- package/dist/exchange/messages.js.map +1 -1
- package/dist/exchange/parse.d.ts +61 -0
- package/dist/exchange/parse.js +115 -12
- package/dist/exchange/parse.js.map +1 -1
- 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/messages.d.ts +7 -4
- package/dist/join/messages.js +15 -8
- package/dist/join/messages.js.map +1 -1
- 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 -11
- 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/preflight-view.js +28 -1
- package/dist/report/preflight-view.js.map +1 -1
- package/dist/report/preflight.d.ts +4 -4
- package/dist/report/preflight.js +81 -46
- package/dist/report/preflight.js.map +1 -1
- 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/sarif.d.ts +2 -8
- package/dist/report/sarif.js +2 -10
- package/dist/report/sarif.js.map +1 -1
- package/docs/BRIDGE-EVENTS.md +87 -0
- package/docs/BRIDGE-MESSAGES.md +23 -8
- package/docs/GRAPH-EXCHANGE.md +141 -14
- package/docs/IMPACT.md +1 -1
- package/docs/MCP.md +62 -0
- package/docs/PREFLIGHT.md +24 -8
- package/docs/RUNTIME.md +2 -2
- package/docs/TOOLCHAIN.md +5 -5
- package/package.json +5 -2
- package/scripts/capture-preflight.mjs +32 -44
- package/scripts/run-child.mjs +1 -0
|
@@ -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,11 @@
|
|
|
1
|
+
import { type CaptureFileSystem, type CommandResult, type ReadTextFile, type WriteTextFile } from './command-support.ts';
|
|
2
|
+
/**
|
|
3
|
+
* capture 설정 scaffold를 JSON으로 쓴다.
|
|
4
|
+
*
|
|
5
|
+
* producer를 실행하거나 탐지하지 않는다 — 기본 producer 명령은 PATH 이름이고,
|
|
6
|
+
* `--toolchain`을 주면 이미 구축한 `isthmus-built-toolchain` JSON의 실제 명령을
|
|
7
|
+
* 채운다. `prepare`는 앱마다 달라 자리표시자로 남긴다.
|
|
8
|
+
*/
|
|
9
|
+
export declare function runInitCommand(arguments_: readonly string[], readTextFile: ReadTextFile, writeTextFile: WriteTextFile, fileSystem: CaptureFileSystem, cwd: string): Promise<CommandResult>;
|
|
10
|
+
/** init 명령의 한 줄 사용법이다. */
|
|
11
|
+
export declare const initUsage: string;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { isJsonObject, isSafeNonEmptyString } from "../exchange/parse.js";
|
|
3
|
+
import { encodeSortedJson } from "../report/sorted-json.js";
|
|
4
|
+
import { parseCommandArguments } from "./parse-arguments.js";
|
|
5
|
+
import { inputFailure, isJsonParseFailure, MAX_INPUT_TEXT_LENGTH, } from "./command-support.js";
|
|
6
|
+
/**
|
|
7
|
+
* capture 설정 scaffold를 JSON으로 쓴다.
|
|
8
|
+
*
|
|
9
|
+
* producer를 실행하거나 탐지하지 않는다 — 기본 producer 명령은 PATH 이름이고,
|
|
10
|
+
* `--toolchain`을 주면 이미 구축한 `isthmus-built-toolchain` JSON의 실제 명령을
|
|
11
|
+
* 채운다. `prepare`는 앱마다 달라 자리표시자로 남긴다.
|
|
12
|
+
*/
|
|
13
|
+
export async function runInitCommand(arguments_, readTextFile, writeTextFile, fileSystem, cwd) {
|
|
14
|
+
if (arguments_[0] !== 'init')
|
|
15
|
+
return usageError();
|
|
16
|
+
const parsed = parseCommandArguments(arguments_.slice(1), ['--project', '--toolchain'], ['--force']);
|
|
17
|
+
if (parsed === undefined || parsed.positionals.length > 1)
|
|
18
|
+
return usageError();
|
|
19
|
+
const outputPath = resolve(cwd, parsed.positionals[0] ?? 'capture.json');
|
|
20
|
+
let project;
|
|
21
|
+
try {
|
|
22
|
+
project = await fileSystem.realPath(resolve(cwd, parsed.valueFlags.get('--project') ?? cwd));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return inputFailure('The project path does not exist; pass --project at the app root.\n');
|
|
26
|
+
}
|
|
27
|
+
let producers = { dartograph: ['dartograph'], cartograph: ['cartograph'] };
|
|
28
|
+
const toolchainPath = parsed.valueFlags.get('--toolchain');
|
|
29
|
+
let toolchainAbsolute;
|
|
30
|
+
if (toolchainPath !== undefined) {
|
|
31
|
+
toolchainAbsolute = resolve(cwd, toolchainPath);
|
|
32
|
+
const loaded = await readToolchain(toolchainAbsolute, readTextFile);
|
|
33
|
+
if (loaded === undefined) {
|
|
34
|
+
return inputFailure('The toolchain manifest is missing or invalid; build one with '
|
|
35
|
+
+ 'scripts/build-preflight-toolchain.mjs first.\n');
|
|
36
|
+
}
|
|
37
|
+
producers = loaded;
|
|
38
|
+
}
|
|
39
|
+
if (!parsed.booleanFlags.has('--force') &&
|
|
40
|
+
(await fileSystem.statPath(outputPath)) !== 'missing') {
|
|
41
|
+
return inputFailure(`The capture config already exists at ${outputPath}; pass --force to overwrite it.\n`);
|
|
42
|
+
}
|
|
43
|
+
const scaffold = {
|
|
44
|
+
project,
|
|
45
|
+
...producers,
|
|
46
|
+
prepare: [['replace-with-native-index-build']],
|
|
47
|
+
inputs: ['lib'],
|
|
48
|
+
toolInputs: [toolchainAbsolute ?? 'toolchain.json'],
|
|
49
|
+
selection: { dart: { files: [], symbols: [] } },
|
|
50
|
+
output: '.isthmus/context.json',
|
|
51
|
+
cache: '.isthmus/cache.json',
|
|
52
|
+
};
|
|
53
|
+
try {
|
|
54
|
+
await writeTextFile(outputPath, `${encodeSortedJson(scaffold)}\n`);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return inputFailure('Unable to write the capture config; check that the path is writable.\n');
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
standardOutput: encodeSortedJson({
|
|
61
|
+
format: 'isthmus-init',
|
|
62
|
+
version: 1,
|
|
63
|
+
path: outputPath,
|
|
64
|
+
project,
|
|
65
|
+
wrote: true,
|
|
66
|
+
}),
|
|
67
|
+
standardError: '',
|
|
68
|
+
exitCode: 0,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** `isthmus-built-toolchain` JSON에서 실제 producer 명령을 읽는다. */
|
|
72
|
+
async function readToolchain(path, readTextFile) {
|
|
73
|
+
let text;
|
|
74
|
+
try {
|
|
75
|
+
text = await readTextFile(path);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
if (text.length > MAX_INPUT_TEXT_LENGTH)
|
|
81
|
+
return undefined;
|
|
82
|
+
let value;
|
|
83
|
+
try {
|
|
84
|
+
value = JSON.parse(text);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (isJsonParseFailure(error))
|
|
88
|
+
return undefined;
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
if (!isJsonObject(value) || value.format !== 'isthmus-built-toolchain' ||
|
|
92
|
+
value.version !== 1 || !isJsonObject(value.commands)) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
const commands = value.commands;
|
|
96
|
+
if (!isCommand(commands.dartograph))
|
|
97
|
+
return undefined;
|
|
98
|
+
return {
|
|
99
|
+
dartograph: commands.dartograph,
|
|
100
|
+
...(isCommand(commands.cartograph) ? { cartograph: commands.cartograph } : {}),
|
|
101
|
+
...(isCommand(commands.kartograph) ? { kartograph: commands.kartograph } : {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** 명령 배열은 비어 있지 않은 안전한 문자열의 비어 있지 않은 배열이다. */
|
|
105
|
+
function isCommand(value) {
|
|
106
|
+
return Array.isArray(value) && value.length > 0 && value.every(isSafeNonEmptyString);
|
|
107
|
+
}
|
|
108
|
+
/** 잘못된 CLI 호출을 경로 없는 사용법과 코드 64로 변환한다. */
|
|
109
|
+
function usageError() {
|
|
110
|
+
return { standardOutput: '', standardError: `${initUsage}\n`, exitCode: 64 };
|
|
111
|
+
}
|
|
112
|
+
/** init 명령의 한 줄 사용법이다. */
|
|
113
|
+
export const initUsage = 'Usage: isthmus init [capture.json] [--project <dir>] '
|
|
114
|
+
+ '[--toolchain <toolchain.json>] [--force]';
|
|
115
|
+
//# sourceMappingURL=init-command.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init-command.js","sourceRoot":"","sources":["../../src/cli/init-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { resolve } from 'node:path';\n\nimport { isJsonObject, isSafeNonEmptyString } from '../exchange/parse.ts';\nimport { encodeSortedJson } from '../report/sorted-json.ts';\nimport { parseCommandArguments } from './parse-arguments.ts';\nimport {\n inputFailure,\n isJsonParseFailure,\n MAX_INPUT_TEXT_LENGTH,\n type CaptureFileSystem,\n type CommandResult,\n type ReadTextFile,\n type WriteTextFile,\n} from './command-support.ts';\n\n/** init이 쓰는 scaffold의 producer 명령 묶음이다. */\ninterface InitProducers {\n readonly dartograph: readonly string[];\n readonly cartograph?: readonly string[];\n readonly kartograph?: readonly string[];\n}\n\n/**\n * capture 설정 scaffold를 JSON으로 쓴다.\n *\n * producer를 실행하거나 탐지하지 않는다 — 기본 producer 명령은 PATH 이름이고,\n * `--toolchain`을 주면 이미 구축한 `isthmus-built-toolchain` JSON의 실제 명령을\n * 채운다. `prepare`는 앱마다 달라 자리표시자로 남긴다.\n */\nexport async function runInitCommand(\n arguments_: readonly string[],\n readTextFile: ReadTextFile,\n writeTextFile: WriteTextFile,\n fileSystem: CaptureFileSystem,\n cwd: string,\n): Promise<CommandResult> {\n if (arguments_[0] !== 'init') return usageError();\n const parsed = parseCommandArguments(\n arguments_.slice(1),\n ['--project', '--toolchain'],\n ['--force'],\n );\n if (parsed === undefined || parsed.positionals.length > 1) return usageError();\n const outputPath = resolve(cwd, parsed.positionals[0] ?? 'capture.json');\n\n let project: string;\n try {\n project = await fileSystem.realPath(resolve(cwd, parsed.valueFlags.get('--project') ?? cwd));\n } catch {\n return inputFailure('The project path does not exist; pass --project at the app root.\\n');\n }\n\n let producers: InitProducers = { dartograph: ['dartograph'], cartograph: ['cartograph'] };\n const toolchainPath = parsed.valueFlags.get('--toolchain');\n let toolchainAbsolute: string | undefined;\n if (toolchainPath !== undefined) {\n toolchainAbsolute = resolve(cwd, toolchainPath);\n const loaded = await readToolchain(toolchainAbsolute, readTextFile);\n if (loaded === undefined) {\n return inputFailure(\n 'The toolchain manifest is missing or invalid; build one with '\n + 'scripts/build-preflight-toolchain.mjs first.\\n',\n );\n }\n producers = loaded;\n }\n\n if (!parsed.booleanFlags.has('--force') &&\n (await fileSystem.statPath(outputPath)) !== 'missing') {\n return inputFailure(\n `The capture config already exists at ${outputPath}; pass --force to overwrite it.\\n`,\n );\n }\n\n const scaffold = {\n project,\n ...producers,\n prepare: [['replace-with-native-index-build']],\n inputs: ['lib'],\n toolInputs: [toolchainAbsolute ?? 'toolchain.json'],\n selection: { dart: { files: [], symbols: [] } },\n output: '.isthmus/context.json',\n cache: '.isthmus/cache.json',\n };\n try {\n await writeTextFile(outputPath, `${encodeSortedJson(scaffold)}\\n`);\n } catch {\n return inputFailure('Unable to write the capture config; check that the path is writable.\\n');\n }\n return {\n standardOutput: encodeSortedJson({\n format: 'isthmus-init',\n version: 1,\n path: outputPath,\n project,\n wrote: true,\n }),\n standardError: '',\n exitCode: 0,\n };\n}\n\n/** `isthmus-built-toolchain` JSON에서 실제 producer 명령을 읽는다. */\nasync function readToolchain(\n path: string,\n readTextFile: ReadTextFile,\n): Promise<InitProducers | undefined> {\n let text: string;\n try {\n text = await readTextFile(path);\n } catch {\n return undefined;\n }\n if (text.length > MAX_INPUT_TEXT_LENGTH) return undefined;\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch (error) {\n if (isJsonParseFailure(error)) return undefined;\n throw error;\n }\n if (!isJsonObject(value) || value.format !== 'isthmus-built-toolchain' ||\n value.version !== 1 || !isJsonObject(value.commands)) {\n return undefined;\n }\n const commands = value.commands;\n if (!isCommand(commands.dartograph)) return undefined;\n return {\n dartograph: commands.dartograph,\n ...(isCommand(commands.cartograph) ? { cartograph: commands.cartograph } : {}),\n ...(isCommand(commands.kartograph) ? { kartograph: commands.kartograph } : {}),\n };\n}\n\n/** 명령 배열은 비어 있지 않은 안전한 문자열의 비어 있지 않은 배열이다. */\nfunction isCommand(value: unknown): value is readonly string[] {\n return Array.isArray(value) && value.length > 0 && value.every(isSafeNonEmptyString);\n}\n\n/** 잘못된 CLI 호출을 경로 없는 사용법과 코드 64로 변환한다. */\nfunction usageError(): CommandResult {\n return { standardOutput: '', standardError: `${initUsage}\\n`, exitCode: 64 };\n}\n\n/** init 명령의 한 줄 사용법이다. */\nexport const initUsage =\n 'Usage: isthmus init [capture.json] [--project <dir>] '\n + '[--toolchain <toolchain.json>] [--force]';"]}
|
package/dist/cli/main.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { readdir, readFile, realpath, stat } from 'node:fs/promises';
|
|
3
3
|
import { writeTextAtomically } from "./atomic-write.js";
|
|
4
4
|
import { checkUsage, runCheckCommand } from "./check-command.js";
|
|
5
|
+
import { doctorUsage, runDoctorCommand } from "./doctor-command.js";
|
|
6
|
+
import { initUsage, runInitCommand } from "./init-command.js";
|
|
7
|
+
import { extractJsUsage, runExtractJsCommand, } from "./extract-js-command.js";
|
|
5
8
|
import { graphUsage, runGraphCommand } from "./graph-command.js";
|
|
6
9
|
import { diffUsage, runDiffCommand } from "./diff-command.js";
|
|
7
10
|
import { queryUsage, runQueryCommand } from "./query-command.js";
|
|
@@ -9,6 +12,7 @@ import { impactUsage, runImpactCommand } from "./impact-command.js";
|
|
|
9
12
|
import { runtimeUsage, runRuntimeCommand } from "./runtime-command.js";
|
|
10
13
|
import { preflightUsage, runPreflightCommand } from "./preflight-command.js";
|
|
11
14
|
import { retentionUsage, runRetentionsCommand, } from "./retentions-command.js";
|
|
15
|
+
import { runServeCommand, serveUsage } from "./serve-command.js";
|
|
12
16
|
process.stdout.on('error', handleStreamError);
|
|
13
17
|
process.stderr.on('error', handleStreamError);
|
|
14
18
|
const commandUsages = new Map([
|
|
@@ -20,6 +24,10 @@ const commandUsages = new Map([
|
|
|
20
24
|
['preflight', preflightUsage],
|
|
21
25
|
['verify-runtime', runtimeUsage],
|
|
22
26
|
['retentions', retentionUsage],
|
|
27
|
+
['serve', serveUsage],
|
|
28
|
+
['extract-js', extractJsUsage],
|
|
29
|
+
['doctor', doctorUsage],
|
|
30
|
+
['init', initUsage],
|
|
23
31
|
]);
|
|
24
32
|
const rootHelp = `Usage: isthmus <command> [options]
|
|
25
33
|
|
|
@@ -32,6 +40,10 @@ Commands:
|
|
|
32
40
|
graph Render matched boundary edges
|
|
33
41
|
diff Compare bridge observations before and after a change
|
|
34
42
|
retentions Produce external retention evidence
|
|
43
|
+
extract-js Extract React Native caller-side bridge facts from JS/TS
|
|
44
|
+
doctor Validate a capture config and its executable paths
|
|
45
|
+
init Write a capture config scaffold
|
|
46
|
+
serve Speak MCP over stdio for agent clients
|
|
35
47
|
help Show command help
|
|
36
48
|
|
|
37
49
|
Options:
|
|
@@ -41,6 +53,37 @@ Options:
|
|
|
41
53
|
const arguments_ = process.argv.slice(2);
|
|
42
54
|
const readTextFile = (path) => readFile(path, 'utf8');
|
|
43
55
|
const writeTextFile = (path, text) => writeTextAtomically(path, text);
|
|
56
|
+
/** extract-js가 쓰는 실제 파일시스템 구현이다 — 테스트는 계약만 주입한다. */
|
|
57
|
+
const extractJsFileSystem = {
|
|
58
|
+
statPath: async (path) => {
|
|
59
|
+
try {
|
|
60
|
+
const entry = await stat(path);
|
|
61
|
+
return entry.isDirectory() ? 'directory' : 'file';
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return 'missing';
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
listDirectory: async (path) => (await readdir(path, { withFileTypes: true })).map((entry) => ({
|
|
68
|
+
name: entry.name,
|
|
69
|
+
isFile: entry.isFile(),
|
|
70
|
+
isDirectory: entry.isDirectory(),
|
|
71
|
+
})),
|
|
72
|
+
realPath: (path) => realpath(path),
|
|
73
|
+
};
|
|
74
|
+
/** doctor·init이 쓰는 실제 파일시스템이다. producer는 실행하지 않는다. */
|
|
75
|
+
const captureFileSystem = {
|
|
76
|
+
statPath: async (path) => {
|
|
77
|
+
try {
|
|
78
|
+
const entry = await stat(path);
|
|
79
|
+
return entry.isDirectory() ? 'directory' : 'file';
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return 'missing';
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
realPath: (path) => realpath(path),
|
|
86
|
+
};
|
|
44
87
|
const informationalResult = await runInformationalCommand(arguments_);
|
|
45
88
|
const result = informationalResult ?? await dispatchCommand(arguments_);
|
|
46
89
|
process.stdout.write(result.standardOutput);
|
|
@@ -96,6 +139,14 @@ async function dispatchCommand(commandArguments) {
|
|
|
96
139
|
return runPreflightCommand(commandArguments, readTextFile);
|
|
97
140
|
case 'verify-runtime':
|
|
98
141
|
return runRuntimeCommand(commandArguments, readTextFile);
|
|
142
|
+
case 'extract-js':
|
|
143
|
+
return runExtractJsCommand(commandArguments, extractJsFileSystem, readTextFile, () => new Date(), await readPackageVersion());
|
|
144
|
+
case 'doctor':
|
|
145
|
+
return runDoctorCommand(commandArguments, readTextFile, captureFileSystem, process.cwd(), process.env.PATH ?? '');
|
|
146
|
+
case 'init':
|
|
147
|
+
return runInitCommand(commandArguments, readTextFile, writeTextFile, captureFileSystem, process.cwd());
|
|
148
|
+
case 'serve':
|
|
149
|
+
return runServeCommand(commandArguments, readTextFile, writeTextFile, await readPackageVersion());
|
|
99
150
|
case 'retentions': {
|
|
100
151
|
const version = await readPackageVersion();
|
|
101
152
|
return version === undefined
|