@dsh-workbench/adapter-supply 0.0.0-stage → 1.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +26 -2
- package/dist/bundle-manager.d.ts +11 -0
- package/dist/bundle-manager.js +181 -0
- package/dist/dsh-plugins.d.ts +5 -0
- package/dist/dsh-plugins.js +51 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/installed-module.d.ts +49 -0
- package/dist/installed-module.js +210 -0
- package/dist/package-content.d.ts +6 -0
- package/dist/package-content.js +64 -0
- package/dist/package-kind.d.ts +21 -0
- package/dist/package-kind.js +73 -0
- package/dist/package-route.d.ts +23 -0
- package/dist/package-route.js +74 -0
- package/dist/plugin-catalog.d.ts +65 -0
- package/dist/plugin-catalog.js +192 -0
- package/dist/plugin-login-hint.d.ts +2 -0
- package/dist/plugin-login-hint.js +33 -0
- package/dist/plugin-source.d.ts +2 -0
- package/dist/plugin-source.js +1 -0
- package/dist/plugin-tools.d.ts +15 -0
- package/dist/plugin-tools.js +134 -0
- package/dist/plugin-units.d.ts +48 -0
- package/dist/plugin-units.js +245 -0
- package/dist/registry-declaration.d.ts +23 -0
- package/dist/registry-declaration.js +56 -0
- package/dist/unit-registry-types.d.ts +12 -0
- package/dist/unit-registry-types.js +1 -0
- package/package.json +37 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-workbench contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @dsh-workbench/adapter-supply
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
工作台的供应适配器。
|
|
4
|
+
|
|
5
|
+
## 职责
|
|
6
|
+
|
|
7
|
+
- 已安装模块发现与声明校验
|
|
8
|
+
- bundle / package / plugin 路由与安装计划
|
|
9
|
+
- registry 声明、插件目录与登录提示
|
|
10
|
+
- 插件来源解析(plugin-source)
|
|
11
|
+
|
|
12
|
+
## 入口
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { profileDirOf, profileModules, readInstalledModule } from '@dsh-workbench/adapter-supply';
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 边界
|
|
19
|
+
|
|
20
|
+
只依赖 `@dsh-workbench/shared` 的宿主契约与纯工具;不读取 core 内部文件,不拥有 profile 的 self-update。
|
|
21
|
+
|
|
22
|
+
## 验证
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pnpm --filter @dsh-workbench/adapter-supply run build
|
|
26
|
+
pnpm --filter @dsh-workbench/adapter-supply run test
|
|
27
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DshHostContext } from '@dsh-workbench/shared';
|
|
2
|
+
import type { BundleCancellation, BundleChange, BundleInspection, BundleInstallRequest, BundleRegistries, BundleRegistry } from '@dsh-workbench/shared/contract/protocol/bundle-install';
|
|
3
|
+
export interface BundleManager {
|
|
4
|
+
registries(): Promise<BundleRegistries | null>;
|
|
5
|
+
inspect(spec: string, registry?: BundleRegistry): Promise<BundleInspection | null>;
|
|
6
|
+
install(request: BundleInstallRequest): Promise<BundleChange | null>;
|
|
7
|
+
remove(name: string): Promise<BundleChange | null>;
|
|
8
|
+
setEnabled?(name: string, enabled: boolean): Promise<BundleChange | null>;
|
|
9
|
+
cancel(requestId: string): Promise<BundleCancellation | null>;
|
|
10
|
+
}
|
|
11
|
+
export declare function readBundleManager(ctx: DshHostContext): BundleManager | undefined;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { RESTART_REASONS } from '@dsh-workbench/shared/contract/protocol/bundle-install';
|
|
2
|
+
const MANAGER_SERVICE = 'pluginManager';
|
|
3
|
+
const APPLICATIONS = ['applied', 'restart-required', 'overridden', 'failed', 'cancelled'];
|
|
4
|
+
const STAGES = ['install', 'enable', 'remove'];
|
|
5
|
+
const PROBLEMS = [
|
|
6
|
+
'invalid-spec',
|
|
7
|
+
'already-installed',
|
|
8
|
+
'not-found',
|
|
9
|
+
'not-a-package',
|
|
10
|
+
'not-a-bundle',
|
|
11
|
+
'not-a-workbench-module',
|
|
12
|
+
'network',
|
|
13
|
+
'unknown',
|
|
14
|
+
];
|
|
15
|
+
const SPEC_KINDS = ['registry', 'path', 'git', 'tarball'];
|
|
16
|
+
const FAILURE_KINDS = [
|
|
17
|
+
'pnpm-missing',
|
|
18
|
+
'timeout',
|
|
19
|
+
'not-found',
|
|
20
|
+
'no-matching-version',
|
|
21
|
+
'network',
|
|
22
|
+
'disk-full',
|
|
23
|
+
'permission',
|
|
24
|
+
'build-blocked',
|
|
25
|
+
'integrity',
|
|
26
|
+
'unknown',
|
|
27
|
+
];
|
|
28
|
+
const CANCELLATIONS = ['cancelled', 'too-late', 'not-running'];
|
|
29
|
+
function asRecord(value) {
|
|
30
|
+
return typeof value === 'object' && value !== null ? value : undefined;
|
|
31
|
+
}
|
|
32
|
+
function asText(value) {
|
|
33
|
+
return typeof value === 'string' ? value : '';
|
|
34
|
+
}
|
|
35
|
+
function pick(allowed, value) {
|
|
36
|
+
return typeof value === 'string' && allowed.includes(value) ? value : undefined;
|
|
37
|
+
}
|
|
38
|
+
function asRegistry(value) {
|
|
39
|
+
if (value === null)
|
|
40
|
+
return null;
|
|
41
|
+
return typeof value === 'string' ? value : undefined;
|
|
42
|
+
}
|
|
43
|
+
function asTextArray(value) {
|
|
44
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
|
|
45
|
+
}
|
|
46
|
+
function asRegistries(raw) {
|
|
47
|
+
const record = asRecord(raw);
|
|
48
|
+
if (record === undefined)
|
|
49
|
+
return null;
|
|
50
|
+
const registry = asRegistry(record.registry);
|
|
51
|
+
if (registry === undefined)
|
|
52
|
+
return null;
|
|
53
|
+
return {
|
|
54
|
+
registry,
|
|
55
|
+
fallbackRegistries: asTextArray(record.fallbackRegistries),
|
|
56
|
+
resolved: typeof record.resolved === 'string' ? record.resolved : null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function asPackageResult(raw) {
|
|
60
|
+
const record = asRecord(raw);
|
|
61
|
+
if (record === undefined)
|
|
62
|
+
return undefined;
|
|
63
|
+
const kind = pick(FAILURE_KINDS, record.kind);
|
|
64
|
+
return {
|
|
65
|
+
exitCode: typeof record.exitCode === 'number' ? record.exitCode : -1,
|
|
66
|
+
output: asText(record.output),
|
|
67
|
+
truncated: record.truncated === true,
|
|
68
|
+
logPath: asText(record.logPath),
|
|
69
|
+
...(kind === undefined ? {} : { kind }),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function asChange(raw) {
|
|
73
|
+
const record = asRecord(raw);
|
|
74
|
+
if (record === undefined)
|
|
75
|
+
return null;
|
|
76
|
+
const application = pick(APPLICATIONS, record.application);
|
|
77
|
+
const stage = pick(STAGES, record.stage);
|
|
78
|
+
if (application === undefined || stage === undefined)
|
|
79
|
+
return null;
|
|
80
|
+
const error = asRecord(record.error);
|
|
81
|
+
const packageResult = asPackageResult(record.packageResult);
|
|
82
|
+
const pendingBuilds = asTextArray(record.pendingBuilds);
|
|
83
|
+
const restartReason = pick(RESTART_REASONS, record.restartReason);
|
|
84
|
+
const failedAt = pick(['registry', 'spec-host'], record.failedAt);
|
|
85
|
+
const bundle = asText(record.bundle);
|
|
86
|
+
const version = asText(record.version);
|
|
87
|
+
const errorCode = error === undefined ? '' : asText(error.code);
|
|
88
|
+
const errorDetail = error === undefined ? '' : asText(error.diagnostic);
|
|
89
|
+
return {
|
|
90
|
+
application,
|
|
91
|
+
stage,
|
|
92
|
+
changed: record.changed === true,
|
|
93
|
+
target: asText(record.target),
|
|
94
|
+
...(bundle === '' ? {} : { bundle }),
|
|
95
|
+
...(version === '' ? {} : { version }),
|
|
96
|
+
...(errorCode === '' ? {} : { errorCode }),
|
|
97
|
+
...(errorDetail === '' ? {} : { errorDetail }),
|
|
98
|
+
...(packageResult === undefined ? {} : { packageResult }),
|
|
99
|
+
...(pendingBuilds.length === 0 ? {} : { pendingBuilds }),
|
|
100
|
+
...(restartReason === undefined ? {} : { restartReason }),
|
|
101
|
+
...(failedAt === undefined ? {} : { failedAt }),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function asInspection(raw) {
|
|
105
|
+
const record = asRecord(raw);
|
|
106
|
+
if (record === undefined)
|
|
107
|
+
return null;
|
|
108
|
+
const status = asText(record.status);
|
|
109
|
+
if (status === 'refused') {
|
|
110
|
+
const problem = pick(PROBLEMS, record.problem) ?? 'unknown';
|
|
111
|
+
const registries = Array.isArray(record.registries)
|
|
112
|
+
? record.registries.map(asRegistry).filter((item) => item !== undefined)
|
|
113
|
+
: [];
|
|
114
|
+
return {
|
|
115
|
+
status: 'refused',
|
|
116
|
+
problem,
|
|
117
|
+
reason: asText(record.reason),
|
|
118
|
+
...(registries.length === 0 ? {} : { registries }),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (status !== 'accepted')
|
|
122
|
+
return null;
|
|
123
|
+
const kind = pick(SPEC_KINDS, record.kind);
|
|
124
|
+
const registry = asRegistry(record.registry);
|
|
125
|
+
if (kind === undefined || registry === undefined)
|
|
126
|
+
return null;
|
|
127
|
+
const name = asText(record.name);
|
|
128
|
+
const version = asText(record.version);
|
|
129
|
+
const description = asText(record.description);
|
|
130
|
+
const host = asText(record.host);
|
|
131
|
+
return {
|
|
132
|
+
status: 'accepted',
|
|
133
|
+
kind,
|
|
134
|
+
bundle: typeof record.bundle === 'boolean' ? record.bundle : null,
|
|
135
|
+
registry,
|
|
136
|
+
...(name === '' ? {} : { name }),
|
|
137
|
+
...(version === '' ? {} : { version }),
|
|
138
|
+
...(description === '' ? {} : { description }),
|
|
139
|
+
...(host === '' ? {} : { host }),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
export function readBundleManager(ctx) {
|
|
143
|
+
const face = ctx.get(MANAGER_SERVICE);
|
|
144
|
+
if (face === undefined)
|
|
145
|
+
return undefined;
|
|
146
|
+
if (typeof face.installBundle !== 'function' || typeof face.removeBundle !== 'function')
|
|
147
|
+
return undefined;
|
|
148
|
+
return {
|
|
149
|
+
async registries() {
|
|
150
|
+
if (typeof face.registries !== 'function')
|
|
151
|
+
return null;
|
|
152
|
+
return asRegistries(await face.registries());
|
|
153
|
+
},
|
|
154
|
+
async inspect(spec, registry) {
|
|
155
|
+
if (typeof face.inspect !== 'function')
|
|
156
|
+
return null;
|
|
157
|
+
return asInspection(await face.inspect(spec, registry === undefined ? undefined : { registry }));
|
|
158
|
+
},
|
|
159
|
+
async install(request) {
|
|
160
|
+
const options = {
|
|
161
|
+
...(request.registry === undefined ? {} : { registry: request.registry }),
|
|
162
|
+
...(request.requestId === undefined ? {} : { requestId: request.requestId }),
|
|
163
|
+
...(request.approvedBuilds === undefined ? {} : { approvedBuilds: request.approvedBuilds }),
|
|
164
|
+
};
|
|
165
|
+
return asChange(await face.installBundle?.(request.spec, options));
|
|
166
|
+
},
|
|
167
|
+
async remove(name) {
|
|
168
|
+
return asChange(await face.removeBundle?.(name));
|
|
169
|
+
},
|
|
170
|
+
async setEnabled(name, enabled) {
|
|
171
|
+
if (typeof face.setBundleEnabled !== 'function')
|
|
172
|
+
return null;
|
|
173
|
+
return asChange(await face.setBundleEnabled(name, enabled));
|
|
174
|
+
},
|
|
175
|
+
async cancel(requestId) {
|
|
176
|
+
if (typeof face.cancelInstall !== 'function')
|
|
177
|
+
return null;
|
|
178
|
+
return pick(CANCELLATIONS, (asRecord(await face.cancelInstall(requestId)) ?? {}).status) ?? null;
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { DshHostContext } from '@dsh-workbench/shared';
|
|
2
|
+
import type { RouteHandler } from '@dsh-workbench/shared/host';
|
|
3
|
+
import type { DshPluginsSnapshot } from '@dsh-workbench/shared/contract/protocol/dsh-plugins';
|
|
4
|
+
export declare function readDshPlugins(ctx: DshHostContext): Promise<DshPluginsSnapshot>;
|
|
5
|
+
export declare function dshPluginsHandler(ctx: DshHostContext): RouteHandler;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const MANAGER_SERVICE = 'pluginManager';
|
|
2
|
+
function asRecord(value) {
|
|
3
|
+
return typeof value === 'object' && value !== null ? value : undefined;
|
|
4
|
+
}
|
|
5
|
+
function asArray(value) {
|
|
6
|
+
if (Array.isArray(value))
|
|
7
|
+
return value;
|
|
8
|
+
const record = asRecord(value);
|
|
9
|
+
if (record === undefined)
|
|
10
|
+
return [];
|
|
11
|
+
for (const key of ['rows', 'items', 'list', 'bundles', 'plugins']) {
|
|
12
|
+
const inner = record[key];
|
|
13
|
+
if (Array.isArray(inner))
|
|
14
|
+
return inner;
|
|
15
|
+
}
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
function asText(value) {
|
|
19
|
+
return typeof value === 'string' ? value : '';
|
|
20
|
+
}
|
|
21
|
+
function rowOf(value) {
|
|
22
|
+
const record = asRecord(value);
|
|
23
|
+
if (record === undefined)
|
|
24
|
+
return undefined;
|
|
25
|
+
const name = asText(record.name) || asText(record.id);
|
|
26
|
+
if (name === '')
|
|
27
|
+
return undefined;
|
|
28
|
+
return { name, version: asText(record.version), enabled: record.disabled !== true && record.enabled !== false };
|
|
29
|
+
}
|
|
30
|
+
function rowsOf(value) {
|
|
31
|
+
const rows = [];
|
|
32
|
+
for (const item of asArray(value)) {
|
|
33
|
+
const row = rowOf(item);
|
|
34
|
+
if (row !== undefined)
|
|
35
|
+
rows.push(row);
|
|
36
|
+
}
|
|
37
|
+
return rows;
|
|
38
|
+
}
|
|
39
|
+
export async function readDshPlugins(ctx) {
|
|
40
|
+
const manager = ctx.get(MANAGER_SERVICE);
|
|
41
|
+
if (manager === undefined)
|
|
42
|
+
return { available: false, bundles: [], plugins: [] };
|
|
43
|
+
const [bundles, plugins] = await Promise.all([
|
|
44
|
+
manager.listBundles === undefined ? undefined : manager.listBundles(),
|
|
45
|
+
manager.listPlugins === undefined ? undefined : manager.listPlugins(),
|
|
46
|
+
]);
|
|
47
|
+
return { available: true, bundles: rowsOf(bundles), plugins: rowsOf(plugins) };
|
|
48
|
+
}
|
|
49
|
+
export function dshPluginsHandler(ctx) {
|
|
50
|
+
return () => readDshPlugins(ctx);
|
|
51
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './registry-declaration.js';
|
|
2
|
+
export * from './installed-module.js';
|
|
3
|
+
export * from './package-route.js';
|
|
4
|
+
export * from './plugin-login-hint.js';
|
|
5
|
+
export * from './package-kind.js';
|
|
6
|
+
export * from './package-content.js';
|
|
7
|
+
export * from './plugin-units.js';
|
|
8
|
+
export * from './plugin-tools.js';
|
|
9
|
+
export * from './dsh-plugins.js';
|
|
10
|
+
export * from './plugin-catalog.js';
|
|
11
|
+
export * from './bundle-manager.js';
|
|
12
|
+
export * from './plugin-source.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './registry-declaration.js';
|
|
2
|
+
export * from './installed-module.js';
|
|
3
|
+
export * from './package-route.js';
|
|
4
|
+
export * from './plugin-login-hint.js';
|
|
5
|
+
export * from './package-kind.js';
|
|
6
|
+
export * from './package-content.js';
|
|
7
|
+
export * from './plugin-units.js';
|
|
8
|
+
export * from './plugin-tools.js';
|
|
9
|
+
export * from './dsh-plugins.js';
|
|
10
|
+
export * from './plugin-catalog.js';
|
|
11
|
+
export * from './bundle-manager.js';
|
|
12
|
+
export * from './plugin-source.js';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { DshHostContext } from '@dsh-workbench/shared';
|
|
2
|
+
export interface UnitDeclarationSource {
|
|
3
|
+
readonly pkg: string;
|
|
4
|
+
readonly declaration: unknown;
|
|
5
|
+
}
|
|
6
|
+
export type InstalledModule = {
|
|
7
|
+
readonly kind: 'module';
|
|
8
|
+
readonly pkg: string;
|
|
9
|
+
readonly unit: string;
|
|
10
|
+
readonly version: string;
|
|
11
|
+
readonly registry?: string;
|
|
12
|
+
readonly local?: boolean;
|
|
13
|
+
} | {
|
|
14
|
+
readonly kind: 'not-module';
|
|
15
|
+
readonly pkg: string;
|
|
16
|
+
} | {
|
|
17
|
+
readonly kind: 'unknown';
|
|
18
|
+
readonly pkg: string;
|
|
19
|
+
};
|
|
20
|
+
export declare function isPackageName(value: string): boolean;
|
|
21
|
+
export declare function profileDirOf(ctx: DshHostContext): string | null;
|
|
22
|
+
export interface ProfileModule {
|
|
23
|
+
readonly pkg: string;
|
|
24
|
+
readonly unit: string;
|
|
25
|
+
readonly version: string;
|
|
26
|
+
readonly registry?: string;
|
|
27
|
+
readonly local?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare function profileManifestOf(profileDir: string | null): Readonly<Record<string, unknown>> | undefined;
|
|
30
|
+
export declare function profileModules(profileDir: string | null): readonly ProfileModule[];
|
|
31
|
+
export declare function createDeclarationLookup(profileDir: string | null): (unit: string) => UnitDeclarationSource | undefined;
|
|
32
|
+
export declare function moduleDeclarationOf(profileDir: string | null, pkg: string): unknown;
|
|
33
|
+
export interface DeclarationGap {
|
|
34
|
+
readonly pkg: string;
|
|
35
|
+
readonly problems: readonly string[];
|
|
36
|
+
}
|
|
37
|
+
export declare function declarationGaps(profileDir: string | null): readonly DeclarationGap[] | undefined;
|
|
38
|
+
export declare function declaredPluginCommands(profileDir: string | null): ReadonlyMap<string, ReadonlySet<string>>;
|
|
39
|
+
export declare function readInstalledModule(profileDir: string | null, pkg: string): InstalledModule;
|
|
40
|
+
export declare function isPackedArtifactSpec(spec: string): boolean;
|
|
41
|
+
export declare function isLocalDependencySpec(spec: string): boolean;
|
|
42
|
+
export interface ProfilePackage {
|
|
43
|
+
readonly pkg: string;
|
|
44
|
+
readonly version: string;
|
|
45
|
+
readonly registry?: string;
|
|
46
|
+
readonly local?: boolean;
|
|
47
|
+
}
|
|
48
|
+
export declare function readProfilePackage(profileDir: string | null, pkg: string): ProfilePackage | undefined;
|
|
49
|
+
export declare function packageVersionAt(packageRoot: string): string | undefined;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { declarationProblemsOf, readDeclaration } from '@dsh-workbench/shared/host';
|
|
4
|
+
const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
|
|
5
|
+
function asRecord(value) {
|
|
6
|
+
return typeof value === 'object' && value !== null ? value : undefined;
|
|
7
|
+
}
|
|
8
|
+
function asText(value) {
|
|
9
|
+
return typeof value === 'string' ? value : '';
|
|
10
|
+
}
|
|
11
|
+
export function isPackageName(value) {
|
|
12
|
+
return value.length <= 214 && PACKAGE_NAME.test(value);
|
|
13
|
+
}
|
|
14
|
+
export function profileDirOf(ctx) {
|
|
15
|
+
const profile = ctx.get('profileContext');
|
|
16
|
+
const dir = typeof profile?.dir === 'string' ? profile.dir.trim() : '';
|
|
17
|
+
return dir === '' ? null : dir;
|
|
18
|
+
}
|
|
19
|
+
function stringsOf(value) {
|
|
20
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
|
|
21
|
+
}
|
|
22
|
+
export function profileManifestOf(profileDir) {
|
|
23
|
+
if (profileDir === null)
|
|
24
|
+
return undefined;
|
|
25
|
+
try {
|
|
26
|
+
return asRecord(JSON.parse(fs.readFileSync(path.join(profileDir, 'package.json'), 'utf8')));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function profileModules(profileDir) {
|
|
33
|
+
const record = profileManifestOf(profileDir);
|
|
34
|
+
if (record === undefined)
|
|
35
|
+
return [];
|
|
36
|
+
const bundles = stringsOf(asRecord(asRecord(record.dsh)?.profile)?.bundles);
|
|
37
|
+
const dependencies = asRecord(record.dependencies) ?? {};
|
|
38
|
+
const names = bundles.length > 0 ? bundles : Object.keys(dependencies);
|
|
39
|
+
const found = [];
|
|
40
|
+
for (const pkg of names) {
|
|
41
|
+
const read = readInstalledModule(profileDir, pkg);
|
|
42
|
+
if (read.kind === 'module')
|
|
43
|
+
found.push({
|
|
44
|
+
pkg: read.pkg,
|
|
45
|
+
unit: read.unit,
|
|
46
|
+
version: read.version,
|
|
47
|
+
...(read.registry === undefined ? {} : { registry: read.registry }),
|
|
48
|
+
local: isLocalDependencySpec(asText(dependencies[read.pkg])),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return found;
|
|
52
|
+
}
|
|
53
|
+
export function createDeclarationLookup(profileDir) {
|
|
54
|
+
let roster;
|
|
55
|
+
const scan = () => new Map(profileModules(profileDir).map((row) => [row.unit, row.pkg]));
|
|
56
|
+
return (unit) => {
|
|
57
|
+
roster ??= scan();
|
|
58
|
+
let pkg = roster.get(unit);
|
|
59
|
+
if (pkg === undefined) {
|
|
60
|
+
roster = scan();
|
|
61
|
+
pkg = roster.get(unit);
|
|
62
|
+
}
|
|
63
|
+
return pkg === undefined ? undefined : { pkg, declaration: moduleDeclarationOf(profileDir, pkg) };
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function registryFromLockfile(profileDir, pkg) {
|
|
67
|
+
let text;
|
|
68
|
+
try {
|
|
69
|
+
text = fs.readFileSync(path.join(profileDir, 'pnpm-lock.yaml'), 'utf8');
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
const at = text.indexOf(` ${pkg}@`);
|
|
75
|
+
const quoted = at === -1 ? text.indexOf(` '${pkg}@`) : at;
|
|
76
|
+
if (quoted === -1)
|
|
77
|
+
return undefined;
|
|
78
|
+
const headerEnd = text.indexOf('\n', quoted);
|
|
79
|
+
const end = headerEnd === -1 ? -1 : text.indexOf('\n ', headerEnd + 1);
|
|
80
|
+
const block = text.slice(quoted, end === -1 ? quoted + 900 : end);
|
|
81
|
+
const match = /tarball:\s*([^\s,}]+)/.exec(block);
|
|
82
|
+
const tarball = match?.[1];
|
|
83
|
+
if (tarball === undefined || !tarball.startsWith('http'))
|
|
84
|
+
return undefined;
|
|
85
|
+
const cut = tarball.lastIndexOf(`/${pkg}/`);
|
|
86
|
+
if (cut <= 'https://'.length)
|
|
87
|
+
return undefined;
|
|
88
|
+
return tarball.slice(0, cut);
|
|
89
|
+
}
|
|
90
|
+
export function moduleDeclarationOf(profileDir, pkg) {
|
|
91
|
+
if (profileDir === null || !isPackageName(pkg))
|
|
92
|
+
return undefined;
|
|
93
|
+
try {
|
|
94
|
+
const text = fs.readFileSync(path.join(profileDir, 'node_modules', pkg, 'package.json'), 'utf8');
|
|
95
|
+
return asRecord(asRecord(asRecord(JSON.parse(text))?.dsh)?.workbench);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export function declarationGaps(profileDir) {
|
|
102
|
+
if (profileManifestOf(profileDir) === undefined)
|
|
103
|
+
return undefined;
|
|
104
|
+
const gaps = [];
|
|
105
|
+
for (const module of profileModules(profileDir)) {
|
|
106
|
+
const problems = declarationProblemsOf(module.pkg, moduleDeclarationOf(profileDir, module.pkg));
|
|
107
|
+
if (problems.length > 0)
|
|
108
|
+
gaps.push({ pkg: module.pkg, problems });
|
|
109
|
+
}
|
|
110
|
+
return gaps;
|
|
111
|
+
}
|
|
112
|
+
export function declaredPluginCommands(profileDir) {
|
|
113
|
+
const declared = new Map();
|
|
114
|
+
for (const module of profileModules(profileDir)) {
|
|
115
|
+
const read = readDeclaration(module.pkg, moduleDeclarationOf(profileDir, module.pkg));
|
|
116
|
+
for (const need of read.manifest?.needs ?? []) {
|
|
117
|
+
if (need.kind !== 'extension')
|
|
118
|
+
continue;
|
|
119
|
+
const name = need.name.trim();
|
|
120
|
+
if (name === '')
|
|
121
|
+
continue;
|
|
122
|
+
const commands = declared.get(name) ?? new Set();
|
|
123
|
+
for (const command of need.commands ?? [])
|
|
124
|
+
commands.add(command);
|
|
125
|
+
declared.set(name, commands);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return declared;
|
|
129
|
+
}
|
|
130
|
+
export function readInstalledModule(profileDir, pkg) {
|
|
131
|
+
if (profileDir === null || !isPackageName(pkg))
|
|
132
|
+
return { kind: 'unknown', pkg };
|
|
133
|
+
let manifest;
|
|
134
|
+
try {
|
|
135
|
+
manifest = JSON.parse(fs.readFileSync(path.join(profileDir, 'node_modules', pkg, 'package.json'), 'utf8'));
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return { kind: 'unknown', pkg };
|
|
139
|
+
}
|
|
140
|
+
const record = asRecord(manifest);
|
|
141
|
+
if (record === undefined)
|
|
142
|
+
return { kind: 'unknown', pkg };
|
|
143
|
+
const declared = asRecord(asRecord(record.dsh)?.workbench);
|
|
144
|
+
const unit = declared === undefined ? '' : asText(declared.unit);
|
|
145
|
+
if (unit === '')
|
|
146
|
+
return { kind: 'not-module', pkg };
|
|
147
|
+
const declaredRegistry = asText(asRecord(record.publishConfig)?.registry);
|
|
148
|
+
const registry = declaredRegistry === '' ? registryFromLockfile(profileDir, pkg) : declaredRegistry;
|
|
149
|
+
return {
|
|
150
|
+
kind: 'module',
|
|
151
|
+
pkg,
|
|
152
|
+
unit,
|
|
153
|
+
version: asText(record.version),
|
|
154
|
+
...(registry === '' ? {} : { registry }),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const PACKED_ARTIFACT = /\.(?:tgz|tar\.gz|tar)$/i;
|
|
158
|
+
export function isPackedArtifactSpec(spec) {
|
|
159
|
+
const trimmed = spec.trim();
|
|
160
|
+
const pkgPath = trimmed.startsWith('file:') ? trimmed.slice('file:'.length) : trimmed;
|
|
161
|
+
return PACKED_ARTIFACT.test(pkgPath);
|
|
162
|
+
}
|
|
163
|
+
export function isLocalDependencySpec(spec) {
|
|
164
|
+
const trimmed = spec.trim();
|
|
165
|
+
if (isPackedArtifactSpec(trimmed))
|
|
166
|
+
return false;
|
|
167
|
+
return (trimmed.startsWith('link:') ||
|
|
168
|
+
trimmed.startsWith('file:') ||
|
|
169
|
+
trimmed.startsWith('./') ||
|
|
170
|
+
trimmed.startsWith('../') ||
|
|
171
|
+
trimmed.startsWith('/'));
|
|
172
|
+
}
|
|
173
|
+
export function readProfilePackage(profileDir, pkg) {
|
|
174
|
+
if (profileDir === null || !isPackageName(pkg))
|
|
175
|
+
return undefined;
|
|
176
|
+
let manifest;
|
|
177
|
+
try {
|
|
178
|
+
manifest = JSON.parse(fs.readFileSync(path.join(profileDir, 'node_modules', pkg, 'package.json'), 'utf8'));
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
const record = asRecord(manifest);
|
|
184
|
+
if (record === undefined)
|
|
185
|
+
return undefined;
|
|
186
|
+
const version = asText(record.version);
|
|
187
|
+
if (version === '')
|
|
188
|
+
return undefined;
|
|
189
|
+
const declaredRegistry = asText(asRecord(record.publishConfig)?.registry);
|
|
190
|
+
const registry = declaredRegistry === '' ? registryFromLockfile(profileDir, pkg) : declaredRegistry;
|
|
191
|
+
const dependencies = asRecord(profileManifestOf(profileDir)?.dependencies) ?? {};
|
|
192
|
+
const local = isLocalDependencySpec(asText(dependencies[pkg]));
|
|
193
|
+
return {
|
|
194
|
+
pkg,
|
|
195
|
+
version,
|
|
196
|
+
...(registry === '' ? {} : { registry }),
|
|
197
|
+
...(local ? { local: true } : {}),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
export function packageVersionAt(packageRoot) {
|
|
201
|
+
let manifest;
|
|
202
|
+
try {
|
|
203
|
+
manifest = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
const version = asText(asRecord(manifest)?.version);
|
|
209
|
+
return version === '' ? undefined : version;
|
|
210
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { DshShell } from '@dsh-workbench/shared';
|
|
2
|
+
export declare function withPackageDir<T>(options: {
|
|
3
|
+
readonly shell?: DshShell | undefined;
|
|
4
|
+
readonly spec: string;
|
|
5
|
+
readonly registry?: string | undefined;
|
|
6
|
+
}, visit: (dir: string) => T): Promise<T | undefined>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { npmCacheDir } from '@dsh-workbench/adapter-runtime';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { INSTALL_TIMEOUT_MS, parseJsonLoose, quoteShellArg, runShell } from '@dsh-workbench/shared/host';
|
|
6
|
+
import { splitNpmSpec } from './package-kind.js';
|
|
7
|
+
import { localDirOf } from './registry-declaration.js';
|
|
8
|
+
function packedFileFrom(stdout, root) {
|
|
9
|
+
const parsed = parseJsonLoose(stdout);
|
|
10
|
+
if (Array.isArray(parsed)) {
|
|
11
|
+
const first = parsed[0];
|
|
12
|
+
const filename = typeof first === 'object' && first !== null ? first.filename : undefined;
|
|
13
|
+
if (typeof filename === 'string' && filename.trim() !== '') {
|
|
14
|
+
const at = path.join(root, path.basename(filename.trim()));
|
|
15
|
+
if (fs.existsSync(at))
|
|
16
|
+
return at;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const found = fs.readdirSync(root).filter((name) => name.endsWith('.tgz'));
|
|
21
|
+
return found.length === 1 ? path.join(root, found[0] ?? '') : undefined;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function withPackageDir(options, visit) {
|
|
28
|
+
const local = localDirOf(options.spec);
|
|
29
|
+
if (local !== undefined)
|
|
30
|
+
return visit(local);
|
|
31
|
+
if (splitNpmSpec(options.spec) === undefined)
|
|
32
|
+
return undefined;
|
|
33
|
+
const shell = options.shell;
|
|
34
|
+
if (shell === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
let root;
|
|
37
|
+
try {
|
|
38
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), 'wb-package-'));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const at = options.registry === undefined ? '' : ` --registry ${quoteShellArg(options.registry)}`;
|
|
45
|
+
const packed = await runShell(shell, `npm pack ${quoteShellArg(options.spec)} --ignore-scripts --json --pack-destination ${quoteShellArg(root)} --cache ${quoteShellArg(npmCacheDir())}${at}`, { timeoutMs: INSTALL_TIMEOUT_MS }).catch(() => undefined);
|
|
46
|
+
if (packed === undefined || packed.exitCode !== 0)
|
|
47
|
+
return undefined;
|
|
48
|
+
const file = packedFileFrom(packed.stdout, root);
|
|
49
|
+
if (file === undefined)
|
|
50
|
+
return undefined;
|
|
51
|
+
const unpacked = await runShell(shell, `tar -xzf ${quoteShellArg(file)} -C ${quoteShellArg(root)}`, {
|
|
52
|
+
timeoutMs: INSTALL_TIMEOUT_MS,
|
|
53
|
+
}).catch(() => undefined);
|
|
54
|
+
if (unpacked === undefined || unpacked.exitCode !== 0)
|
|
55
|
+
return undefined;
|
|
56
|
+
const dir = path.join(root, 'package');
|
|
57
|
+
if (!fs.existsSync(dir))
|
|
58
|
+
return undefined;
|
|
59
|
+
return visit(dir);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
}
|