@pecot/tools-mcp 0.0.0-stage → 1.0.1

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 (71) hide show
  1. package/.claude-plugin/plugin.json +8 -0
  2. package/LICENSE +21 -0
  3. package/README.md +113 -2
  4. package/bin/pecot-tools-mcp.mjs +10 -0
  5. package/cli/dist/cli.d.ts +9 -0
  6. package/cli/dist/cli.js +65 -0
  7. package/cli/dist/domain/allowlist.d.ts +228 -0
  8. package/cli/dist/domain/allowlist.js +657 -0
  9. package/cli/dist/domain/arguments.d.ts +37 -0
  10. package/cli/dist/domain/arguments.js +453 -0
  11. package/cli/dist/domain/athena-query.d.ts +22 -0
  12. package/cli/dist/domain/athena-query.js +191 -0
  13. package/cli/dist/domain/failure.d.ts +22 -0
  14. package/cli/dist/domain/failure.js +99 -0
  15. package/cli/dist/domain/redact.d.ts +5 -0
  16. package/cli/dist/domain/redact.js +38 -0
  17. package/cli/dist/domain/s3-input.d.ts +20 -0
  18. package/cli/dist/domain/s3-input.js +50 -0
  19. package/cli/dist/domain/sso-output.d.ts +23 -0
  20. package/cli/dist/domain/sso-output.js +70 -0
  21. package/cli/dist/infra/awscli.d.ts +104 -0
  22. package/cli/dist/infra/awscli.js +245 -0
  23. package/cli/dist/infra/ignore-rejection.d.ts +6 -0
  24. package/cli/dist/infra/ignore-rejection.js +13 -0
  25. package/cli/dist/infra/local-file.d.ts +9 -0
  26. package/cli/dist/infra/local-file.js +21 -0
  27. package/cli/dist/infra/sso/login-registry.d.ts +99 -0
  28. package/cli/dist/infra/sso/login-registry.js +314 -0
  29. package/cli/dist/mcp/executor.d.ts +14 -0
  30. package/cli/dist/mcp/executor.js +44 -0
  31. package/cli/dist/mcp/result.d.ts +57 -0
  32. package/cli/dist/mcp/result.js +53 -0
  33. package/cli/dist/mcp/serve.d.ts +70 -0
  34. package/cli/dist/mcp/serve.js +185 -0
  35. package/cli/dist/mcp/server.d.ts +69 -0
  36. package/cli/dist/mcp/server.js +115 -0
  37. package/cli/dist/mcp/tools/_shared.d.ts +141 -0
  38. package/cli/dist/mcp/tools/_shared.js +290 -0
  39. package/cli/dist/mcp/tools/athena.d.ts +4 -0
  40. package/cli/dist/mcp/tools/athena.js +82 -0
  41. package/cli/dist/mcp/tools/cloudwatch.d.ts +4 -0
  42. package/cli/dist/mcp/tools/cloudwatch.js +46 -0
  43. package/cli/dist/mcp/tools/ec2.d.ts +4 -0
  44. package/cli/dist/mcp/tools/ec2.js +31 -0
  45. package/cli/dist/mcp/tools/ecr.d.ts +4 -0
  46. package/cli/dist/mcp/tools/ecr.js +37 -0
  47. package/cli/dist/mcp/tools/index.d.ts +7 -0
  48. package/cli/dist/mcp/tools/index.js +37 -0
  49. package/cli/dist/mcp/tools/lambda.d.ts +4 -0
  50. package/cli/dist/mcp/tools/lambda.js +36 -0
  51. package/cli/dist/mcp/tools/logs.d.ts +4 -0
  52. package/cli/dist/mcp/tools/logs.js +74 -0
  53. package/cli/dist/mcp/tools/rds.d.ts +4 -0
  54. package/cli/dist/mcp/tools/rds.js +60 -0
  55. package/cli/dist/mcp/tools/s3.d.ts +4 -0
  56. package/cli/dist/mcp/tools/s3.js +96 -0
  57. package/cli/dist/mcp/tools/scheduler.d.ts +4 -0
  58. package/cli/dist/mcp/tools/scheduler.js +44 -0
  59. package/cli/dist/mcp/tools/sqs.d.ts +4 -0
  60. package/cli/dist/mcp/tools/sqs.js +33 -0
  61. package/cli/dist/mcp/tools/sso.d.ts +11 -0
  62. package/cli/dist/mcp/tools/sso.js +240 -0
  63. package/cli/dist/mcp/tools/sts.d.ts +4 -0
  64. package/cli/dist/mcp/tools/sts.js +15 -0
  65. package/cli/dist/node-runtime.d.ts +20 -0
  66. package/cli/dist/node-runtime.js +84 -0
  67. package/cli/dist/package.json +1 -0
  68. package/cli/dist/version.d.ts +19 -0
  69. package/cli/dist/version.js +44 -0
  70. package/npm-shrinkwrap.json +67 -0
  71. package/package.json +21 -4
@@ -0,0 +1,99 @@
1
+ // awscli の失敗理由の種別判定(FR-009 / research R4)。
2
+ //
3
+ // awscli はエラーを構造化して返さないため、終了コードと stderr の文字列比較が唯一の手段になる。
4
+ // 比較表は research R4 の表を元にしている。
5
+ // awscli 2.36.40 の実採取(2026-09-07)で確認した文言を優先して直している。
6
+ // 例えば `Found invalid choice` は、R4 の `Invalid choice` と大小文字が異なる。
7
+ // AccessDenied / ResourceNotFound 系は実アカウントを使っていないため R4 転記のまま。
8
+ /**
9
+ awscli v2 の「構文不正」終了コード。
10
+ */
11
+ const SYNTAX_ERROR_EXIT_CODE = 252;
12
+ /**
13
+ stderr の比較表。上から順に見るため、並びがそのまま優先順になる。
14
+
15
+ auth-expired を auth-missing より先に置くのは、期限切れの stderr に「認証情報が無い」系の
16
+ 文言を併記する場合があるため。
17
+ permission-denied は resource-not-found より先に置く。権限不足を「対象が無い」と誤って伝えないため。
18
+ */
19
+ const STDERR_RULES = [
20
+ {
21
+ category: 'auth-expired',
22
+ fragments: [
23
+ 'Error loading SSO Token',
24
+ 'The SSO session associated with this profile has expired',
25
+ 'ExpiredToken',
26
+ 'InvalidClientTokenId',
27
+ 'Token has expired',
28
+ ],
29
+ },
30
+ {
31
+ category: 'auth-missing',
32
+ fragments: [
33
+ 'Unable to locate credentials',
34
+ 'Missing the following required SSO configuration',
35
+ ],
36
+ // R4 の `The config profile (<name>) could not be found`。
37
+ // `could not be found` の断片だけでは、リソース不在の stderr も auth-missing になる。
38
+ // 例は `The specified log group could not be found`。
39
+ // aws_sso_login を促す誤案内になるので、文全体で比較する。
40
+ patterns: [/The config profile \(.+\) could not be found/],
41
+ },
42
+ {
43
+ category: 'permission-denied',
44
+ fragments: [
45
+ 'AccessDenied',
46
+ 'AccessDeniedException',
47
+ 'UnauthorizedOperation',
48
+ 'not authorized to perform',
49
+ ],
50
+ },
51
+ {
52
+ category: 'resource-not-found',
53
+ fragments: [
54
+ 'ResourceNotFoundException',
55
+ 'NoSuchBucket',
56
+ 'NoSuchKey',
57
+ 'QueueDoesNotExist',
58
+ 'AWS.SimpleQueueService.NonExistentQueue',
59
+ '(404)',
60
+ ],
61
+ },
62
+ {
63
+ category: 'invalid-input',
64
+ fragments: [
65
+ 'Unknown options',
66
+ // 実採取は `argument operation: Found invalid choice 'bogus-op'`。R4 転記の
67
+ // `Invalid choice` は大小文字が違って掛からないため、両方を並べる。
68
+ 'Found invalid choice',
69
+ 'Invalid choice',
70
+ 'the following arguments are required:',
71
+ ],
72
+ // R4 の `argument ... is required`(v1 語法)。オプション名が可変なので正規表現で見る。
73
+ patterns: [/argument .+ is required/],
74
+ },
75
+ ];
76
+ function doesMatch(rule, stderr) {
77
+ return rule.fragments.some(fragment => stderr.includes(fragment)) || (rule.patterns ?? []).some(pattern => pattern.test(stderr));
78
+ }
79
+ /**
80
+ 失敗の種別を決める。
81
+
82
+ 順は ENOENT → 時間切れ → 終了コード 252 → stderr 比較 → other。
83
+ ENOENT を最優先にするのは、awscli 未導入では stderr に何が出ていても打つ手が
84
+ 「awscli を入れる」しかないため。
85
+ */
86
+ export function classifyFailure(signals) {
87
+ if (signals.spawnErrorCode === 'ENOENT')
88
+ return 'awscli-missing';
89
+ if (signals.timedOut)
90
+ return 'timeout';
91
+ if (signals.exitCode === SYNTAX_ERROR_EXIT_CODE)
92
+ return 'invalid-input';
93
+ for (const rule of STDERR_RULES) {
94
+ if (doesMatch(rule, signals.stderr))
95
+ return rule.category;
96
+ }
97
+ return 'other';
98
+ }
99
+ //# sourceMappingURL=failure.js.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ アクセスキー ID・シークレット・セッショントークンを [redacted] に置き換える。
3
+ */
4
+ export declare function redactSecrets(text: string): string;
5
+ //# sourceMappingURL=redact.d.ts.map
@@ -0,0 +1,38 @@
1
+ // 応答へ載せる前の秘匿化(FR-005 / SC-006 / research R10)。
2
+ //
3
+ // awscli の通常出力に秘密は含まれないが、想定外のエラーや --debug 相当の出力で
4
+ // 資格情報が混入する事態を、応答側の最終段で塞ぐ。
5
+ /**
6
+ 置換後の文字列。
7
+ */
8
+ const REDACTED = '[redacted]';
9
+ /**
10
+ アクセスキー ID。R10 の `(AKIA|ASIA)[0-9A-Z]{16}` と同値(`A[KS]IA` は `AKIA|ASIA`)。
11
+ */
12
+ const ACCESS_KEY_ID = /A[KS]IA[\dA-Z]{16}/g;
13
+ /**
14
+ 値部だけを差し替える規則(キー名は残し、何が伏せられたか分かるようにする)。
15
+
16
+ `i` を付けるのは、混入元の本命が環境変数(`AWS_SESSION_TOKEN=...`)とヘッダ
17
+ (`X-Amz-Security-Token: ...`)の大文字形のため。
18
+
19
+ 値部を「空白と引用符以外の連続」にするのは、`\S+` だと JSON 文字列の閉じ引用符まで飲み込むため。
20
+ `{"note":"aws_session_token=X"}` が不正 JSON になり、後段の JSON.parse が何も言わずに
21
+ 行配列へ切り替わる。
22
+ */
23
+ const KEYED_SECRETS = [
24
+ /(aws_secret_access_key\s*=\s*)[^\s"']+/gi,
25
+ /(aws_session_token\s*=\s*)[^\s"']+/gi,
26
+ /(x-amz-security-token:\s*)[^\s"']+/gi,
27
+ ];
28
+ /**
29
+ アクセスキー ID・シークレット・セッショントークンを [redacted] に置き換える。
30
+ */
31
+ export function redactSecrets(text) {
32
+ let redacted = text.replaceAll(ACCESS_KEY_ID, () => REDACTED);
33
+ for (const pattern of KEYED_SECRETS) {
34
+ redacted = redacted.replaceAll(pattern, (_match, key) => `${key}${REDACTED}`);
35
+ }
36
+ return redacted;
37
+ }
38
+ //# sourceMappingURL=redact.js.map
@@ -0,0 +1,20 @@
1
+ /**
2
+ `s3://<bucket>/<key>` 形式か。
3
+
4
+ 空白を含む値は拒否しない。引数は shell を介さず、1 要素 1 値で awscli へ渡る。
5
+ そのため `s3://b/k --recursive` のような値は「そういう名前のキー」として扱われ、
6
+ 別オプションには化けない(research R3)。
7
+ */
8
+ export declare function validateS3Uri(value: string): string | undefined;
9
+ /**
10
+ `s3 ls` の位置引数として妥当か(バケット一覧・prefix 指定の双方を許す)。
11
+ */
12
+ export declare function validateS3ListUri(value: string): string | undefined;
13
+ /**
14
+ 書き出し先のローカルパスとして妥当か。
15
+
16
+ `s3://…` を先に見るのは、絶対パスの検証だけでも弾けるが「相対パスだ」という案内になり、
17
+ 取得元と書き出し先を取り違えた利用者に誤った直し方を促すため。
18
+ */
19
+ export declare function validateLocalPath(value: string): string | undefined;
20
+ //# sourceMappingURL=s3-input.d.ts.map
@@ -0,0 +1,50 @@
1
+ // S3 の入力値(オブジェクト URI・一覧 URI・ローカル出力先)の書式検証。
2
+ //
3
+ // research R7 の方向制約は「入力の形で表現できない」ことで担保しているが、値そのものが
4
+ // 期待どおりの形かはここで見る。書式は contracts/mcp-tools.md の正規表現を逐語で写す。
5
+ //
6
+ // 戻り値は「入力名に続く述部」で、入力名と値の連結は arguments.ts が行う(`<name> <述部>: <value>`)。
7
+ // 述部に助詞まで含めるのは、`s3://…` のような欧文始まりの語で助詞との間に空白が要るかどうかが
8
+ // 語ごとに変わるため。文言の組み立てを 1 箇所に寄せると、規則を足したときに書式が割れない。
9
+ import path from 'node:path';
10
+ /**
11
+ `aws_s3_cp` の取得元。バケットとキーの双方を要求する(契約表 aws_s3_cp の s3-uri)。
12
+ */
13
+ const S3_OBJECT_URI = /^s3:\/\/[^/]+\/.+$/;
14
+ /**
15
+ `aws_s3_ls` の一覧対象。バケット一覧(`s3://`)と prefix 指定の双方を許す(契約表 aws_s3_ls)。
16
+ */
17
+ // eslint-disable-next-line security/detect-unsafe-regex -- 前半の [^/]* と後半の '/' 以降は排他で backtracking しない
18
+ const S3_LIST_URI = /^s3:\/\/[^/]*(?:\/.*)?$/;
19
+ /**
20
+ ローカルパスに紛れ込んだ S3 URI。大小文字を区別せずに見る。
21
+ */
22
+ const S3_SCHEME = /^s3:\/\//i;
23
+ /**
24
+ `s3://<bucket>/<key>` 形式か。
25
+
26
+ 空白を含む値は拒否しない。引数は shell を介さず、1 要素 1 値で awscli へ渡る。
27
+ そのため `s3://b/k --recursive` のような値は「そういう名前のキー」として扱われ、
28
+ 別オプションには化けない(research R3)。
29
+ */
30
+ export function validateS3Uri(value) {
31
+ return S3_OBJECT_URI.test(value) ? undefined : 'は s3://<bucket>/<key> の形式で指定してください';
32
+ }
33
+ /**
34
+ `s3 ls` の位置引数として妥当か(バケット一覧・prefix 指定の双方を許す)。
35
+ */
36
+ export function validateS3ListUri(value) {
37
+ return S3_LIST_URI.test(value) ? undefined : 'は s3://<bucket>[/<prefix>] の形式で指定してください';
38
+ }
39
+ /**
40
+ 書き出し先のローカルパスとして妥当か。
41
+
42
+ `s3://…` を先に見るのは、絶対パスの検証だけでも弾けるが「相対パスだ」という案内になり、
43
+ 取得元と書き出し先を取り違えた利用者に誤った直し方を促すため。
44
+ */
45
+ export function validateLocalPath(value) {
46
+ if (S3_SCHEME.test(value))
47
+ return 'はローカルの絶対パスで指定してください(s3:// は書き出し先にできません)';
48
+ return path.isAbsolute(value) ? undefined : 'は絶対パスで指定してください';
49
+ }
50
+ //# sourceMappingURL=s3-input.js.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ 抽出できた認証 URL とコードの組。片方だけでは利用者が認証を進められないため常に対で扱う。
3
+ */
4
+ export interface SsoLoginPrompt {
5
+ readonly verificationUrl: string;
6
+ readonly userCode: string;
7
+ /**
8
+ コードを自動入力する URL。これを開けばコード入力が要らない。未到達なら無い。
9
+ */
10
+ readonly verificationUrlComplete?: string;
11
+ }
12
+ /**
13
+ login-registry.ts が chunk 単位の再 parse を絞り込むのに使うため export する。
14
+ */
15
+ export declare const AUTOFILL_MARKER = "Alternatively, you may visit the following URL which will autofill the code upon loading:";
16
+ /**
17
+ 累積した出力から認証 URL とコードを取り出す。両方が揃うまでは undefined を返す。
18
+
19
+ 片方だけ返さないのは、URL だけを先に応答へ載せても利用者が認証を完了できないため。
20
+ autofill URL は揃っていれば載せるだけで、これを待って確定を遅らせることはしない。
21
+ */
22
+ export declare function parseSsoLoginOutput(text: string): SsoLoginPrompt | undefined;
23
+ //# sourceMappingURL=sso-output.d.ts.map
@@ -0,0 +1,70 @@
1
+ // `aws sso login --no-browser --use-device-code` の出力から認証 URL とコードを取り出す(research R5)。
2
+ //
3
+ // awscli はこの 2 値を構造化して返さないため、行の並びからの抽出が唯一の手段になる。
4
+ // 実 awscli 2.36.40 の stdout(採取日 2026-09-07)は次の形で、マーカー行の後に空行を挟んで
5
+ // 値が 1 行だけ置かれる。末尾にはコードを自動入力する URL がもう 1 本続く。
6
+ //
7
+ // Browser will not be automatically opened.
8
+ // Please visit the following URL:
9
+ //
10
+ // https://<start>.awsapps.com/start/#/device
11
+ //
12
+ // Then enter the code:
13
+ //
14
+ // ZHJW-WDMF
15
+ //
16
+ // Alternatively, you may visit the following URL which will autofill the code upon loading:
17
+ //
18
+ // `https://<start>.awsapps.com/start/#/device?user_code=ZHJW-WDMF`
19
+ //
20
+ // URL は各マーカー行より後ろの最初の 1 本だけを見る。末尾の autofill URL は
21
+ // 「コードを URL に載せた形」で、これを開けば利用者はコード入力を省ける(RFC 8628 の
22
+ // verification_uri_complete に相当する)。
23
+ //
24
+ // 子プロセスの出力は chunk 単位で届くので、呼び出し側は累積した stdout をそのつど渡し、
25
+ // URL とコードが揃う(= undefined でなくなる)まで待つ。autofill 行は両者より後に届き得るため
26
+ // 確定条件には入れない。遅れて届いた場合は、再度この関数を通せば補われる。
27
+ const URL_MARKER = 'Please visit the following URL:';
28
+ const CODE_MARKER = 'Then enter the code:';
29
+ /**
30
+ login-registry.ts が chunk 単位の再 parse を絞り込むのに使うため export する。
31
+ */
32
+ export const AUTOFILL_MARKER = 'Alternatively, you may visit the following URL which will autofill the code upon loading:';
33
+ /**
34
+ マーカー以降で最初に現れる URL 行。
35
+ */
36
+ const URL_LINE = /^https?:\/\/\S+$/;
37
+ /**
38
+ 認証コード行(`ZHJW-WDMF` のようなハイフン区切りの英数)。
39
+ */
40
+ const CODE_LINE = /^[\dA-Z]{4,}-[\dA-Z]{4,}$/;
41
+ /**
42
+ マーカー行より後で、pattern へ最初に一致する行を返す。
43
+
44
+ 行は trim してから見る。マーカーの比較を完全一致にするためと、CRLF が混在したときに
45
+ 末尾の `\r` で URL 行の `$` が外れるのを防ぐため。
46
+ */
47
+ function findAfterMarker(lines, marker, pattern) {
48
+ const markerIndex = lines.indexOf(marker);
49
+ return markerIndex === -1 ? undefined : lines.slice(markerIndex + 1).find(line => pattern.test(line));
50
+ }
51
+ /**
52
+ 累積した出力から認証 URL とコードを取り出す。両方が揃うまでは undefined を返す。
53
+
54
+ 片方だけ返さないのは、URL だけを先に応答へ載せても利用者が認証を完了できないため。
55
+ autofill URL は揃っていれば載せるだけで、これを待って確定を遅らせることはしない。
56
+ */
57
+ export function parseSsoLoginOutput(text) {
58
+ const lines = text.split('\n').map(line => line.trim());
59
+ const verificationUrl = findAfterMarker(lines, URL_MARKER, URL_LINE);
60
+ const userCode = findAfterMarker(lines, CODE_MARKER, CODE_LINE);
61
+ if (verificationUrl === undefined || userCode === undefined)
62
+ return undefined;
63
+ const verificationUrlComplete = findAfterMarker(lines, AUTOFILL_MARKER, URL_LINE);
64
+ return {
65
+ verificationUrl,
66
+ userCode,
67
+ ...(verificationUrlComplete !== undefined && { verificationUrlComplete }),
68
+ };
69
+ }
70
+ //# sourceMappingURL=sso-output.js.map
@@ -0,0 +1,104 @@
1
+ /**
2
+ 起動するコマンド。PATH 解決に委ねる(絶対パス指定はしない)。
3
+
4
+ SSO のログインレジストリ(infra/sso/login-registry.ts)も同じコマンドで spawn するため
5
+ export する。片方だけ絶対パス化するような差が入らないようにしておく。
6
+ */
7
+ export declare const AWSCLI_COMMAND = "aws";
8
+ /**
9
+ awscli 出力の上限(バイト、research R8)。
10
+
11
+ 応答へ載せる際の切り詰め(mcp/result.ts の limitOutput)と、本モジュールの捕捉上限が
12
+ 同じ値を基準にする。依存方向が infra → mcp を許さないため、定義は infra に置く。
13
+ */
14
+ export declare const OUTPUT_LIMIT_BYTES: number;
15
+ /**
16
+ 子プロセスの stdout / stderr を捕捉する上限(バイト)。超過分は捨てる。
17
+
18
+ research R8 の `filter-log-events` は既定 10,000 件で数 MiB になり得る。全件を溜めると
19
+ 同時実行数(4)と掛かってサーバの常駐メモリに直結するため、応答上限の 4 倍で打ち切る
20
+ (切り詰め後の応答に必要な分は必ず残る)。
21
+ */
22
+ export declare const CAPTURE_LIMIT_BYTES: number;
23
+ /**
24
+ 生存中の awscli 子プロセス全てへ SIGTERM を送る。
25
+
26
+ 終了処理(mcp/serve.ts の onShutdown)から呼ぶ。既に終了した子への kill は no-op で、
27
+ finalize 側が Set から外すため二重送出にはならない。
28
+ */
29
+ export declare function killRunningAwscli(): void;
30
+ /**
31
+ 子プロセスの実行結果。
32
+ */
33
+ export interface SpawnResult {
34
+ readonly exitCode: number | null;
35
+ readonly signal: NodeJS.Signals | null;
36
+ readonly stdout: string;
37
+ readonly stderr: string;
38
+ readonly timedOut: boolean;
39
+ /**
40
+ spawn 自体が失敗したときの errno(ENOENT 等)。成功時はキーごと無い。
41
+ */
42
+ readonly spawnErrorCode?: string;
43
+ }
44
+ export interface RunAwscliOptions {
45
+ readonly timeoutMs: number;
46
+ /**
47
+ 呼び出し元がキャンセルされたら子プロセスへ SIGTERM を送る(任意)。
48
+
49
+ MCP の tools/call は ToolCallContext.signal でキャンセルされる。配線しないと、クライアントが
50
+ 諦めたあとも awscli が孤児として動き続ける。
51
+ */
52
+ readonly signal?: AbortSignal | undefined;
53
+ }
54
+ /**
55
+ 捕捉した stderr と、spawn の error から得た追加行を 1 本に束ねる。
56
+ */
57
+ export declare function mergeStderr(captured: string, extra: string): string;
58
+ /**
59
+ 上限まで chunk を溜める入れ物。
60
+ */
61
+ export interface OutputCapture {
62
+ push(chunk: Readonly<Buffer>): void;
63
+ text(): string;
64
+ }
65
+ /**
66
+ CAPTURE_LIMIT_BYTES まで溜め、超過分は捨てる capture を作る。
67
+
68
+ SSO のログインレジストリ(infra/sso/login-registry.ts)も同じ上限で子プロセスの出力を
69
+ 溜めるため export する。上限が呼び出し元ごとに割れると、常駐メモリの見積もりが立たなくなる。
70
+ */
71
+ export declare function createCapture(): OutputCapture;
72
+ /**
73
+ `aws <args>` を起動し、CAPTURE_LIMIT_BYTES まで capture した stdout / stderr を返す。
74
+
75
+ timeout 到達・abort では SIGTERM を送り、`timedOut` / signal を結果へ載せる。
76
+ 起動 error は reject せず `exitCode: null` + `spawnErrorCode` に詰める。
77
+ そのため呼び出し側は、失敗種別の判定を 1 か所(classifyFailure)に集約できる。
78
+ */
79
+ export declare function runAwscli(arguments_: readonly string[], options: Readonly<RunAwscliOptions>): Promise<SpawnResult>;
80
+ /**
81
+ awscli の存在と版の確認結果。
82
+ */
83
+ export type AwscliProbe = {
84
+ readonly ok: true;
85
+ readonly version: string;
86
+ } | {
87
+ readonly ok: false;
88
+ readonly category: 'awscli-missing';
89
+ readonly message: string;
90
+ };
91
+ /**
92
+ awscli の存在と版を確認し、成功結果だけをプロセス内に記憶する。
93
+
94
+ 毎回 `aws --version` を起こすとツール 1 件あたりの子プロセスが 2 本になるため、
95
+ Promise 自体を memo する(並行呼び出しでも実行は 1 回)。
96
+ ただし失敗(未導入・版不足)を memo すると、利用者が awscli を導入してもサーバ再起動まで全ツールが awscli-missing を返し続ける。
97
+ そのため失敗時は memo を捨てて、次の呼び出しで再判定する。
98
+ */
99
+ export declare function ensureAwscli(): Promise<AwscliProbe>;
100
+ /**
101
+ memo を捨てる。テストが版・PATH を差し替えるときに使う。
102
+ */
103
+ export declare function resetAwscliProbe(): void;
104
+ //# sourceMappingURL=awscli.d.ts.map
@@ -0,0 +1,245 @@
1
+ // awscli の子プロセス実行。
2
+ //
3
+ // shell:false で起動するため、引数配列の要素はそのまま 1 値として awscli に渡る
4
+ // (連結や特殊文字による別コマンド委譲が構造的に起きない)。timeout / AbortSignal は
5
+ // SIGTERM で子を落とし、起動失敗(ENOENT 等)は reject せず spawnErrorCode に詰める。
6
+ import { spawn } from 'node:child_process';
7
+ /**
8
+ 起動するコマンド。PATH 解決に委ねる(絶対パス指定はしない)。
9
+
10
+ SSO のログインレジストリ(infra/sso/login-registry.ts)も同じコマンドで spawn するため
11
+ export する。片方だけ絶対パス化するような差が入らないようにしておく。
12
+ */
13
+ export const AWSCLI_COMMAND = 'aws';
14
+ /**
15
+ `aws --version` の待ち時間。実処理の timeout とは別枠。
16
+ */
17
+ const PROBE_TIMEOUT_MS = 15_000;
18
+ /**
19
+ 要求する最低版 [major, minor, patch](research R5: `--use-device-code` は awscli 2.22 以降)。
20
+ */
21
+ const MINIMUM_VERSION = [2, 22, 0];
22
+ /**
23
+ message に載せる最低版の表記。
24
+ */
25
+ const MINIMUM_VERSION_TEXT = MINIMUM_VERSION.join('.');
26
+ const VERSION_PATTERN = /aws-cli\/(\d+)\.(\d+)\.(\d+)/;
27
+ /**
28
+ awscli 出力の上限(バイト、research R8)。
29
+
30
+ 応答へ載せる際の切り詰め(mcp/result.ts の limitOutput)と、本モジュールの捕捉上限が
31
+ 同じ値を基準にする。依存方向が infra → mcp を許さないため、定義は infra に置く。
32
+ */
33
+ export const OUTPUT_LIMIT_BYTES = 256 * 1024;
34
+ /**
35
+ 子プロセスの stdout / stderr を捕捉する上限(バイト)。超過分は捨てる。
36
+
37
+ research R8 の `filter-log-events` は既定 10,000 件で数 MiB になり得る。全件を溜めると
38
+ 同時実行数(4)と掛かってサーバの常駐メモリに直結するため、応答上限の 4 倍で打ち切る
39
+ (切り詰め後の応答に必要な分は必ず残る)。
40
+ */
41
+ export const CAPTURE_LIMIT_BYTES = OUTPUT_LIMIT_BYTES * 4;
42
+ /**
43
+ 生存中の awscli 子プロセス。
44
+
45
+ runAwscli が spawn 直後に足し、finalize(close / error のどちらか先)で外す。
46
+ サーバが SIGTERM で畳まれるときは親が先に消える。
47
+ そのため、ここを辿って子へも SIGTERM を送らないと、子が PPID 1 へ再親付けされて動き続ける。
48
+ timeout を張っていた親が居なくなるので、打ち切りも働かない。
49
+ */
50
+ const runningChildren = new Set();
51
+ /**
52
+ 生存中の awscli 子プロセス全てへ SIGTERM を送る。
53
+
54
+ 終了処理(mcp/serve.ts の onShutdown)から呼ぶ。既に終了した子への kill は no-op で、
55
+ finalize 側が Set から外すため二重送出にはならない。
56
+ */
57
+ export function killRunningAwscli() {
58
+ for (const child of runningChildren)
59
+ child.kill('SIGTERM');
60
+ }
61
+ function errorCodeOf(error) {
62
+ return 'code' in error && typeof error.code === 'string' ? error.code : undefined;
63
+ }
64
+ /**
65
+ 捕捉した stderr と、spawn の error から得た追加行を 1 本に束ねる。
66
+ */
67
+ export function mergeStderr(captured, extra) {
68
+ if (extra === '')
69
+ return captured;
70
+ if (captured === '')
71
+ return extra;
72
+ return `${captured}\n${extra}`;
73
+ }
74
+ /**
75
+ CAPTURE_LIMIT_BYTES まで溜め、超過分は捨てる capture を作る。
76
+
77
+ SSO のログインレジストリ(infra/sso/login-registry.ts)も同じ上限で子プロセスの出力を
78
+ 溜めるため export する。上限が呼び出し元ごとに割れると、常駐メモリの見積もりが立たなくなる。
79
+ */
80
+ export function createCapture() {
81
+ const chunks = [];
82
+ let captured = 0;
83
+ return {
84
+ push(chunk) {
85
+ const room = CAPTURE_LIMIT_BYTES - captured;
86
+ if (room <= 0)
87
+ return;
88
+ const copied = Buffer.from(chunk.subarray(0, Math.min(room, chunk.length)));
89
+ captured += copied.length;
90
+ chunks.push(copied);
91
+ },
92
+ text() {
93
+ return Buffer.concat(chunks).toString('utf8');
94
+ },
95
+ };
96
+ }
97
+ /**
98
+ `aws <args>` を起動し、CAPTURE_LIMIT_BYTES まで capture した stdout / stderr を返す。
99
+
100
+ timeout 到達・abort では SIGTERM を送り、`timedOut` / signal を結果へ載せる。
101
+ 起動 error は reject せず `exitCode: null` + `spawnErrorCode` に詰める。
102
+ そのため呼び出し側は、失敗種別の判定を 1 か所(classifyFailure)に集約できる。
103
+ */
104
+ export async function runAwscli(arguments_,
105
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- AbortSignal は内部状態を持つ
106
+ options) {
107
+ return new Promise((resolve) => {
108
+ const stdoutCapture = createCapture();
109
+ const stderrCapture = createCapture();
110
+ let isTimedOut = false;
111
+ let isSettled = false;
112
+ const child = spawn(AWSCLI_COMMAND, [...arguments_], {
113
+ stdio: ['ignore', 'pipe', 'pipe'],
114
+ shell: false,
115
+ });
116
+ runningChildren.add(child);
117
+ const timer = setTimeout(() => {
118
+ isTimedOut = true;
119
+ child.kill('SIGTERM');
120
+ }, options.timeoutMs);
121
+ timer.unref();
122
+ // abort 済みの signal に後からリスナを足しても発火しないため、現在値を先に見てから購読する。
123
+ const onAbort = () => {
124
+ child.kill('SIGTERM');
125
+ };
126
+ if (options.signal?.aborted === true)
127
+ onAbort();
128
+ else
129
+ options.signal?.addEventListener('abort', onAbort, { once: true });
130
+ child.stdout.on('data', (chunk) => {
131
+ stdoutCapture.push(chunk);
132
+ });
133
+ child.stderr.on('data', (chunk) => {
134
+ stderrCapture.push(chunk);
135
+ });
136
+ const finalize = (exitCode, signal, extraStderr, spawnErrorCode) => {
137
+ if (isSettled)
138
+ return;
139
+ isSettled = true;
140
+ runningChildren.delete(child);
141
+ clearTimeout(timer);
142
+ options.signal?.removeEventListener('abort', onAbort);
143
+ // ENOENT 等で 'error' の直後に 'close' も発火する Node の仕様に対し、二重 finalize は
144
+ // settled で抑止している。稀に child が生存したまま残る場合への防御で kill しておく。
145
+ // 通常の close 後もここを通る(Node 24 の確認で child.killed は false のまま)。
146
+ // ただし _handle が既に null で kill() は no-op のため無害。
147
+ // catch は防御網で、通常は到達しない。
148
+ /* c8 ignore start -- catch 節(既に終了済みの kill が throw する場合)は観測されない防御網 */
149
+ if (!child.killed) {
150
+ try {
151
+ child.kill();
152
+ }
153
+ catch {
154
+ // 既に終了済みの場合の防御。SpawnResult には影響しないので握り潰す。
155
+ }
156
+ }
157
+ /* c8 ignore stop */
158
+ resolve({
159
+ exitCode,
160
+ signal,
161
+ stdout: stdoutCapture.text(),
162
+ stderr: mergeStderr(stderrCapture.text(), extraStderr),
163
+ timedOut: isTimedOut,
164
+ ...(spawnErrorCode !== undefined && { spawnErrorCode }),
165
+ });
166
+ };
167
+ child.on('error', (error) => {
168
+ // eslint-disable-next-line unicorn/no-null -- exitCode / signal は SpawnResult 契約で null
169
+ finalize(null, null, error.message, errorCodeOf(error));
170
+ });
171
+ child.on('close', (code, signal) => {
172
+ finalize(code, signal, '');
173
+ });
174
+ });
175
+ }
176
+ /**
177
+ 確認結果の memo。外から差し替えられるよう、プロパティを持つ入れ物にしている。
178
+ */
179
+ const probeCache = { pending: undefined };
180
+ /**
181
+ 検出した版が最低版以上か。
182
+
183
+ major / minor / patch を 1 つの数へ畳む比較は minor / patch が 1000 以上で桁が衝突するため、
184
+ 上位から順に見るタプル比較にする。
185
+ */
186
+ function isAtLeastMinimum(major, minor, patch) {
187
+ const [minimumMajor, minimumMinor, minimumPatch] = MINIMUM_VERSION;
188
+ if (major !== minimumMajor)
189
+ return major > minimumMajor;
190
+ if (minor !== minimumMinor)
191
+ return minor > minimumMinor;
192
+ return patch >= minimumPatch;
193
+ }
194
+ async function probeAwscli() {
195
+ const result = await runAwscli(['--version'], { timeoutMs: PROBE_TIMEOUT_MS });
196
+ if (result.spawnErrorCode === 'ENOENT') {
197
+ return {
198
+ ok: false,
199
+ category: 'awscli-missing',
200
+ message: `awscli が見つかりません。awscli v2 (${MINIMUM_VERSION_TEXT} 以上) を導入して PATH を通してください`,
201
+ };
202
+ }
203
+ const matched = VERSION_PATTERN.exec(`${result.stdout}\n${result.stderr}`) ?? undefined;
204
+ if (matched === undefined) {
205
+ return {
206
+ ok: false,
207
+ category: 'awscli-missing',
208
+ message: `awscli の版を判定できませんでした: ${result.stdout.trim()}`,
209
+ };
210
+ }
211
+ const major = Number(matched[1]);
212
+ const minor = Number(matched[2]);
213
+ const patch = Number(matched[3]);
214
+ const version = `${String(major)}.${String(minor)}.${String(patch)}`;
215
+ return isAtLeastMinimum(major, minor, patch)
216
+ ? { ok: true, version }
217
+ : {
218
+ ok: false,
219
+ category: 'awscli-missing',
220
+ message: `awscli ${MINIMUM_VERSION_TEXT} 以上が必要です(検出: ${version})`,
221
+ };
222
+ }
223
+ /**
224
+ awscli の存在と版を確認し、成功結果だけをプロセス内に記憶する。
225
+
226
+ 毎回 `aws --version` を起こすとツール 1 件あたりの子プロセスが 2 本になるため、
227
+ Promise 自体を memo する(並行呼び出しでも実行は 1 回)。
228
+ ただし失敗(未導入・版不足)を memo すると、利用者が awscli を導入してもサーバ再起動まで全ツールが awscli-missing を返し続ける。
229
+ そのため失敗時は memo を捨てて、次の呼び出しで再判定する。
230
+ */
231
+ export async function ensureAwscli() {
232
+ const pending = probeCache.pending ?? probeAwscli();
233
+ probeCache.pending = pending;
234
+ const resolved = await pending;
235
+ if (!resolved.ok && probeCache.pending === pending)
236
+ probeCache.pending = undefined;
237
+ return resolved;
238
+ }
239
+ /**
240
+ memo を捨てる。テストが版・PATH を差し替えるときに使う。
241
+ */
242
+ export function resetAwscliProbe() {
243
+ probeCache.pending = undefined;
244
+ }
245
+ //# sourceMappingURL=awscli.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ promise の完了を待ち、失敗は捨てる。
3
+ 後始末のように、失敗しても呼び出し側の結果に影響しない処理で使う。
4
+ */
5
+ export declare function ignoreRejection(promise: PromiseLike<unknown>): Promise<void>;
6
+ //# sourceMappingURL=ignore-rejection.d.ts.map