@hasna/skills 0.1.12 ā 0.1.14
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/bin/index.js +639 -291
- package/bin/mcp.js +301 -180
- package/dist/index.d.ts +4 -2
- package/dist/index.js +190 -2
- package/dist/lib/config.d.ts +27 -0
- package/dist/lib/config.test.d.ts +1 -0
- package/dist/lib/installer.d.ts +25 -0
- package/dist/lib/registry.d.ts +4 -0
- package/dist/types/api.d.ts +74 -0
- package/package.json +5 -2
- package/skills/skill-academic-journal-matcher/bin/cli.ts +34 -0
- package/skills/skill-action-item-router/bin/cli.ts +34 -0
- package/skills/skill-ad-creative-generator/bin/cli.ts +34 -0
- package/skills/skill-advanced-math/bin/cli.ts +34 -0
- package/skills/skill-analyze-data/bin/cli.ts +19 -0
- package/skills/skill-anomaly-investigator/bin/cli.ts +34 -0
- package/skills/skill-api-test-suite/bin/cli.ts +34 -0
- package/skills/skill-apidocs/bin/cli.ts +87 -0
- package/skills/skill-audio-cleanup-lab/bin/cli.ts +6 -0
- package/skills/skill-audiobook-chapter-proofer/bin/cli.ts +34 -0
- package/skills/skill-banner-ad-suite/bin/cli.ts +34 -0
- package/skills/skill-benchmark-finder/bin/cli.ts +34 -0
- package/skills/skill-bio-sequence-tool/bin/cli.ts +34 -0
- package/skills/skill-blog-topic-cluster/bin/cli.ts +34 -0
- package/skills/skill-brand-style-guide/bin/cli.ts +19 -0
- package/skills/skill-brand-voice-audit/bin/cli.ts +34 -0
- package/skills/skill-budget-variance-analyzer/bin/cli.ts +6 -0
- package/skills/skill-businessactivity/bin/cli.ts +28 -0
- package/skills/skill-calendar-events/bin/cli.ts +34 -0
- package/skills/skill-campaign-metric-brief/bin/cli.ts +34 -0
- package/skills/skill-campaign-moodboard/bin/cli.ts +34 -0
- package/skills/skill-caption-style-stylist/bin/cli.ts +34 -0
- package/skills/skill-chemistry-calculator/bin/cli.ts +34 -0
- package/skills/skill-churn-risk-notifier/bin/cli.ts +34 -0
- package/skills/skill-citation-formatter/bin/cli.ts +34 -0
- package/skills/skill-classroom-newsletter-kit/bin/cli.ts +34 -0
- package/skills/skill-color-palette-harmonizer/bin/cli.ts +34 -0
- package/skills/skill-competitor-ad-analyzer/bin/cli.ts +34 -0
- package/skills/skill-compliance-copy-check/bin/cli.ts +34 -0
- package/skills/skill-compliance-report-pack/bin/cli.ts +34 -0
- package/skills/skill-compress-video/bin/cli.ts +19 -0
- package/skills/skill-consolelog/bin/cli.ts +884 -0
- package/skills/skill-contract-plainlanguage/bin/cli.ts +34 -0
- package/skills/skill-copytone-translator/bin/cli.ts +34 -0
- package/skills/skill-create-blog-article/bin/cli.ts +34 -0
- package/skills/skill-create-ebook/bin/cli.ts +34 -0
- package/skills/skill-crm-note-enhancer/bin/cli.ts +34 -0
- package/skills/skill-customer-journey-mapper/bin/cli.ts +34 -0
- package/skills/skill-dashboard-builder/bin/cli.ts +34 -0
- package/skills/skill-dashboard-narrator/bin/cli.ts +34 -0
- package/skills/skill-data-anonymizer/bin/cli.ts +34 -0
- package/skills/skill-database-explorer/bin/cli.ts +34 -0
- package/skills/skill-dataset-health-check/bin/cli.ts +34 -0
- package/skills/skill-decision-journal/bin/cli.ts +34 -0
- package/skills/skill-delegation-brief-writer/bin/cli.ts +34 -0
- package/skills/skill-destination-briefing/bin/cli.ts +34 -0
- package/skills/skill-diff-viewer/bin/cli.ts +34 -0
- package/skills/skill-domainpurchase/SKILL.md +46 -0
- package/skills/skill-domainpurchase/bin/cli.ts +683 -0
- package/skills/skill-domainsearch/SKILL.md +41 -0
- package/skills/skill-domainsearch/bin/cli.ts +410 -0
- package/skills/skill-educational-resource-finder/bin/cli.ts +34 -0
- package/skills/skill-email-campaign/bin/cli.ts +34 -0
- package/skills/skill-exam-readiness-check/bin/cli.ts +34 -0
- package/skills/skill-experiment-power-calculator/bin/cli.ts +34 -0
- package/skills/skill-extract-audio/bin/cli.ts +19 -0
- package/skills/skill-extract-frames/bin/cli.ts +34 -0
- package/skills/skill-extract-invoice/bin/cli.ts +34 -0
- package/skills/skill-family-activity-curator/bin/cli.ts +34 -0
- package/skills/skill-faq-packager/bin/cli.ts +34 -0
- package/skills/skill-feedback-survey-designer/bin/cli.ts +34 -0
- package/skills/skill-field-trip-planner/bin/cli.ts +34 -0
- package/skills/skill-file-organizer/bin/cli.ts +34 -0
- package/skills/skill-folder-tree/bin/cli.ts +34 -0
- package/skills/skill-forecast-scenario-lab/bin/cli.ts +34 -0
- package/skills/skill-form-filler/bin/cli.ts +34 -0
- package/skills/skill-generate-api-client/bin/cli.ts +34 -0
- package/skills/skill-generate-book-cover/bin/cli.ts +34 -0
- package/skills/skill-generate-chart/bin/cli.ts +34 -0
- package/skills/skill-generate-diagram/bin/cli.ts +34 -0
- package/skills/skill-generate-dockerfile/bin/cli.ts +34 -0
- package/skills/skill-generate-documentation/bin/cli.ts +34 -0
- package/skills/skill-generate-docx/bin/cli.ts +6 -0
- package/skills/skill-generate-env/bin/cli.ts +34 -0
- package/skills/skill-generate-excel/bin/cli.ts +34 -0
- package/skills/skill-generate-favicon/bin/cli.ts +34 -0
- package/skills/skill-generate-mock-data/bin/cli.ts +34 -0
- package/skills/skill-generate-pdf/bin/cli.ts +6 -0
- package/skills/skill-generate-pr-description/bin/cli.ts +34 -0
- package/skills/skill-generate-presentation/bin/cli.ts +34 -0
- package/skills/skill-generate-qrcode/bin/cli.ts +34 -0
- package/skills/skill-generate-regex/bin/cli.ts +34 -0
- package/skills/skill-generate-resume/bin/cli.ts +34 -0
- package/skills/skill-generate-sitemap/bin/cli.ts +34 -0
- package/skills/skill-generate-social-posts/bin/cli.ts +34 -0
- package/skills/skill-generate-sql/bin/cli.ts +34 -0
- package/skills/skill-gif-maker/bin/cli.ts +34 -0
- package/skills/skill-github-manager/bin/cli.ts +34 -0
- package/skills/skill-gmail/bin/cli.ts +34 -0
- package/skills/skill-goal-quarterly-roadmap/bin/cli.ts +34 -0
- package/skills/skill-grant-application-drafter/bin/cli.ts +34 -0
- package/skills/skill-grocery-basket-optimizer/bin/cli.ts +34 -0
- package/skills/skill-guest-communication-suite/bin/cli.ts +34 -0
- package/skills/skill-habit-reflection-digest/bin/cli.ts +34 -0
- package/skills/skill-highlight-reel-generator/bin/cli.ts +34 -0
- package/skills/skill-homework-feedback-coach/bin/cli.ts +34 -0
- package/skills/skill-hook/bunfig.toml +5 -0
- package/skills/skill-household-maintenance-mgr/bin/cli.ts +34 -0
- package/skills/skill-http-server/bin/cli.ts +34 -0
- package/skills/skill-implementation/bunfig.toml +5 -0
- package/skills/skill-implementation-agent/bin/cli.ts +34 -0
- package/skills/skill-implementation-plan/bin/cli.ts +34 -0
- package/skills/skill-implementation-todo/bin/cli.ts +34 -0
- package/skills/skill-inbox-priority-planner/bin/cli.ts +34 -0
- package/skills/skill-invoice/bin/cli.ts +20 -0
- package/skills/skill-invoice-dispute-helper/bin/cli.ts +34 -0
- package/skills/skill-itinerary-architect/bin/cli.ts +34 -0
- package/skills/skill-jingle-composer/bin/cli.ts +34 -0
- package/skills/skill-kpi-digest-generator/bin/cli.ts +34 -0
- package/skills/skill-lab-notebook-formatter/bin/cli.ts +34 -0
- package/skills/skill-landing-page-copy/bin/cli.ts +34 -0
- package/skills/skill-latex-table-generator/bin/cli.ts +34 -0
- package/skills/skill-learning-style-profiler/bin/cli.ts +34 -0
- package/skills/skill-lesson-plan-customizer/bin/cli.ts +34 -0
- package/skills/skill-livestream-runofshow/bin/cli.ts +34 -0
- package/skills/skill-longform-structurer/bin/cli.ts +34 -0
- package/skills/skill-lorem-generator/bin/cli.ts +34 -0
- package/skills/skill-managehook/bin/cli.ts +241 -0
- package/skills/skill-managemcp/bin/cli.ts +241 -0
- package/skills/skill-manageskill/bin/cli.ts +241 -0
- package/skills/skill-markdown-validator/bin/cli.ts +34 -0
- package/skills/skill-mcp-builder/bin/cli.ts +34 -0
- package/skills/skill-meal-plan-designer/bin/cli.ts +34 -0
- package/skills/skill-meeting-insight-summarizer/bin/cli.ts +34 -0
- package/skills/skill-merge-pdfs/bin/cli.ts +34 -0
- package/skills/skill-microcopy-generator/bin/cli.ts +34 -0
- package/skills/skill-mindfulness-prompt-cache/bin/cli.ts +34 -0
- package/skills/skill-notion-manager/bin/cli.ts +34 -0
- package/skills/skill-onboarding-sequence-builder/bin/cli.ts +34 -0
- package/skills/skill-onsite-ops-checklist/bin/cli.ts +34 -0
- package/skills/skill-outreach-cadence-designer/bin/cli.ts +34 -0
- package/skills/skill-packaging-concept-studio/bin/cli.ts +34 -0
- package/skills/skill-packing-plan-pro/bin/cli.ts +34 -0
- package/skills/skill-parent-teacher-brief/bin/cli.ts +34 -0
- package/skills/skill-partner-kit-assembler/bin/cli.ts +34 -0
- package/skills/skill-payroll-change-prepper/bin/cli.ts +34 -0
- package/skills/skill-persona-based-adwriter/bin/cli.ts +34 -0
- package/skills/skill-persona-generator/bin/cli.ts +34 -0
- package/skills/skill-personal-daily-ops/bin/cli.ts +34 -0
- package/skills/skill-pet-care-scheduler/bin/cli.ts +34 -0
- package/skills/skill-podcast-show-notes/bin/cli.ts +34 -0
- package/skills/skill-presentation-theme-maker/bin/cli.ts +34 -0
- package/skills/skill-press-release-drafter/bin/cli.ts +34 -0
- package/skills/skill-print-collateral-designer/bin/cli.ts +34 -0
- package/skills/skill-procurement-scorecard/bin/cli.ts +34 -0
- package/skills/skill-product-demo-script/bin/cli.ts +34 -0
- package/skills/skill-product-mockup/bin/cli.ts +34 -0
- package/skills/skill-project-retro-companion/bin/cli.ts +34 -0
- package/skills/skill-proposal-redline-advisor/bin/cli.ts +34 -0
- package/skills/skill-regex-tester/bin/cli.ts +34 -0
- package/skills/skill-remove-background/bin/cli.ts +34 -0
- package/skills/skill-risk-disclosure-kit/bin/cli.ts +34 -0
- package/skills/skill-roi-comparison-tool/bin/cli.ts +34 -0
- package/skills/skill-sales-call-recapper/bin/cli.ts +34 -0
- package/skills/skill-salescopy/bin/cli.ts +20 -0
- package/skills/skill-scaffold-project/bin/cli.ts +34 -0
- package/skills/skill-scholarship-tracker/bin/cli.ts +34 -0
- package/skills/skill-scientific-figure-check/bin/cli.ts +34 -0
- package/skills/skill-seating-chart-maker/bin/cli.ts +34 -0
- package/skills/skill-security-audit/bin/cli.ts +34 -0
- package/skills/skill-seo-brief-builder/bin/cli.ts +34 -0
- package/skills/skill-slack-assistant/bin/cli.ts +34 -0
- package/skills/skill-sleep-routine-analyzer/bin/cli.ts +34 -0
- package/skills/skill-social-media-kit/bin/cli.ts +34 -0
- package/skills/skill-split-pdf/bin/cli.ts +34 -0
- package/skills/skill-sponsorship-proposal-lab/bin/cli.ts +34 -0
- package/skills/skill-spreadsheet-cleanroom/bin/cli.ts +34 -0
- package/skills/skill-statistical-test-selector/bin/cli.ts +34 -0
- package/skills/skill-stress-relief-playbook/bin/cli.ts +34 -0
- package/skills/skill-study-guide-builder/bin/cli.ts +34 -0
- package/skills/skill-subscription-spend-watcher/bin/cli.ts +34 -0
- package/skills/skill-subtitle/bin/cli.ts +20 -0
- package/skills/skill-survey-insight-extractor/bin/cli.ts +34 -0
- package/skills/skill-terraform-generator/bin/cli.ts +34 -0
- package/skills/skill-testimonial-graphics/bin/cli.ts +34 -0
- package/skills/skill-timesheet/bin/cli.ts +47 -0
- package/skills/skill-travel-budget-balancer/bin/cli.ts +34 -0
- package/skills/skill-validate-config/bin/cli.ts +34 -0
- package/skills/skill-video-cut-suggester/bin/cli.ts +34 -0
- package/skills/skill-video-downloader/bin/cli.ts +34 -0
- package/skills/skill-video-thumbnail/bin/cli.ts +34 -0
- package/skills/skill-voiceover-casting-assistant/bin/cli.ts +34 -0
- package/skills/skill-watermark/bin/cli.ts +34 -0
- package/skills/skill-webcrawling/bin/cli.ts +21 -0
- package/skills/skill-webinar-script-coach/bin/cli.ts +34 -0
- package/skills/skill-wellness-progress-reporter/bin/cli.ts +34 -0
- package/skills/skill-workout-cycle-planner/bin/cli.ts +34 -0
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { createTables } from "../src/db/schema";
|
|
4
|
+
import { ensureInstalled } from "../src/lib/installer";
|
|
5
|
+
|
|
6
|
+
// Ensure service directory exists on startup
|
|
7
|
+
ensureInstalled();
|
|
8
|
+
import {
|
|
9
|
+
createApp,
|
|
10
|
+
getAllApps,
|
|
11
|
+
getAppById,
|
|
12
|
+
getAppByName,
|
|
13
|
+
updateApp,
|
|
14
|
+
deleteApp,
|
|
15
|
+
createPage,
|
|
16
|
+
getPagesByAppId,
|
|
17
|
+
getPageByAppAndPath,
|
|
18
|
+
deletePage,
|
|
19
|
+
createScan,
|
|
20
|
+
getScans,
|
|
21
|
+
getScanById,
|
|
22
|
+
updateScan,
|
|
23
|
+
getConsoleLogs,
|
|
24
|
+
countConsoleLogs,
|
|
25
|
+
createConsoleLogsBatch,
|
|
26
|
+
getLogsByLevel,
|
|
27
|
+
getLastScanForApp,
|
|
28
|
+
createScreenshot,
|
|
29
|
+
getScreenshotsByScanId,
|
|
30
|
+
} from "../src/db/queries";
|
|
31
|
+
import { getDb, closeConnection } from "../src/db/index";
|
|
32
|
+
import { ConsoleMonitor, getMonitor, shutdownMonitor } from "../src/lib/monitor";
|
|
33
|
+
import {
|
|
34
|
+
loadConfig,
|
|
35
|
+
saveConfig,
|
|
36
|
+
getConfigPath,
|
|
37
|
+
isHeadless,
|
|
38
|
+
getDefaultTimeout,
|
|
39
|
+
} from "../src/lib/config";
|
|
40
|
+
import {
|
|
41
|
+
startWatching,
|
|
42
|
+
stopWatching,
|
|
43
|
+
getWatchStatus,
|
|
44
|
+
stopAllWatchers,
|
|
45
|
+
} from "../src/lib/watcher";
|
|
46
|
+
import {
|
|
47
|
+
install,
|
|
48
|
+
uninstall,
|
|
49
|
+
isInstalled,
|
|
50
|
+
logScanStart,
|
|
51
|
+
logScanComplete,
|
|
52
|
+
logWatchStart,
|
|
53
|
+
logWatchStop,
|
|
54
|
+
logError,
|
|
55
|
+
ensureSnapshotsDir,
|
|
56
|
+
generateScreenshotFilename,
|
|
57
|
+
getScreenshotPath,
|
|
58
|
+
getSnapshotsDir,
|
|
59
|
+
} from "../src/lib/installer";
|
|
60
|
+
import type { ConsoleLogLevel, CreateConsoleLogInput } from "../src/lib/types";
|
|
61
|
+
|
|
62
|
+
// Helper for Commander option parsing (parseInt needs explicit radix)
|
|
63
|
+
const parseIntOption = (val: string) => parseInt(val, 10);
|
|
64
|
+
|
|
65
|
+
const program = new Command();
|
|
66
|
+
|
|
67
|
+
program
|
|
68
|
+
.name("service-consolelog")
|
|
69
|
+
.description("Monitor console logs from web applications using Playwright")
|
|
70
|
+
.version("1.0.0");
|
|
71
|
+
|
|
72
|
+
// ============ INIT/INSTALL COMMANDS ============
|
|
73
|
+
|
|
74
|
+
program
|
|
75
|
+
.command("init")
|
|
76
|
+
.description("Initialize service-consolelog in the current project")
|
|
77
|
+
.option("-f, --force", "Force reinstall even if already installed")
|
|
78
|
+
.action((options) => {
|
|
79
|
+
const result = install({ force: options.force });
|
|
80
|
+
if (result.success) {
|
|
81
|
+
console.log(`ā ${result.message}`);
|
|
82
|
+
console.log(` Created: .service-consolelog/README.md`);
|
|
83
|
+
console.log(` Created: .service-consolelog/service-consolelog.log`);
|
|
84
|
+
console.log(` Created: .service-consolelog/snapshots/`);
|
|
85
|
+
} else {
|
|
86
|
+
console.log(`ā ${result.message}`);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
program
|
|
91
|
+
.command("uninit")
|
|
92
|
+
.description("Remove service-consolelog from the current project")
|
|
93
|
+
.action(() => {
|
|
94
|
+
const result = uninstall();
|
|
95
|
+
if (result.success) {
|
|
96
|
+
console.log(`ā ${result.message}`);
|
|
97
|
+
} else {
|
|
98
|
+
console.log(`ā ${result.message}`);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
program
|
|
103
|
+
.command("status")
|
|
104
|
+
.description("Check installation status")
|
|
105
|
+
.action(() => {
|
|
106
|
+
if (isInstalled()) {
|
|
107
|
+
console.log("ā Installed in current project");
|
|
108
|
+
} else {
|
|
109
|
+
console.log("ā Not installed. Run 'service-consolelog init' to set up.");
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ============ APP COMMANDS ============
|
|
114
|
+
|
|
115
|
+
const appCmd = program.command("app").description("Manage monitored applications");
|
|
116
|
+
|
|
117
|
+
appCmd
|
|
118
|
+
.command("add")
|
|
119
|
+
.description("Add a new app to monitor")
|
|
120
|
+
.requiredOption("-n, --name <name>", "App name")
|
|
121
|
+
.requiredOption("-p, --port <port>", "App port", parseIntOption)
|
|
122
|
+
.requiredOption("-u, --url <url>", "Base URL")
|
|
123
|
+
.option("-d, --description <desc>", "Description")
|
|
124
|
+
.action(async (options) => {
|
|
125
|
+
try {
|
|
126
|
+
await ensureDb();
|
|
127
|
+
const app = await createApp({
|
|
128
|
+
name: options.name,
|
|
129
|
+
port: options.port,
|
|
130
|
+
base_url: options.url,
|
|
131
|
+
description: options.description,
|
|
132
|
+
});
|
|
133
|
+
console.log(`ā Created app "${app.name}" (ID: ${app.id})`);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
const err = error as Error;
|
|
136
|
+
if (err.message.includes("unique") || err.message.includes("duplicate")) {
|
|
137
|
+
console.error(`ā App "${options.name}" already exists`);
|
|
138
|
+
} else {
|
|
139
|
+
console.error(`ā Error: ${err.message}`);
|
|
140
|
+
}
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
appCmd
|
|
146
|
+
.command("list")
|
|
147
|
+
.description("List all apps")
|
|
148
|
+
.option("-a, --active-only", "Show only active apps")
|
|
149
|
+
.action(async (options) => {
|
|
150
|
+
await ensureDb();
|
|
151
|
+
const apps = await getAllApps(options.activeOnly);
|
|
152
|
+
|
|
153
|
+
if (apps.length === 0) {
|
|
154
|
+
console.log("No apps found. Add one with: service-consolelog app add");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.log("\nApps:\n");
|
|
159
|
+
for (const app of apps) {
|
|
160
|
+
const status = app.active ? "ā" : "ā";
|
|
161
|
+
console.log(` ${status} ${app.name} (ID: ${app.id})`);
|
|
162
|
+
console.log(` URL: ${app.base_url}`);
|
|
163
|
+
console.log(` Port: ${app.port}`);
|
|
164
|
+
if (app.description) {
|
|
165
|
+
console.log(` Description: ${app.description}`);
|
|
166
|
+
}
|
|
167
|
+
console.log();
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
appCmd
|
|
172
|
+
.command("get <idOrName>")
|
|
173
|
+
.description("Get app details")
|
|
174
|
+
.action(async (idOrName) => {
|
|
175
|
+
await ensureDb();
|
|
176
|
+
const app = await resolveApp(idOrName);
|
|
177
|
+
|
|
178
|
+
if (!app) {
|
|
179
|
+
console.error(`ā App "${idOrName}" not found`);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log(`\nApp: ${app.name}\n`);
|
|
184
|
+
console.log(` ID: ${app.id}`);
|
|
185
|
+
console.log(` URL: ${app.base_url}`);
|
|
186
|
+
console.log(` Port: ${app.port}`);
|
|
187
|
+
console.log(` Active: ${app.active ? "Yes" : "No"}`);
|
|
188
|
+
if (app.description) {
|
|
189
|
+
console.log(` Description: ${app.description}`);
|
|
190
|
+
}
|
|
191
|
+
console.log(` Created: ${app.created_at}`);
|
|
192
|
+
console.log(` Updated: ${app.updated_at}`);
|
|
193
|
+
|
|
194
|
+
const pages = await getPagesByAppId(app.id);
|
|
195
|
+
console.log(` Pages: ${pages.length}`);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
appCmd
|
|
199
|
+
.command("update <idOrName>")
|
|
200
|
+
.description("Update an app")
|
|
201
|
+
.option("-n, --name <name>", "New name")
|
|
202
|
+
.option("-p, --port <port>", "New port", parseIntOption)
|
|
203
|
+
.option("-u, --url <url>", "New base URL")
|
|
204
|
+
.option("-d, --description <desc>", "New description")
|
|
205
|
+
.option("--active <bool>", "Set active status", (v) => v === "true")
|
|
206
|
+
.action(async (idOrName, options) => {
|
|
207
|
+
await ensureDb();
|
|
208
|
+
const app = await resolveApp(idOrName);
|
|
209
|
+
|
|
210
|
+
if (!app) {
|
|
211
|
+
console.error(`ā App "${idOrName}" not found`);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const updated = await updateApp(app.id, {
|
|
216
|
+
name: options.name,
|
|
217
|
+
port: options.port,
|
|
218
|
+
base_url: options.url,
|
|
219
|
+
description: options.description,
|
|
220
|
+
active: options.active,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
console.log(`ā Updated app "${updated?.name}"`);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
appCmd
|
|
227
|
+
.command("remove <idOrName>")
|
|
228
|
+
.description("Remove an app")
|
|
229
|
+
.action(async (idOrName) => {
|
|
230
|
+
await ensureDb();
|
|
231
|
+
const app = await resolveApp(idOrName);
|
|
232
|
+
|
|
233
|
+
if (!app) {
|
|
234
|
+
console.error(`ā App "${idOrName}" not found`);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
await deleteApp(app.id);
|
|
239
|
+
console.log(`ā Removed app "${app.name}"`);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// ============ PAGE COMMANDS ============
|
|
243
|
+
|
|
244
|
+
const pageCmd = program.command("page").description("Manage monitored pages");
|
|
245
|
+
|
|
246
|
+
pageCmd
|
|
247
|
+
.command("add")
|
|
248
|
+
.description("Add a page to monitor")
|
|
249
|
+
.requiredOption("-a, --app <app>", "App name or ID")
|
|
250
|
+
.requiredOption("-p, --path <path>", "Page path (e.g., '/', '/dashboard')")
|
|
251
|
+
.option("-n, --name <name>", "Page name")
|
|
252
|
+
.option("-w, --wait-for <selector>", "CSS selector to wait for")
|
|
253
|
+
.option("-t, --timeout <ms>", "Timeout in milliseconds", parseIntOption)
|
|
254
|
+
.action(async (options) => {
|
|
255
|
+
await ensureDb();
|
|
256
|
+
const app = await resolveApp(options.app);
|
|
257
|
+
|
|
258
|
+
if (!app) {
|
|
259
|
+
console.error(`ā App "${options.app}" not found`);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const page = await createPage({
|
|
265
|
+
app_id: app.id,
|
|
266
|
+
path: options.path,
|
|
267
|
+
name: options.name,
|
|
268
|
+
wait_for: options.waitFor,
|
|
269
|
+
timeout: options.timeout,
|
|
270
|
+
});
|
|
271
|
+
console.log(`ā Added page "${page.path}" to "${app.name}" (ID: ${page.id})`);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
const err = error as Error;
|
|
274
|
+
if (err.message.includes("unique") || err.message.includes("duplicate")) {
|
|
275
|
+
console.error(`ā Page "${options.path}" already exists for "${app.name}"`);
|
|
276
|
+
} else {
|
|
277
|
+
console.error(`ā Error: ${err.message}`);
|
|
278
|
+
}
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
pageCmd
|
|
284
|
+
.command("list")
|
|
285
|
+
.description("List pages for an app")
|
|
286
|
+
.requiredOption("-a, --app <app>", "App name or ID")
|
|
287
|
+
.option("--active-only", "Show only active pages")
|
|
288
|
+
.action(async (options) => {
|
|
289
|
+
await ensureDb();
|
|
290
|
+
const app = await resolveApp(options.app);
|
|
291
|
+
|
|
292
|
+
if (!app) {
|
|
293
|
+
console.error(`ā App "${options.app}" not found`);
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const pages = await getPagesByAppId(app.id, options.activeOnly);
|
|
298
|
+
|
|
299
|
+
if (pages.length === 0) {
|
|
300
|
+
console.log(`No pages for "${app.name}". Add one with: service-consolelog page add`);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
console.log(`\nPages for "${app.name}":\n`);
|
|
305
|
+
for (const page of pages) {
|
|
306
|
+
const status = page.active ? "ā" : "ā";
|
|
307
|
+
console.log(` ${status} ${page.path} (ID: ${page.id})`);
|
|
308
|
+
if (page.name) {
|
|
309
|
+
console.log(` Name: ${page.name}`);
|
|
310
|
+
}
|
|
311
|
+
if (page.wait_for) {
|
|
312
|
+
console.log(` Wait for: ${page.wait_for}`);
|
|
313
|
+
}
|
|
314
|
+
console.log(` Timeout: ${page.timeout}ms`);
|
|
315
|
+
console.log();
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
pageCmd
|
|
320
|
+
.command("remove <id>")
|
|
321
|
+
.description("Remove a page")
|
|
322
|
+
.action(async (id) => {
|
|
323
|
+
await ensureDb();
|
|
324
|
+
const success = await deletePage(parseInt(id, 10));
|
|
325
|
+
|
|
326
|
+
if (success) {
|
|
327
|
+
console.log(`ā Removed page ID ${id}`);
|
|
328
|
+
} else {
|
|
329
|
+
console.error(`ā Page ID ${id} not found`);
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// ============ SCAN COMMANDS ============
|
|
335
|
+
|
|
336
|
+
program
|
|
337
|
+
.command("scan")
|
|
338
|
+
.description("Scan pages for console logs")
|
|
339
|
+
.option("-a, --app <app>", "App name or ID")
|
|
340
|
+
.option("--all", "Scan all active apps")
|
|
341
|
+
.option("-p, --pages <paths...>", "Specific paths to scan")
|
|
342
|
+
.option("--headless <bool>", "Run headless", (v) => v !== "false")
|
|
343
|
+
.action(async (options) => {
|
|
344
|
+
await ensureDb();
|
|
345
|
+
|
|
346
|
+
if (!options.app && !options.all) {
|
|
347
|
+
console.error("ā Specify --app or --all");
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const headless = options.headless ?? isHeadless();
|
|
352
|
+
const monitor = getMonitor(headless);
|
|
353
|
+
await monitor.start();
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
if (options.all) {
|
|
357
|
+
const apps = await getAllApps(true);
|
|
358
|
+
for (const app of apps) {
|
|
359
|
+
await scanApp(app.id, monitor, options.pages);
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
const app = await resolveApp(options.app);
|
|
363
|
+
if (!app) {
|
|
364
|
+
console.error(`ā App "${options.app}" not found`);
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
await scanApp(app.id, monitor, options.pages);
|
|
368
|
+
}
|
|
369
|
+
} finally {
|
|
370
|
+
await shutdownMonitor();
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// ============ LOG COMMANDS ============
|
|
375
|
+
|
|
376
|
+
program
|
|
377
|
+
.command("logs")
|
|
378
|
+
.description("Query console logs")
|
|
379
|
+
.requiredOption("-a, --app <app>", "App name or ID")
|
|
380
|
+
.option("-l, --level <level>", "Filter by level (error, warn, info, log, debug)")
|
|
381
|
+
.option("-s, --scan <id>", "Filter by scan ID", parseIntOption)
|
|
382
|
+
.option("--since <date>", "Filter logs since date (ISO format)")
|
|
383
|
+
.option("--until <date>", "Filter logs until date (ISO format)")
|
|
384
|
+
.option("--limit <n>", "Limit results", parseIntOption, 50)
|
|
385
|
+
.option("--offset <n>", "Offset results", parseIntOption, 0)
|
|
386
|
+
.action(async (options) => {
|
|
387
|
+
await ensureDb();
|
|
388
|
+
const app = await resolveApp(options.app);
|
|
389
|
+
|
|
390
|
+
if (!app) {
|
|
391
|
+
console.error(`ā App "${options.app}" not found`);
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const logs = await getConsoleLogs({
|
|
396
|
+
app_id: app.id,
|
|
397
|
+
level: options.level as ConsoleLogLevel,
|
|
398
|
+
scan_id: isNaN(options.scan) ? undefined : options.scan,
|
|
399
|
+
since: options.since,
|
|
400
|
+
until: options.until,
|
|
401
|
+
limit: isNaN(options.limit) ? 50 : options.limit,
|
|
402
|
+
offset: isNaN(options.offset) ? 0 : options.offset,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
if (logs.length === 0) {
|
|
406
|
+
console.log("No logs found matching criteria.");
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
console.log(`\nLogs for "${app.name}" (${logs.length} results):\n`);
|
|
411
|
+
|
|
412
|
+
for (const log of logs) {
|
|
413
|
+
const levelIcon = getLevelIcon(log.level);
|
|
414
|
+
console.log(`${levelIcon} [${log.timestamp}] ${log.level.toUpperCase()}`);
|
|
415
|
+
console.log(` ${log.message}`);
|
|
416
|
+
if (log.source_url) {
|
|
417
|
+
console.log(` at ${log.source_url}:${log.line_number || "?"}:${log.column_number || "?"}`);
|
|
418
|
+
}
|
|
419
|
+
if (log.stack_trace) {
|
|
420
|
+
console.log(` Stack: ${log.stack_trace.split("\n")[0]}...`);
|
|
421
|
+
}
|
|
422
|
+
console.log();
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
program
|
|
427
|
+
.command("history")
|
|
428
|
+
.description("View scan history")
|
|
429
|
+
.requiredOption("-a, --app <app>", "App name or ID")
|
|
430
|
+
.option("--limit <n>", "Limit results", parseIntOption, 10)
|
|
431
|
+
.action(async (options) => {
|
|
432
|
+
await ensureDb();
|
|
433
|
+
const app = await resolveApp(options.app);
|
|
434
|
+
|
|
435
|
+
if (!app) {
|
|
436
|
+
console.error(`ā App "${options.app}" not found`);
|
|
437
|
+
process.exit(1);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const scans = await getScans({ app_id: app.id, limit: options.limit });
|
|
441
|
+
|
|
442
|
+
if (scans.length === 0) {
|
|
443
|
+
console.log(`No scan history for "${app.name}".`);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
console.log(`\nScan history for "${app.name}":\n`);
|
|
448
|
+
|
|
449
|
+
for (const scan of scans) {
|
|
450
|
+
const statusIcon =
|
|
451
|
+
scan.status === "completed" ? "ā" : scan.status === "failed" ? "ā" : "ā";
|
|
452
|
+
console.log(
|
|
453
|
+
` ${statusIcon} Scan #${scan.id} - ${scan.status.toUpperCase()}`
|
|
454
|
+
);
|
|
455
|
+
console.log(` Started: ${scan.started_at}`);
|
|
456
|
+
if (scan.completed_at) {
|
|
457
|
+
console.log(` Completed: ${scan.completed_at}`);
|
|
458
|
+
}
|
|
459
|
+
console.log(
|
|
460
|
+
` Pages: ${scan.pages_scanned} | Errors: ${scan.errors_found}`
|
|
461
|
+
);
|
|
462
|
+
console.log();
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// ============ EXPORT COMMAND ============
|
|
467
|
+
|
|
468
|
+
program
|
|
469
|
+
.command("export")
|
|
470
|
+
.description("Export logs to file")
|
|
471
|
+
.requiredOption("-a, --app <app>", "App name or ID")
|
|
472
|
+
.requiredOption("-o, --output <path>", "Output file path")
|
|
473
|
+
.option("-f, --format <format>", "Format: json or csv", "json")
|
|
474
|
+
.option("-l, --level <level>", "Filter by level")
|
|
475
|
+
.option("--since <date>", "Filter logs since date")
|
|
476
|
+
.option("--until <date>", "Filter logs until date")
|
|
477
|
+
.action(async (options) => {
|
|
478
|
+
await ensureDb();
|
|
479
|
+
const app = await resolveApp(options.app);
|
|
480
|
+
|
|
481
|
+
if (!app) {
|
|
482
|
+
console.error(`ā App "${options.app}" not found`);
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const logs = await getConsoleLogs({
|
|
487
|
+
app_id: app.id,
|
|
488
|
+
level: options.level as ConsoleLogLevel,
|
|
489
|
+
since: options.since,
|
|
490
|
+
until: options.until,
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
let content: string;
|
|
494
|
+
if (options.format === "csv") {
|
|
495
|
+
const headers = [
|
|
496
|
+
"id",
|
|
497
|
+
"scan_id",
|
|
498
|
+
"page_id",
|
|
499
|
+
"level",
|
|
500
|
+
"message",
|
|
501
|
+
"source_url",
|
|
502
|
+
"line_number",
|
|
503
|
+
"column_number",
|
|
504
|
+
"timestamp",
|
|
505
|
+
];
|
|
506
|
+
const rows = logs.map((l) =>
|
|
507
|
+
[
|
|
508
|
+
l.id,
|
|
509
|
+
l.scan_id,
|
|
510
|
+
l.page_id,
|
|
511
|
+
l.level,
|
|
512
|
+
`"${l.message.replace(/"/g, '""')}"`,
|
|
513
|
+
l.source_url || "",
|
|
514
|
+
l.line_number || "",
|
|
515
|
+
l.column_number || "",
|
|
516
|
+
l.timestamp,
|
|
517
|
+
].join(",")
|
|
518
|
+
);
|
|
519
|
+
content = [headers.join(","), ...rows].join("\n");
|
|
520
|
+
} else {
|
|
521
|
+
content = JSON.stringify(logs, null, 2);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
Bun.write(options.output, content);
|
|
525
|
+
console.log(`ā Exported ${logs.length} logs to ${options.output}`);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// ============ WATCH COMMANDS ============
|
|
529
|
+
|
|
530
|
+
const watchCmd = program.command("watch").description("Continuous monitoring");
|
|
531
|
+
|
|
532
|
+
watchCmd
|
|
533
|
+
.command("start")
|
|
534
|
+
.description("Start watching an app")
|
|
535
|
+
.requiredOption("-a, --app <app>", "App name or ID")
|
|
536
|
+
.option("-i, --interval <min>", "Interval in minutes", parseIntOption, 5)
|
|
537
|
+
.action(async (options) => {
|
|
538
|
+
await ensureDb();
|
|
539
|
+
const app = await resolveApp(options.app);
|
|
540
|
+
|
|
541
|
+
if (!app) {
|
|
542
|
+
console.error(`ā App "${options.app}" not found`);
|
|
543
|
+
process.exit(1);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const intervalMs = options.interval * 60 * 1000;
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
const started = await startWatching(app.id, intervalMs);
|
|
550
|
+
if (started) {
|
|
551
|
+
console.log(`ā Started watching "${app.name}" (interval: ${options.interval}min)`);
|
|
552
|
+
logWatchStart(app.name, intervalMs);
|
|
553
|
+
|
|
554
|
+
// Keep process alive
|
|
555
|
+
console.log("Press Ctrl+C to stop watching.\n");
|
|
556
|
+
process.on("SIGINT", async () => {
|
|
557
|
+
console.log("\nStopping...");
|
|
558
|
+
await stopAllWatchers();
|
|
559
|
+
logWatchStop(app.name);
|
|
560
|
+
process.exit(0);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// Keep the process running
|
|
564
|
+
await new Promise(() => {});
|
|
565
|
+
} else {
|
|
566
|
+
console.log(`Already watching "${app.name}"`);
|
|
567
|
+
}
|
|
568
|
+
} catch (error) {
|
|
569
|
+
const err = error as Error;
|
|
570
|
+
console.error(`ā Error: ${err.message}`);
|
|
571
|
+
process.exit(1);
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
watchCmd
|
|
576
|
+
.command("stop")
|
|
577
|
+
.description("Stop watching an app")
|
|
578
|
+
.requiredOption("-a, --app <app>", "App name or ID")
|
|
579
|
+
.action(async (options) => {
|
|
580
|
+
await ensureDb();
|
|
581
|
+
const app = await resolveApp(options.app);
|
|
582
|
+
|
|
583
|
+
if (!app) {
|
|
584
|
+
console.error(`ā App "${options.app}" not found`);
|
|
585
|
+
process.exit(1);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const stopped = stopWatching(app.id);
|
|
589
|
+
if (stopped) {
|
|
590
|
+
console.log(`ā Stopped watching "${app.name}"`);
|
|
591
|
+
logWatchStop(app.name);
|
|
592
|
+
} else {
|
|
593
|
+
console.log(`Not watching "${app.name}"`);
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
watchCmd
|
|
598
|
+
.command("status")
|
|
599
|
+
.description("Check watch status")
|
|
600
|
+
.action(async () => {
|
|
601
|
+
const statuses = await getWatchStatus();
|
|
602
|
+
|
|
603
|
+
if (statuses.length === 0) {
|
|
604
|
+
console.log("No apps being watched.");
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
console.log("\nActive watchers:\n");
|
|
609
|
+
for (const status of statuses) {
|
|
610
|
+
const intervalMin = (status.intervalMs || 0) / 60000;
|
|
611
|
+
console.log(` ā ${status.appName} (ID: ${status.appId})`);
|
|
612
|
+
console.log(` Interval: ${intervalMin}min`);
|
|
613
|
+
if (status.lastScan) {
|
|
614
|
+
console.log(` Last scan: ${status.lastScan}`);
|
|
615
|
+
}
|
|
616
|
+
console.log();
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
// ============ CONFIG COMMANDS ============
|
|
621
|
+
|
|
622
|
+
program
|
|
623
|
+
.command("config")
|
|
624
|
+
.description("Manage configuration")
|
|
625
|
+
.option("--show", "Show current config")
|
|
626
|
+
.option("--path", "Show config file path")
|
|
627
|
+
.option("--set-headless <bool>", "Set headless mode", (v) => v === "true")
|
|
628
|
+
.option("--set-timeout <ms>", "Set default timeout", parseIntOption)
|
|
629
|
+
.option("--set-db-path <path>", "Set database path")
|
|
630
|
+
.option("--set-watch-interval <ms>", "Set watch interval", parseIntOption)
|
|
631
|
+
.action((options) => {
|
|
632
|
+
if (options.path) {
|
|
633
|
+
console.log(getConfigPath());
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (options.show) {
|
|
638
|
+
const config = loadConfig();
|
|
639
|
+
console.log("\nConfiguration:\n");
|
|
640
|
+
console.log(` Database path: ${config.databasePath}`);
|
|
641
|
+
console.log(` Headless: ${config.headless}`);
|
|
642
|
+
console.log(` Default timeout: ${config.defaultTimeout}ms`);
|
|
643
|
+
console.log(` Watch interval: ${config.watchInterval}ms`);
|
|
644
|
+
console.log(` Log level: ${config.logLevel}`);
|
|
645
|
+
console.log(`\n Config file: ${getConfigPath()}`);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const updates: Record<string, unknown> = {};
|
|
650
|
+
if (options.setHeadless !== undefined) {
|
|
651
|
+
updates.headless = options.setHeadless;
|
|
652
|
+
}
|
|
653
|
+
if (options.setTimeout !== undefined) {
|
|
654
|
+
updates.defaultTimeout = options.setTimeout;
|
|
655
|
+
}
|
|
656
|
+
if (options.setDbPath !== undefined) {
|
|
657
|
+
updates.databasePath = options.setDbPath;
|
|
658
|
+
}
|
|
659
|
+
if (options.setWatchInterval !== undefined) {
|
|
660
|
+
updates.watchInterval = options.setWatchInterval;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (Object.keys(updates).length > 0) {
|
|
664
|
+
saveConfig(updates);
|
|
665
|
+
console.log("ā Configuration updated");
|
|
666
|
+
} else {
|
|
667
|
+
console.log("No changes. Use --show to view config or --set-* to modify.");
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
// ============ SNAPSHOTS COMMAND ============
|
|
672
|
+
|
|
673
|
+
program
|
|
674
|
+
.command("snapshots")
|
|
675
|
+
.description("List snapshots from a scan")
|
|
676
|
+
.option("-s, --scan <id>", "Scan ID", parseIntOption)
|
|
677
|
+
.option("-a, --app <app>", "App name or ID (shows latest scan)")
|
|
678
|
+
.action(async (options) => {
|
|
679
|
+
await ensureDb();
|
|
680
|
+
|
|
681
|
+
let scanId: number | undefined = options.scan;
|
|
682
|
+
|
|
683
|
+
if (!scanId && options.app) {
|
|
684
|
+
const app = await resolveApp(options.app);
|
|
685
|
+
if (!app) {
|
|
686
|
+
console.error(`ā App "${options.app}" not found`);
|
|
687
|
+
process.exit(1);
|
|
688
|
+
}
|
|
689
|
+
const lastScan = await getLastScanForApp(app.id);
|
|
690
|
+
if (!lastScan) {
|
|
691
|
+
console.log(`No scans found for "${app.name}"`);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
scanId = lastScan.id;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (!scanId) {
|
|
698
|
+
console.error("ā Specify --scan or --app");
|
|
699
|
+
process.exit(1);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const screenshots = await getScreenshotsByScanId(scanId);
|
|
703
|
+
|
|
704
|
+
if (screenshots.length === 0) {
|
|
705
|
+
console.log("No screenshots found for this scan.");
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
console.log(`\nSnapshots for scan #${scanId}:\n`);
|
|
710
|
+
for (const ss of screenshots) {
|
|
711
|
+
console.log(` šø ${ss.filename}`);
|
|
712
|
+
console.log(` Path: ${ss.filepath}`);
|
|
713
|
+
console.log(` Created: ${ss.created_at}`);
|
|
714
|
+
console.log();
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
console.log(`Total: ${screenshots.length} screenshots`);
|
|
718
|
+
console.log(`\nTo view a screenshot, open the path in your file viewer.`);
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
// ============ SERVER COMMAND ============
|
|
722
|
+
|
|
723
|
+
program
|
|
724
|
+
.command("server")
|
|
725
|
+
.description("Start the HTTP API server")
|
|
726
|
+
.option("-p, --port <port>", "Server port", parseIntOption, 3100)
|
|
727
|
+
.action(async (options) => {
|
|
728
|
+
await ensureDb();
|
|
729
|
+
console.log(`Starting server on port ${options.port}...`);
|
|
730
|
+
console.log(`See: http://localhost:${options.port}/health`);
|
|
731
|
+
|
|
732
|
+
// Import and start server
|
|
733
|
+
const { startServer } = await import("../src/server/index");
|
|
734
|
+
startServer(options.port);
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
// ============ HELPER FUNCTIONS ============
|
|
738
|
+
|
|
739
|
+
async function ensureDb(): Promise<void> {
|
|
740
|
+
getDb();
|
|
741
|
+
await createTables();
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
async function resolveApp(idOrName: string) {
|
|
745
|
+
const id = parseInt(idOrName, 10);
|
|
746
|
+
if (!isNaN(id)) {
|
|
747
|
+
return await getAppById(id);
|
|
748
|
+
}
|
|
749
|
+
return await getAppByName(idOrName);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
async function scanApp(
|
|
753
|
+
appId: number,
|
|
754
|
+
monitor: ConsoleMonitor,
|
|
755
|
+
specificPaths?: string[]
|
|
756
|
+
): Promise<void> {
|
|
757
|
+
const app = await getAppById(appId);
|
|
758
|
+
if (!app) {
|
|
759
|
+
console.error(`ā App ID ${appId} not found`);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
let pages = await getPagesByAppId(appId, true);
|
|
764
|
+
|
|
765
|
+
if (specificPaths && specificPaths.length > 0) {
|
|
766
|
+
pages = pages.filter((p) => specificPaths.includes(p.path));
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (pages.length === 0) {
|
|
770
|
+
console.log(`No pages to scan for "${app.name}"`);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Ensure snapshots directory exists
|
|
775
|
+
const snapshotsDir = ensureSnapshotsDir();
|
|
776
|
+
|
|
777
|
+
console.log(`\nScanning "${app.name}" (${pages.length} pages)...\n`);
|
|
778
|
+
logScanStart(app.name, pages.length);
|
|
779
|
+
|
|
780
|
+
const scan = await createScan({ app_id: appId });
|
|
781
|
+
let totalErrors = 0;
|
|
782
|
+
let pagesScanned = 0;
|
|
783
|
+
const screenshotPaths: string[] = [];
|
|
784
|
+
|
|
785
|
+
for (const page of pages) {
|
|
786
|
+
const url = `${app.base_url}${page.path}`;
|
|
787
|
+
process.stdout.write(` Scanning ${page.path}... `);
|
|
788
|
+
|
|
789
|
+
// Generate screenshot filename
|
|
790
|
+
const screenshotFilename = generateScreenshotFilename(page.name, page.path);
|
|
791
|
+
const screenshotFullPath = getScreenshotPath(screenshotFilename);
|
|
792
|
+
|
|
793
|
+
try {
|
|
794
|
+
const result = await monitor.scanPage(url, {
|
|
795
|
+
timeout: page.timeout,
|
|
796
|
+
waitFor: page.wait_for || undefined,
|
|
797
|
+
captureScreenshot: true,
|
|
798
|
+
screenshotDir: screenshotFullPath,
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
const { logs, screenshotPath } = result;
|
|
802
|
+
|
|
803
|
+
// Save screenshot record if captured
|
|
804
|
+
if (screenshotPath) {
|
|
805
|
+
await createScreenshot({
|
|
806
|
+
scan_id: scan.id,
|
|
807
|
+
page_id: page.id,
|
|
808
|
+
filename: screenshotFilename,
|
|
809
|
+
filepath: screenshotPath,
|
|
810
|
+
});
|
|
811
|
+
screenshotPaths.push(screenshotPath);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (logs.length > 0) {
|
|
815
|
+
const logInputs: CreateConsoleLogInput[] = logs.map((log) => ({
|
|
816
|
+
scan_id: scan.id,
|
|
817
|
+
page_id: page.id,
|
|
818
|
+
level: log.level as ConsoleLogLevel,
|
|
819
|
+
message: log.message,
|
|
820
|
+
source_url: log.sourceUrl,
|
|
821
|
+
line_number: log.lineNumber,
|
|
822
|
+
column_number: log.columnNumber,
|
|
823
|
+
stack_trace: log.stackTrace,
|
|
824
|
+
}));
|
|
825
|
+
|
|
826
|
+
await createConsoleLogsBatch(logInputs);
|
|
827
|
+
|
|
828
|
+
const errorCount = logs.filter((l) => l.level === "error").length;
|
|
829
|
+
const warnCount = logs.filter((l) => l.level === "warn").length;
|
|
830
|
+
totalErrors += errorCount;
|
|
831
|
+
|
|
832
|
+
console.log(`${logs.length} logs (${errorCount} errors, ${warnCount} warnings)`);
|
|
833
|
+
} else {
|
|
834
|
+
console.log("clean");
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
if (screenshotPath) {
|
|
838
|
+
console.log(` šø ${screenshotPath}`);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
pagesScanned++;
|
|
842
|
+
} catch (error) {
|
|
843
|
+
const err = error as Error;
|
|
844
|
+
console.log(`failed: ${err.message}`);
|
|
845
|
+
logError(`Failed to scan ${url}: ${err.message}`);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
await updateScan(scan.id, {
|
|
850
|
+
status: "completed",
|
|
851
|
+
pages_scanned: pagesScanned,
|
|
852
|
+
errors_found: totalErrors,
|
|
853
|
+
completed_at: new Date().toISOString(),
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
console.log(`\nā Scan complete: ${pagesScanned} pages, ${totalErrors} errors`);
|
|
857
|
+
if (screenshotPaths.length > 0) {
|
|
858
|
+
console.log(`šø Screenshots saved to: ${getSnapshotsDir()}`);
|
|
859
|
+
console.log(`\nScreenshot paths:`);
|
|
860
|
+
for (const path of screenshotPaths) {
|
|
861
|
+
console.log(` ${path}`);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
console.log();
|
|
865
|
+
logScanComplete(app.name, pagesScanned, totalErrors);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function getLevelIcon(level: string): string {
|
|
869
|
+
switch (level) {
|
|
870
|
+
case "error":
|
|
871
|
+
return "ā";
|
|
872
|
+
case "warn":
|
|
873
|
+
return "ā ļø";
|
|
874
|
+
case "info":
|
|
875
|
+
return "ā¹ļø";
|
|
876
|
+
case "debug":
|
|
877
|
+
return "š";
|
|
878
|
+
default:
|
|
879
|
+
return "š";
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Run CLI
|
|
884
|
+
program.parse(process.argv);
|