@miphamai/cli 0.38.0 → 0.39.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.
Files changed (60) hide show
  1. package/bin/mipham.ts +25 -0
  2. package/package.json +1 -1
  3. package/src/agent/sub-agent.ts +4 -3
  4. package/src/config/defaults.ts +19 -1
  5. package/src/config/keys-manager.ts +11 -1
  6. package/src/config/loader.ts +20 -10
  7. package/src/core/context.ts +86 -56
  8. package/src/core/credential-masker/output-scrub.ts +5 -0
  9. package/src/core/crsi-sandbox.ts +19 -7
  10. package/src/core/engine.ts +50 -33
  11. package/src/core/instructions.ts +4 -42
  12. package/src/core/permission.ts +7 -1
  13. package/src/core/red-team.ts +4 -4
  14. package/src/core/self-critique.ts +3 -1
  15. package/src/core/session-log.ts +225 -0
  16. package/src/core/session-store.ts +119 -34
  17. package/src/daemon/auth.ts +9 -6
  18. package/src/daemon/cors.ts +24 -16
  19. package/src/daemon/rate-limiter.ts +4 -0
  20. package/src/daemon/server.ts +24 -3
  21. package/src/index.tsx +54 -28
  22. package/src/mcp/http-transport.ts +2 -0
  23. package/src/mcp/oauth.ts +1 -1
  24. package/src/providers/llm-replay.ts +36 -0
  25. package/src/providers/llm.ts +16 -0
  26. package/src/providers/registry.ts +2 -1
  27. package/src/security/gate.ts +10 -0
  28. package/src/security/url.ts +37 -14
  29. package/src/shared/constants.ts +4 -486
  30. package/src/shared/types.ts +6 -437
  31. package/src/skills/fork-executor.ts +3 -1
  32. package/src/skills/loader.ts +2 -1
  33. package/src/skills/seam.ts +18 -0
  34. package/src/tools/agent/agent.ts +1 -0
  35. package/src/tools/agent/skill.ts +1 -0
  36. package/src/tools/exec/bash.ts +140 -128
  37. package/src/tools/exec/git.ts +17 -0
  38. package/src/tools/file/read.ts +73 -66
  39. package/src/tools/file/write.ts +1 -1
  40. package/src/tools/index.ts +54 -132
  41. package/src/tools/network/web-fetch.ts +41 -26
  42. package/src/tools/seam.ts +25 -0
  43. package/src/tools/validation.ts +91 -0
  44. package/src/ui/commands.ts +17 -5
  45. package/src/vajra/compose/assemble.ts +19 -0
  46. package/src/vajra/compose/bundle.ts +39 -0
  47. package/src/vajra/compose/dump.ts +5 -0
  48. package/src/vajra/compose/index.ts +4 -0
  49. package/src/vajra/compose/mount.ts +30 -0
  50. package/src/vajra/context.ts +183 -0
  51. package/src/vajra/events.ts +13 -0
  52. package/src/vajra/index.ts +4 -0
  53. package/src/vajra/leaf/plan-runner.ts +94 -0
  54. package/src/vajra/service.ts +15 -0
  55. package/src/workflow/primitives/agent.ts +3 -1
  56. package/src/workflow/runtime.ts +23 -15
  57. package/src/workflow/sandbox.ts +51 -9
  58. package/src/core/compaction-progress.ts +0 -205
  59. package/src/core/context-compact.ts +0 -124
  60. package/src/core/context-drain.ts +0 -113
@@ -1,15 +1,13 @@
1
- import type { ToolDefinition, ToolResult } from '../shared/index.ts'
2
- import { sanitizeParams } from '../shared/sanitize'
3
- import { createT } from '../i18n-core/t'
4
- import enUS from '../i18n-core/locales/en-US.json'
5
- import zhCN from '../i18n-core/locales/zh-CN.json'
6
- import type { TranslationMap } from '../i18n-core/types'
7
- import { readTool } from './file/read'
1
+ import type { ToolDefinition } from '../shared/index.ts'
2
+ import { Context } from '../vajra'
3
+ import { withValidation } from './validation'
4
+ import { toolService, collectTools } from './seam'
5
+ import { readToolService } from './file/read'
8
6
  import { writeTool } from './file/write'
9
7
  import { editTool } from './file/edit'
10
8
  import { globTool } from './file/glob'
11
9
  import { grepTool } from './file/grep'
12
- import { bashTool } from './exec/bash'
10
+ import { bashToolService } from './exec/bash'
13
11
  import { gitTool } from './exec/git'
14
12
  import { taskTool } from './exec/task'
15
13
  import { enterWorktreeTool } from './exec/enter-worktree'
@@ -33,141 +31,65 @@ import { listAgentsTool } from './agent/list-agents'
33
31
  import { computerUseTool } from './computer/computer-use'
34
32
  import { scheduleWakeupTool } from './scheduling/schedule-wakeup.js'
35
33
  import { cronCreateTool, cronDeleteTool, cronListTool } from './scheduling/cron.js'
34
+ import { DISABLED_CREDENTIAL_MASKING_CONFIG } from '../config/defaults'
36
35
 
37
- const bundles: Record<string, TranslationMap> = {
38
- 'en-US': enUS as TranslationMap,
39
- 'zh-CN': zhCN as TranslationMap,
36
+ function defaultToolContext(): Context {
37
+ const ctx = new Context()
38
+ // 掩码中立默认:无参调用(daemon/workflow)保持 Read/Bash 挂载,但不启用掩码,
39
+ // 对齐 pre-seam 行为(那些路径从不调用 setter)。显式开启掩码走 index.tsx 的加载配置。
40
+ ctx.provide('credentials', DISABLED_CREDENTIAL_MASKING_CONFIG)
41
+ return ctx
40
42
  }
41
- const t = createT(bundles['en-US'] || (enUS as TranslationMap), enUS as TranslationMap)
42
43
 
43
- /**
44
- * Validate tool parameters against the tool's JSON Schema definition.
45
- * Returns an array of error messages (empty = valid).
46
- */
47
- function validateParams(
48
- schema: Record<string, unknown>,
49
- params: Record<string, unknown>,
50
- ): string[] {
51
- const errors: string[] = []
52
-
53
- // Check required fields
54
- const required = schema.required as string[] | undefined
55
- if (required) {
56
- for (const field of required) {
57
- if (params[field] === undefined || params[field] === null) {
58
- errors.push(t('errors.missing_param', { param: field }))
59
- }
60
- }
61
- }
62
-
63
- // Check types for provided fields
64
- const properties = schema.properties as
65
- Record<string, { type: string; enum?: string[] }> | undefined
66
- if (properties) {
67
- for (const [key, def] of Object.entries(properties)) {
68
- const value = params[key]
69
- if (value === undefined || value === null) continue // already caught by required check
70
-
71
- switch (def.type) {
72
- case 'string':
73
- if (typeof value !== 'string') errors.push(t('errors.type_string', { key }))
74
- else if (def.enum && !def.enum.includes(value)) {
75
- errors.push(t('errors.type_enum', { key, values: def.enum.join(', ') }))
76
- }
77
- break
78
- case 'integer':
79
- case 'number':
80
- if (typeof value !== 'number') errors.push(t('errors.type_number', { key }))
81
- break
82
- case 'boolean':
83
- if (typeof value !== 'boolean') errors.push(t('errors.type_boolean', { key }))
84
- break
85
- case 'object':
86
- if (typeof value !== 'object' || Array.isArray(value)) {
87
- errors.push(t('errors.type_object', { key }))
88
- }
89
- break
90
- case 'array':
91
- if (!Array.isArray(value)) errors.push(t('errors.type_array', { key }))
92
- break
93
- }
94
- }
95
- }
96
-
97
- return errors
98
- }
99
-
100
- /**
101
- * Wrap a tool's execute with parameter validation.
102
- */
103
- function withValidation(tool: ToolDefinition): ToolDefinition {
104
- const schema = tool.parameters as Record<string, unknown>
105
- if (!schema || !schema.properties) return tool // no schema to validate against
106
-
107
- return {
108
- ...tool,
109
- async execute(params, ctx): Promise<ToolResult> {
110
- // Sanitize dangerous Unicode from all inputs
111
- const cleanParams = sanitizeParams(params)
112
- const errors = validateParams(schema, cleanParams)
113
- if (errors.length > 0) {
114
- return {
115
- success: false,
116
- content: '',
117
- error: t('errors.invalid_params', { errors: errors.join('; ') }),
118
- }
119
- }
120
- return tool.execute(cleanParams, ctx)
121
- },
122
- }
123
- }
124
-
125
- export function createToolRegistry(): Map<string, ToolDefinition> {
126
- const tools: ToolDefinition[] = [
44
+ export function createToolRegistry(
45
+ ctx: Context = defaultToolContext(),
46
+ ): Map<string, ToolDefinition> {
47
+ // 普通工具:包 withValidation 后作为 Service 挂载
48
+ const plainTools: ToolDefinition[] = [
127
49
  // File tools
128
- withValidation(readTool),
129
- withValidation(writeTool),
130
- withValidation(editTool),
131
- withValidation(globTool),
132
- withValidation(grepTool),
50
+ writeTool,
51
+ editTool,
52
+ globTool,
53
+ grepTool,
133
54
  // Exec tools
134
- withValidation(bashTool),
135
- withValidation(gitTool),
136
- withValidation(taskTool),
137
- withValidation(enterWorktreeTool),
138
- withValidation(exitWorktreeTool),
55
+ gitTool,
56
+ taskTool,
57
+ enterWorktreeTool,
58
+ exitWorktreeTool,
139
59
  // Agent tools
140
- withValidation(agentTool),
141
- withValidation(skillTool),
142
- withValidation(planTool),
143
- withValidation(enterPlanModeTool),
144
- withValidation(exitPlanModeTool),
145
- withValidation(memoryTool),
146
- withValidation(workflowTool),
147
- withValidation(reportFindingsTool),
148
- withValidation(sendMessageTool),
149
- withValidation(listAgentsTool),
60
+ agentTool,
61
+ skillTool,
62
+ planTool,
63
+ enterPlanModeTool,
64
+ exitPlanModeTool,
65
+ memoryTool,
66
+ workflowTool,
67
+ reportFindingsTool,
68
+ sendMessageTool,
69
+ listAgentsTool,
150
70
  // Network tools
151
- withValidation(webFetchTool),
152
- withValidation(webSearchTool),
71
+ webFetchTool,
72
+ webSearchTool,
153
73
  // System tools
154
- withValidation(configTool),
155
- withValidation(mcpTool),
156
- withValidation(toolSearchTool),
74
+ configTool,
75
+ mcpTool,
76
+ toolSearchTool,
157
77
  // Artifact tools
158
- withValidation(artifactTool),
78
+ artifactTool,
159
79
  // Computer Use tools
160
- withValidation(computerUseTool),
80
+ computerUseTool,
161
81
  // Scheduling tools
162
- withValidation(scheduleWakeupTool),
163
- withValidation(cronCreateTool),
164
- withValidation(cronDeleteTool),
165
- withValidation(cronListTool),
82
+ scheduleWakeupTool,
83
+ cronCreateTool,
84
+ cronDeleteTool,
85
+ cronListTool,
166
86
  ]
167
-
168
- const map = new Map<string, ToolDefinition>()
169
- for (const tool of tools) {
170
- map.set(tool.name, tool)
87
+ for (const tool of plainTools) {
88
+ ctx.mount(toolService(withValidation(tool)))
171
89
  }
172
- return map
90
+ // 注入工具(credentials 依赖):read + bash
91
+ ctx.mount(readToolService)
92
+ ctx.mount(bashToolService)
93
+
94
+ return collectTools(ctx)
173
95
  }
@@ -177,7 +177,7 @@ export const webFetchTool: ToolDefinition = {
177
177
  }
178
178
 
179
179
  // SSRF protection: validate URL before fetching
180
- const validationError = validateUrl(url)
180
+ const validationError = await validateUrl(url)
181
181
  if (validationError) {
182
182
  return { success: false, content: '', error: validationError }
183
183
  }
@@ -186,40 +186,55 @@ export const webFetchTool: ToolDefinition = {
186
186
  const controller = new AbortController()
187
187
  const timer = setTimeout(() => controller.abort(), 30_000) // 30s timeout
188
188
 
189
- const response = await fetch(url, {
190
- headers: {
191
- 'User-Agent': 'Mipham-Code/0.24.0',
192
- Accept: 'text/html,application/xhtml+xml,*/*',
193
- },
194
- redirect: 'follow',
195
- signal: controller.signal,
196
- })
197
-
198
- clearTimeout(timer)
199
-
200
- // Cross-host redirect reporting
201
- if (response.url) {
202
- const finalHost = new URL(response.url).hostname
203
- const originalHost = new URL(url).hostname
204
- if (finalHost !== originalHost) {
205
- return {
206
- success: true,
207
- content: `Redirected to: ${response.url}\n\nFetch from this URL directly to retrieve content.`,
208
- metadata: { redirected: true, originalUrl: url, finalUrl: response.url },
209
- }
210
- }
211
-
212
- // SSRF defense: re-validate the resolved URL after redirects
213
- const redirectError = validateUrl(response.url)
189
+ // Manual redirect handling: follow same-host redirects one hop at a time,
190
+ // validating the target BEFORE each request (closes the SSRF-via-redirect gap).
191
+ let currentUrl = url
192
+ const originalHost = new URL(url).hostname
193
+ let response!: Response
194
+
195
+ for (let hop = 0; hop <= 5; hop++) {
196
+ response = await fetch(currentUrl, {
197
+ headers: {
198
+ 'User-Agent': 'Mipham-Code/0.24.0',
199
+ Accept: 'text/html,application/xhtml+xml,*/*',
200
+ },
201
+ redirect: 'manual',
202
+ signal: controller.signal,
203
+ })
204
+
205
+ // Not a redirect (or no Location) → proceed with this response
206
+ if (response.status < 300 || response.status >= 400) break
207
+ const location = response.headers.get('location')
208
+ if (!location || hop === 5) break
209
+
210
+ const nextUrl = new URL(location, currentUrl).toString()
211
+
212
+ // SSRF defense: validate the redirect target BEFORE following
213
+ const redirectError = await validateUrl(nextUrl)
214
214
  if (redirectError) {
215
+ clearTimeout(timer)
215
216
  return {
216
217
  success: false,
217
218
  content: '',
218
219
  error: `Redirect blocked: ${redirectError}`,
219
220
  }
220
221
  }
222
+
223
+ // Cross-host redirects are returned to the caller (tool contract)
224
+ if (new URL(nextUrl).hostname !== originalHost) {
225
+ clearTimeout(timer)
226
+ return {
227
+ success: true,
228
+ content: `Redirected to: ${nextUrl}\n\nFetch from this URL directly to retrieve content.`,
229
+ metadata: { redirected: true, originalUrl: url, finalUrl: nextUrl },
230
+ }
231
+ }
232
+
233
+ currentUrl = nextUrl
221
234
  }
222
235
 
236
+ clearTimeout(timer)
237
+
223
238
  // Determine content type; only convert HTML to markdown
224
239
  const contentType = response.headers.get('content-type') || ''
225
240
  const isHtml = contentType.includes('text/html') || contentType.includes('application/xhtml')
@@ -0,0 +1,25 @@
1
+ import type { Context, Service } from '../vajra'
2
+ import type { ToolDefinition } from '../shared'
3
+
4
+ /** 工具服务键前缀。一个已挂载工具以 `tool:<name>` 注册于 Context。 */
5
+ export const toolKey = (name: string): string => `tool:${name}`
6
+
7
+ /** 把普通 ToolDefinition 包装成 Vajra Service(无注入依赖)。 */
8
+ export function toolService(tool: ToolDefinition): Service {
9
+ return {
10
+ apply(ctx) {
11
+ ctx.provide(toolKey(tool.name), tool)
12
+ },
13
+ }
14
+ }
15
+
16
+ /** 从 Context 已挂载的工具服务派生 name → definition 的 Map(engine 消费)。 */
17
+ export function collectTools(ctx: Context): Map<string, ToolDefinition> {
18
+ const map = new Map<string, ToolDefinition>()
19
+ for (const key of ctx.keysRecursive()) {
20
+ if (!key.startsWith('tool:')) continue
21
+ const tool = ctx.get<ToolDefinition>(key)
22
+ if (tool) map.set(tool.name, tool)
23
+ }
24
+ return map
25
+ }
@@ -0,0 +1,91 @@
1
+ import { sanitizeParams } from '../shared/sanitize'
2
+ import { createT } from '../i18n-core/t'
3
+ import enUS from '../i18n-core/locales/en-US.json'
4
+ import zhCN from '../i18n-core/locales/zh-CN.json'
5
+ import type { TranslationMap } from '../i18n-core/types'
6
+ import type { ToolDefinition, ToolResult } from '../shared'
7
+
8
+ const bundles: Record<string, TranslationMap> = {
9
+ 'en-US': enUS as TranslationMap,
10
+ 'zh-CN': zhCN as TranslationMap,
11
+ }
12
+ const t = createT(bundles['en-US'] || (enUS as TranslationMap), enUS as TranslationMap)
13
+
14
+ /**
15
+ * Validate tool parameters against the tool's JSON Schema definition.
16
+ * Returns an array of error messages (empty = valid).
17
+ */
18
+ export function validateParams(
19
+ schema: Record<string, unknown>,
20
+ params: Record<string, unknown>,
21
+ ): string[] {
22
+ const errors: string[] = []
23
+
24
+ const required = schema.required as string[] | undefined
25
+ if (required) {
26
+ for (const field of required) {
27
+ if (params[field] === undefined || params[field] === null) {
28
+ errors.push(t('errors.missing_param', { param: field }))
29
+ }
30
+ }
31
+ }
32
+
33
+ const properties = schema.properties as
34
+ Record<string, { type: string; enum?: string[] }> | undefined
35
+ if (properties) {
36
+ for (const [key, def] of Object.entries(properties)) {
37
+ const value = params[key]
38
+ if (value === undefined || value === null) continue
39
+
40
+ switch (def.type) {
41
+ case 'string':
42
+ if (typeof value !== 'string') errors.push(t('errors.type_string', { key }))
43
+ else if (def.enum && !def.enum.includes(value)) {
44
+ errors.push(t('errors.type_enum', { key, values: def.enum.join(', ') }))
45
+ }
46
+ break
47
+ case 'integer':
48
+ case 'number':
49
+ if (typeof value !== 'number') errors.push(t('errors.type_number', { key }))
50
+ break
51
+ case 'boolean':
52
+ if (typeof value !== 'boolean') errors.push(t('errors.type_boolean', { key }))
53
+ break
54
+ case 'object':
55
+ if (typeof value !== 'object' || Array.isArray(value)) {
56
+ errors.push(t('errors.type_object', { key }))
57
+ }
58
+ break
59
+ case 'array':
60
+ if (!Array.isArray(value)) errors.push(t('errors.type_array', { key }))
61
+ break
62
+ }
63
+ }
64
+ }
65
+
66
+ return errors
67
+ }
68
+
69
+ /**
70
+ * Wrap a tool's execute with parameter validation.
71
+ */
72
+ export function withValidation(tool: ToolDefinition): ToolDefinition {
73
+ const schema = tool.parameters as Record<string, unknown>
74
+ if (!schema || !schema.properties) return tool
75
+
76
+ return {
77
+ ...tool,
78
+ async execute(params, ctx): Promise<ToolResult> {
79
+ const cleanParams = sanitizeParams(params)
80
+ const errors = validateParams(schema, cleanParams)
81
+ if (errors.length > 0) {
82
+ return {
83
+ success: false,
84
+ content: '',
85
+ error: t('errors.invalid_params', { errors: errors.join('; ') }),
86
+ }
87
+ }
88
+ return tool.execute(cleanParams, ctx)
89
+ },
90
+ }
91
+ }
@@ -2840,11 +2840,20 @@ const cdCmd: CommandHandler = async (ctx, args) => {
2840
2840
  const { SessionStore } = await import('../core/session-store')
2841
2841
  const saved = SessionStore.load(ctx.sessionId)
2842
2842
  if (saved) {
2843
- SessionStore.save(ctx.sessionId, saved.messages, {
2844
- provider: saved.metadata.provider,
2845
- model: saved.metadata.model,
2846
- cwd: resolved,
2847
- })
2843
+ const log = ctx.engine.getContext().getLog()
2844
+ if (log) {
2845
+ SessionStore.saveLog(ctx.sessionId, log, {
2846
+ provider: saved.metadata.provider,
2847
+ model: saved.metadata.model,
2848
+ cwd: resolved,
2849
+ })
2850
+ } else {
2851
+ SessionStore.save(ctx.sessionId, saved.messages, {
2852
+ provider: saved.metadata.provider,
2853
+ model: saved.metadata.model,
2854
+ cwd: resolved,
2855
+ })
2856
+ }
2848
2857
  }
2849
2858
  } catch {
2850
2859
  /* session persistence is best-effort */
@@ -4049,6 +4058,9 @@ const forkCmd: CommandHandler = async (ctx, args) => {
4049
4058
  ctx.engine.getRegistry(),
4050
4059
  ctx.engine.getTools(),
4051
4060
  ctx.engine.getPermission(),
4061
+ undefined,
4062
+ undefined,
4063
+ ctx.engine.getLlm(),
4052
4064
  )
4053
4065
  const parentContext = ctx.engine.getContext()
4054
4066
  const result = await sa.execute(prompt, 'fork: ' + prompt.slice(0, 60), {
@@ -0,0 +1,19 @@
1
+ import type { Profile, Bundle, BundleLine } from './bundle'
2
+
3
+ export function assemble(profile: Profile, resolveBundle: (name: string) => Bundle): BundleLine[] {
4
+ // 浅拷贝每行,使 patch 不回写共享的 bundle 行对象(不同 profile 共享同一 bundle 时补丁隔离)
5
+ const lines = profile.bundles.flatMap((name) =>
6
+ resolveBundle(name).lines.map((l) => ({ ...l, config: { ...l.config } })),
7
+ )
8
+ if (profile.patch) {
9
+ for (const [id, partial] of Object.entries(profile.patch)) {
10
+ const target = lines.find((l) => l.id === id)
11
+ if (target) {
12
+ Object.assign(target, partial)
13
+ } else {
14
+ lines.push({ id, kind: partial.kind ?? 'tool', config: partial.config ?? {} })
15
+ }
16
+ }
17
+ }
18
+ return lines
19
+ }
@@ -0,0 +1,39 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { parse as parseYaml } from 'yaml'
3
+
4
+ export type BundleLine = {
5
+ id: string
6
+ kind: 'tool' | 'provider' | 'skill' | 'service'
7
+ config: Record<string, unknown>
8
+ }
9
+ export type Bundle = { name: string; lines: BundleLine[] }
10
+ export type Profile = {
11
+ name: string
12
+ bundles: string[]
13
+ patch?: Record<string, Partial<BundleLine>>
14
+ }
15
+
16
+ export function loadBundle(path: string): Bundle {
17
+ const raw = readFileSync(path, 'utf-8')
18
+ const data = parseYaml(raw) as { name?: unknown; lines?: unknown }
19
+ if (data.lines !== undefined && !Array.isArray(data.lines)) {
20
+ throw new Error(`bundle "${path}": "lines" must be an array`)
21
+ }
22
+ return {
23
+ name: typeof data.name === 'string' ? data.name : path,
24
+ lines: (data.lines ?? []) as BundleLine[],
25
+ }
26
+ }
27
+
28
+ export function loadProfile(path: string): Profile {
29
+ const raw = readFileSync(path, 'utf-8')
30
+ const data = parseYaml(raw) as { name?: unknown; bundles?: unknown; patch?: unknown }
31
+ if (data.bundles !== undefined && !Array.isArray(data.bundles)) {
32
+ throw new Error(`profile "${path}": "bundles" must be an array`)
33
+ }
34
+ return {
35
+ name: typeof data.name === 'string' ? data.name : path,
36
+ bundles: (data.bundles ?? []) as string[],
37
+ patch: data.patch as Profile['patch'] | undefined,
38
+ }
39
+ }
@@ -0,0 +1,5 @@
1
+ import type { BundleLine } from './bundle'
2
+
3
+ export function dumpConfig(lines: BundleLine[]): string {
4
+ return lines.map((l) => `${l.id}\t${l.kind}\t${JSON.stringify(l.config)}`).join('\n')
5
+ }
@@ -0,0 +1,4 @@
1
+ export * from './bundle'
2
+ export * from './assemble'
3
+ export * from './dump'
4
+ export * from './mount'
@@ -0,0 +1,30 @@
1
+ import type { Context, Mounted, Service } from '..'
2
+ import type { Bundle, BundleLine, Profile } from './bundle'
3
+ import { assemble } from './assemble'
4
+
5
+ /** 服务解析器:bundle 行的「要挂的代码」——按行解析出要挂载的 Service;纯数据行(如 package-info)返回 undefined 跳过。 */
6
+ export type ServiceResolver = (line: BundleLine) => Service | undefined
7
+
8
+ /** 按行顺序把解析出的 Service 挂进 ctx(依赖未就位则挂起,语义同 ctx.mount)。 */
9
+ export function mountLines(
10
+ ctx: Context,
11
+ lines: BundleLine[],
12
+ resolveService: ServiceResolver,
13
+ ): Mounted[] {
14
+ const mounted: Mounted[] = []
15
+ for (const line of lines) {
16
+ const service = resolveService(line)
17
+ if (service) mounted.push(ctx.mount(service))
18
+ }
19
+ return mounted
20
+ }
21
+
22
+ /** 从 profile 声明式挂载:assemble → mountLines 一整条链。 */
23
+ export function mountProfile(
24
+ ctx: Context,
25
+ profile: Profile,
26
+ resolveBundle: (name: string) => Bundle,
27
+ resolveService: ServiceResolver,
28
+ ): Mounted[] {
29
+ return mountLines(ctx, assemble(profile, resolveBundle), resolveService)
30
+ }