jhste-skills 0.1.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/LICENSE +21 -0
- package/README.ja.md +254 -0
- package/README.ko.md +254 -0
- package/README.md +254 -0
- package/README.zh.md +254 -0
- package/adapters/claude/README.md +7 -0
- package/adapters/codex/README.md +25 -0
- package/adapters/generic/README.md +7 -0
- package/cli/baseline.mjs +32 -0
- package/cli/connect.mjs +84 -0
- package/cli/deep-scan/analyze.mjs +167 -0
- package/cli/deep-scan/collect.mjs +133 -0
- package/cli/deep-scan/report.mjs +197 -0
- package/cli/deep-scan.mjs +56 -0
- package/cli/guard/baseline.mjs +64 -0
- package/cli/guard/config.mjs +48 -0
- package/cli/guard/profile-commands.mjs +87 -0
- package/cli/guard/registry.mjs +47 -0
- package/cli/guard/reporting.mjs +165 -0
- package/cli/guard/scanners/code-health.mjs +213 -0
- package/cli/guard/scanners/data-boundary-locality.mjs +125 -0
- package/cli/guard/scanners/data-boundary.mjs +237 -0
- package/cli/guard/scanners/external-input.mjs +74 -0
- package/cli/guard/scanners/index.mjs +136 -0
- package/cli/guard/scanners/single-responsibility.mjs +205 -0
- package/cli/guard/scanners/ui-runtime.mjs +140 -0
- package/cli/guard/scanners/utils.mjs +167 -0
- package/cli/guard/scope.mjs +181 -0
- package/cli/guard.mjs +125 -0
- package/cli/hook-utils.mjs +127 -0
- package/cli/hooks.mjs +127 -0
- package/cli/index.mjs +35 -0
- package/cli/install-actions/apply-plan.mjs +39 -0
- package/cli/install-actions/bridge-writer.mjs +52 -0
- package/cli/install-actions/output.mjs +45 -0
- package/cli/install-actions/preflight.mjs +58 -0
- package/cli/install-actions/profile-writer.mjs +21 -0
- package/cli/install-actions/skills.mjs +148 -0
- package/cli/install-actions.mjs +4 -0
- package/cli/install-flow/options.mjs +234 -0
- package/cli/install-flow/output.mjs +106 -0
- package/cli/install-flow/plan-helpers.mjs +29 -0
- package/cli/install-flow/plan.mjs +200 -0
- package/cli/install-flow/prompts.mjs +210 -0
- package/cli/install-flow.mjs +16 -0
- package/cli/install.mjs +77 -0
- package/cli/json-file.mjs +39 -0
- package/cli/profile/loader.mjs +13 -0
- package/cli/profile/parser.mjs +226 -0
- package/cli/profile/schema.mjs +81 -0
- package/cli/profile/settings.mjs +45 -0
- package/cli/profile/validator.mjs +86 -0
- package/cli/profile.mjs +5 -0
- package/cli/shared/args.mjs +32 -0
- package/cli/shared/files.mjs +70 -0
- package/cli/shared/git.mjs +28 -0
- package/cli/shared/paths.mjs +27 -0
- package/cli/shared/prompt.mjs +32 -0
- package/cli/shared/templates.mjs +71 -0
- package/cli/shared/time.mjs +3 -0
- package/cli/shared.mjs +7 -0
- package/cli/sync-core.mjs +213 -0
- package/cli/sync.mjs +7 -0
- package/cli/tune.mjs +101 -0
- package/cli/uninstall.mjs +288 -0
- package/cli/update.mjs +7 -0
- package/docs/ACCEPTANCE_CHECK.md +54 -0
- package/docs/CLI.md +212 -0
- package/docs/CONFLICT_RESOLUTION.md +58 -0
- package/docs/PUBLIC_SAFETY.md +26 -0
- package/docs/RULES.md +94 -0
- package/docs/VENDORING.md +23 -0
- package/examples/profile.yaml +45 -0
- package/package.json +51 -0
- package/packs/api.yaml +13 -0
- package/packs/core.yaml +19 -0
- package/packs/crawler.yaml +8 -0
- package/packs/database.yaml +8 -0
- package/packs/web.yaml +10 -0
- package/rules/core/api_contract_compatibility.yaml +25 -0
- package/rules/core/authz_data_isolation.yaml +27 -0
- package/rules/core/build_runtime_env_safety.yaml +26 -0
- package/rules/core/external_input_validation.yaml +27 -0
- package/rules/core/file_size_advisory.yaml +28 -0
- package/rules/core/no_secret_logging.yaml +24 -0
- package/rules/core/no_silent_failure.yaml +30 -0
- package/rules/core/null_state_safety.yaml +25 -0
- package/rules/core/performance_duplicate_fetch.yaml +25 -0
- package/rules/core/public_safe_error.yaml +24 -0
- package/rules/core/responsibility_budget.yaml +44 -0
- package/rules/core/side_effect_boundary.yaml +24 -0
- package/rules/core/single_responsibility_advisory.yaml +35 -0
- package/rules/core/workflow_security.yaml +25 -0
- package/rules/core/write_safety_idempotency.yaml +25 -0
- package/rules/crawler/crawler_producer_boundary.yaml +24 -0
- package/rules/database/db_row_validation.yaml +24 -0
- package/rules/database/sql_parameter_binding.yaml +24 -0
- package/rules/nextjs/thin_api_route.yaml +24 -0
- package/rules/python/broad_exception_advisory.yaml +24 -0
- package/rules/react/component_responsibility.yaml +24 -0
- package/rules/typescript/type_escape_advisory.yaml +24 -0
- package/scripts/docs-check-data.mjs +71 -0
- package/scripts/docs-check.mjs +261 -0
- package/scripts/guard-fixtures/helpers.mjs +58 -0
- package/scripts/guard-fixtures-test.mjs +273 -0
- package/scripts/profile-fixtures-test.mjs +83 -0
- package/scripts/public-safety-check.mjs +88 -0
- package/scripts/public-safety-fixtures-test.mjs +60 -0
- package/scripts/release-gates-test.mjs +52 -0
- package/scripts/single-responsibility-fixtures-test.mjs +86 -0
- package/scripts/smoke/connect-scenarios.mjs +47 -0
- package/scripts/smoke/fixture.mjs +49 -0
- package/scripts/smoke/guard-and-hook-scenarios.mjs +211 -0
- package/scripts/smoke/helpers.mjs +51 -0
- package/scripts/smoke/install-scenarios.mjs +244 -0
- package/scripts/smoke/mode-scenarios.mjs +76 -0
- package/scripts/smoke-test.mjs +17 -0
- package/scripts/syntax-check.mjs +37 -0
- package/scripts/vendor-check.mjs +87 -0
- package/skills/codebase-design/DEEPENING.md +37 -0
- package/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/skills/codebase-design/SKILL.md +122 -0
- package/skills/diagnose/SKILL.md +125 -0
- package/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
- package/skills/diagnosing-bugs/SKILL.md +142 -0
- package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
- package/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/skills/domain-modeling/SKILL.md +82 -0
- package/skills/grill-me/SKILL.md +18 -0
- package/skills/grill-with-docs/ADR-FORMAT.md +47 -0
- package/skills/grill-with-docs/CONTEXT-FORMAT.md +60 -0
- package/skills/grill-with-docs/SKILL.md +96 -0
- package/skills/grilling/SKILL.md +18 -0
- package/skills/handoff/SKILL.md +23 -0
- package/skills/improve-codebase-architecture/DEEPENING.md +37 -0
- package/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
- package/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
- package/skills/improve-codebase-architecture/SKILL.md +93 -0
- package/skills/jhste-architecture-review/SKILL.md +28 -0
- package/skills/jhste-architecture-review/references/architecture-review.md +41 -0
- package/skills/jhste-code-quality/SKILL.md +33 -0
- package/skills/jhste-code-quality/references/code-quality.md +45 -0
- package/skills/jhste-crawler-automation/SKILL.md +23 -0
- package/skills/jhste-crawler-automation/references/crawler-automation.md +11 -0
- package/skills/jhste-db-api-boundary/SKILL.md +28 -0
- package/skills/jhste-db-api-boundary/references/db-api-boundary.md +21 -0
- package/skills/jhste-engineering-judgment/SKILL.md +107 -0
- package/skills/jhste-engineering-judgment/references/structure-templates.md +41 -0
- package/skills/jhste-red-team-review/SKILL.md +101 -0
- package/skills/jhste-red-team-review/references/red-team-review.md +83 -0
- package/skills/prototype/LOGIC.md +79 -0
- package/skills/prototype/SKILL.md +38 -0
- package/skills/prototype/UI.md +112 -0
- package/skills/setup/SKILL.md +21 -0
- package/skills/setup/references/conflict-policy.md +11 -0
- package/skills/setup/references/setup-flow.md +18 -0
- package/skills/to-issues/SKILL.md +91 -0
- package/skills/to-prd/SKILL.md +82 -0
- package/skills/triage/AGENT-BRIEF.md +168 -0
- package/skills/triage/OUT-OF-SCOPE.md +101 -0
- package/skills/triage/SKILL.md +111 -0
- package/skills/write-a-skill/SKILL.md +125 -0
- package/vendor/matt-pocock/LICENSE +21 -0
- package/vendor/matt-pocock/NOTICE.md +10 -0
- package/vendor/matt-pocock/allowlist.json +16 -0
- package/vendor/matt-pocock/source-lock.json +119 -0
package/cli/connect.mjs
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
applyPlan,
|
|
4
|
+
confirmPlan,
|
|
5
|
+
EXIT_CONFIG_FAILURE,
|
|
6
|
+
maybeInstallMissingForConnect,
|
|
7
|
+
normalizeOptions,
|
|
8
|
+
printApplyResult,
|
|
9
|
+
printConfigErrors,
|
|
10
|
+
printPlanSummary,
|
|
11
|
+
resolvePlan,
|
|
12
|
+
usage,
|
|
13
|
+
} from './install-flow.mjs';
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
const nonInteractive = !process.stdin.isTTY;
|
|
17
|
+
const options = normalizeOptions(process.argv.slice(2), {
|
|
18
|
+
command: 'connect',
|
|
19
|
+
cwd: process.cwd(),
|
|
20
|
+
nonInteractive,
|
|
21
|
+
});
|
|
22
|
+
if (options.help) {
|
|
23
|
+
usage('connect');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (options.errors.length > 0) {
|
|
27
|
+
printConfigErrors('connect', options.errors);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!options.mode && !options.yes) {
|
|
32
|
+
options.mode = 'normal';
|
|
33
|
+
} else if (!options.mode && options.yes) {
|
|
34
|
+
options.mode = 'normal';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let resolved = await resolvePlan(options);
|
|
38
|
+
if (resolved.cancelled) {
|
|
39
|
+
console.log('Connection cancelled. No changes made.');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (resolved.errors?.length) {
|
|
43
|
+
printConfigErrors('connect', resolved.errors);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let { plan } = resolved;
|
|
48
|
+
printPlanSummary(plan);
|
|
49
|
+
|
|
50
|
+
let confirmation = await confirmPlan(plan);
|
|
51
|
+
if (confirmation === 'cancel') {
|
|
52
|
+
console.log('Connection cancelled. No changes made.');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (confirmation === 'custom') {
|
|
56
|
+
resolved = await resolvePlan({ ...options, mode: 'custom', explicitMode: true });
|
|
57
|
+
if (resolved.errors?.length) {
|
|
58
|
+
printConfigErrors('connect', resolved.errors);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
plan = resolved.plan;
|
|
62
|
+
printPlanSummary(plan);
|
|
63
|
+
confirmation = await confirmPlan(plan);
|
|
64
|
+
if (confirmation !== 'yes') {
|
|
65
|
+
console.log('Connection cancelled. No changes made.');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const missing = await maybeInstallMissingForConnect(plan);
|
|
71
|
+
if (!missing.ok) {
|
|
72
|
+
printConfigErrors('connect', missing.errors);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const result = applyPlan(plan);
|
|
77
|
+
printApplyResult(plan, result);
|
|
78
|
+
if (result.exitCode) process.exitCode = result.exitCode;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
main().catch((error) => {
|
|
82
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
83
|
+
process.exit(EXIT_CONFIG_FAILURE);
|
|
84
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { scanText as scanSharedGuardText } from '../guard/scanners/index.mjs';
|
|
3
|
+
|
|
4
|
+
function candidate(list, kind, file, line, detail, severity = 'advisory') {
|
|
5
|
+
list.push({ kind, file: file.rel, line, detail, severity });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const SHARED_SCANNER_BUCKETS = new Map([
|
|
9
|
+
['no_silent_failure', 'silentFailures'],
|
|
10
|
+
['broad_exception_advisory', 'silentFailures'],
|
|
11
|
+
['no_secret_logging', 'secretLogging'],
|
|
12
|
+
['file_size_advisory', 'largeFiles'],
|
|
13
|
+
['responsibility_budget', 'responsibilityBudget'],
|
|
14
|
+
['single_responsibility_advisory', 'singleResponsibility'],
|
|
15
|
+
['component_responsibility', 'clientServerSeam'],
|
|
16
|
+
['external_input_validation', 'externalInput'],
|
|
17
|
+
['null_state_safety', 'stateSafety'],
|
|
18
|
+
['authz_data_isolation', 'authzIsolation'],
|
|
19
|
+
['build_runtime_env_safety', 'runtimeEnv'],
|
|
20
|
+
['write_safety_idempotency', 'writeSafety'],
|
|
21
|
+
['api_contract_compatibility', 'apiContract'],
|
|
22
|
+
['public_safe_error', 'apiContract'],
|
|
23
|
+
['db_row_validation', 'apiContract'],
|
|
24
|
+
['performance_duplicate_fetch', 'performanceDuplication'],
|
|
25
|
+
['sql_parameter_binding', 'rawSql'],
|
|
26
|
+
['thin_api_route', 'dbInRoutes'],
|
|
27
|
+
['type_escape_advisory', 'typeEscapes'],
|
|
28
|
+
['side_effect_boundary', 'hiddenSideEffects'],
|
|
29
|
+
['crawler_producer_boundary', 'hiddenSideEffects'],
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
function deepScanSeverity(finding) {
|
|
33
|
+
if (finding.rule_id === 'file_size.review' || finding.severity === 'error') return 'review';
|
|
34
|
+
if (finding.severity === 'warning') return 'warning';
|
|
35
|
+
return 'advisory';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function addSharedScannerCandidates(file, text, findings, thresholds) {
|
|
39
|
+
const sharedFindings = scanSharedGuardText(file.rel, text, {
|
|
40
|
+
applyProfile: false,
|
|
41
|
+
fileSize: thresholds.fileSize,
|
|
42
|
+
responsibilityBudget: thresholds.responsibility,
|
|
43
|
+
singleResponsibility: thresholds.singleResponsibility,
|
|
44
|
+
});
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
for (const finding of sharedFindings) {
|
|
47
|
+
const bucket = SHARED_SCANNER_BUCKETS.get(finding.rule_family);
|
|
48
|
+
if (!bucket || !findings[bucket]) continue;
|
|
49
|
+
const key = `${bucket}:${finding.rule_id}:${finding.line}:${finding.message}`;
|
|
50
|
+
if (seen.has(key)) continue;
|
|
51
|
+
seen.add(key);
|
|
52
|
+
candidate(
|
|
53
|
+
findings[bucket],
|
|
54
|
+
`${finding.rule_family} candidate`,
|
|
55
|
+
file,
|
|
56
|
+
finding.line || 1,
|
|
57
|
+
finding.message,
|
|
58
|
+
deepScanSeverity(finding),
|
|
59
|
+
);
|
|
60
|
+
if (finding.rule_id === 'responsibility.route.budget') {
|
|
61
|
+
candidate(
|
|
62
|
+
findings.routeResponsibility,
|
|
63
|
+
`${finding.rule_family} candidate`,
|
|
64
|
+
file,
|
|
65
|
+
finding.line || 1,
|
|
66
|
+
finding.message,
|
|
67
|
+
deepScanSeverity(finding),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function hasUseClientDirective(text) {
|
|
74
|
+
return /^\s*(?:"use client"|'use client')\s*;?/u.test(text);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isScriptPipeline(file) {
|
|
78
|
+
return /(^|\/)scripts\/(data|ops|import|imports|backfill|repair|migrate|migration)\//.test(file.rel)
|
|
79
|
+
&& /\.(ts|tsx|js|jsx|mjs|cjs|py)$/.test(file.rel);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function matchedResponsibilityHints(text, hintGroups) {
|
|
83
|
+
return hintGroups
|
|
84
|
+
.filter((group) => group.patterns.some((pattern) => pattern.test(text)))
|
|
85
|
+
.map((group) => group.label);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function scanMixedResponsibilities(file, text, findings) {
|
|
89
|
+
if (hasUseClientDirective(text)) {
|
|
90
|
+
const hints = matchedResponsibilityHints(text, [
|
|
91
|
+
{ label: 'browser storage', patterns: [/\b(localStorage|sessionStorage)\b/] },
|
|
92
|
+
{ label: 'network/API', patterns: [/\bfetch\s*\(/, /\baxios\./, /\buse(Query|Mutation)\s*\(/] },
|
|
93
|
+
{ label: 'toast/notification', patterns: [/\btoast\b/, /\bnotify\b/] },
|
|
94
|
+
{ label: 'modal/dialog state', patterns: [/\b(Dialog|Modal|Sheet)\b/, /\bopen[A-Z]\w*\b/, /\bis[A-Z]\w*Open\b/] },
|
|
95
|
+
{ label: 'route navigation', patterns: [/\buseRouter\s*\(/, /\brouter\.(push|replace|refresh)\b/] },
|
|
96
|
+
{ label: 'heavy mapping', patterns: [/\.(map|filter|reduce)\s*\(/] },
|
|
97
|
+
]);
|
|
98
|
+
if (hints.length >= 3) {
|
|
99
|
+
candidate(findings.responsibilityBudget, 'mixed client responsibility candidate', file, 1, `client module mixes ${hints.slice(0, 4).join(', ')}; review hook/adapter/presentation split`, 'warning');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const routeLike = /(^|\/)(api|routes?|controllers?|pages\/api)\//i.test(file.rel) || /route\.(ts|js)$/.test(file.rel);
|
|
104
|
+
if (routeLike) {
|
|
105
|
+
const hints = matchedResponsibilityHints(text, [
|
|
106
|
+
{ label: 'auth/session', patterns: [/\b(auth|session|permission|currentUser|getUser)\b/i] },
|
|
107
|
+
{ label: 'validation', patterns: [/\b(z\.object|safeParse|parseAsync|validate|schema)\b/] },
|
|
108
|
+
{ label: 'database', patterns: [/\b(prisma|pool\.query|client\.query|SELECT|INSERT|UPDATE|DELETE|db\.)\b/i] },
|
|
109
|
+
{ label: 'response formatting', patterns: [/\b(Response\.json|NextResponse\.json|res\.json)\b/] },
|
|
110
|
+
]);
|
|
111
|
+
if (hints.length >= 3) {
|
|
112
|
+
candidate(findings.responsibilityBudget, 'mixed route responsibility candidate', file, 1, `route/controller mixes ${hints.join(', ')}; review route/service/repository/response split`, 'warning');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (isScriptPipeline(file)) {
|
|
117
|
+
const hints = matchedResponsibilityHints(text, [
|
|
118
|
+
{ label: 'CLI parsing', patterns: [/\b(process\.argv|argparse|ArgumentParser|commander)\b/] },
|
|
119
|
+
{ label: 'file IO', patterns: [/\b(readFile|writeFile|open\(|Path\(|fs\.)\b/] },
|
|
120
|
+
{ label: 'data transform', patterns: [/\.(map|filter|reduce)\s*\(/, /\bjson\.loads\b/i, /\bJSON\.parse\b/] },
|
|
121
|
+
{ label: 'persistence/network', patterns: [/\b(fetch|pool\.query|client\.query|INSERT|UPDATE|DELETE|requests\.)\b/i] },
|
|
122
|
+
{ label: 'reporting', patterns: [/\b(console\.|print\(|logger\.)\b/] },
|
|
123
|
+
]);
|
|
124
|
+
if (hints.length >= 4) {
|
|
125
|
+
candidate(findings.responsibilityBudget, 'mixed script responsibility candidate', file, 1, `script mixes ${hints.join(', ')}; review CLI/loader/transform/persist/report seams`, 'warning');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function newFindingsBag() {
|
|
131
|
+
return {
|
|
132
|
+
largeFiles: [],
|
|
133
|
+
silentFailures: [],
|
|
134
|
+
typeEscapes: [],
|
|
135
|
+
rawSql: [],
|
|
136
|
+
dbInRoutes: [],
|
|
137
|
+
routeResponsibility: [],
|
|
138
|
+
responsibilityBudget: [],
|
|
139
|
+
singleResponsibility: [],
|
|
140
|
+
clientServerSeam: [],
|
|
141
|
+
hiddenSideEffects: [],
|
|
142
|
+
secretLogging: [],
|
|
143
|
+
stateSafety: [],
|
|
144
|
+
authzIsolation: [],
|
|
145
|
+
runtimeEnv: [],
|
|
146
|
+
writeSafety: [],
|
|
147
|
+
apiContract: [],
|
|
148
|
+
performanceDuplication: [],
|
|
149
|
+
externalInput: [],
|
|
150
|
+
scanWarnings: [],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function scanFiles(files, thresholds) {
|
|
155
|
+
const findings = newFindingsBag();
|
|
156
|
+
for (const file of files) {
|
|
157
|
+
try {
|
|
158
|
+
const text = fs.readFileSync(file.full, 'utf8');
|
|
159
|
+
addSharedScannerCandidates(file, text, findings, thresholds);
|
|
160
|
+
scanMixedResponsibilities(file, text, findings);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
163
|
+
candidate(findings.scanWarnings, 'scan warning', file, 1, `file could not be scanned and was omitted from rule candidates: ${message}`, 'warning');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return findings;
|
|
167
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { isPathInside, relativeDisplay } from '../shared.mjs';
|
|
5
|
+
|
|
6
|
+
const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py', '.sql', '.md', '.mdx', '.json']);
|
|
7
|
+
const TEXT_EXTENSIONS = new Set([...SOURCE_EXTENSIONS, '.yaml', '.yml', '.toml']);
|
|
8
|
+
const EXCLUDED_DIRS = new Set([
|
|
9
|
+
'.git',
|
|
10
|
+
'node_modules',
|
|
11
|
+
'vendor',
|
|
12
|
+
'dist',
|
|
13
|
+
'build',
|
|
14
|
+
'.next',
|
|
15
|
+
'out',
|
|
16
|
+
'coverage',
|
|
17
|
+
'.turbo',
|
|
18
|
+
'.cache',
|
|
19
|
+
'__pycache__',
|
|
20
|
+
]);
|
|
21
|
+
const EXCLUDED_FILE_NAMES = new Set([
|
|
22
|
+
'package-lock.json',
|
|
23
|
+
'pnpm-lock.yaml',
|
|
24
|
+
'yarn.lock',
|
|
25
|
+
'bun.lockb',
|
|
26
|
+
'bun.lock',
|
|
27
|
+
'poetry.lock',
|
|
28
|
+
'Pipfile.lock',
|
|
29
|
+
]);
|
|
30
|
+
const SECRET_FILE_RE = /(^|\/)(\.env(\..*)?|.*\.(pem|key|p12|pfx|crt)|id_rsa|id_ed25519)$/i;
|
|
31
|
+
const MAX_FILE_BYTES = 1024 * 1024;
|
|
32
|
+
|
|
33
|
+
export function collectFiles(repoRoot) {
|
|
34
|
+
const files = [];
|
|
35
|
+
const skipped = [];
|
|
36
|
+
const source = {
|
|
37
|
+
type: 'git-ls-files',
|
|
38
|
+
command: 'git ls-files --cached --others --exclude-standard -z',
|
|
39
|
+
fallback: false,
|
|
40
|
+
fallback_reason: null,
|
|
41
|
+
listed_count: 0,
|
|
42
|
+
};
|
|
43
|
+
function skip(reason, fullPath) {
|
|
44
|
+
skipped.push({ reason, path: relativeDisplay(repoRoot, fullPath) });
|
|
45
|
+
}
|
|
46
|
+
function consider(full, entryName = path.basename(full)) {
|
|
47
|
+
const rel = relativeDisplay(repoRoot, full);
|
|
48
|
+
if (EXCLUDED_FILE_NAMES.has(entryName) || SECRET_FILE_RE.test(rel)) {
|
|
49
|
+
skip('excluded sensitive or lock file', full);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const ext = path.extname(entryName).toLowerCase();
|
|
53
|
+
if (!TEXT_EXTENSIONS.has(ext)) {
|
|
54
|
+
skip('non-source extension', full);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const stat = fs.statSync(full);
|
|
58
|
+
if (stat.size > MAX_FILE_BYTES) {
|
|
59
|
+
skip('large file', full);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
files.push({ full, rel, ext, size: stat.size });
|
|
63
|
+
}
|
|
64
|
+
function walk(dir) {
|
|
65
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
66
|
+
const full = path.join(dir, entry.name);
|
|
67
|
+
if (entry.isDirectory()) {
|
|
68
|
+
if (EXCLUDED_DIRS.has(entry.name)) {
|
|
69
|
+
skip('excluded directory', full);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
walk(full);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (!entry.isFile()) {
|
|
76
|
+
skip('non-file', full);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
consider(full, entry.name);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
execFileSync('git', ['-C', repoRoot, 'rev-parse', '--is-inside-work-tree'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
84
|
+
const raw = execFileSync('git', ['-C', repoRoot, 'ls-files', '--cached', '--others', '--exclude-standard', '-z'], { encoding: 'buffer', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
85
|
+
const rels = raw.toString('utf8').split('\0').map((item) => item.trim()).filter(Boolean);
|
|
86
|
+
source.listed_count = rels.length;
|
|
87
|
+
for (const rel of [...new Set(rels)].sort()) {
|
|
88
|
+
const full = path.resolve(repoRoot, rel);
|
|
89
|
+
if (!isPathInside(repoRoot, full)) {
|
|
90
|
+
skip('outside repository', full);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (!fs.existsSync(full) || !fs.statSync(full).isFile()) {
|
|
94
|
+
skip('missing or non-file', full);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (rel.split('/').some((part) => EXCLUDED_DIRS.has(part))) {
|
|
98
|
+
skip('excluded directory', full);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
consider(full);
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
source.type = 'filesystem-fallback';
|
|
105
|
+
source.fallback = true;
|
|
106
|
+
source.fallback_reason = error instanceof Error ? error.message : String(error);
|
|
107
|
+
walk(repoRoot);
|
|
108
|
+
source.listed_count = files.length + skipped.length;
|
|
109
|
+
}
|
|
110
|
+
return { files, skipped, source };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function detectStack(repoRoot, files) {
|
|
114
|
+
const rels = files.map((file) => file.rel);
|
|
115
|
+
const packageJsonPath = path.join(repoRoot, 'package.json');
|
|
116
|
+
const packageText = fs.existsSync(packageJsonPath) ? fs.readFileSync(packageJsonPath, 'utf8') : '';
|
|
117
|
+
return {
|
|
118
|
+
typescript: rels.some((rel) => /\.(ts|tsx)$/.test(rel)) || /typescript/.test(packageText),
|
|
119
|
+
react: rels.some((rel) => /\.(jsx|tsx)$/.test(rel)) || /"react"\s*:/.test(packageText),
|
|
120
|
+
nextjs: rels.some((rel) => /(^|\/)(next\.config\.|app\/.*route\.(js|ts)|pages\/api\/)/.test(rel)) || /"next"\s*:/.test(packageText),
|
|
121
|
+
python: rels.some((rel) => rel.endsWith('.py')),
|
|
122
|
+
postgresql: /postgres|pg\b|postgresql/i.test(packageText) || rels.some((rel) => /migrations?|sql|database|db/i.test(rel)),
|
|
123
|
+
crawler: rels.some((rel) => /crawler|scraper|automation|playwright|puppeteer|worker|scheduler/i.test(rel)) || /playwright|puppeteer|scrap/i.test(packageText),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function detectInstructions(repoRoot) {
|
|
128
|
+
return {
|
|
129
|
+
agents: fs.existsSync(path.join(repoRoot, 'AGENTS.md')),
|
|
130
|
+
claude: fs.existsSync(path.join(repoRoot, 'CLAUDE.md')),
|
|
131
|
+
docs: fs.existsSync(path.join(repoRoot, 'docs')),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { nowIso } from '../shared.mjs';
|
|
3
|
+
|
|
4
|
+
function tableRows(rows) {
|
|
5
|
+
if (rows.length === 0) return '- None found in scanned files.\n';
|
|
6
|
+
return rows.slice(0, 50).map((row) => `- \`${row.file}:${row.line}\` — ${row.detail}`).join('\n') + (rows.length > 50 ? `\n- ... ${rows.length - 50} more candidates omitted from summary` : '') + '\n';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function yesNo(value) {
|
|
10
|
+
return value ? 'yes' : 'no';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function recommendedPacks(stack, findings) {
|
|
14
|
+
return [
|
|
15
|
+
['core', 'advisory', 'Safe default for all repositories'],
|
|
16
|
+
['web', stack.react || stack.nextjs ? 'advisory' : 'off', stack.react || stack.nextjs ? 'React or web stack detected' : 'No web stack detected'],
|
|
17
|
+
['api', stack.nextjs || findings.dbInRoutes.length || findings.routeResponsibility.length ? 'changed-files' : 'advisory', 'API boundary candidates can start with changed files only'],
|
|
18
|
+
['database', stack.postgresql || findings.rawSql.length ? 'advisory' : 'off', stack.postgresql || findings.rawSql.length ? 'Database or SQL hints detected' : 'No database hints detected'],
|
|
19
|
+
['crawler', stack.crawler ? 'advisory' : 'off', stack.crawler ? 'Crawler or automation hints detected' : 'No crawler hints detected'],
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function renderReport({ repoRoot, files, skipped, source, stack, instructions, findings }) {
|
|
24
|
+
const packRows = recommendedPacks(stack, findings);
|
|
25
|
+
return `# Deep Scan Report
|
|
26
|
+
|
|
27
|
+
## Summary
|
|
28
|
+
- Scan time: ${nowIso()}
|
|
29
|
+
- Repository: ${path.basename(repoRoot)}
|
|
30
|
+
- Files inspected: ${files.length}
|
|
31
|
+
- Files skipped: ${skipped.length}
|
|
32
|
+
- File collection source: ${source?.type || 'unknown'}
|
|
33
|
+
- File collection fallback: ${source?.fallback ? `yes (${source.fallback_reason || 'unknown reason'})` : 'no'}
|
|
34
|
+
- Recommended packs: ${packRows.filter((row) => row[1] !== 'off').map((row) => row[0]).join(', ') || 'core'}
|
|
35
|
+
- Suggested default mode: advisory
|
|
36
|
+
- Code modified: no
|
|
37
|
+
|
|
38
|
+
## Detected stack
|
|
39
|
+
- TypeScript: ${yesNo(stack.typescript)}
|
|
40
|
+
- React: ${yesNo(stack.react)}
|
|
41
|
+
- Next.js: ${yesNo(stack.nextjs)}
|
|
42
|
+
- Python: ${yesNo(stack.python)}
|
|
43
|
+
- PostgreSQL: ${yesNo(stack.postgresql)}
|
|
44
|
+
- Crawler/automation: ${yesNo(stack.crawler)}
|
|
45
|
+
|
|
46
|
+
## Existing local instructions
|
|
47
|
+
- AGENTS.md: ${instructions.agents ? 'found' : 'not found'}
|
|
48
|
+
- CLAUDE.md: ${instructions.claude ? 'found' : 'not found'}
|
|
49
|
+
- docs guidance: ${instructions.docs ? 'found' : 'not found'}
|
|
50
|
+
- Conflict policy: repo-local instructions remain authoritative
|
|
51
|
+
|
|
52
|
+
## Recommended rule modes
|
|
53
|
+
| Pack | Recommended mode | Reason |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
${packRows.map((row) => `| ${row[0]} | ${row[1]} | ${row[2]} |`).join('\n')}
|
|
56
|
+
|
|
57
|
+
## Existing debt candidates
|
|
58
|
+
|
|
59
|
+
### Existing large files
|
|
60
|
+
${tableRows(findings.largeFiles)}
|
|
61
|
+
### Existing silent failure candidates
|
|
62
|
+
${tableRows(findings.silentFailures)}
|
|
63
|
+
### Existing type escape candidates
|
|
64
|
+
${tableRows(findings.typeEscapes)}
|
|
65
|
+
### Existing raw SQL candidates
|
|
66
|
+
${tableRows(findings.rawSql)}
|
|
67
|
+
### Existing DB/API seam candidates
|
|
68
|
+
${tableRows([...findings.dbInRoutes, ...findings.routeResponsibility])}
|
|
69
|
+
### Existing responsibility budget candidates
|
|
70
|
+
${tableRows(findings.responsibilityBudget)}
|
|
71
|
+
### Existing single-responsibility candidates
|
|
72
|
+
${tableRows(findings.singleResponsibility)}
|
|
73
|
+
### Existing client/server seam candidates
|
|
74
|
+
${tableRows(findings.clientServerSeam)}
|
|
75
|
+
### Existing hidden side-effect candidates
|
|
76
|
+
${tableRows(findings.hiddenSideEffects)}
|
|
77
|
+
### Existing null/state safety candidates
|
|
78
|
+
${tableRows(findings.stateSafety)}
|
|
79
|
+
### Existing auth/data isolation candidates
|
|
80
|
+
${tableRows(findings.authzIsolation)}
|
|
81
|
+
### Existing runtime/env safety candidates
|
|
82
|
+
${tableRows(findings.runtimeEnv)}
|
|
83
|
+
### Existing write safety candidates
|
|
84
|
+
${tableRows(findings.writeSafety)}
|
|
85
|
+
### Existing API contract candidates
|
|
86
|
+
${tableRows(findings.apiContract)}
|
|
87
|
+
### Existing performance duplication candidates
|
|
88
|
+
${tableRows(findings.performanceDuplication)}
|
|
89
|
+
### Existing external input validation candidates
|
|
90
|
+
${tableRows(findings.externalInput)}
|
|
91
|
+
### Secret-like logging candidates
|
|
92
|
+
${tableRows(findings.secretLogging)}
|
|
93
|
+
### Scan warnings
|
|
94
|
+
${tableRows(findings.scanWarnings)}
|
|
95
|
+
|
|
96
|
+
## New-code guard candidates
|
|
97
|
+
- Start with changed-files mode for no_silent_failure and no_secret_logging after human review.
|
|
98
|
+
- Consider baseline-new-only only after accepting a baseline generated from this report.
|
|
99
|
+
- Keep strict disabled unless a user explicitly opts in.
|
|
100
|
+
|
|
101
|
+
## Skipped file summary
|
|
102
|
+
- Excluded generated/vendor/build/dependency folders, lockfiles, large files, binary-like extensions, and secret/env-like files.
|
|
103
|
+
- Skipped entries recorded: ${skipped.length}
|
|
104
|
+
- External input validation candidates: ${findings.externalInput.length}
|
|
105
|
+
- Per-file scan warnings: ${findings.scanWarnings.length}
|
|
106
|
+
|
|
107
|
+
## Risks
|
|
108
|
+
- Static analysis can produce false positives and cannot prove runtime behavior.
|
|
109
|
+
- This report redacts secret-like content and does not include raw sensitive values.
|
|
110
|
+
- Human review is needed before enabling strict mode or CI enforcement.
|
|
111
|
+
`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function renderRecommendedProfile({ stack, findings, thresholds }) {
|
|
115
|
+
const apiMode = stack.nextjs || findings.dbInRoutes.length || findings.routeResponsibility.length ? 'changed-files' : 'advisory';
|
|
116
|
+
const databaseMode = stack.postgresql || findings.rawSql.length ? 'advisory' : 'off';
|
|
117
|
+
const crawlerMode = stack.crawler ? 'advisory' : 'off';
|
|
118
|
+
return `version: 1
|
|
119
|
+
mode: advisory
|
|
120
|
+
recommendations:
|
|
121
|
+
generated_by: deep-scan
|
|
122
|
+
generated_at: "${nowIso()}"
|
|
123
|
+
apply_requires_user_approval: true
|
|
124
|
+
packs:
|
|
125
|
+
core:
|
|
126
|
+
mode: advisory
|
|
127
|
+
web:
|
|
128
|
+
mode: ${stack.react || stack.nextjs ? 'advisory' : 'off'}
|
|
129
|
+
api:
|
|
130
|
+
mode: ${apiMode}
|
|
131
|
+
database:
|
|
132
|
+
mode: ${databaseMode}
|
|
133
|
+
crawler:
|
|
134
|
+
mode: ${crawlerMode}
|
|
135
|
+
rules:
|
|
136
|
+
no_silent_failure:
|
|
137
|
+
mode: changed-files
|
|
138
|
+
no_secret_logging:
|
|
139
|
+
mode: changed-files
|
|
140
|
+
workflow_security:
|
|
141
|
+
mode: advisory
|
|
142
|
+
file_size_advisory:
|
|
143
|
+
mode: advisory
|
|
144
|
+
source_file_warning_lines: ${thresholds.fileSize.source_file_warning_lines}
|
|
145
|
+
source_file_review_lines: ${thresholds.fileSize.source_file_review_lines}
|
|
146
|
+
responsibility_budget:
|
|
147
|
+
mode: advisory
|
|
148
|
+
next_page_review_lines: ${thresholds.responsibility.next_page_review_lines}
|
|
149
|
+
client_module_review_lines: ${thresholds.responsibility.client_module_review_lines}
|
|
150
|
+
route_review_lines: ${thresholds.responsibility.route_review_lines}
|
|
151
|
+
import_ops_script_review_lines: ${thresholds.responsibility.import_ops_script_review_lines}
|
|
152
|
+
python_orchestrator_review_lines: ${thresholds.responsibility.python_orchestrator_review_lines}
|
|
153
|
+
single_responsibility_advisory:
|
|
154
|
+
mode: advisory
|
|
155
|
+
function_review_lines: ${thresholds.singleResponsibility.function_review_lines}
|
|
156
|
+
mixed_responsibility_hints: ${thresholds.singleResponsibility.mixed_responsibility_hints}
|
|
157
|
+
module_export_family_hints: ${thresholds.singleResponsibility.module_export_family_hints}
|
|
158
|
+
external_input_validation:
|
|
159
|
+
mode: advisory
|
|
160
|
+
null_state_safety:
|
|
161
|
+
mode: advisory
|
|
162
|
+
authz_data_isolation:
|
|
163
|
+
mode: advisory
|
|
164
|
+
build_runtime_env_safety:
|
|
165
|
+
mode: advisory
|
|
166
|
+
write_safety_idempotency:
|
|
167
|
+
mode: advisory
|
|
168
|
+
api_contract_compatibility:
|
|
169
|
+
mode: advisory
|
|
170
|
+
performance_duplicate_fetch:
|
|
171
|
+
mode: advisory
|
|
172
|
+
public_safe_error:
|
|
173
|
+
mode: advisory
|
|
174
|
+
sql_parameter_binding:
|
|
175
|
+
mode: ${databaseMode}
|
|
176
|
+
db_row_validation:
|
|
177
|
+
mode: ${databaseMode}
|
|
178
|
+
type_escape_advisory:
|
|
179
|
+
mode: advisory
|
|
180
|
+
thin_api_route:
|
|
181
|
+
mode: ${apiMode}
|
|
182
|
+
component_responsibility:
|
|
183
|
+
mode: ${stack.react || stack.nextjs ? 'advisory' : 'off'}
|
|
184
|
+
side_effect_boundary:
|
|
185
|
+
mode: advisory
|
|
186
|
+
broad_exception_advisory:
|
|
187
|
+
mode: ${stack.python ? 'advisory' : 'off'}
|
|
188
|
+
crawler_producer_boundary:
|
|
189
|
+
mode: ${crawlerMode}
|
|
190
|
+
baseline:
|
|
191
|
+
enabled: false
|
|
192
|
+
candidate_report: .jhste/deep-scan-report.md
|
|
193
|
+
strict:
|
|
194
|
+
enabled: false
|
|
195
|
+
requires_explicit_opt_in: true
|
|
196
|
+
`;
|
|
197
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
atomicWrite,
|
|
5
|
+
ensureDir,
|
|
6
|
+
findGitRoot,
|
|
7
|
+
parseArgs,
|
|
8
|
+
relativeDisplay,
|
|
9
|
+
} from './shared.mjs';
|
|
10
|
+
import {
|
|
11
|
+
fileSizeSettings,
|
|
12
|
+
loadProfileConfig,
|
|
13
|
+
responsibilityBudgetSettings,
|
|
14
|
+
singleResponsibilitySettings,
|
|
15
|
+
} from './profile.mjs';
|
|
16
|
+
import { collectFiles, detectInstructions, detectStack } from './deep-scan/collect.mjs';
|
|
17
|
+
import { scanFiles } from './deep-scan/analyze.mjs';
|
|
18
|
+
import { renderRecommendedProfile, renderReport } from './deep-scan/report.mjs';
|
|
19
|
+
|
|
20
|
+
async function main() {
|
|
21
|
+
const args = parseArgs(process.argv.slice(2));
|
|
22
|
+
const repoRoot = findGitRoot(args.repo || process.cwd());
|
|
23
|
+
const profileState = loadProfileConfig(repoRoot);
|
|
24
|
+
const thresholds = {
|
|
25
|
+
fileSize: fileSizeSettings(profileState.profile),
|
|
26
|
+
responsibility: responsibilityBudgetSettings(profileState.profile),
|
|
27
|
+
singleResponsibility: singleResponsibilitySettings(profileState.profile),
|
|
28
|
+
};
|
|
29
|
+
const { files, skipped, source } = collectFiles(repoRoot);
|
|
30
|
+
const stack = detectStack(repoRoot, files);
|
|
31
|
+
const instructions = detectInstructions(repoRoot);
|
|
32
|
+
const findings = scanFiles(files, thresholds);
|
|
33
|
+
const jhsteDir = path.join(repoRoot, '.jhste');
|
|
34
|
+
ensureDir(jhsteDir);
|
|
35
|
+
const reportPath = path.join(jhsteDir, 'deep-scan-report.md');
|
|
36
|
+
const recommendedPath = path.join(jhsteDir, 'profile.recommended.yaml');
|
|
37
|
+
atomicWrite(reportPath, renderReport({ repoRoot, files, skipped, source, stack, instructions, findings }));
|
|
38
|
+
atomicWrite(recommendedPath, renderRecommendedProfile({ stack, findings, thresholds }));
|
|
39
|
+
|
|
40
|
+
console.log('Deep scan completed. No code was modified.');
|
|
41
|
+
console.log(`- Detected stack: ${Object.entries(stack).filter(([, value]) => value).map(([key]) => key).join(', ') || 'none'}`);
|
|
42
|
+
console.log(`- Files inspected: ${files.length}`);
|
|
43
|
+
console.log(`- Files skipped: ${skipped.length}`);
|
|
44
|
+
console.log(`- File source: ${source.type}${source.fallback ? ` (fallback: ${source.fallback_reason || 'unknown reason'})` : ''}`);
|
|
45
|
+
console.log('- Recommendation: keep advisory as the default; apply changed-files candidates only after user approval');
|
|
46
|
+
console.log('\nOutput files:');
|
|
47
|
+
console.log(`- ${relativeDisplay(repoRoot, reportPath)}`);
|
|
48
|
+
console.log(`- ${relativeDisplay(repoRoot, recommendedPath)}`);
|
|
49
|
+
console.log('\nTo apply the recommended settings:');
|
|
50
|
+
console.log(' npx jhste-skills tune');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
main().catch((error) => {
|
|
54
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
});
|