@hone-ai/cli 1.16.0 → 1.18.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/hone-cli.js +71 -7
- package/lib/splice-repo-specific.js +105 -24
- package/package.json +3 -1
package/hone-cli.js
CHANGED
|
@@ -55,8 +55,28 @@ function getConfig() {
|
|
|
55
55
|
return { token, apiUrl };
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// HC-019y-followup-3: module-level upgrade-warning bucket. Set by the
|
|
59
|
+
// axios response interceptor when the server's X-CLI-Latest-Version
|
|
60
|
+
// header reports a newer version than ours. Printed once at process
|
|
61
|
+
// exit so the warning lands AFTER the command's normal output and
|
|
62
|
+
// doesn't fight for attention with whatever the user is doing.
|
|
63
|
+
let _outdatedWarning = null;
|
|
64
|
+
|
|
65
|
+
function _compareSemverMinor(a, b) {
|
|
66
|
+
// Returns true if `a` is strictly older than `b` for major.minor.patch.
|
|
67
|
+
// Liberal-parse: anything we can't make sense of → false (don't warn).
|
|
68
|
+
const re = /^(\d+)\.(\d+)\.(\d+)/;
|
|
69
|
+
const ma = re.exec(String(a || '')), mb = re.exec(String(b || ''));
|
|
70
|
+
if (!ma || !mb) return false;
|
|
71
|
+
const [, aMaj, aMin, aPat] = ma.map(Number);
|
|
72
|
+
const [, bMaj, bMin, bPat] = mb.map(Number);
|
|
73
|
+
if (aMaj !== bMaj) return aMaj < bMaj;
|
|
74
|
+
if (aMin !== bMin) return aMin < bMin;
|
|
75
|
+
return aPat < bPat;
|
|
76
|
+
}
|
|
77
|
+
|
|
58
78
|
function api(config) {
|
|
59
|
-
|
|
79
|
+
const client = axios.create({
|
|
60
80
|
baseURL: config.apiUrl,
|
|
61
81
|
headers: {
|
|
62
82
|
Authorization: `Bearer ${config.token}`,
|
|
@@ -64,8 +84,35 @@ function api(config) {
|
|
|
64
84
|
},
|
|
65
85
|
timeout: 30_000,
|
|
66
86
|
});
|
|
87
|
+
// HC-019y-followup-3: response interceptor reads server's
|
|
88
|
+
// X-CLI-Latest-Version, sets the upgrade-warning bucket if outdated.
|
|
89
|
+
// Fire-and-forget — never throws. Captures errors silently so a
|
|
90
|
+
// missing/malformed header can't break the command.
|
|
91
|
+
client.interceptors.response.use(
|
|
92
|
+
(response) => {
|
|
93
|
+
try {
|
|
94
|
+
const latest = response?.headers?.['x-cli-latest-version'];
|
|
95
|
+
if (latest && _compareSemverMinor(pkg.version, latest)) {
|
|
96
|
+
_outdatedWarning =
|
|
97
|
+
` ⚠ @hone-ai/cli ${latest} is available — you have ${pkg.version}\n` +
|
|
98
|
+
` Run: npm install -g @hone-ai/cli@latest`;
|
|
99
|
+
}
|
|
100
|
+
} catch { /* never break the response path */ }
|
|
101
|
+
return response;
|
|
102
|
+
},
|
|
103
|
+
(error) => Promise.reject(error)
|
|
104
|
+
);
|
|
105
|
+
return client;
|
|
67
106
|
}
|
|
68
107
|
|
|
108
|
+
// HC-019y-followup-3: emit the upgrade warning at process exit if set.
|
|
109
|
+
// Wrapped in try/catch — a failed warning is fundamentally non-critical.
|
|
110
|
+
process.on('exit', () => {
|
|
111
|
+
if (_outdatedWarning) {
|
|
112
|
+
try { console.error('\n' + _outdatedWarning); } catch { /* swallow */ }
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
69
116
|
// ── SETUP command ─────────────────────────────────────────────────────────────
|
|
70
117
|
program
|
|
71
118
|
.command('setup')
|
|
@@ -1112,24 +1159,41 @@ program
|
|
|
1112
1159
|
console.log('Writing generated skills...');
|
|
1113
1160
|
if (result.skills) {
|
|
1114
1161
|
let preservedCount = 0;
|
|
1162
|
+
let sidecarCount = 0;
|
|
1115
1163
|
for (const [skillName, content] of Object.entries(result.skills)) {
|
|
1116
1164
|
if (!content || content.length < 50) continue;
|
|
1117
1165
|
const skillDir = path.join(repoRoot, '.github', 'skills', skillName);
|
|
1118
1166
|
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
1119
1167
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1168
|
+
const spliceResult = splicePreserveRepoSpecific(skillFile, content);
|
|
1169
|
+
if (spliceResult.sidecar) {
|
|
1170
|
+
// HC-019y-followup-2: legacy unmarked file — write to sidecar,
|
|
1171
|
+
// don't overwrite, prompt adopter to review.
|
|
1172
|
+
fs.writeFileSync(spliceResult.sidecarPath, spliceResult.merged);
|
|
1173
|
+
sidecarCount++;
|
|
1174
|
+
console.log(` ⚠ .github/skills/${skillName}/SKILL.md — NOT overwritten`);
|
|
1175
|
+
console.log(` Wrote derive output to: ${path.relative(repoRoot, spliceResult.sidecarPath)}`);
|
|
1176
|
+
console.log(` Reason: ${spliceResult.reason}`);
|
|
1125
1177
|
} else {
|
|
1126
|
-
|
|
1178
|
+
fs.writeFileSync(skillFile, spliceResult.merged);
|
|
1179
|
+
if (spliceResult.preserved) {
|
|
1180
|
+
preservedCount++;
|
|
1181
|
+
console.log(` ✓ .github/skills/${skillName}/SKILL.md (preserved adopter REPO-SPECIFIC content)`);
|
|
1182
|
+
} else {
|
|
1183
|
+
console.log(` ✓ .github/skills/${skillName}/SKILL.md`);
|
|
1184
|
+
}
|
|
1127
1185
|
}
|
|
1128
1186
|
}
|
|
1129
1187
|
if (preservedCount > 0) {
|
|
1130
1188
|
console.log(`\n Preserved adopter REPO-SPECIFIC content in ${preservedCount} skill(s).`);
|
|
1131
1189
|
console.log(` (HC-019y-followup-1: derive no longer overwrites curated REPO-SPECIFIC sections)`);
|
|
1132
1190
|
}
|
|
1191
|
+
if (sidecarCount > 0) {
|
|
1192
|
+
console.log(`\n ⚠ HC-019y-followup-2: ${sidecarCount} legacy unmarked SKILL(s) routed to .derive-new sidecars.`);
|
|
1193
|
+
console.log(` Review each sidecar; if the derive output is acceptable, replace the original.`);
|
|
1194
|
+
console.log(` To opt into automatic splice protection on the next derive, add a`);
|
|
1195
|
+
console.log(` '<!-- REPO-SPECIFIC -->' marker to the original file.`);
|
|
1196
|
+
}
|
|
1133
1197
|
}
|
|
1134
1198
|
|
|
1135
1199
|
// H-022: surface parser warnings so silent drops become VISIBLE failures.
|
|
@@ -6,6 +6,24 @@
|
|
|
6
6
|
* (everything below the `<!-- REPO-SPECIFIC -->` marker) is NEVER lost
|
|
7
7
|
* across derive runs.
|
|
8
8
|
*
|
|
9
|
+
* HC-019y-followup-2: legacy adopter-curated SKILLs WITHOUT the
|
|
10
|
+
* REPO-SPECIFIC marker were still vulnerable. OptionsFlow's
|
|
11
|
+
* `python-developer/SKILL.md` (834 lines authored by stories E22-E /
|
|
12
|
+
* E22-A / E16-A predating the marker convention) was REPLACED with
|
|
13
|
+
* 69 lines of fresh LLM output on 2026-05-30 because the file had no
|
|
14
|
+
* marker — the helper fell through to "fresh write" and 765 lines of
|
|
15
|
+
* legitimately-curated content were lost. Restored manually in
|
|
16
|
+
* OptionsFlow PR #106.
|
|
17
|
+
*
|
|
18
|
+
* Fix: non-destructive sidecar mode. When the existing file is
|
|
19
|
+
* substantial (>500 bytes) AND has neither the REPO-SPECIFIC marker
|
|
20
|
+
* nor the ENTERPRISE-MANAGED marker, the helper now returns
|
|
21
|
+
* sidecar=true. The caller writes to `<path>.derive-new` instead of
|
|
22
|
+
* overwriting, emits a warning, and prompts the adopter to add a
|
|
23
|
+
* `<!-- REPO-SPECIFIC -->` marker to opt into splice on the next derive.
|
|
24
|
+
* Same pattern as `setup-ai-pipeline.sh copy_skills()` uses a `.bak`
|
|
25
|
+
* fallback.
|
|
26
|
+
*
|
|
9
27
|
* Pre-fix: derive wrote the file blind (fs.writeFileSync(skillFile,
|
|
10
28
|
* content)). 5 OptionsFlow regression tests broke after a single derive
|
|
11
29
|
* because 378 lines of E33-A/E34-B-added content disappeared.
|
|
@@ -21,41 +39,104 @@
|
|
|
21
39
|
*/
|
|
22
40
|
const fs = require('node:fs');
|
|
23
41
|
|
|
42
|
+
const REPO_SPECIFIC_MARKER = '<!-- REPO-SPECIFIC';
|
|
43
|
+
const ENTERPRISE_MARKER = '<!-- ENTERPRISE-MANAGED';
|
|
44
|
+
|
|
45
|
+
// HC-019y-followup-2: the sidecar threshold. Files smaller than this
|
|
46
|
+
// are treated as stubs / placeholders and overwritten normally — only
|
|
47
|
+
// genuinely substantial unmarked content triggers sidecar mode. 500
|
|
48
|
+
// bytes is roughly: 5-10 lines of meaningful markdown. Tonight's
|
|
49
|
+
// evidence (python-developer/SKILL.md at 834 lines / ~32KB) is well
|
|
50
|
+
// above the threshold; trivial scaffolding (a 1-line stub) is well
|
|
51
|
+
// below. Tunable if real adopter data shows this is wrong.
|
|
52
|
+
const SIDECAR_MIN_BYTES = 500;
|
|
53
|
+
|
|
24
54
|
/**
|
|
25
|
-
* Splice adopter REPO-SPECIFIC tail into derive output
|
|
55
|
+
* Splice adopter REPO-SPECIFIC tail into derive output, OR fall back
|
|
56
|
+
* to sidecar mode for legacy unmarked files (HC-019y-followup-2).
|
|
26
57
|
*
|
|
27
58
|
* @param {string} existingPath absolute path of the file being overwritten
|
|
28
59
|
* @param {string} newContent the content from `hone derive` server response
|
|
29
|
-
* @returns {{
|
|
30
|
-
* merged:
|
|
31
|
-
* preserved:
|
|
32
|
-
*
|
|
60
|
+
* @returns {{
|
|
61
|
+
* merged: string,
|
|
62
|
+
* preserved: boolean,
|
|
63
|
+
* sidecar: boolean,
|
|
64
|
+
* sidecarPath: string|null,
|
|
65
|
+
* reason: string|null
|
|
66
|
+
* }}
|
|
67
|
+
* merged: bytes to write (to existingPath OR sidecarPath)
|
|
68
|
+
* preserved: true if adopter REPO-SPECIFIC content was preserved
|
|
69
|
+
* sidecar: true if caller should write to sidecarPath instead of
|
|
70
|
+
* overwriting existingPath; false otherwise
|
|
71
|
+
* sidecarPath: when sidecar=true, the path the caller should write to
|
|
72
|
+
* (always `${existingPath}.derive-new`); null otherwise
|
|
73
|
+
* reason: when sidecar=true, human-readable explanation for the
|
|
74
|
+
* warning the caller should emit; null otherwise
|
|
33
75
|
*/
|
|
34
76
|
function splicePreserveRepoSpecific(existingPath, newContent) {
|
|
35
|
-
const MARKER = '<!-- REPO-SPECIFIC';
|
|
36
77
|
if (!fs.existsSync(existingPath)) {
|
|
37
78
|
// Fresh adopter — no existing file. Write new content as-is.
|
|
38
|
-
return { merged: newContent, preserved: false };
|
|
79
|
+
return { merged: newContent, preserved: false, sidecar: false, sidecarPath: null, reason: null };
|
|
39
80
|
}
|
|
40
81
|
const existing = fs.readFileSync(existingPath, 'utf8');
|
|
41
|
-
const oldIdx = existing.indexOf(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
|
|
82
|
+
const oldIdx = existing.indexOf(REPO_SPECIFIC_MARKER);
|
|
83
|
+
|
|
84
|
+
if (oldIdx >= 0) {
|
|
85
|
+
// Existing has REPO-SPECIFIC marker — splice as before.
|
|
86
|
+
const newIdx = newContent.indexOf(REPO_SPECIFIC_MARKER);
|
|
87
|
+
if (newIdx < 0) {
|
|
88
|
+
// New content has no REPO-SPECIFIC section. Append the existing one.
|
|
89
|
+
return {
|
|
90
|
+
merged: newContent.trimEnd() + '\n\n' + existing.slice(oldIdx),
|
|
91
|
+
preserved: true,
|
|
92
|
+
sidecar: false,
|
|
93
|
+
sidecarPath: null,
|
|
94
|
+
reason: null,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// Both have REPO-SPECIFIC. Take new content up to the marker, then
|
|
98
|
+
// append everything from existing's marker to EOF.
|
|
99
|
+
return {
|
|
100
|
+
merged: newContent.slice(0, newIdx) + existing.slice(oldIdx),
|
|
101
|
+
preserved: true,
|
|
102
|
+
sidecar: false,
|
|
103
|
+
sidecarPath: null,
|
|
104
|
+
reason: null,
|
|
105
|
+
};
|
|
46
106
|
}
|
|
47
|
-
|
|
48
|
-
if
|
|
49
|
-
|
|
50
|
-
|
|
107
|
+
|
|
108
|
+
// No REPO-SPECIFIC marker. HC-019y-followup-2: check if this is a
|
|
109
|
+
// legacy substantial-unmarked file that we'd risk destroying.
|
|
110
|
+
const hasEnterpriseMarker = existing.indexOf(ENTERPRISE_MARKER) >= 0;
|
|
111
|
+
const isSubstantial = Buffer.byteLength(existing, 'utf8') >= SIDECAR_MIN_BYTES;
|
|
112
|
+
|
|
113
|
+
if (isSubstantial && !hasEnterpriseMarker) {
|
|
114
|
+
// Legacy unmarked file with real content. Write new content to a
|
|
115
|
+
// sidecar path instead of overwriting. Adopter reviews + decides
|
|
116
|
+
// whether to adopt the derive output, manually merge, or opt into
|
|
117
|
+
// splice protection by adding a REPO-SPECIFIC marker.
|
|
118
|
+
return {
|
|
119
|
+
merged: newContent,
|
|
120
|
+
preserved: false,
|
|
121
|
+
sidecar: true,
|
|
122
|
+
sidecarPath: `${existingPath}.derive-new`,
|
|
123
|
+
reason:
|
|
124
|
+
`legacy unmarked file (${Buffer.byteLength(existing, 'utf8')} bytes) — ` +
|
|
125
|
+
`wrote derive output to <path>.derive-new instead of overwriting. ` +
|
|
126
|
+
`Review the sidecar, then either: (a) replace the original, ` +
|
|
127
|
+
`(b) manually merge curated sections, or (c) add ` +
|
|
128
|
+
`'<!-- REPO-SPECIFIC -->' to the original to opt into splice on next derive.`,
|
|
129
|
+
};
|
|
51
130
|
}
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
return {
|
|
56
|
-
merged: newContent.slice(0, newIdx) + existing.slice(oldIdx),
|
|
57
|
-
preserved: true,
|
|
58
|
-
};
|
|
131
|
+
|
|
132
|
+
// Either small (stub/placeholder) or has ENTERPRISE-MANAGED marker
|
|
133
|
+
// (deliberately enterprise-controlled). Safe to overwrite.
|
|
134
|
+
return { merged: newContent, preserved: false, sidecar: false, sidecarPath: null, reason: null };
|
|
59
135
|
}
|
|
60
136
|
|
|
61
|
-
module.exports = {
|
|
137
|
+
module.exports = {
|
|
138
|
+
splicePreserveRepoSpecific,
|
|
139
|
+
REPO_SPECIFIC_MARKER,
|
|
140
|
+
ENTERPRISE_MARKER,
|
|
141
|
+
SIDECAR_MIN_BYTES,
|
|
142
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hone-ai/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.0",
|
|
4
4
|
"description": "Hone AI — Enterprise SDLC Pipeline CLI",
|
|
5
5
|
"main": "hone-cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"test": "echo \"No tests yet\" && exit 0",
|
|
18
18
|
"link": "npm link",
|
|
19
|
+
"sync-server-cli-version": "node scripts/sync-server-cli-version.js",
|
|
20
|
+
"prepublishOnly": "node scripts/sync-server-cli-version.js",
|
|
19
21
|
"postinstall": "echo '\\n Hone AI CLI installed successfully.\\n Next: run `hone init --token <YOUR_TOKEN>` to configure.\\n Docs: https://github.com/subbareddyvani/hone-server\\n'"
|
|
20
22
|
},
|
|
21
23
|
"dependencies": {
|