promptex-js 1.0.0 → 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.
package/README.md CHANGED
@@ -15,6 +15,16 @@ the `promptex` command line tool.
15
15
  npm install promptex-js
16
16
  ```
17
17
 
18
+ To start a new project, or to add promptex to an existing one, use
19
+ [`create-promptex-js`](../create-promptex-js/README.md). It writes the config file
20
+ and an example, and leaves your existing files alone:
21
+
22
+ ```console
23
+ pnpm create promptex-js .
24
+ pnpm add promptex-js
25
+ pnpm exec promptex build --install
26
+ ```
27
+
18
28
  ## Minimal example
19
29
 
20
30
  Declarations live in a `prompts/` directory. Anything you `define*` at module
@@ -71,10 +81,46 @@ Switch `--platform codex` and the same declaration lands in Codex's layout inste
71
81
  | `promptex build --export <export-dir> <dir>` | Write the exact tree `--install` would write under `<export-dir>/<unit>/`, with no registry. The unit subtree is cleared first. |
72
82
  | `promptex build --watch <dir>` | Rerun the same flags (`build` alone, `--install` or `--export`) as sources change. |
73
83
  | `promptex manifest <dir>` | Answer registry queries from the current projection. |
74
- | `promptex init <dir>` | Scaffold a new project (`--lang ts`, `python` or `rust`). |
84
+ | `promptex init <dir>` | Deprecated, removed in 2.0. Use `pnpm create promptex-js <dir>` instead. |
85
+ | `promptex version` | Report the package version, the language and the flags each command supports (`--json` for the machine-readable form). |
75
86
 
76
87
  Common flags: `--out <install root>` (single-directory mode), `--platform <name>` (`manifest` only), `--unit <unit name>`, `--json`, `--locale <tag>`.
77
88
 
89
+ ## Machine-readable output
90
+
91
+ `--json` makes a command print one JSON document on stdout. Every document, and every
92
+ file the tool writes under `.promptex/`, carries `formatVersion` (currently `"1.1"`).
93
+ Within 1.x the version only gains keys, so read the keys you know and ignore the rest.
94
+
95
+ `promptex version --json` reports the package, the language and what this CLI
96
+ supports, so a caller can check a feature before using it:
97
+
98
+ ```json
99
+ {"formatVersion":"1.1","package":{"name":"promptex-js","version":"1.0.1"},"language":"ts",
100
+ "capabilities":{"verbs":{"build":{"flags":["--install","--dry-run","…"]},"version":{"flags":[]}},
101
+ "globalFlags":["--json","--locale"]}}
102
+ ```
103
+
104
+ A failing command with `--json` prints an error document on stdout and the same
105
+ message on one line of stderr:
106
+
107
+ ```json
108
+ {"formatVersion":"1.1","error":{"code":"USAGE_UNKNOWN_FLAG","message":"Unknown flag: --foo","details":{"flag":"--foo","usage":"…"}}}
109
+ ```
110
+
111
+ - `code` is stable. A `USAGE_` code means the call was wrong. A `RUN_` code means the
112
+ call was fine and the work failed. Branch on `code`, never on `message`, which is
113
+ localized and may change.
114
+ - `details` is always an object. Its keys are fixed per code and identical in all three
115
+ languages. Its values (paths, usage text) may differ.
116
+ - Exit code is `1` for every failure of the existing commands, usage errors included,
117
+ as before. A usage error of `version` exits with `2`.
118
+ - `config` always prints a JSON document, with or without `--json`. When it fails, that
119
+ document is the error document. A `config write` that is refused is a result, not an
120
+ error: the result document is printed and the exit code is `1`.
121
+ - `build --watch` prints each failed round for a person to read and emits no error
122
+ document, because watching is a long-running loop.
123
+
78
124
  ## What you can declare
79
125
 
80
126
  `defineSkill`, `defineRule`, `defineAgent`, `defineInstruction`, `defineResource`,
@@ -13,6 +13,7 @@ import { register } from 'node:module';
13
13
  import { pathToFileURL } from 'node:url';
14
14
  import { currentLocale, t } from '../i18n.js';
15
15
  import { loadNativeCore } from '../native.js';
16
+ import { failWith } from './error-output.js';
16
17
  import { PACKAGE_JSON, UNIT_SCOPE_LOADER } from '../paths.js';
17
18
  /**
18
19
  * `?u=` 的承襲鉤子只需註冊一次:它是無狀態的(承襲與否只看父模組 URL),
@@ -262,39 +263,42 @@ export async function compile(promptDir, workDir, { projectRoot, select, externa
262
263
  // 排在 `reset()` 之後、任何求值動作之前:`reset()` 一併清掉上一輪的專案根,
263
264
  // 反過來設定會被隨即清掉。
264
265
  setProjectRoot(projectRoot);
265
- const fail = (message) => {
266
- if (exitOnError) {
267
- console.error(message);
268
- process.exit(1);
269
- }
270
- throw new CompileFailure(message);
271
- };
272
266
  /** 逐行印到 stderr;格式住 SDK 的 `formatDiagnostic` 一份,三個呼叫點不各拼一次。 */
273
267
  const printDiagnostics = (diags) => {
274
268
  for (const d of diags)
275
269
  for (const line of formatDiagnostic(d))
276
270
  console.error(line);
277
271
  };
272
+ /**
273
+ * 一批阻斷級診斷擋下建置。`exitOnError` 為真時走統一的失敗輸出(帶 `--json` 時是錯誤 JSON,
274
+ * 否則逐行印診斷加收尾句);為假時(監看的求值子行程)照舊印診斷、拋 `CompileFailure`。
275
+ */
276
+ const failDiagnostics = (diags) => {
277
+ if (exitOnError) {
278
+ failWith('RUN_DIAGNOSTICS_ABORT', { diagnostics: diags }, () => {
279
+ printDiagnostics(diags);
280
+ console.error(abortSummary());
281
+ }, { projectRoot });
282
+ }
283
+ printDiagnostics(diags);
284
+ throw new CompileFailure(abortSummary());
285
+ };
278
286
  // 版本相容檢查排在準備階段之前,即整個編譯流程最早的一步(ADR-005
279
287
  // 「版本範圍宣告與編譯期檢查」):不相容時連 plugin 自己的準備階段都不
280
288
  // 執行,確保零副作用(不建立快取目錄、不發出任何 I/O),比「排在收集遍
281
289
  // 之前」更早地做到「編譯開始前即報錯」(AC-218)。
282
290
  if (plugins.length > 0) {
283
291
  const versionDiags = checkPluginVersions(plugins, FRAMEWORK_VERSION);
284
- if (versionDiags.length > 0) {
285
- printDiagnostics(versionDiags);
286
- return fail(abortSummary());
287
- }
292
+ if (versionDiags.length > 0)
293
+ return failDiagnostics(versionDiags);
288
294
  }
289
295
  // 準備階段排在收集遍之前:資料經 plugin 閉包傳給改寫遍,與源碼求值無關
290
296
  // (ADR-005「準備階段是唯一的非同步生命週期」)。任一 plugin 的準備階段
291
297
  // 失敗即中止,不進入收集遍、不留下部分結果。
292
298
  if (plugins.length > 0) {
293
299
  const prepareDiags = await engine.prepare(plugins, projectRoot);
294
- if (prepareDiags.length > 0) {
295
- printDiagnostics(prepareDiags);
296
- return fail(abortSummary());
297
- }
300
+ if (prepareDiags.length > 0)
301
+ return failDiagnostics(prepareDiags);
298
302
  }
299
303
  // 載入源碼目錄下的全部聚合檔。求值即執行作者的程式碼(PDR-002 不設沙盒):
300
304
  // 撰寫者與執行者是同一人,信任邊界不存在。
@@ -309,8 +313,7 @@ export async function compile(promptDir, workDir, { projectRoot, select, externa
309
313
  catch (e) {
310
314
  if (!(e instanceof SourceDirFailure))
311
315
  throw e;
312
- printDiagnostics(e.diagnostics);
313
- return fail(abortSummary());
316
+ return failDiagnostics(e.diagnostics);
314
317
  }
315
318
  for (const file of sourceFiles) {
316
319
  const href = pathToFileURL(file).href;
@@ -318,9 +321,14 @@ export async function compile(promptDir, workDir, { projectRoot, select, externa
318
321
  await import(cacheBust ? `${href}?u=${encodeURIComponent(cacheBust)}` : href);
319
322
  }
320
323
  catch (e) {
321
- for (const line of describeLoadFailure(file, e))
324
+ const lines = describeLoadFailure(file, e);
325
+ if (exitOnError) {
326
+ const detail = e instanceof Error ? e.message : String(e);
327
+ failWith('RUN_SOURCE_LOAD_FAILED', { path: file, detail }, [...lines, sourceLoadAbort()]);
328
+ }
329
+ for (const line of lines)
322
330
  console.error(line);
323
- return fail(sourceLoadAbort());
331
+ throw new CompileFailure(sourceLoadAbort());
324
332
  }
325
333
  }
326
334
  // 外部引用先登記再解析(AC-058):讓引用它們的 useSkill/useRule 等不被
@@ -337,10 +345,9 @@ export async function compile(promptDir, workDir, { projectRoot, select, externa
337
345
  // 「衝突分級依操作型別」,AC-072、AC-075 同型延伸)。
338
346
  const diagnostics = engine.resolve(plugins);
339
347
  if (diagnostics.length > 0) {
348
+ if (hasErrors(diagnostics))
349
+ return failDiagnostics(diagnostics);
340
350
  printDiagnostics(diagnostics);
341
- if (hasErrors(diagnostics)) {
342
- return fail(abortSummary());
343
- }
344
351
  }
345
352
  // 出處可由環境注入:三個欄位都隨環境變動,逐字比對產物時需要固定值
346
353
  // (情境回歸的 runner 即以此讓決定性可驗證)。未注入時取預設,注入了卻
@@ -350,7 +357,10 @@ export async function compile(promptDir, workDir, { projectRoot, select, externa
350
357
  provenance = parseProvenanceEnv(process.env.PROMPTEX_PROVENANCE);
351
358
  }
352
359
  catch (e) {
353
- return fail(e instanceof Error ? e.message : String(e));
360
+ const message = e instanceof Error ? e.message : String(e);
361
+ if (exitOnError)
362
+ failWith('USAGE_PROVENANCE_INVALID', {}, message);
363
+ throw new CompileFailure(message);
354
364
  }
355
365
  const ir = engine.toIR(provenance, select);
356
366
  // 源碼層宣告核對(E203、E204、E206 至 E209):接在中介表示產出之後、任何
@@ -369,10 +379,8 @@ export async function compile(promptDir, workDir, { projectRoot, select, externa
369
379
  // 排在寫出 ir.json 之前:核對不過即零產物寫入,與求值期診斷的全有全無同一
370
380
  // 條慣例。
371
381
  const sourceDiags = JSON.parse(loadNativeCore().checkSourceDeclarations(JSON.stringify(ir), currentLocale()));
372
- if (sourceDiags.length > 0) {
373
- printDiagnostics(sourceDiags);
374
- return fail(abortSummary());
375
- }
382
+ if (sourceDiags.length > 0)
383
+ return failDiagnostics(sourceDiags);
376
384
  // 交接字串只序列化一次:同一份文字既是交給核心讀的旁置檔、也是跨語言對拍面
377
385
  // (AC-084)。兩處各序列化一次的話,比對的是「測試自己序列化出來的東西」
378
386
  // 而不是實際交出去的那一份,同一份中介表示因此可能對拍通過卻與生產不符。
@@ -0,0 +1,61 @@
1
+ import type { ErrorCode } from '../cli-contract.generated.js';
2
+ /** 錯誤 JSON 的 `details`:鍵照錯誤代碼表。 */
3
+ export type ErrorDetails = Readonly<Record<string, unknown>>;
4
+ /**
5
+ * 用法錯誤:代碼與 `details` 照錯誤表。人讀文字與 `details.usage` 由 `failWith` 依代碼補上,
6
+ * 解析器只回報「是哪一種錯」,不自己組文字。
7
+ */
8
+ export interface UsageFailure {
9
+ code: ErrorCode;
10
+ details: Record<string, unknown>;
11
+ }
12
+ /** 不帶 `--json` 時 stderr 的內容:整段文字、逐行的陣列,或自己印的函式。 */
13
+ export type HumanOutput = string | readonly string[] | (() => void);
14
+ /** 本次呼叫的失敗輸出設定,由 `main()` 在解析參數前依 argv 掃描結果設定。 */
15
+ interface ErrorContext {
16
+ /** 動詞,決定用法錯誤的結束碼。 */
17
+ verb: string;
18
+ /** 呼叫帶了 `--json`:stderr 只寫一行 `message`。 */
19
+ explicitJson: boolean;
20
+ /** stdout 輸出錯誤 JSON:帶 `--json`,或動詞是 `config`。 */
21
+ stdoutJson: boolean;
22
+ /** `--locale` 的值(掃描得到的,可能還沒驗證)。 */
23
+ localeFlag: string | null;
24
+ /** 已確定的語系。`null` 表示還沒確定,輸出時照當時已能解析的來源決定。 */
25
+ locale: string | null;
26
+ }
27
+ /** 更新失敗輸出設定。 */
28
+ export declare function configureErrors(patch: Partial<ErrorContext>): void;
29
+ /** 解析參數之前掃一次 argv:用法錯誤也要輸出錯誤 JSON,所以得先知道有沒有 `--json`。 */
30
+ export declare function scanArgv(argv: readonly string[]): {
31
+ json: boolean;
32
+ locale: string | null;
33
+ };
34
+ /**
35
+ * 結束碼。錯誤表的 `exitCode` 是 2.0 的目標值,1.x 的實際值由這裡依動詞決定。
36
+ */
37
+ export declare function exitCodeFor(code: ErrorCode): number;
38
+ /**
39
+ * 綁定載不起來時的退路:與核心的 `error_envelope_json` 同形。只在 `RUN_BINDING_NOT_FOUND`
40
+ * 這種拿不到核心的情形用,其餘一律由核心組。
41
+ */
42
+ export declare function fallbackErrorEnvelope(code: ErrorCode, details: Record<string, unknown>, locale: string): string;
43
+ /** 組錯誤 JSON,回 `{ envelope, message }`。 */
44
+ export declare function buildErrorEnvelope(code: ErrorCode, details?: ErrorDetails, options?: {
45
+ projectRoot?: string;
46
+ locale?: string;
47
+ }): {
48
+ envelope: string;
49
+ message: string;
50
+ };
51
+ /**
52
+ * 輸出失敗並結束行程。
53
+ *
54
+ * - `details`:鍵照錯誤代碼表。`usage` 不用給,輸出時補。
55
+ * - `human`:不帶 `--json` 時 stderr 的內容,各結束點原本怎麼印就怎麼給。沒給時印該代碼的訊息。
56
+ * - `projectRoot`:給了就把 `details.diagnostics` 的位置換成相對專案根。
57
+ */
58
+ export declare function failWith(code: ErrorCode, details?: ErrorDetails, human?: HumanOutput, options?: {
59
+ projectRoot?: string;
60
+ }): never;
61
+ export {};
@@ -0,0 +1,146 @@
1
+ // 命令列失敗的輸出:所有結束點走同一個函式(`failWith`)。
2
+ //
3
+ // 帶 `--json` 失敗時,stdout 恰好一份錯誤 JSON,stderr 另寫一行和 `message` 同文的文字。
4
+ // 錯誤 JSON 的外層與 `message` 由核心組(`errorEnvelope`),這裡只傳代碼與 `details`。
5
+ // 不帶 `--json` 時 stderr 維持原本給人看的文字。`config` 不帶 `--json` 也輸出 JSON,
6
+ // 所以它的失敗一律在 stdout 輸出錯誤 JSON。
7
+ //
8
+ // 本檔不在模組載入時碰原生綁定:`version` 與解析期的失敗都要在沒有綁定時走得完
9
+ // (`tests/zero-install.test.ts`)。核心的函式在輸出當下才載入;綁定載不起來時(代碼
10
+ // `RUN_BINDING_NOT_FOUND`)退回本檔的同形組裝,`tests/cli-error-json.test.ts` 核對兩者逐字相同。
11
+ import { ERRORS, FORMAT_VERSION } from '../cli-contract.generated.js';
12
+ import { currentLocale, t } from '../i18n.js';
13
+ import { loadNativeCore } from '../native.js';
14
+ import { isStandaloneVerb } from './flag-table.js';
15
+ import { writeStructuredLine } from './stdout.js';
16
+ const context = { verb: '', explicitJson: false, stdoutJson: false, localeFlag: null, locale: null };
17
+ /** 更新失敗輸出設定。 */
18
+ export function configureErrors(patch) {
19
+ Object.assign(context, patch);
20
+ }
21
+ /** 解析參數之前掃一次 argv:用法錯誤也要輸出錯誤 JSON,所以得先知道有沒有 `--json`。 */
22
+ export function scanArgv(argv) {
23
+ const at = argv.lastIndexOf('--locale');
24
+ return { json: argv.includes('--json'), locale: at >= 0 && at + 1 < argv.length ? argv[at + 1] : null };
25
+ }
26
+ /**
27
+ * 結束碼。錯誤表的 `exitCode` 是 2.0 的目標值,1.x 的實際值由這裡依動詞決定。
28
+ */
29
+ export function exitCodeFor(code) {
30
+ const target = ERRORS.find((error) => error.code === code)?.exitCode ?? 1;
31
+ // compat(remove-in=2.0): usage-exit-code
32
+ // 既有動詞(build、manifest、init、config)的用法錯誤在 1.x 維持結束碼 1,
33
+ // 新動詞 `version` 從一開始就用錯誤表的 2。2.0 刪掉這一段,一律用錯誤表的值。
34
+ if (target === 2 && !isStandaloneVerb(context.verb))
35
+ return 1;
36
+ return target;
37
+ }
38
+ /** 錯誤 JSON 的語系:已確定的優先,否則請核心照旗標、環境與作業系統偏好解析,解析不到用英文。 */
39
+ function errorLocale() {
40
+ if (context.locale !== null)
41
+ return context.locale;
42
+ try {
43
+ return loadNativeCore().resolveLocale(context.localeFlag, JSON.stringify(process.env), Intl.DateTimeFormat().resolvedOptions().locale ?? null);
44
+ }
45
+ catch {
46
+ return context.localeFlag ?? currentLocale();
47
+ }
48
+ }
49
+ /** 補上用法文字:錯誤表的 `details` 含 `usage` 而呼叫端沒給時,取該語系的用法。 */
50
+ function withUsage(code, details, locale) {
51
+ const spec = ERRORS.find((error) => error.code === code);
52
+ if (spec?.details.includes('usage') && !('usage' in details))
53
+ return { ...details, usage: t('cli.usage.js', undefined, locale) };
54
+ return { ...details };
55
+ }
56
+ /** 訊息範本的代入值:字串陣列用逗號接起來,其餘轉字串。 */
57
+ function interpolate(value) {
58
+ if (Array.isArray(value))
59
+ return value.map(String).join(', ');
60
+ if (value === null || value === undefined)
61
+ return '';
62
+ return typeof value === 'object' ? JSON.stringify(value) : String(value);
63
+ }
64
+ /** 該代碼的完整人讀文字(用法文字另計)。 */
65
+ function textFor(code, details, locale) {
66
+ const spec = ERRORS.find((error) => error.code === code);
67
+ if (spec === undefined)
68
+ return code;
69
+ const params = {};
70
+ for (const [key, value] of Object.entries(details))
71
+ params[key] = interpolate(value);
72
+ return t(spec.messageKey, params, locale);
73
+ }
74
+ /** 錯誤 JSON 的 `message`:完整文字的第一行,去掉行首的 `✗ ` 記號。 */
75
+ function oneLine(text) {
76
+ const first = text.split('\n')[0] ?? '';
77
+ return (first.startsWith('✗ ') ? first.slice(2) : first).trim();
78
+ }
79
+ /**
80
+ * 綁定載不起來時的退路:與核心的 `error_envelope_json` 同形。只在 `RUN_BINDING_NOT_FOUND`
81
+ * 這種拿不到核心的情形用,其餘一律由核心組。
82
+ */
83
+ export function fallbackErrorEnvelope(code, details, locale) {
84
+ const spec = ERRORS.find((error) => error.code === code);
85
+ const shaped = {};
86
+ for (const key of spec?.details ?? [])
87
+ shaped[key] = details[key] ?? null;
88
+ const message = oneLine(textFor(code, shaped, locale));
89
+ return JSON.stringify({ formatVersion: FORMAT_VERSION, error: { code, message, details: shaped } });
90
+ }
91
+ /** 組錯誤 JSON,回 `{ envelope, message }`。 */
92
+ export function buildErrorEnvelope(code, details = {}, options = {}) {
93
+ const locale = options.locale ?? errorLocale();
94
+ const filled = withUsage(code, details, locale);
95
+ let envelope;
96
+ try {
97
+ envelope = loadNativeCore().errorEnvelope(JSON.stringify({ code, details: filled, projectRoot: options.projectRoot }), locale);
98
+ }
99
+ catch {
100
+ envelope = fallbackErrorEnvelope(code, filled, locale);
101
+ }
102
+ const message = JSON.parse(envelope).error.message;
103
+ return { envelope, message };
104
+ }
105
+ /** 印不帶 `--json` 時給人看的文字。 */
106
+ function printHuman(code, details, human) {
107
+ if (typeof human === 'function')
108
+ human();
109
+ else if (typeof human === 'string')
110
+ console.error(human);
111
+ else if (human !== undefined)
112
+ for (const line of human)
113
+ console.error(line);
114
+ else {
115
+ // 沒指定就印該代碼的訊息,錯誤表的 `details` 含 `usage` 時接上用法文字。
116
+ const spec = ERRORS.find((error) => error.code === code);
117
+ const text = textFor(code, details);
118
+ if (spec?.details.includes('usage'))
119
+ console.error(code === 'USAGE_UNKNOWN_VERB' ? t('cli.usage.js') : `${text}\n${t('cli.usage.js')}`);
120
+ else
121
+ console.error(text);
122
+ }
123
+ }
124
+ /**
125
+ * 輸出失敗並結束行程。
126
+ *
127
+ * - `details`:鍵照錯誤代碼表。`usage` 不用給,輸出時補。
128
+ * - `human`:不帶 `--json` 時 stderr 的內容,各結束點原本怎麼印就怎麼給。沒給時印該代碼的訊息。
129
+ * - `projectRoot`:給了就把 `details.diagnostics` 的位置換成相對專案根。
130
+ */
131
+ export function failWith(code, details = {}, human, options = {}) {
132
+ if (context.stdoutJson) {
133
+ // `message` 與 `details.usage` 用同一個語系。
134
+ const locale = errorLocale();
135
+ const { envelope, message } = buildErrorEnvelope(code, details, { projectRoot: options.projectRoot, locale });
136
+ writeStructuredLine(envelope);
137
+ if (context.explicitJson)
138
+ console.error(message);
139
+ else
140
+ printHuman(code, details, human);
141
+ }
142
+ else {
143
+ printHuman(code, details, human);
144
+ }
145
+ process.exit(exitCodeFor(code));
146
+ }
@@ -0,0 +1,30 @@
1
+ import type { Ecosystem, ErrorCode, FlagSpec } from '../cli-contract.generated.js';
2
+ /** 本命令列所屬的生態。 */
3
+ export declare const ECOSYSTEM: Ecosystem;
4
+ /** 依名稱查本生態認得的旗標,認不得回 `undefined`。 */
5
+ export declare function flagSpec(name: string): FlagSpec | undefined;
6
+ /** 本生態的全部旗標名稱,依表內順序。 */
7
+ export declare function ownFlagNames(): string[];
8
+ /** 旗標對指定動詞是否有效。全域旗標對所有動詞有效。 */
9
+ export declare function flagApplies(flag: FlagSpec, verb: string): boolean;
10
+ /** 表裡的全部動詞名稱。 */
11
+ export declare function verbNames(): string[];
12
+ /** 是否為獨立動詞:不載入專案、不需要原生綁定(如 `version`)。 */
13
+ export declare function isStandaloneVerb(name: string): boolean;
14
+ /**
15
+ * 走完旗標解析後才交給「建置、查詢、初始化」流程的動詞:表裡一層、非獨立的動詞。
16
+ * `config` 有子動作、走另一條路徑,`version` 是獨立動詞,兩者都不在此列。
17
+ */
18
+ export declare function projectVerbs(): string[];
19
+ /**
20
+ * 給了不適用的動詞、而表標了 `rejectOutside` 的第一個旗標(依表內順序)。
21
+ *
22
+ * 沒標 `rejectOutside` 的旗標給了不適用的動詞時靜默接受,那是 1.0.x 的行為,1.x 照舊。
23
+ * compat(remove-in=2.0): silent-inapplicable-flag
24
+ */
25
+ export declare function firstRejectedOutside(given: ReadonlySet<string>, verb: string): string | null;
26
+ /**
27
+ * 第一條被違反的 requires 或 conflicts 規則(依錯誤表內順序)的錯誤代碼,沒有則回 `null`。
28
+ * 規則只對旗標有效的動詞檢查。
29
+ */
30
+ export declare function firstViolatedRule(given: ReadonlySet<string>, verb: string): ErrorCode | null;
@@ -0,0 +1,77 @@
1
+ // 命令列解析器讀旗標定義表的薄層。
2
+ //
3
+ // 表住核心的 `crates/promptex-core/cli/flags.json`,生成物是 `../cli-contract.generated.ts`。
4
+ // 解析器靠本檔回答「這個動詞認不認得」「這個旗標認不認得、帶不帶值」「旗標對哪個動詞有效」
5
+ // 「哪條規則被違反」。旗標的值寫進 `CliArgs` 的哪個欄位仍由 `main.ts` 決定,表不管那一層。
6
+ //
7
+ // 本檔只 import 生成物,不載入原生綁定,零安裝(`tests/zero-install.test.ts`)因此成立。
8
+ import { ERRORS, FLAGS, VERBS } from '../cli-contract.generated.js';
9
+ /** 本命令列所屬的生態。 */
10
+ export const ECOSYSTEM = 'js';
11
+ /** 本生態認得的旗標。 */
12
+ const OWN_FLAGS = FLAGS.filter((flag) => flag.ecosystems.includes(ECOSYSTEM));
13
+ /** 依名稱查本生態認得的旗標,認不得回 `undefined`。 */
14
+ export function flagSpec(name) {
15
+ return OWN_FLAGS.find((flag) => flag.name === name);
16
+ }
17
+ /** 本生態的全部旗標名稱,依表內順序。 */
18
+ export function ownFlagNames() {
19
+ return OWN_FLAGS.map((flag) => flag.name);
20
+ }
21
+ /** 旗標對指定動詞是否有效。全域旗標對所有動詞有效。 */
22
+ export function flagApplies(flag, verb) {
23
+ if (!flag.ecosystems.includes(ECOSYSTEM))
24
+ return false;
25
+ if (flag.global)
26
+ return true;
27
+ if (!flag.verbs.includes(verb))
28
+ return false;
29
+ return (flag.verbEcosystems[verb] ?? flag.ecosystems).includes(ECOSYSTEM);
30
+ }
31
+ /** 表裡的全部動詞名稱。 */
32
+ export function verbNames() {
33
+ return VERBS.map((verb) => verb.name);
34
+ }
35
+ /** 是否為獨立動詞:不載入專案、不需要原生綁定(如 `version`)。 */
36
+ export function isStandaloneVerb(name) {
37
+ return VERBS.some((verb) => verb.name === name && verb.standalone);
38
+ }
39
+ /**
40
+ * 走完旗標解析後才交給「建置、查詢、初始化」流程的動詞:表裡一層、非獨立的動詞。
41
+ * `config` 有子動作、走另一條路徑,`version` 是獨立動詞,兩者都不在此列。
42
+ */
43
+ export function projectVerbs() {
44
+ return VERBS.filter((verb) => verb.actions.length === 0 && !verb.standalone).map((verb) => verb.name);
45
+ }
46
+ /**
47
+ * 給了不適用的動詞、而表標了 `rejectOutside` 的第一個旗標(依表內順序)。
48
+ *
49
+ * 沒標 `rejectOutside` 的旗標給了不適用的動詞時靜默接受,那是 1.0.x 的行為,1.x 照舊。
50
+ * compat(remove-in=2.0): silent-inapplicable-flag
51
+ */
52
+ export function firstRejectedOutside(given, verb) {
53
+ for (const flag of OWN_FLAGS) {
54
+ if (flag.rejectOutside && given.has(flag.name) && !flagApplies(flag, verb))
55
+ return flag.name;
56
+ }
57
+ return null;
58
+ }
59
+ /**
60
+ * 第一條被違反的 requires 或 conflicts 規則(依錯誤表內順序)的錯誤代碼,沒有則回 `null`。
61
+ * 規則只對旗標有效的動詞檢查。
62
+ */
63
+ export function firstViolatedRule(given, verb) {
64
+ for (const error of ERRORS) {
65
+ const rule = error.rule;
66
+ if (rule === null || !error.ecosystems.includes(ECOSYSTEM))
67
+ continue;
68
+ const flag = flagSpec(rule.flag);
69
+ if (flag === undefined || !flagApplies(flag, verb) || !given.has(rule.flag))
70
+ continue;
71
+ if (rule.requires !== undefined && !given.has(rule.requires))
72
+ return error.code;
73
+ if (rule.conflicts !== undefined && given.has(rule.conflicts))
74
+ return error.code;
75
+ }
76
+ return null;
77
+ }
@@ -35,7 +35,17 @@ export interface CliArgs {
35
35
  ops: string | null;
36
36
  apply: boolean;
37
37
  locale: string | null;
38
+ /** 本次呼叫實際給了哪些旗標(旗標全名)。旗標適用的動詞與規則檢查都以它為輸入。 */
39
+ given: ReadonlySet<string>;
38
40
  }
41
+ /**
42
+ * 旗標的值寫進 `CliArgs` 的哪個欄位。旗標認不認得、帶不帶值由定義表決定
43
+ * (`flag-table.ts`),表不管欄位;表新增旗標時這裡也要補一筆,`tests/cli-flag-table.test.ts`
44
+ * 會核對每個旗標都有對應。
45
+ */
46
+ export declare const VALUE_FIELDS: Readonly<Record<string, 'out' | 'platform' | 'unit' | 'lang' | 'ops' | 'locale' | 'exportDir'>>;
47
+ /** 不帶值的旗標寫進 `CliArgs` 的哪個布林欄位。 */
48
+ export declare const SWITCH_FIELDS: Readonly<Record<string, 'assumeYes' | 'json' | 'content' | 'install' | 'dryRun' | 'watch' | 'apply'>>;
39
49
  /**
40
50
  * 寫出本單元的旁置平台設定檔,回傳路徑(無宣告時回空字串、不寫檔)。
41
51
  *