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