superpowers-manager 0.5.1 → 0.5.2
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/README.md +1 -1
- package/dist/cli.js +1 -1
- package/dist/commands/install.js +16 -6
- package/dist/commands/prepare.js +1 -1
- package/dist/commands/uninstall.js +1 -1
- package/dist/harnesses/codex/adapter.js +147 -18
- package/dist/harnesses/codex/generated-plugin.js +1 -1
- package/dist/harnesses/codex/harness.js +35 -17
- package/dist/harnesses/codex/hooks.js +1 -1
- package/dist/harnesses/codex/json.js +6 -2
- package/dist/harnesses/codex/lifecycle.js +1 -1
- package/dist/harnesses/codex/marketplace.js +148 -0
- package/dist/harnesses/codex/paths.js +97 -0
- package/dist/harnesses/codex/prepare.js +34 -2
- package/dist/harnesses/codex/presentation.js +4 -3
- package/dist/harnesses/codex/publication.js +538 -0
- package/dist/harnesses/codex/recovery.js +338 -0
- package/dist/harnesses/codex/state.js +131 -0
- package/dist/harnesses/codex/stored-state.js +249 -0
- package/dist/provenance.js +3 -3
- package/node_modules/smol-toml/LICENSE +24 -0
- package/node_modules/smol-toml/README.md +418 -0
- package/node_modules/smol-toml/dist/date.d.ts +41 -0
- package/node_modules/smol-toml/dist/date.js +127 -0
- package/node_modules/smol-toml/dist/error.d.ts +38 -0
- package/node_modules/smol-toml/dist/error.js +63 -0
- package/node_modules/smol-toml/dist/extract.js +69 -0
- package/node_modules/smol-toml/dist/index.cjs +734 -0
- package/node_modules/smol-toml/dist/index.d.ts +43 -0
- package/node_modules/smol-toml/dist/index.js +33 -0
- package/node_modules/smol-toml/dist/parse.d.ts +36 -0
- package/node_modules/smol-toml/dist/parse.js +149 -0
- package/node_modules/smol-toml/dist/primitive.js +238 -0
- package/node_modules/smol-toml/dist/stringify.d.ts +31 -0
- package/node_modules/smol-toml/dist/stringify.js +181 -0
- package/node_modules/smol-toml/dist/struct.js +179 -0
- package/node_modules/smol-toml/dist/util.d.ts +38 -0
- package/node_modules/smol-toml/dist/util.js +89 -0
- package/node_modules/smol-toml/package.json +68 -0
- package/package.json +10 -2
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { lstat, open, readdir } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join } from "node:path";
|
|
4
|
+
import { parse, TomlDate } from "smol-toml";
|
|
5
|
+
import { SafetyError } from "../../safety-error.js";
|
|
6
|
+
import { codexHome } from "./paths.js";
|
|
7
|
+
const CONFIG_LIMIT = 1024 * 1024;
|
|
8
|
+
const CONFIG_DEPTH_LIMIT = 64;
|
|
9
|
+
const MANAGER_MARKETPLACE = "superpowers-manager";
|
|
10
|
+
const LEGACY_MARKETPLACE = "superpowers-wrapper";
|
|
11
|
+
const MANAGER_PLUGIN = `superpowers@${MANAGER_MARKETPLACE}`;
|
|
12
|
+
const LEGACY_PLUGIN = `superpowers@${LEGACY_MARKETPLACE}`;
|
|
13
|
+
const VERSION_SEGMENT = /^[A-Za-z0-9._+-]+$/u;
|
|
14
|
+
const MARKETPLACE_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/u;
|
|
15
|
+
function storedError(message, cause) {
|
|
16
|
+
return new SafetyError("codex-stored-state", message, { cause });
|
|
17
|
+
}
|
|
18
|
+
function isErrno(cause, code) {
|
|
19
|
+
return (cause !== null &&
|
|
20
|
+
typeof cause === "object" &&
|
|
21
|
+
"code" in cause &&
|
|
22
|
+
cause.code === code);
|
|
23
|
+
}
|
|
24
|
+
function table(value, name) {
|
|
25
|
+
if (value === undefined)
|
|
26
|
+
return {};
|
|
27
|
+
if (value === null ||
|
|
28
|
+
typeof value !== "object" ||
|
|
29
|
+
Array.isArray(value) ||
|
|
30
|
+
value instanceof TomlDate ||
|
|
31
|
+
Object.getPrototypeOf(value) !== Object.prototype) {
|
|
32
|
+
throw storedError(`Codex configuration ${name} must be a table`);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
async function readConfig(path) {
|
|
37
|
+
let handle;
|
|
38
|
+
try {
|
|
39
|
+
const initial = await lstat(path);
|
|
40
|
+
if (!initial.isFile() || initial.isSymbolicLink()) {
|
|
41
|
+
throw storedError(`Codex configuration must be a regular file: ${path}`);
|
|
42
|
+
}
|
|
43
|
+
handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
44
|
+
const before = await handle.stat();
|
|
45
|
+
if (!before.isFile() || before.size > CONFIG_LIMIT) {
|
|
46
|
+
throw storedError(`Codex configuration must be a bounded regular file: ${path}`);
|
|
47
|
+
}
|
|
48
|
+
const buffer = Buffer.alloc(CONFIG_LIMIT + 1);
|
|
49
|
+
let length = 0;
|
|
50
|
+
while (length < buffer.length) {
|
|
51
|
+
const { bytesRead } = await handle.read(buffer, length, buffer.length - length, length);
|
|
52
|
+
if (bytesRead === 0)
|
|
53
|
+
break;
|
|
54
|
+
length += bytesRead;
|
|
55
|
+
}
|
|
56
|
+
const bytes = buffer.subarray(0, length);
|
|
57
|
+
const after = await handle.stat();
|
|
58
|
+
const pathAfter = await lstat(path);
|
|
59
|
+
if (bytes.length > CONFIG_LIMIT ||
|
|
60
|
+
bytes.length !== before.size ||
|
|
61
|
+
before.dev !== after.dev ||
|
|
62
|
+
before.ino !== after.ino ||
|
|
63
|
+
before.size !== after.size ||
|
|
64
|
+
before.mtimeMs !== after.mtimeMs ||
|
|
65
|
+
before.ctimeMs !== after.ctimeMs ||
|
|
66
|
+
before.dev !== pathAfter.dev ||
|
|
67
|
+
before.ino !== pathAfter.ino ||
|
|
68
|
+
!pathAfter.isFile() ||
|
|
69
|
+
pathAfter.isSymbolicLink()) {
|
|
70
|
+
throw storedError(`Codex configuration changed while being read: ${path}`);
|
|
71
|
+
}
|
|
72
|
+
let text;
|
|
73
|
+
try {
|
|
74
|
+
text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
75
|
+
}
|
|
76
|
+
catch (cause) {
|
|
77
|
+
throw storedError(`Codex configuration is not valid UTF-8: ${path}`, cause);
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
return parse(text, {
|
|
81
|
+
maxDepth: CONFIG_DEPTH_LIMIT,
|
|
82
|
+
integersAsBigInt: true,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (cause) {
|
|
86
|
+
throw storedError(`Codex configuration TOML is invalid: ${path}`, cause);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (cause) {
|
|
90
|
+
if (cause instanceof SafetyError && cause.module === "codex-stored-state") {
|
|
91
|
+
throw cause;
|
|
92
|
+
}
|
|
93
|
+
throw storedError(`cannot safely read Codex configuration: ${path}`, cause);
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
await handle?.close().catch(() => { });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function marketplaceRoot(marketplaces, name) {
|
|
100
|
+
const value = marketplaces[name];
|
|
101
|
+
if (value === undefined)
|
|
102
|
+
return null;
|
|
103
|
+
const entry = table(value, `marketplaces.${name}`);
|
|
104
|
+
if (entry.source_type !== "local" ||
|
|
105
|
+
typeof entry.source !== "string" ||
|
|
106
|
+
!isAbsolute(entry.source)) {
|
|
107
|
+
throw storedError(`Codex marketplace ${name} must name an absolute local source`);
|
|
108
|
+
}
|
|
109
|
+
return entry.source;
|
|
110
|
+
}
|
|
111
|
+
async function requireMissingManagerSource(path) {
|
|
112
|
+
try {
|
|
113
|
+
await lstat(path);
|
|
114
|
+
}
|
|
115
|
+
catch (cause) {
|
|
116
|
+
if (isErrno(cause, "ENOENT"))
|
|
117
|
+
return;
|
|
118
|
+
throw storedError(`cannot inspect Codex marketplace source: ${path}`, cause);
|
|
119
|
+
}
|
|
120
|
+
throw storedError(`Codex marketplace source is not missing: ${path}`);
|
|
121
|
+
}
|
|
122
|
+
function validVersionSegment(value) {
|
|
123
|
+
return value !== "." && value !== ".." && VERSION_SEGMENT.test(value);
|
|
124
|
+
}
|
|
125
|
+
async function cachedVersionPresent(searchRoot, marketplace) {
|
|
126
|
+
if (!MARKETPLACE_SEGMENT.test(marketplace))
|
|
127
|
+
return null;
|
|
128
|
+
let base = searchRoot;
|
|
129
|
+
for (const segment of ["plugins", "cache", marketplace, "superpowers"]) {
|
|
130
|
+
let details;
|
|
131
|
+
try {
|
|
132
|
+
details = await lstat(base);
|
|
133
|
+
}
|
|
134
|
+
catch (cause) {
|
|
135
|
+
if (isErrno(cause, "ENOENT"))
|
|
136
|
+
return false;
|
|
137
|
+
throw storedError(`cannot inspect Codex plugin cache: ${base}`, cause);
|
|
138
|
+
}
|
|
139
|
+
if (!details.isDirectory() || details.isSymbolicLink()) {
|
|
140
|
+
throw storedError(`Codex plugin cache must be a directory: ${base}`);
|
|
141
|
+
}
|
|
142
|
+
base = join(base, segment);
|
|
143
|
+
}
|
|
144
|
+
let before;
|
|
145
|
+
try {
|
|
146
|
+
before = await lstat(base);
|
|
147
|
+
}
|
|
148
|
+
catch (cause) {
|
|
149
|
+
if (isErrno(cause, "ENOENT"))
|
|
150
|
+
return false;
|
|
151
|
+
throw storedError(`cannot inspect Codex plugin cache: ${base}`, cause);
|
|
152
|
+
}
|
|
153
|
+
if (!before.isDirectory() || before.isSymbolicLink()) {
|
|
154
|
+
throw storedError(`Codex plugin cache must be a directory: ${base}`);
|
|
155
|
+
}
|
|
156
|
+
let names;
|
|
157
|
+
try {
|
|
158
|
+
names = await readdir(base);
|
|
159
|
+
}
|
|
160
|
+
catch (cause) {
|
|
161
|
+
throw storedError(`cannot inspect Codex plugin cache: ${base}`, cause);
|
|
162
|
+
}
|
|
163
|
+
let present = false;
|
|
164
|
+
for (const name of names) {
|
|
165
|
+
if (!validVersionSegment(name))
|
|
166
|
+
continue;
|
|
167
|
+
const candidate = join(base, name);
|
|
168
|
+
try {
|
|
169
|
+
const details = await lstat(candidate);
|
|
170
|
+
if (details.isSymbolicLink()) {
|
|
171
|
+
throw storedError(`Codex plugin cache entry must not be a symlink: ${candidate}`);
|
|
172
|
+
}
|
|
173
|
+
if (details.isDirectory() && !details.isSymbolicLink())
|
|
174
|
+
present = true;
|
|
175
|
+
}
|
|
176
|
+
catch (cause) {
|
|
177
|
+
if (!isErrno(cause, "ENOENT")) {
|
|
178
|
+
throw storedError(`cannot inspect Codex plugin cache entry: ${candidate}`, cause);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
let after;
|
|
183
|
+
try {
|
|
184
|
+
after = await lstat(base);
|
|
185
|
+
}
|
|
186
|
+
catch (cause) {
|
|
187
|
+
throw storedError(`cannot verify Codex plugin cache: ${base}`, cause);
|
|
188
|
+
}
|
|
189
|
+
if (!after.isDirectory() ||
|
|
190
|
+
after.isSymbolicLink() ||
|
|
191
|
+
before.dev !== after.dev ||
|
|
192
|
+
before.ino !== after.ino ||
|
|
193
|
+
before.mtimeMs !== after.mtimeMs) {
|
|
194
|
+
throw storedError(`Codex plugin cache changed while being inspected: ${base}`);
|
|
195
|
+
}
|
|
196
|
+
return present;
|
|
197
|
+
}
|
|
198
|
+
export async function readCodexStoredState(env, cwd) {
|
|
199
|
+
const root = codexHome(env, cwd);
|
|
200
|
+
try {
|
|
201
|
+
const details = await lstat(root);
|
|
202
|
+
if (!details.isDirectory() || details.isSymbolicLink()) {
|
|
203
|
+
throw storedError(`Codex home must be a directory: ${root}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch (cause) {
|
|
207
|
+
if (cause instanceof SafetyError && cause.module === "codex-stored-state") {
|
|
208
|
+
throw cause;
|
|
209
|
+
}
|
|
210
|
+
throw storedError(`cannot inspect Codex home: ${root}`, cause);
|
|
211
|
+
}
|
|
212
|
+
const config = await readConfig(join(root, "config.toml"));
|
|
213
|
+
const marketplaces = table(config.marketplaces, "marketplaces");
|
|
214
|
+
const managerMarketplaceRoot = marketplaceRoot(marketplaces, MANAGER_MARKETPLACE);
|
|
215
|
+
if (managerMarketplaceRoot === null) {
|
|
216
|
+
throw storedError("Codex manager marketplace is not configured");
|
|
217
|
+
}
|
|
218
|
+
await requireMissingManagerSource(managerMarketplaceRoot);
|
|
219
|
+
const legacyMarketplaceRoot = marketplaceRoot(marketplaces, LEGACY_MARKETPLACE);
|
|
220
|
+
const plugins = table(config.plugins, "plugins");
|
|
221
|
+
const searchRoot = env.SUPERPOWERS_INSTALLED_SEARCH_ROOT || root;
|
|
222
|
+
const installed = [];
|
|
223
|
+
for (const [pluginId, rawEntry] of Object.entries(plugins)) {
|
|
224
|
+
const entry = table(rawEntry, `plugins.${pluginId}`);
|
|
225
|
+
if (typeof entry.enabled !== "boolean") {
|
|
226
|
+
throw storedError(`Codex plugin ${pluginId} has invalid enabled state`);
|
|
227
|
+
}
|
|
228
|
+
if (!pluginId.startsWith("superpowers@"))
|
|
229
|
+
continue;
|
|
230
|
+
const marketplace = pluginId.slice("superpowers@".length);
|
|
231
|
+
installed.push({
|
|
232
|
+
pluginId,
|
|
233
|
+
installed: await cachedVersionPresent(searchRoot, marketplace),
|
|
234
|
+
enabled: entry.enabled === true,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
const byId = (id) => installed.find((plugin) => plugin.pluginId === id);
|
|
238
|
+
const manager = byId(MANAGER_PLUGIN);
|
|
239
|
+
const legacy = byId(LEGACY_PLUGIN);
|
|
240
|
+
return {
|
|
241
|
+
managerMarketplaceRoot,
|
|
242
|
+
legacyMarketplaceRoot,
|
|
243
|
+
managerPluginPresent: manager?.installed === true,
|
|
244
|
+
managerPluginEnabled: manager?.enabled === true,
|
|
245
|
+
legacyPluginPresent: legacy?.installed === true,
|
|
246
|
+
legacyPluginEnabled: legacy?.enabled === true,
|
|
247
|
+
installedListingJson: JSON.stringify({ installed }),
|
|
248
|
+
};
|
|
249
|
+
}
|
package/dist/provenance.js
CHANGED
|
@@ -4,16 +4,16 @@ import { COMMIT_INPUT_RE } from "./domain/refs.js";
|
|
|
4
4
|
import { escapePythonJsonString } from "./python-json.js";
|
|
5
5
|
import { SafetyError } from "./safety-error.js";
|
|
6
6
|
import { parseStrictJson, } from "./strict-json.js";
|
|
7
|
-
|
|
7
|
+
const PROVENANCE_STRICT_PROFILE = {
|
|
8
8
|
duplicateKeys: "last-wins",
|
|
9
9
|
nonStandardConstants: "reject",
|
|
10
10
|
maxDepth: 256,
|
|
11
11
|
};
|
|
12
|
-
|
|
12
|
+
const PROVENANCE_LENIENT_PROFILE = {
|
|
13
13
|
duplicateKeys: "last-wins",
|
|
14
14
|
nonStandardConstants: "reject",
|
|
15
15
|
};
|
|
16
|
-
|
|
16
|
+
const PROVENANCE_CODEX_SOURCE_PROFILE = {
|
|
17
17
|
duplicateKeys: "last-wins",
|
|
18
18
|
nonStandardConstants: "accept",
|
|
19
19
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
2
|
+
|
|
3
|
+
Redistribution and use in source and binary forms, with or without
|
|
4
|
+
modification, are permitted provided that the following conditions are met:
|
|
5
|
+
|
|
6
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
7
|
+
list of conditions and the following disclaimer.
|
|
8
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
9
|
+
this list of conditions and the following disclaimer in the
|
|
10
|
+
documentation and/or other materials provided with the distribution.
|
|
11
|
+
3. Neither the name of the copyright holder nor the names of its contributors
|
|
12
|
+
may be used to endorse or promote products derived from this software without
|
|
13
|
+
specific prior written permission.
|
|
14
|
+
|
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
16
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
17
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
18
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
19
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
20
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
21
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
23
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
24
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|