kairos-lang 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +260 -0
- package/dist/ast.d.ts +142 -0
- package/dist/ast.js +1 -0
- package/dist/cli.d.ts +55 -0
- package/dist/cli.js +309 -0
- package/dist/eval.d.ts +438 -0
- package/dist/eval.js +3198 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +64 -0
- package/dist/lexer.d.ts +33 -0
- package/dist/lexer.js +171 -0
- package/dist/parser.d.ts +11 -0
- package/dist/parser.js +644 -0
- package/dist/tz.d.ts +57 -0
- package/dist/tz.js +216 -0
- package/package.json +63 -0
- package/stdlib/fiscal.kairos +12 -0
- package/stdlib/gregorian.kairos +36 -0
- package/stdlib/isoweek.kairos +16 -0
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import type { ExternalResolver } from './index.ts';
|
|
3
|
+
import type { ResultAnnotation, CoverageEntry } from './eval.ts';
|
|
4
|
+
/** CLI の結果表面。人間表示(renderHuman)と --json はこの一つの器から出す——両表示の乖離を封じる */
|
|
5
|
+
export interface CliReport {
|
|
6
|
+
command: 'list' | 'next';
|
|
7
|
+
version: string;
|
|
8
|
+
tz: string;
|
|
9
|
+
from: string;
|
|
10
|
+
to: string;
|
|
11
|
+
requested?: number;
|
|
12
|
+
found?: number;
|
|
13
|
+
horizonYears?: number;
|
|
14
|
+
results: {
|
|
15
|
+
source: string;
|
|
16
|
+
dates: string[];
|
|
17
|
+
points: number[];
|
|
18
|
+
annotations: ResultAnnotation[];
|
|
19
|
+
}[];
|
|
20
|
+
coverage: CoverageEntry[];
|
|
21
|
+
warnings: string[];
|
|
22
|
+
}
|
|
23
|
+
/** 実行 tz の今日(市民日付)。時計読みは CLI 境界のみ——言語・評価は不変に決定的 */
|
|
24
|
+
export declare function todayIn(tz: string): string;
|
|
25
|
+
interface CmdOpts {
|
|
26
|
+
from: string;
|
|
27
|
+
to?: string;
|
|
28
|
+
tz?: string;
|
|
29
|
+
resolve?: ExternalResolver;
|
|
30
|
+
}
|
|
31
|
+
/** --supply の静的束から external 解決子を作る(ADR-46 の RunOptions.resolve へ渡す形)。
|
|
32
|
+
* 形: {キー: {dates|instants, covering, asof [, labels]}}——キーは束縛名または "premise.束縛名"
|
|
33
|
+
* (premise 修飾が優先。source は named-arg 上書きで多対一になり得るためキーにしない)。
|
|
34
|
+
* ここでは JSON の形だけを検査する——覆域の包含・昇順・実在日などの供給契約 12 種は
|
|
35
|
+
* 評価器側の検査(external の統治)がそのまま掛かる。 */
|
|
36
|
+
export declare function supplyResolver(raw: unknown, origin: string): ExternalResolver;
|
|
37
|
+
/** list: 範囲 [from, to) の全発火+註釈+被覆サマリ */
|
|
38
|
+
export declare function cmdList(source: string, o: CmdOpts & {
|
|
39
|
+
to: string;
|
|
40
|
+
}): CliReport;
|
|
41
|
+
/** next: from 以降の次の N 発火。窓を 1 年から倍々に広げて探索し(上限=horizon 年)、
|
|
42
|
+
* 見つかったら [from, 最終発火日の翌日) で確定再評価——註釈・残走路が答えの範囲と整合する。
|
|
43
|
+
* 地平線まで探して不足なら見つかった分を返す(found < requested)。 */
|
|
44
|
+
export declare function cmdNext(source: string, o: CmdOpts & {
|
|
45
|
+
n: number;
|
|
46
|
+
horizonYears: number;
|
|
47
|
+
}): CliReport;
|
|
48
|
+
/** 表示言語(--lang en で定型出力の枠組みだけ英語化。評価器メッセージ=エラー・註釈文は
|
|
49
|
+
* 日本語が正のまま・--json は言語中立——線引きは en/playground と同一) */
|
|
50
|
+
export type CliLang = 'ja' | 'en';
|
|
51
|
+
/** 人間向け表示(stdout 行列)。--json と同じ CliReport から出す */
|
|
52
|
+
export declare function renderHuman(rep: CliReport, lang?: CliLang): string[];
|
|
53
|
+
/** 入口(SEA ビルドのエントリスタブからも呼ぶ)。戻り値=終了コード */
|
|
54
|
+
export declare function main(argv: string[]): number;
|
|
55
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Kairos CLI — サブコマンド: list(範囲の点列)・next(次の N 発火)
|
|
3
|
+
// 使い方(kairos = node src/cli.ts。配布名は 1.0 で npm bin / SEA に載せる):
|
|
4
|
+
// kairos list [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--tz Zone] [--json] <file.kairos>
|
|
5
|
+
// kairos next [-n 件数] [--from YYYY-MM-DD] [--horizon 年数] [--tz Zone] [--json] <file.kairos>
|
|
6
|
+
// サブコマンド省略時(先頭引数がファイル)は list——旧形式の実行例を全て生かす後方互換。
|
|
7
|
+
// 終了コード: 0=成功・1=エラー・2=next が地平線内に要求件数を見つけられず(部分結果は表示する)。
|
|
8
|
+
// external() は --supply <file.json> の静的束で解決できる(supplyResolver → RunOptions.resolve)。
|
|
9
|
+
// --supply 無しでの解決は供給エラー(ADR-46 の既定どおり)。
|
|
10
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
11
|
+
import { parseArgs } from 'node:util';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { run, formatAnnotation, KairosError, SupplyError } from "./index.js";
|
|
14
|
+
// 実装版(JSON 出力の版規律)。SEA 束ね時は build 側でリテラルへ差し替え(index.ts の stdlib と同型)
|
|
15
|
+
const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
16
|
+
/** 実行 tz の今日(市民日付)。時計読みは CLI 境界のみ——言語・評価は不変に決定的 */
|
|
17
|
+
export function todayIn(tz) {
|
|
18
|
+
return new Intl.DateTimeFormat('en-CA', { timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date());
|
|
19
|
+
}
|
|
20
|
+
// 日付字面の算術(純グレゴリオ・Date.UTC の正規化に委ねる。2/29+1y 等は run() 側の
|
|
21
|
+
// civilDayStart が翌日に正規化=評価範囲の端としては安全)
|
|
22
|
+
const partsOf = (s) => {
|
|
23
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
|
|
24
|
+
if (!m)
|
|
25
|
+
throw new KairosError(`日付は YYYY-MM-DD: ${s}`);
|
|
26
|
+
return [+m[1], +m[2], +m[3]];
|
|
27
|
+
};
|
|
28
|
+
const addYears = (s, k) => {
|
|
29
|
+
const [y, mo, d] = partsOf(s);
|
|
30
|
+
return `${String(y + k).padStart(4, '0')}-${String(mo).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
|
|
31
|
+
};
|
|
32
|
+
const addDays = (s, n) => {
|
|
33
|
+
const [y, mo, d] = partsOf(s);
|
|
34
|
+
return new Date(Date.UTC(y, mo - 1, d + n)).toISOString().slice(0, 10);
|
|
35
|
+
};
|
|
36
|
+
/** --supply の静的束から external 解決子を作る(ADR-46 の RunOptions.resolve へ渡す形)。
|
|
37
|
+
* 形: {キー: {dates|instants, covering, asof [, labels]}}——キーは束縛名または "premise.束縛名"
|
|
38
|
+
* (premise 修飾が優先。source は named-arg 上書きで多対一になり得るためキーにしない)。
|
|
39
|
+
* ここでは JSON の形だけを検査する——覆域の包含・昇順・実在日などの供給契約 12 種は
|
|
40
|
+
* 評価器側の検査(external の統治)がそのまま掛かる。 */
|
|
41
|
+
export function supplyResolver(raw, origin) {
|
|
42
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
43
|
+
throw new KairosError(`--supply は {束縛名: {dates|instants, covering, asof}} の JSON オブジェクト: ${origin}`);
|
|
44
|
+
}
|
|
45
|
+
const table = new Map();
|
|
46
|
+
for (const [key, v] of Object.entries(raw)) {
|
|
47
|
+
const where = `--supply ${origin} の "${key}"`;
|
|
48
|
+
if (typeof v !== 'object' || v === null || Array.isArray(v)) {
|
|
49
|
+
throw new KairosError(`${where} は {dates|instants, covering, asof} のオブジェクト`);
|
|
50
|
+
}
|
|
51
|
+
const e = v;
|
|
52
|
+
const hasDates = 'dates' in e, hasInstants = 'instants' in e;
|
|
53
|
+
if (hasDates === hasInstants) {
|
|
54
|
+
throw new KairosError(`${where}: dates / instants はどちらか一方(wire は宣言の kind と評価器が照合する)`);
|
|
55
|
+
}
|
|
56
|
+
if (typeof e.covering !== 'string' || e.covering === '')
|
|
57
|
+
throw new KairosError(`${where}: covering(文字列)が必須`);
|
|
58
|
+
if (typeof e.asof !== 'string' || e.asof === '')
|
|
59
|
+
throw new KairosError(`${where}: asof(文字列)が必須`);
|
|
60
|
+
if (hasDates && !(Array.isArray(e.dates) && e.dates.every(d => typeof d === 'string'))) {
|
|
61
|
+
throw new KairosError(`${where}: dates は "YYYY-MM-DD" の文字列配列`);
|
|
62
|
+
}
|
|
63
|
+
if (hasInstants && !(Array.isArray(e.instants) && e.instants.every(n => typeof n === 'number'))) {
|
|
64
|
+
throw new KairosError(`${where}: instants は epoch ミリ秒の数値配列(CliReport の points と同じ規約)`);
|
|
65
|
+
}
|
|
66
|
+
if ('labels' in e && !(Array.isArray(e.labels) && e.labels.every(l => typeof l === 'string'))) {
|
|
67
|
+
throw new KairosError(`${where}: labels は文字列配列`);
|
|
68
|
+
}
|
|
69
|
+
table.set(key, {
|
|
70
|
+
...(hasDates ? { dates: e.dates } : { instants: e.instants }),
|
|
71
|
+
covering: e.covering,
|
|
72
|
+
asof: e.asof,
|
|
73
|
+
...('labels' in e ? { labels: e.labels } : {}),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return (premise, binding) => {
|
|
77
|
+
const hit = table.get(`${premise}.${binding}`) ?? table.get(binding);
|
|
78
|
+
if (!hit) {
|
|
79
|
+
throw new SupplyError(`供給エラー: --supply ${origin} に ${binding} がない`
|
|
80
|
+
+ `(キーは束縛名または "premise.束縛名"——ここでは "${premise}.${binding}")`);
|
|
81
|
+
}
|
|
82
|
+
return hit;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function toReport(command, r, o, next) {
|
|
86
|
+
return {
|
|
87
|
+
command,
|
|
88
|
+
version: VERSION,
|
|
89
|
+
tz: o.tz ?? 'Asia/Tokyo',
|
|
90
|
+
from: o.from,
|
|
91
|
+
to: o.to,
|
|
92
|
+
...(next ? { requested: next.requested, found: next.found, horizonYears: next.horizonYears } : {}),
|
|
93
|
+
results: r.results.map(res => ({
|
|
94
|
+
source: res.source, dates: res.dates, points: res.points, annotations: res.annotations,
|
|
95
|
+
})),
|
|
96
|
+
coverage: r.coverage,
|
|
97
|
+
warnings: r.warnings,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** list: 範囲 [from, to) の全発火+註釈+被覆サマリ */
|
|
101
|
+
export function cmdList(source, o) {
|
|
102
|
+
return toReport('list', run(source, { from: o.from, to: o.to,
|
|
103
|
+
...(o.tz ? { tz: o.tz } : {}), ...(o.resolve ? { resolve: o.resolve } : {}) }), o);
|
|
104
|
+
}
|
|
105
|
+
/** next: from 以降の次の N 発火。窓を 1 年から倍々に広げて探索し(上限=horizon 年)、
|
|
106
|
+
* 見つかったら [from, 最終発火日の翌日) で確定再評価——註釈・残走路が答えの範囲と整合する。
|
|
107
|
+
* 地平線まで探して不足なら見つかった分を返す(found < requested)。 */
|
|
108
|
+
export function cmdNext(source, o) {
|
|
109
|
+
const meta = { requested: o.n, found: 0, horizonYears: o.horizonYears };
|
|
110
|
+
const runOpts = { ...(o.tz ? { tz: o.tz } : {}), ...(o.resolve ? { resolve: o.resolve } : {}) };
|
|
111
|
+
let years = Math.min(1, o.horizonYears);
|
|
112
|
+
for (;;) {
|
|
113
|
+
const to = addYears(o.from, years);
|
|
114
|
+
const r = run(source, { from: o.from, to, ...runOpts });
|
|
115
|
+
if (r.results.length === 0)
|
|
116
|
+
throw new KairosError('本体式がない');
|
|
117
|
+
if (r.results.length > 1)
|
|
118
|
+
throw new KairosError(`next は本体式 1 つのファイル向け(${r.results.length} 式ある——list を使うか式を 1 つに)`);
|
|
119
|
+
const found = r.results[0];
|
|
120
|
+
if (found.dates.length >= o.n) {
|
|
121
|
+
// 確定再評価: 右端=N 発火目の翌市民日(同日複数瞬間の切り落としは slice で)
|
|
122
|
+
const toFinal = addDays(found.dates[o.n - 1].slice(0, 10), 1);
|
|
123
|
+
const rf = run(source, { from: o.from, to: toFinal, ...runOpts });
|
|
124
|
+
const res = rf.results[0];
|
|
125
|
+
res.dates = res.dates.slice(0, o.n);
|
|
126
|
+
res.points = res.points.slice(0, o.n);
|
|
127
|
+
return toReport('next', rf, { ...o, to: toFinal }, { ...meta, found: o.n });
|
|
128
|
+
}
|
|
129
|
+
if (years >= o.horizonYears) {
|
|
130
|
+
return toReport('next', r, { ...o, to }, { ...meta, found: found.dates.length });
|
|
131
|
+
}
|
|
132
|
+
years = Math.min(years * 2, o.horizonYears);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const CLI_STRINGS = {
|
|
136
|
+
ja: {
|
|
137
|
+
exprHead: (i, n) => `# 式 ${i}(${n} 件)`,
|
|
138
|
+
coverageHead: '# 被覆サマリ',
|
|
139
|
+
concluded: '(完結主張)',
|
|
140
|
+
runway: (d) => `残走路 ${d === null ? '∞' : `${d} 日`}`,
|
|
141
|
+
warning: (w) => `警告: ${w}`,
|
|
142
|
+
horizonShort: (rep) => `⚠ 地平線 ${rep.horizonYears} 年以内の発火は ${rep.found} 件(要求 ${rep.requested} 件)`,
|
|
143
|
+
},
|
|
144
|
+
en: {
|
|
145
|
+
exprHead: (i, n) => `# expression ${i} (${n} point${n === 1 ? '' : 's'})`,
|
|
146
|
+
coverageHead: '# coverage summary',
|
|
147
|
+
concluded: '(concluded)',
|
|
148
|
+
runway: (d) => `runway ${d === null ? '∞' : `${d} day${d === 1 ? '' : 's'}`}`,
|
|
149
|
+
warning: (w) => `warning: ${w}`,
|
|
150
|
+
horizonShort: (rep) => `⚠ only ${rep.found} firing(s) within the ${rep.horizonYears}-year horizon (requested ${rep.requested})`,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
/** 人間向け表示(stdout 行列)。--json と同じ CliReport から出す */
|
|
154
|
+
export function renderHuman(rep, lang = 'ja') {
|
|
155
|
+
const T = CLI_STRINGS[lang];
|
|
156
|
+
const out = [];
|
|
157
|
+
rep.results.forEach((res, i) => {
|
|
158
|
+
if (rep.results.length > 1)
|
|
159
|
+
out.push(T.exprHead(i + 1, res.dates.length));
|
|
160
|
+
for (const d of res.dates)
|
|
161
|
+
out.push(d);
|
|
162
|
+
// 区間註釈(ADR-37 判断 5/7 (a)): 結果の後に表示——対処は呼び手の責務(判定は外部)
|
|
163
|
+
for (const a of res.annotations)
|
|
164
|
+
out.push(`# ⚠ ${formatAnnotation(a)}`);
|
|
165
|
+
});
|
|
166
|
+
// 被覆サマリ(ADR-37 判断 7 (b)): クリップしない・完結主張も常時表示
|
|
167
|
+
if (rep.coverage.length > 0) {
|
|
168
|
+
out.push(T.coverageHead);
|
|
169
|
+
for (const c of rep.coverage) {
|
|
170
|
+
out.push(`# ${c.source} covering ${c.covering}${c.asof ? ` asof ${c.asof}` : ''}`
|
|
171
|
+
+ `${c.concluded ? T.concluded : ''}`
|
|
172
|
+
+ ` ${T.runway(c.runwayDays)}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
const USAGE_JA = `使い方(kairos = node src/cli.ts):
|
|
178
|
+
kairos list [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--tz Zone] [--supply data.json] [--json] [--lang en] <file.kairos>
|
|
179
|
+
範囲 [from, to) の全発火・区間註釈・被覆サマリ(既定: 実行 tz の今日から 1 年)
|
|
180
|
+
kairos next [-n 件数] [--from YYYY-MM-DD] [--horizon 年数] [--tz Zone] [--supply data.json] [--json] [--lang en] <file.kairos>
|
|
181
|
+
from 以降の次の N 発火(既定: n=1・from=今日・地平線 10 年。本体式 1 つのファイル向け)
|
|
182
|
+
--supply: external の解決値を静的束で渡す——{束縛名: {dates|instants, covering, asof [, labels]}}
|
|
183
|
+
--lang en: 定型出力の枠組みを英語表示(エラー・註釈文は日本語が正のまま・--json は言語中立)
|
|
184
|
+
サブコマンド省略時は list・--version で実装版。終了コード: 0=成功・1=エラー・2=next が地平線内に要求件数未達`;
|
|
185
|
+
const USAGE_EN = `Usage (kairos = node src/cli.ts):
|
|
186
|
+
kairos list [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--tz Zone] [--supply data.json] [--json] [--lang en] <file.kairos>
|
|
187
|
+
All firings in [from, to) plus interval annotations and the coverage summary
|
|
188
|
+
(default: one year from today in the execution tz)
|
|
189
|
+
kairos next [-n count] [--from YYYY-MM-DD] [--horizon years] [--tz Zone] [--supply data.json] [--json] [--lang en] <file.kairos>
|
|
190
|
+
The next N firings at or after from (default: n=1, from=today, horizon 10 years;
|
|
191
|
+
intended for files with a single body expression)
|
|
192
|
+
--supply: static bundle resolving external() — {binding: {dates|instants, covering, asof [, labels]}}
|
|
193
|
+
--lang en: English framing for the human-readable output. Evaluator messages (errors and
|
|
194
|
+
annotation texts) stay in Japanese — the implementation's canonical output language;
|
|
195
|
+
--json output is language-neutral.
|
|
196
|
+
Without a subcommand, list is assumed. --version prints the implementation version.
|
|
197
|
+
Exit codes: 0=success, 1=error, 2=next found fewer firings than requested within the horizon`;
|
|
198
|
+
/** USAGE の言語選択——パース失敗経路でも使えるよう argv の素朴な走査で決める */
|
|
199
|
+
const pickUsage = (argv) => argv.includes('--lang') && argv[argv.indexOf('--lang') + 1] === 'en' ? USAGE_EN : USAGE_JA;
|
|
200
|
+
const OPTS = {
|
|
201
|
+
list: {
|
|
202
|
+
from: { type: 'string' }, to: { type: 'string' },
|
|
203
|
+
tz: { type: 'string' }, supply: { type: 'string' }, json: { type: 'boolean' },
|
|
204
|
+
lang: { type: 'string' },
|
|
205
|
+
},
|
|
206
|
+
next: {
|
|
207
|
+
n: { type: 'string', short: 'n' }, from: { type: 'string' }, horizon: { type: 'string' },
|
|
208
|
+
tz: { type: 'string' }, supply: { type: 'string' }, json: { type: 'boolean' },
|
|
209
|
+
lang: { type: 'string' },
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
const posInt = (s, name) => {
|
|
213
|
+
if (!/^\d+$/.test(s) || +s < 1)
|
|
214
|
+
throw new KairosError(`${name} は正の整数: ${s}`);
|
|
215
|
+
return +s;
|
|
216
|
+
};
|
|
217
|
+
/** 入口(SEA ビルドのエントリスタブからも呼ぶ)。戻り値=終了コード */
|
|
218
|
+
export function main(argv) {
|
|
219
|
+
if (argv[0] === '--version' || argv[0] === '-v') { // 配布バイナリの身元確認(単体で 0 終了)
|
|
220
|
+
console.log(VERSION);
|
|
221
|
+
return 0;
|
|
222
|
+
}
|
|
223
|
+
let cmd;
|
|
224
|
+
let rest = argv;
|
|
225
|
+
if (argv[0] === 'list' || argv[0] === 'next') {
|
|
226
|
+
cmd = argv[0];
|
|
227
|
+
rest = argv.slice(1);
|
|
228
|
+
}
|
|
229
|
+
else if (argv.length > 0 && !argv[0].startsWith('-')) {
|
|
230
|
+
cmd = 'list'; // 旧形式: kairos <file.kairos> --from … --to …
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
console.error(pickUsage(argv));
|
|
234
|
+
return 1;
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
const { values, positionals } = parseArgs({ args: rest, options: OPTS[cmd], allowPositionals: true, strict: true });
|
|
238
|
+
const langValue = values.lang ?? 'ja';
|
|
239
|
+
if (langValue !== 'ja' && langValue !== 'en') {
|
|
240
|
+
throw new KairosError(`--lang は ja または en: ${langValue}`);
|
|
241
|
+
}
|
|
242
|
+
const lang = langValue;
|
|
243
|
+
const T = CLI_STRINGS[lang];
|
|
244
|
+
if (positionals.length !== 1)
|
|
245
|
+
throw new KairosError('ファイルを 1 つ指定する');
|
|
246
|
+
const source = readFileSync(positionals[0], 'utf8');
|
|
247
|
+
const tz = values.tz;
|
|
248
|
+
const from = values.from ?? todayIn(tz ?? 'Asia/Tokyo');
|
|
249
|
+
const supplyPath = values.supply;
|
|
250
|
+
let resolve;
|
|
251
|
+
if (supplyPath) {
|
|
252
|
+
const text = readFileSync(supplyPath, 'utf8');
|
|
253
|
+
let json;
|
|
254
|
+
try {
|
|
255
|
+
json = JSON.parse(text);
|
|
256
|
+
}
|
|
257
|
+
catch (e) {
|
|
258
|
+
throw new KairosError(`--supply の JSON が壊れている(${supplyPath}): ${e.message}`);
|
|
259
|
+
}
|
|
260
|
+
resolve = supplyResolver(json, supplyPath);
|
|
261
|
+
}
|
|
262
|
+
let rep;
|
|
263
|
+
if (cmd === 'list') {
|
|
264
|
+
const to = values.to ?? addYears(from, 1);
|
|
265
|
+
rep = cmdList(source, { from, to, ...(tz ? { tz } : {}), ...(resolve ? { resolve } : {}) });
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
const n = posInt(values.n ?? '1', '-n');
|
|
269
|
+
const horizonYears = posInt(values.horizon ?? '10', '--horizon');
|
|
270
|
+
rep = cmdNext(source, { from, n, horizonYears, ...(tz ? { tz } : {}),
|
|
271
|
+
...(resolve ? { resolve } : {}) });
|
|
272
|
+
}
|
|
273
|
+
if (values.json)
|
|
274
|
+
console.log(JSON.stringify(rep, null, 2));
|
|
275
|
+
else
|
|
276
|
+
for (const line of renderHuman(rep, lang))
|
|
277
|
+
console.log(line);
|
|
278
|
+
for (const w of rep.warnings)
|
|
279
|
+
console.error(T.warning(w));
|
|
280
|
+
if (rep.command === 'next' && rep.found < rep.requested) {
|
|
281
|
+
console.error(T.horizonShort(rep));
|
|
282
|
+
return 2;
|
|
283
|
+
}
|
|
284
|
+
return 0;
|
|
285
|
+
}
|
|
286
|
+
catch (e) {
|
|
287
|
+
console.error(String(e instanceof Error ? e.message : e));
|
|
288
|
+
if (e instanceof Error && 'code' in e && String(e.code).startsWith('ERR_PARSE_ARGS')) {
|
|
289
|
+
console.error(pickUsage(argv)); // 引数の誤り(未知フラグ等)には使い方を添える
|
|
290
|
+
}
|
|
291
|
+
return 1;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// vitest からの import では実行しない(テストは cmdList/cmdNext/renderHuman を直接呼ぶ)。
|
|
295
|
+
// npm の bin は node_modules/.bin/kairos → dist/cli.js のシンボリックリンクで、argv[1] がリンク側・import.meta.url が
|
|
296
|
+
// 実体側になる——URL の文字列一致では main() が走らず黙って終了した(2026-09-07 pack→install 実走で検出)。
|
|
297
|
+
// realpath で比較する。SEA(tools/build-sea)は import.meta.url を undefined に潰すので偽に落ち、二重実行しない。
|
|
298
|
+
const isMain = (() => {
|
|
299
|
+
if (!process.argv[1] || typeof import.meta.url !== 'string')
|
|
300
|
+
return false;
|
|
301
|
+
try {
|
|
302
|
+
return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
})();
|
|
308
|
+
if (isMain)
|
|
309
|
+
process.exit(main(process.argv.slice(2)));
|