@qnroa/qtype 0.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.
- package/CHANGELOG.md +97 -0
- package/CHANGELOG.zh.md +82 -0
- package/LICENSE +201 -0
- package/README.md +113 -0
- package/README.zh.md +114 -0
- package/dist/cli/bootstrap.js +190 -0
- package/dist/cli/commands/check/action.js +56 -0
- package/dist/cli/commands/check/i18n.js +26 -0
- package/dist/cli/commands/check/index.js +20 -0
- package/dist/cli/commands/config/action.js +236 -0
- package/dist/cli/commands/config/i18n.js +64 -0
- package/dist/cli/commands/config/index.js +72 -0
- package/dist/cli/commands/content/index.js +22 -0
- package/dist/cli/commands/content/lint/action.js +102 -0
- package/dist/cli/commands/content/lint/i18n.js +32 -0
- package/dist/cli/commands/content/lint/index.js +19 -0
- package/dist/cli/commands/content/search/action.js +64 -0
- package/dist/cli/commands/content/search/i18n.js +20 -0
- package/dist/cli/commands/content/search/index.js +24 -0
- package/dist/cli/commands/content/validate/action.js +145 -0
- package/dist/cli/commands/content/validate/i18n.js +32 -0
- package/dist/cli/commands/content/validate/index.js +19 -0
- package/dist/cli/commands/index.js +19 -0
- package/dist/cli/commands/init/index.js +53 -0
- package/dist/cli/commands/pack/i18n.js +20 -0
- package/dist/cli/commands/pack/index.js +48 -0
- package/dist/cli/commands/plugin/i18n.js +28 -0
- package/dist/cli/commands/plugin/index.js +41 -0
- package/dist/cli/commands/publish/build/action.js +208 -0
- package/dist/cli/commands/publish/build/i18n.js +50 -0
- package/dist/cli/commands/publish/build/index.js +24 -0
- package/dist/cli/commands/publish/build/mermaidPass.js +93 -0
- package/dist/cli/commands/publish/build/mermaidPrerender.js +199 -0
- package/dist/cli/commands/publish/index.js +21 -0
- package/dist/cli/commands/publish/init/action.js +74 -0
- package/dist/cli/commands/publish/init/i18n.js +22 -0
- package/dist/cli/commands/publish/init/index.js +26 -0
- package/dist/cli/commands/publish/password/action.js +158 -0
- package/dist/cli/commands/publish/password/i18n.js +44 -0
- package/dist/cli/commands/publish/password/index.js +35 -0
- package/dist/cli/commands/publish/serve/action.js +117 -0
- package/dist/cli/commands/publish/serve/i18n.js +32 -0
- package/dist/cli/commands/publish/serve/index.js +37 -0
- package/dist/cli/commands/repo/index.js +19 -0
- package/dist/cli/commands/repo/new/action.js +98 -0
- package/dist/cli/commands/repo/new/i18n.js +46 -0
- package/dist/cli/commands/repo/new/index.js +57 -0
- package/dist/cli/commands/repo/sync/action.js +202 -0
- package/dist/cli/commands/repo/sync/i18n.js +44 -0
- package/dist/cli/commands/repo/sync/index.js +47 -0
- package/dist/cli/commands/repo/templates.js +87 -0
- package/dist/cli/i18n/index.js +141 -0
- package/dist/cli/i18n/shared.js +142 -0
- package/dist/cli/index.js +79 -0
- package/dist/cli/registry/command.js +1 -0
- package/dist/cli/registry/commander-i18n.js +76 -0
- package/dist/cli/registry/help.js +142 -0
- package/dist/cli/registry/mount.js +71 -0
- package/dist/cli/utils/output.js +92 -0
- package/dist/cli/utils/paths.js +13 -0
- package/dist/cli/utils/prompt.js +72 -0
- package/dist/cli/utils/repoGuard.js +41 -0
- package/dist/core/encrypt.js +113 -0
- package/dist/core/index.js +8 -0
- package/dist/core/keystroke/asciiMap.js +39 -0
- package/dist/core/keystroke/chineseMap.js +32 -0
- package/dist/core/keystroke/fullwidthMap.js +41 -0
- package/dist/core/keystroke/index.js +22 -0
- package/dist/core/keystroke/textNormalize.js +68 -0
- package/dist/core/keystroke/types.js +1 -0
- package/dist/core/parser/frontmatter.js +58 -0
- package/dist/core/parser/index.js +2 -0
- package/dist/core/parser/parseMaterial.js +42 -0
- package/dist/core/types.js +1 -0
- package/dist/core/typing/engine.js +112 -0
- package/dist/core/typing/index.js +2 -0
- package/dist/core/typing/metrics.js +20 -0
- package/dist/store/index.js +44 -0
- package/dist/store/kv.js +276 -0
- package/dist/store/storage/browser.js +61 -0
- package/dist/store/storage/json.js +113 -0
- package/dist/store/storage/memory.js +24 -0
- package/dist/store/types.js +47 -0
- package/dist/store/validators.js +71 -0
- package/dist/view/assets/index-DBABH3dG.js +111 -0
- package/dist/view/assets/index-RTLZ-zOC.css +1 -0
- package/dist/view/index.html +14 -0
- package/dist/view/qtype-logo.svg +49 -0
- package/dist/view/qtype-mark.svg +21 -0
- package/docs/README.md +23 -0
- package/docs/README.zh.md +20 -0
- package/docs/en/authoring.md +138 -0
- package/docs/en/cli-reference.md +171 -0
- package/docs/zh/authoring.md +131 -0
- package/docs/zh/cli-reference.md +162 -0
- package/package.json +85 -0
- package/templates/en/QTYPE.md +618 -0
- package/templates/en/README.md +64 -0
- package/templates/en/TODO.md +7 -0
- package/templates/en/index.md +7 -0
- package/templates/en/log.md +3 -0
- package/templates/en/material/.gitkeep +0 -0
- package/templates/en/material/example.md +47 -0
- package/templates/zh/QTYPE.md +618 -0
- package/templates/zh/README.md +64 -0
- package/templates/zh/TODO.md +7 -0
- package/templates/zh/index.md +7 -0
- package/templates/zh/log.md +3 -0
- package/templates/zh/material/.gitkeep +0 -0
- package/templates/zh/material/example.md +47 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* i18n keys shared across commands.
|
|
3
|
+
*
|
|
4
|
+
* A key belongs here if:
|
|
5
|
+
* - It's referenced by more than one command module (e.g. `opt.help`, `opt.path`)
|
|
6
|
+
* - Or it's app-level (e.g. `app.description`, `msg.port.invalid`)
|
|
7
|
+
*
|
|
8
|
+
* Command-specific keys (used by only one command) live in that command's i18n.ts.
|
|
9
|
+
*/
|
|
10
|
+
// The zh-CN dictionary is the "type source of truth": its key set is the
|
|
11
|
+
// full shared-key set. Every other locale (en-US, future additions) must
|
|
12
|
+
// declare the exact same keys — enforced via `Record<keyof typeof zhCN, ...>`.
|
|
13
|
+
export const zhCN = {
|
|
14
|
+
// ── app-level ────────────────────────────────────────
|
|
15
|
+
'app.description': 'QTYPE LLM Material CLI — 仓库管理 / 发布 / 内容检查',
|
|
16
|
+
'app.help.examples.title': '示例:',
|
|
17
|
+
'app.help.more.title': '更多:',
|
|
18
|
+
'app.help.more.perCommand': "每个命令都有 --help:qtype repo new --help",
|
|
19
|
+
'app.help.more.docs': '文档:https://www.npmjs.com/package/@qnroa/qtype',
|
|
20
|
+
'app.help.seeAlso.title': '相关命令:',
|
|
21
|
+
// ── common options ───────────────────────────────────
|
|
22
|
+
'opt.help': '显示帮助',
|
|
23
|
+
'opt.version': '显示版本号',
|
|
24
|
+
'opt.help.cmdDesc': '显示命令帮助',
|
|
25
|
+
'opt.lang': '仓库语言 (zh-CN | en-US)',
|
|
26
|
+
'opt.path': '仓库路径(默认当前目录)',
|
|
27
|
+
'opt.dryRun': '只报告需要补的文件,不写',
|
|
28
|
+
'opt.force': '覆盖已有文件',
|
|
29
|
+
'opt.port': '端口号',
|
|
30
|
+
'opt.dir': '静态站目录(默认 ./dist)',
|
|
31
|
+
'opt.repo': '仓库根目录(默认当前目录)',
|
|
32
|
+
// ── runtime messages ──────────────────────────────────
|
|
33
|
+
'msg.port.invalid': '端口号必须是 1-65535 的整数',
|
|
34
|
+
'msg.home.warn': '当前目录是 $HOME,不建议在家目录初始化仓库。建议先 `mkdir my-material && cd my-material`',
|
|
35
|
+
'msg.debug.stackHint': '设 DEBUG=1 查看堆栈',
|
|
36
|
+
'msg.err.hint': '提示:',
|
|
37
|
+
// ── KV / config errors (KvError code → translation) ────
|
|
38
|
+
'err.kv.key.invalid': "无效的 key: '{raw}'。格式应为点分,如 'cli.lang' / 'publish.password.enabled'",
|
|
39
|
+
'err.kv.unknown.key': "未知 key '{key}'。已注册: {known}",
|
|
40
|
+
'err.kv.storage.unavailable': "{scope} 存储不可用({origin}),当前不在 qtype 仓库里?",
|
|
41
|
+
'err.kv.storage.parseFailed': "读取失败 {origin}:{reason}",
|
|
42
|
+
'err.kv.storage.writeFailed': "写入失败 {origin}:{reason}",
|
|
43
|
+
// ── Schema validator errors (validator return code → translation) ─
|
|
44
|
+
// KvStore injects `{key}` into vars
|
|
45
|
+
'err.schema.enum': "'{key}' 的值必须是 {allowed} 之一",
|
|
46
|
+
'err.schema.nonEmptyString': "'{key}' 必须是非空字符串",
|
|
47
|
+
'err.schema.bool': "'{key}' 必须是 true 或 false",
|
|
48
|
+
'err.schema.positiveInt': "'{key}' 必须是正整数",
|
|
49
|
+
'err.schema.absolutePath': "'{key}' 必须是以 '/' 开头的路径",
|
|
50
|
+
// ── config CLI command-specific errors (thrown by the action layer) ───
|
|
51
|
+
'err.config.scope.violation': "'{name}' 是 {allowed}-only,不能写 {attempted} 层",
|
|
52
|
+
'err.config.unset.ambiguous': "'{raw}' 在 user 和 repo 两层都有,请加 --global 或 --local 明确",
|
|
53
|
+
'err.config.scope.conflict': '--global 和 --local 互斥,只能给一个',
|
|
54
|
+
// ── generic fs / action errors ────────────────────────
|
|
55
|
+
'err.path.notExists': '路径不存在:{path}',
|
|
56
|
+
'err.file.notFound': '文件不存在:{path}',
|
|
57
|
+
'err.dir.notEmpty': '目录已存在且非空:{path}',
|
|
58
|
+
'err.dir.notExists': '目录不存在:{path}',
|
|
59
|
+
'err.file.exists': '{path} 已存在,加 --force 覆盖',
|
|
60
|
+
'err.notQtypeRepo': '{path} 不是 qtype 仓库(未找到 QTYPE.md)',
|
|
61
|
+
'err.repo.invalidName': "无效的仓库名 '{name}':不能包含路径分隔符(/ 或 \\\\)或 '.' / '..'",
|
|
62
|
+
'hint.sync.dirExists': '如要在已有仓库上补齐缺失文件,用 `qtype repo sync`',
|
|
63
|
+
'hint.serve.buildFirst': '先运行 `qtype publish build`',
|
|
64
|
+
'hint.publish.initFirst': '先运行 `qtype publish init`',
|
|
65
|
+
'hint.runRepoNew': '先运行 `qtype repo new <name>` 或 `qtype publish init`',
|
|
66
|
+
// ── commander built-in error translations (see registry/commander-i18n.ts) ───
|
|
67
|
+
'err.cmd.unknownOption': "未知选项 '{arg1}'",
|
|
68
|
+
'err.cmd.unknownOption.suggest': "未知选项 '{arg1}'(你是不是想要 {arg2}?)",
|
|
69
|
+
'err.cmd.unknownCommand': "未知命令 '{arg1}'",
|
|
70
|
+
'err.cmd.unknownCommand.suggest': "未知命令 '{arg1}'(你是不是想要 {arg2}?)",
|
|
71
|
+
'err.cmd.missingArgument': "缺少必填参数 '{arg1}'",
|
|
72
|
+
'err.cmd.missingOption': "缺少必填选项 '{arg1}'",
|
|
73
|
+
'err.cmd.optionMissingArg': "选项 '{arg1}' 缺少参数值",
|
|
74
|
+
'err.cmd.invalidArg': "选项 '{arg1}' 的值 '{arg2}' 非法。{arg3}",
|
|
75
|
+
'err.cmd.conflict': "{arg1} 不能和 {arg2} 同时使用",
|
|
76
|
+
'err.cmd.tooManyArgs': "参数过多。预期 {arg2} 个,实际 {arg3} 个",
|
|
77
|
+
};
|
|
78
|
+
export const enUS = {
|
|
79
|
+
// ── app-level ────────────────────────────────────────
|
|
80
|
+
'app.description': 'QTYPE LLM Material CLI — repo / publish / content',
|
|
81
|
+
'app.help.examples.title': 'Examples:',
|
|
82
|
+
'app.help.more.title': 'More:',
|
|
83
|
+
'app.help.more.perCommand': 'Every command has --help: qtype repo new --help',
|
|
84
|
+
'app.help.more.docs': 'Docs: https://www.npmjs.com/package/@qnroa/qtype',
|
|
85
|
+
'app.help.seeAlso.title': 'See also:',
|
|
86
|
+
// ── common options ───────────────────────────────────
|
|
87
|
+
'opt.help': 'Show help',
|
|
88
|
+
'opt.version': 'Show version',
|
|
89
|
+
'opt.help.cmdDesc': 'Show command help',
|
|
90
|
+
'opt.lang': 'Repo language (zh-CN | en-US)',
|
|
91
|
+
'opt.path': 'Repo path (default: current directory)',
|
|
92
|
+
'opt.dryRun': 'Only report missing files, do not write',
|
|
93
|
+
'opt.force': 'Overwrite existing files',
|
|
94
|
+
'opt.port': 'Port number',
|
|
95
|
+
'opt.dir': 'Static site directory (default: ./dist)',
|
|
96
|
+
'opt.repo': 'Repo root (default: current directory)',
|
|
97
|
+
// ── runtime messages ──────────────────────────────────
|
|
98
|
+
'msg.port.invalid': 'Port must be an integer between 1 and 65535',
|
|
99
|
+
'msg.home.warn': 'Current directory is $HOME. Initializing a repo in home is not recommended. Try `mkdir my-material && cd my-material` first.',
|
|
100
|
+
'msg.debug.stackHint': 'Set DEBUG=1 to see the stack',
|
|
101
|
+
'msg.err.hint': 'hint:',
|
|
102
|
+
// ── KV / config errors (KvError code → translation) ────
|
|
103
|
+
'err.kv.key.invalid': "Invalid key: '{raw}'. Expected dot-separated segments (e.g. 'cli.lang' / 'publish.password.enabled')",
|
|
104
|
+
'err.kv.unknown.key': "Unknown key '{key}'. Registered: {known}",
|
|
105
|
+
'err.kv.storage.unavailable': '{scope} storage unavailable ({origin}); are you inside a qtype repo?',
|
|
106
|
+
'err.kv.storage.parseFailed': 'Failed to read {origin}: {reason}',
|
|
107
|
+
'err.kv.storage.writeFailed': 'Failed to write {origin}: {reason}',
|
|
108
|
+
// ── Schema validator errors ────
|
|
109
|
+
// KvStore injects `{key}` into vars
|
|
110
|
+
'err.schema.enum': "'{key}' must be one of: {allowed}",
|
|
111
|
+
'err.schema.nonEmptyString': "'{key}' must be a non-empty string",
|
|
112
|
+
'err.schema.bool': "'{key}' must be true or false",
|
|
113
|
+
'err.schema.positiveInt': "'{key}' must be a positive integer",
|
|
114
|
+
'err.schema.absolutePath': "'{key}' must be an absolute path starting with '/'",
|
|
115
|
+
// ── config CLI command-level errors ────
|
|
116
|
+
'err.config.scope.violation': "'{name}' is {allowed}-only, cannot write to {attempted} scope",
|
|
117
|
+
'err.config.unset.ambiguous': "'{raw}' exists in both user and repo scopes; use --global or --local",
|
|
118
|
+
'err.config.scope.conflict': '--global and --local are mutually exclusive',
|
|
119
|
+
// ── generic fs / action errors ────────────────────────
|
|
120
|
+
'err.path.notExists': 'Path does not exist: {path}',
|
|
121
|
+
'err.file.notFound': 'File not found: {path}',
|
|
122
|
+
'err.dir.notEmpty': 'Directory already exists and is not empty: {path}',
|
|
123
|
+
'err.dir.notExists': 'Directory does not exist: {path}',
|
|
124
|
+
'err.file.exists': '{path} already exists; add --force to overwrite',
|
|
125
|
+
'err.notQtypeRepo': '{path} is not a qtype repo (no QTYPE.md found)',
|
|
126
|
+
'err.repo.invalidName': "Invalid repo name '{name}': must not contain path separators (/ or \\\\) or be '.' / '..'",
|
|
127
|
+
'hint.sync.dirExists': 'To add missing files to an existing repo, use `qtype repo sync`',
|
|
128
|
+
'hint.serve.buildFirst': 'Run `qtype publish build` first',
|
|
129
|
+
'hint.publish.initFirst': 'Run `qtype publish init` first',
|
|
130
|
+
'hint.runRepoNew': 'Run `qtype repo new <name>` or `qtype publish init` first',
|
|
131
|
+
// ── commander built-in errors (see registry/commander-i18n.ts) ──
|
|
132
|
+
'err.cmd.unknownOption': "error: unknown option '{arg1}'",
|
|
133
|
+
'err.cmd.unknownOption.suggest': "error: unknown option '{arg1}' (did you mean {arg2}?)",
|
|
134
|
+
'err.cmd.unknownCommand': "error: unknown command '{arg1}'",
|
|
135
|
+
'err.cmd.unknownCommand.suggest': "error: unknown command '{arg1}' (did you mean {arg2}?)",
|
|
136
|
+
'err.cmd.missingArgument': "error: missing required argument '{arg1}'",
|
|
137
|
+
'err.cmd.missingOption': "error: required option '{arg1}' not specified",
|
|
138
|
+
'err.cmd.optionMissingArg': "error: option '{arg1}' argument missing",
|
|
139
|
+
'err.cmd.invalidArg': "error: option '{arg1}' argument '{arg2}' is invalid. {arg3}",
|
|
140
|
+
'err.cmd.conflict': "error: {arg1} cannot be used with {arg2}",
|
|
141
|
+
'err.cmd.tooManyArgs': "error: too many arguments. Expected {arg2}, got {arg3}",
|
|
142
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* qtype CLI entry point.
|
|
4
|
+
*
|
|
5
|
+
* Only does 4 things:
|
|
6
|
+
* 1. Bootstrap side effect (builds userKv / repoKv singletons)
|
|
7
|
+
* 2. Create the program, attach top-level options
|
|
8
|
+
* 3. Mount all top-level commands (from `commands/index.ts`)
|
|
9
|
+
* 4. Parse argv
|
|
10
|
+
*
|
|
11
|
+
* Adding a new command doesn't require touching this file — just edit the
|
|
12
|
+
* `commands/index.ts` barrel.
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { Command } from 'commander';
|
|
18
|
+
import './bootstrap.js'; // Side effect: builds userKv / repoKv singletons
|
|
19
|
+
import { t } from './i18n/index.js';
|
|
20
|
+
import { bold, dim } from './utils/output.js';
|
|
21
|
+
import { mountAll } from './registry/mount.js';
|
|
22
|
+
import { formatTwoColumn, applyRootHelpConfig } from './registry/help.js';
|
|
23
|
+
import { installI18nErrorOutput } from './registry/commander-i18n.js';
|
|
24
|
+
import { allCommands } from './commands/index.js';
|
|
25
|
+
function readVersion() {
|
|
26
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const candidates = [
|
|
28
|
+
path.resolve(here, '..', '..', 'package.json'),
|
|
29
|
+
path.resolve(here, '..', '..', '..', 'package.json'),
|
|
30
|
+
];
|
|
31
|
+
for (const c of candidates) {
|
|
32
|
+
try {
|
|
33
|
+
const pkg = JSON.parse(fs.readFileSync(c, 'utf8'));
|
|
34
|
+
if (typeof pkg.version === 'string')
|
|
35
|
+
return pkg.version;
|
|
36
|
+
}
|
|
37
|
+
catch { /* try next */ }
|
|
38
|
+
}
|
|
39
|
+
return '0.0.0';
|
|
40
|
+
}
|
|
41
|
+
const VERSION = readVersion();
|
|
42
|
+
// CLI UI language is resolved by i18n/index.ts at module load (env → config
|
|
43
|
+
// → system locale). Values are BCP 47 codes (`zh-CN` / `en-US`); short
|
|
44
|
+
// codes (`zh` / `en`) are accepted for backwards compatibility. Change
|
|
45
|
+
// persistently with `qtype config set cli.lang <zh-CN|en-US> --global`.
|
|
46
|
+
const program = new Command();
|
|
47
|
+
program
|
|
48
|
+
.name('qtype')
|
|
49
|
+
.description(t('app.description'))
|
|
50
|
+
.version(VERSION, '-v, --version', t('opt.version'))
|
|
51
|
+
.helpOption('-h, --help', t('opt.help'))
|
|
52
|
+
.addHelpCommand('help [command]', t('opt.help.cmdDesc'));
|
|
53
|
+
applyRootHelpConfig(program);
|
|
54
|
+
installI18nErrorOutput(program);
|
|
55
|
+
mountAll(program, allCommands);
|
|
56
|
+
// Top-level Examples
|
|
57
|
+
program.addHelpText('after', () => {
|
|
58
|
+
const examples = formatTwoColumn([
|
|
59
|
+
['$ qtype repo new my-material', dim(t('repo.new.summary'))],
|
|
60
|
+
['$ qtype init', dim(t('repo.sync.summary'))],
|
|
61
|
+
['$ qtype publish build', dim(t('publish.build.summary'))],
|
|
62
|
+
['$ qtype config list', dim(t('config.list.summary'))],
|
|
63
|
+
]);
|
|
64
|
+
return ('\n' +
|
|
65
|
+
bold(t('app.help.examples.title')) +
|
|
66
|
+
'\n' +
|
|
67
|
+
examples +
|
|
68
|
+
'\n\n' +
|
|
69
|
+
bold(t('app.help.more.title')) +
|
|
70
|
+
'\n ' +
|
|
71
|
+
t('app.help.more.perCommand') +
|
|
72
|
+
'\n ' +
|
|
73
|
+
t('app.help.more.docs') +
|
|
74
|
+
'\n');
|
|
75
|
+
});
|
|
76
|
+
if (process.argv.length <= 2) {
|
|
77
|
+
program.help({ error: false });
|
|
78
|
+
}
|
|
79
|
+
program.parse();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { t } from '../i18n/index.js';
|
|
2
|
+
const PATTERNS = [
|
|
3
|
+
{
|
|
4
|
+
regex: /^error: unknown option '([^']+)'(?:\s*\(did you mean (.+)\?\))?/,
|
|
5
|
+
key: 'err.cmd.unknownOption',
|
|
6
|
+
suggestKey: 'err.cmd.unknownOption.suggest',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
regex: /^error: unknown command '([^']+)'(?:\s*\(did you mean (.+)\?\))?/,
|
|
10
|
+
key: 'err.cmd.unknownCommand',
|
|
11
|
+
suggestKey: 'err.cmd.unknownCommand.suggest',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
regex: /^error: missing required argument '([^']+)'/,
|
|
15
|
+
key: 'err.cmd.missingArgument',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
regex: /^error: required option '([^']+)' not specified/,
|
|
19
|
+
key: 'err.cmd.missingOption',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
regex: /^error: option '([^']+)' argument missing/,
|
|
23
|
+
key: 'err.cmd.optionMissingArg',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
regex: /^error: option '([^']+)' argument '([^']+)' is invalid\.?\s*(.*)$/,
|
|
27
|
+
key: 'err.cmd.invalidArg',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
regex: /^error: (.+?) cannot be used with (.+?)$/,
|
|
31
|
+
key: 'err.cmd.conflict',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
regex: /^error: too many arguments(?:\s+for '([^']+)')?\.\s*Expected (\d+) argument(?:s)? but got (\d+)\./,
|
|
35
|
+
key: 'err.cmd.tooManyArgs',
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* Translate a single commander error string. If no pattern matches, return
|
|
40
|
+
* the original.
|
|
41
|
+
*/
|
|
42
|
+
export function translateCommanderError(raw) {
|
|
43
|
+
const line = raw.replace(/\n$/, '').trim();
|
|
44
|
+
for (const p of PATTERNS) {
|
|
45
|
+
const m = p.regex.exec(line);
|
|
46
|
+
if (!m)
|
|
47
|
+
continue;
|
|
48
|
+
const vars = {};
|
|
49
|
+
for (let i = 1; i < m.length; i++) {
|
|
50
|
+
if (m[i] != null)
|
|
51
|
+
vars[`arg${i}`] = m[i];
|
|
52
|
+
}
|
|
53
|
+
// If there's a suggestKey and a suggestion was actually captured, use the suggest variant
|
|
54
|
+
const key = p.suggestKey && vars['arg2']
|
|
55
|
+
? p.suggestKey
|
|
56
|
+
: p.key;
|
|
57
|
+
return t(key, vars);
|
|
58
|
+
}
|
|
59
|
+
return raw;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Attach a custom outputError to the program (and to every subcommand):
|
|
63
|
+
* English original → translate → emit to stderr
|
|
64
|
+
*
|
|
65
|
+
* Note: each subcommand created by `Command.command()` has its own
|
|
66
|
+
* `_outputConfiguration`, but commander's subcommand **inherits** the parent's
|
|
67
|
+
* configuration (see `copyInheritedSettings` in command.js). So attaching at
|
|
68
|
+
* the top-level program is enough.
|
|
69
|
+
*/
|
|
70
|
+
export function installI18nErrorOutput(program) {
|
|
71
|
+
program.configureOutput({
|
|
72
|
+
outputError: (str, write) => {
|
|
73
|
+
write(translateCommanderError(str) + '\n');
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { bold, dim } from '../utils/output.js';
|
|
2
|
+
import { t } from '../i18n/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Global help style. Tweaking these numbers = tweaking the help look of
|
|
5
|
+
* every command.
|
|
6
|
+
*/
|
|
7
|
+
export const HELP_STYLE = {
|
|
8
|
+
/** Fixed help width — does not follow terminal resize (so alignment tests stay stable). */
|
|
9
|
+
helpWidth: 100,
|
|
10
|
+
/** Minimum number of spaces between Examples' two columns. */
|
|
11
|
+
twoColumnGap: 3,
|
|
12
|
+
/** Sort options alphabetically (true) or by registration order (false). */
|
|
13
|
+
sortOptions: true,
|
|
14
|
+
/** Sort subcommands alphabetically (false keeps business order: new before sync). */
|
|
15
|
+
sortSubcommands: false,
|
|
16
|
+
/** Whether subcommand help re-lists global options (kubectl/gh: false). */
|
|
17
|
+
showGlobalOptions: false,
|
|
18
|
+
// ── Field length constraints (used by lint; not enforced at runtime) ──
|
|
19
|
+
summaryMax: 60,
|
|
20
|
+
describeMax: 120,
|
|
21
|
+
optionDescribeMax: 80,
|
|
22
|
+
exampleDescribeMax: 50,
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Apply CommandModule metadata to a commander node.
|
|
26
|
+
*
|
|
27
|
+
* mount.ts calls this to translate summary, describe, long, examples,
|
|
28
|
+
* seeAlso etc. into commander's help output.
|
|
29
|
+
*/
|
|
30
|
+
export function applyHelpMetadata(cmd, mod) {
|
|
31
|
+
// summary serves as commander's description (the parent help's list item).
|
|
32
|
+
// commander also renders it again at the top of the subcommand's help —
|
|
33
|
+
// so below we use addHelpText('beforeAll') to inject the full describe
|
|
34
|
+
// and long, and suppress the duplication.
|
|
35
|
+
cmd.description(mod.summary());
|
|
36
|
+
cmd.configureHelp({
|
|
37
|
+
helpWidth: HELP_STYLE.helpWidth,
|
|
38
|
+
sortOptions: HELP_STYLE.sortOptions,
|
|
39
|
+
sortSubcommands: HELP_STYLE.sortSubcommands,
|
|
40
|
+
showGlobalOptions: HELP_STYLE.showGlobalOptions,
|
|
41
|
+
// In the subcommand list show only the command name, no `[options] [args]`
|
|
42
|
+
// usage hint. Industry convention (git/kubectl/gh/cargo): keep the
|
|
43
|
+
// parent's subcommand list clean — the usage hint only shows up on the
|
|
44
|
+
// `Usage:` line of `--help`.
|
|
45
|
+
subcommandTerm: (c) => c.name(),
|
|
46
|
+
});
|
|
47
|
+
// Top: if describe carries more content than summary, show an extra
|
|
48
|
+
// paragraph in the help header.
|
|
49
|
+
const desc = mod.describe();
|
|
50
|
+
if (desc !== mod.summary()) {
|
|
51
|
+
cmd.addHelpText('beforeAll', ''); // blank line separator
|
|
52
|
+
cmd.addHelpText('before', desc + '\n');
|
|
53
|
+
}
|
|
54
|
+
// Long description (multi-paragraph).
|
|
55
|
+
if (mod.long) {
|
|
56
|
+
const longText = mod.long();
|
|
57
|
+
if (longText.trim())
|
|
58
|
+
cmd.addHelpText('before', longText.trim() + '\n');
|
|
59
|
+
}
|
|
60
|
+
// Examples — structured array; the renderer aligns them itself
|
|
61
|
+
const examples = mod.examples?.();
|
|
62
|
+
if (examples && examples.length > 0) {
|
|
63
|
+
cmd.addHelpText('after', renderExamples(examples));
|
|
64
|
+
}
|
|
65
|
+
// See also
|
|
66
|
+
if (mod.seeAlso && mod.seeAlso.length > 0) {
|
|
67
|
+
cmd.addHelpText('after', renderSeeAlso(mod.seeAlso));
|
|
68
|
+
}
|
|
69
|
+
// Aliases
|
|
70
|
+
if (mod.aliases && mod.aliases.length > 0) {
|
|
71
|
+
for (const alias of mod.aliases)
|
|
72
|
+
cmd.alias(alias);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Help configuration for the root program. Mirrors applyHelpMetadata, but
|
|
77
|
+
* the latter requires a CommandModule and the root program doesn't have a
|
|
78
|
+
* matching module — hence a separate entry point.
|
|
79
|
+
*/
|
|
80
|
+
export function applyRootHelpConfig(program) {
|
|
81
|
+
program.configureHelp({
|
|
82
|
+
helpWidth: HELP_STYLE.helpWidth,
|
|
83
|
+
sortOptions: HELP_STYLE.sortOptions,
|
|
84
|
+
sortSubcommands: HELP_STYLE.sortSubcommands,
|
|
85
|
+
showGlobalOptions: HELP_STYLE.showGlobalOptions,
|
|
86
|
+
subcommandTerm: (c) => c.name(),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/** Two-column alignment: each row is [command, describe]. Command column width = longest command + gap. */
|
|
90
|
+
export function formatTwoColumn(rows, gap = HELP_STYLE.twoColumnGap) {
|
|
91
|
+
if (rows.length === 0)
|
|
92
|
+
return '';
|
|
93
|
+
const maxLeft = Math.max(...rows.map(([l]) => visualWidth(l)));
|
|
94
|
+
return rows
|
|
95
|
+
.map(([l, r]) => {
|
|
96
|
+
const pad = ' '.repeat(maxLeft - visualWidth(l) + gap);
|
|
97
|
+
return ` ${l}${pad}${r}`;
|
|
98
|
+
})
|
|
99
|
+
.join('\n');
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Compute the "visual width" of a string. CJK characters count as 2
|
|
103
|
+
* columns; ASCII counts as 1.
|
|
104
|
+
*
|
|
105
|
+
* Why this matters: if an example's describe is in Chinese, using
|
|
106
|
+
* `.length` to count characters severely underestimates width and
|
|
107
|
+
* alignment goes crooked. This implementation does the simplest CJK
|
|
108
|
+
* detection — good enough.
|
|
109
|
+
*/
|
|
110
|
+
function visualWidth(s) {
|
|
111
|
+
let w = 0;
|
|
112
|
+
for (const ch of s) {
|
|
113
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
114
|
+
// Simple version: CJK Unified Ideographs + common full-width ranges
|
|
115
|
+
if ((code >= 0x1100 && code <= 0x115f) ||
|
|
116
|
+
(code >= 0x2e80 && code <= 0x9fff) ||
|
|
117
|
+
(code >= 0xa960 && code <= 0xa97f) ||
|
|
118
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
119
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
120
|
+
(code >= 0xff00 && code <= 0xff60)) {
|
|
121
|
+
w += 2;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
w += 1;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return w;
|
|
128
|
+
}
|
|
129
|
+
function renderExamples(examples) {
|
|
130
|
+
const rows = examples.map((e) => [
|
|
131
|
+
e.command,
|
|
132
|
+
dim(e.describe()),
|
|
133
|
+
]);
|
|
134
|
+
return '\n' + bold(t('app.help.examples.title')) + '\n' + formatTwoColumn(rows) + '\n';
|
|
135
|
+
}
|
|
136
|
+
function renderSeeAlso(items) {
|
|
137
|
+
return ('\n' +
|
|
138
|
+
bold(t('app.help.seeAlso.title')) +
|
|
139
|
+
'\n' +
|
|
140
|
+
items.map((i) => ` qtype ${i}`).join('\n') +
|
|
141
|
+
'\n');
|
|
142
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mount(): hangs a CommandModule onto a commander Command.
|
|
3
|
+
*
|
|
4
|
+
* Responsibility split:
|
|
5
|
+
* - This file: recursive mounting; handles "input" metadata like
|
|
6
|
+
* args/options/action
|
|
7
|
+
* - `help.ts`: handles help output (summary/describe/long/examples/seeAlso/...)
|
|
8
|
+
* - `command.ts`: defines the CommandModule interface
|
|
9
|
+
*
|
|
10
|
+
* Command modules themselves **do not import commander** — this file is the
|
|
11
|
+
* sole glue layer.
|
|
12
|
+
*/
|
|
13
|
+
import { InvalidArgumentError, Option } from 'commander';
|
|
14
|
+
import { extend, t } from '../i18n/index.js';
|
|
15
|
+
import { wrapAction } from '../utils/output.js';
|
|
16
|
+
import { applyHelpMetadata } from './help.js';
|
|
17
|
+
/** Hang a single CommandModule onto a parent commander node. */
|
|
18
|
+
export function mount(parent, mod) {
|
|
19
|
+
// 1. Merge i18n (must happen before reading any describe)
|
|
20
|
+
if (mod.messages)
|
|
21
|
+
extend(mod.messages);
|
|
22
|
+
const cmd = parent
|
|
23
|
+
.command(mod.name)
|
|
24
|
+
.helpOption('-h, --help', t('opt.help'));
|
|
25
|
+
// 2. Positional arguments
|
|
26
|
+
for (const a of mod.args ?? []) {
|
|
27
|
+
cmd.argument(a.flag, a.describe());
|
|
28
|
+
}
|
|
29
|
+
// 3. Options
|
|
30
|
+
for (const o of mod.options ?? []) {
|
|
31
|
+
cmd.addOption(buildOption(o));
|
|
32
|
+
}
|
|
33
|
+
// 4. Recurse into subcommands
|
|
34
|
+
for (const sub of mod.subcommands ?? []) {
|
|
35
|
+
mount(cmd, sub);
|
|
36
|
+
}
|
|
37
|
+
// 5. Help metadata (description / long / examples / seeAlso / aliases)
|
|
38
|
+
applyHelpMetadata(cmd, mod);
|
|
39
|
+
// 6. Action
|
|
40
|
+
if (mod.action) {
|
|
41
|
+
cmd.action(wrapAction(mod.action));
|
|
42
|
+
}
|
|
43
|
+
return cmd;
|
|
44
|
+
}
|
|
45
|
+
/** Mount the top-level command list onto the program. */
|
|
46
|
+
export function mountAll(program, modules) {
|
|
47
|
+
for (const m of modules)
|
|
48
|
+
mount(program, m);
|
|
49
|
+
}
|
|
50
|
+
function buildOption(o) {
|
|
51
|
+
const opt = new Option(o.flags, o.describe());
|
|
52
|
+
if (o.choices)
|
|
53
|
+
opt.choices([...o.choices]);
|
|
54
|
+
if (o.defaultValue !== undefined)
|
|
55
|
+
opt.default(o.defaultValue);
|
|
56
|
+
if (o.parser)
|
|
57
|
+
opt.argParser(wrapParser(o.parser));
|
|
58
|
+
return opt;
|
|
59
|
+
}
|
|
60
|
+
function wrapParser(parser) {
|
|
61
|
+
return (v) => {
|
|
62
|
+
try {
|
|
63
|
+
return parser(v);
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
if (e instanceof InvalidArgumentError)
|
|
67
|
+
throw e;
|
|
68
|
+
throw new InvalidArgumentError(e instanceof Error ? e.message : String(e));
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified CLI output utilities: colors, symbols, error exits.
|
|
3
|
+
*/
|
|
4
|
+
import { KvError } from '../../store/index.js';
|
|
5
|
+
import { t } from '../i18n/index.js';
|
|
6
|
+
const RESET = '\x1b[0m';
|
|
7
|
+
const RED = '\x1b[31m';
|
|
8
|
+
const YELLOW = '\x1b[33m';
|
|
9
|
+
const GREEN = '\x1b[32m';
|
|
10
|
+
const CYAN = '\x1b[36m';
|
|
11
|
+
const DIM = '\x1b[2m';
|
|
12
|
+
const BOLD = '\x1b[1m';
|
|
13
|
+
// Detect whether the terminal supports color (disabled when not a TTY or when NO_COLOR is set)
|
|
14
|
+
const supportsColor = process.stdout.isTTY === true && process.env.NO_COLOR == null;
|
|
15
|
+
function colorize(code, text) {
|
|
16
|
+
if (!supportsColor)
|
|
17
|
+
return text;
|
|
18
|
+
return `${code}${text}${RESET}`;
|
|
19
|
+
}
|
|
20
|
+
/** Error message (red ✗) */
|
|
21
|
+
export function error(message) {
|
|
22
|
+
console.error(`${colorize(RED, '✗')} ${message}`);
|
|
23
|
+
}
|
|
24
|
+
/** Warning message (yellow ⚠) */
|
|
25
|
+
export function warn(message) {
|
|
26
|
+
console.warn(`${colorize(YELLOW, '⚠')} ${message}`);
|
|
27
|
+
}
|
|
28
|
+
/** Success message (green ✓) */
|
|
29
|
+
export function success(message) {
|
|
30
|
+
console.log(`${colorize(GREEN, '✓')} ${message}`);
|
|
31
|
+
}
|
|
32
|
+
/** Info message (ℹ prefix, cyan) */
|
|
33
|
+
export function info(message) {
|
|
34
|
+
console.log(`${colorize(CYAN, 'ℹ')} ${message}`);
|
|
35
|
+
}
|
|
36
|
+
/** Dim message (secondary information) */
|
|
37
|
+
export function dim(message) {
|
|
38
|
+
return colorize(DIM, message);
|
|
39
|
+
}
|
|
40
|
+
/** Bold */
|
|
41
|
+
export function bold(text) {
|
|
42
|
+
return colorize(BOLD, text);
|
|
43
|
+
}
|
|
44
|
+
/** Direct color helpers (used by some commands that need inline color). */
|
|
45
|
+
export const green = (s) => colorize(GREEN, s);
|
|
46
|
+
export const red = (s) => colorize(RED, s);
|
|
47
|
+
export const yellow = (s) => colorize(YELLOW, s);
|
|
48
|
+
export const cyan = (s) => colorize(CYAN, s);
|
|
49
|
+
/**
|
|
50
|
+
* Report a fatal error and exit (exit code = 1).
|
|
51
|
+
* Format: red ✗ at the start, with an optional hint.
|
|
52
|
+
*/
|
|
53
|
+
export function fatal(message, hint) {
|
|
54
|
+
error(message);
|
|
55
|
+
if (hint)
|
|
56
|
+
console.error(` ${dim(t('msg.err.hint'))} ${hint}`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Wrap a command action so uncaught exceptions become friendly error
|
|
61
|
+
* output.
|
|
62
|
+
*
|
|
63
|
+
* - `KvError` gets special handling: goes through i18n (err.code → t()
|
|
64
|
+
* dictionary lookup)
|
|
65
|
+
* - Other `Error`s use `message` directly
|
|
66
|
+
*
|
|
67
|
+
* No stack trace in production; set DEBUG=1 to print the stack for
|
|
68
|
+
* debugging.
|
|
69
|
+
*/
|
|
70
|
+
export function wrapAction(fn) {
|
|
71
|
+
return async (...args) => {
|
|
72
|
+
try {
|
|
73
|
+
await fn(...args);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
if (err instanceof KvError) {
|
|
77
|
+
error(t(err.code, err.vars));
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
const e = err;
|
|
81
|
+
error(e.message || String(e));
|
|
82
|
+
}
|
|
83
|
+
if (process.env.DEBUG) {
|
|
84
|
+
console.error(dim(err.stack ?? ''));
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
console.error(` ${dim(t('msg.debug.stackHint'))}`);
|
|
88
|
+
}
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the path to a bundled resource (templates/, examples/) relative to
|
|
5
|
+
* the CLI's install location. Works both from source (src/cli/) and dist
|
|
6
|
+
* (dist/cli/).
|
|
7
|
+
*/
|
|
8
|
+
export function resolveBundle(relative) {
|
|
9
|
+
// this file at: src/cli/utils/paths.ts → up 3 → package root
|
|
10
|
+
// this file at: dist/cli/utils/paths.js → up 3 → package root
|
|
11
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
return path.resolve(here, '..', '..', '..', relative);
|
|
13
|
+
}
|