@dsh-workbench/shared 0.1.11 → 0.1.13
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/dist/components/ChoiceGroup.css +49 -0
- package/dist/components/ChoiceGroup.d.ts +13 -3
- package/dist/components/ChoiceGroup.js +38 -5
- package/dist/components/StateDot.d.ts +5 -0
- package/dist/components/StateDot.js +10 -0
- package/dist/host/__tests__/fakes.d.ts +41 -0
- package/dist/host/__tests__/fakes.js +76 -0
- package/dist/host/__tests__/preflight-fixtures.d.ts +19 -0
- package/dist/host/__tests__/preflight-fixtures.js +47 -0
- package/dist/host/context-inject.d.ts +24 -0
- package/dist/host/context-inject.js +74 -0
- package/dist/host/events.d.ts +9 -0
- package/dist/host/events.js +70 -0
- package/dist/host/mount-units.d.ts +52 -0
- package/dist/host/mount-units.js +186 -0
- package/dist/host/preflight.d.ts +58 -0
- package/dist/host/preflight.js +233 -0
- package/dist/host/probe-cache.d.ts +6 -0
- package/dist/host/probe-cache.js +19 -0
- package/dist/host/probe-libs.d.ts +45 -0
- package/dist/host/probe-libs.js +61 -0
- package/dist/host/probe-units.d.ts +17 -0
- package/dist/host/probe-units.js +102 -0
- package/dist/host/probe-verdict.d.ts +7 -0
- package/dist/host/probe-verdict.js +143 -0
- package/dist/host/tool-scope.d.ts +32 -0
- package/dist/host/tool-scope.js +84 -0
- package/dist/host/unit-readiness.d.ts +7 -0
- package/dist/host/unit-readiness.js +5 -0
- package/dist/host/unit-registry.d.ts +9 -0
- package/dist/host/unit-registry.js +33 -0
- package/dist/host/version-range.d.ts +2 -0
- package/dist/host/version-range.js +73 -0
- package/dist/lib/ui.js +8 -1
- package/package.json +1 -1
- package/src/components/ChoiceGroup.css +49 -0
- package/src/components/ChoiceGroup.test.tsx +201 -0
- package/src/components/ChoiceGroup.tsx +112 -19
- package/src/lib/ui.test.ts +8 -0
- package/src/lib/ui.ts +8 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { OPENCLI_TIMEOUT_MS, PROBE_TTL_MS } from '@dsh-workbench/shared';
|
|
2
|
+
import { createTimedMemo } from "./probe-cache";
|
|
3
|
+
import { parseJsonLoose } from '@dsh-workbench/shared';
|
|
4
|
+
import { npmRegistryOf } from "./plugin-source";
|
|
5
|
+
import { probeRelease } from "./release";
|
|
6
|
+
import { probeLibs } from "./probe-libs";
|
|
7
|
+
import { evaluateUnit } from "./probe-verdict";
|
|
8
|
+
import { runShell } from "./shell";
|
|
9
|
+
export function parsePluginList(stdout) {
|
|
10
|
+
const parsed = parseJsonLoose(stdout);
|
|
11
|
+
if (Array.isArray(parsed))
|
|
12
|
+
return parsed;
|
|
13
|
+
return /no plugins? installed/i.test(stdout) ? [] : undefined;
|
|
14
|
+
}
|
|
15
|
+
async function probeReleases(units, options) {
|
|
16
|
+
const needs = new Map();
|
|
17
|
+
for (const unit of units) {
|
|
18
|
+
for (const need of unit.plugins ?? []) {
|
|
19
|
+
const source = need.installSource;
|
|
20
|
+
if (source?.kind === "npm")
|
|
21
|
+
needs.set(need.name, source);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const found = new Map();
|
|
25
|
+
for (const [plugin, source] of needs) {
|
|
26
|
+
const registry = npmRegistryOf(source, process.env);
|
|
27
|
+
const signal = await probeRelease({
|
|
28
|
+
shell: options.shell,
|
|
29
|
+
package: source.package,
|
|
30
|
+
...(registry.ok ? { registry: registry.value } : {}),
|
|
31
|
+
force: options.force,
|
|
32
|
+
});
|
|
33
|
+
if (signal !== undefined)
|
|
34
|
+
found.set(plugin, signal);
|
|
35
|
+
}
|
|
36
|
+
return found;
|
|
37
|
+
}
|
|
38
|
+
export async function probeUnitCommands(options) {
|
|
39
|
+
const all = await probeUnits({ ...options, units: [options.unit] });
|
|
40
|
+
return (all[0] ??
|
|
41
|
+
evaluateUnit(undefined, options.unit, "unchecked", options.command ?? "opencli"));
|
|
42
|
+
}
|
|
43
|
+
export async function probeUnits(options) {
|
|
44
|
+
const { shell, command = "opencli", units, force = false } = options;
|
|
45
|
+
const hits = units.map((unit) => unitCache.get(`${command}:${unit.unit}`));
|
|
46
|
+
if (!force && hits.every((hit) => hit !== undefined)) {
|
|
47
|
+
return hits;
|
|
48
|
+
}
|
|
49
|
+
const result = await runShell(shell, `${command} plugin list -f json`, {
|
|
50
|
+
timeoutMs: OPENCLI_TIMEOUT_MS,
|
|
51
|
+
}).then((value) => ({
|
|
52
|
+
kind: "ok",
|
|
53
|
+
stdout: value.exitCode === 0 ? value.stdout : "",
|
|
54
|
+
stderr: value.stderr,
|
|
55
|
+
}), (error) => ({
|
|
56
|
+
kind: "error",
|
|
57
|
+
stdout: "",
|
|
58
|
+
stderr: String(error),
|
|
59
|
+
}));
|
|
60
|
+
const list = result.kind === "ok" ? parsePluginList(result.stdout) : undefined;
|
|
61
|
+
const failure = result.kind === "error"
|
|
62
|
+
? result.stderr
|
|
63
|
+
: list === undefined
|
|
64
|
+
? "plugin list 的输出读不懂(不是 JSON 数组)"
|
|
65
|
+
: undefined;
|
|
66
|
+
if (failure !== undefined) {
|
|
67
|
+
return units.map((unit) => {
|
|
68
|
+
const capability = {
|
|
69
|
+
unit: unit.unit,
|
|
70
|
+
...(unit.plugins?.[0] === undefined
|
|
71
|
+
? {}
|
|
72
|
+
: { plugin: unit.plugins[0].name }),
|
|
73
|
+
status: "unchecked",
|
|
74
|
+
required: (unit.plugins ?? []).flatMap((need) => need.commands ?? []),
|
|
75
|
+
missing: [],
|
|
76
|
+
detail: failure,
|
|
77
|
+
};
|
|
78
|
+
unitCache.set(`${command}:${unit.unit}`, capability);
|
|
79
|
+
return capability;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const [releases, libs] = await Promise.all([
|
|
83
|
+
probeReleases(units, { shell, force }),
|
|
84
|
+
probeLibs({
|
|
85
|
+
needs: units.flatMap((unit) => unit.libs ?? []),
|
|
86
|
+
shell,
|
|
87
|
+
force,
|
|
88
|
+
}),
|
|
89
|
+
]);
|
|
90
|
+
return units.map((unit) => {
|
|
91
|
+
const capability = evaluateUnit(list, unit, undefined, command, releases, libsOfUnit(unit, libs));
|
|
92
|
+
unitCache.set(`${command}:${unit.unit}`, capability);
|
|
93
|
+
return capability;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function libsOfUnit(unit, libs) {
|
|
97
|
+
return (unit.libs ?? [])
|
|
98
|
+
.map((need) => libs.get(need.name))
|
|
99
|
+
.filter((state) => state !== undefined);
|
|
100
|
+
}
|
|
101
|
+
const unitCache = createTimedMemo(PROBE_TTL_MS);
|
|
102
|
+
export const resetProbeCache = () => void unitCache.clear();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PluginRelease, UnitPluginNeed } from '@dsh-workbench/shared';
|
|
2
|
+
import type { UnitCapability, UnitCommands, UnitLibState } from "./probe-libs";
|
|
3
|
+
export declare function evaluateUnit(list: readonly unknown[] | undefined, unit: UnitCommands, failure: string | undefined, command?: string, releases?: ReadonlyMap<string, PluginRelease>, libs?: readonly UnitLibState[]): UnitCapability;
|
|
4
|
+
export declare function gapsText(gaps: readonly string[]): string;
|
|
5
|
+
export declare function pluginUpdateCommand(plugin: string | undefined): string | undefined;
|
|
6
|
+
export declare function pluginInstallCommand(plugin: string | undefined): string;
|
|
7
|
+
export declare function upgradeLine(need: UnitPluginNeed, command: string, latest: string): string | undefined;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { resolvePluginSource } from "./plugin-source";
|
|
2
|
+
import { isNewerVersion } from "./release";
|
|
3
|
+
export function evaluateUnit(list, unit, failure, command = "opencli", releases = new Map(), libs = []) {
|
|
4
|
+
const isPlugin = (item) => typeof item === "object" && item !== null;
|
|
5
|
+
const needs = unit.plugins ?? [];
|
|
6
|
+
const required = needs.flatMap((need) => need.commands ?? []);
|
|
7
|
+
const primary = needs[0]?.name;
|
|
8
|
+
if (failure !== undefined || list === undefined) {
|
|
9
|
+
return {
|
|
10
|
+
unit: unit.unit,
|
|
11
|
+
...(primary === undefined ? {} : { plugin: primary }),
|
|
12
|
+
status: "unchecked",
|
|
13
|
+
required,
|
|
14
|
+
missing: [],
|
|
15
|
+
detail: failure ?? "探不动",
|
|
16
|
+
...(libs.length === 0 ? {} : { libs }),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const plugins = list.filter(isPlugin);
|
|
20
|
+
const installed = new Map(plugins.map((item) => [String(item.name), item]));
|
|
21
|
+
const states = needs.map((need) => {
|
|
22
|
+
const hit = installed.get(need.name);
|
|
23
|
+
if (hit === undefined) {
|
|
24
|
+
const source = resolvePluginSource(need.installSource, process.env, {
|
|
25
|
+
command,
|
|
26
|
+
plugin: need.name,
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
name: need.name,
|
|
30
|
+
status: "missing",
|
|
31
|
+
missing: [],
|
|
32
|
+
install: source.ok ? "ready" : source.problem,
|
|
33
|
+
command: source.ok ? source.line : pluginInstallCommand(need.name),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const available = new Set((Array.isArray(hit.commands) ? hit.commands : []).map(String));
|
|
37
|
+
const missing = (need.commands ?? []).filter((name) => !available.has(name));
|
|
38
|
+
const release = releases.get(need.name);
|
|
39
|
+
const behind = release?.latest !== undefined &&
|
|
40
|
+
isNewerVersion(release.latest, release.installed);
|
|
41
|
+
const commandsBehind = missing.length > 0;
|
|
42
|
+
const upgrade = behind && release?.latest !== undefined
|
|
43
|
+
? upgradeLine(need, command, release.latest)
|
|
44
|
+
: undefined;
|
|
45
|
+
return {
|
|
46
|
+
name: need.name,
|
|
47
|
+
status: commandsBehind || behind ? "outdated" : "ok",
|
|
48
|
+
missing,
|
|
49
|
+
...(release === undefined ? {} : { release }),
|
|
50
|
+
...(commandsBehind || behind
|
|
51
|
+
? {
|
|
52
|
+
command: upgrade ??
|
|
53
|
+
pluginUpdateCommand(need.name) ??
|
|
54
|
+
pluginInstallCommand(need.name),
|
|
55
|
+
}
|
|
56
|
+
: {}),
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
const absent = states.filter((state) => state.status === "missing");
|
|
60
|
+
const outdated = states.filter((state) => state.status === "outdated");
|
|
61
|
+
const missing = states.flatMap((state) => state.missing);
|
|
62
|
+
const libMissing = libs.filter((state) => state.status === "missing");
|
|
63
|
+
const libMismatch = libs.filter((state) => state.status === "unsatisfied");
|
|
64
|
+
const libBehind = libs.filter((state) => state.status === "outdated");
|
|
65
|
+
const status = absent.length > 0 || libMissing.length > 0 || libMismatch.length > 0
|
|
66
|
+
? "missing-plugin"
|
|
67
|
+
: missing.length > 0
|
|
68
|
+
? "missing-commands"
|
|
69
|
+
: "ok";
|
|
70
|
+
const blocked = absent.find((state) => state.install !== "ready");
|
|
71
|
+
const install = absent.length === 0
|
|
72
|
+
?
|
|
73
|
+
libMissing.length > 0 || libMismatch.length > 0
|
|
74
|
+
? "ready"
|
|
75
|
+
: undefined
|
|
76
|
+
: blocked === undefined
|
|
77
|
+
? "ready"
|
|
78
|
+
: blocked.install;
|
|
79
|
+
const fix = status === "missing-plugin"
|
|
80
|
+
? install === "ready"
|
|
81
|
+
? "install"
|
|
82
|
+
: undefined
|
|
83
|
+
: outdated.length > 0 || libBehind.length > 0
|
|
84
|
+
? "update"
|
|
85
|
+
: undefined;
|
|
86
|
+
const first = absent[0] ?? outdated[0] ?? libMissing[0] ?? libMismatch[0] ?? libBehind[0];
|
|
87
|
+
const gaps = [...(missing.length === 0 ? [] : [`缺必需:${missing.join("、")}`])];
|
|
88
|
+
const newer = states.filter((state) => state.release?.latest !== undefined &&
|
|
89
|
+
isNewerVersion(state.release.latest, state.release.installed));
|
|
90
|
+
const versionText = newer.length === 0
|
|
91
|
+
? ""
|
|
92
|
+
: `(源上有新版:${newer.map((state) => `${state.release?.installed ?? ""} → ${state.release?.latest ?? ""}`).join("、")})`;
|
|
93
|
+
const absentNames = [
|
|
94
|
+
...absent.map((state) => state.name),
|
|
95
|
+
...libMissing.map((state) => state.name),
|
|
96
|
+
];
|
|
97
|
+
const mismatchText = libMismatch.map((state) => {
|
|
98
|
+
const declared = unit.libs?.find((need) => need.name === state.name)?.version?.trim();
|
|
99
|
+
return `${state.name}(装的是 ${state.installed ?? "未知"},声明要 ${declared === undefined || declared === "" ? "任意版本" : declared})`;
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
unit: unit.unit,
|
|
103
|
+
...(primary === undefined ? {} : { plugin: primary }),
|
|
104
|
+
status,
|
|
105
|
+
required,
|
|
106
|
+
missing,
|
|
107
|
+
detail: absentNames.length > 0 || mismatchText.length > 0
|
|
108
|
+
? [
|
|
109
|
+
...(absentNames.length === 0
|
|
110
|
+
? []
|
|
111
|
+
: [`${plugins.length} 个已装插件里没有 ${absentNames.join("、")}`]),
|
|
112
|
+
...mismatchText,
|
|
113
|
+
].join(";")
|
|
114
|
+
: `${needs.length} 份安装都在${gapsText(gaps)}${versionText}${libs.length === 0
|
|
115
|
+
? ""
|
|
116
|
+
: `;交叉依赖:${libs.map((state) => `${state.name}${state.installed === undefined ? " 未装" : ` ${state.installed}`}`).join("、")}${libBehind.length === 0 ? "" : "(源上有新版)"}`}`,
|
|
117
|
+
...(fix === undefined && first === undefined
|
|
118
|
+
? {}
|
|
119
|
+
: { command: fix === "install" ? (absent[0] ?? first)?.command : first?.command }),
|
|
120
|
+
...(fix === undefined ? {} : { fix }),
|
|
121
|
+
...(install === undefined ? {} : { install }),
|
|
122
|
+
plugins: states,
|
|
123
|
+
...(libs.length === 0 ? {} : { libs }),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export function gapsText(gaps) {
|
|
127
|
+
return gaps.length === 0 ? "" : `(${gaps.join(";")})`;
|
|
128
|
+
}
|
|
129
|
+
export function pluginUpdateCommand(plugin) {
|
|
130
|
+
return plugin === undefined ? undefined : `opencli plugin update ${plugin}`;
|
|
131
|
+
}
|
|
132
|
+
export function pluginInstallCommand(plugin) {
|
|
133
|
+
return `opencli plugin install <${plugin ?? "插件"}源>`;
|
|
134
|
+
}
|
|
135
|
+
export function upgradeLine(need, command, latest) {
|
|
136
|
+
const source = resolvePluginSource(need.installSource, process.env, {
|
|
137
|
+
command,
|
|
138
|
+
plugin: need.name,
|
|
139
|
+
version: latest,
|
|
140
|
+
mode: "update",
|
|
141
|
+
});
|
|
142
|
+
return source.ok ? source.line : undefined;
|
|
143
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { DshPromptAssembly } from '@dsh-workbench/shared';
|
|
2
|
+
export interface ScopedToolRegistry {
|
|
3
|
+
restrict(filter: {
|
|
4
|
+
readonly allow?: readonly string[];
|
|
5
|
+
readonly deny?: readonly string[];
|
|
6
|
+
}): () => void;
|
|
7
|
+
}
|
|
8
|
+
export interface ToolScopeAgent {
|
|
9
|
+
readonly ctx?: {
|
|
10
|
+
readonly tools?: ScopedToolRegistry | undefined;
|
|
11
|
+
get?<T = unknown>(name: string): T | undefined;
|
|
12
|
+
} | undefined;
|
|
13
|
+
readonly session?: {
|
|
14
|
+
readonly header?: {
|
|
15
|
+
readonly cwd?: string;
|
|
16
|
+
} | undefined;
|
|
17
|
+
} | undefined;
|
|
18
|
+
}
|
|
19
|
+
export interface ToolScopeOptions {
|
|
20
|
+
deniedTools(workspace: string | null): readonly string[];
|
|
21
|
+
logger?: {
|
|
22
|
+
info?(message: string): void;
|
|
23
|
+
warn?(message: string): void;
|
|
24
|
+
} | undefined;
|
|
25
|
+
}
|
|
26
|
+
export interface ToolScope {
|
|
27
|
+
apply(agent: unknown): void;
|
|
28
|
+
narrow(assembly: DshPromptAssembly, agent: unknown): DshPromptAssembly;
|
|
29
|
+
reapply(): void;
|
|
30
|
+
disposeAll(): void;
|
|
31
|
+
}
|
|
32
|
+
export declare function createToolScope(options: ToolScopeOptions): ToolScope;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
function agentOf(value) {
|
|
2
|
+
return typeof value === 'object' && value !== null ? value : undefined;
|
|
3
|
+
}
|
|
4
|
+
function workspaceOf(handle) {
|
|
5
|
+
const cwd = handle.session?.header?.cwd;
|
|
6
|
+
return typeof cwd === 'string' && cwd !== '' ? cwd : null;
|
|
7
|
+
}
|
|
8
|
+
export function createToolScope(options) {
|
|
9
|
+
const applied = new WeakMap();
|
|
10
|
+
const seen = new Set();
|
|
11
|
+
function toolsOf(handle) {
|
|
12
|
+
const byProperty = handle.ctx?.tools;
|
|
13
|
+
if (byProperty?.restrict !== undefined)
|
|
14
|
+
return byProperty;
|
|
15
|
+
const byGet = handle.ctx?.get?.('tools');
|
|
16
|
+
return byGet?.restrict === undefined ? undefined : byGet;
|
|
17
|
+
}
|
|
18
|
+
function sync(handle) {
|
|
19
|
+
const tools = toolsOf(handle);
|
|
20
|
+
if (tools === undefined) {
|
|
21
|
+
options.logger?.warn?.('[dsh-workbench] 拿不到 agent 作用域的工具面:这次不收窄(下次再试)');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const workspace = workspaceOf(handle);
|
|
25
|
+
const deny = [...options.deniedTools(workspace)];
|
|
26
|
+
const signature = `${workspace ?? ''}\u0000${deny.join(',')}`;
|
|
27
|
+
const previous = applied.get(handle);
|
|
28
|
+
if (previous !== undefined && previous.signature === signature)
|
|
29
|
+
return;
|
|
30
|
+
if (previous === undefined)
|
|
31
|
+
seen.add(new WeakRef(handle));
|
|
32
|
+
previous?.dispose();
|
|
33
|
+
if (deny.length === 0) {
|
|
34
|
+
options.logger?.info?.('[dsh-workbench] 这个工作区启用了全部套件:不收窄工具面');
|
|
35
|
+
applied.delete(handle);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
applied.set(handle, { signature, dispose: tools.restrict({ deny }) });
|
|
40
|
+
options.logger?.info?.(`[dsh-workbench] 按工作区收窄工具面:${workspace ?? '(未知工作区)'} 里收起 ${deny.length} 个工具(${deny.join(', ')})`);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
options.logger?.warn?.(`[dsh-workbench] 收窄工具面失败:${String(error)}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
apply(agent) {
|
|
48
|
+
const handle = agentOf(agent);
|
|
49
|
+
if (handle === undefined)
|
|
50
|
+
return;
|
|
51
|
+
sync(handle);
|
|
52
|
+
},
|
|
53
|
+
narrow(assembly, agent) {
|
|
54
|
+
const handle = agentOf(agent);
|
|
55
|
+
const tools = assembly?.tools;
|
|
56
|
+
if (handle === undefined || tools === undefined)
|
|
57
|
+
return assembly;
|
|
58
|
+
const deny = new Set(options.deniedTools(workspaceOf(handle)));
|
|
59
|
+
if (deny.size === 0)
|
|
60
|
+
return assembly;
|
|
61
|
+
const kept = tools.filter((tool) => !deny.has(tool.name));
|
|
62
|
+
return kept.length === tools.length ? assembly : { ...assembly, tools: kept };
|
|
63
|
+
},
|
|
64
|
+
reapply() {
|
|
65
|
+
for (const ref of [...seen]) {
|
|
66
|
+
const handle = ref.deref();
|
|
67
|
+
if (handle === undefined)
|
|
68
|
+
seen.delete(ref);
|
|
69
|
+
else
|
|
70
|
+
sync(handle);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
disposeAll() {
|
|
74
|
+
for (const ref of [...seen]) {
|
|
75
|
+
const handle = ref.deref();
|
|
76
|
+
if (handle === undefined)
|
|
77
|
+
continue;
|
|
78
|
+
applied.get(handle)?.dispose();
|
|
79
|
+
applied.delete(handle);
|
|
80
|
+
}
|
|
81
|
+
seen.clear();
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { UnitManifest } from '@dsh-workbench/shared';
|
|
2
|
+
export interface UnitReadiness {
|
|
3
|
+
readonly canMount: boolean;
|
|
4
|
+
readonly missingHard: readonly string[];
|
|
5
|
+
readonly missingSoft: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function unitReadiness(manifest: UnitManifest, lookup: (name: string) => unknown): UnitReadiness;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export function unitReadiness(manifest, lookup) {
|
|
2
|
+
const missingHard = manifest.host.inject.filter((name) => lookup(name) === undefined);
|
|
3
|
+
const missingSoft = (manifest.host.optional ?? []).filter((name) => lookup(name) === undefined);
|
|
4
|
+
return { canMount: missingHard.length === 0, missingHard, missingSoft };
|
|
5
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { UnitManifest } from '@dsh-workbench/shared';
|
|
2
|
+
import type { UnitHostFace } from './unit-face';
|
|
3
|
+
export interface UnitRegistry {
|
|
4
|
+
register(face: UnitHostFace): () => void;
|
|
5
|
+
all(): readonly UnitHostFace[];
|
|
6
|
+
manifests(): readonly UnitManifest[];
|
|
7
|
+
subscribe(listener: () => void): () => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function createUnitRegistry(): UnitRegistry;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function createUnitRegistry() {
|
|
2
|
+
const faces = [];
|
|
3
|
+
const ids = new Set();
|
|
4
|
+
const listeners = new Set();
|
|
5
|
+
function notify() {
|
|
6
|
+
for (const listener of listeners)
|
|
7
|
+
listener();
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
register(face) {
|
|
11
|
+
const id = face.manifest.id;
|
|
12
|
+
if (ids.has(id))
|
|
13
|
+
throw new Error(`套件 id 重复:${id}(id 会进持久化配置,不能撞)`);
|
|
14
|
+
ids.add(id);
|
|
15
|
+
faces.push(face);
|
|
16
|
+
notify();
|
|
17
|
+
return () => {
|
|
18
|
+
const index = faces.indexOf(face);
|
|
19
|
+
if (index < 0)
|
|
20
|
+
return;
|
|
21
|
+
faces.splice(index, 1);
|
|
22
|
+
ids.delete(id);
|
|
23
|
+
notify();
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
all: () => faces,
|
|
27
|
+
manifests: () => faces.map((face) => face.manifest),
|
|
28
|
+
subscribe(listener) {
|
|
29
|
+
listeners.add(listener);
|
|
30
|
+
return () => listeners.delete(listener);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { compareVersions } from './release';
|
|
2
|
+
function readable(version) {
|
|
3
|
+
return compareVersions(version, '0.0.0') !== undefined;
|
|
4
|
+
}
|
|
5
|
+
function segmentsOf(version) {
|
|
6
|
+
if (!readable(version))
|
|
7
|
+
return undefined;
|
|
8
|
+
return version.split('.').map((part) => Number.parseInt(part, 10));
|
|
9
|
+
}
|
|
10
|
+
function caretUpper(base) {
|
|
11
|
+
const [major = 0, minor = 0, patch = 0] = base;
|
|
12
|
+
if (major > 0)
|
|
13
|
+
return `${major + 1}.0.0`;
|
|
14
|
+
return minor > 0 ? `0.${minor + 1}.0` : `0.0.${patch + 1}`;
|
|
15
|
+
}
|
|
16
|
+
function tildeUpper(base) {
|
|
17
|
+
const [major = 0, minor = 0] = base;
|
|
18
|
+
return `${major}.${minor + 1}.0`;
|
|
19
|
+
}
|
|
20
|
+
function comparator(token, version) {
|
|
21
|
+
const match = /^(>=|<=|>|<|=|\^|~)?\s*(.+)$/.exec(token.trim());
|
|
22
|
+
if (match === null)
|
|
23
|
+
return undefined;
|
|
24
|
+
const [, operator = '', raw = ''] = match;
|
|
25
|
+
const base = segmentsOf(raw.trim());
|
|
26
|
+
if (base === undefined)
|
|
27
|
+
return undefined;
|
|
28
|
+
const at = (other) => compareVersions(version, other);
|
|
29
|
+
switch (operator) {
|
|
30
|
+
case '^':
|
|
31
|
+
return (at(raw) ?? -1) >= 0 && (at(caretUpper(base)) ?? 1) < 0;
|
|
32
|
+
case '~':
|
|
33
|
+
return (at(raw) ?? -1) >= 0 && (at(tildeUpper(base)) ?? 1) < 0;
|
|
34
|
+
default:
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
const compared = at(raw.trim());
|
|
38
|
+
if (compared === undefined)
|
|
39
|
+
return undefined;
|
|
40
|
+
switch (operator) {
|
|
41
|
+
case '>=':
|
|
42
|
+
return compared >= 0;
|
|
43
|
+
case '<=':
|
|
44
|
+
return compared <= 0;
|
|
45
|
+
case '>':
|
|
46
|
+
return compared > 0;
|
|
47
|
+
case '<':
|
|
48
|
+
return compared < 0;
|
|
49
|
+
case '=':
|
|
50
|
+
case '':
|
|
51
|
+
return compared === 0;
|
|
52
|
+
default:
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function checkVersionRange(contract, version) {
|
|
57
|
+
const spec = contract?.trim() ?? '';
|
|
58
|
+
if (spec === '')
|
|
59
|
+
return 'satisfied';
|
|
60
|
+
if (!readable(version))
|
|
61
|
+
return 'malformed';
|
|
62
|
+
const tokens = spec.split(/\s+/).filter((token) => token !== '');
|
|
63
|
+
if (tokens.length === 0)
|
|
64
|
+
return 'satisfied';
|
|
65
|
+
for (const token of tokens) {
|
|
66
|
+
const verdict = comparator(token, version);
|
|
67
|
+
if (verdict === undefined)
|
|
68
|
+
return 'malformed';
|
|
69
|
+
if (!verdict)
|
|
70
|
+
return 'unsatisfied';
|
|
71
|
+
}
|
|
72
|
+
return 'satisfied';
|
|
73
|
+
}
|
package/dist/lib/ui.js
CHANGED
|
@@ -14,7 +14,14 @@ export function relativeTime(epochMs, t) {
|
|
|
14
14
|
return t('time.hours', { n: Math.floor(diff / (60 * MINUTE)) });
|
|
15
15
|
if (diff < 7 * 24 * 60 * MINUTE)
|
|
16
16
|
return t('time.days', { n: Math.floor(diff / (24 * 60 * MINUTE)) });
|
|
17
|
-
return new Date(epochMs).
|
|
17
|
+
return new Date(epochMs).toLocaleString(undefined, {
|
|
18
|
+
year: 'numeric',
|
|
19
|
+
month: 'numeric',
|
|
20
|
+
day: 'numeric',
|
|
21
|
+
hour: '2-digit',
|
|
22
|
+
minute: '2-digit',
|
|
23
|
+
second: '2-digit',
|
|
24
|
+
});
|
|
18
25
|
}
|
|
19
26
|
export function shortenPath(path) {
|
|
20
27
|
if (typeof path !== 'string')
|
package/package.json
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
.wb-choiceOption {
|
|
13
|
+
position: relative;
|
|
13
14
|
display: flex;
|
|
14
15
|
align-items: center;
|
|
15
16
|
gap: var(--wb-len-6);
|
|
@@ -22,6 +23,54 @@
|
|
|
22
23
|
color: var(--dsw-alias-label-primary);
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
.wb-choiceOptionOn {
|
|
27
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.wb-checkbox {
|
|
31
|
+
position: absolute;
|
|
32
|
+
width: var(--wb-len-16);
|
|
33
|
+
height: var(--wb-len-16);
|
|
34
|
+
margin: 0;
|
|
35
|
+
opacity: 0;
|
|
36
|
+
pointer-events: none;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.wb-checkboxBox {
|
|
40
|
+
display: inline-flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
justify-content: center;
|
|
43
|
+
box-sizing: border-box;
|
|
44
|
+
flex: none;
|
|
45
|
+
width: var(--wb-len-16);
|
|
46
|
+
height: var(--wb-len-16);
|
|
47
|
+
border: 1px solid var(--dsw-alias-border-l3);
|
|
48
|
+
border-radius: var(--dsw-radius-xs);
|
|
49
|
+
corner-shape: round;
|
|
50
|
+
color: var(--dsw-alias-label-primary-foreground);
|
|
51
|
+
transition:
|
|
52
|
+
background-color var(--wb-dur-0-12) ease, border-color var(--wb-dur-0-12) ease;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.wb-choiceOption:hover .wb-checkboxBox {
|
|
56
|
+
border-color: var(--dsw-alias-label-tertiary);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.wb-checkbox:checked + .wb-checkboxBox {
|
|
60
|
+
border-color: var(--dsw-alias-brand-primary);
|
|
61
|
+
background: var(--dsw-alias-brand-primary);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.wb-checkbox:disabled + .wb-checkboxBox {
|
|
65
|
+
opacity: 0.5;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.wb-checkbox:focus-visible + .wb-checkboxBox {
|
|
69
|
+
outline: var(--dsw-focus-ring-width, var(--wb-len-2)) solid
|
|
70
|
+
var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
|
|
71
|
+
outline-offset: 2px;
|
|
72
|
+
}
|
|
73
|
+
|
|
25
74
|
.wb-choiceOption:hover {
|
|
26
75
|
background: var(--dsw-alias-interactive-bg-hover);
|
|
27
76
|
}
|