@astrosheep/square 0.3.5 → 0.3.7

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.
Files changed (57) hide show
  1. package/codex-plugin/.codex-plugin/plugin.json +3 -2
  2. package/codex-plugin/hooks/hooks.json +3 -14
  3. package/dist/activity-feed.js +26 -18
  4. package/dist/activity.js +10 -10
  5. package/dist/artifact.js +138 -203
  6. package/dist/boundary-presentation.js +77 -0
  7. package/dist/claude-hook.js +4 -94
  8. package/dist/cli/context.js +7 -7
  9. package/dist/cli/maintenance-commands.js +10 -26
  10. package/dist/cli/meta-commands.js +3 -6
  11. package/dist/cli/observation-commands.js +48 -53
  12. package/dist/cli/program.js +4 -4
  13. package/dist/cli/registry.js +5 -5
  14. package/dist/cli/square-commands.js +27 -20
  15. package/dist/cmd/notify-once.js +23 -21
  16. package/dist/codex-hook.js +22 -0
  17. package/dist/compact.js +1 -1
  18. package/dist/decisions.js +61 -88
  19. package/dist/delivery-health.js +104 -210
  20. package/dist/delivery.js +68 -18
  21. package/dist/doctor.js +9 -8
  22. package/dist/harness-claude.js +38 -245
  23. package/dist/harness-codex.js +82 -616
  24. package/dist/harness-stage.js +36 -0
  25. package/dist/harness.js +3 -5
  26. package/dist/help.js +43 -35
  27. package/dist/inbox.js +12 -11
  28. package/dist/index.js +10 -121
  29. package/dist/list.js +1 -1
  30. package/dist/model.js +0 -6
  31. package/dist/notification-failures.js +54 -0
  32. package/dist/notifications.js +47 -62
  33. package/dist/paseo-delivery.js +160 -0
  34. package/dist/paseo-state.js +31 -0
  35. package/dist/paseo-timeline.js +58 -188
  36. package/dist/presentation.js +57 -64
  37. package/dist/presented.js +9 -8
  38. package/dist/registry.js +55 -45
  39. package/dist/runtime.js +27 -84
  40. package/dist/square-application.js +135 -130
  41. package/dist/square-core.js +3 -11
  42. package/dist/stream.js +27 -126
  43. package/dist/wake-sink.js +3 -214
  44. package/dist/watch.js +65 -122
  45. package/extensions/square-opencode.js +8 -73
  46. package/extensions/square-pi.js +8 -132
  47. package/guides/architect.md +3 -3
  48. package/guides/participant.md +25 -16
  49. package/package.json +2 -2
  50. package/skills/brainstorm/SKILL.md +25 -32
  51. package/skills/square/.claude-plugin/plugin.json +1 -1
  52. package/skills/square/SKILL.md +39 -107
  53. package/skills/square/hooks/hooks.json +2 -13
  54. package/skills/square-feedback/SKILL.md +4 -4
  55. package/dist/harness-lifecycle.js +0 -102
  56. package/dist/square-store.js +0 -111
  57. package/dist/terminal.js +0 -125
@@ -1,653 +1,119 @@
1
- import { spawn, spawnSync } from 'node:child_process';
1
+ import { spawnSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { SQUARE_IDENTITY } from './identity.js';
6
- import { reconcileInstall, reconcileUninstall, staleManagedRegistrations, } from './harness-lifecycle.js';
6
+ import { stageReplacement } from './harness-stage.js';
7
7
  export const CODEX_HOOK_COMMAND = SQUARE_IDENTITY.hookCommand;
8
8
  export const SQUARE_CODEX_MARKER = SQUARE_IDENTITY.hookMarker;
9
9
  export const CODEX_PLUGIN_ID = SQUARE_IDENTITY.pluginId;
10
10
  export const CODEX_MARKETPLACE_NAME = SQUARE_IDENTITY.marketplaceName;
11
+ const LEGACY_MARKETPLACES = ['astrosheep-square'];
11
12
  export function codexMarketplaceRoot(homeDir) {
12
13
  return path.join(homeDir, '.square', 'codex', 'marketplaces', CODEX_MARKETPLACE_NAME);
13
14
  }
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
- }
15
+ export function codexPluginRoot(homeDir) { return path.join(codexMarketplaceRoot(homeDir), 'plugins', SQUARE_IDENTITY.pluginName); }
16
+ export function codexPluginHooksPath(homeDir) { return path.join(codexPluginRoot(homeDir), 'hooks', 'hooks.json'); }
17
+ function codexHome(homeDir) { return path.join(homeDir, '.codex'); }
18
+ export function codexHomeHooksPath(homeDir) { return path.join(codexHome(homeDir), 'hooks.json'); }
19
+ export function codexConfigPath(homeDir) { return path.join(codexHome(homeDir), 'config.toml'); }
80
20
  function runCodex(homeDir, args) {
81
21
  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,
22
+ encoding: 'utf8', env: { ...process.env, HOME: homeDir, CODEX_HOME: codexHome(homeDir) }, timeout: 30_000,
90
23
  });
91
24
  if (result.error)
92
25
  throw result.error;
93
- return {
94
- status: result.status ?? 1,
95
- stdout: result.stdout || '',
96
- stderr: result.stderr || '',
97
- };
26
+ return { status: result.status ?? 1, stdout: result.stdout || '', stderr: result.stderr || '' };
98
27
  }
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
- });
28
+ function requireSuccess(result, operation, allowMissing = false) {
29
+ if (result.status === 0 || (allowMissing && /not configured|not installed|not found/i.test(result.stderr)))
30
+ return;
31
+ throw new Error(`Codex ${operation} failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.status}`}`);
115
32
  }
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 });
33
+ export function upsertTomlSectionKey(text, section, key, value) {
34
+ const lines = text.replace(/\n$/, '').split('\n').filter((line, i) => line !== '' || i < text.length);
35
+ const header = `[${section}]`;
36
+ const start = lines.findIndex((line) => line.trim() === header);
37
+ if (start < 0)
38
+ return `${text.trimEnd()}${text.trim() ? '\n\n' : ''}${header}\n${key} = ${value}\n`;
39
+ const end = lines.findIndex((line, i) => i > start && /^\s*\[[^\]]+\]/.test(line));
40
+ const stop = end < 0 ? lines.length : end;
41
+ const match = new RegExp(`^\\s*${key.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&')}\\s*=`);
42
+ const existing = lines.findIndex((line, i) => i > start && i < stop && match.test(line));
43
+ if (existing < 0)
44
+ lines.splice(stop, 0, `${key} = ${value}`);
140
45
  else
141
- writeJsonAtomic(filePath, root);
142
- return true;
46
+ lines[existing] = `${key} = ${value}`;
47
+ return `${lines.join('\n')}\n`;
143
48
  }
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
- };
49
+ function writeAtomic(file, text) {
50
+ fs.mkdirSync(path.dirname(file), { recursive: true });
51
+ const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
52
+ fs.writeFileSync(temp, text, { mode: 0o600 });
53
+ fs.renameSync(temp, file);
157
54
  }
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');
55
+ export async function installCodexPlugin(homeDir, run = runCodex) {
56
+ const root = codexMarketplaceRoot(homeDir);
57
+ const config = codexConfigPath(homeDir);
58
+ const current = fs.existsSync(config) ? fs.readFileSync(config, 'utf8') : '';
59
+ const staged = stageReplacement(root, (stage) => {
60
+ const plugin = path.join(stage, 'plugins', SQUARE_IDENTITY.pluginName);
61
+ fs.cpSync(fileURLToPath(new URL('../codex-plugin/', import.meta.url)), plugin, { recursive: true });
62
+ const skill = path.join(plugin, 'skills', 'square', 'SKILL.md');
167
63
  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
- });
64
+ fs.copyFileSync(path.join(fileURLToPath(new URL('../skills/square/', import.meta.url)), 'SKILL.md'), skill);
65
+ writeAtomic(path.join(stage, '.agents', 'plugins', 'marketplace.json'), `${JSON.stringify({ name: CODEX_MARKETPLACE_NAME, plugins: [{ name: SQUARE_IDENTITY.pluginName, source: { source: 'local', path: './plugins/square' } }] }, null, 2)}\n`);
412
66
  });
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') : '';
67
+ const notes = [];
68
+ try {
426
69
  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');
70
+ requireSuccess(run(homeDir, ['plugin', 'marketplace', 'add', root, '--json']), 'marketplace install', true);
71
+ const installed = run(homeDir, ['plugin', 'add', CODEX_PLUGIN_ID, '--json']);
72
+ requireSuccess(installed, 'plugin install');
73
+ let installedPath;
436
74
  try {
437
- const payload = JSON.parse(result.stdout);
438
- if (typeof payload.installedPath === 'string')
439
- installed.path = payload.installedPath;
75
+ const value = JSON.parse(installed.stdout).installedPath;
76
+ if (typeof value === 'string')
77
+ installedPath = value;
440
78
  }
441
79
  catch {
442
- notes.push('Codex installed the plugin but returned non-JSON output.');
80
+ notes.push('Codex returned non-JSON installation output.');
443
81
  }
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)}`);
82
+ for (const marketplace of LEGACY_MARKETPLACES) {
83
+ const pluginId = `${SQUARE_IDENTITY.pluginName}@${marketplace}`;
84
+ if (!current.includes(`[marketplaces.${marketplace}]`) && !current.includes(`[plugins."${pluginId}"]`))
85
+ continue;
86
+ requireSuccess(run(homeDir, ['plugin', 'remove', pluginId, '--json']), 'legacy plugin removal', true);
87
+ requireSuccess(run(homeDir, ['plugin', 'marketplace', 'remove', marketplace, '--json']), 'legacy marketplace removal', true);
88
+ notes.push(`retired ${pluginId}`);
493
89
  }
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`;
90
+ staged.finalize();
91
+ return { configPath: config, marketplaceRoot: root, pluginRoot: codexPluginRoot(homeDir), ...(installedPath ? { installedPath } : {}), notes };
600
92
  }
601
- catch {
602
- return `○ ${CODEX_PLUGIN_ID} unavailable`;
93
+ catch (error) {
94
+ staged.rollback();
95
+ throw error;
603
96
  }
604
97
  }
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})`;
98
+ export async function uninstallCodexPlugin(homeDir, run = runCodex) {
99
+ requireSuccess(run(homeDir, ['plugin', 'remove', CODEX_PLUGIN_ID, '--json']), 'plugin removal', true);
100
+ requireSuccess(run(homeDir, ['plugin', 'marketplace', 'remove', CODEX_MARKETPLACE_NAME, '--json']), 'marketplace removal', true);
101
+ for (const marketplace of LEGACY_MARKETPLACES) {
102
+ requireSuccess(run(homeDir, ['plugin', 'remove', `${SQUARE_IDENTITY.pluginName}@${marketplace}`, '--json']), 'legacy plugin removal', true);
103
+ requireSuccess(run(homeDir, ['plugin', 'marketplace', 'remove', marketplace, '--json']), 'legacy marketplace removal', true);
616
104
  }
105
+ const root = codexMarketplaceRoot(homeDir);
106
+ fs.rmSync(root, { recursive: true, force: true });
107
+ fs.rmSync(codexHomeHooksPath(homeDir), { force: true });
108
+ return { paths: [root, codexConfigPath(homeDir), codexHomeHooksPath(homeDir)], notes: [] };
617
109
  }
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));
110
+ export async function doctorCodexPlugin(homeDir, run = runCodex) {
111
+ const root = codexMarketplaceRoot(homeDir);
628
112
  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),
113
+ const listed = run(homeDir, ['plugin', 'list', '--json']);
114
+ return [
115
+ /^hooks\s*=\s*true$/m.test(fs.existsSync(config) ? fs.readFileSync(config, 'utf8') : '') ? `✓ features.hooks=true in ${config}` : `○ features.hooks missing in ${config}`,
116
+ fs.existsSync(codexPluginHooksPath(homeDir)) ? `✓ Square plugin hooks ${codexPluginHooksPath(homeDir)}` : `○ Square plugin bundle missing ${root}`,
117
+ listed.status === 0 && listed.stdout.includes(CODEX_PLUGIN_ID) ? `✓ ${CODEX_PLUGIN_ID} installed` : `○ ${CODEX_PLUGIN_ID} unavailable`,
644
118
  ];
645
- if (stale.length > 0)
646
- lines.push(`✕ ${stale.length} stale Square marketplace registration(s)`);
647
- return lines;
648
119
  }
649
- export const codexHarness = Object.freeze({
650
- install: installCodexPlugin,
651
- uninstall: uninstallCodexPlugin,
652
- doctor: doctorCodexPlugin,
653
- });