@astrosheep/square 0.3.4 → 0.3.5
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/.codex-plugin/plugin.json +1 -1
- package/dist/activity.js +15 -13
- package/dist/cli/context.js +143 -0
- package/dist/cli/harness-command.js +50 -0
- package/dist/cli/maintenance-commands.js +92 -0
- package/dist/cli/meta-commands.js +31 -0
- package/dist/cli/observation-commands.js +461 -0
- package/dist/cli/program.js +48 -0
- package/dist/cli/registry.js +40 -0
- package/dist/cli/square-commands.js +221 -0
- package/dist/compact.js +5 -18
- package/dist/harness-claude.js +275 -0
- package/dist/harness-codex.js +653 -0
- package/dist/harness-lifecycle.js +102 -0
- package/dist/harness-links.js +123 -0
- package/dist/harness.js +97 -577
- package/dist/help.js +2 -1
- package/dist/index.js +45 -32
- package/dist/runtime.js +0 -54
- package/dist/square-application.js +259 -0
- package/dist/square-store.js +111 -0
- package/dist/square.js +5 -1362
- package/dist/watch.js +17 -19
- package/package.json +1 -1
- package/skills/square/.claude-plugin/plugin.json +1 -1
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { SQUARE_IDENTITY } from './identity.js';
|
|
6
|
+
import { reconcileInstall, reconcileUninstall, staleManagedRegistrations, } from './harness-lifecycle.js';
|
|
7
|
+
export const CODEX_HOOK_COMMAND = SQUARE_IDENTITY.hookCommand;
|
|
8
|
+
export const SQUARE_CODEX_MARKER = SQUARE_IDENTITY.hookMarker;
|
|
9
|
+
export const CODEX_PLUGIN_ID = SQUARE_IDENTITY.pluginId;
|
|
10
|
+
export const CODEX_MARKETPLACE_NAME = SQUARE_IDENTITY.marketplaceName;
|
|
11
|
+
export function codexMarketplaceRoot(homeDir) {
|
|
12
|
+
return path.join(homeDir, '.square', 'codex', 'marketplaces', CODEX_MARKETPLACE_NAME);
|
|
13
|
+
}
|
|
14
|
+
export function codexPluginRoot(homeDir) {
|
|
15
|
+
return path.join(codexMarketplaceRoot(homeDir), 'plugins', SQUARE_IDENTITY.pluginName);
|
|
16
|
+
}
|
|
17
|
+
export function codexPluginHooksPath(homeDir) {
|
|
18
|
+
return path.join(codexPluginRoot(homeDir), 'hooks', 'hooks.json');
|
|
19
|
+
}
|
|
20
|
+
function codexHome(homeDir) {
|
|
21
|
+
return process.env.CODEX_HOME?.trim() || path.join(homeDir, '.codex');
|
|
22
|
+
}
|
|
23
|
+
export function codexHomeHooksPath(homeDir) {
|
|
24
|
+
return path.join(codexHome(homeDir), 'hooks.json');
|
|
25
|
+
}
|
|
26
|
+
export function codexConfigPath(homeDir) {
|
|
27
|
+
return path.join(codexHome(homeDir), 'config.toml');
|
|
28
|
+
}
|
|
29
|
+
function managedHooksPath(homeDir) {
|
|
30
|
+
return path.join(homeDir, '.square', 'codex', 'hooks.json');
|
|
31
|
+
}
|
|
32
|
+
function sectionRange(lines, section) {
|
|
33
|
+
const escaped = section.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
34
|
+
const header = new RegExp(`^\\s*\\[${escaped}\\]\\s*(?:#.*)?$`);
|
|
35
|
+
const start = lines.findIndex((line) => header.test(line));
|
|
36
|
+
if (start < 0)
|
|
37
|
+
return undefined;
|
|
38
|
+
const next = lines.findIndex((line, index) => index > start && /^\s*\[[^\]]+\]\s*(?:#.*)?$/.test(line));
|
|
39
|
+
return { start, end: next < 0 ? lines.length : next };
|
|
40
|
+
}
|
|
41
|
+
export function upsertTomlSectionKey(text, section, key, value) {
|
|
42
|
+
const lines = text === '' ? [] : text.replace(/\n$/, '').split('\n');
|
|
43
|
+
const range = sectionRange(lines, section);
|
|
44
|
+
const assignment = `${key} = ${value}`;
|
|
45
|
+
if (range === undefined) {
|
|
46
|
+
if (lines.length > 0 && lines.at(-1) !== '')
|
|
47
|
+
lines.push('');
|
|
48
|
+
lines.push(`[${section}]`, assignment);
|
|
49
|
+
return `${lines.join('\n')}\n`;
|
|
50
|
+
}
|
|
51
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
52
|
+
const keyPattern = new RegExp(`^\\s*${escaped}\\s*=`);
|
|
53
|
+
const existing = lines.findIndex((line, index) => index > range.start && index < range.end && keyPattern.test(line));
|
|
54
|
+
if (existing < 0)
|
|
55
|
+
lines.splice(range.end, 0, assignment);
|
|
56
|
+
else
|
|
57
|
+
lines[existing] = assignment;
|
|
58
|
+
return `${lines.join('\n')}\n`;
|
|
59
|
+
}
|
|
60
|
+
function writeAtomic(filePath, contents) {
|
|
61
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
62
|
+
const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
63
|
+
fs.writeFileSync(temporary, contents, { mode: 0o600 });
|
|
64
|
+
fs.renameSync(temporary, filePath);
|
|
65
|
+
}
|
|
66
|
+
function writeJsonAtomic(filePath, value) {
|
|
67
|
+
writeAtomic(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
68
|
+
}
|
|
69
|
+
function backup(filePath) {
|
|
70
|
+
const backupPath = `${filePath}.square-bak`;
|
|
71
|
+
if (fs.existsSync(filePath) && !fs.existsSync(backupPath))
|
|
72
|
+
fs.copyFileSync(filePath, backupPath);
|
|
73
|
+
}
|
|
74
|
+
function requireSuccess(result, operation) {
|
|
75
|
+
if (result.status === 0)
|
|
76
|
+
return;
|
|
77
|
+
const detail = result.stderr.trim() || result.stdout.trim() || `exit ${result.status}`;
|
|
78
|
+
throw new Error(`Codex ${operation} failed: ${detail}`);
|
|
79
|
+
}
|
|
80
|
+
function runCodex(homeDir, args) {
|
|
81
|
+
const result = spawnSync(process.env.SQUARE_CODEX_BIN || 'codex', args, {
|
|
82
|
+
encoding: 'utf8',
|
|
83
|
+
env: {
|
|
84
|
+
...process.env,
|
|
85
|
+
HOME: homeDir,
|
|
86
|
+
CODEX_HOME: codexHome(homeDir),
|
|
87
|
+
},
|
|
88
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
89
|
+
timeout: 30_000,
|
|
90
|
+
});
|
|
91
|
+
if (result.error)
|
|
92
|
+
throw result.error;
|
|
93
|
+
return {
|
|
94
|
+
status: result.status ?? 1,
|
|
95
|
+
stdout: result.stdout || '',
|
|
96
|
+
stderr: result.stderr || '',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function packageAsset(relative) {
|
|
100
|
+
return fileURLToPath(new URL(relative, import.meta.url));
|
|
101
|
+
}
|
|
102
|
+
function isSquareGroup(group) {
|
|
103
|
+
if (group === null || typeof group !== 'object')
|
|
104
|
+
return false;
|
|
105
|
+
const hooks = group.hooks;
|
|
106
|
+
if (!Array.isArray(hooks))
|
|
107
|
+
return false;
|
|
108
|
+
return hooks.some((hook) => {
|
|
109
|
+
if (hook === null || typeof hook !== 'object')
|
|
110
|
+
return false;
|
|
111
|
+
const command = hook.command;
|
|
112
|
+
const statusMessage = hook.statusMessage;
|
|
113
|
+
return command === CODEX_HOOK_COMMAND || statusMessage === SQUARE_CODEX_MARKER;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
export function stripSquareCodexHooks(filePath) {
|
|
117
|
+
if (!fs.existsSync(filePath))
|
|
118
|
+
return false;
|
|
119
|
+
const root = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
120
|
+
if (root === null || typeof root !== 'object' || root.hooks === undefined || typeof root.hooks !== 'object') {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
let changed = false;
|
|
124
|
+
for (const [event, groups] of Object.entries(root.hooks)) {
|
|
125
|
+
if (!Array.isArray(groups))
|
|
126
|
+
continue;
|
|
127
|
+
const remaining = groups.filter((group) => !isSquareGroup(group));
|
|
128
|
+
if (remaining.length === groups.length)
|
|
129
|
+
continue;
|
|
130
|
+
changed = true;
|
|
131
|
+
if (remaining.length === 0)
|
|
132
|
+
delete root.hooks[event];
|
|
133
|
+
else
|
|
134
|
+
root.hooks[event] = remaining;
|
|
135
|
+
}
|
|
136
|
+
if (!changed)
|
|
137
|
+
return false;
|
|
138
|
+
if (Object.keys(root.hooks).length === 0)
|
|
139
|
+
fs.rmSync(filePath, { force: true });
|
|
140
|
+
else
|
|
141
|
+
writeJsonAtomic(filePath, root);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
function codexMarketplaceDocument() {
|
|
145
|
+
return {
|
|
146
|
+
name: CODEX_MARKETPLACE_NAME,
|
|
147
|
+
interface: { displayName: SQUARE_IDENTITY.productName },
|
|
148
|
+
plugins: [
|
|
149
|
+
{
|
|
150
|
+
name: SQUARE_IDENTITY.pluginName,
|
|
151
|
+
source: { source: 'local', path: './plugins/square' },
|
|
152
|
+
policy: { installation: 'AVAILABLE', authentication: 'ON_USE' },
|
|
153
|
+
category: 'Developer Tools',
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function stageCodexBundle(_homeDir, marketplaceRoot) {
|
|
159
|
+
const token = `${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
|
|
160
|
+
const stage = `${marketplaceRoot}.${token}.stage`;
|
|
161
|
+
const backupPath = `${marketplaceRoot}.${token}.previous`;
|
|
162
|
+
const pluginRoot = path.join(stage, 'plugins', SQUARE_IDENTITY.pluginName);
|
|
163
|
+
fs.mkdirSync(path.dirname(marketplaceRoot), { recursive: true });
|
|
164
|
+
try {
|
|
165
|
+
fs.cpSync(packageAsset('../codex-plugin/'), pluginRoot, { recursive: true });
|
|
166
|
+
const skill = path.join(pluginRoot, 'skills', 'square', 'SKILL.md');
|
|
167
|
+
fs.mkdirSync(path.dirname(skill), { recursive: true });
|
|
168
|
+
fs.copyFileSync(path.join(packageAsset('../skills/square/'), 'SKILL.md'), skill);
|
|
169
|
+
writeJsonAtomic(path.join(stage, '.agents', 'plugins', 'marketplace.json'), codexMarketplaceDocument());
|
|
170
|
+
if (fs.existsSync(marketplaceRoot))
|
|
171
|
+
fs.renameSync(marketplaceRoot, backupPath);
|
|
172
|
+
fs.renameSync(stage, marketplaceRoot);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
try {
|
|
176
|
+
fs.rmSync(stage, { recursive: true, force: true });
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Keep the source error; staging cleanup is best effort.
|
|
180
|
+
}
|
|
181
|
+
if (fs.existsSync(backupPath) && !fs.existsSync(marketplaceRoot)) {
|
|
182
|
+
fs.renameSync(backupPath, marketplaceRoot);
|
|
183
|
+
}
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
desired: {
|
|
188
|
+
marketplaceName: CODEX_MARKETPLACE_NAME,
|
|
189
|
+
marketplaceRoot,
|
|
190
|
+
pluginId: CODEX_PLUGIN_ID,
|
|
191
|
+
},
|
|
192
|
+
rollback() {
|
|
193
|
+
fs.rmSync(marketplaceRoot, { recursive: true, force: true });
|
|
194
|
+
if (fs.existsSync(backupPath))
|
|
195
|
+
fs.renameSync(backupPath, marketplaceRoot);
|
|
196
|
+
},
|
|
197
|
+
finalize() {
|
|
198
|
+
fs.rmSync(backupPath, { recursive: true, force: true });
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function parseInventory(stdout) {
|
|
203
|
+
let payload;
|
|
204
|
+
try {
|
|
205
|
+
payload = JSON.parse(stdout);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
throw new Error('Codex marketplace inventory returned invalid JSON.');
|
|
209
|
+
}
|
|
210
|
+
if (payload === null || typeof payload !== 'object' || !Array.isArray(payload.marketplaces)) {
|
|
211
|
+
throw new Error('Codex marketplace inventory is malformed.');
|
|
212
|
+
}
|
|
213
|
+
const marketplaces = payload.marketplaces.map(parseMarketplaceRegistration);
|
|
214
|
+
return { marketplaces };
|
|
215
|
+
}
|
|
216
|
+
function parseMarketplaceRegistration(value) {
|
|
217
|
+
if (value === null || typeof value !== 'object') {
|
|
218
|
+
throw new Error('Codex marketplace inventory entry is invalid.');
|
|
219
|
+
}
|
|
220
|
+
const entry = value;
|
|
221
|
+
if (typeof entry.name !== 'string' || typeof entry.root !== 'string') {
|
|
222
|
+
throw new Error('Codex marketplace inventory entry is malformed.');
|
|
223
|
+
}
|
|
224
|
+
const source = entry.marketplaceSource;
|
|
225
|
+
if (source === undefined)
|
|
226
|
+
return { name: entry.name, source: entry.root, local: false };
|
|
227
|
+
if (source === null || typeof source !== 'object')
|
|
228
|
+
throw new Error('Codex marketplace source is malformed.');
|
|
229
|
+
const marketplaceSource = source;
|
|
230
|
+
if (typeof marketplaceSource.sourceType !== 'string') {
|
|
231
|
+
throw new Error('Codex marketplace source is malformed.');
|
|
232
|
+
}
|
|
233
|
+
if (marketplaceSource.sourceType !== 'local') {
|
|
234
|
+
const stableSource = typeof marketplaceSource.source === 'string'
|
|
235
|
+
? marketplaceSource.source
|
|
236
|
+
: entry.root;
|
|
237
|
+
return {
|
|
238
|
+
name: entry.name,
|
|
239
|
+
source: `${marketplaceSource.sourceType}:${stableSource}`,
|
|
240
|
+
local: false,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (typeof marketplaceSource.source !== 'string') {
|
|
244
|
+
throw new Error('Codex local marketplace inventory entry is malformed.');
|
|
245
|
+
}
|
|
246
|
+
return { name: entry.name, source: marketplaceSource.source, local: true };
|
|
247
|
+
}
|
|
248
|
+
function pluginHooksFromList(result) {
|
|
249
|
+
const entries = result?.data;
|
|
250
|
+
if (!Array.isArray(entries))
|
|
251
|
+
throw new Error('Codex hooks/list returned invalid data.');
|
|
252
|
+
const hooks = [];
|
|
253
|
+
for (const entry of entries) {
|
|
254
|
+
if (!Array.isArray(entry.hooks))
|
|
255
|
+
continue;
|
|
256
|
+
for (const value of entry.hooks) {
|
|
257
|
+
if (value === null || typeof value !== 'object')
|
|
258
|
+
continue;
|
|
259
|
+
const hook = value;
|
|
260
|
+
if (hook.pluginId !== CODEX_PLUGIN_ID)
|
|
261
|
+
continue;
|
|
262
|
+
if (typeof hook.key !== 'string' ||
|
|
263
|
+
typeof hook.eventName !== 'string' ||
|
|
264
|
+
typeof hook.enabled !== 'boolean' ||
|
|
265
|
+
typeof hook.currentHash !== 'string' ||
|
|
266
|
+
typeof hook.trustStatus !== 'string') {
|
|
267
|
+
throw new Error('Codex hooks/list returned an invalid Square hook.');
|
|
268
|
+
}
|
|
269
|
+
hooks.push({
|
|
270
|
+
key: hook.key,
|
|
271
|
+
eventName: hook.eventName,
|
|
272
|
+
pluginId: CODEX_PLUGIN_ID,
|
|
273
|
+
enabled: hook.enabled,
|
|
274
|
+
currentHash: hook.currentHash,
|
|
275
|
+
trustStatus: hook.trustStatus,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return hooks;
|
|
280
|
+
}
|
|
281
|
+
function expectedHooks(hooks) {
|
|
282
|
+
if (hooks.length !== 2 || hooks.some((hook) => !hook.enabled))
|
|
283
|
+
return false;
|
|
284
|
+
const events = hooks.map((hook) => hook.eventName).sort();
|
|
285
|
+
return events[0] === 'stop' && events[1] === 'userPromptSubmit';
|
|
286
|
+
}
|
|
287
|
+
function trustUpdate(hooks) {
|
|
288
|
+
return Object.fromEntries(hooks.map((hook) => [hook.key, { trusted_hash: hook.currentHash }]));
|
|
289
|
+
}
|
|
290
|
+
export function inspectCodexPluginHooks(homeDir, trust) {
|
|
291
|
+
return new Promise((resolve, reject) => {
|
|
292
|
+
const child = spawn(process.env.SQUARE_CODEX_BIN || 'codex', ['app-server', '--stdio'], {
|
|
293
|
+
env: {
|
|
294
|
+
...process.env,
|
|
295
|
+
HOME: homeDir,
|
|
296
|
+
CODEX_HOME: codexHome(homeDir),
|
|
297
|
+
},
|
|
298
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
299
|
+
});
|
|
300
|
+
let stdout = '';
|
|
301
|
+
let stderr = '';
|
|
302
|
+
let settled = false;
|
|
303
|
+
const finish = (error, hooks) => {
|
|
304
|
+
if (settled)
|
|
305
|
+
return;
|
|
306
|
+
settled = true;
|
|
307
|
+
clearTimeout(timer);
|
|
308
|
+
child.kill('SIGTERM');
|
|
309
|
+
if (error !== undefined)
|
|
310
|
+
reject(error);
|
|
311
|
+
else
|
|
312
|
+
resolve(hooks ?? []);
|
|
313
|
+
};
|
|
314
|
+
const timer = setTimeout(() => finish(new Error(`Codex hooks API timed out${stderr.trim() ? `: ${stderr.trim()}` : ''}`)), 30_000);
|
|
315
|
+
const send = (message) => child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
316
|
+
const requestHooks = (id) => {
|
|
317
|
+
send({ id, method: 'hooks/list', params: { cwds: [process.cwd()] } });
|
|
318
|
+
};
|
|
319
|
+
const sendTrust = (hooks) => {
|
|
320
|
+
send({
|
|
321
|
+
id: 2,
|
|
322
|
+
method: 'config/batchWrite',
|
|
323
|
+
params: {
|
|
324
|
+
edits: [{ keyPath: 'hooks.state', value: trustUpdate(hooks), mergeStrategy: 'upsert' }],
|
|
325
|
+
reloadUserConfig: true,
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
};
|
|
329
|
+
const failForMessage = (message) => {
|
|
330
|
+
if (message.error === undefined)
|
|
331
|
+
return false;
|
|
332
|
+
const detail = typeof message.error.message === 'string'
|
|
333
|
+
? message.error.message
|
|
334
|
+
: JSON.stringify(message.error);
|
|
335
|
+
finish(new Error(`Codex hooks API failed: ${detail}`));
|
|
336
|
+
return true;
|
|
337
|
+
};
|
|
338
|
+
const handleMessage = (message) => {
|
|
339
|
+
if (failForMessage(message))
|
|
340
|
+
return;
|
|
341
|
+
if (message.id === 0) {
|
|
342
|
+
send({ method: 'initialized', params: {} });
|
|
343
|
+
requestHooks(1);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (message.id === 1) {
|
|
347
|
+
const hooks = pluginHooksFromList(message.result);
|
|
348
|
+
if (!trust) {
|
|
349
|
+
finish(undefined, hooks);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (!expectedHooks(hooks)) {
|
|
353
|
+
finish(new Error(`Codex discovered an unexpected hook set for ${CODEX_PLUGIN_ID}.`));
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
sendTrust(hooks);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (message.id === 2) {
|
|
360
|
+
requestHooks(3);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (message.id === 3)
|
|
364
|
+
finish(undefined, pluginHooksFromList(message.result));
|
|
365
|
+
};
|
|
366
|
+
const consumeStdout = (chunk) => {
|
|
367
|
+
stdout += String(chunk);
|
|
368
|
+
while (stdout.includes('\n')) {
|
|
369
|
+
const newline = stdout.indexOf('\n');
|
|
370
|
+
const line = stdout.slice(0, newline);
|
|
371
|
+
stdout = stdout.slice(newline + 1);
|
|
372
|
+
if (!line.trim())
|
|
373
|
+
continue;
|
|
374
|
+
try {
|
|
375
|
+
handleMessage(JSON.parse(line));
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
const diagnostic = error instanceof Error ? error : new Error(String(error));
|
|
379
|
+
if (diagnostic instanceof SyntaxError) {
|
|
380
|
+
finish(new Error(`Codex hooks API returned invalid JSON: ${line}`));
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
finish(diagnostic);
|
|
384
|
+
}
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
child.on('error', (error) => finish(error));
|
|
390
|
+
child.stdin.on('error', (error) => finish(error));
|
|
391
|
+
child.stderr.on('data', (chunk) => {
|
|
392
|
+
stderr += String(chunk);
|
|
393
|
+
});
|
|
394
|
+
child.stdout.on('data', consumeStdout);
|
|
395
|
+
child.on('exit', (code) => {
|
|
396
|
+
if (!settled) {
|
|
397
|
+
const suffix = stderr.trim() ? `: ${stderr.trim()}` : '';
|
|
398
|
+
finish(new Error(`Codex hooks API exited ${code ?? 'without status'}${suffix}`));
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
send({
|
|
402
|
+
id: 0,
|
|
403
|
+
method: 'initialize',
|
|
404
|
+
params: {
|
|
405
|
+
clientInfo: {
|
|
406
|
+
name: SQUARE_IDENTITY.clientName,
|
|
407
|
+
title: SQUARE_IDENTITY.productName,
|
|
408
|
+
version: SQUARE_IDENTITY.packageVersion,
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
function codexProtocol(run, hooksRuntime, notes, installed) {
|
|
415
|
+
const inspectInventory = (homeDir) => {
|
|
416
|
+
const result = run(homeDir, ['plugin', 'marketplace', 'list', '--json']);
|
|
417
|
+
requireSuccess(result, 'marketplace inventory');
|
|
418
|
+
return parseInventory(result.stdout);
|
|
419
|
+
};
|
|
420
|
+
const stageBundle = (homeDir, marketplaceRoot) => {
|
|
421
|
+
const config = codexConfigPath(homeDir);
|
|
422
|
+
const hooks = codexHomeHooksPath(homeDir);
|
|
423
|
+
backup(config);
|
|
424
|
+
backup(hooks);
|
|
425
|
+
const current = fs.existsSync(config) ? fs.readFileSync(config, 'utf8') : '';
|
|
426
|
+
writeAtomic(config, upsertTomlSectionKey(current, 'features', 'hooks', 'true'));
|
|
427
|
+
return stageCodexBundle(homeDir, marketplaceRoot);
|
|
428
|
+
};
|
|
429
|
+
const registerMarketplace = (homeDir, marketplaceRoot) => {
|
|
430
|
+
const result = run(homeDir, ['plugin', 'marketplace', 'add', marketplaceRoot, '--json']);
|
|
431
|
+
requireSuccess(result, 'marketplace install');
|
|
432
|
+
};
|
|
433
|
+
const installOrUpdate = (homeDir, pluginId) => {
|
|
434
|
+
const result = run(homeDir, ['plugin', 'add', pluginId, '--json']);
|
|
435
|
+
requireSuccess(result, 'plugin install');
|
|
436
|
+
try {
|
|
437
|
+
const payload = JSON.parse(result.stdout);
|
|
438
|
+
if (typeof payload.installedPath === 'string')
|
|
439
|
+
installed.path = payload.installedPath;
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
notes.push('Codex installed the plugin but returned non-JSON output.');
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
const verifyPluginAndHooks = async (homeDir, marketplaceName, pluginId) => {
|
|
446
|
+
const listed = run(homeDir, ['plugin', 'list', '--marketplace', marketplaceName, '--json']);
|
|
447
|
+
requireSuccess(listed, 'plugin inventory');
|
|
448
|
+
let entries;
|
|
449
|
+
try {
|
|
450
|
+
entries = JSON.parse(listed.stdout).installed;
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
throw new Error('Codex plugin inventory returned invalid JSON.');
|
|
454
|
+
}
|
|
455
|
+
const installedAndEnabled = Array.isArray(entries) && entries.some((entry) => {
|
|
456
|
+
if (entry === null || typeof entry !== 'object')
|
|
457
|
+
return false;
|
|
458
|
+
const plugin = entry;
|
|
459
|
+
return plugin.pluginId === pluginId && plugin.installed === true && plugin.enabled === true;
|
|
460
|
+
});
|
|
461
|
+
if (!installedAndEnabled) {
|
|
462
|
+
throw new Error(`Codex did not verify ${pluginId} as installed and enabled.`);
|
|
463
|
+
}
|
|
464
|
+
const hooks = await hooksRuntime(homeDir, true);
|
|
465
|
+
if (!expectedHooks(hooks)) {
|
|
466
|
+
throw new Error(`Codex did not discover both Square plugin hooks for ${pluginId}.`);
|
|
467
|
+
}
|
|
468
|
+
const untrusted = hooks.filter((hook) => hook.trustStatus !== 'trusted');
|
|
469
|
+
if (untrusted.length > 0) {
|
|
470
|
+
throw new Error(`Codex did not trust Square plugin hooks: ${untrusted.map((hook) => hook.key).join(', ')}`);
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
const removePlugin = (homeDir, pluginId) => {
|
|
474
|
+
const result = run(homeDir, ['plugin', 'remove', pluginId, '--json']);
|
|
475
|
+
if (result.status !== 0 && !result.stderr.includes('is not configured or installed')) {
|
|
476
|
+
requireSuccess(result, 'plugin removal');
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
const removeMarketplace = (homeDir, marketplaceName) => {
|
|
480
|
+
const result = run(homeDir, ['plugin', 'marketplace', 'remove', marketplaceName, '--json']);
|
|
481
|
+
if (result.status !== 0 && !result.stderr.includes('is not configured or installed')) {
|
|
482
|
+
requireSuccess(result, 'marketplace removal');
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
const retireDirectDelivery = (homeDir) => {
|
|
486
|
+
const hooks = codexHomeHooksPath(homeDir);
|
|
487
|
+
try {
|
|
488
|
+
if (stripSquareCodexHooks(hooks))
|
|
489
|
+
notes.push(`Removed legacy Square handlers from ${hooks}.`);
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
notes.push(`Legacy hooks were left untouched: ${error instanceof Error ? error.message : String(error)}`);
|
|
493
|
+
}
|
|
494
|
+
fs.rmSync(managedHooksPath(homeDir), { force: true });
|
|
495
|
+
};
|
|
496
|
+
return {
|
|
497
|
+
host: 'codex',
|
|
498
|
+
marketplaceName: CODEX_MARKETPLACE_NAME,
|
|
499
|
+
pluginId: CODEX_PLUGIN_ID,
|
|
500
|
+
managedRoot: (homeDir) => path.join(homeDir, '.square', 'codex'),
|
|
501
|
+
stageBundle,
|
|
502
|
+
inspectInventory,
|
|
503
|
+
registerMarketplace(homeDir, desired) {
|
|
504
|
+
registerMarketplace(homeDir, desired.marketplaceRoot);
|
|
505
|
+
},
|
|
506
|
+
installOrUpdate(homeDir, desired) {
|
|
507
|
+
installOrUpdate(homeDir, desired.pluginId);
|
|
508
|
+
},
|
|
509
|
+
verifyPluginAndHooks(homeDir, desired) {
|
|
510
|
+
return verifyPluginAndHooks(homeDir, desired.marketplaceName, desired.pluginId);
|
|
511
|
+
},
|
|
512
|
+
removePlugin(homeDir, pluginId) {
|
|
513
|
+
removePlugin(homeDir, pluginId);
|
|
514
|
+
},
|
|
515
|
+
removeMarketplace(homeDir, marketplaceName) {
|
|
516
|
+
removeMarketplace(homeDir, marketplaceName);
|
|
517
|
+
},
|
|
518
|
+
removeManagedSource(source) {
|
|
519
|
+
fs.rmSync(source, { recursive: true, force: true });
|
|
520
|
+
},
|
|
521
|
+
retireDirectDelivery,
|
|
522
|
+
removeManagedRoot(homeDir) {
|
|
523
|
+
fs.rmSync(path.join(homeDir, '.square', 'codex'), { recursive: true, force: true });
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
function isManagedMarketplace(homeDir, registration) {
|
|
528
|
+
if (!registration.local)
|
|
529
|
+
return false;
|
|
530
|
+
const root = path.join(homeDir, '.square', 'codex');
|
|
531
|
+
const relative = path.relative(path.resolve(root), path.resolve(registration.source));
|
|
532
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
533
|
+
}
|
|
534
|
+
function marketplaceRootFromInventory(homeDir, inventory) {
|
|
535
|
+
return inventory.marketplaces.find((entry) => entry.name === CODEX_MARKETPLACE_NAME && isManagedMarketplace(homeDir, entry))?.source ?? codexMarketplaceRoot(homeDir);
|
|
536
|
+
}
|
|
537
|
+
export async function installCodexPlugin(homeDir, run = runCodex, hooksRuntime = inspectCodexPluginHooks) {
|
|
538
|
+
const notes = [];
|
|
539
|
+
const installed = {};
|
|
540
|
+
const protocol = codexProtocol(run, hooksRuntime, notes, installed);
|
|
541
|
+
const inventory = await reconcileInstall(homeDir, protocol);
|
|
542
|
+
const marketplaceRoot = marketplaceRootFromInventory(homeDir, inventory);
|
|
543
|
+
return {
|
|
544
|
+
configPath: codexConfigPath(homeDir),
|
|
545
|
+
marketplaceRoot,
|
|
546
|
+
pluginRoot: path.join(marketplaceRoot, 'plugins', SQUARE_IDENTITY.pluginName),
|
|
547
|
+
...(installed.path === undefined ? {} : { installedPath: installed.path }),
|
|
548
|
+
notes,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
export async function uninstallCodexPlugin(homeDir, run = runCodex) {
|
|
552
|
+
const notes = [];
|
|
553
|
+
const base = codexProtocol(run, async () => [], notes, {});
|
|
554
|
+
const protocol = {
|
|
555
|
+
...base,
|
|
556
|
+
async inspectInventory(currentHome) {
|
|
557
|
+
const inventory = await base.inspectInventory(currentHome);
|
|
558
|
+
if (inventory.marketplaces.some((entry) => isManagedMarketplace(currentHome, entry))) {
|
|
559
|
+
return inventory;
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
marketplaces: [
|
|
563
|
+
...inventory.marketplaces,
|
|
564
|
+
{
|
|
565
|
+
name: CODEX_MARKETPLACE_NAME,
|
|
566
|
+
source: codexMarketplaceRoot(currentHome),
|
|
567
|
+
local: true,
|
|
568
|
+
pluginIds: [CODEX_PLUGIN_ID],
|
|
569
|
+
},
|
|
570
|
+
],
|
|
571
|
+
};
|
|
572
|
+
},
|
|
573
|
+
};
|
|
574
|
+
await reconcileUninstall(homeDir, protocol);
|
|
575
|
+
const root = codexMarketplaceRoot(homeDir);
|
|
576
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
577
|
+
fs.rmSync(managedHooksPath(homeDir), { force: true });
|
|
578
|
+
return { paths: [root, codexHomeHooksPath(homeDir), managedHooksPath(homeDir)], notes };
|
|
579
|
+
}
|
|
580
|
+
function pluginHooksInstalled(filePath) {
|
|
581
|
+
try {
|
|
582
|
+
const contents = fs.readFileSync(filePath, 'utf8');
|
|
583
|
+
const hooks = JSON.parse(contents);
|
|
584
|
+
return Array.isArray(hooks.hooks?.UserPromptSubmit) && Array.isArray(hooks.hooks?.Stop);
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function codexPluginStatus(homeDir, run) {
|
|
591
|
+
const listed = run(homeDir, ['plugin', 'list', '--marketplace', CODEX_MARKETPLACE_NAME, '--json']);
|
|
592
|
+
if (listed.status !== 0)
|
|
593
|
+
return `○ ${CODEX_PLUGIN_ID} unavailable`;
|
|
594
|
+
try {
|
|
595
|
+
const payload = JSON.parse(listed.stdout);
|
|
596
|
+
const plugin = payload.installed?.find((item) => item.pluginId === CODEX_PLUGIN_ID);
|
|
597
|
+
return plugin?.installed === true && plugin.enabled === true
|
|
598
|
+
? `✓ ${CODEX_PLUGIN_ID} installed and enabled`
|
|
599
|
+
: `○ ${CODEX_PLUGIN_ID} is not installed and enabled`;
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
return `○ ${CODEX_PLUGIN_ID} unavailable`;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async function codexHookStatus(homeDir, hooksRuntime) {
|
|
606
|
+
try {
|
|
607
|
+
const hooks = await hooksRuntime(homeDir, false);
|
|
608
|
+
const trusted = hooks.every((hook) => hook.trustStatus === 'trusted');
|
|
609
|
+
return expectedHooks(hooks) && trusted
|
|
610
|
+
? `✓ ${CODEX_PLUGIN_ID} hooks trusted by Codex`
|
|
611
|
+
: `✕ Codex runtime did not discover both ${CODEX_PLUGIN_ID} hooks`;
|
|
612
|
+
}
|
|
613
|
+
catch (error) {
|
|
614
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
615
|
+
return `✕ Codex hooks runtime unavailable (${detail})`;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
export async function doctorCodexPlugin(homeDir, run = runCodex, hooksRuntime = inspectCodexPluginHooks) {
|
|
619
|
+
const protocol = codexProtocol(run, hooksRuntime, [], {});
|
|
620
|
+
const inventory = await protocol.inspectInventory(homeDir);
|
|
621
|
+
const marketplaceRoot = marketplaceRootFromInventory(homeDir, inventory);
|
|
622
|
+
const stale = staleManagedRegistrations(inventory, protocol.managedRoot(homeDir), {
|
|
623
|
+
marketplaceName: CODEX_MARKETPLACE_NAME,
|
|
624
|
+
marketplaceRoot,
|
|
625
|
+
pluginId: CODEX_PLUGIN_ID,
|
|
626
|
+
});
|
|
627
|
+
const current = inventory.marketplaces.some((entry) => entry.name === CODEX_MARKETPLACE_NAME && isManagedMarketplace(homeDir, entry));
|
|
628
|
+
const config = codexConfigPath(homeDir);
|
|
629
|
+
const configText = fs.existsSync(config) ? fs.readFileSync(config, 'utf8') : '';
|
|
630
|
+
const bundleRoot = path.join(marketplaceRoot, 'plugins', SQUARE_IDENTITY.pluginName);
|
|
631
|
+
const bundleHooks = path.join(bundleRoot, 'hooks', 'hooks.json');
|
|
632
|
+
const lines = [
|
|
633
|
+
/^hooks = true$/m.test(configText)
|
|
634
|
+
? `✓ features.hooks=true in ${config}`
|
|
635
|
+
: `○ features.hooks missing in ${config}`,
|
|
636
|
+
pluginHooksInstalled(bundleHooks)
|
|
637
|
+
? `✓ Square plugin hooks ${bundleHooks}`
|
|
638
|
+
: `○ Square plugin bundle missing ${bundleRoot}`,
|
|
639
|
+
current
|
|
640
|
+
? `✓ ${CODEX_PLUGIN_ID} marketplace registered`
|
|
641
|
+
: `○ ${CODEX_PLUGIN_ID} marketplace is not registered`,
|
|
642
|
+
codexPluginStatus(homeDir, run),
|
|
643
|
+
await codexHookStatus(homeDir, hooksRuntime),
|
|
644
|
+
];
|
|
645
|
+
if (stale.length > 0)
|
|
646
|
+
lines.push(`✕ ${stale.length} stale Square marketplace registration(s)`);
|
|
647
|
+
return lines;
|
|
648
|
+
}
|
|
649
|
+
export const codexHarness = Object.freeze({
|
|
650
|
+
install: installCodexPlugin,
|
|
651
|
+
uninstall: uninstallCodexPlugin,
|
|
652
|
+
doctor: doctorCodexPlugin,
|
|
653
|
+
});
|