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.
Files changed (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +254 -0
  3. package/README.ko.md +254 -0
  4. package/README.md +254 -0
  5. package/README.zh.md +254 -0
  6. package/adapters/claude/README.md +7 -0
  7. package/adapters/codex/README.md +25 -0
  8. package/adapters/generic/README.md +7 -0
  9. package/cli/baseline.mjs +32 -0
  10. package/cli/connect.mjs +84 -0
  11. package/cli/deep-scan/analyze.mjs +167 -0
  12. package/cli/deep-scan/collect.mjs +133 -0
  13. package/cli/deep-scan/report.mjs +197 -0
  14. package/cli/deep-scan.mjs +56 -0
  15. package/cli/guard/baseline.mjs +64 -0
  16. package/cli/guard/config.mjs +48 -0
  17. package/cli/guard/profile-commands.mjs +87 -0
  18. package/cli/guard/registry.mjs +47 -0
  19. package/cli/guard/reporting.mjs +165 -0
  20. package/cli/guard/scanners/code-health.mjs +213 -0
  21. package/cli/guard/scanners/data-boundary-locality.mjs +125 -0
  22. package/cli/guard/scanners/data-boundary.mjs +237 -0
  23. package/cli/guard/scanners/external-input.mjs +74 -0
  24. package/cli/guard/scanners/index.mjs +136 -0
  25. package/cli/guard/scanners/single-responsibility.mjs +205 -0
  26. package/cli/guard/scanners/ui-runtime.mjs +140 -0
  27. package/cli/guard/scanners/utils.mjs +167 -0
  28. package/cli/guard/scope.mjs +181 -0
  29. package/cli/guard.mjs +125 -0
  30. package/cli/hook-utils.mjs +127 -0
  31. package/cli/hooks.mjs +127 -0
  32. package/cli/index.mjs +35 -0
  33. package/cli/install-actions/apply-plan.mjs +39 -0
  34. package/cli/install-actions/bridge-writer.mjs +52 -0
  35. package/cli/install-actions/output.mjs +45 -0
  36. package/cli/install-actions/preflight.mjs +58 -0
  37. package/cli/install-actions/profile-writer.mjs +21 -0
  38. package/cli/install-actions/skills.mjs +148 -0
  39. package/cli/install-actions.mjs +4 -0
  40. package/cli/install-flow/options.mjs +234 -0
  41. package/cli/install-flow/output.mjs +106 -0
  42. package/cli/install-flow/plan-helpers.mjs +29 -0
  43. package/cli/install-flow/plan.mjs +200 -0
  44. package/cli/install-flow/prompts.mjs +210 -0
  45. package/cli/install-flow.mjs +16 -0
  46. package/cli/install.mjs +77 -0
  47. package/cli/json-file.mjs +39 -0
  48. package/cli/profile/loader.mjs +13 -0
  49. package/cli/profile/parser.mjs +226 -0
  50. package/cli/profile/schema.mjs +81 -0
  51. package/cli/profile/settings.mjs +45 -0
  52. package/cli/profile/validator.mjs +86 -0
  53. package/cli/profile.mjs +5 -0
  54. package/cli/shared/args.mjs +32 -0
  55. package/cli/shared/files.mjs +70 -0
  56. package/cli/shared/git.mjs +28 -0
  57. package/cli/shared/paths.mjs +27 -0
  58. package/cli/shared/prompt.mjs +32 -0
  59. package/cli/shared/templates.mjs +71 -0
  60. package/cli/shared/time.mjs +3 -0
  61. package/cli/shared.mjs +7 -0
  62. package/cli/sync-core.mjs +213 -0
  63. package/cli/sync.mjs +7 -0
  64. package/cli/tune.mjs +101 -0
  65. package/cli/uninstall.mjs +288 -0
  66. package/cli/update.mjs +7 -0
  67. package/docs/ACCEPTANCE_CHECK.md +54 -0
  68. package/docs/CLI.md +212 -0
  69. package/docs/CONFLICT_RESOLUTION.md +58 -0
  70. package/docs/PUBLIC_SAFETY.md +26 -0
  71. package/docs/RULES.md +94 -0
  72. package/docs/VENDORING.md +23 -0
  73. package/examples/profile.yaml +45 -0
  74. package/package.json +51 -0
  75. package/packs/api.yaml +13 -0
  76. package/packs/core.yaml +19 -0
  77. package/packs/crawler.yaml +8 -0
  78. package/packs/database.yaml +8 -0
  79. package/packs/web.yaml +10 -0
  80. package/rules/core/api_contract_compatibility.yaml +25 -0
  81. package/rules/core/authz_data_isolation.yaml +27 -0
  82. package/rules/core/build_runtime_env_safety.yaml +26 -0
  83. package/rules/core/external_input_validation.yaml +27 -0
  84. package/rules/core/file_size_advisory.yaml +28 -0
  85. package/rules/core/no_secret_logging.yaml +24 -0
  86. package/rules/core/no_silent_failure.yaml +30 -0
  87. package/rules/core/null_state_safety.yaml +25 -0
  88. package/rules/core/performance_duplicate_fetch.yaml +25 -0
  89. package/rules/core/public_safe_error.yaml +24 -0
  90. package/rules/core/responsibility_budget.yaml +44 -0
  91. package/rules/core/side_effect_boundary.yaml +24 -0
  92. package/rules/core/single_responsibility_advisory.yaml +35 -0
  93. package/rules/core/workflow_security.yaml +25 -0
  94. package/rules/core/write_safety_idempotency.yaml +25 -0
  95. package/rules/crawler/crawler_producer_boundary.yaml +24 -0
  96. package/rules/database/db_row_validation.yaml +24 -0
  97. package/rules/database/sql_parameter_binding.yaml +24 -0
  98. package/rules/nextjs/thin_api_route.yaml +24 -0
  99. package/rules/python/broad_exception_advisory.yaml +24 -0
  100. package/rules/react/component_responsibility.yaml +24 -0
  101. package/rules/typescript/type_escape_advisory.yaml +24 -0
  102. package/scripts/docs-check-data.mjs +71 -0
  103. package/scripts/docs-check.mjs +261 -0
  104. package/scripts/guard-fixtures/helpers.mjs +58 -0
  105. package/scripts/guard-fixtures-test.mjs +273 -0
  106. package/scripts/profile-fixtures-test.mjs +83 -0
  107. package/scripts/public-safety-check.mjs +88 -0
  108. package/scripts/public-safety-fixtures-test.mjs +60 -0
  109. package/scripts/release-gates-test.mjs +52 -0
  110. package/scripts/single-responsibility-fixtures-test.mjs +86 -0
  111. package/scripts/smoke/connect-scenarios.mjs +47 -0
  112. package/scripts/smoke/fixture.mjs +49 -0
  113. package/scripts/smoke/guard-and-hook-scenarios.mjs +211 -0
  114. package/scripts/smoke/helpers.mjs +51 -0
  115. package/scripts/smoke/install-scenarios.mjs +244 -0
  116. package/scripts/smoke/mode-scenarios.mjs +76 -0
  117. package/scripts/smoke-test.mjs +17 -0
  118. package/scripts/syntax-check.mjs +37 -0
  119. package/scripts/vendor-check.mjs +87 -0
  120. package/skills/codebase-design/DEEPENING.md +37 -0
  121. package/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  122. package/skills/codebase-design/SKILL.md +122 -0
  123. package/skills/diagnose/SKILL.md +125 -0
  124. package/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
  125. package/skills/diagnosing-bugs/SKILL.md +142 -0
  126. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
  127. package/skills/domain-modeling/ADR-FORMAT.md +47 -0
  128. package/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  129. package/skills/domain-modeling/SKILL.md +82 -0
  130. package/skills/grill-me/SKILL.md +18 -0
  131. package/skills/grill-with-docs/ADR-FORMAT.md +47 -0
  132. package/skills/grill-with-docs/CONTEXT-FORMAT.md +60 -0
  133. package/skills/grill-with-docs/SKILL.md +96 -0
  134. package/skills/grilling/SKILL.md +18 -0
  135. package/skills/handoff/SKILL.md +23 -0
  136. package/skills/improve-codebase-architecture/DEEPENING.md +37 -0
  137. package/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
  138. package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
  139. package/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
  140. package/skills/improve-codebase-architecture/SKILL.md +93 -0
  141. package/skills/jhste-architecture-review/SKILL.md +28 -0
  142. package/skills/jhste-architecture-review/references/architecture-review.md +41 -0
  143. package/skills/jhste-code-quality/SKILL.md +33 -0
  144. package/skills/jhste-code-quality/references/code-quality.md +45 -0
  145. package/skills/jhste-crawler-automation/SKILL.md +23 -0
  146. package/skills/jhste-crawler-automation/references/crawler-automation.md +11 -0
  147. package/skills/jhste-db-api-boundary/SKILL.md +28 -0
  148. package/skills/jhste-db-api-boundary/references/db-api-boundary.md +21 -0
  149. package/skills/jhste-engineering-judgment/SKILL.md +107 -0
  150. package/skills/jhste-engineering-judgment/references/structure-templates.md +41 -0
  151. package/skills/jhste-red-team-review/SKILL.md +101 -0
  152. package/skills/jhste-red-team-review/references/red-team-review.md +83 -0
  153. package/skills/prototype/LOGIC.md +79 -0
  154. package/skills/prototype/SKILL.md +38 -0
  155. package/skills/prototype/UI.md +112 -0
  156. package/skills/setup/SKILL.md +21 -0
  157. package/skills/setup/references/conflict-policy.md +11 -0
  158. package/skills/setup/references/setup-flow.md +18 -0
  159. package/skills/to-issues/SKILL.md +91 -0
  160. package/skills/to-prd/SKILL.md +82 -0
  161. package/skills/triage/AGENT-BRIEF.md +168 -0
  162. package/skills/triage/OUT-OF-SCOPE.md +101 -0
  163. package/skills/triage/SKILL.md +111 -0
  164. package/skills/write-a-skill/SKILL.md +125 -0
  165. package/vendor/matt-pocock/LICENSE +21 -0
  166. package/vendor/matt-pocock/NOTICE.md +10 -0
  167. package/vendor/matt-pocock/allowlist.json +16 -0
  168. package/vendor/matt-pocock/source-lock.json +119 -0
package/cli/guard.mjs ADDED
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+ import path from 'node:path';
3
+ import { KIT_ROOT, findGitRoot, parseArgs, relativeDisplay } from './shared.mjs';
4
+ import {
5
+ fileSizeSettings,
6
+ loadProfileConfig,
7
+ responsibilityBudgetSettings,
8
+ singleResponsibilitySettings,
9
+ validateProfileConfig,
10
+ } from './profile.mjs';
11
+ import { readJsonFile, validateJsonObject } from './json-file.mjs';
12
+ import { loadBaseline, writeBaseline, applyBaseline } from './guard/baseline.mjs';
13
+ import { resolveGuardConfig } from './guard/config.mjs';
14
+ import { profileCommandExecutionErrors, runProfileCommands } from './guard/profile-commands.mjs';
15
+ import { guardResult, printResult, exitCodeFor } from './guard/reporting.mjs';
16
+ import { resolveScopeFiles } from './guard/scope.mjs';
17
+ import { scanFile } from './guard/scanners/index.mjs';
18
+
19
+ const EXIT_VIOLATION = 1;
20
+ const EXIT_GUARD_FAILURE = 2;
21
+ const EXIT_CONFIG_FAILURE = 3;
22
+ let currentFormat = 'text';
23
+
24
+ function inManagedHook() {
25
+ return process.env.JHSTE_HOOK_ACTIVE === '1';
26
+ }
27
+
28
+ function failGuard(message, details = []) {
29
+ const result = guardResult([], [{ code: 'guard.runtime', message, details }]);
30
+ printResult(result, currentFormat);
31
+ process.exit(EXIT_GUARD_FAILURE);
32
+ }
33
+
34
+ function failConfig(message, details = []) {
35
+ const result = guardResult([], [{ code: 'guard.config', message, details }]);
36
+ printResult(result, currentFormat);
37
+ process.exit(EXIT_CONFIG_FAILURE);
38
+ }
39
+
40
+ function toolVersion() {
41
+ try {
42
+ const pkg = readJsonFile(path.join(KIT_ROOT, 'package.json'), {
43
+ description: 'package.json',
44
+ validate: validateJsonObject,
45
+ });
46
+ return String(pkg.version || '0.0.0');
47
+ } catch {
48
+ return '0.0.0';
49
+ }
50
+ }
51
+
52
+ function requestedOutputFormat(args, profileState = null) {
53
+ const requested = String(args.format || profileState?.profile?.guard?.default_format || 'text');
54
+ return ['text', 'json'].includes(requested) ? requested : 'text';
55
+ }
56
+
57
+ async function main() {
58
+ const startedAt = Date.now();
59
+ const args = parseArgs(process.argv.slice(2));
60
+ currentFormat = requestedOutputFormat(args);
61
+ const repoRoot = findGitRoot(args.repo || process.cwd());
62
+ const profileState = loadProfileConfig(repoRoot);
63
+ currentFormat = requestedOutputFormat(args, profileState);
64
+ const profileErrors = validateProfileConfig(profileState.profile);
65
+ if (profileErrors.length) failConfig(`Invalid profile ${relativeDisplay(repoRoot, profileState.path)}.`, profileErrors);
66
+ const { format, failOn, baselineMode, baselinePath, scopedArgs } = resolveGuardConfig(args, profileState, repoRoot, {
67
+ failConfig,
68
+ inManagedHook,
69
+ });
70
+ currentFormat = format;
71
+ const scope = resolveScopeFiles(repoRoot, scopedArgs, { failConfig, failGuard });
72
+ const violations = [];
73
+ const failures = [];
74
+ const scanSettings = {
75
+ profile: profileState.profile,
76
+ scope: scope.scope,
77
+ fileSize: fileSizeSettings(profileState.profile),
78
+ responsibilityBudget: responsibilityBudgetSettings(profileState.profile),
79
+ singleResponsibility: singleResponsibilitySettings(profileState.profile),
80
+ };
81
+ for (const relPath of scope.files) {
82
+ const result = scanFile(repoRoot, relPath, scanSettings);
83
+ violations.push(...result.violations);
84
+ if (result.failure) failures.push(result.failure);
85
+ }
86
+ if (args['run-profile-commands']) {
87
+ if (inManagedHook()) {
88
+ failConfig('Managed hook execution is read-only; --run-profile-commands is not allowed while JHSTE_HOOK_ACTIVE=1.');
89
+ }
90
+ const executionErrors = profileCommandExecutionErrors(profileState.profile.commands, {
91
+ trusted: Boolean(args['trust-repo-profile']),
92
+ allowShell: Boolean(args['allow-profile-shell']),
93
+ });
94
+ if (executionErrors.length) failConfig('Profile command execution requires explicit trust.', executionErrors);
95
+ const profile = runProfileCommands(repoRoot, profileState.profile.commands, {
96
+ allowShell: Boolean(args['allow-profile-shell']),
97
+ });
98
+ violations.push(...profile.violations);
99
+ failures.push(...profile.failures);
100
+ }
101
+ const baselineMap = loadBaseline(repoRoot, baselinePath, { failConfig });
102
+ if (baselineMode === 'update' && failures.length === 0) writeBaseline(baselinePath, violations, baselineMap);
103
+ const managed = applyBaseline(violations, baselineMap, baselineMode, { failConfig });
104
+ const result = guardResult(managed, failures, {
105
+ tool_version: toolVersion(),
106
+ scope: scope.scope,
107
+ files_considered: scope.files_considered,
108
+ files_scanned: scope.files.length,
109
+ fail_on: failOn,
110
+ baseline_mode: baselineMode,
111
+ baseline_path: relativeDisplay(repoRoot, baselinePath),
112
+ profile_path: profileState.exists ? relativeDisplay(repoRoot, profileState.path) : null,
113
+ duration_ms: Date.now() - startedAt,
114
+ git: scope.git,
115
+ });
116
+ printResult(result, format);
117
+ process.exit(exitCodeFor(result, failOn, baselineMode));
118
+ }
119
+
120
+ main().catch((error) => {
121
+ const message = error instanceof Error ? error.message : String(error);
122
+ const result = guardResult([], [{ code: 'guard.unhandled', message, details: [] }]);
123
+ printResult(result, currentFormat);
124
+ process.exit(EXIT_GUARD_FAILURE);
125
+ });
@@ -0,0 +1,127 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { ensureDir, KIT_ROOT, relativeDisplay } from './shared.mjs';
6
+ import { readJsonFile, validateJsonObject } from './json-file.mjs';
7
+
8
+ export const EXIT_CONFIG_FAILURE = 3;
9
+ export const MANAGED_START = '# jhste-skills managed hook start';
10
+ export const MANAGED_END = '# jhste-skills managed hook end';
11
+ export const HOOKS = new Set(['pre-commit', 'pre-push']);
12
+
13
+ export function git(repoRoot, args) {
14
+ return execFileSync('git', ['-C', repoRoot, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
15
+ }
16
+
17
+ export function gitHooksDir(repoRoot) {
18
+ git(repoRoot, ['rev-parse', '--is-inside-work-tree']);
19
+ return git(repoRoot, ['rev-parse', '--path-format=absolute', '--git-path', 'hooks']);
20
+ }
21
+
22
+ export function selectedHooks(value) {
23
+ const hook = String(value || 'pre-commit');
24
+ if (hook === 'all') return ['pre-commit', 'pre-push'];
25
+ if (!HOOKS.has(hook)) throw new Error('--hook must be pre-commit, pre-push, or all.');
26
+ return [hook];
27
+ }
28
+
29
+ export function isManagedHook(content) {
30
+ return content.includes(MANAGED_START) && content.includes(MANAGED_END);
31
+ }
32
+
33
+ export function guardScopeForHook(hook) {
34
+ return hook === 'pre-push' ? 'changed' : 'staged';
35
+ }
36
+
37
+ function toolVersion() {
38
+ try {
39
+ const pkg = readJsonFile(path.join(KIT_ROOT, 'package.json'), {
40
+ description: 'package.json',
41
+ validate: validateJsonObject,
42
+ });
43
+ return String(pkg.version || '0.0.0');
44
+ } catch {
45
+ return '0.0.0';
46
+ }
47
+ }
48
+
49
+ export function hookScript({ hook, mode, failOn }) {
50
+ const scope = guardScopeForHook(hook);
51
+ const cliPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'index.mjs');
52
+ const escapedCliPath = cliPath.replaceAll("'", "'\\''");
53
+ return `#!/usr/bin/env sh
54
+ ${MANAGED_START}
55
+ # mode=${mode} hook=${hook} scope=${scope}
56
+ # jhste-skills version=${toolVersion()}
57
+ set -u
58
+ if [ "\${JHSTE_HOOK_ACTIVE:-}" = "1" ]; then
59
+ echo "jhste-skills: nested managed hook invocation skipped."
60
+ exit 0
61
+ fi
62
+ run_jhste_skills() {
63
+ if [ -f '${escapedCliPath}' ]; then
64
+ node '${escapedCliPath}' "$@"
65
+ elif command -v jhste-skills >/dev/null 2>&1; then
66
+ jhste-skills "$@"
67
+ else
68
+ echo "jhste-skills: CLI not found. Expected '${escapedCliPath}' or jhste-skills on PATH." >&2
69
+ return 2
70
+ fi
71
+ }
72
+
73
+ echo "jhste-skills: running guard --scope ${scope} --fail-on ${failOn} (${mode})"
74
+ export JHSTE_HOOK_ACTIVE=1
75
+ run_jhste_skills guard --scope ${scope} --format text --fail-on ${failOn}
76
+ status=$?
77
+ if [ "$status" -eq 2 ] || [ "$status" -eq 3 ]; then
78
+ echo "jhste-skills: guard runtime/config failure is not a validation pass (exit $status)."
79
+ fi
80
+ if [ "${mode}" = "advisory" ]; then
81
+ if [ "$status" -ne 0 ]; then
82
+ echo "jhste-skills: advisory hook reported issues but did not block. Use --mode blocking to enforce."
83
+ fi
84
+ exit 0
85
+ fi
86
+ exit "$status"
87
+ ${MANAGED_END}
88
+ `;
89
+ }
90
+
91
+ export function preflightHookTarget(repoRoot, target) {
92
+ let hooksDir;
93
+ try {
94
+ hooksDir = gitHooksDir(repoRoot);
95
+ } catch {
96
+ return { target, status: 'failed', reason: `not a git repository: ${repoRoot}` };
97
+ }
98
+
99
+ const file = path.join(hooksDir, target);
100
+ if (!fs.existsSync(file)) return { target, status: 'will-install', path: file };
101
+ const existing = fs.readFileSync(file, 'utf8');
102
+ if (isManagedHook(existing)) return { target, status: 'will-refresh', path: file };
103
+ return {
104
+ target,
105
+ status: 'skipped-non-managed',
106
+ path: file,
107
+ reason: `${relativeDisplay(repoRoot, file)} already exists and is not managed by jhste-skills`,
108
+ };
109
+ }
110
+
111
+ export function installHookTarget(repoRoot, { target, mode, failOn }) {
112
+ const preflight = preflightHookTarget(repoRoot, target);
113
+ if (preflight.status === 'failed' || preflight.status === 'skipped-non-managed') {
114
+ return { ...preflight, mode, failOn };
115
+ }
116
+ const hooksDir = path.dirname(preflight.path);
117
+ ensureDir(hooksDir);
118
+ fs.writeFileSync(preflight.path, hookScript({ hook: target, mode, failOn }), { mode: 0o755 });
119
+ fs.chmodSync(preflight.path, 0o755);
120
+ return {
121
+ target,
122
+ mode,
123
+ failOn,
124
+ path: preflight.path,
125
+ status: preflight.status === 'will-refresh' ? 'refreshed-managed' : 'installed',
126
+ };
127
+ }
package/cli/hooks.mjs ADDED
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { findGitRoot, parseArgs, relativeDisplay } from './shared.mjs';
5
+ import {
6
+ EXIT_CONFIG_FAILURE,
7
+ HOOKS,
8
+ gitHooksDir,
9
+ hookScript,
10
+ isManagedHook,
11
+ selectedHooks,
12
+ } from './hook-utils.mjs';
13
+
14
+ function usage() {
15
+ console.log(`jhste-skills hooks
16
+
17
+ Usage:
18
+ jhste-skills hooks install [--repo <path>] [--mode advisory|blocking] [--hook pre-commit|pre-push|all] [--fail-on none|warning|error]
19
+ jhste-skills hooks uninstall [--repo <path>] [--hook pre-commit|pre-push|all]
20
+ jhste-skills hooks doctor [--repo <path>]
21
+
22
+ Notes:
23
+ install never overwrites a non-managed existing hook.
24
+ advisory hooks print guard output but return success.
25
+ blocking hooks return the guard exit code.
26
+ `);
27
+ }
28
+
29
+ function fail(message) {
30
+ console.error(`jhste-skills hooks: ${message}`);
31
+ process.exit(EXIT_CONFIG_FAILURE);
32
+ }
33
+
34
+ function install(repoRoot, args) {
35
+ const mode = String(args.mode || 'advisory');
36
+ if (!['advisory', 'blocking'].includes(mode)) fail('--mode must be advisory or blocking.');
37
+ const failOn = String(args['fail-on'] || (mode === 'blocking' ? 'error' : 'none'));
38
+ if (!['none', 'warning', 'error'].includes(failOn)) fail('--fail-on must be none, warning, or error.');
39
+ let hooksDir;
40
+ try {
41
+ hooksDir = gitHooksDir(repoRoot);
42
+ } catch {
43
+ fail(`not a git repository: ${repoRoot}`);
44
+ }
45
+ fs.mkdirSync(hooksDir, { recursive: true });
46
+ let hooks;
47
+ try {
48
+ hooks = selectedHooks(args.hook);
49
+ } catch (error) {
50
+ fail(error.message);
51
+ }
52
+ for (const hook of hooks) {
53
+ const file = path.join(hooksDir, hook);
54
+ if (fs.existsSync(file)) {
55
+ const existing = fs.readFileSync(file, 'utf8');
56
+ if (!isManagedHook(existing)) {
57
+ fail(`${relativeDisplay(repoRoot, file)} already exists and is not managed by jhste-skills; refusing to overwrite.`);
58
+ }
59
+ }
60
+ }
61
+ for (const hook of hooks) {
62
+ const file = path.join(hooksDir, hook);
63
+ fs.writeFileSync(file, hookScript({ hook, mode, failOn }), { mode: 0o755 });
64
+ fs.chmodSync(file, 0o755);
65
+ console.log(`installed ${relativeDisplay(repoRoot, file)} (${mode}, fail-on=${failOn})`);
66
+ }
67
+ }
68
+
69
+ function uninstall(repoRoot, args) {
70
+ let hooksDir;
71
+ try {
72
+ hooksDir = gitHooksDir(repoRoot);
73
+ } catch {
74
+ fail(`not a git repository: ${repoRoot}`);
75
+ }
76
+ let hooks;
77
+ try {
78
+ hooks = selectedHooks(args.hook);
79
+ } catch (error) {
80
+ fail(error.message);
81
+ }
82
+ for (const hook of hooks) {
83
+ const file = path.join(hooksDir, hook);
84
+ if (!fs.existsSync(file)) {
85
+ console.log(`${hook}: not installed`);
86
+ continue;
87
+ }
88
+ const existing = fs.readFileSync(file, 'utf8');
89
+ if (!isManagedHook(existing)) {
90
+ console.log(`${hook}: existing non-managed hook left untouched`);
91
+ continue;
92
+ }
93
+ fs.rmSync(file);
94
+ console.log(`${hook}: removed managed hook`);
95
+ }
96
+ }
97
+
98
+ function doctor(repoRoot) {
99
+ let hooksDir;
100
+ try {
101
+ hooksDir = gitHooksDir(repoRoot);
102
+ } catch {
103
+ fail(`not a git repository: ${repoRoot}`);
104
+ }
105
+ for (const hook of HOOKS) {
106
+ const file = path.join(hooksDir, hook);
107
+ if (!fs.existsSync(file)) {
108
+ console.log(`${hook}: absent`);
109
+ continue;
110
+ }
111
+ const existing = fs.readFileSync(file, 'utf8');
112
+ console.log(`${hook}: ${isManagedHook(existing) ? 'managed by jhste-skills' : 'present but not managed'}`);
113
+ }
114
+ }
115
+
116
+ const [subcommand, ...rest] = process.argv.slice(2);
117
+ if (!subcommand || subcommand === '--help' || subcommand === '-h') {
118
+ usage();
119
+ process.exit(0);
120
+ }
121
+
122
+ const args = parseArgs(rest);
123
+ const repoRoot = findGitRoot(args.repo || process.cwd());
124
+ if (subcommand === 'install') install(repoRoot, args);
125
+ else if (subcommand === 'uninstall') uninstall(repoRoot, args);
126
+ else if (subcommand === 'doctor') doctor(repoRoot);
127
+ else fail(`unknown subcommand: ${subcommand}`);
package/cli/index.mjs ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const cliDir = path.dirname(fileURLToPath(import.meta.url));
7
+ const [command, ...rest] = process.argv.slice(2);
8
+ const commands = new Set(['install', 'connect', 'sync', 'update', 'deep-scan', 'guard', 'hooks', 'tune', 'baseline', 'uninstall']);
9
+
10
+ if (!command || command === '--help' || command === '-h') {
11
+ console.log(`jhste-skills
12
+
13
+ Usage:
14
+ jhste-skills install [--mode minimal|normal|full|custom] [--yes] [--repo <path>]
15
+ jhste-skills connect [--mode normal|full|custom] [--repo <path>]
16
+ jhste-skills sync [--repo <path>] [--skills-dir <path>] [--yes]
17
+ jhste-skills update [--repo <path>] [--skills-dir <path>] [--yes]
18
+ jhste-skills deep-scan [--repo <path>]
19
+ jhste-skills guard [--repo <path>] [--scope changed|staged|all|files-from] [--format text|json]
20
+ jhste-skills hooks install|uninstall|doctor [--repo <path>]
21
+ jhste-skills tune [--repo <path>]
22
+ jhste-skills baseline [--repo <path>]
23
+ jhste-skills uninstall [--repo <path>] [--skills-dir <path>] [--yes]
24
+ `);
25
+ process.exit(0);
26
+ }
27
+
28
+ if (!commands.has(command)) {
29
+ console.error(`Unknown command: ${command}`);
30
+ process.exit(1);
31
+ }
32
+
33
+ const script = path.join(cliDir, `${command}.mjs`);
34
+ const result = spawnSync(process.execPath, [script, ...rest], { stdio: 'inherit' });
35
+ process.exit(result.status ?? 1);
@@ -0,0 +1,39 @@
1
+ import path from 'node:path';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { KIT_ROOT } from '../shared.mjs';
4
+ import { installHookTarget } from '../hook-utils.mjs';
5
+ import { bridgeTargetNames, writeManagedBridge } from './bridge-writer.mjs';
6
+ import { writeProfile } from './profile-writer.mjs';
7
+ import { installSkills } from './skills.mjs';
8
+
9
+ const EXIT_CONFIG_FAILURE = 3;
10
+
11
+ export function applyPlan(plan) {
12
+ const result = { skillResults: [], profileResult: null, bridgeResults: [], hookResults: [], deepScanResult: null, exitCode: 0 };
13
+
14
+ if (plan.installSkills || (plan.command === 'connect' && plan.installMissing && plan.preflight.skills.missing.length > 0)) {
15
+ result.skillResults = installSkills(plan.skillsDir, {
16
+ force: plan.forceSkills ?? plan.force,
17
+ allowUnmanagedOverwrite: plan.allowUnmanagedSkillOverwrite,
18
+ skillSet: plan.skillNames ?? plan.skillSet,
19
+ });
20
+ if (result.skillResults.some((item) => ['skipped-unmanaged-different', 'invalid-manifest'].includes(item.status))) {
21
+ result.exitCode = EXIT_CONFIG_FAILURE;
22
+ return result;
23
+ }
24
+ }
25
+
26
+ if (plan.writeProfile && plan.repoRoot) result.profileResult = writeProfile(plan.repoRoot, { force: plan.force, lineLimit: plan.lineLimit });
27
+ if (plan.writeBridge && plan.repoRoot) result.bridgeResults = bridgeTargetNames(plan).map((name) => writeManagedBridge(plan.repoRoot, name));
28
+ if (plan.hooks.length && plan.repoRoot) {
29
+ result.hookResults = plan.hooks.map((hook) => installHookTarget(plan.repoRoot, hook));
30
+ if (result.hookResults.some((hook) => hook.mode === 'blocking' && ['failed', 'skipped-non-managed'].includes(hook.status))) result.exitCode = EXIT_CONFIG_FAILURE;
31
+ }
32
+ if (plan.deepScan && plan.repoRoot) {
33
+ const scan = spawnSync(process.execPath, [path.join(KIT_ROOT, 'cli', 'deep-scan.mjs'), '--repo', plan.repoRoot], { stdio: 'inherit', timeout: 5 * 60 * 1000 });
34
+ if (scan.error) result.deepScanResult = { status: 'warning', reason: scan.error.message };
35
+ else if ((scan.status ?? 1) !== 0) result.deepScanResult = { status: 'warning', exitCode: scan.status ?? 1 };
36
+ else result.deepScanResult = { status: 'completed' };
37
+ }
38
+ return result;
39
+ }
@@ -0,0 +1,52 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { BRIDGE_BLOCK, BRIDGE_END, BRIDGE_START, MANAGED_BRIDGE_BLOCK, readIfExists } from '../shared.mjs';
4
+
5
+ function escapeRegExp(value) {
6
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7
+ }
8
+
9
+ export function bridgeTargetNames(plan) {
10
+ if (!plan.writeBridge || !plan.repoRoot) return [];
11
+ const names = ['AGENTS.md', 'CLAUDE.md'];
12
+ const existing = names.filter((name) => fs.existsSync(path.join(plan.repoRoot, name)));
13
+ return existing.length ? existing : ['AGENTS.md'];
14
+ }
15
+
16
+ export function bridgeStatus(repoRoot, fileName) {
17
+ const target = path.join(repoRoot, fileName);
18
+ const existing = readIfExists(target);
19
+ if (existing === null) return { fileName, path: target, status: 'will-create' };
20
+ if (existing.includes(BRIDGE_START) && existing.includes(BRIDGE_END)) {
21
+ return existing.includes(MANAGED_BRIDGE_BLOCK)
22
+ ? { fileName, path: target, status: 'already-managed' }
23
+ : { fileName, path: target, status: 'will-update-managed' };
24
+ }
25
+ if (existing.includes(BRIDGE_BLOCK)) return { fileName, path: target, status: 'will-migrate-legacy' };
26
+ if (/^##\s+Agent skills\s*$/m.test(existing) || /jhste skills/i.test(existing)) return { fileName, path: target, status: 'manual-review' };
27
+ return { fileName, path: target, status: 'will-append-managed' };
28
+ }
29
+
30
+ export function writeManagedBridge(repoRoot, fileName) {
31
+ const target = path.join(repoRoot, fileName);
32
+ const existing = readIfExists(target);
33
+ if (existing === null) {
34
+ fs.writeFileSync(target, `${MANAGED_BRIDGE_BLOCK}\n`);
35
+ return { status: 'created', path: target };
36
+ }
37
+ if (existing.includes(BRIDGE_START) && existing.includes(BRIDGE_END)) {
38
+ const pattern = new RegExp(`${escapeRegExp(BRIDGE_START)}[\\s\\S]*?${escapeRegExp(BRIDGE_END)}`);
39
+ const updated = existing.replace(pattern, MANAGED_BRIDGE_BLOCK);
40
+ if (updated === existing) return { status: 'unchanged', path: target };
41
+ fs.writeFileSync(target, updated);
42
+ return { status: 'updated-managed', path: target };
43
+ }
44
+ if (existing.includes(BRIDGE_BLOCK)) {
45
+ fs.writeFileSync(target, existing.replace(BRIDGE_BLOCK, MANAGED_BRIDGE_BLOCK));
46
+ return { status: 'migrated-legacy', path: target };
47
+ }
48
+ if (/^##\s+Agent skills\s*$/m.test(existing) || /jhste skills/i.test(existing)) return { status: 'manual-review', path: target, snippet: MANAGED_BRIDGE_BLOCK };
49
+ const prefix = existing.endsWith('\n') ? existing : `${existing}\n`;
50
+ fs.writeFileSync(target, `${prefix}\n${MANAGED_BRIDGE_BLOCK}\n`);
51
+ return { status: 'appended-managed', path: target };
52
+ }
@@ -0,0 +1,45 @@
1
+ import path from 'node:path';
2
+ import { relativeDisplay } from '../shared.mjs';
3
+
4
+ function summarizeStatuses(results) {
5
+ return results.reduce((acc, item) => {
6
+ acc[item.status] = (acc[item.status] || 0) + 1;
7
+ return acc;
8
+ }, {});
9
+ }
10
+
11
+ export function printApplyResult(plan, result) {
12
+ const labels = { connect: 'Connection', install: 'Install', sync: 'Sync', update: 'Update' };
13
+ console.log(`\n${labels[plan.command] || 'Run'} completed.`);
14
+ if (result.skillResults.length) {
15
+ const summary = summarizeStatuses(result.skillResults);
16
+ console.log(`- Skills: ${Object.entries(summary).map(([k, v]) => `${k}=${v}`).join(', ') || 'none'}`);
17
+ for (const skill of result.skillResults.filter((item) => item.reason)) console.log(` - ${skill.status}: ${skill.reason}`);
18
+ } else {
19
+ console.log('- Skills: no changes');
20
+ }
21
+ if (result.profileResult) console.log(`- Profile: ${result.profileResult.status} (${relativeDisplay(plan.repoRoot, result.profileResult.path)})`);
22
+ else console.log('- Profile: no changes');
23
+ for (const bridge of result.bridgeResults) {
24
+ console.log(`- Bridge: ${path.basename(bridge.path)} ${bridge.status}`);
25
+ if (bridge.status === 'manual-review') {
26
+ console.log(' Manual snippet:');
27
+ console.log(bridge.snippet.split('\n').map((line) => ` ${line}`).join('\n'));
28
+ }
29
+ }
30
+ for (const hook of result.hookResults) {
31
+ const reason = hook.reason ? ` - ${hook.reason}` : '';
32
+ const failOn = hook.failOn && hook.failOn !== 'none' ? `, fail-on=${hook.failOn}` : '';
33
+ console.log(`- Hook ${hook.target}: ${hook.status}${hook.mode ? ` (${hook.mode}${failOn})` : ''}${reason}`);
34
+ }
35
+ if (result.deepScanResult) {
36
+ if (result.deepScanResult.status === 'completed') console.log('- Deep scan: completed');
37
+ else console.log(`- Deep scan: warning${result.deepScanResult.reason ? ` (${result.deepScanResult.reason})` : result.deepScanResult.exitCode ? ` (exit ${result.deepScanResult.exitCode})` : ''}`);
38
+ }
39
+ console.log('- CI/package.json/lockfile/source code: unchanged by installer');
40
+ console.log('- Non-managed hooks: never overwritten');
41
+ if (!plan.deepScan) {
42
+ console.log('\nTo run deep scan later:');
43
+ console.log(' npx jhste-skills deep-scan');
44
+ }
45
+ }
@@ -0,0 +1,58 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { preflightHookTarget } from '../hook-utils.mjs';
4
+ import { bridgeStatus, bridgeTargetNames } from './bridge-writer.mjs';
5
+ import { installedSkillStatus, selectedSkillNames } from './skills.mjs';
6
+
7
+ function preflightSkills(plan) {
8
+ const status = installedSkillStatus(plan.skillsDir, selectedSkillNames(plan));
9
+ if (plan.installSkills) return { enabled: true, skillSet: plan.skillSet, expected: status.expected.length, missing: status.missing, action: 'install-or-refresh' };
10
+ if (plan.command === 'connect') {
11
+ return {
12
+ enabled: false,
13
+ skillSet: plan.skillSet,
14
+ expected: status.expected.length,
15
+ missing: status.missing,
16
+ action: status.missing.length ? (plan.installMissing ? 'install-missing' : 'require-existing') : 'reuse-existing',
17
+ };
18
+ }
19
+ return { enabled: false, skillSet: plan.skillSet, expected: status.expected.length, missing: status.missing, action: 'none' };
20
+ }
21
+
22
+ function preflightProfile(plan) {
23
+ if (!plan.writeProfile || !plan.repoRoot) return { enabled: false, status: 'skipped' };
24
+ const profilePath = path.join(plan.repoRoot, '.jhste', 'profile.yaml');
25
+ if (!fs.existsSync(profilePath)) return { enabled: true, status: 'will-create', path: profilePath };
26
+ return { enabled: true, status: plan.force ? 'will-overwrite-managed' : 'will-keep-existing', path: profilePath };
27
+ }
28
+
29
+ function preflightBridges(plan) {
30
+ if (!plan.writeBridge || !plan.repoRoot) return [];
31
+ return bridgeTargetNames(plan).map((name) => bridgeStatus(plan.repoRoot, name));
32
+ }
33
+
34
+ function preflightHooks(plan) {
35
+ if (!plan.connectRepo || !plan.repoRoot || plan.hooks.length === 0) return [];
36
+ return plan.hooks.map((hook) => ({ ...hook, ...preflightHookTarget(plan.repoRoot, hook.target) }));
37
+ }
38
+
39
+ function preflightDeepScan(plan) {
40
+ if (!plan.deepScan) return { enabled: false, status: 'skipped' };
41
+ if (!plan.repoRoot) return { enabled: false, status: 'skipped-no-repo' };
42
+ return {
43
+ enabled: true,
44
+ status: 'will-run',
45
+ report: path.join(plan.repoRoot, '.jhste', 'deep-scan-report.md'),
46
+ recommendedProfile: path.join(plan.repoRoot, '.jhste', 'profile.recommended.yaml'),
47
+ };
48
+ }
49
+
50
+ export function preflightPlan(plan) {
51
+ return {
52
+ skills: preflightSkills(plan),
53
+ profile: preflightProfile(plan),
54
+ bridges: preflightBridges(plan),
55
+ hooks: preflightHooks(plan),
56
+ deepScan: preflightDeepScan(plan),
57
+ };
58
+ }
@@ -0,0 +1,21 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { DEFAULT_PROFILE, ensureDir, nowIso } from '../shared.mjs';
4
+
5
+ function renderProfile(lineLimit) {
6
+ const base = DEFAULT_PROFILE.replace('<installed_at>', nowIso());
7
+ const limit = lineLimit || { enabled: true, maxLines: 300, enforcement: 'advisory' };
8
+ const fileSizeBlock = limit.enabled
9
+ ? ` file_size_advisory:\n mode: advisory\n source_file_warning_lines: ${limit.maxLines}\n source_file_review_lines: ${limit.maxLines}`
10
+ : ` file_size_advisory:\n mode: off`;
11
+ return base.replace(/ file_size_advisory:\n(?: .+\n){2,3}/, `${fileSizeBlock}\n`);
12
+ }
13
+
14
+ export function writeProfile(repoRoot, { force = false, lineLimit = null } = {}) {
15
+ const profilePath = path.join(repoRoot, '.jhste', 'profile.yaml');
16
+ if (fs.existsSync(profilePath) && !force) return { status: 'skipped-existing', path: profilePath };
17
+ const existed = fs.existsSync(profilePath);
18
+ ensureDir(path.dirname(profilePath));
19
+ fs.writeFileSync(profilePath, renderProfile(lineLimit));
20
+ return { status: existed ? 'overwritten-managed' : 'created', path: profilePath };
21
+ }