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,200 @@
|
|
|
1
|
+
import { ask, findGitRootInfo } from '../shared.mjs';
|
|
2
|
+
import { preflightPlan } from '../install-actions.mjs';
|
|
3
|
+
import {
|
|
4
|
+
defaultLineLimit,
|
|
5
|
+
disabledLineLimit,
|
|
6
|
+
hookActions,
|
|
7
|
+
targetList,
|
|
8
|
+
} from './plan-helpers.mjs';
|
|
9
|
+
import {
|
|
10
|
+
askFullEnforcement,
|
|
11
|
+
askLineLimitPolicy,
|
|
12
|
+
chooseMode,
|
|
13
|
+
customInstallPlan,
|
|
14
|
+
} from './prompts.mjs';
|
|
15
|
+
|
|
16
|
+
function presetPlan(command, mode) {
|
|
17
|
+
if (command === 'connect') return presetConnectPlan(mode);
|
|
18
|
+
if (mode === 'minimal') {
|
|
19
|
+
return {
|
|
20
|
+
mode,
|
|
21
|
+
installSkills: true,
|
|
22
|
+
skillSet: 'core',
|
|
23
|
+
connectRepo: false,
|
|
24
|
+
writeProfile: false,
|
|
25
|
+
writeBridge: false,
|
|
26
|
+
hooks: [],
|
|
27
|
+
deepScan: false,
|
|
28
|
+
lineLimit: disabledLineLimit(),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (mode === 'full') {
|
|
32
|
+
return {
|
|
33
|
+
mode,
|
|
34
|
+
installSkills: true,
|
|
35
|
+
skillSet: 'all',
|
|
36
|
+
connectRepo: true,
|
|
37
|
+
writeProfile: true,
|
|
38
|
+
writeBridge: true,
|
|
39
|
+
hooks: hookActions(['pre-commit', 'pre-push'], 'advisory'),
|
|
40
|
+
deepScan: true,
|
|
41
|
+
lineLimit: defaultLineLimit(),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
mode,
|
|
46
|
+
installSkills: true,
|
|
47
|
+
skillSet: 'all',
|
|
48
|
+
connectRepo: true,
|
|
49
|
+
writeProfile: true,
|
|
50
|
+
writeBridge: true,
|
|
51
|
+
hooks: hookActions(['pre-commit'], 'advisory'),
|
|
52
|
+
deepScan: false,
|
|
53
|
+
lineLimit: defaultLineLimit(),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function presetConnectPlan(mode) {
|
|
58
|
+
if (mode === 'full') {
|
|
59
|
+
return {
|
|
60
|
+
mode,
|
|
61
|
+
installSkills: false,
|
|
62
|
+
skillSet: 'all',
|
|
63
|
+
connectRepo: true,
|
|
64
|
+
writeProfile: true,
|
|
65
|
+
writeBridge: true,
|
|
66
|
+
hooks: hookActions(['pre-commit', 'pre-push'], 'advisory'),
|
|
67
|
+
deepScan: true,
|
|
68
|
+
lineLimit: defaultLineLimit(),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
mode,
|
|
73
|
+
installSkills: false,
|
|
74
|
+
skillSet: 'all',
|
|
75
|
+
connectRepo: true,
|
|
76
|
+
writeProfile: true,
|
|
77
|
+
writeBridge: true,
|
|
78
|
+
hooks: hookActions(['pre-commit'], 'advisory'),
|
|
79
|
+
deepScan: false,
|
|
80
|
+
lineLimit: defaultLineLimit(),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function applyOptionOverrides(plan, options) {
|
|
85
|
+
const overrides = [];
|
|
86
|
+
if (options.explicitSkillSet) {
|
|
87
|
+
plan.skillSet = options.skillSet;
|
|
88
|
+
overrides.push(`--skill-set ${options.skillSet}`);
|
|
89
|
+
}
|
|
90
|
+
if (options.noBridge) {
|
|
91
|
+
plan.writeBridge = false;
|
|
92
|
+
overrides.push('--no-bridge');
|
|
93
|
+
}
|
|
94
|
+
if (options.skipDeepScan) {
|
|
95
|
+
plan.deepScan = false;
|
|
96
|
+
overrides.push('--skip-deep-scan');
|
|
97
|
+
}
|
|
98
|
+
if (options.skipHooks) {
|
|
99
|
+
plan.hooks = [];
|
|
100
|
+
overrides.push('--skip-hooks');
|
|
101
|
+
} else if (options.explicitHooks || options.explicitHookTargets) {
|
|
102
|
+
plan.connectRepo = true;
|
|
103
|
+
const targets = options.hookTargets || targetList(plan);
|
|
104
|
+
const mode = options.hookMode || (plan.hooks[0]?.mode || 'advisory');
|
|
105
|
+
plan.hooks = hookActions(targets, mode);
|
|
106
|
+
if (options.explicitHooks) overrides.push(`--hooks ${mode}`);
|
|
107
|
+
if (options.explicitHookTargets) overrides.push(`--hook ${targets.length === 2 ? 'all' : targets[0]}`);
|
|
108
|
+
}
|
|
109
|
+
return overrides;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function attachCommonPlanState(plan, options, overrides) {
|
|
113
|
+
plan.command = options.command;
|
|
114
|
+
plan.force = options.force;
|
|
115
|
+
plan.allowUnmanagedSkillOverwrite = options.allowUnmanagedSkillOverwrite;
|
|
116
|
+
plan.installMissing = options.installMissing;
|
|
117
|
+
plan.overrides = overrides;
|
|
118
|
+
plan.skillsDir = options.skillsDir;
|
|
119
|
+
plan.repoStart = options.repoStart;
|
|
120
|
+
plan.explicitRepo = options.explicitRepo;
|
|
121
|
+
plan.yes = options.yes;
|
|
122
|
+
const repoInfo = findGitRootInfo(options.repoStart);
|
|
123
|
+
plan.repoInfo = repoInfo;
|
|
124
|
+
plan.repoRoot = repoInfo.repoRoot;
|
|
125
|
+
return repoInfo;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function validateRepoAttachment(plan, options, repoInfo) {
|
|
129
|
+
if (options.command === 'connect' && !repoInfo.isGitRepo) {
|
|
130
|
+
return { errors: [`connect requires a git repository: ${options.repoStart}`] };
|
|
131
|
+
}
|
|
132
|
+
if (plan.connectRepo && !repoInfo.isGitRepo) {
|
|
133
|
+
if (options.explicitRepo) {
|
|
134
|
+
return { errors: [`--repo must point inside a git repository for ${plan.mode} mode: ${options.repoStart}`] };
|
|
135
|
+
}
|
|
136
|
+
plan.repoSkippedReason = 'No current project detected, so project connection was skipped';
|
|
137
|
+
plan.connectRepo = false;
|
|
138
|
+
plan.writeProfile = false;
|
|
139
|
+
plan.writeBridge = false;
|
|
140
|
+
plan.hooks = [];
|
|
141
|
+
plan.deepScan = false;
|
|
142
|
+
}
|
|
143
|
+
if (!plan.connectRepo) {
|
|
144
|
+
plan.writeProfile = false;
|
|
145
|
+
plan.writeBridge = false;
|
|
146
|
+
plan.hooks = [];
|
|
147
|
+
plan.deepScan = false;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export { chooseMode };
|
|
153
|
+
|
|
154
|
+
export async function resolvePlan(options) {
|
|
155
|
+
const selectedMode = await chooseMode(options);
|
|
156
|
+
if (selectedMode === 'cancel') return { cancelled: true };
|
|
157
|
+
if (options.command === 'connect' && selectedMode === 'minimal') {
|
|
158
|
+
return { errors: ['connect does not support minimal mode. Use install --mode minimal for computer-only setup.'] };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const plan = selectedMode === 'custom'
|
|
162
|
+
? await customInstallPlan(options.command)
|
|
163
|
+
: presetPlan(options.command, selectedMode);
|
|
164
|
+
const overrides = applyOptionOverrides(plan, options);
|
|
165
|
+
const repoInfo = attachCommonPlanState(plan, options, overrides);
|
|
166
|
+
const repoError = validateRepoAttachment(plan, options, repoInfo);
|
|
167
|
+
if (repoError) return repoError;
|
|
168
|
+
|
|
169
|
+
await askFullEnforcement(plan, options);
|
|
170
|
+
await askLineLimitPolicy(plan, options);
|
|
171
|
+
plan.preflight = preflightPlan(plan);
|
|
172
|
+
return { plan };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function maybeInstallMissingForConnect(plan) {
|
|
176
|
+
const missing = plan.preflight.skills.missing;
|
|
177
|
+
if (plan.command !== 'connect' || missing.length === 0 || plan.installMissing) return { ok: true };
|
|
178
|
+
if (plan.yes) {
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
errors: [
|
|
182
|
+
`connect requires ${plan.skillSet} skills but ${missing.length} are missing.`,
|
|
183
|
+
'Run `jhste-skills install` first or pass --install-missing to install missing skills explicitly.',
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const answer = await ask(`\n${missing.length} required skills are missing. Install them now? [y=install / Enter=cancel] `);
|
|
188
|
+
if (String(answer).trim().toLowerCase() === 'y') {
|
|
189
|
+
plan.installMissing = true;
|
|
190
|
+
plan.preflight.skills.action = 'install-missing';
|
|
191
|
+
return { ok: true };
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
ok: false,
|
|
195
|
+
errors: [
|
|
196
|
+
`connect requires ${plan.skillSet} skills but ${missing.length} are missing.`,
|
|
197
|
+
'Run `jhste-skills install` first or use `--install-missing`.',
|
|
198
|
+
],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { ask } from '../shared.mjs';
|
|
2
|
+
import { DEFAULT_LINE_LIMIT, MODE_ALIASES } from './options.mjs';
|
|
3
|
+
import {
|
|
4
|
+
applyLineLimitToHooks,
|
|
5
|
+
defaultLineLimit,
|
|
6
|
+
disabledLineLimit,
|
|
7
|
+
hookActions,
|
|
8
|
+
} from './plan-helpers.mjs';
|
|
9
|
+
|
|
10
|
+
function parseLineLimitAnswer(value) {
|
|
11
|
+
const normalized = String(value).trim();
|
|
12
|
+
if (!normalized) return DEFAULT_LINE_LIMIT;
|
|
13
|
+
const parsed = Number(normalized);
|
|
14
|
+
return Number.isInteger(parsed) && parsed >= 50 && parsed <= 5000 ? parsed : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function applyLineLimitOptions(plan, options) {
|
|
18
|
+
if (options.noLineLimit || options.lineLimitMode === 'off') {
|
|
19
|
+
plan.lineLimit = { enabled: false, maxLines: options.lineLimit, enforcement: 'off' };
|
|
20
|
+
plan.overrides?.push(options.noLineLimit ? '--no-line-limit' : '--line-limit-mode off');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (!plan.lineLimit) plan.lineLimit = defaultLineLimit();
|
|
24
|
+
if (options.explicitLineLimit) {
|
|
25
|
+
plan.lineLimit.maxLines = options.lineLimit;
|
|
26
|
+
plan.overrides?.push(`--line-limit ${options.lineLimit}`);
|
|
27
|
+
}
|
|
28
|
+
if (options.lineLimitMode) {
|
|
29
|
+
plan.lineLimit.enforcement = options.lineLimitMode;
|
|
30
|
+
plan.overrides?.push(`--line-limit-mode ${options.lineLimitMode}`);
|
|
31
|
+
}
|
|
32
|
+
if (options.explicitLineLimit || options.explicitLineLimitMode) applyLineLimitToHooks(plan, options);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function askLineLimitPolicy(plan, options) {
|
|
36
|
+
if (!plan.connectRepo || !plan.writeProfile) return;
|
|
37
|
+
applyLineLimitOptions(plan, options);
|
|
38
|
+
if (options.yes || options.noLineLimit || options.explicitLineLimit || options.explicitLineLimitMode) return;
|
|
39
|
+
const answer = await ask(`Configure a file length limit?
|
|
40
|
+
Large files make review, edits, and test boundaries harder to reason about.
|
|
41
|
+
|
|
42
|
+
1) Warn only at ${DEFAULT_LINE_LIMIT} lines
|
|
43
|
+
2) Block commits at ${DEFAULT_LINE_LIMIT} lines
|
|
44
|
+
3) Do not use a line limit
|
|
45
|
+
4) Enter a custom line count
|
|
46
|
+
|
|
47
|
+
Choice [Enter=1]: `);
|
|
48
|
+
const choice = String(answer).trim();
|
|
49
|
+
if (choice === '3') {
|
|
50
|
+
plan.lineLimit = disabledLineLimit();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
let enforcement = choice === '2' ? 'blocking' : 'advisory';
|
|
54
|
+
let maxLines = DEFAULT_LINE_LIMIT;
|
|
55
|
+
if (choice === '4') {
|
|
56
|
+
const limitAnswer = await ask(`Line limit [Enter=${DEFAULT_LINE_LIMIT}]: `);
|
|
57
|
+
const parsed = parseLineLimitAnswer(limitAnswer);
|
|
58
|
+
if (parsed === null) {
|
|
59
|
+
console.log(`Expected an integer between ${DEFAULT_LINE_LIMIT} and 5000. Using the default ${DEFAULT_LINE_LIMIT}-line limit.`);
|
|
60
|
+
} else {
|
|
61
|
+
maxLines = parsed;
|
|
62
|
+
}
|
|
63
|
+
const blockAnswer = await ask('Block commits when this limit is exceeded? [y=block / Enter=warn only] ');
|
|
64
|
+
if (String(blockAnswer).trim().toLowerCase() === 'y') enforcement = 'blocking';
|
|
65
|
+
}
|
|
66
|
+
plan.lineLimit = { enabled: true, maxLines, enforcement };
|
|
67
|
+
applyLineLimitToHooks(plan, options);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function chooseMode(options) {
|
|
71
|
+
if (options.mode) return options.mode;
|
|
72
|
+
if (options.yes) return 'normal';
|
|
73
|
+
while (true) {
|
|
74
|
+
const answer = await ask(`Choose an install mode.
|
|
75
|
+
|
|
76
|
+
1) Minimal - Install the lightest setup
|
|
77
|
+
2) Normal - Use the recommended setup
|
|
78
|
+
3) Full - Install every safe optional feature
|
|
79
|
+
4) Custom - Choose settings manually
|
|
80
|
+
|
|
81
|
+
Choice [Enter=Normal / q=cancel]: `);
|
|
82
|
+
const normalized = String(answer).trim().toLowerCase();
|
|
83
|
+
if (!normalized) return 'normal';
|
|
84
|
+
if (normalized === 'q' || normalized === 'quit' || normalized === 'n' || normalized === 'no') return 'cancel';
|
|
85
|
+
const selected = MODE_ALIASES.get(normalized);
|
|
86
|
+
if (selected) return selected;
|
|
87
|
+
console.log('Unknown choice. Enter 1, 2, 3, 4, or q.');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function customInstallPlan(command) {
|
|
92
|
+
const plan = {
|
|
93
|
+
mode: 'custom',
|
|
94
|
+
installSkills: command === 'install',
|
|
95
|
+
skillSet: 'all',
|
|
96
|
+
connectRepo: true,
|
|
97
|
+
writeProfile: true,
|
|
98
|
+
writeBridge: true,
|
|
99
|
+
hooks: hookActions(['pre-commit'], 'advisory'),
|
|
100
|
+
deepScan: false,
|
|
101
|
+
lineLimit: defaultLineLimit(),
|
|
102
|
+
};
|
|
103
|
+
await askCustomScope(plan, command);
|
|
104
|
+
if (plan.connectRepo) await askCustomProjectSettings(plan);
|
|
105
|
+
return plan;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function askCustomScope(plan, command) {
|
|
109
|
+
if (command === 'install') {
|
|
110
|
+
const scope = await ask(`Where should this apply?
|
|
111
|
+
|
|
112
|
+
1) This computer and the current project
|
|
113
|
+
2) This computer only
|
|
114
|
+
3) The current project only
|
|
115
|
+
|
|
116
|
+
Choice [Enter=1]: `);
|
|
117
|
+
const normalizedScope = String(scope).trim();
|
|
118
|
+
if (normalizedScope === '2') {
|
|
119
|
+
plan.installSkills = true;
|
|
120
|
+
plan.connectRepo = false;
|
|
121
|
+
plan.writeProfile = false;
|
|
122
|
+
plan.writeBridge = false;
|
|
123
|
+
plan.hooks = [];
|
|
124
|
+
plan.deepScan = false;
|
|
125
|
+
plan.lineLimit = disabledLineLimit();
|
|
126
|
+
} else if (normalizedScope === '3') {
|
|
127
|
+
plan.installSkills = false;
|
|
128
|
+
plan.connectRepo = true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (plan.installSkills) {
|
|
132
|
+
const featureRange = await ask(`Choose the feature set to install.
|
|
133
|
+
|
|
134
|
+
1) All bundled features
|
|
135
|
+
2) jhste core guardrail features only
|
|
136
|
+
|
|
137
|
+
Choice [Enter=1]: `);
|
|
138
|
+
if (String(featureRange).trim() === '2') plan.skillSet = 'core';
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
plan.installSkills = false;
|
|
144
|
+
plan.connectRepo = true;
|
|
145
|
+
const featureRange = await ask(`Choose the feature set for the current project.
|
|
146
|
+
|
|
147
|
+
1) All bundled features
|
|
148
|
+
2) jhste core guardrail features only
|
|
149
|
+
|
|
150
|
+
Choice [Enter=1]: `);
|
|
151
|
+
if (String(featureRange).trim() === '2') plan.skillSet = 'core';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function askCustomProjectSettings(plan) {
|
|
155
|
+
const guidance = await ask(`Write project configuration files?
|
|
156
|
+
|
|
157
|
+
1) Add the config file and AI bridge text
|
|
158
|
+
2) Add the config file only
|
|
159
|
+
3) Add nothing
|
|
160
|
+
|
|
161
|
+
Choice [Enter=1]: `);
|
|
162
|
+
if (String(guidance).trim() === '2') {
|
|
163
|
+
plan.writeProfile = true;
|
|
164
|
+
plan.writeBridge = false;
|
|
165
|
+
} else if (String(guidance).trim() === '3') {
|
|
166
|
+
plan.writeProfile = false;
|
|
167
|
+
plan.writeBridge = false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const checks = await ask(`Run automatic checks at commit time?
|
|
171
|
+
|
|
172
|
+
1) Show warnings only
|
|
173
|
+
2) Do not run checks
|
|
174
|
+
3) Block the commit when clearly risky issues are detected
|
|
175
|
+
|
|
176
|
+
Choice [Enter=1]: `);
|
|
177
|
+
if (String(checks).trim() === '2') plan.hooks = [];
|
|
178
|
+
if (String(checks).trim() === '3') plan.hooks = hookActions(['pre-commit'], 'blocking');
|
|
179
|
+
|
|
180
|
+
const scan = await ask(`Run a deep scan on the current project?
|
|
181
|
+
This can take a few minutes and does not modify code.
|
|
182
|
+
|
|
183
|
+
1) Later
|
|
184
|
+
2) Run now
|
|
185
|
+
|
|
186
|
+
Choice [Enter=1]: `);
|
|
187
|
+
if (String(scan).trim() === '2') plan.deepScan = true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function askFullEnforcement(plan, options) {
|
|
191
|
+
if (options.yes || options.explicitHooks || options.explicitHookTargets || options.skipHooks) return;
|
|
192
|
+
if (plan.mode !== 'full' || !plan.connectRepo || plan.hooks.length === 0) return;
|
|
193
|
+
const answer = await ask(`Full mode preserves existing files and installs every safe optional feature.
|
|
194
|
+
How should automatic check results be handled?
|
|
195
|
+
|
|
196
|
+
1) Show warnings only - surface problems but do not block commit or push
|
|
197
|
+
2) Block at commit time - stop the commit on clear errors
|
|
198
|
+
3) Block at commit and push time - stricter enforcement
|
|
199
|
+
|
|
200
|
+
Choice [Enter=1]: `);
|
|
201
|
+
const normalized = String(answer).trim();
|
|
202
|
+
if (normalized === '2') {
|
|
203
|
+
plan.hooks = [
|
|
204
|
+
...hookActions(['pre-commit'], 'blocking'),
|
|
205
|
+
...hookActions(['pre-push'], 'advisory'),
|
|
206
|
+
];
|
|
207
|
+
} else if (normalized === '3') {
|
|
208
|
+
plan.hooks = hookActions(['pre-commit', 'pre-push'], 'blocking');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { applyPlan, printApplyResult } from './install-actions.mjs';
|
|
2
|
+
export {
|
|
3
|
+
EXIT_CONFIG_FAILURE,
|
|
4
|
+
normalizeOptions,
|
|
5
|
+
usage,
|
|
6
|
+
} from './install-flow/options.mjs';
|
|
7
|
+
export {
|
|
8
|
+
chooseMode,
|
|
9
|
+
maybeInstallMissingForConnect,
|
|
10
|
+
resolvePlan,
|
|
11
|
+
} from './install-flow/plan.mjs';
|
|
12
|
+
export {
|
|
13
|
+
confirmPlan,
|
|
14
|
+
printConfigErrors,
|
|
15
|
+
printPlanSummary,
|
|
16
|
+
} from './install-flow/output.mjs';
|
package/cli/install.mjs
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
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: 'install',
|
|
19
|
+
cwd: process.cwd(),
|
|
20
|
+
nonInteractive,
|
|
21
|
+
});
|
|
22
|
+
if (options.help) {
|
|
23
|
+
usage('install');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (options.errors.length > 0) {
|
|
27
|
+
printConfigErrors('install', options.errors);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let resolved = await resolvePlan(options);
|
|
32
|
+
if (resolved.cancelled) {
|
|
33
|
+
console.log('Install cancelled. No changes made.');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (resolved.errors?.length) {
|
|
37
|
+
printConfigErrors('install', resolved.errors);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
let { plan } = resolved;
|
|
41
|
+
printPlanSummary(plan);
|
|
42
|
+
|
|
43
|
+
let confirmation = await confirmPlan(plan);
|
|
44
|
+
if (confirmation === 'cancel') {
|
|
45
|
+
console.log('Install cancelled. No changes made.');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (confirmation === 'custom') {
|
|
49
|
+
resolved = await resolvePlan({ ...options, mode: 'custom', explicitMode: true });
|
|
50
|
+
if (resolved.errors?.length) {
|
|
51
|
+
printConfigErrors('install', resolved.errors);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
plan = resolved.plan;
|
|
55
|
+
printPlanSummary(plan);
|
|
56
|
+
confirmation = await confirmPlan(plan);
|
|
57
|
+
if (confirmation !== 'yes') {
|
|
58
|
+
console.log('Install cancelled. No changes made.');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const missing = await maybeInstallMissingForConnect(plan);
|
|
64
|
+
if (!missing.ok) {
|
|
65
|
+
printConfigErrors('install', missing.errors);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const result = applyPlan(plan);
|
|
70
|
+
printApplyResult(plan, result);
|
|
71
|
+
if (result.exitCode) process.exitCode = result.exitCode;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
main().catch((error) => {
|
|
75
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
76
|
+
process.exit(EXIT_CONFIG_FAILURE);
|
|
77
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
3
|
+
function describePath(file, description) {
|
|
4
|
+
return description || file;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function parseJsonText(text, { description = 'JSON input', validate = null } = {}) {
|
|
8
|
+
let value;
|
|
9
|
+
try {
|
|
10
|
+
value = JSON.parse(text);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
13
|
+
throw new Error(`${description} is not valid JSON: ${message}`);
|
|
14
|
+
}
|
|
15
|
+
if (validate) {
|
|
16
|
+
const validationError = validate(value);
|
|
17
|
+
if (validationError) throw new Error(`${description} failed validation: ${validationError}`);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function readJsonFile(file, { description, validate = null } = {}) {
|
|
23
|
+
return parseJsonText(fs.readFileSync(file, 'utf8'), {
|
|
24
|
+
description: describePath(file, description),
|
|
25
|
+
validate,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function validateJsonObject(value) {
|
|
30
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
31
|
+
? null
|
|
32
|
+
: 'expected a JSON object';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function validateStringArray(value) {
|
|
36
|
+
if (!Array.isArray(value)) return 'expected a JSON array';
|
|
37
|
+
const invalidIndex = value.findIndex((item) => typeof item !== 'string' || item.trim() === '');
|
|
38
|
+
return invalidIndex === -1 ? null : `expected a non-empty string at index ${invalidIndex}`;
|
|
39
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseProfileText } from './parser.mjs';
|
|
4
|
+
|
|
5
|
+
export function loadProfileConfig(repoRoot) {
|
|
6
|
+
const profilePath = path.join(repoRoot, '.jhste', 'profile.yaml');
|
|
7
|
+
if (!fs.existsSync(profilePath)) return { path: profilePath, exists: false, profile: parseProfileText('') };
|
|
8
|
+
return {
|
|
9
|
+
path: profilePath,
|
|
10
|
+
exists: true,
|
|
11
|
+
profile: parseProfileText(fs.readFileSync(profilePath, 'utf8')),
|
|
12
|
+
};
|
|
13
|
+
}
|