@astrosheep/square 0.3.4 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/activity.js +15 -13
- package/dist/cli/context.js +143 -0
- package/dist/cli/harness-command.js +50 -0
- package/dist/cli/maintenance-commands.js +92 -0
- package/dist/cli/meta-commands.js +31 -0
- package/dist/cli/observation-commands.js +461 -0
- package/dist/cli/program.js +48 -0
- package/dist/cli/registry.js +40 -0
- package/dist/cli/square-commands.js +221 -0
- package/dist/compact.js +5 -18
- package/dist/harness-claude.js +275 -0
- package/dist/harness-codex.js +653 -0
- package/dist/harness-lifecycle.js +102 -0
- package/dist/harness-links.js +123 -0
- package/dist/harness.js +97 -577
- package/dist/help.js +2 -1
- package/dist/index.js +45 -32
- package/dist/runtime.js +0 -54
- package/dist/square-application.js +259 -0
- package/dist/square-store.js +111 -0
- package/dist/square.js +5 -1362
- package/dist/watch.js +17 -19
- package/package.json +1 -1
- package/skills/square/.claude-plugin/plugin.json +1 -1
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { cmdActivity } from '../activity.js';
|
|
2
|
+
import { loadSquare } from '../artifact.js';
|
|
3
|
+
import { cmdCompact } from '../compact.js';
|
|
4
|
+
import { SquareError, formatHardCap, validateParticipantName, } from '../model.js';
|
|
5
|
+
import { participantCommandPrefix, quoteShell, renderEventCli, renderPublicTail, withJoinNextOutput, withPathOutput, } from '../presentation.js';
|
|
6
|
+
import { hasAutomaticDeliveryIdentity, recordLocalDone, recordLocalJoin } from '../registry.js';
|
|
7
|
+
import { inSquareCount, isCurrentlyJoined, nowMs, resolveRosterName } from '../runtime.js';
|
|
8
|
+
import { createSquare, execute } from '../square-application.js';
|
|
9
|
+
import { fail, parseHardCap, parsePositiveInteger, readPipedBodyFallback, readStdinSync, requireParticipant, requireValue, resolveBody, usage, } from './context.js';
|
|
10
|
+
function parseBuild(argv) {
|
|
11
|
+
const options = { force: false };
|
|
12
|
+
for (let index = 0; index < argv.length; index++) {
|
|
13
|
+
const flag = argv[index];
|
|
14
|
+
switch (flag) {
|
|
15
|
+
case '--cap':
|
|
16
|
+
options.hardCap = parseHardCap(requireValue(argv, index, flag));
|
|
17
|
+
index += 1;
|
|
18
|
+
break;
|
|
19
|
+
case '--template':
|
|
20
|
+
options.template = requireValue(argv, index, flag);
|
|
21
|
+
index += 1;
|
|
22
|
+
break;
|
|
23
|
+
case '--throttle':
|
|
24
|
+
case '--throttle-per-minute':
|
|
25
|
+
options.throttlePerMinute = parsePositiveInteger(requireValue(argv, index, flag), flag);
|
|
26
|
+
index += 1;
|
|
27
|
+
break;
|
|
28
|
+
case '--force':
|
|
29
|
+
case '-f':
|
|
30
|
+
options.force = true;
|
|
31
|
+
break;
|
|
32
|
+
default:
|
|
33
|
+
fail(`Unknown build option: ${flag}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (options.template !== undefined && !/^[a-zA-Z0-9-]+$/.test(options.template)) {
|
|
37
|
+
fail('Invalid template name: only letters, digits, and hyphens allowed.');
|
|
38
|
+
}
|
|
39
|
+
if (options.hardCap === undefined)
|
|
40
|
+
fail('Missing required build option: --cap must be a positive integer or -1.');
|
|
41
|
+
if (options.throttlePerMinute !== undefined && options.throttlePerMinute <= 0) {
|
|
42
|
+
fail('Invalid build option: --throttle must be a positive integer.');
|
|
43
|
+
}
|
|
44
|
+
const snippet = readStdinSync();
|
|
45
|
+
if (snippet.trim() === '')
|
|
46
|
+
fail('Missing Markdown body snippet on stdin.');
|
|
47
|
+
return { options: options, snippet };
|
|
48
|
+
}
|
|
49
|
+
export const buildCommand = {
|
|
50
|
+
parse: (argv) => parseBuild(argv),
|
|
51
|
+
async execute(intent, context) {
|
|
52
|
+
await createSquare(context.squarePath, intent.options, intent.snippet);
|
|
53
|
+
const cap = formatHardCap(intent.options.hardCap);
|
|
54
|
+
const throttle = intent.options.throttlePerMinute === undefined ? [] : [` · throttle ${intent.options.throttlePerMinute}/min`];
|
|
55
|
+
return withPathOutput(context.squarePath, ['✓ built', ` · cap ${cap === '-1' ? 'unlimited' : cap}`, ...throttle, ' · participants (none seeded — first join adds names)'].join('\n'), { participantCount: 0 });
|
|
56
|
+
},
|
|
57
|
+
present: (result) => process.stdout.write(result),
|
|
58
|
+
};
|
|
59
|
+
function parseJoin(argv, context) {
|
|
60
|
+
let lastN = 10;
|
|
61
|
+
for (let index = 0; index < argv.length; index++) {
|
|
62
|
+
if (argv[index] === '--last') {
|
|
63
|
+
lastN = parsePositiveInteger(requireValue(argv, index, argv[index]), argv[index]);
|
|
64
|
+
index += 1;
|
|
65
|
+
}
|
|
66
|
+
else if (argv[index] === '--all') {
|
|
67
|
+
lastN = null;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
usage(context.command);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { name: requireParticipant(context.name), lastN };
|
|
74
|
+
}
|
|
75
|
+
export const joinCommand = {
|
|
76
|
+
parse: parseJoin,
|
|
77
|
+
async execute(intent, context) {
|
|
78
|
+
validateParticipantName(intent.name);
|
|
79
|
+
try {
|
|
80
|
+
const committed = await execute(context.squarePath, { type: 'join', name: intent.name, now: nowMs() });
|
|
81
|
+
const joinedName = committed.result.joinedName;
|
|
82
|
+
const isRejoin = !committed.result.addParticipant;
|
|
83
|
+
const after = loadSquare(context.squarePath);
|
|
84
|
+
const preamble = after.preamble.at(-1) === '---' ? after.preamble.slice(0, -1) : after.preamble;
|
|
85
|
+
recordLocalJoin(joinedName, context.squarePath);
|
|
86
|
+
const activities = renderPublicTail(after.acts, intent.lastN, nowMs(), joinedName);
|
|
87
|
+
const contextText = preamble.join('\n').trim();
|
|
88
|
+
const fallback = hasAutomaticDeliveryIdentity()
|
|
89
|
+
? []
|
|
90
|
+
: ['', `» ${participantCommandPrefix(context.squarePath, joinedName)} catch --idle 30m`, ' no session delivery detected — keep this catch open for new activity'];
|
|
91
|
+
const output = [
|
|
92
|
+
`● ${joinedName} stepped into the square`,
|
|
93
|
+
...(isRejoin || contextText === '' ? [] : ['', 'context', contextText]),
|
|
94
|
+
...(activities === '' ? [] : ['', 'recent activity', activities]),
|
|
95
|
+
...(isRejoin ? [] : ['', `» ${participantCommandPrefix(context.squarePath, joinedName)} warmup`]),
|
|
96
|
+
...fallback,
|
|
97
|
+
].join('\n');
|
|
98
|
+
return withJoinNextOutput(context.squarePath, output, { participantCount: inSquareCount(after) });
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (!(error instanceof SquareError) || error.code !== 'conflict')
|
|
102
|
+
throw error;
|
|
103
|
+
const doc = loadSquare(context.squarePath);
|
|
104
|
+
const joinedName = resolveRosterName(doc, intent.name);
|
|
105
|
+
if (joinedName === undefined || !isCurrentlyJoined(doc.acts, joinedName))
|
|
106
|
+
throw error;
|
|
107
|
+
recordLocalJoin(joinedName, context.squarePath);
|
|
108
|
+
const fallback = hasAutomaticDeliveryIdentity()
|
|
109
|
+
? ''
|
|
110
|
+
: `\n» ${participantCommandPrefix(context.squarePath, joinedName)} catch --idle 30m\n no session delivery detected — keep this catch open for new activity`;
|
|
111
|
+
return withJoinNextOutput(context.squarePath, `● ${joinedName} is already in the square${fallback}`, { participantCount: inSquareCount(doc) });
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
present: (result) => process.stdout.write(result),
|
|
115
|
+
};
|
|
116
|
+
function parseActivity(argv, context) {
|
|
117
|
+
let force = false;
|
|
118
|
+
let noWait = false;
|
|
119
|
+
let beside;
|
|
120
|
+
let bell = false;
|
|
121
|
+
const bodyArgs = [];
|
|
122
|
+
for (let index = 0; index < argv.length; index++) {
|
|
123
|
+
const argument = argv[index];
|
|
124
|
+
if (argument === '-f' || argument === '--force')
|
|
125
|
+
force = true;
|
|
126
|
+
else if (argument === '--no-wait')
|
|
127
|
+
noWait = true;
|
|
128
|
+
else if (argument === '--beside') {
|
|
129
|
+
beside = requireValue(argv, index, argument);
|
|
130
|
+
index += 1;
|
|
131
|
+
}
|
|
132
|
+
else if (argument === '--bell')
|
|
133
|
+
bell = true;
|
|
134
|
+
else
|
|
135
|
+
bodyArgs.push(argument);
|
|
136
|
+
}
|
|
137
|
+
if (bell && beside !== undefined)
|
|
138
|
+
fail('Invalid act options: --beside and --bell are mutually exclusive.');
|
|
139
|
+
const reach = bell ? 'bell' : beside === undefined ? undefined : { beside };
|
|
140
|
+
if (bodyArgs.length !== 1) {
|
|
141
|
+
if (bodyArgs.length === 0) {
|
|
142
|
+
const piped = readPipedBodyFallback();
|
|
143
|
+
if (piped !== undefined)
|
|
144
|
+
return { name: requireParticipant(context.name), activity: piped, force, noWait, reach };
|
|
145
|
+
}
|
|
146
|
+
fail("act requires a body argument (a quoted string or '-' with piped stdin)");
|
|
147
|
+
}
|
|
148
|
+
return { name: requireParticipant(context.name), activity: bodyArgs[0], force, noWait, reach };
|
|
149
|
+
}
|
|
150
|
+
export const actCommand = {
|
|
151
|
+
parse: parseActivity,
|
|
152
|
+
async execute(intent, context) {
|
|
153
|
+
const reachArg = intent.reach === 'bell' ? ' --bell' : intent.reach === undefined ? '' : ` --beside ${quoteShell(intent.reach.beside)}`;
|
|
154
|
+
await cmdActivity(context.squarePath, intent.name, intent.activity, resolveBody, {
|
|
155
|
+
force: intent.force,
|
|
156
|
+
noWait: intent.noWait,
|
|
157
|
+
reach: intent.reach,
|
|
158
|
+
forceCommand: `${participantCommandPrefix(context.squarePath, intent.name)} act --force${reachArg} -`,
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
present: () => { },
|
|
162
|
+
};
|
|
163
|
+
function parseDone(argv, context) {
|
|
164
|
+
if (argv.length > 1)
|
|
165
|
+
usage(context.command);
|
|
166
|
+
return { name: requireParticipant(context.name), body: argv.length === 1 ? argv[0] : readPipedBodyFallback() };
|
|
167
|
+
}
|
|
168
|
+
export const doneCommand = {
|
|
169
|
+
parse: parseDone,
|
|
170
|
+
async execute(intent, context) {
|
|
171
|
+
const body = resolveBody(intent.body ?? '').replace(/\r\n/g, '\n').trim();
|
|
172
|
+
const committed = await execute(context.squarePath, { type: 'done', name: intent.name, body, now: nowMs() });
|
|
173
|
+
const name = committed.acts[0].act.actor;
|
|
174
|
+
recordLocalDone(name, context.squarePath);
|
|
175
|
+
return withPathOutput(context.squarePath, `× ${name} steps out of the square — done · just now`, { participantCount: inSquareCount(loadSquare(context.squarePath)) });
|
|
176
|
+
},
|
|
177
|
+
present: (result) => process.stdout.write(result),
|
|
178
|
+
};
|
|
179
|
+
function parseHold(argv, context) {
|
|
180
|
+
if (argv.length > 1)
|
|
181
|
+
usage(context.command);
|
|
182
|
+
return { name: requireParticipant(context.name), body: argv[0] };
|
|
183
|
+
}
|
|
184
|
+
export const holdCommand = {
|
|
185
|
+
parse: parseHold,
|
|
186
|
+
async execute(intent, context) {
|
|
187
|
+
const committed = await execute(context.squarePath, { type: 'hold', actor: intent.name, body: resolveBody(intent.body ?? '').replace(/\r\n/g, '\n').trim(), now: nowMs() });
|
|
188
|
+
const doc = loadSquare(context.squarePath);
|
|
189
|
+
return withPathOutput(context.squarePath, renderEventCli(committed.acts[0].act), { participantCount: inSquareCount(doc), held: true });
|
|
190
|
+
},
|
|
191
|
+
present: (result) => process.stdout.write(result),
|
|
192
|
+
};
|
|
193
|
+
export const resumeCommand = {
|
|
194
|
+
parse(argv, context) {
|
|
195
|
+
if (argv.length !== 0)
|
|
196
|
+
usage(context.command);
|
|
197
|
+
return { name: requireParticipant(context.name) };
|
|
198
|
+
},
|
|
199
|
+
async execute(intent, context) {
|
|
200
|
+
const committed = await execute(context.squarePath, { type: 'resume', actor: intent.name, now: nowMs() });
|
|
201
|
+
const doc = loadSquare(context.squarePath);
|
|
202
|
+
return withPathOutput(context.squarePath, renderEventCli(committed.acts[0].act), { participantCount: inSquareCount(doc) });
|
|
203
|
+
},
|
|
204
|
+
present: (result) => process.stdout.write(result),
|
|
205
|
+
};
|
|
206
|
+
export const compactCommand = {
|
|
207
|
+
parse(argv, context) {
|
|
208
|
+
let keep = 50;
|
|
209
|
+
for (let index = 0; index < argv.length; index++) {
|
|
210
|
+
if (argv[index] !== '--keep')
|
|
211
|
+
usage(context.command);
|
|
212
|
+
keep = parsePositiveInteger(requireValue(argv, index, argv[index]), argv[index]);
|
|
213
|
+
index += 1;
|
|
214
|
+
}
|
|
215
|
+
return { keep };
|
|
216
|
+
},
|
|
217
|
+
async execute(intent, context) {
|
|
218
|
+
await cmdCompact(context.squarePath, intent);
|
|
219
|
+
},
|
|
220
|
+
present: () => { },
|
|
221
|
+
};
|
package/dist/compact.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import { loadSquare, renderArtifactAct } from './artifact.js';
|
|
3
1
|
import { SquareError } from './model.js';
|
|
4
2
|
import { withPathOutput } from './presentation.js';
|
|
5
|
-
import {
|
|
6
|
-
import { coreCompact } from './decisions.js';
|
|
3
|
+
import { execute } from './square-application.js';
|
|
7
4
|
function sidecarPath(squarePath) {
|
|
8
5
|
return squarePath.replace(/\.md$/, '') + '.archive.md';
|
|
9
6
|
}
|
|
@@ -12,20 +9,10 @@ export async function cmdCompact(squarePath, opts) {
|
|
|
12
9
|
let archivedCount;
|
|
13
10
|
let keptCount;
|
|
14
11
|
const archive = sidecarPath(squarePath);
|
|
15
|
-
await
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
keptCount = result.doc.acts.length;
|
|
20
|
-
if (archivedCount > 0) {
|
|
21
|
-
const sidecarExists = fs.existsSync(archive);
|
|
22
|
-
const block = result.archived
|
|
23
|
-
.map((act, index) => renderArtifactAct(act, { first: !sidecarExists && index === 0 }))
|
|
24
|
-
.join('\n');
|
|
25
|
-
fs.appendFileSync(archive, block + '\n');
|
|
26
|
-
writeSquareDoc(squarePath, result.doc);
|
|
27
|
-
}
|
|
28
|
-
});
|
|
12
|
+
const committed = await execute(squarePath, { type: 'compact', keep: opts.keep, archivePath: archive });
|
|
13
|
+
const result = committed.result;
|
|
14
|
+
archivedCount = result.archived.length;
|
|
15
|
+
keptCount = result.doc.acts.length;
|
|
29
16
|
const summary = ['✓ compacted', ` · archived ${archivedCount} acts`, ` · kept ${keptCount} acts`, ...(archivedCount > 0 ? [` · sidecar ${archive}`] : [])].join('\n');
|
|
30
17
|
process.stdout.write(withPathOutput(squarePath, summary));
|
|
31
18
|
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { SQUARE_IDENTITY } from './identity.js';
|
|
6
|
+
import { reconcileInstall, reconcileUninstall, staleManagedRegistrations, } from './harness-lifecycle.js';
|
|
7
|
+
export const CLAUDE_PLUGIN_ID = SQUARE_IDENTITY.pluginId;
|
|
8
|
+
export const CLAUDE_MARKETPLACE_NAME = SQUARE_IDENTITY.marketplaceName;
|
|
9
|
+
export function claudeMarketplaceRoot(homeDir) {
|
|
10
|
+
return path.join(homeDir, '.square', 'claude', 'marketplaces', CLAUDE_MARKETPLACE_NAME);
|
|
11
|
+
}
|
|
12
|
+
function packageAssets(relative) {
|
|
13
|
+
return fileURLToPath(new URL(relative, import.meta.url));
|
|
14
|
+
}
|
|
15
|
+
function runClaudeCommand(homeDir, args) {
|
|
16
|
+
const result = spawnSync(process.env.SQUARE_CLAUDE_BIN || 'claude', args, {
|
|
17
|
+
encoding: 'utf8',
|
|
18
|
+
env: { ...process.env, HOME: homeDir, CLAUDE_CONFIG_DIR: path.join(homeDir, '.claude') },
|
|
19
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
20
|
+
timeout: 30_000,
|
|
21
|
+
});
|
|
22
|
+
if (result.error)
|
|
23
|
+
throw result.error;
|
|
24
|
+
return { status: result.status ?? 1, stdout: result.stdout || '', stderr: result.stderr || '' };
|
|
25
|
+
}
|
|
26
|
+
function requireSuccess(result, operation) {
|
|
27
|
+
if (result.status === 0)
|
|
28
|
+
return;
|
|
29
|
+
throw new Error(`Claude ${operation} failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.status}`}`);
|
|
30
|
+
}
|
|
31
|
+
function writeJsonAtomic(filePath, value) {
|
|
32
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
33
|
+
const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
34
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
35
|
+
fs.renameSync(temporary, filePath);
|
|
36
|
+
}
|
|
37
|
+
function stageClaudeBundle(homeDir, marketplaceRoot) {
|
|
38
|
+
const token = `${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
|
|
39
|
+
const stage = `${marketplaceRoot}.${token}.stage`;
|
|
40
|
+
const backup = `${marketplaceRoot}.${token}.previous`;
|
|
41
|
+
const pluginRoot = path.join(stage, 'plugins', SQUARE_IDENTITY.pluginName);
|
|
42
|
+
fs.mkdirSync(path.dirname(marketplaceRoot), { recursive: true });
|
|
43
|
+
try {
|
|
44
|
+
fs.cpSync(packageAssets('../skills/square/'), pluginRoot, { recursive: true });
|
|
45
|
+
writeJsonAtomic(path.join(stage, '.claude-plugin', 'marketplace.json'), {
|
|
46
|
+
name: CLAUDE_MARKETPLACE_NAME,
|
|
47
|
+
description: `${SQUARE_IDENTITY.productName} harness integrations`,
|
|
48
|
+
owner: { name: SQUARE_IDENTITY.productName },
|
|
49
|
+
plugins: [{ name: SQUARE_IDENTITY.pluginName, description: `Native Claude Code delivery for ${SQUARE_IDENTITY.productName}`, source: './plugins/square' }],
|
|
50
|
+
});
|
|
51
|
+
if (fs.existsSync(marketplaceRoot))
|
|
52
|
+
fs.renameSync(marketplaceRoot, backup);
|
|
53
|
+
fs.renameSync(stage, marketplaceRoot);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
try {
|
|
57
|
+
fs.rmSync(stage, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
catch { }
|
|
60
|
+
if (fs.existsSync(backup) && !fs.existsSync(marketplaceRoot))
|
|
61
|
+
fs.renameSync(backup, marketplaceRoot);
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
desired: { marketplaceName: CLAUDE_MARKETPLACE_NAME, marketplaceRoot, pluginId: CLAUDE_PLUGIN_ID },
|
|
66
|
+
rollback() {
|
|
67
|
+
fs.rmSync(marketplaceRoot, { recursive: true, force: true });
|
|
68
|
+
if (fs.existsSync(backup))
|
|
69
|
+
fs.renameSync(backup, marketplaceRoot);
|
|
70
|
+
},
|
|
71
|
+
finalize() { fs.rmSync(backup, { recursive: true, force: true }); },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function isRecord(value) {
|
|
75
|
+
return value !== null && typeof value === 'object';
|
|
76
|
+
}
|
|
77
|
+
function isManagedSource(managedRoot, source) {
|
|
78
|
+
const relative = path.relative(path.resolve(managedRoot), path.resolve(source));
|
|
79
|
+
return relative === '' || (relative !== '..' &&
|
|
80
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
81
|
+
!path.isAbsolute(relative));
|
|
82
|
+
}
|
|
83
|
+
function isCurrentManagedMarketplace(marketplace, managedRoot) {
|
|
84
|
+
return marketplace.name === CLAUDE_MARKETPLACE_NAME &&
|
|
85
|
+
marketplace.local &&
|
|
86
|
+
isManagedSource(managedRoot, marketplace.source);
|
|
87
|
+
}
|
|
88
|
+
function parseClaudeMarketplaceRegistration(value) {
|
|
89
|
+
if (!isRecord(value)) {
|
|
90
|
+
throw new Error('Claude marketplace inventory entry is invalid.');
|
|
91
|
+
}
|
|
92
|
+
const name = value.name;
|
|
93
|
+
const source = value.source;
|
|
94
|
+
const installLocation = value.installLocation;
|
|
95
|
+
if (typeof name !== 'string' ||
|
|
96
|
+
typeof source !== 'string' ||
|
|
97
|
+
typeof installLocation !== 'string') {
|
|
98
|
+
throw new Error('Claude marketplace inventory entry is malformed.');
|
|
99
|
+
}
|
|
100
|
+
if (source === 'directory') {
|
|
101
|
+
const sourcePath = value.path;
|
|
102
|
+
if (typeof sourcePath !== 'string') {
|
|
103
|
+
throw new Error('Claude directory marketplace entry is malformed.');
|
|
104
|
+
}
|
|
105
|
+
return { name, source: sourcePath, local: true };
|
|
106
|
+
}
|
|
107
|
+
const stableSource = typeof value.repo === 'string' ? value.repo : name;
|
|
108
|
+
return { name, source: `${source}:${stableSource}`, local: false };
|
|
109
|
+
}
|
|
110
|
+
function parseInventory(stdout) {
|
|
111
|
+
try {
|
|
112
|
+
const payload = JSON.parse(stdout);
|
|
113
|
+
if (!Array.isArray(payload)) {
|
|
114
|
+
throw new Error('Claude marketplace inventory must be a JSON array.');
|
|
115
|
+
}
|
|
116
|
+
const marketplaces = payload.map(parseClaudeMarketplaceRegistration);
|
|
117
|
+
return { marketplaces };
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function parseClaudePluginInventory(stdout) {
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(stdout);
|
|
126
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
throw new Error('Claude plugin inventory returned invalid JSON.');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function isVerifiedClaudePlugin(entry) {
|
|
133
|
+
if (!isRecord(entry))
|
|
134
|
+
return false;
|
|
135
|
+
return entry.id === CLAUDE_PLUGIN_ID &&
|
|
136
|
+
entry.enabled === true &&
|
|
137
|
+
entry.version === SQUARE_IDENTITY.packageVersion;
|
|
138
|
+
}
|
|
139
|
+
function claudeProtocol(run) {
|
|
140
|
+
return {
|
|
141
|
+
host: 'claude',
|
|
142
|
+
marketplaceName: CLAUDE_MARKETPLACE_NAME,
|
|
143
|
+
pluginId: CLAUDE_PLUGIN_ID,
|
|
144
|
+
managedRoot: (homeDir) => path.join(homeDir, '.square', 'claude'),
|
|
145
|
+
stageBundle: stageClaudeBundle,
|
|
146
|
+
inspectInventory(homeDir) {
|
|
147
|
+
const result = run(homeDir, ['plugin', 'marketplace', 'list', '--json']);
|
|
148
|
+
requireSuccess(result, 'marketplace inventory');
|
|
149
|
+
return parseInventory(result.stdout);
|
|
150
|
+
},
|
|
151
|
+
registerMarketplace(homeDir, desired) {
|
|
152
|
+
const result = run(homeDir, ['plugin', 'marketplace', 'add', desired.marketplaceRoot]);
|
|
153
|
+
requireSuccess(result, 'marketplace install');
|
|
154
|
+
},
|
|
155
|
+
installOrUpdate(homeDir, desired) {
|
|
156
|
+
requireSuccess(run(homeDir, ['plugin', 'install', desired.pluginId]), 'plugin install');
|
|
157
|
+
requireSuccess(run(homeDir, ['plugin', 'update', desired.pluginId]), 'plugin update');
|
|
158
|
+
},
|
|
159
|
+
verifyPluginAndHooks(homeDir, desired) {
|
|
160
|
+
const hooksPath = path.join(desired.marketplaceRoot, 'plugins', SQUARE_IDENTITY.pluginName, 'hooks', 'hooks.json');
|
|
161
|
+
if (!fs.existsSync(hooksPath)) {
|
|
162
|
+
throw new Error(`Claude plugin hooks missing from ${desired.marketplaceRoot}`);
|
|
163
|
+
}
|
|
164
|
+
const listed = run(homeDir, ['plugin', 'list', '--json']);
|
|
165
|
+
requireSuccess(listed, 'plugin inventory');
|
|
166
|
+
const plugins = parseClaudePluginInventory(listed.stdout);
|
|
167
|
+
if (!plugins.some(isVerifiedClaudePlugin)) {
|
|
168
|
+
throw new Error(`Claude did not verify ${CLAUDE_PLUGIN_ID} as installed and enabled.`);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
removePlugin(homeDir, pluginId) {
|
|
172
|
+
const result = run(homeDir, ['plugin', 'remove', pluginId]);
|
|
173
|
+
const isMissing = result.stderr.includes('is not configured or installed');
|
|
174
|
+
if (result.status !== 0 && !isMissing) {
|
|
175
|
+
requireSuccess(result, 'plugin removal');
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
removeMarketplace(homeDir, marketplaceName) {
|
|
179
|
+
const result = run(homeDir, ['plugin', 'marketplace', 'remove', marketplaceName]);
|
|
180
|
+
const isMissing = result.stderr.includes('is not configured or installed');
|
|
181
|
+
if (result.status !== 0 && !isMissing) {
|
|
182
|
+
requireSuccess(result, 'marketplace removal');
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
removeManagedSource(source) {
|
|
186
|
+
fs.rmSync(source, { recursive: true, force: true });
|
|
187
|
+
},
|
|
188
|
+
retireDirectDelivery(homeDir) {
|
|
189
|
+
const legacySkill = path.join(homeDir, '.claude', 'skills', 'square');
|
|
190
|
+
try {
|
|
191
|
+
if (fs.lstatSync(legacySkill).isSymbolicLink()) {
|
|
192
|
+
fs.rmSync(legacySkill, { force: true });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch { }
|
|
196
|
+
},
|
|
197
|
+
removeManagedRoot(homeDir) {
|
|
198
|
+
fs.rmSync(path.join(homeDir, '.square', 'claude'), { recursive: true, force: true });
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
export async function installClaudePlugin(homeDir, run = runClaudeCommand) {
|
|
203
|
+
const protocol = claudeProtocol(run);
|
|
204
|
+
const inventory = await reconcileInstall(homeDir, protocol);
|
|
205
|
+
const managedRoot = protocol.managedRoot(homeDir);
|
|
206
|
+
const marketplaceRoot = inventory.marketplaces.find((entry) => isCurrentManagedMarketplace(entry, managedRoot))?.source ?? claudeMarketplaceRoot(homeDir);
|
|
207
|
+
const pluginRoot = path.join(marketplaceRoot, 'plugins', SQUARE_IDENTITY.pluginName);
|
|
208
|
+
return { marketplaceRoot, pluginRoot };
|
|
209
|
+
}
|
|
210
|
+
export async function uninstallClaudePlugin(homeDir, run = runClaudeCommand) {
|
|
211
|
+
const base = claudeProtocol(run);
|
|
212
|
+
const protocol = {
|
|
213
|
+
...base,
|
|
214
|
+
async inspectInventory(currentHome) {
|
|
215
|
+
const inventory = await base.inspectInventory(currentHome);
|
|
216
|
+
const managedRoot = base.managedRoot(currentHome);
|
|
217
|
+
const hasManagedMarketplace = inventory.marketplaces.some((entry) => entry.local && isManagedSource(managedRoot, entry.source));
|
|
218
|
+
if (hasManagedMarketplace)
|
|
219
|
+
return inventory;
|
|
220
|
+
const fallback = {
|
|
221
|
+
name: CLAUDE_MARKETPLACE_NAME,
|
|
222
|
+
source: claudeMarketplaceRoot(currentHome),
|
|
223
|
+
local: true,
|
|
224
|
+
pluginIds: [CLAUDE_PLUGIN_ID],
|
|
225
|
+
};
|
|
226
|
+
return { marketplaces: [...inventory.marketplaces, fallback] };
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
await reconcileUninstall(homeDir, protocol);
|
|
230
|
+
return { paths: [claudeMarketplaceRoot(homeDir)], notes: [] };
|
|
231
|
+
}
|
|
232
|
+
export async function doctorClaudePlugin(homeDir, run = runClaudeCommand) {
|
|
233
|
+
const protocol = claudeProtocol(run);
|
|
234
|
+
const inventory = await protocol.inspectInventory(homeDir);
|
|
235
|
+
const managedRoot = protocol.managedRoot(homeDir);
|
|
236
|
+
const current = inventory.marketplaces.find((entry) => isCurrentManagedMarketplace(entry, managedRoot));
|
|
237
|
+
const root = current?.source ?? claudeMarketplaceRoot(homeDir);
|
|
238
|
+
const desired = {
|
|
239
|
+
marketplaceName: CLAUDE_MARKETPLACE_NAME,
|
|
240
|
+
marketplaceRoot: root,
|
|
241
|
+
pluginId: CLAUDE_PLUGIN_ID,
|
|
242
|
+
};
|
|
243
|
+
const stale = staleManagedRegistrations(inventory, managedRoot, desired);
|
|
244
|
+
let pluginStatus;
|
|
245
|
+
const listed = run(homeDir, ['plugin', 'list', '--json']);
|
|
246
|
+
if (listed.status !== 0) {
|
|
247
|
+
pluginStatus = `○ ${CLAUDE_PLUGIN_ID} plugin inventory unavailable`;
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
try {
|
|
251
|
+
const plugins = parseClaudePluginInventory(listed.stdout);
|
|
252
|
+
const valid = plugins.some(isVerifiedClaudePlugin);
|
|
253
|
+
pluginStatus = valid
|
|
254
|
+
? `✓ ${CLAUDE_PLUGIN_ID} installed, enabled, and version ${SQUARE_IDENTITY.packageVersion}`
|
|
255
|
+
: `○ ${CLAUDE_PLUGIN_ID} is not installed, enabled, and version ${SQUARE_IDENTITY.packageVersion}`;
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
pluginStatus = `○ ${CLAUDE_PLUGIN_ID} plugin inventory unavailable`;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const bundlePath = path.join(root, 'plugins', SQUARE_IDENTITY.pluginName);
|
|
262
|
+
const bundleStatus = fs.existsSync(bundlePath)
|
|
263
|
+
? `✓ Square Claude plugin bundle ${root}`
|
|
264
|
+
: `○ Square Claude plugin bundle missing ${root}`;
|
|
265
|
+
const marketplaceStatus = current !== undefined
|
|
266
|
+
? `✓ ${CLAUDE_PLUGIN_ID} marketplace registered`
|
|
267
|
+
: `○ ${CLAUDE_PLUGIN_ID} marketplace is not registered`;
|
|
268
|
+
return [
|
|
269
|
+
bundleStatus,
|
|
270
|
+
marketplaceStatus,
|
|
271
|
+
pluginStatus,
|
|
272
|
+
...(stale.length === 0 ? [] : [`✕ ${stale.length} stale Square marketplace registration(s)`]),
|
|
273
|
+
];
|
|
274
|
+
}
|
|
275
|
+
export const claudeHarness = Object.freeze({ install: installClaudePlugin, uninstall: uninstallClaudePlugin, doctor: doctorClaudePlugin });
|