@tbox.cn/app-toolkit 0.1.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 +50 -0
- package/dist/chunk-VV3ERZXS.js +1 -0
- package/dist/contracts-resolver-QM4FBQQV.js +1 -0
- package/dist/index.d.ts +1909 -0
- package/dist/index.js +8 -0
- package/package.json +47 -0
- package/src/assembly/app-manifest.ts +132 -0
- package/src/assembly/provider-catalog.ts +167 -0
- package/src/assembly/walk-manifests.ts +461 -0
- package/src/core/contracts-expected.ts +31 -0
- package/src/core/contracts-resolver.ts +274 -0
- package/src/core/credential-format.ts +98 -0
- package/src/core/credential-mask.ts +19 -0
- package/src/core/env-expansion.ts +51 -0
- package/src/core/errors.ts +46 -0
- package/src/core/file-cache.ts +65 -0
- package/src/core/fskit.ts +21 -0
- package/src/core/module-schema.ts +139 -0
- package/src/dto.ts +298 -0
- package/src/factory.ts +297 -0
- package/src/index.ts +137 -0
- package/src/integrations/credentials.ts +153 -0
- package/src/integrations/mock-bindings.ts +55 -0
- package/src/integrations/predicate-io.ts +199 -0
- package/src/integrations/predicate.ts +598 -0
- package/src/integrations/read.ts +53 -0
- package/src/integrations/write.ts +448 -0
- package/src/views/app.ts +28 -0
- package/src/views/context.ts +70 -0
- package/src/views/modules.ts +48 -0
- package/src/views/providers.ts +165 -0
- package/src/views/service-detail.ts +32 -0
- package/src/views/service-resolutions.ts +323 -0
- package/tests/contracts-resolver.test.ts +203 -0
- package/tests/demo-app.ts +146 -0
- package/tests/dev-manifest-catalog.test.ts +114 -0
- package/tests/error-name-safety.test.ts +26 -0
- package/tests/file-cache.test.ts +242 -0
- package/tests/import-layers.test.ts +111 -0
- package/tests/module-schema.test.ts +84 -0
- package/tests/naming-alignment.test.ts +50 -0
- package/tests/views.test.ts +427 -0
- package/tests/write-core.test.ts +188 -0
- package/tsconfig.json +11 -0
- package/tsup.config.ts +29 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import type { WalkManifestsResult } from '../assembly/walk-manifests.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 谓词装配单源(FX-2b/F1):CLI doctor 规则 与 toolkit 工厂(求值轴 / SaveResult.issues)
|
|
7
|
+
* 共用同一 I/O 装配与供给/需求聚合——「面板 issues ≡ doctor issues」的机制保证。
|
|
8
|
+
*
|
|
9
|
+
* 迁移源:cli/src/doctor/schema-reader.ts(collectVendorSchemaProfiles,105 行)
|
|
10
|
+
* + doctor 规则壳内联段(#1 effective stem 收集含 root ByInstance/标量兜底 + env 通道 +
|
|
11
|
+
* #9 realmSwitchHints + malls 注册表 + moduleResourceNeeds)。
|
|
12
|
+
*
|
|
13
|
+
* **装配源 = walkManifests 双通道产物**(声明性改进:sdk 模式模块(npmModules 登记、
|
|
14
|
+
* node_modules 消费)从 doctor 原单通道(listLocalModules = packages/* 本地扫描)的静默
|
|
15
|
+
* 忽略,转纳入检查面——与 toolkit 求值轴同源)。
|
|
16
|
+
*
|
|
17
|
+
* env 参数注入(默认 process.env——工厂边缘):#1 的 env 通道兜底(wanda env 化形态:
|
|
18
|
+
* stem kebab → UPPER_SNAKE,env 键在场即视为已供给,不算缺文件)。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export interface PredicateIoInputs {
|
|
22
|
+
/** #1:effective 候选 stem → 缺文件(env 通道在场不算缺失) */
|
|
23
|
+
credentialFileMissing: Record<string, boolean>;
|
|
24
|
+
/** #2:凭据文件 stem → .type(无 type 则缺键) */
|
|
25
|
+
credentialFileType: Record<string, string | undefined>;
|
|
26
|
+
/** #6:credentialType → schema 引用文件缺失 */
|
|
27
|
+
credentialSchemaMissing: Record<string, boolean>;
|
|
28
|
+
/** #7:厂商 → configSchema properties 键集并集 */
|
|
29
|
+
vendorConfigKeys: Record<string, Set<string>>;
|
|
30
|
+
/** #7:厂商 → configSchema 引用缺失(跳过该厂商死配置判定) */
|
|
31
|
+
vendorConfigSchemaMissing: Record<string, boolean>;
|
|
32
|
+
/** #9:实现覆盖跨厂商提示文案(已成形——谓词直投) */
|
|
33
|
+
realmSwitchHints: string[];
|
|
34
|
+
/** #8:root instances 注册表 ids(在场才传;缺席 → 键检查跳过) */
|
|
35
|
+
malls?: string[];
|
|
36
|
+
/** #15-#16:已装模块 id → manifest contributes.resources 声明 id 集 */
|
|
37
|
+
moduleResourceNeeds: Record<string, ReadonlySet<string>>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 装配全产物(= PredicateIoInputs + 供给/需求聚合——喂 evaluateIntegrationServices 的
|
|
41
|
+
* 非 contracts/integrations 入参全集,两消费方 spread 直投,零各自组装) */
|
|
42
|
+
export interface PredicateAssembly extends PredicateIoInputs {
|
|
43
|
+
suppliedProviders: Record<string, { services: string[]; owner: string; credentialType?: string }>;
|
|
44
|
+
demandedServices: Record<string, { optional: boolean; owners: string[] }>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** stem → env 键(kebab → UPPER_SNAKE;与 SDK/provider-secret env 兜底同公式) */
|
|
48
|
+
export function refToEnvKey(stem: string): string {
|
|
49
|
+
return stem.replaceAll('-', '_').toUpperCase();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readJsonOrNull(file: string): Record<string, unknown> | null {
|
|
53
|
+
if (!existsSync(file)) return null;
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8')) as unknown;
|
|
56
|
+
return parsed && typeof parsed === 'object' ? (parsed as Record<string, unknown>) : null;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 提取 JSON schema properties 键集($ref 单级;properties 缺失 → 空集) */
|
|
63
|
+
function collectPropertyKeys(schema: Record<string, unknown>): Set<string> {
|
|
64
|
+
const keys = new Set<string>();
|
|
65
|
+
const props = schema.properties;
|
|
66
|
+
if (props && typeof props === 'object' && !Array.isArray(props)) {
|
|
67
|
+
for (const k of Object.keys(props)) keys.add(k);
|
|
68
|
+
}
|
|
69
|
+
return keys;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 装配谓词输入全集(供给/需求聚合 + I/O 档案)。
|
|
74
|
+
* @param walk 调用方已执行的 walkManifests 产物(目录枚举零缓存——每方法调用独立)
|
|
75
|
+
*/
|
|
76
|
+
export function buildPredicateAssembly(
|
|
77
|
+
appDir: string,
|
|
78
|
+
walk: WalkManifestsResult,
|
|
79
|
+
integrations: Record<string, unknown>,
|
|
80
|
+
env: Readonly<Record<string, string | undefined>> = process.env,
|
|
81
|
+
): PredicateAssembly {
|
|
82
|
+
// ── 供给/需求聚合(walk catalog 双通道——工厂与 CLI 同源)──
|
|
83
|
+
const suppliedProviders: PredicateAssembly['suppliedProviders'] = {};
|
|
84
|
+
for (const [vendor, impls] of Object.entries(walk.catalog.providers)) {
|
|
85
|
+
for (const [impl, entry] of Object.entries(impls)) {
|
|
86
|
+
suppliedProviders[`${vendor}\u0000${impl}`] = {
|
|
87
|
+
services: [...entry.services],
|
|
88
|
+
owner: entry.owner,
|
|
89
|
+
credentialType: entry.credentialType,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const demandedServices: PredicateAssembly['demandedServices'] = {};
|
|
94
|
+
for (const [service, entry] of Object.entries(walk.catalog.services)) {
|
|
95
|
+
demandedServices[service] = { optional: entry.optional, owners: [...entry.owners] };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── #6/#7 schema 档案 + #15/#16 需求面(manifest 引用 → 存在性 + 键集;双通道)──
|
|
99
|
+
const vendorConfigKeys: Record<string, Set<string>> = {};
|
|
100
|
+
const vendorConfigSchemaMissing: Record<string, boolean> = {};
|
|
101
|
+
const credentialSchemaMissing: Record<string, boolean> = {};
|
|
102
|
+
const moduleResourceNeeds: Record<string, ReadonlySet<string>> = {};
|
|
103
|
+
for (const mod of walk.modules) {
|
|
104
|
+
moduleResourceNeeds[mod.id] = new Set((mod.descriptor.contributes.resources ?? []).map((r) => r.id));
|
|
105
|
+
for (const slot of mod.descriptor.contributes.providers?.slots ?? []) {
|
|
106
|
+
if (slot.configSchema) {
|
|
107
|
+
const schema = readJsonOrNull(join(mod.dir, slot.configSchema));
|
|
108
|
+
if (schema) {
|
|
109
|
+
const bucket = (vendorConfigKeys[slot.provider] ??= new Set<string>());
|
|
110
|
+
for (const k of collectPropertyKeys(schema)) bucket.add(k);
|
|
111
|
+
} else {
|
|
112
|
+
vendorConfigSchemaMissing[slot.provider] = true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (slot.credentialSchema && !existsSync(join(mod.dir, slot.credentialSchema))) {
|
|
116
|
+
credentialSchemaMissing[slot.credentialType] = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── #1/#2 凭据文件索引(config/credentials/ 目录扫描)──
|
|
122
|
+
const credentialFileType: Record<string, string | undefined> = {};
|
|
123
|
+
const credStems = new Set<string>();
|
|
124
|
+
const credDir = join(appDir, 'config', 'credentials');
|
|
125
|
+
if (existsSync(credDir)) {
|
|
126
|
+
for (const file of readdirSync(credDir)) {
|
|
127
|
+
if (!file.endsWith('.json')) continue;
|
|
128
|
+
const stem = file.slice(0, -'.json'.length);
|
|
129
|
+
credStems.add(stem);
|
|
130
|
+
const cred = readJsonOrNull(join(credDir, file));
|
|
131
|
+
if (cred && typeof cred.type === 'string') credentialFileType[stem] = cred.type;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const ig = integrations as {
|
|
136
|
+
services?: Record<string, { provider?: unknown; credentialRef?: unknown; instances?: Record<string, { provider?: unknown; credentialRef?: unknown }> }>;
|
|
137
|
+
credentialRefByInstance?: Record<string, unknown>;
|
|
138
|
+
credentialRef?: unknown;
|
|
139
|
+
instances?: unknown;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// ── #1 effective 候选 stem 收集(槽级 + 实例级 + root ByInstance + root 标量;env 通道兜底)──
|
|
143
|
+
const refs: string[] = [];
|
|
144
|
+
const pushRef = (v: unknown): void => {
|
|
145
|
+
if (typeof v === 'string' && v.startsWith('secret://')) refs.push(v);
|
|
146
|
+
};
|
|
147
|
+
for (const svc of Object.values(ig.services ?? {})) {
|
|
148
|
+
if (svc && typeof svc === 'object') {
|
|
149
|
+
pushRef(svc.credentialRef);
|
|
150
|
+
for (const inst of Object.values(svc.instances ?? {})) {
|
|
151
|
+
if (inst && typeof inst === 'object') pushRef(inst.credentialRef);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
for (const v of Object.values(ig.credentialRefByInstance ?? {})) pushRef(v);
|
|
156
|
+
pushRef(ig.credentialRef);
|
|
157
|
+
const credentialFileMissing: Record<string, boolean> = {};
|
|
158
|
+
for (const ref of refs) {
|
|
159
|
+
const stem = ref.slice('secret://'.length);
|
|
160
|
+
// env 通道:文件缺席但 env 键在场 → 不算缺失(wanda env 化)
|
|
161
|
+
if (env[refToEnvKey(stem)] !== undefined) continue;
|
|
162
|
+
if (!credStems.has(stem)) credentialFileMissing[stem] = true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── #9 realm switch 提示(实例级双字段厂商 ≠ 槽级厂商)──
|
|
166
|
+
const realmSwitchHints: string[] = [];
|
|
167
|
+
for (const [serviceId, svc] of Object.entries(ig.services ?? {})) {
|
|
168
|
+
const slotProvider = svc && typeof svc === 'object' && typeof svc.provider === 'string' ? svc.provider : undefined;
|
|
169
|
+
for (const [instanceId, inst] of Object.entries((svc && typeof svc === 'object' ? svc.instances : undefined) ?? {})) {
|
|
170
|
+
const instProvider = inst && typeof inst === 'object' && typeof (inst as { provider?: unknown }).provider === 'string'
|
|
171
|
+
? (inst as { provider: string }).provider
|
|
172
|
+
: undefined;
|
|
173
|
+
if (slotProvider && instProvider && instProvider !== slotProvider) {
|
|
174
|
+
realmSwitchHints.push(
|
|
175
|
+
`integrations: ${serviceId}@${instanceId} 实现覆盖跨厂商(${slotProvider} → ${instProvider})——凭据域切换,确认该 mall 对应凭据在场`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── #8 malls(root instances 注册表 ids;参考表在场才传——缺席 → 键检查跳过)──
|
|
182
|
+
const rawInstances = Array.isArray(ig.instances) ? ig.instances : undefined;
|
|
183
|
+
const mallIds = (rawInstances ?? [])
|
|
184
|
+
.map((i) => (i && typeof i === 'object' && typeof (i as { id?: unknown }).id === 'string' ? (i as { id: string }).id : undefined))
|
|
185
|
+
.filter((id): id is string => typeof id === 'string');
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
suppliedProviders,
|
|
189
|
+
demandedServices,
|
|
190
|
+
credentialFileMissing,
|
|
191
|
+
credentialFileType,
|
|
192
|
+
credentialSchemaMissing,
|
|
193
|
+
vendorConfigKeys,
|
|
194
|
+
vendorConfigSchemaMissing,
|
|
195
|
+
realmSwitchHints,
|
|
196
|
+
moduleResourceNeeds,
|
|
197
|
+
...(mallIds.length > 0 ? { malls: mallIds } : {}),
|
|
198
|
+
};
|
|
199
|
+
}
|