isthmus-cli 0.4.0 → 0.5.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.
Files changed (44) hide show
  1. package/README.ko.md +31 -9
  2. package/README.md +36 -16
  3. package/dist/cli/atomic-write.d.ts +5 -2
  4. package/dist/cli/atomic-write.js +10 -3
  5. package/dist/cli/atomic-write.js.map +1 -1
  6. package/dist/cli/check-command.d.ts +2 -25
  7. package/dist/cli/check-command.js +27 -179
  8. package/dist/cli/check-command.js.map +1 -1
  9. package/dist/cli/command-support.d.ts +48 -0
  10. package/dist/cli/command-support.js +144 -0
  11. package/dist/cli/command-support.js.map +1 -0
  12. package/dist/cli/diff-command.d.ts +1 -1
  13. package/dist/cli/diff-command.js +1 -1
  14. package/dist/cli/diff-command.js.map +1 -1
  15. package/dist/cli/graph-command.d.ts +1 -1
  16. package/dist/cli/graph-command.js +15 -14
  17. package/dist/cli/graph-command.js.map +1 -1
  18. package/dist/cli/main.js +20 -13
  19. package/dist/cli/main.js.map +1 -1
  20. package/dist/cli/parse-arguments.d.ts +18 -0
  21. package/dist/cli/parse-arguments.js +48 -0
  22. package/dist/cli/parse-arguments.js.map +1 -0
  23. package/dist/cli/query-command.d.ts +1 -1
  24. package/dist/cli/query-command.js +30 -10
  25. package/dist/cli/query-command.js.map +1 -1
  26. package/dist/cli/retentions-command.d.ts +1 -1
  27. package/dist/cli/retentions-command.js +11 -10
  28. package/dist/cli/retentions-command.js.map +1 -1
  29. package/dist/exchange/parse.js +12 -3
  30. package/dist/exchange/parse.js.map +1 -1
  31. package/dist/join/join.js +6 -1
  32. package/dist/join/join.js.map +1 -1
  33. package/dist/report/diff.d.ts +40 -33
  34. package/dist/report/diff.js +4 -1
  35. package/dist/report/diff.js.map +1 -1
  36. package/dist/report/query.js +9 -2
  37. package/dist/report/query.js.map +1 -1
  38. package/dist/report/retentions.d.ts +7 -0
  39. package/dist/report/retentions.js +20 -5
  40. package/dist/report/retentions.js.map +1 -1
  41. package/dist/report/sarif.d.ts +9 -2
  42. package/dist/report/sarif.js +4 -10
  43. package/dist/report/sarif.js.map +1 -1
  44. package/package.json +1 -1
@@ -0,0 +1,48 @@
1
+ import type { BridgeFactsDocument } from '../exchange/parse.ts';
2
+ /**
3
+ * 모든 분석 하위 명령이 공유하는 CLI 인프라다.
4
+ *
5
+ * 입력 문서 읽기·예산, 입력 실패의 분류와 메시지, 프로세스 경계 결과 형태를
6
+ * 한 곳에 모아 check-command의 편중을 떼어낸다. baseline 같은 명령 고유
7
+ * 오류는 각 명령이 자신의 실패 매퍼를 덧붙이는 방식으로 확장한다.
8
+ */
9
+ /** 파일 경로를 받아 UTF-8 텍스트를 읽는 주입 경계다. */
10
+ export type ReadTextFile = (path: string) => Promise<string>;
11
+ /** 파일 경로에 UTF-8 텍스트를 쓰는 주입 경계다. */
12
+ export type WriteTextFile = (path: string, text: string) => Promise<void>;
13
+ /** 생성 시각을 테스트 가능하게 주입하는 시계다. */
14
+ export type Clock = () => Date;
15
+ /** CLI가 프로세스 경계에 쓸 출력과 종료 코드다. */
16
+ export interface CommandResult {
17
+ readonly standardOutput: string;
18
+ readonly standardError: string;
19
+ readonly exitCode: 0 | 1 | 2 | 64;
20
+ }
21
+ /** 한 입력 파일에서 허용하는 최대 UTF-16 문자열 길이다. */
22
+ export declare const MAX_INPUT_TEXT_LENGTH: number;
23
+ /** 한 명령에서 허용하는 전체 UTF-16 입력 문자열 길이다. */
24
+ export declare const MAX_TOTAL_INPUT_TEXT_LENGTH: number;
25
+ /** 입력을 순서대로 읽어 파일 수·메모리 상한 안에서 교환 문서로 파싱한다. */
26
+ export declare function readBridgeDocuments(inputPaths: readonly string[], readTextFile: ReadTextFile): Promise<BridgeFactsDocument[]>;
27
+ /**
28
+ * 사용자 입력 JSON 파싱 실패인지 확인한다.
29
+ *
30
+ * 깊은 중첩은 SyntaxError가 아니라 RangeError(스택 초과)로 실패한다.
31
+ * 둘 다 입력 탓이라 내부 오류 메시지로 오분류하지 않는다.
32
+ */
33
+ export declare function isJsonParseFailure(error: unknown): boolean;
34
+ /** 알려진 입력·조인 실패를 원인별 해결 방향을 담은 코드 2 결과로 바꾼다.
35
+
36
+ 명령 고유의 입력(baseline 등)은 각 명령이 자신의 매퍼를 이 뒤에 붙인다. */
37
+ export declare function inputFailureResult(error: unknown): CommandResult | undefined;
38
+ /** 원인 메시지만 stdout 없이 코드 2로 내보낸다. */
39
+ export declare function inputFailure(standardError: string): CommandResult;
40
+ /**
41
+ * 전체 조인 보류를 깨끗한 결과와 구분하는 코드 2 결과로 바꾼다.
42
+ *
43
+ * 관찰량을 숫자 보간으로 함께 알린다. 보류된 조인은 문서를 출력하지 않으므로,
44
+ * 이 숫자가 "몇 개를 봤는데 못 조인했는지"가 도달하는 유일한 경로다.
45
+ */
46
+ export declare function bridgeJoinDeferredError(observedFacts: number, documentCount: number): CommandResult;
47
+ /** 내부 결함을 입력 탓으로 돌리지 않는 경로 없는 코드 2 결과다. */
48
+ export declare function internalError(): CommandResult;
@@ -0,0 +1,144 @@
1
+ import { BridgeFactsValidationError, parseBridgeFactsDocument, } from "../exchange/parse.js";
2
+ import { BridgeJoinValidationError } from "../join/join.js";
3
+ /** 한 입력 파일에서 허용하는 최대 UTF-16 문자열 길이다. */
4
+ export const MAX_INPUT_TEXT_LENGTH = 16 * 1024 * 1024;
5
+ /** 한 명령에서 허용하는 전체 UTF-16 입력 문자열 길이다. */
6
+ export const MAX_TOTAL_INPUT_TEXT_LENGTH = 64 * 1024 * 1024;
7
+ /** 입력을 순서대로 읽어 파일 수·메모리 상한 안에서 교환 문서로 파싱한다. */
8
+ export async function readBridgeDocuments(inputPaths, readTextFile) {
9
+ const documents = [];
10
+ let totalTextLength = 0;
11
+ for (const [index, path] of inputPaths.entries()) {
12
+ const inputPosition = index + 1;
13
+ let text;
14
+ try {
15
+ text = await readTextFile(path);
16
+ }
17
+ catch {
18
+ throw new BridgeInputReadError(inputPosition);
19
+ }
20
+ totalTextLength += text.length;
21
+ if (text.length > MAX_INPUT_TEXT_LENGTH ||
22
+ totalTextLength > MAX_TOTAL_INPUT_TEXT_LENGTH) {
23
+ throw new BridgeInputLimitError(inputPosition);
24
+ }
25
+ // JSON 구문 오류와 문서 검증 실패를 같은 try로 감싸면 검증기의 예외가
26
+ // 구문 오류로 오분류된다. 파싱과 검증을 나눠 분류 경계를 지킨다.
27
+ let parsed;
28
+ try {
29
+ parsed = JSON.parse(text);
30
+ }
31
+ catch (error) {
32
+ if (isJsonParseFailure(error)) {
33
+ throw new BridgeInputJsonError(inputPosition);
34
+ }
35
+ throw error;
36
+ }
37
+ try {
38
+ documents.push(parseBridgeFactsDocument(parsed));
39
+ }
40
+ catch (error) {
41
+ if (error instanceof BridgeFactsValidationError) {
42
+ throw new BridgeInputContractError(inputPosition, error.message);
43
+ }
44
+ throw error;
45
+ }
46
+ }
47
+ return documents;
48
+ }
49
+ /**
50
+ * 사용자 입력 JSON 파싱 실패인지 확인한다.
51
+ *
52
+ * 깊은 중첩은 SyntaxError가 아니라 RangeError(스택 초과)로 실패한다.
53
+ * 둘 다 입력 탓이라 내부 오류 메시지로 오분류하지 않는다.
54
+ */
55
+ export function isJsonParseFailure(error) {
56
+ return error instanceof SyntaxError || error instanceof RangeError;
57
+ }
58
+ /** 실패한 입력의 1부터 시작하는 순서를 보존하는 입력 오류다. */
59
+ class BridgeInputError extends Error {
60
+ inputPosition;
61
+ constructor(name, inputPosition) {
62
+ super(`${name} at input ${inputPosition}`);
63
+ this.name = name;
64
+ this.inputPosition = inputPosition;
65
+ }
66
+ }
67
+ /** 입력 파일을 읽지 못한 경우를 구분한다. */
68
+ class BridgeInputReadError extends BridgeInputError {
69
+ constructor(inputPosition) {
70
+ super('BridgeInputReadError', inputPosition);
71
+ }
72
+ }
73
+ /** 입력 텍스트가 JSON이 아닌 경우를 구분한다. */
74
+ class BridgeInputJsonError extends BridgeInputError {
75
+ constructor(inputPosition) {
76
+ super('BridgeInputJsonError', inputPosition);
77
+ }
78
+ }
79
+ /** 입력 문서가 교환 계약을 어긴 경우와 그 이유를 구분한다. */
80
+ class BridgeInputContractError extends BridgeInputError {
81
+ reason;
82
+ constructor(inputPosition, reason) {
83
+ super('BridgeInputContractError', inputPosition);
84
+ this.reason = reason;
85
+ }
86
+ }
87
+ /** 입력 텍스트의 크기만 계약을 넘은 경우를 구분한다. */
88
+ class BridgeInputLimitError extends BridgeInputError {
89
+ constructor(inputPosition) {
90
+ super('BridgeInputLimitError', inputPosition);
91
+ }
92
+ }
93
+ /** 알려진 입력·조인 실패를 원인별 해결 방향을 담은 코드 2 결과로 바꾼다.
94
+
95
+ 명령 고유의 입력(baseline 등)은 각 명령이 자신의 매퍼를 이 뒤에 붙인다. */
96
+ export function inputFailureResult(error) {
97
+ if (error instanceof BridgeInputReadError) {
98
+ return inputFailure(`Unable to read bridge facts input ${error.inputPosition}; `
99
+ + 'check that the file exists and is readable.\n');
100
+ }
101
+ if (error instanceof BridgeInputJsonError) {
102
+ return inputFailure(`Bridge facts input ${error.inputPosition} is not valid JSON; `
103
+ + 'regenerate it with a bridge-facts producer.\n');
104
+ }
105
+ if (error instanceof BridgeInputContractError) {
106
+ return inputFailure(`Bridge facts input ${error.inputPosition} violates the bridge-facts `
107
+ + `contract: ${error.reason}\n`);
108
+ }
109
+ if (error instanceof BridgeInputLimitError) {
110
+ return inputFailure(`Bridge facts input ${error.inputPosition} exceeds the input size limits; `
111
+ + 'split the extraction into smaller documents.\n');
112
+ }
113
+ if (error instanceof BridgeJoinValidationError) {
114
+ return inputFailure(`${error.message}\n`);
115
+ }
116
+ return undefined;
117
+ }
118
+ /** 원인 메시지만 stdout 없이 코드 2로 내보낸다. */
119
+ export function inputFailure(standardError) {
120
+ return { standardOutput: '', standardError, exitCode: 2 };
121
+ }
122
+ /**
123
+ * 전체 조인 보류를 깨끗한 결과와 구분하는 코드 2 결과로 바꾼다.
124
+ *
125
+ * 관찰량을 숫자 보간으로 함께 알린다. 보류된 조인은 문서를 출력하지 않으므로,
126
+ * 이 숫자가 "몇 개를 봤는데 못 조인했는지"가 도달하는 유일한 경로다.
127
+ */
128
+ export function bridgeJoinDeferredError(observedFacts, documentCount) {
129
+ return {
130
+ standardOutput: '',
131
+ standardError: 'Bridge facts could not be joined; split mixed bridge targets and retry. '
132
+ + `The inputs observed ${observedFacts} facts across ${documentCount} documents.\n`,
133
+ exitCode: 2,
134
+ };
135
+ }
136
+ /** 내부 결함을 입력 탓으로 돌리지 않는 경로 없는 코드 2 결과다. */
137
+ export function internalError() {
138
+ return {
139
+ standardOutput: '',
140
+ standardError: 'Internal isthmus error; retry with a current version.\n',
141
+ exitCode: 2,
142
+ };
143
+ }
144
+ //# sourceMappingURL=command-support.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command-support.js","sourceRoot":"","sources":["../../src/cli/command-support.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BridgeFactsDocument } from '../exchange/parse.ts';\nimport {\n BridgeFactsValidationError,\n parseBridgeFactsDocument,\n} from '../exchange/parse.ts';\nimport { BridgeJoinValidationError } from '../join/join.ts';\n\n/**\n * 모든 분석 하위 명령이 공유하는 CLI 인프라다.\n *\n * 입력 문서 읽기·예산, 입력 실패의 분류와 메시지, 프로세스 경계 결과 형태를\n * 한 곳에 모아 check-command의 편중을 떼어낸다. baseline 같은 명령 고유\n * 오류는 각 명령이 자신의 실패 매퍼를 덧붙이는 방식으로 확장한다.\n */\n\n/** 파일 경로를 받아 UTF-8 텍스트를 읽는 주입 경계다. */\nexport type ReadTextFile = (path: string) => Promise<string>;\n\n/** 파일 경로에 UTF-8 텍스트를 쓰는 주입 경계다. */\nexport type WriteTextFile = (path: string, text: string) => Promise<void>;\n\n/** 생성 시각을 테스트 가능하게 주입하는 시계다. */\nexport type Clock = () => Date;\n\n/** CLI가 프로세스 경계에 쓸 출력과 종료 코드다. */\nexport interface CommandResult {\n readonly standardOutput: string;\n readonly standardError: string;\n readonly exitCode: 0 | 1 | 2 | 64;\n}\n\n/** 한 입력 파일에서 허용하는 최대 UTF-16 문자열 길이다. */\nexport const MAX_INPUT_TEXT_LENGTH = 16 * 1024 * 1024;\n\n/** 한 명령에서 허용하는 전체 UTF-16 입력 문자열 길이다. */\nexport const MAX_TOTAL_INPUT_TEXT_LENGTH = 64 * 1024 * 1024;\n\n/** 입력을 순서대로 읽어 파일 수·메모리 상한 안에서 교환 문서로 파싱한다. */\nexport async function readBridgeDocuments(\n inputPaths: readonly string[],\n readTextFile: ReadTextFile,\n): Promise<BridgeFactsDocument[]> {\n const documents: BridgeFactsDocument[] = [];\n let totalTextLength = 0;\n for (const [index, path] of inputPaths.entries()) {\n const inputPosition = index + 1;\n let text: string;\n try {\n text = await readTextFile(path);\n } catch {\n throw new BridgeInputReadError(inputPosition);\n }\n totalTextLength += text.length;\n if (\n text.length > MAX_INPUT_TEXT_LENGTH ||\n totalTextLength > MAX_TOTAL_INPUT_TEXT_LENGTH\n ) {\n throw new BridgeInputLimitError(inputPosition);\n }\n // JSON 구문 오류와 문서 검증 실패를 같은 try로 감싸면 검증기의 예외가\n // 구문 오류로 오분류된다. 파싱과 검증을 나눠 분류 경계를 지킨다.\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch (error) {\n if (isJsonParseFailure(error)) {\n throw new BridgeInputJsonError(inputPosition);\n }\n throw error;\n }\n try {\n documents.push(parseBridgeFactsDocument(parsed));\n } catch (error) {\n if (error instanceof BridgeFactsValidationError) {\n throw new BridgeInputContractError(inputPosition, error.message);\n }\n throw error;\n }\n }\n return documents;\n}\n\n/**\n * 사용자 입력 JSON 파싱 실패인지 확인한다.\n *\n * 깊은 중첩은 SyntaxError가 아니라 RangeError(스택 초과)로 실패한다.\n * 둘 다 입력 탓이라 내부 오류 메시지로 오분류하지 않는다.\n */\nexport function isJsonParseFailure(error: unknown): boolean {\n return error instanceof SyntaxError || error instanceof RangeError;\n}\n\n/** 실패한 입력의 1부터 시작하는 순서를 보존하는 입력 오류다. */\nclass BridgeInputError extends Error {\n readonly inputPosition: number;\n\n constructor(name: string, inputPosition: number) {\n super(`${name} at input ${inputPosition}`);\n this.name = name;\n this.inputPosition = inputPosition;\n }\n}\n\n/** 입력 파일을 읽지 못한 경우를 구분한다. */\nclass BridgeInputReadError extends BridgeInputError {\n constructor(inputPosition: number) {\n super('BridgeInputReadError', inputPosition);\n }\n}\n\n/** 입력 텍스트가 JSON이 아닌 경우를 구분한다. */\nclass BridgeInputJsonError extends BridgeInputError {\n constructor(inputPosition: number) {\n super('BridgeInputJsonError', inputPosition);\n }\n}\n\n/** 입력 문서가 교환 계약을 어긴 경우와 그 이유를 구분한다. */\nclass BridgeInputContractError extends BridgeInputError {\n readonly reason: string;\n\n constructor(inputPosition: number, reason: string) {\n super('BridgeInputContractError', inputPosition);\n this.reason = reason;\n }\n}\n\n/** 입력 텍스트의 크기만 계약을 넘은 경우를 구분한다. */\nclass BridgeInputLimitError extends BridgeInputError {\n constructor(inputPosition: number) {\n super('BridgeInputLimitError', inputPosition);\n }\n}\n\n/** 알려진 입력·조인 실패를 원인별 해결 방향을 담은 코드 2 결과로 바꾼다.\n\n명령 고유의 입력(baseline 등)은 각 명령이 자신의 매퍼를 이 뒤에 붙인다. */\nexport function inputFailureResult(error: unknown): CommandResult | undefined {\n if (error instanceof BridgeInputReadError) {\n return inputFailure(\n `Unable to read bridge facts input ${error.inputPosition}; `\n + 'check that the file exists and is readable.\\n',\n );\n }\n if (error instanceof BridgeInputJsonError) {\n return inputFailure(\n `Bridge facts input ${error.inputPosition} is not valid JSON; `\n + 'regenerate it with a bridge-facts producer.\\n',\n );\n }\n if (error instanceof BridgeInputContractError) {\n return inputFailure(\n `Bridge facts input ${error.inputPosition} violates the bridge-facts `\n + `contract: ${error.reason}\\n`,\n );\n }\n if (error instanceof BridgeInputLimitError) {\n return inputFailure(\n `Bridge facts input ${error.inputPosition} exceeds the input size limits; `\n + 'split the extraction into smaller documents.\\n',\n );\n }\n if (error instanceof BridgeJoinValidationError) {\n return inputFailure(`${error.message}\\n`);\n }\n return undefined;\n}\n\n/** 원인 메시지만 stdout 없이 코드 2로 내보낸다. */\nexport function inputFailure(standardError: string): CommandResult {\n return { standardOutput: '', standardError, exitCode: 2 };\n}\n\n/**\n * 전체 조인 보류를 깨끗한 결과와 구분하는 코드 2 결과로 바꾼다.\n *\n * 관찰량을 숫자 보간으로 함께 알린다. 보류된 조인은 문서를 출력하지 않으므로,\n * 이 숫자가 \"몇 개를 봤는데 못 조인했는지\"가 도달하는 유일한 경로다.\n */\nexport function bridgeJoinDeferredError(\n observedFacts: number,\n documentCount: number,\n): CommandResult {\n return {\n standardOutput: '',\n standardError:\n 'Bridge facts could not be joined; split mixed bridge targets and retry. '\n + `The inputs observed ${observedFacts} facts across ${documentCount} documents.\\n`,\n exitCode: 2,\n };\n}\n\n/** 내부 결함을 입력 탓으로 돌리지 않는 경로 없는 코드 2 결과다. */\nexport function internalError(): CommandResult {\n return {\n standardOutput: '',\n standardError: 'Internal isthmus error; retry with a current version.\\n',\n exitCode: 2,\n };\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { type CommandResult, type ReadTextFile } from './check-command.ts';
1
+ import { type CommandResult, type ReadTextFile } from './command-support.ts';
2
2
  /** 전후 교환 파일을 하나의 입력 예산으로 읽어 새 경계 오류만 CI 실패로 표시한다. */
3
3
  export declare function runDiffCommand(arguments_: readonly string[], readTextFile: ReadTextFile): Promise<CommandResult>;
4
4
  /** 각 snapshot은 caller와 receiver 문서를 함께 제공해야 한다. */
@@ -1,7 +1,7 @@
1
1
  import { MAX_DOCUMENTS_PER_JOIN } from "../join/join.js";
2
2
  import { createBridgeDiff } from "../report/diff.js";
3
3
  import { encodeSortedJson } from "../report/sorted-json.js";
4
- import { internalError, inputFailureResult, readBridgeDocuments, } from "./check-command.js";
4
+ import { inputFailureResult, internalError, readBridgeDocuments, } from "./command-support.js";
5
5
  /** 전후 교환 파일을 하나의 입력 예산으로 읽어 새 경계 오류만 CI 실패로 표시한다. */
6
6
  export async function runDiffCommand(arguments_, readTextFile) {
7
7
  const strictFlags = arguments_.filter((argument) => argument === '--strict');
@@ -1 +1 @@
1
- {"version":3,"file":"diff-command.js","sourceRoot":"","sources":["../../src/cli/diff-command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,mBAAmB,GAGpB,MAAM,oBAAoB,CAAC;AAE5B,qDAAqD;AACrD,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,UAA6B,EAC7B,YAA0B;IAE1B,MAAM,WAAW,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,KAAK,UAAU,CAAC,CAAC;IAC7E,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,KAAK,UAAU,CAAC,CAAC;IACtE,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;IAC7C,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC;IAC7C,MAAM,KAAK,GAAG,CAAC,GAAG,WAAW,EAAE,GAAG,UAAU,CAAC,CAAC;IAC9C,IACE,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM;QAClB,IAAI,CAAC,CAAC,CAAC,KAAK,UAAU;QACtB,WAAW,CAAC,MAAM,GAAG,CAAC;QACtB,SAAS,GAAG,CAAC;QACb,WAAW,CAAC,MAAM,GAAG,CAAC;QACtB,UAAU,CAAC,MAAM,GAAG,CAAC;QACrB,KAAK,CAAC,MAAM,GAAG,sBAAsB;QACrC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,EACtE,CAAC;QACD,OAAO,EAAE,cAAc,EAAE,EAAE,EAAE,aAAa,EAAE,GAAG,SAAS,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IAC/E,CAAC;IACD,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,MAAM,mBAAmB,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;QACjE,MAAM,MAAM,GAAG,gBAAgB,CAC7B,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,EACtC,SAAS,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CACpC,CAAC;QACF,OAAO;YACL,cAAc,EAAE,gBAAgB,CAAC,MAAM,CAAC;YACxC,aAAa,EAAE,EAAE;YACjB,QAAQ,EAAE,WAAW,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,gBAAgB,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;SAClF,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,kBAAkB,CAAC,KAAK,CAAC,IAAI,aAAa,EAAE,CAAC;IACtD,CAAC;AACH,CAAC;AAED,mDAAmD;AACnD,MAAM,CAAC,MAAM,SAAS,GACpB,kEAAkE;MAChE,uDAAuD,CAAC","sourcesContent":["import { MAX_DOCUMENTS_PER_JOIN } from '../join/join.ts';\nimport { createBridgeDiff } from '../report/diff.ts';\nimport { encodeSortedJson } from '../report/sorted-json.ts';\nimport {\n internalError,\n inputFailureResult,\n readBridgeDocuments,\n type CommandResult,\n type ReadTextFile,\n} from './check-command.ts';\n\n/** 전후 교환 파일을 하나의 입력 예산으로 읽어 새 경계 오류만 CI 실패로 표시한다. */\nexport async function runDiffCommand(\n arguments_: readonly string[],\n readTextFile: ReadTextFile,\n): Promise<CommandResult> {\n const strictFlags = arguments_.filter((argument) => argument === '--strict');\n const args = arguments_.filter((argument) => argument !== '--strict');\n const separator = args.indexOf('--after');\n const beforePaths = args.slice(2, separator);\n const afterPaths = args.slice(separator + 1);\n const paths = [...beforePaths, ...afterPaths];\n if (\n args[0] !== 'diff' ||\n args[1] !== '--before' ||\n strictFlags.length > 1 ||\n separator < 4 ||\n beforePaths.length < 2 ||\n afterPaths.length < 2 ||\n paths.length > MAX_DOCUMENTS_PER_JOIN ||\n paths.some((path) => path.trim().length === 0 || path.startsWith('-'))\n ) {\n return { standardOutput: '', standardError: `${diffUsage}\\n`, exitCode: 64 };\n }\n try {\n const documents = await readBridgeDocuments(paths, readTextFile);\n const report = createBridgeDiff(\n documents.slice(0, beforePaths.length),\n documents.slice(beforePaths.length),\n );\n return {\n standardOutput: encodeSortedJson(report),\n standardError: '',\n exitCode: strictFlags.length === 1 && report.summary.introducedErrors > 0 ? 1 : 0,\n };\n } catch (error) {\n return inputFailureResult(error) ?? internalError();\n }\n}\n\n/** 각 snapshot은 caller와 receiver 문서를 함께 제공해야 한다. */\nexport const diffUsage =\n 'Usage: isthmus diff --before <dart.json> <swift.json> [more...] '\n + '--after <dart.json> <swift.json> [more...] [--strict]';\n"]}
1
+ {"version":3,"file":"diff-command.js","sourceRoot":"","sources":["../../src/cli/diff-command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,mBAAmB,GAGpB,MAAM,sBAAsB,CAAC;AAE9B,qDAAqD;AACrD,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,UAA6B,EAC7B,YAA0B;IAE1B,MAAM,WAAW,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,KAAK,UAAU,CAAC,CAAC;IAC7E,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,KAAK,UAAU,CAAC,CAAC;IACtE,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;IAC7C,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC;IAC7C,MAAM,KAAK,GAAG,CAAC,GAAG,WAAW,EAAE,GAAG,UAAU,CAAC,CAAC;IAC9C,IACE,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM;QAClB,IAAI,CAAC,CAAC,CAAC,KAAK,UAAU;QACtB,WAAW,CAAC,MAAM,GAAG,CAAC;QACtB,SAAS,GAAG,CAAC;QACb,WAAW,CAAC,MAAM,GAAG,CAAC;QACtB,UAAU,CAAC,MAAM,GAAG,CAAC;QACrB,KAAK,CAAC,MAAM,GAAG,sBAAsB;QACrC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,EACtE,CAAC;QACD,OAAO,EAAE,cAAc,EAAE,EAAE,EAAE,aAAa,EAAE,GAAG,SAAS,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IAC/E,CAAC;IACD,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,MAAM,mBAAmB,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;QACjE,MAAM,MAAM,GAAG,gBAAgB,CAC7B,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,EACtC,SAAS,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CACpC,CAAC;QACF,OAAO;YACL,cAAc,EAAE,gBAAgB,CAAC,MAAM,CAAC;YACxC,aAAa,EAAE,EAAE;YACjB,QAAQ,EAAE,WAAW,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,gBAAgB,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;SAClF,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,kBAAkB,CAAC,KAAK,CAAC,IAAI,aAAa,EAAE,CAAC;IACtD,CAAC;AACH,CAAC;AAED,mDAAmD;AACnD,MAAM,CAAC,MAAM,SAAS,GACpB,kEAAkE;MAChE,uDAAuD,CAAC","sourcesContent":["import { MAX_DOCUMENTS_PER_JOIN } from '../join/join.ts';\nimport { createBridgeDiff } from '../report/diff.ts';\nimport { encodeSortedJson } from '../report/sorted-json.ts';\nimport {\n inputFailureResult,\n internalError,\n readBridgeDocuments,\n type CommandResult,\n type ReadTextFile,\n} from './command-support.ts';\n\n/** 전후 교환 파일을 하나의 입력 예산으로 읽어 새 경계 오류만 CI 실패로 표시한다. */\nexport async function runDiffCommand(\n arguments_: readonly string[],\n readTextFile: ReadTextFile,\n): Promise<CommandResult> {\n const strictFlags = arguments_.filter((argument) => argument === '--strict');\n const args = arguments_.filter((argument) => argument !== '--strict');\n const separator = args.indexOf('--after');\n const beforePaths = args.slice(2, separator);\n const afterPaths = args.slice(separator + 1);\n const paths = [...beforePaths, ...afterPaths];\n if (\n args[0] !== 'diff' ||\n args[1] !== '--before' ||\n strictFlags.length > 1 ||\n separator < 4 ||\n beforePaths.length < 2 ||\n afterPaths.length < 2 ||\n paths.length > MAX_DOCUMENTS_PER_JOIN ||\n paths.some((path) => path.trim().length === 0 || path.startsWith('-'))\n ) {\n return { standardOutput: '', standardError: `${diffUsage}\\n`, exitCode: 64 };\n }\n try {\n const documents = await readBridgeDocuments(paths, readTextFile);\n const report = createBridgeDiff(\n documents.slice(0, beforePaths.length),\n documents.slice(beforePaths.length),\n );\n return {\n standardOutput: encodeSortedJson(report),\n standardError: '',\n exitCode: strictFlags.length === 1 && report.summary.introducedErrors > 0 ? 1 : 0,\n };\n } catch (error) {\n return inputFailureResult(error) ?? internalError();\n }\n}\n\n/** 각 snapshot은 caller와 receiver 문서를 함께 제공해야 한다. */\nexport const diffUsage =\n 'Usage: isthmus diff --before <dart.json> <swift.json> [more...] '\n + '--after <dart.json> <swift.json> [more...] [--strict]';\n"]}
@@ -1,4 +1,4 @@
1
- import { type CommandResult, type ReadTextFile } from './check-command.ts';
1
+ import { type CommandResult, type ReadTextFile } from './command-support.ts';
2
2
  /** graph 인자를 실행해 경계 그래프와 종료 코드를 반환한다. */
3
3
  export declare function runGraphCommand(arguments_: readonly string[], readTextFile: ReadTextFile): Promise<CommandResult>;
4
4
  /** graph 명령의 한 줄 사용법이다. */
@@ -1,6 +1,7 @@
1
1
  import { isBridgeJoinDeferred, joinBridgeDocuments, MAX_DOCUMENTS_PER_JOIN, } from "../join/join.js";
2
2
  import { BridgeGraphLimitError, BridgeGraphValidationError, createBridgeGraph, renderBridgeGraph, } from "../report/graph.js";
3
- import { bridgeJoinDeferredError, internalError, inputFailureResult, readBridgeDocuments, } from "./check-command.js";
3
+ import { bridgeJoinDeferredError, inputFailureResult, internalError, readBridgeDocuments, } from "./command-support.js";
4
+ import { parseCommandArguments } from "./parse-arguments.js";
4
5
  /** graph 인자를 실행해 경계 그래프와 종료 코드를 반환한다. */
5
6
  export async function runGraphCommand(arguments_, readTextFile) {
6
7
  const options = parseGraphArguments(arguments_);
@@ -9,8 +10,9 @@ export async function runGraphCommand(arguments_, readTextFile) {
9
10
  try {
10
11
  const documents = await readBridgeDocuments(options.inputPaths, readTextFile);
11
12
  const joined = joinBridgeDocuments(documents);
12
- if (isBridgeJoinDeferred(joined))
13
- return bridgeJoinDeferredError();
13
+ if (isBridgeJoinDeferred(joined)) {
14
+ return bridgeJoinDeferredError(joined.observedFacts, documents.length);
15
+ }
14
16
  const graph = createBridgeGraph(joined);
15
17
  return {
16
18
  standardOutput: renderBridgeGraph(graph, options.format),
@@ -30,23 +32,22 @@ export async function runGraphCommand(arguments_, readTextFile) {
30
32
  return inputFailureResult(error) ?? internalError();
31
33
  }
32
34
  }
33
- /** graph의 입력 파일과 선택 출력 형식을 검증한다. */
35
+ /** graph의 입력 파일과 선택 출력 형식을 검증한다. 플래그는 어디에 와도 된다. */
34
36
  function parseGraphArguments(arguments_) {
35
37
  if (arguments_[0] !== 'graph')
36
38
  return undefined;
37
- const formatIndex = arguments_.indexOf('--format');
38
- const inputPaths = arguments_.slice(1, formatIndex < 0 ? undefined : formatIndex);
39
+ const parsed = parseCommandArguments(arguments_.slice(1), ['--format'], []);
40
+ if (parsed === undefined)
41
+ return undefined;
42
+ const format = parsed.valueFlags.get('--format');
43
+ if (format !== undefined && !isGraphFormat(format))
44
+ return undefined;
45
+ const inputPaths = [...parsed.positionals];
39
46
  if (inputPaths.length < 2 ||
40
- inputPaths.length > MAX_DOCUMENTS_PER_JOIN ||
41
- inputPaths.some((path) => path.startsWith('-'))) {
47
+ inputPaths.length > MAX_DOCUMENTS_PER_JOIN) {
42
48
  return undefined;
43
49
  }
44
- if (formatIndex < 0)
45
- return { inputPaths, format: 'json' };
46
- if (formatIndex !== arguments_.length - 2)
47
- return undefined;
48
- const format = arguments_[formatIndex + 1];
49
- return isGraphFormat(format) ? { inputPaths, format } : undefined;
50
+ return { inputPaths, format: format ?? 'json' };
50
51
  }
51
52
  /** 지원하는 그래프 형식인지 확인한다. */
52
53
  function isGraphFormat(value) {
@@ -1 +1 @@
1
- {"version":3,"file":"graph-command.js","sourceRoot":"","sources":["../../src/cli/graph-command.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n isBridgeJoinDeferred,\n joinBridgeDocuments,\n MAX_DOCUMENTS_PER_JOIN,\n} from '../join/join.ts';\nimport {\n BridgeGraphLimitError,\n BridgeGraphValidationError,\n createBridgeGraph,\n renderBridgeGraph,\n} from '../report/graph.ts';\nimport {\n bridgeJoinDeferredError,\n internalError,\n inputFailureResult,\n readBridgeDocuments,\n type CommandResult,\n type ReadTextFile,\n} from './check-command.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 documents = await readBridgeDocuments(options.inputPaths, readTextFile);\n const joined = joinBridgeDocuments(documents);\n if (isBridgeJoinDeferred(joined)) return bridgeJoinDeferredError();\n const graph = createBridgeGraph(joined);\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 formatIndex = arguments_.indexOf('--format');\n const inputPaths = arguments_.slice(1, formatIndex < 0 ? undefined : formatIndex);\n if (\n inputPaths.length < 2 ||\n inputPaths.length > MAX_DOCUMENTS_PER_JOIN ||\n inputPaths.some((path) => path.startsWith('-'))\n ) {\n return undefined;\n }\n if (formatIndex < 0) return { inputPaths, format: 'json' };\n if (formatIndex !== arguments_.length - 2) return undefined;\n const format = arguments_[formatIndex + 1];\n return isGraphFormat(format) ? { inputPaths, format } : undefined;\n}\n\n/** 지원하는 그래프 형식인지 확인한다. */\nfunction isGraphFormat(value: string | undefined): 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"]}
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,mBAAmB,GAGpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAE7D,yCAAyC;AACzC,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,UAA6B,EAC7B,YAA0B;IAE1B,MAAM,OAAO,GAAG,mBAAmB,CAAC,UAAU,CAAC,CAAC;IAChD,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,eAAe,EAAE,CAAC;IACpD,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,MAAM,mBAAmB,CAAC,OAAO,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;QAC9E,MAAM,MAAM,GAAG,mBAAmB,CAAC,SAAS,CAAC,CAAC;QAC9C,IAAI,oBAAoB,CAAC,MAAM,CAAC,EAAE,CAAC;YACjC,OAAO,uBAAuB,CAAC,MAAM,CAAC,aAAa,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;QACzE,CAAC;QACD,MAAM,KAAK,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;QACxC,OAAO;YACL,cAAc,EAAE,iBAAiB,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC;YACxD,aAAa,EAAE,EAAE;YACjB,QAAQ,EAAE,CAAC;SACZ,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IACE,KAAK,YAAY,qBAAqB;YACtC,KAAK,YAAY,0BAA0B,EAC3C,CAAC;YACD,OAAO;gBACL,cAAc,EAAE,EAAE;gBAClB,aAAa,EAAE,GAAG,KAAK,CAAC,OAAO,IAAI;gBACnC,QAAQ,EAAE,CAAC;aACZ,CAAC;QACJ,CAAC;QACD,OAAO,kBAAkB,CAAC,KAAK,CAAC,IAAI,aAAa,EAAE,CAAC;IACtD,CAAC;AACH,CAAC;AAED,oDAAoD;AACpD,SAAS,mBAAmB,CAAC,UAA6B;IACxD,IAAI,UAAU,CAAC,CAAC,CAAC,KAAK,OAAO;QAAE,OAAO,SAAS,CAAC;IAChD,MAAM,MAAM,GAAG,qBAAqB,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC;IAC5E,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC3C,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IACjD,IAAI,MAAM,KAAK,SAAS,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC;QAAE,OAAO,SAAS,CAAC;IACrE,MAAM,UAAU,GAAG,CAAC,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;IAC3C,IACE,UAAU,CAAC,MAAM,GAAG,CAAC;QACrB,UAAU,CAAC,MAAM,GAAG,sBAAsB,EAC1C,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,IAAI,MAAM,EAAE,CAAC;AAClD,CAAC;AAED,0BAA0B;AAC1B,SAAS,aAAa,CAAC,KAAa;IAClC,OAAO,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,SAAS,CAAC;AACpE,CAAC;AAED,qCAAqC;AACrC,SAAS,eAAe;IACtB,OAAO;QACL,cAAc,EAAE,EAAE;QAClB,aAAa,EAAE,GAAG,UAAU,IAAI;QAChC,QAAQ,EAAE,EAAE;KACb,CAAC;AACJ,CAAC;AAUD,2BAA2B;AAC3B,MAAM,CAAC,MAAM,UAAU,GACrB,+DAA+D;MAC7D,uCAAuC,CAAC","sourcesContent":["import {\n isBridgeJoinDeferred,\n joinBridgeDocuments,\n MAX_DOCUMENTS_PER_JOIN,\n} from '../join/join.ts';\nimport {\n BridgeGraphLimitError,\n BridgeGraphValidationError,\n createBridgeGraph,\n renderBridgeGraph,\n} from '../report/graph.ts';\nimport {\n bridgeJoinDeferredError,\n inputFailureResult,\n internalError,\n readBridgeDocuments,\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 documents = await readBridgeDocuments(options.inputPaths, readTextFile);\n const joined = joinBridgeDocuments(documents);\n if (isBridgeJoinDeferred(joined)) {\n return bridgeJoinDeferredError(joined.observedFacts, documents.length);\n }\n const graph = createBridgeGraph(joined);\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"]}
package/dist/cli/main.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFile } from 'node:fs/promises';
3
- import { checkUsage, runCheckCommand, } from "./check-command.js";
4
3
  import { writeTextAtomically } from "./atomic-write.js";
4
+ import { checkUsage, runCheckCommand } from "./check-command.js";
5
5
  import { graphUsage, runGraphCommand } from "./graph-command.js";
6
6
  import { diffUsage, runDiffCommand } from "./diff-command.js";
7
7
  import { queryUsage, runQueryCommand } from "./query-command.js";
@@ -23,9 +23,10 @@ Commands:
23
23
  graph Render matched boundary edges
24
24
  diff Compare bridge observations before and after a change
25
25
  retentions Produce external retention evidence
26
+ help Show command help
26
27
 
27
28
  Options:
28
- -h, --help Show help
29
+ -h, --help Show help; after a command it shows that command's usage
29
30
  --version Show version
30
31
  `;
31
32
  const arguments_ = process.argv.slice(2);
@@ -46,22 +47,28 @@ function handleStreamError(error) {
46
47
  }
47
48
  /** 도움말과 버전처럼 입력 문서가 필요 없는 명령을 실행한다. */
48
49
  async function runInformationalCommand(commandArguments) {
49
- const first = commandArguments[0];
50
- if (commandArguments.length === 1 &&
51
- (first === '--help' || first === '-h')) {
52
- return successfulText(rootHelp);
50
+ // 도움말 플래그는 어느 위치에 있든 이긴다. 긴 호출 끝에 붙여도 사용법을 본다.
51
+ const helpFlagIndex = commandArguments.findIndex((argument) => argument === '--help' || argument === '-h');
52
+ if (helpFlagIndex >= 0)
53
+ return commandHelp(commandArguments[0]);
54
+ if (commandArguments[0] === 'help') {
55
+ return commandHelp(commandArguments[1]);
53
56
  }
54
- if (commandArguments.length === 1 && first === '--version') {
57
+ if (commandArguments.length === 1 &&
58
+ commandArguments[0] === '--version') {
55
59
  return readVersion();
56
60
  }
57
- if (commandArguments.length === 2 &&
58
- (commandArguments[1] === '--help' || commandArguments[1] === '-h')) {
59
- const usage = first === undefined ? undefined : commandUsages.get(first);
60
- if (usage !== undefined)
61
- return successfulText(`${usage}\n`);
62
- }
63
61
  return undefined;
64
62
  }
63
+ /** 명령 이름에 맞는 사용법을, 모르면 루트 도움말을 돌려준다. */
64
+ function commandHelp(command) {
65
+ if (command === undefined)
66
+ return successfulText(rootHelp);
67
+ const usage = commandUsages.get(command);
68
+ return usage === undefined
69
+ ? successfulText(rootHelp)
70
+ : successfulText(`${usage}\n`);
71
+ }
65
72
  /** 알려진 분석 하위 명령으로만 라우팅한다. */
66
73
  async function dispatchCommand(commandArguments) {
67
74
  switch (commandArguments[0]) {
@@ -1 +1 @@
1
- {"version":3,"file":"main.js","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n\nimport { readFile } from 'node:fs/promises';\n\nimport {\n checkUsage,\n runCheckCommand,\n type CommandResult,\n} from './check-command.ts';\nimport { writeTextAtomically } from './atomic-write.ts';\nimport { graphUsage, runGraphCommand } from './graph-command.ts';\nimport { diffUsage, runDiffCommand } from './diff-command.ts';\nimport { queryUsage, runQueryCommand } from './query-command.ts';\nimport {\n retentionUsage,\n runRetentionsCommand,\n} from './retentions-command.ts';\n\nprocess.stdout.on('error', handleStreamError);\nprocess.stderr.on('error', handleStreamError);\n\nconst commandUsages = new Map([\n ['check', checkUsage],\n ['graph', graphUsage],\n ['diff', diffUsage],\n ['query', queryUsage],\n ['retentions', retentionUsage],\n]);\n\nconst rootHelp = `Usage: isthmus <command> [options]\n\nCommands:\n check Report unmatched bridge calls and handlers\n query Find both sides of a channel or method\n graph Render matched boundary edges\n diff Compare bridge observations before and after a change\n retentions Produce external retention evidence\n\nOptions:\n -h, --help Show help\n --version Show version\n`;\n\nconst arguments_ = process.argv.slice(2);\nconst readTextFile = (path: string) => readFile(path, 'utf8');\nconst writeTextFile = (path: string, text: string) =>\n writeTextAtomically(path, text);\nconst informationalResult = await runInformationalCommand(arguments_);\nconst result = informationalResult ?? await dispatchCommand(arguments_);\n\nprocess.stdout.write(result.standardOutput);\nprocess.stderr.write(result.standardError);\nprocess.exitCode = result.exitCode;\n\n/** 파이프 소비자가 먼저 닫힌 정상 상황만 조용히 종료한다. */\nfunction handleStreamError(error: NodeJS.ErrnoException): void {\n if (error.code === 'EPIPE') {\n process.exitCode = 0;\n return;\n }\n throw error;\n}\n\n/** 도움말과 버전처럼 입력 문서가 필요 없는 명령을 실행한다. */\nasync function runInformationalCommand(\n commandArguments: readonly string[],\n): Promise<CommandResult | undefined> {\n const first = commandArguments[0];\n if (\n commandArguments.length === 1 &&\n (first === '--help' || first === '-h')\n ) {\n return successfulText(rootHelp);\n }\n if (commandArguments.length === 1 && first === '--version') {\n return readVersion();\n }\n if (\n commandArguments.length === 2 &&\n (commandArguments[1] === '--help' || commandArguments[1] === '-h')\n ) {\n const usage = first === undefined ? undefined : commandUsages.get(first);\n if (usage !== undefined) return successfulText(`${usage}\\n`);\n }\n return undefined;\n}\n\n/** 알려진 분석 하위 명령으로만 라우팅한다. */\nasync function dispatchCommand(\n commandArguments: readonly string[],\n): Promise<CommandResult> {\n switch (commandArguments[0]) {\n case 'check':\n // SARIF driver에만 필요하므로 메타데이터 부재는 json 보고서를 막지 않는다.\n return runCheckCommand(\n commandArguments,\n readTextFile,\n writeTextFile,\n undefined,\n await readPackageVersion(),\n );\n case 'graph':\n return runGraphCommand(commandArguments, readTextFile);\n case 'diff':\n return runDiffCommand(commandArguments, readTextFile);\n case 'query':\n return runQueryCommand(commandArguments, readTextFile);\n case 'retentions': {\n const version = await readPackageVersion();\n return version === undefined\n ? packageMetadataError()\n : runRetentionsCommand(\n commandArguments,\n readTextFile,\n () => new Date(),\n version,\n );\n }\n default:\n return {\n standardOutput: '',\n standardError: rootHelp,\n exitCode: 64,\n };\n }\n}\n\n/** 설치된 package.json을 단일 출처로 사용해 버전을 출력한다. */\nasync function readVersion(): Promise<CommandResult> {\n const version = await readPackageVersion();\n return version === undefined\n ? packageMetadataError()\n : successfulText(`${version}\\n`);\n}\n\n/** 설치된 package.json에서 검증된 버전 문자열을 읽는다. */\nasync function readPackageVersion(): Promise<string | undefined> {\n try {\n const text = await readFile(new URL('../../package.json', import.meta.url), 'utf8');\n const document: unknown = JSON.parse(text);\n return isVersionDocument(document) ? document.version : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** package metadata가 비어 있지 않은 버전을 제공하는지 확인한다. */\nfunction isVersionDocument(value: unknown): value is { readonly version: string } {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'version' in value &&\n typeof value.version === 'string' &&\n value.version.length > 0\n );\n}\n\n/** stdout 텍스트와 성공 종료 코드를 만든다. */\nfunction successfulText(standardOutput: string): CommandResult {\n return { standardOutput, standardError: '', exitCode: 0 };\n}\n\n/** package metadata 실패를 민감정보 없는 코드 2로 바꾼다. */\nfunction packageMetadataError(): CommandResult {\n return {\n standardOutput: '',\n standardError: 'Unable to read package metadata.\\n',\n exitCode: 2,\n };\n}\n"]}
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n\nimport { readFile } from 'node:fs/promises';\n\nimport { writeTextAtomically } from './atomic-write.ts';\nimport { checkUsage, runCheckCommand } from './check-command.ts';\nimport type { CommandResult } from './command-support.ts';\nimport { graphUsage, runGraphCommand } from './graph-command.ts';\nimport { diffUsage, runDiffCommand } from './diff-command.ts';\nimport { queryUsage, runQueryCommand } from './query-command.ts';\nimport {\n retentionUsage,\n runRetentionsCommand,\n} from './retentions-command.ts';\n\nprocess.stdout.on('error', handleStreamError);\nprocess.stderr.on('error', handleStreamError);\n\nconst commandUsages = new Map([\n ['check', checkUsage],\n ['graph', graphUsage],\n ['diff', diffUsage],\n ['query', queryUsage],\n ['retentions', retentionUsage],\n]);\n\nconst rootHelp = `Usage: isthmus <command> [options]\n\nCommands:\n check Report unmatched bridge calls and handlers\n query Find both sides of a channel or method\n graph Render matched boundary edges\n diff Compare bridge observations before and after a change\n retentions Produce external retention evidence\n help Show command help\n\nOptions:\n -h, --help Show help; after a command it shows that command's usage\n --version Show version\n`;\n\nconst arguments_ = process.argv.slice(2);\nconst readTextFile = (path: string) => readFile(path, 'utf8');\nconst writeTextFile = (path: string, text: string) =>\n writeTextAtomically(path, text);\nconst informationalResult = await runInformationalCommand(arguments_);\nconst result = informationalResult ?? await dispatchCommand(arguments_);\n\nprocess.stdout.write(result.standardOutput);\nprocess.stderr.write(result.standardError);\nprocess.exitCode = result.exitCode;\n\n/** 파이프 소비자가 먼저 닫힌 정상 상황만 조용히 종료한다. */\nfunction handleStreamError(error: NodeJS.ErrnoException): void {\n if (error.code === 'EPIPE') {\n process.exitCode = 0;\n return;\n }\n throw error;\n}\n\n/** 도움말과 버전처럼 입력 문서가 필요 없는 명령을 실행한다. */\nasync function runInformationalCommand(\n commandArguments: readonly string[],\n): Promise<CommandResult | undefined> {\n // 도움말 플래그는 어느 위치에 있든 이긴다. 긴 호출 끝에 붙여도 사용법을 본다.\n const helpFlagIndex = commandArguments.findIndex(\n (argument) => argument === '--help' || argument === '-h',\n );\n if (helpFlagIndex >= 0) return commandHelp(commandArguments[0]);\n if (commandArguments[0] === 'help') {\n return commandHelp(commandArguments[1]);\n }\n if (\n commandArguments.length === 1 &&\n commandArguments[0] === '--version'\n ) {\n return readVersion();\n }\n return undefined;\n}\n\n/** 명령 이름에 맞는 사용법을, 모르면 루트 도움말을 돌려준다. */\nfunction commandHelp(command: string | undefined): CommandResult {\n if (command === undefined) return successfulText(rootHelp);\n const usage = commandUsages.get(command);\n return usage === undefined\n ? successfulText(rootHelp)\n : successfulText(`${usage}\\n`);\n}\n\n/** 알려진 분석 하위 명령으로만 라우팅한다. */\nasync function dispatchCommand(\n commandArguments: readonly string[],\n): Promise<CommandResult> {\n switch (commandArguments[0]) {\n case 'check':\n // SARIF driver에만 필요하므로 메타데이터 부재는 json 보고서를 막지 않는다.\n return runCheckCommand(\n commandArguments,\n readTextFile,\n writeTextFile,\n undefined,\n await readPackageVersion(),\n );\n case 'graph':\n return runGraphCommand(commandArguments, readTextFile);\n case 'diff':\n return runDiffCommand(commandArguments, readTextFile);\n case 'query':\n return runQueryCommand(commandArguments, readTextFile);\n case 'retentions': {\n const version = await readPackageVersion();\n return version === undefined\n ? packageMetadataError()\n : runRetentionsCommand(\n commandArguments,\n readTextFile,\n () => new Date(),\n version,\n );\n }\n default:\n return {\n standardOutput: '',\n standardError: rootHelp,\n exitCode: 64,\n };\n }\n}\n\n/** 설치된 package.json을 단일 출처로 사용해 버전을 출력한다. */\nasync function readVersion(): Promise<CommandResult> {\n const version = await readPackageVersion();\n return version === undefined\n ? packageMetadataError()\n : successfulText(`${version}\\n`);\n}\n\n/** 설치된 package.json에서 검증된 버전 문자열을 읽는다. */\nasync function readPackageVersion(): Promise<string | undefined> {\n try {\n const text = await readFile(new URL('../../package.json', import.meta.url), 'utf8');\n const document: unknown = JSON.parse(text);\n return isVersionDocument(document) ? document.version : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** package metadata가 비어 있지 않은 버전을 제공하는지 확인한다. */\nfunction isVersionDocument(value: unknown): value is { readonly version: string } {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'version' in value &&\n typeof value.version === 'string' &&\n value.version.length > 0\n );\n}\n\n/** stdout 텍스트와 성공 종료 코드를 만든다. */\nfunction successfulText(standardOutput: string): CommandResult {\n return { standardOutput, standardError: '', exitCode: 0 };\n}\n\n/** package metadata 실패를 민감정보 없는 코드 2로 바꾼다. */\nfunction packageMetadataError(): CommandResult {\n return {\n standardOutput: '',\n standardError: 'Unable to read package metadata.\\n',\n exitCode: 2,\n };\n}\n"]}
@@ -0,0 +1,18 @@
1
+ /** 플래그와 위치 인수로 나뉜 명령 인수다. */
2
+ export interface ParsedCommandArguments {
3
+ /** 값 플래그 이름과 그 값이다. 같은 플래그를 두 번 주면 사용법 오류다. */
4
+ readonly valueFlags: ReadonlyMap<string, string>;
5
+ /** 값 없는 플래그 이름들이다. 반복은 한 번과 같다. */
6
+ readonly booleanFlags: ReadonlySet<string>;
7
+ /** 플래그가 아닌 인수다. `--` 뒤는 `-`로 시작해도 모두 여기에 담긴다. */
8
+ readonly positionals: readonly string[];
9
+ }
10
+ /**
11
+ * 모든 분석 하위 명령이 같은 규칙으로 인수를 읽게 하는 공유 파서다.
12
+ *
13
+ * 플래그는 위치 인수 앞뒤 어디에 와도 된다. 값 플래그의 값은 비어 있지 않고
14
+ * `-`로 시작하지 않아야 한다. `--` 이후는 모두 위치 인수로 읽어 `-`로
15
+ * 시작하는 경로·이름도 전달할 수 있다. 모르는 플래그, 중복 값 플래그, 빈
16
+ * 플래그 값은 사용법 오류다(undefined).
17
+ */
18
+ export declare function parseCommandArguments(arguments_: readonly string[], valueFlagNames: readonly string[], booleanFlagNames: readonly string[]): ParsedCommandArguments | undefined;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * 모든 분석 하위 명령이 같은 규칙으로 인수를 읽게 하는 공유 파서다.
3
+ *
4
+ * 플래그는 위치 인수 앞뒤 어디에 와도 된다. 값 플래그의 값은 비어 있지 않고
5
+ * `-`로 시작하지 않아야 한다. `--` 이후는 모두 위치 인수로 읽어 `-`로
6
+ * 시작하는 경로·이름도 전달할 수 있다. 모르는 플래그, 중복 값 플래그, 빈
7
+ * 플래그 값은 사용법 오류다(undefined).
8
+ */
9
+ export function parseCommandArguments(arguments_, valueFlagNames, booleanFlagNames) {
10
+ const valueFlags = new Set(valueFlagNames);
11
+ const booleanFlags = new Set(booleanFlagNames);
12
+ const values = new Map();
13
+ const booleans = new Set();
14
+ const positionals = [];
15
+ let onlyPositionals = false;
16
+ for (let index = 0; index < arguments_.length; index++) {
17
+ const argument = arguments_[index];
18
+ if (argument === undefined)
19
+ return undefined;
20
+ if (onlyPositionals || !argument.startsWith('-')) {
21
+ positionals.push(argument);
22
+ continue;
23
+ }
24
+ if (argument === '--') {
25
+ onlyPositionals = true;
26
+ continue;
27
+ }
28
+ if (valueFlags.has(argument)) {
29
+ if (values.has(argument))
30
+ return undefined;
31
+ const value = arguments_[index + 1];
32
+ // 빈 값은 경로가 아니라 호출 오류다. 읽기 실패(코드 2)보다 사용법(64)이 맞다.
33
+ if (value === undefined || value.length === 0 || value.startsWith('-')) {
34
+ return undefined;
35
+ }
36
+ values.set(argument, value);
37
+ index++;
38
+ continue;
39
+ }
40
+ if (booleanFlags.has(argument)) {
41
+ booleans.add(argument);
42
+ continue;
43
+ }
44
+ return undefined;
45
+ }
46
+ return { valueFlags: values, booleanFlags: booleans, positionals };
47
+ }
48
+ //# sourceMappingURL=parse-arguments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-arguments.js","sourceRoot":"","sources":["../../src/cli/parse-arguments.ts"],"names":[],"mappings":"AAUA;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB,CACnC,UAA6B,EAC7B,cAAiC,EACjC,gBAAmC;IAEnC,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,cAAc,CAAC,CAAC;IAC3C,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzC,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;IACnC,MAAM,WAAW,GAAa,EAAE,CAAC;IACjC,IAAI,eAAe,GAAG,KAAK,CAAC;IAC5B,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,UAAU,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC;QACvD,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;QACnC,IAAI,QAAQ,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QAC7C,IAAI,eAAe,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACjD,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC3B,SAAS;QACX,CAAC;QACD,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;YACtB,eAAe,GAAG,IAAI,CAAC;YACvB,SAAS;QACX,CAAC;QACD,IAAI,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC7B,IAAI,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;gBAAE,OAAO,SAAS,CAAC;YAC3C,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;YACpC,kDAAkD;YAClD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;gBACvE,OAAO,SAAS,CAAC;YACnB,CAAC;YACD,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;YAC5B,KAAK,EAAE,CAAC;YACR,SAAS;QACX,CAAC;QACD,IAAI,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC/B,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACvB,SAAS;QACX,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;AACrE,CAAC","sourcesContent":["/** 플래그와 위치 인수로 나뉜 명령 인수다. */\nexport interface ParsedCommandArguments {\n /** 값 플래그 이름과 그 값이다. 같은 플래그를 두 번 주면 사용법 오류다. */\n readonly valueFlags: ReadonlyMap<string, string>;\n /** 값 없는 플래그 이름들이다. 반복은 한 번과 같다. */\n readonly booleanFlags: ReadonlySet<string>;\n /** 플래그가 아닌 인수다. `--` 뒤는 `-`로 시작해도 모두 여기에 담긴다. */\n readonly positionals: readonly string[];\n}\n\n/**\n * 모든 분석 하위 명령이 같은 규칙으로 인수를 읽게 하는 공유 파서다.\n *\n * 플래그는 위치 인수 앞뒤 어디에 와도 된다. 값 플래그의 값은 비어 있지 않고\n * `-`로 시작하지 않아야 한다. `--` 이후는 모두 위치 인수로 읽어 `-`로\n * 시작하는 경로·이름도 전달할 수 있다. 모르는 플래그, 중복 값 플래그, 빈\n * 플래그 값은 사용법 오류다(undefined).\n */\nexport function parseCommandArguments(\n arguments_: readonly string[],\n valueFlagNames: readonly string[],\n booleanFlagNames: readonly string[],\n): ParsedCommandArguments | undefined {\n const valueFlags = new Set(valueFlagNames);\n const booleanFlags = new Set(booleanFlagNames);\n const values = new Map<string, string>();\n const booleans = new Set<string>();\n const positionals: string[] = [];\n let onlyPositionals = false;\n for (let index = 0; index < arguments_.length; index++) {\n const argument = arguments_[index];\n if (argument === undefined) return undefined;\n if (onlyPositionals || !argument.startsWith('-')) {\n positionals.push(argument);\n continue;\n }\n if (argument === '--') {\n onlyPositionals = true;\n continue;\n }\n if (valueFlags.has(argument)) {\n if (values.has(argument)) return undefined;\n const value = arguments_[index + 1];\n // 빈 값은 경로가 아니라 호출 오류다. 읽기 실패(코드 2)보다 사용법(64)이 맞다.\n if (value === undefined || value.length === 0 || value.startsWith('-')) {\n return undefined;\n }\n values.set(argument, value);\n index++;\n continue;\n }\n if (booleanFlags.has(argument)) {\n booleans.add(argument);\n continue;\n }\n return undefined;\n }\n return { valueFlags: values, booleanFlags: booleans, positionals };\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { type CommandResult, type ReadTextFile } from './check-command.ts';
1
+ import { type CommandResult, type ReadTextFile } from './command-support.ts';
2
2
  /** query 인자를 실행해 bridge 질의 JSON과 종료 코드를 반환한다. */
3
3
  export declare function runQueryCommand(arguments_: readonly string[], readTextFile: ReadTextFile): Promise<CommandResult>;
4
4
  /** query 명령의 한 줄 사용법이다. */
@@ -1,27 +1,30 @@
1
1
  import { isBridgeJoinDeferred, joinBridgeDocuments, MAX_DOCUMENTS_PER_JOIN, } from "../join/join.js";
2
2
  import { createBridgeQuery, encodeBridgeQuery } from "../report/query.js";
3
- import { bridgeJoinDeferredError, internalError, inputFailureResult, readBridgeDocuments, } from "./check-command.js";
3
+ import { bridgeJoinDeferredError, inputFailureResult, internalError, readBridgeDocuments, } from "./command-support.js";
4
+ import { parseCommandArguments } from "./parse-arguments.js";
4
5
  /** query 인자를 실행해 bridge 질의 JSON과 종료 코드를 반환한다. */
5
6
  export async function runQueryCommand(arguments_, readTextFile) {
6
- const requested = arguments_[1];
7
- const inputPaths = arguments_.slice(2);
8
- if (arguments_[0] !== 'query' ||
9
- requested === undefined ||
7
+ const parsed = parseCommandArguments(arguments_.slice(1), [], []);
8
+ if (parsed === undefined)
9
+ return queryUsageError();
10
+ const requested = parsed.positionals[0];
11
+ const inputPaths = parsed.positionals.slice(1);
12
+ if (requested === undefined ||
10
13
  requested.trim().length === 0 ||
11
14
  inputPaths.length < 2 ||
12
- inputPaths.length > MAX_DOCUMENTS_PER_JOIN ||
13
- inputPaths.some((path) => path.startsWith('-'))) {
15
+ inputPaths.length > MAX_DOCUMENTS_PER_JOIN) {
14
16
  return queryUsageError();
15
17
  }
16
18
  try {
17
19
  const documents = await readBridgeDocuments(inputPaths, readTextFile);
18
20
  const joined = joinBridgeDocuments(documents);
19
- if (isBridgeJoinDeferred(joined))
20
- return bridgeJoinDeferredError();
21
+ if (isBridgeJoinDeferred(joined)) {
22
+ return bridgeJoinDeferredError(joined.observedFacts, documents.length);
23
+ }
21
24
  const query = createBridgeQuery(joined, requested);
22
25
  return {
23
26
  standardOutput: encodeBridgeQuery(query),
24
- standardError: '',
27
+ standardError: queryStatusHint(query),
25
28
  exitCode: query.status === 'found' ? 0 : 64,
26
29
  };
27
30
  }
@@ -29,6 +32,23 @@ export async function runQueryCommand(arguments_, readTextFile) {
29
32
  return inputFailureResult(error) ?? internalError();
30
33
  }
31
34
  }
35
+ /**
36
+ * 찾지 못한 질의에 원인을 한 줄로 알린다.
37
+ *
38
+ * 문서 자체는 stdout JSON이 이미 다 담고 있으므로, 사람이 종료 코드만 봐도
39
+ * "인수가 잘못됐는지"와 "이름이 없는지"를 구분할 수 있게 하는 보조 문구다.
40
+ * 정적 문자열과 숫자만 보간한다.
41
+ */
42
+ function queryStatusHint(query) {
43
+ if (query.status === 'found')
44
+ return '';
45
+ if (query.status === 'ambiguous') {
46
+ return `The requested name matches ${query.candidates?.length ?? 0} bridge keys; `
47
+ + 'repeat the query with a qualifiedName from candidates.\n';
48
+ }
49
+ return 'No bridge channel or method matches the requested name; '
50
+ + 'query a channel or method name that the inputs observed.\n';
51
+ }
32
52
  /** 잘못된 query 호출을 사용법과 코드 64로 바꾼다. */
33
53
  function queryUsageError() {
34
54
  return {