@doubao-dev/plugin-cli 0.0.0-stage → 0.0.40-canary-dea0ddc73-20261026073950

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.

Potentially problematic release.


This version of @doubao-dev/plugin-cli might be problematic. Click here for more details.

@@ -0,0 +1,165 @@
1
+ import type { Command } from 'commander';
2
+ import { z } from 'zod';
3
+
4
+ export declare type AddCommandHook = (ctx: {
5
+ program: Command;
6
+ hooks: PluginHooks;
7
+ }) => Awaitable<void>;
8
+
9
+ export declare type AfterUploadHook = (ctx: UploadContext, result: Readonly<UploadResult>) => Awaitable<void>;
10
+
11
+ declare interface AppProject {
12
+ root: string;
13
+ }
14
+
15
+ export declare type Awaitable<T> = T | Promise<T>;
16
+
17
+ export declare interface CLIOptions {
18
+ readonly plugins?: readonly CLIPlugin[];
19
+ }
20
+
21
+ export declare interface CLIPlugin {
22
+ readonly name: string;
23
+ readonly setup: (api: PluginAPI) => Awaitable<void>;
24
+ }
25
+
26
+ export declare function definePlugin(plugin: CLIPlugin): CLIPlugin;
27
+
28
+ declare interface DevAction {
29
+ label: string;
30
+ run(): Awaitable<void>;
31
+ }
32
+
33
+ declare interface DevContext {
34
+ project: PluginProject;
35
+ options: Readonly<Record<string, unknown>>;
36
+ signal: AbortSignal;
37
+ }
38
+
39
+ export declare type DevHook = (ctx: DevContext) => Awaitable<readonly DevService[]>;
40
+
41
+ declare interface DevService {
42
+ name: string;
43
+ title: string;
44
+ option?: string;
45
+ watch?: readonly string[];
46
+ appliesTo(ctx: DevContext): Awaitable<boolean>;
47
+ start(ctx: DevServiceContext): Promise<void>;
48
+ createUI?(ctx: DevContext, onExit: (interrupted: boolean) => void): Promise<DevUI>;
49
+ }
50
+
51
+ declare interface DevServiceContext extends DevContext {
52
+ interactive: boolean;
53
+ log: Logger;
54
+ report(state: Partial<Omit<DevServiceState, 'name' | 'title'>>): void;
55
+ onDispose(dispose: () => Awaitable<void>): void;
56
+ onFileChange(listener: () => Awaitable<void>): void;
57
+ }
58
+
59
+ declare interface DevServiceState {
60
+ name: string;
61
+ title: string;
62
+ status: DevStatus;
63
+ message: string;
64
+ details?: Readonly<Record<string, string>>;
65
+ links?: Readonly<Record<string, string>>;
66
+ actions?: Readonly<Record<string, DevAction>>;
67
+ progress?: number;
68
+ busy?: boolean;
69
+ panel?: {
70
+ title: string;
71
+ message: string;
72
+ status: DevStatus;
73
+ qr?: string;
74
+ select?: boolean;
75
+ };
76
+ }
77
+
78
+ declare type DevStatus = 'pending' | 'success' | 'error';
79
+
80
+ declare interface DevUI {
81
+ log: Logger;
82
+ update(services: readonly DevServiceState[]): void;
83
+ close(): Promise<void>;
84
+ }
85
+
86
+ declare type Logger = Record<'info' | 'success' | 'warn' | 'error', (message: string) => void>;
87
+
88
+ declare type McpServer = Record<string, unknown>;
89
+
90
+ export declare type ModifyPackagePlanHook = (ctx: UploadContext, plan: Readonly<PackagePlan>) => Awaitable<PackagePlan>;
91
+
92
+ declare interface PackagePlan {
93
+ version?: string;
94
+ exclude?: readonly string[];
95
+ }
96
+
97
+ export declare interface PluginAPI {
98
+ onAddCommand(callback: AddCommandHook): void;
99
+ onDev(callback: DevHook): void;
100
+ onModifyPackagePlan(callback: ModifyPackagePlanHook): void;
101
+ onAfterUpload(callback: AfterUploadHook): void;
102
+ }
103
+
104
+ export declare interface PluginHooks {
105
+ addCommand: RegisteredHook<AddCommandHook>[];
106
+ dev: RegisteredHook<DevHook>[];
107
+ modifyPackagePlan: RegisteredHook<ModifyPackagePlanHook>[];
108
+ afterUpload: RegisteredHook<AfterUploadHook>[];
109
+ }
110
+
111
+ declare type PluginManifest = z.infer<typeof pluginManifestSchema>;
112
+
113
+ declare const pluginManifestSchema: z.ZodObject<{
114
+ $schema: z.ZodOptional<z.ZodString>;
115
+ name: z.ZodString;
116
+ id: z.ZodOptional<z.ZodString>;
117
+ displayName: z.ZodOptional<z.ZodString>;
118
+ description: z.ZodOptional<z.ZodString>;
119
+ extensions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
120
+ }, z.core.$loose>;
121
+
122
+ declare class PluginProject {
123
+ readonly root: string;
124
+ readonly plugin: PluginManifest;
125
+ readonly app?: AppProject;
126
+ private readonly pluginManifestPath;
127
+ constructor(cwd?: string);
128
+ updatePluginId(id: string): Promise<void>;
129
+ pack({ version, exclude }?: PackagePlan): Promise<Blob>;
130
+ readSkills(): Skill[];
131
+ get mcpServers(): McpServer[];
132
+ }
133
+
134
+ export declare interface RegisteredHook<T> {
135
+ pluginName: string;
136
+ callback: T;
137
+ }
138
+
139
+ export declare function runCLI(argv?: string[], options?: CLIOptions): Promise<void>;
140
+
141
+ declare interface Skill {
142
+ root: string;
143
+ content: string;
144
+ }
145
+
146
+ declare interface UploadContext {
147
+ project: PluginProject;
148
+ options: Readonly<UploadOptions>;
149
+ }
150
+
151
+ declare interface UploadOptions {
152
+ version?: string;
153
+ }
154
+
155
+ declare interface UploadResult {
156
+ packageId: string;
157
+ draft: {
158
+ name?: string;
159
+ ability_id?: string;
160
+ version?: string;
161
+ };
162
+ version?: string;
163
+ }
164
+
165
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ function definePlugin(plugin) {
2
+ return plugin;
3
+ }
4
+ export { main as runCLI } from "./188.js";
5
+ export { definePlugin };
package/dist/main.d.ts ADDED
@@ -0,0 +1,163 @@
1
+ import type { Command } from 'commander';
2
+ import { z } from 'zod';
3
+
4
+ declare type AddCommandHook = (ctx: {
5
+ program: Command;
6
+ hooks: PluginHooks;
7
+ }) => Awaitable<void>;
8
+
9
+ declare type AfterUploadHook = (ctx: UploadContext, result: Readonly<UploadResult>) => Awaitable<void>;
10
+
11
+ declare interface AppProject {
12
+ root: string;
13
+ }
14
+
15
+ declare type Awaitable<T> = T | Promise<T>;
16
+
17
+ declare interface CLIOptions {
18
+ readonly plugins?: readonly CLIPlugin[];
19
+ }
20
+
21
+ declare interface CLIPlugin {
22
+ readonly name: string;
23
+ readonly setup: (api: PluginAPI) => Awaitable<void>;
24
+ }
25
+
26
+ declare interface DevAction {
27
+ label: string;
28
+ run(): Awaitable<void>;
29
+ }
30
+
31
+ declare interface DevContext {
32
+ project: PluginProject;
33
+ options: Readonly<Record<string, unknown>>;
34
+ signal: AbortSignal;
35
+ }
36
+
37
+ declare type DevHook = (ctx: DevContext) => Awaitable<readonly DevService[]>;
38
+
39
+ declare interface DevService {
40
+ name: string;
41
+ title: string;
42
+ option?: string;
43
+ watch?: readonly string[];
44
+ appliesTo(ctx: DevContext): Awaitable<boolean>;
45
+ start(ctx: DevServiceContext): Promise<void>;
46
+ createUI?(ctx: DevContext, onExit: (interrupted: boolean) => void): Promise<DevUI>;
47
+ }
48
+
49
+ declare interface DevServiceContext extends DevContext {
50
+ interactive: boolean;
51
+ log: Logger;
52
+ report(state: Partial<Omit<DevServiceState, 'name' | 'title'>>): void;
53
+ onDispose(dispose: () => Awaitable<void>): void;
54
+ onFileChange(listener: () => Awaitable<void>): void;
55
+ }
56
+
57
+ declare interface DevServiceState {
58
+ name: string;
59
+ title: string;
60
+ status: DevStatus;
61
+ message: string;
62
+ details?: Readonly<Record<string, string>>;
63
+ links?: Readonly<Record<string, string>>;
64
+ actions?: Readonly<Record<string, DevAction>>;
65
+ progress?: number;
66
+ busy?: boolean;
67
+ panel?: {
68
+ title: string;
69
+ message: string;
70
+ status: DevStatus;
71
+ qr?: string;
72
+ select?: boolean;
73
+ };
74
+ }
75
+
76
+ declare type DevStatus = 'pending' | 'success' | 'error';
77
+
78
+ declare interface DevUI {
79
+ log: Logger;
80
+ update(services: readonly DevServiceState[]): void;
81
+ close(): Promise<void>;
82
+ }
83
+
84
+ declare type Logger = Record<'info' | 'success' | 'warn' | 'error', (message: string) => void>;
85
+
86
+ export declare function main(argv?: string[], options?: CLIOptions): Promise<void>;
87
+
88
+ declare type McpServer = Record<string, unknown>;
89
+
90
+ declare type ModifyPackagePlanHook = (ctx: UploadContext, plan: Readonly<PackagePlan>) => Awaitable<PackagePlan>;
91
+
92
+ declare interface PackagePlan {
93
+ version?: string;
94
+ exclude?: readonly string[];
95
+ }
96
+
97
+ declare interface PluginAPI {
98
+ onAddCommand(callback: AddCommandHook): void;
99
+ onDev(callback: DevHook): void;
100
+ onModifyPackagePlan(callback: ModifyPackagePlanHook): void;
101
+ onAfterUpload(callback: AfterUploadHook): void;
102
+ }
103
+
104
+ declare interface PluginHooks {
105
+ addCommand: RegisteredHook<AddCommandHook>[];
106
+ dev: RegisteredHook<DevHook>[];
107
+ modifyPackagePlan: RegisteredHook<ModifyPackagePlanHook>[];
108
+ afterUpload: RegisteredHook<AfterUploadHook>[];
109
+ }
110
+
111
+ declare type PluginManifest = z.infer<typeof pluginManifestSchema>;
112
+
113
+ declare const pluginManifestSchema: z.ZodObject<{
114
+ $schema: z.ZodOptional<z.ZodString>;
115
+ name: z.ZodString;
116
+ id: z.ZodOptional<z.ZodString>;
117
+ displayName: z.ZodOptional<z.ZodString>;
118
+ description: z.ZodOptional<z.ZodString>;
119
+ extensions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
120
+ }, z.core.$loose>;
121
+
122
+ declare class PluginProject {
123
+ readonly root: string;
124
+ readonly plugin: PluginManifest;
125
+ readonly app?: AppProject;
126
+ private readonly pluginManifestPath;
127
+ constructor(cwd?: string);
128
+ updatePluginId(id: string): Promise<void>;
129
+ pack({ version, exclude }?: PackagePlan): Promise<Blob>;
130
+ readSkills(): Skill[];
131
+ get mcpServers(): McpServer[];
132
+ }
133
+
134
+ declare interface RegisteredHook<T> {
135
+ pluginName: string;
136
+ callback: T;
137
+ }
138
+
139
+ declare interface Skill {
140
+ root: string;
141
+ content: string;
142
+ }
143
+
144
+ declare interface UploadContext {
145
+ project: PluginProject;
146
+ options: Readonly<UploadOptions>;
147
+ }
148
+
149
+ declare interface UploadOptions {
150
+ version?: string;
151
+ }
152
+
153
+ declare interface UploadResult {
154
+ packageId: string;
155
+ draft: {
156
+ name?: string;
157
+ ability_id?: string;
158
+ version?: string;
159
+ };
160
+ version?: string;
161
+ }
162
+
163
+ export { }
package/dist/main.js ADDED
@@ -0,0 +1 @@
1
+ export { main } from "./188.js";
package/dist/plugin.js ADDED
@@ -0,0 +1,97 @@
1
+ import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { arrayBuffer } from "node:stream/consumers";
4
+ import archiver from "archiver";
5
+ import { readJson, readJsonSync, writeJson } from "fs-extra/esm";
6
+ import { APP_GENERATED_DIRECTORIES, pluginManifestSchema, DOUBAO_APP_DIRECTORY, SKILL_FILENAME, findApp, mcpConfigSchema, PROJECT_EXCLUDED_FILES, findProjectRoot, PLUGIN_MANIFEST_FILENAME, MCP_CONFIG_FILENAME, PROJECT_EXCLUDED_DIRECTORIES } from "./188.js";
7
+ class PluginProject {
8
+ root;
9
+ plugin;
10
+ app;
11
+ pluginManifestPath;
12
+ constructor(cwd = process.cwd()){
13
+ this.root = findProjectRoot(cwd);
14
+ this.pluginManifestPath = join(this.root, PLUGIN_MANIFEST_FILENAME);
15
+ this.plugin = pluginManifestSchema.parse(readJsonSync(this.pluginManifestPath));
16
+ this.app = findApp(this.root);
17
+ }
18
+ async updatePluginId(id) {
19
+ const manifest = pluginManifestSchema.parse(await readJson(this.pluginManifestPath));
20
+ manifest.id = id;
21
+ await writeJson(this.pluginManifestPath, manifest, {
22
+ spaces: 2
23
+ });
24
+ this.plugin.id = id;
25
+ }
26
+ async pack({ version, exclude = [] } = {}) {
27
+ const archive = archiver('zip', {
28
+ zlib: {
29
+ level: 9
30
+ }
31
+ });
32
+ const excludedDirectories = [
33
+ ...exclude,
34
+ ...this.app ? APP_GENERATED_DIRECTORIES.map((directory)=>`${DOUBAO_APP_DIRECTORY}/${directory}`) : []
35
+ ];
36
+ archive.on('warning', (error)=>archive.destroy(error));
37
+ archive.directory(this.root, false, (entry)=>{
38
+ const parts = entry.name.split('/');
39
+ if (entry.stats?.isSymbolicLink() || '.agents' === parts[0] || 'AGENTS.md' === entry.name || version && entry.name === PLUGIN_MANIFEST_FILENAME || excludedDirectories.some((directory)=>entry.name === directory || entry.name.startsWith(`${directory}/`)) || parts.some((part)=>PROJECT_EXCLUDED_DIRECTORIES.includes(part) || PROJECT_EXCLUDED_FILES.includes(part) || ".env" === part || part.startsWith(".env."))) return false;
40
+ const directory = entry.stats?.isDirectory() ? entry.name : dirname(entry.name);
41
+ if (findProjectRoot(join(this.root, directory)) !== this.root) return false;
42
+ return entry;
43
+ });
44
+ if (version) archive.append(JSON.stringify({
45
+ ...this.plugin,
46
+ version
47
+ }, null, 2), {
48
+ name: PLUGIN_MANIFEST_FILENAME
49
+ });
50
+ try {
51
+ const [content] = await Promise.all([
52
+ arrayBuffer(archive),
53
+ archive.finalize()
54
+ ]);
55
+ return new Blob([
56
+ content
57
+ ], {
58
+ type: 'application/zip'
59
+ });
60
+ } catch (error) {
61
+ archive.abort();
62
+ throw error;
63
+ }
64
+ }
65
+ readSkills() {
66
+ const directory = join(this.root, "skills");
67
+ const directoryStat = lstatSync(directory, {
68
+ throwIfNoEntry: false
69
+ });
70
+ if (!directoryStat) return [];
71
+ if (!directoryStat.isDirectory()) throw new Error(`${directory} 必须是目录`);
72
+ return readdirSync(directory, {
73
+ withFileTypes: true
74
+ }).filter((entry)=>entry.isDirectory()).map((entry)=>entry.name).sort().flatMap((name)=>{
75
+ const skillRoot = join(directory, name);
76
+ const filePath = join(skillRoot, SKILL_FILENAME);
77
+ const fileStat = lstatSync(filePath, {
78
+ throwIfNoEntry: false
79
+ });
80
+ if (!fileStat) return [];
81
+ if (!fileStat.isFile()) throw new Error(`${filePath} 必须是普通文件`);
82
+ return [
83
+ {
84
+ root: skillRoot,
85
+ content: readFileSync(filePath, 'utf8')
86
+ }
87
+ ];
88
+ });
89
+ }
90
+ get mcpServers() {
91
+ const filePath = join(this.root, MCP_CONFIG_FILENAME);
92
+ if (!existsSync(filePath)) return [];
93
+ const { mcpServers } = mcpConfigSchema.parse(readJsonSync(filePath));
94
+ return Object.values(mcpServers);
95
+ }
96
+ }
97
+ export { PluginProject };
@@ -0,0 +1,248 @@
1
+ import { relative, resolve, sep } from "node:path";
2
+ import { cancel, isCancel, select as prompts_select } from "@clack/prompts";
3
+ import { watch } from "chokidar";
4
+ import { terminal_log, callHook, isInteractive, errorMessage } from "./188.js";
5
+ import { ensureLogin } from "./command~1.js";
6
+ function createLogUI() {
7
+ const previous = new Map();
8
+ return {
9
+ log: terminal_log,
10
+ update: (services)=>{
11
+ for (const service of services){
12
+ const last = previous.get(service.name);
13
+ previous.set(service.name, service);
14
+ if (last?.message !== service.message && 'error' !== service.status && !(void 0 !== service.progress && 'pending' === service.status)) ('success' === service.status ? terminal_log.success : terminal_log.info)(`${service.title}:${service.message}`);
15
+ for (const [label, url] of Object.entries(service.links ?? {}))if (last?.links?.[label] !== url) terminal_log.info(`${label}:${url}`);
16
+ }
17
+ },
18
+ close: async ()=>{}
19
+ };
20
+ }
21
+ async function runSession(project, options, hooks) {
22
+ const controller = new AbortController();
23
+ const completion = Promise.withResolvers();
24
+ const stop = (interrupted = false)=>{
25
+ if (interrupted) process.exitCode = 130;
26
+ controller.abort();
27
+ completion.resolve();
28
+ };
29
+ const ctx = {
30
+ project,
31
+ options,
32
+ signal: controller.signal
33
+ };
34
+ const selected = await selectServices(ctx, hooks);
35
+ if (!selected) return;
36
+ await ensureLogin(false !== options.tui && isInteractive());
37
+ const services = selected.map((item)=>({
38
+ ...item,
39
+ state: {
40
+ name: item.service.name,
41
+ title: item.service.title,
42
+ status: 'pending',
43
+ message: '正在启动'
44
+ },
45
+ phase: 'starting',
46
+ paths: (item.service.watch ?? []).map((path)=>relative(project.root, resolve(project.root, path))),
47
+ dirty: false,
48
+ disposers: []
49
+ }));
50
+ const renderers = services.filter(({ service })=>service.createUI);
51
+ if (renderers.length > 1) throw new Error('一次只能选择一个交互调试目标');
52
+ let ui = createLogUI();
53
+ let rendering = Promise.resolve();
54
+ let interactive = false;
55
+ const write = (level, message)=>{
56
+ try {
57
+ ui.log[level](message);
58
+ } catch (error) {
59
+ completion.reject(error);
60
+ }
61
+ };
62
+ const log = {
63
+ info: (message)=>write('info', message),
64
+ success: (message)=>write('success', message),
65
+ warn: (message)=>write('warn', message),
66
+ error: (message)=>write('error', message)
67
+ };
68
+ const snapshot = ()=>services.map(({ state })=>state);
69
+ const onSigint = ()=>stop(true);
70
+ const onSigterm = ()=>{
71
+ process.exitCode = 143;
72
+ stop();
73
+ };
74
+ process.on('SIGINT', onSigint);
75
+ process.on('SIGTERM', onSigterm);
76
+ let timer;
77
+ const dispose = async (runtime)=>{
78
+ for (const callback of runtime.disposers.splice(0).reverse())try {
79
+ await callback();
80
+ } catch (error) {
81
+ log.warn(`[${runtime.pluginName}] 清理失败:${errorMessage(error)}`);
82
+ }
83
+ };
84
+ const report = (runtime, patch)=>{
85
+ runtime.state = {
86
+ ...runtime.state,
87
+ ...patch
88
+ };
89
+ try {
90
+ ui.update(snapshot());
91
+ } catch (error) {
92
+ completion.reject(error);
93
+ }
94
+ };
95
+ const fail = (runtime, error)=>{
96
+ if (controller.signal.aborted) return;
97
+ report(runtime, {
98
+ status: 'error',
99
+ message: '运行失败',
100
+ busy: false
101
+ });
102
+ log.error(`[${runtime.pluginName}/${runtime.service.name}] ${errorMessage(error)}`);
103
+ };
104
+ const update = (runtime)=>{
105
+ if (!runtime.dirty || 'ready' !== runtime.phase || runtime.running || controller.signal.aborted) return;
106
+ runtime.running = Promise.resolve().then(async ()=>{
107
+ try {
108
+ while(runtime.dirty && !controller.signal.aborted){
109
+ runtime.dirty = false;
110
+ try {
111
+ await runtime.onChange?.();
112
+ } catch (error) {
113
+ fail(runtime, error);
114
+ }
115
+ }
116
+ } finally{
117
+ runtime.running = void 0;
118
+ }
119
+ });
120
+ };
121
+ try {
122
+ const renderer = renderers[0];
123
+ rendering = (async ()=>{
124
+ if (false === options.tui || !isInteractive() || !renderer) return;
125
+ let created;
126
+ try {
127
+ created = await renderer.service.createUI(ctx, stop);
128
+ } catch (error) {
129
+ if (!controller.signal.aborted) log.warn(`调试界面加载失败,使用普通日志:${errorMessage(error)}`);
130
+ return;
131
+ }
132
+ if (controller.signal.aborted) return void await created.close();
133
+ ui = created;
134
+ interactive = true;
135
+ ui.update(snapshot());
136
+ })().catch(completion.reject);
137
+ const matches = (value, path)=>!path || value === path || value.startsWith(`${path}${sep}`);
138
+ const watcher = watch(project.root, {
139
+ ignoreInitial: true,
140
+ followSymlinks: false,
141
+ ignored: (path)=>{
142
+ const value = relative(project.root, path);
143
+ return Boolean(value) && !services.some((runtime)=>runtime.paths.some((path)=>matches(value, path) || matches(path, value)));
144
+ }
145
+ });
146
+ watcher.on('all', (_event, path)=>{
147
+ if (controller.signal.aborted) return;
148
+ const value = relative(project.root, path);
149
+ for (const runtime of services)if (runtime.paths.some((path)=>matches(value, path))) runtime.dirty = true;
150
+ clearTimeout(timer);
151
+ timer = setTimeout(()=>services.forEach(update), 400);
152
+ });
153
+ watcher.on('error', completion.reject);
154
+ const starting = Promise.all(services.map(async (runtime)=>{
155
+ try {
156
+ if (runtime === renderer) await rendering;
157
+ controller.signal.throwIfAborted();
158
+ await runtime.service.start({
159
+ ...ctx,
160
+ interactive,
161
+ log,
162
+ report: (patch)=>report(runtime, patch),
163
+ onDispose: (callback)=>runtime.disposers.push(callback),
164
+ onFileChange: (listener)=>{
165
+ runtime.onChange = listener;
166
+ }
167
+ });
168
+ runtime.phase = 'ready';
169
+ update(runtime);
170
+ } catch (error) {
171
+ runtime.phase = 'failed';
172
+ fail(runtime, error);
173
+ await dispose(runtime);
174
+ }
175
+ }));
176
+ try {
177
+ await completion.promise;
178
+ } finally{
179
+ controller.abort();
180
+ clearTimeout(timer);
181
+ try {
182
+ await watcher.close();
183
+ } finally{
184
+ await starting;
185
+ await Promise.all(services.map((runtime)=>runtime.running));
186
+ }
187
+ }
188
+ } catch (error) {
189
+ if (error !== controller.signal.reason && !(error instanceof Error && 'AbortError' === error.name)) throw error;
190
+ } finally{
191
+ controller.abort();
192
+ await rendering;
193
+ for (const runtime of services.toReversed())await dispose(runtime);
194
+ try {
195
+ await ui.close();
196
+ } finally{
197
+ process.off('SIGINT', onSigint);
198
+ process.off('SIGTERM', onSigterm);
199
+ }
200
+ }
201
+ }
202
+ async function selectServices(ctx, hooks) {
203
+ const { options } = ctx;
204
+ const available = [];
205
+ const names = new Set();
206
+ for (const hook of hooks){
207
+ const contributed = await callHook(hook, ctx);
208
+ const { pluginName } = hook;
209
+ for (const service of contributed){
210
+ if (!service.name || names.has(service.name)) throw new Error(`[${pluginName}] 开发服务名称为空或重复:${service.name}`);
211
+ names.add(service.name);
212
+ const applies = await callHook({
213
+ pluginName,
214
+ callback: service.appliesTo
215
+ }, ctx);
216
+ if (!applies && service.option && options[service.option]) throw new Error(`[${pluginName}] 当前项目不支持:${service.title}`);
217
+ if (applies) available.push({
218
+ pluginName,
219
+ service
220
+ });
221
+ }
222
+ }
223
+ const targets = available.filter(({ service })=>service.option);
224
+ let target = '';
225
+ if (targets.length && !options.plugin && !targets.some(({ service })=>options[service.option]) && false !== options.tui && isInteractive()) {
226
+ const choice = await prompts_select({
227
+ message: '请选择调试目标',
228
+ options: [
229
+ {
230
+ value: '',
231
+ label: '插件调试(Skill / MCP)'
232
+ },
233
+ ...targets.map(({ service })=>({
234
+ value: service.name,
235
+ label: service.title
236
+ }))
237
+ ]
238
+ });
239
+ if (isCancel(choice)) {
240
+ cancel('已取消调试。');
241
+ process.exitCode = 130;
242
+ return;
243
+ }
244
+ target = choice;
245
+ }
246
+ return available.filter(({ service })=>!service.option || options[service.option] || service.name === target);
247
+ }
248
+ export { runSession };