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
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { directoryDigest, ensureDir, KIT_ROOT, listDirectories, nowIso } from '../shared.mjs';
|
|
4
|
+
import { readJsonFile, validateJsonObject, validateStringArray } from '../json-file.mjs';
|
|
5
|
+
|
|
6
|
+
export const SKILLS_MANIFEST_NAME = '.jhste-skills-manifest.json';
|
|
7
|
+
export const MANIFEST_MANAGED_BY = 'jhste-skills';
|
|
8
|
+
|
|
9
|
+
function vendoredSkillNames() {
|
|
10
|
+
const allowlistPath = path.join(KIT_ROOT, 'vendor', 'matt-pocock', 'allowlist.json');
|
|
11
|
+
return new Set(readJsonFile(allowlistPath, {
|
|
12
|
+
description: 'vendor/matt-pocock/allowlist.json',
|
|
13
|
+
validate: validateStringArray,
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function skillNamesForSet(skillSet) {
|
|
18
|
+
const sourceRoot = path.join(KIT_ROOT, 'skills');
|
|
19
|
+
const all = listDirectories(sourceRoot);
|
|
20
|
+
const vendored = vendoredSkillNames();
|
|
21
|
+
if (skillSet === 'all') return all;
|
|
22
|
+
if (skillSet === 'vendor') return all.filter((name) => vendored.has(name));
|
|
23
|
+
return all.filter((name) => !vendored.has(name));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function selectedSkillNames(plan) {
|
|
27
|
+
if (Array.isArray(plan.skillNames) && plan.skillNames.length > 0) return [...plan.skillNames];
|
|
28
|
+
return skillNamesForSet(plan.skillSet);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function installedSkillStatus(skillsDir, skillSet) {
|
|
32
|
+
const expected = Array.isArray(skillSet) ? skillSet : skillNamesForSet(skillSet);
|
|
33
|
+
const missing = expected.filter((name) => !fs.existsSync(path.join(skillsDir, name, 'SKILL.md')));
|
|
34
|
+
return { expected, missing };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function manifestPath(skillsDir) {
|
|
38
|
+
return path.join(skillsDir, SKILLS_MANIFEST_NAME);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function loadSkillsManifest(skillsDir) {
|
|
42
|
+
const file = manifestPath(skillsDir);
|
|
43
|
+
if (!fs.existsSync(file)) return null;
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
46
|
+
if (!parsed || typeof parsed !== 'object' || parsed.managed_by !== MANIFEST_MANAGED_BY || typeof parsed.skills !== 'object' || Array.isArray(parsed.skills)) {
|
|
47
|
+
return { invalid: true, reason: `${SKILLS_MANIFEST_NAME} is not a valid ${MANIFEST_MANAGED_BY} manifest.` };
|
|
48
|
+
}
|
|
49
|
+
return parsed;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return { invalid: true, reason: error instanceof Error ? error.message : String(error) };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function writeSkillsManifest(skillsDir, manifest) {
|
|
56
|
+
ensureDir(skillsDir);
|
|
57
|
+
fs.writeFileSync(manifestPath(skillsDir), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function packageVersion() {
|
|
61
|
+
try {
|
|
62
|
+
return String(readJsonFile(path.join(KIT_ROOT, 'package.json'), {
|
|
63
|
+
description: 'package.json',
|
|
64
|
+
validate: validateJsonObject,
|
|
65
|
+
}).version || '0.0.0');
|
|
66
|
+
} catch {
|
|
67
|
+
return '0.0.0';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function copyManagedSkill(source, destination, name, { force = false, allowUnmanagedOverwrite = false, manifest = null, nextManifest } = {}) {
|
|
72
|
+
if (!fs.existsSync(source)) return { status: 'missing-source', source, destination };
|
|
73
|
+
const sourceHash = directoryDigest(source);
|
|
74
|
+
const destinationExists = fs.existsSync(destination);
|
|
75
|
+
const destinationHash = destinationExists ? directoryDigest(destination) : null;
|
|
76
|
+
const manifestEntry = manifest?.skills?.[name] || null;
|
|
77
|
+
const manifestOwnsDestination = Boolean(manifestEntry);
|
|
78
|
+
|
|
79
|
+
function recordManaged() {
|
|
80
|
+
nextManifest.skills[name] = { digest: sourceHash };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!destinationExists) {
|
|
84
|
+
ensureDir(path.dirname(destination));
|
|
85
|
+
fs.cpSync(source, destination, { recursive: true });
|
|
86
|
+
recordManaged();
|
|
87
|
+
return { status: 'created', source, destination };
|
|
88
|
+
}
|
|
89
|
+
if (sourceHash === destinationHash) {
|
|
90
|
+
recordManaged();
|
|
91
|
+
return { status: 'unchanged', source, destination };
|
|
92
|
+
}
|
|
93
|
+
if (!force) return { status: 'skipped-existing-different', source, destination };
|
|
94
|
+
if (!manifestOwnsDestination && !allowUnmanagedOverwrite) {
|
|
95
|
+
return {
|
|
96
|
+
status: 'skipped-unmanaged-different',
|
|
97
|
+
source,
|
|
98
|
+
destination,
|
|
99
|
+
reason: `${path.basename(destination)} differs and is not recorded as managed by ${MANIFEST_MANAGED_BY}; pass --allow-unmanaged-skill-overwrite only after review`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
fs.rmSync(destination, { recursive: true, force: true });
|
|
103
|
+
fs.cpSync(source, destination, { recursive: true });
|
|
104
|
+
recordManaged();
|
|
105
|
+
return { status: manifestOwnsDestination ? 'overwritten-managed' : 'overwritten-unmanaged', source, destination };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function unmanagedSkillConflicts(selected, sourceRoot, skillsDir, currentManifest) {
|
|
109
|
+
const out = [];
|
|
110
|
+
for (const name of selected) {
|
|
111
|
+
const source = path.join(sourceRoot, name);
|
|
112
|
+
const destination = path.join(skillsDir, name);
|
|
113
|
+
if (!fs.existsSync(source) || !fs.existsSync(destination)) continue;
|
|
114
|
+
if (directoryDigest(source) === directoryDigest(destination)) continue;
|
|
115
|
+
if (!currentManifest?.skills?.[name]) {
|
|
116
|
+
out.push({
|
|
117
|
+
status: 'skipped-unmanaged-different',
|
|
118
|
+
source,
|
|
119
|
+
destination,
|
|
120
|
+
reason: `${name} differs and is not recorded as managed by ${MANIFEST_MANAGED_BY}; pass --allow-unmanaged-skill-overwrite only after review`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function installSkills(skillsDir, { force = false, skillSet = 'core', allowUnmanagedOverwrite = false } = {}) {
|
|
128
|
+
const sourceRoot = path.join(KIT_ROOT, 'skills');
|
|
129
|
+
ensureDir(skillsDir);
|
|
130
|
+
const selected = Array.isArray(skillSet) ? skillSet : skillNamesForSet(skillSet);
|
|
131
|
+
const currentManifest = loadSkillsManifest(skillsDir);
|
|
132
|
+
if (currentManifest?.invalid) return [{ status: 'invalid-manifest', source: '', destination: manifestPath(skillsDir), reason: currentManifest.reason }];
|
|
133
|
+
const nextManifest = currentManifest || { managed_by: MANIFEST_MANAGED_BY, version: packageVersion(), installed_at: nowIso(), skills: {} };
|
|
134
|
+
nextManifest.managed_by = MANIFEST_MANAGED_BY;
|
|
135
|
+
nextManifest.version = packageVersion() || String(nextManifest.version || '0.0.0');
|
|
136
|
+
nextManifest.updated_at = nowIso();
|
|
137
|
+
nextManifest.skills ||= {};
|
|
138
|
+
const conflicts = force && !allowUnmanagedOverwrite ? unmanagedSkillConflicts(selected, sourceRoot, skillsDir, currentManifest) : [];
|
|
139
|
+
if (conflicts.length) return conflicts;
|
|
140
|
+
const results = selected.map((name) => copyManagedSkill(path.join(sourceRoot, name), path.join(skillsDir, name), name, {
|
|
141
|
+
force,
|
|
142
|
+
allowUnmanagedOverwrite,
|
|
143
|
+
manifest: currentManifest,
|
|
144
|
+
nextManifest,
|
|
145
|
+
}));
|
|
146
|
+
writeSkillsManifest(skillsDir, nextManifest);
|
|
147
|
+
return results;
|
|
148
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { parseArgs } from '../shared.mjs';
|
|
5
|
+
|
|
6
|
+
export const EXIT_CONFIG_FAILURE = 3;
|
|
7
|
+
export const DEFAULT_LINE_LIMIT = 300;
|
|
8
|
+
export const MODE_ALIASES = new Map([
|
|
9
|
+
['minimal', 'minimal'],
|
|
10
|
+
['min', 'minimal'],
|
|
11
|
+
['1', 'minimal'],
|
|
12
|
+
['normal', 'normal'],
|
|
13
|
+
['default', 'normal'],
|
|
14
|
+
['2', 'normal'],
|
|
15
|
+
['full', 'full'],
|
|
16
|
+
['3', 'full'],
|
|
17
|
+
['custom', 'custom'],
|
|
18
|
+
['c', 'custom'],
|
|
19
|
+
['4', 'custom'],
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const SKILL_SET_ALIASES = new Map([
|
|
23
|
+
['core', 'core'],
|
|
24
|
+
['core-only', 'core'],
|
|
25
|
+
['basic', 'core'],
|
|
26
|
+
['vendor', 'vendor'],
|
|
27
|
+
['vendor-only', 'vendor'],
|
|
28
|
+
['all', 'all'],
|
|
29
|
+
['full', 'all'],
|
|
30
|
+
]);
|
|
31
|
+
const BOOLEAN_OPTIONS = new Set(['yes', 'force', 'skip-hooks', 'no-bridge', 'skip-deep-scan', 'install-missing', 'no-line-limit', 'allow-unmanaged-skill-overwrite']);
|
|
32
|
+
const VALUE_OPTIONS = new Set(['repo', 'skills-dir', 'hooks', 'hook', 'skill-set', 'mode', 'line-limit', 'line-limit-mode']);
|
|
33
|
+
const HELP_OPTIONS = new Set(['help', 'h']);
|
|
34
|
+
const COMMON_OPTIONS = new Set([...BOOLEAN_OPTIONS, ...VALUE_OPTIONS, ...HELP_OPTIONS]);
|
|
35
|
+
const HOOK_TARGETS = new Set(['pre-commit', 'pre-push', 'all']);
|
|
36
|
+
const HOOK_MODES = new Set(['advisory', 'blocking']);
|
|
37
|
+
const LINE_LIMIT_MODES = new Set(['advisory', 'blocking', 'off']);
|
|
38
|
+
|
|
39
|
+
function hasOption(args, key) {
|
|
40
|
+
return Object.prototype.hasOwnProperty.call(args, key);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readBooleanOption(args, key, errors) {
|
|
44
|
+
if (!hasOption(args, key)) return false;
|
|
45
|
+
if (args[key] !== true) errors.push(`--${key} does not take a value.`);
|
|
46
|
+
return args[key] === true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readPathOption(args, key, errors) {
|
|
50
|
+
if (!hasOption(args, key)) return undefined;
|
|
51
|
+
const value = args[key];
|
|
52
|
+
if (value === true || String(value).trim() === '') {
|
|
53
|
+
errors.push(`--${key} requires a path value.`);
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return String(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeMode(value, errors, { command }) {
|
|
60
|
+
if (value === undefined || value === true || String(value).trim() === '') return undefined;
|
|
61
|
+
const normalized = MODE_ALIASES.get(String(value).toLowerCase());
|
|
62
|
+
if (!normalized) {
|
|
63
|
+
errors.push(`--mode must be ${command === 'connect' ? 'normal, full, or custom' : 'minimal, normal, full, or custom'}.`);
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
if (command === 'connect' && normalized === 'minimal') {
|
|
67
|
+
errors.push('connect does not support --mode minimal because connect always changes the current project.');
|
|
68
|
+
}
|
|
69
|
+
return normalized;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeSkillSet(value, errors) {
|
|
73
|
+
const requested = value === undefined ? 'all' : String(value).toLowerCase();
|
|
74
|
+
const skillSet = SKILL_SET_ALIASES.get(requested);
|
|
75
|
+
if (!skillSet) errors.push('--skill-set must be core, vendor, or all.');
|
|
76
|
+
return skillSet || 'all';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeHookMode(value, errors) {
|
|
80
|
+
if (value === undefined) return undefined;
|
|
81
|
+
if (value === true) return 'advisory';
|
|
82
|
+
const requested = String(value || '').toLowerCase();
|
|
83
|
+
const hookMode = requested === 'true' ? 'advisory' : requested;
|
|
84
|
+
if (!HOOK_MODES.has(hookMode)) errors.push('--hooks must be advisory or blocking.');
|
|
85
|
+
return HOOK_MODES.has(hookMode) ? hookMode : undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeHookTargets(value, errors) {
|
|
89
|
+
if (value === undefined) return undefined;
|
|
90
|
+
const requested = value === true ? 'pre-commit' : String(value || '').toLowerCase();
|
|
91
|
+
if (!HOOK_TARGETS.has(requested)) {
|
|
92
|
+
errors.push('--hook must be pre-commit, pre-push, or all.');
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
return requested === 'all' ? ['pre-commit', 'pre-push'] : [requested];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function normalizeLineLimit(value, errors) {
|
|
99
|
+
if (value === undefined) return DEFAULT_LINE_LIMIT;
|
|
100
|
+
if (value === true || String(value).trim() === '') {
|
|
101
|
+
errors.push('--line-limit requires a positive number.');
|
|
102
|
+
return DEFAULT_LINE_LIMIT;
|
|
103
|
+
}
|
|
104
|
+
const parsed = Number(String(value).trim());
|
|
105
|
+
if (!Number.isInteger(parsed) || parsed < 50 || parsed > 5000) {
|
|
106
|
+
errors.push('--line-limit must be an integer from 50 to 5000.');
|
|
107
|
+
return DEFAULT_LINE_LIMIT;
|
|
108
|
+
}
|
|
109
|
+
return parsed;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function normalizeLineLimitMode(value, errors) {
|
|
113
|
+
if (value === undefined) return undefined;
|
|
114
|
+
if (value === true || String(value).trim() === '') return 'advisory';
|
|
115
|
+
const normalized = String(value).toLowerCase();
|
|
116
|
+
if (!LINE_LIMIT_MODES.has(normalized)) {
|
|
117
|
+
errors.push('--line-limit-mode must be advisory, blocking, or off.');
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
return normalized;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function usage(command = 'install') {
|
|
124
|
+
if (command === 'connect') {
|
|
125
|
+
console.log(`jhste-skills connect
|
|
126
|
+
Usage:
|
|
127
|
+
jhste-skills connect [--mode normal|full|custom] [--repo <path>] [--skills-dir <path>]
|
|
128
|
+
jhste-skills connect --yes --mode normal|full [--install-missing] [--skip-hooks | --hooks advisory|blocking]
|
|
129
|
+
Notes:
|
|
130
|
+
connect attaches an existing jhste-skills install to the current git repository.
|
|
131
|
+
connect requires a git repository and never overwrites non-managed hooks.
|
|
132
|
+
--mode minimal is intentionally invalid for connect.
|
|
133
|
+
`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
console.log(`jhste-skills install
|
|
137
|
+
Usage:
|
|
138
|
+
jhste-skills install [--mode minimal|normal|full|custom] [--yes] [--repo <path>] [--skills-dir <path>]
|
|
139
|
+
jhste-skills install --yes [--skill-set core|vendor|all] [--line-limit <lines>] [--line-limit-mode advisory|blocking|off]
|
|
140
|
+
jhste-skills install --yes [--skip-hooks | --hooks advisory|blocking] [--hook pre-commit|pre-push|all]
|
|
141
|
+
Notes:
|
|
142
|
+
Non-interactive installs require explicit --yes or -y.
|
|
143
|
+
The default mode is normal.
|
|
144
|
+
Normal installs all bundled skills by default; use --skill-set core for only jhste core skills.
|
|
145
|
+
Full installs all safe managed features; blocking hooks require an explicit interactive or CLI choice.
|
|
146
|
+
Line limit defaults to 300 lines when repo profile writing is enabled.
|
|
147
|
+
--skip-hooks and --hooks are mutually exclusive.
|
|
148
|
+
--force does not overwrite unmanaged skill directories; use --allow-unmanaged-skill-overwrite only after review.
|
|
149
|
+
`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function normalizeOptions(argv, { command, cwd, nonInteractive }) {
|
|
153
|
+
const args = parseArgs(argv);
|
|
154
|
+
if (args.help || args.h) return { help: true, errors: [] };
|
|
155
|
+
const errors = [];
|
|
156
|
+
for (const key of Object.keys(args)) {
|
|
157
|
+
if (key !== '_' && !COMMON_OPTIONS.has(key)) errors.push(`unknown option --${key}.`);
|
|
158
|
+
}
|
|
159
|
+
if (args._.length > 0) errors.push(`unexpected positional argument: ${args._[0]}`);
|
|
160
|
+
const yes = readBooleanOption(args, 'yes', errors);
|
|
161
|
+
const force = readBooleanOption(args, 'force', errors);
|
|
162
|
+
const skipHooks = readBooleanOption(args, 'skip-hooks', errors);
|
|
163
|
+
const noBridge = readBooleanOption(args, 'no-bridge', errors);
|
|
164
|
+
const skipDeepScan = readBooleanOption(args, 'skip-deep-scan', errors);
|
|
165
|
+
const installMissing = readBooleanOption(args, 'install-missing', errors);
|
|
166
|
+
const noLineLimit = readBooleanOption(args, 'no-line-limit', errors);
|
|
167
|
+
const allowUnmanagedSkillOverwrite = readBooleanOption(args, 'allow-unmanaged-skill-overwrite', errors);
|
|
168
|
+
const repoInput = readPathOption(args, 'repo', errors);
|
|
169
|
+
const skillsDirInput = readPathOption(args, 'skills-dir', errors);
|
|
170
|
+
const mode = normalizeMode(hasOption(args, 'mode') ? args.mode : undefined, errors, { command });
|
|
171
|
+
const skillSet = normalizeSkillSet(hasOption(args, 'skill-set') ? args['skill-set'] : undefined, errors);
|
|
172
|
+
const explicitSkillSet = hasOption(args, 'skill-set');
|
|
173
|
+
const explicitHooks = hasOption(args, 'hooks');
|
|
174
|
+
const explicitHookTargets = hasOption(args, 'hook');
|
|
175
|
+
const hookMode = normalizeHookMode(explicitHooks ? args.hooks : undefined, errors);
|
|
176
|
+
const hookTargets = normalizeHookTargets(explicitHookTargets ? args.hook : undefined, errors);
|
|
177
|
+
const explicitLineLimit = hasOption(args, 'line-limit');
|
|
178
|
+
const explicitLineLimitMode = hasOption(args, 'line-limit-mode');
|
|
179
|
+
const lineLimit = normalizeLineLimit(explicitLineLimit ? args['line-limit'] : undefined, errors);
|
|
180
|
+
const lineLimitMode = normalizeLineLimitMode(explicitLineLimitMode ? args['line-limit-mode'] : undefined, errors);
|
|
181
|
+
|
|
182
|
+
if (skipHooks && explicitHooks) errors.push('--skip-hooks and --hooks are mutually exclusive.');
|
|
183
|
+
if (skipHooks && explicitHookTargets) errors.push('--skip-hooks and --hook are mutually exclusive.');
|
|
184
|
+
if (noLineLimit && explicitLineLimit) errors.push('--no-line-limit and --line-limit are mutually exclusive.');
|
|
185
|
+
if (noLineLimit && explicitLineLimitMode && lineLimitMode !== 'off') errors.push('--no-line-limit conflicts with --line-limit-mode unless it is off.');
|
|
186
|
+
if (skipHooks && lineLimitMode === 'blocking') errors.push('--line-limit-mode blocking requires managed hooks; do not combine it with --skip-hooks.');
|
|
187
|
+
|
|
188
|
+
const repoStart = path.resolve(repoInput || cwd);
|
|
189
|
+
if (repoInput) {
|
|
190
|
+
try {
|
|
191
|
+
if (!fs.statSync(repoStart).isDirectory()) errors.push(`--repo must be a directory: ${repoInput}`);
|
|
192
|
+
} catch {
|
|
193
|
+
errors.push(`--repo path does not exist: ${repoInput}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const skillsDir = path.resolve(skillsDirInput || path.join(os.homedir(), '.jhste', 'skills'));
|
|
198
|
+
if (fs.existsSync(skillsDir) && !fs.statSync(skillsDir).isDirectory()) {
|
|
199
|
+
errors.push(`--skills-dir must be a directory: ${skillsDirInput || skillsDir}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (nonInteractive && !yes) errors.push(`non-interactive ${command} requires explicit --yes or -y; refusing to change files.`);
|
|
203
|
+
if (yes && mode === 'custom') errors.push('--mode custom requires interactive answers; do not combine it with --yes.');
|
|
204
|
+
if (nonInteractive && mode === 'custom') errors.push('--mode custom requires an interactive terminal.');
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
args,
|
|
208
|
+
command,
|
|
209
|
+
errors,
|
|
210
|
+
explicitHookTargets,
|
|
211
|
+
explicitHooks,
|
|
212
|
+
explicitMode: hasOption(args, 'mode'),
|
|
213
|
+
explicitRepo: Boolean(repoInput),
|
|
214
|
+
explicitLineLimit,
|
|
215
|
+
explicitLineLimitMode,
|
|
216
|
+
explicitSkillSet,
|
|
217
|
+
force,
|
|
218
|
+
hookMode,
|
|
219
|
+
hookTargets,
|
|
220
|
+
installMissing,
|
|
221
|
+
allowUnmanagedSkillOverwrite,
|
|
222
|
+
lineLimit,
|
|
223
|
+
lineLimitMode,
|
|
224
|
+
mode,
|
|
225
|
+
noBridge,
|
|
226
|
+
noLineLimit,
|
|
227
|
+
repoStart,
|
|
228
|
+
skillSet,
|
|
229
|
+
skillsDir,
|
|
230
|
+
skipDeepScan,
|
|
231
|
+
skipHooks,
|
|
232
|
+
yes,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { ask, relativeDisplay } from '../shared.mjs';
|
|
2
|
+
import { EXIT_CONFIG_FAILURE } from './options.mjs';
|
|
3
|
+
|
|
4
|
+
function describeSkillSet(skillSet) {
|
|
5
|
+
if (skillSet === 'detected') return 'Currently installed skills';
|
|
6
|
+
if (skillSet === 'all') return 'Core features + all optional features';
|
|
7
|
+
if (skillSet === 'vendor') return 'Optional features only (advanced option)';
|
|
8
|
+
return 'Core features';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function describeHookMode(mode) {
|
|
12
|
+
return mode === 'blocking' ? 'Block on detected issues' : 'Show warnings only';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function printPlanSummary(plan) {
|
|
16
|
+
const heading = {
|
|
17
|
+
connect: 'Connection plan',
|
|
18
|
+
install: 'Install plan',
|
|
19
|
+
sync: 'Sync plan',
|
|
20
|
+
update: 'Update plan',
|
|
21
|
+
}[plan.command] || 'Plan';
|
|
22
|
+
console.log(`\n${heading}:`);
|
|
23
|
+
console.log(`- Command: ${plan.command}`);
|
|
24
|
+
console.log(`- Mode: ${plan.mode}`);
|
|
25
|
+
if (plan.overrides.length) console.log(`- Applied overrides: ${plan.overrides.join(', ')}`);
|
|
26
|
+
console.log(`- Feature set: ${describeSkillSet(plan.skillSet)}`);
|
|
27
|
+
console.log(`- Skills directory: ${plan.skillsDir}`);
|
|
28
|
+
if (plan.preflight.skills.enabled) {
|
|
29
|
+
console.log(`- Skills: ${plan.preflight.skills.expected} to install or update`);
|
|
30
|
+
} else if (plan.command === 'connect') {
|
|
31
|
+
if (plan.preflight.skills.action === 'install-missing') {
|
|
32
|
+
console.log(`- Skills: install missing after checking existing copies (${plan.preflight.skills.expected} required, missing=${plan.preflight.skills.missing.length})`);
|
|
33
|
+
} else {
|
|
34
|
+
console.log(`- Skills: use existing installation (${plan.preflight.skills.expected} required, missing=${plan.preflight.skills.missing.length})`);
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
console.log('- Skills: not installing');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (plan.connectRepo) {
|
|
41
|
+
console.log(`- Current project: ${plan.repoRoot}`);
|
|
42
|
+
console.log(`- Config file: ${plan.preflight.profile.status}`);
|
|
43
|
+
if (plan.writeBridge) {
|
|
44
|
+
for (const bridge of plan.preflight.bridges) {
|
|
45
|
+
console.log(`- AI bridge: ${bridge.fileName} ${bridge.status}`);
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
console.log('- AI bridge: not adding');
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
console.log(`- Current project: ${plan.repoSkippedReason || 'not connecting'}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
printLineLimitSummary(plan);
|
|
55
|
+
printHookSummary(plan);
|
|
56
|
+
if (plan.preflight.deepScan.enabled) {
|
|
57
|
+
console.log('- Deep scan: run now (may take a few minutes, does not modify source code)');
|
|
58
|
+
console.log(` - Report: ${relativeDisplay(plan.repoRoot, plan.preflight.deepScan.report)}`);
|
|
59
|
+
console.log(` - Recommended settings: ${relativeDisplay(plan.repoRoot, plan.preflight.deepScan.recommendedProfile)}`);
|
|
60
|
+
} else {
|
|
61
|
+
console.log('- Deep scan: not running');
|
|
62
|
+
}
|
|
63
|
+
console.log('- Left untouched: CI, package.json, lockfile, source code, non-managed hooks');
|
|
64
|
+
if (plan.force) {
|
|
65
|
+
console.log('- Note: --force refreshes only jhste-managed outputs and does not overwrite user-owned hooks, source, or CI files');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function printLineLimitSummary(plan) {
|
|
70
|
+
if (!plan.writeProfile || !plan.lineLimit) return;
|
|
71
|
+
if (plan.lineLimit.enabled) {
|
|
72
|
+
const behavior = plan.lineLimit.enforcement === 'blocking' ? 'block commits' : 'show warnings only';
|
|
73
|
+
console.log(`- Line limit: ${behavior} above ${plan.lineLimit.maxLines} lines`);
|
|
74
|
+
} else {
|
|
75
|
+
console.log('- Line limit: not used');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function printHookSummary(plan) {
|
|
80
|
+
if (plan.hooks.length) {
|
|
81
|
+
console.log('- Automatic checks:');
|
|
82
|
+
for (const hook of plan.preflight.hooks) {
|
|
83
|
+
const failOn = hook.failOn && hook.failOn !== 'none' ? `, fail-on=${hook.failOn}` : '';
|
|
84
|
+
console.log(` - ${hook.target}: ${describeHookMode(hook.mode)}${failOn} (${hook.status})`);
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
console.log('- Automatic checks: not installing');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function confirmPlan(plan) {
|
|
92
|
+
if (plan.yes) {
|
|
93
|
+
console.log('\n--yes was provided, so this will continue without confirmation.');
|
|
94
|
+
return 'yes';
|
|
95
|
+
}
|
|
96
|
+
const answer = await ask('\nContinue? [Enter=yes / c=customize / q=cancel] ');
|
|
97
|
+
const normalized = String(answer).trim().toLowerCase();
|
|
98
|
+
if (normalized === 'q' || normalized === 'n' || normalized === 'no') return 'cancel';
|
|
99
|
+
if (normalized === 'c') return 'custom';
|
|
100
|
+
return 'yes';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function printConfigErrors(command, errors) {
|
|
104
|
+
for (const error of errors) console.error(`jhste-skills ${command}: ${error}`);
|
|
105
|
+
process.exitCode = EXIT_CONFIG_FAILURE;
|
|
106
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { DEFAULT_LINE_LIMIT } from './options.mjs';
|
|
2
|
+
|
|
3
|
+
export function defaultLineLimit() {
|
|
4
|
+
return { enabled: true, maxLines: DEFAULT_LINE_LIMIT, enforcement: 'advisory' };
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function disabledLineLimit() {
|
|
8
|
+
return { enabled: false, maxLines: DEFAULT_LINE_LIMIT, enforcement: 'off' };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function hookActions(targets, mode, failOn) {
|
|
12
|
+
return targets.map((target) => ({
|
|
13
|
+
target,
|
|
14
|
+
mode,
|
|
15
|
+
failOn: failOn || (mode === 'blocking' ? 'error' : 'none'),
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function targetList(plan, fallback = ['pre-commit']) {
|
|
20
|
+
const existing = [...new Set((plan.hooks || []).map((hook) => hook.target))];
|
|
21
|
+
return existing.length ? existing : fallback;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function applyLineLimitToHooks(plan, options) {
|
|
25
|
+
if (!plan.lineLimit?.enabled || plan.lineLimit.enforcement !== 'blocking') return;
|
|
26
|
+
if (options.skipHooks) return;
|
|
27
|
+
const targets = targetList(plan);
|
|
28
|
+
plan.hooks = hookActions(targets, 'blocking', 'warning');
|
|
29
|
+
}
|