@zyaiting/keelson 0.4.1 → 0.5.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/README.md +15 -12
- package/README_CN.md +15 -12
- package/hooks/codebuddy-session.mjs +13 -28
- package/hooks/codex-session.mjs +16 -0
- package/hooks/prompt-state.mjs +5 -29
- package/hooks/session-start.mjs +7 -0
- package/hooks/workflow-guard.mjs +95 -0
- package/package.json +1 -1
- package/registry/platforms.json +4 -4
- package/registry/runtime-hashes.json +41 -0
- package/skills/keelson/SKILL.md +6 -3
- package/skills/keelson/references/build.md +12 -0
- package/skills/keelson/references/discover.md +5 -3
- package/skills/keelson/references/frontend.md +1 -1
- package/skills/keelson/references/interview.md +26 -7
- package/skills/keelson/references/land.md +2 -0
- package/skills/keelson/references/shape.md +3 -3
- package/skills/keelson/references/verify.md +1 -1
- package/skills/keelson/templates/resident-block.md +1 -1
- package/skills/keelson/templates/workflow.md +3 -3
- package/skills/zh/keelson/SKILL.md +6 -3
- package/skills/zh/keelson/references/build.md +12 -0
- package/skills/zh/keelson/references/discover.md +5 -3
- package/skills/zh/keelson/references/frontend.md +1 -1
- package/skills/zh/keelson/references/interview.md +26 -7
- package/skills/zh/keelson/references/land.md +2 -0
- package/skills/zh/keelson/references/shape.md +3 -3
- package/skills/zh/keelson/references/verify.md +2 -0
- package/skills/zh/keelson/templates/resident-block.md +1 -1
- package/skills/zh/keelson/templates/workflow.md +3 -3
- package/src/cli.js +4 -3
- package/src/commands/ablate.js +2 -1
- package/src/commands/ask.js +4 -1
- package/src/commands/context.js +7 -0
- package/src/commands/doctor.js +3 -1
- package/src/commands/hook.js +6 -1
- package/src/commands/init.js +12 -7
- package/src/commands/new.js +1 -1
- package/src/commands/platforms.js +1 -1
- package/src/commands/start.js +33 -0
- package/src/commands/uninstall.js +1 -1
- package/src/lib/decisions.js +3 -1
- package/src/lib/hook-context.js +30 -0
- package/src/lib/markdown.js +1 -1
- package/src/lib/rules.js +10 -2
- package/src/lib/workflow.js +102 -0
- package/src/platforms/integration.js +75 -93
- package/src/platforms/runtime.js +68 -17
package/src/platforms/runtime.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { PKG_ROOT } from '../lib/paths.js';
|
|
5
|
-
import { exists, read, walk, write, rmrf, replaceDirSafe, withLock } from '../lib/fs.js';
|
|
5
|
+
import { exists, read, readJson, walk, write, rmrf, replaceDirSafe, withLock } from '../lib/fs.js';
|
|
6
6
|
import { PLATFORMS } from './registry.js';
|
|
7
7
|
|
|
8
8
|
export const CANONICAL_SKILL_DIR = path.join('.keelson', 'skill');
|
|
@@ -124,6 +124,42 @@ function treeHash(dir) {
|
|
|
124
124
|
} catch { return null; }
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
// Before 0.5.0, manifests carried a version but no content digest. Bootstrap
|
|
128
|
+
// only from exact published output; future releases use the installed digest.
|
|
129
|
+
const HISTORICAL_RUNTIME = readJson(path.join(PKG_ROOT, 'registry', 'runtime-hashes.json'), {}).versions ?? {};
|
|
130
|
+
|
|
131
|
+
function runtimeDigest(root, rel, kind) {
|
|
132
|
+
const target = path.join(root, rel);
|
|
133
|
+
if (kind !== 'workflows') return treeHash(target);
|
|
134
|
+
try { return fs.lstatSync(target).isFile() ? fileHash(read(target)) : null; } catch { return null; }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function previousRuntimeMatches(root, rel, kind, previousState) {
|
|
138
|
+
if (!previousState) return false;
|
|
139
|
+
const owned = kind === 'shims'
|
|
140
|
+
? previousState.targets?.some((t) => t.skillsDir && path.join(t.skillsDir, 'keelson') === rel)
|
|
141
|
+
: previousState.vendor === true;
|
|
142
|
+
if (!owned) return false;
|
|
143
|
+
const actual = runtimeDigest(root, rel, kind);
|
|
144
|
+
if (!actual) return false;
|
|
145
|
+
const recorded = previousState.runtime?.[rel];
|
|
146
|
+
if (recorded) return actual === recorded;
|
|
147
|
+
return (HISTORICAL_RUNTIME[previousState.packageVersion]?.[kind] ?? []).includes(actual);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function captureRuntimeOwnership(root, targets, vendor) {
|
|
151
|
+
const runtime = {};
|
|
152
|
+
for (const target of targets) {
|
|
153
|
+
const rel = path.join(target.skillsDir, 'keelson');
|
|
154
|
+
runtime[rel] = runtimeDigest(root, rel, 'shims');
|
|
155
|
+
}
|
|
156
|
+
if (vendor) {
|
|
157
|
+
runtime[CANONICAL_SKILL_DIR] = runtimeDigest(root, CANONICAL_SKILL_DIR, 'skills');
|
|
158
|
+
runtime[CANONICAL_WORKFLOW] = runtimeDigest(root, CANONICAL_WORKFLOW, 'workflows');
|
|
159
|
+
}
|
|
160
|
+
return runtime;
|
|
161
|
+
}
|
|
162
|
+
|
|
127
163
|
function canonicalSkillMatches(root, { lang, profile, version }) {
|
|
128
164
|
const dest = path.join(root, CANONICAL_SKILL_DIR);
|
|
129
165
|
const files = renderSkillFiles(lang, profile, version);
|
|
@@ -132,12 +168,19 @@ function canonicalSkillMatches(root, { lang, profile, version }) {
|
|
|
132
168
|
return version === '0.3.0' && treeHash(dest) === LEGACY_V03_SKILL_TREE_HASHES.get(`${lang}/${profile}`);
|
|
133
169
|
}
|
|
134
170
|
|
|
135
|
-
export function
|
|
171
|
+
export function assertCanonicalSkillInstallable(root, { lang, profile, version, previousState, force = false }) {
|
|
136
172
|
const dest = path.join(root, CANONICAL_SKILL_DIR);
|
|
137
|
-
|
|
138
|
-
|
|
173
|
+
if (fs.lstatSync(dest, { throwIfNoEntry: false }) && !canonicalSkillMatches(root, { lang, profile, version }) &&
|
|
174
|
+
!previousRuntimeMatches(root, CANONICAL_SKILL_DIR, 'skills', previousState) &&
|
|
175
|
+
!(previousState?.packageVersion === '0.3.0' && canonicalSkillMatches(root, { lang, profile, version: '0.3.0' })) && !force) {
|
|
139
176
|
throw new Error('vendored skill differs from this CLI output; Keelson left it unchanged. Review it, then pass --force only if replacing the whole directory is intended.');
|
|
140
177
|
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function installCanonicalSkill(root, options) {
|
|
181
|
+
assertCanonicalSkillInstallable(root, options);
|
|
182
|
+
const dest = path.join(root, CANONICAL_SKILL_DIR);
|
|
183
|
+
const files = renderSkillFiles(options.lang, options.profile, options.version);
|
|
141
184
|
withLock(dest, () => {
|
|
142
185
|
replaceDirSafe(dest, (tmp) => {
|
|
143
186
|
for (const f of files) write(path.join(tmp, f.rel), f.content);
|
|
@@ -157,10 +200,10 @@ function skillShimMatches(dest, content, legacyVersion) {
|
|
|
157
200
|
(normalize(read(skill)) === content || legacyV03SkillShimMatches(dest, legacyVersion));
|
|
158
201
|
}
|
|
159
202
|
|
|
160
|
-
function assertSkillInstallable(root, target, { lang, version, legacyVersion, previousVersion, force }) {
|
|
203
|
+
function assertSkillInstallable(root, target, { lang, version, legacyVersion, previousVersion, previousState, force }) {
|
|
161
204
|
const p = typeof target === 'string' ? PLATFORMS[target] : target;
|
|
162
205
|
const dest = path.join(root, p.skillsDir, 'keelson');
|
|
163
|
-
const priorOutput = previousVersion && managedSkillShimMatches(root, p, previousVersion);
|
|
206
|
+
const priorOutput = previousVersion && managedSkillShimMatches(root, p, previousVersion, previousState);
|
|
164
207
|
if (fs.lstatSync(dest, { throwIfNoEntry: false }) && !skillShimMatches(dest, renderSkillShim(lang, version), legacyVersion) && !priorOutput && !force) {
|
|
165
208
|
throw new Error('discovery shim differs from this CLI output; Keelson left it unchanged. Review it, then pass --force only if replacing the whole directory is intended.');
|
|
166
209
|
}
|
|
@@ -177,18 +220,19 @@ export function assertSkillsInstallable(root, targets, options) {
|
|
|
177
220
|
}
|
|
178
221
|
}
|
|
179
222
|
|
|
180
|
-
/** True only for a complete
|
|
181
|
-
export function managedSkillShimMatches(root, target, version) {
|
|
223
|
+
/** True only for a complete generated shim or its recorded/published digest. */
|
|
224
|
+
export function managedSkillShimMatches(root, target, version, previousState) {
|
|
182
225
|
const p = typeof target === 'string' ? PLATFORMS[target] : target;
|
|
183
226
|
const dest = path.join(root, p.skillsDir, 'keelson');
|
|
184
|
-
return
|
|
227
|
+
return previousRuntimeMatches(root, path.join(p.skillsDir, 'keelson'), 'shims', previousState) ||
|
|
228
|
+
['en', 'zh'].some((lang) => skillShimMatches(dest, renderSkillShim(lang, version), version));
|
|
185
229
|
}
|
|
186
230
|
|
|
187
|
-
export function installSkill(root, target, { lang, version, legacyVersion = null, previousVersion = null, force = false }) {
|
|
231
|
+
export function installSkill(root, target, { lang, version, legacyVersion = null, previousVersion = null, previousState, force = false }) {
|
|
188
232
|
const p = typeof target === 'string' ? PLATFORMS[target] : target;
|
|
189
233
|
const dest = path.join(root, p.skillsDir, 'keelson');
|
|
190
234
|
const content = renderSkillShim(lang, version);
|
|
191
|
-
assertSkillInstallable(root, p, { lang, version, legacyVersion, previousVersion, force });
|
|
235
|
+
assertSkillInstallable(root, p, { lang, version, legacyVersion, previousVersion, previousState, force });
|
|
192
236
|
withLock(dest, () => replaceDirSafe(dest, (tmp) => write(path.join(tmp, 'SKILL.md'), content)));
|
|
193
237
|
return path.relative(root, dest);
|
|
194
238
|
}
|
|
@@ -210,14 +254,21 @@ export function plannedWorkflowFile(root, { lang, guide = false }) {
|
|
|
210
254
|
return { path: path.relative(root, target), status };
|
|
211
255
|
}
|
|
212
256
|
|
|
213
|
-
export function
|
|
257
|
+
export function assertWorkflowInstallable(root, { lang, guide = false, previousState, force = false }) {
|
|
214
258
|
const target = path.join(root, CANONICAL_WORKFLOW);
|
|
215
259
|
const content = workflowContent(lang, guide);
|
|
216
260
|
const stat = fs.lstatSync(target, { throwIfNoEntry: false });
|
|
217
|
-
if (stat && (stat.
|
|
261
|
+
if (stat && (!stat.isFile() || normalize(read(target)) !== content) &&
|
|
262
|
+
!previousRuntimeMatches(root, CANONICAL_WORKFLOW, 'workflows', previousState) &&
|
|
263
|
+
!(previousState?.packageVersion === '0.3.0' && stat.isFile() && LEGACY_V03_WORKFLOW_HASHES.has(fileHash(read(target)))) && !force) {
|
|
218
264
|
throw new Error('vendored workflow differs from this CLI output; Keelson left it unchanged. Review it, then pass --force only if replacing it is intended.');
|
|
219
265
|
}
|
|
220
|
-
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function installWorkflow(root, options) {
|
|
269
|
+
assertWorkflowInstallable(root, options);
|
|
270
|
+
const target = path.join(root, CANONICAL_WORKFLOW);
|
|
271
|
+
write(target, workflowContent(options.lang, options.guide));
|
|
221
272
|
return path.relative(root, target);
|
|
222
273
|
}
|
|
223
274
|
|
|
@@ -225,12 +276,12 @@ export function residentBlock(lang) {
|
|
|
225
276
|
return read(path.join(skillSource(lang), 'templates', 'resident-block.md'));
|
|
226
277
|
}
|
|
227
278
|
|
|
228
|
-
export function removeCanonicalRuntime(root, { lang, profile, version, guide = false } = {}) {
|
|
279
|
+
export function removeCanonicalRuntime(root, { lang, profile, version, guide = false, previousState } = {}) {
|
|
229
280
|
const removed = [];
|
|
230
281
|
const preserved = [];
|
|
231
282
|
const skill = path.join(root, CANONICAL_SKILL_DIR);
|
|
232
283
|
if (exists(skill)) {
|
|
233
|
-
if (canonicalSkillMatches(root, { lang, profile, version })) {
|
|
284
|
+
if (canonicalSkillMatches(root, { lang, profile, version }) || previousRuntimeMatches(root, CANONICAL_SKILL_DIR, 'skills', previousState)) {
|
|
234
285
|
rmrf(skill);
|
|
235
286
|
removed.push(CANONICAL_SKILL_DIR);
|
|
236
287
|
} else preserved.push(CANONICAL_SKILL_DIR);
|
|
@@ -240,7 +291,7 @@ export function removeCanonicalRuntime(root, { lang, profile, version, guide = f
|
|
|
240
291
|
preserved.push(CANONICAL_WORKFLOW);
|
|
241
292
|
} else if (exists(workflow)) {
|
|
242
293
|
const legacyWorkflow = version === '0.3.0' && LEGACY_V03_WORKFLOW_HASHES.has(fileHash(read(workflow)));
|
|
243
|
-
if (normalize(read(workflow)) === workflowContent(lang, guide) || legacyWorkflow) {
|
|
294
|
+
if (normalize(read(workflow)) === workflowContent(lang, guide) || legacyWorkflow || previousRuntimeMatches(root, CANONICAL_WORKFLOW, 'workflows', previousState)) {
|
|
244
295
|
rmrf(workflow);
|
|
245
296
|
removed.push(CANONICAL_WORKFLOW);
|
|
246
297
|
} else preserved.push(CANONICAL_WORKFLOW);
|