@topmindspace/topmind-writing-skills 0.9.0 → 0.9.1

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/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## [0.9.1] - 2026-10-28
2
+
3
+ - 安装器新增 `install all`、`update` 与 `status`:整套首次安装只需一条命令,后续更新只处理已装技能,状态直接显示安装版本与包快照。
4
+ - 默认目标优先识别真实安装与宿主配置;多个候选要求明确 `--to`,避免静默写入错误目录。
5
+ - 安装记录用于识别本地改动;受管副本的改动在自动更新时会阻止覆盖,显式强制更新与旧版未受管安装会保留旧副本。软链接和异名目录不再被替换。
6
+ - 根 README 中英文对齐新的上手、升级与本地安装流程。
7
+
1
8
  ## [0.9.0] - 2026-10-27
2
9
 
3
10
  - 扩展 8 个技能的创作方向、完整示例和适用条件,保留事实、排版、保真与工具协议,清理统一审美门槛和旧规则冲突。
package/README.en.md CHANGED
@@ -14,10 +14,12 @@ English | [中文](./README.md)
14
14
  </p>
15
15
 
16
16
  ```bash
17
- npx @topmindspace/topmind-writing-skills install topmind-viral-posts # install one
18
- npx @topmindspace/topmind-writing-skills list # see all
17
+ npx @topmindspace/topmind-writing-skills install all # first-time setup
18
+ npx @topmindspace/topmind-writing-skills@latest update # later, update installed skills
19
19
  ```
20
20
 
21
+ Then tell your AI assistant "revise this", "write a quick brief", or "make a cover for this article". To install just one skill, use `install topmind-revise` or another ID from `list`. Desktop and Obsidian already include a reviewed writing snapshot; update it through each app's About & Updates screen rather than these npx commands.
22
+
21
23
  Hosts start with the active note and allow explicit source changes. Drafts, a small read-only memory window and the selected skill are loaded in layers; other resources are retrieved as needed. Compaction does not reset completed work or budgets.
22
24
 
23
25
  ## Desktop and Obsidian integration
@@ -240,15 +242,19 @@ Every skill drafts only; you publish.
240
242
  **npm = pinned snapshot; GitHub = latest.**
241
243
 
242
244
  ```bash
245
+ npx @topmindspace/topmind-writing-skills install all --to ./.claude/skills
246
+ npx @topmindspace/topmind-writing-skills status --to ./.claude/skills
247
+ npx @topmindspace/topmind-writing-skills@latest update --to ./.claude/skills
243
248
  npx @topmindspace/topmind-writing-skills install topmind-briefs --to ./.claude/skills
244
- npx @topmindspace/topmind-writing-skills@latest install topmind-briefs # upgrade in place
245
- npx @topmindspace/topmind-writing-skills install topmind-briefs --force # reinstall / downgrade
249
+ npx @topmindspace/topmind-writing-skills install topmind-briefs --force --to ./.claude/skills
246
250
  npx @topmindspace/topmind-writing-skills uninstall topmind-briefs --to ./.claude/skills
247
- npx github:topmindspace/topmind-writing-skills install topmind-briefs # latest from GitHub
251
+ npx github:topmindspace/topmind-writing-skills update --to ./.claude/skills
248
252
  ```
249
253
 
250
- - Probes `./.agents` → `./.claude` → `./.cursor` → `./.codex` → `./.mimocode`, then user-level directories; the summary prints the target.
251
- - Older installs are staged before replacement; failures retain the previous copy. Equal versions report up to date; reinstalling the same version or downgrading needs `--force`. Back up local modifications first.
254
+ - `install all` installs the full set once; `update` changes only already installed skills, and `status` compares the package snapshot with a destination. All accept `--to`.
255
+ - Without `--to`, the CLI selects a single existing TopMind installation, skills directory, or host configuration. If several match, it lists them and asks for `--to`. With no host configured, it uses `./.agents/skills` in the current project. Output always names the destination.
256
+ - Replacements are staged first; failures keep the old copy. Local edits to a managed skill stop automatic updates until `--force` is explicit, which retains the previous copy. Upgrading an older untracked installation also retains a backup. Symlinks and unrelated directories are never replaced.
257
+ - Equal versions are left alone; downgrading a newer installed version needs `--force`. Reopen the AI session after updating so the host discovers new skills.
252
258
  - Skill ids are not npm packages; don't `npm install topmind-briefs`.
253
259
 
254
260
  ### Upgrade from a local checkout
@@ -256,12 +262,11 @@ npx github:topmindspace/topmind-writing-skills install topmind-briefs # late
256
262
  Install local, unpublished changes from the checkout; `@latest` contains only the published npm snapshot. Verify the target first. If tools share skills through symlinks, update the resolved shared directory and preserve those links. For a standalone Codex installation, run from the repository root:
257
263
 
258
264
  ```bash
259
- for skill in $(node scripts/discover_skills.js); do
260
- node bin/topmind-writing-skills.js install "$skill" --to "$HOME/.codex/skills" || break
261
- done
265
+ node bin/topmind-writing-skills.js status --to "$HOME/.codex/skills"
266
+ node bin/topmind-writing-skills.js update --to "$HOME/.codex/skills"
262
267
  ```
263
268
 
264
- Replace `--to` with the verified shared destination when appropriate and check every result. Reopen the session to refresh skill discovery; unpublished changes with the same version require an explicit `--force` reinstall.
269
+ Use `install all` for the first install. Replace `--to` with the verified shared destination when appropriate. Unpublished changes with the same skill version require `install <id> --force --to <actual-dir>`. The CLI copies each skill's self-contained resources and never writes to a destination's root `shared/` directory.
265
270
 
266
271
  <details>
267
272
  <summary>Repository layout and releases</summary>
package/README.md CHANGED
@@ -14,10 +14,12 @@
14
14
  </p>
15
15
 
16
16
  ```bash
17
- npx @topmindspace/topmind-writing-skills install topmind-viral-posts # 装一个
18
- npx @topmindspace/topmind-writing-skills list # 看全部
17
+ npx @topmindspace/topmind-writing-skills install all # 首次安装整套
18
+ npx @topmindspace/topmind-writing-skills@latest update # 以后只更新已装技能
19
19
  ```
20
20
 
21
+ 装好后直接对 AI 说「改这篇」「写条快讯」「给这篇做封面」。只想装一个,用 `install topmind-revise` 等技能 ID;`list` 查看全部。Desktop 和 Obsidian 已内置经过验证的写作快照,应用内更新从各自“关于与更新”进入,不需要再运行上面的 npx 命令。
22
+
21
23
  宿主默认绑定活跃笔记,允许显式换稿。当前稿件、少量全局记忆和所选技能分层加载;其余资源按需读取,压缩不重置已完成操作和预算。
22
24
 
23
25
  ## 在 Desktop 与 Obsidian 中使用
@@ -258,15 +260,19 @@ python3 scripts/de_ai_lint.py 文章.md --genre long # 长文
258
260
  **npm = 钉版本快照;GitHub = 跟仓库最新。**
259
261
 
260
262
  ```bash
263
+ npx @topmindspace/topmind-writing-skills install all --to ./.claude/skills
264
+ npx @topmindspace/topmind-writing-skills status --to ./.claude/skills
265
+ npx @topmindspace/topmind-writing-skills@latest update --to ./.claude/skills
261
266
  npx @topmindspace/topmind-writing-skills install topmind-briefs --to ./.claude/skills
262
- npx @topmindspace/topmind-writing-skills@latest install topmind-briefs # 升级:暂存完成再替换,失败保留旧版
263
- npx @topmindspace/topmind-writing-skills install topmind-briefs --force # 同版本重装 / 降级
267
+ npx @topmindspace/topmind-writing-skills install topmind-briefs --force --to ./.claude/skills
264
268
  npx @topmindspace/topmind-writing-skills uninstall topmind-briefs --to ./.claude/skills
265
- npx github:topmindspace/topmind-writing-skills install topmind-briefs # 跟仓库最新
269
+ npx github:topmindspace/topmind-writing-skills update --to ./.claude/skills
266
270
  ```
267
271
 
268
- - 默认探测 `./.agents` → `./.claude` → `./.cursor` → `./.codex` → `./.mimocode`,再到用户级目录;安装摘要会打印实际目标目录。
269
- - 更旧的安装先暂存再替换,失败保留旧版;版本相同提示已是最新;同版本重装或降级用 `--force`。有本地修改先备份。
272
+ - `install all` 首次安装整套;`update` 默认只更新已装的技能,不会擅自加装新技能;`status` 显示当前包快照与安装目录的差异。三者都支持 `--to` 指定实际目录。
273
+ - 不写 `--to` 时先找唯一的 TopMind 安装,再找唯一的技能目录或宿主配置;若同时存在多个候选,会列出目录并要求明确选择。没有宿主配置时使用当前项目的 `./.agents/skills`。结果会打印实际目录。
274
+ - 更新先复制到临时目录,完整后才替换;失败保留旧版。受管安装发现本地改动会停止,需明确 `--force`;强制替换时保留旧副本。升级早期未带安装记录的副本也会自动留备份。软链接或非同名技能目录不会被覆盖。
275
+ - 同版本不重复复制;已装版本比包新时拒绝降级,除非显式 `--force`。升级后重新打开 AI 会话,让宿主重新发现技能。
270
276
  - 技能 id 不是独立 npm 包,不要 `npm install topmind-briefs`。
271
277
 
272
278
  ### 从本地仓库升级
@@ -274,11 +280,12 @@ npx github:topmindspace/topmind-writing-skills install topmind-briefs # 跟
274
280
  本地未发布改动要从当前仓库安装,`@latest` 只包含 npm 已发布版本。先核实目标目录;已有跨工具软链接时更新其共享实际目录,保留链接关系。Codex 独立安装可在仓库根目录运行:
275
281
 
276
282
  ```bash
277
- for skill in $(node scripts/discover_skills.js); do
278
- node bin/topmind-writing-skills.js install "$skill" --to "$HOME/.codex/skills" || break
279
- done
283
+ node bin/topmind-writing-skills.js status --to "$HOME/.codex/skills"
284
+ node bin/topmind-writing-skills.js update --to "$HOME/.codex/skills"
280
285
  ```
281
286
 
287
+ 首次安装改用 `install all`;修改过技能内容而未改版本号时,用 `install <id> --force --to <实际目录>` 明确重装。CLI 只复制单个技能目录内的独立资源,不写入目标根目录的 `shared/`。
288
+
282
289
  若 Codex 技能链接到其他共享目录,将 `--to` 换为核实后的实际目录;检查每项安装输出。安装了新技能或更新入口后,重开会话让宿主重新发现;同版本未发布内容需要明确加 `--force` 才会重装。
283
290
 
284
291
  <details>
@@ -17,11 +17,13 @@
17
17
 
18
18
  const fs = require('fs');
19
19
  const path = require('path');
20
+ const crypto = require('crypto');
20
21
 
21
22
  const ROOT = path.resolve(__dirname, '..');
22
23
  // Skills live at repo root (one directory per skill with SKILL.md).
23
24
  const INFRA_DIRS = new Set(['bin', 'docs', 'scripts', 'shared', 'evals', 'node_modules', 'dist', 'release-assets']);
24
25
  const SKILL_ID_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
26
+ const INSTALL_RECORD = '.topmind-install.json';
25
27
 
26
28
  function die(msg, code = 1) {
27
29
  console.error(msg);
@@ -113,7 +115,7 @@ function installerVersion() {
113
115
 
114
116
  function resolveDefaultTarget() {
115
117
  const home = process.env.HOME || process.env.USERPROFILE || '';
116
- const candidates = [
118
+ const candidates = [...new Set([
117
119
  path.join(process.cwd(), '.agents', 'skills'),
118
120
  path.join(process.cwd(), '.claude', 'skills'),
119
121
  path.join(process.cwd(), '.cursor', 'skills'),
@@ -123,25 +125,89 @@ function resolveDefaultTarget() {
123
125
  path.join(home, '.agents', 'skills'),
124
126
  path.join(home, '.cursor', 'skills'),
125
127
  path.join(home, '.codex', 'skills'),
126
- ];
127
- for (const dir of candidates) {
128
- if (dir && fs.existsSync(path.dirname(dir))) {
129
- return dir;
130
- }
128
+ ])];
129
+ const choose = (paths) => {
130
+ if (paths.length === 1) return paths[0];
131
+ if (paths.length > 1) die(`More than one skill directory matches:\n ${paths.join('\n ')}\nChoose one with --to <dir>.`);
132
+ return null;
133
+ };
134
+ // Prefer the user's existing TopMind installation, then an existing skills
135
+ // directory, then a single host configuration. A mere cwd is not a signal.
136
+ const ids = listSkillIds();
137
+ const withTopmind = candidates.filter((dir) => ids.some((id) => fs.existsSync(path.join(dir, id, 'SKILL.md'))));
138
+ const withSkills = candidates.filter((dir) => fs.existsSync(dir));
139
+ const withHost = candidates.filter((dir) => fs.existsSync(path.dirname(dir)) && path.dirname(dir) !== process.cwd() && path.dirname(dir) !== home);
140
+ for (const group of [withTopmind, withSkills, withHost]) {
141
+ const selected = choose(group);
142
+ if (selected) return selected;
131
143
  }
144
+ // No host exists yet: use the cross-agent project convention explicitly.
132
145
  return path.join(process.cwd(), '.agents', 'skills');
133
146
  }
134
147
 
148
+ function readSkillName(skillPath) {
149
+ try {
150
+ const text = fs.readFileSync(path.join(skillPath, 'SKILL.md'), 'utf8');
151
+ const fm = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
152
+ return (fm && fm[1].match(/^name:\s*([a-z0-9-]+)\s*$/m) || [])[1] || '';
153
+ } catch { return ''; }
154
+ }
155
+
156
+ function skipGeneratedEntry(entry) {
157
+ return (entry.isDirectory() && ['node_modules', 'dist', '__pycache__'].includes(entry.name)) ||
158
+ (entry.isFile() && entry.name.endsWith('.pyc'));
159
+ }
160
+
161
+ function skillDigest(skillPath) {
162
+ const hash = crypto.createHash('sha256');
163
+ const walk = (dir, relative = '') => {
164
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
165
+ if (!relative && entry.name === INSTALL_RECORD) continue;
166
+ if (skipGeneratedEntry(entry)) continue;
167
+ const rel = path.join(relative, entry.name);
168
+ const absolute = path.join(dir, entry.name);
169
+ if (entry.isDirectory()) {
170
+ walk(absolute, rel);
171
+ } else {
172
+ hash.update(rel.replace(/\\/g, '/') + '\0');
173
+ if (entry.isFile()) hash.update(fs.readFileSync(absolute));
174
+ else hash.update(`non-regular:${entry.name}`);
175
+ hash.update('\0');
176
+ }
177
+ }
178
+ };
179
+ walk(skillPath);
180
+ return hash.digest('hex');
181
+ }
182
+
183
+ function installedState(skillId, targetRoot) {
184
+ const dest = path.join(targetRoot, skillId);
185
+ let stat;
186
+ try { stat = fs.lstatSync(dest); } catch (error) {
187
+ if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return { kind: 'missing', version: '' };
188
+ throw error;
189
+ }
190
+ if (!stat.isDirectory() || readSkillName(dest) !== skillId) return { kind: 'conflict', version: '' };
191
+ const version = readSkillVersion(dest);
192
+ const recordPath = path.join(dest, INSTALL_RECORD);
193
+ if (!fs.existsSync(recordPath)) return { kind: 'legacy', version };
194
+ try {
195
+ const record = JSON.parse(fs.readFileSync(recordPath, 'utf8'));
196
+ if (record.id === skillId && record.digest === skillDigest(dest)) return { kind: 'managed', version };
197
+ } catch { /* Treat unreadable records as local changes. */ }
198
+ return { kind: 'modified', version };
199
+ }
200
+
135
201
  function copyDir(src, dest) {
136
202
  fs.mkdirSync(dest, { recursive: true });
137
203
  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
138
204
  const s = path.join(src, entry.name);
139
205
  const d = path.join(dest, entry.name);
140
206
  if (entry.isDirectory()) {
141
- if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === '__pycache__') continue;
207
+ if (skipGeneratedEntry(entry)) continue;
142
208
  copyDir(s, d);
143
209
  } else if (entry.isFile()) {
144
- if (entry.name.endsWith('.pyc')) continue;
210
+ if (skipGeneratedEntry(entry)) continue;
145
211
  fs.copyFileSync(s, d);
146
212
  } else {
147
213
  console.warn(`warning: skipping non-regular file: ${s}`);
@@ -207,7 +273,7 @@ function compareVersions(a, b) {
207
273
  return 0;
208
274
  }
209
275
 
210
- function install(skillId, targetRoot, { force = false, defaultTarget = false } = {}) {
276
+ function planInstall(skillId, targetRoot, force) {
211
277
  assertSkillId(skillId);
212
278
  const src = skillDir(skillId);
213
279
  if (!fs.existsSync(path.join(src, 'SKILL.md'))) {
@@ -215,6 +281,26 @@ function install(skillId, targetRoot, { force = false, defaultTarget = false } =
215
281
  }
216
282
  const dest = path.join(targetRoot, skillId);
217
283
  assertDestOutsideSource(src, dest);
284
+ const state = installedState(skillId, targetRoot);
285
+ if (state.kind === 'conflict') die(`Refusing to replace ${dest}: it is not a ${skillId} skill directory.`);
286
+ if (state.kind === 'modified' && !force) {
287
+ die(`Local changes found in ${dest}. Keep them or pass --force to replace; the previous copy will be retained.`);
288
+ }
289
+ const packageVer = readSkillVersion(src) || 'unknown';
290
+ const cmp = compareVersions(packageVer, state.version);
291
+ if (state.kind !== 'missing' && !force && cmp < 0) {
292
+ die(`Installed ${skillId} ${state.version} is newer than packaged ${packageVer}; not downgrading. Pass --force (or -f) to downgrade.`);
293
+ }
294
+ return { src, dest, state, packageVer, cmp };
295
+ }
296
+
297
+ function install(skillId, targetRoot, { force = false, defaultTarget = false } = {}) {
298
+ const { src, dest, state, cmp } = planInstall(skillId, targetRoot, force);
299
+ if (state.kind !== 'missing' && !force && cmp === 0) {
300
+ console.log(`Already up to date: ${skillId} ${state.version} (use --force to reinstall)`);
301
+ console.log(` at: ${dest}`);
302
+ return;
303
+ }
218
304
  try {
219
305
  fs.mkdirSync(targetRoot, { recursive: true });
220
306
  } catch (e) {
@@ -224,37 +310,28 @@ function install(skillId, targetRoot, { force = false, defaultTarget = false } =
224
310
  'Choose a --to directory that can be created (not a file, not unwritable).'
225
311
  );
226
312
  }
227
- let upgradedFrom = null;
228
- if (fs.existsSync(dest)) {
229
- // 已装过:默认升级。打包版本更新 → 直接覆盖(不备份);相同 → 已是最新;已装版本更新 → 需 --force。
230
- const installedVer = readSkillVersion(dest);
231
- const packageVer = readSkillVersion(src) || 'unknown';
232
- const cmp = compareVersions(packageVer, installedVer);
233
- if (!force && cmp === 0) {
234
- console.log(`Already up to date: ${skillId} ${installedVer} (use --force to reinstall)`);
235
- console.log(` at: ${dest}`);
236
- return;
237
- }
238
- if (!force && cmp < 0) {
239
- die(
240
- `Installed ${skillId} ${installedVer} is newer than packaged ${packageVer}; not downgrading. ` +
241
- 'Pass --force (or -f) to downgrade.'
242
- );
243
- }
244
- upgradedFrom = cmp > 0 ? installedVer || 'unknown' : null;
245
- }
313
+ const upgradedFrom = state.kind !== 'missing' && cmp > 0 ? state.version || 'unknown' : null;
314
+ const retainPrevious = state.kind === 'legacy' || state.kind === 'modified';
246
315
  const stage = fs.mkdtempSync(path.join(targetRoot, `.${skillId}-stage-`));
247
316
  let previous = null;
248
317
  try {
249
318
  copyDir(src, stage);
250
- if (fs.existsSync(dest)) {
319
+ fs.writeFileSync(path.join(stage, INSTALL_RECORD), JSON.stringify({
320
+ id: skillId,
321
+ version: readSkillVersion(src),
322
+ digest: skillDigest(stage),
323
+ }, null, 2) + '\n');
324
+ if (state.kind !== 'missing') {
251
325
  previous = fs.mkdtempSync(path.join(targetRoot, `.${skillId}-previous-`));
252
326
  fs.rmdirSync(previous);
253
327
  fs.renameSync(dest, previous);
254
328
  }
255
329
  try { fs.renameSync(stage, dest); }
256
330
  catch (error) { if (previous) { fs.renameSync(previous, dest); previous = null; } throw error; }
257
- if (previous) { try { fs.rmSync(previous, { recursive: true, force: true }); } catch { console.warn(`Previous copy retained at: ${previous}`); } }
331
+ if (previous && !retainPrevious) {
332
+ try { fs.rmSync(previous, { recursive: true, force: true }); }
333
+ catch { console.warn(`Previous copy retained at: ${previous}`); }
334
+ }
258
335
  } catch (error) {
259
336
  fs.rmSync(stage, { recursive: true, force: true });
260
337
  die(`Install failed; the previous installed skill was retained. ${error.message}`);
@@ -266,9 +343,25 @@ function install(skillId, targetRoot, { force = false, defaultTarget = false } =
266
343
  else console.log(`Installed ${skillId}`);
267
344
  console.log(` to: ${dest}${toNote}`);
268
345
  console.log(` version: skill ${skillVer} (installer ${installerVersion()})`);
346
+ if (previous && retainPrevious) console.log(` backup: ${previous} (previous untracked or locally changed copy)`);
269
347
  console.log(` next: ${nextStep(skillId, dest)}`);
270
348
  }
271
349
 
350
+ function status(targetRoot) {
351
+ console.log(`Skill directory: ${targetRoot}`);
352
+ console.log('Packaged version | Installed version | State | Skill');
353
+ for (const id of listSkillIds()) {
354
+ const packaged = readSkillVersion(skillDir(id)) || 'unknown';
355
+ const state = installedState(id, targetRoot);
356
+ let label = state.kind;
357
+ if (state.kind === 'managed' || state.kind === 'legacy') {
358
+ const cmp = compareVersions(packaged, state.version);
359
+ label = cmp > 0 ? 'update available' : cmp < 0 ? 'installed newer' : state.kind === 'legacy' ? 'untracked' : 'current';
360
+ }
361
+ console.log(`${packaged} | ${state.version || '—'} | ${label} | ${id}`);
362
+ }
363
+ }
364
+
272
365
  function nextStep(skillId, dest) {
273
366
  return 'restart your agent so it picks up the skill';
274
367
  }
@@ -319,22 +412,25 @@ function usageText() {
319
412
 
320
413
  Usage:
321
414
  topmind-writing-skills list|ls
322
- topmind-writing-skills install <skill-id> [--to|-t <dir>] [--force|-f]
415
+ topmind-writing-skills status [--to|-t <dir>]
416
+ topmind-writing-skills install <skill-id|all> [--to|-t <dir>] [--force|-f]
417
+ topmind-writing-skills update [skill-id|all] [--to|-t <dir>] [--force|-f]
323
418
  topmind-writing-skills uninstall <skill-id> [--to|-t <dir>]
324
419
  topmind-writing-skills help
325
420
 
326
421
  Behavior:
327
- install copies <skill-id>/ into <dir>/<skill-id>/. If the skill is already
328
- installed: a newer packaged version upgrades it in place (no backup,
329
- prints "Upgraded <id> <old> -> <new>"); the same version prints
330
- "Already up to date" (--force reinstalls); an installed version newer
331
- than the package is refused unless --force. A missing/unparseable
332
- installed version counts as an upgrade. Prints where/what-version/
333
- next-steps when done.
422
+ status compares the local package with skills installed in <dir>.
423
+ install installs one skill or all eight; newer packaged versions upgrade
424
+ existing copies. Equal versions are left alone. A newer installed
425
+ version requires --force. Locally changed managed copies are kept
426
+ unless --force is explicit. Untracked older copies are backed up
427
+ during upgrade. The replacement is staged before switching.
428
+ update upgrades only installed skills (all by default); does not add new ones.
334
429
  uninstall removes <dir>/<skill-id>/ (refuses: not installed, not a skill
335
430
  directory, or the package's own skill source).
336
- No --to: the target directory is auto-detected (probe order below) and
337
- printed explicitly in the install summary.
431
+ No --to: choose a single existing TopMind installation, skills directory or
432
+ host configuration. Ambiguous matches require --to. With no host configured,
433
+ use ./.agents/skills and print the chosen location.
338
434
 
339
435
  Target probe order (no --to): ./.agents/skills, ./.claude/skills,
340
436
  ./.cursor/skills, ./.codex/skills, ./.mimocode/skills, then
@@ -342,6 +438,9 @@ Target probe order (no --to): ./.agents/skills, ./.claude/skills,
342
438
 
343
439
  Examples (npm recommended when published):
344
440
  npx @topmindspace/topmind-writing-skills list
441
+ npx @topmindspace/topmind-writing-skills status
442
+ npx @topmindspace/topmind-writing-skills install all
443
+ npx @topmindspace/topmind-writing-skills@latest update
345
444
  npx @topmindspace/topmind-writing-skills install topmind-briefs
346
445
  npx @topmindspace/topmind-writing-skills install topmind-briefs --to ./.agents/skills
347
446
  npx @topmindspace/topmind-writing-skills install topmind-briefs --force
@@ -358,6 +457,22 @@ function usage() {
358
457
  console.log(usageText());
359
458
  }
360
459
 
460
+ function parseOptions(args, start, { allowForce = false } = {}) {
461
+ let to = null;
462
+ let force = false;
463
+ for (let i = start; i < args.length; i++) {
464
+ if (args[i] === '--to' || args[i] === '-t') {
465
+ to = args[++i];
466
+ if (!to || to.startsWith('-')) die('Option --to requires a directory path.');
467
+ } else if (allowForce && (args[i] === '--force' || args[i] === '-f')) {
468
+ force = true;
469
+ } else {
470
+ die(`Unknown option: ${args[i]}\n\n` + usageText());
471
+ }
472
+ }
473
+ return { targetRoot: to ? path.resolve(to) : resolveDefaultTarget(), force, defaultTarget: !to };
474
+ }
475
+
361
476
  function main(argv) {
362
477
  const args = argv.slice(2);
363
478
  const cmd = args[0];
@@ -381,25 +496,32 @@ function main(argv) {
381
496
  return;
382
497
  }
383
498
 
384
- if (cmd === 'install') {
385
- const skillId = args[1];
386
- if (!skillId || skillId.startsWith('-')) {
499
+ if (cmd === 'status') {
500
+ const { targetRoot } = parseOptions(args, 1);
501
+ status(targetRoot);
502
+ return;
503
+ }
504
+
505
+ if (cmd === 'install' || cmd === 'update') {
506
+ const requested = cmd === 'update' && (!args[1] || args[1].startsWith('-')) ? 'all' : args[1];
507
+ if (!requested || requested.startsWith('-')) {
387
508
  die('Missing skill id.\n\n' + usageText());
388
509
  }
389
- let to = null;
390
- let force = false;
391
- for (let i = 2; i < args.length; i++) {
392
- if (args[i] === '--to' || args[i] === '-t') {
393
- to = args[++i];
394
- if (!to) die('Option --to requires a directory path.');
395
- } else if (args[i] === '--force' || args[i] === '-f') {
396
- force = true;
397
- } else {
398
- die(`Unknown option: ${args[i]}\n\n` + usageText());
399
- }
510
+ const optionStart = cmd === 'update' && requested === 'all' && args[1] !== 'all' ? 1 : 2;
511
+ const options = parseOptions(args, optionStart, { allowForce: true });
512
+ const ids = requested === 'all' ? listSkillIds() : [requested];
513
+ const selected = cmd === 'update'
514
+ ? ids.filter(id => installedState(id, options.targetRoot).kind !== 'missing')
515
+ : ids;
516
+ if (!selected.length) {
517
+ console.log(`No TopMind writing skills installed in ${options.targetRoot}. Run install all to get started.`);
518
+ return;
400
519
  }
401
- const targetRoot = to ? path.resolve(to) : resolveDefaultTarget();
402
- install(skillId, targetRoot, { force, defaultTarget: !to });
520
+ // Check every target before the first write; a conflict should not leave
521
+ // an avoidably half-updated set.
522
+ for (const id of selected) planInstall(id, options.targetRoot, options.force);
523
+ for (const id of selected) install(id, options.targetRoot, options);
524
+ if (selected.length > 1) console.log(`${cmd === 'install' ? 'Installed' : 'Updated'} ${selected.length} writing skills in ${options.targetRoot}.`);
403
525
  return;
404
526
  }
405
527
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topmindspace/topmind-writing-skills",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "TopMindSpace 写作技能合集 — 通过 npm / npx 安装技能(公众号长文、X 长文、干货短文、X 引流短帖、改稿与质检、封面图、长图海报)。只出稿与发布包,不代发。",
5
5
  "license": "MIT",
6
6
  "author": "TopMindspace",
@@ -76,7 +76,7 @@
76
76
  "check:pack": "node scripts/check_npm_pack.js",
77
77
  "validate:spec": "bash -c 'for s in $(node scripts/discover_skills.js); do ${AGENTSKILLS:-agentskills} validate \"$s\" || exit 1; done'",
78
78
  "prepack": "node scripts/sync_npm_files.js --check && node scripts/sync_shared.js --check && python3 shared/scripts/knowledge.py validate",
79
- "install:skills": "node -e \"const {discoverSkills}=require('./scripts/discover_skills'); const {spawnSync}=require('child_process'); for (const id of discoverSkills()) { const r=spawnSync(process.execPath,['bin/topmind-writing-skills.js','install',id],{stdio:'inherit'}); if(r.status) process.exit(r.status||1); }\"",
79
+ "install:skills": "node bin/topmind-writing-skills.js install all",
80
80
  "check": "node scripts/run_skill_gates.js check",
81
81
  "privacy": "python3 scripts/ci_privacy_scan.py",
82
82
  "audit": "node scripts/run_skill_gates.js audit",