sofia-cli 0.1.2 → 0.1.4
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/README.md +42 -20
- package/dist/infra/deploy.sh +193 -0
- package/dist/infra/gather-env.sh +211 -0
- package/dist/infra/infra/deploy.sh +193 -0
- package/dist/infra/infra/gather-env.sh +211 -0
- package/dist/infra/infra/main.bicep +90 -0
- package/dist/infra/infra/main.bicepparam +18 -0
- package/dist/infra/infra/resources.bicep +134 -0
- package/dist/infra/infra/teardown.sh +114 -0
- package/dist/infra/main.bicep +90 -0
- package/dist/infra/main.bicepparam +18 -0
- package/dist/infra/resources.bicep +134 -0
- package/dist/infra/teardown.sh +114 -0
- package/dist/src/cli/developCommand.js +0 -2
- package/dist/src/cli/index.js +8 -1
- package/dist/src/cli/workshopCommand.js +1 -1
- package/dist/src/develop/index.js +1 -1
- package/dist/src/develop/pocUtils.js +228 -0
- package/dist/src/develop/ralphLoop.js +8 -27
- package/dist/src/shared/data/cards.json +655 -670
- package/docs/architecture.md +2 -1
- package/package.json +5 -3
- package/src/cli/developCommand.ts +1 -3
- package/src/cli/index.ts +11 -1
- package/src/cli/workshopCommand.ts +21 -17
- package/src/develop/dynamicScaffolder.ts +36 -30
- package/src/develop/index.ts +13 -2
- package/src/develop/pocUtils.ts +296 -0
- package/src/develop/ralphLoop.ts +8 -28
- package/src/develop/templateRegistry.ts +19 -18
- package/src/shared/data/cards.json +655 -670
- package/tests/e2e/developE2e.spec.ts +3 -61
- package/tests/e2e/developFailureE2e.spec.ts +34 -38
- package/tests/integration/pocGithubMcp.spec.ts +29 -39
- package/tests/integration/pocLocalFallback.spec.ts +29 -39
- package/tests/integration/ralphLoopFlow.spec.ts +46 -66
- package/tests/integration/ralphLoopPartial.spec.ts +30 -37
- package/tests/unit/develop/githubMcpAdapter.spec.ts +0 -134
- package/tests/unit/develop/outputValidator.spec.ts +45 -21
- package/tests/unit/develop/ralphLoop.spec.ts +58 -94
- package/tsconfig.json +2 -1
- package/vitest.workspace.ts +5 -0
- package/dist/src/develop/pocScaffolder.js +0 -542
- package/dist/tests/e2e/developE2e.spec.js +0 -126
- package/dist/tests/e2e/developFailureE2e.spec.js +0 -247
- package/dist/tests/e2e/developPty.spec.js +0 -75
- package/dist/tests/e2e/discoveryWebSearchRelevance.spec.js +0 -84
- package/dist/tests/e2e/harness.spec.js +0 -83
- package/dist/tests/e2e/mcpLive.spec.js +0 -120
- package/dist/tests/e2e/newSession.e2e.spec.js +0 -177
- package/dist/tests/e2e/ralphLoopEnrichmentComparison.spec.js +0 -62
- package/dist/tests/e2e/workiqEnrichment.spec.js +0 -56
- package/dist/tests/e2e/zavaSimulation.spec.js +0 -452
- package/dist/tests/fixtures/test-fixture-project/src/add.js +0 -3
- package/dist/tests/fixtures/test-fixture-project/tests/failing.test.js +0 -6
- package/dist/tests/fixtures/test-fixture-project/tests/hanging.test.js +0 -8
- package/dist/tests/fixtures/test-fixture-project/tests/passing.test.js +0 -10
- package/dist/tests/fixtures/test-fixture-project/vitest.config.js +0 -6
- package/dist/tests/integration/autoStartConversation.spec.js +0 -138
- package/dist/tests/integration/defaultCommand.spec.js +0 -147
- package/dist/tests/integration/directCommandNonTty.spec.js +0 -224
- package/dist/tests/integration/directCommandTty.spec.js +0 -151
- package/dist/tests/integration/discoveryEnrichmentFlow.spec.js +0 -175
- package/dist/tests/integration/exportArtifacts.spec.js +0 -202
- package/dist/tests/integration/exportFallbackFlow.spec.js +0 -99
- package/dist/tests/integration/mcpDegradationFlow.spec.js +0 -190
- package/dist/tests/integration/mcpTransportFlow.spec.js +0 -139
- package/dist/tests/integration/newSessionFlow.spec.js +0 -343
- package/dist/tests/integration/pocGithubMcp.spec.js +0 -186
- package/dist/tests/integration/pocLocalFallback.spec.js +0 -171
- package/dist/tests/integration/pocScaffold.spec.js +0 -163
- package/dist/tests/integration/ralphLoopFlow.spec.js +0 -359
- package/dist/tests/integration/ralphLoopPartial.spec.js +0 -368
- package/dist/tests/integration/resumeAndBacktrack.spec.js +0 -247
- package/dist/tests/integration/spinnerLifecycle.spec.js +0 -220
- package/dist/tests/integration/summarizationFlow.spec.js +0 -115
- package/dist/tests/integration/testRunnerReal.spec.js +0 -52
- package/dist/tests/integration/webSearchAgent.spec.js +0 -128
- package/dist/tests/live/copilotSdkLive.spec.js +0 -107
- package/dist/tests/live/zavaFullWorkshop.spec.js +0 -392
- package/dist/tests/setup/loadEnv.js +0 -3
- package/dist/tests/unit/cli/developCommand.spec.js +0 -567
- package/dist/tests/unit/cli/directCommands.spec.js +0 -279
- package/dist/tests/unit/cli/envLoader.spec.js +0 -58
- package/dist/tests/unit/cli/ioContext.spec.js +0 -119
- package/dist/tests/unit/cli/preflight.spec.js +0 -108
- package/dist/tests/unit/cli/statusCommand.spec.js +0 -111
- package/dist/tests/unit/cli/workshopClientFallback.spec.js +0 -80
- package/dist/tests/unit/cli/workshopCommand.spec.js +0 -328
- package/dist/tests/unit/config/vitestEnvSetup.spec.js +0 -13
- package/dist/tests/unit/develop/checkpointState.spec.js +0 -315
- package/dist/tests/unit/develop/codeGenerator.spec.js +0 -355
- package/dist/tests/unit/develop/githubMcpAdapter.spec.js +0 -231
- package/dist/tests/unit/develop/mcpContextEnricher.spec.js +0 -433
- package/dist/tests/unit/develop/outputValidator.spec.js +0 -119
- package/dist/tests/unit/develop/pocScaffolder.spec.js +0 -353
- package/dist/tests/unit/develop/ralphLoop.spec.js +0 -1248
- package/dist/tests/unit/develop/templateRegistry.spec.js +0 -85
- package/dist/tests/unit/develop/testRunner.spec.js +0 -249
- package/dist/tests/unit/infraBicep.spec.js +0 -92
- package/dist/tests/unit/infraDeploy.spec.js +0 -82
- package/dist/tests/unit/infraTeardown.spec.js +0 -63
- package/dist/tests/unit/logging/logger.spec.js +0 -43
- package/dist/tests/unit/loop/conversationLoop.spec.js +0 -592
- package/dist/tests/unit/loop/phaseSummarizer.spec.js +0 -141
- package/dist/tests/unit/loop/streamingMarkdown.spec.js +0 -147
- package/dist/tests/unit/mcp/mcpManager.spec.js +0 -279
- package/dist/tests/unit/mcp/mcpTransport.spec.js +0 -529
- package/dist/tests/unit/mcp/retryPolicy.spec.js +0 -218
- package/dist/tests/unit/mcp/timeoutValidation.spec.js +0 -46
- package/dist/tests/unit/mcp/webSearch.spec.js +0 -567
- package/dist/tests/unit/phases/contextSummarizer.spec.js +0 -140
- package/dist/tests/unit/phases/discoveryEnricher.repeatCalls.spec.js +0 -93
- package/dist/tests/unit/phases/discoveryEnricher.spec.js +0 -411
- package/dist/tests/unit/phases/phaseExtractors.spec.js +0 -352
- package/dist/tests/unit/phases/phaseHandlers.spec.js +0 -425
- package/dist/tests/unit/prompts/promptLoader.spec.js +0 -118
- package/dist/tests/unit/schemas/pocSchemas.spec.js +0 -412
- package/dist/tests/unit/schemas/session.spec.js +0 -257
- package/dist/tests/unit/sessions/exportPaths.spec.js +0 -31
- package/dist/tests/unit/sessions/exportWriter.spec.js +0 -655
- package/dist/tests/unit/sessions/sessionManager.spec.js +0 -151
- package/dist/tests/unit/sessions/sessionStore.spec.js +0 -116
- package/dist/tests/unit/shared/activitySpinner.spec.js +0 -175
- package/dist/tests/unit/shared/cardsLoader.spec.js +0 -76
- package/dist/tests/unit/shared/copilotClient.spec.js +0 -155
- package/dist/tests/unit/shared/errorClassifier.spec.js +0 -131
- package/dist/tests/unit/shared/events.spec.js +0 -55
- package/dist/tests/unit/shared/markdownRenderer.spec.js +0 -35
- package/dist/tests/unit/shared/markdownRendererChunks.spec.js +0 -70
- package/dist/tests/unit/shared/tableRenderer.spec.js +0 -34
- package/dist/vitest.config.js +0 -14
- package/dist/vitest.live.config.js +0 -18
- package/src/develop/pocScaffolder.ts +0 -646
- package/tests/integration/pocScaffold.spec.ts +0 -220
- package/tests/unit/develop/pocScaffolder.spec.ts +0 -451
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PoC Utilities.
|
|
3
|
+
*
|
|
4
|
+
* Standalone helper functions and types extracted from the former
|
|
5
|
+
* PocScaffolder class: git init, TODO scanning, output validation,
|
|
6
|
+
* and shared type definitions used by the scaffold / Ralph loop pipeline.
|
|
7
|
+
*/
|
|
8
|
+
import { writeFile, access, readFile, readdir, stat } from 'node:fs/promises';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { execSync } from 'node:child_process';
|
|
11
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
12
|
+
/**
|
|
13
|
+
* Convert an idea title to a kebab-case project name.
|
|
14
|
+
*/
|
|
15
|
+
export function toKebabCase(title) {
|
|
16
|
+
return title
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
19
|
+
.replace(/^-+|-+$/g, '')
|
|
20
|
+
.substring(0, 64);
|
|
21
|
+
}
|
|
22
|
+
async function fileExists(filePath) {
|
|
23
|
+
try {
|
|
24
|
+
await access(filePath);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// ── Build Context ────────────────────────────────────────────────────────────
|
|
32
|
+
/**
|
|
33
|
+
* Build a ScaffoldContext from a workshop session.
|
|
34
|
+
*/
|
|
35
|
+
export function buildScaffoldContext(session, outputDir, templateEntry) {
|
|
36
|
+
const idea = session.ideas?.find((i) => i.id === session.selection?.ideaId);
|
|
37
|
+
const ideaTitle = idea?.title ?? 'AI PoC';
|
|
38
|
+
const ideaDescription = idea?.description ?? 'A proof-of-concept AI application.';
|
|
39
|
+
const planSummary = session.plan?.architectureNotes
|
|
40
|
+
? session.plan.architectureNotes
|
|
41
|
+
: (session.plan?.milestones?.map((m) => m.title).join(', ') ?? 'See plan for details');
|
|
42
|
+
const techStack = templateEntry?.techStack
|
|
43
|
+
? { ...templateEntry.techStack }
|
|
44
|
+
: {
|
|
45
|
+
language: 'TypeScript',
|
|
46
|
+
runtime: 'Node.js 20',
|
|
47
|
+
testRunner: 'npm test',
|
|
48
|
+
buildCommand: 'npm run build',
|
|
49
|
+
framework: undefined,
|
|
50
|
+
};
|
|
51
|
+
// Infer framework from plan if present
|
|
52
|
+
if (session.plan?.architectureNotes) {
|
|
53
|
+
const notes = session.plan.architectureNotes.toLowerCase();
|
|
54
|
+
if (notes.includes('express'))
|
|
55
|
+
techStack.framework = 'Express';
|
|
56
|
+
else if (notes.includes('fastapi'))
|
|
57
|
+
techStack.framework = 'FastAPI';
|
|
58
|
+
else if (notes.includes('next'))
|
|
59
|
+
techStack.framework = 'Next.js';
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
projectName: toKebabCase(ideaTitle),
|
|
63
|
+
ideaTitle,
|
|
64
|
+
ideaDescription,
|
|
65
|
+
techStack,
|
|
66
|
+
planSummary,
|
|
67
|
+
sessionId: session.sessionId,
|
|
68
|
+
outputDir,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// ── Git Initialization ───────────────────────────────────────────────────────
|
|
72
|
+
/**
|
|
73
|
+
* Initialize a local git repository in the output directory.
|
|
74
|
+
* Creates an initial commit with all scaffold files.
|
|
75
|
+
*
|
|
76
|
+
* @param outputDir The directory to initialize git in
|
|
77
|
+
* @returns true if successful, false otherwise
|
|
78
|
+
*/
|
|
79
|
+
export async function initializeGitRepo(outputDir) {
|
|
80
|
+
try {
|
|
81
|
+
const gitDir = join(outputDir, '.git');
|
|
82
|
+
const exists = await fileExists(gitDir);
|
|
83
|
+
if (exists) {
|
|
84
|
+
return true; // Already initialized
|
|
85
|
+
}
|
|
86
|
+
execSync('git init', { cwd: outputDir, stdio: 'ignore' });
|
|
87
|
+
execSync('git add .', { cwd: outputDir, stdio: 'ignore' });
|
|
88
|
+
execSync('git commit -m "chore: initial scaffold from sofIA"', {
|
|
89
|
+
cwd: outputDir,
|
|
90
|
+
stdio: 'ignore',
|
|
91
|
+
env: {
|
|
92
|
+
...process.env,
|
|
93
|
+
GIT_AUTHOR_NAME: 'sofIA',
|
|
94
|
+
GIT_AUTHOR_EMAIL: 'sofia@workshop.local',
|
|
95
|
+
GIT_COMMITTER_NAME: 'sofIA',
|
|
96
|
+
GIT_COMMITTER_EMAIL: 'sofia@workshop.local',
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
catch (_err) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// ── TODO Scanning ────────────────────────────────────────────────────────────
|
|
106
|
+
/**
|
|
107
|
+
* Scan scaffold files for TODO markers and update .sofia-metadata.json.
|
|
108
|
+
*/
|
|
109
|
+
export async function scanAndRecordTodos(outputDir) {
|
|
110
|
+
const markers = [];
|
|
111
|
+
async function scanDir(dir, base) {
|
|
112
|
+
let entries;
|
|
113
|
+
try {
|
|
114
|
+
entries = await readdir(dir);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
if (entry === 'node_modules' || entry === '.git' || entry === 'dist')
|
|
121
|
+
continue;
|
|
122
|
+
const full = join(dir, entry);
|
|
123
|
+
const rel = base ? `${base}/${entry}` : entry;
|
|
124
|
+
let s;
|
|
125
|
+
try {
|
|
126
|
+
s = await stat(full);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (s.isDirectory()) {
|
|
132
|
+
await scanDir(full, rel);
|
|
133
|
+
}
|
|
134
|
+
else if (s.isFile()) {
|
|
135
|
+
try {
|
|
136
|
+
const content = await readFile(full, 'utf-8');
|
|
137
|
+
const lines = content.split('\n');
|
|
138
|
+
for (let i = 0; i < lines.length; i++) {
|
|
139
|
+
if (lines[i].includes('TODO:')) {
|
|
140
|
+
markers.push(`${rel}:${i + 1}: ${lines[i].trim()}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// skip binary or unreadable files
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
await scanDir(outputDir, '');
|
|
151
|
+
const todos = {
|
|
152
|
+
totalInitial: markers.length,
|
|
153
|
+
remaining: markers.length,
|
|
154
|
+
markers,
|
|
155
|
+
};
|
|
156
|
+
// Update .sofia-metadata.json with TODO info
|
|
157
|
+
const metadataPath = join(outputDir, '.sofia-metadata.json');
|
|
158
|
+
try {
|
|
159
|
+
const raw = await readFile(metadataPath, 'utf-8');
|
|
160
|
+
const metadata = JSON.parse(raw);
|
|
161
|
+
metadata.todos = todos;
|
|
162
|
+
await writeFile(metadataPath, JSON.stringify(metadata, null, 2) + '\n', 'utf-8');
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Metadata file may not exist yet
|
|
166
|
+
}
|
|
167
|
+
return todos;
|
|
168
|
+
}
|
|
169
|
+
// ── Output Validator ─────────────────────────────────────────────────────────
|
|
170
|
+
/**
|
|
171
|
+
* Validate that a scaffold directory meets the poc-output contract requirements.
|
|
172
|
+
*/
|
|
173
|
+
export async function validatePocOutput(outputDir) {
|
|
174
|
+
const requiredFiles = [
|
|
175
|
+
'package.json',
|
|
176
|
+
'README.md',
|
|
177
|
+
'tsconfig.json',
|
|
178
|
+
'.gitignore',
|
|
179
|
+
'.sofia-metadata.json',
|
|
180
|
+
];
|
|
181
|
+
const missingFiles = [];
|
|
182
|
+
const errors = [];
|
|
183
|
+
for (const file of requiredFiles) {
|
|
184
|
+
const exists = await fileExists(join(outputDir, file));
|
|
185
|
+
if (!exists) {
|
|
186
|
+
missingFiles.push(file);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (!missingFiles.includes('package.json')) {
|
|
190
|
+
try {
|
|
191
|
+
const pkgContent = await readFile(join(outputDir, 'package.json'), 'utf-8');
|
|
192
|
+
const pkg = JSON.parse(pkgContent);
|
|
193
|
+
if (!pkg.scripts?.test) {
|
|
194
|
+
errors.push('package.json is missing "test" script');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
errors.push('package.json is not valid JSON');
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
let hasSrcTs = false;
|
|
202
|
+
try {
|
|
203
|
+
const srcFiles = await readdir(join(outputDir, 'src'));
|
|
204
|
+
hasSrcTs = srcFiles.some((f) => f.endsWith('.ts'));
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// src/ doesn't exist
|
|
208
|
+
}
|
|
209
|
+
if (!hasSrcTs) {
|
|
210
|
+
errors.push('No TypeScript files found in src/');
|
|
211
|
+
}
|
|
212
|
+
let hasTestFile = false;
|
|
213
|
+
try {
|
|
214
|
+
const testFiles = await readdir(join(outputDir, 'tests'));
|
|
215
|
+
hasTestFile = testFiles.some((f) => f.endsWith('.test.ts'));
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// tests/ doesn't exist
|
|
219
|
+
}
|
|
220
|
+
if (!hasTestFile) {
|
|
221
|
+
errors.push('No test files found in tests/');
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
valid: missingFiles.length === 0 && errors.length === 0,
|
|
225
|
+
missingFiles,
|
|
226
|
+
errors,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
@@ -12,7 +12,7 @@ import { join } from 'node:path';
|
|
|
12
12
|
import { createActivityEvent } from '../shared/events.js';
|
|
13
13
|
// McpManager import removed - accessed via McpContextEnricher.mcpManager public property
|
|
14
14
|
import { exportWorkshopDocs } from '../sessions/exportWriter.js';
|
|
15
|
-
import {
|
|
15
|
+
import { initializeGitRepo, scanAndRecordTodos, validatePocOutput } from './pocUtils.js';
|
|
16
16
|
import { generateDynamicScaffold } from './dynamicScaffolder.js';
|
|
17
17
|
import { TestRunner } from './testRunner.js';
|
|
18
18
|
import { CodeGenerator } from './codeGenerator.js';
|
|
@@ -141,30 +141,11 @@ export class RalphLoop {
|
|
|
141
141
|
spinner?.startThinking();
|
|
142
142
|
const scaffoldStart = Date.now();
|
|
143
143
|
try {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
createdFiles: legacyResult.createdFiles,
|
|
150
|
-
techStack: scaffoldCtx.techStack
|
|
151
|
-
? {
|
|
152
|
-
language: scaffoldCtx.techStack.language ?? 'unknown',
|
|
153
|
-
runtime: scaffoldCtx.techStack.runtime ?? 'unknown',
|
|
154
|
-
testRunner: scaffoldCtx.techStack.testRunner ?? 'unknown',
|
|
155
|
-
framework: scaffoldCtx.techStack.framework,
|
|
156
|
-
buildCommand: scaffoldCtx.techStack.buildCommand,
|
|
157
|
-
}
|
|
158
|
-
: { language: 'unknown', runtime: 'unknown', testRunner: 'unknown' },
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
scaffoldResult = await generateDynamicScaffold({
|
|
163
|
-
client,
|
|
164
|
-
session,
|
|
165
|
-
outputDir,
|
|
166
|
-
});
|
|
167
|
-
}
|
|
144
|
+
scaffoldResult = await generateDynamicScaffold({
|
|
145
|
+
client,
|
|
146
|
+
session,
|
|
147
|
+
outputDir,
|
|
148
|
+
});
|
|
168
149
|
techStack = scaffoldResult.techStack;
|
|
169
150
|
}
|
|
170
151
|
catch (err) {
|
|
@@ -193,7 +174,7 @@ export class RalphLoop {
|
|
|
193
174
|
io.writeActivity('⚠️ Could not export workshop documentation');
|
|
194
175
|
}
|
|
195
176
|
// Initialize local git repository
|
|
196
|
-
const gitInitialized = await
|
|
177
|
+
const gitInitialized = await initializeGitRepo(outputDir);
|
|
197
178
|
if (gitInitialized) {
|
|
198
179
|
io.writeActivity('✓ Initialized git repository with initial commit');
|
|
199
180
|
io.writeActivity('');
|
|
@@ -404,7 +385,7 @@ export class RalphLoop {
|
|
|
404
385
|
}
|
|
405
386
|
// FR-022: Rescan TODO markers after applying changes
|
|
406
387
|
try {
|
|
407
|
-
await
|
|
388
|
+
await scanAndRecordTodos(outputDir);
|
|
408
389
|
}
|
|
409
390
|
catch {
|
|
410
391
|
// Non-critical — ignore scanning errors
|