@aiready/cli 0.13.1 → 0.13.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.
- package/.turbo/turbo-build.log +8 -8
- package/dist/cli.js +48 -4
- package/dist/cli.mjs +49 -4
- package/package.json +8 -8
- package/src/cli.ts +2 -1
- package/src/commands/scan.ts +61 -2
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @aiready/cli@0.13.
|
|
3
|
+
> @aiready/cli@0.13.2 build /Users/pengcao/projects/aiready/packages/cli
|
|
4
4
|
> tsup src/index.ts src/cli.ts --format cjs,esm
|
|
5
5
|
|
|
6
6
|
[34mCLI[39m Building entry: src/cli.ts, src/index.ts
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
[34mCJS[39m Build start
|
|
11
11
|
[34mESM[39m Build start
|
|
12
12
|
|
|
13
|
-
[43m[30m WARN [39m[49m [33m▲ [43;33m[[43;30mWARNING[43;33m][0m [1m"import.meta" is not available with the "cjs" output format and will be empty[0m [empty-import-meta] [90m7:
|
|
13
|
+
[43m[30m WARN [39m[49m [33m▲ [43;33m[[43;30mWARNING[43;33m][0m [1m"import.meta" is not available with the "cjs" output format and will be empty[0m [empty-import-meta] [90m7:51:15 pm[39m
|
|
14
14
|
|
|
15
15
|
src/cli.ts:32:31:
|
|
16
16
|
[37m 32 │ return dirname(fileURLToPath([32mimport.meta[37m.url));
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
[32mCJS[39m [1mdist/cli.js [22m[32m80.29 KB[39m
|
|
24
|
-
[32mCJS[39m [1mdist/index.js [22m[32m10.62 KB[39m
|
|
25
|
-
[32mCJS[39m ⚡️ Build success in 77ms
|
|
26
|
-
[32mESM[39m [1mdist/cli.mjs [22m[32m66.94 KB[39m
|
|
27
|
-
[32mESM[39m [1mdist/chunk-VOKP7FGM.mjs [22m[32m9.52 KB[39m
|
|
28
23
|
[32mESM[39m [1mdist/index.mjs [22m[32m170.00 B[39m
|
|
29
|
-
[32mESM[39m
|
|
24
|
+
[32mESM[39m [1mdist/chunk-VOKP7FGM.mjs [22m[32m9.52 KB[39m
|
|
25
|
+
[32mESM[39m [1mdist/cli.mjs [22m[32m68.68 KB[39m
|
|
26
|
+
[32mESM[39m ⚡️ Build success in 30ms
|
|
27
|
+
[32mCJS[39m [1mdist/index.js [22m[32m10.62 KB[39m
|
|
28
|
+
[32mCJS[39m [1mdist/cli.js [22m[32m82.21 KB[39m
|
|
29
|
+
[32mCJS[39m ⚡️ Build success in 30ms
|
package/dist/cli.js
CHANGED
|
@@ -550,6 +550,24 @@ async function scanAction(directory, options) {
|
|
|
550
550
|
case "cost":
|
|
551
551
|
profileTools = [import_core3.ToolName.PatternDetect, import_core3.ToolName.ContextAnalyzer];
|
|
552
552
|
break;
|
|
553
|
+
case "logic":
|
|
554
|
+
profileTools = [
|
|
555
|
+
import_core3.ToolName.TestabilityIndex,
|
|
556
|
+
import_core3.ToolName.NamingConsistency,
|
|
557
|
+
import_core3.ToolName.ContextAnalyzer,
|
|
558
|
+
import_core3.ToolName.PatternDetect,
|
|
559
|
+
import_core3.ToolName.ChangeAmplification
|
|
560
|
+
];
|
|
561
|
+
break;
|
|
562
|
+
case "ui":
|
|
563
|
+
profileTools = [
|
|
564
|
+
import_core3.ToolName.NamingConsistency,
|
|
565
|
+
import_core3.ToolName.ContextAnalyzer,
|
|
566
|
+
import_core3.ToolName.PatternDetect,
|
|
567
|
+
import_core3.ToolName.DocDrift,
|
|
568
|
+
import_core3.ToolName.AiSignalClarity
|
|
569
|
+
];
|
|
570
|
+
break;
|
|
553
571
|
case "security":
|
|
554
572
|
profileTools = [
|
|
555
573
|
import_core3.ToolName.NamingConsistency,
|
|
@@ -619,6 +637,7 @@ async function scanAction(directory, options) {
|
|
|
619
637
|
);
|
|
620
638
|
}
|
|
621
639
|
};
|
|
640
|
+
const scoringProfile = options.profile || baseOptions.scoring?.profile || "default";
|
|
622
641
|
const results = await analyzeUnified({
|
|
623
642
|
...finalOptions,
|
|
624
643
|
progressCallback,
|
|
@@ -635,9 +654,16 @@ async function scanAction(directory, options) {
|
|
|
635
654
|
);
|
|
636
655
|
let scoringResult;
|
|
637
656
|
if (options.score || finalOptions.scoring?.showBreakdown) {
|
|
638
|
-
scoringResult = await scoreUnified(results,
|
|
657
|
+
scoringResult = await scoreUnified(results, {
|
|
658
|
+
...finalOptions,
|
|
659
|
+
scoring: {
|
|
660
|
+
...finalOptions.scoring,
|
|
661
|
+
profile: scoringProfile
|
|
662
|
+
}
|
|
663
|
+
});
|
|
639
664
|
console.log(import_chalk3.default.bold("\n\u{1F4CA} AI Readiness Overall Score"));
|
|
640
665
|
console.log(` ${(0, import_core3.formatScore)(scoringResult)}`);
|
|
666
|
+
console.log(import_chalk3.default.dim(` (Scoring Profile: ${scoringProfile})`));
|
|
641
667
|
if (options.compareTo) {
|
|
642
668
|
try {
|
|
643
669
|
const prevReport = JSON.parse(
|
|
@@ -735,8 +761,26 @@ async function scanAction(directory, options) {
|
|
|
735
761
|
console.log(import_chalk3.default.bold("\nTool breakdown:"));
|
|
736
762
|
scoringResult.breakdown.forEach((tool) => {
|
|
737
763
|
const rating = (0, import_core3.getRating)(tool.score);
|
|
738
|
-
|
|
764
|
+
const emoji = (0, import_core3.getRatingDisplay)(rating).emoji;
|
|
765
|
+
console.log(
|
|
766
|
+
` - ${tool.toolName}: ${tool.score}/100 (${rating}) ${emoji}`
|
|
767
|
+
);
|
|
739
768
|
});
|
|
769
|
+
const allRecs = scoringResult.breakdown.flatMap(
|
|
770
|
+
(t) => (t.recommendations || []).map((r) => ({ ...r, tool: t.toolName }))
|
|
771
|
+
).sort((a, b) => b.estimatedImpact - a.estimatedImpact).slice(0, 3);
|
|
772
|
+
if (allRecs.length > 0) {
|
|
773
|
+
console.log(import_chalk3.default.bold("\n\u{1F3AF} Top Actionable Recommendations:"));
|
|
774
|
+
allRecs.forEach((rec, i) => {
|
|
775
|
+
const priorityIcon = rec.priority === "high" ? "\u{1F534}" : rec.priority === "medium" ? "\u{1F7E1}" : "\u{1F535}";
|
|
776
|
+
console.log(
|
|
777
|
+
` ${i + 1}. ${priorityIcon} ${import_chalk3.default.bold(rec.action)}`
|
|
778
|
+
);
|
|
779
|
+
console.log(
|
|
780
|
+
` Impact: ${import_chalk3.default.green(`+${rec.estimatedImpact} points`)} to ${rec.tool}`
|
|
781
|
+
);
|
|
782
|
+
});
|
|
783
|
+
}
|
|
740
784
|
}
|
|
741
785
|
}
|
|
742
786
|
const mapToUnifiedReport = (res, scoring) => {
|
|
@@ -1869,11 +1913,11 @@ program.command("scan").description(
|
|
|
1869
1913
|
"Tools to run (comma-separated: patterns,context,consistency,doc-drift,deps-health,aiSignalClarity,grounding,testability,changeAmplification)"
|
|
1870
1914
|
).option(
|
|
1871
1915
|
"--profile <type>",
|
|
1872
|
-
"Scan profile to use (agentic, cost, security, onboarding)"
|
|
1916
|
+
"Scan profile to use (agentic, cost, logic, ui, security, onboarding)"
|
|
1873
1917
|
).option(
|
|
1874
1918
|
"--compare-to <path>",
|
|
1875
1919
|
"Compare results against a previous AIReady report JSON"
|
|
1876
|
-
).option("--include <patterns>", "File patterns to include (comma-separated)").option("--exclude <patterns>", "File patterns to exclude (comma-separated)").option("-o, --output <format>", "Output format: console, json", "console").option("--output-file <path>", "Output file path (for json)").option(
|
|
1920
|
+
).option("--include <patterns>", "File patterns to include (comma-separated)").option("--exclude <patterns>", "File patterns to exclude (comma-separated)").option("-o, --output <format>", "Output format: console, json", "console").option("--output-file <path>", "Output file path (for json)").option("--score", "Calculate and display AI Readiness Score (0-100)").option(
|
|
1877
1921
|
"--no-score",
|
|
1878
1922
|
"Disable calculating AI Readiness Score (enabled by default)"
|
|
1879
1923
|
).option("--weights <weights>", "Custom scoring weights").option("--threshold <score>", "Fail CI/CD if score below threshold (0-100)").option(
|
package/dist/cli.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
formatScore,
|
|
24
24
|
calculateTokenBudget,
|
|
25
25
|
getRating,
|
|
26
|
+
getRatingDisplay,
|
|
26
27
|
getRepoMetadata,
|
|
27
28
|
Severity,
|
|
28
29
|
ToolName,
|
|
@@ -289,6 +290,24 @@ async function scanAction(directory, options) {
|
|
|
289
290
|
case "cost":
|
|
290
291
|
profileTools = [ToolName.PatternDetect, ToolName.ContextAnalyzer];
|
|
291
292
|
break;
|
|
293
|
+
case "logic":
|
|
294
|
+
profileTools = [
|
|
295
|
+
ToolName.TestabilityIndex,
|
|
296
|
+
ToolName.NamingConsistency,
|
|
297
|
+
ToolName.ContextAnalyzer,
|
|
298
|
+
ToolName.PatternDetect,
|
|
299
|
+
ToolName.ChangeAmplification
|
|
300
|
+
];
|
|
301
|
+
break;
|
|
302
|
+
case "ui":
|
|
303
|
+
profileTools = [
|
|
304
|
+
ToolName.NamingConsistency,
|
|
305
|
+
ToolName.ContextAnalyzer,
|
|
306
|
+
ToolName.PatternDetect,
|
|
307
|
+
ToolName.DocDrift,
|
|
308
|
+
ToolName.AiSignalClarity
|
|
309
|
+
];
|
|
310
|
+
break;
|
|
292
311
|
case "security":
|
|
293
312
|
profileTools = [
|
|
294
313
|
ToolName.NamingConsistency,
|
|
@@ -358,6 +377,7 @@ async function scanAction(directory, options) {
|
|
|
358
377
|
);
|
|
359
378
|
}
|
|
360
379
|
};
|
|
380
|
+
const scoringProfile = options.profile || baseOptions.scoring?.profile || "default";
|
|
361
381
|
const results = await analyzeUnified({
|
|
362
382
|
...finalOptions,
|
|
363
383
|
progressCallback,
|
|
@@ -374,9 +394,16 @@ async function scanAction(directory, options) {
|
|
|
374
394
|
);
|
|
375
395
|
let scoringResult;
|
|
376
396
|
if (options.score || finalOptions.scoring?.showBreakdown) {
|
|
377
|
-
scoringResult = await scoreUnified(results,
|
|
397
|
+
scoringResult = await scoreUnified(results, {
|
|
398
|
+
...finalOptions,
|
|
399
|
+
scoring: {
|
|
400
|
+
...finalOptions.scoring,
|
|
401
|
+
profile: scoringProfile
|
|
402
|
+
}
|
|
403
|
+
});
|
|
378
404
|
console.log(chalk3.bold("\n\u{1F4CA} AI Readiness Overall Score"));
|
|
379
405
|
console.log(` ${formatScore(scoringResult)}`);
|
|
406
|
+
console.log(chalk3.dim(` (Scoring Profile: ${scoringProfile})`));
|
|
380
407
|
if (options.compareTo) {
|
|
381
408
|
try {
|
|
382
409
|
const prevReport = JSON.parse(
|
|
@@ -474,8 +501,26 @@ async function scanAction(directory, options) {
|
|
|
474
501
|
console.log(chalk3.bold("\nTool breakdown:"));
|
|
475
502
|
scoringResult.breakdown.forEach((tool) => {
|
|
476
503
|
const rating = getRating(tool.score);
|
|
477
|
-
|
|
504
|
+
const emoji = getRatingDisplay(rating).emoji;
|
|
505
|
+
console.log(
|
|
506
|
+
` - ${tool.toolName}: ${tool.score}/100 (${rating}) ${emoji}`
|
|
507
|
+
);
|
|
478
508
|
});
|
|
509
|
+
const allRecs = scoringResult.breakdown.flatMap(
|
|
510
|
+
(t) => (t.recommendations || []).map((r) => ({ ...r, tool: t.toolName }))
|
|
511
|
+
).sort((a, b) => b.estimatedImpact - a.estimatedImpact).slice(0, 3);
|
|
512
|
+
if (allRecs.length > 0) {
|
|
513
|
+
console.log(chalk3.bold("\n\u{1F3AF} Top Actionable Recommendations:"));
|
|
514
|
+
allRecs.forEach((rec, i) => {
|
|
515
|
+
const priorityIcon = rec.priority === "high" ? "\u{1F534}" : rec.priority === "medium" ? "\u{1F7E1}" : "\u{1F535}";
|
|
516
|
+
console.log(
|
|
517
|
+
` ${i + 1}. ${priorityIcon} ${chalk3.bold(rec.action)}`
|
|
518
|
+
);
|
|
519
|
+
console.log(
|
|
520
|
+
` Impact: ${chalk3.green(`+${rec.estimatedImpact} points`)} to ${rec.tool}`
|
|
521
|
+
);
|
|
522
|
+
});
|
|
523
|
+
}
|
|
479
524
|
}
|
|
480
525
|
}
|
|
481
526
|
const mapToUnifiedReport = (res, scoring) => {
|
|
@@ -1630,11 +1675,11 @@ program.command("scan").description(
|
|
|
1630
1675
|
"Tools to run (comma-separated: patterns,context,consistency,doc-drift,deps-health,aiSignalClarity,grounding,testability,changeAmplification)"
|
|
1631
1676
|
).option(
|
|
1632
1677
|
"--profile <type>",
|
|
1633
|
-
"Scan profile to use (agentic, cost, security, onboarding)"
|
|
1678
|
+
"Scan profile to use (agentic, cost, logic, ui, security, onboarding)"
|
|
1634
1679
|
).option(
|
|
1635
1680
|
"--compare-to <path>",
|
|
1636
1681
|
"Compare results against a previous AIReady report JSON"
|
|
1637
|
-
).option("--include <patterns>", "File patterns to include (comma-separated)").option("--exclude <patterns>", "File patterns to exclude (comma-separated)").option("-o, --output <format>", "Output format: console, json", "console").option("--output-file <path>", "Output file path (for json)").option(
|
|
1682
|
+
).option("--include <patterns>", "File patterns to include (comma-separated)").option("--exclude <patterns>", "File patterns to exclude (comma-separated)").option("-o, --output <format>", "Output format: console, json", "console").option("--output-file <path>", "Output file path (for json)").option("--score", "Calculate and display AI Readiness Score (0-100)").option(
|
|
1638
1683
|
"--no-score",
|
|
1639
1684
|
"Disable calculating AI Readiness Score (enabled by default)"
|
|
1640
1685
|
).option("--weights <weights>", "Custom scoring weights").option("--threshold <score>", "Fail CI/CD if score below threshold (0-100)").option(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/cli",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.2",
|
|
4
4
|
"description": "Unified CLI for AIReady analysis tools",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -11,18 +11,18 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"chalk": "^5.3.0",
|
|
13
13
|
"commander": "^14.0.0",
|
|
14
|
-
"@aiready/agent-grounding": "0.12.0",
|
|
15
|
-
"@aiready/context-analyzer": "0.20.0",
|
|
16
|
-
"@aiready/core": "0.22.0",
|
|
17
|
-
"@aiready/deps": "0.12.0",
|
|
18
14
|
"@aiready/consistency": "0.19.0",
|
|
19
15
|
"@aiready/clawmart": "0.1.2",
|
|
16
|
+
"@aiready/context-analyzer": "0.20.0",
|
|
17
|
+
"@aiready/agent-grounding": "0.12.0",
|
|
18
|
+
"@aiready/core": "0.22.1",
|
|
20
19
|
"@aiready/doc-drift": "0.12.0",
|
|
21
20
|
"@aiready/change-amplification": "0.12.0",
|
|
22
|
-
"@aiready/
|
|
23
|
-
"@aiready/pattern-detect": "0.15.0",
|
|
21
|
+
"@aiready/deps": "0.12.0",
|
|
24
22
|
"@aiready/ai-signal-clarity": "0.12.0",
|
|
25
|
-
"@aiready/visualizer": "0.5.0"
|
|
23
|
+
"@aiready/visualizer": "0.5.0",
|
|
24
|
+
"@aiready/testability": "0.5.0",
|
|
25
|
+
"@aiready/pattern-detect": "0.15.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^24.0.0",
|
package/src/cli.ts
CHANGED
|
@@ -95,7 +95,7 @@ program
|
|
|
95
95
|
)
|
|
96
96
|
.option(
|
|
97
97
|
'--profile <type>',
|
|
98
|
-
'Scan profile to use (agentic, cost, security, onboarding)'
|
|
98
|
+
'Scan profile to use (agentic, cost, logic, ui, security, onboarding)'
|
|
99
99
|
)
|
|
100
100
|
.option(
|
|
101
101
|
'--compare-to <path>',
|
|
@@ -105,6 +105,7 @@ program
|
|
|
105
105
|
.option('--exclude <patterns>', 'File patterns to exclude (comma-separated)')
|
|
106
106
|
.option('-o, --output <format>', 'Output format: console, json', 'console')
|
|
107
107
|
.option('--output-file <path>', 'Output file path (for json)')
|
|
108
|
+
.option('--score', 'Calculate and display AI Readiness Score (0-100)')
|
|
108
109
|
.option(
|
|
109
110
|
'--no-score',
|
|
110
111
|
'Disable calculating AI Readiness Score (enabled by default)'
|
package/src/commands/scan.ts
CHANGED
|
@@ -98,6 +98,24 @@ export async function scanAction(directory: string, options: ScanOptions) {
|
|
|
98
98
|
case 'cost':
|
|
99
99
|
profileTools = [ToolName.PatternDetect, ToolName.ContextAnalyzer];
|
|
100
100
|
break;
|
|
101
|
+
case 'logic':
|
|
102
|
+
profileTools = [
|
|
103
|
+
ToolName.TestabilityIndex,
|
|
104
|
+
ToolName.NamingConsistency,
|
|
105
|
+
ToolName.ContextAnalyzer,
|
|
106
|
+
ToolName.PatternDetect,
|
|
107
|
+
ToolName.ChangeAmplification,
|
|
108
|
+
];
|
|
109
|
+
break;
|
|
110
|
+
case 'ui':
|
|
111
|
+
profileTools = [
|
|
112
|
+
ToolName.NamingConsistency,
|
|
113
|
+
ToolName.ContextAnalyzer,
|
|
114
|
+
ToolName.PatternDetect,
|
|
115
|
+
ToolName.DocDrift,
|
|
116
|
+
ToolName.AiSignalClarity,
|
|
117
|
+
];
|
|
118
|
+
break;
|
|
101
119
|
case 'security':
|
|
102
120
|
profileTools = [
|
|
103
121
|
ToolName.NamingConsistency,
|
|
@@ -182,6 +200,10 @@ export async function scanAction(directory: string, options: ScanOptions) {
|
|
|
182
200
|
}
|
|
183
201
|
};
|
|
184
202
|
|
|
203
|
+
// Determine scoring profile for project-type-aware weighting
|
|
204
|
+
const scoringProfile =
|
|
205
|
+
options.profile || baseOptions.scoring?.profile || 'default';
|
|
206
|
+
|
|
185
207
|
const results = await analyzeUnified({
|
|
186
208
|
...finalOptions,
|
|
187
209
|
progressCallback,
|
|
@@ -199,10 +221,18 @@ export async function scanAction(directory: string, options: ScanOptions) {
|
|
|
199
221
|
|
|
200
222
|
let scoringResult: ScoringResult | undefined;
|
|
201
223
|
if (options.score || finalOptions.scoring?.showBreakdown) {
|
|
202
|
-
|
|
224
|
+
// Pass the profile to scoreUnified
|
|
225
|
+
scoringResult = await scoreUnified(results, {
|
|
226
|
+
...finalOptions,
|
|
227
|
+
scoring: {
|
|
228
|
+
...finalOptions.scoring,
|
|
229
|
+
profile: scoringProfile,
|
|
230
|
+
},
|
|
231
|
+
});
|
|
203
232
|
|
|
204
233
|
console.log(chalk.bold('\n📊 AI Readiness Overall Score'));
|
|
205
234
|
console.log(` ${formatScore(scoringResult)}`);
|
|
235
|
+
console.log(chalk.dim(` (Scoring Profile: ${scoringProfile})`));
|
|
206
236
|
|
|
207
237
|
// Trend comparison logic
|
|
208
238
|
if (options.compareTo) {
|
|
@@ -313,8 +343,37 @@ export async function scanAction(directory: string, options: ScanOptions) {
|
|
|
313
343
|
console.log(chalk.bold('\nTool breakdown:'));
|
|
314
344
|
scoringResult.breakdown.forEach((tool) => {
|
|
315
345
|
const rating = getRating(tool.score);
|
|
316
|
-
|
|
346
|
+
const emoji = getRatingDisplay(rating).emoji;
|
|
347
|
+
console.log(
|
|
348
|
+
` - ${tool.toolName}: ${tool.score}/100 (${rating}) ${emoji}`
|
|
349
|
+
);
|
|
317
350
|
});
|
|
351
|
+
|
|
352
|
+
// Top Actionable Recommendations
|
|
353
|
+
const allRecs = scoringResult.breakdown
|
|
354
|
+
.flatMap((t) =>
|
|
355
|
+
(t.recommendations || []).map((r) => ({ ...r, tool: t.toolName }))
|
|
356
|
+
)
|
|
357
|
+
.sort((a, b) => b.estimatedImpact - a.estimatedImpact)
|
|
358
|
+
.slice(0, 3);
|
|
359
|
+
|
|
360
|
+
if (allRecs.length > 0) {
|
|
361
|
+
console.log(chalk.bold('\n🎯 Top Actionable Recommendations:'));
|
|
362
|
+
allRecs.forEach((rec, i) => {
|
|
363
|
+
const priorityIcon =
|
|
364
|
+
rec.priority === 'high'
|
|
365
|
+
? '🔴'
|
|
366
|
+
: rec.priority === 'medium'
|
|
367
|
+
? '🟡'
|
|
368
|
+
: '🔵';
|
|
369
|
+
console.log(
|
|
370
|
+
` ${i + 1}. ${priorityIcon} ${chalk.bold(rec.action)}`
|
|
371
|
+
);
|
|
372
|
+
console.log(
|
|
373
|
+
` Impact: ${chalk.green(`+${rec.estimatedImpact} points`)} to ${rec.tool}`
|
|
374
|
+
);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
318
377
|
}
|
|
319
378
|
}
|
|
320
379
|
|