@house365/comate 0.7.0 → 0.8.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/bin/init-run.mjs CHANGED
@@ -11,10 +11,12 @@ import {
11
11
  SKILLS_TIMEOUT_MS,
12
12
  npmInvocation,
13
13
  openSpecState,
14
+ pruneDeprecatedSkills,
14
15
  run,
15
16
  scaffoldDocs,
16
17
  skillsInvocation,
17
18
  superpowersInstalled,
19
+ userSkillsRoot,
18
20
  } from './init.mjs';
19
21
 
20
22
  // `comate init` — no flags. Skills go user-level (~/.agents/skills), matching how
@@ -133,6 +135,13 @@ export async function init(pkgRoot) {
133
135
  results.push(['comate', r.ok ? 'installed' : 'failed', r.ok ? '' : r.reason, r.output]);
134
136
  }
135
137
 
138
+ // Prune comate skill dirs renamed/dropped in past releases. `skills add` above
139
+ // only adds/overwrites, so /comate-hotfix etc. would otherwise linger as dead
140
+ // slash commands. The postinstall hook does this too, but --ignore-scripts
141
+ // users never run it — init is their only path, so it must clean up here.
142
+ const pruned = pruneDeprecatedSkills(userSkillsRoot(home));
143
+ if (pruned.length) results.push(['清理废弃 skill', 'installed', pruned.join('、')]);
144
+
136
145
  // 4. openspec/ project structure. Not optional: comate's open and archive
137
146
  // phases read it, so an install without it cannot actually run. An existing
138
147
  // one is left alone, which covers projects that already keep it elsewhere.
package/bin/init.mjs CHANGED
@@ -9,7 +9,7 @@
9
9
  // ponytail: no SKILLS_AGENT_MAP here. The skills CLI detects the running agent
10
10
  // on its own, so translating platform ids to agent names would be dead code.
11
11
  import { spawnSync } from 'node:child_process';
12
- import { existsSync, mkdirSync, readdirSync, copyFileSync, statSync } from 'node:fs';
12
+ import { existsSync, mkdirSync, readdirSync, copyFileSync, statSync, lstatSync, rmSync } from 'node:fs';
13
13
  import { createRequire } from 'node:module';
14
14
  import { dirname, join } from 'node:path';
15
15
 
@@ -211,6 +211,83 @@ function scaffoldDocs(pkgRoot, cwd = process.cwd()) {
211
211
  return { ok: true, created, skipped };
212
212
  }
213
213
 
214
+ // comate skill dirs that later releases renamed or dropped. `skills add` only
215
+ // adds/overwrites, never removes, so without an explicit prune these linger in
216
+ // ~/.agents/skills as dead slash commands (e.g. /comate-hotfix after it became
217
+ // /comate-fix). ONLY these exact names are ever deleted: the dir also holds
218
+ // unrelated third-party skills (lark-*, openspec-*), so a prefix sweep or a
219
+ // diff-against-current strategy could wipe a user's own work. Add a line here
220
+ // whenever a comate skill is renamed or removed.
221
+ const DEPRECATED_SKILLS = ['comate-hotfix', 'comate-tweak', 'comate-intake'];
222
+
223
+ function userSkillsRoot(home = process.env.HOME ?? process.env.USERPROFILE ?? '') {
224
+ return join(home, '.agents', 'skills');
225
+ }
226
+
227
+ // lstat, not stat: only a real directory is a comate-installed skill. A symlink
228
+ // is a developer's deliberate dev setup pointing elsewhere — never remove it.
229
+ function isRealDir(p) {
230
+ try {
231
+ return lstatSync(p).isDirectory();
232
+ } catch {
233
+ return false;
234
+ }
235
+ }
236
+
237
+ // Delete only the exact DEPRECATED_SKILLS dirs present under skillsRoot. Every
238
+ // dependency is injectable so tests never touch a real ~/.agents/skills. Never
239
+ // throws — a permission error or race on one dir must not abort an install.
240
+ // Returns the names actually removed (for the caller to report).
241
+ function pruneDeprecatedSkills(
242
+ skillsRoot,
243
+ {
244
+ names = DEPRECATED_SKILLS,
245
+ exists = existsSync,
246
+ isDir = isRealDir,
247
+ rm = (p) => rmSync(p, { recursive: true, force: true }),
248
+ } = {},
249
+ ) {
250
+ const removed = [];
251
+ for (const name of names) {
252
+ const dir = join(skillsRoot, name);
253
+ if (!exists(dir) || !isDir(dir)) continue;
254
+ try {
255
+ rm(dir);
256
+ removed.push(name);
257
+ } catch {
258
+ // A stuck dir (locked, permission) is not worth failing the install over.
259
+ }
260
+ }
261
+ return removed;
262
+ }
263
+
264
+ // True only when the pinned `skills` dependency resolves locally. A postinstall
265
+ // must never reach the network, so this gates syncComateSkills: if resolution
266
+ // fails we skip rather than let skillsInvocation fall back to `npx skills@latest`.
267
+ function skillsResolvesLocally(pkgRoot) {
268
+ try {
269
+ createRequire(join(pkgRoot, 'package.json')).resolve('skills/package.json');
270
+ return true;
271
+ } catch {
272
+ return false;
273
+ }
274
+ }
275
+
276
+ // Copy comate's own skills into ~/.agents/skills via the bundled skills CLI —
277
+ // the exact command `comate init` runs, so the layout is identical. Local-only:
278
+ // skips (never goes to network) when the skills dep can't be resolved, leaving
279
+ // `comate init` as the fallback. Returns { ok, skipped }.
280
+ function syncComateSkills(pkgRoot, { home = process.env.HOME ?? process.env.USERPROFILE ?? '', runner = run } = {}) {
281
+ if (!skillsResolvesLocally(pkgRoot)) return { ok: false, skipped: 'no-local-cli' };
282
+ const r = runner(
283
+ skillsInvocation(pkgRoot, ['add', pkgRoot, '--skill', '*', '-g', '-y']),
284
+ SKILLS_TIMEOUT_MS,
285
+ { quiet: true },
286
+ );
287
+ const landed = existsSync(join(home, '.agents', 'skills', 'comate', 'SKILL.md'));
288
+ return { ok: r.ok && landed, skipped: null };
289
+ }
290
+
214
291
  export {
215
292
  MIN_OPENSPEC,
216
293
  NPM_TIMEOUT_MS,
@@ -228,4 +305,8 @@ export {
228
305
  scaffoldSourceDir,
229
306
  copyTreeSkipExisting,
230
307
  scaffoldDocs,
308
+ DEPRECATED_SKILLS,
309
+ userSkillsRoot,
310
+ pruneDeprecatedSkills,
311
+ syncComateSkills,
231
312
  };
@@ -14,13 +14,33 @@ if (isCI() || isDevInstall()) {
14
14
 
15
15
  const { banner, pkgVersion } = await import('./init-run.mjs');
16
16
 
17
+ // Best-effort: refresh the user-level skills so `npm i -g @latest` alone updates
18
+ // them, and prune dirs renamed/dropped in past releases. All guarded — a failure
19
+ // here must never break the npm install, and `comate init` remains the fallback
20
+ // for --ignore-scripts users (who never reach this hook) and per-project setup.
21
+ const sync = await syncSkillsQuietly();
22
+
17
23
  console.log(banner(pkgVersion(pkgRoot)));
18
24
  console.log(` 🎉 Congratulations! comate 已成功安装。\n`);
25
+ if (sync.synced) console.log(' ✓ 已把 skills 同步到 ~/.agents/skills');
26
+ if (sync.removed.length) console.log(` ✓ 已清理废弃 skill:${sync.removed.join('、')}`);
19
27
  console.log(' 下一步:');
20
- console.log(' 在你的项目目录下运行 comate init 开始使用');
28
+ console.log(' 在你的项目目录下运行 comate init 完成项目初始化(openspec/、docs/)');
21
29
  console.log(' 或运行 comate doctor 健康检查');
22
30
  console.log(' 更多帮助 comate --help\n');
23
31
 
32
+ async function syncSkillsQuietly() {
33
+ try {
34
+ const { syncComateSkills, pruneDeprecatedSkills, userSkillsRoot } = await import('./init.mjs');
35
+ const { ok } = syncComateSkills(pkgRoot);
36
+ const removed = pruneDeprecatedSkills(userSkillsRoot());
37
+ return { synced: ok, removed };
38
+ } catch {
39
+ // Any failure (no skills CLI resolvable, fs error) is silent: init covers it.
40
+ return { synced: false, removed: [] };
41
+ }
42
+ }
43
+
24
44
  function isCI() {
25
45
  return Boolean(process.env.CI || process.env.GITHUB_ACTIONS);
26
46
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@house365/comate",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "comate —— OpenSpec + Superpowers 规约驱动的 AI 开发编排器(Agent Skills)",
5
5
  "type": "module",
6
6
  "license": "MIT",