@astrosheep/square 0.4.0 → 0.4.1
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/artifact.d.ts +1 -3
- package/dist/artifact.js +12 -36
- package/dist/attention-presentation.d.ts +2 -22
- package/dist/attention-presentation.js +9 -12
- package/dist/boundary-presentation.d.ts +3 -14
- package/dist/boundary-presentation.js +29 -104
- package/dist/card-presentation.d.ts +1 -0
- package/dist/card-presentation.js +2 -0
- package/dist/catch-decisions.js +1 -7
- package/dist/claude-delivery.d.ts +13 -8
- package/dist/claude-delivery.js +40 -93
- package/dist/claude-mcp-context.js +2 -2
- package/dist/claude-mod.js +28 -140
- package/dist/claude-transport.d.ts +3 -3
- package/dist/claude-transport.js +20 -44
- package/dist/cli/context.js +7 -9
- package/dist/cli/observation-commands.d.ts +2 -0
- package/dist/cli/observation-commands.js +12 -2
- package/dist/cli/registry.js +3 -1
- package/dist/codex-address.d.ts +2 -2
- package/dist/codex-boundary-state.d.ts +1 -5
- package/dist/codex-boundary-state.js +0 -4
- package/dist/codex-hook.d.ts +7 -10
- package/dist/codex-hook.js +69 -33
- package/dist/codex-queue.d.ts +10 -11
- package/dist/codex-queue.js +32 -32
- package/dist/codex-watch.d.ts +2 -0
- package/dist/codex-watch.js +100 -0
- package/dist/decisions.js +2 -2
- package/dist/delivery-deferral.d.ts +8 -17
- package/dist/delivery-deferral.js +0 -28
- package/dist/delivery-frame.d.ts +7 -0
- package/dist/delivery-frame.js +11 -0
- package/dist/delivery-ledger-sqlite.d.ts +97 -0
- package/dist/delivery-ledger-sqlite.js +496 -0
- package/dist/delivery-ledger.d.ts +196 -0
- package/dist/delivery-ledger.js +268 -0
- package/dist/delivery-lifetime.d.ts +6 -0
- package/dist/delivery-lifetime.js +45 -0
- package/dist/harness-links.js +61 -7
- package/dist/harness.js +12 -17
- package/dist/help.js +2 -0
- package/dist/http-delivery.d.ts +5 -9
- package/dist/http-delivery.js +20 -77
- package/dist/inbox.d.ts +6 -49
- package/dist/inbox.js +24 -371
- package/dist/index.d.ts +6 -3
- package/dist/index.js +4 -1
- package/dist/mcp.js +1 -1
- package/dist/model.d.ts +7 -47
- package/dist/model.js +1 -7
- package/dist/notifications.d.ts +24 -20
- package/dist/notifications.js +39 -108
- package/dist/operation-control.d.ts +4 -0
- package/dist/operation-control.js +20 -0
- package/dist/packages/agent-delivery/src/claude-native.d.ts +2 -0
- package/dist/packages/agent-delivery/src/claude-native.js +5 -0
- package/dist/packages/agent-delivery/src/codex-boundary.d.ts +11 -6
- package/dist/packages/agent-delivery/src/codex-boundary.js +31 -40
- package/dist/packages/agent-delivery/src/codex-hook.d.ts +17 -12
- package/dist/packages/agent-delivery/src/codex-hook.js +26 -39
- package/dist/packages/agent-delivery/src/codex.d.ts +3 -0
- package/dist/packages/agent-delivery/src/codex.js +41 -4
- package/dist/packages/agent-delivery/src/index.d.ts +3 -1
- package/dist/packages/agent-delivery/src/index.js +1 -1
- package/dist/packages/agent-delivery/src/paseo.js +11 -2
- package/dist/packages/agent-delivery/src/types.d.ts +1 -0
- package/dist/paseo-delivery.d.ts +5 -5
- package/dist/paseo-delivery.js +19 -97
- package/dist/paseo.d.ts +1 -1
- package/dist/paseo.js +1 -1
- package/dist/plugins/opencode/index.d.ts +5 -3
- package/dist/plugins/opencode/index.js +99 -21
- package/dist/plugins/opencode-shared/delivery.d.ts +23 -0
- package/dist/plugins/opencode-shared/delivery.js +109 -0
- package/dist/plugins/opencode-v1/index.d.ts +28 -0
- package/dist/plugins/opencode-v1/index.js +41 -0
- package/dist/plugins/opencode-v1/runtime.d.ts +56 -0
- package/dist/plugins/opencode-v1/runtime.js +152 -0
- package/dist/plugins/opencode-v1/transport.d.ts +13 -0
- package/dist/plugins/opencode-v1/transport.js +50 -0
- package/dist/plugins/paseo/shared/card-presentation.d.ts +10 -0
- package/dist/plugins/paseo/shared/card-presentation.js +7 -0
- package/dist/ports.d.ts +31 -90
- package/dist/session-lifecycle.d.ts +8 -7
- package/dist/session-lifecycle.js +40 -42
- package/dist/sqlite-square.d.ts +14 -23
- package/dist/sqlite-square.js +132 -139
- package/dist/square-api.js +4 -7
- package/dist/square-core.d.ts +0 -1
- package/dist/square-domain.d.ts +5 -17
- package/dist/square-domain.js +104 -221
- package/dist/square-facade.d.ts +18 -15
- package/dist/square-post.d.ts +53 -0
- package/dist/square-post.js +4 -0
- package/dist/square-posting.d.ts +13 -0
- package/dist/square-posting.js +174 -0
- package/dist/square-producer-sqlite.d.ts +11 -0
- package/dist/square-producer-sqlite.js +210 -0
- package/dist/square-sqlite.d.ts +15 -25
- package/dist/square-sqlite.js +150 -226
- package/dist/views.d.ts +2 -2
- package/dist/views.js +0 -1
- package/dist/wake-sink.d.ts +4 -11
- package/dist/wake-sink.js +8 -32
- package/dist/wake-transports.d.ts +4 -7
- package/dist/wake-transports.js +8 -39
- package/examples/symmetric-domains.mjs +29 -249
- package/guides/change-waits.md +13 -53
- package/guides/claude-native.md +19 -175
- package/guides/codex-delivery.md +55 -80
- package/guides/delivery-primitives.md +48 -207
- package/guides/operation-cancellation.md +2 -5
- package/guides/participant-ownership.md +25 -89
- package/guides/pi-native-delivery.md +19 -56
- package/guides/pi-tools.md +6 -8
- package/guides/square-api.md +113 -615
- package/package.json +8 -8
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/hooks/hooks.json +1 -1
- package/plugins/claude/{hooks/register.js → mods/square.js} +4 -7
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/codex/hooks/hooks.json +11 -23
- package/plugins/opencode/index.ts +91 -23
- package/plugins/opencode-shared/delivery.ts +91 -0
- package/plugins/opencode-v1/index.ts +36 -0
- package/plugins/opencode-v1/runtime.ts +134 -0
- package/plugins/opencode-v1/transport.ts +41 -0
- package/plugins/paseo/OVERVIEW.md +5 -5
- package/plugins/paseo/README.md +7 -6
- package/plugins/paseo/client/markdown.tsx +5 -5
- package/plugins/paseo/client/square-message.tsx +28 -24
- package/plugins/paseo/client/transform.ts +8 -5
- package/plugins/paseo/shared/card-presentation.ts +16 -0
- package/plugins/paseo/shared/message.ts +6 -11
- package/plugins/pi/square-pi.js +156 -417
- package/dist/channel-artifact.d.ts +0 -10
- package/dist/channel-artifact.js +0 -55
- package/dist/channel-delivery.d.ts +0 -11
- package/dist/channel-delivery.js +0 -51
- package/dist/channel-domain.d.ts +0 -3
- package/dist/channel-domain.js +0 -83
- package/dist/channel-facade.d.ts +0 -76
- package/dist/channel-facade.js +0 -1
- package/dist/channel-internal.d.ts +0 -126
- package/dist/channel-internal.js +0 -440
- package/dist/channel-metadata.d.ts +0 -11
- package/dist/channel-metadata.js +0 -1
- package/dist/channel-ports.d.ts +0 -241
- package/dist/channel-ports.js +0 -1
- package/dist/channel-presentation.d.ts +0 -17
- package/dist/channel-presentation.js +0 -22
- package/dist/channel-sqlite-schema.d.ts +0 -13
- package/dist/channel-sqlite-schema.js +0 -81
- package/dist/channel-transport.d.ts +0 -62
- package/dist/channel-transport.js +0 -1
- package/dist/channel.d.ts +0 -84
- package/dist/channel.js +0 -60
- package/dist/claude-delivery-status.d.ts +0 -13
- package/dist/claude-delivery-status.js +0 -46
- package/dist/cmd/notify-once.d.ts +0 -2
- package/dist/cmd/notify-once.js +0 -29
- package/dist/delivery-attempts.d.ts +0 -52
- package/dist/delivery-attempts.js +0 -73
- package/dist/delivery-effects.d.ts +0 -139
- package/dist/delivery-effects.js +0 -163
- package/dist/delivery-health.d.ts +0 -51
- package/dist/delivery-health.js +0 -173
- package/dist/delivery-operations.d.ts +0 -22
- package/dist/delivery-operations.js +0 -198
- package/dist/delivery-primitives.d.ts +0 -5
- package/dist/delivery-primitives.js +0 -3
- package/dist/delivery.d.ts +0 -56
- package/dist/delivery.js +0 -80
- package/dist/plugins/opencode/delivery.d.ts +0 -19
- package/dist/plugins/opencode/delivery.js +0 -223
- package/dist/presentation-operations.d.ts +0 -5
- package/dist/presentation-operations.js +0 -86
- package/dist/sqlite-channel.d.ts +0 -15
- package/dist/sqlite-channel.js +0 -600
- package/dist/square-index.d.ts +0 -16
- package/dist/square-index.js +0 -106
- package/dist/wake-preview.d.ts +0 -7
- package/dist/wake-preview.js +0 -15
- package/plugins/opencode/delivery.ts +0 -198
- /package/dist/{channel-values.d.ts → square-values.d.ts} +0 -0
- /package/dist/{channel-values.js → square-values.js} +0 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { type Owner } from './neutral.js';
|
|
2
|
+
import { type VersionObserver } from './file-changes.js';
|
|
3
|
+
export interface DeliveryRoute {
|
|
4
|
+
readonly kind: string;
|
|
5
|
+
readonly address: Readonly<Record<string, string>>;
|
|
6
|
+
}
|
|
7
|
+
export interface DeliverySession {
|
|
8
|
+
readonly owner: Owner;
|
|
9
|
+
readonly generation: string;
|
|
10
|
+
readonly state: 'attached' | 'detached' | 'deleted';
|
|
11
|
+
readonly routes: readonly DeliveryRoute[];
|
|
12
|
+
readonly at: number;
|
|
13
|
+
}
|
|
14
|
+
export interface DeliveryCard {
|
|
15
|
+
readonly seq: number;
|
|
16
|
+
readonly owner: Owner;
|
|
17
|
+
readonly key: string;
|
|
18
|
+
readonly text: string;
|
|
19
|
+
readonly details?: Readonly<Record<string, unknown>>;
|
|
20
|
+
readonly postedAt: number;
|
|
21
|
+
}
|
|
22
|
+
export interface DeliveryCorrelation {
|
|
23
|
+
readonly token?: string;
|
|
24
|
+
readonly messageId?: string;
|
|
25
|
+
readonly payloadHash?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface DeliveryHandoffRef {
|
|
28
|
+
readonly token: string;
|
|
29
|
+
readonly owner: Owner;
|
|
30
|
+
readonly generation: string;
|
|
31
|
+
readonly payloadHash: string;
|
|
32
|
+
readonly routeKind: string;
|
|
33
|
+
}
|
|
34
|
+
export type DeliveryOutcome = 'dispatching' | 'accepted' | 'presented' | 'failed' | 'unknown' | 'released';
|
|
35
|
+
export type PresentationOutcome = 'presented' | 'failed' | 'unknown' | 'released';
|
|
36
|
+
export interface DeliveryAttempt {
|
|
37
|
+
readonly token: string;
|
|
38
|
+
readonly seq: number;
|
|
39
|
+
readonly generation: string;
|
|
40
|
+
readonly track: 'handoff' | 'presentation';
|
|
41
|
+
readonly routeKind: string | null;
|
|
42
|
+
readonly outcome: DeliveryOutcome;
|
|
43
|
+
readonly correlation: DeliveryCorrelation;
|
|
44
|
+
readonly startedAt: number;
|
|
45
|
+
readonly expiresAt: number;
|
|
46
|
+
readonly settledAt?: number;
|
|
47
|
+
}
|
|
48
|
+
export interface DeliveryReceipt {
|
|
49
|
+
readonly owner: Owner;
|
|
50
|
+
readonly key: string;
|
|
51
|
+
readonly at: number;
|
|
52
|
+
}
|
|
53
|
+
export type PostResult = {
|
|
54
|
+
readonly status: 'posted' | 'duplicate' | 'conflict' | 'unknown-session' | 'deleted-session' | 'suppressed';
|
|
55
|
+
};
|
|
56
|
+
export type SuppressionResult = {
|
|
57
|
+
readonly id: string;
|
|
58
|
+
readonly status: 'suppressed' | 'in-custody';
|
|
59
|
+
};
|
|
60
|
+
export interface DeliveryHealth {
|
|
61
|
+
readonly unknownAttempts: readonly DeliveryAttempt[];
|
|
62
|
+
readonly repeatedFailures: readonly {
|
|
63
|
+
readonly owner: Owner;
|
|
64
|
+
readonly key: string;
|
|
65
|
+
readonly count: number;
|
|
66
|
+
}[];
|
|
67
|
+
readonly sessionsWithoutRoute: readonly Owner[];
|
|
68
|
+
}
|
|
69
|
+
export interface PresentationClaim {
|
|
70
|
+
readonly cards: readonly DeliveryCard[];
|
|
71
|
+
readonly attempts: readonly DeliveryAttempt[];
|
|
72
|
+
/** Recheck immediately before handing text to the external receiver. */
|
|
73
|
+
current(signal?: AbortSignal): Promise<boolean>;
|
|
74
|
+
/** Captured custody survives attachment rotation; errors propagate without a false receipt. */
|
|
75
|
+
settle(outcome: PresentationOutcome): Promise<boolean>;
|
|
76
|
+
}
|
|
77
|
+
export interface DeliveryTransportContext {
|
|
78
|
+
readonly token: string;
|
|
79
|
+
readonly deadline: number;
|
|
80
|
+
readonly signal: AbortSignal;
|
|
81
|
+
prepare(correlation: DeliveryCorrelation): Promise<boolean>;
|
|
82
|
+
/** Adapters with awaits before I/O recheck here, then check signal/deadline synchronously. */
|
|
83
|
+
beforeSend(): Promise<boolean>;
|
|
84
|
+
}
|
|
85
|
+
export type TransportOutcome = 'accepted' | 'presented' | 'failed' | 'unknown' | 'not-capable';
|
|
86
|
+
export interface DeliveryTransport {
|
|
87
|
+
readonly kind: string;
|
|
88
|
+
send(card: DeliveryCard, address: Readonly<Record<string, string>>, context: DeliveryTransportContext): Promise<TransportOutcome>;
|
|
89
|
+
}
|
|
90
|
+
export interface LateHandoffSettlement {
|
|
91
|
+
readonly outcome: TransportOutcome | 'released';
|
|
92
|
+
readonly persistence: {
|
|
93
|
+
readonly status: 'recorded' | 'not-recorded';
|
|
94
|
+
} | {
|
|
95
|
+
readonly status: 'error';
|
|
96
|
+
readonly error: unknown;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export interface HandoffResult {
|
|
100
|
+
readonly outcome: TransportOutcome | 'released' | 'not-invoked';
|
|
101
|
+
readonly token?: string;
|
|
102
|
+
readonly persistence?: 'recorded' | 'not-recorded';
|
|
103
|
+
/** Interrupted effects may complete later; this handle never rejects and may stay pending. */
|
|
104
|
+
readonly lateSettlement?: Promise<LateHandoffSettlement>;
|
|
105
|
+
}
|
|
106
|
+
/** Shared by host and producer callers; an explicit delivery override always wins. */
|
|
107
|
+
export declare function deliveryLedgerLocation(env?: NodeJS.ProcessEnv, homeDir?: string): string;
|
|
108
|
+
/** One user-owned ledger; producers supply finished text and callers choose routes. */
|
|
109
|
+
export declare class DeliveryLedger {
|
|
110
|
+
private readonly store;
|
|
111
|
+
private readonly leaseMs;
|
|
112
|
+
private constructor();
|
|
113
|
+
static open(options?: {
|
|
114
|
+
readonly location?: string;
|
|
115
|
+
readonly clock?: () => number;
|
|
116
|
+
readonly leaseMs?: number;
|
|
117
|
+
}): Promise<DeliveryLedger>;
|
|
118
|
+
close(): Promise<void>;
|
|
119
|
+
attach(input: {
|
|
120
|
+
readonly owner: Owner;
|
|
121
|
+
readonly expectedGeneration: string | null;
|
|
122
|
+
readonly routes: readonly DeliveryRoute[];
|
|
123
|
+
}): Promise<DeliverySession | false>;
|
|
124
|
+
updateRoutes(input: {
|
|
125
|
+
readonly owner: Owner;
|
|
126
|
+
readonly generation: string;
|
|
127
|
+
readonly routes: readonly DeliveryRoute[];
|
|
128
|
+
}): Promise<boolean>;
|
|
129
|
+
detach(input: {
|
|
130
|
+
readonly owner: Owner;
|
|
131
|
+
readonly generation: string;
|
|
132
|
+
}): Promise<boolean>;
|
|
133
|
+
markDeleted(input: {
|
|
134
|
+
readonly owner: Owner;
|
|
135
|
+
readonly generation: string;
|
|
136
|
+
}): Promise<boolean>;
|
|
137
|
+
private lifecycle;
|
|
138
|
+
session(owner: Owner): Promise<DeliverySession | undefined>;
|
|
139
|
+
sessions(): Promise<readonly DeliverySession[]>;
|
|
140
|
+
pending(owner: Owner, options: {
|
|
141
|
+
readonly generation: string;
|
|
142
|
+
}): Promise<readonly DeliveryCard[]>;
|
|
143
|
+
handoffs(owner: Owner, options: {
|
|
144
|
+
readonly generation: string;
|
|
145
|
+
}): Promise<readonly DeliveryAttempt[]>;
|
|
146
|
+
post(input: {
|
|
147
|
+
readonly owner: Owner;
|
|
148
|
+
readonly key: string;
|
|
149
|
+
readonly text: string;
|
|
150
|
+
readonly details?: Readonly<Record<string, unknown>>;
|
|
151
|
+
}): Promise<PostResult>;
|
|
152
|
+
suppress(input: {
|
|
153
|
+
readonly owner: Owner;
|
|
154
|
+
readonly key: string;
|
|
155
|
+
readonly scope: 'exact' | 'prefix';
|
|
156
|
+
readonly expiresAt?: number;
|
|
157
|
+
}): Promise<SuppressionResult>;
|
|
158
|
+
renewSuppression(input: {
|
|
159
|
+
readonly owner: Owner;
|
|
160
|
+
readonly id: string;
|
|
161
|
+
readonly expiresAt: number;
|
|
162
|
+
}): Promise<boolean>;
|
|
163
|
+
releaseSuppression(input: {
|
|
164
|
+
readonly owner: Owner;
|
|
165
|
+
readonly id: string;
|
|
166
|
+
}): Promise<boolean>;
|
|
167
|
+
cancel(input: {
|
|
168
|
+
readonly owner: Owner;
|
|
169
|
+
readonly generation: string;
|
|
170
|
+
}): Promise<boolean>;
|
|
171
|
+
receipts(owner: Owner, options?: {
|
|
172
|
+
readonly keys?: readonly string[];
|
|
173
|
+
readonly since?: number;
|
|
174
|
+
}): Promise<readonly DeliveryReceipt[]>;
|
|
175
|
+
claimPresentation(owner: Owner, options: {
|
|
176
|
+
readonly generation: string;
|
|
177
|
+
readonly budgetChars: number;
|
|
178
|
+
readonly signal?: AbortSignal;
|
|
179
|
+
}): Promise<PresentationClaim | undefined>;
|
|
180
|
+
/** Resolve the original prepared handoff, including its stored route kind. */
|
|
181
|
+
handoff(token: string): Promise<DeliveryHandoffRef | undefined>;
|
|
182
|
+
guardHandoff(ref: DeliveryHandoffRef): Promise<boolean>;
|
|
183
|
+
acceptHandoff(ref: DeliveryHandoffRef): Promise<boolean>;
|
|
184
|
+
/** Confirms the exact existing external effect; never sends or returns text. */
|
|
185
|
+
observePresentation(handoff: string | DeliveryHandoffRef, options?: {
|
|
186
|
+
readonly requireCurrent?: boolean;
|
|
187
|
+
}): Promise<boolean>;
|
|
188
|
+
health(owner?: Owner): Promise<DeliveryHealth>;
|
|
189
|
+
/** Subscribe first; read the returned version, inspect state, then await changed(version,...). */
|
|
190
|
+
observe(owner: Owner): Promise<VersionObserver<string>>;
|
|
191
|
+
dispatchHandoff(owner: Owner, key: string, transport: DeliveryTransport, options: {
|
|
192
|
+
readonly generation: string;
|
|
193
|
+
readonly timeoutMs: number;
|
|
194
|
+
readonly signal?: AbortSignal;
|
|
195
|
+
}): Promise<HandoffResult>;
|
|
196
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { validateOwner } from './neutral.js';
|
|
4
|
+
import { observeFileMetadata } from './file-changes.js';
|
|
5
|
+
import { LedgerStore } from './delivery-ledger-sqlite.js';
|
|
6
|
+
function nonempty(value) {
|
|
7
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
8
|
+
throw new TypeError('Expected a nonempty string');
|
|
9
|
+
}
|
|
10
|
+
function positive(value) {
|
|
11
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
12
|
+
throw new RangeError('Expected a positive safe integer');
|
|
13
|
+
}
|
|
14
|
+
function routes(value) {
|
|
15
|
+
if (!Array.isArray(value))
|
|
16
|
+
throw new TypeError('Expected routes');
|
|
17
|
+
const kinds = new Set();
|
|
18
|
+
for (const route of value) {
|
|
19
|
+
nonempty(route.kind);
|
|
20
|
+
if (kinds.has(route.kind))
|
|
21
|
+
throw new TypeError('Duplicate route kind');
|
|
22
|
+
kinds.add(route.kind);
|
|
23
|
+
if (route.address === null || typeof route.address !== 'object' || Array.isArray(route.address)
|
|
24
|
+
|| !Object.values(route.address).every(item => typeof item === 'string'))
|
|
25
|
+
throw new TypeError('Expected string address fields');
|
|
26
|
+
if (Object.keys(route).some(key => key !== 'kind' && key !== 'address'))
|
|
27
|
+
throw new TypeError('Unexpected route field');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function correlation(value) {
|
|
31
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
32
|
+
|| !Object.entries(value).every(([key, item]) => ['token', 'messageId', 'payloadHash'].includes(key) && typeof item === 'string'))
|
|
33
|
+
throw new TypeError('Invalid correlation');
|
|
34
|
+
}
|
|
35
|
+
/** Shared by host and producer callers; an explicit delivery override always wins. */
|
|
36
|
+
export function deliveryLedgerLocation(env = process.env, homeDir = os.homedir()) {
|
|
37
|
+
return env.SQUARE_DELIVERY ?? path.join(homeDir, '.square', 'delivery.v3.sqlite');
|
|
38
|
+
}
|
|
39
|
+
function encodeDetails(details) {
|
|
40
|
+
const ancestors = new Set();
|
|
41
|
+
const encode = (value) => {
|
|
42
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean'
|
|
43
|
+
|| typeof value === 'number' && Number.isFinite(value))
|
|
44
|
+
return value;
|
|
45
|
+
if (typeof value !== 'object' || !value || ancestors.has(value)
|
|
46
|
+
|| !Array.isArray(value) && ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
|
|
47
|
+
throw new TypeError('Expected JSON card details');
|
|
48
|
+
ancestors.add(value);
|
|
49
|
+
const result = Array.isArray(value) ? Array.from(value, encode)
|
|
50
|
+
: Object.fromEntries(Object.keys(value).sort().map(key => [key, encode(value[key])]));
|
|
51
|
+
ancestors.delete(value);
|
|
52
|
+
return result;
|
|
53
|
+
};
|
|
54
|
+
if (!details || typeof details !== 'object' || Array.isArray(details))
|
|
55
|
+
throw new TypeError('Expected a card details object');
|
|
56
|
+
return JSON.stringify(encode(details));
|
|
57
|
+
}
|
|
58
|
+
/** One user-owned ledger; producers supply finished text and callers choose routes. */
|
|
59
|
+
export class DeliveryLedger {
|
|
60
|
+
store;
|
|
61
|
+
leaseMs;
|
|
62
|
+
constructor(store, leaseMs) {
|
|
63
|
+
this.store = store;
|
|
64
|
+
this.leaseMs = leaseMs;
|
|
65
|
+
}
|
|
66
|
+
static async open(options = {}) {
|
|
67
|
+
const location = options.location ?? deliveryLedgerLocation();
|
|
68
|
+
if (!path.isAbsolute(location))
|
|
69
|
+
throw new TypeError('Delivery location must be absolute');
|
|
70
|
+
const leaseMs = options.leaseMs ?? 30_000;
|
|
71
|
+
positive(leaseMs);
|
|
72
|
+
return new DeliveryLedger(await LedgerStore.open(location, options.clock ?? Date.now), leaseMs);
|
|
73
|
+
}
|
|
74
|
+
close() { return this.store.close(); }
|
|
75
|
+
attach(input) {
|
|
76
|
+
validateOwner(input.owner);
|
|
77
|
+
routes(input.routes);
|
|
78
|
+
if (input.expectedGeneration !== null)
|
|
79
|
+
nonempty(input.expectedGeneration);
|
|
80
|
+
return this.store.attach(input);
|
|
81
|
+
}
|
|
82
|
+
updateRoutes(input) {
|
|
83
|
+
validateOwner(input.owner);
|
|
84
|
+
nonempty(input.generation);
|
|
85
|
+
routes(input.routes);
|
|
86
|
+
return this.store.updateRoutes(input);
|
|
87
|
+
}
|
|
88
|
+
detach(input) { return this.lifecycle(input, 'detached'); }
|
|
89
|
+
markDeleted(input) { return this.lifecycle(input, 'deleted'); }
|
|
90
|
+
lifecycle(input, state) {
|
|
91
|
+
validateOwner(input.owner);
|
|
92
|
+
nonempty(input.generation);
|
|
93
|
+
return this.store.lifecycle(input, state);
|
|
94
|
+
}
|
|
95
|
+
session(owner) { validateOwner(owner); return this.store.session(owner); }
|
|
96
|
+
sessions() { return this.store.sessions(); }
|
|
97
|
+
pending(owner, options) {
|
|
98
|
+
validateOwner(owner);
|
|
99
|
+
nonempty(options.generation);
|
|
100
|
+
return this.store.pendingCards(owner, options.generation);
|
|
101
|
+
}
|
|
102
|
+
handoffs(owner, options) {
|
|
103
|
+
validateOwner(owner);
|
|
104
|
+
nonempty(options.generation);
|
|
105
|
+
return this.store.handoffs(owner, options.generation);
|
|
106
|
+
}
|
|
107
|
+
post(input) {
|
|
108
|
+
validateOwner(input.owner);
|
|
109
|
+
nonempty(input.key);
|
|
110
|
+
if (typeof input.text !== 'string')
|
|
111
|
+
throw new TypeError('Expected card text');
|
|
112
|
+
return this.store.post({ ...input, details: input.details === undefined ? null : encodeDetails(input.details) });
|
|
113
|
+
}
|
|
114
|
+
suppress(input) {
|
|
115
|
+
validateOwner(input.owner);
|
|
116
|
+
nonempty(input.key);
|
|
117
|
+
if (!['exact', 'prefix'].includes(input.scope) || input.expiresAt !== undefined && !Number.isFinite(input.expiresAt))
|
|
118
|
+
throw new TypeError('Invalid suppression');
|
|
119
|
+
return this.store.suppress(input);
|
|
120
|
+
}
|
|
121
|
+
renewSuppression(input) {
|
|
122
|
+
validateOwner(input.owner);
|
|
123
|
+
nonempty(input.id);
|
|
124
|
+
if (!Number.isFinite(input.expiresAt))
|
|
125
|
+
throw new TypeError('Expected finite suppression expiry');
|
|
126
|
+
return this.store.renewSuppression(input);
|
|
127
|
+
}
|
|
128
|
+
releaseSuppression(input) {
|
|
129
|
+
validateOwner(input.owner);
|
|
130
|
+
nonempty(input.id);
|
|
131
|
+
return this.store.releaseSuppression(input);
|
|
132
|
+
}
|
|
133
|
+
cancel(input) {
|
|
134
|
+
validateOwner(input.owner);
|
|
135
|
+
nonempty(input.generation);
|
|
136
|
+
return this.store.cancel(input);
|
|
137
|
+
}
|
|
138
|
+
receipts(owner, options = {}) {
|
|
139
|
+
validateOwner(owner);
|
|
140
|
+
if (options.keys)
|
|
141
|
+
options.keys.forEach(nonempty);
|
|
142
|
+
if (options.since !== undefined && !Number.isFinite(options.since))
|
|
143
|
+
throw new TypeError('Expected finite receipt time');
|
|
144
|
+
return this.store.receipts(owner, options);
|
|
145
|
+
}
|
|
146
|
+
async claimPresentation(owner, options) {
|
|
147
|
+
validateOwner(owner);
|
|
148
|
+
nonempty(options.generation);
|
|
149
|
+
positive(options.budgetChars);
|
|
150
|
+
if (options.signal?.aborted)
|
|
151
|
+
return undefined;
|
|
152
|
+
const claim = await this.store.claimPresentation(owner, options.generation, options.budgetChars, this.leaseMs, options.signal);
|
|
153
|
+
if (!claim)
|
|
154
|
+
return undefined;
|
|
155
|
+
const tokens = claim.attempts.map(attempt => attempt.token);
|
|
156
|
+
return { ...claim,
|
|
157
|
+
current: signal => this.store.current(tokens, signal ?? options.signal),
|
|
158
|
+
settle: outcome => {
|
|
159
|
+
if (!['presented', 'failed', 'unknown', 'released'].includes(outcome))
|
|
160
|
+
throw new TypeError('Invalid presentation outcome');
|
|
161
|
+
return this.store.settle(tokens, outcome);
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/** Resolve the original prepared handoff, including its stored route kind. */
|
|
166
|
+
handoff(token) {
|
|
167
|
+
nonempty(token);
|
|
168
|
+
return this.store.handoff(token);
|
|
169
|
+
}
|
|
170
|
+
guardHandoff(ref) { return this.store.guardHandoff(ref); }
|
|
171
|
+
acceptHandoff(ref) { return this.store.acceptHandoff(ref); }
|
|
172
|
+
/** Confirms the exact existing external effect; never sends or returns text. */
|
|
173
|
+
observePresentation(handoff, options) {
|
|
174
|
+
if (typeof handoff === 'string')
|
|
175
|
+
nonempty(handoff);
|
|
176
|
+
return this.store.observePresentation(handoff, options);
|
|
177
|
+
}
|
|
178
|
+
health(owner) {
|
|
179
|
+
if (owner !== undefined)
|
|
180
|
+
validateOwner(owner);
|
|
181
|
+
return this.store.health(owner);
|
|
182
|
+
}
|
|
183
|
+
/** Subscribe first; read the returned version, inspect state, then await changed(version,...). */
|
|
184
|
+
observe(owner) {
|
|
185
|
+
validateOwner(owner);
|
|
186
|
+
return observeFileMetadata(this.store.location);
|
|
187
|
+
}
|
|
188
|
+
async dispatchHandoff(owner, key, transport, options) {
|
|
189
|
+
validateOwner(owner);
|
|
190
|
+
nonempty(key);
|
|
191
|
+
nonempty(options.generation);
|
|
192
|
+
positive(options.timeoutMs);
|
|
193
|
+
if (options.signal?.aborted)
|
|
194
|
+
return { outcome: 'not-invoked' };
|
|
195
|
+
nonempty(transport.kind);
|
|
196
|
+
const claim = await this.store.claimHandoff(owner, key, options.generation, transport.kind, Math.max(this.leaseMs, options.timeoutMs), options.signal);
|
|
197
|
+
if (!claim)
|
|
198
|
+
return { outcome: 'not-invoked' };
|
|
199
|
+
const token = claim.attempt.token;
|
|
200
|
+
const controller = new AbortController();
|
|
201
|
+
const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
|
|
202
|
+
const deadline = Date.now() + options.timeoutMs;
|
|
203
|
+
let persistenceFailure;
|
|
204
|
+
const persist = async (operation) => {
|
|
205
|
+
try {
|
|
206
|
+
return await operation;
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
persistenceFailure = { error };
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
const current = () => persist(this.store.current([token], signal, claim.route));
|
|
214
|
+
const finish = async (outcome) => {
|
|
215
|
+
if (!['accepted', 'presented', 'failed', 'unknown', 'not-capable', 'released'].includes(outcome))
|
|
216
|
+
throw new TypeError('Invalid transport outcome');
|
|
217
|
+
const recorded = await this.store.settle([token], outcome === 'not-capable' ? 'released' : outcome);
|
|
218
|
+
if (persistenceFailure)
|
|
219
|
+
throw persistenceFailure.error;
|
|
220
|
+
return { outcome, token, persistence: recorded ? 'recorded' : 'not-recorded' };
|
|
221
|
+
};
|
|
222
|
+
if (!await current() || signal.aborted || Date.now() >= deadline)
|
|
223
|
+
return finish('released');
|
|
224
|
+
let timer;
|
|
225
|
+
let onAbort;
|
|
226
|
+
try {
|
|
227
|
+
const interrupted = new Promise(resolve => {
|
|
228
|
+
onAbort = () => resolve('unknown');
|
|
229
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
230
|
+
timer = setTimeout(() => { resolve('unknown'); controller.abort(new Error('Delivery deadline elapsed')); }, options.timeoutMs);
|
|
231
|
+
});
|
|
232
|
+
// Invoke synchronously after the final fence; adapters own checks after their own awaits.
|
|
233
|
+
const effect = (async () => {
|
|
234
|
+
if (signal.aborted || Date.now() >= deadline)
|
|
235
|
+
return 'released';
|
|
236
|
+
return transport.send(claim.card, claim.route.address, { token, deadline, signal,
|
|
237
|
+
beforeSend: current,
|
|
238
|
+
prepare: value => { correlation(value); return persist(this.store.prepare(token, value, signal)); },
|
|
239
|
+
});
|
|
240
|
+
})().catch(() => 'unknown');
|
|
241
|
+
const first = await Promise.race([
|
|
242
|
+
effect.then(outcome => ({ outcome, interrupted: false })),
|
|
243
|
+
interrupted.then(outcome => ({ outcome, interrupted: true })),
|
|
244
|
+
]);
|
|
245
|
+
if (!first.interrupted)
|
|
246
|
+
return await finish(first.outcome);
|
|
247
|
+
const unknown = finish('unknown');
|
|
248
|
+
// Register before awaiting persistence; even an already completed effect waits for unknown.
|
|
249
|
+
const lateSettlement = unknown.then(async () => {
|
|
250
|
+
const outcome = await effect;
|
|
251
|
+
try {
|
|
252
|
+
const result = await finish(outcome);
|
|
253
|
+
return { outcome, persistence: { status: result.persistence } };
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
return { outcome, persistence: { status: 'error', error } };
|
|
257
|
+
}
|
|
258
|
+
}, error => ({ outcome: 'unknown', persistence: { status: 'error', error } }));
|
|
259
|
+
return { ...await unknown, lateSettlement };
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
if (timer !== undefined)
|
|
263
|
+
clearTimeout(timer);
|
|
264
|
+
if (onAbort)
|
|
265
|
+
signal.removeEventListener('abort', onAbort);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { DeliveryLedger, HandoffResult } from './delivery-ledger.js';
|
|
2
|
+
/** Retire the caller's connection without abandoning an already-invoked effect. */
|
|
3
|
+
export declare function retainDeliveryLedger(ledger: DeliveryLedger): {
|
|
4
|
+
track(result: HandoffResult): void;
|
|
5
|
+
close(): Promise<void>;
|
|
6
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { redactCurrentDiagnostic } from './diagnostic-redaction.js';
|
|
2
|
+
/** Retire the caller's connection without abandoning an already-invoked effect. */
|
|
3
|
+
export function retainDeliveryLedger(ledger) {
|
|
4
|
+
const pending = new Set();
|
|
5
|
+
let retired = false, closing;
|
|
6
|
+
const diagnose = (event, result, detail) => {
|
|
7
|
+
try {
|
|
8
|
+
const diagnostic = redactCurrentDiagnostic({ event, token: result?.token, outcome: result?.outcome, ...detail });
|
|
9
|
+
try {
|
|
10
|
+
console.warn(diagnostic);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
process.stderr.write(`${JSON.stringify(diagnostic)}\n`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
catch { /* Closed diagnostic sinks cannot prevent settlement or resource retirement. */ }
|
|
17
|
+
};
|
|
18
|
+
const closeOnce = () => closing ??= ledger.close();
|
|
19
|
+
const settled = (handle) => {
|
|
20
|
+
pending.delete(handle);
|
|
21
|
+
if (retired && !pending.size)
|
|
22
|
+
void closeOnce().catch(() => diagnose('delivery-ledger-deferred-close-error'));
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
track(result) {
|
|
26
|
+
const handle = result.lateSettlement;
|
|
27
|
+
if (!handle || pending.has(handle))
|
|
28
|
+
return;
|
|
29
|
+
pending.add(handle);
|
|
30
|
+
void handle.then(late => {
|
|
31
|
+
if (late.persistence.status !== 'recorded')
|
|
32
|
+
diagnose('delivery-late-settlement', result, { lateOutcome: late.outcome, persistence: late.persistence.status });
|
|
33
|
+
settled(handle);
|
|
34
|
+
}, () => {
|
|
35
|
+
diagnose('delivery-late-settlement-rejected', result);
|
|
36
|
+
settled(handle);
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
async close() {
|
|
40
|
+
retired = true;
|
|
41
|
+
if (!pending.size)
|
|
42
|
+
await closeOnce();
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
package/dist/harness-links.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
3
4
|
import { isDeepStrictEqual } from 'node:util';
|
|
4
5
|
import crossSpawn from 'cross-spawn';
|
|
5
6
|
import { applyEdits, createScanner, findNodeAtLocation, modify, parse, parseTree } from 'jsonc-parser';
|
|
@@ -28,22 +29,33 @@ export function installOpenCodePlugin(homeDir, force = false, run = runOpenCode)
|
|
|
28
29
|
// Explicit `square install` replaces owned pins/duplicates with one unpinned
|
|
29
30
|
// current-field entry; updating an exact old pin otherwise succeeds unchanged.
|
|
30
31
|
void force;
|
|
32
|
+
const version = openCodeCommand(homeDir, ['--version'], run, 'version check').trim();
|
|
33
|
+
const match = /^(\d+)\.\d+\.\d+(?:[-+][\w.-]+)?$/.exec(version);
|
|
34
|
+
const major = match && Number(match[1]);
|
|
35
|
+
if (major !== 1 && major !== 2)
|
|
36
|
+
throw new Error(`Unsupported OpenCode version ${JSON.stringify(version)}; install OpenCode 1.x or 2.x and retry.`);
|
|
31
37
|
const configs = readOpenCodeConfigs(homeDir);
|
|
32
38
|
const entries = configs.flatMap((config) => config.entries);
|
|
33
39
|
const options = entries.map((entry) => entry.options).filter((value) => value !== undefined);
|
|
34
40
|
if (options.some((value) => !isDeepStrictEqual(value, options[0]))) {
|
|
35
41
|
throw new Error('Conflicting Square plugin options in OpenCode configuration; reconcile the owned plugin/plugins entries before installing.');
|
|
36
42
|
}
|
|
37
|
-
|
|
43
|
+
if (major === 1 && options.length)
|
|
44
|
+
throw new Error('Square plugin options cannot be translated to OpenCode 1.x; reconcile the owned plugin options before installing.');
|
|
45
|
+
const canonical = major === 1 ? pathToFileURL(path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'plugins/opencode-v1/index.js')).href
|
|
46
|
+
: options.length ? { package: SQUARE_IDENTITY.packageName, options: options[0] } : SQUARE_IDENTITY.packageName;
|
|
47
|
+
if (major === 1 && !fs.existsSync(fileURLToPath(canonical)))
|
|
48
|
+
throw new Error('Square OpenCode 1.x plugin is missing from this installation; reinstall Square.');
|
|
49
|
+
const field = major === 1 ? 'plugin' : 'plugins';
|
|
38
50
|
const targets = configs.length ? configs : [{ target: openCodeConfigPaths(homeDir)[0], source: '{}', entries: [] }];
|
|
39
|
-
const alreadyCanonical = entries.length === 1 && configs[0]?.entries[0]?.field ===
|
|
51
|
+
const alreadyCanonical = entries.length === 1 && configs[0]?.entries[0]?.field === field
|
|
40
52
|
&& isDeepStrictEqual(entries[0].value, canonical);
|
|
41
53
|
const changes = alreadyCanonical ? [] : targets.map(({ target, source, entries }, index) => {
|
|
42
54
|
let next = source;
|
|
43
55
|
for (const { field, index } of entries.toReversed())
|
|
44
56
|
next = removeOpenCodeEntry(next, field, index);
|
|
45
57
|
if (index === 0)
|
|
46
|
-
next = applyEdits(next, modify(next, [
|
|
58
|
+
next = applyEdits(next, modify(next, [field, -1], canonical, {}));
|
|
47
59
|
return { target, source, next };
|
|
48
60
|
}).filter(({ source, next }) => source !== next);
|
|
49
61
|
// Appending just our element avoids native add's whole-array rewrite, which
|
|
@@ -52,6 +64,8 @@ export function installOpenCodePlugin(homeDir, force = false, run = runOpenCode)
|
|
|
52
64
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
53
65
|
fs.writeFileSync(target, next);
|
|
54
66
|
}
|
|
67
|
+
if (major === 1)
|
|
68
|
+
return [SQUARE_IDENTITY.packageName];
|
|
55
69
|
if (!entries.some(({ spec }) => spec === SQUARE_IDENTITY.packageName)) {
|
|
56
70
|
try {
|
|
57
71
|
openCodeCommand(homeDir, ['plugin', 'add', SQUARE_IDENTITY.packageName], run, 'install');
|
|
@@ -100,6 +114,7 @@ function readOpenCodeConfigs(homeDir) {
|
|
|
100
114
|
if (errors.length || typeof config !== 'object' || config === null || Array.isArray(config)) {
|
|
101
115
|
throw new Error(`Invalid OpenCode configuration: ${target}`);
|
|
102
116
|
}
|
|
117
|
+
const unproven = [];
|
|
103
118
|
const entries = ['plugin', 'plugins'].flatMap((field) => {
|
|
104
119
|
const plugins = config[field];
|
|
105
120
|
if (plugins === undefined)
|
|
@@ -108,12 +123,15 @@ function readOpenCodeConfigs(homeDir) {
|
|
|
108
123
|
throw new Error(`Invalid OpenCode ${field} configuration: ${target}`);
|
|
109
124
|
return plugins.flatMap((entry, index) => {
|
|
110
125
|
const spec = openCodePluginSpec(entry);
|
|
126
|
+
const raw = openCodeRawSpec(entry);
|
|
127
|
+
if (spec === undefined && raw !== undefined && openCodeV1Root(raw) !== undefined)
|
|
128
|
+
unproven.push(raw);
|
|
111
129
|
const options = Array.isArray(entry) ? entry[1]
|
|
112
130
|
: typeof entry === 'object' && entry !== null ? entry.options : undefined;
|
|
113
131
|
return spec === undefined ? [] : [{ field, index, spec, options, value: entry }];
|
|
114
132
|
});
|
|
115
133
|
});
|
|
116
|
-
return [{ target, source, entries }];
|
|
134
|
+
return [{ target, source, entries, unproven }];
|
|
117
135
|
});
|
|
118
136
|
}
|
|
119
137
|
export function uninstallOpenCodePlugin(homeDir) {
|
|
@@ -148,11 +166,37 @@ function removeOpenCodeEntry(source, field, index) {
|
|
|
148
166
|
...(comma === undefined ? [] : [{ offset: comma, length: 1, content: '' }]),
|
|
149
167
|
]);
|
|
150
168
|
}
|
|
151
|
-
function
|
|
169
|
+
function openCodeRawSpec(entry) {
|
|
152
170
|
const spec = typeof entry === 'string' ? entry : Array.isArray(entry) ? entry[0]
|
|
153
171
|
: typeof entry === 'object' && entry !== null && 'package' in entry ? entry.package : undefined;
|
|
154
|
-
return typeof spec === 'string'
|
|
155
|
-
|
|
172
|
+
return typeof spec === 'string' ? spec : undefined;
|
|
173
|
+
}
|
|
174
|
+
function openCodeV1Root(spec) {
|
|
175
|
+
try {
|
|
176
|
+
const file = fileURLToPath(spec);
|
|
177
|
+
return file.endsWith(`${path.sep}dist${path.sep}plugins${path.sep}opencode-v1${path.sep}index.js`)
|
|
178
|
+
? path.resolve(path.dirname(file), '../../..') : undefined;
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function openCodePluginSpec(entry) {
|
|
185
|
+
const spec = openCodeRawSpec(entry);
|
|
186
|
+
if (spec === undefined)
|
|
187
|
+
return undefined;
|
|
188
|
+
if (spec === SQUARE_IDENTITY.packageName || spec.startsWith(`${SQUARE_IDENTITY.packageName}@`))
|
|
189
|
+
return spec;
|
|
190
|
+
const root = openCodeV1Root(spec);
|
|
191
|
+
if (root === undefined)
|
|
192
|
+
return undefined;
|
|
193
|
+
try {
|
|
194
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
195
|
+
return manifest.name === SQUARE_IDENTITY.packageName ? spec : undefined;
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
156
200
|
}
|
|
157
201
|
function parseOpenCodeConfig(stdout) {
|
|
158
202
|
let sources;
|
|
@@ -171,6 +215,16 @@ function parseOpenCodeConfig(stdout) {
|
|
|
171
215
|
/** Config sources prove configuration, not that a running session loaded the plugin. */
|
|
172
216
|
export function verifyOpenCodeRuntime(homeDir, run = runOpenCode) {
|
|
173
217
|
try {
|
|
218
|
+
const configs = readOpenCodeConfigs(homeDir);
|
|
219
|
+
const local = configs.flatMap(config => config.entries).filter(entry => entry.spec.startsWith('file:'));
|
|
220
|
+
const missing = local.find(entry => !fs.existsSync(fileURLToPath(entry.spec)));
|
|
221
|
+
if (missing)
|
|
222
|
+
return `✕ OpenCode 1.x plugin file missing: ${missing.spec}; reinstall Square and run square install opencode`;
|
|
223
|
+
const unproven = configs.flatMap(config => config.unproven)[0];
|
|
224
|
+
if (unproven)
|
|
225
|
+
return `○ OpenCode plugin ownership unverified: ${unproven}; inspect it before removing or replacing it`;
|
|
226
|
+
if (local.length)
|
|
227
|
+
return '✓ OpenCode 1.x plugin configured (runtime loading not verified)';
|
|
174
228
|
const result = run(homeDir, ['debug', 'config']);
|
|
175
229
|
if (result.status !== 0) {
|
|
176
230
|
const diagnostic = result.stderr.trim() || result.stdout.trim() || `exit ${result.status}`;
|