cairn-ai 0.2.8 → 0.2.9
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/.codex-plugin/plugin.json +1 -1
- package/README.ko.md +4 -2
- package/README.md +4 -2
- package/commands/cairn-install.md +1 -0
- package/package.json +1 -1
- package/scripts/cairn-lifecycle-claude.mjs +142 -0
- package/scripts/cairn-lifecycle-commands.mjs +41 -3
- package/scripts/cairn-lifecycle-integrity.mjs +15 -2
- package/scripts/cairn-lifecycle-staging.mjs +9 -2
- package/scripts/cairn-lifecycle.mjs +5 -2
package/README.ko.md
CHANGED
|
@@ -109,7 +109,7 @@ cairn uninstall
|
|
|
109
109
|
cairn toolcheck
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
-
- `cairn install`: custom marketplace source, versioned Codex runtime cache, Cairn-owned config section, Claude Code mirror
|
|
112
|
+
- `cairn install`: custom marketplace source, versioned Codex runtime cache, Cairn-owned config section, Claude Code mirror와 플러그인, 현재 Antigravity IDE/CLI skill 표면을 transaction으로 설치합니다.
|
|
113
113
|
- `cairn upgrade`: ownership manifest의 installed digest와 현재 값이 같은 항목만 교체하며, staged validation과 역순 rollback을 적용합니다.
|
|
114
114
|
- `cairn doctor`: ownership manifest, managed digest, 유효 Codex feature, 실제 plugin installed/enabled/version, mirror/runtime locator를 수정 없이 진단합니다.
|
|
115
115
|
- `cairn uninstall`: 먼저 config 외 모든 ownership manifest 항목을 설치 digest와 대조하고, 수정되거나 삭제된 managed artifact가 하나라도 있으면 아무것도 바꾸지 않고 uninstall 전체를 거부합니다. 그렇지 않으면 managed 항목을 제거·복원하고 빈 managed cache scaffold를 정리하며, unmanaged target과 그 상위 디렉터리는 보존하고 commit 전 실패는 rollback합니다.
|
|
@@ -129,7 +129,9 @@ cairn toolcheck
|
|
|
129
129
|
|
|
130
130
|
Clean uninstall은 모든 managed scaffold가 비었을 때만 recursive 삭제 없이 빈 디렉터리 제거로 Cairn marketplace cache root를 정리합니다. 소스 저장소와 저장소의 `MEMORY.md`/`PLAN.md`, 사용자 홈 Cairn goal 상태, 전역 `cairn-ai` package, package-manager download cache, legacy backup, 현재 ownership manifest 밖의 legacy shared setting은 지우지 않으며 별도의 명시적 정리 판단이 필요합니다.
|
|
131
131
|
|
|
132
|
-
Codex는 `skills/`와 `commands/`를 사용합니다. Claude Code는
|
|
132
|
+
Codex는 `skills/`와 `commands/`를 사용합니다. Claude Code는 `~/.claude/` 아래의 mirror command·agent definition과 Claude Code 플러그인 `cairn@cairn`을 사용합니다. Antigravity는 `.agents/workflows`와 global skills mirror를 사용합니다.
|
|
133
|
+
|
|
134
|
+
Claude Code 플러그인은 Cairn이 소유한 로컬 marketplace `~/.codex/plugins/cache/cairn/claude`에 생성되며, Cairn skill 4개와 SessionStart·UserPromptSubmit·PostToolUse·Stop·SubagentStop hook을 담습니다. hook은 설치된 버전 런타임을 절대경로로 호출합니다. 파일 commit 뒤 `cairn install`/`upgrade`가 `claude plugin marketplace add`와 `claude plugin install cairn@cairn --scope user`로 등록하므로 다음 세션부터 Claude Code 터미널·데스크톱 앱·IDE 플러그인 목록에 표시됩니다. `claude` CLI가 없으면 등록을 건너뛰고 수동 명령을 안내하며, 이미 `cairn`이라는 다른 marketplace가 있으면 건드리지 않습니다. `cairn uninstall`은 파일 제거 전에 Cairn 소유 marketplace만 등록 해제하고, uninstall이 rollback되면 다시 등록합니다. `cairn doctor`는 `Claude plugin files`, `Claude plugin registered`, `Claude plugin enabled`를 보고합니다.
|
|
133
135
|
|
|
134
136
|
## Antigravity Compatibility
|
|
135
137
|
|
package/README.md
CHANGED
|
@@ -117,7 +117,7 @@ cairn uninstall
|
|
|
117
117
|
cairn toolcheck
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
-
- `cairn install`: transactionally installs the custom marketplace source, versioned Codex runtime cache, Cairn-owned config sections, Claude Code mirrors, and current Antigravity IDE/CLI skill surfaces.
|
|
120
|
+
- `cairn install`: transactionally installs the custom marketplace source, versioned Codex runtime cache, Cairn-owned config sections, Claude Code mirrors and plugin, and current Antigravity IDE/CLI skill surfaces.
|
|
121
121
|
- `cairn upgrade`: replaces only ownership-manifest entries whose installed digests still match, with staged validation and reverse-order rollback on failure.
|
|
122
122
|
- `cairn doctor`: validates the ownership manifest, managed digests, effective Codex features, actual installed/enabled/version plugin state, and mirror/runtime locators without repairing them.
|
|
123
123
|
- `cairn uninstall`: verifies every non-config ownership-manifest entry against its installed digest first and refuses the whole uninstall, changing nothing, if any managed artifact was modified or removed. Otherwise it removes or restores the managed entries, prunes empty managed cache scaffolds, preserves unmanaged targets and their parent directories, and rolls back pre-commit failures.
|
|
@@ -137,7 +137,9 @@ Install and upgrade keep the custom marketplace lifecycle; they do not invoke `c
|
|
|
137
137
|
|
|
138
138
|
Clean uninstall removes the Cairn marketplace cache root only when every managed scaffold is empty, using empty-directory removal rather than recursive deletion. It does not delete the source repository, repository `MEMORY.md`/`PLAN.md`, user-home Cairn goal state, the global `cairn-ai` package, package-manager download caches, legacy backups, or legacy shared settings outside the current ownership manifest; those require a separate explicit cleanup decision.
|
|
139
139
|
|
|
140
|
-
Codex uses `skills/` and `commands/`. Claude Code uses mirrored commands and agent definitions under
|
|
140
|
+
Codex uses `skills/` and `commands/`. Claude Code uses mirrored commands and agent definitions under `~/.claude/` plus a Claude Code plugin, `cairn@cairn`. Antigravity uses `.agents/workflows` and global skills mirrors.
|
|
141
|
+
|
|
142
|
+
The Claude Code plugin is generated in a Cairn-owned local marketplace at `~/.codex/plugins/cache/cairn/claude` and carries the four Cairn skills and the SessionStart, UserPromptSubmit, PostToolUse, Stop, and SubagentStop hooks, which call the installed versioned runtime by absolute path. After the files commit, `cairn install`/`upgrade` registers it through `claude plugin marketplace add` and `claude plugin install cairn@cairn --scope user`, so it appears in the Claude Code terminal, desktop app, and IDE plugin lists from the next session. Registration is skipped with a manual command when the `claude` CLI is missing, and a different marketplace already named `cairn` is never touched. `cairn uninstall` unregisters only Cairn's own marketplace before removing files, and re-registers it if the uninstall rolls back. `cairn doctor` reports `Claude plugin files`, `Claude plugin registered`, and `Claude plugin enabled`.
|
|
141
143
|
|
|
142
144
|
## Antigravity Compatibility
|
|
143
145
|
|
|
@@ -16,6 +16,7 @@ Actions:
|
|
|
16
16
|
- Add the `hooks/hooks.json` link to the installed manifest.
|
|
17
17
|
- Register only Cairn-owned marketplace, plugin, and hook trust sections in `~/.codex/config.toml`; do not rewrite public feature or agent settings.
|
|
18
18
|
- Install `cairn-*` commands and agents mirror files for Claude Code.
|
|
19
|
+
- Generate the Claude Code plugin `cairn@cairn` (four skills plus hooks that call the installed runtime) in the Cairn-owned local marketplace, then register it with `claude plugin marketplace add` and `claude plugin install cairn@cairn --scope user` after the files commit. Skip with a manual command when the `claude` CLI is missing, and never touch a different marketplace named `cairn`.
|
|
19
20
|
- Install `cairn-*` skills and workflows mirror files for Antigravity IDE/CLI.
|
|
20
21
|
- Write runtime locators so all mirrors resolve scripts, templates, and model guidance from the installed plugin root.
|
|
21
22
|
- Record every current managed destination and digest in the ownership manifest under the custom marketplace, and rollback committed phases in reverse order on failure.
|
package/package.json
CHANGED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { cp, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { quoteShellArg, renderMirrorFile, writeLocator } from "./cairn-lifecycle-mirror.mjs";
|
|
5
|
+
|
|
6
|
+
// Claude Code loads a plugin's default hooks/hooks.json and merges any manifest hooks, and it only expands
|
|
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
|
|
9
|
+
// existing ~/.claude mirrors so they are not listed twice.
|
|
10
|
+
export async function stageClaudePlugin({
|
|
11
|
+
stageRoot, runtimeStage, destinationRoot, versionedPluginRoot, marketplaceName, pluginName,
|
|
12
|
+
releaseVersion, skillNames, platform = process.platform,
|
|
13
|
+
}) {
|
|
14
|
+
const staged = join(stageRoot, "claude-plugin");
|
|
15
|
+
const pluginDir = join(staged, pluginName);
|
|
16
|
+
const packageJson = JSON.parse(await readFile(join(runtimeStage, "package.json"), "utf8"));
|
|
17
|
+
const description = packageJson.description ?? "Cairn agent harness";
|
|
18
|
+
|
|
19
|
+
await writeJson(join(staged, ".claude-plugin", "marketplace.json"), {
|
|
20
|
+
name: marketplaceName,
|
|
21
|
+
description: "Local Cairn marketplace managed by `cairn install`",
|
|
22
|
+
owner: { name: packageJson.author?.name ?? "Cairn" },
|
|
23
|
+
plugins: [{ name: pluginName, source: `./${pluginName}`, description, version: releaseVersion }],
|
|
24
|
+
});
|
|
25
|
+
await writeJson(join(pluginDir, ".claude-plugin", "plugin.json"), {
|
|
26
|
+
name: pluginName,
|
|
27
|
+
version: releaseVersion,
|
|
28
|
+
description,
|
|
29
|
+
author: { name: packageJson.author?.name ?? "Cairn" },
|
|
30
|
+
...(packageJson.homepage ? { homepage: packageJson.homepage } : {}),
|
|
31
|
+
...(packageJson.license ? { license: packageJson.license } : {}),
|
|
32
|
+
});
|
|
33
|
+
await writeJson(join(pluginDir, "hooks", "hooks.json"), await claudeHooks(runtimeStage, versionedPluginRoot, platform));
|
|
34
|
+
|
|
35
|
+
for (const name of skillNames) {
|
|
36
|
+
const skillStage = join(pluginDir, "skills", name);
|
|
37
|
+
await cp(join(runtimeStage, "skills", name), skillStage, { recursive: true });
|
|
38
|
+
const locatorPath = join(destinationRoot, pluginName, "skills", name, "references", "cairn-runtime.json");
|
|
39
|
+
await renderMirrorFile(join(runtimeStage, "skills", name, "SKILL.md"), join(skillStage, "SKILL.md"), {
|
|
40
|
+
locatorPath, runtimeRoot: versionedPluginRoot, includeLocatorNotice: true,
|
|
41
|
+
});
|
|
42
|
+
await writeLocator(join(skillStage, "references", "cairn-runtime.json"), versionedPluginRoot);
|
|
43
|
+
}
|
|
44
|
+
return staged;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function claudeHooks(runtimeStage, versionedPluginRoot, platform) {
|
|
48
|
+
const source = JSON.parse(await readFile(join(runtimeStage, "hooks", "hooks.json"), "utf8"));
|
|
49
|
+
const stateScript = quoteShellArg(join(versionedPluginRoot, "scripts", "cairn-state.mjs"), platform);
|
|
50
|
+
const hooks = {};
|
|
51
|
+
for (const [eventName, groups] of Object.entries(source.hooks ?? {})) {
|
|
52
|
+
hooks[eventName] = groups.map((group) => ({
|
|
53
|
+
...group,
|
|
54
|
+
hooks: (group.hooks ?? []).map((handler) => {
|
|
55
|
+
const event = String(handler.command ?? "").match(/cairn-state\.mjs"?\s+([a-z-]+)\s*$/)?.[1];
|
|
56
|
+
if (!event) throw new Error(`Unsupported Cairn hook command for Claude Code: ${handler.command}`);
|
|
57
|
+
return { ...handler, command: `node ${stateScript} ${event}` };
|
|
58
|
+
}),
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
return { hooks };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function writeJson(path, value) {
|
|
65
|
+
await mkdir(dirname(path), { recursive: true });
|
|
66
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Registration goes through the documented `claude plugin` CLI. CLAUDE_CONFIG_DIR is only set when the caller
|
|
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 }) {
|
|
72
|
+
const cli = process.env.CAIRN_CLAUDE_CLI ?? "claude";
|
|
73
|
+
const pluginId = `${pluginName}@${marketplaceName}`;
|
|
74
|
+
const env = claudeHomeOverridden ? { ...process.env, CLAUDE_CONFIG_DIR: claudeHome } : process.env;
|
|
75
|
+
|
|
76
|
+
// A .mjs/.js override runs through the current Node binary so tests can supply a portable stand-in CLI.
|
|
77
|
+
function run(args) {
|
|
78
|
+
if (/\.m?js$/.test(cli)) return spawnSync(process.execPath, [cli, ...args], { encoding: "utf8", env, timeout: 120_000 });
|
|
79
|
+
return spawnSync(cli, args, { encoding: "utf8", env, timeout: 120_000, shell: process.platform === "win32" });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function available() {
|
|
83
|
+
if (cli === "none") return false;
|
|
84
|
+
const result = run(["--version"]);
|
|
85
|
+
return !result.error && result.status === 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function json(args) {
|
|
89
|
+
const result = run([...args, "--json"]);
|
|
90
|
+
if (result.error || result.status !== 0) {
|
|
91
|
+
throw new Error(`claude ${args.join(" ")} failed: ${(result.stderr || result.stdout || result.error?.message || "").trim().split("\n").at(-1)}`);
|
|
92
|
+
}
|
|
93
|
+
const text = result.stdout.trim();
|
|
94
|
+
try {
|
|
95
|
+
return JSON.parse(text);
|
|
96
|
+
} catch {
|
|
97
|
+
return JSON.parse(text.split("\n").filter(Boolean).at(-1));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const ownsMarketplace = (entry) => entry?.name === marketplaceName
|
|
102
|
+
&& typeof entry.path === "string" && resolve(entry.path) === resolve(marketplaceRoot);
|
|
103
|
+
const ownedPlugins = (list) => (Array.isArray(list) ? list : [])
|
|
104
|
+
.filter((entry) => entry?.id === pluginId && entry.scope === "user");
|
|
105
|
+
|
|
106
|
+
function status({ checked = false } = {}) {
|
|
107
|
+
if (!checked && !available()) return { available: false, marketplace: false, installed: false, enabled: false };
|
|
108
|
+
const marketplace = json(["plugin", "marketplace", "list"]).find((entry) => entry?.name === marketplaceName) ?? null;
|
|
109
|
+
const plugin = ownedPlugins(json(["plugin", "list"]))[0] ?? null;
|
|
110
|
+
return {
|
|
111
|
+
available: true,
|
|
112
|
+
marketplace: ownsMarketplace(marketplace),
|
|
113
|
+
foreignMarketplace: Boolean(marketplace) && !ownsMarketplace(marketplace),
|
|
114
|
+
installed: Boolean(plugin),
|
|
115
|
+
enabled: Boolean(plugin?.enabled),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function register() {
|
|
120
|
+
if (!available()) return { registered: false, reason: "claude CLI not found" };
|
|
121
|
+
const current = status({ checked: true });
|
|
122
|
+
if (current.foreignMarketplace) {
|
|
123
|
+
return { registered: false, reason: `a different Claude Code marketplace named "${marketplaceName}" already exists` };
|
|
124
|
+
}
|
|
125
|
+
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 };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function unregister() {
|
|
132
|
+
if (!available()) return { unregistered: false, reason: "claude CLI not found" };
|
|
133
|
+
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" };
|
|
136
|
+
if (current.installed) json(["plugin", "uninstall", pluginId, "--scope", "user"]);
|
|
137
|
+
json(["plugin", "marketplace", "remove", marketplaceName]);
|
|
138
|
+
return { unregistered: true };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { available, register, status, unregister, pluginId };
|
|
142
|
+
}
|
|
@@ -5,11 +5,12 @@ import { hasSetting, readSharedConfigSnapshot, removeCairnConfig, splitSections
|
|
|
5
5
|
import {
|
|
6
6
|
installedSkillLocatorPath, validRuntimeLocator, validateAntigravitySkills,
|
|
7
7
|
} from "./cairn-lifecycle-mirror.mjs";
|
|
8
|
+
import { createClaudeRegistration } from "./cairn-lifecycle-claude.mjs";
|
|
8
9
|
|
|
9
10
|
export function createCommands(context) {
|
|
10
11
|
const {
|
|
11
|
-
antigravityCliHome, antigravityHome,
|
|
12
|
-
codexHome, configPath, installedPluginRoot, installedRuntimeLocatorPath,
|
|
12
|
+
antigravityCliHome, antigravityHome, claudeHome, claudeHomeOverridden, claudePluginMarketplaceRoot,
|
|
13
|
+
claudeRuntimeLocatorPath, codexHome, marketplaceName, configPath, installedPluginRoot, installedRuntimeLocatorPath,
|
|
13
14
|
marketplaceJsonPath, ownershipPath, phases, pluginName, pluginRoot,
|
|
14
15
|
pruneUninstallScaffolds, releaseVersion, samePath, skillNames, t, transaction,
|
|
15
16
|
versionedPluginRoot, staging, integrity,
|
|
@@ -19,6 +20,9 @@ export function createCommands(context) {
|
|
|
19
20
|
removeManagedTarget, replaceTarget, rollback,
|
|
20
21
|
} = transaction;
|
|
21
22
|
const { legacyMigrationTargets, stageInstall } = staging;
|
|
23
|
+
const claudeRegistration = createClaudeRegistration({
|
|
24
|
+
claudeHome, claudeHomeOverridden, marketplaceRoot: claudePluginMarketplaceRoot, marketplaceName, pluginName,
|
|
25
|
+
});
|
|
22
26
|
const {
|
|
23
27
|
adoptLegacy, anyManagedArtifactExists, assertOwnershipDigests,
|
|
24
28
|
assertTargetsReplaceable, ownershipDigestsValid, readOwnership, targetDigest,
|
|
@@ -80,6 +84,24 @@ async function install(mode) {
|
|
|
80
84
|
if (transaction.state !== "committed") await rollback(transaction);
|
|
81
85
|
throw error;
|
|
82
86
|
}
|
|
87
|
+
reportClaudeRegistration();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Files are committed first; registering with Claude Code is idempotent, and a failure leaves the files
|
|
91
|
+
// installed with a manual command to finish, instead of rolling back a working Codex/Antigravity install.
|
|
92
|
+
function reportClaudeRegistration() {
|
|
93
|
+
const manual = `claude plugin marketplace add ${JSON.stringify(claudePluginMarketplaceRoot)} && claude plugin install ${claudeRegistration.pluginId} --scope user`;
|
|
94
|
+
try {
|
|
95
|
+
const result = claudeRegistration.register();
|
|
96
|
+
if (result.registered) {
|
|
97
|
+
console.log(`Claude Code plugin: ${claudeRegistration.pluginId} (${claudePluginMarketplaceRoot})`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
console.log(`Claude Code plugin not registered: ${result.reason}. To register: ${manual}`);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.log(`Claude Code plugin registration failed: ${error.message}. To register: ${manual}`);
|
|
103
|
+
process.exitCode = 1;
|
|
104
|
+
}
|
|
83
105
|
}
|
|
84
106
|
|
|
85
107
|
async function uninstall() {
|
|
@@ -92,6 +114,8 @@ async function uninstall() {
|
|
|
92
114
|
if (record.type !== "config" && current !== record.installedDigest) conflicts.push(record.path);
|
|
93
115
|
}
|
|
94
116
|
if (conflicts.length > 0) throw new Error(`Refusing to uninstall modified managed artifacts: ${conflicts.join(", ")}`);
|
|
117
|
+
// Unregister before removing files so Claude Code never points at a deleted plugin; a failure aborts with nothing changed.
|
|
118
|
+
const unregistered = claudeRegistration.unregister().unregistered;
|
|
95
119
|
const transaction = await createTransaction();
|
|
96
120
|
try {
|
|
97
121
|
const configRecord = ownership.targets.find((record) => record.type === "config");
|
|
@@ -114,7 +138,13 @@ async function uninstall() {
|
|
|
114
138
|
await finishTransaction(transaction);
|
|
115
139
|
injectFailure("uninstall-after-commit");
|
|
116
140
|
} catch (error) {
|
|
117
|
-
if (transaction.state !== "committed")
|
|
141
|
+
if (transaction.state !== "committed") {
|
|
142
|
+
await rollback(transaction);
|
|
143
|
+
// The rollback restored the plugin files, so restore the Claude Code registration that preceded them.
|
|
144
|
+
if (unregistered) {
|
|
145
|
+
try { claudeRegistration.register(); } catch (registrationError) { console.error(`Claude Code plugin re-registration failed: ${registrationError.message}`); }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
118
148
|
throw error;
|
|
119
149
|
}
|
|
120
150
|
await pruneUninstallScaffolds();
|
|
@@ -141,6 +171,14 @@ async function doctor() {
|
|
|
141
171
|
checks.push(["config marketplace", splitSections(config).some((section) => section.header === "marketplaces.cairn")]);
|
|
142
172
|
checks.push(["config plugin enabled", hasSetting(config, 'plugins."cairn@cairn"', "enabled", "true")]);
|
|
143
173
|
checks.push(["Claude runtime locator", await validRuntimeLocator(claudeRuntimeLocatorPath, versionedPluginRoot)]);
|
|
174
|
+
const claudePluginManifest = await readJson(join(claudePluginMarketplaceRoot, pluginName, ".claude-plugin", "plugin.json"));
|
|
175
|
+
checks.push(["Claude plugin files", claudePluginManifest?.version === releaseVersion
|
|
176
|
+
&& await exists(join(claudePluginMarketplaceRoot, ".claude-plugin", "marketplace.json"))
|
|
177
|
+
&& await exists(join(claudePluginMarketplaceRoot, pluginName, "hooks", "hooks.json"))]);
|
|
178
|
+
let claudeStatus = null;
|
|
179
|
+
try { claudeStatus = claudeRegistration.status(); } catch { claudeStatus = null; }
|
|
180
|
+
checks.push(["Claude plugin registered", Boolean(claudeStatus?.marketplace && claudeStatus.installed)]);
|
|
181
|
+
checks.push(["Claude plugin enabled", Boolean(claudeStatus?.enabled)]);
|
|
144
182
|
checks.push(["Antigravity IDE skills", await exists(join(antigravityHome, "skills", "cairn-plan", "SKILL.md"))]);
|
|
145
183
|
checks.push(["Antigravity CLI flat skills", await exists(join(antigravityCliHome, "skills", "cairn-plan.md"))]);
|
|
146
184
|
|
|
@@ -7,7 +7,7 @@ import { antigravityRuntimeLocatorPath } from "./cairn-lifecycle-mirror.mjs";
|
|
|
7
7
|
|
|
8
8
|
export function createIntegrity(context) {
|
|
9
9
|
const {
|
|
10
|
-
antigravityCliHome, antigravityHome, claudeHome, claudeRuntimeLocatorPath,
|
|
10
|
+
antigravityCliHome, antigravityHome, claudeHome, claudePluginMarketplaceRoot, claudeRuntimeLocatorPath,
|
|
11
11
|
commandNames, configPath, installedPluginRoot, legacyAntigravityHome,
|
|
12
12
|
marketplaceJsonPath, marketplaceRoot, ownershipPath, phases, pluginName,
|
|
13
13
|
releaseVersion, skillNames,
|
|
@@ -97,13 +97,25 @@ function validateOwnershipShape(ownership, { required = false } = {}) {
|
|
|
97
97
|
paths.add(record.path);
|
|
98
98
|
}
|
|
99
99
|
const requiredTargets = requiredOwnershipTargets(ownership.version);
|
|
100
|
-
|
|
100
|
+
const optionalIds = new Set(requiredTargets.has("claude-plugin") ? [] : ["claude-plugin"]);
|
|
101
|
+
if ([...ids].some((id) => !requiredTargets.has(id) && !optionalIds.has(id)) || [...requiredTargets.keys()].some((id) => !ids.has(id))) {
|
|
101
102
|
throw new Error("Ownership manifest does not contain the complete managed target set.");
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
const CLAUDE_PLUGIN_SINCE = "0.2.9";
|
|
107
|
+
|
|
108
|
+
function compareVersions(left, right) {
|
|
109
|
+
const parse = (value) => String(value).split("-")[0].split(".").map(Number);
|
|
110
|
+
const [a, b] = [parse(left), parse(right)];
|
|
111
|
+
for (let index = 0; index < 3; index += 1) if (a[index] !== b[index]) return a[index] - b[index];
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
105
115
|
function requiredOwnershipTargets(version = releaseVersion) {
|
|
106
116
|
const ids = ["codex-source", "codex-runtime", "marketplace", "claude-runtime", "ide-runtime", "cli-runtime", "codex-config"];
|
|
117
|
+
// Manifests written before the Claude Code plugin existed must stay valid so those installs can upgrade.
|
|
118
|
+
if (compareVersions(version, CLAUDE_PLUGIN_SINCE) >= 0) ids.push("claude-plugin");
|
|
107
119
|
for (const name of commandNames()) ids.push(`claude-command-${name}`, `ide-workflow-${name}`, `cli-workflow-${name}`);
|
|
108
120
|
for (const name of ["explorer", "worker"]) ids.push(`claude-agent-${name}`);
|
|
109
121
|
for (const name of skillNames()) ids.push(`ide-skill-${name}`, `cli-skill-${name}`);
|
|
@@ -116,6 +128,7 @@ function expectedManagedTarget(id, version = releaseVersion) {
|
|
|
116
128
|
"codex-runtime": { phase: "codex", path: versionedRuntimeRoot(version), type: "tree" },
|
|
117
129
|
marketplace: { phase: "codex", path: marketplaceJsonPath, type: "file" },
|
|
118
130
|
"claude-runtime": { phase: "claude", path: claudeRuntimeLocatorPath, type: "file" },
|
|
131
|
+
"claude-plugin": { phase: "claude", path: claudePluginMarketplaceRoot, type: "tree" },
|
|
119
132
|
"ide-runtime": { phase: "antigravity", path: antigravityRuntimeLocatorPath(antigravityHome), type: "file" },
|
|
120
133
|
"cli-runtime": { phase: "antigravity", path: antigravityRuntimeLocatorPath(antigravityCliHome), type: "file" },
|
|
121
134
|
"legacy-ide-runtime": { phase: "antigravity", path: antigravityRuntimeLocatorPath(legacyAntigravityHome), type: "file" },
|
|
@@ -4,11 +4,12 @@ import { readSharedConfigSnapshot, trustedHookStates } from "./cairn-lifecycle-c
|
|
|
4
4
|
import {
|
|
5
5
|
antigravityRuntimeLocatorPath, antigravitySkillLocatorPath, renderMirrorFile, writeLocator,
|
|
6
6
|
} from "./cairn-lifecycle-mirror.mjs";
|
|
7
|
+
import { stageClaudePlugin } from "./cairn-lifecycle-claude.mjs";
|
|
7
8
|
|
|
8
9
|
export function createStaging(context) {
|
|
9
10
|
const {
|
|
10
|
-
agentNames, antigravityCliHome, antigravityHome, claudeHome,
|
|
11
|
-
claudeRuntimeLocatorPath, commandNames, configPath, installedPluginRoot,
|
|
11
|
+
agentNames, antigravityCliHome, antigravityHome, claudeHome, claudePluginMarketplaceRoot,
|
|
12
|
+
claudeRuntimeLocatorPath, commandNames, releaseVersion, configPath, installedPluginRoot,
|
|
12
13
|
legacyAntigravityHome, marketplaceJsonPath, marketplaceName, pluginName,
|
|
13
14
|
pluginRoot, skillNames, updateConfig, versionedPluginRoot, assertRegularTree,
|
|
14
15
|
} = context;
|
|
@@ -44,6 +45,12 @@ async function stageInstall(stageRoot) {
|
|
|
44
45
|
add(`claude-agent-${name}`, "claude", join(claudeHome, "agents", `cairn-${name}.md`), staged, "file");
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
const claudePluginStage = await stageClaudePlugin({
|
|
49
|
+
stageRoot, runtimeStage, destinationRoot: claudePluginMarketplaceRoot, versionedPluginRoot,
|
|
50
|
+
marketplaceName, pluginName, releaseVersion, skillNames: skillNames(),
|
|
51
|
+
});
|
|
52
|
+
add("claude-plugin", "claude", claudePluginMarketplaceRoot, claudePluginStage);
|
|
53
|
+
|
|
47
54
|
await stageAntigravity(targets, stageRoot, runtimeStage, antigravityHome, "ide", false);
|
|
48
55
|
await stageAntigravity(targets, stageRoot, runtimeStage, antigravityCliHome, "cli", true);
|
|
49
56
|
|
|
@@ -30,6 +30,7 @@ export {
|
|
|
30
30
|
const pluginRoot = resolvePluginRoot(import.meta.url);
|
|
31
31
|
const homeRoot = process.env.HOME ?? process.env.USERPROFILE ?? homedir() ?? ".";
|
|
32
32
|
const codexHome = resolve(process.env.CODEX_HOME ?? join(homeRoot, ".codex"));
|
|
33
|
+
const claudeHomeOverridden = typeof process.env.CLAUDE_HOME === "string" && process.env.CLAUDE_HOME.length > 0;
|
|
33
34
|
const claudeHome = resolve(process.env.CLAUDE_HOME ?? join(homeRoot, ".claude"));
|
|
34
35
|
const antigravityHome = resolve(process.env.ANTIGRAVITY_HOME ?? join(homeRoot, ".gemini", "config"));
|
|
35
36
|
const legacyAntigravityHome = resolve(process.env.CAIRN_LEGACY_ANTIGRAVITY_HOME ?? join(homeRoot, ".agents"));
|
|
@@ -45,6 +46,7 @@ const transactionPath = join(marketplaceRoot, ".cairn", "transaction.json");
|
|
|
45
46
|
const transactionRoot = join(marketplaceRoot, ".cairn", "transactions");
|
|
46
47
|
const lifecycleLockPath = join(marketplaceRoot, ".cairn", "lifecycle.lock");
|
|
47
48
|
const claudeRuntimeLocatorPath = join(claudeHome, "cairn", "runtime.json");
|
|
49
|
+
const claudePluginMarketplaceRoot = join(marketplaceRoot, "claude");
|
|
48
50
|
const sourceManifest = JSON.parse(await readFile(join(pluginRoot, ".codex-plugin", "plugin.json"), "utf8"));
|
|
49
51
|
const releaseVersion = sourceManifest.version;
|
|
50
52
|
const versionedPluginRoot = join(marketplaceRoot, pluginName, releaseVersion);
|
|
@@ -53,7 +55,8 @@ const phases = ["codex", "claude", "antigravity", "config", "cleanup"];
|
|
|
53
55
|
const MAX_LIFECYCLE_LOCK_TIMEOUT_MS = 2_147_483_647;
|
|
54
56
|
|
|
55
57
|
const lifecycleContext = {
|
|
56
|
-
agentNames, antigravityCliHome, antigravityHome, claudeHome,
|
|
58
|
+
agentNames, antigravityCliHome, antigravityHome, claudeHome, claudeHomeOverridden,
|
|
59
|
+
claudePluginMarketplaceRoot, claudeRuntimeLocatorPath,
|
|
57
60
|
codexHome, commandNames, configPath, installedPluginRoot, installedRuntimeLocatorPath,
|
|
58
61
|
legacyAntigravityHome, lifecycleLockPath, marketplaceJsonPath, marketplaceName,
|
|
59
62
|
marketplaceRoot, ownershipPath, phases, pluginName, pluginRoot, releaseVersion,
|
|
@@ -105,7 +108,7 @@ function lifecycleLockTimeout() {
|
|
|
105
108
|
async function assertLifecycleMutationPaths() {
|
|
106
109
|
const roots = [
|
|
107
110
|
[codexHome, lifecycleLockPath], [codexHome, installedPluginRoot], [codexHome, versionedPluginRoot],
|
|
108
|
-
[codexHome, marketplaceJsonPath], [isInside(codexHome, configPath) ? codexHome : dirname(configPath), configPath],
|
|
111
|
+
[codexHome, marketplaceJsonPath], [codexHome, claudePluginMarketplaceRoot], [isInside(codexHome, configPath) ? codexHome : dirname(configPath), configPath],
|
|
109
112
|
[claudeHome, join(claudeHome, "commands")], [claudeHome, join(claudeHome, "agents")],
|
|
110
113
|
[claudeHome, claudeRuntimeLocatorPath], [antigravityHome, join(antigravityHome, "skills")],
|
|
111
114
|
...skillNames().map((name) => [antigravityHome, join(antigravityHome, "skills", name)]),
|