@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
package/dist/harness.js
CHANGED
|
@@ -1,589 +1,109 @@
|
|
|
1
|
-
import { spawn, spawnSync } from 'node:child_process';
|
|
2
1
|
import fs from 'node:fs';
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
function sectionRange(lines, section) {
|
|
13
|
-
const header = new RegExp(`^\\s*\\[${section.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\]\\s*(?:#.*)?$`);
|
|
14
|
-
const start = lines.findIndex((line) => header.test(line));
|
|
15
|
-
if (start < 0)
|
|
16
|
-
return undefined;
|
|
17
|
-
let end = lines.length;
|
|
18
|
-
for (let index = start + 1; index < lines.length; index++) {
|
|
19
|
-
if (/^\s*\[[^\]]+\]\s*(?:#.*)?$/.test(lines[index])) {
|
|
20
|
-
end = index;
|
|
21
|
-
break;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return { start, end };
|
|
25
|
-
}
|
|
26
|
-
export function upsertTomlSectionKey(text, section, key, value) {
|
|
27
|
-
const lines = text === '' ? [] : text.replace(/\n$/, '').split('\n');
|
|
28
|
-
const range = sectionRange(lines, section);
|
|
29
|
-
const assignment = `${key} = ${value}`;
|
|
30
|
-
if (!range) {
|
|
31
|
-
if (lines.length > 0 && lines.at(-1) !== '')
|
|
32
|
-
lines.push('');
|
|
33
|
-
lines.push(`[${section}]`, assignment);
|
|
34
|
-
return `${lines.join('\n')}\n`;
|
|
35
|
-
}
|
|
36
|
-
const keyPattern = new RegExp(`^\\s*${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=`);
|
|
37
|
-
const existing = lines.findIndex((line, index) => index > range.start && index < range.end && keyPattern.test(line));
|
|
38
|
-
if (existing >= 0) {
|
|
39
|
-
if (lines[existing] === assignment)
|
|
40
|
-
return text.endsWith('\n') ? text : `${text}\n`;
|
|
41
|
-
lines[existing] = assignment;
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
lines.splice(range.end, 0, assignment);
|
|
45
|
-
}
|
|
46
|
-
return `${lines.join('\n')}\n`;
|
|
47
|
-
}
|
|
48
|
-
function isSquareGroup(group) {
|
|
49
|
-
if (group === null || typeof group !== 'object')
|
|
50
|
-
return false;
|
|
51
|
-
const handlers = group.hooks;
|
|
52
|
-
return (Array.isArray(handlers) &&
|
|
53
|
-
handlers.some((handler) => handler !== null &&
|
|
54
|
-
typeof handler === 'object' &&
|
|
55
|
-
(handler.command === CODEX_HOOK_COMMAND ||
|
|
56
|
-
handler.statusMessage === SQUARE_CODEX_MARKER)));
|
|
57
|
-
}
|
|
58
|
-
function writeJsonAtomic(filePath, value) {
|
|
59
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
60
|
-
const temp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
61
|
-
fs.writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
62
|
-
fs.renameSync(temp, filePath);
|
|
63
|
-
}
|
|
64
|
-
function writeTextAtomic(filePath, text) {
|
|
65
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
66
|
-
const temp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
67
|
-
fs.writeFileSync(temp, text, { mode: 0o600 });
|
|
68
|
-
fs.renameSync(temp, filePath);
|
|
69
|
-
}
|
|
70
|
-
function backupFile(filePath) {
|
|
71
|
-
if (!fs.existsSync(filePath))
|
|
72
|
-
return undefined;
|
|
73
|
-
const backup = `${filePath}.square-bak`;
|
|
74
|
-
if (!fs.existsSync(backup))
|
|
75
|
-
fs.copyFileSync(filePath, backup);
|
|
76
|
-
return backup;
|
|
77
|
-
}
|
|
78
|
-
export function codexMarketplaceRoot(homeDir) {
|
|
79
|
-
return path.join(homeDir, '.square', 'codex', 'marketplace');
|
|
80
|
-
}
|
|
81
|
-
export function codexPluginRoot(homeDir) {
|
|
82
|
-
return path.join(codexMarketplaceRoot(homeDir), 'plugins', 'square');
|
|
83
|
-
}
|
|
84
|
-
export function codexPluginHooksPath(homeDir) {
|
|
85
|
-
return path.join(codexPluginRoot(homeDir), 'hooks', 'hooks.json');
|
|
86
|
-
}
|
|
87
|
-
function activeCodexHome(homeDir) {
|
|
88
|
-
return process.env['CODEX_HOME']?.trim() || path.join(homeDir, '.codex');
|
|
89
|
-
}
|
|
90
|
-
export function codexHomeHooksPath(homeDir) {
|
|
91
|
-
return path.join(activeCodexHome(homeDir), 'hooks.json');
|
|
92
|
-
}
|
|
93
|
-
export function codexConfigPath(homeDir) {
|
|
94
|
-
return path.join(activeCodexHome(homeDir), 'config.toml');
|
|
95
|
-
}
|
|
96
|
-
function legacyManagedHooksPath(homeDir) {
|
|
97
|
-
return path.join(homeDir, '.square', 'codex', 'hooks.json');
|
|
98
|
-
}
|
|
99
|
-
export function stripSquareCodexHooks(filePath) {
|
|
100
|
-
if (!fs.existsSync(filePath))
|
|
101
|
-
return false;
|
|
102
|
-
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
103
|
-
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
104
|
-
throw new Error(`Invalid Codex hooks file: ${filePath}`);
|
|
105
|
-
}
|
|
106
|
-
const root = parsed;
|
|
107
|
-
const hooksValue = root['hooks'];
|
|
108
|
-
if (hooksValue === undefined)
|
|
109
|
-
return false;
|
|
110
|
-
if (hooksValue === null || typeof hooksValue !== 'object' || Array.isArray(hooksValue)) {
|
|
111
|
-
throw new Error(`Invalid Codex hooks map: ${filePath}`);
|
|
112
|
-
}
|
|
113
|
-
const hooks = hooksValue;
|
|
114
|
-
let changed = false;
|
|
115
|
-
for (const event of Object.keys(hooks)) {
|
|
116
|
-
const value = hooks[event];
|
|
117
|
-
if (!Array.isArray(value))
|
|
118
|
-
continue;
|
|
119
|
-
const next = value.filter((group) => !isSquareGroup(group));
|
|
120
|
-
if (next.length !== value.length) {
|
|
121
|
-
changed = true;
|
|
122
|
-
if (next.length === 0)
|
|
123
|
-
delete hooks[event];
|
|
124
|
-
else
|
|
125
|
-
hooks[event] = next;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
if (!changed)
|
|
129
|
-
return false;
|
|
130
|
-
if (Object.keys(hooks).length === 0)
|
|
131
|
-
fs.rmSync(filePath, { force: true });
|
|
132
|
-
else
|
|
133
|
-
writeJsonAtomic(filePath, root);
|
|
134
|
-
return true;
|
|
135
|
-
}
|
|
136
|
-
function codexPluginSourcePath() {
|
|
137
|
-
return fileURLToPath(new URL('../codex-plugin/', import.meta.url));
|
|
138
|
-
}
|
|
139
|
-
function claudePluginSourcePath() {
|
|
140
|
-
return fileURLToPath(new URL('../skills/square/', import.meta.url));
|
|
141
|
-
}
|
|
142
|
-
function squareSkillSourcePath() {
|
|
143
|
-
return fileURLToPath(new URL('../skills/square/', import.meta.url));
|
|
144
|
-
}
|
|
145
|
-
function runClaudeCommand(homeDir, args) {
|
|
146
|
-
const result = spawnSync(process.env['SQUARE_CLAUDE_BIN'] || 'claude', args, {
|
|
147
|
-
encoding: 'utf8',
|
|
148
|
-
env: {
|
|
149
|
-
...process.env,
|
|
150
|
-
HOME: homeDir,
|
|
151
|
-
CLAUDE_CONFIG_DIR: path.join(homeDir, '.claude'),
|
|
152
|
-
},
|
|
153
|
-
maxBuffer: 4 * 1024 * 1024,
|
|
154
|
-
timeout: 30_000,
|
|
155
|
-
});
|
|
156
|
-
if (result.error)
|
|
157
|
-
throw result.error;
|
|
158
|
-
return {
|
|
159
|
-
status: result.status ?? 1,
|
|
160
|
-
stdout: result.stdout || '',
|
|
161
|
-
stderr: result.stderr || '',
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
function runCodexCommand(homeDir, args) {
|
|
165
|
-
const result = spawnSync(process.env['SQUARE_CODEX_BIN'] || 'codex', args, {
|
|
166
|
-
encoding: 'utf8',
|
|
167
|
-
env: {
|
|
168
|
-
...process.env,
|
|
169
|
-
HOME: homeDir,
|
|
170
|
-
CODEX_HOME: activeCodexHome(homeDir),
|
|
171
|
-
},
|
|
172
|
-
maxBuffer: 4 * 1024 * 1024,
|
|
173
|
-
timeout: 30_000,
|
|
174
|
-
});
|
|
175
|
-
if (result.error)
|
|
176
|
-
throw result.error;
|
|
177
|
-
return {
|
|
178
|
-
status: result.status ?? 1,
|
|
179
|
-
stdout: result.stdout || '',
|
|
180
|
-
stderr: result.stderr || '',
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
function requireCodexSuccess(result, operation) {
|
|
184
|
-
if (result.status === 0)
|
|
185
|
-
return;
|
|
186
|
-
const detail = result.stderr.trim() || result.stdout.trim() || `exit ${result.status}`;
|
|
187
|
-
throw new Error(`Codex ${operation} failed: ${detail}`);
|
|
188
|
-
}
|
|
189
|
-
function pluginHooksFromList(result) {
|
|
190
|
-
const entries = result?.data;
|
|
191
|
-
if (!Array.isArray(entries))
|
|
192
|
-
throw new Error('Codex hooks/list returned invalid data.');
|
|
193
|
-
const hooks = [];
|
|
194
|
-
for (const entry of entries) {
|
|
195
|
-
if (!Array.isArray(entry.hooks))
|
|
196
|
-
continue;
|
|
197
|
-
for (const value of entry.hooks) {
|
|
198
|
-
if (value === null || typeof value !== 'object')
|
|
199
|
-
continue;
|
|
200
|
-
const hook = value;
|
|
201
|
-
if (hook['pluginId'] !== CODEX_PLUGIN_ID)
|
|
202
|
-
continue;
|
|
203
|
-
if (typeof hook['key'] !== 'string' ||
|
|
204
|
-
typeof hook['eventName'] !== 'string' ||
|
|
205
|
-
typeof hook['enabled'] !== 'boolean' ||
|
|
206
|
-
typeof hook['currentHash'] !== 'string' ||
|
|
207
|
-
typeof hook['trustStatus'] !== 'string') {
|
|
208
|
-
throw new Error('Codex hooks/list returned an invalid Square hook.');
|
|
209
|
-
}
|
|
210
|
-
hooks.push({
|
|
211
|
-
key: hook['key'],
|
|
212
|
-
eventName: hook['eventName'],
|
|
213
|
-
pluginId: CODEX_PLUGIN_ID,
|
|
214
|
-
enabled: hook['enabled'],
|
|
215
|
-
currentHash: hook['currentHash'],
|
|
216
|
-
trustStatus: hook['trustStatus'],
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
return hooks;
|
|
221
|
-
}
|
|
222
|
-
function expectedPluginHooks(hooks) {
|
|
223
|
-
if (hooks.length !== 2 || hooks.some((hook) => !hook.enabled))
|
|
224
|
-
return false;
|
|
225
|
-
const events = hooks.map((hook) => hook.eventName).sort();
|
|
226
|
-
return events[0] === 'stop' && events[1] === 'userPromptSubmit';
|
|
227
|
-
}
|
|
228
|
-
export function inspectCodexPluginHooks(homeDir, trust) {
|
|
229
|
-
return new Promise((resolve, reject) => {
|
|
230
|
-
const child = spawn(process.env['SQUARE_CODEX_BIN'] || 'codex', ['app-server', '--stdio'], {
|
|
231
|
-
env: {
|
|
232
|
-
...process.env,
|
|
233
|
-
HOME: homeDir,
|
|
234
|
-
CODEX_HOME: activeCodexHome(homeDir),
|
|
235
|
-
},
|
|
236
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
237
|
-
});
|
|
238
|
-
let stdout = '';
|
|
239
|
-
let stderr = '';
|
|
240
|
-
let settled = false;
|
|
241
|
-
const finish = (error, hooks) => {
|
|
242
|
-
if (settled)
|
|
243
|
-
return;
|
|
244
|
-
settled = true;
|
|
245
|
-
clearTimeout(timer);
|
|
246
|
-
child.kill('SIGTERM');
|
|
247
|
-
if (error)
|
|
248
|
-
reject(error);
|
|
249
|
-
else
|
|
250
|
-
resolve(hooks ?? []);
|
|
251
|
-
};
|
|
252
|
-
const timer = setTimeout(() => finish(new Error(`Codex hooks API timed out${stderr.trim() ? `: ${stderr.trim()}` : ''}`)), 30_000);
|
|
253
|
-
const send = (message) => child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
254
|
-
const failForMessage = (message) => {
|
|
255
|
-
if (!message.error)
|
|
256
|
-
return false;
|
|
257
|
-
const detail = typeof message.error.message === 'string' ? message.error.message : JSON.stringify(message.error);
|
|
258
|
-
finish(new Error(`Codex hooks API failed: ${detail}`));
|
|
259
|
-
return true;
|
|
260
|
-
};
|
|
261
|
-
const requestHooks = (id) => send({ id, method: 'hooks/list', params: { cwds: [process.cwd()] } });
|
|
262
|
-
child.on('error', (error) => finish(error));
|
|
263
|
-
child.stdin.on('error', (error) => finish(error));
|
|
264
|
-
child.stderr.on('data', (chunk) => {
|
|
265
|
-
stderr += String(chunk);
|
|
266
|
-
});
|
|
267
|
-
child.stdout.on('data', (chunk) => {
|
|
268
|
-
stdout += String(chunk);
|
|
269
|
-
while (stdout.includes('\n')) {
|
|
270
|
-
const newline = stdout.indexOf('\n');
|
|
271
|
-
const line = stdout.slice(0, newline);
|
|
272
|
-
stdout = stdout.slice(newline + 1);
|
|
273
|
-
if (!line.trim())
|
|
274
|
-
continue;
|
|
275
|
-
let message;
|
|
276
|
-
try {
|
|
277
|
-
message = JSON.parse(line);
|
|
278
|
-
}
|
|
279
|
-
catch {
|
|
280
|
-
finish(new Error(`Codex hooks API returned invalid JSON: ${line}`));
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
if (failForMessage(message))
|
|
284
|
-
return;
|
|
285
|
-
if (message.id === 0) {
|
|
286
|
-
send({ method: 'initialized', params: {} });
|
|
287
|
-
requestHooks(1);
|
|
288
|
-
continue;
|
|
289
|
-
}
|
|
290
|
-
if (message.id === 1) {
|
|
291
|
-
let hooks;
|
|
292
|
-
try {
|
|
293
|
-
hooks = pluginHooksFromList(message.result);
|
|
294
|
-
}
|
|
295
|
-
catch (error) {
|
|
296
|
-
finish(error instanceof Error ? error : new Error(String(error)));
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
if (!trust) {
|
|
300
|
-
finish(undefined, hooks);
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
if (!expectedPluginHooks(hooks)) {
|
|
304
|
-
finish(new Error(`Codex discovered an unexpected hook set for ${CODEX_PLUGIN_ID}.`));
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
const updates = Object.fromEntries(hooks.map((hook) => [hook.key, { trusted_hash: hook.currentHash }]));
|
|
308
|
-
send({
|
|
309
|
-
id: 2,
|
|
310
|
-
method: 'config/batchWrite',
|
|
311
|
-
params: {
|
|
312
|
-
edits: [{ keyPath: 'hooks.state', value: updates, mergeStrategy: 'upsert' }],
|
|
313
|
-
reloadUserConfig: true,
|
|
314
|
-
},
|
|
315
|
-
});
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
if (message.id === 2) {
|
|
319
|
-
requestHooks(3);
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
if (message.id === 3) {
|
|
323
|
-
try {
|
|
324
|
-
finish(undefined, pluginHooksFromList(message.result));
|
|
325
|
-
}
|
|
326
|
-
catch (error) {
|
|
327
|
-
finish(error instanceof Error ? error : new Error(String(error)));
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
});
|
|
332
|
-
child.on('exit', (code) => {
|
|
333
|
-
if (!settled) {
|
|
334
|
-
finish(new Error(`Codex hooks API exited ${code ?? 'without status'}${stderr.trim() ? `: ${stderr.trim()}` : ''}`));
|
|
335
|
-
}
|
|
336
|
-
});
|
|
337
|
-
send({
|
|
338
|
-
id: 0,
|
|
339
|
-
method: 'initialize',
|
|
340
|
-
params: {
|
|
341
|
-
clientInfo: {
|
|
342
|
-
name: SQUARE_IDENTITY.clientName,
|
|
343
|
-
title: SQUARE_IDENTITY.productName,
|
|
344
|
-
version: SQUARE_IDENTITY.packageVersion,
|
|
345
|
-
},
|
|
346
|
-
},
|
|
347
|
-
});
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
function claudeMarketplaceDocument() {
|
|
351
|
-
return {
|
|
352
|
-
name: CLAUDE_MARKETPLACE_NAME,
|
|
353
|
-
description: `${SQUARE_IDENTITY.productName} harness integrations`,
|
|
354
|
-
owner: { name: SQUARE_IDENTITY.productName },
|
|
355
|
-
plugins: [
|
|
356
|
-
{
|
|
357
|
-
name: SQUARE_IDENTITY.pluginName,
|
|
358
|
-
description: `Native Claude Code turn-boundary delivery for ${SQUARE_IDENTITY.productName}`,
|
|
359
|
-
source: './plugins/square',
|
|
360
|
-
},
|
|
361
|
-
],
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
function marketplaceDocument() {
|
|
365
|
-
return {
|
|
366
|
-
name: CODEX_MARKETPLACE_NAME,
|
|
367
|
-
interface: { displayName: SQUARE_IDENTITY.productName },
|
|
368
|
-
plugins: [
|
|
369
|
-
{
|
|
370
|
-
name: SQUARE_IDENTITY.pluginName,
|
|
371
|
-
source: { source: 'local', path: './plugins/square' },
|
|
372
|
-
policy: { installation: 'AVAILABLE', authentication: 'ON_USE' },
|
|
373
|
-
category: 'Developer Tools',
|
|
374
|
-
},
|
|
375
|
-
],
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
|
-
function stageClaudePlugin(homeDir) {
|
|
379
|
-
const marketplaceRoot = path.join(homeDir, '.square', 'claude', 'marketplace');
|
|
380
|
-
const stage = `${marketplaceRoot}.${process.pid}.${Date.now()}.stage`;
|
|
381
|
-
const pluginRoot = path.join(stage, 'plugins', 'square');
|
|
382
|
-
fs.mkdirSync(path.dirname(marketplaceRoot), { recursive: true });
|
|
383
|
-
fs.rmSync(stage, { recursive: true, force: true });
|
|
384
|
-
try {
|
|
385
|
-
fs.cpSync(claudePluginSourcePath(), pluginRoot, { recursive: true });
|
|
386
|
-
writeJsonAtomic(path.join(stage, '.claude-plugin', 'marketplace.json'), claudeMarketplaceDocument());
|
|
387
|
-
fs.rmSync(marketplaceRoot, { recursive: true, force: true });
|
|
388
|
-
fs.renameSync(stage, marketplaceRoot);
|
|
389
|
-
}
|
|
390
|
-
catch (error) {
|
|
391
|
-
fs.rmSync(stage, { recursive: true, force: true });
|
|
392
|
-
throw error;
|
|
393
|
-
}
|
|
394
|
-
return { marketplaceRoot, pluginRoot: path.join(marketplaceRoot, 'plugins', 'square') };
|
|
395
|
-
}
|
|
396
|
-
function stageCodexPlugin(homeDir) {
|
|
397
|
-
const marketplaceRoot = codexMarketplaceRoot(homeDir);
|
|
398
|
-
const parent = path.dirname(marketplaceRoot);
|
|
399
|
-
const stage = `${marketplaceRoot}.${process.pid}.${Date.now()}.stage`;
|
|
400
|
-
const pluginRoot = path.join(stage, 'plugins', 'square');
|
|
401
|
-
fs.mkdirSync(parent, { recursive: true });
|
|
402
|
-
fs.rmSync(stage, { recursive: true, force: true });
|
|
2
|
+
import { doctorDeliveryHealth } from './delivery-health.js';
|
|
3
|
+
import { CLAUDE_MARKETPLACE_NAME, CLAUDE_PLUGIN_ID, claudeHarness, doctorClaudePlugin, installClaudePlugin, uninstallClaudePlugin, } from './harness-claude.js';
|
|
4
|
+
import { CODEX_HOOK_COMMAND, CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_ID, SQUARE_CODEX_MARKER, codexHarness, codexConfigPath, codexHomeHooksPath, codexMarketplaceRoot, codexPluginHooksPath, codexPluginRoot, doctorCodexPlugin, installCodexPlugin, inspectCodexPluginHooks, stripSquareCodexHooks, uninstallCodexPlugin, upsertTomlSectionKey, } from './harness-codex.js';
|
|
5
|
+
import { doctorHarnessLinks, installHarnessLinks, opencodeExtensionLink, piExtensionLink, skillLinks, uninstallHarnessLinks, verifyOpenCodeRuntime, } from './harness-links.js';
|
|
6
|
+
export { CLAUDE_MARKETPLACE_NAME, CLAUDE_PLUGIN_ID, CODEX_HOOK_COMMAND, CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_ID, SQUARE_CODEX_MARKER, codexConfigPath, codexHomeHooksPath, codexMarketplaceRoot, codexPluginHooksPath, codexPluginRoot, doctorClaudePlugin, doctorCodexPlugin, installClaudePlugin, installCodexPlugin, inspectCodexPluginHooks, stripSquareCodexHooks, uninstallClaudePlugin, uninstallCodexPlugin, upsertTomlSectionKey, };
|
|
7
|
+
function result(lines, notes = []) {
|
|
8
|
+
return { lines, notes };
|
|
9
|
+
}
|
|
10
|
+
async function doctorHost(label, inspect) {
|
|
403
11
|
try {
|
|
404
|
-
|
|
405
|
-
const stagedSkill = path.join(pluginRoot, 'skills', 'square', 'SKILL.md');
|
|
406
|
-
fs.mkdirSync(path.dirname(stagedSkill), { recursive: true });
|
|
407
|
-
fs.copyFileSync(path.join(squareSkillSourcePath(), 'SKILL.md'), stagedSkill);
|
|
408
|
-
writeJsonAtomic(path.join(stage, '.agents', 'plugins', 'marketplace.json'), marketplaceDocument());
|
|
409
|
-
fs.rmSync(marketplaceRoot, { recursive: true, force: true });
|
|
410
|
-
fs.renameSync(stage, marketplaceRoot);
|
|
12
|
+
return result(await inspect());
|
|
411
13
|
}
|
|
412
14
|
catch (error) {
|
|
413
|
-
|
|
414
|
-
throw error;
|
|
15
|
+
return result([`○ ${label} doctor unavailable (${error instanceof Error ? error.message : String(error)})`]);
|
|
415
16
|
}
|
|
416
|
-
return { marketplaceRoot, pluginRoot: codexPluginRoot(homeDir) };
|
|
417
|
-
}
|
|
418
|
-
function pluginHooksInstalled(filePath) {
|
|
419
|
-
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
420
|
-
return (Array.isArray(parsed.hooks?.UserPromptSubmit) &&
|
|
421
|
-
parsed.hooks.UserPromptSubmit.some(isSquareGroup) &&
|
|
422
|
-
Array.isArray(parsed.hooks?.Stop) &&
|
|
423
|
-
parsed.hooks.Stop.some(isSquareGroup));
|
|
424
17
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const staged = stageClaudePlugin(homeDir);
|
|
428
|
-
requireCodexSuccess(run(homeDir, ['plugin', 'marketplace', 'add', staged.marketplaceRoot]), 'Claude marketplace install');
|
|
429
|
-
requireCodexSuccess(run(homeDir, ['plugin', 'install', CLAUDE_PLUGIN_ID]), 'Claude plugin install');
|
|
430
|
-
requireCodexSuccess(run(homeDir, ['plugin', 'update', CLAUDE_PLUGIN_ID]), 'Claude plugin update');
|
|
431
|
-
const legacySkill = path.join(homeDir, '.claude', 'skills', 'square');
|
|
432
|
-
if (lstatMaybeForHarness(legacySkill)?.isSymbolicLink())
|
|
433
|
-
fs.rmSync(legacySkill, { force: true });
|
|
434
|
-
return staged;
|
|
18
|
+
function openCodeLinks(homeDir) {
|
|
19
|
+
return [opencodeExtensionLink(homeDir), ...skillLinks(homeDir, ['.agents'])];
|
|
435
20
|
}
|
|
436
|
-
function
|
|
437
|
-
|
|
438
|
-
return
|
|
439
|
-
}
|
|
440
|
-
catch (error) {
|
|
441
|
-
if (error.code === 'ENOENT')
|
|
442
|
-
return undefined;
|
|
443
|
-
throw error;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
/** Install the Square Codex plugin and migrate away from direct home hook edits. */
|
|
447
|
-
export async function installCodexPlugin(homeDir, run = runCodexCommand, hooksRuntime = inspectCodexPluginHooks) {
|
|
448
|
-
const configPath = codexConfigPath(homeDir);
|
|
449
|
-
const activeHooksPath = codexHomeHooksPath(homeDir);
|
|
450
|
-
const notes = [];
|
|
451
|
-
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
452
|
-
backupFile(configPath);
|
|
453
|
-
backupFile(activeHooksPath);
|
|
454
|
-
const configText = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : '';
|
|
455
|
-
const normalizedConfig = configText === '' || configText.endsWith('\n') ? configText : `${configText}\n`;
|
|
456
|
-
const nextConfig = upsertTomlSectionKey(configText, 'features', 'hooks', 'true');
|
|
457
|
-
if (nextConfig !== normalizedConfig)
|
|
458
|
-
writeTextAtomic(configPath, nextConfig);
|
|
459
|
-
const staged = stageCodexPlugin(homeDir);
|
|
460
|
-
const marketplace = run(homeDir, [
|
|
461
|
-
'plugin',
|
|
462
|
-
'marketplace',
|
|
463
|
-
'add',
|
|
464
|
-
staged.marketplaceRoot,
|
|
465
|
-
'--json',
|
|
466
|
-
]);
|
|
467
|
-
requireCodexSuccess(marketplace, 'marketplace install');
|
|
468
|
-
const installed = run(homeDir, ['plugin', 'add', CODEX_PLUGIN_ID, '--json']);
|
|
469
|
-
requireCodexSuccess(installed, 'plugin install');
|
|
470
|
-
let installedPath;
|
|
21
|
+
function readableSquarePath(squarePath) {
|
|
22
|
+
if (squarePath === undefined)
|
|
23
|
+
return false;
|
|
471
24
|
try {
|
|
472
|
-
|
|
473
|
-
if (typeof result.installedPath === 'string')
|
|
474
|
-
installedPath = result.installedPath;
|
|
25
|
+
return fs.statSync(squarePath).isFile() && (fs.accessSync(squarePath, fs.constants.R_OK), true);
|
|
475
26
|
}
|
|
476
27
|
catch {
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
const runtimeHooks = await hooksRuntime(homeDir, true);
|
|
480
|
-
if (!expectedPluginHooks(runtimeHooks)) {
|
|
481
|
-
throw new Error(`Codex did not discover both Square plugin hooks for ${CODEX_PLUGIN_ID}.`);
|
|
482
|
-
}
|
|
483
|
-
const untrusted = runtimeHooks.filter((hook) => hook.trustStatus !== 'trusted');
|
|
484
|
-
if (untrusted.length > 0) {
|
|
485
|
-
throw new Error(`Codex did not trust Square plugin hooks: ${untrusted.map((hook) => hook.key).join(', ')}`);
|
|
486
|
-
}
|
|
487
|
-
try {
|
|
488
|
-
if (stripSquareCodexHooks(activeHooksPath))
|
|
489
|
-
notes.push(`Removed legacy Square handlers from ${activeHooksPath}.`);
|
|
490
|
-
}
|
|
491
|
-
catch (error) {
|
|
492
|
-
notes.push(`Legacy hooks were left untouched: ${error instanceof Error ? error.message : String(error)}`);
|
|
493
|
-
}
|
|
494
|
-
fs.rmSync(legacyManagedHooksPath(homeDir), { force: true });
|
|
495
|
-
return {
|
|
496
|
-
configPath,
|
|
497
|
-
marketplaceRoot: staged.marketplaceRoot,
|
|
498
|
-
pluginRoot: staged.pluginRoot,
|
|
499
|
-
...(installedPath ? { installedPath } : {}),
|
|
500
|
-
notes,
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
export function uninstallCodexPlugin(homeDir, run = runCodexCommand) {
|
|
504
|
-
const notes = [];
|
|
505
|
-
const removed = run(homeDir, ['plugin', 'remove', CODEX_PLUGIN_ID, '--json']);
|
|
506
|
-
requireCodexSuccess(removed, 'plugin removal');
|
|
507
|
-
const marketplace = run(homeDir, ['plugin', 'marketplace', 'remove', CODEX_MARKETPLACE_NAME, '--json']);
|
|
508
|
-
if (marketplace.status !== 0 && !marketplace.stderr.includes('is not configured or installed')) {
|
|
509
|
-
requireCodexSuccess(marketplace, 'marketplace removal');
|
|
510
|
-
}
|
|
511
|
-
const activeHooksPath = codexHomeHooksPath(homeDir);
|
|
512
|
-
if (stripSquareCodexHooks(activeHooksPath))
|
|
513
|
-
notes.push(`Removed legacy Square handlers from ${activeHooksPath}.`);
|
|
514
|
-
const marketplaceRoot = codexMarketplaceRoot(homeDir);
|
|
515
|
-
const managedHooksPath = legacyManagedHooksPath(homeDir);
|
|
516
|
-
fs.rmSync(marketplaceRoot, { recursive: true, force: true });
|
|
517
|
-
fs.rmSync(managedHooksPath, { force: true });
|
|
518
|
-
return { paths: [marketplaceRoot, activeHooksPath, managedHooksPath], notes };
|
|
519
|
-
}
|
|
520
|
-
export async function doctorCodexPlugin(homeDir, run = runCodexCommand, hooksRuntime = inspectCodexPluginHooks) {
|
|
521
|
-
const lines = [];
|
|
522
|
-
const configPath = codexConfigPath(homeDir);
|
|
523
|
-
const configText = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : '';
|
|
524
|
-
const features = sectionRange(configText.split('\n'), 'features');
|
|
525
|
-
const hooksEnabled = features !== undefined &&
|
|
526
|
-
configText
|
|
527
|
-
.split('\n')
|
|
528
|
-
.slice(features.start + 1, features.end)
|
|
529
|
-
.some((line) => /^\s*hooks\s*=\s*true\s*(?:#.*)?$/.test(line));
|
|
530
|
-
lines.push(hooksEnabled ? `✓ features.hooks=true in ${configPath}` : `○ features.hooks missing in ${configPath}`);
|
|
531
|
-
const hooksPath = codexPluginHooksPath(homeDir);
|
|
532
|
-
if (!fs.existsSync(hooksPath)) {
|
|
533
|
-
lines.push(`○ Square plugin bundle missing ${codexPluginRoot(homeDir)}`);
|
|
534
|
-
}
|
|
535
|
-
else {
|
|
536
|
-
try {
|
|
537
|
-
lines.push(pluginHooksInstalled(hooksPath)
|
|
538
|
-
? `✓ Square plugin hooks ${hooksPath}`
|
|
539
|
-
: `✕ Square plugin hooks incomplete ${hooksPath}`);
|
|
540
|
-
}
|
|
541
|
-
catch {
|
|
542
|
-
lines.push(`✕ invalid Square plugin hooks ${hooksPath}`);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
const listed = run(homeDir, ['plugin', 'list', '--marketplace', CODEX_MARKETPLACE_NAME, '--json']);
|
|
546
|
-
if (listed.status !== 0) {
|
|
547
|
-
lines.push(`○ ${CODEX_PLUGIN_ID} unavailable (${listed.stderr.trim() || `exit ${listed.status}`})`);
|
|
548
|
-
}
|
|
549
|
-
else {
|
|
550
|
-
try {
|
|
551
|
-
const payload = JSON.parse(listed.stdout);
|
|
552
|
-
const plugin = payload.installed?.find((entry) => entry.pluginId === CODEX_PLUGIN_ID);
|
|
553
|
-
lines.push(plugin?.installed === true && plugin.enabled === true
|
|
554
|
-
? `✓ ${CODEX_PLUGIN_ID} installed and enabled`
|
|
555
|
-
: `○ ${CODEX_PLUGIN_ID} is not installed and enabled`);
|
|
556
|
-
}
|
|
557
|
-
catch {
|
|
558
|
-
lines.push('✕ Codex returned invalid plugin inventory JSON');
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
try {
|
|
562
|
-
const runtimeHooks = await hooksRuntime(homeDir, false);
|
|
563
|
-
if (!expectedPluginHooks(runtimeHooks)) {
|
|
564
|
-
lines.push(`✕ Codex runtime did not discover both ${CODEX_PLUGIN_ID} hooks`);
|
|
565
|
-
}
|
|
566
|
-
else {
|
|
567
|
-
const untrusted = runtimeHooks.filter((hook) => hook.trustStatus !== 'trusted');
|
|
568
|
-
lines.push(untrusted.length === 0
|
|
569
|
-
? `✓ ${CODEX_PLUGIN_ID} hooks trusted by Codex`
|
|
570
|
-
: `✕ ${untrusted.length} ${CODEX_PLUGIN_ID} hooks need trust`);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
catch (error) {
|
|
574
|
-
lines.push(`✕ Codex hooks runtime unavailable (${error instanceof Error ? error.message : String(error)})`);
|
|
575
|
-
}
|
|
576
|
-
const activeHooksPath = codexHomeHooksPath(homeDir);
|
|
577
|
-
if (fs.existsSync(activeHooksPath)) {
|
|
578
|
-
try {
|
|
579
|
-
const parsed = JSON.parse(fs.readFileSync(activeHooksPath, 'utf8'));
|
|
580
|
-
const legacy = Object.values(parsed.hooks ?? {}).some((groups) => Array.isArray(groups) && groups.some(isSquareGroup));
|
|
581
|
-
if (legacy)
|
|
582
|
-
lines.push(`▲ legacy Square handlers still present in ${activeHooksPath}`);
|
|
583
|
-
}
|
|
584
|
-
catch {
|
|
585
|
-
lines.push(`✕ invalid hooks file ${activeHooksPath}`);
|
|
586
|
-
}
|
|
28
|
+
return false;
|
|
587
29
|
}
|
|
588
|
-
return lines;
|
|
589
30
|
}
|
|
31
|
+
const TARGETS = [
|
|
32
|
+
{
|
|
33
|
+
name: 'skills',
|
|
34
|
+
capabilities: ['install', 'uninstall', 'doctor'],
|
|
35
|
+
install: ({ homeDir, force }) => result(installHarnessLinks(skillLinks(homeDir), force)),
|
|
36
|
+
uninstall: ({ homeDir }) => result(uninstallHarnessLinks(skillLinks(homeDir))),
|
|
37
|
+
doctor: ({ homeDir }) => result(doctorHarnessLinks(skillLinks(homeDir))),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'claude',
|
|
41
|
+
capabilities: ['install', 'uninstall', 'doctor'],
|
|
42
|
+
async install({ homeDir }) {
|
|
43
|
+
const installed = await installClaudePlugin(homeDir);
|
|
44
|
+
return result([installed.marketplaceRoot, installed.pluginRoot]);
|
|
45
|
+
},
|
|
46
|
+
async uninstall({ homeDir }) {
|
|
47
|
+
const removed = await uninstallClaudePlugin(homeDir);
|
|
48
|
+
return result(removed.paths, removed.notes);
|
|
49
|
+
},
|
|
50
|
+
async doctor({ homeDir }) { return doctorHost('Claude', () => doctorClaudePlugin(homeDir)); },
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'codex',
|
|
54
|
+
capabilities: ['install', 'uninstall', 'doctor'],
|
|
55
|
+
async install({ homeDir }) {
|
|
56
|
+
const installed = await installCodexPlugin(homeDir);
|
|
57
|
+
const lines = [
|
|
58
|
+
installed.configPath,
|
|
59
|
+
installed.marketplaceRoot,
|
|
60
|
+
installed.pluginRoot,
|
|
61
|
+
installed.installedPath,
|
|
62
|
+
].filter((line) => line !== undefined);
|
|
63
|
+
return result(lines, installed.notes);
|
|
64
|
+
},
|
|
65
|
+
async uninstall({ homeDir }) {
|
|
66
|
+
const removed = await uninstallCodexPlugin(homeDir);
|
|
67
|
+
return result(removed.paths, removed.notes);
|
|
68
|
+
},
|
|
69
|
+
async doctor({ homeDir }) { return doctorHost('Codex', () => doctorCodexPlugin(homeDir)); },
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'opencode',
|
|
73
|
+
capabilities: ['install', 'uninstall', 'doctor'],
|
|
74
|
+
install: ({ homeDir, force }) => result(installHarnessLinks(openCodeLinks(homeDir), force)),
|
|
75
|
+
uninstall: ({ homeDir }) => result(uninstallHarnessLinks(openCodeLinks(homeDir))),
|
|
76
|
+
doctor: ({ homeDir }) => result([...doctorHarnessLinks(openCodeLinks(homeDir)), verifyOpenCodeRuntime(homeDir)]),
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'pi',
|
|
80
|
+
capabilities: ['install', 'uninstall', 'doctor'],
|
|
81
|
+
install: ({ homeDir, force }) => result(installHarnessLinks([piExtensionLink(homeDir)], force)),
|
|
82
|
+
uninstall: ({ homeDir }) => result(uninstallHarnessLinks([piExtensionLink(homeDir)])),
|
|
83
|
+
doctor: ({ homeDir }) => result(doctorHarnessLinks([piExtensionLink(homeDir)])),
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'delivery',
|
|
87
|
+
capabilities: ['doctor'],
|
|
88
|
+
doctor: ({ squarePath }) => result(readableSquarePath(squarePath)
|
|
89
|
+
? doctorDeliveryHealth(squarePath)
|
|
90
|
+
: ['○ delivery health skipped (no readable square path)']),
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
export function harnessTargets() {
|
|
94
|
+
return TARGETS;
|
|
95
|
+
}
|
|
96
|
+
export function requireHarnessCapability(target, action) {
|
|
97
|
+
const found = TARGETS.find((candidate) => candidate.name === target);
|
|
98
|
+
if (found === undefined || !found.capabilities.includes(action))
|
|
99
|
+
throw new Error(`Unsupported harness capability: ${action} ${target}`);
|
|
100
|
+
return found;
|
|
101
|
+
}
|
|
102
|
+
export async function executeHarnessTarget(targetName, action, context) {
|
|
103
|
+
const target = requireHarnessCapability(targetName, action);
|
|
104
|
+
const capability = target[action];
|
|
105
|
+
if (capability === undefined)
|
|
106
|
+
throw new Error(`Unsupported harness capability: ${action} ${targetName}`);
|
|
107
|
+
return capability(context);
|
|
108
|
+
}
|
|
109
|
+
export const harnessHosts = Object.freeze({ claude: claudeHarness, codex: codexHarness });
|