@astrosheep/square 0.3.92 → 0.3.93
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/dist/harness-links.d.ts
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
export interface HarnessLink {
|
|
2
|
-
source: string;
|
|
3
|
-
target: string;
|
|
4
|
-
kind?: 'skill' | 'extension';
|
|
5
|
-
}
|
|
6
|
-
export declare function installHarnessLinks(links: HarnessLink[], force?: boolean): string[];
|
|
7
|
-
export declare function uninstallHarnessLinks(links: HarnessLink[]): string[];
|
|
8
|
-
export declare function doctorHarnessLinks(links: HarnessLink[]): string[];
|
|
9
1
|
export type OpenCodeCommandRunner = (homeDir: string, args: string[]) => {
|
|
10
2
|
status: number;
|
|
11
3
|
stdout: string;
|
|
@@ -13,6 +5,5 @@ export type OpenCodeCommandRunner = (homeDir: string, args: string[]) => {
|
|
|
13
5
|
};
|
|
14
6
|
export declare function installOpenCodePlugin(homeDir: string, force?: boolean, run?: OpenCodeCommandRunner): string[];
|
|
15
7
|
export declare function uninstallOpenCodePlugin(homeDir: string): string[];
|
|
16
|
-
/**
|
|
8
|
+
/** Config sources prove configuration, not that a running session loaded the plugin. */
|
|
17
9
|
export declare function verifyOpenCodeRuntime(homeDir: string, run?: OpenCodeCommandRunner): string;
|
|
18
|
-
export declare function skillLinks(homeDir?: string, parents?: Array<'.claude' | '.agents'>): HarnessLink[];
|
package/dist/harness-links.js
CHANGED
|
@@ -1,79 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import os from 'node:os';
|
|
3
2
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
5
4
|
import crossSpawn from 'cross-spawn';
|
|
5
|
+
import { applyEdits, createScanner, findNodeAtLocation, modify, parse, parseTree } from 'jsonc-parser';
|
|
6
6
|
import { SQUARE_IDENTITY } from './identity.js';
|
|
7
7
|
import { truncateExternalDiagnostic } from './presentation.js';
|
|
8
|
-
function packageRoot() {
|
|
9
|
-
// Emitted modules live in dist; package assets are one level above them.
|
|
10
|
-
return fileURLToPath(new URL('../', import.meta.url));
|
|
11
|
-
}
|
|
12
|
-
export function installHarnessLinks(links, force = false) {
|
|
13
|
-
const prepared = links.map((link) => {
|
|
14
|
-
if (!fs.existsSync(link.source)) {
|
|
15
|
-
throw new Error(`Harness link source is missing: ${link.source}`);
|
|
16
|
-
}
|
|
17
|
-
const existing = (() => {
|
|
18
|
-
try {
|
|
19
|
-
return fs.lstatSync(link.target);
|
|
20
|
-
}
|
|
21
|
-
catch (error) {
|
|
22
|
-
if (error.code === 'ENOENT')
|
|
23
|
-
return undefined;
|
|
24
|
-
throw error;
|
|
25
|
-
}
|
|
26
|
-
})();
|
|
27
|
-
return {
|
|
28
|
-
...link,
|
|
29
|
-
existing,
|
|
30
|
-
sourceIsDirectory: fs.statSync(link.source).isDirectory(),
|
|
31
|
-
alreadyManaged: existing !== undefined && sameLink(link.source, link.target),
|
|
32
|
-
};
|
|
33
|
-
});
|
|
34
|
-
for (const { target, existing, alreadyManaged } of prepared) {
|
|
35
|
-
if (existing !== undefined && !alreadyManaged && !force)
|
|
36
|
-
throw new Error(`Refusing to overwrite existing link: ${target}\nPass -f to replace it.`);
|
|
37
|
-
}
|
|
38
|
-
for (const { source, target, existing, sourceIsDirectory, alreadyManaged } of prepared) {
|
|
39
|
-
if (alreadyManaged)
|
|
40
|
-
continue;
|
|
41
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
42
|
-
if (existing !== undefined)
|
|
43
|
-
fs.rmSync(target, { recursive: true, force: true });
|
|
44
|
-
const symlinkType = os.platform() === 'win32' && sourceIsDirectory ? 'junction' : 'file';
|
|
45
|
-
fs.symlinkSync(source, target, symlinkType);
|
|
46
|
-
}
|
|
47
|
-
return prepared.map(({ target }) => target);
|
|
48
|
-
}
|
|
49
|
-
function sameLink(source, target) {
|
|
50
|
-
try {
|
|
51
|
-
return fs.realpathSync(source) === fs.realpathSync(target);
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
export function uninstallHarnessLinks(links) {
|
|
58
|
-
const removed = [];
|
|
59
|
-
for (const link of links) {
|
|
60
|
-
try {
|
|
61
|
-
if (fs.lstatSync(link.target).isSymbolicLink() && sameLink(link.source, link.target)) {
|
|
62
|
-
fs.rmSync(link.target, { force: true });
|
|
63
|
-
removed.push(link.target);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
// A missing or user-owned target is preserved.
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return removed;
|
|
71
|
-
}
|
|
72
|
-
export function doctorHarnessLinks(links) {
|
|
73
|
-
return links.map((link) => sameLink(link.source, link.target)
|
|
74
|
-
? `✓ Square ${link.kind ?? 'link'} ${link.target}`
|
|
75
|
-
: `○ Square ${link.kind ?? 'link'} missing ${link.target}`);
|
|
76
|
-
}
|
|
77
8
|
function runOpenCode(homeDir, args) {
|
|
78
9
|
const prefix = (() => {
|
|
79
10
|
try {
|
|
@@ -94,39 +25,150 @@ function runOpenCode(homeDir, args) {
|
|
|
94
25
|
return { status: result.status ?? 1, stdout: result.stdout || '', stderr: result.stderr || '' };
|
|
95
26
|
}
|
|
96
27
|
export function installOpenCodePlugin(homeDir, force = false, run = runOpenCode) {
|
|
97
|
-
//
|
|
28
|
+
// Explicit `square install` replaces owned pins/duplicates with one unpinned
|
|
29
|
+
// current-field entry; updating an exact old pin otherwise succeeds unchanged.
|
|
98
30
|
void force;
|
|
99
|
-
const
|
|
31
|
+
const configs = readOpenCodeConfigs(homeDir);
|
|
32
|
+
const entries = configs.flatMap((config) => config.entries);
|
|
33
|
+
const options = entries.map((entry) => entry.options).filter((value) => value !== undefined);
|
|
34
|
+
if (options.some((value) => !isDeepStrictEqual(value, options[0]))) {
|
|
35
|
+
throw new Error('Conflicting Square plugin options in OpenCode configuration; reconcile the owned plugin/plugins entries before installing.');
|
|
36
|
+
}
|
|
37
|
+
const canonical = options.length ? { package: SQUARE_IDENTITY.packageName, options: options[0] } : SQUARE_IDENTITY.packageName;
|
|
38
|
+
const targets = configs.length ? configs : [{ target: openCodeConfigPaths(homeDir)[0], source: '{}', entries: [] }];
|
|
39
|
+
const alreadyCanonical = entries.length === 1 && configs[0]?.entries[0]?.field === 'plugins'
|
|
40
|
+
&& isDeepStrictEqual(entries[0].value, canonical);
|
|
41
|
+
const changes = alreadyCanonical ? [] : targets.map(({ target, source, entries }, index) => {
|
|
42
|
+
let next = source;
|
|
43
|
+
for (const { field, index } of entries.toReversed())
|
|
44
|
+
next = removeOpenCodeEntry(next, field, index);
|
|
45
|
+
if (index === 0)
|
|
46
|
+
next = applyEdits(next, modify(next, ['plugins', -1], canonical, {}));
|
|
47
|
+
return { target, source, next };
|
|
48
|
+
}).filter(({ source, next }) => source !== next);
|
|
49
|
+
// Appending just our element avoids native add's whole-array rewrite, which
|
|
50
|
+
// drops comments belonging to unrelated plugins. Native add still owns npm.
|
|
51
|
+
for (const { target, next } of changes) {
|
|
52
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
53
|
+
fs.writeFileSync(target, next);
|
|
54
|
+
}
|
|
55
|
+
if (!entries.some(({ spec }) => spec === SQUARE_IDENTITY.packageName)) {
|
|
56
|
+
try {
|
|
57
|
+
openCodeCommand(homeDir, ['plugin', 'add', SQUARE_IDENTITY.packageName], run, 'install');
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
// Undo only our preparation, never files the native command/user changed.
|
|
61
|
+
for (const { target, source, next } of changes) {
|
|
62
|
+
if (fs.existsSync(target) && fs.readFileSync(target, 'utf8') === next) {
|
|
63
|
+
if (configs.length)
|
|
64
|
+
fs.writeFileSync(target, source);
|
|
65
|
+
else
|
|
66
|
+
fs.rmSync(target);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Even add can reuse an older cached package. Always request the latest.
|
|
73
|
+
openCodeCommand(homeDir, ['plugin', 'update', SQUARE_IDENTITY.packageName], run, 'update');
|
|
74
|
+
return [SQUARE_IDENTITY.packageName];
|
|
75
|
+
}
|
|
76
|
+
function openCodeCommand(homeDir, args, run, operation) {
|
|
100
77
|
const result = run(homeDir, args);
|
|
101
78
|
if (result.status !== 0)
|
|
102
|
-
throw new Error(`OpenCode
|
|
103
|
-
return
|
|
79
|
+
throw new Error(`OpenCode ${operation} failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.status}`}`);
|
|
80
|
+
return result.stdout;
|
|
104
81
|
}
|
|
105
|
-
function
|
|
82
|
+
function openCodeConfigPaths(homeDir) {
|
|
106
83
|
const configHome = process.env.XDG_CONFIG_HOME ?? path.join(homeDir, '.config');
|
|
107
|
-
|
|
84
|
+
const root = path.resolve(configHome, 'opencode');
|
|
85
|
+
return [root, path.join(root, '.opencode')].flatMap((directory) => ['opencode.json', 'opencode.jsonc'].map((name) => path.join(directory, name)));
|
|
86
|
+
}
|
|
87
|
+
function readOpenCodeConfigs(homeDir) {
|
|
88
|
+
return openCodeConfigPaths(homeDir).flatMap((target) => {
|
|
89
|
+
let source;
|
|
90
|
+
try {
|
|
91
|
+
source = fs.readFileSync(target, 'utf8');
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (error.code === 'ENOENT')
|
|
95
|
+
return [];
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
const errors = [];
|
|
99
|
+
const config = parse(source, errors, { allowTrailingComma: true });
|
|
100
|
+
if (errors.length || typeof config !== 'object' || config === null || Array.isArray(config)) {
|
|
101
|
+
throw new Error(`Invalid OpenCode configuration: ${target}`);
|
|
102
|
+
}
|
|
103
|
+
const entries = ['plugin', 'plugins'].flatMap((field) => {
|
|
104
|
+
const plugins = config[field];
|
|
105
|
+
if (plugins === undefined)
|
|
106
|
+
return [];
|
|
107
|
+
if (!Array.isArray(plugins))
|
|
108
|
+
throw new Error(`Invalid OpenCode ${field} configuration: ${target}`);
|
|
109
|
+
return plugins.flatMap((entry, index) => {
|
|
110
|
+
const spec = openCodePluginSpec(entry);
|
|
111
|
+
const options = Array.isArray(entry) ? entry[1]
|
|
112
|
+
: typeof entry === 'object' && entry !== null ? entry.options : undefined;
|
|
113
|
+
return spec === undefined ? [] : [{ field, index, spec, options, value: entry }];
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
return [{ target, source, entries }];
|
|
117
|
+
});
|
|
108
118
|
}
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
|
|
119
|
+
export function uninstallOpenCodePlugin(homeDir) {
|
|
120
|
+
// Native remove only handles `plugins` in one file. Delete our elements in
|
|
121
|
+
// both fields/files, from the end so indexes remain stable, keeping JSONC.
|
|
122
|
+
const changes = readOpenCodeConfigs(homeDir).filter(({ entries }) => entries.length).map(({ target, source, entries }) => {
|
|
123
|
+
for (const { field, index } of entries.reverse()) {
|
|
124
|
+
source = removeOpenCodeEntry(source, field, index);
|
|
125
|
+
}
|
|
126
|
+
return { target, source };
|
|
127
|
+
});
|
|
128
|
+
for (const { target, source } of changes)
|
|
129
|
+
fs.writeFileSync(target, source);
|
|
130
|
+
return changes.length ? [SQUARE_IDENTITY.packageName] : [];
|
|
131
|
+
}
|
|
132
|
+
// jsonc-parser's array `modify` also removes intervening comments. Use its AST
|
|
133
|
+
// and scanner to remove only the owned value and one separator, not the trivia.
|
|
134
|
+
function removeOpenCodeEntry(source, field, index) {
|
|
135
|
+
const array = findNodeAtLocation(parseTree(source), [field]);
|
|
136
|
+
const node = array.children[index];
|
|
137
|
+
const scanner = createScanner(source, true);
|
|
138
|
+
scanner.setPosition(node.offset + node.length);
|
|
139
|
+
let comma = scanner.scan() === 5 /* SyntaxKind.CommaToken */ ? scanner.getTokenOffset() : undefined;
|
|
140
|
+
if (comma === undefined && index > 0) {
|
|
141
|
+
const previous = array.children[index - 1];
|
|
142
|
+
scanner.setPosition(previous.offset + previous.length);
|
|
143
|
+
if (scanner.scan() === 5 /* SyntaxKind.CommaToken */)
|
|
144
|
+
comma = scanner.getTokenOffset();
|
|
145
|
+
}
|
|
146
|
+
return applyEdits(source, [
|
|
147
|
+
{ offset: node.offset, length: node.length, content: '' },
|
|
148
|
+
...(comma === undefined ? [] : [{ offset: comma, length: 1, content: '' }]),
|
|
149
|
+
]);
|
|
150
|
+
}
|
|
151
|
+
function openCodePluginSpec(entry) {
|
|
152
|
+
const spec = typeof entry === 'string' ? entry : Array.isArray(entry) ? entry[0]
|
|
153
|
+
: typeof entry === 'object' && entry !== null && 'package' in entry ? entry.package : undefined;
|
|
154
|
+
return typeof spec === 'string' && (spec === SQUARE_IDENTITY.packageName || spec.startsWith(`${SQUARE_IDENTITY.packageName}@`))
|
|
155
|
+
? spec : undefined;
|
|
156
|
+
}
|
|
157
|
+
function parseOpenCodeConfig(stdout) {
|
|
158
|
+
let sources;
|
|
112
159
|
try {
|
|
113
|
-
|
|
160
|
+
sources = JSON.parse(stdout);
|
|
114
161
|
}
|
|
115
162
|
catch {
|
|
116
|
-
|
|
163
|
+
throw new Error('OpenCode debug config returned invalid JSON');
|
|
117
164
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
fs.writeFileSync(target, next);
|
|
124
|
-
return true;
|
|
125
|
-
}
|
|
126
|
-
export function uninstallOpenCodePlugin(homeDir) {
|
|
127
|
-
return removeConfiguredPlugin(homeDir) ? [SQUARE_IDENTITY.packageName] : [];
|
|
165
|
+
if (!Array.isArray(sources) || sources.some((source) => source === null || typeof source !== 'object'
|
|
166
|
+
|| !['document', 'directory'].includes(source.type))) {
|
|
167
|
+
throw new Error('OpenCode debug config returned unexpected configuration sources');
|
|
168
|
+
}
|
|
169
|
+
return sources;
|
|
128
170
|
}
|
|
129
|
-
/**
|
|
171
|
+
/** Config sources prove configuration, not that a running session loaded the plugin. */
|
|
130
172
|
export function verifyOpenCodeRuntime(homeDir, run = runOpenCode) {
|
|
131
173
|
try {
|
|
132
174
|
const result = run(homeDir, ['debug', 'config']);
|
|
@@ -134,28 +176,21 @@ export function verifyOpenCodeRuntime(homeDir, run = runOpenCode) {
|
|
|
134
176
|
const diagnostic = result.stderr.trim() || result.stdout.trim() || `exit ${result.status}`;
|
|
135
177
|
return `✕ OpenCode debug config failed: ${truncateExternalDiagnostic(diagnostic)}`;
|
|
136
178
|
}
|
|
137
|
-
let
|
|
179
|
+
let sources;
|
|
138
180
|
try {
|
|
139
|
-
|
|
181
|
+
sources = parseOpenCodeConfig(result.stdout);
|
|
140
182
|
}
|
|
141
|
-
catch {
|
|
142
|
-
return
|
|
183
|
+
catch (error) {
|
|
184
|
+
return `✕ ${error.message}`;
|
|
143
185
|
}
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
186
|
+
const configured = sources.some((source) => source.type === 'document'
|
|
187
|
+
&& Array.isArray(source.info?.plugins) && source.info.plugins.some((entry) => openCodePluginSpec(entry) !== undefined));
|
|
188
|
+
return configured
|
|
189
|
+
? '✓ OpenCode npm plugin configured (runtime loading not verified)'
|
|
190
|
+
: `○ OpenCode npm plugin not configured: ${SQUARE_IDENTITY.packageName}`;
|
|
149
191
|
}
|
|
150
192
|
catch (error) {
|
|
151
193
|
const diagnostic = error instanceof Error ? error.message : String(error);
|
|
152
194
|
return `○ OpenCode runtime unavailable (${truncateExternalDiagnostic(diagnostic)})`;
|
|
153
195
|
}
|
|
154
196
|
}
|
|
155
|
-
export function skillLinks(homeDir = os.homedir(), parents = ['.claude', '.agents']) {
|
|
156
|
-
return parents.flatMap((parent) => ['square'].map((name) => ({
|
|
157
|
-
source: path.join(packageRoot(), 'skills', name),
|
|
158
|
-
target: path.join(homeDir, parent, 'skills', name),
|
|
159
|
-
kind: 'skill',
|
|
160
|
-
})));
|
|
161
|
-
}
|
package/dist/harness.js
CHANGED
|
@@ -5,7 +5,7 @@ import { truncateExternalDiagnostic } from './presentation.js';
|
|
|
5
5
|
import { doctorClaudePlugin, installClaudePlugin, uninstallClaudePlugin, } from './harness-claude.js';
|
|
6
6
|
import { doctorCodexPlugin, installCodexPlugin, uninstallCodexPlugin, } from './harness-codex.js';
|
|
7
7
|
import { doctorPiPackage, installPiPackage, uninstallPiPackage, } from './harness-pi.js';
|
|
8
|
-
import {
|
|
8
|
+
import { installOpenCodePlugin, uninstallOpenCodePlugin, verifyOpenCodeRuntime, } from './harness-links.js';
|
|
9
9
|
function result(lines, notes = []) {
|
|
10
10
|
return { lines, notes };
|
|
11
11
|
}
|
|
@@ -18,9 +18,6 @@ async function doctorHost(label, inspect) {
|
|
|
18
18
|
return result([`○ ${label} doctor unavailable (${truncateExternalDiagnostic(diagnostic)})`]);
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
-
function openCodeLinks(homeDir) {
|
|
22
|
-
return skillLinks(homeDir, ['.agents']);
|
|
23
|
-
}
|
|
24
21
|
function readableSquarePath(squarePath) {
|
|
25
22
|
if (squarePath === undefined)
|
|
26
23
|
return false;
|
|
@@ -67,15 +64,9 @@ const TARGETS = [
|
|
|
67
64
|
{
|
|
68
65
|
name: 'opencode',
|
|
69
66
|
capabilities: ['install', 'uninstall', 'doctor'],
|
|
70
|
-
install:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
]),
|
|
74
|
-
uninstall: ({ homeDir }) => result([
|
|
75
|
-
...uninstallOpenCodePlugin(homeDir),
|
|
76
|
-
...uninstallHarnessLinks(openCodeLinks(homeDir)),
|
|
77
|
-
]),
|
|
78
|
-
doctor: ({ homeDir }) => result([verifyOpenCodeRuntime(homeDir), ...doctorHarnessLinks(openCodeLinks(homeDir))]),
|
|
67
|
+
install: ({ homeDir, force }) => result(installOpenCodePlugin(homeDir, force)),
|
|
68
|
+
uninstall: ({ homeDir }) => result(uninstallOpenCodePlugin(homeDir)),
|
|
69
|
+
doctor: ({ homeDir }) => result([verifyOpenCodeRuntime(homeDir)]),
|
|
79
70
|
},
|
|
80
71
|
{
|
|
81
72
|
name: 'pi',
|
package/dist/square-location.js
CHANGED
|
@@ -3,7 +3,8 @@ import os from 'node:os';
|
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { SquareError } from './model.js';
|
|
5
5
|
export function parseSquareLocation(value) {
|
|
6
|
-
|
|
6
|
+
// Windows roots include drive-relative prefixes such as D:.
|
|
7
|
+
if (path.win32.parse(value).root !== '' || !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value))
|
|
7
8
|
return { kind: 'local', path: value };
|
|
8
9
|
// Parse the raw components, before URL dot-segment normalization or host lowercasing.
|
|
9
10
|
const match = /^stdio:\/\/([A-Za-z0-9_-]+)\/([^/?#]+)$/.exec(value);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrosheep/square",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.93",
|
|
4
4
|
"description": "A shared public square where agents join, catch activity, express, and step out when done.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
"dependencies": {
|
|
81
81
|
"@modelcontextprotocol/sdk": "^1.32.0",
|
|
82
82
|
"cross-spawn": "^7.0.6",
|
|
83
|
+
"jsonc-parser": "^3.3.1",
|
|
83
84
|
"zod": "^4.6.5"
|
|
84
85
|
},
|
|
85
86
|
"peerDependencies": {
|