@nseng-ai/areg 0.1.1

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 (45) hide show
  1. package/package.json +29 -0
  2. package/src/cli.ts +109 -0
  3. package/src/context.ts +59 -0
  4. package/src/fake-gateways.ts +1009 -0
  5. package/src/gateways/command-constants.ts +1 -0
  6. package/src/gateways/errors.ts +5 -0
  7. package/src/gateways/fs-utils.ts +26 -0
  8. package/src/gateways/github-gateway.ts +111 -0
  9. package/src/gateways/host-gateway.ts +35 -0
  10. package/src/gateways/mutation-policy.ts +94 -0
  11. package/src/gateways/npx-skills-gateway.ts +53 -0
  12. package/src/gateways/project-fs.ts +320 -0
  13. package/src/gateways/project-gateway.ts +801 -0
  14. package/src/gateways/prompt-gateway.ts +21 -0
  15. package/src/gateways/skill-kind-classification.ts +39 -0
  16. package/src/gateways/skillx-workspace-gateway.ts +220 -0
  17. package/src/gateways.ts +361 -0
  18. package/src/index.ts +48 -0
  19. package/src/operations/check.ts +559 -0
  20. package/src/operations/doctor-skills-report.ts +109 -0
  21. package/src/operations/doctor-skills-severity.ts +9 -0
  22. package/src/operations/doctor-skills.ts +471 -0
  23. package/src/operations/file-state.ts +77 -0
  24. package/src/operations/frontmatter.ts +151 -0
  25. package/src/operations/init.ts +548 -0
  26. package/src/operations/lockfile.ts +107 -0
  27. package/src/operations/managed-markdown-block.ts +47 -0
  28. package/src/operations/pi-replacement.ts +33 -0
  29. package/src/operations/pi-settings.ts +60 -0
  30. package/src/operations/project-agents.ts +115 -0
  31. package/src/operations/project-inspection.ts +165 -0
  32. package/src/operations/project-mutations.ts +383 -0
  33. package/src/operations/project-resolution.ts +53 -0
  34. package/src/operations/skill-find.ts +295 -0
  35. package/src/operations/skill-kind-apply-plan.ts +531 -0
  36. package/src/operations/skill-kind-frontmatter.ts +55 -0
  37. package/src/operations/skill-kind-inference.ts +391 -0
  38. package/src/operations/skill-kind.ts +525 -0
  39. package/src/operations/skill-mirror-conventions.ts +126 -0
  40. package/src/operations/skillx.ts +305 -0
  41. package/src/operations/toml-section.ts +53 -0
  42. package/src/operations/update-skills.ts +325 -0
  43. package/src/real-gateways.ts +6 -0
  44. package/src/skill-lookup.ts +254 -0
  45. package/src/sort.ts +7 -0
@@ -0,0 +1,1009 @@
1
+ import { optionalEntry } from "@nseng-ai/foundation/primitives";
2
+ import { resultErr, resultOk } from "@nseng-ai/foundation/result";
3
+ import {
4
+ skillLookupBaseRelativePath,
5
+ skillLookupDescriptorForRoot,
6
+ skillLookupDescriptorForSourceType,
7
+ type SkillLookupRoot,
8
+ type SkillLookupSourceType,
9
+ } from "./skill-lookup.ts";
10
+
11
+ import { missingCheckSkillInspection } from "./gateways.ts";
12
+ import type {
13
+ AregCheckPairingDirectory,
14
+ AregCheckSkillInspection,
15
+ AregErrorInfo,
16
+ AregGithubGateway,
17
+ AregGithubSkillFileResult,
18
+ AregGithubSkillListResult,
19
+ AregHostGateway,
20
+ AregHostToolName,
21
+ AregNpxSkillsAddRequest,
22
+ AregNpxSkillsAddResult,
23
+ AregNpxSkillsGateway,
24
+ AregOperationResult,
25
+ AregPathState,
26
+ AregPiSkillInventoryInspection,
27
+ AregProjectBaseInspection,
28
+ AregProjectDirRequest,
29
+ AregProjectFileDeleteRequest,
30
+ AregProjectGateway,
31
+ AregProjectInspectionRequest,
32
+ AregProjectMutationResult,
33
+ AregProjectRemoveEmptyDirRequest,
34
+ AregProjectRemoveEmptyDirResult,
35
+ AregProjectSymlinkDeleteRequest,
36
+ AregProjectTextWriteRequest,
37
+ AregPromptGateway,
38
+ AregSkillFindRootsInspection,
39
+ AregSkillFindSkillInspection,
40
+ AregSkillInspectionRequest,
41
+ AregSkillKindResolveRequest,
42
+ AregSkillKindResolveResult,
43
+ AregSkillKindSkillInspection,
44
+ AregSkillKindSourceType,
45
+ AregSkillxInstallRequest,
46
+ AregSkillxInstallResult,
47
+ AregSkillxInstalledSkill,
48
+ AregSkillxWorkspaceCleanupRequest,
49
+ AregSkillxWorkspaceGateway,
50
+ AregTextFileState,
51
+ AregToolCheckResult,
52
+ } from "./gateways.ts";
53
+ import { classifyResolvedSkillKindInspection } from "./gateways/skill-kind-classification.ts";
54
+ import {
55
+ classifySkillMirrorSymlinkState,
56
+ parseSkillMirrorRelativePath,
57
+ } from "./operations/skill-mirror-conventions.ts";
58
+
59
+ export type FakeAregProjectOperation =
60
+ | { type: "inspect-project-base"; cwd: string; projectPath: string }
61
+ | { type: "inspect-instruction-files"; projectDir: string }
62
+ | { type: "inspect-pi-artifacts"; projectDir: string }
63
+ | { type: "inspect-pi-skill-inventory"; projectDir: string }
64
+ | { type: "inspect-skill-name-inventory"; projectDir: string }
65
+ | { type: "inspect-skill-find-roots"; projectDir: string }
66
+ | { type: "inspect-check-skill"; projectDir: string; skillName: string }
67
+ | { type: "inspect-skill-kind-skill"; projectDir: string; skillName: string }
68
+ | { type: "inspect-pairing-directories"; projectDir: string }
69
+ | { type: "read-locally-excluded-skill-names"; projectDir: string }
70
+ | { type: "resolve-skill-kind-spec"; projectDir: string; spec: string; cwd: string }
71
+ | ({ type: "preflight-write-text-file" } & Omit<AregProjectTextWriteRequest, "env">)
72
+ | ({ type: "preflight-delete-file" } & Omit<AregProjectFileDeleteRequest, "env">)
73
+ | ({ type: "preflight-delete-symlink" } & Omit<AregProjectSymlinkDeleteRequest, "env">)
74
+ | ({ type: "preflight-remove-empty-dir" } & Omit<AregProjectRemoveEmptyDirRequest, "env">)
75
+ | ({ type: "write-text-file" } & Omit<AregProjectTextWriteRequest, "env">)
76
+ | ({ type: "delete-file" } & Omit<AregProjectFileDeleteRequest, "env">)
77
+ | ({ type: "delete-symlink" } & Omit<AregProjectSymlinkDeleteRequest, "env">)
78
+ | ({ type: "remove-empty-dir" } & Omit<AregProjectRemoveEmptyDirRequest, "env">);
79
+
80
+ export interface FakeAregCheckSkillOptions {
81
+ name: string;
82
+ skillsPath?: AregPathState;
83
+ agentsPath?: AregPathState;
84
+ claudePath?: AregPathState;
85
+ localSkillMd?: AregTextFileState | string;
86
+ remoteSkillMd?: AregTextFileState | string;
87
+ openaiPolicy?: AregTextFileState | string;
88
+ }
89
+
90
+ export interface FakeAregSkillKindSkillOptions {
91
+ name: string;
92
+ sourceType?: AregSkillKindSourceType;
93
+ baseRelativePath?: string;
94
+ skillDir?: AregPathState;
95
+ skillMd?: AregTextFileState | string;
96
+ openaiPolicy?: AregTextFileState | string;
97
+ agentsPath?: AregPathState;
98
+ claudePath?: AregPathState;
99
+ }
100
+
101
+ export interface FakeAregSkillFindSkillOptions {
102
+ name: string;
103
+ root?: SkillLookupRoot;
104
+ sourceType?: SkillLookupSourceType;
105
+ baseRelativePath?: string;
106
+ skillDir?: AregPathState;
107
+ skillMd?: AregTextFileState | string;
108
+ }
109
+
110
+ export interface FakeAregProjectGatewayOptions {
111
+ projectDir?: string;
112
+ projectPathState?: AregPathState;
113
+ targetPathState?: AregPathState;
114
+ lockfile?: AregTextFileState | object | string;
115
+ nsToml?: AregTextFileState | string;
116
+ aregJson?: AregTextFileState | object | string;
117
+ agentsMd?: AregTextFileState | string;
118
+ claudeMd?: AregTextFileState | string;
119
+ claudeDir?: AregPathState;
120
+ claudeSettings?: AregTextFileState | string;
121
+ piDir?: AregPathState;
122
+ piSettings?: AregTextFileState | object | string;
123
+ replacementSurfaces?: readonly string[];
124
+ piSkillInventory?: Partial<AregPiSkillInventoryInspection>;
125
+ skillsDirectoryNames?: readonly string[];
126
+ agentsSkillNames?: readonly string[];
127
+ claudeSkillNames?: readonly string[];
128
+ excludedSkillNames?: readonly string[];
129
+ checkSkills?: readonly FakeAregCheckSkillOptions[];
130
+ localSkills?: readonly FakeAregSkillKindSkillOptions[];
131
+ findSkills?: readonly FakeAregSkillFindSkillOptions[];
132
+ pairingDirectories?: readonly AregCheckPairingDirectory[];
133
+ resolveFailures?: Readonly<Record<string, AregErrorInfo>>;
134
+ preflightFailures?: Readonly<Record<string, AregErrorInfo>>;
135
+ mutationFailures?: Readonly<Record<string, AregErrorInfo>>;
136
+ applyFailure?: AregErrorInfo;
137
+ }
138
+
139
+ export class FakeAregProjectGateway implements AregProjectGateway {
140
+ private readonly projectDir: string;
141
+ private readonly projectPathState: AregPathState;
142
+ private readonly files: Map<string, AregTextFileState>;
143
+ private readonly claudeDir: AregPathState;
144
+ private readonly piDir: AregPathState;
145
+ private readonly replacementSurfaces: readonly string[];
146
+ private readonly piSkillInventory: AregPiSkillInventoryInspection | undefined;
147
+ private readonly skillsDirectoryNames: readonly string[];
148
+ private readonly agentsSkillNames: readonly string[];
149
+ private readonly claudeSkillNames: readonly string[];
150
+ private readonly excludedSkillNames: readonly string[];
151
+ private readonly checkSkills: AregCheckSkillInspection[];
152
+ private readonly localSkills: AregSkillKindSkillInspection[];
153
+ private readonly explicitFindSkills: AregSkillFindSkillInspection[] | undefined;
154
+ private readonly pairingDirectories: readonly AregCheckPairingDirectory[];
155
+ private readonly resolveFailures: ReadonlyMap<string, AregErrorInfo>;
156
+ private readonly preflightFailures: ReadonlyMap<string, AregErrorInfo>;
157
+ private readonly mutationFailures: ReadonlyMap<string, AregErrorInfo>;
158
+ private readonly log: FakeAregProjectOperation[] = [];
159
+
160
+ constructor(options: FakeAregProjectGatewayOptions = {}) {
161
+ this.projectDir = options.projectDir ?? "/repo";
162
+ this.projectPathState = copyPathState(
163
+ options.projectPathState ?? options.targetPathState ?? { type: "directory" },
164
+ );
165
+ this.files = new Map([
166
+ ["skills-lock.json", normalizeTextFileState(options.lockfile ?? { version: 1, skills: {} })],
167
+ ["ns.toml", normalizeTextFileState(options.nsToml ?? { type: "missing" })],
168
+ ["areg.json", normalizeTextFileState(options.aregJson ?? { type: "missing" })],
169
+ ["AGENTS.md", normalizeTextFileState(options.agentsMd ?? { type: "missing" })],
170
+ ["CLAUDE.md", normalizeTextFileState(options.claudeMd ?? { type: "missing" })],
171
+ [
172
+ ".claude/settings.local.json",
173
+ normalizeTextFileState(options.claudeSettings ?? { type: "missing" }),
174
+ ],
175
+ [".pi/settings.json", normalizeTextFileState(options.piSettings ?? { type: "missing" })],
176
+ ]);
177
+ this.claudeDir = copyPathState(options.claudeDir ?? { type: "missing" });
178
+ this.piDir = copyPathState(options.piDir ?? { type: "missing" });
179
+ this.replacementSurfaces = [...(options.replacementSurfaces ?? [])];
180
+ this.piSkillInventory =
181
+ options.piSkillInventory === undefined
182
+ ? undefined
183
+ : copyPiSkillInventory(options.piSkillInventory);
184
+ this.skillsDirectoryNames = [...(options.skillsDirectoryNames ?? [])];
185
+ this.agentsSkillNames = [...(options.agentsSkillNames ?? [])];
186
+ this.claudeSkillNames = [...(options.claudeSkillNames ?? [])];
187
+ this.excludedSkillNames = [...(options.excludedSkillNames ?? [])];
188
+ this.checkSkills = (options.checkSkills ?? []).map(copyFakeCheckSkill);
189
+ this.localSkills = (options.localSkills ?? []).map(copyFakeSkillKindSkill);
190
+ this.explicitFindSkills =
191
+ options.findSkills === undefined ? undefined : options.findSkills.map(copyFakeSkillFindSkill);
192
+ this.pairingDirectories = (options.pairingDirectories ?? []).map(copyPairingDirectory);
193
+ this.resolveFailures = new Map(
194
+ Object.entries(options.resolveFailures ?? {}).map(([key, value]) => [
195
+ key,
196
+ copyErrorInfo(value),
197
+ ]),
198
+ );
199
+ this.preflightFailures = new Map(
200
+ Object.entries(options.preflightFailures ?? {}).map(([key, value]) => [
201
+ key,
202
+ copyErrorInfo(value),
203
+ ]),
204
+ );
205
+ const mutationFailures =
206
+ options.applyFailure === undefined
207
+ ? options.mutationFailures
208
+ : { ...options.mutationFailures, "*": options.applyFailure };
209
+ this.mutationFailures = new Map(
210
+ Object.entries(mutationFailures ?? {}).map(([key, value]) => [key, copyErrorInfo(value)]),
211
+ );
212
+ }
213
+
214
+ async inspectProjectBase(
215
+ request: AregProjectInspectionRequest,
216
+ ): Promise<AregProjectBaseInspection> {
217
+ this.log.push({
218
+ type: "inspect-project-base",
219
+ cwd: request.cwd,
220
+ projectPath: request.projectPath,
221
+ });
222
+ return {
223
+ projectDir: this.projectDir,
224
+ projectPathState: copyPathState(this.projectPathState),
225
+ lockfile: this.fileState("skills-lock.json"),
226
+ nsToml: this.fileState("ns.toml"),
227
+ aregJson: this.fileState("areg.json"),
228
+ };
229
+ }
230
+
231
+ async inspectInstructionFiles(request: AregProjectDirRequest) {
232
+ this.log.push({ type: "inspect-instruction-files", projectDir: request.projectDir });
233
+ return {
234
+ agentsMd: this.fileState("AGENTS.md"),
235
+ claudeMd: this.fileState("CLAUDE.md"),
236
+ claudeDir: copyPathState(this.claudeDir),
237
+ claudeSettings: this.fileState(".claude/settings.local.json"),
238
+ };
239
+ }
240
+
241
+ async inspectPiArtifacts(request: AregProjectDirRequest) {
242
+ this.log.push({ type: "inspect-pi-artifacts", projectDir: request.projectDir });
243
+ return {
244
+ piDir: copyPathState(this.piDir),
245
+ piSettings: this.fileState(".pi/settings.json"),
246
+ replacement: { verifiedSurfaces: [...this.replacementSurfaces] },
247
+ };
248
+ }
249
+
250
+ async inspectPiSkillInventory(
251
+ request: AregProjectDirRequest,
252
+ ): Promise<AregPiSkillInventoryInspection> {
253
+ this.log.push({ type: "inspect-pi-skill-inventory", projectDir: request.projectDir });
254
+ if (this.piSkillInventory !== undefined) return copyPiSkillInventory(this.piSkillInventory);
255
+ return {
256
+ skillNames: this.localSkills
257
+ .filter((skill) => skill.skillDir.type === "directory" && skill.skillMd.type === "file")
258
+ .map((skill) => skill.name),
259
+ isApproximation: true,
260
+ source: "fake-repo-fallback-resolvable-skill-roots",
261
+ };
262
+ }
263
+
264
+ async inspectSkillNameInventory(request: AregProjectDirRequest) {
265
+ this.log.push({ type: "inspect-skill-name-inventory", projectDir: request.projectDir });
266
+ return {
267
+ skillsDirectoryNames: [...this.skillsDirectoryNames],
268
+ agentsSkillNames: [...this.agentsSkillNames],
269
+ claudeSkillNames: [...this.claudeSkillNames],
270
+ skillKindNames: this.localSkills.map((skill) => skill.name),
271
+ };
272
+ }
273
+
274
+ async inspectSkillFindRoots(
275
+ request: AregProjectDirRequest,
276
+ ): Promise<AregSkillFindRootsInspection> {
277
+ this.log.push({ type: "inspect-skill-find-roots", projectDir: request.projectDir });
278
+ return { skills: this.currentFindSkills().map(copySkillFindSkill) };
279
+ }
280
+
281
+ async inspectCheckSkill(request: AregSkillInspectionRequest): Promise<AregCheckSkillInspection> {
282
+ this.log.push({
283
+ type: "inspect-check-skill",
284
+ projectDir: request.projectDir,
285
+ skillName: request.skillName,
286
+ });
287
+ const skill = this.checkSkills.find((candidate) => candidate.name === request.skillName);
288
+ return skill === undefined
289
+ ? missingCheckSkillInspection(request.skillName)
290
+ : copyCheckSkill(skill);
291
+ }
292
+
293
+ async inspectSkillKindSkill(
294
+ request: AregSkillInspectionRequest,
295
+ ): Promise<AregSkillKindSkillInspection> {
296
+ this.log.push({
297
+ type: "inspect-skill-kind-skill",
298
+ projectDir: request.projectDir,
299
+ skillName: request.skillName,
300
+ });
301
+ const skill = this.localSkills.find((candidate) => candidate.name === request.skillName);
302
+ return skill === undefined
303
+ ? missingSkillKindSkill(request.skillName)
304
+ : copySkillKindSkill(skill);
305
+ }
306
+
307
+ async inspectPairingDirectories(
308
+ request: AregProjectDirRequest,
309
+ ): Promise<readonly AregCheckPairingDirectory[]> {
310
+ this.log.push({ type: "inspect-pairing-directories", projectDir: request.projectDir });
311
+ return this.pairingDirectories.map(copyPairingDirectory);
312
+ }
313
+
314
+ async readLocallyExcludedSkillNames(request: AregProjectDirRequest): Promise<readonly string[]> {
315
+ this.log.push({ type: "read-locally-excluded-skill-names", projectDir: request.projectDir });
316
+ return [...this.excludedSkillNames];
317
+ }
318
+
319
+ async resolveSkillKindSpec(
320
+ request: AregSkillKindResolveRequest,
321
+ ): Promise<AregSkillKindResolveResult> {
322
+ this.log.push({
323
+ type: "resolve-skill-kind-spec",
324
+ projectDir: request.projectDir,
325
+ spec: request.spec,
326
+ cwd: request.cwd,
327
+ });
328
+ const failure = this.resolveFailures.get(request.spec);
329
+ if (failure !== undefined) return { type: "error", error: copyErrorInfo(failure) };
330
+ const skillName = fakeResolveSkillName(request.spec);
331
+ const skill =
332
+ this.localSkills.find((candidate) => candidate.name === skillName) ??
333
+ missingSkillKindSkill(skillName);
334
+ return classifyResolvedSkillKindInspection({
335
+ spec: request.spec,
336
+ skillName,
337
+ inspection: skill,
338
+ });
339
+ }
340
+
341
+ async preflightWriteTextFile(
342
+ request: AregProjectTextWriteRequest,
343
+ ): Promise<AregProjectMutationResult> {
344
+ this.log.push({
345
+ type: "preflight-write-text-file",
346
+ projectDir: request.projectDir,
347
+ relativePath: request.relativePath,
348
+ content: request.content,
349
+ description: request.description,
350
+ createParent: request.createParent,
351
+ policy: request.policy,
352
+ });
353
+ const failure = this.preflightFailure(request.relativePath);
354
+ return failure === undefined ? { ok: true } : { ok: false, error: failure };
355
+ }
356
+
357
+ async preflightDeleteFile(
358
+ request: AregProjectFileDeleteRequest,
359
+ ): Promise<AregProjectMutationResult> {
360
+ this.log.push({
361
+ type: "preflight-delete-file",
362
+ projectDir: request.projectDir,
363
+ relativePath: request.relativePath,
364
+ description: request.description,
365
+ policy: request.policy,
366
+ });
367
+ const failure = this.preflightFailure(request.relativePath);
368
+ return failure === undefined ? { ok: true } : { ok: false, error: failure };
369
+ }
370
+
371
+ async preflightDeleteSymlink(
372
+ request: AregProjectSymlinkDeleteRequest,
373
+ ): Promise<AregProjectMutationResult> {
374
+ this.log.push({
375
+ type: "preflight-delete-symlink",
376
+ projectDir: request.projectDir,
377
+ relativePath: request.relativePath,
378
+ description: request.description,
379
+ policy: request.policy,
380
+ });
381
+ const failure =
382
+ this.preflightFailure(request.relativePath) ??
383
+ this.deleteSymlinkContractFailure(request.relativePath, request.description);
384
+ return failure === undefined ? { ok: true } : { ok: false, error: failure };
385
+ }
386
+
387
+ async preflightRemoveEmptyDir(
388
+ request: AregProjectRemoveEmptyDirRequest,
389
+ ): Promise<AregProjectMutationResult> {
390
+ this.log.push({
391
+ type: "preflight-remove-empty-dir",
392
+ projectDir: request.projectDir,
393
+ relativePath: request.relativePath,
394
+ description: request.description,
395
+ policy: request.policy,
396
+ });
397
+ const failure = this.preflightFailure(request.relativePath);
398
+ return failure === undefined ? { ok: true } : { ok: false, error: failure };
399
+ }
400
+
401
+ async writeTextFile(request: AregProjectTextWriteRequest): Promise<AregProjectMutationResult> {
402
+ this.log.push({
403
+ type: "write-text-file",
404
+ projectDir: request.projectDir,
405
+ relativePath: request.relativePath,
406
+ content: request.content,
407
+ description: request.description,
408
+ createParent: request.createParent,
409
+ policy: request.policy,
410
+ });
411
+ const failure = this.mutationFailure(request.relativePath);
412
+ if (failure !== undefined) return { ok: false, error: failure };
413
+ this.files.set(request.relativePath, { type: "file", text: request.content });
414
+ const skill = skillForRelativePath(this.localSkills, request.relativePath);
415
+ if (skill !== undefined && request.relativePath.endsWith("/SKILL.md"))
416
+ skill.skillMd = { type: "file", text: request.content };
417
+ if (skill !== undefined && request.relativePath.endsWith("/agents/openai.yaml"))
418
+ skill.openaiPolicy = { type: "file", text: request.content };
419
+ return { ok: true };
420
+ }
421
+
422
+ async deleteFile(request: AregProjectFileDeleteRequest): Promise<AregProjectMutationResult> {
423
+ this.log.push({
424
+ type: "delete-file",
425
+ projectDir: request.projectDir,
426
+ relativePath: request.relativePath,
427
+ description: request.description,
428
+ policy: request.policy,
429
+ });
430
+ const failure = this.mutationFailure(request.relativePath);
431
+ if (failure !== undefined) return { ok: false, error: failure };
432
+ this.files.set(request.relativePath, { type: "missing" });
433
+ const skill = skillForRelativePath(this.localSkills, request.relativePath);
434
+ if (skill !== undefined && request.relativePath.endsWith("/agents/openai.yaml"))
435
+ skill.openaiPolicy = { type: "missing" };
436
+ return { ok: true };
437
+ }
438
+
439
+ async deleteSymlink(
440
+ request: AregProjectSymlinkDeleteRequest,
441
+ ): Promise<AregProjectMutationResult> {
442
+ this.log.push({
443
+ type: "delete-symlink",
444
+ projectDir: request.projectDir,
445
+ relativePath: request.relativePath,
446
+ description: request.description,
447
+ policy: request.policy,
448
+ });
449
+ const failure =
450
+ this.mutationFailure(request.relativePath) ??
451
+ this.deleteSymlinkContractFailure(request.relativePath, request.description);
452
+ if (failure !== undefined) return { ok: false, error: failure };
453
+ const mirror = parseSkillMirrorRelativePath(request.relativePath);
454
+ const skill = mirror === undefined ? undefined : this.skillForMirrorName(mirror.skillName);
455
+ if (skill !== undefined && mirror !== undefined) {
456
+ if (mirror.mirrorKind === "agents") skill.agentsPath = { type: "missing" };
457
+ if (mirror.mirrorKind === "claude") skill.claudePath = { type: "missing" };
458
+ }
459
+ return { ok: true };
460
+ }
461
+
462
+ async removeEmptyDir(
463
+ request: AregProjectRemoveEmptyDirRequest,
464
+ ): Promise<AregProjectRemoveEmptyDirResult> {
465
+ this.log.push({
466
+ type: "remove-empty-dir",
467
+ projectDir: request.projectDir,
468
+ relativePath: request.relativePath,
469
+ description: request.description,
470
+ policy: request.policy,
471
+ });
472
+ const failure = this.mutationFailure(request.relativePath);
473
+ if (failure !== undefined) return { ok: false, error: failure };
474
+ return { ok: true, removed: true };
475
+ }
476
+
477
+ text(relativePath: string): string | undefined {
478
+ const state = this.files.get(relativePath);
479
+ return state?.type === "file" ? state.text : undefined;
480
+ }
481
+
482
+ operations(): readonly FakeAregProjectOperation[] {
483
+ return this.log.map(copyProjectOperation);
484
+ }
485
+
486
+ private fileState(relativePath: string): AregTextFileState {
487
+ return copyTextFileState(this.files.get(relativePath) ?? { type: "missing" });
488
+ }
489
+
490
+ private currentFindSkills(): AregSkillFindSkillInspection[] {
491
+ return this.explicitFindSkills ?? this.localSkills.map(skillKindSkillToFindSkill);
492
+ }
493
+
494
+ private preflightFailure(relativePath: string): AregErrorInfo | undefined {
495
+ const failure = this.preflightFailures.get(relativePath) ?? this.preflightFailures.get("*");
496
+ return failure === undefined ? undefined : copyErrorInfo(failure);
497
+ }
498
+
499
+ private mutationFailure(relativePath: string): AregErrorInfo | undefined {
500
+ const failure = this.mutationFailures.get(relativePath) ?? this.mutationFailures.get("*");
501
+ return failure === undefined ? undefined : copyErrorInfo(failure);
502
+ }
503
+
504
+ private skillForMirrorName(skillName: string): AregSkillKindSkillInspection | undefined {
505
+ return this.localSkills.find((candidate) => candidate.name === skillName);
506
+ }
507
+
508
+ private deleteSymlinkContractFailure(
509
+ relativePath: string,
510
+ description: string,
511
+ ): AregErrorInfo | undefined {
512
+ const target = `${this.projectDir}/${relativePath}`;
513
+ const mirror = parseSkillMirrorRelativePath(relativePath);
514
+ const skill = mirror === undefined ? undefined : this.skillForMirrorName(mirror.skillName);
515
+ const state =
516
+ mirror === undefined
517
+ ? undefined
518
+ : mirror.mirrorKind === "agents"
519
+ ? skill?.agentsPath
520
+ : skill?.claudePath;
521
+ return classifySkillMirrorSymlinkState(relativePath, state, description, target);
522
+ }
523
+ }
524
+
525
+ export type FakeAregHostOperation = { type: "check-tool"; tool: AregHostToolName; cwd: string };
526
+
527
+ export interface FakeAregHostGatewayOptions {
528
+ tools?: Partial<Record<AregHostToolName, string | null>>;
529
+ }
530
+
531
+ export class FakeAregHostGateway implements AregHostGateway {
532
+ private readonly tools: ReadonlyMap<AregHostToolName, string | null>;
533
+ private readonly log: FakeAregHostOperation[] = [];
534
+
535
+ constructor(options: FakeAregHostGatewayOptions = {}) {
536
+ this.tools = new Map(
537
+ Object.entries(options.tools ?? {}) as Array<[AregHostToolName, string | null]>,
538
+ );
539
+ }
540
+
541
+ async checkTool(options: {
542
+ tool: AregHostToolName;
543
+ cwd: string;
544
+ env: NodeJS.ProcessEnv;
545
+ }): Promise<AregToolCheckResult> {
546
+ this.log.push({ type: "check-tool", tool: options.tool, cwd: options.cwd });
547
+ const path = this.tools.get(options.tool);
548
+ if (path === null)
549
+ return {
550
+ type: "missing",
551
+ tool: options.tool,
552
+ message: `Required host tool is missing: ${options.tool}`,
553
+ };
554
+ return { type: "found", tool: options.tool, path: path ?? `/fake/bin/${options.tool}` };
555
+ }
556
+
557
+ operations(): readonly FakeAregHostOperation[] {
558
+ return this.log.map((operation) => ({ ...operation }));
559
+ }
560
+ }
561
+
562
+ export type FakeAregGithubOperation =
563
+ | {
564
+ type: "list-skill-directory-names";
565
+ repo: string;
566
+ ref?: string;
567
+ }
568
+ | {
569
+ type: "check-skill-file";
570
+ repo: string;
571
+ path: string;
572
+ ref?: string;
573
+ };
574
+
575
+ export interface FakeAregGithubGatewayOptions {
576
+ repos?: Record<string, readonly string[] | "missing" | "auth-error" | AregErrorInfo>;
577
+ files?: Record<string, "found" | "missing" | "auth-error" | AregErrorInfo>;
578
+ }
579
+
580
+ export class FakeAregGithubGateway implements AregGithubGateway {
581
+ private readonly repos: ReadonlyMap<
582
+ string,
583
+ readonly string[] | "missing" | "auth-error" | AregErrorInfo
584
+ >;
585
+ private readonly files: ReadonlyMap<string, "found" | "missing" | "auth-error" | AregErrorInfo>;
586
+ private readonly log: FakeAregGithubOperation[] = [];
587
+
588
+ constructor(options: FakeAregGithubGatewayOptions = {}) {
589
+ this.repos = new Map(
590
+ Object.entries(options.repos ?? {}).map(([repo, value]) => [repo, copyGithubState(value)]),
591
+ );
592
+ this.files = new Map(
593
+ Object.entries(options.files ?? {}).map(([path, value]) => [
594
+ path,
595
+ copyGithubFileState(value),
596
+ ]),
597
+ );
598
+ }
599
+
600
+ async listSkillDirectoryNames(options: {
601
+ repo: string;
602
+ ref?: string;
603
+ env: NodeJS.ProcessEnv;
604
+ }): Promise<AregGithubSkillListResult> {
605
+ this.log.push({
606
+ type: "list-skill-directory-names",
607
+ repo: options.repo,
608
+ ...optionalEntry("ref", options.ref),
609
+ });
610
+ const state = this.repos.get(options.repo);
611
+ if (state === undefined || state === "missing")
612
+ return { type: "missing", message: `Skill source not found: ${options.repo}` };
613
+ if (state === "auth-error")
614
+ return { type: "auth-error", message: `GitHub authentication failed for ${options.repo}` };
615
+ if (isReadonlyStringArray(state)) return { type: "ok", skillNames: [...state] };
616
+ return { type: "error", error: copyErrorInfo(state) };
617
+ }
618
+
619
+ async checkSkillFile(options: {
620
+ repo: string;
621
+ path: string;
622
+ ref?: string;
623
+ env: NodeJS.ProcessEnv;
624
+ }): Promise<AregGithubSkillFileResult> {
625
+ this.log.push({
626
+ type: "check-skill-file",
627
+ repo: options.repo,
628
+ path: options.path,
629
+ ...optionalEntry("ref", options.ref),
630
+ });
631
+ const state = this.files.get(githubFileKey(options.repo, options.path, options.ref));
632
+ if (state === undefined || state === "found") return { type: "found" };
633
+ if (state === "missing")
634
+ return { type: "missing", message: `Skill file not found: ${options.repo}/${options.path}` };
635
+ if (state === "auth-error")
636
+ return { type: "auth-error", message: `GitHub authentication failed for ${options.repo}` };
637
+ return { type: "error", error: copyErrorInfo(state) };
638
+ }
639
+
640
+ operations(): readonly FakeAregGithubOperation[] {
641
+ return this.log.map((operation) => ({ ...operation }));
642
+ }
643
+ }
644
+
645
+ export type FakeAregNpxSkillsOperation = { type: "add-skills" } & Omit<
646
+ AregNpxSkillsAddRequest,
647
+ "env"
648
+ >;
649
+
650
+ export interface FakeAregNpxSkillsGatewayOptions {
651
+ failure?: AregErrorInfo;
652
+ failures?: Readonly<Record<string, AregErrorInfo>>;
653
+ }
654
+
655
+ export class FakeAregNpxSkillsGateway implements AregNpxSkillsGateway {
656
+ private readonly failure: AregErrorInfo | undefined;
657
+ private readonly failures: ReadonlyMap<string, AregErrorInfo>;
658
+ private readonly log: FakeAregNpxSkillsOperation[] = [];
659
+
660
+ constructor(options: FakeAregNpxSkillsGatewayOptions = {}) {
661
+ this.failure = options.failure === undefined ? undefined : copyErrorInfo(options.failure);
662
+ this.failures = new Map(
663
+ Object.entries(options.failures ?? {}).map(([key, value]) => [key, copyErrorInfo(value)]),
664
+ );
665
+ }
666
+
667
+ async addSkills(request: AregNpxSkillsAddRequest): Promise<AregNpxSkillsAddResult> {
668
+ this.log.push({
669
+ type: "add-skills",
670
+ sourceRepo: request.sourceRepo,
671
+ skillNames: [...request.skillNames],
672
+ targetAgents: [...request.targetAgents],
673
+ cwd: request.cwd,
674
+ });
675
+ if (this.failure !== undefined) return { type: "error", error: copyErrorInfo(this.failure) };
676
+ const keyedFailure = this.failures.get(failureKey(request.sourceRepo, request.skillNames));
677
+ if (keyedFailure !== undefined) return { type: "error", error: copyErrorInfo(keyedFailure) };
678
+ return { type: "ok" };
679
+ }
680
+
681
+ operations(): readonly FakeAregNpxSkillsOperation[] {
682
+ return this.log.map((operation) => ({
683
+ ...operation,
684
+ skillNames: [...operation.skillNames],
685
+ targetAgents: [...operation.targetAgents],
686
+ }));
687
+ }
688
+ }
689
+
690
+ export type FakeAregPromptOperation = {
691
+ type: "confirm";
692
+ message: string;
693
+ defaultValue: boolean;
694
+ response: boolean;
695
+ };
696
+
697
+ export interface FakeAregPromptGatewayOptions {
698
+ responses?: readonly boolean[];
699
+ shouldConfirmByDefault?: boolean;
700
+ }
701
+
702
+ export class FakeAregPromptGateway implements AregPromptGateway {
703
+ private readonly responses: boolean[];
704
+ private readonly shouldConfirmByDefault: boolean;
705
+ private readonly log: FakeAregPromptOperation[] = [];
706
+
707
+ constructor(options: FakeAregPromptGatewayOptions = {}) {
708
+ this.responses = [...(options.responses ?? [])];
709
+ this.shouldConfirmByDefault = options.shouldConfirmByDefault ?? false;
710
+ }
711
+
712
+ async confirm(request: { message: string; defaultValue: boolean }): Promise<boolean> {
713
+ const response = this.responses.shift() ?? this.shouldConfirmByDefault;
714
+ this.log.push({
715
+ type: "confirm",
716
+ message: request.message,
717
+ defaultValue: request.defaultValue,
718
+ response,
719
+ });
720
+ return response;
721
+ }
722
+
723
+ operations(): readonly FakeAregPromptOperation[] {
724
+ return this.log.map((operation) => ({ ...operation }));
725
+ }
726
+ }
727
+
728
+ export type FakeAregSkillxOperation =
729
+ | ({ type: "install-into-workspace" } & Omit<AregSkillxInstallRequest, "env">)
730
+ | ({ type: "cleanup-workspace" } & AregSkillxWorkspaceCleanupRequest);
731
+
732
+ export interface FakeAregSkillxWorkspaceGatewayOptions {
733
+ workspaceRoot?: string;
734
+ installedSkills?: readonly AregSkillxInstalledSkill[];
735
+ failure?: AregErrorInfo;
736
+ cleanupFailure?: AregErrorInfo;
737
+ }
738
+
739
+ export class FakeAregSkillxWorkspaceGateway implements AregSkillxWorkspaceGateway {
740
+ private readonly workspaceRoot: string;
741
+ private readonly installedSkills: readonly AregSkillxInstalledSkill[];
742
+ private readonly failure: AregErrorInfo | undefined;
743
+ private readonly cleanupFailure: AregErrorInfo | undefined;
744
+ private readonly log: FakeAregSkillxOperation[] = [];
745
+
746
+ constructor(options: FakeAregSkillxWorkspaceGatewayOptions = {}) {
747
+ this.workspaceRoot = options.workspaceRoot ?? "/tmp/areg-skillx";
748
+ this.installedSkills = (options.installedSkills ?? []).map(copyInstalledSkill);
749
+ this.failure = options.failure === undefined ? undefined : copyErrorInfo(options.failure);
750
+ this.cleanupFailure =
751
+ options.cleanupFailure === undefined ? undefined : copyErrorInfo(options.cleanupFailure);
752
+ }
753
+
754
+ async installIntoWorkspace(request: AregSkillxInstallRequest): Promise<AregSkillxInstallResult> {
755
+ this.log.push({
756
+ type: "install-into-workspace",
757
+ sourceRepo: request.sourceRepo,
758
+ ...optionalEntry("skillName", request.skillName),
759
+ cwd: request.cwd,
760
+ });
761
+ if (this.failure !== undefined) return { type: "error", error: copyErrorInfo(this.failure) };
762
+ return {
763
+ type: "ok",
764
+ workspace: {
765
+ workspaceRoot: this.workspaceRoot,
766
+ installedSkills: this.installedSkills.map(copyInstalledSkill),
767
+ },
768
+ };
769
+ }
770
+
771
+ async cleanupWorkspace(request: AregSkillxWorkspaceCleanupRequest): Promise<AregOperationResult> {
772
+ this.log.push({ type: "cleanup-workspace", workspaceRoot: request.workspaceRoot });
773
+ if (this.cleanupFailure !== undefined) return resultErr(copyErrorInfo(this.cleanupFailure));
774
+ return resultOk(undefined);
775
+ }
776
+
777
+ operations(): readonly FakeAregSkillxOperation[] {
778
+ return this.log.map((operation) => ({ ...operation }));
779
+ }
780
+ }
781
+
782
+ function failureKey(sourceRepo: string, skillNames: readonly string[]): string {
783
+ return `${sourceRepo}:${skillNames.join(",")}`;
784
+ }
785
+
786
+ function copyProjectOperation(operation: FakeAregProjectOperation): FakeAregProjectOperation {
787
+ switch (operation.type) {
788
+ case "write-text-file":
789
+ return { ...operation };
790
+ case "delete-file":
791
+ return { ...operation };
792
+ case "remove-empty-dir":
793
+ return { ...operation };
794
+ default:
795
+ return { ...operation };
796
+ }
797
+ }
798
+
799
+ function copyFakeCheckSkill(skill: FakeAregCheckSkillOptions): AregCheckSkillInspection {
800
+ return {
801
+ name: skill.name,
802
+ skillsPath: copyPathState(skill.skillsPath ?? { type: "missing" }),
803
+ agentsPath: copyPathState(skill.agentsPath ?? { type: "missing" }),
804
+ claudePath: copyPathState(skill.claudePath ?? { type: "missing" }),
805
+ localSkillMd: normalizeTextFileState(skill.localSkillMd ?? { type: "missing" }),
806
+ remoteSkillMd: normalizeTextFileState(skill.remoteSkillMd ?? { type: "missing" }),
807
+ openaiPolicy: normalizeTextFileState(skill.openaiPolicy ?? { type: "missing" }),
808
+ };
809
+ }
810
+
811
+ function copyFakeSkillFindSkill(
812
+ skill: FakeAregSkillFindSkillOptions,
813
+ ): AregSkillFindSkillInspection {
814
+ const root = skill.root ?? skillLookupDescriptorForSourceType(skill.sourceType ?? "repo").root;
815
+ const sourceType = skill.sourceType ?? skillLookupDescriptorForRoot(root).sourceType;
816
+ return {
817
+ name: skill.name,
818
+ root,
819
+ sourceType,
820
+ baseRelativePath: skill.baseRelativePath ?? skillLookupBaseRelativePath(root, skill.name),
821
+ skillDir: copyPathState(skill.skillDir ?? { type: "directory" }),
822
+ skillMd: normalizeTextFileState(
823
+ skill.skillMd ?? `---\nname: ${skill.name}\ndescription: ${skill.name}\n---\n`,
824
+ ),
825
+ };
826
+ }
827
+
828
+ function copyFakeSkillKindSkill(
829
+ skill: FakeAregSkillKindSkillOptions,
830
+ ): AregSkillKindSkillInspection {
831
+ const sourceType = skill.sourceType ?? "repo";
832
+ const descriptor = skillLookupDescriptorForSourceType(sourceType);
833
+ return {
834
+ name: skill.name,
835
+ sourceType,
836
+ baseRelativePath:
837
+ skill.baseRelativePath ?? skillLookupBaseRelativePath(descriptor.root, skill.name),
838
+ skillDir: copyPathState(skill.skillDir ?? { type: "directory" }),
839
+ skillMd: normalizeTextFileState(
840
+ skill.skillMd ?? `---\nname: ${skill.name}\ndescription: ${skill.name}\n---\n`,
841
+ ),
842
+ openaiPolicy: normalizeTextFileState(skill.openaiPolicy ?? { type: "missing" }),
843
+ agentsPath: copyPathState(skill.agentsPath ?? { type: "missing" }),
844
+ claudePath: copyPathState(skill.claudePath ?? { type: "missing" }),
845
+ };
846
+ }
847
+
848
+ function copyCheckSkill(skill: AregCheckSkillInspection): AregCheckSkillInspection {
849
+ return {
850
+ name: skill.name,
851
+ skillsPath: copyPathState(skill.skillsPath),
852
+ agentsPath: copyPathState(skill.agentsPath),
853
+ claudePath: copyPathState(skill.claudePath),
854
+ localSkillMd: copyTextFileState(skill.localSkillMd),
855
+ remoteSkillMd: copyTextFileState(skill.remoteSkillMd),
856
+ openaiPolicy: copyTextFileState(skill.openaiPolicy),
857
+ };
858
+ }
859
+
860
+ interface SkillInspectionCore {
861
+ name: string;
862
+ baseRelativePath: string;
863
+ skillDir: AregPathState;
864
+ skillMd: AregTextFileState;
865
+ }
866
+
867
+ function copySkillInspectionCore(skill: SkillInspectionCore): SkillInspectionCore {
868
+ return {
869
+ name: skill.name,
870
+ baseRelativePath: skill.baseRelativePath,
871
+ skillDir: copyPathState(skill.skillDir),
872
+ skillMd: copyTextFileState(skill.skillMd),
873
+ };
874
+ }
875
+
876
+ function copySkillKindSkill(skill: AregSkillKindSkillInspection): AregSkillKindSkillInspection {
877
+ return {
878
+ ...copySkillInspectionCore(skill),
879
+ sourceType: skill.sourceType,
880
+ openaiPolicy: copyTextFileState(skill.openaiPolicy),
881
+ agentsPath: copyPathState(skill.agentsPath),
882
+ claudePath: copyPathState(skill.claudePath),
883
+ };
884
+ }
885
+
886
+ function copySkillFindSkill(skill: AregSkillFindSkillInspection): AregSkillFindSkillInspection {
887
+ return {
888
+ ...copySkillInspectionCore(skill),
889
+ root: skill.root,
890
+ sourceType: skill.sourceType,
891
+ };
892
+ }
893
+
894
+ function skillKindSkillToFindSkill(
895
+ skill: AregSkillKindSkillInspection,
896
+ ): AregSkillFindSkillInspection {
897
+ const descriptor = skillLookupDescriptorForSourceType(skill.sourceType);
898
+ return {
899
+ ...copySkillInspectionCore(skill),
900
+ root: descriptor.root,
901
+ sourceType: descriptor.sourceType,
902
+ };
903
+ }
904
+
905
+ function missingSkillKindSkill(name: string): AregSkillKindSkillInspection {
906
+ const missing = { type: "missing" as const };
907
+ return {
908
+ name,
909
+ sourceType: "repo",
910
+ baseRelativePath: skillLookupBaseRelativePath("skills", name),
911
+ skillDir: missing,
912
+ skillMd: missing,
913
+ openaiPolicy: missing,
914
+ agentsPath: missing,
915
+ claudePath: missing,
916
+ };
917
+ }
918
+
919
+ function fakeResolveSkillName(spec: string): string {
920
+ const normalized = spec.replaceAll("\\", "/");
921
+ const withoutSkillMd = normalized.endsWith("/SKILL.md")
922
+ ? normalized.slice(0, -"/SKILL.md".length)
923
+ : normalized;
924
+ const parts = withoutSkillMd.split("/").filter((part) => part.length > 0);
925
+ const skillsIndex = parts.lastIndexOf("skills");
926
+ const skillPart = skillsIndex === -1 ? undefined : parts[skillsIndex + 1];
927
+ if (skillPart !== undefined) return skillPart;
928
+ return parts.at(-1) ?? spec;
929
+ }
930
+
931
+ function skillForRelativePath(
932
+ skills: readonly AregSkillKindSkillInspection[],
933
+ relativePath: string,
934
+ ): AregSkillKindSkillInspection | undefined {
935
+ return skills.find((skill) => relativePath.startsWith(`${skill.baseRelativePath}/`));
936
+ }
937
+
938
+ function normalizeTextFileState(value: AregTextFileState | object | string): AregTextFileState {
939
+ if (typeof value === "string") return { type: "file", text: value };
940
+ if ("type" in value) return copyTextFileState(value as AregTextFileState);
941
+ return { type: "file", text: `${JSON.stringify(value, null, 2)}\n` };
942
+ }
943
+
944
+ function copyTextFileState(state: AregTextFileState): AregTextFileState {
945
+ return { ...state };
946
+ }
947
+
948
+ function copyPiSkillInventory(
949
+ inventory: Partial<AregPiSkillInventoryInspection>,
950
+ ): AregPiSkillInventoryInspection {
951
+ return {
952
+ skillNames: [...(inventory.skillNames ?? [])],
953
+ isApproximation: inventory.isApproximation ?? true,
954
+ source: inventory.source ?? "fake-pi-skill-inventory",
955
+ };
956
+ }
957
+
958
+ function copyPathState(state: AregPathState): AregPathState {
959
+ return { ...state };
960
+ }
961
+
962
+ function copyPairingDirectory(directory: AregCheckPairingDirectory): AregCheckPairingDirectory {
963
+ return {
964
+ relativeDir: directory.relativeDir,
965
+ hasAgents: directory.hasAgents,
966
+ hasClaude: directory.hasClaude,
967
+ ...optionalEntry("claudeText", directory.claudeText),
968
+ };
969
+ }
970
+
971
+ function copyGithubState(
972
+ value: readonly string[] | "missing" | "auth-error" | AregErrorInfo,
973
+ ): readonly string[] | "missing" | "auth-error" | AregErrorInfo {
974
+ if (isReadonlyStringArray(value)) return [...value];
975
+ if (value === "missing" || value === "auth-error") return value;
976
+ return copyErrorInfo(value);
977
+ }
978
+
979
+ function copyGithubFileState(
980
+ value: "found" | "missing" | "auth-error" | AregErrorInfo,
981
+ ): "found" | "missing" | "auth-error" | AregErrorInfo {
982
+ if (value === "found" || value === "missing" || value === "auth-error") return value;
983
+ return copyErrorInfo(value);
984
+ }
985
+
986
+ function githubFileKey(repo: string, path: string, ref: string | undefined): string {
987
+ return ref === undefined ? `${repo}:${path}` : `${repo}:${path}@${ref}`;
988
+ }
989
+
990
+ function isReadonlyStringArray(
991
+ value: readonly string[] | "missing" | "auth-error" | AregErrorInfo,
992
+ ): value is readonly string[] {
993
+ return Array.isArray(value);
994
+ }
995
+
996
+ function copyInstalledSkill(skill: AregSkillxInstalledSkill): AregSkillxInstalledSkill {
997
+ return {
998
+ name: skill.name,
999
+ directory: skill.directory,
1000
+ skillFile: skill.skillFile,
1001
+ relativeFiles: [...skill.relativeFiles],
1002
+ };
1003
+ }
1004
+
1005
+ function copyErrorInfo(error: AregErrorInfo): AregErrorInfo {
1006
+ return error.displayCommand === undefined
1007
+ ? { code: error.code, message: error.message }
1008
+ : { code: error.code, message: error.message, displayCommand: error.displayCommand };
1009
+ }