@qnroa/qtype 0.2.0 → 0.2.2
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 +73 -0
- package/CHANGELOG.zh.md +57 -0
- package/dist/cli/commands/config/action.js +68 -0
- package/dist/cli/commands/config/i18n.js +8 -0
- package/dist/cli/commands/config/index.js +50 -0
- package/dist/cli/commands/publish/build/action.js +63 -9
- package/dist/cli/commands/repo/new/action.js +14 -0
- package/dist/cli/registry/help.js +6 -0
- package/dist/core/schemas/both.js +15 -13
- package/dist/core/schemas/repo.js +8 -15
- package/dist/core/schemas/user.js +4 -0
- package/dist/view/assets/{index-BsZ7hVEw.js → index-BEGTg0_e.js} +2 -2
- package/dist/view/assets/index-C59VhMlk.css +1 -0
- package/dist/view/assets/{index-DpS4p4Fg.js → index-Dd26zdqJ.js} +1 -1
- package/dist/view/assets/{index-zP1tnFzO.js → index-Dzj2d3KO.js} +2 -2
- package/dist/view/index.html +3 -3
- package/dist/view/logo.svg +21 -0
- package/package.json +1 -1
- package/templates/en/.qtype/assets/favicon.svg +21 -0
- package/templates/en/.qtype/assets/logo.svg +21 -0
- package/templates/zh/.qtype/assets/favicon.svg +21 -0
- package/templates/zh/.qtype/assets/logo.svg +21 -0
- package/dist/view/assets/index-DB9GS9vg.css +0 -1
- /package/dist/view/{qtype-mark.svg → favicon.svg} +0 -0
- /package/dist/view/{qtype-logo.svg → wordmark.svg} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,79 @@ can land in any minor bump (`0.x.0`).
|
|
|
13
13
|
|
|
14
14
|
## [Unreleased]
|
|
15
15
|
|
|
16
|
+
## [0.2.2] — 2026-09-01
|
|
17
|
+
|
|
18
|
+
A responsive footer, a convention-based way to swap the site logo
|
|
19
|
+
and favicon without touching code, and a housecleaning rename of the
|
|
20
|
+
public-facing brand assets to drop the redundant `qtype-` prefix.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- **Custom site logo via `.qtype/assets/logo.{svg,png}`.** `qtype repo
|
|
24
|
+
new` seeds `<repo>/.qtype/assets/logo.svg` from the language template,
|
|
25
|
+
and `qtype publish build` copies that file over the default
|
|
26
|
+
`logo.<ext>` in `dist/` when it's present. Users edit the file in
|
|
27
|
+
place (keeping the filename `logo.<ext>`) — no config key, no CLI
|
|
28
|
+
flag. Missing file → the built-in default keeps working. Used by the
|
|
29
|
+
Header brand mark; the browser tab now reads a separate `favicon`
|
|
30
|
+
(see below).
|
|
31
|
+
- **Custom favicon via `.qtype/assets/favicon.{svg,png,ico}`.** Same
|
|
32
|
+
convention as the logo — drop a file at this path, `publish build`
|
|
33
|
+
ships it as `dist/favicon.<ext>` and rewrites `index.html`'s
|
|
34
|
+
`<link rel="icon">` to point at the resolved format. Separating
|
|
35
|
+
favicon from logo lets users keep a wide Header brand mark while
|
|
36
|
+
still having a square browser-tab icon.
|
|
37
|
+
- **`data-qtype-logo-ext` on `<html>`** — new boot attribute so the
|
|
38
|
+
Header's runtime `<img>` can point at `/logo.<ext>` with the right
|
|
39
|
+
extension after a build.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
- **Public brand assets renamed** to drop the `qtype-` prefix: files
|
|
43
|
+
in `src/view/public/` are now `logo.svg`, `favicon.svg`,
|
|
44
|
+
`wordmark.svg`; the built site emits `dist/logo.<ext>`,
|
|
45
|
+
`dist/favicon.<ext>`, and `dist/wordmark.svg` (unused for now, kept
|
|
46
|
+
around for future brand surfaces). Any external consumer referencing
|
|
47
|
+
`qtype-mark.svg` / `qtype-logo.svg` (the old file names) in
|
|
48
|
+
hard-coded URLs will need to update — but the runtime `<link rel="icon">`
|
|
49
|
+
and Header `<img>` are both regenerated on build so a fresh
|
|
50
|
+
`publish build` in an existing repo is enough.
|
|
51
|
+
- **Footer material name is now responsive.** The left-side title
|
|
52
|
+
truncates with an ellipsis on all viewports (max width 240px on
|
|
53
|
+
desktop, 80px on narrow screens, hidden below 400px) so an unusually
|
|
54
|
+
long material title can't crowd the absolute-centered CardNav or
|
|
55
|
+
push the theme/settings buttons around.
|
|
56
|
+
- **Footer layout wraps the title in an inner span**, which lets the
|
|
57
|
+
outer flex slot keep claiming the remaining space (so the right-side
|
|
58
|
+
buttons stay pinned to the edge) while the inner span alone handles
|
|
59
|
+
the ellipsis. Fixes a mobile-only regression where the settings /
|
|
60
|
+
theme buttons drifted next to the title instead of hugging the right
|
|
61
|
+
edge.
|
|
62
|
+
|
|
63
|
+
## [0.2.1] — 2026-09-01
|
|
64
|
+
|
|
65
|
+
Discoverability pass for `qtype config`. Users no longer have to grep
|
|
66
|
+
the source (or the wiki) to find out what keys they can set.
|
|
67
|
+
|
|
68
|
+
### Added
|
|
69
|
+
- **`qtype config --help` now lists every configurable key** at the
|
|
70
|
+
bottom of the help output — one line per key with its default, scope,
|
|
71
|
+
description, and enum options where applicable. Same source of truth
|
|
72
|
+
as the actual schema, so it stays honest.
|
|
73
|
+
- **`qtype config list --all`** — verbose per-key view: current value
|
|
74
|
+
(or default if unset), source (which file / default), scope, and
|
|
75
|
+
enum options. Complements `list` (only shows keys the user has
|
|
76
|
+
actually set).
|
|
77
|
+
- **`SchemaEntry.description` + `SchemaEntry.options`** fields, both
|
|
78
|
+
optional. Populated for every currently-registered key so the two
|
|
79
|
+
new help surfaces have something to show.
|
|
80
|
+
- **`CommandModule.afterHelp`** hook — commands can append reference
|
|
81
|
+
material after the standard help sections. `config` uses it; other
|
|
82
|
+
commands can add their own without touching the registry.
|
|
83
|
+
|
|
84
|
+
### Changed
|
|
85
|
+
- Cleaned up in-line doc comments in the schema files (they've been
|
|
86
|
+
moved into the `description` field where possible, so they're
|
|
87
|
+
reachable at runtime).
|
|
88
|
+
|
|
16
89
|
## [0.2.0] — 2026-08-31
|
|
17
90
|
|
|
18
91
|
Substantial rework of the typing page. The screen-flow is now a real
|
package/CHANGELOG.zh.md
CHANGED
|
@@ -12,6 +12,63 @@ English: [CHANGELOG.md](https://www.npmjs.com/package/@qnroa/qtype?activeTab=cod
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.2.2] — 2026-09-01
|
|
16
|
+
|
|
17
|
+
响应式页脚,约定式自定义 logo 和 favicon(不用改代码),以及把 view
|
|
18
|
+
的公用品牌资源改名去掉 `qtype-` 前缀。
|
|
19
|
+
|
|
20
|
+
### 新增
|
|
21
|
+
- **通过 `.qtype/assets/logo.{svg,png}` 自定义站点 logo。** `qtype repo
|
|
22
|
+
new` 会从语言模板复制 `logo.svg` 到 `<repo>/.qtype/assets/`;
|
|
23
|
+
`publish build` 时若该文件存在,就用它覆盖 dist 里的 `logo.<ext>`。
|
|
24
|
+
用户直接编辑该文件(保留文件名 `logo.<ext>`) —— 不用 config key,
|
|
25
|
+
不用 CLI 参数,文件不存在就用内置默认。用于 Header 品牌图标;浏
|
|
26
|
+
览器 tab 用独立的 favicon(见下)。
|
|
27
|
+
- **通过 `.qtype/assets/favicon.{svg,png,ico}` 自定义浏览器 tab 图标。**
|
|
28
|
+
和 logo 同样的约定式覆盖机制,`publish build` 会把用户的 favicon
|
|
29
|
+
复制成 `dist/favicon.<ext>` 并重写 `index.html` 里的
|
|
30
|
+
`<link rel="icon">`。favicon 和 logo 分开是为了让用户可以放宽横
|
|
31
|
+
的 logo,同时保留方形的 tab 图标。
|
|
32
|
+
- **`<html data-qtype-logo-ext>` 新 boot 属性** —— 让 Header 的
|
|
33
|
+
`<img src="/logo.<ext>">` 在运行时挑对扩展名。
|
|
34
|
+
|
|
35
|
+
### 变更
|
|
36
|
+
- **view 的公用品牌资源改名去掉 `qtype-` 前缀。** `src/view/public/`
|
|
37
|
+
下现在叫 `logo.svg`、`favicon.svg`、`wordmark.svg`;build 出来的
|
|
38
|
+
站点里也是 `dist/logo.<ext>`、`dist/favicon.<ext>` 和
|
|
39
|
+
`dist/wordmark.svg`(暂时没被引用,保留给以后用)。外部如果硬编码
|
|
40
|
+
引用了旧文件名(`qtype-mark.svg` / `qtype-logo.svg`)需要更新
|
|
41
|
+
—— 但运行时 `<link rel="icon">` 和 Header 的 `<img>` 都是构建时
|
|
42
|
+
重新生成的,已有 repo 只要跑一次 `publish build` 就同步了。
|
|
43
|
+
- **页脚材料名做了响应式截断。** 左边的材料标题在所有断点上都会
|
|
44
|
+
ellipsis(桌面 240px 上限,窄屏 80px,超窄 400px 以下隐藏),避免过
|
|
45
|
+
长的材料标题挤占 CardNav 绝对居中区域或推走右侧的主题 / 设置按钮。
|
|
46
|
+
- **标题外面包了一层 inner span**,让外层 flex 槽继续占满剩余空间
|
|
47
|
+
(右侧按钮才能贴到最右边),ellipsis 只作用在 inner span 上。修复
|
|
48
|
+
了窄屏上出现的一个视觉回归 —— 设置 / 主题按钮会飘到标题旁边,而
|
|
49
|
+
不是紧贴右侧。
|
|
50
|
+
|
|
51
|
+
## [0.2.1] — 2026-09-01
|
|
52
|
+
|
|
53
|
+
`qtype config` 的可发现性升级。用户不再需要翻源码或 wiki 才知道能设
|
|
54
|
+
置哪些 key。
|
|
55
|
+
|
|
56
|
+
### 新增
|
|
57
|
+
- **`qtype config --help` 现在会在底部列出所有可配置的 key**,一行一个,
|
|
58
|
+
含默认值、作用域、描述、枚举选项(如果有)。数据源就是 schema 本身,
|
|
59
|
+
不会脱节。
|
|
60
|
+
- **`qtype config list --all`** —— 详版:每个 key 显示当前值(或未设置
|
|
61
|
+
时的默认)、来源(哪个文件 / 默认)、作用域、枚举选项。和 `list`(只
|
|
62
|
+
显示用户设过的)互补。
|
|
63
|
+
- **`SchemaEntry.description` + `SchemaEntry.options`** 两个可选字段。
|
|
64
|
+
现在每个已注册 key 都填了,给上面两个新的 help 出口提供内容。
|
|
65
|
+
- **`CommandModule.afterHelp` 钩子** —— 命令可以在标准 help 段落后追加
|
|
66
|
+
参考内容。`config` 用它,其它命令想加也行,不用改 registry。
|
|
67
|
+
|
|
68
|
+
### 变更
|
|
69
|
+
- 清理了 schema 文件里的行内文档注释(能放进 `description` 的都挪进
|
|
70
|
+
去了,这样运行时就能读到)。
|
|
71
|
+
|
|
15
72
|
## [0.2.0] — 2026-08-31
|
|
16
73
|
|
|
17
74
|
打字页面的整体架构重写。屏幕流程改成了真正的有限状态机(xstate),
|
|
@@ -178,6 +178,12 @@ export async function configUnsetAction(key, opts) {
|
|
|
178
178
|
/** `qtype config list [--global|--local] [--show-origin]` */
|
|
179
179
|
export async function configListAction(opts) {
|
|
180
180
|
const scope = pickScope(opts);
|
|
181
|
+
// --all: enumerate every schema-known key, filling in defaults for
|
|
182
|
+
// ones the user hasn't set. Useful for discovering what can be
|
|
183
|
+
// configured.
|
|
184
|
+
if (opts.all) {
|
|
185
|
+
return listAllKnown(scope);
|
|
186
|
+
}
|
|
181
187
|
const userEntries = scope === 'repo' ? [] : await userKv.list();
|
|
182
188
|
const repoEntries = scope === 'user' ? [] : (await repoKv.available()) ? await repoKv.list() : [];
|
|
183
189
|
const rows = [];
|
|
@@ -220,6 +226,68 @@ export async function configListAction(opts) {
|
|
|
220
226
|
// ═══════════════════════════════════════════════════════════════
|
|
221
227
|
// helpers
|
|
222
228
|
// ═══════════════════════════════════════════════════════════════
|
|
229
|
+
/**
|
|
230
|
+
* List every key known to the schema in the given scope (all if scope
|
|
231
|
+
* is undefined). For each key: current value + source, or default +
|
|
232
|
+
* `(default)` marker if unset, plus type / options / description.
|
|
233
|
+
*/
|
|
234
|
+
async function listAllKnown(scope) {
|
|
235
|
+
const rows = [];
|
|
236
|
+
const seen = new Set();
|
|
237
|
+
if (scope !== 'repo') {
|
|
238
|
+
for (const k of userKv.knownKeys()) {
|
|
239
|
+
if (seen.has(k))
|
|
240
|
+
continue;
|
|
241
|
+
seen.add(k);
|
|
242
|
+
rows.push({ key: k, kv: userKv, scope: 'user' });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (scope !== 'user') {
|
|
246
|
+
for (const k of repoKv.knownKeys()) {
|
|
247
|
+
if (seen.has(k))
|
|
248
|
+
continue;
|
|
249
|
+
seen.add(k);
|
|
250
|
+
rows.push({ key: k, kv: repoKv, scope: 'repo' });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
rows.sort((a, b) => a.key.localeCompare(b.key));
|
|
254
|
+
const repoAvailable = scope === 'user' ? false : await repoKv.available();
|
|
255
|
+
for (const r of rows) {
|
|
256
|
+
const entry = r.kv.getSchema(r.key);
|
|
257
|
+
// Prefer the stored value (from user or repo scope); fall back to
|
|
258
|
+
// the schema default. `get` returns default when unset, so guard
|
|
259
|
+
// with `has` first to know whether the value is user-set.
|
|
260
|
+
let currentDisplay = '';
|
|
261
|
+
let sourceLabel = '';
|
|
262
|
+
const canRead = r.scope === 'user' || repoAvailable;
|
|
263
|
+
if (canRead && (await r.kv.has(r.key))) {
|
|
264
|
+
const raw = await r.kv.get(r.key);
|
|
265
|
+
currentDisplay = formatValue(raw);
|
|
266
|
+
sourceLabel = `${r.scope}:${r.kv.origin}`;
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
currentDisplay = formatValue(entry.default);
|
|
270
|
+
sourceLabel = 'default';
|
|
271
|
+
}
|
|
272
|
+
const parts = [`${r.key}=${currentDisplay}`];
|
|
273
|
+
if (entry.description)
|
|
274
|
+
parts.push(dim(entry.description));
|
|
275
|
+
console.log(parts.join(' '));
|
|
276
|
+
const meta = [];
|
|
277
|
+
if (entry.options && entry.options.length > 0) {
|
|
278
|
+
meta.push(`options: ${entry.options.join(' | ')}`);
|
|
279
|
+
}
|
|
280
|
+
if (entry.default !== undefined && sourceLabel === 'default') {
|
|
281
|
+
// already shown as current; keep meta uncluttered
|
|
282
|
+
}
|
|
283
|
+
else if (entry.default !== undefined) {
|
|
284
|
+
meta.push(`default: ${formatValue(entry.default)}`);
|
|
285
|
+
}
|
|
286
|
+
meta.push(`scope: ${r.scope}`);
|
|
287
|
+
meta.push(`source: ${sourceLabel}`);
|
|
288
|
+
console.log(' ' + dim(meta.join(' · ')));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
223
291
|
/** Format a value as a single-line string. Objects / arrays use JSON. */
|
|
224
292
|
function formatValue(v) {
|
|
225
293
|
if (v == null)
|
|
@@ -7,8 +7,10 @@ export const messages = {
|
|
|
7
7
|
'config.opt.global': '操作 user 层(~/.config/qtype/config.json)',
|
|
8
8
|
'config.opt.local': '操作 repo 层(<repo>/.qtype/config.json)',
|
|
9
9
|
'config.opt.showOrigin': '显示配置项来源(文件路径 / 哪一层)',
|
|
10
|
+
'config.opt.all': '列出所有可配置项(含 schema 里未设置的默认值)',
|
|
10
11
|
'config.example.setLang': '设置 CLI 语言',
|
|
11
12
|
'config.example.list': '列出所有生效配置',
|
|
13
|
+
'config.list.example.all': '列出所有可配置项及默认值',
|
|
12
14
|
'config.get.summary': '读配置',
|
|
13
15
|
'config.get.description': '读取指定 key 的值(默认合并两层;key 不存在 exit 1)',
|
|
14
16
|
'config.get.example.basic': '读 CLI 语言偏好',
|
|
@@ -29,6 +31,8 @@ export const messages = {
|
|
|
29
31
|
'config.list.example.global': '只看 user 层',
|
|
30
32
|
'config.list.msg.empty': '配置为空',
|
|
31
33
|
'config.list.msg.emptyScoped': '配置为空 ({scope})',
|
|
34
|
+
'config.help.keysHeader': '可配置项:',
|
|
35
|
+
'config.help.keysFooter': '用 `qtype config set <key> <value>` 修改;`qtype config list --all` 查看完整状态。',
|
|
32
36
|
},
|
|
33
37
|
'en-US': {
|
|
34
38
|
'config.summary': 'Manage config',
|
|
@@ -38,8 +42,10 @@ export const messages = {
|
|
|
38
42
|
'config.opt.global': 'Operate on user scope (~/.config/qtype/config.json)',
|
|
39
43
|
'config.opt.local': 'Operate on repo scope (<repo>/.qtype/config.json)',
|
|
40
44
|
'config.opt.showOrigin': 'Show where each entry comes from (file path and scope)',
|
|
45
|
+
'config.opt.all': 'List every configurable key, including schema defaults for unset keys',
|
|
41
46
|
'config.example.setLang': 'Set CLI language',
|
|
42
47
|
'config.example.list': 'List all effective config',
|
|
48
|
+
'config.list.example.all': 'List every configurable key with defaults',
|
|
43
49
|
'config.get.summary': 'Get value',
|
|
44
50
|
'config.get.description': 'Get value for a key (merged by default; exit 1 if key not set)',
|
|
45
51
|
'config.get.example.basic': 'Read CLI language preference',
|
|
@@ -60,5 +66,7 @@ export const messages = {
|
|
|
60
66
|
'config.list.example.global': 'Only user scope',
|
|
61
67
|
'config.list.msg.empty': 'Config is empty',
|
|
62
68
|
'config.list.msg.emptyScoped': 'Config is empty ({scope})',
|
|
69
|
+
'config.help.keysHeader': 'Available keys:',
|
|
70
|
+
'config.help.keysFooter': 'Use `qtype config set <key> <value>` to change one; `qtype config list --all` to see the full state.',
|
|
63
71
|
},
|
|
64
72
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { t } from '../../i18n/index.js';
|
|
2
2
|
import { messages } from './i18n.js';
|
|
3
|
+
import { userKv, repoKv } from '../../bootstrap.js';
|
|
4
|
+
import { bold, dim } from '../../utils/output.js';
|
|
3
5
|
import { configGetAction, configSetAction, configUnsetAction, configListAction, } from './action.js';
|
|
4
6
|
/** Shared --global / --local / --show-origin options. */
|
|
5
7
|
const scopeOptions = [
|
|
@@ -52,13 +54,59 @@ const listCmd = {
|
|
|
52
54
|
options: [
|
|
53
55
|
...scopeOptions,
|
|
54
56
|
{ flags: '--show-origin', describe: () => t('config.opt.showOrigin') },
|
|
57
|
+
{ flags: '--all', describe: () => t('config.opt.all') },
|
|
55
58
|
],
|
|
56
59
|
examples: () => [
|
|
57
60
|
{ command: 'qtype config list', describe: () => t('config.list.example.merged') },
|
|
58
61
|
{ command: 'qtype config list --global', describe: () => t('config.list.example.global') },
|
|
62
|
+
{ command: 'qtype config list --all', describe: () => t('config.list.example.all') },
|
|
59
63
|
],
|
|
60
64
|
action: async (opts) => configListAction(opts),
|
|
61
65
|
};
|
|
66
|
+
/**
|
|
67
|
+
* Render every configurable key (from both user and repo schemas)
|
|
68
|
+
* as a `key = <default> — description` list. Used by `qtype
|
|
69
|
+
* config --help` so users can discover what can be tuned without
|
|
70
|
+
* hunting through docs.
|
|
71
|
+
*/
|
|
72
|
+
function renderAvailableKeys() {
|
|
73
|
+
const rows = [];
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
for (const k of userKv.knownKeys()) {
|
|
76
|
+
if (seen.has(k))
|
|
77
|
+
continue;
|
|
78
|
+
seen.add(k);
|
|
79
|
+
const e = userKv.getSchema(k);
|
|
80
|
+
rows.push({ key: k, scope: 'user', default: e.default, description: e.description, options: e.options });
|
|
81
|
+
}
|
|
82
|
+
for (const k of repoKv.knownKeys()) {
|
|
83
|
+
if (seen.has(k))
|
|
84
|
+
continue;
|
|
85
|
+
seen.add(k);
|
|
86
|
+
const e = repoKv.getSchema(k);
|
|
87
|
+
rows.push({ key: k, scope: 'repo', default: e.default, description: e.description, options: e.options });
|
|
88
|
+
}
|
|
89
|
+
rows.sort((a, b) => a.key.localeCompare(b.key));
|
|
90
|
+
const fmtDefault = (v) => {
|
|
91
|
+
if (v === undefined)
|
|
92
|
+
return '(no default)';
|
|
93
|
+
if (typeof v === 'string')
|
|
94
|
+
return v || '""';
|
|
95
|
+
return JSON.stringify(v);
|
|
96
|
+
};
|
|
97
|
+
const lines = [bold(t('config.help.keysHeader'))];
|
|
98
|
+
for (const r of rows) {
|
|
99
|
+
lines.push(` ${r.key} ${dim('=')} ${fmtDefault(r.default)} ${dim(`(${r.scope})`)}`);
|
|
100
|
+
if (r.description)
|
|
101
|
+
lines.push(` ${dim(r.description)}`);
|
|
102
|
+
if (r.options && r.options.length > 0) {
|
|
103
|
+
lines.push(` ${dim(`options: ${r.options.join(' | ')}`)}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
lines.push('');
|
|
107
|
+
lines.push(dim(t('config.help.keysFooter')));
|
|
108
|
+
return lines.join('\n');
|
|
109
|
+
}
|
|
62
110
|
export const configCmd = {
|
|
63
111
|
name: 'config',
|
|
64
112
|
messages,
|
|
@@ -67,6 +115,8 @@ export const configCmd = {
|
|
|
67
115
|
examples: () => [
|
|
68
116
|
{ command: 'qtype config set cli.lang zh', describe: () => t('config.example.setLang') },
|
|
69
117
|
{ command: 'qtype config list', describe: () => t('config.example.list') },
|
|
118
|
+
{ command: 'qtype config list --all', describe: () => t('config.list.example.all') },
|
|
70
119
|
],
|
|
120
|
+
afterHelp: () => renderAvailableKeys(),
|
|
71
121
|
subcommands: [getCmd, setCmd, unsetCmd, listCmd],
|
|
72
122
|
};
|
|
@@ -80,11 +80,20 @@ export async function publishBuildAction(opts = {}) {
|
|
|
80
80
|
fs.rmSync(outDir, { recursive: true, force: true });
|
|
81
81
|
fs.mkdirSync(outDir, { recursive: true });
|
|
82
82
|
copyDir(viewBundle, outDir);
|
|
83
|
+
// Optional brand overrides: user may drop custom assets under
|
|
84
|
+
// `<repo>/.qtype/assets/`:
|
|
85
|
+
// - logo.{svg,png} → Header brand mark (dist/logo.<ext>)
|
|
86
|
+
// - favicon.{svg,png,ico} → browser tab (dist/favicon.<ext>)
|
|
87
|
+
// Whichever the user provides overrides the view bundle default;
|
|
88
|
+
// the losing sibling extensions are removed so we don't ship stale
|
|
89
|
+
// duplicates.
|
|
90
|
+
const logoExt = resolveCustomAsset(cwd, outDir, 'logo', ['svg', 'png']);
|
|
91
|
+
const faviconExt = resolveCustomAsset(cwd, outDir, 'favicon', ['svg', 'png', 'ico']);
|
|
83
92
|
// Rewrite dist/index.html to carry the site's name — both in the
|
|
84
93
|
// <title> (browser tab) and as a `data-qtype-repo-name` attribute on
|
|
85
94
|
// <html> (read by the view's `readBootConfig()` for the header
|
|
86
95
|
// brand label and anywhere else the repo name is shown).
|
|
87
|
-
rewriteIndexHtml(path.join(outDir, 'index.html'), name);
|
|
96
|
+
rewriteIndexHtml(path.join(outDir, 'index.html'), name, logoExt, faviconExt);
|
|
88
97
|
// Defensive: clear any stale material/ that might have snuck into the
|
|
89
98
|
// view bundle. Since dev-seed content lives at repo root's examples/
|
|
90
99
|
// (served only by the vite dev middleware, not the bundle), this is a
|
|
@@ -304,7 +313,43 @@ async function resolveRepoName(cwd) {
|
|
|
304
313
|
}
|
|
305
314
|
return path.basename(cwd);
|
|
306
315
|
}
|
|
307
|
-
|
|
316
|
+
/**
|
|
317
|
+
* Resolve a user-provided brand asset (`logo` or `favicon`) by walking
|
|
318
|
+
* the accepted extensions in preference order and copying the first
|
|
319
|
+
* hit to `dist/<name>.<ext>`. Losing sibling extensions in the
|
|
320
|
+
* output are cleaned up so the site never carries stale defaults
|
|
321
|
+
* alongside the user's override. Returns the winning extension so
|
|
322
|
+
* downstream code (index.html rewrite, Header runtime) can build the
|
|
323
|
+
* right URL. Missing files → keep the view bundle's default (svg).
|
|
324
|
+
*/
|
|
325
|
+
function resolveCustomAsset(cwd, outDir, name, order) {
|
|
326
|
+
const candidates = order.map((ext) => ({
|
|
327
|
+
ext,
|
|
328
|
+
src: path.join(cwd, '.qtype', 'assets', `${name}.${ext}`),
|
|
329
|
+
}));
|
|
330
|
+
for (const c of candidates) {
|
|
331
|
+
if (!fs.existsSync(c.src))
|
|
332
|
+
continue;
|
|
333
|
+
fs.copyFileSync(c.src, path.join(outDir, `${name}.${c.ext}`));
|
|
334
|
+
for (const other of candidates) {
|
|
335
|
+
if (other.ext === c.ext)
|
|
336
|
+
continue;
|
|
337
|
+
const stale = path.join(outDir, `${name}.${other.ext}`);
|
|
338
|
+
if (fs.existsSync(stale))
|
|
339
|
+
fs.rmSync(stale, { force: true });
|
|
340
|
+
}
|
|
341
|
+
return c.ext;
|
|
342
|
+
}
|
|
343
|
+
return 'svg';
|
|
344
|
+
}
|
|
345
|
+
function extToMime(ext) {
|
|
346
|
+
if (ext === 'png')
|
|
347
|
+
return 'image/png';
|
|
348
|
+
if (ext === 'ico')
|
|
349
|
+
return 'image/x-icon';
|
|
350
|
+
return 'image/svg+xml';
|
|
351
|
+
}
|
|
352
|
+
function rewriteIndexHtml(indexPath, name, logoExt, faviconExt) {
|
|
308
353
|
if (!fs.existsSync(indexPath))
|
|
309
354
|
return;
|
|
310
355
|
const html = fs.readFileSync(indexPath, 'utf8');
|
|
@@ -314,13 +359,22 @@ function rewriteIndexHtml(indexPath, name) {
|
|
|
314
359
|
.replace(/>/g, '>')
|
|
315
360
|
.replace(/"/g, '"');
|
|
316
361
|
const withTitle = html.replace(/<title>[^<]*<\/title>/, `<title>${escaped}</title>`);
|
|
317
|
-
// Add or replace data-qtype-repo-name on the
|
|
318
|
-
// Two branches so a re-run cleanly
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
362
|
+
// Add or replace data-qtype-repo-name / data-qtype-logo-ext on the
|
|
363
|
+
// root <html> tag. Two branches per attribute so a re-run cleanly
|
|
364
|
+
// overwrites the existing value.
|
|
365
|
+
let out = withTitle;
|
|
366
|
+
out = /<html\b[^>]*\bdata-qtype-repo-name=/.test(out)
|
|
367
|
+
? out.replace(/(<html\b[^>]*\bdata-qtype-repo-name=)"[^"]*"/, `$1"${escaped}"`)
|
|
368
|
+
: out.replace(/<html\b/, `<html data-qtype-repo-name="${escaped}"`);
|
|
369
|
+
out = /<html\b[^>]*\bdata-qtype-logo-ext=/.test(out)
|
|
370
|
+
? out.replace(/(<html\b[^>]*\bdata-qtype-logo-ext=)"[^"]*"/, `$1"${logoExt}"`)
|
|
371
|
+
: out.replace(/<html\b/, `<html data-qtype-logo-ext="${logoExt}"`);
|
|
372
|
+
// Rewrite the favicon <link> to point at the resolved favicon file
|
|
373
|
+
// (SVG / PNG / ICO). Kept independent of the logo so the Header
|
|
374
|
+
// and the browser tab can carry different aspect ratios.
|
|
375
|
+
out = out.replace(/<link\s+rel="icon"[^>]*>/, `<link rel="icon" type="${extToMime(faviconExt)}" href="/favicon.${faviconExt}" />`);
|
|
376
|
+
if (out !== html)
|
|
377
|
+
fs.writeFileSync(indexPath, out);
|
|
324
378
|
}
|
|
325
379
|
async function obtainPassword() {
|
|
326
380
|
const envPwd = process.env.QTYPE_PUBLISH_PASSWORD;
|
|
@@ -93,6 +93,20 @@ export async function repoNewAction(opts) {
|
|
|
93
93
|
fs.mkdirSync(qtypeDir, { recursive: true });
|
|
94
94
|
fs.writeFileSync(path.join(qtypeDir, 'config.json'), JSON.stringify({ repo: { lang: canonicalLang } }, null, 2) + '\n');
|
|
95
95
|
console.log(` + .qtype/config.json`);
|
|
96
|
+
// 5b. Seed brand assets from the language template so users can
|
|
97
|
+
// rebrand by editing files in place. `publish build` picks them
|
|
98
|
+
// up over the defaults that ship in the view bundle.
|
|
99
|
+
// - logo.svg → Header brand mark
|
|
100
|
+
// - favicon.svg → browser tab icon
|
|
101
|
+
for (const rel of ['logo.svg', 'favicon.svg']) {
|
|
102
|
+
const src = path.join(templatesDir, '.qtype', 'assets', rel);
|
|
103
|
+
if (!fs.existsSync(src))
|
|
104
|
+
continue;
|
|
105
|
+
const dest = path.join(repoDir, '.qtype', 'assets', rel);
|
|
106
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
107
|
+
fs.copyFileSync(src, dest);
|
|
108
|
+
console.log(` + .qtype/assets/${rel}`);
|
|
109
|
+
}
|
|
96
110
|
// 6. git init (optional; requires explicit --git)
|
|
97
111
|
if (opts.git === true) {
|
|
98
112
|
try {
|
|
@@ -66,6 +66,12 @@ export function applyHelpMetadata(cmd, mod) {
|
|
|
66
66
|
if (mod.seeAlso && mod.seeAlso.length > 0) {
|
|
67
67
|
cmd.addHelpText('after', renderSeeAlso(mod.seeAlso));
|
|
68
68
|
}
|
|
69
|
+
// Free-form afterHelp — printed last, useful for reference lists.
|
|
70
|
+
if (mod.afterHelp) {
|
|
71
|
+
const extra = mod.afterHelp();
|
|
72
|
+
if (extra && extra.trim())
|
|
73
|
+
cmd.addHelpText('after', '\n' + extra);
|
|
74
|
+
}
|
|
69
75
|
// Aliases
|
|
70
76
|
if (mod.aliases && mod.aliases.length > 0) {
|
|
71
77
|
for (const alias of mod.aliases)
|
|
@@ -5,48 +5,50 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { enumValidator, boolValidator, positiveIntValidator, } from '../../store/index.js';
|
|
7
7
|
export const bothSchema = {
|
|
8
|
-
// Repo language. Drives:
|
|
9
|
-
// • which template locale (examples/zh vs examples/en) new repos are
|
|
10
|
-
// scaffolded from
|
|
11
|
-
// • the web UI language — the reader is locked to repo.lang so the UI
|
|
12
|
-
// matches the material's own language
|
|
13
|
-
//
|
|
14
|
-
// Values are BCP 47 codes (`zh-CN` / `en-US`); the short forms `zh` /
|
|
15
|
-
// `en` are accepted for backwards compatibility with older `.qtype/
|
|
16
|
-
// config.json` files and normalised at read time.
|
|
17
8
|
'repo.lang': {
|
|
18
9
|
validator: enumValidator(['zh-CN', 'en-US', 'zh', 'en']),
|
|
19
10
|
default: 'en-US',
|
|
11
|
+
description: 'Repo (and its published web UI) language.',
|
|
12
|
+
options: ['zh-CN', 'en-US'],
|
|
20
13
|
},
|
|
21
14
|
'typing.theme': {
|
|
22
15
|
validator: enumValidator(['mocha', 'latte']),
|
|
23
16
|
default: 'mocha',
|
|
17
|
+
description: 'Web reader theme.',
|
|
18
|
+
options: ['mocha', 'latte'],
|
|
24
19
|
},
|
|
25
20
|
'typing.targetWpm': {
|
|
26
21
|
validator: positiveIntValidator(),
|
|
22
|
+
description: 'Ghost-pacer target speed in WPM. 0 = disabled.',
|
|
27
23
|
},
|
|
28
24
|
'typing.tabBehavior': {
|
|
29
25
|
validator: enumValidator(['reset', 'input', 'disabled']),
|
|
30
26
|
default: 'reset',
|
|
27
|
+
description: 'What pressing Tab in the arena does.',
|
|
28
|
+
options: ['reset', 'input', 'disabled'],
|
|
31
29
|
},
|
|
32
30
|
'typing.hint': {
|
|
33
31
|
validator: enumValidator(['none', 'ghost']),
|
|
34
32
|
default: 'none',
|
|
33
|
+
description: 'Whether to render a faint ghost hint under the caret.',
|
|
34
|
+
options: ['none', 'ghost'],
|
|
35
35
|
},
|
|
36
36
|
'typing.showKeyboard': {
|
|
37
37
|
validator: boolValidator(),
|
|
38
38
|
default: true,
|
|
39
|
+
description: 'Show the on-screen keyboard beneath the arena.',
|
|
40
|
+
options: ['true', 'false'],
|
|
39
41
|
},
|
|
40
42
|
'typing.showHands': {
|
|
41
43
|
validator: boolValidator(),
|
|
42
44
|
default: true,
|
|
45
|
+
description: 'Show the hand overlay on the on-screen keyboard.',
|
|
46
|
+
options: ['true', 'false'],
|
|
43
47
|
},
|
|
44
|
-
// Mermaid pre-render mode. `prerender` (default) uses puppeteer at build
|
|
45
|
-
// time to bake SVGs into dist/material/mermaid.json; `client` ships raw
|
|
46
|
-
// ```mermaid fences and lets the browser render them; `off` treats mermaid
|
|
47
|
-
// as plain code blocks.
|
|
48
48
|
'publish.mermaid': {
|
|
49
49
|
validator: enumValidator(['prerender', 'client', 'off']),
|
|
50
50
|
default: 'prerender',
|
|
51
|
+
description: 'Mermaid diagram handling at `publish build` time.',
|
|
52
|
+
options: ['prerender', 'client', 'off'],
|
|
51
53
|
},
|
|
52
54
|
};
|
|
@@ -6,43 +6,35 @@ import { enumValidator, nonEmptyStringValidator, boolValidator, } from '../../st
|
|
|
6
6
|
export const repoOnlySchema = {
|
|
7
7
|
'repo.name': {
|
|
8
8
|
validator: nonEmptyStringValidator(),
|
|
9
|
+
description: 'Repo display name (folder title, package.json name).',
|
|
9
10
|
},
|
|
10
11
|
// ── Password protection for `publish build` ─────────────
|
|
11
|
-
// On/off toggle. When true, `publish build` reads the other three fields
|
|
12
|
-
// and encrypts every material/*.md before writing to dist.
|
|
13
12
|
'publish.password.enabled': {
|
|
14
13
|
validator: boolValidator(),
|
|
15
14
|
default: false,
|
|
15
|
+
description: 'Encrypt materials at `publish build` time.',
|
|
16
|
+
options: ['true', 'false'],
|
|
16
17
|
},
|
|
17
|
-
// Argon2id PHC-style hash of the plaintext password, used to verify the
|
|
18
|
-
// password entered at build time. NOT the AES key — that's derived
|
|
19
|
-
// separately via `publish.password.aesSalt`. Empty string when protection
|
|
20
|
-
// is off; shape enforced by `publish password set` writing a well-formed
|
|
21
|
-
// PHC string.
|
|
22
18
|
'publish.password.hash': {
|
|
23
19
|
validator: (v) => typeof v === 'string'
|
|
24
20
|
? null
|
|
25
21
|
: { code: 'err.schema.nonEmptyString', fallback: 'must be a string' },
|
|
26
22
|
default: '',
|
|
23
|
+
description: 'Argon2id PHC hash of the publish password (managed by `qtype publish password`).',
|
|
27
24
|
},
|
|
28
|
-
// Base64 salt used with argon2id (password → AES-256 key). Rotated only
|
|
29
|
-
// when the user calls `publish password set` — stable across rebuilds so
|
|
30
|
-
// the same password unlocks previously published sites.
|
|
31
25
|
'publish.password.aesSalt': {
|
|
32
26
|
validator: (v) => typeof v === 'string'
|
|
33
27
|
? null
|
|
34
28
|
: { code: 'err.schema.nonEmptyString', fallback: 'must be a string' },
|
|
35
29
|
default: '',
|
|
30
|
+
description: 'Base64 salt used to derive the AES key (managed by `qtype publish password`).',
|
|
36
31
|
},
|
|
37
|
-
// What to encrypt. `all` covers material/ + any future encrypted assets;
|
|
38
|
-
// `material-only` limits encryption to material/*.md (same behavior in
|
|
39
|
-
// v0 since qtype has no other content streams).
|
|
40
32
|
'publish.password.scope': {
|
|
41
33
|
validator: enumValidator(['all', 'material-only', 'selected']),
|
|
42
34
|
default: 'all',
|
|
35
|
+
description: 'What to encrypt when password protection is on.',
|
|
36
|
+
options: ['all', 'material-only', 'selected'],
|
|
43
37
|
},
|
|
44
|
-
// When scope='selected', the list of file names (relative to material/)
|
|
45
|
-
// to encrypt. Ignored otherwise.
|
|
46
38
|
'publish.password.selected': {
|
|
47
39
|
validator: (v) => Array.isArray(v) && v.every((x) => typeof x === 'string')
|
|
48
40
|
? null
|
|
@@ -51,5 +43,6 @@ export const repoOnlySchema = {
|
|
|
51
43
|
fallback: 'must be an array of strings',
|
|
52
44
|
},
|
|
53
45
|
default: [],
|
|
46
|
+
description: 'File names to encrypt when scope is `selected` (JSON array).',
|
|
54
47
|
},
|
|
55
48
|
};
|
|
@@ -11,9 +11,13 @@ export const userOnlySchema = {
|
|
|
11
11
|
'cli.lang': {
|
|
12
12
|
validator: enumValidator(['zh-CN', 'en-US', 'zh', 'en']),
|
|
13
13
|
default: 'en-US',
|
|
14
|
+
description: 'CLI display language.',
|
|
15
|
+
options: ['zh-CN', 'en-US'],
|
|
14
16
|
},
|
|
15
17
|
'cli.color': {
|
|
16
18
|
validator: enumValidator(['always', 'never', 'auto']),
|
|
17
19
|
default: 'auto',
|
|
20
|
+
description: 'Color output policy for CLI messages.',
|
|
21
|
+
options: ['always', 'never', 'auto'],
|
|
18
22
|
},
|
|
19
23
|
};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/index-
|
|
2
|
-
import{r as l,_ as h,u as g,j as f,C as p,M as m,T as y,a as w}from"./index-
|
|
1
|
+
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/index-Dd26zdqJ.js","assets/index-Dzj2d3KO.js","assets/index-C59VhMlk.css"])))=>i.map(i=>d[i]);
|
|
2
|
+
import{r as l,_ as h,u as g,j as f,C as p,M as m,T as y,a as w}from"./index-Dzj2d3KO.js";let o=null,c=null;async function L(){return o||c||(c=h(()=>import("./index-Dd26zdqJ.js"),__vite__mapDeps([0,1,2])).then(async e=>(o=await e.createHighlighter({themes:["github-dark","github-light"],langs:[]}),o)),c)}async function _(e,t){if(!e.getLoadedLanguages().includes(t))try{await e.loadLanguage(t)}catch{}}function x(e){const[t,n]=l.useState(o);return l.useEffect(()=>{if(!e)return;let r=!1;return(async()=>{const s=await L();await _(s,e),r||n(s)})(),()=>{r=!0}},[e]),t}const A=/```(\w+)/;function E(e){const t=e.toLowerCase();return t.includes("typescript")||t.endsWith(".ts")?"typescript":t.includes("python")||t.endsWith(".py")?"python":t.includes("cpp")||t.includes("c++")?"cpp":t.includes("java")&&!t.includes("script")?"java":t.includes("go")?"go":t.includes("rust")||t.endsWith(".rs")?"rust":t.includes("code-c")||t.endsWith(".c")?"c":"javascript"}function j(e,t){if(e){const n=e.match(A);if(n)return n[1]}return E(t)}function k(e,t,n,r){if(!r||!r.getLoadedLanguages().includes(t))return new Array(Array.from(e).length).fill(null);try{const s=r.codeToTokens(e,{lang:t,theme:n}),i=[];for(const u of s.tokens){for(const a of u)for(const d of a.content)i.push(a.color??null);i.push(null)}return!e.endsWith(`
|
|
3
3
|
`)&&i.length>Array.from(e).length&&i.pop(),i}catch{return new Array(Array.from(e).length).fill(null)}}function v(e){var d;const{material:t,card:n}=e,[r]=g(),s=l.useMemo(()=>r?j(n.question,t.filePath):"",[n.question,t.filePath,r]),i=typeof document<"u"&&((d=document.documentElement.getAttribute("data-theme"))!=null&&d.includes("latte"))?"github-light":"github-dark",u=x(r?s:""),a=l.useMemo(()=>r?k(n.answer,s,i,u):void 0,[n.answer,s,i,u,r]);return f.jsx(p,{type:"code",variant:"flow",slots:[{role:"title",content:n.title},n.question?{role:"question",content:f.jsx(m,{source:n.question})}:{role:"question",content:null},{role:"arena",content:f.jsx(y,{...w(e),charColors:a},n.id)}]})}export{v as CodeCard};
|