cairn-ai 0.6.0 → 0.7.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.
@@ -5,10 +5,10 @@ import { quoteShellArg, renderMirrorFile, writeLocator } from "./cairn-lifecycle
5
5
 
6
6
  // Claude Code loads a plugin's default hooks/hooks.json and merges any manifest hooks, and it only expands
7
7
  // ${CLAUDE_PLUGIN_ROOT}. The shared package hooks use Codex's ${PLUGIN_ROOT}, so Claude gets a separate thin
8
- // plugin whose hooks call the installed versioned runtime by absolute path. Commands and agents stay in the
8
+ // plugin whose hooks call the installed canonical runtime by absolute path. Commands and agents stay in the
9
9
  // existing ~/.claude mirrors so they are not listed twice.
10
10
  export async function stageClaudePlugin({
11
- stageRoot, runtimeStage, destinationRoot, versionedPluginRoot, marketplaceName, pluginName,
11
+ stageRoot, runtimeStage, destinationRoot, runtimeRoot, marketplaceName, pluginName,
12
12
  releaseVersion, skillNames, platform = process.platform,
13
13
  }) {
14
14
  const staged = join(stageRoot, "claude-plugin");
@@ -30,23 +30,23 @@ export async function stageClaudePlugin({
30
30
  ...(packageJson.homepage ? { homepage: packageJson.homepage } : {}),
31
31
  ...(packageJson.license ? { license: packageJson.license } : {}),
32
32
  });
33
- await writeJson(join(pluginDir, "hooks", "hooks.json"), await claudeHooks(runtimeStage, versionedPluginRoot, platform));
33
+ await writeJson(join(pluginDir, "hooks", "hooks.json"), await claudeHooks(runtimeStage, runtimeRoot, platform));
34
34
 
35
35
  for (const name of skillNames) {
36
36
  const skillStage = join(pluginDir, "skills", name);
37
37
  await cp(join(runtimeStage, "skills", name), skillStage, { recursive: true });
38
38
  const locatorPath = join(destinationRoot, pluginName, "skills", name, "references", "cairn-runtime.json");
39
39
  await renderMirrorFile(join(runtimeStage, "skills", name, "SKILL.md"), join(skillStage, "SKILL.md"), {
40
- locatorPath, runtimeRoot: versionedPluginRoot, includeLocatorNotice: true,
40
+ locatorPath, runtimeRoot, includeLocatorNotice: true,
41
41
  });
42
- await writeLocator(join(skillStage, "references", "cairn-runtime.json"), versionedPluginRoot);
42
+ await writeLocator(join(skillStage, "references", "cairn-runtime.json"), runtimeRoot);
43
43
  }
44
44
  return staged;
45
45
  }
46
46
 
47
- async function claudeHooks(runtimeStage, versionedPluginRoot, platform) {
47
+ async function claudeHooks(runtimeStage, runtimeRoot, platform) {
48
48
  const source = JSON.parse(await readFile(join(runtimeStage, "hooks", "hooks.json"), "utf8"));
49
- const stateScript = quoteShellArg(join(versionedPluginRoot, "scripts", "cairn-state.mjs"), platform);
49
+ const stateScript = quoteShellArg(join(runtimeRoot, "scripts", "cairn-state.mjs"), platform);
50
50
  const hooks = {};
51
51
  for (const [eventName, groups] of Object.entries(source.hooks ?? {})) {
52
52
  hooks[eventName] = groups.map((group) => ({
@@ -68,7 +68,11 @@ async function writeJson(path, value) {
68
68
 
69
69
  // Registration goes through the documented `claude plugin` CLI. CLAUDE_CONFIG_DIR is only set when the caller
70
70
  // overrode the Claude home, because setting it for the default home would move Claude's global config file.
71
- export function createClaudeRegistration({ claudeHome, claudeHomeOverridden, marketplaceRoot, marketplaceName, pluginName }) {
71
+ // `legacyMarketplaceRoot` is the 0.6.0-layout marketplace directory under the Codex cache; a registration that
72
+ // points there is Cairn's own and is moved to `marketplaceRoot`, while any other directory stays foreign.
73
+ export function createClaudeRegistration({
74
+ claudeHome, claudeHomeOverridden, marketplaceRoot, legacyMarketplaceRoot, marketplaceName, pluginName, releaseVersion,
75
+ }) {
72
76
  const cli = process.env.CAIRN_CLAUDE_CLI ?? "claude";
73
77
  const pluginId = `${pluginName}@${marketplaceName}`;
74
78
  const env = claudeHomeOverridden ? { ...process.env, CLAUDE_CONFIG_DIR: claudeHome } : process.env;
@@ -98,8 +102,10 @@ export function createClaudeRegistration({ claudeHome, claudeHomeOverridden, mar
98
102
  }
99
103
  }
100
104
 
101
- const ownsMarketplace = (entry) => entry?.name === marketplaceName
102
- && typeof entry.path === "string" && resolve(entry.path) === resolve(marketplaceRoot);
105
+ const pointsAt = (entry, root) => Boolean(root) && entry?.name === marketplaceName
106
+ && typeof entry.path === "string" && resolve(entry.path) === resolve(root);
107
+ const ownsMarketplace = (entry) => pointsAt(entry, marketplaceRoot);
108
+ const ownsLegacyMarketplace = (entry) => pointsAt(entry, legacyMarketplaceRoot);
103
109
  const ownedPlugins = (list) => (Array.isArray(list) ? list : [])
104
110
  .filter((entry) => entry?.id === pluginId && entry.scope === "user");
105
111
 
@@ -110,29 +116,69 @@ export function createClaudeRegistration({ claudeHome, claudeHomeOverridden, mar
110
116
  return {
111
117
  available: true,
112
118
  marketplace: ownsMarketplace(marketplace),
113
- foreignMarketplace: Boolean(marketplace) && !ownsMarketplace(marketplace),
119
+ ownedLegacy: ownsLegacyMarketplace(marketplace),
120
+ foreignMarketplace: Boolean(marketplace) && !ownsMarketplace(marketplace) && !ownsLegacyMarketplace(marketplace),
114
121
  installed: Boolean(plugin),
115
122
  enabled: Boolean(plugin?.enabled),
123
+ version: plugin?.version,
116
124
  };
117
125
  }
118
126
 
119
127
  function register() {
120
- if (!available()) return { registered: false, reason: "claude CLI not found" };
121
- const current = status({ checked: true });
128
+ if (!available()) return { registered: false, cliMissing: true, reason: "claude CLI not found" };
129
+ let current = status({ checked: true });
122
130
  if (current.foreignMarketplace) {
123
131
  return { registered: false, reason: `a different Claude Code marketplace named "${marketplaceName}" already exists` };
124
132
  }
133
+ if (current.ownedLegacy) {
134
+ // The files already moved to marketplaceRoot, so the old registration points at a removed directory.
135
+ try {
136
+ if (current.installed) json(["plugin", "uninstall", pluginId, "--scope", "user"]);
137
+ json(["plugin", "marketplace", "remove", marketplaceName]);
138
+ } catch (error) {
139
+ error.legacyMarketplace = true;
140
+ throw error;
141
+ }
142
+ current = { ...current, ownedLegacy: false, installed: false, enabled: false, version: undefined };
143
+ }
125
144
  if (!current.marketplace) json(["plugin", "marketplace", "add", marketplaceRoot, "--scope", "user"]);
126
- if (!current.installed) json(["plugin", "install", pluginId, "--scope", "user"]);
127
- else if (!current.enabled) json(["plugin", "enable", pluginId, "--scope", "user"]);
128
- return { registered: true };
145
+ // A fresh install copies the current marketplace version into Claude Code's plugin cache.
146
+ if (!current.installed) {
147
+ json(["plugin", "install", pluginId, "--scope", "user"]);
148
+ return { registered: true };
149
+ }
150
+ if (!current.enabled) json(["plugin", "enable", pluginId, "--scope", "user"]);
151
+ if (current.version === releaseVersion) return { registered: true };
152
+ return { registered: true, cacheRefresh: refreshCache(current.version) };
153
+ }
154
+
155
+ // An existing install keeps serving its cached copy until `plugin update` replaces it. The update is only
156
+ // requested when the cached version differs, so re-running install on a current cache stays a no-op.
157
+ function refreshCache(cachedVersion) {
158
+ try {
159
+ json(["plugin", "marketplace", "update", marketplaceName]);
160
+ } catch {
161
+ // A directory marketplace needs no refresh; `plugin update` below still reads the current files.
162
+ }
163
+ try {
164
+ json(["plugin", "update", pluginId, "--scope", "user"]);
165
+ // Without a reported version there is nothing to verify; the update itself is the best available signal.
166
+ if (cachedVersion === undefined) return { refreshed: true };
167
+ const version = ownedPlugins(json(["plugin", "list"]))[0]?.version;
168
+ if (version !== releaseVersion) {
169
+ return { refreshed: false, reason: `${pluginId} is still at ${version ?? "an unknown version"} (expected ${releaseVersion})` };
170
+ }
171
+ return { refreshed: true };
172
+ } catch (error) {
173
+ return { refreshed: false, reason: error.message };
174
+ }
129
175
  }
130
176
 
131
177
  function unregister() {
132
178
  if (!available()) return { unregistered: false, reason: "claude CLI not found" };
133
179
  const current = status({ checked: true });
134
- // Only remove a registration that points at Cairn's own marketplace directory.
135
- if (!current.marketplace) return { unregistered: false, reason: "no Cairn-owned Claude Code marketplace is registered" };
180
+ // Only remove a registration that points at Cairn's own marketplace directory (current or 0.6.0 layout).
181
+ if (!current.marketplace && !current.ownedLegacy) return { unregistered: false, reason: "no Cairn-owned Claude Code marketplace is registered" };
136
182
  if (current.installed) json(["plugin", "uninstall", pluginId, "--scope", "user"]);
137
183
  json(["plugin", "marketplace", "remove", marketplaceName]);
138
184
  return { unregistered: true };