@comento/pota-harness 0.0.0-stage → 1.0.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/pota-doctor.md +16 -0
- package/.agents/workflows/pota-install.md +15 -0
- package/.agents/workflows/pota-memory.md +17 -0
- package/.agents/workflows/pota-plan.md +22 -0
- package/.agents/workflows/pota-review.md +16 -0
- package/.agents/workflows/pota-toolcheck.md +9 -0
- package/.agents/workflows/pota-uninstall.md +9 -0
- package/.agents/workflows/pota-upgrade.md +15 -0
- package/.agents/workflows/pota-work.md +24 -0
- package/.claude/agents/explorer.md +7 -0
- package/.claude/agents/worker.md +7 -0
- package/.claude/commands/pota-doctor.md +5 -0
- package/.claude/commands/pota-install.md +9 -0
- package/.claude/commands/pota-memory.md +5 -0
- package/.claude/commands/pota-plan.md +5 -0
- package/.claude/commands/pota-review.md +5 -0
- package/.claude/commands/pota-toolcheck.md +7 -0
- package/.claude/commands/pota-uninstall.md +5 -0
- package/.claude/commands/pota-upgrade.md +9 -0
- package/.claude/commands/pota-work.md +7 -0
- package/.codex-plugin/plugin.json +33 -0
- package/LICENSE +21 -0
- package/README.de.md +166 -0
- package/README.es.md +166 -0
- package/README.fr.md +166 -0
- package/README.ja.md +166 -0
- package/README.ko.md +184 -0
- package/README.md +191 -2
- package/README.pt.md +166 -0
- package/README.zh.md +166 -0
- package/agents/explorer.md +36 -0
- package/agents/worker.md +45 -0
- package/commands/pota-doctor.md +28 -0
- package/commands/pota-install.md +29 -0
- package/commands/pota-memory.md +17 -0
- package/commands/pota-plan.md +22 -0
- package/commands/pota-review.md +16 -0
- package/commands/pota-toolcheck.md +17 -0
- package/commands/pota-uninstall.md +27 -0
- package/commands/pota-upgrade.md +26 -0
- package/commands/pota-work.md +24 -0
- package/docs/model-guidance/README.md +67 -0
- package/docs/model-guidance/claude.md +49 -0
- package/docs/model-guidance/codex.md +68 -0
- package/hooks/hooks.json +65 -0
- package/package.json +47 -4
- package/scripts/pota +14 -0
- package/scripts/pota-auto-upgrade.mjs +342 -0
- package/scripts/pota-cleanup.mjs +194 -0
- package/scripts/pota-goal-cli.mjs +337 -0
- package/scripts/pota-goal-evidence.mjs +186 -0
- package/scripts/pota-goal-history.mjs +316 -0
- package/scripts/pota-goal-operations-evidence.mjs +213 -0
- package/scripts/pota-goal-operations-mutations.mjs +97 -0
- package/scripts/pota-goal-operations.mjs +281 -0
- package/scripts/pota-goal-policy.mjs +62 -0
- package/scripts/pota-goal-state-validation.mjs +361 -0
- package/scripts/pota-goal-state.mjs +136 -0
- package/scripts/pota-goal.mjs +69 -0
- package/scripts/pota-index.mjs +1052 -0
- package/scripts/pota-lifecycle-claude.mjs +173 -0
- package/scripts/pota-lifecycle-commands.mjs +418 -0
- package/scripts/pota-lifecycle-config.mjs +153 -0
- package/scripts/pota-lifecycle-hosts.mjs +98 -0
- package/scripts/pota-lifecycle-integrity.mjs +272 -0
- package/scripts/pota-lifecycle-layout.mjs +52 -0
- package/scripts/pota-lifecycle-lock.mjs +205 -0
- package/scripts/pota-lifecycle-mirror.mjs +83 -0
- package/scripts/pota-lifecycle-staging.mjs +156 -0
- package/scripts/pota-lifecycle-transaction-journal.mjs +244 -0
- package/scripts/pota-lifecycle-transaction-targets.mjs +119 -0
- package/scripts/pota-lifecycle-transaction.mjs +12 -0
- package/scripts/pota-lifecycle.mjs +244 -0
- package/scripts/pota-paths.mjs +190 -0
- package/scripts/pota-safe-fs.mjs +277 -0
- package/scripts/pota-state-context.mjs +281 -0
- package/scripts/pota-state-hook.mjs +26 -0
- package/scripts/pota-state-init.mjs +80 -0
- package/scripts/pota-state.mjs +245 -0
- package/scripts/pota-toolcheck-detect.mjs +229 -0
- package/scripts/pota-toolcheck-runtime.mjs +178 -0
- package/scripts/pota-toolcheck.mjs +221 -0
- package/scripts/pota-update-state.mjs +219 -0
- package/scripts/pota.mjs +200 -0
- package/skills/pota-memory/SKILL.md +52 -0
- package/skills/pota-plan/SKILL.md +66 -0
- package/skills/pota-plan/delegation.md +65 -0
- package/skills/pota-plan/heavy-path.md +28 -0
- package/skills/pota-plan/planning-rules.md +78 -0
- package/skills/pota-review/SKILL.md +56 -0
- package/skills/pota-work/SKILL.md +71 -0
- package/skills/pota-work/delegation.md +65 -0
- package/skills/pota-work/heavy-path.md +18 -0
- package/skills/pota-work/recovery.md +29 -0
- package/skills/pota-work/verification.md +79 -0
- package/templates/MEMORY.md +12 -0
- package/templates/PLAN.md +9 -0
- package/templates/domain-memory.md +22 -0
- package/templates/work-plan.md +177 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { lstat, mkdir, readFile, readdir, rename, rm, rmdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { hostname } from "node:os";
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_LIFECYCLE_LOCK_TIMEOUT_MS = 60_000;
|
|
7
|
+
|
|
8
|
+
export function createLifecycleLock({ lifecycleLockPath }) {
|
|
9
|
+
async function withLifecycleLock(operation, {
|
|
10
|
+
timeoutMs = DEFAULT_LIFECYCLE_LOCK_TIMEOUT_MS,
|
|
11
|
+
retryMs = 25,
|
|
12
|
+
malformedStaleMs = 30_000,
|
|
13
|
+
afterRelease,
|
|
14
|
+
testHooks = {},
|
|
15
|
+
} = {}) {
|
|
16
|
+
await mkdir(dirname(lifecycleLockPath), { recursive: true });
|
|
17
|
+
if (process.env.POTA_TEST_PRUNE_LOCK_PARENT_BEFORE_ACQUIRE === "1") await rmdir(dirname(lifecycleLockPath));
|
|
18
|
+
const owner = { schemaVersion: 1, pid: process.pid, hostname: hostname(), nonce: randomUUID(), acquiredAt: new Date().toISOString() };
|
|
19
|
+
const deadline = Date.now() + timeoutMs;
|
|
20
|
+
let waitReported = false;
|
|
21
|
+
while (true) {
|
|
22
|
+
let acquired = false;
|
|
23
|
+
try {
|
|
24
|
+
await writeFile(lifecycleLockPath, `${JSON.stringify(owner)}\n`, { flag: "wx" });
|
|
25
|
+
await testHooks.afterLockWritten?.({ owner: structuredClone(owner) });
|
|
26
|
+
if (process.env.POTA_TEST_REPLACE_LOCK_AFTER_ACQUIRE === "1") {
|
|
27
|
+
await writeFile(lifecycleLockPath, `${JSON.stringify({ ...owner, nonce: randomUUID() })}\n`);
|
|
28
|
+
}
|
|
29
|
+
const written = parseLifecycleLock(await readFile(lifecycleLockPath, "utf8"));
|
|
30
|
+
if (written?.nonce !== owner.nonce) throw new Error("Pota lifecycle lock ownership changed after acquisition");
|
|
31
|
+
acquired = !(await activeReclaimClaimExists());
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error?.code === "ENOENT") {
|
|
34
|
+
if (Date.now() >= deadline) throw new Error(`Timed out waiting for Pota lifecycle lock after ${timeoutMs}ms`);
|
|
35
|
+
await mkdir(dirname(lifecycleLockPath), { recursive: true });
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (error?.code !== "EEXIST") throw error;
|
|
39
|
+
if (!waitReported && process.env.POTA_TEST_REPORT_LOCK_WAIT === "1") {
|
|
40
|
+
console.log("POTA_TEST_LOCK_WAIT");
|
|
41
|
+
waitReported = true;
|
|
42
|
+
}
|
|
43
|
+
await testHooks.beforeContendedLockConfirmation?.();
|
|
44
|
+
acquired = await confirmsUnclaimedLifecycleLockOwnership(owner.nonce);
|
|
45
|
+
if (!acquired && await reclaimLifecycleLock(owner, malformedStaleMs, testHooks)) continue;
|
|
46
|
+
}
|
|
47
|
+
if (acquired) {
|
|
48
|
+
if (process.env.POTA_TEST_HOLD_LOCK_STDIN === "1") {
|
|
49
|
+
console.log("POTA_TEST_LOCK_ACQUIRED");
|
|
50
|
+
await new Promise((resolvePromise) => process.stdin.once("data", resolvePromise));
|
|
51
|
+
}
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
if (Date.now() >= deadline) throw new Error(`Timed out waiting for Pota lifecycle lock after ${timeoutMs}ms`);
|
|
55
|
+
await delay(Math.min(retryMs, Math.max(1, deadline - Date.now())));
|
|
56
|
+
}
|
|
57
|
+
let operationSucceeded = false;
|
|
58
|
+
try {
|
|
59
|
+
const result = await operation();
|
|
60
|
+
operationSucceeded = true;
|
|
61
|
+
return result;
|
|
62
|
+
} finally {
|
|
63
|
+
if (process.env.POTA_TEST_REPLACE_LOCK_BEFORE_RELEASE === "1") {
|
|
64
|
+
await writeFile(lifecycleLockPath, `${JSON.stringify({ ...owner, nonce: randomUUID() })}\n`);
|
|
65
|
+
}
|
|
66
|
+
const released = await releaseOwnedLock(lifecycleLockPath, owner);
|
|
67
|
+
if (operationSucceeded && released && afterRelease) await afterRelease();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function releaseOwnedLock(path, owner) {
|
|
72
|
+
let current;
|
|
73
|
+
try {
|
|
74
|
+
current = parseLifecycleLock(await readFile(path, "utf8"));
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error?.code === "ENOENT") return false;
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
if (current?.nonce !== owner.nonce) return false;
|
|
80
|
+
const releasedPath = `${path}.released.${owner.nonce}`;
|
|
81
|
+
try {
|
|
82
|
+
await rename(path, releasedPath);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (error?.code === "ENOENT") return false;
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
const released = parseLifecycleLock(await readFile(releasedPath, "utf8"));
|
|
88
|
+
if (released?.nonce !== owner.nonce) {
|
|
89
|
+
if (!(await exists(path))) await rename(releasedPath, path);
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
await rm(releasedPath, { force: true });
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function reclaimLifecycleLock(owner, malformedStaleMs, testHooks) {
|
|
97
|
+
let observed;
|
|
98
|
+
let info;
|
|
99
|
+
try {
|
|
100
|
+
[observed, info] = await Promise.all([readFile(lifecycleLockPath, "utf8"), lstat(lifecycleLockPath)]);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (error?.code === "ENOENT") return true;
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
if (info.isSymbolicLink()) throw new Error(`Lifecycle lock must not be a symlink: ${lifecycleLockPath}`);
|
|
106
|
+
const record = parseLifecycleLock(observed);
|
|
107
|
+
if (!isReclaimableLifecycleLock(record, info, malformedStaleMs)) return false;
|
|
108
|
+
await testHooks.afterStaleObserved?.();
|
|
109
|
+
|
|
110
|
+
const claimPath = `${lifecycleLockPath}.reclaim.${owner.nonce}`;
|
|
111
|
+
await writeFile(claimPath, `${JSON.stringify(owner)}\n`, { flag: "wx" });
|
|
112
|
+
try {
|
|
113
|
+
await testHooks.afterReclaimInterlock?.();
|
|
114
|
+
let claimed;
|
|
115
|
+
let claimedInfo;
|
|
116
|
+
try {
|
|
117
|
+
[claimed, claimedInfo] = await Promise.all([readFile(lifecycleLockPath, "utf8"), lstat(lifecycleLockPath)]);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error?.code === "ENOENT") return true;
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
if (claimedInfo.isSymbolicLink()) throw new Error(`Lifecycle lock must not be a symlink: ${lifecycleLockPath}`);
|
|
123
|
+
const claimedRecord = parseLifecycleLock(claimed);
|
|
124
|
+
if (!isReclaimableLifecycleLock(claimedRecord, claimedInfo, malformedStaleMs)) return false;
|
|
125
|
+
|
|
126
|
+
const quarantine = `${lifecycleLockPath}.stale.${claimedRecord?.nonce ?? "malformed"}.${randomUUID()}`;
|
|
127
|
+
try {
|
|
128
|
+
await rename(lifecycleLockPath, quarantine);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (error?.code === "ENOENT") return true;
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
if (await readFile(quarantine, "utf8") !== claimed) {
|
|
134
|
+
if (!(await exists(lifecycleLockPath))) await rename(quarantine, lifecycleLockPath);
|
|
135
|
+
throw new Error("Pota lifecycle lock ownership changed during stale recovery");
|
|
136
|
+
}
|
|
137
|
+
await rm(quarantine, { force: true });
|
|
138
|
+
return true;
|
|
139
|
+
} finally {
|
|
140
|
+
await releaseOwnedLock(claimPath, owner);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isReclaimableLifecycleLock(record, info, malformedStaleMs) {
|
|
145
|
+
if (record) return record.hostname === hostname() && !pidIsAlive(record.pid);
|
|
146
|
+
return Date.now() - info.mtimeMs > malformedStaleMs;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function confirmsUnclaimedLifecycleLockOwnership(nonce) {
|
|
150
|
+
if (await activeReclaimClaimExists()) return false;
|
|
151
|
+
try {
|
|
152
|
+
return parseLifecycleLock(await readFile(lifecycleLockPath, "utf8"))?.nonce === nonce;
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (error?.code === "ENOENT") return false;
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function activeReclaimClaimExists() {
|
|
160
|
+
const claimNames = (await readdir(dirname(lifecycleLockPath)))
|
|
161
|
+
.filter((name) => name.startsWith(`${basename(lifecycleLockPath)}.reclaim.`));
|
|
162
|
+
for (const name of claimNames) {
|
|
163
|
+
const claimPath = join(dirname(lifecycleLockPath), name);
|
|
164
|
+
let info;
|
|
165
|
+
let record;
|
|
166
|
+
try {
|
|
167
|
+
[info, record] = await Promise.all([lstat(claimPath), readFile(claimPath, "utf8").then(parseLifecycleLock)]);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (error?.code === "ENOENT") continue;
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
if (info.isSymbolicLink()) throw new Error(`Lifecycle reclaim claim must not be a symlink: ${claimPath}`);
|
|
173
|
+
if (record?.hostname === hostname() && !pidIsAlive(record.pid)) {
|
|
174
|
+
await releaseOwnedLock(claimPath, record);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function parseLifecycleLock(text) {
|
|
183
|
+
try {
|
|
184
|
+
const value = JSON.parse(text);
|
|
185
|
+
if (value?.schemaVersion !== 1 || !Number.isSafeInteger(value.pid) || value.pid <= 0
|
|
186
|
+
|| typeof value.hostname !== "string" || value.hostname.length === 0 || typeof value.nonce !== "string" || value.nonce.length === 0
|
|
187
|
+
|| typeof value.acquiredAt !== "string" || Number.isNaN(Date.parse(value.acquiredAt))) return null;
|
|
188
|
+
return value;
|
|
189
|
+
} catch { return null; }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function pidIsAlive(pid) {
|
|
193
|
+
try { process.kill(pid, 0); return true; } catch (error) { return error?.code !== "ESRCH"; }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function delay(ms) { return new Promise((resolvePromise) => setTimeout(resolvePromise, ms)); }
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
return { withLifecycleLock };
|
|
200
|
+
|
|
201
|
+
async function exists(path) {
|
|
202
|
+
try { await lstat(path); return true; }
|
|
203
|
+
catch (error) { if (error?.code === "ENOENT") return false; throw error; }
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { lstat, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, posix, resolve, win32 } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
RUNTIME_LOCATOR_SCHEMA_VERSION,
|
|
5
|
+
createRuntimeLocator,
|
|
6
|
+
runtimeRequiredPaths,
|
|
7
|
+
} from "./pota-paths.mjs";
|
|
8
|
+
|
|
9
|
+
export function antigravityRuntimeLocatorPath(root) {
|
|
10
|
+
return join(root, "pota", "runtime.json");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function installedSkillLocatorPath(runtimeRoot, name) {
|
|
14
|
+
return join(runtimeRoot, "skills", name, "references", "pota-runtime.json");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function antigravitySkillLocatorPath(root, name) {
|
|
18
|
+
return join(root, "skills", name, "references", "pota-runtime.json");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function writeLocator(path, root) {
|
|
22
|
+
await mkdir(dirname(path), { recursive: true });
|
|
23
|
+
await writeFile(path, `${JSON.stringify(createRuntimeLocator(root), null, 2)}\n`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function validRuntimeLocator(path, runtimeRoot) {
|
|
27
|
+
try {
|
|
28
|
+
const locator = JSON.parse(await readFile(path, "utf8"));
|
|
29
|
+
const expected = createRuntimeLocator(runtimeRoot);
|
|
30
|
+
if (locator?.schemaVersion !== RUNTIME_LOCATOR_SCHEMA_VERSION || resolve(locator.pluginRoot ?? ".") !== resolve(runtimeRoot)) return false;
|
|
31
|
+
for (const [name, value] of Object.entries(expected.entrypoints)) if (locator.entrypoints?.[name] !== value) return false;
|
|
32
|
+
for (const [name, value] of Object.entries(expected.resources)) if (locator.resources?.[name] !== value) return false;
|
|
33
|
+
return (await Promise.all(runtimeRequiredPaths(locator).map(exists))).every(Boolean);
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function renderMirrorFile(source, destination, { locatorPath, runtimeRoot, includeLocatorNotice = false }) {
|
|
40
|
+
const content = await readFile(source, "utf8");
|
|
41
|
+
const notice = includeLocatorNotice ? `\n\n## Installed Pota runtime\n\nRead the structured runtime locator at \`${JSON.stringify(locatorPath)}\`. Resolve Pota scripts and static resources from its absolute paths, never from the target project.\n` : "";
|
|
42
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
43
|
+
await writeFile(destination, `${renderInstalledMirror(content, { runtimeRoot, locatorPath }).trimEnd()}${notice}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function renderInstalledMirror(content, { runtimeRoot, locatorPath, platform = process.platform }) {
|
|
47
|
+
const platformJoin = platform === "win32" ? win32.join : posix.join;
|
|
48
|
+
const quotedCli = quoteShellArg(platformJoin(runtimeRoot, "scripts", "pota.mjs"), platform);
|
|
49
|
+
let rendered = content.replaceAll("{{POTA_RUNTIME_LOCATOR_JSON}}", JSON.stringify(locatorPath))
|
|
50
|
+
.replace(/\bnode\s+scripts\/pota\.mjs\b/g, `node ${quotedCli}`)
|
|
51
|
+
.replace(/\bnode\s+scripts\/([A-Za-z0-9._/-]+)/g, (_, path) => `node ${quoteShellArg(platformJoin(runtimeRoot, "scripts", path), platform)}`);
|
|
52
|
+
rendered = rendered.replace(/(^|[\s`("'=])(commands|agents|templates|docs\/model-guidance)\/([A-Za-z0-9._<>/-]+)/gm,
|
|
53
|
+
(_, prefix, directory, suffix) => `${prefix}${platformJoin(runtimeRoot, directory, suffix)}`);
|
|
54
|
+
return rendered;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function quoteShellArg(value, platform = process.platform) {
|
|
58
|
+
return platform === "win32" ? `"${value.replaceAll('"', '""')}"` : `'${value.replaceAll("'", "'\\''")}'`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function validateAntigravitySkills(root, skillNames, runtimeRoot) {
|
|
62
|
+
try {
|
|
63
|
+
if (!(await validRuntimeLocator(antigravityRuntimeLocatorPath(root), runtimeRoot))) return false;
|
|
64
|
+
for (const name of skillNames) {
|
|
65
|
+
const content = await readFile(join(root, "skills", `${name}.md`), "utf8");
|
|
66
|
+
if (!/^---\r?\n[\s\S]*?\r?\n---\r?\n/.test(content) || !/^name:\s*\S+/m.test(content) || !/^description:\s*\S+/m.test(content)) return false;
|
|
67
|
+
if (!content.includes(JSON.stringify(antigravityRuntimeLocatorPath(root)))) return false;
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function exists(path) {
|
|
76
|
+
try {
|
|
77
|
+
await lstat(path);
|
|
78
|
+
return true;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (error?.code === "ENOENT") return false;
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { copyFile, cp, lstat, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
3
|
+
import { readSharedConfigSnapshot, trustedHookStates } from "./pota-lifecycle-config.mjs";
|
|
4
|
+
import {
|
|
5
|
+
antigravityRuntimeLocatorPath, antigravitySkillLocatorPath, renderMirrorFile, writeLocator,
|
|
6
|
+
} from "./pota-lifecycle-mirror.mjs";
|
|
7
|
+
import { stageClaudePlugin } from "./pota-lifecycle-claude.mjs";
|
|
8
|
+
import { HOSTS } from "./pota-lifecycle-hosts.mjs";
|
|
9
|
+
|
|
10
|
+
export function createStaging(context) {
|
|
11
|
+
const {
|
|
12
|
+
agentNames, antigravityCliHome, antigravityHome, claudeHome, claudePluginMarketplaceRoot,
|
|
13
|
+
claudeRuntimeLocatorPath, codexRuntimeRoot, commandNames, releaseVersion, configPath, installedPluginRoot,
|
|
14
|
+
marketplaceJsonPath, marketplaceName, pluginName,
|
|
15
|
+
pluginRoot, runtimeRoot, skillNames, updateConfig, assertRegularTree,
|
|
16
|
+
} = context;
|
|
17
|
+
// The canonical runtime is always staged; each selected host adds its own targets (see pota-lifecycle-hosts.mjs).
|
|
18
|
+
async function stageInstall(stageRoot, hosts = HOSTS) {
|
|
19
|
+
const targets = [];
|
|
20
|
+
const add = (id, phase, destination, staged, type = "tree", details = {}) => targets.push({ id, phase, path: destination, staged, type, ...details });
|
|
21
|
+
// Every copy carries locators to the canonical runtime, so mirrors and hooks never depend on a host cache.
|
|
22
|
+
const runtimeStage = join(stageRoot, "runtime");
|
|
23
|
+
await copyPluginCandidate(runtimeStage, runtimeRoot);
|
|
24
|
+
add("runtime", "core", runtimeRoot, runtimeStage);
|
|
25
|
+
if (hosts.includes("codex")) await stageCodex(stageRoot, add);
|
|
26
|
+
if (hosts.includes("claude")) await stageClaude(stageRoot, runtimeStage, add);
|
|
27
|
+
if (hosts.includes("antigravity")) {
|
|
28
|
+
await stageAntigravity(targets, stageRoot, runtimeStage, antigravityHome, "ide", false);
|
|
29
|
+
await stageAntigravity(targets, stageRoot, runtimeStage, antigravityCliHome, "cli", true);
|
|
30
|
+
}
|
|
31
|
+
if (hosts.includes("codex")) {
|
|
32
|
+
const configStage = join(stageRoot, "config.toml");
|
|
33
|
+
const hookStates = await trustedHookStates(runtimeStage);
|
|
34
|
+
const configSnapshot = await readSharedConfigSnapshot(configPath);
|
|
35
|
+
await writeFile(configStage, updateConfig(configSnapshot.text, hookStates));
|
|
36
|
+
add("codex-config", "config", configPath, configStage, "config", { configSourceDigest: configSnapshot.digest });
|
|
37
|
+
}
|
|
38
|
+
for (const target of targets) await assertRegularTree(target.staged);
|
|
39
|
+
return targets;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function stageCodex(stageRoot, add) {
|
|
43
|
+
const sourceStage = join(stageRoot, "codex-source");
|
|
44
|
+
const codexRuntimeStage = join(stageRoot, "codex-runtime");
|
|
45
|
+
await copyPluginCandidate(sourceStage, runtimeRoot);
|
|
46
|
+
await copyPluginCandidate(codexRuntimeStage, runtimeRoot);
|
|
47
|
+
add("codex-source", "codex", installedPluginRoot, sourceStage);
|
|
48
|
+
add("codex-runtime", "codex", codexRuntimeRoot, codexRuntimeStage);
|
|
49
|
+
|
|
50
|
+
const marketplaceStage = join(stageRoot, "marketplace.json");
|
|
51
|
+
await writeFile(marketplaceStage, `${JSON.stringify({
|
|
52
|
+
name: marketplaceName,
|
|
53
|
+
interface: { displayName: "Pota" },
|
|
54
|
+
plugins: [{ name: pluginName, source: { source: "local", path: "./plugins/pota" }, policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" }, category: "Productivity" }],
|
|
55
|
+
}, null, 2)}\n`);
|
|
56
|
+
add("marketplace", "codex", marketplaceJsonPath, marketplaceStage, "file");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function stageClaude(stageRoot, runtimeStage, add) {
|
|
60
|
+
const claudeLocator = join(stageRoot, "claude-runtime.json");
|
|
61
|
+
await writeLocator(claudeLocator, runtimeRoot);
|
|
62
|
+
add("claude-runtime", "claude", claudeRuntimeLocatorPath, claudeLocator, "file");
|
|
63
|
+
for (const name of commandNames()) {
|
|
64
|
+
const staged = join(stageRoot, "claude-commands", `pota-${name}.md`);
|
|
65
|
+
await renderMirrorFile(join(runtimeStage, ".claude", "commands", `pota-${name}.md`), staged, { locatorPath: claudeRuntimeLocatorPath, runtimeRoot });
|
|
66
|
+
add(`claude-command-${name}`, "claude", join(claudeHome, "commands", `pota-${name}.md`), staged, "file");
|
|
67
|
+
}
|
|
68
|
+
for (const name of agentNames()) {
|
|
69
|
+
const staged = join(stageRoot, "claude-agents", `pota-${name}.md`);
|
|
70
|
+
await renderMirrorFile(join(runtimeStage, ".claude", "agents", `${name}.md`), staged, { locatorPath: claudeRuntimeLocatorPath, runtimeRoot });
|
|
71
|
+
add(`claude-agent-${name}`, "claude", join(claudeHome, "agents", `pota-${name}.md`), staged, "file");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const claudePluginStage = await stageClaudePlugin({
|
|
75
|
+
stageRoot, runtimeStage, destinationRoot: claudePluginMarketplaceRoot, runtimeRoot,
|
|
76
|
+
marketplaceName, pluginName, releaseVersion, skillNames: skillNames(),
|
|
77
|
+
});
|
|
78
|
+
add("claude-plugin", "claude", claudePluginMarketplaceRoot, claudePluginStage);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function stageAntigravity(targets, stageRoot, runtimeStage, destinationRoot, label, flatSkills) {
|
|
82
|
+
const locatorPath = antigravityRuntimeLocatorPath(destinationRoot);
|
|
83
|
+
const locatorStage = join(stageRoot, `${label}-runtime.json`);
|
|
84
|
+
await writeLocator(locatorStage, runtimeRoot);
|
|
85
|
+
targets.push({ id: `${label}-runtime`, phase: "antigravity", path: locatorPath, staged: locatorStage, type: "file" });
|
|
86
|
+
for (const name of skillNames()) {
|
|
87
|
+
if (flatSkills) {
|
|
88
|
+
const staged = join(stageRoot, `${label}-skills`, `${name}.md`);
|
|
89
|
+
await renderMirrorFile(join(runtimeStage, "skills", name, "SKILL.md"), staged, { locatorPath, runtimeRoot, includeLocatorNotice: true });
|
|
90
|
+
targets.push({ id: `${label}-skill-${name}`, phase: "antigravity", path: join(destinationRoot, "skills", `${name}.md`), staged, type: "file" });
|
|
91
|
+
} else {
|
|
92
|
+
const staged = join(stageRoot, `${label}-skills`, name);
|
|
93
|
+
await cp(join(runtimeStage, "skills", name), staged, { recursive: true });
|
|
94
|
+
await renderMirrorFile(join(runtimeStage, "skills", name, "SKILL.md"), join(staged, "SKILL.md"), {
|
|
95
|
+
locatorPath: antigravitySkillLocatorPath(destinationRoot, name),
|
|
96
|
+
runtimeRoot,
|
|
97
|
+
includeLocatorNotice: true,
|
|
98
|
+
});
|
|
99
|
+
await writeLocator(join(staged, "references", "pota-runtime.json"), runtimeRoot);
|
|
100
|
+
targets.push({ id: `${label}-skill-${name}`, phase: "antigravity", path: join(destinationRoot, "skills", name), staged, type: "tree" });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for (const name of commandNames()) {
|
|
104
|
+
const staged = join(stageRoot, `${label}-workflows`, `pota-${name}.md`);
|
|
105
|
+
await renderMirrorFile(join(runtimeStage, ".agents", "workflows", `pota-${name}.md`), staged, { locatorPath, runtimeRoot });
|
|
106
|
+
targets.push({ id: `${label}-workflow-${name}`, phase: "antigravity", path: join(destinationRoot, "workflows", `pota-${name}.md`), staged, type: "file" });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function copyPluginCandidate(destination, locatorRoot) {
|
|
111
|
+
await copyPluginTree(pluginRoot, destination);
|
|
112
|
+
const manifestPath = join(destination, ".codex-plugin", "plugin.json");
|
|
113
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
114
|
+
manifest.hooks = "./hooks/hooks.json";
|
|
115
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
116
|
+
await writeLocator(join(destination, ".pota-runtime.json"), locatorRoot);
|
|
117
|
+
for (const name of skillNames()) await writeLocator(join(destination, "skills", name, "references", "pota-runtime.json"), locatorRoot);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function copyPluginTree(source, destination) {
|
|
121
|
+
if (!shouldCopyPluginPath(source, pluginRoot)) return;
|
|
122
|
+
const info = await lstat(source);
|
|
123
|
+
if (info.isSymbolicLink() || (!info.isDirectory() && !info.isFile())) {
|
|
124
|
+
throw new Error(`Plugin source must contain only regular files and directories: ${source}`);
|
|
125
|
+
}
|
|
126
|
+
if (info.isFile()) {
|
|
127
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
128
|
+
await copyFile(source, destination);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
await mkdir(destination, { recursive: true });
|
|
132
|
+
const entries = await readdir(source);
|
|
133
|
+
entries.sort();
|
|
134
|
+
for (const entry of entries) await copyPluginTree(join(source, entry), join(destination, entry));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { stageInstall };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function pathHasSegment(path, segment) { return path.split(/[\\/]+/).includes(segment); }
|
|
141
|
+
|
|
142
|
+
export function shouldCopyPluginPath(source, pluginRoot) {
|
|
143
|
+
const name = source.split(/[\\/]+/).at(-1);
|
|
144
|
+
if ([".git", "node_modules", ".pota"].includes(name)) return false;
|
|
145
|
+
if (sep === "/" && source.includes("\\")) return true;
|
|
146
|
+
const relativePath = relative(pluginRoot, source);
|
|
147
|
+
if (isAbsolute(relativePath)) return true;
|
|
148
|
+
const path = relativePath.split(sep).join("/");
|
|
149
|
+
if (path === "") return true;
|
|
150
|
+
if (path.startsWith("../") || path === "..") return true;
|
|
151
|
+
if (/^(package\.json|LICENSE|README(?:\.[^.]+)?\.md)$/.test(path)) return true;
|
|
152
|
+
if (path === "docs") return true;
|
|
153
|
+
if (path.startsWith("docs/")) return path === "docs/model-guidance" || path.startsWith("docs/model-guidance/");
|
|
154
|
+
return [".agents", ".claude", ".codex-plugin", "agents", "commands", "hooks", "scripts", "skills", "templates"]
|
|
155
|
+
.some((directory) => path === directory || path.startsWith(`${directory}/`));
|
|
156
|
+
}
|