@pecot/tools-mcp 1.0.1 → 1.1.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 (39) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/cli/dist/cli.d.ts +1 -2
  3. package/cli/dist/cli.js +9 -13
  4. package/cli/dist/diagnostics.d.ts +38 -0
  5. package/cli/dist/diagnostics.js +46 -0
  6. package/cli/dist/domain/arguments.d.ts +2 -15
  7. package/cli/dist/domain/arguments.js +38 -99
  8. package/cli/dist/domain/athena-query.d.ts +2 -8
  9. package/cli/dist/domain/athena-query.js +10 -35
  10. package/cli/dist/domain/s3-input.d.ts +2 -9
  11. package/cli/dist/domain/s3-input.js +8 -21
  12. package/cli/dist/infra/awscli.d.ts +8 -36
  13. package/cli/dist/infra/awscli.js +15 -55
  14. package/cli/dist/infra/sso/login-registry.d.ts +4 -21
  15. package/cli/dist/infra/sso/login-registry.js +13 -56
  16. package/cli/dist/mcp/serve.d.ts +6 -28
  17. package/cli/dist/mcp/serve.js +20 -64
  18. package/cli/dist/mcp/server.d.ts +4 -20
  19. package/cli/dist/mcp/server.js +9 -35
  20. package/cli/dist/mcp/tools/_shared.d.ts +6 -27
  21. package/cli/dist/mcp/tools/_shared.js +36 -65
  22. package/cli/dist/mcp/tools/athena.js +17 -27
  23. package/cli/dist/mcp/tools/cloudwatch.js +13 -15
  24. package/cli/dist/mcp/tools/ec2.js +7 -10
  25. package/cli/dist/mcp/tools/ecr.js +10 -16
  26. package/cli/dist/mcp/tools/index.js +1 -5
  27. package/cli/dist/mcp/tools/lambda.js +10 -10
  28. package/cli/dist/mcp/tools/logs.js +22 -22
  29. package/cli/dist/mcp/tools/rds.js +15 -18
  30. package/cli/dist/mcp/tools/s3.js +19 -29
  31. package/cli/dist/mcp/tools/scheduler.js +10 -13
  32. package/cli/dist/mcp/tools/sqs.js +8 -8
  33. package/cli/dist/mcp/tools/sso.d.ts +1 -5
  34. package/cli/dist/mcp/tools/sso.js +33 -59
  35. package/cli/dist/mcp/tools/sts.js +2 -2
  36. package/cli/dist/node-runtime.d.ts +6 -1
  37. package/cli/dist/node-runtime.js +16 -8
  38. package/npm-shrinkwrap.json +3 -3
  39. package/package.json +2 -2
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pecot-t",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "pecot-t(pecot-tools)— 許可リストで限定した CLI コマンドを MCP ツールとして提供するプラグイン",
5
5
  "author": {
6
6
  "name": "pecot"
package/cli/dist/cli.d.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  export declare function createProgram(): Command;
3
3
  /**
4
- commander の既定である process.exit() 直呼びを止め、例外へ振り替える。
5
- サブコマンドを足したときに取りこぼさないよう再帰で適用する。
4
+ commander の process.exit() 直呼びを止め、例外へ振り替える。再帰で全サブコマンドに適用する
6
5
  */
7
6
  export declare function applyExitOverrideRecursively(command: Command): void;
8
7
  export declare function main(nodeVersion?: string): void;
package/cli/dist/cli.js CHANGED
@@ -1,19 +1,17 @@
1
- // pecot-tools CLI のエントリポイント。MCP サーバの起動 (`mcp`) だけを持つ薄い Command で、
2
- // MCP SDK の読み込みは action 内の dynamic import に閉じる。
1
+ // pecot-tools CLI のエントリポイント。MCP SDK は action 内の dynamic import に閉じる。
3
2
  import { realpathSync } from 'node:fs';
4
3
  import { pathToFileURL } from 'node:url';
5
4
  import { Command, CommanderError } from 'commander';
6
- import { checkNodeRuntime } from './node-runtime.js';
5
+ import { createDiagnostics } from './diagnostics.js';
6
+ import { nodeRuntimeGuidance } from './node-runtime.js';
7
7
  import { readVersion } from './version.js';
8
8
  export function createProgram() {
9
9
  const program = new Command();
10
10
  program.name('pecot-tools').version(readVersion());
11
11
  const mcp = program
12
12
  .command('mcp')
13
- .description('MCP サーバとして stdio (stdin/stdout) で待ち受ける');
14
- /* c8 ignore start -- action は dynamic import して実 process.stdin/stdout へ接続する。
15
- 単体テストから起動すると vitest worker の stdout ごと差し替えてしまう。
16
- tests/e2e/mcp-stdio.test.ts が実プロセスの spawn で担保する。 */
13
+ .description('Serve as an MCP server over stdio (stdin/stdout)');
14
+ /* c8 ignore start -- 実 stdin/stdout へ接続する。tests/e2e/mcp-stdio.test.ts が担保 */
17
15
  mcp.action(async () => {
18
16
  const { executeMcpServe, mcpServeDefaultDependencies } = await import('./mcp/serve.js');
19
17
  executeMcpServe(mcpServeDefaultDependencies);
@@ -22,8 +20,7 @@ export function createProgram() {
22
20
  return program;
23
21
  }
24
22
  /**
25
- commander の既定である process.exit() 直呼びを止め、例外へ振り替える。
26
- サブコマンドを足したときに取りこぼさないよう再帰で適用する。
23
+ commander の process.exit() 直呼びを止め、例外へ振り替える。再帰で全サブコマンドに適用する
27
24
  */
28
25
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- exitOverride() で変更する
29
26
  export function applyExitOverrideRecursively(command) {
@@ -32,9 +29,9 @@ export function applyExitOverrideRecursively(command) {
32
29
  applyExitOverrideRecursively(child);
33
30
  }
34
31
  export function main(nodeVersion = process.versions.node) {
35
- const runtimeError = checkNodeRuntime(nodeVersion);
32
+ const runtimeError = nodeRuntimeGuidance(nodeVersion);
36
33
  if (runtimeError !== undefined) {
37
- process.stderr.write(`pecot-tools: ${runtimeError}\n`);
34
+ createDiagnostics(process.stderr).error(runtimeError);
38
35
  process.exitCode = 1;
39
36
  return;
40
37
  }
@@ -53,8 +50,7 @@ export function main(nodeVersion = process.versions.node) {
53
50
  }
54
51
  /* v8 ignore start -- 直接起動時にだけ通る。挙動は tests/e2e/version.test.ts で担保 */
55
52
  if (process.argv[1] !== undefined) {
56
- // import.meta.url は symlink を解決した実体パスを指すため、argv[1] 側も realpath に揃える。
57
- // 揃えないと bin ラッパー等の symlink 経由起動で一致せず、何もせず exit 0 で終わる。
53
+ // import.meta.url は実体パスなので argv[1] も realpath に揃える。symlink 経由起動で一致しなくなるため。
58
54
  // eslint-disable-next-line security/detect-non-literal-fs-filename -- 起動された自分自身のパス
59
55
  const selfUrl = pathToFileURL(realpathSync(process.argv[1])).href;
60
56
  if (import.meta.url === selfUrl) {
@@ -0,0 +1,38 @@
1
+ import { z } from 'zod';
2
+ export interface DiagnosticStream {
3
+ write(text: string): unknown;
4
+ }
5
+ /**
6
+ done の動詞。自由な文字列を受け取らない
7
+ */
8
+ export declare const DiagnosticVerbSchema: z.ZodEnum<{
9
+ add: "add";
10
+ create: "create";
11
+ }>;
12
+ export type DiagnosticVerb = z.infer<typeof DiagnosticVerbSchema>;
13
+ export interface DiagnosticOperation {
14
+ readonly verb: DiagnosticVerb;
15
+ readonly target: string;
16
+ }
17
+ export interface Diagnostics {
18
+ readonly error: (message: string) => void;
19
+ readonly warning: (message: string) => void;
20
+ /**
21
+ 情報の診断だけに使う。進捗の行には使わない
22
+ */
23
+ readonly note: (message: string) => void;
24
+ /**
25
+ 実行した操作を `<過去分詞>: <target>` で出す
26
+ */
27
+ readonly done: (operation: DiagnosticOperation) => void;
28
+ }
29
+ export declare function createDiagnostics(stderr: DiagnosticStream): Diagnostics;
30
+ /**
31
+ 結果の本体を stdout にそのまま出す。末尾に改行が無ければ 1 つ足す
32
+ */
33
+ export declare function writeData(stdout: DiagnosticStream, text: string): void;
34
+ /**
35
+ commander が組み立てた使い方エラー・ヘルプの枠を、手を加えずに書く。commander が `error: ` を付ける
36
+ */
37
+ export declare function passthrough(stream: DiagnosticStream, text: string): void;
38
+ //# sourceMappingURL=diagnostics.d.ts.map
@@ -0,0 +1,46 @@
1
+ // stderr の診断行と stdout のデータを書く、唯一の場所。
2
+ // 行の形は specs/021-cli-output-functions/contracts/output-lines.md が決める。
3
+ // 期待の行は tests/diagnostics-cases.json に置く。
4
+ import { z } from 'zod';
5
+ /**
6
+ done の動詞。自由な文字列を受け取らない
7
+ */
8
+ export const DiagnosticVerbSchema = z.enum(['create', 'add']);
9
+ function pastOf(verb) {
10
+ switch (verb) {
11
+ case 'create': {
12
+ return 'created';
13
+ }
14
+ case 'add': {
15
+ return 'added';
16
+ }
17
+ }
18
+ }
19
+ /**
20
+ 末尾の改行を取り除き、2 行目以降の先頭に空白 2 つを付け、末尾に改行を 1 つ付ける
21
+ */
22
+ function formatMessage(prefix, message) {
23
+ const lines = message.replace(/\n+$/, '').split('\n');
24
+ return `${prefix}: ${lines.join('\n ')}\n`;
25
+ }
26
+ export function createDiagnostics(stderr) {
27
+ return {
28
+ error: (message) => { stderr.write(formatMessage('error', message)); },
29
+ warning: (message) => { stderr.write(formatMessage('warning', message)); },
30
+ note: (message) => { stderr.write(formatMessage('note', message)); },
31
+ done: ({ verb, target }) => { stderr.write(`${pastOf(verb)}: ${target}\n`); },
32
+ };
33
+ }
34
+ /**
35
+ 結果の本体を stdout にそのまま出す。末尾に改行が無ければ 1 つ足す
36
+ */
37
+ export function writeData(stdout, text) {
38
+ stdout.write(text.endsWith('\n') ? text : `${text}\n`);
39
+ }
40
+ /**
41
+ commander が組み立てた使い方エラー・ヘルプの枠を、手を加えずに書く。commander が `error: ` を付ける
42
+ */
43
+ export function passthrough(stream, text) {
44
+ stream.write(text);
45
+ }
46
+ //# sourceMappingURL=diagnostics.js.map
@@ -14,24 +14,11 @@ export interface CommonOptions {
14
14
  readonly region?: string;
15
15
  }
16
16
  /**
17
- 入力を検証する。問題があれば invalid-input を、無ければ undefined を返す。
18
-
19
- profile / region も同じ処理で検証する(別扱いにすると書式検証が抜ける)。
17
+ 入力を検証する。問題があれば invalid-input を、無ければ undefined を返す。profile / region も同じ処理で検証する。
20
18
  */
21
19
  export declare function validateInput(entry: Readonly<AllowlistEntry>, input: Readonly<Record<string, unknown>>): InvalidInput | undefined;
22
20
  /**
23
- awscli へ渡す引数配列を組み立てる。
24
-
25
- 並びは `<service> <subcommand> <位置引数…> <フラグ…> --output json --no-cli-pager
26
- --profile <p> --region <r>`。位置引数を先・フラグを後に置くと、string-list(nargs が貪欲)と
27
- 位置引数が同居しても awscli v2 の argparse が取り違えない(動作確認済み)。
28
-
29
- `--no-cli-pager` は jsonOutput の有無に依らず常に付ける(非 tty では無害で、分岐を持たない方が単純)。
30
-
31
- `--profile` / `--region` は末尾で 1 度だけ付ける。エントリ側が inputs に同名を宣言していても
32
- フラグ列挙では飛ばす。
33
-
34
- 値が '-' で始まる string は `--flag=value` の 1 トークンにする(flagTokens)。
21
+ awscli へ渡す引数配列を組み立てる。位置引数を先・フラグを後に置くと、nargs が貪欲な string-list と同居しても取り違えない。
35
22
  */
36
23
  export declare function buildArguments(entry: Readonly<AllowlistEntry>, input: Readonly<Record<string, unknown>>, common?: Readonly<CommonOptions>): string[];
37
24
  //# sourceMappingURL=arguments.d.ts.map
@@ -1,29 +1,13 @@
1
1
  // 入力の検証と awscli 引数配列の組み立て。
2
- //
3
- // shell を介さず spawn するため、利用者の値は必ず 1 要素 1 値で配列へ入れる。
4
- // 値が '-' で始まると awscli の引数解析でオプションと解釈され得るので、原則として先頭 '-' を
5
- // 拒否する(research R3)。例外はフラグで渡す 1 値の string だけで、こちらは
6
- // `--flag=value` の 1 トークン形にして渡す(doesAllowLeadingDash / flagTokens)。
7
- //
8
- // shorthand-list(`--dimensions` / `--filters` / `--image-ids`)は JSON.stringify で 1 トークンに
9
- // する。awscli はこれらのフラグを shorthand と JSON の双方で受ける。
10
- // JSON を選べば区切り文字の解釈がエスケープに閉じ、利用者の値が構文へ漏れる余地が無くなる(`,` を含むタグ値も渡せる)。
2
+ // 先頭 '-' の値は原則拒否し、例外の string は `--flag=value` の 1 トークンで渡す(research R3)。
11
3
  import { PROFILE_INPUT, REGION_INPUT } from './allowlist.js';
12
4
  import { validateLocalPath, validateS3ListUri, validateS3Uri } from './s3-input.js';
13
5
  /**
14
- shorthand の構成要素の値に混入すると構文として解釈される文字。
15
-
16
- awscli の shorthand は `Key=value,Key=value` の書式。
17
- 構成要素の値に `,` や `=` を含めると、契約に無いキー(`ExpectedBucketOwner` 等)を後ろに足せる。
18
- 実 awscli が受理することを動作確認済みなので、組み立て前に弾く。対象は文字列を連結する `shorthand`(Athena / ECR の
19
- `--filter`)だけで、JSON で組み立てる `shorthand-list` には要らない。
6
+ shorthand の値に混入すると構文になる文字。`,` や `=` で契約外のキーを足せるため、組み立て前に弾く。
20
7
  */
21
8
  const SHORTHAND_SYNTAX = /[,={}]/;
22
9
  /**
23
- 制御文字(U+0000〜U+001F と U+007F)を含むか。
24
-
25
- argv に NUL を載せると spawn が同期 throw し、ExecutionResult を伴わない応答になる。
26
- 他の制御文字も awscli の解釈が値ごとに変わるため、検証の段階で invalid-input にする。
10
+ 制御文字(U+0000〜U+001F と U+007F)を含むか。NUL は spawn が同期 throw するため、検証で弾く。
27
11
  */
28
12
  function hasControlCharacter(text) {
29
13
  for (const character of text) {
@@ -34,10 +18,7 @@ function hasControlCharacter(text) {
34
18
  return false;
35
19
  }
36
20
  /**
37
- 共通入力として末尾で付与するフラグ。
38
-
39
- エントリ側が同じフラグを inputs に宣言していても、付与箇所は末尾の 1 本に絞る
40
- (フラグ列挙でも出すと `--profile x --profile x` が並ぶ)。
21
+ 共通入力として末尾で付与するフラグ。inputs 側の宣言と重複して並ばないよう、付与は末尾の 1 本に絞る。
41
22
  */
42
23
  const COMMON_FLAGS = new Set([PROFILE_INPUT.awsFlag, REGION_INPUT.awsFlag].filter(flag => flag !== undefined));
43
24
  /**
@@ -108,17 +89,7 @@ function kindPredicate(kind, value) {
108
89
  }
109
90
  }
110
91
  /**
111
- 値の先頭 '-' を許す入力か。
112
-
113
- 対象はフラグで渡す `kind: 'string'` の 1 値だけ。CloudWatch Logs の除外パターン(`-INFO`)の
114
- ように、先頭 '-' に意味を持つ値が実在する。
115
- 実 awscli 2.36.40 では `--filter-pattern=-INFO` が受理される(`--filter-pattern -INFO` は 252)。組み立ては buildArguments が
116
- `--flag=value` の 1 トークン形で行うため、オプション注入にはならない。
117
-
118
- string-list は nargs が貪欲で、1 トークン形へ畳めない。
119
- 位置引数には `--flag=value` の逃がし先が無い。
120
- そのため、いずれも従来どおり拒否する(enum は許容値が許可リストの静的宣言なので、
121
- そもそも利用者の値が本判定に届かない)。
92
+ 値の先頭 '-' を許す入力か。対象はフラグで渡す string の 1 値だけで、`--flag=value` の 1 トークンで組み立てる。
122
93
  */
123
94
  function doesAllowLeadingDash(spec) {
124
95
  return spec.kind === 'string' && spec.awsFlag !== undefined && spec.positional === undefined;
@@ -128,65 +99,55 @@ string 系 1 値の検証。制御文字・先頭 '-' 拒否と kind 固有の
128
99
  */
129
100
  function validateStringValue(spec, value) {
130
101
  if (hasControlCharacter(value)) {
131
- return invalid(`${spec.name} は制御文字を含まない値で指定してください`);
102
+ return invalid(`${spec.name} must not contain control characters`);
132
103
  }
133
104
  if (value.startsWith('-') && !doesAllowLeadingDash(spec)) {
134
- return invalid(`${spec.name} の値は '-' で始められません: ${value}`);
105
+ return invalid(`${spec.name} must not start with '-': ${value}`);
135
106
  }
136
107
  const predicate = kindPredicate(spec.kind, value);
137
108
  if (predicate !== undefined) {
138
109
  return invalid(`${spec.name} ${predicate}: ${value}`);
139
110
  }
140
111
  if (spec.pattern !== undefined && !spec.pattern.test(value)) {
141
- return invalid(`${spec.name} の形式が不正です: ${value}`);
112
+ return invalid(`${spec.name} has an invalid format: ${value}`);
142
113
  }
143
114
  return undefined;
144
115
  }
145
116
  /**
146
- shorthand の構成要素として載る値を検証する。
147
-
148
- shorthand 自身は入力レコードから読まない(利用者は指定できない)。見るのは構成要素の
149
- 取得元入力の値だけで、取得元自体の型・書式は通常の入力として別途検証されている。
117
+ shorthand の構成要素の値を検証する。shorthand 自身は読まず、取得元入力の値だけを見る。
150
118
  */
151
119
  function validateShorthandParts(spec, input) {
152
120
  const declaredParts = spec.parts ?? [];
153
121
  for (const part of declaredParts) {
154
122
  const value = readInput(input, part.from);
155
123
  if (typeof value === 'string' && SHORTHAND_SYNTAX.test(value)) {
156
- return invalid(`${part.from} に , = { } は含められません: ${value}`);
124
+ return invalid(`${part.from} must not contain , = { }: ${value}`);
157
125
  }
158
126
  }
159
127
  return undefined;
160
128
  }
161
129
  /**
162
- shorthand-list の構成要素の値 1 つずつを検証する。
163
-
164
- 区切り文字(`, = { }`)は見ない。組み立てが JSON.stringify で、値はエスケープされて JSON の
165
- 文字列リテラルに収まるため、要素の中へ契約外のキーを足す手段が無い。
166
- 空文字は awscli が空値として解釈するので弾く。制御文字は spawn へ届く前にここで拒否する。
130
+ shorthand-list の構成要素の値を検証する。JSON.stringify がエスケープするので、区切り文字は見ない。
167
131
  */
168
132
  function validateElementTexts(spec, field, texts) {
169
133
  for (const text of texts) {
170
134
  if (text === '')
171
- return invalid(`${spec.name} の ${field} に空文字は指定できません`);
135
+ return invalid(`${spec.name} ${field} must not be empty`);
172
136
  if (hasControlCharacter(text)) {
173
- return invalid(`${spec.name} の ${field} は制御文字を含まない値で指定してください`);
137
+ return invalid(`${spec.name} ${field} must not contain control characters`);
174
138
  }
175
139
  }
176
140
  return undefined;
177
141
  }
178
142
  /**
179
- shorthand-list の要素 1 件を検証する。
180
-
181
- 値を持つ構成要素が 1 つも無い要素は拒否する。`--filters` に空の要素を渡すと awscli が
182
- 空トークンを受け取り、意図しない絞り込みになるため。
143
+ shorthand-list の要素 1 件を検証する。値を持つ構成要素が無い要素は、意図しない絞り込みになるので拒否する。
183
144
  */
184
145
  function validateShorthandListElement(spec, element) {
185
146
  const parts = spec.elements ?? [];
186
147
  /* c8 ignore start -- inputSchema の z.strictObject が入力検証エラーで先に落とすため到達しない(二重防御) */
187
148
  for (const key of Object.keys(element)) {
188
149
  if (parts.every(part => part.field !== key)) {
189
- return invalid(`${spec.name} の要素に指定できないキーです: ${key}`);
150
+ return invalid(`${spec.name} element has an unsupported key: ${key}`);
190
151
  }
191
152
  }
192
153
  /* c8 ignore stop */
@@ -198,16 +159,16 @@ function validateShorthandListElement(spec, element) {
198
159
  let texts;
199
160
  if (part.list === true) {
200
161
  if (!isStringArray(value)) {
201
- return invalid(`${spec.name} の ${part.field} は文字列の配列で指定してください`);
162
+ return invalid(`${spec.name} ${part.field} must be an array of strings`);
202
163
  }
203
164
  if (value.length === 0) {
204
- return invalid(`${spec.name} の ${part.field} は 1 つ以上の値で指定してください`);
165
+ return invalid(`${spec.name} ${part.field} must have at least one value`);
205
166
  }
206
167
  texts = value;
207
168
  }
208
169
  else {
209
170
  if (typeof value !== 'string') {
210
- return invalid(`${spec.name} の ${part.field} は文字列で指定してください`);
171
+ return invalid(`${spec.name} ${part.field} must be a string`);
211
172
  }
212
173
  texts = [value];
213
174
  }
@@ -216,17 +177,17 @@ function validateShorthandListElement(spec, element) {
216
177
  return rejected;
217
178
  filled += 1;
218
179
  }
219
- return filled === 0 ? invalid(`${spec.name} の要素は ${parts.map(part => part.field).join(' / ')} のいずれかで指定してください`) : undefined;
180
+ return filled === 0 ? invalid(`${spec.name} element must have at least one of ${parts.map(part => part.field).join(' / ')}`) : undefined;
220
181
  }
221
182
  /**
222
183
  shorthand-list の値(オブジェクトの配列)を検証する。
223
184
  */
224
185
  function validateShorthandList(spec, value) {
225
186
  if (!Array.isArray(value))
226
- return invalid(`${spec.name} はオブジェクトの配列で指定してください`);
187
+ return invalid(`${spec.name} must be an array of objects`);
227
188
  for (const element of value) {
228
189
  if (!isElementRecord(element)) {
229
- return invalid(`${spec.name} の要素はオブジェクトで指定してください`);
190
+ return invalid(`${spec.name} element must be an object`);
230
191
  }
231
192
  const rejected = validateShorthandListElement(spec, element);
232
193
  if (rejected !== undefined)
@@ -240,11 +201,11 @@ function validateValue(spec, value) {
240
201
  case 'abs-path':
241
202
  case 's3-uri':
242
203
  case 's3-list-uri': {
243
- return typeof value === 'string' ? validateStringValue(spec, value) : invalid(`${spec.name} は文字列で指定してください`);
204
+ return typeof value === 'string' ? validateStringValue(spec, value) : invalid(`${spec.name} must be a string`);
244
205
  }
245
206
  case 'string-list': {
246
207
  if (!isStringArray(value))
247
- return invalid(`${spec.name} は文字列の配列で指定してください`);
208
+ return invalid(`${spec.name} must be an array of strings`);
248
209
  for (const element of value) {
249
210
  const rejected = validateStringValue(spec, element);
250
211
  if (rejected !== undefined)
@@ -252,31 +213,31 @@ function validateValue(spec, value) {
252
213
  // 列挙を持つ string-list(--statistics 等)は要素ごとに比較する。awscli は列挙を
253
214
  // クライアント側で検証しないため、ここを抜くと綴り違いがそのまま API へ届く。
254
215
  if (spec.values !== undefined && !spec.values.includes(element)) {
255
- return invalid(`${spec.name} に指定できない値です: ${element}`);
216
+ return invalid(`${spec.name} has an unsupported value: ${element}`);
256
217
  }
257
218
  }
258
219
  return undefined;
259
220
  }
260
221
  case 'integer': {
261
222
  if (typeof value !== 'number' || !Number.isSafeInteger(value)) {
262
- return invalid(`${spec.name} は整数で指定してください`);
223
+ return invalid(`${spec.name} must be an integer`);
263
224
  }
264
225
  if (spec.min !== undefined && value < spec.min) {
265
- return invalid(`${spec.name} は ${String(spec.min)} 以上で指定してください: ${String(value)}`);
226
+ return invalid(`${spec.name} must be at least ${String(spec.min)}: ${String(value)}`);
266
227
  }
267
228
  if (spec.max !== undefined && value > spec.max) {
268
- return invalid(`${spec.name} は ${String(spec.max)} 以下で指定してください: ${String(value)}`);
229
+ return invalid(`${spec.name} must be at most ${String(spec.max)}: ${String(value)}`);
269
230
  }
270
231
  return undefined;
271
232
  }
272
233
  case 'boolean': {
273
- return typeof value === 'boolean' ? undefined : invalid(`${spec.name} は true / false で指定してください`);
234
+ return typeof value === 'boolean' ? undefined : invalid(`${spec.name} must be true or false`);
274
235
  }
275
236
  case 'enum': {
276
237
  if (typeof value !== 'string')
277
- return invalid(`${spec.name} は文字列で指定してください`);
238
+ return invalid(`${spec.name} must be a string`);
278
239
  if (spec.values?.includes(value) !== true) {
279
- return invalid(`${spec.name} に指定できない値です: ${value}`);
240
+ return invalid(`${spec.name} has an unsupported value: ${value}`);
280
241
  }
281
242
  return undefined;
282
243
  }
@@ -285,15 +246,13 @@ function validateValue(spec, value) {
285
246
  }
286
247
  /* c8 ignore start -- shorthand は validateInput が先に分岐するため到達しない。switch の網羅性のために置く */
287
248
  case 'shorthand': {
288
- return invalid(`${spec.name} は直接指定できません`);
249
+ return invalid(`${spec.name} cannot be specified directly`);
289
250
  }
290
251
  /* c8 ignore stop */
291
252
  }
292
253
  }
293
254
  /**
294
- 入力を検証する。問題があれば invalid-input を、無ければ undefined を返す。
295
-
296
- profile / region も同じ処理で検証する(別扱いにすると書式検証が抜ける)。
255
+ 入力を検証する。問題があれば invalid-input を、無ければ undefined を返す。profile / region も同じ処理で検証する。
297
256
  */
298
257
  export function validateInput(entry, input) {
299
258
  for (const spec of specsOf(entry)) {
@@ -306,7 +265,7 @@ export function validateInput(entry, input) {
306
265
  const value = readInput(input, spec.name);
307
266
  if (value === undefined) {
308
267
  if (spec.required)
309
- return invalid(`${spec.name} は必須です`);
268
+ return invalid(`${spec.name} is required`);
310
269
  continue;
311
270
  }
312
271
  const rejected = validateValue(spec, value);
@@ -317,7 +276,7 @@ export function validateInput(entry, input) {
317
276
  for (const group of exclusiveGroups) {
318
277
  const specified = group.filter(name => readInput(input, name) !== undefined);
319
278
  if (specified.length > 1) {
320
- return invalid(`${specified.join(' と ')} は同時に指定できません`);
279
+ return invalid(`${specified.join(' and ')} cannot be combined`);
321
280
  }
322
281
  }
323
282
  return undefined;
@@ -330,9 +289,7 @@ function effectiveValue(spec, input) {
330
289
  return isInputValue(value) ? value : undefined;
331
290
  }
332
291
  /**
333
- shorthand フラグに載せる `Key=value,…` を組み立てる。
334
-
335
- 値を持つ構成要素だけを並べ、1 つも無ければ undefined(= フラグごと省く)。
292
+ shorthand フラグに載せる `Key=value,…` を組み立てる。値を持つ構成要素が無ければ undefined(フラグごと省く)。
336
293
  */
337
294
  function shorthandValue(spec, input) {
338
295
  const parts = [];
@@ -345,11 +302,7 @@ function shorthandValue(spec, input) {
345
302
  return parts.length === 0 ? undefined : parts.join(',');
346
303
  }
347
304
  /**
348
- shorthand-list フラグに載せる JSON 1 トークンを組み立てる。
349
-
350
- 配列全体が `[{"Name":"…","Value":"…"}]` の 1 トークンになり、`list` の構成要素は JSON の配列のまま載せる。
351
- 値を持つ構成要素が無い要素は落とす。要素が 1 つも残らなければ undefined
352
- (= フラグごと省く)。
305
+ shorthand-list フラグに載せる JSON 1 トークンを組み立てる。要素が 1 つも残らなければ undefined。
353
306
  */
354
307
  function shorthandListToken(spec, input) {
355
308
  const value = readInput(input, spec.name);
@@ -384,27 +337,13 @@ function valueTexts(value) {
384
337
  return [typeof value === 'string' ? value : String(value)];
385
338
  }
386
339
  /**
387
- フラグ 1 つ分のトークン列。値が '-' で始まる string のときだけ `--flag=value` の 1 トークンにする。
388
-
389
- 別トークンで渡すと awscli の argparse が値をオプションとして読む。`=` 連結は shell を
390
- 介さない配列渡しなので、別トークンへ割れることも別コマンドへ委譲されることも無い。
340
+ フラグ 1 つ分のトークン列。値が '-' で始まる string は、argparse の誤読を避けて `--flag=value` の 1 トークンにする。
391
341
  */
392
342
  function flagTokens(flag, value) {
393
343
  return typeof value === 'string' && value.startsWith('-') ? [`${flag}=${value}`] : [flag, ...valueTexts(value)];
394
344
  }
395
345
  /**
396
- awscli へ渡す引数配列を組み立てる。
397
-
398
- 並びは `<service> <subcommand> <位置引数…> <フラグ…> --output json --no-cli-pager
399
- --profile <p> --region <r>`。位置引数を先・フラグを後に置くと、string-list(nargs が貪欲)と
400
- 位置引数が同居しても awscli v2 の argparse が取り違えない(動作確認済み)。
401
-
402
- `--no-cli-pager` は jsonOutput の有無に依らず常に付ける(非 tty では無害で、分岐を持たない方が単純)。
403
-
404
- `--profile` / `--region` は末尾で 1 度だけ付ける。エントリ側が inputs に同名を宣言していても
405
- フラグ列挙では飛ばす。
406
-
407
- 値が '-' で始まる string は `--flag=value` の 1 トークンにする(flagTokens)。
346
+ awscli へ渡す引数配列を組み立てる。位置引数を先・フラグを後に置くと、nargs が貪欲な string-list と同居しても取り違えない。
408
347
  */
409
348
  export function buildArguments(entry, input, common = {}) {
410
349
  const argv = [entry.service, entry.subcommand];
@@ -1,8 +1,5 @@
1
1
  /**
2
- 拒否理由。message は呼び出し元エージェントへそのまま返す。
3
-
4
- `unterminated`(引用符・コメントが閉じていない)を `not-read-only` と分けるのは、reason だけを
5
- 見る呼び出し元が「変更系だから拒否」と誤読しないようにするため。倒す先はどちらも拒否で同じ。
2
+ 拒否理由。message は呼び出し元へそのまま返す。unterminated を分けるのは、変更系と誤読させないため。
6
3
  */
7
4
  export type AthenaQueryRejectionReason = 'empty' | 'multi-statement' | 'not-read-only' | 'unterminated';
8
5
  export type AthenaQueryClassification = {
@@ -13,10 +10,7 @@ export type AthenaQueryClassification = {
13
10
  readonly message: string;
14
11
  };
15
12
  /**
16
- クエリを参照専用 / 拒否に分類する。
17
-
18
- 判定順は 未終端 → 複文 → 空 → 先頭キーワード。複文を空より先に見るのは、
19
- `; SELECT 1` のように 1 文目が空の入力を「空」ではなく「複文」と伝えるため。
13
+ クエリを参照専用 / 拒否に分類する。複文を空より先に見るのは、`; SELECT 1` を「複文」と伝えるため。
20
14
  */
21
15
  export declare function classifyAthenaQuery(query: string): AthenaQueryClassification;
22
16
  //# sourceMappingURL=athena-query.d.ts.map
@@ -1,12 +1,5 @@
1
1
  // Athena に投げるクエリが参照専用かの判定(FR-008 / research R6)。
2
- //
3
- // 完全な SQL パーサは持たない。目的は「変更系と複文を awscli 起動前に落とす」ことなので、
4
- // 判定できない入力(未終端の引用符・コメント)は許可側ではなく拒否側へ倒す。
5
- // 判定は Trino 構文を前提にする。Athena では `WITH` で始まる文は必ず問い合わせで、
6
- // `INSERT INTO … WITH` / `CREATE TABLE … AS WITH` の `WITH` は先頭に来ない。
7
- // STG1 での確認(2026-09-07): `WITH … INSERT` は StartQueryExecution 自体が失敗する。
8
- // 失敗の内容は InvalidRequestException / MALFORMED_QUERY で、実行に至らない。
9
- // 例外は内側の文を実際に実行する `EXPLAIN ANALYZE` だけで、これは `EXPLAIN` を剥がす反復で塞ぐ。
2
+ // 判定できない入力は拒否側へ倒す。`EXPLAIN ANALYZE` は内側の文を実行するので、`EXPLAIN` を剥がす反復で塞ぐ。
10
3
  /**
11
4
  参照専用として受け付ける先頭キーワード。判定は大小文字を区別しない。
12
5
  */
@@ -20,10 +13,10 @@ const EXPLAIN_MODIFIERS = new Set(['ANALYZE', 'VERBOSE']);
20
13
  */
21
14
  const LEADING_WORD = /^\s*[A-Z]+/i;
22
15
  const REJECTION_MESSAGES = {
23
- 'empty': 'query_string が空です。参照クエリを指定してください',
24
- 'multi-statement': '複数の文は実行できません。; の後ろに文を続けず 1 文だけ指定してください',
25
- 'not-read-only': '参照クエリ(SELECT / WITH / SHOW / DESCRIBE / EXPLAIN)だけを実行できます',
26
- 'unterminated': '引用符またはコメントが閉じていないため、参照クエリだと判定できません',
16
+ 'empty': 'query_string is empty. Specify a read-only query',
17
+ 'multi-statement': 'Multiple statements are not allowed. Specify a single statement with nothing after the ;',
18
+ 'not-read-only': 'Only read-only queries (SELECT / WITH / SHOW / DESCRIBE / EXPLAIN) can be executed',
19
+ 'unterminated': 'Cannot confirm the query is read-only because a quote or comment is not closed',
27
20
  };
28
21
  function reject(reason, message) {
29
22
  return { allowed: false, reason, message };
@@ -43,11 +36,7 @@ function skipBlockComment(query, start) {
43
36
  return end === -1 ? undefined : end + 2;
44
37
  }
45
38
  /**
46
- 引用の終端(閉じ引用符の次)。start は開き引用符の次を指す。
47
-
48
- `'…'` 文字列リテラルと `"…"` 引用識別子のエスケープは、同じ引用符の 2 重化だけ。
49
- Trino では `\` は文字列リテラル中でも普通の文字なので、`'\'` は「閉じていない」ではなく
50
- 「バックスラッシュ 1 文字のリテラル」になる。
39
+ 引用の終端(閉じ引用符の次)。エスケープは引用符の 2 重化だけで、Trino では `\` は普通の文字。
51
40
  */
52
41
  function skipQuoted(query, start, quote) {
53
42
  let index = start;
@@ -65,11 +54,7 @@ function skipQuoted(query, start, quote) {
65
54
  return undefined;
66
55
  }
67
56
  /**
68
- コメントを空白へ、引用の中身を空の引用へ均す。
69
-
70
- 中身を捨てるのは、後段が見るのは「リテラル外の `;`」と「先頭キーワード」だけだから。
71
- リテラルの内容は判定材料にならない。均しておけばリテラル内の `;` や `--` が
72
- 後段の単純な文字列走査に引っ掛からない。
57
+ コメントを空白へ、引用の中身を空の引用へ均す。リテラル内の `;` や `--` を後段の走査に拾わせないため。
73
58
  */
74
59
  function scan(query) {
75
60
  let code = '';
@@ -126,10 +111,7 @@ function afterLeadingWord(text) {
126
111
  return matched === undefined ? text : text.slice(matched[0].length);
127
112
  }
128
113
  /**
129
- 先頭が `(` ならその対応括弧までを剥がす(`EXPLAIN (TYPE DISTRIBUTED) SELECT …`)。
130
-
131
- 対応する `)` が無い場合は剥がさずに返す。剥がせないと、後段の語判定は左括弧を見て
132
- 語を取れず、拒否側へ倒れる。
114
+ 先頭が `(` ならその対応括弧までを剥がす。対応する `)` が無ければ剥がさず、拒否側へ倒す。
133
115
  */
134
116
  function afterOptionGroup(text) {
135
117
  const trimmed = text.trimStart();
@@ -149,11 +131,7 @@ function afterOptionGroup(text) {
149
131
  return trimmed;
150
132
  }
151
133
  /**
152
- 1 文が参照専用か。
153
-
154
- `EXPLAIN` は修飾語とオプション群を剥がして内側を見直す。1 周ごとに必ず `EXPLAIN` の 1 語を
155
- 消費するので停止する。再帰ではなくループにするのは、`EXPLAIN` を数万段重ねた入力で
156
- コールスタックを溢れさせないため(溢れると失敗種別の外の例外になる)。
134
+ 1 文が参照専用か。再帰でなくループなのは、`EXPLAIN` を重ねた入力でコールスタックを溢れさせないため。
157
135
  */
158
136
  function isReadOnlyStatement(statement) {
159
137
  let current = statement;
@@ -169,10 +147,7 @@ function isReadOnlyStatement(statement) {
169
147
  return READ_ONLY_KEYWORDS.has(word);
170
148
  }
171
149
  /**
172
- クエリを参照専用 / 拒否に分類する。
173
-
174
- 判定順は 未終端 → 複文 → 空 → 先頭キーワード。複文を空より先に見るのは、
175
- `; SELECT 1` のように 1 文目が空の入力を「空」ではなく「複文」と伝えるため。
150
+ クエリを参照専用 / 拒否に分類する。複文を空より先に見るのは、`; SELECT 1` を「複文」と伝えるため。
176
151
  */
177
152
  export function classifyAthenaQuery(query) {
178
153
  const scanned = scan(query);
@@ -1,9 +1,5 @@
1
1
  /**
2
- `s3://<bucket>/<key>` 形式か。
3
-
4
- 空白を含む値は拒否しない。引数は shell を介さず、1 要素 1 値で awscli へ渡る。
5
- そのため `s3://b/k --recursive` のような値は「そういう名前のキー」として扱われ、
6
- 別オプションには化けない(research R3)。
2
+ `s3://<bucket>/<key>` 形式か。空白を含む値は、shell を介さず 1 値で渡るので拒否しない(research R3)。
7
3
  */
8
4
  export declare function validateS3Uri(value: string): string | undefined;
9
5
  /**
@@ -11,10 +7,7 @@ export declare function validateS3Uri(value: string): string | undefined;
11
7
  */
12
8
  export declare function validateS3ListUri(value: string): string | undefined;
13
9
  /**
14
- 書き出し先のローカルパスとして妥当か。
15
-
16
- `s3://…` を先に見るのは、絶対パスの検証だけでも弾けるが「相対パスだ」という案内になり、
17
- 取得元と書き出し先を取り違えた利用者に誤った直し方を促すため。
10
+ 書き出し先のローカルパスとして妥当か。`s3://…` を先に弾くのは、取得元と取り違えた利用者に誤案内しないため。
18
11
  */
19
12
  export declare function validateLocalPath(value: string): string | undefined;
20
13
  //# sourceMappingURL=s3-input.d.ts.map