jishushell 0.4.24 → 0.5.15
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/INSTALL-NOTICE +11 -0
- package/apps/anythingllm-container.yaml +287 -0
- package/apps/browserless-chromium-container.yaml +90 -0
- package/apps/filebrowser-container.yaml +163 -0
- package/apps/hermes-container.yaml +36 -2
- package/apps/ollama-binary.yaml +91 -90
- package/apps/ollama-cpu-container.yaml +8 -1
- package/apps/ollama-with-hollama-binary.yaml +91 -90
- package/apps/openclaw-binary.yaml +38 -1
- package/apps/openclaw-container.yaml +45 -2
- package/apps/openclaw-with-ollama-container.yaml +11 -2
- package/apps/openclaw-with-searxng-container.yaml +26 -2
- package/apps/openwebui-container.yaml +45 -1
- package/apps/playwright-container.yaml +7 -1
- package/apps/searxng-container.yaml +58 -7
- package/apps/weknora-container.yaml +471 -0
- package/dist/cli/app.js +79 -9
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/doctor.d.ts +12 -12
- package/dist/cli/doctor.js +242 -55
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/llm.d.ts +4 -3
- package/dist/cli/llm.js +4 -3
- package/dist/cli/llm.js.map +1 -1
- package/dist/cli/panel.d.ts +6 -5
- package/dist/cli/panel.js +10 -9
- package/dist/cli/panel.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.js +99 -1
- package/dist/config.js.map +1 -1
- package/dist/control.d.ts +7 -6
- package/dist/control.js +7 -6
- package/dist/control.js.map +1 -1
- package/dist/install.js +3 -3
- package/dist/install.js.map +1 -1
- package/dist/routes/agent-apps.d.ts +1 -1
- package/dist/routes/agent-apps.js +1 -1
- package/dist/routes/apps.js +44 -11
- package/dist/routes/apps.js.map +1 -1
- package/dist/routes/auth.js +5 -2
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/backup.js +64 -11
- package/dist/routes/backup.js.map +1 -1
- package/dist/routes/external-mounts.d.ts +17 -0
- package/dist/routes/external-mounts.js +73 -0
- package/dist/routes/external-mounts.js.map +1 -0
- package/dist/routes/file-mounts.d.ts +13 -0
- package/dist/routes/file-mounts.js +90 -0
- package/dist/routes/file-mounts.js.map +1 -0
- package/dist/routes/files-organize.d.ts +28 -0
- package/dist/routes/files-organize.js +167 -0
- package/dist/routes/files-organize.js.map +1 -0
- package/dist/routes/files.d.ts +31 -0
- package/dist/routes/files.js +321 -0
- package/dist/routes/files.js.map +1 -0
- package/dist/routes/instances.js +826 -17
- package/dist/routes/instances.js.map +1 -1
- package/dist/routes/internal.d.ts +2 -0
- package/dist/routes/internal.js +59 -0
- package/dist/routes/internal.js.map +1 -0
- package/dist/routes/llm.js +24 -35
- package/dist/routes/llm.js.map +1 -1
- package/dist/routes/setup.js +10 -10
- package/dist/routes/setup.js.map +1 -1
- package/dist/routes/system.js +1 -1
- package/dist/routes/system.js.map +1 -1
- package/dist/routes/webdav.d.ts +17 -0
- package/dist/routes/webdav.js +114 -0
- package/dist/routes/webdav.js.map +1 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.js +751 -20
- package/dist/server.js.map +1 -1
- package/dist/services/agent-apps/catalog.js +4 -3
- package/dist/services/agent-apps/catalog.js.map +1 -1
- package/dist/services/agent-apps/index.d.ts +1 -1
- package/dist/services/agent-apps/index.js +1 -1
- package/dist/services/agent-apps/installers/adapter.d.ts +1 -1
- package/dist/services/agent-apps/installers/adapter.js +1 -1
- package/dist/services/agent-apps/installers/shell-script.d.ts +1 -1
- package/dist/services/agent-apps/installers/shell-script.js +3 -3
- package/dist/services/agent-apps/installers/shell-script.js.map +1 -1
- package/dist/services/agent-apps/types.d.ts +2 -2
- package/dist/services/agent-apps/types.js +1 -1
- package/dist/services/app/app-compiler.d.ts +1 -1
- package/dist/services/app/app-compiler.js +5 -5
- package/dist/services/app/app-compiler.js.map +1 -1
- package/dist/services/app/app-manager.d.ts +25 -1
- package/dist/services/app/app-manager.js +829 -150
- package/dist/services/app/app-manager.js.map +1 -1
- package/dist/services/app/custom-manager.js.map +1 -1
- package/dist/services/app/hermes-agent-manager.js +7 -4
- package/dist/services/app/hermes-agent-manager.js.map +1 -1
- package/dist/services/app/ollama-manager.js +1 -1
- package/dist/services/app/ollama-manager.js.map +1 -1
- package/dist/services/app/openclaw-manager.js +20 -3
- package/dist/services/app/openclaw-manager.js.map +1 -1
- package/dist/services/app/platform-transform.d.ts +32 -0
- package/dist/services/app/platform-transform.js +65 -0
- package/dist/services/app/platform-transform.js.map +1 -0
- package/dist/services/app/provide-resolver.d.ts +29 -0
- package/dist/services/app/provide-resolver.js +112 -0
- package/dist/services/app/provide-resolver.js.map +1 -0
- package/dist/services/app-passwords.d.ts +61 -0
- package/dist/services/app-passwords.js +173 -0
- package/dist/services/app-passwords.js.map +1 -0
- package/dist/services/backup-manager.d.ts +11 -0
- package/dist/services/backup-manager.js +177 -4
- package/dist/services/backup-manager.js.map +1 -1
- package/dist/services/capability-endpoint-validator.d.ts +41 -0
- package/dist/services/capability-endpoint-validator.js +104 -0
- package/dist/services/capability-endpoint-validator.js.map +1 -0
- package/dist/services/capability-health.d.ts +16 -0
- package/dist/services/capability-health.js +121 -0
- package/dist/services/capability-health.js.map +1 -0
- package/dist/services/capability-registry.d.ts +106 -0
- package/dist/services/capability-registry.js +313 -0
- package/dist/services/capability-registry.js.map +1 -0
- package/dist/services/connection-apply.d.ts +91 -0
- package/dist/services/connection-apply.js +475 -0
- package/dist/services/connection-apply.js.map +1 -0
- package/dist/services/connection-resolver.d.ts +65 -0
- package/dist/services/connection-resolver.js +281 -0
- package/dist/services/connection-resolver.js.map +1 -0
- package/dist/services/connection-transactor.d.ts +39 -0
- package/dist/services/connection-transactor.js +351 -0
- package/dist/services/connection-transactor.js.map +1 -0
- package/dist/services/external-mounts.d.ts +40 -0
- package/dist/services/external-mounts.js +187 -0
- package/dist/services/external-mounts.js.map +1 -0
- package/dist/services/files-manager.d.ts +252 -0
- package/dist/services/files-manager.js +1075 -0
- package/dist/services/files-manager.js.map +1 -0
- package/dist/services/files-mounts.d.ts +42 -0
- package/dist/services/files-mounts.js +207 -0
- package/dist/services/files-mounts.js.map +1 -0
- package/dist/services/instance-manager.d.ts +13 -0
- package/dist/services/instance-manager.js +138 -46
- package/dist/services/instance-manager.js.map +1 -1
- package/dist/services/llm-proxy/index.d.ts +16 -2
- package/dist/services/llm-proxy/index.js +48 -44
- package/dist/services/llm-proxy/index.js.map +1 -1
- package/dist/services/llm-proxy/probe.d.ts +6 -0
- package/dist/services/llm-proxy/probe.js +85 -0
- package/dist/services/llm-proxy/probe.js.map +1 -0
- package/dist/services/llm-proxy/ssrf.d.ts +1 -0
- package/dist/services/llm-proxy/ssrf.js +24 -9
- package/dist/services/llm-proxy/ssrf.js.map +1 -1
- package/dist/services/nomad-manager.d.ts +4 -0
- package/dist/services/nomad-manager.js +428 -35
- package/dist/services/nomad-manager.js.map +1 -1
- package/dist/services/organize/applier.d.ts +46 -0
- package/dist/services/organize/applier.js +218 -0
- package/dist/services/organize/applier.js.map +1 -0
- package/dist/services/organize/rules.d.ts +57 -0
- package/dist/services/organize/rules.js +286 -0
- package/dist/services/organize/rules.js.map +1 -0
- package/dist/services/organize/scanner.d.ts +50 -0
- package/dist/services/organize/scanner.js +366 -0
- package/dist/services/organize/scanner.js.map +1 -0
- package/dist/services/organize/store.d.ts +14 -0
- package/dist/services/organize/store.js +82 -0
- package/dist/services/organize/store.js.map +1 -0
- package/dist/services/panel-manager.js +20 -1
- package/dist/services/panel-manager.js.map +1 -1
- package/dist/services/process-manager.js +4 -3
- package/dist/services/process-manager.js.map +1 -1
- package/dist/services/runtime/adapters/hermes.d.ts +30 -1
- package/dist/services/runtime/adapters/hermes.js +219 -6
- package/dist/services/runtime/adapters/hermes.js.map +1 -1
- package/dist/services/runtime/adapters/openclaw-mcporter.d.ts +45 -0
- package/dist/services/runtime/adapters/openclaw-mcporter.js +108 -0
- package/dist/services/runtime/adapters/openclaw-mcporter.js.map +1 -0
- package/dist/services/runtime/adapters/openclaw-routes.d.ts +8 -2
- package/dist/services/runtime/adapters/openclaw-routes.js +68 -0
- package/dist/services/runtime/adapters/openclaw-routes.js.map +1 -1
- package/dist/services/runtime/adapters/openclaw.d.ts +177 -0
- package/dist/services/runtime/adapters/openclaw.js +1171 -11
- package/dist/services/runtime/adapters/openclaw.js.map +1 -1
- package/dist/services/runtime/instance.d.ts +1 -1
- package/dist/services/runtime/instance.js +1 -1
- package/dist/services/runtime/instance.js.map +1 -1
- package/dist/services/runtime/mcp-shims/anythingllm-shim.d.ts +46 -0
- package/dist/services/runtime/mcp-shims/anythingllm-shim.js +281 -0
- package/dist/services/runtime/mcp-shims/anythingllm-shim.js.map +1 -0
- package/dist/services/runtime/mcp-shims/drive-shim.d.ts +54 -0
- package/dist/services/runtime/mcp-shims/drive-shim.js +489 -0
- package/dist/services/runtime/mcp-shims/drive-shim.js.map +1 -0
- package/dist/services/runtime/mcp-shims/firewall.d.ts +26 -0
- package/dist/services/runtime/mcp-shims/firewall.js +129 -0
- package/dist/services/runtime/mcp-shims/firewall.js.map +1 -0
- package/dist/services/runtime/mcp-shims/searxng-shim.d.ts +27 -0
- package/dist/services/runtime/mcp-shims/searxng-shim.js +125 -0
- package/dist/services/runtime/mcp-shims/searxng-shim.js.map +1 -0
- package/dist/services/runtime/mcp-shims/write-mcp-entry.d.ts +83 -0
- package/dist/services/runtime/mcp-shims/write-mcp-entry.js +127 -0
- package/dist/services/runtime/mcp-shims/write-mcp-entry.js.map +1 -0
- package/dist/services/runtime/migrations.d.ts +8 -0
- package/dist/services/runtime/migrations.js +100 -0
- package/dist/services/runtime/migrations.js.map +1 -1
- package/dist/services/runtime/types.d.ts +46 -0
- package/dist/services/setup-manager.js +99 -24
- package/dist/services/setup-manager.js.map +1 -1
- package/dist/services/suggestions.d.ts +27 -0
- package/dist/services/suggestions.js +133 -0
- package/dist/services/suggestions.js.map +1 -0
- package/dist/services/task-registry.js +4 -2
- package/dist/services/task-registry.js.map +1 -1
- package/dist/services/telemetry/device-fingerprint.d.ts +1 -1
- package/dist/services/telemetry/device-fingerprint.js +1 -1
- package/dist/services/types-shim.d.ts +16 -0
- package/dist/services/types-shim.js +2 -0
- package/dist/services/types-shim.js.map +1 -0
- package/dist/services/webdav/server.d.ts +24 -0
- package/dist/services/webdav/server.js +420 -0
- package/dist/services/webdav/server.js.map +1 -0
- package/dist/services/webdav/xml-builder.d.ts +73 -0
- package/dist/services/webdav/xml-builder.js +156 -0
- package/dist/services/webdav/xml-builder.js.map +1 -0
- package/dist/services/workspace-builder.d.ts +29 -0
- package/dist/services/workspace-builder.js +188 -0
- package/dist/services/workspace-builder.js.map +1 -0
- package/dist/types.d.ts +231 -1
- package/dist/utils/instance-lock.d.ts +22 -0
- package/dist/utils/instance-lock.js +48 -0
- package/dist/utils/instance-lock.js.map +1 -0
- package/dist/utils/path-locks.d.ts +30 -0
- package/dist/utils/path-locks.js +63 -0
- package/dist/utils/path-locks.js.map +1 -0
- package/dist/utils/path-safety.d.ts +41 -0
- package/dist/utils/path-safety.js +119 -0
- package/dist/utils/path-safety.js.map +1 -0
- package/dist/utils/safe-json.js +55 -22
- package/dist/utils/safe-json.js.map +1 -1
- package/dist/utils/safe-write.d.ts +24 -0
- package/dist/utils/safe-write.js +82 -0
- package/dist/utils/safe-write.js.map +1 -0
- package/install/jishu-install.sh +323 -27
- package/install/jishu-uninstall.sh +353 -20
- package/package.json +18 -1
- package/public/assets/Dashboard-BdWPtroF.js +1 -0
- package/public/assets/{HermesChatPanel-mFSureyc.js → HermesChatPanel-B_2HlVBQ.js} +1 -1
- package/public/assets/HermesConfigForm-DVlhg3WV.js +4 -0
- package/public/assets/{InitPassword-CVA8wQA6.js → InitPassword-D7glTExX.js} +1 -1
- package/public/assets/InstanceDetail-CxSy2cpe.js +92 -0
- package/public/assets/{Login-BWsZH2mu.js → Login-Cfr5c2sv.js} +1 -1
- package/public/assets/NewInstance-BIYDmJis.js +1 -0
- package/public/assets/ProviderRecommendations-BuRnvRcI.js +1 -0
- package/public/assets/Settings-Cc-tYBil.js +1 -0
- package/public/assets/Setup-lGZEk5jq.js +1 -0
- package/public/assets/{WeixinLoginPanel-CnjR8xMu.js → WeixinLoginPanel-CoGqzxeV.js} +2 -2
- package/public/assets/index-87IJXG-w.css +1 -0
- package/public/assets/index-BZc5zH7u.js +19 -0
- package/public/assets/providers-DtNXh9JD.js +1 -0
- package/public/assets/registry-BWnkJgZ1.js +2 -0
- package/public/assets/{usePolling-Do5Erqm_.js → usePolling-CwwT9KrC.js} +1 -1
- package/public/assets/{vendor-i18n-ucpM0OR0.js → vendor-i18n-y9V7Sfuu.js} +1 -1
- package/public/assets/{vendor-react-Bk1hRGiY.js → vendor-react-BWrEVJVb.js} +6 -6
- package/public/index.html +4 -4
- package/scripts/check-app-spec.mjs +457 -0
- package/scripts/check-i18n.mjs +154 -0
- package/scripts/check-new-file-tests.mjs +230 -0
- package/scripts/check-quarantine-expiry.mjs +105 -0
- package/scripts/perf/README.md +49 -0
- package/scripts/perf/auth.js +99 -0
- package/scripts/perf/config.js +63 -0
- package/scripts/perf/instances.js +143 -0
- package/scripts/perf/proxy.js +96 -0
- package/scripts/run.sh +4 -4
- package/scripts/smoke/files-w1.sh +142 -0
- package/scripts/smoke-backend.mjs +122 -0
- package/scripts/smoke-post-publish.mjs +346 -0
- package/public/assets/Dashboard-B-JoOjBQ.js +0 -1
- package/public/assets/HermesConfigForm-DvR05LK1.js +0 -4
- package/public/assets/InstanceDetail-DcZW2QGO.js +0 -91
- package/public/assets/NewInstance-BCIrAd86.js +0 -1
- package/public/assets/Settings-xkDcduFz.js +0 -1
- package/public/assets/Setup-Cfuwj4gV.js +0 -1
- package/public/assets/index-CPhVFEsx.css +0 -1
- package/public/assets/index-DQsM6Joa.js +0 -19
- package/public/assets/providers-V-vwrExZ.js +0 -1
- package/public/assets/registry-B4UFJdpA.js +0 -2
package/public/index.html
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
from the instance cards, making the panel look like Hermes. -->
|
|
10
10
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
11
11
|
<title>JishuShell</title>
|
|
12
|
-
<script type="module" crossorigin src="/assets/index-
|
|
13
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-react-
|
|
14
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-i18n-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
12
|
+
<script type="module" crossorigin src="/assets/index-BZc5zH7u.js"></script>
|
|
13
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-react-BWrEVJVb.js">
|
|
14
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-i18n-y9V7Sfuu.js">
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-87IJXG-w.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body class="bg-white min-h-screen">
|
|
18
18
|
<div id="root"></div>
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Validates `apps/*.yaml` against the Provider Authoring Contract
|
|
4
|
+
* (docs/app-interconnect-design.md §13.2.3).
|
|
5
|
+
*
|
|
6
|
+
* Three provide classes:
|
|
7
|
+
* A. Bindable category — capability has CAPABILITY_CATEGORIES prefix
|
|
8
|
+
* (`llm-` / `search-` / `browser-` / `mcp-`). Must declare task,
|
|
9
|
+
* port (resolvable to a task port via host_port ?? port), protocol
|
|
10
|
+
* from category whitelist, optional health, description.
|
|
11
|
+
* B. Legacy bindable exact — capability is in LEGACY_BINDABLE_EXACT_ALLOWLIST
|
|
12
|
+
* OR appears in some yaml's `requires.capability`. Same field
|
|
13
|
+
* requirements as A but protocol whitelist is `default` (broader).
|
|
14
|
+
* C. UI-only — capability is in UI_LEGACY_ALLOWLIST or matches a
|
|
15
|
+
* `web-*` / `*-terminal` / `*-dashboard` / `*-ui` pattern. Looser
|
|
16
|
+
* checks: only naming + (port resolves OR no port for terminal-only).
|
|
17
|
+
*
|
|
18
|
+
* `requires[*]` checks: capability is a category token OR is in
|
|
19
|
+
* `allowedRequiresExact = LEGACY_BINDABLE_EXACT_ALLOWLIST ∪ exactRequiresInRepo`.
|
|
20
|
+
* Plus an "orphan require" check that matches each require to at least one
|
|
21
|
+
* provide (severity gated by `required` — error for required:true, warning
|
|
22
|
+
* otherwise).
|
|
23
|
+
*/
|
|
24
|
+
import { readFileSync, readdirSync, } from "fs";
|
|
25
|
+
import { join, dirname } from "path";
|
|
26
|
+
import { fileURLToPath } from "url";
|
|
27
|
+
import { parse as parseYaml } from "yaml";
|
|
28
|
+
|
|
29
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
const APPS_DIR = process.env.APPS_DIR_OVERRIDE ?? join(__dirname, "..", "apps");
|
|
31
|
+
|
|
32
|
+
// ── Allowlists (kept here so check-app-spec stays mechanically driven) ──
|
|
33
|
+
|
|
34
|
+
// Keep in sync with CAPABILITY_CATEGORIES in
|
|
35
|
+
// src/services/connection-resolver.ts. `files` (W1-W2) and `knowledge`
|
|
36
|
+
// (W2, AnythingLLM) are full categories with adapter hooks and slot UX.
|
|
37
|
+
const CAPABILITY_CATEGORIES = new Set([
|
|
38
|
+
"llm", "search", "browser", "mcp", "files", "knowledge",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
const LEGACY_BINDABLE_EXACT_ALLOWLIST = new Set([
|
|
42
|
+
"ollama-api",
|
|
43
|
+
"browserless-api",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const UI_LEGACY_ALLOWLIST = new Set([
|
|
47
|
+
"openwebui-web",
|
|
48
|
+
"hollama-web",
|
|
49
|
+
"playwright-ui",
|
|
50
|
+
"openclaw-dashboard",
|
|
51
|
+
"ollama-terminal",
|
|
52
|
+
"web-ui",
|
|
53
|
+
"web-searxng",
|
|
54
|
+
"web-openwebui",
|
|
55
|
+
"web-hermes",
|
|
56
|
+
"browserless-debugger",
|
|
57
|
+
"browserless-docs",
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const PROTOCOL_WHITELIST = {
|
|
61
|
+
llm: new Set(["http", "https"]),
|
|
62
|
+
search: new Set(["http", "https"]),
|
|
63
|
+
browser: new Set(["ws", "wss", "http", "https"]),
|
|
64
|
+
mcp: new Set(["http", "https", "ws", "sse"]),
|
|
65
|
+
// Filebrowser exposes an HTTP Web UI under /apps/filebrowser; webdav adds
|
|
66
|
+
// PROPFIND/PUT over the same HTTP origin. Both fit `http(s)`.
|
|
67
|
+
files: new Set(["http", "https"]),
|
|
68
|
+
// AnythingLLM exposes an OpenAI-compatible HTTP API for kb_search; baseUrl
|
|
69
|
+
// is always http(s) regardless of LAN vs container host topology.
|
|
70
|
+
knowledge: new Set(["http", "https"]),
|
|
71
|
+
default: new Set(["http", "https", "ws", "wss", "tcp"]),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// ── Helpers ──
|
|
75
|
+
|
|
76
|
+
function categoryOf(capability) {
|
|
77
|
+
const dashIdx = capability.indexOf("-");
|
|
78
|
+
if (dashIdx <= 0) return null;
|
|
79
|
+
const head = capability.slice(0, dashIdx);
|
|
80
|
+
return CAPABILITY_CATEGORIES.has(head) ? head : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isUILegacy(capability) {
|
|
84
|
+
if (UI_LEGACY_ALLOWLIST.has(capability)) return true;
|
|
85
|
+
if (capability.startsWith("web-")) return true;
|
|
86
|
+
return /-(terminal|dashboard|ui)$/.test(capability);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Auth block validation (§6 of agents-as-llm-provider spec) ──
|
|
90
|
+
|
|
91
|
+
const AUTH_KINDS = new Set(["none", "bearer", "header", "query"]);
|
|
92
|
+
const AUTH_OPTIONAL_FIELDS_BY_KIND = {
|
|
93
|
+
none: new Set(),
|
|
94
|
+
bearer: new Set(["headerName", "tokenPrefix"]),
|
|
95
|
+
header: new Set(["headerName", "tokenPrefix"]),
|
|
96
|
+
query: new Set(["headerName", "tokenPrefix"]),
|
|
97
|
+
};
|
|
98
|
+
const TOKEN_SOURCE_PATTERNS = [
|
|
99
|
+
/^instance\.env\.[A-Z][A-Z0-9_]*$/,
|
|
100
|
+
/^instance\.config\..+$/,
|
|
101
|
+
/^proxy\.token$/,
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Validate the optional `auth` block on a provides[] entry.
|
|
106
|
+
* Reports errors via the `err(file, msg)` callback; returns nothing.
|
|
107
|
+
*/
|
|
108
|
+
function validateAuth(file, capLabel, auth) {
|
|
109
|
+
if (auth === undefined || auth === null) return; // optional
|
|
110
|
+
|
|
111
|
+
if (typeof auth !== "object" || Array.isArray(auth)) {
|
|
112
|
+
err(file, `provide '${capLabel}' auth must be an object`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const kind = auth.kind;
|
|
117
|
+
if (typeof kind !== "string" || !AUTH_KINDS.has(kind)) {
|
|
118
|
+
err(file, `provide '${capLabel}' auth.kind must be one of ${[...AUTH_KINDS].join("|")}`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Reject unknown keys
|
|
123
|
+
const allowed = new Set(["kind"]);
|
|
124
|
+
if (kind !== "none") allowed.add("tokenSource");
|
|
125
|
+
for (const opt of AUTH_OPTIONAL_FIELDS_BY_KIND[kind]) allowed.add(opt);
|
|
126
|
+
for (const key of Object.keys(auth)) {
|
|
127
|
+
if (!allowed.has(key)) {
|
|
128
|
+
err(file, `provide '${capLabel}' auth has unknown key '${key}' for kind '${kind}'`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (kind === "none") {
|
|
133
|
+
return; // nothing more to check
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// tokenSource required for non-none kinds
|
|
137
|
+
if (typeof auth.tokenSource !== "string" || !auth.tokenSource) {
|
|
138
|
+
err(file, `provide '${capLabel}' auth.tokenSource required for kind '${kind}'`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const matchesPattern = TOKEN_SOURCE_PATTERNS.some((re) => re.test(auth.tokenSource));
|
|
142
|
+
if (!matchesPattern) {
|
|
143
|
+
err(
|
|
144
|
+
file,
|
|
145
|
+
`provide '${capLabel}' auth.tokenSource '${auth.tokenSource}' does not match any of: instance.env.<NAME>, instance.config.<path>, proxy.token`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (auth.headerName !== undefined) {
|
|
150
|
+
if (typeof auth.headerName !== "string" || !auth.headerName) {
|
|
151
|
+
err(file, `provide '${capLabel}' auth.headerName must be a non-empty string when present`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (auth.tokenPrefix !== undefined) {
|
|
155
|
+
if (typeof auth.tokenPrefix !== "string") {
|
|
156
|
+
err(file, `provide '${capLabel}' auth.tokenPrefix must be a string when present`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function findTask(spec, taskName) {
|
|
162
|
+
return (spec.tasks ?? []).find((t) => t.name === taskName);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function hostPortOf(p) {
|
|
166
|
+
if (typeof p?.host_port === "number" && p.host_port > 0) return p.host_port;
|
|
167
|
+
if (typeof p?.port === "number" && p.port > 0) return p.port;
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function findPortInTask(task, providePort) {
|
|
172
|
+
return (task?.ports ?? []).find((p) => hostPortOf(p) === providePort);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── Main ──
|
|
176
|
+
|
|
177
|
+
const errors = [];
|
|
178
|
+
const warnings = [];
|
|
179
|
+
|
|
180
|
+
function err(file, msg) {
|
|
181
|
+
errors.push(`${file}: ${msg}`);
|
|
182
|
+
}
|
|
183
|
+
function warn(file, msg) {
|
|
184
|
+
warnings.push(`${file}: ${msg}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const yamlFiles = readdirSync(APPS_DIR)
|
|
188
|
+
.filter((f) => f.endsWith(".yaml"))
|
|
189
|
+
.map((f) => join(APPS_DIR, f));
|
|
190
|
+
|
|
191
|
+
const specs = [];
|
|
192
|
+
for (const file of yamlFiles) {
|
|
193
|
+
try {
|
|
194
|
+
const text = readFileSync(file, "utf-8");
|
|
195
|
+
const doc = parseYaml(text);
|
|
196
|
+
if (!doc || typeof doc !== "object") {
|
|
197
|
+
err(file, "yaml does not parse to an object");
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
specs.push({ file: file.replace(APPS_DIR + "/", "apps/"), spec: doc });
|
|
201
|
+
} catch (e) {
|
|
202
|
+
err(file, `yaml parse failed: ${e.message}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// First pass: collect exact-name requires across the repo (B class membership).
|
|
207
|
+
const exactRequiresInRepo = new Set();
|
|
208
|
+
for (const { spec } of specs) {
|
|
209
|
+
for (const req of spec.requires ?? []) {
|
|
210
|
+
if (typeof req?.capability !== "string") continue;
|
|
211
|
+
if (CAPABILITY_CATEGORIES.has(req.capability)) continue;
|
|
212
|
+
exactRequiresInRepo.add(req.capability);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const allowedRequiresExact = new Set([
|
|
217
|
+
...LEGACY_BINDABLE_EXACT_ALLOWLIST,
|
|
218
|
+
...exactRequiresInRepo,
|
|
219
|
+
]);
|
|
220
|
+
|
|
221
|
+
// Index every provide capability across the repo for orphan-require check.
|
|
222
|
+
const providersByCapability = new Map();
|
|
223
|
+
for (const { spec } of specs) {
|
|
224
|
+
for (const provide of spec.provides ?? []) {
|
|
225
|
+
if (typeof provide?.capability !== "string") continue;
|
|
226
|
+
if (!providersByCapability.has(provide.capability)) {
|
|
227
|
+
providersByCapability.set(provide.capability, []);
|
|
228
|
+
}
|
|
229
|
+
providersByCapability.get(provide.capability).push({ spec });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function hasProviderForRequire(req) {
|
|
234
|
+
// A path: prefix match against any provide name starting with `${req}-`.
|
|
235
|
+
if (CAPABILITY_CATEGORIES.has(req)) {
|
|
236
|
+
for (const cap of providersByCapability.keys()) {
|
|
237
|
+
if (cap.startsWith(req + "-")) return true;
|
|
238
|
+
}
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
// B path: exact match.
|
|
242
|
+
return providersByCapability.has(req);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Second pass: validate each spec.
|
|
246
|
+
for (const { file, spec } of specs) {
|
|
247
|
+
const _id = typeof spec.id === "string" ? spec.id : "<no id>";
|
|
248
|
+
|
|
249
|
+
// ── provides[*] ──────────────────────────────────────────────────────
|
|
250
|
+
for (const provide of spec.provides ?? []) {
|
|
251
|
+
if (typeof provide?.capability !== "string" || !provide.capability) {
|
|
252
|
+
err(file, `provide missing capability`);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const cap = provide.capability;
|
|
256
|
+
const cat = categoryOf(cap);
|
|
257
|
+
const isB =
|
|
258
|
+
!cat &&
|
|
259
|
+
(LEGACY_BINDABLE_EXACT_ALLOWLIST.has(cap) || exactRequiresInRepo.has(cap)) &&
|
|
260
|
+
!UI_LEGACY_ALLOWLIST.has(cap);
|
|
261
|
+
const isUI = !cat && !isB && isUILegacy(cap);
|
|
262
|
+
|
|
263
|
+
// url-only provide: skip endpoint checks (not in registry per §5.1).
|
|
264
|
+
if (typeof provide.url === "string" && provide.url.trim()) {
|
|
265
|
+
if (provide.auth !== undefined) {
|
|
266
|
+
err(file, `provide '${cap}' auth is not allowed on UI-only or url-only provides`);
|
|
267
|
+
}
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (cat) {
|
|
272
|
+
// Class A
|
|
273
|
+
if (typeof provide.task !== "string" || !provide.task) {
|
|
274
|
+
err(file, `provide '${cap}' (A) missing task`);
|
|
275
|
+
} else if (!findTask(spec, provide.task)) {
|
|
276
|
+
err(file, `provide '${cap}' (A) task '${provide.task}' not declared in tasks[]`);
|
|
277
|
+
}
|
|
278
|
+
if (typeof provide.port !== "number") {
|
|
279
|
+
err(file, `provide '${cap}' (A) missing port`);
|
|
280
|
+
} else {
|
|
281
|
+
const task = findTask(spec, provide.task);
|
|
282
|
+
if (task && !findPortInTask(task, provide.port)) {
|
|
283
|
+
err(file, `provide '${cap}' (A) port ${provide.port} not in task '${provide.task}' ports`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const proto = (provide.protocol ?? "http").toLowerCase();
|
|
287
|
+
if (!PROTOCOL_WHITELIST[cat].has(proto)) {
|
|
288
|
+
err(file, `provide '${cap}' (A) protocol '${proto}' not in ${cat} whitelist`);
|
|
289
|
+
}
|
|
290
|
+
if (typeof provide.description !== "string" || !provide.description.trim()) {
|
|
291
|
+
warn(file, `provide '${cap}' (A) missing description`);
|
|
292
|
+
}
|
|
293
|
+
validateAuth(file, cap, provide.auth);
|
|
294
|
+
} else if (isB) {
|
|
295
|
+
// Class B
|
|
296
|
+
if (typeof provide.task !== "string" || !provide.task) {
|
|
297
|
+
err(file, `provide '${cap}' (B legacy bindable exact) missing task`);
|
|
298
|
+
} else if (!findTask(spec, provide.task)) {
|
|
299
|
+
err(file, `provide '${cap}' (B) task '${provide.task}' not declared`);
|
|
300
|
+
}
|
|
301
|
+
if (typeof provide.port !== "number") {
|
|
302
|
+
err(file, `provide '${cap}' (B) missing port`);
|
|
303
|
+
} else {
|
|
304
|
+
const task = findTask(spec, provide.task);
|
|
305
|
+
if (task && !findPortInTask(task, provide.port)) {
|
|
306
|
+
err(file, `provide '${cap}' (B) port ${provide.port} not in task '${provide.task}' ports`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const proto = (provide.protocol ?? "").toLowerCase();
|
|
310
|
+
if (!proto) {
|
|
311
|
+
err(file, `provide '${cap}' (B) protocol must be declared (one of ${[...PROTOCOL_WHITELIST.default].join("|")})`);
|
|
312
|
+
} else if (!PROTOCOL_WHITELIST.default.has(proto)) {
|
|
313
|
+
err(file, `provide '${cap}' (B) protocol '${proto}' not in default whitelist`);
|
|
314
|
+
}
|
|
315
|
+
if (typeof provide.description !== "string" || !provide.description.trim()) {
|
|
316
|
+
warn(file, `provide '${cap}' (B) missing description`);
|
|
317
|
+
}
|
|
318
|
+
validateAuth(file, cap, provide.auth);
|
|
319
|
+
} else if (isUI) {
|
|
320
|
+
// Class C — looser. Terminal capabilities legitimately have no port.
|
|
321
|
+
if (provide.auth !== undefined) {
|
|
322
|
+
err(file, `provide '${cap}' auth is not allowed on UI-only or url-only provides`);
|
|
323
|
+
}
|
|
324
|
+
if (typeof provide.port === "number") {
|
|
325
|
+
if (typeof provide.task === "string" && provide.task) {
|
|
326
|
+
const task = findTask(spec, provide.task);
|
|
327
|
+
if (task && !findPortInTask(task, provide.port)) {
|
|
328
|
+
err(file, `provide '${cap}' (C) port ${provide.port} not in task '${provide.task}' ports`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
// Unrecognized — author must explicitly opt into one of A/B/C.
|
|
334
|
+
err(
|
|
335
|
+
file,
|
|
336
|
+
`provide '${cap}' is not recognized as A/B/C; rename with category prefix (${[...CAPABILITY_CATEGORIES].map((c) => c + "-").join("/")}), add to LEGACY_BINDABLE_EXACT_ALLOWLIST, or add to UI_LEGACY_ALLOWLIST`,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── §17 (PR 8): tool_schema validation — applies to any bindable
|
|
341
|
+
// capability that an LLM-binding adapter may turn into an MCP
|
|
342
|
+
// server entry (i.e. category A search-/mcp-, plus any future
|
|
343
|
+
// bindable cap). Warn-only this release; promoted to err in v0.5.
|
|
344
|
+
if (provide.tool_schema !== undefined) {
|
|
345
|
+
const ts = provide.tool_schema;
|
|
346
|
+
if (typeof ts !== "object" || ts === null) {
|
|
347
|
+
err(file, `provide '${cap}' tool_schema must be an object`);
|
|
348
|
+
} else {
|
|
349
|
+
if (typeof ts.name !== "string" || !/^[a-z][a-z0-9_]*$/.test(ts.name)) {
|
|
350
|
+
err(file, `provide '${cap}' tool_schema.name must match /^[a-z][a-z0-9_]*$/ (got ${JSON.stringify(ts.name)})`);
|
|
351
|
+
}
|
|
352
|
+
if (typeof ts.description !== "string" || !ts.description.trim()) {
|
|
353
|
+
err(file, `provide '${cap}' tool_schema.description is required`);
|
|
354
|
+
}
|
|
355
|
+
if (typeof ts.parameters !== "object" || ts.parameters === null) {
|
|
356
|
+
err(file, `provide '${cap}' tool_schema.parameters must be an object`);
|
|
357
|
+
} else {
|
|
358
|
+
if (ts.parameters.type !== "object") {
|
|
359
|
+
err(file, `provide '${cap}' tool_schema.parameters.type must be "object"`);
|
|
360
|
+
}
|
|
361
|
+
const props = ts.parameters.properties ?? {};
|
|
362
|
+
if (typeof props !== "object" || props === null) {
|
|
363
|
+
err(file, `provide '${cap}' tool_schema.parameters.properties must be an object`);
|
|
364
|
+
} else {
|
|
365
|
+
const required = Array.isArray(ts.parameters.required) ? ts.parameters.required : [];
|
|
366
|
+
for (const r of required) {
|
|
367
|
+
if (!(r in props)) {
|
|
368
|
+
err(file, `provide '${cap}' tool_schema.parameters.required has '${r}' not in properties`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (typeof ts.upstream !== "object" || ts.upstream === null) {
|
|
374
|
+
err(file, `provide '${cap}' tool_schema.upstream is required`);
|
|
375
|
+
} else if (typeof ts.upstream.command !== "string" || !ts.upstream.command) {
|
|
376
|
+
err(file, `provide '${cap}' tool_schema.upstream.command is required`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ── requires[*] ──────────────────────────────────────────────────────
|
|
383
|
+
const seenSlots = new Set();
|
|
384
|
+
for (const req of spec.requires ?? []) {
|
|
385
|
+
if (typeof req?.capability !== "string" || !req.capability) {
|
|
386
|
+
err(file, `require missing capability`);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const isCategory = CAPABILITY_CATEGORIES.has(req.capability);
|
|
390
|
+
const isExact = allowedRequiresExact.has(req.capability);
|
|
391
|
+
if (!isCategory && !isExact) {
|
|
392
|
+
err(
|
|
393
|
+
file,
|
|
394
|
+
`require '${req.capability}' is neither a category token nor in LEGACY_BINDABLE_EXACT_ALLOWLIST / repo exact set`,
|
|
395
|
+
);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (typeof req.inject_as !== "string" || !req.inject_as) {
|
|
400
|
+
err(file, `require '${req.capability}' missing inject_as`);
|
|
401
|
+
} else {
|
|
402
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(req.inject_as)) {
|
|
403
|
+
warn(file, `require '${req.capability}' inject_as '${req.inject_as}' should be UPPER_SNAKE_CASE`);
|
|
404
|
+
}
|
|
405
|
+
if (seenSlots.has(req.inject_as)) {
|
|
406
|
+
err(file, `require slot '${req.inject_as}' duplicated`);
|
|
407
|
+
}
|
|
408
|
+
seenSlots.add(req.inject_as);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (req.cardinality && !["one", "many"].includes(req.cardinality)) {
|
|
412
|
+
err(file, `require '${req.capability}' cardinality must be 'one' or 'many'`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (req.apply !== undefined) {
|
|
416
|
+
if (!["proxy-upstream", "openai-env"].includes(req.apply)) {
|
|
417
|
+
err(file, `require '${req.capability}' apply must be 'proxy-upstream' or 'openai-env'`);
|
|
418
|
+
}
|
|
419
|
+
if (req.capability !== "llm" && categoryOf(req.capability) !== "llm") {
|
|
420
|
+
err(file, `require '${req.capability}' apply field is only valid for llm category`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Orphan-require check: at least one provider in the repo must satisfy.
|
|
425
|
+
if (!hasProviderForRequire(req.capability)) {
|
|
426
|
+
const isRequired = req.required !== false; // default true
|
|
427
|
+
if (isRequired) {
|
|
428
|
+
err(
|
|
429
|
+
file,
|
|
430
|
+
`orphan required require '${req.capability}' — no provider matches; add provider, fix typo, or set required: false`,
|
|
431
|
+
);
|
|
432
|
+
} else {
|
|
433
|
+
warn(
|
|
434
|
+
file,
|
|
435
|
+
`orphan optional require '${req.capability}' — no provider in repo (will resolve at runtime if user installs one)`,
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ── Report ──
|
|
443
|
+
|
|
444
|
+
if (warnings.length) {
|
|
445
|
+
console.warn("⚠ Warnings:");
|
|
446
|
+
for (const w of warnings) console.warn(" " + w);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (errors.length) {
|
|
450
|
+
console.error("✗ check-app-spec failed:");
|
|
451
|
+
for (const e of errors) console.error(" " + e);
|
|
452
|
+
process.exit(1);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
console.log(
|
|
456
|
+
`✓ check-app-spec passed: ${specs.length} yamls, ${warnings.length} warnings`,
|
|
457
|
+
);
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Verify i18n integrity:
|
|
3
|
+
// 1. en and zh namespace files are paired (same set of .json files)
|
|
4
|
+
// 2. every flat dotted key in one language exists in the other
|
|
5
|
+
// 3. every t('...') call in frontend/src (excluding tests & dynamic template
|
|
6
|
+
// keys that use ${}) resolves to a key present in some locale file
|
|
7
|
+
// 4. Chinese-defaultValue regression guard: each source file's count of
|
|
8
|
+
// `defaultValue: '中文…'` literals must not exceed its baseline below.
|
|
9
|
+
// Locale files are the source of truth — the Chinese fallback is dead
|
|
10
|
+
// code (the existing key check above guarantees both en and zh have an
|
|
11
|
+
// entry), but it misleads readers and rots over time. New files must
|
|
12
|
+
// not introduce any; existing files may drop the count to clean up.
|
|
13
|
+
// Exits non-zero on any mismatch so pre-commit / CI can block the commit.
|
|
14
|
+
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
|
|
18
|
+
const REPO = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
|
|
19
|
+
const SRC = path.join(REPO, "frontend/src");
|
|
20
|
+
const LOC = path.join(SRC, "i18n/locales");
|
|
21
|
+
|
|
22
|
+
// Baseline of files with Chinese-defaultValue literals at the time the rule
|
|
23
|
+
// was introduced. Keep counts in sync as you clean up — when a file's count
|
|
24
|
+
// drops, lower the baseline (or delete the entry) so regressions are caught
|
|
25
|
+
// at the new floor.
|
|
26
|
+
const CHINESE_DEFAULT_VALUE_BASELINE = Object.freeze({
|
|
27
|
+
"frontend/src/components/instance/BackupActions.tsx": 73,
|
|
28
|
+
"frontend/src/pages/NewInstance.tsx": 46,
|
|
29
|
+
"frontend/src/components/instance/BackupStatus.tsx": 17,
|
|
30
|
+
"frontend/src/components/instance/ImportFlow.tsx": 15,
|
|
31
|
+
"frontend/src/pages/InstanceDetail.tsx": 12,
|
|
32
|
+
"frontend/src/components/instance/HermesConfigForm.tsx": 5,
|
|
33
|
+
"frontend/src/components/instance/FeishuLoginPanel.tsx": 1,
|
|
34
|
+
"frontend/src/components/instance/WeixinLoginPanel.tsx": 1,
|
|
35
|
+
"frontend/src/pages/Dashboard.tsx": 1,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function walkSrc(dir, out = []) {
|
|
39
|
+
for (const name of fs.readdirSync(dir)) {
|
|
40
|
+
const p = path.join(dir, name);
|
|
41
|
+
const st = fs.statSync(p);
|
|
42
|
+
if (st.isDirectory()) {
|
|
43
|
+
if (!/node_modules|dist|build|locales$|i18n$/.test(p)) walkSrc(p, out);
|
|
44
|
+
} else if (/\.(ts|tsx)$/.test(p)
|
|
45
|
+
&& !/\.test\.(ts|tsx)$/.test(p)
|
|
46
|
+
&& !/providers\.ts$/.test(p)) {
|
|
47
|
+
out.push(p);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function flatten(obj, prefix = "", out = {}) {
|
|
54
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
55
|
+
const key = prefix ? `${prefix}.${k}` : k;
|
|
56
|
+
if (v && typeof v === "object" && !Array.isArray(v)) flatten(v, key, out);
|
|
57
|
+
else out[key] = v;
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const errors = [];
|
|
63
|
+
|
|
64
|
+
// ── 1/2: compare locale files ────────────────────────────────────────────────
|
|
65
|
+
const langs = fs.readdirSync(LOC).filter(f =>
|
|
66
|
+
fs.statSync(path.join(LOC, f)).isDirectory());
|
|
67
|
+
if (langs.length < 2) {
|
|
68
|
+
errors.push(`Expected at least two language directories under ${LOC}, got ${langs.join(",")}`);
|
|
69
|
+
}
|
|
70
|
+
const fileSets = Object.fromEntries(langs.map(l =>
|
|
71
|
+
[l, new Set(fs.readdirSync(path.join(LOC, l)).filter(f => f.endsWith(".json")))]));
|
|
72
|
+
const allFiles = new Set(langs.flatMap(l => [...fileSets[l]]));
|
|
73
|
+
for (const f of allFiles) {
|
|
74
|
+
for (const l of langs) {
|
|
75
|
+
if (!fileSets[l].has(f)) errors.push(`Missing locale file: ${l}/${f}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const allKeys = new Set();
|
|
80
|
+
const perLangKeys = Object.fromEntries(langs.map(l => [l, {}]));
|
|
81
|
+
for (const l of langs) {
|
|
82
|
+
for (const f of fileSets[l]) {
|
|
83
|
+
const ns = f.replace(/\.json$/, "");
|
|
84
|
+
let data;
|
|
85
|
+
try { data = JSON.parse(fs.readFileSync(path.join(LOC, l, f), "utf8")); }
|
|
86
|
+
catch (e) { errors.push(`${l}/${f}: invalid JSON — ${e.message}`); continue; }
|
|
87
|
+
const flat = flatten(data);
|
|
88
|
+
// Also accept flat-dotted keys stored as top-level string values
|
|
89
|
+
for (const k of Object.keys(data)) {
|
|
90
|
+
if (typeof data[k] === "string") flat[k] = data[k];
|
|
91
|
+
}
|
|
92
|
+
perLangKeys[l][ns] = flat;
|
|
93
|
+
for (const k of Object.keys(flat)) allKeys.add(`${ns}:${k}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const tag of allKeys) {
|
|
98
|
+
const [ns, ...rest] = tag.split(":");
|
|
99
|
+
const k = rest.join(":");
|
|
100
|
+
for (const l of langs) {
|
|
101
|
+
const bag = perLangKeys[l][ns];
|
|
102
|
+
if (!bag) { errors.push(`${l}/${ns}.json missing entirely`); break; }
|
|
103
|
+
if (bag[k] === undefined) errors.push(`Missing key in ${l}: ${ns}:${k}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── 3: source code t('...') coverage ─────────────────────────────────────────
|
|
108
|
+
const tCallRe = /\bt\(\s*['"`]([^'"`]+)['"`]/g;
|
|
109
|
+
const files = walkSrc(SRC);
|
|
110
|
+
for (const file of files) {
|
|
111
|
+
const src = fs.readFileSync(file, "utf8");
|
|
112
|
+
let m;
|
|
113
|
+
while ((m = tCallRe.exec(src)) !== null) {
|
|
114
|
+
const key = m[1];
|
|
115
|
+
if (key.includes("${") || key.includes("`")) continue; // dynamic, skip
|
|
116
|
+
const line = src.slice(0, m.index).split("\n").length;
|
|
117
|
+
let hit = false;
|
|
118
|
+
if (key.includes(":")) {
|
|
119
|
+
hit = allKeys.has(key);
|
|
120
|
+
} else {
|
|
121
|
+
for (const full of allKeys) if (full.endsWith(`:${key}`)) { hit = true; break; }
|
|
122
|
+
}
|
|
123
|
+
if (!hit) errors.push(`${path.relative(REPO, file)}:${line} — t('${key}') has no matching locale key`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── 4: Chinese-defaultValue regression guard ─────────────────────────────────
|
|
128
|
+
const cnDefaultValueRe = /defaultValue:\s*(['"`])([^'"`]*[一-鿿][^'"`]*)\1/g;
|
|
129
|
+
let cnDefaultValueTotal = 0;
|
|
130
|
+
for (const file of files) {
|
|
131
|
+
const rel = path.relative(REPO, file).split(path.sep).join("/");
|
|
132
|
+
const src = fs.readFileSync(file, "utf8");
|
|
133
|
+
let count = 0;
|
|
134
|
+
while (cnDefaultValueRe.exec(src) !== null) count += 1;
|
|
135
|
+
cnDefaultValueRe.lastIndex = 0;
|
|
136
|
+
cnDefaultValueTotal += count;
|
|
137
|
+
const allowed = CHINESE_DEFAULT_VALUE_BASELINE[rel] ?? 0;
|
|
138
|
+
if (count > allowed) {
|
|
139
|
+
errors.push(
|
|
140
|
+
`${rel} — Chinese defaultValue count ${count} exceeds baseline ${allowed}; ` +
|
|
141
|
+
"add the locale entry to en/zh JSON instead of writing a Chinese fallback in source.",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (errors.length) {
|
|
147
|
+
console.error(`i18n check failed (${errors.length} issues):`);
|
|
148
|
+
for (const e of errors) console.error(" " + e);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
const cnNote = cnDefaultValueTotal
|
|
152
|
+
? `, ${cnDefaultValueTotal} legacy Chinese-defaultValue literals (under baseline)`
|
|
153
|
+
: "";
|
|
154
|
+
console.log(`i18n check passed: ${allKeys.size} keys across ${langs.join("/")} × ${allFiles.size} namespaces, ${files.length} source files scanned${cnNote}`);
|