@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,102 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
export class HarnessLifecycleError extends Error {
|
|
3
|
+
host;
|
|
4
|
+
phase;
|
|
5
|
+
retryCommand;
|
|
6
|
+
constructor(host, phase, retryCommand, cause) {
|
|
7
|
+
super(`${host} ${phase} failed: ${cause instanceof Error ? cause.message : String(cause)}\n» ${retryCommand}`);
|
|
8
|
+
this.host = host;
|
|
9
|
+
this.phase = phase;
|
|
10
|
+
this.retryCommand = retryCommand;
|
|
11
|
+
this.name = 'HarnessLifecycleError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function containsPath(root, target) {
|
|
15
|
+
const relative = path.relative(path.resolve(root), path.resolve(target));
|
|
16
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
17
|
+
}
|
|
18
|
+
function staleRegistrations(inventory, managedRoot, desired) {
|
|
19
|
+
return inventory.marketplaces.filter((registration) => registration.local &&
|
|
20
|
+
containsPath(managedRoot, registration.source) &&
|
|
21
|
+
(registration.name !== desired.marketplaceName || path.resolve(registration.source) !== path.resolve(desired.marketplaceRoot)));
|
|
22
|
+
}
|
|
23
|
+
function pluginIdForRegistration(desired, registration) {
|
|
24
|
+
if (registration.pluginIds !== undefined && registration.pluginIds.length > 0)
|
|
25
|
+
return registration.pluginIds;
|
|
26
|
+
const pluginName = desired.pluginId.split('@', 1)[0];
|
|
27
|
+
return [`${pluginName}@${registration.name}`];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Reconcile a host from Square-owned desired state. Cleanup deliberately begins
|
|
31
|
+
* only after plugin activation and hook verification, so an old registration is
|
|
32
|
+
* still usable when staging, registration, install, or verification fails.
|
|
33
|
+
*/
|
|
34
|
+
export async function reconcileInstall(homeDir, protocol, retryCommand = `square harness install ${protocol.host}`) {
|
|
35
|
+
let staged;
|
|
36
|
+
let activated = false;
|
|
37
|
+
let phase = 'stage';
|
|
38
|
+
try {
|
|
39
|
+
const before = await protocol.inspectInventory(homeDir);
|
|
40
|
+
const managedRoot = protocol.managedRoot(homeDir);
|
|
41
|
+
const current = before.marketplaces.find((registration) => registration.name === protocol.marketplaceName &&
|
|
42
|
+
registration.local &&
|
|
43
|
+
containsPath(managedRoot, registration.source));
|
|
44
|
+
const marketplaceRoot = current?.source ?? path.join(managedRoot, 'marketplaces', protocol.marketplaceName);
|
|
45
|
+
staged = await protocol.stageBundle(homeDir, marketplaceRoot);
|
|
46
|
+
const stale = staleRegistrations(before, protocol.managedRoot(homeDir), staged.desired);
|
|
47
|
+
if (current === undefined) {
|
|
48
|
+
phase = 'register';
|
|
49
|
+
await protocol.registerMarketplace(homeDir, staged.desired);
|
|
50
|
+
}
|
|
51
|
+
phase = 'install';
|
|
52
|
+
await protocol.installOrUpdate(homeDir, staged.desired);
|
|
53
|
+
phase = 'verify';
|
|
54
|
+
await protocol.verifyPluginAndHooks(homeDir, staged.desired);
|
|
55
|
+
activated = true;
|
|
56
|
+
phase = 'cleanup';
|
|
57
|
+
for (const registration of stale) {
|
|
58
|
+
for (const pluginId of pluginIdForRegistration(staged.desired, registration)) {
|
|
59
|
+
await protocol.removePlugin(homeDir, pluginId, registration.name);
|
|
60
|
+
}
|
|
61
|
+
await protocol.removeMarketplace(homeDir, registration.name);
|
|
62
|
+
if (path.resolve(registration.source) !== path.resolve(staged.desired.marketplaceRoot)) {
|
|
63
|
+
await protocol.removeManagedSource?.(registration.source);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
await protocol.retireDirectDelivery?.(homeDir);
|
|
67
|
+
await staged.finalize?.();
|
|
68
|
+
return await protocol.inspectInventory(homeDir);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (!activated && staged !== undefined) {
|
|
72
|
+
try {
|
|
73
|
+
await staged.rollback();
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// The original failure is the actionable phase; rollback is best effort.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (error instanceof HarnessLifecycleError)
|
|
80
|
+
throw error;
|
|
81
|
+
throw new HarnessLifecycleError(protocol.host, phase, retryCommand, error);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export async function reconcileUninstall(homeDir, protocol) {
|
|
85
|
+
const inventory = await protocol.inspectInventory(homeDir);
|
|
86
|
+
const root = protocol.managedRoot(homeDir);
|
|
87
|
+
const pluginName = protocol.pluginId.split('@', 1)[0];
|
|
88
|
+
for (const registration of inventory.marketplaces) {
|
|
89
|
+
if (!registration.local || !containsPath(root, registration.source))
|
|
90
|
+
continue;
|
|
91
|
+
for (const pluginId of registration.pluginIds ?? [`${pluginName}@${registration.name}`]) {
|
|
92
|
+
await protocol.removePlugin(homeDir, pluginId, registration.name);
|
|
93
|
+
}
|
|
94
|
+
await protocol.removeMarketplace(homeDir, registration.name);
|
|
95
|
+
await protocol.removeManagedSource?.(registration.source);
|
|
96
|
+
}
|
|
97
|
+
await protocol.retireDirectDelivery?.(homeDir);
|
|
98
|
+
await protocol.removeManagedRoot?.(homeDir);
|
|
99
|
+
}
|
|
100
|
+
export function staleManagedRegistrations(inventory, managedRoot, desired) {
|
|
101
|
+
return staleRegistrations(inventory, managedRoot, desired);
|
|
102
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
|
+
function packageRoot() {
|
|
7
|
+
// Emitted modules live in dist; package assets are one level above them.
|
|
8
|
+
return fileURLToPath(new URL('../', import.meta.url));
|
|
9
|
+
}
|
|
10
|
+
function lstatMaybe(target) {
|
|
11
|
+
try {
|
|
12
|
+
return fs.lstatSync(target);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (error.code === 'ENOENT')
|
|
16
|
+
return undefined;
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function installHarnessLinks(links, force = false) {
|
|
21
|
+
const prepared = links.map((link) => {
|
|
22
|
+
if (!fs.existsSync(link.source)) {
|
|
23
|
+
throw new Error(`Harness link source is missing: ${link.source}`);
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
...link,
|
|
27
|
+
existing: lstatMaybe(link.target),
|
|
28
|
+
sourceIsDirectory: fs.statSync(link.source).isDirectory(),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
for (const { target, existing } of prepared) {
|
|
32
|
+
if (existing !== undefined && !force)
|
|
33
|
+
throw new Error(`Refusing to overwrite existing link: ${target}\nPass -f to replace it.`);
|
|
34
|
+
}
|
|
35
|
+
for (const { source, target, existing, sourceIsDirectory } of prepared) {
|
|
36
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
37
|
+
if (existing !== undefined)
|
|
38
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
39
|
+
const symlinkType = os.platform() === 'win32' && sourceIsDirectory ? 'junction' : 'file';
|
|
40
|
+
fs.symlinkSync(source, target, symlinkType);
|
|
41
|
+
}
|
|
42
|
+
return prepared.map(({ target }) => target);
|
|
43
|
+
}
|
|
44
|
+
function sameLink(source, target) {
|
|
45
|
+
try {
|
|
46
|
+
return fs.realpathSync(source) === fs.realpathSync(target);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function uninstallHarnessLinks(links) {
|
|
53
|
+
const removed = [];
|
|
54
|
+
for (const link of links) {
|
|
55
|
+
try {
|
|
56
|
+
if (fs.lstatSync(link.target).isSymbolicLink() && sameLink(link.source, link.target)) {
|
|
57
|
+
fs.rmSync(link.target, { force: true });
|
|
58
|
+
removed.push(link.target);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// A missing or user-owned target is preserved.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return removed;
|
|
66
|
+
}
|
|
67
|
+
export function doctorHarnessLinks(links) {
|
|
68
|
+
return links.map((link) => sameLink(link.source, link.target)
|
|
69
|
+
? `✓ Square ${link.kind ?? 'link'} ${link.target}`
|
|
70
|
+
: `○ Square ${link.kind ?? 'link'} missing ${link.target}`);
|
|
71
|
+
}
|
|
72
|
+
function runOpenCode(homeDir, args) {
|
|
73
|
+
const result = spawnSync(process.env.SQUARE_OPENCODE_BIN || 'opencode', args, {
|
|
74
|
+
encoding: 'utf8',
|
|
75
|
+
env: { ...process.env, HOME: homeDir, XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME ?? path.join(homeDir, '.config') },
|
|
76
|
+
timeout: 30_000,
|
|
77
|
+
});
|
|
78
|
+
if (result.error)
|
|
79
|
+
throw result.error;
|
|
80
|
+
return { status: result.status ?? 1, stdout: result.stdout || '', stderr: result.stderr || '' };
|
|
81
|
+
}
|
|
82
|
+
/** Verify that OpenCode accepts its resolved runtime configuration after links are installed. */
|
|
83
|
+
export function verifyOpenCodeRuntime(homeDir, run = runOpenCode) {
|
|
84
|
+
try {
|
|
85
|
+
const result = run(homeDir, ['debug', 'config']);
|
|
86
|
+
if (result.status !== 0) {
|
|
87
|
+
return `✕ OpenCode debug config failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.status}`}`;
|
|
88
|
+
}
|
|
89
|
+
let config;
|
|
90
|
+
try {
|
|
91
|
+
config = JSON.parse(result.stdout);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return '✕ OpenCode debug config returned invalid JSON';
|
|
95
|
+
}
|
|
96
|
+
const plugin = config.config?.plugin;
|
|
97
|
+
const expected = pathToFileURL(opencodeExtensionLink(homeDir).target).href;
|
|
98
|
+
if (Array.isArray(plugin) && plugin.includes(expected))
|
|
99
|
+
return '✓ OpenCode debug config loaded';
|
|
100
|
+
return `○ OpenCode plugin not loaded: ${expected}`;
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
return `○ OpenCode runtime unavailable (${error instanceof Error ? error.message : String(error)})`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export function skillLinks(homeDir = os.homedir(), parents = ['.claude', '.agents']) {
|
|
107
|
+
return parents.flatMap((parent) => ['square', 'brainstorm'].map((name) => ({
|
|
108
|
+
source: path.join(packageRoot(), 'skills', name),
|
|
109
|
+
target: path.join(homeDir, parent, 'skills', name),
|
|
110
|
+
kind: 'skill',
|
|
111
|
+
})));
|
|
112
|
+
}
|
|
113
|
+
export function opencodeExtensionLink(homeDir = os.homedir()) {
|
|
114
|
+
const configHome = process.env.XDG_CONFIG_HOME ?? path.join(homeDir, '.config');
|
|
115
|
+
return {
|
|
116
|
+
source: path.join(packageRoot(), 'extensions', 'square-opencode.js'),
|
|
117
|
+
target: path.join(configHome, 'opencode', 'plugins', 'square.js'),
|
|
118
|
+
kind: 'extension',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export function piExtensionLink(homeDir = os.homedir()) {
|
|
122
|
+
return { source: path.join(packageRoot(), 'extensions', 'square-pi.js'), target: path.join(homeDir, '.pi', 'agent', 'extensions', 'square.js'), kind: 'extension' };
|
|
123
|
+
}
|