docguard-cli 0.5.2 → 0.7.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/PHILOSOPHY.md +22 -0
- package/README.md +13 -0
- package/cli/commands/diagnose.mjs +224 -33
- package/cli/commands/generate.mjs +501 -87
- package/cli/commands/guard.mjs +23 -6
- package/cli/commands/publish.mjs +246 -0
- package/cli/commands/score.mjs +31 -0
- package/cli/commands/trace.mjs +311 -0
- package/cli/docguard.mjs +31 -3
- package/cli/scanners/doc-tools.mjs +351 -0
- package/cli/scanners/routes.mjs +461 -0
- package/cli/scanners/schemas.mjs +567 -0
- package/package.json +1 -1
package/cli/docguard.mjs
CHANGED
|
@@ -15,7 +15,13 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { readFileSync, existsSync } from 'node:fs';
|
|
18
|
-
import { resolve, basename } from 'node:path';
|
|
18
|
+
import { resolve, basename, dirname } from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
// Read version from package.json (single source of truth)
|
|
22
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const PKG = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf-8'));
|
|
24
|
+
const VERSION = PKG.version;
|
|
19
25
|
import { runAudit } from './commands/audit.mjs';
|
|
20
26
|
import { runInit } from './commands/init.mjs';
|
|
21
27
|
import { runGuard } from './commands/guard.mjs';
|
|
@@ -29,6 +35,8 @@ import { runCI } from './commands/ci.mjs';
|
|
|
29
35
|
import { runFix } from './commands/fix.mjs';
|
|
30
36
|
import { runWatch } from './commands/watch.mjs';
|
|
31
37
|
import { runDiagnose } from './commands/diagnose.mjs';
|
|
38
|
+
import { runPublish } from './commands/publish.mjs';
|
|
39
|
+
import { runTrace } from './commands/trace.mjs';
|
|
32
40
|
|
|
33
41
|
// ── Colors (ANSI escape codes, zero deps) ──────────────────────────────────
|
|
34
42
|
export const c = {
|
|
@@ -253,7 +261,7 @@ function deepMerge(target, source) {
|
|
|
253
261
|
function printBanner() {
|
|
254
262
|
console.log(`
|
|
255
263
|
${c.cyan}${c.bold} ╔═══════════════════════════════════════════╗
|
|
256
|
-
║ DocGuard
|
|
264
|
+
║ DocGuard v${VERSION.padEnd(27)}║
|
|
257
265
|
║ Canonical-Driven Development (CDD) ║
|
|
258
266
|
╚═══════════════════════════════════════════╝${c.reset}
|
|
259
267
|
`);
|
|
@@ -279,6 +287,8 @@ ${c.bold}Commands:${c.reset}
|
|
|
279
287
|
${c.green}ci${c.reset} Single command for CI/CD pipelines (guard + score)
|
|
280
288
|
${c.green}fix${c.reset} Find issues and generate AI fix instructions
|
|
281
289
|
${c.green}watch${c.reset} Watch for file changes and re-run guard automatically
|
|
290
|
+
${c.green}publish${c.reset} Scaffold external docs (Mintlify, Docusaurus)
|
|
291
|
+
${c.green}trace${c.reset} Generate requirements traceability matrix
|
|
282
292
|
|
|
283
293
|
${c.bold}Options:${c.reset}
|
|
284
294
|
--dir <path> Project directory (default: current directory)
|
|
@@ -295,6 +305,7 @@ ${c.bold}Options:${c.reset}
|
|
|
295
305
|
--auto Auto-fix what's possible (used with fix command)
|
|
296
306
|
--doc <name> Generate AI prompt for specific doc (architecture, security, etc.)
|
|
297
307
|
--profile <p> Compliance profile: starter, standard, enterprise (init command)
|
|
308
|
+
--platform <p> Doc platform: mintlify (publish command)
|
|
298
309
|
--tax Show estimated documentation maintenance cost (with score)
|
|
299
310
|
--help Show this help message
|
|
300
311
|
--version Show version
|
|
@@ -384,6 +395,15 @@ function main() {
|
|
|
384
395
|
flags.autoFix = true;
|
|
385
396
|
} else if (args[i] === '--skip-prompts') {
|
|
386
397
|
flags.skipPrompts = true;
|
|
398
|
+
} else if (args[i] === '--platform' && args[i + 1]) {
|
|
399
|
+
flags.platform = args[i + 1];
|
|
400
|
+
i++;
|
|
401
|
+
} else if (args[i] === '--no-fix') {
|
|
402
|
+
flags.noFix = true;
|
|
403
|
+
} else if (args[i] === '--signals') {
|
|
404
|
+
flags.signals = true;
|
|
405
|
+
} else if (args[i] === '--debate') {
|
|
406
|
+
flags.debate = true;
|
|
387
407
|
}
|
|
388
408
|
}
|
|
389
409
|
|
|
@@ -395,7 +415,7 @@ function main() {
|
|
|
395
415
|
}
|
|
396
416
|
|
|
397
417
|
if (command === '--version' || command === '-v') {
|
|
398
|
-
console.log(
|
|
418
|
+
console.log(`docguard v${VERSION}`);
|
|
399
419
|
process.exit(0);
|
|
400
420
|
}
|
|
401
421
|
|
|
@@ -448,6 +468,14 @@ function main() {
|
|
|
448
468
|
case 'watch':
|
|
449
469
|
runWatch(projectDir, config, flags);
|
|
450
470
|
break;
|
|
471
|
+
case 'publish':
|
|
472
|
+
case 'pub':
|
|
473
|
+
runPublish(projectDir, config, flags);
|
|
474
|
+
break;
|
|
475
|
+
case 'trace':
|
|
476
|
+
case 'traceability':
|
|
477
|
+
runTrace(projectDir, config, flags);
|
|
478
|
+
break;
|
|
451
479
|
default:
|
|
452
480
|
console.error(`${c.red}Unknown command: ${command}${c.reset}`);
|
|
453
481
|
console.log(`Run ${c.cyan}docguard --help${c.reset} for usage.`);
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doc Tool Detection Scanner
|
|
3
|
+
* Detects existing documentation tools in a project (OpenAPI, TypeDoc, JSDoc, Storybook, etc.)
|
|
4
|
+
* and extracts available data from their outputs.
|
|
5
|
+
*
|
|
6
|
+
* Philosophy: Detect and leverage, never replace.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
10
|
+
import { resolve, join } from 'node:path';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Detect all documentation tools present in the project.
|
|
14
|
+
* @param {string} dir - Project root directory
|
|
15
|
+
* @returns {object} Detected tools with their config and extracted data
|
|
16
|
+
*/
|
|
17
|
+
export function detectDocTools(dir) {
|
|
18
|
+
const tools = {
|
|
19
|
+
openapi: detectOpenAPI(dir),
|
|
20
|
+
typedoc: detectTypeDoc(dir),
|
|
21
|
+
jsdoc: detectJSDoc(dir),
|
|
22
|
+
storybook: detectStorybook(dir),
|
|
23
|
+
docusaurus: detectDocusaurus(dir),
|
|
24
|
+
mintlify: detectMintlify(dir),
|
|
25
|
+
redocly: detectRedocly(dir),
|
|
26
|
+
swagger: detectSwagger(dir),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Count detected tools
|
|
30
|
+
tools._detected = Object.entries(tools)
|
|
31
|
+
.filter(([k, v]) => k !== '_detected' && v.found)
|
|
32
|
+
.map(([k]) => k);
|
|
33
|
+
|
|
34
|
+
return tools;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── OpenAPI / Swagger Spec ─────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
function detectOpenAPI(dir) {
|
|
40
|
+
const candidates = [
|
|
41
|
+
'openapi.yaml', 'openapi.yml', 'openapi.json',
|
|
42
|
+
'swagger.yaml', 'swagger.yml', 'swagger.json',
|
|
43
|
+
'api/openapi.yaml', 'api/openapi.yml', 'api/openapi.json',
|
|
44
|
+
'docs/openapi.yaml', 'docs/openapi.yml',
|
|
45
|
+
'spec/openapi.yaml', 'spec/openapi.yml',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
for (const candidate of candidates) {
|
|
49
|
+
const fullPath = resolve(dir, candidate);
|
|
50
|
+
if (existsSync(fullPath)) {
|
|
51
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
52
|
+
const spec = parseOpenAPISpec(content, candidate);
|
|
53
|
+
return {
|
|
54
|
+
found: true,
|
|
55
|
+
path: candidate,
|
|
56
|
+
version: spec.version,
|
|
57
|
+
endpoints: spec.endpoints,
|
|
58
|
+
schemas: spec.schemas,
|
|
59
|
+
info: spec.info,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { found: false };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseOpenAPISpec(content, filename) {
|
|
68
|
+
const result = { version: null, endpoints: [], schemas: [], info: {} };
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
let spec;
|
|
72
|
+
if (filename.endsWith('.json')) {
|
|
73
|
+
spec = JSON.parse(content);
|
|
74
|
+
} else {
|
|
75
|
+
// Simple YAML parsing for common patterns (no dependency)
|
|
76
|
+
spec = parseSimpleYAML(content);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Version
|
|
80
|
+
result.version = spec.openapi || spec.swagger || 'unknown';
|
|
81
|
+
|
|
82
|
+
// Info
|
|
83
|
+
result.info = {
|
|
84
|
+
title: spec.info?.title || '',
|
|
85
|
+
description: spec.info?.description || '',
|
|
86
|
+
version: spec.info?.version || '',
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Endpoints
|
|
90
|
+
if (spec.paths) {
|
|
91
|
+
for (const [path, methods] of Object.entries(spec.paths)) {
|
|
92
|
+
for (const [method, details] of Object.entries(methods)) {
|
|
93
|
+
if (['get', 'post', 'put', 'delete', 'patch', 'options', 'head'].includes(method)) {
|
|
94
|
+
result.endpoints.push({
|
|
95
|
+
method: method.toUpperCase(),
|
|
96
|
+
path,
|
|
97
|
+
summary: details?.summary || details?.description || '',
|
|
98
|
+
tags: details?.tags || [],
|
|
99
|
+
operationId: details?.operationId || '',
|
|
100
|
+
auth: !!(details?.security?.length),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Schemas
|
|
108
|
+
const schemas = spec.components?.schemas || spec.definitions || {};
|
|
109
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
110
|
+
const fields = [];
|
|
111
|
+
if (schema.properties) {
|
|
112
|
+
for (const [fieldName, fieldDef] of Object.entries(schema.properties)) {
|
|
113
|
+
fields.push({
|
|
114
|
+
name: fieldName,
|
|
115
|
+
type: fieldDef.type || fieldDef.$ref?.split('/').pop() || 'object',
|
|
116
|
+
required: (schema.required || []).includes(fieldName),
|
|
117
|
+
description: fieldDef.description || '',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
result.schemas.push({ name, fields, description: schema.description || '' });
|
|
122
|
+
}
|
|
123
|
+
} catch { /* spec parsing failed, return empty */ }
|
|
124
|
+
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Minimal YAML parser for OpenAPI specs.
|
|
130
|
+
* Handles the most common structures without external dependencies.
|
|
131
|
+
* NOT a full YAML parser — covers 80% of real-world OpenAPI files.
|
|
132
|
+
*/
|
|
133
|
+
function parseSimpleYAML(content) {
|
|
134
|
+
// Try JSON first (some .yaml files are actually JSON)
|
|
135
|
+
try { return JSON.parse(content); } catch { /* not JSON */ }
|
|
136
|
+
|
|
137
|
+
const result = {};
|
|
138
|
+
const lines = content.split('\n');
|
|
139
|
+
const stack = [{ obj: result, indent: -1 }];
|
|
140
|
+
|
|
141
|
+
for (const rawLine of lines) {
|
|
142
|
+
const line = rawLine.replace(/\r$/, '');
|
|
143
|
+
if (!line.trim() || line.trim().startsWith('#')) continue;
|
|
144
|
+
|
|
145
|
+
const indent = line.search(/\S/);
|
|
146
|
+
const trimmed = line.trim();
|
|
147
|
+
|
|
148
|
+
// Pop stack to find parent
|
|
149
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
|
|
150
|
+
stack.pop();
|
|
151
|
+
}
|
|
152
|
+
const parent = stack[stack.length - 1].obj;
|
|
153
|
+
|
|
154
|
+
// Array item
|
|
155
|
+
if (trimmed.startsWith('- ')) {
|
|
156
|
+
const value = trimmed.slice(2).trim();
|
|
157
|
+
if (Array.isArray(parent)) {
|
|
158
|
+
if (value.includes(':')) {
|
|
159
|
+
const obj = {};
|
|
160
|
+
const [k, ...rest] = value.split(':');
|
|
161
|
+
obj[k.trim()] = rest.join(':').trim().replace(/^['"]|['"]$/g, '');
|
|
162
|
+
parent.push(obj);
|
|
163
|
+
stack.push({ obj, indent });
|
|
164
|
+
} else {
|
|
165
|
+
parent.push(value.replace(/^['"]|['"]$/g, ''));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Key-value pair
|
|
172
|
+
const colonIdx = trimmed.indexOf(':');
|
|
173
|
+
if (colonIdx > 0) {
|
|
174
|
+
const key = trimmed.substring(0, colonIdx).trim();
|
|
175
|
+
const rawVal = trimmed.substring(colonIdx + 1).trim();
|
|
176
|
+
|
|
177
|
+
if (rawVal === '' || rawVal === '|' || rawVal === '>') {
|
|
178
|
+
// Nested object or block
|
|
179
|
+
const child = {};
|
|
180
|
+
if (typeof parent === 'object' && !Array.isArray(parent)) {
|
|
181
|
+
parent[key] = child;
|
|
182
|
+
}
|
|
183
|
+
stack.push({ obj: child, indent });
|
|
184
|
+
} else if (rawVal.startsWith('[')) {
|
|
185
|
+
// Inline array
|
|
186
|
+
try {
|
|
187
|
+
parent[key] = JSON.parse(rawVal);
|
|
188
|
+
} catch {
|
|
189
|
+
parent[key] = rawVal;
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
// Simple value
|
|
193
|
+
let val = rawVal.replace(/^['"]|['"]$/g, '');
|
|
194
|
+
if (val === 'true') val = true;
|
|
195
|
+
else if (val === 'false') val = false;
|
|
196
|
+
else if (/^\d+$/.test(val)) val = parseInt(val);
|
|
197
|
+
if (typeof parent === 'object' && !Array.isArray(parent)) {
|
|
198
|
+
parent[key] = val;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ── TypeDoc ────────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
function detectTypeDoc(dir) {
|
|
210
|
+
const configs = ['typedoc.json', 'typedoc.config.js', 'typedoc.config.mjs'];
|
|
211
|
+
for (const config of configs) {
|
|
212
|
+
if (existsSync(resolve(dir, config))) {
|
|
213
|
+
return { found: true, config };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Check package.json devDeps
|
|
218
|
+
const pkgPath = resolve(dir, 'package.json');
|
|
219
|
+
if (existsSync(pkgPath)) {
|
|
220
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
221
|
+
if (pkg.devDependencies?.typedoc) {
|
|
222
|
+
return { found: true, config: 'package.json (devDependency)' };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return { found: false };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── JSDoc ──────────────────────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
function detectJSDoc(dir) {
|
|
232
|
+
const configs = ['jsdoc.json', '.jsdoc.json', 'jsdoc.conf.json'];
|
|
233
|
+
for (const config of configs) {
|
|
234
|
+
if (existsSync(resolve(dir, config))) {
|
|
235
|
+
return { found: true, config };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const pkgPath = resolve(dir, 'package.json');
|
|
240
|
+
if (existsSync(pkgPath)) {
|
|
241
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
242
|
+
if (pkg.devDependencies?.jsdoc) {
|
|
243
|
+
return { found: true, config: 'package.json (devDependency)' };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return { found: false };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── Storybook ──────────────────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
function detectStorybook(dir) {
|
|
253
|
+
if (existsSync(resolve(dir, '.storybook'))) {
|
|
254
|
+
// Count stories
|
|
255
|
+
let storyCount = 0;
|
|
256
|
+
const storyDirs = ['src', 'components', 'stories'];
|
|
257
|
+
for (const sd of storyDirs) {
|
|
258
|
+
const fullDir = resolve(dir, sd);
|
|
259
|
+
if (existsSync(fullDir)) {
|
|
260
|
+
storyCount += countFiles(fullDir, /\.(stories|story)\.(js|jsx|ts|tsx|mdx)$/);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return { found: true, config: '.storybook/', storyCount };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return { found: false };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── Docusaurus ─────────────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
function detectDocusaurus(dir) {
|
|
272
|
+
const configs = ['docusaurus.config.js', 'docusaurus.config.ts', 'docusaurus.config.mjs'];
|
|
273
|
+
for (const config of configs) {
|
|
274
|
+
if (existsSync(resolve(dir, config))) {
|
|
275
|
+
return { found: true, config };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return { found: false };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── Mintlify ───────────────────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
function detectMintlify(dir) {
|
|
284
|
+
// Check for docs.json (new) or mint.json (legacy)
|
|
285
|
+
for (const config of ['docs.json', 'mint.json']) {
|
|
286
|
+
const fullPath = resolve(dir, config);
|
|
287
|
+
if (existsSync(fullPath)) {
|
|
288
|
+
try {
|
|
289
|
+
const content = JSON.parse(readFileSync(fullPath, 'utf-8'));
|
|
290
|
+
return {
|
|
291
|
+
found: true,
|
|
292
|
+
config,
|
|
293
|
+
name: content.name || '',
|
|
294
|
+
version: config === 'docs.json' ? 'v2' : 'v1',
|
|
295
|
+
};
|
|
296
|
+
} catch {
|
|
297
|
+
return { found: true, config };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return { found: false };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── Redocly ────────────────────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
function detectRedocly(dir) {
|
|
307
|
+
const configs = ['redocly.yaml', 'redocly.yml', '.redocly.yaml', '.redocly.yml'];
|
|
308
|
+
for (const config of configs) {
|
|
309
|
+
if (existsSync(resolve(dir, config))) {
|
|
310
|
+
return { found: true, config };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return { found: false };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ── Swagger UI ─────────────────────────────────────────────────────────────
|
|
317
|
+
|
|
318
|
+
function detectSwagger(dir) {
|
|
319
|
+
const pkgPath = resolve(dir, 'package.json');
|
|
320
|
+
if (existsSync(pkgPath)) {
|
|
321
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
322
|
+
const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
323
|
+
if (allDeps['swagger-ui-express'] || allDeps['@fastify/swagger'] || allDeps['swagger-jsdoc']) {
|
|
324
|
+
return {
|
|
325
|
+
found: true,
|
|
326
|
+
middleware: allDeps['swagger-ui-express'] ? 'swagger-ui-express' :
|
|
327
|
+
allDeps['@fastify/swagger'] ? '@fastify/swagger' : 'swagger-jsdoc',
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return { found: false };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
function countFiles(dir, pattern) {
|
|
337
|
+
let count = 0;
|
|
338
|
+
try {
|
|
339
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
340
|
+
for (const entry of entries) {
|
|
341
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
342
|
+
const fullPath = join(dir, entry.name);
|
|
343
|
+
if (entry.isDirectory()) {
|
|
344
|
+
count += countFiles(fullPath, pattern);
|
|
345
|
+
} else if (pattern.test(entry.name)) {
|
|
346
|
+
count++;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
} catch { /* skip */ }
|
|
350
|
+
return count;
|
|
351
|
+
}
|