dsh-tower 2.4.1 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/COMPATIBILITY.md +2 -0
- package/CONTRIBUTING.md +11 -5
- package/GLOSSARY.md +67 -0
- package/README.md +6 -0
- package/app/archive.js +16 -1
- package/app/board.js +16 -1
- package/app/gate.js +14 -0
- package/app/spawn-prechecks.js +18 -3
- package/cordis.patch.yml +27 -8
- package/docs/adr/0004-surface-budget.md +10 -4
- package/docs/adr/0005-component-surface.md +166 -0
- package/docs/architecture/overview.md +47 -20
- package/docs/guide/install-upgrade-migration.md +21 -0
- package/docs/guide/quickstart.md +71 -0
- package/docs/guide/release.md +60 -19
- package/docs/reference/component-surface.md +29 -17
- package/docs/reference/config.generated.md +4 -7
- package/docs/reference/config.md +7 -5
- package/docs/reference/doctor.md +11 -5
- package/docs/reference/surface-budget.md +29 -11
- package/docs/reference/tools.generated.md +1 -1
- package/docs/reference/tools.md +2 -2
- package/docs/zh/guide/install-upgrade-migration.md +1 -1
- package/docs/zh/reference/config.md +7 -5
- package/docs/zh/reference/doctor.md +7 -2
- package/docs/zh/reference/tools.md +3 -2
- package/domain/component-rules.js +216 -0
- package/domain/components.js +169 -49
- package/domain/config/schema.js +46 -23
- package/domain/gate.js +17 -2
- package/domain/scope.js +176 -5
- package/domain/surface.js +21 -7
- package/host/capability-archive.js +18 -0
- package/host/capability-board.js +18 -0
- package/host/capability-registry.js +4 -0
- package/host/doctor.js +50 -5
- package/host/preset-reviewer.js +27 -0
- package/host/preset-worker.js +32 -0
- package/host/presets.js +17 -0
- package/host/tool-registry.js +21 -4
- package/index.js +17 -6
- package/locale/archive/en.json +6 -0
- package/locale/archive/zh.json +6 -0
- package/locale/batch/en.json +1 -1
- package/locale/batch/zh.json +1 -1
- package/locale/board/en.json +6 -0
- package/locale/board/zh.json +6 -0
- package/locale/en.json +41 -21
- package/locale/hmr/en.json +2 -2
- package/locale/hmr/zh.json +1 -1
- package/locale/import/en.json +1 -1
- package/locale/import/zh.json +1 -1
- package/locale/leases/en.json +2 -2
- package/locale/leases/zh.json +2 -2
- package/locale/preset-reviewer/en.json +6 -0
- package/locale/preset-reviewer/zh.json +6 -0
- package/locale/preset-worker/en.json +6 -0
- package/locale/preset-worker/zh.json +6 -0
- package/locale/skills/en.json +1 -1
- package/locale/skills/zh.json +1 -1
- package/locale/tests/en.json +2 -2
- package/locale/tests/zh.json +2 -2
- package/locale/zh.json +40 -20
- package/package.json +15 -2
- package/scripts/check-components.mjs +642 -42
- package/scripts/check-pack.mjs +7 -3
- package/scripts/component-surface.json +87 -29
- package/scripts/generate-capability-locale.mjs +100 -21
- package/scripts/mount-smoke/probe.mjs +33 -3
- package/scripts/mount-smoke/surface.mjs +144 -1
- package/scripts/mount-smoke.mjs +24 -4
- package/scripts/pack-manifest.txt +15 -0
- package/scripts/release-notes.mjs +256 -0
- package/scripts/surface-baseline.json +32 -3
- package/scripts/surface-baseline.mjs +15 -5
- package/skills.js +4 -1
- package/tools/complete.js +3 -1
- package/tools/spawn.js +5 -5
- package/tools/test.js +7 -7
- package/ui-bundle/locale/en.json +1 -1
- package/ui-bundle/locale/zh.json +1 -1
package/scripts/check-pack.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { execFileSync } from 'node:child_process'
|
|
|
10
10
|
import fs from 'node:fs'
|
|
11
11
|
import path from 'node:path'
|
|
12
12
|
import { fileURLToPath } from 'node:url'
|
|
13
|
-
import { capabilityLocalePaths } from './generate-capability-locale.mjs'
|
|
13
|
+
import { capabilityLocalePaths, presetLocalePaths } from './generate-capability-locale.mjs'
|
|
14
14
|
|
|
15
15
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
16
16
|
const REQUIRED_FILES = [
|
|
@@ -18,9 +18,13 @@ const REQUIRED_FILES = [
|
|
|
18
18
|
'index.js',
|
|
19
19
|
'locale/en.json',
|
|
20
20
|
'locale/zh.json',
|
|
21
|
-
// #121: every optional capability row
|
|
22
|
-
// through a subpath locale resource, so those
|
|
21
|
+
// #121: every optional capability row and both preset-declaring rows explain
|
|
22
|
+
// themselves on the Plugins page through a subpath locale resource, so those
|
|
23
|
+
// generated files are required.
|
|
23
24
|
...capabilityLocalePaths(),
|
|
25
|
+
...presetLocalePaths(),
|
|
26
|
+
'host/preset-worker.js',
|
|
27
|
+
'host/preset-reviewer.js',
|
|
24
28
|
'ui-bundle/index.js',
|
|
25
29
|
'ui-bundle/package.json',
|
|
26
30
|
'ui-bundle/cordis.patch.yml',
|
|
@@ -19,7 +19,16 @@
|
|
|
19
19
|
"id": "tower-host",
|
|
20
20
|
"scope": "internal",
|
|
21
21
|
"kind": "row",
|
|
22
|
-
"fallback": "The Host plugin itself, nested under the mandatory tower anchor. A mandatory dependency, never its own switch."
|
|
22
|
+
"fallback": "The Host plugin itself, nested under the mandatory tower anchor. A mandatory dependency, never its own switch.",
|
|
23
|
+
"allowedOverrides": [
|
|
24
|
+
"mode.allowAgentControl",
|
|
25
|
+
"mode.workspaceLock",
|
|
26
|
+
"mode.workspaceLockScope",
|
|
27
|
+
"mode.planModePolicy",
|
|
28
|
+
"init.autoGitInit",
|
|
29
|
+
"send.interruptDefault",
|
|
30
|
+
"spawn.staleMs"
|
|
31
|
+
]
|
|
23
32
|
},
|
|
24
33
|
{
|
|
25
34
|
"id": "tower-skills",
|
|
@@ -43,7 +52,7 @@
|
|
|
43
52
|
"id": "tower-leases",
|
|
44
53
|
"scope": "visible",
|
|
45
54
|
"kind": "row",
|
|
46
|
-
"fallback": "tower_resource is not registered; advisory leases are coordination only, so
|
|
55
|
+
"fallback": "tower_resource is not registered; advisory leases are coordination only, but spawn.requireLease still needs the tool to acquire one, so a config that sets spawn.requireLease is rejected at runtime preconditions instead of failing every spawn at lease-busy."
|
|
47
56
|
},
|
|
48
57
|
{
|
|
49
58
|
"id": "tower-tests",
|
|
@@ -51,26 +60,48 @@
|
|
|
51
60
|
"kind": "row",
|
|
52
61
|
"fallback": "tower_test is not registered and no test receipt can be recorded. With gates.requireTests:true planning/mode entry is rejected with an explanation, and the merge gate still reports tests-missing."
|
|
53
62
|
},
|
|
63
|
+
{
|
|
64
|
+
"id": "tower-board",
|
|
65
|
+
"scope": "visible",
|
|
66
|
+
"kind": "row",
|
|
67
|
+
"fallback": "MISSIONS.md is not written or refreshed, so an existing board goes stale. Mission state tools, gates and every other capability keep working; docs.boardLocation still chooses where a board would be written."
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"id": "tower-archive",
|
|
71
|
+
"scope": "visible",
|
|
72
|
+
"kind": "row",
|
|
73
|
+
"fallback": "Terminal missions are not moved out of state.json, so the hot state keeps growing and status/doctor keep loading their full history. Merges, gates and every other capability keep working; nothing is deleted. afterDays/maxMissions keep their tuning meaning."
|
|
74
|
+
},
|
|
54
75
|
{
|
|
55
76
|
"id": "tower-hmr",
|
|
56
77
|
"scope": "visible",
|
|
57
78
|
"kind": "row",
|
|
58
|
-
"fallback": "tower_reload and /tower reload are unavailable
|
|
79
|
+
"fallback": "Development-only tooling: tower_reload and /tower reload are unavailable, so a linked-profile reload has to be triggered outside Tower. A normal profile behaves exactly as before; the row ships disabled on purpose."
|
|
59
80
|
},
|
|
60
81
|
{
|
|
61
82
|
"id": "preset-tower-worker",
|
|
62
83
|
"scope": "internal",
|
|
63
84
|
"kind": "row",
|
|
64
|
-
"fallback": "tower_spawn fails without the worker preset. A mandatory dependency, never a switch."
|
|
85
|
+
"fallback": "Declared by dsh-tower/preset-worker: tower_spawn fails without the worker preset. A mandatory dependency, never a switch."
|
|
65
86
|
},
|
|
66
87
|
{
|
|
67
88
|
"id": "preset-tower-reviewer",
|
|
68
89
|
"scope": "visible",
|
|
69
90
|
"kind": "row",
|
|
70
|
-
"fallback": "
|
|
91
|
+
"fallback": "Declared by dsh-tower/preset-reviewer: the reviewer cannot be dispatched. Supported while review.requireReview is false (merge is unaffected); with the gate on planning/mode entry is locked with an explanatory error."
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"id": "tower-ui",
|
|
95
|
+
"scope": "visible",
|
|
96
|
+
"kind": "row",
|
|
97
|
+
"fallback": "The Tower web UI plugin does not load in this profile: no Tower tool cards, composer-dock summary or status strip. Host tools, missions, state and the MISSIONS.md board are unaffected."
|
|
71
98
|
}
|
|
72
99
|
],
|
|
73
100
|
"gateableTools": {},
|
|
101
|
+
"behaviorKnobs": [
|
|
102
|
+
"spawn.requireLease",
|
|
103
|
+
"tests.admission"
|
|
104
|
+
],
|
|
74
105
|
"keys": [
|
|
75
106
|
{
|
|
76
107
|
"name": "section",
|
|
@@ -85,17 +116,35 @@
|
|
|
85
116
|
{
|
|
86
117
|
"name": "init.autoGitInit",
|
|
87
118
|
"class": "optional-config",
|
|
88
|
-
"note": "Bootstrap convenience; off only means tower_init will not run git init."
|
|
119
|
+
"note": "Bootstrap convenience; off only means tower_init will not run git init.",
|
|
120
|
+
"exemption": {
|
|
121
|
+
"date": "2026-11-03",
|
|
122
|
+
"ref": "docs/adr/0005-component-surface.md",
|
|
123
|
+
"issue": "#138",
|
|
124
|
+
"reason": "Tuning, not capability: tower_init stays the component and the switch only decides whether it runs git init."
|
|
125
|
+
}
|
|
89
126
|
},
|
|
90
127
|
{
|
|
91
128
|
"name": "init.snapshotDirty",
|
|
92
129
|
"class": "optional-config",
|
|
93
|
-
"note": "Dirty-work baseline convenience; no capability is removed."
|
|
130
|
+
"note": "Dirty-work baseline convenience; no capability is removed.",
|
|
131
|
+
"exemption": {
|
|
132
|
+
"date": "2026-11-03",
|
|
133
|
+
"ref": "docs/adr/0005-component-surface.md",
|
|
134
|
+
"issue": "#138",
|
|
135
|
+
"reason": "Tuning, not capability: the init component remains available with or without a dirty-work snapshot baseline."
|
|
136
|
+
}
|
|
94
137
|
},
|
|
95
138
|
{
|
|
96
139
|
"name": "init.autoExclude",
|
|
97
140
|
"class": "optional-config",
|
|
98
|
-
"note": "Keeps Tower artifacts out of .git/info/exclude; safe either way."
|
|
141
|
+
"note": "Keeps Tower artifacts out of .git/info/exclude; safe either way.",
|
|
142
|
+
"exemption": {
|
|
143
|
+
"date": "2026-11-03",
|
|
144
|
+
"ref": "docs/adr/0005-component-surface.md",
|
|
145
|
+
"issue": "#138",
|
|
146
|
+
"reason": "Tuning, not capability: both values keep the init component and only change which artifacts the exclude file names."
|
|
147
|
+
}
|
|
99
148
|
},
|
|
100
149
|
{
|
|
101
150
|
"name": "mode.promptSection",
|
|
@@ -250,7 +299,13 @@
|
|
|
250
299
|
{
|
|
251
300
|
"name": "spawn.requireLease",
|
|
252
301
|
"class": "optional-config",
|
|
253
|
-
"note": "Advisory-lease gate before a worker starts."
|
|
302
|
+
"note": "Advisory-lease gate before a worker starts.",
|
|
303
|
+
"exemption": {
|
|
304
|
+
"date": "2026-11-03",
|
|
305
|
+
"ref": "docs/adr/0005-component-surface.md",
|
|
306
|
+
"issue": "#138",
|
|
307
|
+
"reason": "Tuning the safety level inside the optional tower-leases row; with leases off the Host refuses requireLease instead of removing the spawn capability."
|
|
308
|
+
}
|
|
254
309
|
},
|
|
255
310
|
{
|
|
256
311
|
"name": "send.maxPerMinute",
|
|
@@ -260,17 +315,24 @@
|
|
|
260
315
|
{
|
|
261
316
|
"name": "send.interruptDefault",
|
|
262
317
|
"class": "optional-config",
|
|
263
|
-
"note": "Default for the interrupt-before-deliver flag."
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
318
|
+
"note": "Default for the interrupt-before-deliver flag.",
|
|
319
|
+
"exemption": {
|
|
320
|
+
"date": "2026-11-03",
|
|
321
|
+
"ref": "docs/adr/0005-component-surface.md",
|
|
322
|
+
"issue": "#138",
|
|
323
|
+
"reason": "Tuning, not capability: the send component always exists and the switch only picks a delivery default."
|
|
324
|
+
}
|
|
269
325
|
},
|
|
270
326
|
{
|
|
271
327
|
"name": "tests.admission",
|
|
272
328
|
"class": "optional-config",
|
|
273
|
-
"note": "Chooses when discovery runs inside the optional tests capability."
|
|
329
|
+
"note": "Chooses when discovery runs inside the optional tests capability.",
|
|
330
|
+
"exemption": {
|
|
331
|
+
"date": "2026-11-03",
|
|
332
|
+
"ref": "docs/adr/0005-component-surface.md",
|
|
333
|
+
"issue": "#138",
|
|
334
|
+
"reason": "Tuning when the admission check runs inside the optional tower-tests row, which still owns whether test tooling exists."
|
|
335
|
+
}
|
|
274
336
|
},
|
|
275
337
|
{
|
|
276
338
|
"name": "tests.timeoutMs",
|
|
@@ -285,7 +347,13 @@
|
|
|
285
347
|
{
|
|
286
348
|
"name": "tests.autoChecks",
|
|
287
349
|
"class": "optional-config",
|
|
288
|
-
"note": "Attaches a tests check to completion when discovery runs."
|
|
350
|
+
"note": "Attaches a tests check to completion when discovery runs.",
|
|
351
|
+
"exemption": {
|
|
352
|
+
"date": "2026-11-03",
|
|
353
|
+
"ref": "docs/adr/0005-component-surface.md",
|
|
354
|
+
"issue": "#138",
|
|
355
|
+
"reason": "Tuning the default receipt policy inside the optional tower-tests row, which still owns whether any test tooling exists."
|
|
356
|
+
}
|
|
289
357
|
},
|
|
290
358
|
{
|
|
291
359
|
"name": "tests.allowPackageDiscovery",
|
|
@@ -312,25 +380,15 @@
|
|
|
312
380
|
"class": "forbidden-to-disable",
|
|
313
381
|
"note": "The report cap is part of the completion contract; the refusal itself is never configurable away."
|
|
314
382
|
},
|
|
315
|
-
{
|
|
316
|
-
"name": "docs.board",
|
|
317
|
-
"class": "optional-config",
|
|
318
|
-
"note": "Writes the human-readable board; no capability is removed."
|
|
319
|
-
},
|
|
320
383
|
{
|
|
321
384
|
"name": "docs.boardLocation",
|
|
322
385
|
"class": "optional-config",
|
|
323
|
-
"note": "Chooses the board
|
|
324
|
-
},
|
|
325
|
-
{
|
|
326
|
-
"name": "archive.enabled",
|
|
327
|
-
"class": "optional-config",
|
|
328
|
-
"note": "Archival is maintenance, not a tool capability; off only leaves terminal missions hot."
|
|
386
|
+
"note": "Chooses where the board is written; whether it is written is the tower-board component row (#133)."
|
|
329
387
|
},
|
|
330
388
|
{
|
|
331
389
|
"name": "archive.afterDays",
|
|
332
390
|
"class": "optional-config",
|
|
333
|
-
"note": "Archive age threshold."
|
|
391
|
+
"note": "Archive age threshold inside the optional tower-archive component (#135)."
|
|
334
392
|
},
|
|
335
393
|
{
|
|
336
394
|
"name": "archive.maxMissions",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// Generate the per-
|
|
1
|
+
// Generate the per-row locale files (#121).
|
|
2
2
|
//
|
|
3
|
-
// The DSH Plugins page renders
|
|
3
|
+
// The DSH Plugins page renders a row as
|
|
4
4
|
// `row.meta.title ?? row.moduleName` plus `row.meta.description`, and the Host
|
|
5
5
|
// fills `row.meta` by resolving `<specifier>/locale/en.json` and then every
|
|
6
6
|
// sibling `<lang>.json` in that directory. A subpath row without such a resource
|
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
// Those files are generated, not hand-written: the single source of truth is the
|
|
11
11
|
// `components` block of `locale/<lang>.json` that `npm run docs:check:components`
|
|
12
12
|
// already audits. `meta.title` is the component `title`; `meta.description`
|
|
13
|
-
// composes that component's `summary` (what the
|
|
14
|
-
// `disables` text (what turning it off removes),
|
|
15
|
-
//
|
|
13
|
+
// composes that component's `summary` (what the row provides) with either its
|
|
14
|
+
// `disables` text (what turning it off removes) or, for the mandatory worker
|
|
15
|
+
// preset, an explicit "mandatory dependency, not a switch" label. The same
|
|
16
|
+
// generator covers capability rows and the two preset-declaring rows, so their
|
|
17
|
+
// labels cannot drift apart.
|
|
16
18
|
//
|
|
17
19
|
// Usage:
|
|
18
20
|
// node scripts/generate-capability-locale.mjs [--root <dir>] [--check]
|
|
@@ -28,6 +30,11 @@ const LANGUAGES = Object.keys(LANGUAGE_FILES)
|
|
|
28
30
|
// `disables` prose, so the check can prove every description names what the
|
|
29
31
|
// switch removes without depending on one English or Chinese phrasing.
|
|
30
32
|
const OFF_EFFECT_LABEL = { en: 'When off:', zh: '关闭后:' }
|
|
33
|
+
export { OFF_EFFECT_LABEL }
|
|
34
|
+
// The worker preset has no off state; it is a mandatory dependency of the Tower
|
|
35
|
+
// host. Its description says so with an explicit marker instead of a "When off"
|
|
36
|
+
// label that would make the row read like a switch.
|
|
37
|
+
const DEPENDENCY_LABEL = { en: 'Mandatory dependency, not a switch:', zh: '强制依赖,不是开关:' }
|
|
31
38
|
|
|
32
39
|
/**
|
|
33
40
|
* The optional capability rows that need their own subpath locale resource, in
|
|
@@ -40,9 +47,27 @@ export const CAPABILITY_LOCALES = [
|
|
|
40
47
|
{ specifier: 'import', component: 'tower-import' },
|
|
41
48
|
{ specifier: 'leases', component: 'tower-leases' },
|
|
42
49
|
{ specifier: 'tests', component: 'tower-tests' },
|
|
50
|
+
{ specifier: 'board', component: 'tower-board' },
|
|
51
|
+
{ specifier: 'archive', component: 'tower-archive' },
|
|
43
52
|
{ specifier: 'hmr', component: 'tower-hmr' },
|
|
44
53
|
]
|
|
45
54
|
|
|
55
|
+
/**
|
|
56
|
+
* The two preset-declaring rows. They used to mount the shared
|
|
57
|
+
* `@deepseek-ai/dsh-agent-preset`, so DSH resolved one locale resource for both
|
|
58
|
+
* and rendered the same title twice. Each now owns a Tower declaration module
|
|
59
|
+
* (`host/preset-*.js`) and therefore its own `<specifier>/locale/*.json` label.
|
|
60
|
+
* `mandatory` is the worker dependency; it is asserted against the component
|
|
61
|
+
* surface scope by the gate/tests so the flag cannot drift from the policy.
|
|
62
|
+
*/
|
|
63
|
+
export const PRESET_LOCALES = [
|
|
64
|
+
{ specifier: 'preset-worker', component: 'preset-tower-worker', mandatory: true },
|
|
65
|
+
{ specifier: 'preset-reviewer', component: 'preset-tower-reviewer', mandatory: false },
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
/** Every row that ships a generated subpath locale resource. */
|
|
69
|
+
export const ROW_LOCALES = [...CAPABILITY_LOCALES, ...PRESET_LOCALES]
|
|
70
|
+
|
|
46
71
|
export const CAPABILITY_LOCALE_LANGUAGES = LANGUAGES
|
|
47
72
|
|
|
48
73
|
/** Repo-relative paths of every generated capability locale file. */
|
|
@@ -52,7 +77,19 @@ export function capabilityLocalePaths() {
|
|
|
52
77
|
)
|
|
53
78
|
}
|
|
54
79
|
|
|
55
|
-
/**
|
|
80
|
+
/** Repo-relative paths of every generated preset row locale file. */
|
|
81
|
+
export function presetLocalePaths() {
|
|
82
|
+
return PRESET_LOCALES.flatMap(({ specifier }) =>
|
|
83
|
+
LANGUAGES.map((lang) => `locale/${specifier}/${lang}.json`),
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Repo-relative paths of every generated row locale file (capability + preset). */
|
|
88
|
+
export function rowLocalePaths() {
|
|
89
|
+
return [...capabilityLocalePaths(), ...presetLocalePaths()]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The `package.json` exports entry that exposes one row's locale files. */
|
|
56
93
|
export function capabilityLocaleExport(specifier) {
|
|
57
94
|
return { key: `./${specifier}/locale/*.json`, target: `./locale/${specifier}/*.json` }
|
|
58
95
|
}
|
|
@@ -63,15 +100,22 @@ function requireText(value, label) {
|
|
|
63
100
|
return text
|
|
64
101
|
}
|
|
65
102
|
|
|
103
|
+
/** Normalize the three locale prose fields a generated row label needs. */
|
|
104
|
+
function rowText(entry, lang) {
|
|
105
|
+
return {
|
|
106
|
+
title: requireText(entry?.title, `${lang} row title`),
|
|
107
|
+
summary: requireText(entry?.summary, `${lang} row summary`).replace(/\s+/g, ' '),
|
|
108
|
+
disables: requireText(entry?.disables, `${lang} row disables`).replace(/\s+/g, ' '),
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
66
112
|
/**
|
|
67
113
|
* Compose the display metadata for one capability row in one language.
|
|
68
114
|
* @param {{ title: string, summary: string, disables: string }} entry
|
|
69
115
|
* @param {'en' | 'zh'} lang
|
|
70
116
|
*/
|
|
71
117
|
export function renderCapabilityMeta(entry, lang) {
|
|
72
|
-
const title =
|
|
73
|
-
const summary = requireText(entry?.summary, `${lang} capability summary`).replace(/\s+/g, ' ')
|
|
74
|
-
const disables = requireText(entry?.disables, `${lang} capability disables`).replace(/\s+/g, ' ')
|
|
118
|
+
const { title, summary, disables } = rowText(entry, lang)
|
|
75
119
|
// Chinese prose does not space CJK punctuation, so the label joins directly.
|
|
76
120
|
const space = lang === 'zh' ? '' : ' '
|
|
77
121
|
return {
|
|
@@ -80,23 +124,57 @@ export function renderCapabilityMeta(entry, lang) {
|
|
|
80
124
|
}
|
|
81
125
|
|
|
82
126
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
127
|
+
* Compose the display metadata for one preset-declaring row in one language.
|
|
128
|
+
* The mandatory worker dependency is labelled as a dependency, never as a
|
|
129
|
+
* switch; the conditional reviewer keeps the ordinary off-effect label.
|
|
130
|
+
* @param {{ title: string, summary: string, disables: string }} entry
|
|
131
|
+
* @param {'en' | 'zh'} lang
|
|
132
|
+
* @param {boolean} mandatory
|
|
86
133
|
*/
|
|
87
|
-
export function
|
|
134
|
+
export function renderPresetMeta(entry, lang, mandatory) {
|
|
135
|
+
const { title, summary, disables } = rowText(entry, lang)
|
|
136
|
+
const space = lang === 'zh' ? '' : ' '
|
|
137
|
+
const label = mandatory ? DEPENDENCY_LABEL[lang] : OFF_EFFECT_LABEL[lang]
|
|
138
|
+
return {
|
|
139
|
+
meta: { title, description: `${summary}${space}${label}${space}${disables}` },
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function readLocales(root) {
|
|
88
144
|
const locales = {}
|
|
89
145
|
for (const lang of LANGUAGES) {
|
|
90
146
|
const file = path.join(root, LANGUAGE_FILES[lang])
|
|
91
147
|
if (!fs.existsSync(file)) throw new Error(`${LANGUAGE_FILES[lang]} is missing under ${root}`)
|
|
92
148
|
locales[lang] = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
93
149
|
}
|
|
150
|
+
return locales
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Pull one row's components entry out of both locales, or throw a named error. */
|
|
154
|
+
function componentEntry(locales, lang, component) {
|
|
155
|
+
const entry = locales[lang]?.components?.[component]
|
|
156
|
+
if (!entry) throw new Error(`${LANGUAGE_FILES[lang]} has no components.${component} entry`)
|
|
157
|
+
return entry
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Render every generated file for one repository root.
|
|
162
|
+
* @param {string} root
|
|
163
|
+
* @returns {Map<string, string>} repo-relative path -> file text
|
|
164
|
+
*/
|
|
165
|
+
export function renderRowLocaleFiles(root) {
|
|
166
|
+
const locales = readLocales(root)
|
|
94
167
|
const files = new Map()
|
|
95
168
|
for (const { specifier, component } of CAPABILITY_LOCALES) {
|
|
96
169
|
for (const lang of LANGUAGES) {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
170
|
+
const rendered = renderCapabilityMeta(componentEntry(locales, lang, component), lang)
|
|
171
|
+
files.set(`locale/${specifier}/${lang}.json`, `${JSON.stringify(rendered, null, 2)}\n`)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
for (const { specifier, component, mandatory } of PRESET_LOCALES) {
|
|
175
|
+
for (const lang of LANGUAGES) {
|
|
176
|
+
const rendered = renderPresetMeta(componentEntry(locales, lang, component), lang, mandatory)
|
|
177
|
+
files.set(`locale/${specifier}/${lang}.json`, `${JSON.stringify(rendered, null, 2)}\n`)
|
|
100
178
|
}
|
|
101
179
|
}
|
|
102
180
|
return files
|
|
@@ -116,11 +194,12 @@ function main(argv) {
|
|
|
116
194
|
--help print this text
|
|
117
195
|
|
|
118
196
|
The files are generated from the components block of locale/en.json and
|
|
119
|
-
locale/zh.json; do not edit locale/<
|
|
197
|
+
locale/zh.json; do not edit locale/<row>/<lang>.json by hand. Capability and
|
|
198
|
+
preset-declaring rows share this generator so their labels cannot drift.`)
|
|
120
199
|
return 0
|
|
121
200
|
}
|
|
122
201
|
const root = path.resolve(options.root)
|
|
123
|
-
const files =
|
|
202
|
+
const files = renderRowLocaleFiles(root)
|
|
124
203
|
if (options.check) {
|
|
125
204
|
const stale = []
|
|
126
205
|
for (const [relative, expected] of files) {
|
|
@@ -129,11 +208,11 @@ locale/zh.json; do not edit locale/<capability>/<lang>.json by hand.`)
|
|
|
129
208
|
if (actual !== expected) stale.push(relative)
|
|
130
209
|
}
|
|
131
210
|
if (stale.length > 0) {
|
|
132
|
-
console.error('
|
|
211
|
+
console.error('row locale files are stale; run npm run docs:generate:locale')
|
|
133
212
|
for (const relative of stale) console.error(` ${relative}`)
|
|
134
213
|
return 1
|
|
135
214
|
}
|
|
136
|
-
console.log(`
|
|
215
|
+
console.log(`row locales: ${files.size} files in sync (${ROW_LOCALES.length} specifiers, en/zh)`)
|
|
137
216
|
return 0
|
|
138
217
|
}
|
|
139
218
|
for (const [relative, text] of files) {
|
|
@@ -141,7 +220,7 @@ locale/zh.json; do not edit locale/<capability>/<lang>.json by hand.`)
|
|
|
141
220
|
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
142
221
|
fs.writeFileSync(target, text)
|
|
143
222
|
}
|
|
144
|
-
console.log(`wrote ${files.size}
|
|
223
|
+
console.log(`wrote ${files.size} row locale files (${ROW_LOCALES.length} specifiers, en/zh)`)
|
|
145
224
|
return 0
|
|
146
225
|
}
|
|
147
226
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import fs from 'node:fs'
|
|
12
12
|
import path from 'node:path'
|
|
13
13
|
import { pathToFileURL } from 'node:url'
|
|
14
|
+
import { PRESET_SPECIFIERS } from './surface.mjs'
|
|
14
15
|
|
|
15
16
|
function parseArgs(argv) {
|
|
16
17
|
const options = {}
|
|
@@ -45,7 +46,7 @@ for (const file of [profileBoot, appBoot]) {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
const { runProfile } = await import(pathToFileURL(profileBoot).href)
|
|
48
|
-
const { loadLayeredEnv } = await import(pathToFileURL(appBoot).href)
|
|
49
|
+
const { loadLayeredEnv, readPluginMeta } = await import(pathToFileURL(appBoot).href)
|
|
49
50
|
if (typeof runProfile !== 'function') throw new Error(`probe: host ${hostVersion} does not export runProfile`)
|
|
50
51
|
|
|
51
52
|
/** Run one surface read, keeping a missing service a reportable value. */
|
|
@@ -64,7 +65,7 @@ const { ctx, shutdown } = await runProfile({
|
|
|
64
65
|
args: [],
|
|
65
66
|
})
|
|
66
67
|
|
|
67
|
-
const surface = { host: { version: hostVersion }, profile: {}, tools: null, skills: null, presets: null, commands: null, tree: null }
|
|
68
|
+
const surface = { host: { version: hostVersion }, profile: {}, tools: null, skills: null, presets: null, labels: null, commands: null, tree: null }
|
|
68
69
|
try {
|
|
69
70
|
const profileContext = ctx.get('profileContext')
|
|
70
71
|
surface.profile = {
|
|
@@ -88,9 +89,38 @@ try {
|
|
|
88
89
|
|
|
89
90
|
const presets = ctx.get('agentPresets')
|
|
90
91
|
surface.presets = presets
|
|
91
|
-
? await read(async () =>
|
|
92
|
+
? await read(async () => {
|
|
93
|
+
// Both preset rows declare themselves through Tower-owned modules
|
|
94
|
+
// (`dsh-tower/preset-*`). Keep the display metadata and the activation
|
|
95
|
+
// diagnostic so the smoke can prove each row still mounts, not only
|
|
96
|
+
// that two ids exist (#121).
|
|
97
|
+
const list = await presets.list()
|
|
98
|
+
return {
|
|
99
|
+
ids: list.map((preset) => preset.id),
|
|
100
|
+
entries: list.map(({ id, name, description, order, broken }) => ({
|
|
101
|
+
id,
|
|
102
|
+
...name === undefined ? {} : { name },
|
|
103
|
+
...description === undefined ? {} : { description },
|
|
104
|
+
...order === undefined ? {} : { order },
|
|
105
|
+
...broken === undefined ? {} : { broken },
|
|
106
|
+
})),
|
|
107
|
+
}
|
|
108
|
+
})
|
|
92
109
|
: { error: 'the agentPresets service is not mounted (profile has no preset registry)' }
|
|
93
110
|
|
|
111
|
+
// The Plugins page labels a row through this exact call. Resolve both preset
|
|
112
|
+
// row specifiers from the installed profile so the smoke proves the two rows
|
|
113
|
+
// render distinct titles/descriptions instead of one shared label (#121).
|
|
114
|
+
surface.labels =
|
|
115
|
+
typeof readPluginMeta === 'function' && typeof profileContext?.dir === 'string'
|
|
116
|
+
? await read(() => {
|
|
117
|
+
const base = pathToFileURL(path.join(profileContext.dir, 'cordis.yml')).href
|
|
118
|
+
return Object.fromEntries(
|
|
119
|
+
PRESET_SPECIFIERS.map((specifier) => [specifier, readPluginMeta(specifier, base) ?? null]),
|
|
120
|
+
)
|
|
121
|
+
})
|
|
122
|
+
: { error: 'the host exports no readPluginMeta, so preset row labels cannot be read' }
|
|
123
|
+
|
|
94
124
|
const commands = ctx.get('commands')
|
|
95
125
|
surface.commands = commands
|
|
96
126
|
? await read(() => commands.list(undefined).map((command) => command.name))
|