@godv61/dsh-task-engine 0.22.6 → 0.23.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/.p0-test.mjs +20 -12
- package/.resource-test.mjs +133 -0
- package/LICENSE +21 -0
- package/README.md +216 -206
- package/docs/listing/godv61__dsh-task-engine.yml +6 -0
- package/docs/listing/submission.md +9 -0
- package/docs/manual.html +385 -382
- package/docs/release-0.23.0.md +34 -0
- package/docs/resource-install.md +18 -0
- package/docs/testing/0.23.0//346/265/213/350/257/225/346/211/247/350/241/214/350/256/260/345/275/225.md +189 -0
- package/docs/testing/0.23.0//346/265/213/350/257/225/346/212/245/345/221/212.md +42 -0
- package/lib/client.js +567 -612
- package/lib/client.js.map +4 -4
- package/lib/controller.d.ts +49 -0
- package/lib/controller.js +119 -90
- package/lib/controller.js.map +1 -1
- package/lib/dev-task.js +2 -1
- package/lib/dev-task.js.map +1 -1
- package/lib/engine.d.ts +2 -0
- package/lib/engine.js.map +1 -1
- package/lib/resource-import.d.ts +21 -0
- package/lib/resource-import.js +210 -0
- package/lib/resource-import.js.map +1 -0
- package/lib/resource-types.d.ts +25 -0
- package/lib/resource-types.js +2 -0
- package/lib/resource-types.js.map +1 -0
- package/lib/workspace-access.d.ts +6 -0
- package/lib/workspace-access.js +13 -0
- package/lib/workspace-access.js.map +1 -0
- package/package.json +94 -85
package/lib/controller.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @module dsh-task-engine/controller
|
|
10
10
|
*/
|
|
11
|
+
import type { ResourceImportRequest, ResourcePreview } from './resource-types.ts';
|
|
11
12
|
import type { Context } from '@deepseek-ai/cordis';
|
|
12
13
|
import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
13
14
|
import { type StageBinding, type WorkflowConfig } from './engine.ts';
|
|
@@ -72,6 +73,28 @@ export interface InstallSkillRequest {
|
|
|
72
73
|
/** Absolute workspace directory (required when level is `project`). */
|
|
73
74
|
path?: string;
|
|
74
75
|
}
|
|
76
|
+
/** One child directory entry shown by the install directory picker. */
|
|
77
|
+
export interface ListDirsEntry {
|
|
78
|
+
name: string;
|
|
79
|
+
/** True when this directory carries a SKILL.md of its own. */
|
|
80
|
+
hasSkill: boolean;
|
|
81
|
+
}
|
|
82
|
+
/** `listDirs` request: the absolute path to list (empty = the host home directory). */
|
|
83
|
+
export interface ListDirsRequest {
|
|
84
|
+
path: string;
|
|
85
|
+
}
|
|
86
|
+
/** `listDirs` result: the subdirectories of one absolute path for the install picker. */
|
|
87
|
+
export interface ListDirsView {
|
|
88
|
+
ok: boolean;
|
|
89
|
+
/** The absolute directory that was listed (normalized input). */
|
|
90
|
+
path: string;
|
|
91
|
+
entries: ListDirsEntry[];
|
|
92
|
+
/** Filesystem roots offered as jump targets (Windows drive letters, `/` on POSIX). */
|
|
93
|
+
roots: string[];
|
|
94
|
+
/** True when the listed directory itself carries a SKILL.md (installable as-is). */
|
|
95
|
+
currentHasSkill: boolean;
|
|
96
|
+
error?: string;
|
|
97
|
+
}
|
|
75
98
|
/** Create a project- or user-level rule. */
|
|
76
99
|
export interface WriteRuleRequest {
|
|
77
100
|
name: string;
|
|
@@ -163,6 +186,10 @@ export interface TaskLedgerItem {
|
|
|
163
186
|
}
|
|
164
187
|
/** One task's ledger projection: identity, stage, and the per-item audit trail. */
|
|
165
188
|
export interface TaskLedgerEntry {
|
|
189
|
+
risk_level?: string;
|
|
190
|
+
updated_at?: string;
|
|
191
|
+
verification_passed?: boolean;
|
|
192
|
+
review_outcome?: string;
|
|
166
193
|
task_id: string;
|
|
167
194
|
title: string;
|
|
168
195
|
stage: string;
|
|
@@ -218,6 +245,20 @@ export interface InitWriteResult {
|
|
|
218
245
|
*/
|
|
219
246
|
export default class TaskEngineController extends TypertRemoteService {
|
|
220
247
|
constructor(ctx: Context);
|
|
248
|
+
private authorizedPath;
|
|
249
|
+
/** Absolute installation roots for a selected workspace. */
|
|
250
|
+
resourceRoots(request: {
|
|
251
|
+
kind: 'skill' | 'rule';
|
|
252
|
+
path: string;
|
|
253
|
+
}): Promise<{
|
|
254
|
+
project: string;
|
|
255
|
+
user: string;
|
|
256
|
+
}>;
|
|
257
|
+
/** Inspect uploaded bytes or a host directory without writing. */
|
|
258
|
+
previewResource(request: ResourceImportRequest): Promise<ResourcePreview>;
|
|
259
|
+
/** Install exactly the reviewed bytes and target, without overwriting. */
|
|
260
|
+
importResource(request: ResourceImportRequest): Promise<ResourcePreview>;
|
|
261
|
+
private resourceImport;
|
|
221
262
|
/**
|
|
222
263
|
* Resolve one workspace's effective workflow: a preset flow plus the project's
|
|
223
264
|
* stage-binding override.
|
|
@@ -292,6 +333,14 @@ export default class TaskEngineController extends TypertRemoteService {
|
|
|
292
333
|
* @returns the installed path, or `ok: false` with an error.
|
|
293
334
|
*/
|
|
294
335
|
installSkill(request: InstallSkillRequest): Promise<WriteResourceResult>;
|
|
336
|
+
/**
|
|
337
|
+
* List the direct subdirectories of an absolute path for the install
|
|
338
|
+
* directory picker. Each child is flagged when it carries its own SKILL.md,
|
|
339
|
+
* so the dialog can tell a skill root apart from a plain container or folder.
|
|
340
|
+
* @param request - absolute directory to list (empty = the host home directory).
|
|
341
|
+
* @returns subdirectory names, each with its hasSkill flag.
|
|
342
|
+
*/
|
|
343
|
+
listDirs(request: ListDirsRequest): Promise<ListDirsView>;
|
|
295
344
|
/**
|
|
296
345
|
* Create a project- or user-level rule by writing its markdown file.
|
|
297
346
|
* @param request - name, body, and target level.
|
package/lib/controller.js
CHANGED
|
@@ -42,7 +42,9 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
|
|
|
42
42
|
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
43
43
|
done = true;
|
|
44
44
|
};
|
|
45
|
-
import {
|
|
45
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
46
|
+
import { authorizedWorkspace } from "./workspace-access.js";
|
|
47
|
+
import { prepareImport, commitImport } from "./resource-import.js";
|
|
46
48
|
import { homedir } from 'node:os';
|
|
47
49
|
import { dirname, isAbsolute, join } from 'node:path';
|
|
48
50
|
import { fileURLToPath } from 'node:url';
|
|
@@ -100,38 +102,18 @@ function renderSkillFile(name, description, whenToUse, content) {
|
|
|
100
102
|
head.push(`whenToUse: ${whenToUse.trim()}`);
|
|
101
103
|
return `---\n${head.join('\n')}\n---\n\n${content.trimEnd()}\n`;
|
|
102
104
|
}
|
|
103
|
-
/**
|
|
104
|
-
const
|
|
105
|
-
/**
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const walk = (src, dst) => {
|
|
116
|
-
mkdirSync(dst, { recursive: true });
|
|
117
|
-
for (const entry of readdirSync(src, { withFileTypes: true })) {
|
|
118
|
-
if (entry.isDirectory()) {
|
|
119
|
-
if (SKILL_SKIP_DIRS.has(entry.name))
|
|
120
|
-
continue;
|
|
121
|
-
walk(join(src, entry.name), join(dst, entry.name));
|
|
122
|
-
}
|
|
123
|
-
else if (entry.isFile()) {
|
|
124
|
-
const srcPath = join(src, entry.name);
|
|
125
|
-
bytes += statSync(srcPath).size;
|
|
126
|
-
files += 1;
|
|
127
|
-
if (files > SKILL_MAX_FILES || bytes > SKILL_MAX_BYTES) {
|
|
128
|
-
throw new Error(`skill 目录过大(上限 ${SKILL_MAX_FILES} 个文件 / ${Math.round(SKILL_MAX_BYTES / 1024 / 1024)} MB),已排除 node_modules/.git 等缓存目录`);
|
|
129
|
-
}
|
|
130
|
-
copyFileSync(srcPath, join(dst, entry.name));
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
walk(source, dest);
|
|
105
|
+
/** Dirs hidden from the install directory picker (plain dependency/ignore caches). */
|
|
106
|
+
const SKILL_DIR_FILTER = new Set(['node_modules', '.git', '__pycache__', '.venv', 'venv']);
|
|
107
|
+
/** Filesystem roots the picker offers as jump targets (drive letters on Windows, `/` elsewhere). */
|
|
108
|
+
function filesystemRoots() {
|
|
109
|
+
if (process.platform !== 'win32')
|
|
110
|
+
return ['/'];
|
|
111
|
+
const roots = [];
|
|
112
|
+
for (const letter of 'CDEFGHIJKLMNOPQRSTUVWXYZ') {
|
|
113
|
+
if (existsSync(`${letter}:\\`))
|
|
114
|
+
roots.push(`${letter}:\\`);
|
|
115
|
+
}
|
|
116
|
+
return roots;
|
|
135
117
|
}
|
|
136
118
|
/** Scan a markdown directory, returning `{ name }` per `.md` file. */
|
|
137
119
|
function scanRuleDir(dir, source) {
|
|
@@ -210,6 +192,9 @@ function listSkillsFromDir(dir, source) {
|
|
|
210
192
|
let TaskEngineController = (() => {
|
|
211
193
|
let _classSuper = TypertRemoteService;
|
|
212
194
|
let _instanceExtraInitializers = [];
|
|
195
|
+
let _resourceRoots_decorators;
|
|
196
|
+
let _previewResource_decorators;
|
|
197
|
+
let _importResource_decorators;
|
|
213
198
|
let _read_decorators;
|
|
214
199
|
let _write_decorators;
|
|
215
200
|
let _listSkills_decorators;
|
|
@@ -220,6 +205,7 @@ let TaskEngineController = (() => {
|
|
|
220
205
|
let _generateInit_decorators;
|
|
221
206
|
let _writeSkill_decorators;
|
|
222
207
|
let _installSkill_decorators;
|
|
208
|
+
let _listDirs_decorators;
|
|
223
209
|
let _writeRule_decorators;
|
|
224
210
|
let _readSkill_decorators;
|
|
225
211
|
let _readRule_decorators;
|
|
@@ -228,6 +214,9 @@ let TaskEngineController = (() => {
|
|
|
228
214
|
return class TaskEngineController extends _classSuper {
|
|
229
215
|
static {
|
|
230
216
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
217
|
+
_resourceRoots_decorators = [Remote];
|
|
218
|
+
_previewResource_decorators = [Remote];
|
|
219
|
+
_importResource_decorators = [Remote];
|
|
231
220
|
_read_decorators = [Remote];
|
|
232
221
|
_write_decorators = [Remote];
|
|
233
222
|
_listSkills_decorators = [Remote];
|
|
@@ -238,11 +227,15 @@ let TaskEngineController = (() => {
|
|
|
238
227
|
_generateInit_decorators = [Remote];
|
|
239
228
|
_writeSkill_decorators = [Remote];
|
|
240
229
|
_installSkill_decorators = [Remote];
|
|
230
|
+
_listDirs_decorators = [Remote];
|
|
241
231
|
_writeRule_decorators = [Remote];
|
|
242
232
|
_readSkill_decorators = [Remote];
|
|
243
233
|
_readRule_decorators = [Remote];
|
|
244
234
|
_deleteSkill_decorators = [Remote];
|
|
245
235
|
_deleteRule_decorators = [Remote];
|
|
236
|
+
__esDecorate(this, null, _resourceRoots_decorators, { kind: "method", name: "resourceRoots", static: false, private: false, access: { has: obj => "resourceRoots" in obj, get: obj => obj.resourceRoots }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
237
|
+
__esDecorate(this, null, _previewResource_decorators, { kind: "method", name: "previewResource", static: false, private: false, access: { has: obj => "previewResource" in obj, get: obj => obj.previewResource }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
238
|
+
__esDecorate(this, null, _importResource_decorators, { kind: "method", name: "importResource", static: false, private: false, access: { has: obj => "importResource" in obj, get: obj => obj.importResource }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
246
239
|
__esDecorate(this, null, _read_decorators, { kind: "method", name: "read", static: false, private: false, access: { has: obj => "read" in obj, get: obj => obj.read }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
247
240
|
__esDecorate(this, null, _write_decorators, { kind: "method", name: "write", static: false, private: false, access: { has: obj => "write" in obj, get: obj => obj.write }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
248
241
|
__esDecorate(this, null, _listSkills_decorators, { kind: "method", name: "listSkills", static: false, private: false, access: { has: obj => "listSkills" in obj, get: obj => obj.listSkills }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
@@ -253,6 +246,7 @@ let TaskEngineController = (() => {
|
|
|
253
246
|
__esDecorate(this, null, _generateInit_decorators, { kind: "method", name: "generateInit", static: false, private: false, access: { has: obj => "generateInit" in obj, get: obj => obj.generateInit }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
254
247
|
__esDecorate(this, null, _writeSkill_decorators, { kind: "method", name: "writeSkill", static: false, private: false, access: { has: obj => "writeSkill" in obj, get: obj => obj.writeSkill }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
255
248
|
__esDecorate(this, null, _installSkill_decorators, { kind: "method", name: "installSkill", static: false, private: false, access: { has: obj => "installSkill" in obj, get: obj => obj.installSkill }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
249
|
+
__esDecorate(this, null, _listDirs_decorators, { kind: "method", name: "listDirs", static: false, private: false, access: { has: obj => "listDirs" in obj, get: obj => obj.listDirs }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
256
250
|
__esDecorate(this, null, _writeRule_decorators, { kind: "method", name: "writeRule", static: false, private: false, access: { has: obj => "writeRule" in obj, get: obj => obj.writeRule }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
257
251
|
__esDecorate(this, null, _readSkill_decorators, { kind: "method", name: "readSkill", static: false, private: false, access: { has: obj => "readSkill" in obj, get: obj => obj.readSkill }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
258
252
|
__esDecorate(this, null, _readRule_decorators, { kind: "method", name: "readRule", static: false, private: false, access: { has: obj => "readRule" in obj, get: obj => obj.readRule }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
@@ -264,6 +258,35 @@ let TaskEngineController = (() => {
|
|
|
264
258
|
super(ctx, 'taskEngineController', { namespace: 'task-engine' });
|
|
265
259
|
__runInitializers(this, _instanceExtraInitializers);
|
|
266
260
|
}
|
|
261
|
+
async authorizedPath(path) {
|
|
262
|
+
return authorizedWorkspace(this.ctx, path, checkedPath);
|
|
263
|
+
}
|
|
264
|
+
/** Absolute installation roots for a selected workspace. */
|
|
265
|
+
async resourceRoots(request) {
|
|
266
|
+
if (!request.path.trim())
|
|
267
|
+
throw new Error('请选择工作区');
|
|
268
|
+
const workspace = await this.authorizedPath(request.path);
|
|
269
|
+
return { project: join(workspace, '.dsh', request.kind + 's'), user: join(dshHome(), request.kind + 's') };
|
|
270
|
+
}
|
|
271
|
+
/** Inspect uploaded bytes or a host directory without writing. */
|
|
272
|
+
async previewResource(request) {
|
|
273
|
+
return this.resourceImport(request, false);
|
|
274
|
+
}
|
|
275
|
+
/** Install exactly the reviewed bytes and target, without overwriting. */
|
|
276
|
+
async importResource(request) {
|
|
277
|
+
return this.resourceImport(request, true);
|
|
278
|
+
}
|
|
279
|
+
async resourceImport(request, commit) {
|
|
280
|
+
try {
|
|
281
|
+
const roots = await this.resourceRoots(request);
|
|
282
|
+
const base = roots[request.level];
|
|
283
|
+
const bundled = request.kind === 'skill' ? bundledSkillExists : bundledRuleExists;
|
|
284
|
+
return commit ? commitImport(request, base, bundled) : prepareImport(request, base, bundled).preview;
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
return { ok: false, name: '', description: '', content: '', target: '', files: 0, bytes: 0, hash: '', conflict: false, error: error instanceof Error ? error.message : String(error) };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
267
290
|
/**
|
|
268
291
|
* Resolve one workspace's effective workflow: a preset flow plus the project's
|
|
269
292
|
* stage-binding override.
|
|
@@ -271,7 +294,7 @@ let TaskEngineController = (() => {
|
|
|
271
294
|
* @returns the preset, project, or invalid selection plus its validation problems.
|
|
272
295
|
*/
|
|
273
296
|
async read(path) {
|
|
274
|
-
path =
|
|
297
|
+
path = await this.authorizedPath(path);
|
|
275
298
|
const fs = this.fs();
|
|
276
299
|
const raw = await readEngText(fs, path);
|
|
277
300
|
if (raw === undefined) {
|
|
@@ -325,7 +348,7 @@ let TaskEngineController = (() => {
|
|
|
325
348
|
* `problems` filled when validation fails (nothing is written then).
|
|
326
349
|
*/
|
|
327
350
|
async write(request) {
|
|
328
|
-
request.path =
|
|
351
|
+
request.path = await this.authorizedPath(request.path);
|
|
329
352
|
const resolved = resolveFlow(request.flow, request.stage_bindings !== undefined ? { stage_bindings: request.stage_bindings } : undefined);
|
|
330
353
|
if (!resolved.ok) {
|
|
331
354
|
return {
|
|
@@ -362,7 +385,7 @@ let TaskEngineController = (() => {
|
|
|
362
385
|
* @returns the mountable skill catalog.
|
|
363
386
|
*/
|
|
364
387
|
async listSkills(path) {
|
|
365
|
-
path =
|
|
388
|
+
path = await this.authorizedPath(path);
|
|
366
389
|
const project = path === '' ? [] : listSkillsFromDir(join(path, '.dsh/skills'), 'project');
|
|
367
390
|
const user = listSkillsFromDir(join(dshHome(), 'skills'), 'user');
|
|
368
391
|
const bundled = listSkillsFromDir(BUNDLED_SKILLS_DIR, 'bundled');
|
|
@@ -381,7 +404,7 @@ let TaskEngineController = (() => {
|
|
|
381
404
|
* @returns the mountable rule catalog.
|
|
382
405
|
*/
|
|
383
406
|
async listRules(path) {
|
|
384
|
-
path =
|
|
407
|
+
path = await this.authorizedPath(path);
|
|
385
408
|
const project = path === '' ? [] : scanRuleDir(join(path, '.dsh/rules'), 'project');
|
|
386
409
|
const user = scanRuleDir(join(dshHome(), 'rules'), 'user');
|
|
387
410
|
const bundled = scanRuleDir(BUNDLED_RULES_DIR, 'bundled');
|
|
@@ -405,7 +428,7 @@ let TaskEngineController = (() => {
|
|
|
405
428
|
* @returns the ledger entries, empty when no task records exist.
|
|
406
429
|
*/
|
|
407
430
|
async readTasks(path) {
|
|
408
|
-
path =
|
|
431
|
+
path = await this.authorizedPath(path);
|
|
409
432
|
const fs = this.fs();
|
|
410
433
|
const tasks = [];
|
|
411
434
|
try {
|
|
@@ -422,14 +445,20 @@ let TaskEngineController = (() => {
|
|
|
422
445
|
state = JSON.parse(raw);
|
|
423
446
|
}
|
|
424
447
|
catch {
|
|
425
|
-
|
|
448
|
+
throw new Error('任务记录无法解析:' + entry.name);
|
|
426
449
|
}
|
|
450
|
+
if (!state || ['id', 'title', 'stage', 'branch'].some(key => typeof state[key] !== 'string') || (state.items !== undefined && !Array.isArray(state.items)))
|
|
451
|
+
throw new Error('任务记录格式无效:' + entry.name);
|
|
427
452
|
tasks.push({
|
|
453
|
+
...(state.risk_level ? { risk_level: state.risk_level } : {}),
|
|
454
|
+
...(state.updated_at ? { updated_at: state.updated_at } : {}),
|
|
455
|
+
verification_passed: state.verification?.passed ?? false,
|
|
456
|
+
review_outcome: state.review?.outcome ?? 'pending',
|
|
428
457
|
task_id: state.id,
|
|
429
458
|
title: state.title,
|
|
430
459
|
stage: state.stage,
|
|
431
460
|
branch: state.branch,
|
|
432
|
-
items: state.items.map(item => ({
|
|
461
|
+
items: (state.items ?? []).map(item => ({
|
|
433
462
|
id: item.id,
|
|
434
463
|
title: item.title,
|
|
435
464
|
status: item.status,
|
|
@@ -439,8 +468,10 @@ let TaskEngineController = (() => {
|
|
|
439
468
|
});
|
|
440
469
|
}
|
|
441
470
|
}
|
|
442
|
-
catch {
|
|
443
|
-
//
|
|
471
|
+
catch (error) {
|
|
472
|
+
// Only an absent directory is an empty ledger; denied access and corruption remain visible.
|
|
473
|
+
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
|
|
474
|
+
throw error;
|
|
444
475
|
}
|
|
445
476
|
return { tasks };
|
|
446
477
|
}
|
|
@@ -450,7 +481,7 @@ let TaskEngineController = (() => {
|
|
|
450
481
|
* @returns existence, body, and line count.
|
|
451
482
|
*/
|
|
452
483
|
async readInit(path) {
|
|
453
|
-
path =
|
|
484
|
+
path = await this.authorizedPath(path);
|
|
454
485
|
const { root } = locateInitRoot(path);
|
|
455
486
|
const raw = await readTextAt(this.fs(), root, INITFILE);
|
|
456
487
|
if (raw === undefined)
|
|
@@ -468,7 +499,7 @@ let TaskEngineController = (() => {
|
|
|
468
499
|
* @returns `ok: false` with `lines` over the cap when the body is too long.
|
|
469
500
|
*/
|
|
470
501
|
async writeInit(request) {
|
|
471
|
-
request.path =
|
|
502
|
+
request.path = await this.authorizedPath(request.path);
|
|
472
503
|
const lines = lineCount(request.content);
|
|
473
504
|
if (lines > INIT_MAX_LINES) {
|
|
474
505
|
return { ok: false, lines, error: `AGENTS.md 为 ${lines} 行,超过上限 ${INIT_MAX_LINES} 行;请精简到项目骨架后重试。` };
|
|
@@ -500,7 +531,7 @@ let TaskEngineController = (() => {
|
|
|
500
531
|
* @returns the drafted body, or `ok: false` with an error when generation fails.
|
|
501
532
|
*/
|
|
502
533
|
async generateInit(request) {
|
|
503
|
-
request.path =
|
|
534
|
+
request.path = await this.authorizedPath(request.path);
|
|
504
535
|
const { root } = locateInitRoot(request.path);
|
|
505
536
|
const snapshot = scanProject(root);
|
|
506
537
|
const llm = this.ctx.get('llm');
|
|
@@ -551,7 +582,7 @@ let TaskEngineController = (() => {
|
|
|
551
582
|
*/
|
|
552
583
|
async writeSkill(request) {
|
|
553
584
|
if (request.path !== undefined)
|
|
554
|
-
request.path =
|
|
585
|
+
request.path = await this.authorizedPath(request.path);
|
|
555
586
|
const name = sanitizeName(request.name);
|
|
556
587
|
if (name === '' || request.description.trim() === '') {
|
|
557
588
|
return { ok: false, name, path: '', error: 'skill name and description must not be empty' };
|
|
@@ -574,45 +605,39 @@ let TaskEngineController = (() => {
|
|
|
574
605
|
* @returns the installed path, or `ok: false` with an error.
|
|
575
606
|
*/
|
|
576
607
|
async installSkill(request) {
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
if (bundledSkillExists(name)) {
|
|
600
|
-
return { ok: false, name, path: '', error: `"${name}" 是内置技能,不可覆盖;给 SKILL.md 换个名字` };
|
|
601
|
-
}
|
|
602
|
-
const base = request.level === 'project'
|
|
603
|
-
? join(request.path !== undefined ? checkedPath(request.path) : '', '.dsh/skills')
|
|
604
|
-
: join(dshHome(), 'skills');
|
|
605
|
-
const dest = join(base, name);
|
|
606
|
-
if (existsSync(dest)) {
|
|
607
|
-
return { ok: false, name, path: dest, error: `目标位置已存在同名技能(${dest})——先删除或换名再装` };
|
|
608
|
-
}
|
|
608
|
+
const input = { kind: 'skill', level: request.level, path: request.path ?? process.cwd(), files: [], sourceDir: request.sourceDir };
|
|
609
|
+
const preview = await this.previewResource(input);
|
|
610
|
+
if (!preview.ok)
|
|
611
|
+
return { ok: false, name: preview.name, path: preview.target, error: preview.error ?? '安装校验失败' };
|
|
612
|
+
const result = await this.importResource({ ...input, expectedHash: preview.hash });
|
|
613
|
+
return result.ok ? { ok: true, name: result.name, path: result.target } : { ok: false, name: result.name, path: result.target, error: result.error ?? '安装失败' };
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* List the direct subdirectories of an absolute path for the install
|
|
617
|
+
* directory picker. Each child is flagged when it carries its own SKILL.md,
|
|
618
|
+
* so the dialog can tell a skill root apart from a plain container or folder.
|
|
619
|
+
* @param request - absolute directory to list (empty = the host home directory).
|
|
620
|
+
* @returns subdirectory names, each with its hasSkill flag.
|
|
621
|
+
*/
|
|
622
|
+
async listDirs(request) {
|
|
623
|
+
const raw = request.path.trim();
|
|
624
|
+
const dir = raw === '' ? homedir() : raw;
|
|
625
|
+
const roots = filesystemRoots();
|
|
626
|
+
if (!isAbsolute(dir)) {
|
|
627
|
+
return { ok: false, path: dir, entries: [], roots, currentHasSkill: false, error: '路径必须是绝对路径' };
|
|
628
|
+
}
|
|
629
|
+
let entries;
|
|
609
630
|
try {
|
|
610
|
-
|
|
631
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
611
632
|
}
|
|
612
633
|
catch (error) {
|
|
613
|
-
return { ok: false,
|
|
634
|
+
return { ok: false, path: dir, entries: [], roots, currentHasSkill: false, error: error instanceof Error ? error.message : String(error) };
|
|
614
635
|
}
|
|
615
|
-
|
|
636
|
+
const list = entries
|
|
637
|
+
.filter(entry => entry.isDirectory() && !SKILL_DIR_FILTER.has(entry.name))
|
|
638
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
639
|
+
.map(entry => ({ name: entry.name, hasSkill: existsSync(join(dir, entry.name, 'SKILL.md')) }));
|
|
640
|
+
return { ok: true, path: dir, entries: list, roots, currentHasSkill: existsSync(join(dir, 'SKILL.md')) };
|
|
616
641
|
}
|
|
617
642
|
/**
|
|
618
643
|
* Create a project- or user-level rule by writing its markdown file.
|
|
@@ -621,7 +646,7 @@ let TaskEngineController = (() => {
|
|
|
621
646
|
*/
|
|
622
647
|
async writeRule(request) {
|
|
623
648
|
if (request.path !== undefined)
|
|
624
|
-
request.path =
|
|
649
|
+
request.path = await this.authorizedPath(request.path);
|
|
625
650
|
const name = sanitizeName(request.name);
|
|
626
651
|
if (name === '' || request.content.trim() === '') {
|
|
627
652
|
return { ok: false, name, path: '', error: 'rule name and content must not be empty' };
|
|
@@ -643,7 +668,7 @@ let TaskEngineController = (() => {
|
|
|
643
668
|
*/
|
|
644
669
|
async readSkill(request) {
|
|
645
670
|
if (request.path !== undefined)
|
|
646
|
-
request.path =
|
|
671
|
+
request.path = await this.authorizedPath(request.path);
|
|
647
672
|
const name = sanitizeName(request.name);
|
|
648
673
|
const base = request.level === 'bundled'
|
|
649
674
|
? BUNDLED_SKILLS_DIR
|
|
@@ -668,7 +693,7 @@ let TaskEngineController = (() => {
|
|
|
668
693
|
*/
|
|
669
694
|
async readRule(request) {
|
|
670
695
|
if (request.path !== undefined)
|
|
671
|
-
request.path =
|
|
696
|
+
request.path = await this.authorizedPath(request.path);
|
|
672
697
|
const name = sanitizeName(request.name);
|
|
673
698
|
const base = request.level === 'bundled'
|
|
674
699
|
? BUNDLED_RULES_DIR
|
|
@@ -690,7 +715,7 @@ let TaskEngineController = (() => {
|
|
|
690
715
|
*/
|
|
691
716
|
async deleteSkill(request) {
|
|
692
717
|
if (request.path !== undefined)
|
|
693
|
-
request.path =
|
|
718
|
+
request.path = await this.authorizedPath(request.path);
|
|
694
719
|
const name = sanitizeName(request.name);
|
|
695
720
|
if (name === '')
|
|
696
721
|
return { ok: false, name, path: '', error: 'skill name must not be empty' };
|
|
@@ -707,7 +732,7 @@ let TaskEngineController = (() => {
|
|
|
707
732
|
*/
|
|
708
733
|
async deleteRule(request) {
|
|
709
734
|
if (request.path !== undefined)
|
|
710
|
-
request.path =
|
|
735
|
+
request.path = await this.authorizedPath(request.path);
|
|
711
736
|
const name = sanitizeName(request.name);
|
|
712
737
|
if (name === '')
|
|
713
738
|
return { ok: false, name, path: '', error: 'rule name must not be empty' };
|
|
@@ -802,7 +827,8 @@ class WorkspaceRegistry {
|
|
|
802
827
|
}
|
|
803
828
|
}
|
|
804
829
|
function normalizeKey(path) {
|
|
805
|
-
|
|
830
|
+
const key = path.replace(/\\/gu, '/').replace(/\/+$/u, '');
|
|
831
|
+
return process.platform === 'win32' ? key.toLowerCase() : key;
|
|
806
832
|
}
|
|
807
833
|
const workspaceRegistry = new WorkspaceRegistry();
|
|
808
834
|
/** When true, an empty registry no longer falls back to checkedPath — Remote calls fail closed until a host registers workspaces. */
|
|
@@ -825,13 +851,16 @@ export function enableStrictWorkspaces() {
|
|
|
825
851
|
*/
|
|
826
852
|
/** Exported for tests and host integrations: the Remote path guard itself. */
|
|
827
853
|
export function checkedPath(raw) {
|
|
828
|
-
if (raw.trim() === '')
|
|
829
|
-
|
|
854
|
+
if (raw.trim() === '') {
|
|
855
|
+
if (strictWorkspaces)
|
|
856
|
+
throw new Error('workspace path must be absolute');
|
|
857
|
+
return raw;
|
|
858
|
+
} // empty means the host default, preserved for compatibility
|
|
830
859
|
if (!isAbsolute(raw)) {
|
|
831
860
|
throw new RemoteError('gateway/internal', `task-engine: workspace path must be absolute: ${raw}`, {});
|
|
832
861
|
}
|
|
833
862
|
const normalized = raw.replace(/[\\/]+$/u, '');
|
|
834
|
-
const lower = normalized.toLowerCase();
|
|
863
|
+
const lower = normalized.replace(/\\/gu, '/').toLowerCase();
|
|
835
864
|
const forbidden = ['c:/windows', 'c:/program files', 'c:/program files (x86)', 'c:/users'];
|
|
836
865
|
for (const prefix of forbidden) {
|
|
837
866
|
if (lower === prefix || lower.startsWith(`${prefix}/`)) {
|