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.
- package/.agents/workflows/cairn-doctor.md +4 -1
- package/.agents/workflows/cairn-install.md +3 -1
- package/.agents/workflows/cairn-uninstall.md +3 -1
- package/.agents/workflows/cairn-upgrade.md +3 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.de.md +32 -5
- package/README.es.md +32 -5
- package/README.fr.md +32 -5
- package/README.ja.md +32 -5
- package/README.ko.md +34 -8
- package/README.md +34 -8
- package/README.pt.md +32 -5
- package/README.zh.md +32 -5
- package/commands/cairn-doctor.md +4 -1
- package/commands/cairn-install.md +11 -5
- package/commands/cairn-uninstall.md +9 -4
- package/commands/cairn-upgrade.md +8 -1
- package/package.json +2 -2
- package/scripts/cairn-auto-upgrade.mjs +334 -0
- package/scripts/cairn-lifecycle-claude.mjs +64 -18
- package/scripts/cairn-lifecycle-commands.mjs +355 -79
- package/scripts/cairn-lifecycle-hosts.mjs +98 -0
- package/scripts/cairn-lifecycle-integrity.mjs +106 -33
- package/scripts/cairn-lifecycle-layout.mjs +69 -0
- package/scripts/cairn-lifecycle-staging.mjs +45 -30
- package/scripts/cairn-lifecycle-transaction-journal.mjs +50 -7
- package/scripts/cairn-lifecycle.mjs +148 -45
- package/scripts/cairn-paths.mjs +12 -0
- package/scripts/cairn-state-context.mjs +25 -0
- package/scripts/cairn-state.mjs +54 -21
- package/scripts/cairn-update-state.mjs +219 -0
- package/scripts/cairn.mjs +13 -8
|
@@ -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
|
|
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,
|
|
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,
|
|
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
|
|
40
|
+
locatorPath, runtimeRoot, includeLocatorNotice: true,
|
|
41
41
|
});
|
|
42
|
-
await writeLocator(join(skillStage, "references", "cairn-runtime.json"),
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
|
102
|
-
&& typeof entry.path === "string" && resolve(entry.path) === resolve(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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 };
|