@cryptolibertus/pi-peer 0.3.2
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/LICENSE +21 -0
- package/README.md +70 -0
- package/extensions/pi-peer/index.ts +753 -0
- package/package.json +58 -0
- package/src/peers/command.mjs +289 -0
- package/src/peers/comms.mjs +676 -0
- package/src/peers/config.mjs +356 -0
- package/src/peers/extension-lifecycle.mjs +21 -0
- package/src/peers/goal-board.mjs +528 -0
- package/src/peers/guidance.mjs +45 -0
- package/src/peers/inbound-bridge.mjs +240 -0
- package/src/peers/local-transport.mjs +814 -0
- package/src/peers/message-store.mjs +114 -0
- package/src/peers/protocol.mjs +256 -0
- package/src/peers/role-collaboration-demo.mjs +71 -0
- package/src/peers/runtime.mjs +200 -0
- package/src/peers/status.mjs +158 -0
- package/src/peers/tool-results.mjs +154 -0
- package/src/utils.mjs +83 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { redactPeerAuditValue } from "./protocol.mjs";
|
|
2
|
+
|
|
3
|
+
export async function collectPeerRuntimeStatus(runtime, options = {}) {
|
|
4
|
+
const enabled = runtime?.enabled === true;
|
|
5
|
+
const peers = options.peers || (enabled && runtime?.comms?.listPeers ? await runtime.comms.listPeers() : []);
|
|
6
|
+
const messages = options.messages || (runtime?.comms?.listMessages ? await runtime.comms.listMessages() : []);
|
|
7
|
+
return derivePeerRuntimeStatus(runtime, { ...options, peers, messages });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function derivePeerRuntimeStatus(runtime = {}, options = {}) {
|
|
11
|
+
const peers = Array.isArray(options.peers) ? options.peers : [];
|
|
12
|
+
const messages = Array.isArray(options.messages) ? options.messages : [];
|
|
13
|
+
const endpoint = runtime.localEndpoint || null;
|
|
14
|
+
const enabled = runtime.enabled === true;
|
|
15
|
+
const activePeers = peers.filter((peer) => peer.status === "active");
|
|
16
|
+
const discoveredPeers = peers.filter((peer) => peer.discoveredAt || peer.socketPath || peer.pipeName);
|
|
17
|
+
const pendingMessages = messages.filter((message) => ["queued", "running"].includes(message.status));
|
|
18
|
+
const activeTasks = pendingMessages.map(activeTaskSummary);
|
|
19
|
+
const disconnectedTasks = messages.filter((message) => message.status === "disconnected").map(activeTaskSummary);
|
|
20
|
+
const warnings = [...(runtime.config?.warnings || runtime.summary?.warnings || [])];
|
|
21
|
+
const fanoutSuggestion = deriveFanoutSuggestion(peers, pendingMessages);
|
|
22
|
+
if (fanoutSuggestion.warning) warnings.push(fanoutSuggestion.warning);
|
|
23
|
+
const localProfile = runtime.summary?.localPeerProfile || runtime.config?.localPeerProfile || endpoint || {};
|
|
24
|
+
const localCapabilities = endpoint?.capabilities || runtime.config?.manifest?.capabilities || runtime.summary?.manifest?.capabilities || {};
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
enabled,
|
|
28
|
+
source: runtime.source || runtime.summary?.source || "none",
|
|
29
|
+
localPeerId: runtime.localPeerId || runtime.summary?.localPeerId || "unknown",
|
|
30
|
+
localPeerIdSource: runtime.summary?.localPeerIdSource || runtime.config?.localPeerIdSource,
|
|
31
|
+
protocolVersion: endpoint?.protocolVersion || runtime.summary?.protocolVersion || runtime.config?.manifest?.protocolVersion,
|
|
32
|
+
localTrust: endpoint?.trust || runtime.config?.manifest?.trust || runtime.summary?.manifest?.trust,
|
|
33
|
+
localCapabilities,
|
|
34
|
+
localRole: safeStatusText(localProfile.role || endpoint?.role),
|
|
35
|
+
localPersona: safeStatusText(localProfile.persona || endpoint?.persona),
|
|
36
|
+
endpointStatus: enabled ? (endpoint ? "listening" : "not listening") : "disabled",
|
|
37
|
+
authStatus: enabled ? (endpoint?.authRequired ? "required" : endpoint ? "open" : "not advertised") : "disabled",
|
|
38
|
+
configuredPeers: Number(runtime.summary?.peerCount || 0),
|
|
39
|
+
peers,
|
|
40
|
+
peerCount: peers.length,
|
|
41
|
+
discoveredCount: discoveredPeers.length,
|
|
42
|
+
activeCount: activePeers.length,
|
|
43
|
+
pendingCount: pendingMessages.length,
|
|
44
|
+
activeTasks,
|
|
45
|
+
disconnectedTasks,
|
|
46
|
+
fanoutSuggestion,
|
|
47
|
+
warnings,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function formatPeerStatusLines(status = {}) {
|
|
52
|
+
const enabledText = status.enabled ? "enabled" : "disabled";
|
|
53
|
+
const color = status.enabled ? "success" : "muted";
|
|
54
|
+
const profileText = [status.localRole ? `role ${status.localRole}` : "", status.localPersona ? `persona ${status.localPersona}` : ""].filter(Boolean).join(" · ");
|
|
55
|
+
const protocolText = status.protocolVersion ? ` · protocol v${status.protocolVersion}` : "";
|
|
56
|
+
const capsText = capabilitySummary(status.localCapabilities);
|
|
57
|
+
const lines = [
|
|
58
|
+
line("state", color, `🔗 peers ${enabledText} · id ${status.localPeerId || "unknown"}${profileText ? ` · ${profileText}` : ""}${protocolText} · source ${status.source || "none"}`),
|
|
59
|
+
line("endpoint", status.endpointStatus === "listening" ? "success" : status.enabled ? "warning" : "muted", `endpoint ${status.endpointStatus || "unknown"} · auth ${status.authStatus || "unknown"}`),
|
|
60
|
+
line("peers", status.activeCount > 0 ? "accent" : "muted", `peers discovered ${status.discoveredCount || 0} · active ${status.activeCount || 0} · configured ${status.configuredPeers || 0}${capsText ? ` · caps ${capsText}` : ""}`),
|
|
61
|
+
line("messages", status.pendingCount > 0 ? "accent" : "muted", `messages pending ${status.pendingCount || 0}`),
|
|
62
|
+
];
|
|
63
|
+
for (const task of (status.activeTasks || []).slice(0, 2)) lines.push(line("task", "accent", formatActiveTaskLine(task)));
|
|
64
|
+
const extraTasks = (status.activeTasks || []).length - 2;
|
|
65
|
+
if (extraTasks > 0) lines.push(line("task", "accent", `tasks +${extraTasks} more active`));
|
|
66
|
+
for (const warning of (status.warnings || []).slice(0, 3)) lines.push(line("warning", "warning", `warning ${warning}`));
|
|
67
|
+
return lines;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function formatPeerStatusText(status = {}) {
|
|
71
|
+
return formatPeerStatusLines(status).map((item) => item.text).join("\n");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function shouldShowPeerWidget(status = {}) {
|
|
75
|
+
return status.enabled === true || (status.warnings || []).length > 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function activeTaskSummary(message = {}) {
|
|
79
|
+
const body = message.request?.body || {};
|
|
80
|
+
const metadata = body.metadata && typeof body.metadata === "object" ? body.metadata : {};
|
|
81
|
+
return {
|
|
82
|
+
messageId: message.messageId,
|
|
83
|
+
conversationId: message.conversationId,
|
|
84
|
+
peerId: message.peerId,
|
|
85
|
+
status: message.status,
|
|
86
|
+
intent: body.intent || "ask",
|
|
87
|
+
claimedPaths: Array.isArray(metadata.claimedPaths) ? metadata.claimedPaths.filter((item) => typeof item === "string") : [],
|
|
88
|
+
goalId: typeof metadata.goalId === "string" ? metadata.goalId : undefined,
|
|
89
|
+
goalClaimId: typeof metadata.goalClaimId === "string" ? metadata.goalClaimId : undefined,
|
|
90
|
+
lastEvent: message.lastEvent,
|
|
91
|
+
lastHeartbeatAt: message.lastHeartbeatAt,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function formatActiveTaskLine(task = {}) {
|
|
96
|
+
const id = task.messageId || "unknown-message";
|
|
97
|
+
const peer = task.peerId || "unknown-peer";
|
|
98
|
+
const intent = task.intent || "ask";
|
|
99
|
+
const claimed = task.claimedPaths?.length ? ` · claims ${task.claimedPaths.join(", ")}` : "";
|
|
100
|
+
const goal = task.goalId ? ` · goal ${task.goalId}${task.goalClaimId ? ` claim ${task.goalClaimId}` : ""}` : "";
|
|
101
|
+
const last = task.lastEvent?.summary ? ` · last ${safeStatusText(task.lastEvent.summary) || "event"}` : "";
|
|
102
|
+
return `task ${id} → ${peer} ${intent} · ${task.status || "unknown"}${claimed}${goal}${last}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function derivePeerDoctorReport(status = {}) {
|
|
106
|
+
const checks = [];
|
|
107
|
+
checks.push({ name: "enabled", ok: status.enabled === true, detail: status.enabled ? `source ${status.source || "unknown"}` : "peer messaging disabled" });
|
|
108
|
+
checks.push({ name: "local identity", ok: Boolean(status.localPeerId && status.localPeerId !== "unknown"), detail: status.localPeerId || "missing" });
|
|
109
|
+
checks.push({ name: "protocol", ok: !status.protocolVersion || status.protocolVersion === 1, detail: status.protocolVersion ? `v${status.protocolVersion}` : "not advertised" });
|
|
110
|
+
checks.push({ name: "endpoint", ok: !status.enabled || status.endpointStatus === "listening", detail: status.endpointStatus || "unknown" });
|
|
111
|
+
checks.push({ name: "peers", ok: (status.peerCount || 0) > 0, detail: `${status.peerCount || 0} available (${status.activeCount || 0} active)` });
|
|
112
|
+
for (const peer of status.peers || []) {
|
|
113
|
+
checks.push({ name: `peer ${peer.peerId}`, ok: peer.compatible !== false && peer.status !== "unsupported", detail: `${peer.transport || "coms"} ${peer.trust || "read-only"} ${peer.status || "configured"}${peer.protocolVersion ? ` protocol v${peer.protocolVersion}` : ""}` });
|
|
114
|
+
}
|
|
115
|
+
const disconnected = status.disconnectedTasks || (status.activeTasks || []).filter((task) => task.status === "disconnected");
|
|
116
|
+
if (disconnected.length) checks.push({ name: "resume", ok: false, detail: `${disconnected.length} disconnected task(s); use /peer reconnect then /peer resume <message-id>` });
|
|
117
|
+
for (const warning of status.warnings || []) checks.push({ name: "warning", ok: false, detail: warning });
|
|
118
|
+
return { ok: checks.every((check) => check.ok), checks, status };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function formatPeerDoctorText(report = {}) {
|
|
122
|
+
const lines = [`Peer doctor: ${report.ok ? "ok" : "attention needed"}`];
|
|
123
|
+
for (const check of report.checks || []) lines.push(`${check.ok ? "✓" : "!"} ${check.name}: ${check.detail || (check.ok ? "ok" : "check")}`);
|
|
124
|
+
lines.push("Next: /peer setup creates config, /peer reconnect refreshes discovery, /peer list shows descriptors, /peer resume continues disconnected messages.");
|
|
125
|
+
return lines.join("\n");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function capabilitySummary(capabilities = {}) {
|
|
129
|
+
if (!capabilities || typeof capabilities !== "object") return "";
|
|
130
|
+
if (Array.isArray(capabilities.intents) && capabilities.intents.length) return `intents:${capabilities.intents.join(",")}`;
|
|
131
|
+
return Object.keys(capabilities).slice(0, 4).join(",");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function deriveFanoutSuggestion(peers = [], pendingMessages = []) {
|
|
135
|
+
const availablePeers = peers
|
|
136
|
+
.filter((peer) => !peer.current && !peer.self && peer.trust !== "disabled")
|
|
137
|
+
.map((peer) => peer.peerId)
|
|
138
|
+
.filter(Boolean);
|
|
139
|
+
const activePeerTasks = pendingMessages.filter((message) => ["queued", "running"].includes(message.status));
|
|
140
|
+
const recommended = availablePeers.length > 0 && activePeerTasks.length === 0;
|
|
141
|
+
return {
|
|
142
|
+
recommended,
|
|
143
|
+
availablePeers,
|
|
144
|
+
activePeerTaskCount: activePeerTasks.length,
|
|
145
|
+
warning: recommended ? `fan-out available for multi-lane work: ${availablePeers.slice(0, 4).join(", ")} — use /peer goal fanout or peer_send` : undefined,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function line(kind, color, text) {
|
|
150
|
+
return { kind, color, text };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function safeStatusText(value) {
|
|
154
|
+
if (typeof value !== "string" || !value.trim()) return undefined;
|
|
155
|
+
const redacted = redactPeerAuditValue(value);
|
|
156
|
+
if (typeof redacted !== "string" || !redacted.trim()) return undefined;
|
|
157
|
+
return redacted.trim().replace(/\s+/g, " ").slice(0, 120);
|
|
158
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
export function peerListToolResult(runtime, peers) {
|
|
2
|
+
const enabled = runtime.enabled === true;
|
|
3
|
+
return {
|
|
4
|
+
content: [{ type: "text", text: formatPeerList(enabled, peers, runtime.config?.warnings || []) }],
|
|
5
|
+
details: {
|
|
6
|
+
ok: enabled,
|
|
7
|
+
kind: "peer_list",
|
|
8
|
+
enabled,
|
|
9
|
+
source: runtime.source,
|
|
10
|
+
count: peers.length,
|
|
11
|
+
peers,
|
|
12
|
+
warnings: runtime.config?.warnings || [],
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function peerSendQueuedToolResult(handle) {
|
|
18
|
+
return {
|
|
19
|
+
content: [{ type: "text", text: `Peer message queued: ${handle.messageId} in ${handle.conversationId}` }],
|
|
20
|
+
details: {
|
|
21
|
+
ok: true,
|
|
22
|
+
kind: "peer_send",
|
|
23
|
+
mode: "queued",
|
|
24
|
+
status: handle.status,
|
|
25
|
+
messageId: handle.messageId,
|
|
26
|
+
conversationId: handle.conversationId,
|
|
27
|
+
peerId: handle.peerId,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function peerSendResponseToolResult(handle, response) {
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: "text", text: responseText(response) }],
|
|
35
|
+
details: {
|
|
36
|
+
ok: response.status !== "ERROR" && response.status !== "CANCELLED",
|
|
37
|
+
kind: "peer_send",
|
|
38
|
+
mode: "awaited",
|
|
39
|
+
status: response.status,
|
|
40
|
+
messageId: handle.messageId,
|
|
41
|
+
conversationId: handle.conversationId,
|
|
42
|
+
peerId: handle.peerId,
|
|
43
|
+
response,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function peerSendTimeoutToolResult(handle, error, message) {
|
|
49
|
+
const details = error?.details || {};
|
|
50
|
+
const status = details.status || message?.status || handle.status;
|
|
51
|
+
return {
|
|
52
|
+
content: [{
|
|
53
|
+
type: "text",
|
|
54
|
+
text: `Timed out waiting for peer '${handle.peerId}' message ${handle.messageId} (${status}). The peer task may still be running. Use peer_await with messageId ${handle.messageId} or peer_get ${handle.messageId} to inspect it.`,
|
|
55
|
+
}],
|
|
56
|
+
details: {
|
|
57
|
+
ok: false,
|
|
58
|
+
kind: "peer_send",
|
|
59
|
+
mode: "await_timeout",
|
|
60
|
+
timedOut: true,
|
|
61
|
+
taskStillRunning: details.taskStillRunning ?? ["queued", "running"].includes(status),
|
|
62
|
+
status,
|
|
63
|
+
messageId: handle.messageId,
|
|
64
|
+
conversationId: handle.conversationId,
|
|
65
|
+
peerId: handle.peerId,
|
|
66
|
+
error: { message: error?.message || String(error), code: error?.code || "PI_PEER_AWAIT_TIMEOUT" },
|
|
67
|
+
message,
|
|
68
|
+
suggestedNextActions: [
|
|
69
|
+
`peer_await({ messageId: "${handle.messageId}" })`,
|
|
70
|
+
`peer_get({ id: "${handle.messageId}" })`,
|
|
71
|
+
"peer_get({ id: \"tasks\" })",
|
|
72
|
+
"peer_get({ id: \"goals\" })",
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function peerGetToolResult(id, type, value) {
|
|
79
|
+
const found = value !== undefined;
|
|
80
|
+
return {
|
|
81
|
+
content: [{ type: "text", text: found ? JSON.stringify(value, null, 2) : `No peer state found for ${id}` }],
|
|
82
|
+
details: {
|
|
83
|
+
ok: found,
|
|
84
|
+
kind: "peer_get",
|
|
85
|
+
id,
|
|
86
|
+
type,
|
|
87
|
+
found,
|
|
88
|
+
value,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function peerAwaitToolResult(results) {
|
|
94
|
+
const ok = results.every((item) => item.response && item.response.status !== "ERROR" && item.response.status !== "CANCELLED");
|
|
95
|
+
return {
|
|
96
|
+
content: [{ type: "text", text: results.map(formatAwaitLine).join("\n") }],
|
|
97
|
+
details: {
|
|
98
|
+
ok,
|
|
99
|
+
kind: "peer_await",
|
|
100
|
+
count: results.length,
|
|
101
|
+
responses: results,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function formatPeerList(enabled, peers, warnings = []) {
|
|
107
|
+
if (!enabled) return "Pi-to-Pi peer messaging is disabled. Set experimental.peerMessaging: true in .pi/settings.json or enabled: true in .pi/peers.json.";
|
|
108
|
+
if (!peers.length) return "Pi-to-Pi peer messaging is enabled, but no peers are configured. Next: start another Pi session with PI_PEER_ID=<peer-id> pi, or edit .pi/peers.json to add a peer, then run /peer list again.";
|
|
109
|
+
const lines = peers.map(formatPeerListLine);
|
|
110
|
+
lines.push("Next: use /peer send <peer> <prompt> or peer_send with one of the peer ids above.");
|
|
111
|
+
if (warnings.length) lines.push(`warnings: ${warnings.join("; ")}`);
|
|
112
|
+
return lines.join("\n");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function formatPeerListLine(peer) {
|
|
116
|
+
const parts = [peer.peerId];
|
|
117
|
+
if (peer.current || peer.self) parts.push("current/self");
|
|
118
|
+
if (peer.role) parts.push(`role:${peer.role}`);
|
|
119
|
+
parts.push(peer.transport, peer.trust, peer.status);
|
|
120
|
+
if (peer.protocolVersion) parts.push(`protocol:v${peer.protocolVersion}`);
|
|
121
|
+
const caps = capabilitySummary(peer.capabilities);
|
|
122
|
+
if (caps) parts.push(`caps:${caps}`);
|
|
123
|
+
const writeAccess = peer.identity?.writeAccess ?? peer.writeAccess;
|
|
124
|
+
if (typeof writeAccess === "boolean") parts.push(`write:${writeAccess ? "yes" : "no"}`);
|
|
125
|
+
if (peer.unsupportedReason) parts.push(peer.unsupportedReason);
|
|
126
|
+
return parts.join(" · ");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function capabilitySummary(capabilities = {}) {
|
|
130
|
+
if (!capabilities || typeof capabilities !== "object") return "";
|
|
131
|
+
if (Array.isArray(capabilities.intents) && capabilities.intents.length) return `intents=${capabilities.intents.join(",")}`;
|
|
132
|
+
return Object.keys(capabilities).slice(0, 3).join(",");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function formatAwaitLine(item) {
|
|
136
|
+
if (item.error) {
|
|
137
|
+
if (item.error.code === "PI_PEER_AWAIT_TIMEOUT") return formatAwaitTimeoutLine(item);
|
|
138
|
+
return `${item.messageId}: ERROR ${item.error.message}`;
|
|
139
|
+
}
|
|
140
|
+
return `${item.messageId}: ${responseText(item.response)}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function formatAwaitTimeoutLine(item) {
|
|
144
|
+
const message = item.message || {};
|
|
145
|
+
const status = message.status || "unknown";
|
|
146
|
+
const peer = message.peerId ? ` for ${message.peerId}` : "";
|
|
147
|
+
const lastEvent = message.lastEvent?.summary ? ` · last: ${message.lastEvent.summary}` : "";
|
|
148
|
+
const conversation = message.conversationId ? ` · conversation ${message.conversationId}` : "";
|
|
149
|
+
return `${item.messageId}: TIMEOUT${peer} (${status})${conversation}${lastEvent}. Still inspectable with peer_await ${item.messageId}, peer_get ${item.messageId}, or peer_get tasks.`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function responseText(response) {
|
|
153
|
+
return response?.finalAssistantMessage || response?.summary || response?.status || "No peer response body returned";
|
|
154
|
+
}
|
package/src/utils.mjs
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export function splitCommandLine(input) {
|
|
2
|
+
const out = [];
|
|
3
|
+
let current = "";
|
|
4
|
+
let quote = null;
|
|
5
|
+
let escaped = false;
|
|
6
|
+
let tokenStarted = false;
|
|
7
|
+
|
|
8
|
+
for (const ch of String(input || "")) {
|
|
9
|
+
if (escaped) {
|
|
10
|
+
current += ch;
|
|
11
|
+
escaped = false;
|
|
12
|
+
tokenStarted = true;
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (ch === "\\") {
|
|
16
|
+
escaped = true;
|
|
17
|
+
tokenStarted = true;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (quote) {
|
|
21
|
+
if (ch === quote) quote = null;
|
|
22
|
+
else {
|
|
23
|
+
current += ch;
|
|
24
|
+
tokenStarted = true;
|
|
25
|
+
}
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (ch === "'" || ch === '"') {
|
|
29
|
+
quote = ch;
|
|
30
|
+
tokenStarted = true;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (/\s/.test(ch)) {
|
|
34
|
+
if (tokenStarted) {
|
|
35
|
+
out.push(current);
|
|
36
|
+
current = "";
|
|
37
|
+
tokenStarted = false;
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
current += ch;
|
|
42
|
+
tokenStarted = true;
|
|
43
|
+
}
|
|
44
|
+
if (escaped) current += "\\";
|
|
45
|
+
if (tokenStarted) out.push(current);
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function parseFlags(args) {
|
|
50
|
+
const flags = {};
|
|
51
|
+
const positionals = [];
|
|
52
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
53
|
+
const arg = args[i];
|
|
54
|
+
if (!arg.startsWith("--")) {
|
|
55
|
+
positionals.push(arg);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const [rawKey, rawValue] = arg.slice(2).split(/=(.*)/s, 2);
|
|
59
|
+
const key = rawKey.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
60
|
+
if (rawValue !== undefined) {
|
|
61
|
+
flags[key] = rawValue;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const next = args[i + 1];
|
|
65
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
66
|
+
flags[key] = next;
|
|
67
|
+
i += 1;
|
|
68
|
+
} else {
|
|
69
|
+
flags[key] = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { flags, positionals };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function flagEnabled(value) {
|
|
76
|
+
if (value === true) return true;
|
|
77
|
+
if (typeof value === "number") return Number.isFinite(value) && value !== 0;
|
|
78
|
+
if (typeof value === "string") {
|
|
79
|
+
const normalized = value.trim().toLowerCase();
|
|
80
|
+
return ["true", "1", "yes", "y", "on"].includes(normalized);
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|