@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.
Files changed (110) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/CHANGELOG.zh.md +82 -0
  3. package/LICENSE +201 -0
  4. package/README.md +113 -0
  5. package/README.zh.md +114 -0
  6. package/dist/cli/bootstrap.js +190 -0
  7. package/dist/cli/commands/check/action.js +56 -0
  8. package/dist/cli/commands/check/i18n.js +26 -0
  9. package/dist/cli/commands/check/index.js +20 -0
  10. package/dist/cli/commands/config/action.js +236 -0
  11. package/dist/cli/commands/config/i18n.js +64 -0
  12. package/dist/cli/commands/config/index.js +72 -0
  13. package/dist/cli/commands/content/index.js +22 -0
  14. package/dist/cli/commands/content/lint/action.js +102 -0
  15. package/dist/cli/commands/content/lint/i18n.js +32 -0
  16. package/dist/cli/commands/content/lint/index.js +19 -0
  17. package/dist/cli/commands/content/search/action.js +64 -0
  18. package/dist/cli/commands/content/search/i18n.js +20 -0
  19. package/dist/cli/commands/content/search/index.js +24 -0
  20. package/dist/cli/commands/content/validate/action.js +145 -0
  21. package/dist/cli/commands/content/validate/i18n.js +32 -0
  22. package/dist/cli/commands/content/validate/index.js +19 -0
  23. package/dist/cli/commands/index.js +19 -0
  24. package/dist/cli/commands/init/index.js +53 -0
  25. package/dist/cli/commands/pack/i18n.js +20 -0
  26. package/dist/cli/commands/pack/index.js +48 -0
  27. package/dist/cli/commands/plugin/i18n.js +28 -0
  28. package/dist/cli/commands/plugin/index.js +41 -0
  29. package/dist/cli/commands/publish/build/action.js +208 -0
  30. package/dist/cli/commands/publish/build/i18n.js +50 -0
  31. package/dist/cli/commands/publish/build/index.js +24 -0
  32. package/dist/cli/commands/publish/build/mermaidPass.js +93 -0
  33. package/dist/cli/commands/publish/build/mermaidPrerender.js +199 -0
  34. package/dist/cli/commands/publish/index.js +21 -0
  35. package/dist/cli/commands/publish/init/action.js +74 -0
  36. package/dist/cli/commands/publish/init/i18n.js +22 -0
  37. package/dist/cli/commands/publish/init/index.js +26 -0
  38. package/dist/cli/commands/publish/password/action.js +158 -0
  39. package/dist/cli/commands/publish/password/i18n.js +44 -0
  40. package/dist/cli/commands/publish/password/index.js +35 -0
  41. package/dist/cli/commands/publish/serve/action.js +117 -0
  42. package/dist/cli/commands/publish/serve/i18n.js +32 -0
  43. package/dist/cli/commands/publish/serve/index.js +37 -0
  44. package/dist/cli/commands/repo/index.js +19 -0
  45. package/dist/cli/commands/repo/new/action.js +98 -0
  46. package/dist/cli/commands/repo/new/i18n.js +46 -0
  47. package/dist/cli/commands/repo/new/index.js +57 -0
  48. package/dist/cli/commands/repo/sync/action.js +202 -0
  49. package/dist/cli/commands/repo/sync/i18n.js +44 -0
  50. package/dist/cli/commands/repo/sync/index.js +47 -0
  51. package/dist/cli/commands/repo/templates.js +87 -0
  52. package/dist/cli/i18n/index.js +141 -0
  53. package/dist/cli/i18n/shared.js +142 -0
  54. package/dist/cli/index.js +79 -0
  55. package/dist/cli/registry/command.js +1 -0
  56. package/dist/cli/registry/commander-i18n.js +76 -0
  57. package/dist/cli/registry/help.js +142 -0
  58. package/dist/cli/registry/mount.js +71 -0
  59. package/dist/cli/utils/output.js +92 -0
  60. package/dist/cli/utils/paths.js +13 -0
  61. package/dist/cli/utils/prompt.js +72 -0
  62. package/dist/cli/utils/repoGuard.js +41 -0
  63. package/dist/core/encrypt.js +113 -0
  64. package/dist/core/index.js +8 -0
  65. package/dist/core/keystroke/asciiMap.js +39 -0
  66. package/dist/core/keystroke/chineseMap.js +32 -0
  67. package/dist/core/keystroke/fullwidthMap.js +41 -0
  68. package/dist/core/keystroke/index.js +22 -0
  69. package/dist/core/keystroke/textNormalize.js +68 -0
  70. package/dist/core/keystroke/types.js +1 -0
  71. package/dist/core/parser/frontmatter.js +58 -0
  72. package/dist/core/parser/index.js +2 -0
  73. package/dist/core/parser/parseMaterial.js +42 -0
  74. package/dist/core/types.js +1 -0
  75. package/dist/core/typing/engine.js +112 -0
  76. package/dist/core/typing/index.js +2 -0
  77. package/dist/core/typing/metrics.js +20 -0
  78. package/dist/store/index.js +44 -0
  79. package/dist/store/kv.js +276 -0
  80. package/dist/store/storage/browser.js +61 -0
  81. package/dist/store/storage/json.js +113 -0
  82. package/dist/store/storage/memory.js +24 -0
  83. package/dist/store/types.js +47 -0
  84. package/dist/store/validators.js +71 -0
  85. package/dist/view/assets/index-DBABH3dG.js +111 -0
  86. package/dist/view/assets/index-RTLZ-zOC.css +1 -0
  87. package/dist/view/index.html +14 -0
  88. package/dist/view/qtype-logo.svg +49 -0
  89. package/dist/view/qtype-mark.svg +21 -0
  90. package/docs/README.md +23 -0
  91. package/docs/README.zh.md +20 -0
  92. package/docs/en/authoring.md +138 -0
  93. package/docs/en/cli-reference.md +171 -0
  94. package/docs/zh/authoring.md +131 -0
  95. package/docs/zh/cli-reference.md +162 -0
  96. package/package.json +85 -0
  97. package/templates/en/QTYPE.md +618 -0
  98. package/templates/en/README.md +64 -0
  99. package/templates/en/TODO.md +7 -0
  100. package/templates/en/index.md +7 -0
  101. package/templates/en/log.md +3 -0
  102. package/templates/en/material/.gitkeep +0 -0
  103. package/templates/en/material/example.md +47 -0
  104. package/templates/zh/QTYPE.md +618 -0
  105. package/templates/zh/README.md +64 -0
  106. package/templates/zh/TODO.md +7 -0
  107. package/templates/zh/index.md +7 -0
  108. package/templates/zh/log.md +3 -0
  109. package/templates/zh/material/.gitkeep +0 -0
  110. 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
+ }