@foldspace_npm/harness 0.1.2 → 0.1.3
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/README.md +61 -12
- package/bin/attach.mjs +779 -107
- package/bin/cli.mjs +122 -44
- package/bin/deploy.mjs +37 -33
- package/bin/inject.mjs +64 -295
- package/package.json +5 -2
- package/src/action-observer.mjs +271 -0
- package/src/attach-helpers.mjs +162 -0
- package/src/attach-preflight.mjs +332 -0
- package/src/bootstrap-script.mjs +120 -0
- package/src/cdp-request-manager.mjs +61 -0
- package/src/cli-help.mjs +143 -0
- package/src/cli-registry.mjs +309 -0
- package/src/diagnostics.mjs +482 -0
- package/src/init.mjs +2 -2
- package/src/project-config.mjs +37 -0
- package/src/protocol.mjs +181 -0
- package/src/session-summary.mjs +133 -0
- package/templates/agent-starter/CLAUDE.md +31 -8
- package/templates/agent-starter/README.md +26 -3
- package/templates/agent-starter/foldspace.dev.json +1 -3
- package/bin/buildExtension.mjs +0 -90
- package/bin/packageExtension.mjs +0 -29
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ATTACH_MODES,
|
|
3
|
+
actionRequestMatchesTarget,
|
|
4
|
+
parseActionRequest,
|
|
5
|
+
} from "./attach-helpers.mjs";
|
|
6
|
+
import { ERROR_CODES } from "./protocol.mjs";
|
|
7
|
+
|
|
8
|
+
export function productIdFromAgentKey(key) {
|
|
9
|
+
if (typeof key !== "string") return null;
|
|
10
|
+
const match = key.match(/^EU-([A-Za-z0-9_]+)-\d+-\d+$/);
|
|
11
|
+
return match?.[1] || null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function detectedProductIds(sdkState) {
|
|
15
|
+
const keys = [sdkState?.trackerKey, sdkState?.sdkKey];
|
|
16
|
+
for (const source of sdkState?.scripts || []) {
|
|
17
|
+
try {
|
|
18
|
+
keys.push(new URL(source).searchParams.get("k"));
|
|
19
|
+
} catch {
|
|
20
|
+
// Ignore malformed script URLs reported by the page.
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return [
|
|
24
|
+
...new Set(keys.map(productIdFromAgentKey).filter(Boolean)),
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function guardAttachMode({
|
|
29
|
+
mode,
|
|
30
|
+
characterization,
|
|
31
|
+
expectedProductId,
|
|
32
|
+
pageProductIds = [],
|
|
33
|
+
}) {
|
|
34
|
+
const productMismatch =
|
|
35
|
+
pageProductIds.length > 0 &&
|
|
36
|
+
!pageProductIds.includes(expectedProductId);
|
|
37
|
+
|
|
38
|
+
if (mode === ATTACH_MODES.BOOTSTRAP) {
|
|
39
|
+
if (characterization.status === "no-sdk") {
|
|
40
|
+
return { ok: true, mode };
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
mode,
|
|
45
|
+
code: ERROR_CODES.REGISTRATION_MISMATCH,
|
|
46
|
+
message: "Bootstrap mode requires a page without the Foldspace SDK.",
|
|
47
|
+
suggestion:
|
|
48
|
+
characterization.status === "same-agent"
|
|
49
|
+
? "Use swap mode without --bootstrap."
|
|
50
|
+
: "Use --replace to make the configured agent own the page.",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (mode === ATTACH_MODES.SWAP) {
|
|
55
|
+
if (characterization.status === "no-sdk") {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
mode,
|
|
59
|
+
code: ERROR_CODES.SDK_NOT_FOUND,
|
|
60
|
+
message: "Swap mode requires an existing Foldspace SDK and agent.",
|
|
61
|
+
suggestion: "Use --bootstrap when the page does not embed Foldspace.",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (characterization.status !== "same-agent") {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
mode,
|
|
68
|
+
code: ERROR_CODES.REGISTRATION_MISMATCH,
|
|
69
|
+
message:
|
|
70
|
+
"Swap mode requires exactly the configured agent on the page.",
|
|
71
|
+
suggestion:
|
|
72
|
+
"Use --replace when the page embeds a different agent, or close duplicate agent instances.",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (pageProductIds.length === 0) {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
mode,
|
|
79
|
+
code: ERROR_CODES.REGISTRATION_MISMATCH,
|
|
80
|
+
message:
|
|
81
|
+
"Swap mode could not verify which product owns the page agent.",
|
|
82
|
+
suggestion:
|
|
83
|
+
"Use --replace, or update the page SDK so its tracker key is inspectable.",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (productMismatch) {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
mode,
|
|
90
|
+
code: ERROR_CODES.REGISTRATION_MISMATCH,
|
|
91
|
+
message:
|
|
92
|
+
"The page agent API name matches, but its product does not match the configured product.",
|
|
93
|
+
suggestion: "Use --replace for a cross-product development session.",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return { ok: true, mode };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (mode === ATTACH_MODES.REPLACE) {
|
|
100
|
+
if (characterization.status === "no-sdk") {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
mode,
|
|
104
|
+
code: ERROR_CODES.SDK_NOT_FOUND,
|
|
105
|
+
message: "Replace mode requires a page-provided Foldspace SDK bootstrap.",
|
|
106
|
+
suggestion: "Use --bootstrap when the page does not embed Foldspace.",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return { ok: true, mode };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
mode,
|
|
115
|
+
code: ERROR_CODES.INVALID_REQUEST,
|
|
116
|
+
message: `Unknown attach mode: ${mode}`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function shouldFulfillActionRequest(url, target) {
|
|
121
|
+
return actionRequestMatchesTarget(parseActionRequest(url), target);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function sdkScriptIdentity(url) {
|
|
125
|
+
try {
|
|
126
|
+
const parsed = new URL(url);
|
|
127
|
+
if (
|
|
128
|
+
parsed.protocol !== "https:" ||
|
|
129
|
+
parsed.hostname !== "script.eucerahive.io" ||
|
|
130
|
+
!/^\/web\/sdk\/(?:foldspace|eucera)\.js$/i.test(parsed.pathname)
|
|
131
|
+
) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return `${parsed.origin}${parsed.pathname}`;
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function isSdkScriptRequest(url, allowedSdkUrls = []) {
|
|
141
|
+
const identity = sdkScriptIdentity(url);
|
|
142
|
+
if (!identity) return false;
|
|
143
|
+
const allowedIdentities = allowedSdkUrls
|
|
144
|
+
.map(sdkScriptIdentity)
|
|
145
|
+
.filter(Boolean);
|
|
146
|
+
return (
|
|
147
|
+
allowedIdentities.length === 0 || allowedIdentities.includes(identity)
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function rewriteSdkRequestKey(url, agentKey, allowedSdkUrls = []) {
|
|
152
|
+
if (!isSdkScriptRequest(url, allowedSdkUrls)) return null;
|
|
153
|
+
const rewritten = new URL(url);
|
|
154
|
+
rewritten.searchParams.set("k", agentKey);
|
|
155
|
+
return rewritten.toString();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function guardScriptForHosts(source, hostPatterns) {
|
|
159
|
+
return `(() => {
|
|
160
|
+
const hostname = window.location?.hostname || "";
|
|
161
|
+
const allowed = ${JSON.stringify(hostPatterns)}.some((pattern) =>
|
|
162
|
+
pattern.startsWith("*.")
|
|
163
|
+
? hostname === pattern.slice(2) || hostname.endsWith(pattern.slice(1))
|
|
164
|
+
: hostname === pattern
|
|
165
|
+
);
|
|
166
|
+
if (!allowed) return;
|
|
167
|
+
${source}
|
|
168
|
+
})();`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function buildReplacePrelude({
|
|
172
|
+
agentKey,
|
|
173
|
+
agentApiName,
|
|
174
|
+
mode = "OVERLAY",
|
|
175
|
+
namespace = "foldspace",
|
|
176
|
+
aliases = [],
|
|
177
|
+
}) {
|
|
178
|
+
const source = `(() => {
|
|
179
|
+
if (window.top !== window.self) return;
|
|
180
|
+
const KEY = ${JSON.stringify(agentKey)};
|
|
181
|
+
const TARGET = ${JSON.stringify(agentApiName)};
|
|
182
|
+
const MODE = ${JSON.stringify(mode.toUpperCase())};
|
|
183
|
+
const TARGET_ID = MODE.toLowerCase() + "-" + TARGET;
|
|
184
|
+
const ALIASES = new Set(${JSON.stringify(
|
|
185
|
+
[...new Set(aliases.filter((alias) => alias && alias !== agentApiName))],
|
|
186
|
+
)});
|
|
187
|
+
const NS = ${JSON.stringify(namespace)};
|
|
188
|
+
|
|
189
|
+
const stub = typeof window[NS] === "function"
|
|
190
|
+
? window[NS]
|
|
191
|
+
: function () { (stub.q = stub.q || []).push(arguments); };
|
|
192
|
+
stub.q = stub.q || [];
|
|
193
|
+
try {
|
|
194
|
+
Object.defineProperty(stub, "k", {
|
|
195
|
+
configurable: true,
|
|
196
|
+
enumerable: true,
|
|
197
|
+
get: () => KEY,
|
|
198
|
+
set: () => {},
|
|
199
|
+
});
|
|
200
|
+
} catch {
|
|
201
|
+
stub.k = KEY;
|
|
202
|
+
}
|
|
203
|
+
window.__FOLD_SPACE__ = NS;
|
|
204
|
+
window[NS] = stub;
|
|
205
|
+
|
|
206
|
+
stub("when", "ready", () => {
|
|
207
|
+
const fs = window[NS];
|
|
208
|
+
if (!fs || typeof fs.agent !== "function") return;
|
|
209
|
+
const originalAgent = fs.agent.bind(fs);
|
|
210
|
+
let target = null;
|
|
211
|
+
let rawAddHandlers = null;
|
|
212
|
+
const aliasFacades = new Map();
|
|
213
|
+
|
|
214
|
+
const splitId = (id) => {
|
|
215
|
+
const cut = typeof id === "string" ? id.indexOf("-") : -1;
|
|
216
|
+
return cut > 0
|
|
217
|
+
? { mode: id.slice(0, cut).toUpperCase(), apiName: id.slice(cut + 1) }
|
|
218
|
+
: null;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const removeOtherAgents = () => {
|
|
222
|
+
let ids = [];
|
|
223
|
+
try { ids = typeof fs.agentIds === "function" ? fs.agentIds() : []; }
|
|
224
|
+
catch {}
|
|
225
|
+
for (const id of ids) {
|
|
226
|
+
const parsed = splitId(id);
|
|
227
|
+
if (!parsed || (parsed.apiName === TARGET && parsed.mode === MODE)) continue;
|
|
228
|
+
try { originalAgent(parsed)?.remove?.(); } catch {}
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const ensureTarget = () => {
|
|
233
|
+
removeOtherAgents();
|
|
234
|
+
if (!target) {
|
|
235
|
+
target = originalAgent({
|
|
236
|
+
apiName: TARGET,
|
|
237
|
+
mode: MODE,
|
|
238
|
+
configuration: { enableDebugLogs: true },
|
|
239
|
+
});
|
|
240
|
+
window.__FOLDSPACE_AGENT__ = target;
|
|
241
|
+
}
|
|
242
|
+
if (!target) return null;
|
|
243
|
+
|
|
244
|
+
const localActions = window.__FOLDSPACE_REMOTE_ACTIONS__;
|
|
245
|
+
if (!rawAddHandlers && typeof target.addActionHandlers === "function") {
|
|
246
|
+
rawAddHandlers = target.addActionHandlers.bind(target);
|
|
247
|
+
target.addActionHandlers = (handlers) => {
|
|
248
|
+
const result = rawAddHandlers(handlers);
|
|
249
|
+
const currentLocal = window.__FOLDSPACE_REMOTE_ACTIONS__;
|
|
250
|
+
if (handlers !== currentLocal && currentLocal) {
|
|
251
|
+
setTimeout(() => rawAddHandlers(currentLocal), 0);
|
|
252
|
+
}
|
|
253
|
+
return result;
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
try { if (localActions && rawAddHandlers) rawAddHandlers(localActions); }
|
|
257
|
+
catch {}
|
|
258
|
+
try { target.setTestMode?.(true); } catch {}
|
|
259
|
+
try { target.show?.(); } catch {}
|
|
260
|
+
try { target.open?.(); } catch {}
|
|
261
|
+
// A regular extension can intentionally hide the SDK view while its own
|
|
262
|
+
// dock is closed. Explicit replace mode owns visibility, so override only
|
|
263
|
+
// that external display:none rule for the lifetime of this document.
|
|
264
|
+
if (MODE === "OVERLAY") {
|
|
265
|
+
try {
|
|
266
|
+
const widget = window.document?.querySelector?.("#eucera-agent-view");
|
|
267
|
+
if (
|
|
268
|
+
widget &&
|
|
269
|
+
window.getComputedStyle?.(widget)?.display === "none"
|
|
270
|
+
) {
|
|
271
|
+
widget.style.setProperty("display", "block", "important");
|
|
272
|
+
}
|
|
273
|
+
widget?.setAttribute?.(
|
|
274
|
+
"data-foldspace-dev-agent-id",
|
|
275
|
+
TARGET_ID,
|
|
276
|
+
);
|
|
277
|
+
} catch {}
|
|
278
|
+
}
|
|
279
|
+
return target;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const aliasFacade = (alias, selected) => {
|
|
283
|
+
if (aliasFacades.has(alias)) return aliasFacades.get(alias);
|
|
284
|
+
let facade;
|
|
285
|
+
facade = new Proxy(selected, {
|
|
286
|
+
get(object, property) {
|
|
287
|
+
if (property === "addActionHandlers") {
|
|
288
|
+
return () => facade;
|
|
289
|
+
}
|
|
290
|
+
if (
|
|
291
|
+
property === "hide" ||
|
|
292
|
+
property === "close" ||
|
|
293
|
+
property === "remove"
|
|
294
|
+
) {
|
|
295
|
+
return () => facade;
|
|
296
|
+
}
|
|
297
|
+
const value = Reflect.get(object, property, object);
|
|
298
|
+
return typeof value === "function" ? value.bind(object) : value;
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
aliasFacades.set(alias, facade);
|
|
302
|
+
return facade;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
fs.agent = (settings) => {
|
|
306
|
+
const requested =
|
|
307
|
+
typeof settings === "string" ? settings : settings?.apiName;
|
|
308
|
+
if (!requested) return originalAgent(settings);
|
|
309
|
+
const selected = ensureTarget();
|
|
310
|
+
setTimeout(ensureTarget, 0);
|
|
311
|
+
return selected ? aliasFacade(requested, selected) : selected;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
ensureTarget();
|
|
315
|
+
setTimeout(ensureTarget, 0);
|
|
316
|
+
setInterval(ensureTarget, 1000);
|
|
317
|
+
window.__FOLDSPACE_DEV_REPLACE__ = {
|
|
318
|
+
agentApiName: TARGET,
|
|
319
|
+
mode: MODE,
|
|
320
|
+
aliases: Array.from(ALIASES),
|
|
321
|
+
key: KEY,
|
|
322
|
+
};
|
|
323
|
+
console.log("[foldspace-dev] replacement active", {
|
|
324
|
+
agent: TARGET,
|
|
325
|
+
mode: MODE,
|
|
326
|
+
aliases: Array.from(ALIASES),
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
})();`;
|
|
330
|
+
|
|
331
|
+
return source;
|
|
332
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export function buildBootstrapScript({
|
|
2
|
+
sdkUrl,
|
|
3
|
+
productId,
|
|
4
|
+
agentApiName,
|
|
5
|
+
mode = "OVERLAY",
|
|
6
|
+
overrideKey = null,
|
|
7
|
+
namespace = "foldspace",
|
|
8
|
+
}) {
|
|
9
|
+
for (const [name, value] of Object.entries({
|
|
10
|
+
sdkUrl,
|
|
11
|
+
productId,
|
|
12
|
+
agentApiName,
|
|
13
|
+
})) {
|
|
14
|
+
if (typeof value !== "string" || !value) {
|
|
15
|
+
throw new TypeError(`${name} is required`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return `(() => {
|
|
20
|
+
if (window.top !== window.self) return;
|
|
21
|
+
const SDK_URL = ${JSON.stringify(sdkUrl)};
|
|
22
|
+
const PRODUCT_ID = ${JSON.stringify(productId)};
|
|
23
|
+
const AGENT_API_NAME = ${JSON.stringify(agentApiName)};
|
|
24
|
+
const MODE = ${JSON.stringify(String(mode).toUpperCase())};
|
|
25
|
+
const OVERRIDE_KEY = ${JSON.stringify(overrideKey)};
|
|
26
|
+
const NAMESPACE = ${JSON.stringify(namespace)};
|
|
27
|
+
|
|
28
|
+
const sdkLoaded = () =>
|
|
29
|
+
typeof window[NAMESPACE] === "function" &&
|
|
30
|
+
typeof window[NAMESPACE].agent === "function";
|
|
31
|
+
|
|
32
|
+
const appendSdkScript = () => {
|
|
33
|
+
const key = OVERRIDE_KEY || ("EU-" + PRODUCT_ID + "-1-1");
|
|
34
|
+
window[NAMESPACE] =
|
|
35
|
+
window[NAMESPACE] ||
|
|
36
|
+
function foldspaceQueue() {
|
|
37
|
+
(window[NAMESPACE].q = window[NAMESPACE].q || []).push(arguments);
|
|
38
|
+
};
|
|
39
|
+
window.__FOLD_SPACE__ = NAMESPACE;
|
|
40
|
+
window[NAMESPACE].k = key;
|
|
41
|
+
if (document.querySelector("script[data-foldspace-sdk]")) return;
|
|
42
|
+
if (!document.body) throw new Error("document.body not ready");
|
|
43
|
+
const script = document.createElement("script");
|
|
44
|
+
script.async = true;
|
|
45
|
+
script.src = SDK_URL + "?k=" + encodeURIComponent(key);
|
|
46
|
+
script.setAttribute("data-foldspace-sdk", "1");
|
|
47
|
+
const first = document.getElementsByTagName("script")[0];
|
|
48
|
+
if (first?.parentNode) first.parentNode.insertBefore(script, first);
|
|
49
|
+
else (document.head || document.documentElement).appendChild(script);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const tryAppendSdkScript = () => {
|
|
53
|
+
try { appendSdkScript(); } catch {}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
if (!sdkLoaded()) {
|
|
57
|
+
let attempts = 0;
|
|
58
|
+
const retry = setInterval(() => {
|
|
59
|
+
if (sdkLoaded() || ++attempts > 60) {
|
|
60
|
+
clearInterval(retry);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
tryAppendSdkScript();
|
|
64
|
+
}, 250);
|
|
65
|
+
document.addEventListener("DOMContentLoaded", tryAppendSdkScript);
|
|
66
|
+
tryAppendSdkScript();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let initialized = false;
|
|
70
|
+
const initializeAgent = () => {
|
|
71
|
+
if (initialized) return;
|
|
72
|
+
const sdk = window[NAMESPACE];
|
|
73
|
+
if (typeof sdk?.agent !== "function") return;
|
|
74
|
+
initialized = true;
|
|
75
|
+
const configuration = { enableDebugLogs: true };
|
|
76
|
+
if (MODE === "EMBEDDED") {
|
|
77
|
+
let container = document.getElementById("foldspace-container");
|
|
78
|
+
if (!container) {
|
|
79
|
+
container = document.createElement("div");
|
|
80
|
+
container.id = "foldspace-container";
|
|
81
|
+
document.body.appendChild(container);
|
|
82
|
+
}
|
|
83
|
+
configuration.embeddedConfiguration = { container };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const agent = sdk.agent({
|
|
87
|
+
apiName: AGENT_API_NAME,
|
|
88
|
+
mode: MODE,
|
|
89
|
+
configuration,
|
|
90
|
+
});
|
|
91
|
+
window.__FOLDSPACE_AGENT__ = agent;
|
|
92
|
+
|
|
93
|
+
let attachedActions = null;
|
|
94
|
+
const attachActions = () => {
|
|
95
|
+
const actions = window.__FOLDSPACE_REMOTE_ACTIONS__;
|
|
96
|
+
if (!actions || actions === attachedActions) return;
|
|
97
|
+
agent.addActionHandlers(actions);
|
|
98
|
+
attachedActions = actions;
|
|
99
|
+
delete window.__FOLDSPACE_REMOTE_ACTIONS__;
|
|
100
|
+
console.log(
|
|
101
|
+
"[foldspace-dev] actions attached:",
|
|
102
|
+
Object.keys(actions).length,
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
agent.on?.("*", (event) => {
|
|
106
|
+
if (event?.eventName === "agent.ready") attachActions();
|
|
107
|
+
});
|
|
108
|
+
attachActions();
|
|
109
|
+
setTimeout(attachActions, 0);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
try { window[NAMESPACE]("when", "ready", initializeAgent); } catch {}
|
|
113
|
+
try { if (sdkLoaded()) initializeAgent(); } catch {}
|
|
114
|
+
console.log("[foldspace-dev] bootstrap queued", {
|
|
115
|
+
agent: AGENT_API_NAME,
|
|
116
|
+
product: PRODUCT_ID,
|
|
117
|
+
mode: MODE,
|
|
118
|
+
});
|
|
119
|
+
})();`;
|
|
120
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export function createCdpRequestManager({ timeoutMs = 10_000 } = {}) {
|
|
2
|
+
let nextId = 1;
|
|
3
|
+
const pending = new Map();
|
|
4
|
+
|
|
5
|
+
function call(socket, method, params = {}, sessionId) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
if (!socket || socket.readyState !== 1) {
|
|
8
|
+
reject(new Error(`CDP socket is not open for ${method}`));
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const id = nextId++;
|
|
13
|
+
const timer = setTimeout(() => {
|
|
14
|
+
pending.delete(id);
|
|
15
|
+
reject(new Error(`CDP ${method} timed out after ${timeoutMs}ms`));
|
|
16
|
+
}, timeoutMs);
|
|
17
|
+
pending.set(id, { method, resolve, reject, timer });
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
socket.send(JSON.stringify({ id, method, params, sessionId }));
|
|
21
|
+
} catch (error) {
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
pending.delete(id);
|
|
24
|
+
reject(error);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function handleMessage(message) {
|
|
30
|
+
const request = pending.get(message?.id);
|
|
31
|
+
if (!request) return false;
|
|
32
|
+
|
|
33
|
+
clearTimeout(request.timer);
|
|
34
|
+
pending.delete(message.id);
|
|
35
|
+
if (message.error) {
|
|
36
|
+
const error = new Error(
|
|
37
|
+
`CDP ${request.method} failed: ${message.error.message || "Unknown error"}`,
|
|
38
|
+
);
|
|
39
|
+
error.code = message.error.code;
|
|
40
|
+
request.reject(error);
|
|
41
|
+
} else {
|
|
42
|
+
request.resolve(message.result);
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function failAll(error = new Error("CDP socket closed")) {
|
|
48
|
+
for (const request of pending.values()) {
|
|
49
|
+
clearTimeout(request.timer);
|
|
50
|
+
request.reject(error);
|
|
51
|
+
}
|
|
52
|
+
pending.clear();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
call,
|
|
57
|
+
handleMessage,
|
|
58
|
+
failAll,
|
|
59
|
+
pendingCount: () => pending.size,
|
|
60
|
+
};
|
|
61
|
+
}
|
package/src/cli-help.mjs
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const GROUPS = [
|
|
2
|
+
["start", "Start an actions project"],
|
|
3
|
+
["develop", "Build the portable artifact"],
|
|
4
|
+
["verify", "Verify against a live application"],
|
|
5
|
+
["publish", "Publish explicitly"],
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
function rows(entries) {
|
|
9
|
+
const width = Math.max(...entries.map(([label]) => label.length), 0);
|
|
10
|
+
return entries
|
|
11
|
+
.map(([label, description]) => ` ${label.padEnd(width)} ${description}`)
|
|
12
|
+
.join("\n");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function renderGeneralHelp(registry) {
|
|
16
|
+
const sections = [
|
|
17
|
+
"usage: foldspace [--help | --version] <command> [<args>]",
|
|
18
|
+
"",
|
|
19
|
+
"Build and verify a portable Foldspace action artifact.",
|
|
20
|
+
"",
|
|
21
|
+
"Typical local workflow:",
|
|
22
|
+
" foldspace init <directory>",
|
|
23
|
+
" foldspace build",
|
|
24
|
+
" foldspace inject",
|
|
25
|
+
" foldspace attach",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
for (const [group, title] of GROUPS) {
|
|
29
|
+
const commands = registry.commands
|
|
30
|
+
.filter((command) => command.group === group)
|
|
31
|
+
.map((command) => [command.name, command.summary]);
|
|
32
|
+
if (!commands.length) continue;
|
|
33
|
+
sections.push("", `${title}:`, rows(commands));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
sections.push(
|
|
37
|
+
"",
|
|
38
|
+
"Choose an attach mode:",
|
|
39
|
+
" swap Default. The page already has the configured product and agent.",
|
|
40
|
+
" bootstrap Use --bootstrap only when the page has no Foldspace SDK.",
|
|
41
|
+
" replace Use --replace for a different page product or agent.",
|
|
42
|
+
"",
|
|
43
|
+
"Discovery:",
|
|
44
|
+
" foldspace help <command> Show command-specific guidance",
|
|
45
|
+
" foldspace <command> --help Same command-specific guidance",
|
|
46
|
+
" foldspace help --json Print the machine-readable CLI contract",
|
|
47
|
+
" foldspace help <command> --json Print one command contract",
|
|
48
|
+
" foldspace --version Show package and protocol versions",
|
|
49
|
+
"",
|
|
50
|
+
"attach loads local actions and observes the normal agent experience.",
|
|
51
|
+
"deploy is a separate remote publication step.",
|
|
52
|
+
);
|
|
53
|
+
return sections.join("\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function renderCommandHelp(registry, name) {
|
|
57
|
+
const command = registry.commands.find((entry) => entry.name === name);
|
|
58
|
+
if (!command) return null;
|
|
59
|
+
const sections = [
|
|
60
|
+
`usage: ${command.usage}`,
|
|
61
|
+
"",
|
|
62
|
+
command.summary,
|
|
63
|
+
"",
|
|
64
|
+
`Risk: ${command.risk} — ${
|
|
65
|
+
registry.risks.find(({ id }) => id === command.risk)?.description || ""
|
|
66
|
+
}`,
|
|
67
|
+
`Environment: ${command.environment} — ${
|
|
68
|
+
registry.environments.find(({ id }) => id === command.environment)
|
|
69
|
+
?.description || ""
|
|
70
|
+
}`,
|
|
71
|
+
];
|
|
72
|
+
if (command.options.length) {
|
|
73
|
+
const optionRows = command.options.map((option) => [
|
|
74
|
+
`${option.name}${option.value ? ` <${option.value}>` : ""}`,
|
|
75
|
+
`${option.description}${
|
|
76
|
+
option.default ? ` (default: ${option.default})` : ""
|
|
77
|
+
}`,
|
|
78
|
+
]);
|
|
79
|
+
sections.push("", "Options:", rows(optionRows));
|
|
80
|
+
}
|
|
81
|
+
if (command.modes?.length) {
|
|
82
|
+
sections.push(
|
|
83
|
+
"",
|
|
84
|
+
"Modes:",
|
|
85
|
+
rows(
|
|
86
|
+
command.modes.map((mode) => [
|
|
87
|
+
mode.flag || mode.name,
|
|
88
|
+
mode.description,
|
|
89
|
+
]),
|
|
90
|
+
),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (command.environmentVariables.length) {
|
|
94
|
+
sections.push(
|
|
95
|
+
"",
|
|
96
|
+
"Environment variables:",
|
|
97
|
+
...command.environmentVariables.map((name) => ` - ${name}`),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (command.prerequisites.length) {
|
|
101
|
+
sections.push(
|
|
102
|
+
"",
|
|
103
|
+
"Prerequisites:",
|
|
104
|
+
...command.prerequisites.map((item) => ` - ${item}`),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
if (command.effects.length) {
|
|
108
|
+
sections.push(
|
|
109
|
+
"",
|
|
110
|
+
"Effects:",
|
|
111
|
+
...command.effects.map((item) => ` - ${item}`),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (command.next.length) {
|
|
115
|
+
sections.push(
|
|
116
|
+
"",
|
|
117
|
+
"Next:",
|
|
118
|
+
...command.next.map((item) => ` - ${item}`),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return sections.join("\n");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function helpDocument(registry, topic) {
|
|
125
|
+
if (!topic) return registry;
|
|
126
|
+
const command = registry.commands.find((entry) => entry.name === topic);
|
|
127
|
+
if (!command) return null;
|
|
128
|
+
return {
|
|
129
|
+
kind: "foldspace.cli.command",
|
|
130
|
+
schemaVersion: registry.schemaVersion,
|
|
131
|
+
package: registry.package,
|
|
132
|
+
protocolVersion: registry.protocolVersion,
|
|
133
|
+
command,
|
|
134
|
+
risk: registry.risks.find(({ id }) => id === command.risk),
|
|
135
|
+
environment: registry.environments.find(
|
|
136
|
+
({ id }) => id === command.environment,
|
|
137
|
+
),
|
|
138
|
+
capabilities: registry.capabilities.filter((capability) =>
|
|
139
|
+
command.capabilities.includes(capability.id),
|
|
140
|
+
),
|
|
141
|
+
diagnostics: command.name === "attach" ? registry.diagnostics : [],
|
|
142
|
+
};
|
|
143
|
+
}
|