@nseng-ai/areg 0.1.1 → 0.1.2

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 (43) hide show
  1. package/package.json +7 -6
  2. package/src/cli.ts +0 -33
  3. package/src/context.ts +3 -29
  4. package/src/fake-gateways.ts +108 -380
  5. package/src/gateways/errors.ts +2 -2
  6. package/src/gateways/fs-utils.ts +2 -2
  7. package/src/gateways/project-fs.ts +29 -41
  8. package/src/gateways/project-gateway.ts +98 -31
  9. package/src/gateways.ts +34 -150
  10. package/src/index.ts +2 -28
  11. package/src/operations/check.ts +33 -20
  12. package/src/operations/doctor-skills.ts +71 -7
  13. package/src/operations/file-state.ts +4 -4
  14. package/src/operations/manifest-source-findings.ts +45 -0
  15. package/src/operations/manifest-sources.ts +119 -0
  16. package/src/operations/pi-settings.ts +2 -5
  17. package/src/operations/project-inspection.ts +32 -28
  18. package/src/operations/project-mutations.ts +5 -21
  19. package/src/operations/project-resolution.ts +2 -2
  20. package/src/operations/skill-find.ts +23 -3
  21. package/src/operations/skill-kind-apply-plan.ts +2 -2
  22. package/src/operations/skill-kind-frontmatter.ts +1 -1
  23. package/src/operations/skill-kind-inference.ts +30 -14
  24. package/src/operations/skill-kind.ts +34 -12
  25. package/src/sort.ts +3 -3
  26. package/src/gateways/command-constants.ts +0 -1
  27. package/src/gateways/github-gateway.ts +0 -111
  28. package/src/gateways/host-gateway.ts +0 -35
  29. package/src/gateways/mutation-policy.ts +0 -94
  30. package/src/gateways/npx-skills-gateway.ts +0 -53
  31. package/src/gateways/prompt-gateway.ts +0 -21
  32. package/src/gateways/skillx-workspace-gateway.ts +0 -220
  33. package/src/operations/frontmatter.ts +0 -151
  34. package/src/operations/init.ts +0 -548
  35. package/src/operations/lockfile.ts +0 -107
  36. package/src/operations/managed-markdown-block.ts +0 -47
  37. package/src/operations/project-agents.ts +0 -115
  38. package/src/operations/skill-mirror-conventions.ts +0 -126
  39. package/src/operations/skillx.ts +0 -305
  40. package/src/operations/toml-section.ts +0 -53
  41. package/src/operations/update-skills.ts +0 -325
  42. package/src/real-gateways.ts +0 -6
  43. package/src/skill-lookup.ts +0 -254
@@ -11,6 +11,7 @@ import { renderTextTable, type TextTableColumn } from "@nseng-ai/foundation/text
11
11
  import { z } from "zod";
12
12
 
13
13
  import type { AregCliContext } from "../context.ts";
14
+ import { aregManifestSkillSourceViewSchema } from "./manifest-sources.ts";
14
15
  import { isPathStateError } from "./file-state.ts";
15
16
  import { inspectSkillKindProject } from "./project-inspection.ts";
16
17
  import { inspectResolvedProjectGitRoot } from "./project-resolution.ts";
@@ -73,6 +74,8 @@ const skillKindReplacementSchema = z.object({
73
74
  advice: z.string().optional(),
74
75
  });
75
76
 
77
+ const skillKindManifestSourceSchema = aregManifestSkillSourceViewSchema;
78
+
76
79
  const skillKindRecordSchema = z.object({
77
80
  skill: z.string(),
78
81
  kind: z.enum(INFERRED_SKILL_INVOCATION_KINDS),
@@ -81,6 +84,7 @@ const skillKindRecordSchema = z.object({
81
84
  piExtension: z.enum(PI_EXTENSION_STATUSES),
82
85
  artifacts: skillKindArtifactFactsSchema,
83
86
  replacement: skillKindReplacementSchema,
87
+ manifestSources: z.array(skillKindManifestSourceSchema),
84
88
  notes: z.array(z.string()),
85
89
  });
86
90
 
@@ -127,7 +131,7 @@ export const skillKindApplyRequestSchema = z.object({
127
131
  .default(".")
128
132
  .describe("Project directory or subdirectory to mutate (default: current directory)."),
129
133
  dryRun: z.boolean().default(false).describe("Show planned edits without writing files."),
130
- yes: z.boolean().default(false).describe("Approve deletion prompts for managed artifacts."),
134
+ yes: z.boolean().default(false).describe("Approve deletion prompts for harness overlays."),
131
135
  kind: z.enum(SKILL_INVOCATION_KINDS).describe("Desired skill invocation kind."),
132
136
  skills: z.array(z.string()).min(1).describe("Installed skill names or path-like skill specs."),
133
137
  });
@@ -193,7 +197,7 @@ export function buildSkillGroup(): ClinkrGroup<AregCliContext> {
193
197
  skillGroup.command({
194
198
  name: "apply",
195
199
  description:
196
- "Apply the managed artifacts for a skill invocation kind. This reconciles managed artifacts to the requested kind. The unlisted kind additionally deletes the .agents/skills and .claude/skills mirror symlinks so the skill disappears from every harness typeahead. It is not a historical undo system; use git to roll back exact previous file contents.",
200
+ "Apply the harness overlays for a skill invocation kind. This reconciles harness overlays to the requested kind. The unlisted kind additionally deletes the .agents/skills and .claude/skills mirror symlinks so the skill disappears from every harness typeahead. It is not a historical undo system; use git to roll back exact previous file contents.",
197
201
  schema: skillKindApplyRequestSchema,
198
202
  positionals: { kind: { position: 0 }, skills: { position: 1 } },
199
203
  options: { yes: { short: "-y" } },
@@ -314,7 +318,6 @@ export async function runSkillKindApply(
314
318
  const applyResult = await applyProjectMutationPlan({
315
319
  ctx,
316
320
  projectDir,
317
- policy: "skill-kind",
318
321
  writes: plans.flatMap(plannedWrites),
319
322
  deletes: plans.flatMap(plannedDeletes),
320
323
  deleteSymlinks: plans.flatMap(plannedDeleteSymlinks),
@@ -363,7 +366,8 @@ export function renderSkillKindList(
363
366
  caps: RenderCapabilities = { canEmitAnsi: false },
364
367
  ): string {
365
368
  if (result.skills.length === 0) return "No managed skills found.";
366
- const includeNotes = result.skills.some((record) => record.notes.length > 0);
369
+ const hasSources = result.skills.some((record) => record.manifestSources.length > 0);
370
+ const shouldIncludeNotes = result.skills.some((record) => record.notes.length > 0);
367
371
  const columns: TextTableColumn[] = [
368
372
  { header: "SKILL", style: "bold-cyan" },
369
373
  { header: "KIND" },
@@ -371,7 +375,8 @@ export function renderSkillKindList(
371
375
  { header: "NATIVE" },
372
376
  { header: "PI" },
373
377
  ];
374
- if (includeNotes) columns.push({ header: "NOTES", style: "dim" });
378
+ if (hasSources) columns.push({ header: "SOURCES" });
379
+ if (shouldIncludeNotes) columns.push({ header: "NOTES", style: "dim" });
375
380
  return renderTextTable({
376
381
  columns,
377
382
  rows: result.skills.map((record) => {
@@ -382,7 +387,8 @@ export function renderSkillKindList(
382
387
  record.nativeDirect,
383
388
  record.piExtension,
384
389
  ];
385
- if (includeNotes) base.push(record.notes.join("; "));
390
+ if (hasSources) base.push(skillKindSourcesLabel(record));
391
+ if (shouldIncludeNotes) base.push(record.notes.join("; "));
386
392
  return base;
387
393
  }),
388
394
  canEmitAnsi: caps.canEmitAnsi,
@@ -408,6 +414,14 @@ export function renderSkillKindShow(result: SkillKindShowResult): string {
408
414
  `- .claude/skills mirror: ${presence(record.artifacts.claudeMirror)}`,
409
415
  `- Pi replacement: ${record.replacement.label}`,
410
416
  ];
417
+ if (record.manifestSources.length > 0) {
418
+ lines.push("Manifest sources:");
419
+ for (const source of record.manifestSources) {
420
+ lines.push(
421
+ `- ${source.harness} ${source.manifestKey}: ${source.packageName}@${source.version} -> ${source.targetSkillRelativePath}`,
422
+ );
423
+ }
424
+ }
411
425
  if (record.notes.length > 0) {
412
426
  lines.push("Notes:");
413
427
  for (const note of record.notes) lines.push(`- ${note}`);
@@ -472,26 +486,33 @@ function toSkillKindRecordResult(record: SkillKindRecord): SkillKindRecordResult
472
486
  evidence: record.replacement.evidence,
473
487
  advice: record.replacement.advice,
474
488
  },
489
+ manifestSources: record.manifestSources.map((source) => ({ ...source })),
475
490
  notes: [...record.notes],
476
491
  };
477
492
  }
478
493
 
494
+ function skillKindSourcesLabel(record: SkillKindRecordResult): string {
495
+ if (record.manifestSources.length === 0) return "";
496
+ const labels = new Set(record.manifestSources.map((source) => source.harness));
497
+ return ["manifest", ...labels].join(":");
498
+ }
499
+
479
500
  function renderApplyOperation(
480
501
  operation: SkillKindApplyResult["skills"][number]["operations"][number],
481
- dryRun: boolean,
502
+ isDryRun: boolean,
482
503
  ): string | undefined {
483
504
  if (!("isApplied" in operation)) return undefined;
484
505
  switch (operation.type) {
485
506
  case "write":
486
- return `${dryRun ? "Would write" : "Wrote"} ${operation.path}`;
507
+ return `${isDryRun ? "Would write" : "Wrote"} ${operation.path}`;
487
508
  case "skip":
488
- return `${dryRun ? "Would skip" : "Skipped"} ${operation.path}: ${operation.reason ?? "already current"}`;
509
+ return `${isDryRun ? "Would skip" : "Skipped"} ${operation.path}: ${operation.reason ?? "already current"}`;
489
510
  case "delete":
490
- return `${dryRun ? "Would delete" : "Deleted"} ${operation.path}`;
511
+ return `${isDryRun ? "Would delete" : "Deleted"} ${operation.path}`;
491
512
  case "delete-symlink":
492
- return `${dryRun ? "Would delete symlink" : "Deleted symlink"} ${operation.path}`;
513
+ return `${isDryRun ? "Would delete symlink" : "Deleted symlink"} ${operation.path}`;
493
514
  case "remove-empty-dir":
494
- if (dryRun) return `Would remove ${operation.path} if empty`;
515
+ if (isDryRun) return `Would remove ${operation.path} if empty`;
495
516
  return operation.isApplied ? `Removed ${operation.path}` : undefined;
496
517
  }
497
518
  }
@@ -519,6 +540,7 @@ function emptyShowResult(projectDir: string, skillName: string): SkillKindShowRe
519
540
  claudeMirror: false,
520
541
  },
521
542
  replacement: { verified: false, label: "replacement-missing" },
543
+ manifestSources: [],
522
544
  notes: [],
523
545
  },
524
546
  };
package/src/sort.ts CHANGED
@@ -1,7 +1,7 @@
1
- export function sortStrings(values: readonly string[]): string[] {
2
- return [...values].sort();
1
+ export function sortStringsLocaleAware(values: Iterable<string>): string[] {
2
+ return [...values].sort((left, right) => left.localeCompare(right));
3
3
  }
4
4
 
5
5
  export function uniqueSortedStrings(values: readonly string[]): string[] {
6
- return sortStrings([...new Set(values)]);
6
+ return sortStringsLocaleAware(new Set(values));
7
7
  }
@@ -1 +0,0 @@
1
- export const COMMAND_TIMEOUT_MS = 60_000;
@@ -1,111 +0,0 @@
1
- import {
2
- formatCommand,
3
- formatCommandResultFailure,
4
- runCommand,
5
- stripTerminalEscapes,
6
- type CommandRunner,
7
- } from "@nseng-ai/foundation/exec";
8
-
9
- import type {
10
- AregGithubGateway,
11
- AregGithubSkillFileResult,
12
- AregGithubSkillListResult,
13
- } from "../gateways.ts";
14
- import { COMMAND_TIMEOUT_MS } from "./command-constants.ts";
15
- import { errorInfo } from "./errors.ts";
16
-
17
- export class RealAregGithubGateway implements AregGithubGateway {
18
- private readonly runner: CommandRunner;
19
-
20
- constructor(options: { runner?: CommandRunner } = {}) {
21
- this.runner = options.runner ?? runCommand;
22
- }
23
-
24
- async listSkillDirectoryNames(options: {
25
- repo: string;
26
- ref?: string;
27
- env: NodeJS.ProcessEnv;
28
- }): Promise<AregGithubSkillListResult> {
29
- const resource = githubContentsResource(options.repo, "skills", options.ref);
30
- const args = ["api", resource, "--jq", ".[].name"];
31
- const displayCommand = formatCommand("gh", args);
32
- const result = await this.runner("gh", args, { env: options.env, timeout: COMMAND_TIMEOUT_MS });
33
- if (result.code === 0) {
34
- return {
35
- type: "ok",
36
- skillNames: result.stdout
37
- .split("\n")
38
- .map((line) => line.trim())
39
- .filter((line) => line.length > 0),
40
- };
41
- }
42
- return classifyGithubFailure({
43
- result,
44
- args,
45
- displayCommand,
46
- missingMessage: `No skills directory found in ${options.repo}`,
47
- authMessage: `Authentication error accessing ${options.repo}`,
48
- });
49
- }
50
-
51
- async checkSkillFile(options: {
52
- repo: string;
53
- path: string;
54
- ref?: string;
55
- env: NodeJS.ProcessEnv;
56
- }): Promise<AregGithubSkillFileResult> {
57
- const resource = githubContentsResource(options.repo, options.path, options.ref);
58
- const args = ["api", resource, "--jq", ".type"];
59
- const displayCommand = formatCommand("gh", args);
60
- const result = await this.runner("gh", args, { env: options.env, timeout: COMMAND_TIMEOUT_MS });
61
- if (result.code === 0) return { type: "found" };
62
- return classifyGithubFailure({
63
- result,
64
- args,
65
- displayCommand,
66
- missingMessage: `Skill file not found in ${options.repo}: ${options.path}`,
67
- authMessage: `Authentication error accessing ${options.repo}`,
68
- });
69
- }
70
- }
71
-
72
- interface GithubFailureInput {
73
- result: Awaited<ReturnType<CommandRunner>>;
74
- args: readonly string[];
75
- displayCommand: string;
76
- missingMessage: string;
77
- authMessage: string;
78
- }
79
-
80
- function githubContentsResource(repo: string, path: string, ref: string | undefined): string {
81
- const normalizedPath = path
82
- .split("/")
83
- .filter((part) => part.length > 0)
84
- .map(encodeURIComponent)
85
- .join("/");
86
- const resource = `repos/${repo}/contents/${normalizedPath}`;
87
- if (ref === undefined) return resource;
88
- return `${resource}?ref=${encodeURIComponent(ref)}`;
89
- }
90
-
91
- function classifyGithubFailure(
92
- input: GithubFailureInput,
93
- ):
94
- | { type: "missing"; message: string }
95
- | { type: "auth-error"; message: string }
96
- | { type: "error"; error: ReturnType<typeof errorInfo> } {
97
- const combined = stripTerminalEscapes(
98
- `${input.result.stdout}\n${input.result.stderr}`,
99
- ).toLowerCase();
100
- if (combined.includes("404")) return { type: "missing", message: input.missingMessage };
101
- if (combined.includes("401") || combined.includes("403"))
102
- return { type: "auth-error", message: input.authMessage };
103
- return {
104
- type: "error",
105
- error: errorInfo(
106
- input.result.startupError === undefined ? "gh-failed" : "gh-startup-failed",
107
- formatCommandResultFailure("gh api failed", "gh", input.args, input.result),
108
- input.displayCommand,
109
- ),
110
- };
111
- }
@@ -1,35 +0,0 @@
1
- import { constants } from "node:fs";
2
- import { access } from "node:fs/promises";
3
- import path from "node:path";
4
-
5
- import type { AregHostGateway, AregHostToolName, AregToolCheckResult } from "../gateways.ts";
6
-
7
- export class RealAregHostGateway implements AregHostGateway {
8
- async checkTool(options: {
9
- tool: AregHostToolName;
10
- cwd: string;
11
- env: NodeJS.ProcessEnv;
12
- }): Promise<AregToolCheckResult> {
13
- const pathValue = options.env.PATH ?? "";
14
- for (const directory of pathValue.split(path.delimiter)) {
15
- if (directory.length === 0) continue;
16
- const candidate = path.join(directory, options.tool);
17
- if (await isExecutable(candidate))
18
- return { type: "found", tool: options.tool, path: candidate };
19
- }
20
- return {
21
- type: "missing",
22
- tool: options.tool,
23
- message: `Required host tool is missing: ${options.tool}`,
24
- };
25
- }
26
- }
27
-
28
- async function isExecutable(candidate: string): Promise<boolean> {
29
- try {
30
- await access(candidate, constants.X_OK);
31
- return true;
32
- } catch {
33
- return false;
34
- }
35
- }
@@ -1,94 +0,0 @@
1
- import type { AregProjectMutationPolicy } from "../gateways.ts";
2
-
3
- interface AregProjectMutationPolicyDescriptor {
4
- policy: AregProjectMutationPolicy;
5
- isAllowedRelativePath(relativePath: string): boolean;
6
- refusedTargetCode: string;
7
- shouldCheckUnsupportedFirst: boolean;
8
- unsupportedMessage(relativePath: string, description: string): string;
9
- unsafeMessage(relativePath: string, description: string): string;
10
- outsideMessage(relativePath: string, description: string): string;
11
- symlinkCode: string;
12
- notFileCode: string;
13
- parentSymlinkCode: string;
14
- parentNotDirectoryCode: string;
15
- parentMissingCode: string;
16
- parentCreateFailedCode: string;
17
- writeFailedCode: string;
18
- }
19
-
20
- const INIT_MUTATION_POLICY = {
21
- policy: "init",
22
- isAllowedRelativePath: isAllowedInitRelativePath,
23
- refusedTargetCode: "init-write-target-refused",
24
- shouldCheckUnsupportedFirst: true,
25
- unsupportedMessage: (relativePath) =>
26
- `Refusing to write unsupported init target: ${relativePath}`,
27
- unsafeMessage: (relativePath) => `Refusing to write unsafe init target: ${relativePath}`,
28
- outsideMessage: (relativePath) => `Refusing to write outside project root: ${relativePath}`,
29
- symlinkCode: "init-symlink",
30
- notFileCode: "init-not-file",
31
- parentSymlinkCode: "init-parent-symlink",
32
- parentNotDirectoryCode: "init-parent-not-directory",
33
- parentMissingCode: "init-parent-missing",
34
- parentCreateFailedCode: "init-parent-create-failed",
35
- writeFailedCode: "init-write-failed",
36
- } satisfies AregProjectMutationPolicyDescriptor;
37
-
38
- const SKILL_KIND_MUTATION_POLICY = {
39
- policy: "skill-kind",
40
- isAllowedRelativePath: isAllowedSkillKindRelativePath,
41
- refusedTargetCode: "skill-kind-target-refused",
42
- shouldCheckUnsupportedFirst: false,
43
- unsupportedMessage: (candidate, description) =>
44
- `Refusing to manage unsupported ${description} target: ${candidate}`,
45
- unsafeMessage: (candidate, description) =>
46
- `Refusing to manage unsafe ${description} target: ${candidate}`,
47
- outsideMessage: (candidate, description) =>
48
- `Refusing to manage ${description} outside project root: ${candidate}`,
49
- symlinkCode: "skill-kind-symlink",
50
- notFileCode: "skill-kind-not-file",
51
- parentSymlinkCode: "skill-kind-parent-symlink",
52
- parentNotDirectoryCode: "skill-kind-parent-not-directory",
53
- parentMissingCode: "skill-kind-parent-missing",
54
- parentCreateFailedCode: "skill-kind-parent-create-failed",
55
- writeFailedCode: "skill-kind-write-failed",
56
- } satisfies AregProjectMutationPolicyDescriptor;
57
-
58
- export function getAregProjectMutationPolicyDescriptor(
59
- policy: AregProjectMutationPolicy,
60
- ): AregProjectMutationPolicyDescriptor {
61
- switch (policy) {
62
- case "init":
63
- return INIT_MUTATION_POLICY;
64
- case "skill-kind":
65
- return SKILL_KIND_MUTATION_POLICY;
66
- }
67
- }
68
-
69
- function isAllowedInitRelativePath(relativePath: string): boolean {
70
- return ["ns.toml", "AGENTS.md", "CLAUDE.md", ".claude/settings.local.json"].includes(
71
- relativePath,
72
- );
73
- }
74
-
75
- function isAllowedSkillKindRelativePath(relativePath: string): boolean {
76
- if (relativePath === ".pi/settings.json") return true;
77
- const parts = relativePath.split("/");
78
- return (
79
- isAllowedSkillKindRelativePathParts(parts, 1) || isAllowedSkillKindRelativePathParts(parts, 2)
80
- );
81
- }
82
-
83
- function isAllowedSkillKindRelativePathParts(parts: readonly string[], rootLength: 1 | 2): boolean {
84
- const isLocalRoot = rootLength === 1 && parts[0] === "skills";
85
- const isVendoredRoot = rootLength === 2 && parts[0] === ".agents" && parts[1] === "skills";
86
- if (!isLocalRoot && !isVendoredRoot) return false;
87
- return (
88
- (parts.length === rootLength + 2 && parts[rootLength + 1] === "SKILL.md") ||
89
- (parts.length === rootLength + 3 &&
90
- parts[rootLength + 1] === "agents" &&
91
- parts[rootLength + 2] === "openai.yaml") ||
92
- (parts.length === rootLength + 2 && parts[rootLength + 1] === "agents")
93
- );
94
- }
@@ -1,53 +0,0 @@
1
- import {
2
- formatCommand,
3
- formatCommandResultFailure,
4
- runCommand,
5
- type CommandRunner,
6
- } from "@nseng-ai/foundation/exec";
7
-
8
- import type {
9
- AregNpxSkillsAddRequest,
10
- AregNpxSkillsAddResult,
11
- AregNpxSkillsGateway,
12
- } from "../gateways.ts";
13
- import { COMMAND_TIMEOUT_MS } from "./command-constants.ts";
14
- import { errorInfo } from "./errors.ts";
15
-
16
- export class RealAregNpxSkillsGateway implements AregNpxSkillsGateway {
17
- private readonly runner: CommandRunner;
18
-
19
- constructor(options: { runner?: CommandRunner } = {}) {
20
- this.runner = options.runner ?? runCommand;
21
- }
22
-
23
- async addSkills(request: AregNpxSkillsAddRequest): Promise<AregNpxSkillsAddResult> {
24
- const args = buildNpxSkillsAddArgs(request);
25
- const displayCommand = formatCommand("npx", args);
26
- const result = await this.runner("npx", args, {
27
- cwd: request.cwd,
28
- env: request.env,
29
- timeout: COMMAND_TIMEOUT_MS,
30
- });
31
- if (result.code === 0) return { type: "ok" };
32
- return {
33
- type: "error",
34
- error: errorInfo(
35
- result.startupError === undefined ? "npx-failed" : "npx-startup-failed",
36
- formatCommandResultFailure("npx skills add failed", "npx", args, result),
37
- displayCommand,
38
- ),
39
- };
40
- }
41
- }
42
-
43
- export function buildNpxSkillsAddArgs(request: AregNpxSkillsAddRequest): string[] {
44
- const args = ["skills", "add", request.sourceRepo];
45
- for (const skillName of request.skillNames) {
46
- args.push("--skill", skillName);
47
- }
48
- for (const agent of request.targetAgents) {
49
- args.push("--agent", agent);
50
- }
51
- args.push("-y");
52
- return args;
53
- }
@@ -1,21 +0,0 @@
1
- import readline from "node:readline/promises";
2
-
3
- import type { AregPromptGateway } from "../gateways.ts";
4
-
5
- export class RealAregPromptGateway implements AregPromptGateway {
6
- async confirm(request: { message: string; defaultValue: boolean }): Promise<boolean> {
7
- const suffix = request.defaultValue ? " [Y/n] " : " [y/N] ";
8
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
9
- try {
10
- while (true) {
11
- const answer = (await rl.question(`${request.message}${suffix}`)).trim().toLowerCase();
12
- if (answer.length === 0) return request.defaultValue;
13
- if (answer === "y" || answer === "yes") return true;
14
- if (answer === "n" || answer === "no") return false;
15
- process.stdout.write("Please answer yes or no.\n");
16
- }
17
- } finally {
18
- rl.close();
19
- }
20
- }
21
- }