rush-ai 0.14.0 → 0.15.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 CHANGED
@@ -141,12 +141,24 @@ rush-ai task status <id> --json
141
141
 
142
142
  | 命令 | 说明 |
143
143
  |------|------|
144
- | `marketplace add <source>` | 注册 marketplace(`github:owner/repo` 或 `directory:/abs/path`) |
144
+ | `marketplace add <source>` | 注册 marketplace(`github:owner/repo`、`directory:/abs/path`、`rush://<host>`) |
145
145
  | `marketplace list` / `remove` / `update` | 管理本地 marketplace 缓存 |
146
- | `plugin install <ref>` | 一条命令同步装到 Claude Code + Codex + Cursor;`--target` 单独装;`--dry-run` / `--force` |
146
+ | `plugin install <ref>` | 一条命令同步装到 Claude Code + Codex + Cursor;`--target` 单独装;`--dry-run` / `--force` / `--secret KEY=VALUE` |
147
147
  | `plugin list` / `uninstall` / `update` | 对称管理 |
148
148
 
149
- `<ref>` 格式:`<name>` 或 `<name>@<marketplace>`(例 `rush@rush-plugin`)。
149
+ `<ref>` 格式:`<name>` 或 `<name>@<marketplace>`(例 `my-plugin@rush`)。
150
+
151
+ `rush://` source 特性:
152
+ - 首次 `plugin install xxx@rush` 自动注册 Rush 平台 marketplace(无需手动 `marketplace add`)
153
+ - 自动从 API 获取 plugin manifest + 用 reskill 下载完整 skill 目录
154
+ - `--secret KEY=VALUE`(可重复)预设 MCP 凭证,跳过交互式输入
155
+ - `--force` 重新拉取 manifest 并更新 secrets
156
+
157
+ `claude-plugins-official` source 特性:
158
+ - 首次 `plugin install xxx@claude-plugins-official` 自动注册 Anthropic 官方 marketplace
159
+ - 支持 URL source 按需 clone:外部 git repo 的 plugin 自动 clone 到 `~/.rush/plugin-cache/`
160
+ - 支持 git-subdir 模式(一个 repo 内多个 plugin)
161
+ - `--force` 重新 clone 最新版本
150
162
 
151
163
  ### 认证 / 配置 / 其他
152
164
 
@@ -194,20 +206,34 @@ rush-ai config list
194
206
 
195
207
  ### 插件分发(可选)
196
208
 
197
- 如果你想把 Rush 生态的 skill / command / rule / MCP 同时装到本地的 Claude Code / Codex / Cursor
209
+ Rush 平台上创建的 Plugin(Skill + MCP 组合包)一键装到本地 IDE
198
210
 
199
211
  ```bash
200
- rush-ai marketplace add github:kanyun-inc/rush-plugin
201
- rush-ai plugin install rush
202
- # ✓ Claude Code (commands + skills + rules + MCP)
212
+ # 从 Rush 平台安装(自动注册 marketplace,无需手动 add
213
+ rush-ai plugin install my-plugin@rush
214
+ # ✓ Claude Code (skills + MCP)
203
215
  # ✓ Codex (skills + MCP)
204
- # ✓ Cursor (skills + rules + MCP)
216
+ # ✓ Cursor (skills + MCP)
217
+
218
+ # 带 MCP secrets(CI/非交互模式)
219
+ rush-ai plugin install my-plugin@rush --secret SHIMO_TOKEN=xxx --secret API_KEY=yyy
205
220
 
206
221
  # 只装某一家
207
- rush-ai plugin install rush --target claude-code
222
+ rush-ai plugin install my-plugin@rush --target claude-code
223
+
224
+ # 更新 secrets / 刷新 skill
225
+ rush-ai plugin install my-plugin@rush --force --secret SHIMO_TOKEN=new-value
208
226
 
209
227
  # 预览不落盘
210
- rush-ai plugin install rush --dry-run
228
+ rush-ai plugin install my-plugin@rush --dry-run
229
+
230
+ # Anthropic 官方 marketplace(170+ 社区 plugin,自动注册)
231
+ rush-ai plugin install sentry@claude-plugins-official
232
+ rush-ai plugin install aikido@claude-plugins-official --target claude-code
233
+
234
+ # GitHub marketplace 也支持
235
+ rush-ai marketplace add github:kanyun-inc/rush-plugin
236
+ rush-ai plugin install rush
211
237
  ```
212
238
 
213
239
  这条路径和每个 IDE 自己的 `/plugin install` 等价 —— 装完之后 IDE 的 `/plugin list` 能看到、`/plugin uninstall` 也能卸载。不用这条路径就忽略;日常的 `task create` / `task push` 和插件分发完全独立。
package/dist/index.js CHANGED
@@ -4291,6 +4291,7 @@ function registerMcpCommand(program) {
4291
4291
  }
4292
4292
 
4293
4293
  // src/commands/plugin/install.ts
4294
+ import { rm as rm12 } from "fs/promises";
4294
4295
  import { resolve as resolve20 } from "path";
4295
4296
 
4296
4297
  // src/installers/claude-code/installer.ts
@@ -4518,7 +4519,7 @@ var ClaudeCodeInstaller = class {
4518
4519
  const { ref, version } = plugin;
4519
4520
  const pluginVersionDir = this.paths.pluginVersionDir(ref, version);
4520
4521
  const key = pluginKey(ref);
4521
- if (!opts.force) {
4522
+ if (!opts.force && version !== "unknown") {
4522
4523
  let already = false;
4523
4524
  try {
4524
4525
  already = await this.isAlreadyInstalledAtVersion(ref, version);
@@ -5837,7 +5838,7 @@ var CodexInstaller = class {
5837
5838
  };
5838
5839
  }
5839
5840
  const ref = plugin.ref;
5840
- if (!opts.force && await this.isInstalled(ref)) {
5841
+ if (!opts.force && plugin.version !== "unknown" && await this.isInstalled(ref)) {
5841
5842
  const existingVersion = await detectInstalledVersion(this.home, ref);
5842
5843
  if (existingVersion?.version === plugin.version) {
5843
5844
  return {
@@ -6647,8 +6648,8 @@ async function writeFileAtomic(filePath, content) {
6647
6648
  const tmp = `${filePath}.${Math.random().toString(36).slice(2)}.tmp`;
6648
6649
  try {
6649
6650
  await writeFile6(tmp, content, { encoding: "utf8", flag: "w" });
6650
- const { rename: rename9 } = await import("fs/promises");
6651
- await rename9(tmp, filePath);
6651
+ const { rename: rename10 } = await import("fs/promises");
6652
+ await rename10(tmp, filePath);
6652
6653
  } catch (err) {
6653
6654
  await rm7(tmp, { force: true }).catch(() => {
6654
6655
  });
@@ -7141,7 +7142,7 @@ var CursorInstaller = class {
7141
7142
  const { included, skipped } = partitionCapabilities(plugin.capabilities);
7142
7143
  const force = opts.force === true;
7143
7144
  const dryRun = opts.dryRun === true;
7144
- if (!force && !dryRun) {
7145
+ if (!force && !dryRun && plugin.version !== "unknown") {
7145
7146
  const store = await RushRegistryStore.load({ home: this.home });
7146
7147
  const existing = store.get(plugin.ref)?.targets?.cursor;
7147
7148
  if (existing && existing.version === plugin.version) {
@@ -7591,8 +7592,8 @@ async function fileExists(p) {
7591
7592
  }
7592
7593
  }
7593
7594
  async function safeRename(from, to) {
7594
- const { rename: rename9 } = await import("fs/promises");
7595
- await rename9(from, to);
7595
+ const { rename: rename10 } = await import("fs/promises");
7596
+ await rename10(from, to);
7596
7597
  }
7597
7598
  async function classifyArtifactPath(p) {
7598
7599
  let s;
@@ -7830,9 +7831,12 @@ async function appendMigrationFailure(reason, opts = {}) {
7830
7831
 
7831
7832
  // src/migration/migrate.ts
7832
7833
  import { access as access12, readFile as readFile13 } from "fs/promises";
7833
- import { homedir as homedir11 } from "os";
7834
+ import { homedir as homedir12 } from "os";
7834
7835
 
7835
7836
  // src/plugins/resolver.ts
7837
+ import { randomUUID as randomUUID8 } from "crypto";
7838
+ import { mkdir as mkdir11, rename as rename9, rm as rm11 } from "fs/promises";
7839
+ import { homedir as homedir11 } from "os";
7836
7840
  import {
7837
7841
  isAbsolute as isAbsolute5,
7838
7842
  relative as pathRelative2,
@@ -8031,15 +8035,15 @@ var PluginManifestCorruptError = class extends PluginResolverError {
8031
8035
  this.name = "PluginManifestCorruptError";
8032
8036
  }
8033
8037
  };
8034
- var InvalidPluginVersionError = class extends PluginResolverError {
8035
- constructor(pluginName, manifestPath, actualValue) {
8038
+ var PluginCloneFailedError = class extends PluginResolverError {
8039
+ constructor(pluginName, marketplaceName, cause) {
8036
8040
  super(
8037
- `plugin.json at '${manifestPath}' has invalid version field (got ${actualValue === void 0 ? "undefined" : `'${actualValue}'`}). Plugin '${pluginName}' must declare a non-empty version (e.g. "0.1.0"); 'unknown' is not allowed (see #906).`
8041
+ `Failed to clone plugin '${pluginName}' from marketplace '${marketplaceName}': ${cause instanceof Error ? cause.message : String(cause)}`
8038
8042
  );
8039
8043
  this.pluginName = pluginName;
8040
- this.manifestPath = manifestPath;
8041
- this.actualValue = actualValue;
8042
- this.name = "InvalidPluginVersionError";
8044
+ this.marketplaceName = marketplaceName;
8045
+ this.cause = cause;
8046
+ this.name = "PluginCloneFailedError";
8043
8047
  }
8044
8048
  };
8045
8049
 
@@ -8081,13 +8085,8 @@ function parsePluginManifest(pluginName, raw, sourcePathForError) {
8081
8085
  "'name' must be a non-empty string"
8082
8086
  );
8083
8087
  }
8084
- const version = obj.version;
8085
- if (typeof version !== "string" || version.length === 0 || version.toLowerCase() === "unknown") {
8086
- throw new InvalidPluginVersionError(
8087
- pluginName,
8088
- sourcePathForError,
8089
- typeof version === "string" ? version : void 0
8090
- );
8088
+ if (typeof obj.version !== "string" || obj.version.length === 0) {
8089
+ obj.version = "unknown";
8091
8090
  }
8092
8091
  if (obj.mcpServers !== void 0) {
8093
8092
  const m = obj.mcpServers;
@@ -8108,6 +8107,10 @@ var RUSH_AI_MARKETPLACE_NAME3 = "rush-marketplace";
8108
8107
  var RUSH_MCP_SERVER_KEY3 = "rush";
8109
8108
  async function resolvePlugin(ref, marketplace, options = {}) {
8110
8109
  const entry = findPluginEntry(ref, marketplace);
8110
+ await ensurePluginCloned(ref, marketplace, entry, {
8111
+ runner: options.gitRunner,
8112
+ dryRun: options.dryRun
8113
+ });
8111
8114
  const sourceDir = resolvePluginSourceDir(ref, marketplace, entry);
8112
8115
  const manifest = await readPluginManifest(ref.name, sourceDir);
8113
8116
  const capabilities = await scanCapabilities(sourceDir, manifest);
@@ -8137,6 +8140,9 @@ function findPluginEntry(ref, marketplace) {
8137
8140
  return entry;
8138
8141
  }
8139
8142
  function resolvePluginSourceDir(ref, marketplace, entry) {
8143
+ if (hasUrlSource(entry)) {
8144
+ return extractUrlSourceDir(marketplace, entry);
8145
+ }
8140
8146
  const relPath = extractRelativeSourcePath(ref, marketplace, entry);
8141
8147
  if (isAbsolute5(relPath)) {
8142
8148
  throw new PluginSourceUnresolvableError(
@@ -8158,6 +8164,29 @@ function resolvePluginSourceDir(ref, marketplace, entry) {
8158
8164
  }
8159
8165
  return abs;
8160
8166
  }
8167
+ function extractUrlSourceDir(marketplace, entry) {
8168
+ const src = entry.source;
8169
+ const cacheDir = pluginCacheDir(marketplace.name, entry.name);
8170
+ const subPath = typeof src.path === "string" ? src.path.trim() : "";
8171
+ if (!subPath) return cacheDir;
8172
+ if (isAbsolute5(subPath)) {
8173
+ throw new PluginSourceUnresolvableError(
8174
+ entry.name,
8175
+ marketplace.name,
8176
+ `URL source subpath must be relative; got absolute '${subPath}'`
8177
+ );
8178
+ }
8179
+ const resolved = pathResolve2(cacheDir, subPath);
8180
+ const rel = pathRelative2(cacheDir, resolved);
8181
+ if (rel === ".." || rel.startsWith(`..${pathSep}`) || isAbsolute5(rel)) {
8182
+ throw new PluginSourceUnresolvableError(
8183
+ entry.name,
8184
+ marketplace.name,
8185
+ `URL source subpath '${subPath}' escapes plugin cache dir`
8186
+ );
8187
+ }
8188
+ return resolved;
8189
+ }
8161
8190
  function extractRelativeSourcePath(ref, marketplace, entry) {
8162
8191
  if (entry.source === void 0 || entry.source === null) {
8163
8192
  return `plugins/${entry.name}`;
@@ -8227,12 +8256,93 @@ function defaultRushAiBinaryResolver2() {
8227
8256
  }
8228
8257
  return void 0;
8229
8258
  }
8259
+ function pluginCacheDir(marketplaceName, pluginName) {
8260
+ assertSafePathComponent(marketplaceName, "marketplace name");
8261
+ assertSafePathComponent(pluginName, "plugin name");
8262
+ return pathResolve2(
8263
+ homedir11(),
8264
+ ".rush",
8265
+ "plugin-cache",
8266
+ marketplaceName,
8267
+ pluginName
8268
+ );
8269
+ }
8270
+ function assertSafePathComponent(value, label) {
8271
+ if (value.includes("/") || value.includes("\\") || value === ".." || value.startsWith("../") || value.startsWith("..\\")) {
8272
+ throw new PluginSourceUnresolvableError(
8273
+ value,
8274
+ "",
8275
+ `${label} '${value}' contains unsafe path characters`
8276
+ );
8277
+ }
8278
+ }
8279
+ function hasUrlSource(entry) {
8280
+ const src = entry.source;
8281
+ return !!src && typeof src === "object" && !Array.isArray(src) && typeof src.url === "string" && src.url.length > 0;
8282
+ }
8283
+ async function ensurePluginCloned(ref, marketplace, entry, opts) {
8284
+ const src = entry.source;
8285
+ if (!src || typeof src !== "object" || Array.isArray(src)) return;
8286
+ if (typeof src.url !== "string" || !src.url) return;
8287
+ const cacheDir = pluginCacheDir(marketplace.name, entry.name);
8288
+ if (await pathExists2(cacheDir)) return;
8289
+ if (opts?.dryRun) {
8290
+ throw new PluginSourceUnresolvableError(
8291
+ ref.name,
8292
+ marketplace.name,
8293
+ `Plugin '${ref.name}' has not been cached yet. Run without --dry-run first.`
8294
+ );
8295
+ }
8296
+ const git = opts?.runner ?? defaultGitRunner;
8297
+ const parent = pathResolve2(cacheDir, "..");
8298
+ await mkdir11(parent, { recursive: true });
8299
+ const tmpDir = `${cacheDir}.${randomUUID8()}.tmp`;
8300
+ try {
8301
+ const args = ["clone", "--depth", "1"];
8302
+ if (typeof src.ref === "string" && src.ref.length > 0) {
8303
+ args.push("--branch", src.ref);
8304
+ }
8305
+ args.push("--", src.url, tmpDir);
8306
+ const result = await git(args);
8307
+ if (result.exitCode !== 0) {
8308
+ throw new Error(
8309
+ `git clone exited with code ${result.exitCode}: ${result.stderr.trim()}`
8310
+ );
8311
+ }
8312
+ if (typeof src.sha === "string" && src.sha.length > 0) {
8313
+ try {
8314
+ const head = await git(["rev-parse", "HEAD"], { cwd: tmpDir });
8315
+ const actualSha = head.stdout.trim();
8316
+ if (!actualSha.startsWith(src.sha) && !src.sha.startsWith(actualSha)) {
8317
+ output.warn(
8318
+ `Plugin '${ref.name}' sha mismatch (expected ${src.sha.slice(0, 7)}, got ${actualSha.slice(0, 7)})`
8319
+ );
8320
+ }
8321
+ } catch {
8322
+ }
8323
+ }
8324
+ try {
8325
+ await rename9(tmpDir, cacheDir);
8326
+ } catch (err) {
8327
+ if (err.code === "ENOTEMPTY" && await pathExists2(cacheDir)) {
8328
+ await rm11(tmpDir, { recursive: true, force: true });
8329
+ return;
8330
+ }
8331
+ throw err;
8332
+ }
8333
+ } catch (err) {
8334
+ await rm11(tmpDir, { recursive: true, force: true }).catch(() => {
8335
+ });
8336
+ if (err instanceof PluginSourceUnresolvableError) throw err;
8337
+ throw new PluginCloneFailedError(ref.name, marketplace.name, err);
8338
+ }
8339
+ }
8230
8340
 
8231
8341
  // src/migration/migrate.ts
8232
8342
  var LEGACY_MIGRATION_KEY = "legacy-rush-0.7.x";
8233
8343
  var SKIP_MIGRATION_ENV = "RUSH_SKIP_MIGRATION";
8234
8344
  async function maybeRunMigration(input) {
8235
- const home = input.home ?? homedir11();
8345
+ const home = input.home ?? homedir12();
8236
8346
  const now = input.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
8237
8347
  const envGet = input.env ?? ((key) => process.env[key]);
8238
8348
  const reporter = input.reporter ?? {};
@@ -8804,6 +8914,10 @@ async function runInstall(input) {
8804
8914
  const rushSource = parseSource(`rush://${apiHost}`);
8805
8915
  await cache.add(rushSource, { as: ref.marketplace });
8806
8916
  marketplace = await cache.get(ref.marketplace);
8917
+ } else if (ref.marketplace === "claude-plugins-official") {
8918
+ const source = parseSource("github:anthropics/claude-plugins-official");
8919
+ await cache.add(source, { as: "claude-plugins-official" });
8920
+ marketplace = await cache.get("claude-plugins-official");
8807
8921
  } else {
8808
8922
  throw new RushError(
8809
8923
  `Marketplace '${ref.marketplace}' is not in the local cache. Run 'rush-ai marketplace add <source>' first.`,
@@ -8900,15 +9014,24 @@ async function runInstall(input) {
8900
9014
  }
8901
9015
  }
8902
9016
  }
9017
+ if (force && !dryRun) {
9018
+ const entry = marketplace.manifest.plugins.find((p) => p.name === ref.name);
9019
+ if (entry && hasUrlSource(entry)) {
9020
+ await rm12(pluginCacheDir(ref.marketplace, ref.name), {
9021
+ recursive: true,
9022
+ force: true
9023
+ });
9024
+ }
9025
+ }
8903
9026
  const resolveFn = input.resolvePluginFn ?? resolvePlugin;
8904
9027
  let plugin;
8905
9028
  try {
8906
- plugin = await resolveFn(ref, marketplace);
9029
+ plugin = await resolveFn(ref, marketplace, { dryRun });
8907
9030
  } catch (err) {
8908
9031
  if (err instanceof PluginNotFoundInMarketplaceError && marketplace.source.kind === "rush") {
8909
9032
  marketplace = await cache.update(ref.marketplace);
8910
9033
  try {
8911
- plugin = await resolveFn(ref, marketplace);
9034
+ plugin = await resolveFn(ref, marketplace, { dryRun });
8912
9035
  } catch (retryErr) {
8913
9036
  if (retryErr instanceof PluginNotFoundInMarketplaceError) {
8914
9037
  throw new RushError(
@@ -9106,8 +9229,9 @@ function registerInstallCommand(group, _root) {
9106
9229
  function printInstallSummary(result) {
9107
9230
  const { plugin, results, targetExplicit, dryRun } = result;
9108
9231
  const tag = dryRun ? " (dry-run)" : "";
9232
+ const ver = plugin.version !== "unknown" ? ` v${plugin.version}` : "";
9109
9233
  output.log(
9110
- `Installing ${plugin.ref.name}@${plugin.ref.marketplace} v${plugin.version}...${tag}`
9234
+ `Installing ${plugin.ref.name}@${plugin.ref.marketplace}${ver}...${tag}`
9111
9235
  );
9112
9236
  output.newline();
9113
9237
  for (const r of results) {
@@ -9768,7 +9892,7 @@ function registerSkillCommand(program) {
9768
9892
 
9769
9893
  // src/commands/task/index.ts
9770
9894
  import { createWriteStream } from "fs";
9771
- import { mkdir as mkdir11, readFile as readFile14, stat as stat10 } from "fs/promises";
9895
+ import { mkdir as mkdir12, readFile as readFile14, stat as stat10 } from "fs/promises";
9772
9896
  import path3 from "path";
9773
9897
  import { Readable } from "stream";
9774
9898
  import { pipeline } from "stream/promises";
@@ -10136,7 +10260,7 @@ function writeHandoffFile(projectPath, content) {
10136
10260
  var NO_PROJECT_HINT = [
10137
10261
  "No rush project detected for this directory. Either:",
10138
10262
  ' rush-ai task create -a web-builder -p "..." (build a new site from a prompt)',
10139
- " rush-ai task init (link an existing local project)"
10263
+ " rush-ai task link (link an existing local project)"
10140
10264
  ].join("\n");
10141
10265
  function registerPushSubcommand(task, program) {
10142
10266
  task.command("push").description(
@@ -10314,7 +10438,7 @@ function pushWithRouting(projectPath, source, envGitRemote) {
10314
10438
  if (!envRemoteUsable) {
10315
10439
  return {
10316
10440
  success: false,
10317
- stderr: "Cannot push: .rush/env.md identifies a Rush project but has no usable GIT_REMOTE. Re-run `rush-ai task init` to refresh the deploy token, or set `origin` to the Rush GitLab URL."
10441
+ stderr: "Cannot push: .rush/env.md identifies a Rush project but has no usable GIT_REMOTE. Re-run `rush-ai task link` to refresh the deploy token, or set `origin` to the Rush GitLab URL."
10318
10442
  };
10319
10443
  }
10320
10444
  return gitPushUrl(projectPath, envGitRemote);
@@ -11341,7 +11465,7 @@ async function downloadFile(file2, destPath, client) {
11341
11465
  if (!response.body) {
11342
11466
  throw new Error("No response body");
11343
11467
  }
11344
- await mkdir11(path3.dirname(destPath), { recursive: true });
11468
+ await mkdir12(path3.dirname(destPath), { recursive: true });
11345
11469
  const nodeStream = Readable.fromWeb(
11346
11470
  response.body
11347
11471
  );
@@ -11947,8 +12071,8 @@ function registerCommands(program) {
11947
12071
  }
11948
12072
 
11949
12073
  // src/util/update-check.ts
11950
- import { mkdir as mkdir12, readFile as readFile15, writeFile as writeFile9 } from "fs/promises";
11951
- import { homedir as homedir12 } from "os";
12074
+ import { mkdir as mkdir13, readFile as readFile15, writeFile as writeFile9 } from "fs/promises";
12075
+ import { homedir as homedir13 } from "os";
11952
12076
  import { dirname as dirname10, join as join10 } from "path";
11953
12077
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
11954
12078
  var FETCH_TIMEOUT_MS = 3e3;
@@ -11972,13 +12096,13 @@ function isNewerVersion(current, latest) {
11972
12096
  }
11973
12097
  async function writeLastCheck(checkFile) {
11974
12098
  try {
11975
- await mkdir12(dirname10(checkFile), { recursive: true });
12099
+ await mkdir13(dirname10(checkFile), { recursive: true });
11976
12100
  await writeFile9(checkFile, JSON.stringify({ lastCheck: Date.now() }));
11977
12101
  } catch {
11978
12102
  }
11979
12103
  }
11980
12104
  async function checkForUpdate(currentVersion) {
11981
- const rushDir = join10(homedir12(), ".rush");
12105
+ const rushDir = join10(homedir13(), ".rush");
11982
12106
  const checkFile = join10(rushDir, "update-check.json");
11983
12107
  try {
11984
12108
  if (process.env.CI) return;