@dsh-workbench/shared 1.1.7 → 1.1.9
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/api/blocker-table.d.ts +1 -0
- package/dist/api/blocker-table.js +1 -1
- package/dist/components/SelectField.css +0 -6
- package/dist/components/SelectField.d.ts +2 -3
- package/dist/components/SelectField.js +9 -30
- package/dist/components/SelectFieldNodes.d.ts +3 -1
- package/dist/components/SelectFieldNodes.js +2 -1
- package/dist/components/SelectFieldWheel.d.ts +9 -0
- package/dist/components/SelectFieldWheel.js +35 -0
- package/dist/config/constants.d.ts +4 -4
- package/dist/config/constants.js +4 -4
- package/dist/contract/capabilities.d.ts +29 -0
- package/dist/contract/capabilities.js +1 -0
- package/dist/contract/needs.d.ts +3 -3
- package/dist/contract/needs.js +1 -1
- package/dist/contract/plugin-source.d.ts +19 -0
- package/dist/contract/protocol/bundle-install.d.ts +3 -1
- package/dist/contract/protocol/bundle-install.js +3 -0
- package/dist/contract/protocol/market-index.d.ts +1 -0
- package/dist/contract/protocol/platform.d.ts +1 -1
- package/dist/contract/runner.d.ts +6 -0
- package/dist/contract/runner.js +1 -0
- package/dist/contract/unit.d.ts +4 -4
- package/dist/host/adapter-contract.d.ts +3 -0
- package/dist/host/adapter-contract.js +93 -0
- package/dist/host/adapter-set.js +6 -4
- package/dist/host/adapter-types.d.ts +1 -1
- package/dist/host/bundle-manager.d.ts +3 -0
- package/dist/host/bundle-manager.js +181 -0
- package/dist/host/cli.js +3 -1
- package/dist/host/config-sync.d.ts +48 -0
- package/dist/host/config-sync.js +342 -0
- package/dist/host/dsh-plugins.d.ts +5 -0
- package/dist/host/dsh-plugins.js +51 -0
- package/dist/host/extension.js +1 -1
- package/dist/host/flow/check.d.ts +9 -0
- package/dist/host/flow/check.js +179 -0
- package/dist/host/flow/expr.d.ts +30 -0
- package/dist/host/flow/expr.js +99 -0
- package/dist/host/flow/kit.d.ts +14 -0
- package/dist/host/flow/kit.js +13 -0
- package/dist/host/flow/run.d.ts +13 -0
- package/dist/host/flow/run.js +287 -0
- package/dist/host/flow/types.d.ts +188 -0
- package/dist/host/flow/types.js +1 -0
- package/dist/host/host-actions.d.ts +2 -0
- package/dist/host/host-actions.js +5 -0
- package/dist/host/host-kit.d.ts +19 -0
- package/dist/host/host-kit.js +14 -0
- package/dist/host/http.d.ts +2 -6
- package/dist/host/http.js +2 -2
- package/dist/host/installed-module.d.ts +23 -0
- package/dist/host/installed-module.js +264 -0
- package/dist/host/lib-install.d.ts +7 -9
- package/dist/host/lib-install.js +9 -12
- package/dist/host/lib-runners.d.ts +3 -5
- package/dist/host/lib-runners.js +2 -2
- package/dist/host/local-declaration.d.ts +7 -0
- package/dist/host/local-declaration.js +33 -0
- package/dist/host/market-index-claim.d.ts +0 -1
- package/dist/host/market-index-claim.js +0 -3
- package/dist/host/market-index-doc.d.ts +1 -45
- package/dist/host/market-index-parse.d.ts +9 -4
- package/dist/host/market-index-parse.js +17 -92
- package/dist/host/market-index.d.ts +5 -6
- package/dist/host/market-index.js +29 -87
- package/dist/host/module-root.d.ts +1 -0
- package/dist/host/module-root.js +5 -0
- package/dist/host/npm-registry.d.ts +10 -0
- package/dist/host/npm-registry.js +16 -0
- package/dist/host/opencli-runner.d.ts +2 -1
- package/dist/host/package-content.d.ts +7 -0
- package/dist/host/package-content.js +69 -0
- package/dist/host/package-kind.d.ts +13 -0
- package/dist/host/package-kind.js +51 -0
- package/dist/host/package-route.d.ts +31 -0
- package/dist/host/package-route.js +57 -0
- package/dist/host/paths.d.ts +11 -19
- package/dist/host/paths.js +16 -43
- package/dist/host/platform.js +1 -1
- package/dist/host/plugin-ledger.d.ts +8 -8
- package/dist/host/plugin-ledger.js +23 -16
- package/dist/host/plugin.d.ts +1 -2
- package/dist/host/plugin.js +4 -5
- package/dist/host/port-types.d.ts +42 -9
- package/dist/host/preflight.d.ts +3 -2
- package/dist/host/profile-create.d.ts +18 -0
- package/dist/host/profile-create.js +118 -0
- package/dist/host/profile-registries.d.ts +10 -0
- package/dist/host/profile-registries.js +150 -0
- package/dist/host/registry-declaration.d.ts +21 -0
- package/dist/host/registry-declaration.js +33 -0
- package/dist/host/release-probe.d.ts +34 -0
- package/dist/host/release-probe.js +88 -0
- package/dist/host/runner.d.ts +26 -0
- package/dist/host/runner.js +50 -0
- package/dist/host/shell.d.ts +0 -2
- package/dist/host/shell.js +0 -7
- package/dist/host/spec.js +1 -1
- package/dist/host/supply-index.d.ts +1 -0
- package/dist/host/supply-index.js +8 -7
- package/dist/host/supply-port.js +13 -3
- package/dist/host/unit-face.d.ts +2 -1
- package/dist/host/version.js +11 -6
- package/dist/host.d.ts +24 -6
- package/dist/host.js +23 -6
- package/dist/i18n/en.js +19 -20
- package/dist/i18n/index.d.ts +20 -21
- package/dist/i18n/runtime-terms.d.ts +15 -0
- package/dist/i18n/runtime-terms.js +22 -0
- package/dist/i18n/zh.d.ts +19 -20
- package/dist/i18n/zh.js +19 -20
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/testing/fake-adapter-set.d.ts +5 -0
- package/dist/testing/fake-adapter-set.js +150 -0
- package/dist/testing/needs.js +1 -1
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +1 -0
- package/package.json +1 -1
- package/src/api/__tests__/blocker-table.test.ts +25 -0
- package/src/api/{workbench-client.test.ts → __tests__/workbench-client.test.ts} +2 -2
- package/src/api/blocker-table.ts +6 -2
- package/src/components/SelectField.css +0 -6
- package/src/components/SelectField.tsx +7 -36
- package/src/components/SelectFieldNodes.tsx +4 -1
- package/src/components/{Card.test.tsx → __tests__/Card.test.tsx} +2 -2
- package/src/components/{CardAnchor.test.tsx → __tests__/CardAnchor.test.tsx} +3 -3
- package/src/components/{CardHead.test.tsx → __tests__/CardHead.test.tsx} +1 -1
- package/src/components/{ChoiceGroup.test.tsx → __tests__/ChoiceGroup.test.tsx} +2 -2
- package/src/components/{ConfirmBar.test.tsx → __tests__/ConfirmBar.test.tsx} +5 -5
- package/src/components/{Field.test.tsx → __tests__/Field.test.tsx} +1 -1
- package/src/components/{Form.test.tsx → __tests__/Form.test.tsx} +5 -5
- package/src/components/{FormActions.test.tsx → __tests__/FormActions.test.tsx} +3 -3
- package/src/components/{FormRows.test.tsx → __tests__/FormRows.test.tsx} +2 -2
- package/src/components/{FormTypes.test.ts → __tests__/FormTypes.test.ts} +1 -1
- package/src/components/{FormWiring.test.tsx → __tests__/FormWiring.test.tsx} +4 -4
- package/src/components/{Link.test.tsx → __tests__/Link.test.tsx} +1 -1
- package/src/components/{Rows.test.tsx → __tests__/Rows.test.tsx} +2 -2
- package/src/components/{SelectField.test.tsx → __tests__/SelectField.test.tsx} +7 -37
- package/src/components/{Spinner.test.tsx → __tests__/Spinner.test.tsx} +4 -4
- package/src/config/{constants.test.ts → __tests__/constants.test.ts} +1 -1
- package/src/config/{storage.test.ts → __tests__/storage.test.ts} +1 -1
- package/src/config/constants.ts +4 -4
- package/src/contract/capabilities.ts +30 -0
- package/src/contract/needs.ts +4 -4
- package/src/contract/plugin-source.ts +24 -0
- package/src/contract/protocol/bundle-install.ts +7 -1
- package/src/contract/protocol/market-index.ts +1 -0
- package/src/contract/protocol/platform.ts +1 -1
- package/src/contract/runner.ts +6 -0
- package/src/contract/unit.ts +4 -4
- package/src/host/{extension.test.ts → __tests__/extension.test.ts} +4 -4
- package/src/host/{install-failure.test.ts → __tests__/install-failure.test.ts} +1 -1
- package/src/host/{installed-sources.test.ts → __tests__/installed-sources.test.ts} +1 -1
- package/src/host/__tests__/lib-runners.test.ts +11 -2
- package/src/host/__tests__/market-index-assemble.test.ts +26 -0
- package/src/host/__tests__/market-index.test.ts +41 -51
- package/src/host/__tests__/plugin-ledger.test.ts +19 -6
- package/src/host/{recipe.test.ts → __tests__/recipe.test.ts} +3 -3
- package/src/host/__tests__/runtime-layout.test.ts +14 -0
- package/src/host/__tests__/supply-index.test.ts +2 -2
- package/src/host/__tests__/supply-port.test.ts +32 -0
- package/src/host/adapter-contract.ts +93 -0
- package/src/host/adapter-set.ts +6 -4
- package/src/host/adapter-types.ts +1 -1
- package/src/host/bundle-manager.ts +195 -0
- package/src/host/cli.ts +3 -1
- package/src/host/config-sync.ts +386 -0
- package/src/host/dsh-plugins.ts +60 -0
- package/src/host/extension.ts +1 -1
- package/src/host/flow/__tests__/advanced.test.ts +302 -0
- package/src/host/flow/__tests__/check.test.ts +169 -0
- package/src/host/flow/__tests__/run.test.ts +301 -0
- package/src/host/flow/check.ts +216 -0
- package/src/host/flow/expr.ts +94 -0
- package/src/host/flow/kit.ts +30 -0
- package/src/host/flow/run.ts +325 -0
- package/src/host/flow/types.ts +199 -0
- package/src/host/host-actions.ts +7 -0
- package/src/host/host-kit.ts +26 -0
- package/src/host/http.ts +6 -8
- package/src/host/installed-module.ts +272 -0
- package/src/host/lib-install.ts +14 -12
- package/src/host/lib-runners.ts +6 -8
- package/src/host/local-declaration.ts +37 -0
- package/src/host/market-index-claim.ts +0 -4
- package/src/host/market-index-doc.ts +1 -53
- package/src/host/market-index-parse.ts +21 -85
- package/src/host/market-index.ts +33 -127
- package/src/host/module-root.ts +6 -0
- package/src/host/npm-registry.ts +21 -0
- package/src/host/package-content.ts +76 -0
- package/src/host/package-kind.ts +63 -0
- package/src/host/package-route.ts +93 -0
- package/src/host/paths.ts +27 -52
- package/src/host/platform.ts +1 -1
- package/src/host/plugin-ledger.ts +25 -18
- package/src/host/plugin.ts +4 -5
- package/src/host/port-types.ts +43 -9
- package/src/host/preflight.ts +3 -2
- package/src/host/profile-create.ts +133 -0
- package/src/host/profile-registries.ts +145 -0
- package/src/host/registry-declaration.ts +70 -0
- package/src/host/release-probe.ts +202 -0
- package/src/host/shell.ts +0 -9
- package/src/host/spec.ts +1 -1
- package/src/host/supply-index.ts +9 -7
- package/src/host/supply-port.ts +13 -3
- package/src/host/unit-face.ts +2 -1
- package/src/host/version.ts +12 -7
- package/src/host.ts +34 -6
- package/src/i18n/{index.test.ts → __tests__/index.test.ts} +3 -3
- package/src/i18n/{merge.test.ts → __tests__/merge.test.ts} +1 -1
- package/src/i18n/{zh.test.ts → __tests__/zh.test.ts} +2 -2
- package/src/i18n/en.ts +19 -21
- package/src/i18n/runtime-terms.ts +26 -0
- package/src/i18n/zh.ts +19 -21
- package/src/index.ts +4 -0
- package/src/lib/{board-fact.test.ts → __tests__/board-fact.test.ts} +1 -1
- package/src/lib/{busy.test.ts → __tests__/busy.test.ts} +1 -1
- package/src/lib/{form-problem.test.ts → __tests__/form-problem.test.ts} +1 -1
- package/src/lib/{git-address.test.ts → __tests__/git-address.test.ts} +1 -1
- package/src/lib/{react.test.ts → __tests__/react.test.ts} +1 -1
- package/src/lib/{text.test.ts → __tests__/text.test.ts} +1 -1
- package/src/lib/{ui.test.ts → __tests__/ui.test.ts} +1 -1
- package/src/testing/__tests__/fake-adapter-set.test.ts +28 -0
- package/src/testing/fake-adapter-set.ts +173 -0
- package/src/testing/needs.ts +1 -1
- package/src/testing.ts +1 -0
- package/src/host/__tests__/command-name.test.ts +0 -16
- package/src/host/__tests__/opencli-runner.test.ts +0 -120
- package/src/host/channel-plugin.ts +0 -55
- package/src/host/command-name.ts +0 -15
- package/src/host/opencli-runner.ts +0 -76
- package/src/host/paths.test.ts +0 -26
- package/src/host/plugin-source.ts +0 -137
- package/src/host/runtime-links.test.ts +0 -31
- package/src/host/runtime-links.ts +0 -23
|
@@ -3,7 +3,7 @@ import type { ModuleStore } from './workspace-store.js';
|
|
|
3
3
|
export const PLUGIN_LEDGER_FILE = 'plugins.json';
|
|
4
4
|
|
|
5
5
|
export interface PluginLedgerEntry {
|
|
6
|
-
readonly
|
|
6
|
+
readonly home: string;
|
|
7
7
|
readonly package: string;
|
|
8
8
|
readonly name?: string;
|
|
9
9
|
readonly registry?: string;
|
|
@@ -13,10 +13,10 @@ export interface PluginLedgerEntry {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export function ledgerKeyOf(entry: {
|
|
16
|
-
readonly
|
|
16
|
+
readonly home: string;
|
|
17
17
|
readonly package: string;
|
|
18
18
|
}): string {
|
|
19
|
-
return `${entry.
|
|
19
|
+
return `${entry.home}\u0000${entry.package}`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function textOf(value: unknown): string | undefined {
|
|
@@ -25,18 +25,25 @@ function textOf(value: unknown): string | undefined {
|
|
|
25
25
|
return trimmed === '' ? undefined : trimmed;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function legacyHomeOf(box: Readonly<Record<string, unknown>>): unknown {
|
|
29
|
+
for (const [key, value] of Object.entries(box)) {
|
|
30
|
+
if (key !== 'home' && key.toLowerCase().endsWith('home')) return value;
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
28
35
|
function entryOf(value: unknown): PluginLedgerEntry | undefined {
|
|
29
36
|
if (typeof value !== 'object' || value === null) return undefined;
|
|
30
37
|
const box = value as Readonly<Record<string, unknown>>;
|
|
31
|
-
const
|
|
38
|
+
const home = textOf(box['home']) ?? textOf(legacyHomeOf(box));
|
|
32
39
|
const pkg = textOf(box['package']);
|
|
33
40
|
const installedAt = textOf(box['installedAt']);
|
|
34
|
-
if (
|
|
41
|
+
if (home === undefined || pkg === undefined || installedAt === undefined) return undefined;
|
|
35
42
|
const name = textOf(box['name']);
|
|
36
43
|
const registry = textOf(box['registry']);
|
|
37
44
|
const releasedAt = textOf(box['releasedAt']);
|
|
38
45
|
return {
|
|
39
|
-
|
|
46
|
+
home,
|
|
40
47
|
package: pkg,
|
|
41
48
|
installedAt,
|
|
42
49
|
...(name === undefined ? {} : { name }),
|
|
@@ -74,9 +81,9 @@ export function writePluginLedger(store: ModuleStore, entries: readonly PluginLe
|
|
|
74
81
|
|
|
75
82
|
export function pluginEntriesOfHome(
|
|
76
83
|
entries: readonly PluginLedgerEntry[],
|
|
77
|
-
|
|
84
|
+
home: string,
|
|
78
85
|
): PluginLedgerEntry[] {
|
|
79
|
-
return entries.filter((entry) => entry.
|
|
86
|
+
return entries.filter((entry) => entry.home === home);
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
export function isHeldEntry(entry: PluginLedgerEntry): boolean {
|
|
@@ -85,30 +92,30 @@ export function isHeldEntry(entry: PluginLedgerEntry): boolean {
|
|
|
85
92
|
|
|
86
93
|
export function heldEntriesOfHome(
|
|
87
94
|
entries: readonly PluginLedgerEntry[],
|
|
88
|
-
|
|
95
|
+
home: string,
|
|
89
96
|
): PluginLedgerEntry[] {
|
|
90
|
-
return pluginEntriesOfHome(entries,
|
|
97
|
+
return pluginEntriesOfHome(entries, home).filter(isHeldEntry);
|
|
91
98
|
}
|
|
92
99
|
|
|
93
100
|
export function heldEntryOfPlugin(
|
|
94
101
|
entries: readonly PluginLedgerEntry[],
|
|
95
|
-
|
|
102
|
+
home: string,
|
|
96
103
|
plugin: string,
|
|
97
104
|
): PluginLedgerEntry | undefined {
|
|
98
105
|
const key = plugin.trim();
|
|
99
106
|
if (key === '') return undefined;
|
|
100
|
-
return heldEntriesOfHome(entries,
|
|
107
|
+
return heldEntriesOfHome(entries, home).find(
|
|
101
108
|
(entry) => entry.name === key || entry.package === key,
|
|
102
109
|
);
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
export function withReleasedEntry(
|
|
106
113
|
entries: readonly PluginLedgerEntry[],
|
|
107
|
-
|
|
114
|
+
home: string,
|
|
108
115
|
pkg: string,
|
|
109
116
|
releasedAt: string,
|
|
110
117
|
): PluginLedgerEntry[] {
|
|
111
|
-
const entry = pluginEntryOf(entries,
|
|
118
|
+
const entry = pluginEntryOf(entries, home, pkg);
|
|
112
119
|
if (entry === undefined || entry.releasedAt !== undefined) return [...entries];
|
|
113
120
|
return withPluginEntry(entries, { ...entry, releasedAt });
|
|
114
121
|
}
|
|
@@ -127,10 +134,10 @@ export function pluginRetentionOf(options: {
|
|
|
127
134
|
|
|
128
135
|
export function pluginEntryOf(
|
|
129
136
|
entries: readonly PluginLedgerEntry[],
|
|
130
|
-
|
|
137
|
+
home: string,
|
|
131
138
|
pkg: string,
|
|
132
139
|
): PluginLedgerEntry | undefined {
|
|
133
|
-
const key = ledgerKeyOf({
|
|
140
|
+
const key = ledgerKeyOf({ home, package: pkg });
|
|
134
141
|
return entries.find((entry) => ledgerKeyOf(entry) === key);
|
|
135
142
|
}
|
|
136
143
|
|
|
@@ -144,9 +151,9 @@ export function withPluginEntry(
|
|
|
144
151
|
|
|
145
152
|
export function withoutPluginEntry(
|
|
146
153
|
entries: readonly PluginLedgerEntry[],
|
|
147
|
-
|
|
154
|
+
home: string,
|
|
148
155
|
pkg: string,
|
|
149
156
|
): PluginLedgerEntry[] {
|
|
150
|
-
const key = ledgerKeyOf({
|
|
157
|
+
const key = ledgerKeyOf({ home, package: pkg });
|
|
151
158
|
return entries.filter((entry) => ledgerKeyOf(entry) !== key);
|
|
152
159
|
}
|
package/src/host/plugin.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
|
|
4
|
-
export const OPENCLI_MANIFEST_FILE = 'opencli-plugin.json';
|
|
5
4
|
|
|
6
5
|
function asRecord(value: unknown): Readonly<Record<string, unknown>> | undefined {
|
|
7
6
|
return typeof value === 'object' && value !== null ? (value as Readonly<Record<string, unknown>>) : undefined;
|
|
@@ -13,9 +12,9 @@ function isCommandFile(name: string): boolean {
|
|
|
13
12
|
return name.endsWith('.ts') || name.endsWith('.js');
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
function pluginManifestOf(dir: string): Readonly<Record<string, unknown>> | undefined {
|
|
15
|
+
function pluginManifestOf(dir: string, manifestFile: string): Readonly<Record<string, unknown>> | undefined {
|
|
17
16
|
try {
|
|
18
|
-
const parsed: unknown = JSON.parse(readFileSync(join(dir,
|
|
17
|
+
const parsed: unknown = JSON.parse(readFileSync(join(dir, manifestFile), 'utf8'));
|
|
19
18
|
if (Array.isArray(parsed)) return undefined;
|
|
20
19
|
return asRecord(parsed);
|
|
21
20
|
} catch {
|
|
@@ -31,8 +30,8 @@ function hasCommandFile(dir: string): boolean {
|
|
|
31
30
|
}
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
export function pluginAt(dir: string): { readonly name?: string } | undefined {
|
|
35
|
-
const manifest = pluginManifestOf(dir);
|
|
33
|
+
export function pluginAt(dir: string, manifestFile: string): { readonly name?: string } | undefined {
|
|
34
|
+
const manifest = pluginManifestOf(dir, manifestFile);
|
|
36
35
|
if (manifest === undefined || !hasCommandFile(dir)) return undefined;
|
|
37
36
|
const name = manifest.name;
|
|
38
37
|
return typeof name === 'string' && name.trim() !== '' ? { name: name.trim() } : {};
|
package/src/host/port-types.ts
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import type { DshHostContext, DshShell } from '../api/dsh.js';
|
|
2
|
+
import type { RuntimeCapabilities, RuntimeProfile } from '../contract/capabilities.js';
|
|
3
|
+
import type { PluginPackageKind } from '../contract/protocol/bundle-install.js';
|
|
2
4
|
import type { LibNeed, SupplySource } from '../contract/needs.js';
|
|
3
5
|
import type { NeedProbeInput, UnitCapability } from '../contract/unit-capability.js';
|
|
6
|
+
import type { DataReader } from '../contract/data.js';
|
|
7
|
+
import type { LibRunner } from '../contract/runner.js';
|
|
4
8
|
import type { WorkbenchSelfUpdate } from '../contract/protocol/bundle-install.js';
|
|
5
9
|
import type {
|
|
6
10
|
BundleManager,
|
|
7
11
|
InstalledModule,
|
|
8
|
-
OpencliResolution,
|
|
9
12
|
ProfileModule,
|
|
10
13
|
ProfilePackage,
|
|
11
14
|
RegistryNotebook,
|
|
15
|
+
RuntimeStatus,
|
|
12
16
|
SelfUpdateRequest,
|
|
13
17
|
SelfUpdateStart,
|
|
14
18
|
} from './adapter-types.js';
|
|
15
19
|
import type { PostResource, RouteHandler, RouteTable } from './http.js';
|
|
16
|
-
import type { PluginSourceOptions, ResolvedPluginSource } from '
|
|
20
|
+
import type { PluginSourceOptions, ResolvedPluginSource } from '../contract/plugin-source.js';
|
|
17
21
|
|
|
18
22
|
export interface LoggerLike {
|
|
19
23
|
info?(message: string): void;
|
|
@@ -29,7 +33,7 @@ export interface ProfilePort {
|
|
|
29
33
|
export type SupplyPluginSource = ResolvedPluginSource;
|
|
30
34
|
|
|
31
35
|
export interface SupplyPackageRoute {
|
|
32
|
-
readonly kind:
|
|
36
|
+
readonly kind: PluginPackageKind | 'dsh-bundle' | 'neither';
|
|
33
37
|
readonly pkg?: string;
|
|
34
38
|
readonly pluginName?: string;
|
|
35
39
|
}
|
|
@@ -85,6 +89,7 @@ export interface EnvSupplyPort {
|
|
|
85
89
|
readonly source: Extract<SupplySource, { kind: 'npm' }>;
|
|
86
90
|
readonly version?: string | undefined;
|
|
87
91
|
}): string;
|
|
92
|
+
libUninstallCommand(options: { readonly need: LibNeed }): string;
|
|
88
93
|
pluginInstallPlan(options: SupplyPluginInstallOptions): ResolvedPluginSource;
|
|
89
94
|
latestVersion(options: SupplyLatestOptions): Promise<string | undefined>;
|
|
90
95
|
missingReleaseParts(options: SupplyMissingPartsOptions): Promise<readonly string[]>;
|
|
@@ -93,10 +98,14 @@ export interface EnvSupplyPort {
|
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
export interface ModulePort {
|
|
101
|
+
isolatedRoot(): string;
|
|
96
102
|
isolatedHome(): string;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
pluginPackagePrefix?(): string;
|
|
104
|
+
pluginManifestFile?(): string;
|
|
105
|
+
cliCatalogPath?(): string;
|
|
106
|
+
vendorPluginsDir?(): string;
|
|
107
|
+
runtimeInstallCommand?(): string;
|
|
108
|
+
afterLibInstall(): void;
|
|
100
109
|
profileDirOf(ctx: DshHostContext): string | null;
|
|
101
110
|
profileModules(dir: string | null): readonly ProfileModule[];
|
|
102
111
|
readInstalledModule(dir: string | null, pkg: string): InstalledModule;
|
|
@@ -140,8 +149,10 @@ export interface ProbePreflightInput {
|
|
|
140
149
|
readonly routes: RouteTable;
|
|
141
150
|
readonly ctx: DshHostContext;
|
|
142
151
|
readonly shell: DshShell;
|
|
143
|
-
readonly
|
|
144
|
-
readonly
|
|
152
|
+
readonly runtimeCommands: Readonly<Record<string, string>>;
|
|
153
|
+
readonly runtimeCapabilities: RuntimeCapabilities;
|
|
154
|
+
readonly runtimeCheck?: (() => Promise<RuntimeCheck>) | undefined;
|
|
155
|
+
readonly runtimeLabel?: string | undefined;
|
|
145
156
|
readonly warmReads: () => readonly (readonly string[])[];
|
|
146
157
|
}
|
|
147
158
|
|
|
@@ -156,9 +167,32 @@ export interface RuntimeSeedOptions {
|
|
|
156
167
|
readonly log: (message: string) => void;
|
|
157
168
|
}
|
|
158
169
|
|
|
170
|
+
export type { RuntimeCapabilities, RuntimeProfile, RuntimeTerms } from '../contract/capabilities.js';
|
|
171
|
+
|
|
172
|
+
export interface RuntimeCheck {
|
|
173
|
+
readonly ok: boolean;
|
|
174
|
+
readonly detail: string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface RuntimeCommandSurface {
|
|
178
|
+
readonly name: string;
|
|
179
|
+
readonly pluginPrefix: string;
|
|
180
|
+
invoke(site: string, command?: string): string;
|
|
181
|
+
help(site: string): string;
|
|
182
|
+
browser?(verb: readonly string[]): string;
|
|
183
|
+
background?(command: string, argv: readonly string[]): string;
|
|
184
|
+
readonly authorHint?: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
159
187
|
export interface RuntimePort {
|
|
160
188
|
packageRoot(moduleUrl: string): string;
|
|
161
|
-
|
|
189
|
+
capabilities(ctx: DshHostContext): RuntimeCapabilities;
|
|
190
|
+
profile?(ctx: DshHostContext): RuntimeProfile;
|
|
191
|
+
commandSurface?(ctx: DshHostContext): RuntimeCommandSurface | undefined;
|
|
192
|
+
check?(ctx: DshHostContext): Promise<RuntimeCheck>;
|
|
193
|
+
commands(ctx: DshHostContext): Readonly<Record<string, string>>;
|
|
194
|
+
runners(ctx: DshHostContext, data: DataReader): Readonly<Record<string, LibRunner>>;
|
|
195
|
+
health(ctx: DshHostContext): Readonly<Record<string, RuntimeStatus>>;
|
|
162
196
|
seed(ctx: DshHostContext, options: RuntimeSeedOptions): void;
|
|
163
197
|
warnUnwritableRoot(ctx: DshHostContext): void;
|
|
164
198
|
}
|
package/src/host/preflight.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
export type PreflightStepId = 'toolchain' | '
|
|
1
|
+
export type PreflightStepId = 'toolchain' | 'cli' | 'bridge' | 'runtime' | 'diy' | 'warm';
|
|
2
2
|
export type PreflightStepStatus = 'pending' | 'running' | 'ok' | 'warn' | 'failed';
|
|
3
3
|
export type PreflightFailure = 'permission' | 'network' | 'missing' | 'unknown';
|
|
4
4
|
export type PreflightGuidance = 'install' | 'path' | 'shell';
|
|
5
|
-
export type PreflightBridgeIssue =
|
|
5
|
+
export type PreflightBridgeIssue = string;
|
|
6
6
|
|
|
7
7
|
export interface PreflightStep {
|
|
8
8
|
readonly id: PreflightStepId;
|
|
9
|
+
readonly label?: string;
|
|
9
10
|
readonly status: PreflightStepStatus;
|
|
10
11
|
readonly detail?: string;
|
|
11
12
|
readonly action?: string;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
2
|
+
import { join, resolve, sep } from 'node:path';
|
|
3
|
+
import type { PostResource, RouteHandler } from './http.js';
|
|
4
|
+
import { quoteShellArg } from './shell-text.js';
|
|
5
|
+
import type {
|
|
6
|
+
ProfileCreateApplied,
|
|
7
|
+
ProfileCreateProfile,
|
|
8
|
+
ProfileCreateSnapshot,
|
|
9
|
+
} from '../contract/protocol/profile-create.js';
|
|
10
|
+
import { PROFILE_CREATE_RESOURCE } from '../contract/protocol/profile-create.js';
|
|
11
|
+
|
|
12
|
+
const PROFILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
13
|
+
const BUNDLE = '@dsh-workbench/core';
|
|
14
|
+
|
|
15
|
+
export interface ProfileCreateDeps {
|
|
16
|
+
readonly home: string;
|
|
17
|
+
readonly self: string;
|
|
18
|
+
readonly packageRoot: string;
|
|
19
|
+
readonly runSetup?: ((env: Readonly<Record<string, string>>) => Promise<{ ok: boolean; log: string }>) | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function textOf(value: unknown): string {
|
|
23
|
+
return typeof value === 'string' ? value : '';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function profileNames(profilesDir: string): readonly string[] {
|
|
27
|
+
try {
|
|
28
|
+
return readdirSync(profilesDir, { withFileTypes: true })
|
|
29
|
+
.filter((entry) => entry.isDirectory() || entry.isSymbolicLink())
|
|
30
|
+
.map((entry) => entry.name)
|
|
31
|
+
.filter((name) => existsSync(join(profilesDir, name, 'package.json')))
|
|
32
|
+
.sort((a, b) => a.localeCompare(b));
|
|
33
|
+
} catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function profileCreateSnapshot(deps: ProfileCreateDeps): ProfileCreateSnapshot {
|
|
39
|
+
const profiles: ProfileCreateProfile[] = profileNames(join(deps.home, 'profiles')).map((name) => ({
|
|
40
|
+
name,
|
|
41
|
+
current: name === deps.self,
|
|
42
|
+
readable: true,
|
|
43
|
+
}));
|
|
44
|
+
return { current: deps.self, profiles };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function profileCreateHandler(deps: ProfileCreateDeps): RouteHandler {
|
|
48
|
+
return () => ({ ok: true, ...profileCreateSnapshot(deps) });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function inside(base: string, path: string): boolean {
|
|
52
|
+
const root = resolve(base);
|
|
53
|
+
const target = resolve(path);
|
|
54
|
+
return target === root ? false : target.startsWith(`${root}${sep}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function profileCreateApply(
|
|
58
|
+
deps: ProfileCreateDeps,
|
|
59
|
+
payload: unknown,
|
|
60
|
+
): Promise<ProfileCreateApplied | { ok: false; text: string }> {
|
|
61
|
+
const body = typeof payload === 'object' && payload !== null ? (payload as Record<string, unknown>) : {};
|
|
62
|
+
const profile = textOf(body['profile']).trim();
|
|
63
|
+
const source = textOf(body['source']).trim();
|
|
64
|
+
const copy = typeof body['copy'] === 'object' && body['copy'] !== null ? (body['copy'] as Record<string, unknown>) : {};
|
|
65
|
+
const flags = {
|
|
66
|
+
settings: copy['settings'] === true,
|
|
67
|
+
};
|
|
68
|
+
if (!PROFILE_NAME.test(profile)) return { ok: false, text: '新工作台的名字不合法:只用字母、数字、点、下划线或短横线' };
|
|
69
|
+
if (source === '') return { ok: false, text: '先选一个来源 profile' };
|
|
70
|
+
if (source === profile) return { ok: false, text: '来源不能是新工作台自己' };
|
|
71
|
+
const profilesDir = join(deps.home, 'profiles');
|
|
72
|
+
if (!profileNames(profilesDir).includes(source)) return { ok: false, text: `没有这个 profile:${source}` };
|
|
73
|
+
if (existsSync(join(profilesDir, profile))) return { ok: false, text: `已经有一个叫 ${profile} 的 profile` };
|
|
74
|
+
const targetHome = join(deps.home, profile);
|
|
75
|
+
if (existsSync(targetHome)) return { ok: false, text: `运行时目录已经存在:${targetHome}` };
|
|
76
|
+
const env = {
|
|
77
|
+
DSH_HOME: deps.home,
|
|
78
|
+
DSH_WORKBENCH_HOME: targetHome,
|
|
79
|
+
WORKBENCH_PROFILE: profile,
|
|
80
|
+
WORKBENCH_SOURCE_PROFILE: source,
|
|
81
|
+
WB_CLONE: '1',
|
|
82
|
+
WB_COPY_SETTINGS: flags.settings ? '1' : '0',
|
|
83
|
+
};
|
|
84
|
+
const setup = deps.runSetup;
|
|
85
|
+
if (setup === undefined) {
|
|
86
|
+
return { ok: false, text: '这台机器上没有装工作台的脚本入口,建不了新的工作台。' };
|
|
87
|
+
}
|
|
88
|
+
const cleanup = (): void => {
|
|
89
|
+
try {
|
|
90
|
+
if (inside(profilesDir, join(profilesDir, profile))) rmSync(join(profilesDir, profile), { recursive: true, force: true });
|
|
91
|
+
if (inside(deps.home, targetHome)) rmSync(targetHome, { recursive: true, force: true });
|
|
92
|
+
} catch {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const result = await setup(env);
|
|
97
|
+
if (!result.ok) {
|
|
98
|
+
cleanup();
|
|
99
|
+
return { ok: false, text: `创建失败:${result.log.trim().slice(-1200) || 'setup 没给出原因'}` };
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const manifest = JSON.parse(readFileSync(join(profilesDir, profile, 'package.json'), 'utf8')) as {
|
|
103
|
+
dsh?: { profile?: { bundles?: unknown } };
|
|
104
|
+
};
|
|
105
|
+
const bundles = manifest.dsh?.profile?.bundles;
|
|
106
|
+
if (!Array.isArray(bundles) || !bundles.includes(BUNDLE)) {
|
|
107
|
+
cleanup();
|
|
108
|
+
return { ok: false, text: 'profile 建好了,但工作台 bundle 没被选中;先跑一次 setup 修好它' };
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
cleanup();
|
|
112
|
+
return { ok: false, text: `profile 建好了,但读不了清单:${error instanceof Error ? error.message : String(error)}` };
|
|
113
|
+
}
|
|
114
|
+
const copied = Object.entries(flags)
|
|
115
|
+
.filter(([, on]) => on)
|
|
116
|
+
.map(([name]) => name);
|
|
117
|
+
const bin = join(deps.packageRoot, 'bin', 'dsh-workbench');
|
|
118
|
+
return {
|
|
119
|
+
ok: true,
|
|
120
|
+
profile,
|
|
121
|
+
source,
|
|
122
|
+
home: targetHome,
|
|
123
|
+
command: `WORKBENCH_PROFILE=${quoteShellArg(profile)} ${quoteShellArg(bin)}`,
|
|
124
|
+
copied,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function profileCreateAction(deps: ProfileCreateDeps): PostResource {
|
|
129
|
+
return {
|
|
130
|
+
resource: PROFILE_CREATE_RESOURCE,
|
|
131
|
+
handle: (payload: unknown) => profileCreateApply(deps, payload),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { RegistryEntry, RegistryNotebook } from './adapter-types.js';
|
|
2
|
+
import { normalizeRegistry, sameRegistry, scopeOf } from './adapter-types.js';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
export const REGISTRY_MARKER =
|
|
6
|
+
'# dsh-workbench 写的:这一行的包只在这个源上(dsh 卸载没有源参数,pnpm 得问得到)';
|
|
7
|
+
|
|
8
|
+
function registryRootOf(name: string, tarball: string): string | null {
|
|
9
|
+
const at = tarball.indexOf(`/${name}/-/`);
|
|
10
|
+
if (at > 0) return normalizeRegistry(tarball.slice(0, at));
|
|
11
|
+
try {
|
|
12
|
+
return normalizeRegistry(new URL(tarball).origin);
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const PACKAGE_KEY = /^ {2}'?(\S+)'?:\s*$/;
|
|
19
|
+
const PACKAGE_NAME = /^(?:@[^/\s]+\/)?[^/\s@]+$/;
|
|
20
|
+
const TARBALL = /tarball:\s*(\S+?)\}?\s*$/;
|
|
21
|
+
|
|
22
|
+
export function sourcesFromLockfile(text: string): readonly RegistryEntry[] {
|
|
23
|
+
const found = new Map<string, string | null>();
|
|
24
|
+
let name: string | null = null;
|
|
25
|
+
for (const line of text.split('\n')) {
|
|
26
|
+
const key = PACKAGE_KEY.exec(line);
|
|
27
|
+
if (key !== null) {
|
|
28
|
+
const raw = key[1] ?? '';
|
|
29
|
+
const at = raw.lastIndexOf('@');
|
|
30
|
+
const candidate = at > 0 ? raw.slice(0, at) : '';
|
|
31
|
+
name = PACKAGE_NAME.test(candidate) ? candidate : null;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (name === null) continue;
|
|
35
|
+
const tarball = TARBALL.exec(line);
|
|
36
|
+
if (tarball === null) continue;
|
|
37
|
+
const scope = scopeOf(name);
|
|
38
|
+
const root = registryRootOf(name, tarball[1] ?? '');
|
|
39
|
+
name = null;
|
|
40
|
+
if (scope === null || root === null) continue;
|
|
41
|
+
const seen = found.get(scope);
|
|
42
|
+
if (seen === undefined) found.set(scope, root);
|
|
43
|
+
else if (!sameRegistry(seen, root)) found.set(scope, null);
|
|
44
|
+
}
|
|
45
|
+
return [...found.entries()]
|
|
46
|
+
.filter((entry): entry is [string, string] => entry[1] !== null)
|
|
47
|
+
.map(([scope, registry]) => ({ scope, registry }));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface RegistryConfigMerge {
|
|
51
|
+
readonly text: string;
|
|
52
|
+
readonly added: readonly string[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function mergeRegistryConfig(text: string, entries: readonly RegistryEntry[]): RegistryConfigMerge {
|
|
56
|
+
const lines = text.split('\n');
|
|
57
|
+
const present = (key: string): boolean => lines.some((line) => line.trim().startsWith(`${key}=`));
|
|
58
|
+
const appended: string[] = [];
|
|
59
|
+
const added: string[] = [];
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
const key = `${entry.scope}:registry`;
|
|
62
|
+
if (entry.registry.trim() === '' || present(key)) continue;
|
|
63
|
+
lines.push(`${key}=${normalizeRegistry(entry.registry)}`);
|
|
64
|
+
appended.push(`${key}=${normalizeRegistry(entry.registry)}`);
|
|
65
|
+
added.push(key);
|
|
66
|
+
}
|
|
67
|
+
if (appended.length === 0) return { text, added: [] };
|
|
68
|
+
const base = text === '' ? '' : text.endsWith('\n') ? text : `${text}\n`;
|
|
69
|
+
const marker = base.includes(REGISTRY_MARKER) ? '' : `${REGISTRY_MARKER}\n`;
|
|
70
|
+
return { text: `${base}${marker}${appended.join('\n')}\n`, added };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function replaceScopeRegistry(text: string, scope: string, registry: string): string {
|
|
74
|
+
const key = `${scope}:registry`;
|
|
75
|
+
const value = normalizeRegistry(registry);
|
|
76
|
+
let found = false;
|
|
77
|
+
const lines = text.split('\n').map((line) => {
|
|
78
|
+
if (!line.trim().startsWith(`${key}=`)) return line;
|
|
79
|
+
found = true;
|
|
80
|
+
return `${key}=${value}`;
|
|
81
|
+
});
|
|
82
|
+
if (found) return lines.join('\n');
|
|
83
|
+
const base = text === '' ? '' : text.endsWith('\n') ? text : `${text}\n`;
|
|
84
|
+
return `${base}${key}=${value}\n`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readNpmrc(file: string): string {
|
|
88
|
+
try {
|
|
89
|
+
return fs.readFileSync(file, 'utf8');
|
|
90
|
+
} catch {
|
|
91
|
+
return '';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function writeNpmrc(file: string, text: string): void {
|
|
96
|
+
const temp = `${file}.dsh-workbench.tmp`;
|
|
97
|
+
fs.writeFileSync(temp, text, { mode: 0o600 });
|
|
98
|
+
fs.renameSync(temp, file);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function createRegistryNotebook(dir: string | null): RegistryNotebook {
|
|
102
|
+
return {
|
|
103
|
+
sources() {
|
|
104
|
+
if (dir === null) return [];
|
|
105
|
+
try {
|
|
106
|
+
return sourcesFromLockfile(fs.readFileSync(path.join(dir, 'pnpm-lock.yaml'), 'utf8'));
|
|
107
|
+
} catch {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
remember(entries) {
|
|
112
|
+
if (dir === null || entries.length === 0) return [];
|
|
113
|
+
const file = path.join(dir, '.npmrc');
|
|
114
|
+
const merged = mergeRegistryConfig(readNpmrc(file), entries);
|
|
115
|
+
if (merged.added.length === 0) return [];
|
|
116
|
+
writeNpmrc(file, merged.text);
|
|
117
|
+
return merged.added;
|
|
118
|
+
},
|
|
119
|
+
swapScope(scope, registry) {
|
|
120
|
+
if (dir === null || scope.trim() === '' || registry.trim() === '') return () => {};
|
|
121
|
+
const file = path.join(dir, '.npmrc');
|
|
122
|
+
const key = `${scope.trim()}:registry`;
|
|
123
|
+
const value = normalizeRegistry(registry);
|
|
124
|
+
const before = readNpmrc(file);
|
|
125
|
+
const previous = before.split('\n').find((line) => line.trim().startsWith(`${key}=`));
|
|
126
|
+
const after = replaceScopeRegistry(before, scope.trim(), registry);
|
|
127
|
+
if (after === before) return () => {};
|
|
128
|
+
writeNpmrc(file, after);
|
|
129
|
+
return () => {
|
|
130
|
+
const now = readNpmrc(file);
|
|
131
|
+
if (now === after) {
|
|
132
|
+
if (before === '') fs.rmSync(file, { force: true });
|
|
133
|
+
else writeNpmrc(file, before);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (!now.includes(`${key}=${value}`)) return;
|
|
137
|
+
const restored =
|
|
138
|
+
previous === undefined
|
|
139
|
+
? now.split('\n').filter((line) => !line.trim().startsWith(`${key}=`)).join('\n')
|
|
140
|
+
: now.replace(`${key}=${value}`, previous);
|
|
141
|
+
writeNpmrc(file, restored);
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { DshShell } from '../api/dsh.js';
|
|
2
|
+
import { runShell } from './shell.js';
|
|
3
|
+
import { quoteShellArg } from './shell-text.js';
|
|
4
|
+
import { parseJsonLoose } from '../lib/text.js';
|
|
5
|
+
import { VERSION_CHECK_TIMEOUT_MS, VERSION_CHECK_TTL_MS } from '../config/constants.js';
|
|
6
|
+
|
|
7
|
+
function recordOf(value: unknown): Readonly<Record<string, unknown>> | undefined {
|
|
8
|
+
return typeof value === 'object' && value !== null ? (value as Readonly<Record<string, unknown>>) : undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface RegistryDshLookup {
|
|
12
|
+
readonly readable: boolean;
|
|
13
|
+
readonly raw: unknown;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface RegistryLookup {
|
|
17
|
+
registryDshOf(options: RegistryDshOptions): Promise<RegistryDshLookup>;
|
|
18
|
+
registryDeclarationOf(options: RegistryDeclarationOptions): Promise<unknown | undefined>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface RegistryDshOptions {
|
|
22
|
+
readonly shell: DshShell;
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly registry?: string | undefined;
|
|
25
|
+
readonly force?: boolean;
|
|
26
|
+
readonly cacheDir?: string | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface RegistryDeclarationOptions extends RegistryDshOptions {
|
|
30
|
+
readonly registry: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createRegistryLookup(): RegistryLookup {
|
|
34
|
+
const cache = new Map<string, { at: number; raw: unknown }>();
|
|
35
|
+
return {
|
|
36
|
+
registryDshOf: (options) => dshOf(cache, options),
|
|
37
|
+
registryDeclarationOf: async (options) => {
|
|
38
|
+
const found = await dshOf(cache, options);
|
|
39
|
+
return found.readable ? recordOf(found.raw)?.workbench : undefined;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function dshOf(cache: Map<string, { at: number; raw: unknown }>, options: {
|
|
45
|
+
shell: DshShell;
|
|
46
|
+
name: string;
|
|
47
|
+
registry?: string | undefined;
|
|
48
|
+
force?: boolean;
|
|
49
|
+
cacheDir?: string | undefined;
|
|
50
|
+
}): Promise<RegistryDshLookup> {
|
|
51
|
+
const { shell, name, registry, force = false, cacheDir } = options;
|
|
52
|
+
const key = `${registry ?? ''}|${name}`;
|
|
53
|
+
const hit = cache.get(key);
|
|
54
|
+
if (!force && hit !== undefined && Date.now() - hit.at < VERSION_CHECK_TTL_MS) {
|
|
55
|
+
return { readable: true, raw: hit.raw };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const at = registry === undefined ? '' : ` --registry ${quoteShellArg(registry)}`;
|
|
59
|
+
const cached = cacheDir === undefined ? '' : ` --cache ${quoteShellArg(cacheDir)}`;
|
|
60
|
+
const result = await runShell(
|
|
61
|
+
shell,
|
|
62
|
+
`npm view ${quoteShellArg(name)} dsh --json${at}${cached}`,
|
|
63
|
+
{ timeoutMs: VERSION_CHECK_TIMEOUT_MS },
|
|
64
|
+
).catch(() => undefined);
|
|
65
|
+
const parsed = result?.exitCode === 0 ? parseJsonLoose(result.stdout) : undefined;
|
|
66
|
+
if (parsed === undefined) return { readable: false, raw: undefined };
|
|
67
|
+
cache.set(key, { at: Date.now(), raw: parsed });
|
|
68
|
+
return { readable: true, raw: parsed };
|
|
69
|
+
}
|
|
70
|
+
|