@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,244 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { lstat, mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { potaConfigProjection, readSharedConfigSnapshot, removePotaConfig } from "./pota-lifecycle-config.mjs";
|
|
5
|
+
|
|
6
|
+
export function createTransactionJournal(context) {
|
|
7
|
+
const {
|
|
8
|
+
layout, ownershipPath, transactionPath,
|
|
9
|
+
transactionRoot, expectedManagedTarget, targetDigest,
|
|
10
|
+
} = context;
|
|
11
|
+
// Journal-only id that removes the Codex cache runtime a version change leaves behind when it is not retained.
|
|
12
|
+
const previousRuntimeParents = { "previous-codex-runtime": layout.codexRuntimeParent };
|
|
13
|
+
// The versioned runtimes of any release (the installation's, not only this CLI's): a rollback or recovery by a CLI
|
|
14
|
+
// of another version still has to read them.
|
|
15
|
+
const versionedRuntimeIds = ["runtime", "codex-runtime"];
|
|
16
|
+
// Journal-only ids that remove a retained runtime: `retired-<runtime|codex-runtime>-<version>`, during install
|
|
17
|
+
// cleanup or uninstall core.
|
|
18
|
+
const retiredRuntimeRoots = { runtime: layout.runtimeRootFor, "codex-runtime": layout.codexRuntimeRootFor };
|
|
19
|
+
async function createTransaction() {
|
|
20
|
+
const id = randomUUID();
|
|
21
|
+
const root = join(transactionRoot, id);
|
|
22
|
+
const transaction = {
|
|
23
|
+
schemaVersion: 1,
|
|
24
|
+
id,
|
|
25
|
+
state: "active",
|
|
26
|
+
root,
|
|
27
|
+
stageRoot: join(root, "stage"),
|
|
28
|
+
backupRoot: join(root, "backup"),
|
|
29
|
+
entries: [],
|
|
30
|
+
};
|
|
31
|
+
await mkdir(transaction.backupRoot, { recursive: true });
|
|
32
|
+
await writeTransactionJournal(transaction);
|
|
33
|
+
return transaction;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function rollback(transaction) {
|
|
37
|
+
await rollbackEntries(transaction);
|
|
38
|
+
await rm(transaction.root, { recursive: true, force: true });
|
|
39
|
+
await rm(transactionPath, { force: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function finishTransaction(transaction) {
|
|
43
|
+
transaction.state = "committed";
|
|
44
|
+
try {
|
|
45
|
+
await writeTransactionJournal(transaction);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
transaction.state = "active";
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
await rm(transaction.root, { recursive: true, force: true });
|
|
51
|
+
await rm(transactionPath, { force: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function writeTransactionJournal(transaction) {
|
|
55
|
+
const durable = {
|
|
56
|
+
schemaVersion: transaction.schemaVersion,
|
|
57
|
+
id: transaction.id,
|
|
58
|
+
state: transaction.state,
|
|
59
|
+
entries: transaction.entries,
|
|
60
|
+
};
|
|
61
|
+
await durableReplaceFile(transactionPath, `${JSON.stringify(durable, null, 2)}\n`, transaction.id);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function durableReplaceFile(path, content, nonce = randomUUID()) {
|
|
65
|
+
await mkdir(dirname(path), { recursive: true });
|
|
66
|
+
const candidate = `${path}.${nonce}.tmp`;
|
|
67
|
+
const file = await open(candidate, "w", 0o600);
|
|
68
|
+
try {
|
|
69
|
+
await file.writeFile(content, "utf8");
|
|
70
|
+
await file.sync();
|
|
71
|
+
} finally {
|
|
72
|
+
await file.close();
|
|
73
|
+
}
|
|
74
|
+
await rename(candidate, path);
|
|
75
|
+
// Directory fsync is unavailable on some Windows/filesystem combinations. The
|
|
76
|
+
// file fsync+atomic rename remains the portable floor; directory sync is best effort.
|
|
77
|
+
try {
|
|
78
|
+
const directory = await open(dirname(path), "r");
|
|
79
|
+
try { await directory.sync(); } finally { await directory.close(); }
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (!(["EINVAL", "ENOTSUP", "EISDIR", "EPERM", "EACCES"].includes(error?.code))) throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function recoverInterruptedTransaction() {
|
|
86
|
+
if (!(await exists(transactionPath))) return;
|
|
87
|
+
let journal;
|
|
88
|
+
try {
|
|
89
|
+
journal = JSON.parse(await readFile(transactionPath, "utf8"));
|
|
90
|
+
} catch {
|
|
91
|
+
throw new Error(`Lifecycle transaction journal is invalid: ${transactionPath}`);
|
|
92
|
+
}
|
|
93
|
+
validateTransactionJournal(journal, { strict: true });
|
|
94
|
+
if (journal.state === "committed") {
|
|
95
|
+
await rm(join(transactionRoot, journal.id), { recursive: true, force: true });
|
|
96
|
+
await rm(transactionPath, { force: true });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
await rollbackEntries(journal);
|
|
100
|
+
await rm(join(transactionRoot, journal.id), { recursive: true, force: true });
|
|
101
|
+
await rm(transactionPath, { force: true });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function validateTransactionJournal(journal, { strict = false } = {}) {
|
|
105
|
+
if (journal?.schemaVersion !== 1 || typeof journal.id !== "string" || !/^[a-f0-9-]{20,}$/i.test(journal.id)
|
|
106
|
+
|| !["active", "committed"].includes(journal.state) || !Array.isArray(journal.entries)) {
|
|
107
|
+
throw new Error("Lifecycle transaction journal has an invalid schema.");
|
|
108
|
+
}
|
|
109
|
+
if (strict && !sameKeys(journal, ["schemaVersion", "id", "state", "entries"])) throw new Error("Lifecycle transaction journal has unknown fields.");
|
|
110
|
+
const allowedBackupRoot = resolve(transactionRoot, journal.id, "backup");
|
|
111
|
+
const ids = new Set();
|
|
112
|
+
const paths = new Set();
|
|
113
|
+
for (const entry of journal.entries) {
|
|
114
|
+
const expected = entry.id === "ownership"
|
|
115
|
+
? { phase: "manifest", path: ownershipPath, type: "file" }
|
|
116
|
+
: Object.hasOwn(previousRuntimeParents, entry.id)
|
|
117
|
+
? previousRuntimeTarget(previousRuntimeParents[entry.id], entry.path)
|
|
118
|
+
: typeof entry.id === "string" && entry.id.startsWith("retired-")
|
|
119
|
+
? (entry.operation === "remove" ? retiredRuntimeTarget(entry.id, entry.phase) : null)
|
|
120
|
+
: versionedRuntimeIds.includes(entry.id)
|
|
121
|
+
? versionedRuntimeTarget(entry.id, entry.path)
|
|
122
|
+
: expectedManagedTarget(entry.id);
|
|
123
|
+
if (!expected || expected.path !== entry.path || expected.phase !== entry.phase || expected.type !== entry.type
|
|
124
|
+
|| !["prepared", "applied"].includes(entry.status)
|
|
125
|
+
|| !["replace", "remove"].includes(entry.operation)
|
|
126
|
+
|| typeof entry.existed !== "boolean"
|
|
127
|
+
|| !isInside(allowedBackupRoot, entry.backup)
|
|
128
|
+
|| typeof entry.expectedNewDigest !== "string" || typeof entry.previousDigest !== "string") {
|
|
129
|
+
throw new Error(`Lifecycle transaction journal contains an unsafe entry: ${entry?.id ?? "unknown"}`);
|
|
130
|
+
}
|
|
131
|
+
if (strict && !sameKeys(entry, ["id", "phase", "path", "type", "backup", "existed", "previousDigest", "expectedNewDigest", "operation", "status"])) {
|
|
132
|
+
throw new Error(`Lifecycle transaction journal entry has unknown fields: ${entry.id}`);
|
|
133
|
+
}
|
|
134
|
+
if (ids.has(entry.id) || paths.has(entry.path)) throw new Error(`Lifecycle transaction journal contains duplicate targets: ${entry.id}`);
|
|
135
|
+
if (entry.operation === "remove" && entry.expectedNewDigest !== "missing") throw new Error(`Lifecycle remove entry has an invalid new digest: ${entry.id}`);
|
|
136
|
+
ids.add(entry.id);
|
|
137
|
+
paths.add(entry.path);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// A direct `<version>` child of `parent`; the version is any release, because the journal may come from a CLI of
|
|
142
|
+
// another version than this one.
|
|
143
|
+
function previousRuntimeTarget(parent, path) {
|
|
144
|
+
const relation = versionedChild(parent, path);
|
|
145
|
+
return relation ? { phase: "cleanup", path: join(parent, relation), type: "tree" } : null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// `runtime` and `codex-runtime` keep their phase and type; only the version directory may differ from this release.
|
|
149
|
+
function versionedRuntimeTarget(id, path) {
|
|
150
|
+
const expected = expectedManagedTarget(id);
|
|
151
|
+
if (!expected) return null;
|
|
152
|
+
const parent = dirname(expected.path);
|
|
153
|
+
const relation = versionedChild(parent, path);
|
|
154
|
+
return relation ? { ...expected, path: join(parent, relation) } : null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function versionedChild(parent, path) {
|
|
158
|
+
if (typeof path !== "string") return null;
|
|
159
|
+
const relation = relative(parent, resolve(path));
|
|
160
|
+
if (!relation || relation.includes(sep) || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(relation)) return null;
|
|
161
|
+
return relation;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function retiredRuntimeTarget(id, phase) {
|
|
165
|
+
const match = /^retired-(runtime|codex-runtime)-(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/.exec(id);
|
|
166
|
+
if (!match || !Object.hasOwn(retiredRuntimeRoots, match[1]) || !["core", "cleanup"].includes(phase)) return null;
|
|
167
|
+
return { phase, path: retiredRuntimeRoots[match[1]](match[2]), type: "tree" };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function sameKeys(value, expected) {
|
|
171
|
+
return Object.keys(value).sort().join("\0") === [...expected].sort().join("\0");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function rollbackEntries(transaction) {
|
|
175
|
+
validateTransactionJournal(transaction);
|
|
176
|
+
for (const entry of [...transaction.entries].reverse()) {
|
|
177
|
+
const destinationExists = await exists(entry.path);
|
|
178
|
+
const backupExists = await exists(entry.backup);
|
|
179
|
+
if (backupExists) {
|
|
180
|
+
if (entry.type === "config" && entry.status === "prepared") {
|
|
181
|
+
const previousConfig = (await readSharedConfigSnapshot(entry.backup)).text;
|
|
182
|
+
if (destinationExists) {
|
|
183
|
+
const currentConfig = (await readSharedConfigSnapshot(entry.path)).text;
|
|
184
|
+
await writeFile(entry.path, `${removePotaConfig(currentConfig)}${potaConfigProjection(previousConfig)}`);
|
|
185
|
+
await rm(entry.backup, { force: true });
|
|
186
|
+
} else {
|
|
187
|
+
await mkdir(dirname(entry.path), { recursive: true });
|
|
188
|
+
await rename(entry.backup, entry.path);
|
|
189
|
+
}
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (!entry.existed) throw new Error(`Cannot recover transaction because an unexpected backup exists: ${entry.path}`);
|
|
193
|
+
if (await targetDigest(entry.backup, entry.type) !== entry.previousDigest) {
|
|
194
|
+
throw new Error(`Cannot recover transaction because its durable backup changed: ${entry.path}`);
|
|
195
|
+
}
|
|
196
|
+
if (destinationExists) {
|
|
197
|
+
const current = await targetDigest(entry.path, entry.type);
|
|
198
|
+
if (current !== entry.expectedNewDigest) throw new Error(`Cannot recover transaction because destination changed: ${entry.path}`);
|
|
199
|
+
if (entry.type === "config") {
|
|
200
|
+
const currentConfig = (await readSharedConfigSnapshot(entry.path)).text;
|
|
201
|
+
const previousConfig = (await readSharedConfigSnapshot(entry.backup)).text;
|
|
202
|
+
await writeFile(entry.path, `${removePotaConfig(currentConfig)}${potaConfigProjection(previousConfig)}`);
|
|
203
|
+
await rm(entry.backup, { force: true });
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
await rm(entry.path, { recursive: true, force: true });
|
|
207
|
+
}
|
|
208
|
+
await mkdir(dirname(entry.path), { recursive: true });
|
|
209
|
+
await rename(entry.backup, entry.path);
|
|
210
|
+
} else if (entry.status === "prepared") {
|
|
211
|
+
continue;
|
|
212
|
+
} else if (!entry.existed) {
|
|
213
|
+
if (destinationExists) {
|
|
214
|
+
const current = await targetDigest(entry.path, entry.type);
|
|
215
|
+
if (current !== entry.expectedNewDigest) throw new Error(`Cannot recover new transaction target because it changed: ${entry.path}`);
|
|
216
|
+
if (entry.type === "config") {
|
|
217
|
+
const preserved = removePotaConfig((await readSharedConfigSnapshot(entry.path)).text);
|
|
218
|
+
if (preserved.length > 0) await writeFile(entry.path, preserved);
|
|
219
|
+
else await rm(entry.path, { force: true });
|
|
220
|
+
} else await rm(entry.path, { recursive: true, force: true });
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
if (!destinationExists || await targetDigest(entry.path, entry.type) !== entry.previousDigest) {
|
|
224
|
+
throw new Error(`Cannot recover transaction because its durable backup is missing: ${entry.path}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
createTransaction, finishTransaction, recoverInterruptedTransaction, rollback,
|
|
232
|
+
writeTransactionJournal,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
async function exists(path) {
|
|
236
|
+
try { await lstat(path); return true; }
|
|
237
|
+
catch (error) { if (error?.code === "ENOENT") return false; throw error; }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function isInside(root, candidate) {
|
|
241
|
+
const path = resolve(candidate);
|
|
242
|
+
return path === root || path.startsWith(`${root}${sep}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { lstat, mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { potaConfigProjection, readSharedConfigSnapshot, removePotaConfig } from "./pota-lifecycle-config.mjs";
|
|
4
|
+
|
|
5
|
+
export function createTransactionTargets(context) {
|
|
6
|
+
const { targetDigest, writeTransactionJournal } = context;
|
|
7
|
+
async function replaceTarget(target, transaction) {
|
|
8
|
+
let configProjection = null;
|
|
9
|
+
if (target.type === "config") {
|
|
10
|
+
const snapshot = await readSharedConfigSnapshot(target.path);
|
|
11
|
+
configProjection = potaConfigProjection((await readSharedConfigSnapshot(target.staged)).text);
|
|
12
|
+
if (snapshot.digest !== target.configSourceDigest) {
|
|
13
|
+
await writeFile(target.staged, `${removePotaConfig(snapshot.text)}${configProjection}`);
|
|
14
|
+
target.configSourceDigest = snapshot.digest;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const existed = await exists(target.path);
|
|
18
|
+
const backup = join(transaction.backupRoot, String(transaction.entries.length));
|
|
19
|
+
await mkdir(dirname(backup), { recursive: true });
|
|
20
|
+
const entry = {
|
|
21
|
+
id: target.id,
|
|
22
|
+
phase: target.phase,
|
|
23
|
+
path: target.path,
|
|
24
|
+
type: target.type,
|
|
25
|
+
backup,
|
|
26
|
+
existed,
|
|
27
|
+
previousDigest: existed ? await targetDigest(target.path, target.type) : "missing",
|
|
28
|
+
expectedNewDigest: await targetDigest(target.staged, target.type),
|
|
29
|
+
operation: "replace",
|
|
30
|
+
status: "prepared",
|
|
31
|
+
};
|
|
32
|
+
transaction.entries.push(entry);
|
|
33
|
+
await writeTransactionJournal(transaction);
|
|
34
|
+
if (process.env.POTA_TEST_MUTATE_AFTER_PREFLIGHT_ID === target.id) {
|
|
35
|
+
await writeFile(target.path, process.env.POTA_TEST_MUTATION_CONTENT ?? "concurrent mutation\n");
|
|
36
|
+
}
|
|
37
|
+
if (target.type !== "config" && target.preflightDigest !== undefined
|
|
38
|
+
&& await targetDigest(target.path, target.type) !== target.preflightDigest) {
|
|
39
|
+
throw new Error(`Managed artifact changed after lifecycle preflight: ${target.path}`);
|
|
40
|
+
}
|
|
41
|
+
if (target.type === "config" && process.env.POTA_TEST_APPEND_CONFIG_BEFORE_REPLACE) {
|
|
42
|
+
const snapshot = await readSharedConfigSnapshot(target.path);
|
|
43
|
+
await writeFile(target.path, `${snapshot.text}${process.env.POTA_TEST_APPEND_CONFIG_BEFORE_REPLACE}`);
|
|
44
|
+
}
|
|
45
|
+
if (target.type === "config" && process.env.POTA_TEST_REMOVE_CONFIG_BEFORE_REPLACE === "1") {
|
|
46
|
+
await rm(target.path, { force: true });
|
|
47
|
+
}
|
|
48
|
+
if (target.type === "config") {
|
|
49
|
+
let capturedExists = true;
|
|
50
|
+
try {
|
|
51
|
+
await renameLifecycleTarget(target.path, backup, target.id);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error?.code !== "ENOENT") throw error;
|
|
54
|
+
capturedExists = false;
|
|
55
|
+
}
|
|
56
|
+
if (process.env.POTA_TEST_CRASH_AFTER_CONFIG_CAPTURE === "1") process.exit(86);
|
|
57
|
+
const captured = await readSharedConfigSnapshot(backup);
|
|
58
|
+
if (capturedExists) await writeFile(target.staged, `${removePotaConfig(captured.text)}${configProjection}`);
|
|
59
|
+
else await writeFile(target.staged, configProjection);
|
|
60
|
+
entry.existed = capturedExists;
|
|
61
|
+
entry.previousDigest = capturedExists ? await targetDigest(backup, target.type) : "missing";
|
|
62
|
+
entry.expectedNewDigest = await targetDigest(target.staged, target.type);
|
|
63
|
+
await writeTransactionJournal(transaction);
|
|
64
|
+
} else if (existed) await renameLifecycleTarget(target.path, backup, target.id);
|
|
65
|
+
await mkdir(dirname(target.path), { recursive: true });
|
|
66
|
+
await renameStagedLifecycleTarget(target.staged, target.path, target.id);
|
|
67
|
+
entry.status = "applied";
|
|
68
|
+
await writeTransactionJournal(transaction);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function renameStagedLifecycleTarget(source, destination, targetId) {
|
|
72
|
+
if (process.env.POTA_TEST_FORCE_EXDEV_ID === targetId) {
|
|
73
|
+
throw new Error(`Cross-device replacement is not supported safely for lifecycle target: ${targetId}`);
|
|
74
|
+
}
|
|
75
|
+
await renameLifecycleTarget(source, destination, targetId);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function renameLifecycleTarget(source, destination, targetId) {
|
|
79
|
+
try {
|
|
80
|
+
await rename(source, destination);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (error?.code === "EXDEV") {
|
|
83
|
+
throw new Error(`Cross-device replacement is not supported safely for lifecycle target: ${targetId}`);
|
|
84
|
+
}
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function removeManagedTarget(target, transaction) {
|
|
90
|
+
if (!(await exists(target.path))) throw new Error(`Managed uninstall target is missing: ${target.path}`);
|
|
91
|
+
const backup = join(transaction.backupRoot, String(transaction.entries.length));
|
|
92
|
+
await mkdir(dirname(backup), { recursive: true });
|
|
93
|
+
const entry = {
|
|
94
|
+
id: target.id,
|
|
95
|
+
phase: target.phase,
|
|
96
|
+
path: target.path,
|
|
97
|
+
type: target.type,
|
|
98
|
+
backup,
|
|
99
|
+
existed: true,
|
|
100
|
+
previousDigest: await targetDigest(target.path, target.type),
|
|
101
|
+
expectedNewDigest: "missing",
|
|
102
|
+
operation: "remove",
|
|
103
|
+
status: "prepared",
|
|
104
|
+
};
|
|
105
|
+
transaction.entries.push(entry);
|
|
106
|
+
await writeTransactionJournal(transaction);
|
|
107
|
+
await renameLifecycleTarget(target.path, backup, target.id);
|
|
108
|
+
entry.status = "applied";
|
|
109
|
+
await writeTransactionJournal(transaction);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
return { removeManagedTarget, replaceTarget };
|
|
114
|
+
|
|
115
|
+
async function exists(path) {
|
|
116
|
+
try { await lstat(path); return true; }
|
|
117
|
+
catch (error) { if (error?.code === "ENOENT") return false; throw error; }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createLifecycleLock, DEFAULT_LIFECYCLE_LOCK_TIMEOUT_MS } from "./pota-lifecycle-lock.mjs";
|
|
2
|
+
import { createTransactionJournal } from "./pota-lifecycle-transaction-journal.mjs";
|
|
3
|
+
import { createTransactionTargets } from "./pota-lifecycle-transaction-targets.mjs";
|
|
4
|
+
|
|
5
|
+
export { DEFAULT_LIFECYCLE_LOCK_TIMEOUT_MS };
|
|
6
|
+
|
|
7
|
+
export function createTransactionManager(context) {
|
|
8
|
+
const lock = createLifecycleLock(context);
|
|
9
|
+
const journal = createTransactionJournal(context);
|
|
10
|
+
const targets = createTransactionTargets({ ...context, writeTransactionJournal: journal.writeTransactionJournal });
|
|
11
|
+
return { ...journal, ...lock, ...targets };
|
|
12
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { lstat, readFile, rmdir } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
import { resolvePluginRoot } from "./pota-paths.mjs";
|
|
8
|
+
import {
|
|
9
|
+
canonical, ensureSetting, hasSetting, hookHash, removePotaConfig, splitSections,
|
|
10
|
+
updateConfig as projectConfig,
|
|
11
|
+
} from "./pota-lifecycle-config.mjs";
|
|
12
|
+
import { createCommands } from "./pota-lifecycle-commands.mjs";
|
|
13
|
+
import { createIntegrity } from "./pota-lifecycle-integrity.mjs";
|
|
14
|
+
import { HOSTS, isHostSelection, parseHostsOption, promptHosts } from "./pota-lifecycle-hosts.mjs";
|
|
15
|
+
import { createLayout } from "./pota-lifecycle-layout.mjs";
|
|
16
|
+
import { removeUpdateState } from "./pota-update-state.mjs";
|
|
17
|
+
import {
|
|
18
|
+
antigravityRuntimeLocatorPath, quoteShellArg, renderInstalledMirror as projectMirror,
|
|
19
|
+
} from "./pota-lifecycle-mirror.mjs";
|
|
20
|
+
import {
|
|
21
|
+
pathHasSegment, shouldCopyPluginPath as projectShouldCopyPluginPath,
|
|
22
|
+
} from "./pota-lifecycle-staging.mjs";
|
|
23
|
+
import { createStaging } from "./pota-lifecycle-staging.mjs";
|
|
24
|
+
import {
|
|
25
|
+
createTransactionManager, DEFAULT_LIFECYCLE_LOCK_TIMEOUT_MS,
|
|
26
|
+
} from "./pota-lifecycle-transaction.mjs";
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
canonical, DEFAULT_LIFECYCLE_LOCK_TIMEOUT_MS, ensureSetting, hasSetting, hookHash,
|
|
30
|
+
pathHasSegment, quoteShellArg, removePotaConfig, splitSections,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const pluginRoot = resolvePluginRoot(import.meta.url);
|
|
34
|
+
const homeRoot = process.env.HOME ?? process.env.USERPROFILE ?? homedir() ?? ".";
|
|
35
|
+
const claudeHomeOverridden = typeof process.env.CLAUDE_HOME === "string" && process.env.CLAUDE_HOME.length > 0;
|
|
36
|
+
const claudeHome = resolve(process.env.CLAUDE_HOME ?? join(homeRoot, ".claude"));
|
|
37
|
+
const antigravityHome = resolve(process.env.ANTIGRAVITY_HOME ?? join(homeRoot, ".gemini", "config"));
|
|
38
|
+
const antigravityCliHome = resolve(process.env.ANTIGRAVITY_CLI_HOME ?? join(homeRoot, ".gemini", "antigravity-cli"));
|
|
39
|
+
const marketplaceName = "pota";
|
|
40
|
+
const pluginName = "pota";
|
|
41
|
+
const claudeRuntimeLocatorPath = join(claudeHome, "pota", "runtime.json");
|
|
42
|
+
const sourceManifest = JSON.parse(await readFile(join(pluginRoot, ".codex-plugin", "plugin.json"), "utf8"));
|
|
43
|
+
const releaseVersion = sourceManifest.version;
|
|
44
|
+
const layout = createLayout({ env: process.env, releaseVersion, marketplaceName, pluginName });
|
|
45
|
+
const {
|
|
46
|
+
potaHomeRoot, claudePluginMarketplaceRoot, codexCacheRoot, codexHome, codexRuntimeRoot, installRoot,
|
|
47
|
+
installedPluginRoot, lifecycleLockPath, marketplaceJsonPath, marketplaceRoot, ownershipPath, runtimeRoot,
|
|
48
|
+
transactionPath, transactionRoot,
|
|
49
|
+
} = layout;
|
|
50
|
+
const configPath = resolve(process.env.CODEX_CONFIG_PATH ?? join(codexHome, "config.toml"));
|
|
51
|
+
const installedRuntimeLocatorPath = join(runtimeRoot, ".pota-runtime.json");
|
|
52
|
+
const phases = ["core", "codex", "claude", "antigravity", "config", "cleanup"];
|
|
53
|
+
const MAX_LIFECYCLE_LOCK_TIMEOUT_MS = 2_147_483_647;
|
|
54
|
+
|
|
55
|
+
const lifecycleContext = {
|
|
56
|
+
agentNames, antigravityCliHome, antigravityHome, claudeHome, claudeHomeOverridden,
|
|
57
|
+
claudePluginMarketplaceRoot, claudeRuntimeLocatorPath, codexCacheRoot, codexRuntimeRoot,
|
|
58
|
+
codexHome, commandNames, configPath, installRoot, installedPluginRoot, installedRuntimeLocatorPath,
|
|
59
|
+
layout, lifecycleLockPath, marketplaceJsonPath, marketplaceName,
|
|
60
|
+
marketplaceRoot, ownershipPath, phases, pluginName, pluginRoot, releaseVersion, runtimeRoot,
|
|
61
|
+
samePath, skillNames, transactionPath, transactionRoot, updateConfig,
|
|
62
|
+
};
|
|
63
|
+
const integrity = createIntegrity(lifecycleContext);
|
|
64
|
+
const transaction = createTransactionManager({ ...lifecycleContext, ...integrity });
|
|
65
|
+
const staging = createStaging({ ...lifecycleContext, assertRegularTree: integrity.assertRegularTree });
|
|
66
|
+
const commands = createCommands({
|
|
67
|
+
...lifecycleContext, integrity,
|
|
68
|
+
pruneUninstallScaffolds: () => pruneUninstallScaffolds(layout), staging, t, transaction,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (isCliEntry()) {
|
|
72
|
+
try {
|
|
73
|
+
const { command, hosts: requestedHosts, auto, allowDowngrade } = parseLifecycleInvocation(process.argv.slice(2));
|
|
74
|
+
if (command === "install" || command === "upgrade") {
|
|
75
|
+
// The prompt runs before any lock is taken so a waiting answer never blocks another lifecycle run.
|
|
76
|
+
const hosts = requestedHosts ?? (command === "install" && await shouldPromptHosts() ? await promptHosts({ input: process.stdin, output: process.stdout, ko: localeFamily() === "ko" }) : null);
|
|
77
|
+
await assertLifecycleMutationPaths(hosts);
|
|
78
|
+
await withLifecycleLocks(() => commands.install(command, { hosts, auto, allowDowngrade }), { afterRelease: pruneUninstallRoots });
|
|
79
|
+
} else if (command === "doctor") {
|
|
80
|
+
await assertNoSymlinkAncestor(potaHomeRoot, lifecycleLockPath);
|
|
81
|
+
await transaction.withLifecycleLock(commands.doctor, { timeoutMs: lifecycleLockTimeout(), afterRelease: pruneUninstallRoots });
|
|
82
|
+
} else if (command === "uninstall") {
|
|
83
|
+
await assertLifecycleMutationPaths(null);
|
|
84
|
+
// Auto-upgrade state goes with the installation; config.json and goal state stay.
|
|
85
|
+
await withLifecycleLocks(commands.uninstall, { afterRelease: async () => { await pruneUninstallRoots(); await removeUpdateState(process.env); } });
|
|
86
|
+
} else help();
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error(`Pota lifecycle error: ${error.message}`);
|
|
89
|
+
process.exitCode = 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Only install and upgrade take options: `--hosts <list>` or `--hosts=<list>`, and `--allow-downgrade` (replace an
|
|
94
|
+
// installation of a newer version). upgrade also takes `--auto` (the auto-upgrade runner's call, which keeps the
|
|
95
|
+
// recorded hosts and never downgrades); every other trailing argument (including --help) is rejected before any lock
|
|
96
|
+
// or stage, as before.
|
|
97
|
+
function parseLifecycleInvocation(args) {
|
|
98
|
+
if (args.length === 0) return { command: "help", hosts: null, auto: false, allowDowngrade: false };
|
|
99
|
+
const [command, ...trailing] = args;
|
|
100
|
+
if (!["install", "upgrade", "doctor", "uninstall", "help"].includes(command)) throw new Error(`Unknown lifecycle command: ${command}`);
|
|
101
|
+
const unexpected = [];
|
|
102
|
+
let hostsValue;
|
|
103
|
+
let hostsGiven = false;
|
|
104
|
+
let auto = false;
|
|
105
|
+
let allowDowngrade = false;
|
|
106
|
+
for (let index = 0; index < trailing.length; index += 1) {
|
|
107
|
+
const arg = trailing[index];
|
|
108
|
+
if (command === "upgrade" && arg === "--auto" && !auto) { auto = true; continue; }
|
|
109
|
+
if ((command === "install" || command === "upgrade") && arg === "--allow-downgrade" && !allowDowngrade) { allowDowngrade = true; continue; }
|
|
110
|
+
const hostsArg = command === "install" || command === "upgrade" ? arg === "--hosts" || arg.startsWith("--hosts=") : false;
|
|
111
|
+
if (!hostsArg) { unexpected.push(arg); continue; }
|
|
112
|
+
if (hostsGiven) throw new Error("Invalid --hosts value: --hosts may only be provided once");
|
|
113
|
+
hostsGiven = true;
|
|
114
|
+
if (arg === "--hosts") { hostsValue = trailing[index + 1]; index += 1; } else hostsValue = arg.slice("--hosts=".length);
|
|
115
|
+
}
|
|
116
|
+
if (unexpected.length > 0) {
|
|
117
|
+
// doctor, uninstall, and help keep the original message listing every trailing argument.
|
|
118
|
+
const listed = command === "install" || command === "upgrade" ? unexpected : trailing;
|
|
119
|
+
throw new Error(`Lifecycle command ${command} does not accept arguments: ${listed.join(" ")}`);
|
|
120
|
+
}
|
|
121
|
+
if (auto && hostsGiven) throw new Error("--auto cannot be combined with --hosts");
|
|
122
|
+
if (auto && allowDowngrade) throw new Error("--auto cannot be combined with --allow-downgrade");
|
|
123
|
+
return { command, hosts: hostsGiven ? parseHostsOption(hostsValue) : null, auto, allowDowngrade };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// install asks only when nothing is installed yet (no ownership manifest), a person is at a terminal, and
|
|
127
|
+
// CI is not set. POTA_TEST_HOSTS_PROMPT_TTY stands in for the terminal check so tests can answer through a pipe.
|
|
128
|
+
async function shouldPromptHosts() {
|
|
129
|
+
if (process.env.CI) return false;
|
|
130
|
+
const terminal = process.env.POTA_TEST_HOSTS_PROMPT_TTY === "1" || (process.stdin.isTTY && process.stdout.isTTY);
|
|
131
|
+
if (!terminal) return false;
|
|
132
|
+
return !(await pathExists(ownershipPath));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// The hosts whose paths an install, upgrade, or uninstall may touch: the requested selection plus whatever the
|
|
136
|
+
// current manifest records. Without a readable selection every host is checked.
|
|
137
|
+
async function hostsInScope(requested) {
|
|
138
|
+
let recorded = null;
|
|
139
|
+
try { recorded = JSON.parse(await readFile(ownershipPath, "utf8")).hosts; } catch { recorded = null; }
|
|
140
|
+
if (!isHostSelection(recorded)) recorded = (await pathExists(ownershipPath)) ? [...HOSTS] : null;
|
|
141
|
+
const scope = new Set([...(requested ?? []), ...(recorded ?? [])]);
|
|
142
|
+
if (!requested && !recorded) for (const host of HOSTS) scope.add(host);
|
|
143
|
+
return scope;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function lifecycleLockTimeout() {
|
|
147
|
+
const raw = process.env.POTA_LIFECYCLE_LOCK_TIMEOUT_MS;
|
|
148
|
+
if (raw === undefined) return DEFAULT_LIFECYCLE_LOCK_TIMEOUT_MS;
|
|
149
|
+
if (!/^[1-9]\d*$/.test(raw)) throw new Error(`POTA_LIFECYCLE_LOCK_TIMEOUT_MS must be an integer between 1 and ${MAX_LIFECYCLE_LOCK_TIMEOUT_MS}`);
|
|
150
|
+
const timeoutMs = Number(raw);
|
|
151
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs > MAX_LIFECYCLE_LOCK_TIMEOUT_MS) throw new Error(`POTA_LIFECYCLE_LOCK_TIMEOUT_MS must be an integer between 1 and ${MAX_LIFECYCLE_LOCK_TIMEOUT_MS}`);
|
|
152
|
+
return timeoutMs;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function withLifecycleLocks(operation, { afterRelease } = {}) {
|
|
156
|
+
return transaction.withLifecycleLock(operation, { timeoutMs: lifecycleLockTimeout(), afterRelease });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Symlinked ancestors are checked only for the hosts in scope, so a symlinked home of a host the user did not select
|
|
160
|
+
// never blocks the others.
|
|
161
|
+
async function assertLifecycleMutationPaths(requestedHosts) {
|
|
162
|
+
const hosts = await hostsInScope(requestedHosts);
|
|
163
|
+
const roots = [
|
|
164
|
+
[potaHomeRoot, lifecycleLockPath], [potaHomeRoot, transactionRoot], [potaHomeRoot, runtimeRoot],
|
|
165
|
+
...(hosts.has("codex") ? [
|
|
166
|
+
[potaHomeRoot, installedPluginRoot], [potaHomeRoot, marketplaceJsonPath],
|
|
167
|
+
[codexHome, codexRuntimeRoot], [isInside(codexHome, configPath) ? codexHome : dirname(configPath), configPath],
|
|
168
|
+
] : []),
|
|
169
|
+
...(hosts.has("claude") ? [
|
|
170
|
+
[potaHomeRoot, claudePluginMarketplaceRoot],
|
|
171
|
+
[claudeHome, join(claudeHome, "commands")], [claudeHome, join(claudeHome, "agents")], [claudeHome, claudeRuntimeLocatorPath],
|
|
172
|
+
] : []),
|
|
173
|
+
...(hosts.has("antigravity") ? [
|
|
174
|
+
[antigravityHome, join(antigravityHome, "skills")],
|
|
175
|
+
...skillNames().map((name) => [antigravityHome, join(antigravityHome, "skills", name)]),
|
|
176
|
+
[antigravityHome, join(antigravityHome, "workflows")], [antigravityHome, antigravityRuntimeLocatorPath(antigravityHome)],
|
|
177
|
+
[antigravityCliHome, join(antigravityCliHome, "skills")], [antigravityCliHome, join(antigravityCliHome, "workflows")],
|
|
178
|
+
[antigravityCliHome, antigravityRuntimeLocatorPath(antigravityCliHome)],
|
|
179
|
+
] : []),
|
|
180
|
+
];
|
|
181
|
+
for (const [root, destination] of roots) await assertNoSymlinkAncestor(root, destination);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function assertNoSymlinkAncestor(root, destination) {
|
|
185
|
+
const relation = relative(root, destination);
|
|
186
|
+
if (relation === ".." || relation.startsWith(`..${sep}`) || isAbsolute(relation)) return;
|
|
187
|
+
const paths = [resolve(root)];
|
|
188
|
+
let current = resolve(root);
|
|
189
|
+
for (const segment of relation.split(sep).filter(Boolean)) { current = join(current, segment); paths.push(current); }
|
|
190
|
+
for (const path of paths) {
|
|
191
|
+
let info;
|
|
192
|
+
try { info = await lstat(path); }
|
|
193
|
+
catch (error) { if (error?.code === "ENOENT") continue; throw error; }
|
|
194
|
+
if (info.isSymbolicLink()) {
|
|
195
|
+
if (path === configPath) throw new Error(`Managed config is not a regular file: ${path}`);
|
|
196
|
+
throw new Error(`Lifecycle path ancestor must not be a symlink: ${path}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function pruneUninstallScaffolds(paths) {
|
|
202
|
+
for (const path of [
|
|
203
|
+
join(paths.marketplaceRoot, "plugins"), join(paths.marketplaceRoot, ".agents", "plugins"), join(paths.marketplaceRoot, ".agents"),
|
|
204
|
+
paths.marketplaceRoot, paths.runtimeParent, paths.transactionRoot, paths.codexRuntimeParent,
|
|
205
|
+
]) await removeEmptyDirectory(path);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Only empty managed roots are removed; POTA_HOME itself also holds goal state and is left in place.
|
|
209
|
+
async function pruneUninstallRoots() {
|
|
210
|
+
await removeEmptyDirectory(installRoot);
|
|
211
|
+
await removeEmptyDirectory(codexCacheRoot);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function pathExists(path) {
|
|
215
|
+
try { await lstat(path); return true; }
|
|
216
|
+
catch (error) { if (error?.code === "ENOENT") return false; throw error; }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function removeEmptyDirectory(path) {
|
|
220
|
+
try { await rmdir(path); }
|
|
221
|
+
// ENOTDIR: a symlinked scaffold of a host that was never selected is not Pota's to prune.
|
|
222
|
+
catch (error) { if (!["ENOENT", "ENOTEMPTY", "EEXIST", "ENOTDIR"].includes(error?.code)) throw error; }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function isInside(root, candidate) {
|
|
226
|
+
const path = resolve(candidate);
|
|
227
|
+
return path === root || path.startsWith(`${root}${sep}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export async function targetDigest(path, type) { return integrity.targetDigest(path, type); }
|
|
231
|
+
export function updateConfig(config, hookStates, options = {}) { return projectConfig(config, hookStates, { marketplacePath: options.marketplacePath ?? marketplaceRoot }); }
|
|
232
|
+
export function shouldCopyPluginPath(source) { return projectShouldCopyPluginPath(source, pluginRoot); }
|
|
233
|
+
export function samePath(left, right) { try { return realpathSync(left) === realpathSync(right); } catch { return resolve(left) === resolve(right); } }
|
|
234
|
+
export function commandNames() { return ["install", "upgrade", "doctor", "uninstall", "memory", "plan", "work", "review", "toolcheck"]; }
|
|
235
|
+
export function agentNames() { return ["explorer", "worker"]; }
|
|
236
|
+
export function skillNames() { return ["pota-memory", "pota-plan", "pota-work", "pota-review"]; }
|
|
237
|
+
export function renderInstalledMirror(content, options = {}) { return projectMirror(content, { runtimeRoot: options.runtimeRoot ?? runtimeRoot, locatorPath: options.locatorPath ?? installedRuntimeLocatorPath, platform: options.platform ?? process.platform }); }
|
|
238
|
+
export function findPluginStatus(value, id) { const entries = Array.isArray(value) ? value : value?.installed ?? value?.plugins ?? value?.data ?? []; return entries.find((entry) => entry.pluginId === id || entry.id === id || entry.name === id || `${entry.name}@${entry.marketplaceName ?? entry.marketplace}` === id) ?? null; }
|
|
239
|
+
export function parseCodexFeatureList(output) { if (typeof output !== "string" || output.trim().length === 0) return null; const features = {}; for (const line of output.split(/\r?\n/)) { const match = line.trim().match(/^([A-Za-z0-9_]+)\s+.+?\s+(true|false)$/); if (match) features[match[1]] = match[2] === "true"; } return Object.keys(features).length > 0 ? features : null; }
|
|
240
|
+
export function localeFamily() { const locale = [process.env.LC_ALL, process.env.LC_MESSAGES, process.env.LANG].find((value) => typeof value === "string" && value.length > 0) ?? Intl.DateTimeFormat().resolvedOptions().locale; return locale.toLowerCase().startsWith("ko") ? "ko" : "en"; }
|
|
241
|
+
|
|
242
|
+
function help() { console.log(t("usage")); }
|
|
243
|
+
function t(key) { const messages = { en: { installComplete: "Pota install complete", upgradeComplete: "Pota upgrade complete", uninstallComplete: "Pota uninstall complete", usage: "Usage: pota install|upgrade [--hosts all|codex,claude,antigravity] [--allow-downgrade] | doctor | uninstall | toolcheck" }, ko: { installComplete: "Pota 설치 완료", upgradeComplete: "Pota 업그레이드 완료", uninstallComplete: "Pota 언인스톨 완료", usage: "사용법: pota install|upgrade [--hosts all|codex,claude,antigravity] [--allow-downgrade] | doctor | uninstall | toolcheck" } }; return messages[localeFamily()]?.[key] ?? messages.en[key]; }
|
|
244
|
+
function isCliEntry() { if (!process.argv[1]) return false; try { return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]); } catch { return import.meta.url === pathToFileURL(process.argv[1]).href; } }
|