@dsh-workbench/shared 1.1.8 → 1.1.10
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 -1
- 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/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 +23 -24
- package/dist/i18n/index.d.ts +23 -24
- package/dist/i18n/runtime-terms.d.ts +15 -0
- package/dist/i18n/runtime-terms.js +22 -0
- package/dist/i18n/zh.d.ts +22 -23
- package/dist/i18n/zh.js +22 -23
- 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__/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/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 +23 -25
- package/src/i18n/runtime-terms.ts +26 -0
- package/src/i18n/zh.ts +22 -24
- 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
|
@@ -1,10 +1,14 @@
|
|
|
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
|
-
import type { BundleManager, InstalledModule,
|
|
9
|
+
import type { BundleManager, InstalledModule, ProfileModule, ProfilePackage, RegistryNotebook, RuntimeStatus, SelfUpdateRequest, SelfUpdateStart } from './adapter-types.js';
|
|
6
10
|
import type { PostResource, RouteHandler, RouteTable } from './http.js';
|
|
7
|
-
import type { PluginSourceOptions, ResolvedPluginSource } from '
|
|
11
|
+
import type { PluginSourceOptions, ResolvedPluginSource } from '../contract/plugin-source.js';
|
|
8
12
|
export interface LoggerLike {
|
|
9
13
|
info?(message: string): void;
|
|
10
14
|
warn?(message: string): void;
|
|
@@ -16,7 +20,7 @@ export interface ProfilePort {
|
|
|
16
20
|
}
|
|
17
21
|
export type SupplyPluginSource = ResolvedPluginSource;
|
|
18
22
|
export interface SupplyPackageRoute {
|
|
19
|
-
readonly kind:
|
|
23
|
+
readonly kind: PluginPackageKind | 'dsh-bundle' | 'neither';
|
|
20
24
|
readonly pkg?: string;
|
|
21
25
|
readonly pluginName?: string;
|
|
22
26
|
}
|
|
@@ -63,6 +67,9 @@ export interface EnvSupplyPort {
|
|
|
63
67
|
}>;
|
|
64
68
|
readonly version?: string | undefined;
|
|
65
69
|
}): string;
|
|
70
|
+
libUninstallCommand(options: {
|
|
71
|
+
readonly need: LibNeed;
|
|
72
|
+
}): string;
|
|
66
73
|
pluginInstallPlan(options: SupplyPluginInstallOptions): ResolvedPluginSource;
|
|
67
74
|
latestVersion(options: SupplyLatestOptions): Promise<string | undefined>;
|
|
68
75
|
missingReleaseParts(options: SupplyMissingPartsOptions): Promise<readonly string[]>;
|
|
@@ -70,10 +77,14 @@ export interface EnvSupplyPort {
|
|
|
70
77
|
splitNpmSpec(spec: string): SupplyNpmRef | undefined;
|
|
71
78
|
}
|
|
72
79
|
export interface ModulePort {
|
|
80
|
+
isolatedRoot(): string;
|
|
73
81
|
isolatedHome(): string;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
82
|
+
pluginPackagePrefix?(): string;
|
|
83
|
+
pluginManifestFile?(): string;
|
|
84
|
+
cliCatalogPath?(): string;
|
|
85
|
+
vendorPluginsDir?(): string;
|
|
86
|
+
runtimeInstallCommand?(): string;
|
|
87
|
+
afterLibInstall(): void;
|
|
77
88
|
profileDirOf(ctx: DshHostContext): string | null;
|
|
78
89
|
profileModules(dir: string | null): readonly ProfileModule[];
|
|
79
90
|
readInstalledModule(dir: string | null, pkg: string): InstalledModule;
|
|
@@ -116,8 +127,10 @@ export interface ProbePreflightInput {
|
|
|
116
127
|
readonly routes: RouteTable;
|
|
117
128
|
readonly ctx: DshHostContext;
|
|
118
129
|
readonly shell: DshShell;
|
|
119
|
-
readonly
|
|
120
|
-
readonly
|
|
130
|
+
readonly runtimeCommands: Readonly<Record<string, string>>;
|
|
131
|
+
readonly runtimeCapabilities: RuntimeCapabilities;
|
|
132
|
+
readonly runtimeCheck?: (() => Promise<RuntimeCheck>) | undefined;
|
|
133
|
+
readonly runtimeLabel?: string | undefined;
|
|
121
134
|
readonly warmReads: () => readonly (readonly string[])[];
|
|
122
135
|
}
|
|
123
136
|
export interface ProbePort {
|
|
@@ -129,9 +142,29 @@ export interface RuntimeSeedOptions {
|
|
|
129
142
|
readonly corePackageUrl: string;
|
|
130
143
|
readonly log: (message: string) => void;
|
|
131
144
|
}
|
|
145
|
+
export type { RuntimeCapabilities, RuntimeProfile, RuntimeTerms } from '../contract/capabilities.js';
|
|
146
|
+
export interface RuntimeCheck {
|
|
147
|
+
readonly ok: boolean;
|
|
148
|
+
readonly detail: string;
|
|
149
|
+
}
|
|
150
|
+
export interface RuntimeCommandSurface {
|
|
151
|
+
readonly name: string;
|
|
152
|
+
readonly pluginPrefix: string;
|
|
153
|
+
invoke(site: string, command?: string): string;
|
|
154
|
+
help(site: string): string;
|
|
155
|
+
browser?(verb: readonly string[]): string;
|
|
156
|
+
background?(command: string, argv: readonly string[]): string;
|
|
157
|
+
readonly authorHint?: string;
|
|
158
|
+
}
|
|
132
159
|
export interface RuntimePort {
|
|
133
160
|
packageRoot(moduleUrl: string): string;
|
|
134
|
-
|
|
161
|
+
capabilities(ctx: DshHostContext): RuntimeCapabilities;
|
|
162
|
+
profile?(ctx: DshHostContext): RuntimeProfile;
|
|
163
|
+
commandSurface?(ctx: DshHostContext): RuntimeCommandSurface | undefined;
|
|
164
|
+
check?(ctx: DshHostContext): Promise<RuntimeCheck>;
|
|
165
|
+
commands(ctx: DshHostContext): Readonly<Record<string, string>>;
|
|
166
|
+
runners(ctx: DshHostContext, data: DataReader): Readonly<Record<string, LibRunner>>;
|
|
167
|
+
health(ctx: DshHostContext): Readonly<Record<string, RuntimeStatus>>;
|
|
135
168
|
seed(ctx: DshHostContext, options: RuntimeSeedOptions): void;
|
|
136
169
|
warnUnwritableRoot(ctx: DshHostContext): void;
|
|
137
170
|
}
|
package/dist/host/preflight.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
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
|
export interface PreflightStep {
|
|
7
7
|
readonly id: PreflightStepId;
|
|
8
|
+
readonly label?: string;
|
|
8
9
|
readonly status: PreflightStepStatus;
|
|
9
10
|
readonly detail?: string;
|
|
10
11
|
readonly action?: string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { PostResource, RouteHandler } from './http.js';
|
|
2
|
+
import type { ProfileCreateApplied, ProfileCreateSnapshot } from '../contract/protocol/profile-create.js';
|
|
3
|
+
export interface ProfileCreateDeps {
|
|
4
|
+
readonly home: string;
|
|
5
|
+
readonly self: string;
|
|
6
|
+
readonly packageRoot: string;
|
|
7
|
+
readonly runSetup?: ((env: Readonly<Record<string, string>>) => Promise<{
|
|
8
|
+
ok: boolean;
|
|
9
|
+
log: string;
|
|
10
|
+
}>) | undefined;
|
|
11
|
+
}
|
|
12
|
+
export declare function profileCreateSnapshot(deps: ProfileCreateDeps): ProfileCreateSnapshot;
|
|
13
|
+
export declare function profileCreateHandler(deps: ProfileCreateDeps): RouteHandler;
|
|
14
|
+
export declare function profileCreateApply(deps: ProfileCreateDeps, payload: unknown): Promise<ProfileCreateApplied | {
|
|
15
|
+
ok: false;
|
|
16
|
+
text: string;
|
|
17
|
+
}>;
|
|
18
|
+
export declare function profileCreateAction(deps: ProfileCreateDeps): PostResource;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
2
|
+
import { join, resolve, sep } from 'node:path';
|
|
3
|
+
import { quoteShellArg } from './shell-text.js';
|
|
4
|
+
import { PROFILE_CREATE_RESOURCE } from '../contract/protocol/profile-create.js';
|
|
5
|
+
const PROFILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
6
|
+
const BUNDLE = '@dsh-workbench/core';
|
|
7
|
+
function textOf(value) {
|
|
8
|
+
return typeof value === 'string' ? value : '';
|
|
9
|
+
}
|
|
10
|
+
function profileNames(profilesDir) {
|
|
11
|
+
try {
|
|
12
|
+
return readdirSync(profilesDir, { withFileTypes: true })
|
|
13
|
+
.filter((entry) => entry.isDirectory() || entry.isSymbolicLink())
|
|
14
|
+
.map((entry) => entry.name)
|
|
15
|
+
.filter((name) => existsSync(join(profilesDir, name, 'package.json')))
|
|
16
|
+
.sort((a, b) => a.localeCompare(b));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function profileCreateSnapshot(deps) {
|
|
23
|
+
const profiles = profileNames(join(deps.home, 'profiles')).map((name) => ({
|
|
24
|
+
name,
|
|
25
|
+
current: name === deps.self,
|
|
26
|
+
readable: true,
|
|
27
|
+
}));
|
|
28
|
+
return { current: deps.self, profiles };
|
|
29
|
+
}
|
|
30
|
+
export function profileCreateHandler(deps) {
|
|
31
|
+
return () => ({ ok: true, ...profileCreateSnapshot(deps) });
|
|
32
|
+
}
|
|
33
|
+
function inside(base, path) {
|
|
34
|
+
const root = resolve(base);
|
|
35
|
+
const target = resolve(path);
|
|
36
|
+
return target === root ? false : target.startsWith(`${root}${sep}`);
|
|
37
|
+
}
|
|
38
|
+
export async function profileCreateApply(deps, payload) {
|
|
39
|
+
const body = typeof payload === 'object' && payload !== null ? payload : {};
|
|
40
|
+
const profile = textOf(body['profile']).trim();
|
|
41
|
+
const source = textOf(body['source']).trim();
|
|
42
|
+
const copy = typeof body['copy'] === 'object' && body['copy'] !== null ? body['copy'] : {};
|
|
43
|
+
const flags = {
|
|
44
|
+
settings: copy['settings'] === true,
|
|
45
|
+
};
|
|
46
|
+
if (!PROFILE_NAME.test(profile))
|
|
47
|
+
return { ok: false, text: '新工作台的名字不合法:只用字母、数字、点、下划线或短横线' };
|
|
48
|
+
if (source === '')
|
|
49
|
+
return { ok: false, text: '先选一个来源 profile' };
|
|
50
|
+
if (source === profile)
|
|
51
|
+
return { ok: false, text: '来源不能是新工作台自己' };
|
|
52
|
+
const profilesDir = join(deps.home, 'profiles');
|
|
53
|
+
if (!profileNames(profilesDir).includes(source))
|
|
54
|
+
return { ok: false, text: `没有这个 profile:${source}` };
|
|
55
|
+
if (existsSync(join(profilesDir, profile)))
|
|
56
|
+
return { ok: false, text: `已经有一个叫 ${profile} 的 profile` };
|
|
57
|
+
const targetHome = join(deps.home, profile);
|
|
58
|
+
if (existsSync(targetHome))
|
|
59
|
+
return { ok: false, text: `运行时目录已经存在:${targetHome}` };
|
|
60
|
+
const env = {
|
|
61
|
+
DSH_HOME: deps.home,
|
|
62
|
+
DSH_WORKBENCH_HOME: targetHome,
|
|
63
|
+
WORKBENCH_PROFILE: profile,
|
|
64
|
+
WORKBENCH_SOURCE_PROFILE: source,
|
|
65
|
+
WB_CLONE: '1',
|
|
66
|
+
WB_COPY_SETTINGS: flags.settings ? '1' : '0',
|
|
67
|
+
};
|
|
68
|
+
const setup = deps.runSetup;
|
|
69
|
+
if (setup === undefined) {
|
|
70
|
+
return { ok: false, text: '这台机器上没有装工作台的脚本入口,建不了新的工作台。' };
|
|
71
|
+
}
|
|
72
|
+
const cleanup = () => {
|
|
73
|
+
try {
|
|
74
|
+
if (inside(profilesDir, join(profilesDir, profile)))
|
|
75
|
+
rmSync(join(profilesDir, profile), { recursive: true, force: true });
|
|
76
|
+
if (inside(deps.home, targetHome))
|
|
77
|
+
rmSync(targetHome, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const result = await setup(env);
|
|
84
|
+
if (!result.ok) {
|
|
85
|
+
cleanup();
|
|
86
|
+
return { ok: false, text: `创建失败:${result.log.trim().slice(-1200) || 'setup 没给出原因'}` };
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const manifest = JSON.parse(readFileSync(join(profilesDir, profile, 'package.json'), 'utf8'));
|
|
90
|
+
const bundles = manifest.dsh?.profile?.bundles;
|
|
91
|
+
if (!Array.isArray(bundles) || !bundles.includes(BUNDLE)) {
|
|
92
|
+
cleanup();
|
|
93
|
+
return { ok: false, text: 'profile 建好了,但工作台 bundle 没被选中;先跑一次 setup 修好它' };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
cleanup();
|
|
98
|
+
return { ok: false, text: `profile 建好了,但读不了清单:${error instanceof Error ? error.message : String(error)}` };
|
|
99
|
+
}
|
|
100
|
+
const copied = Object.entries(flags)
|
|
101
|
+
.filter(([, on]) => on)
|
|
102
|
+
.map(([name]) => name);
|
|
103
|
+
const bin = join(deps.packageRoot, 'bin', 'dsh-workbench');
|
|
104
|
+
return {
|
|
105
|
+
ok: true,
|
|
106
|
+
profile,
|
|
107
|
+
source,
|
|
108
|
+
home: targetHome,
|
|
109
|
+
command: `WORKBENCH_PROFILE=${quoteShellArg(profile)} ${quoteShellArg(bin)}`,
|
|
110
|
+
copied,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export function profileCreateAction(deps) {
|
|
114
|
+
return {
|
|
115
|
+
resource: PROFILE_CREATE_RESOURCE,
|
|
116
|
+
handle: (payload) => profileCreateApply(deps, payload),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RegistryEntry, RegistryNotebook } from './adapter-types.js';
|
|
2
|
+
export declare const REGISTRY_MARKER = "# dsh-workbench \u5199\u7684\uFF1A\u8FD9\u4E00\u884C\u7684\u5305\u53EA\u5728\u8FD9\u4E2A\u6E90\u4E0A\uFF08dsh \u5378\u8F7D\u6CA1\u6709\u6E90\u53C2\u6570\uFF0Cpnpm \u5F97\u95EE\u5F97\u5230\uFF09";
|
|
3
|
+
export declare function sourcesFromLockfile(text: string): readonly RegistryEntry[];
|
|
4
|
+
export interface RegistryConfigMerge {
|
|
5
|
+
readonly text: string;
|
|
6
|
+
readonly added: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function mergeRegistryConfig(text: string, entries: readonly RegistryEntry[]): RegistryConfigMerge;
|
|
9
|
+
export declare function replaceScopeRegistry(text: string, scope: string, registry: string): string;
|
|
10
|
+
export declare function createRegistryNotebook(dir: string | null): RegistryNotebook;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { normalizeRegistry, sameRegistry, scopeOf } from './adapter-types.js';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
export const REGISTRY_MARKER = '# dsh-workbench 写的:这一行的包只在这个源上(dsh 卸载没有源参数,pnpm 得问得到)';
|
|
5
|
+
function registryRootOf(name, tarball) {
|
|
6
|
+
const at = tarball.indexOf(`/${name}/-/`);
|
|
7
|
+
if (at > 0)
|
|
8
|
+
return normalizeRegistry(tarball.slice(0, at));
|
|
9
|
+
try {
|
|
10
|
+
return normalizeRegistry(new URL(tarball).origin);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const PACKAGE_KEY = /^ {2}'?(\S+)'?:\s*$/;
|
|
17
|
+
const PACKAGE_NAME = /^(?:@[^/\s]+\/)?[^/\s@]+$/;
|
|
18
|
+
const TARBALL = /tarball:\s*(\S+?)\}?\s*$/;
|
|
19
|
+
export function sourcesFromLockfile(text) {
|
|
20
|
+
const found = new Map();
|
|
21
|
+
let name = null;
|
|
22
|
+
for (const line of text.split('\n')) {
|
|
23
|
+
const key = PACKAGE_KEY.exec(line);
|
|
24
|
+
if (key !== null) {
|
|
25
|
+
const raw = key[1] ?? '';
|
|
26
|
+
const at = raw.lastIndexOf('@');
|
|
27
|
+
const candidate = at > 0 ? raw.slice(0, at) : '';
|
|
28
|
+
name = PACKAGE_NAME.test(candidate) ? candidate : null;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (name === null)
|
|
32
|
+
continue;
|
|
33
|
+
const tarball = TARBALL.exec(line);
|
|
34
|
+
if (tarball === null)
|
|
35
|
+
continue;
|
|
36
|
+
const scope = scopeOf(name);
|
|
37
|
+
const root = registryRootOf(name, tarball[1] ?? '');
|
|
38
|
+
name = null;
|
|
39
|
+
if (scope === null || root === null)
|
|
40
|
+
continue;
|
|
41
|
+
const seen = found.get(scope);
|
|
42
|
+
if (seen === undefined)
|
|
43
|
+
found.set(scope, root);
|
|
44
|
+
else if (!sameRegistry(seen, root))
|
|
45
|
+
found.set(scope, null);
|
|
46
|
+
}
|
|
47
|
+
return [...found.entries()]
|
|
48
|
+
.filter((entry) => entry[1] !== null)
|
|
49
|
+
.map(([scope, registry]) => ({ scope, registry }));
|
|
50
|
+
}
|
|
51
|
+
export function mergeRegistryConfig(text, entries) {
|
|
52
|
+
const lines = text.split('\n');
|
|
53
|
+
const present = (key) => lines.some((line) => line.trim().startsWith(`${key}=`));
|
|
54
|
+
const appended = [];
|
|
55
|
+
const added = [];
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const key = `${entry.scope}:registry`;
|
|
58
|
+
if (entry.registry.trim() === '' || present(key))
|
|
59
|
+
continue;
|
|
60
|
+
lines.push(`${key}=${normalizeRegistry(entry.registry)}`);
|
|
61
|
+
appended.push(`${key}=${normalizeRegistry(entry.registry)}`);
|
|
62
|
+
added.push(key);
|
|
63
|
+
}
|
|
64
|
+
if (appended.length === 0)
|
|
65
|
+
return { text, added: [] };
|
|
66
|
+
const base = text === '' ? '' : text.endsWith('\n') ? text : `${text}\n`;
|
|
67
|
+
const marker = base.includes(REGISTRY_MARKER) ? '' : `${REGISTRY_MARKER}\n`;
|
|
68
|
+
return { text: `${base}${marker}${appended.join('\n')}\n`, added };
|
|
69
|
+
}
|
|
70
|
+
export function replaceScopeRegistry(text, scope, registry) {
|
|
71
|
+
const key = `${scope}:registry`;
|
|
72
|
+
const value = normalizeRegistry(registry);
|
|
73
|
+
let found = false;
|
|
74
|
+
const lines = text.split('\n').map((line) => {
|
|
75
|
+
if (!line.trim().startsWith(`${key}=`))
|
|
76
|
+
return line;
|
|
77
|
+
found = true;
|
|
78
|
+
return `${key}=${value}`;
|
|
79
|
+
});
|
|
80
|
+
if (found)
|
|
81
|
+
return lines.join('\n');
|
|
82
|
+
const base = text === '' ? '' : text.endsWith('\n') ? text : `${text}\n`;
|
|
83
|
+
return `${base}${key}=${value}\n`;
|
|
84
|
+
}
|
|
85
|
+
function readNpmrc(file) {
|
|
86
|
+
try {
|
|
87
|
+
return fs.readFileSync(file, 'utf8');
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return '';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function writeNpmrc(file, text) {
|
|
94
|
+
const temp = `${file}.dsh-workbench.tmp`;
|
|
95
|
+
fs.writeFileSync(temp, text, { mode: 0o600 });
|
|
96
|
+
fs.renameSync(temp, file);
|
|
97
|
+
}
|
|
98
|
+
export function createRegistryNotebook(dir) {
|
|
99
|
+
return {
|
|
100
|
+
sources() {
|
|
101
|
+
if (dir === null)
|
|
102
|
+
return [];
|
|
103
|
+
try {
|
|
104
|
+
return sourcesFromLockfile(fs.readFileSync(path.join(dir, 'pnpm-lock.yaml'), 'utf8'));
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
remember(entries) {
|
|
111
|
+
if (dir === null || entries.length === 0)
|
|
112
|
+
return [];
|
|
113
|
+
const file = path.join(dir, '.npmrc');
|
|
114
|
+
const merged = mergeRegistryConfig(readNpmrc(file), entries);
|
|
115
|
+
if (merged.added.length === 0)
|
|
116
|
+
return [];
|
|
117
|
+
writeNpmrc(file, merged.text);
|
|
118
|
+
return merged.added;
|
|
119
|
+
},
|
|
120
|
+
swapScope(scope, registry) {
|
|
121
|
+
if (dir === null || scope.trim() === '' || registry.trim() === '')
|
|
122
|
+
return () => { };
|
|
123
|
+
const file = path.join(dir, '.npmrc');
|
|
124
|
+
const key = `${scope.trim()}:registry`;
|
|
125
|
+
const value = normalizeRegistry(registry);
|
|
126
|
+
const before = readNpmrc(file);
|
|
127
|
+
const previous = before.split('\n').find((line) => line.trim().startsWith(`${key}=`));
|
|
128
|
+
const after = replaceScopeRegistry(before, scope.trim(), registry);
|
|
129
|
+
if (after === before)
|
|
130
|
+
return () => { };
|
|
131
|
+
writeNpmrc(file, after);
|
|
132
|
+
return () => {
|
|
133
|
+
const now = readNpmrc(file);
|
|
134
|
+
if (now === after) {
|
|
135
|
+
if (before === '')
|
|
136
|
+
fs.rmSync(file, { force: true });
|
|
137
|
+
else
|
|
138
|
+
writeNpmrc(file, before);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (!now.includes(`${key}=${value}`))
|
|
142
|
+
return;
|
|
143
|
+
const restored = previous === undefined
|
|
144
|
+
? now.split('\n').filter((line) => !line.trim().startsWith(`${key}=`)).join('\n')
|
|
145
|
+
: now.replace(`${key}=${value}`, previous);
|
|
146
|
+
writeNpmrc(file, restored);
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { DshShell } from '../api/dsh.js';
|
|
2
|
+
export interface RegistryDshLookup {
|
|
3
|
+
readonly readable: boolean;
|
|
4
|
+
readonly raw: unknown;
|
|
5
|
+
}
|
|
6
|
+
export interface RegistryLookup {
|
|
7
|
+
registryDshOf(options: RegistryDshOptions): Promise<RegistryDshLookup>;
|
|
8
|
+
registryDeclarationOf(options: RegistryDeclarationOptions): Promise<unknown | undefined>;
|
|
9
|
+
}
|
|
10
|
+
interface RegistryDshOptions {
|
|
11
|
+
readonly shell: DshShell;
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly registry?: string | undefined;
|
|
14
|
+
readonly force?: boolean;
|
|
15
|
+
readonly cacheDir?: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
interface RegistryDeclarationOptions extends RegistryDshOptions {
|
|
18
|
+
readonly registry: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function createRegistryLookup(): RegistryLookup;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { runShell } from './shell.js';
|
|
2
|
+
import { quoteShellArg } from './shell-text.js';
|
|
3
|
+
import { parseJsonLoose } from '../lib/text.js';
|
|
4
|
+
import { VERSION_CHECK_TIMEOUT_MS, VERSION_CHECK_TTL_MS } from '../config/constants.js';
|
|
5
|
+
function recordOf(value) {
|
|
6
|
+
return typeof value === 'object' && value !== null ? value : undefined;
|
|
7
|
+
}
|
|
8
|
+
export function createRegistryLookup() {
|
|
9
|
+
const cache = new Map();
|
|
10
|
+
return {
|
|
11
|
+
registryDshOf: (options) => dshOf(cache, options),
|
|
12
|
+
registryDeclarationOf: async (options) => {
|
|
13
|
+
const found = await dshOf(cache, options);
|
|
14
|
+
return found.readable ? recordOf(found.raw)?.workbench : undefined;
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
async function dshOf(cache, options) {
|
|
19
|
+
const { shell, name, registry, force = false, cacheDir } = options;
|
|
20
|
+
const key = `${registry ?? ''}|${name}`;
|
|
21
|
+
const hit = cache.get(key);
|
|
22
|
+
if (!force && hit !== undefined && Date.now() - hit.at < VERSION_CHECK_TTL_MS) {
|
|
23
|
+
return { readable: true, raw: hit.raw };
|
|
24
|
+
}
|
|
25
|
+
const at = registry === undefined ? '' : ` --registry ${quoteShellArg(registry)}`;
|
|
26
|
+
const cached = cacheDir === undefined ? '' : ` --cache ${quoteShellArg(cacheDir)}`;
|
|
27
|
+
const result = await runShell(shell, `npm view ${quoteShellArg(name)} dsh --json${at}${cached}`, { timeoutMs: VERSION_CHECK_TIMEOUT_MS }).catch(() => undefined);
|
|
28
|
+
const parsed = result?.exitCode === 0 ? parseJsonLoose(result.stdout) : undefined;
|
|
29
|
+
if (parsed === undefined)
|
|
30
|
+
return { readable: false, raw: undefined };
|
|
31
|
+
cache.set(key, { at: Date.now(), raw: parsed });
|
|
32
|
+
return { readable: true, raw: parsed };
|
|
33
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { DshShell } from '../api/dsh.js';
|
|
2
|
+
import type { PluginRelease } from '../contract/unit.js';
|
|
3
|
+
export declare function parseLedgerVersion(stdout: string, name: string): string | undefined;
|
|
4
|
+
export interface ReleaseProbe {
|
|
5
|
+
latestVersion(options: LatestVersionOptions): Promise<string | undefined>;
|
|
6
|
+
missingReleaseParts(options: MissingPartsOptions): Promise<readonly string[]>;
|
|
7
|
+
probeRelease(options: ProbeReleaseOptions): Promise<PluginRelease | undefined>;
|
|
8
|
+
reset(): void;
|
|
9
|
+
}
|
|
10
|
+
export declare function createReleaseProbe(): ReleaseProbe;
|
|
11
|
+
interface LatestVersionOptions {
|
|
12
|
+
readonly shell: DshShell;
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly registry: string;
|
|
15
|
+
readonly force?: boolean;
|
|
16
|
+
readonly cacheDir?: string | undefined;
|
|
17
|
+
}
|
|
18
|
+
interface MissingPartsOptions {
|
|
19
|
+
readonly shell: DshShell;
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly registry: string;
|
|
22
|
+
readonly version: string;
|
|
23
|
+
readonly force?: boolean;
|
|
24
|
+
readonly cacheDir?: string | undefined;
|
|
25
|
+
}
|
|
26
|
+
interface ProbeReleaseOptions {
|
|
27
|
+
readonly shell: DshShell;
|
|
28
|
+
readonly package: string;
|
|
29
|
+
readonly registry?: string | undefined;
|
|
30
|
+
readonly force?: boolean;
|
|
31
|
+
readonly cacheDir?: string | undefined;
|
|
32
|
+
readonly prefix?: string | undefined;
|
|
33
|
+
}
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { RELEASE_PACKAGES } from './release-packages.generated.js';
|
|
2
|
+
import { VERSION_CHECK_TIMEOUT_MS, VERSION_CHECK_TTL_MS } from '../config/constants.js';
|
|
3
|
+
import { isValidVersion } from './version.js';
|
|
4
|
+
import { parseJsonLoose } from '../lib/text.js';
|
|
5
|
+
import { quoteShellArg } from './shell-text.js';
|
|
6
|
+
import { runShell } from './shell.js';
|
|
7
|
+
function cacheFlag(cacheDir) {
|
|
8
|
+
return cacheDir === undefined ? '' : ` --cache ${quoteShellArg(cacheDir)}`;
|
|
9
|
+
}
|
|
10
|
+
export function parseLedgerVersion(stdout, name) {
|
|
11
|
+
const parsed = parseJsonLoose(stdout);
|
|
12
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
13
|
+
return undefined;
|
|
14
|
+
const entry = parsed
|
|
15
|
+
.dependencies?.[name];
|
|
16
|
+
if (typeof entry !== "object" || entry === null)
|
|
17
|
+
return undefined;
|
|
18
|
+
const version = entry.version;
|
|
19
|
+
return typeof version === "string" && isValidVersion(version.trim())
|
|
20
|
+
? version.trim()
|
|
21
|
+
: undefined;
|
|
22
|
+
}
|
|
23
|
+
function parseVersion(stdout) {
|
|
24
|
+
const value = stdout.trim();
|
|
25
|
+
return isValidVersion(value) ? value : undefined;
|
|
26
|
+
}
|
|
27
|
+
export function createReleaseProbe() {
|
|
28
|
+
const latestCache = new Map();
|
|
29
|
+
const existsCache = new Map();
|
|
30
|
+
return {
|
|
31
|
+
latestVersion: async (options) => latestVersionOf(latestCache, options),
|
|
32
|
+
missingReleaseParts: async (options) => missingPartsOf(latestCache, existsCache, options),
|
|
33
|
+
probeRelease: async (options) => probeOf(latestCache, options),
|
|
34
|
+
reset: () => {
|
|
35
|
+
latestCache.clear();
|
|
36
|
+
existsCache.clear();
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async function latestVersionOf(latestCache, options) {
|
|
41
|
+
const { shell, name, registry, force = false, cacheDir } = options;
|
|
42
|
+
const key = `${registry}|${name}`;
|
|
43
|
+
const hit = latestCache.get(key);
|
|
44
|
+
if (!force && hit !== undefined && Date.now() - hit.at < VERSION_CHECK_TTL_MS)
|
|
45
|
+
return hit.value;
|
|
46
|
+
const result = await runShell(shell, `npm view ${quoteShellArg(name)} version --registry ${quoteShellArg(registry)}${cacheFlag(cacheDir)}`, { timeoutMs: VERSION_CHECK_TIMEOUT_MS }).catch(() => undefined);
|
|
47
|
+
const read = result?.exitCode === 0 ? parseVersion(result.stdout) : undefined;
|
|
48
|
+
latestCache.set(key, { at: Date.now(), value: read });
|
|
49
|
+
return read;
|
|
50
|
+
}
|
|
51
|
+
async function versionExists(existsCache, options) {
|
|
52
|
+
const { shell, name, version, registry, force = false, cacheDir } = options;
|
|
53
|
+
const key = `${registry}|${name}@${version}`;
|
|
54
|
+
const hit = existsCache.get(key);
|
|
55
|
+
if (!force && hit !== undefined && (hit.value || Date.now() - hit.at < VERSION_CHECK_TTL_MS))
|
|
56
|
+
return hit.value;
|
|
57
|
+
const result = await runShell(shell, `npm view ${quoteShellArg(`${name}@${version}`)} version --registry ${quoteShellArg(registry)}${cacheFlag(cacheDir)}`, { timeoutMs: VERSION_CHECK_TIMEOUT_MS }).catch(() => undefined);
|
|
58
|
+
const value = result?.exitCode === 0 && parseVersion(result.stdout) !== undefined;
|
|
59
|
+
existsCache.set(key, { at: Date.now(), value });
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
async function missingPartsOf(latestCache, existsCache, options) {
|
|
63
|
+
const { shell, name, registry, version, force = false, cacheDir } = options;
|
|
64
|
+
const checked = await Promise.all(RELEASE_PACKAGES.filter((part) => part !== name).map(async (part) => ({
|
|
65
|
+
part,
|
|
66
|
+
present: await versionExists(existsCache, { shell, name: part, version, registry, force, cacheDir }),
|
|
67
|
+
})));
|
|
68
|
+
return checked.filter((entry) => !entry.present).map((entry) => entry.part);
|
|
69
|
+
}
|
|
70
|
+
async function probeOf(latestCache, options) {
|
|
71
|
+
const { shell, package: pkg, registry, force = false, cacheDir, prefix } = options;
|
|
72
|
+
if (pkg === "")
|
|
73
|
+
return undefined;
|
|
74
|
+
const ledger = await runShell(shell, `npm ls --prefix ${quoteShellArg(prefix ?? '')} ${quoteShellArg(pkg)} --json --depth=0${cacheFlag(cacheDir)}`, { timeoutMs: VERSION_CHECK_TIMEOUT_MS }).catch(() => undefined);
|
|
75
|
+
const installed = ledger === undefined ? undefined : parseLedgerVersion(ledger.stdout, pkg);
|
|
76
|
+
if (installed === undefined)
|
|
77
|
+
return undefined;
|
|
78
|
+
const source = registry === undefined || registry.trim() === ""
|
|
79
|
+
? undefined
|
|
80
|
+
: registry.trim();
|
|
81
|
+
const latest = source === undefined
|
|
82
|
+
? undefined
|
|
83
|
+
: await latestVersionOf(latestCache, { shell, name: pkg, registry: source, force, cacheDir });
|
|
84
|
+
return {
|
|
85
|
+
installed,
|
|
86
|
+
...(latest === undefined ? {} : { latest }),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { DataRequest, DataResult } from '../contract/data.js';
|
|
2
|
+
/**
|
|
3
|
+
* 一条 opencli 命令的执行器:同一形状的 `execFile('opencli', args)`,结果只留 stdout。
|
|
4
|
+
*/
|
|
5
|
+
export type LibRunner = (args: readonly string[]) => Promise<{
|
|
6
|
+
readonly stdout: string;
|
|
7
|
+
}>;
|
|
8
|
+
export type OpencliRunner = LibRunner;
|
|
9
|
+
/**
|
|
10
|
+
* 读缝(`DataReader.read`)的形状;模块直接把自己的 `data.read` 传进来即可。
|
|
11
|
+
*/
|
|
12
|
+
export type OpencliRead = (request: DataRequest) => Promise<DataResult<unknown>>;
|
|
13
|
+
export interface OpencliRunnerOptions {
|
|
14
|
+
readonly timeoutMs?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 把读缝包成一条 opencli 命令的执行器:给还按 `execFile('opencli', args)` 收结果的编排库用,
|
|
18
|
+
* 让它们跑在面板用的那份 opencli 上(同一套插件、同一份登录态)。
|
|
19
|
+
*
|
|
20
|
+
* 失败按 opencli 自己的约定还原:未登录是 `code: AUTH_REQUIRED` + 退出码 77,编排库的
|
|
21
|
+
* 「等登录再重试」才接得上;其它失败保留 `Command failed: opencli …` 那句外壳。
|
|
22
|
+
* @param read 模块手里的读缝(`data.read`)。
|
|
23
|
+
* @param options `timeoutMs` 透传给读缝;不传就用读缝自己的默认值。
|
|
24
|
+
* @returns 执行器;直接交给编排库的 `setOpencliRunner`。
|
|
25
|
+
*/
|
|
26
|
+
export declare function opencliRunnerOf(read: OpencliRead, options?: OpencliRunnerOptions): OpencliRunner;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const AUTH_REQUIRED_EXIT_CODE = 77;
|
|
2
|
+
function textOf(value) {
|
|
3
|
+
return typeof value === 'string' ? value : '';
|
|
4
|
+
}
|
|
5
|
+
function failedOf(args, error) {
|
|
6
|
+
const head = `Command failed: opencli ${args.join(' ')}`;
|
|
7
|
+
const auth = error?.pageCode === 'auth-required';
|
|
8
|
+
const helpUrl = textOf(error?.helpUrl);
|
|
9
|
+
const detail = textOf(error?.detail);
|
|
10
|
+
const hint = textOf(error?.hint);
|
|
11
|
+
const failed = new Error([
|
|
12
|
+
head,
|
|
13
|
+
...(auth
|
|
14
|
+
? ['code: AUTH_REQUIRED', `message: ${textOf(error?.message)}`]
|
|
15
|
+
: [textOf(error?.message) || 'opencli 这条命令没跑通']),
|
|
16
|
+
...(detail === '' ? [] : [detail]),
|
|
17
|
+
...(hint === '' ? [] : [hint]),
|
|
18
|
+
...(auth && helpUrl !== ''
|
|
19
|
+
? [`help: Please open Chrome or Chromium and log in to ${helpUrl}`]
|
|
20
|
+
: []),
|
|
21
|
+
].join('\n'));
|
|
22
|
+
if (auth)
|
|
23
|
+
failed.code = AUTH_REQUIRED_EXIT_CODE;
|
|
24
|
+
return failed;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 把读缝包成一条 opencli 命令的执行器:给还按 `execFile('opencli', args)` 收结果的编排库用,
|
|
28
|
+
* 让它们跑在面板用的那份 opencli 上(同一套插件、同一份登录态)。
|
|
29
|
+
*
|
|
30
|
+
* 失败按 opencli 自己的约定还原:未登录是 `code: AUTH_REQUIRED` + 退出码 77,编排库的
|
|
31
|
+
* 「等登录再重试」才接得上;其它失败保留 `Command failed: opencli …` 那句外壳。
|
|
32
|
+
* @param read 模块手里的读缝(`data.read`)。
|
|
33
|
+
* @param options `timeoutMs` 透传给读缝;不传就用读缝自己的默认值。
|
|
34
|
+
* @returns 执行器;直接交给编排库的 `setOpencliRunner`。
|
|
35
|
+
*/
|
|
36
|
+
export function opencliRunnerOf(read, options = {}) {
|
|
37
|
+
return async (args) => {
|
|
38
|
+
const result = await read({
|
|
39
|
+
kind: 'command',
|
|
40
|
+
adapter: 'opencli',
|
|
41
|
+
argv: [...args],
|
|
42
|
+
...(options.timeoutMs === undefined
|
|
43
|
+
? {}
|
|
44
|
+
: { timeoutMs: options.timeoutMs }),
|
|
45
|
+
});
|
|
46
|
+
if (result.ok)
|
|
47
|
+
return { stdout: JSON.stringify(result.data) };
|
|
48
|
+
throw failedOf(args, result.error);
|
|
49
|
+
};
|
|
50
|
+
}
|