@piagent/platform 1.4.0 → 1.5.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/CHANGELOG.md +151 -0
- package/README.md +12 -2
- package/README.vi.md +47 -0
- package/docs/command-reference-vietnamese.md +2 -2
- package/docs/distribution-standard.md +7 -7
- package/docs/en/architecture.md +13 -0
- package/docs/languages.json +20 -4
- package/docs/mcp-and-tools.md +112 -1
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/publishing-for-teams.md +42 -0
- package/docs/release-install-policy.md +21 -8
- package/docs/runtime-harness-standard.md +1 -1
- package/docs/security-threat-model.md +1 -1
- package/docs/task-implementation-contract.md +11 -4
- package/docs/team-onboarding.md +2 -2
- package/docs/usage-observability.md +70 -0
- package/docs/vi/architecture.md +13 -0
- package/docs-site/architecture.html +5 -1
- package/docs-site/benchmark.html +5 -1
- package/docs-site/commands.html +5 -1
- package/docs-site/content/en/index.html +49 -0
- package/docs-site/content/en/runtime.html +13 -0
- package/docs-site/content/index.html +49 -0
- package/docs-site/content/runtime.html +28 -0
- package/docs-site/context-engine.html +5 -1
- package/docs-site/en/architecture.html +5 -1
- package/docs-site/en/benchmark.html +5 -1
- package/docs-site/en/commands.html +5 -1
- package/docs-site/en/context-engine.html +5 -1
- package/docs-site/en/index.html +54 -1
- package/docs-site/en/mcp-approval.html +5 -1
- package/docs-site/en/mcp-auth.html +5 -1
- package/docs-site/en/mcp-servers.html +5 -1
- package/docs-site/en/mcp.html +5 -1
- package/docs-site/en/permissions.html +5 -1
- package/docs-site/en/profiles.html +5 -1
- package/docs-site/en/quickstart.html +5 -1
- package/docs-site/en/runtime.html +18 -1
- package/docs-site/en/scope.html +5 -1
- package/docs-site/en/security.html +5 -1
- package/docs-site/en/team.html +5 -1
- package/docs-site/en/whats-new.html +5 -1
- package/docs-site/en/workflows.html +5 -1
- package/docs-site/index.html +54 -1
- package/docs-site/mcp-approval.html +5 -1
- package/docs-site/mcp-auth.html +5 -1
- package/docs-site/mcp-servers.html +5 -1
- package/docs-site/mcp.html +5 -1
- package/docs-site/permissions.html +5 -1
- package/docs-site/profiles.html +5 -1
- package/docs-site/quickstart.html +5 -1
- package/docs-site/runtime.html +33 -1
- package/docs-site/scope.html +5 -1
- package/docs-site/security.html +5 -1
- package/docs-site/team.html +5 -1
- package/docs-site/whats-new.html +5 -1
- package/docs-site/workflows.html +5 -1
- package/evals/fixtures/piagent-webui/document-workspace-v1.invalid.json +17 -0
- package/evals/fixtures/piagent-webui/document-workspace-v1.valid.json +42 -0
- package/evals/fixtures/piagent-webui/session-command-v1.valid.json +2 -1
- package/package.json +14 -2
- package/packages/piagent-core/README.md +3 -2
- package/packages/piagent-core/capabilities/capability-verification-cache.js +17 -7
- package/packages/piagent-core/capabilities/runtime-integrity.js +51 -4
- package/packages/piagent-core/extensions/document-intake.ts +6 -3
- package/packages/piagent-core/extensions/piagent-guard.ts +20 -237
- package/packages/piagent-core/extensions/runtime-evidence.js +12 -1
- package/packages/piagent-core/extensions/shell-reach.ts +250 -0
- package/packages/piagent-core/mcp/mcp-command-actions.js +1 -1
- package/packages/piagent-core/mcp/mcp-server-catalog.js +135 -5
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/runtime/hooks/tool-call-hook.ts +9 -2
- package/packages/piagent-core/runtime/input/attachment-document-command.ts +39 -0
- package/packages/piagent-core/runtime/input/attachment-reservation.ts +36 -0
- package/packages/{piagent-webui/extension → piagent-core/runtime/input}/attachment-store.ts +221 -44
- package/packages/piagent-core/runtime/inspection/document-workspace-projection.ts +276 -0
- package/packages/piagent-core/runtime/inspection/project-runtime-inspection.ts +20 -5
- package/packages/piagent-core/runtime/inspection/session-identity.ts +20 -0
- package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +25 -2
- package/packages/piagent-core/runtime/policy/block-remedy.ts +139 -0
- package/packages/piagent-core/runtime/registration/task-start-tool.ts +9 -13
- package/packages/piagent-core/runtime/registration/workflow-commands.ts +5 -4
- package/packages/piagent-core/runtime/session/token-economics.js +34 -0
- package/packages/piagent-core/runtime/tools/phase-tool-runtime.ts +2 -1
- package/packages/piagent-core/runtime/workflows/input-routing.ts +13 -2
- package/packages/piagent-core/runtime/workflows/task-intake.ts +5 -0
- package/packages/piagent-core/security/sensitive-data.js +7 -0
- package/packages/piagent-webui/dist/client/assets/MarkdownMessage-Uouhx1QO.js +29 -0
- package/packages/piagent-webui/dist/client/assets/index-BV-_03yl.css +1 -0
- package/packages/piagent-webui/dist/client/assets/index-MBZq8XLV.js +174 -0
- package/packages/piagent-webui/dist/client/index.html +2 -2
- package/packages/piagent-webui/extension/lifecycle-controller.ts +15 -0
- package/packages/piagent-webui/extension/piagent-webui.ts +19 -6
- package/packages/piagent-webui/extension/same-session-bridge.ts +12 -12
- package/packages/piagent-webui/gateway/ai-capability-inspection.ts +53 -0
- package/packages/piagent-webui/gateway/extension-authority.ts +47 -0
- package/packages/piagent-webui/gateway/gateway-service.ts +23 -3
- package/packages/piagent-webui/gateway/native-project-picker.ts +63 -7
- package/packages/piagent-webui/gateway/session-attachment-registry.ts +165 -0
- package/packages/piagent-webui/gateway/session-catalog.ts +62 -9
- package/packages/piagent-webui/gateway/session-command-controller.ts +47 -14
- package/packages/piagent-webui/gateway/session-inspection-registry.ts +67 -12
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +24 -11
- package/packages/piagent-webui/package.json +3 -1
- package/packages/piagent-webui/server/core-inspection-provider.ts +52 -15
- package/packages/piagent-webui/server/ipc-read-model-client.ts +2 -0
- package/packages/piagent-webui/server/loopback-server.ts +63 -25
- package/packages/piagent-webui/server/read-model-provider.ts +2 -0
- package/packages/piagent-webui/server/read-only-router.ts +6 -0
- package/packages/piagent-webui/server/transcript-projection.ts +58 -2
- package/schemas/piagent-webui/attachment-v1.schema.json +24 -4
- package/schemas/piagent-webui/capabilities-v1.schema.json +2 -2
- package/schemas/piagent-webui/catalog-v1.json +9 -0
- package/schemas/piagent-webui/document-workspace-v1.schema.json +155 -0
- package/schemas/piagent-webui/session-command-v1.schema.json +14 -3
- package/schemas/piagent-webui/transcript-v1.schema.json +27 -0
- package/scripts/build-docs-site.mjs +21 -1
- package/scripts/explain-command.mjs +240 -0
- package/scripts/install-global.sh +104 -3
- package/scripts/mcp-manage.mjs +17 -4
- package/scripts/pi-session-stats.sh +10 -2
- package/scripts/piagent-cli.mjs +14 -1
- package/scripts/piagent-dashboard.mjs +55 -2
- package/scripts/setup.sh +69 -33
- package/scripts/traffic-snapshot.mjs +214 -0
- package/scripts/typescript-loader.mjs +20 -4
- package/scripts/uninstall-global.sh +53 -2
- package/scripts/update-global.mjs +33 -1
- package/scripts/verify-local.sh +20 -1
- package/scripts/verify-translation-pairing.mjs +121 -0
- package/packages/piagent-webui/dist/client/assets/index-DjMntaK5.js +0 -172
- package/packages/piagent-webui/dist/client/assets/index-DwKCWbwd.css +0 -1
package/scripts/mcp-manage.mjs
CHANGED
|
@@ -536,7 +536,7 @@ function commandPreset(args) {
|
|
|
536
536
|
presets: CATALOG_PRESETS,
|
|
537
537
|
servers: Object.fromEntries(Object.entries(CATALOG_SERVERS).map(([name, server]) => [
|
|
538
538
|
name,
|
|
539
|
-
{ description: server.description, requires: CATALOG_PREREQUISITES[name] ?? [] }
|
|
539
|
+
{ description: server.description, maturity: server.maturity ?? "stable", requires: CATALOG_PREREQUISITES[name] ?? [] }
|
|
540
540
|
]))
|
|
541
541
|
}, null, 2)}\n`);
|
|
542
542
|
return 0;
|
|
@@ -563,7 +563,9 @@ function commandPreset(args) {
|
|
|
563
563
|
const added = [];
|
|
564
564
|
const kept = [];
|
|
565
565
|
const replaced = [];
|
|
566
|
-
|
|
566
|
+
const resolvedPreset = resolvePreset(presetName);
|
|
567
|
+
const experimental = resolvedPreset.filter((server) => server.maturity === "experimental").map((server) => server.name);
|
|
568
|
+
for (const server of resolvedPreset) {
|
|
567
569
|
if (Object.hasOwn(config.mcpServers, server.name)) {
|
|
568
570
|
if (!replace) {
|
|
569
571
|
kept.push(server.name);
|
|
@@ -585,6 +587,7 @@ function commandPreset(args) {
|
|
|
585
587
|
added,
|
|
586
588
|
kept,
|
|
587
589
|
replaced,
|
|
590
|
+
experimental,
|
|
588
591
|
serverCount: Object.keys(config.mcpServers).length
|
|
589
592
|
};
|
|
590
593
|
|
|
@@ -594,11 +597,21 @@ function commandPreset(args) {
|
|
|
594
597
|
return 0;
|
|
595
598
|
}
|
|
596
599
|
|
|
600
|
+
// Warn before the write. Naming a Google preset is explicit, but it is not
|
|
601
|
+
// evidence that the operator knows the endpoint is pre-GA or that Pi OAuth
|
|
602
|
+
// has not passed a credentialed release gate yet.
|
|
603
|
+
if (experimental.length > 0) {
|
|
604
|
+
process.stderr.write(
|
|
605
|
+
`\nEXPERIMENTAL: ${experimental.join(", ")} use Google Workspace Developer Preview endpoints. `
|
|
606
|
+
+ "They are not part of the default presets and have no Pi OAuth end-to-end release proof yet.\n"
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
|
|
597
610
|
writeMcpConfig(file, config);
|
|
598
611
|
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
599
612
|
|
|
600
|
-
// stdout stays machine-readable;
|
|
601
|
-
const lines =
|
|
613
|
+
// stdout stays machine-readable; operator notes go to stderr.
|
|
614
|
+
const lines = resolvedPreset.flatMap((server) => server.requires.map((item) => ` ${server.name}: ${item}`));
|
|
602
615
|
if (lines.length > 0) {
|
|
603
616
|
process.stderr.write("\nServer definitions are written. Each still needs this before it can connect:\n");
|
|
604
617
|
for (const line of lines) process.stderr.write(`${line}\n`);
|
|
@@ -108,8 +108,13 @@ RPC_OUTPUT="$(
|
|
|
108
108
|
| pi -ne --mode rpc --session "$SESSION_FILE" --offline --approve
|
|
109
109
|
)"
|
|
110
110
|
|
|
111
|
-
node --input-type=module - "$RPC_OUTPUT" <<'NODE'
|
|
111
|
+
node --input-type=module - "$RPC_OUTPUT" "$SCRIPT_DIR/../packages/piagent-core/runtime/session/token-economics.js" <<'NODE'
|
|
112
|
+
import { pathToFileURL } from "node:url";
|
|
113
|
+
|
|
112
114
|
const input = process.argv[2] ?? "";
|
|
115
|
+
// The token arithmetic lives in a module both this command and its test can
|
|
116
|
+
// read, so the terminal cannot drift from the definition the benchmark uses.
|
|
117
|
+
const { sessionTokenEconomics, formatTokenEconomics } = await import(pathToFileURL(process.argv[3]).href);
|
|
113
118
|
|
|
114
119
|
const line = input
|
|
115
120
|
.split(/\n+/)
|
|
@@ -126,6 +131,7 @@ const input = process.argv[2] ?? "";
|
|
|
126
131
|
const data = response.data;
|
|
127
132
|
const tokens = data.tokens ?? {};
|
|
128
133
|
const context = data.contextUsage ?? {};
|
|
134
|
+
const economics = sessionTokenEconomics(tokens);
|
|
129
135
|
const number = (value) => value === null || value === undefined ? "unknown" : Math.round(Number(value)).toLocaleString("en-US");
|
|
130
136
|
const money = (value) => value === null || value === undefined ? "unknown" : `$${Number(value).toFixed(6)}`;
|
|
131
137
|
const percent = (value) => value === null || value === undefined ? "unknown" : `${Number(value).toFixed(1)}%`;
|
|
@@ -144,7 +150,8 @@ const input = process.argv[2] ?? "";
|
|
|
144
150
|
output: tokens.output ?? null,
|
|
145
151
|
cacheRead: tokens.cacheRead ?? null,
|
|
146
152
|
cacheWrite: tokens.cacheWrite ?? null,
|
|
147
|
-
total: tokens.total ?? null
|
|
153
|
+
total: tokens.total ?? null,
|
|
154
|
+
...economics
|
|
148
155
|
},
|
|
149
156
|
cost: data.cost ?? null,
|
|
150
157
|
contextUsage: context.tokens === undefined ? null : {
|
|
@@ -154,6 +161,7 @@ const input = process.argv[2] ?? "";
|
|
|
154
161
|
},
|
|
155
162
|
summary: {
|
|
156
163
|
tokens: `${number(tokens.total)} total (${number(tokens.input)} input, ${number(tokens.output)} output, ${number(tokens.cacheRead)} cache read, ${number(tokens.cacheWrite)} cache write)`,
|
|
164
|
+
...formatTokenEconomics(economics, number, percent),
|
|
157
165
|
cost: money(data.cost),
|
|
158
166
|
context: context.tokens === undefined ? "unknown" : `${number(context.tokens)} / ${number(context.contextWindow)} (${percent(context.percent)})`
|
|
159
167
|
}
|
package/scripts/piagent-cli.mjs
CHANGED
|
@@ -24,6 +24,7 @@ const scriptByCommand = {
|
|
|
24
24
|
"piagent-import-instructions": "scripts/import-agent-instructions.mjs",
|
|
25
25
|
"piagent-auto": "scripts/pi-auto.sh",
|
|
26
26
|
"piagent-context": "scripts/context-engine.mjs",
|
|
27
|
+
"piagent-explain": "scripts/explain-command.mjs",
|
|
27
28
|
"piagent-webui": "scripts/piagent-webui-launcher.mjs",
|
|
28
29
|
"piagent-dashboard": "scripts/piagent-dashboard.mjs"
|
|
29
30
|
};
|
|
@@ -38,8 +39,20 @@ if (invokedAs === "piagent") {
|
|
|
38
39
|
if (subcommand === "dashboard") {
|
|
39
40
|
script = "scripts/piagent-dashboard.mjs";
|
|
40
41
|
forwardedArgs = forwardedArgs.slice(1);
|
|
42
|
+
} else if (subcommand === "explain") {
|
|
43
|
+
script = "scripts/explain-command.mjs";
|
|
44
|
+
forwardedArgs = forwardedArgs.slice(1);
|
|
41
45
|
} else if ([undefined, "help", "--help", "-h"].includes(subcommand)) {
|
|
42
|
-
|
|
46
|
+
// Subcommands are listed, their flags are not. Restating them here meant a
|
|
47
|
+
// second copy that drifts: this line still advertised the dashboard without
|
|
48
|
+
// `--repair` after that flag shipped. Each command states its own surface.
|
|
49
|
+
console.log("Usage: piagent <command> [options]");
|
|
50
|
+
console.log("");
|
|
51
|
+
console.log("Commands:");
|
|
52
|
+
console.log(" dashboard Open and manage the local session hub");
|
|
53
|
+
console.log(" explain Say why the guard would allow or block a shell command");
|
|
54
|
+
console.log("");
|
|
55
|
+
console.log("Run `piagent <command> --help` for that command's options.");
|
|
43
56
|
process.exit(0);
|
|
44
57
|
}
|
|
45
58
|
}
|
|
@@ -22,6 +22,8 @@ const json = args.includes("--json");
|
|
|
22
22
|
const repair = args.includes("--repair");
|
|
23
23
|
const agentDirIndex = args.indexOf("--agent-dir");
|
|
24
24
|
const agentDir = agentDirIndex >= 0 ? args[agentDirIndex + 1] : undefined;
|
|
25
|
+
const GATEWAY_ERROR_LOG = "gateway-stderr.log";
|
|
26
|
+
const GATEWAY_ERROR_MAX_BYTES = 4_000;
|
|
25
27
|
|
|
26
28
|
function output(value) {
|
|
27
29
|
process.stdout.write(`${json ? JSON.stringify(value) : value}\n`);
|
|
@@ -68,6 +70,42 @@ function removeOwnedSocket(target) {
|
|
|
68
70
|
} catch { return false; }
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
function gatewayErrorLog(state) {
|
|
74
|
+
return path.join(state.root, GATEWAY_ERROR_LOG);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function writeGatewayErrorLog(state, message = "") {
|
|
78
|
+
const target = gatewayErrorLog(state);
|
|
79
|
+
const descriptor = fs.openSync(target, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC
|
|
80
|
+
| (fs.constants.O_NOFOLLOW ?? 0), 0o600);
|
|
81
|
+
try {
|
|
82
|
+
const stat = fs.fstatSync(descriptor);
|
|
83
|
+
if (!stat.isFile() || typeof process.getuid === "function" && stat.uid !== process.getuid()) {
|
|
84
|
+
throw new Error("gateway-error-log-invalid");
|
|
85
|
+
}
|
|
86
|
+
fs.fchmodSync(descriptor, 0o600);
|
|
87
|
+
if (message) fs.writeFileSync(descriptor, message.slice(-GATEWAY_ERROR_MAX_BYTES));
|
|
88
|
+
} finally { fs.closeSync(descriptor); }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function readGatewayErrorLog(state) {
|
|
92
|
+
const target = gatewayErrorLog(state);
|
|
93
|
+
try {
|
|
94
|
+
const before = fs.lstatSync(target);
|
|
95
|
+
if (!before.isFile() || before.isSymbolicLink()
|
|
96
|
+
|| typeof process.getuid === "function" && before.uid !== process.getuid()) return "";
|
|
97
|
+
const descriptor = fs.openSync(target, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
98
|
+
try {
|
|
99
|
+
const stat = fs.fstatSync(descriptor);
|
|
100
|
+
if (!stat.isFile() || stat.dev !== before.dev || stat.ino !== before.ino) return "";
|
|
101
|
+
const length = Math.min(stat.size, GATEWAY_ERROR_MAX_BYTES);
|
|
102
|
+
const body = Buffer.alloc(length);
|
|
103
|
+
fs.readSync(descriptor, body, 0, length, Math.max(0, stat.size - length));
|
|
104
|
+
return body.toString("utf8");
|
|
105
|
+
} finally { fs.closeSync(descriptor); }
|
|
106
|
+
} catch { return ""; }
|
|
107
|
+
}
|
|
108
|
+
|
|
71
109
|
async function control(action, timeoutMs = 1_500) {
|
|
72
110
|
const state = gatewayProfileState(agentDir);
|
|
73
111
|
return await requestGatewayControl(state.controlSocket, { action }, timeoutMs);
|
|
@@ -88,6 +126,8 @@ async function ensureStarted() {
|
|
|
88
126
|
const existing = await currentLaunchUrl();
|
|
89
127
|
if (existing) return existing;
|
|
90
128
|
const effectiveAgentDir = path.resolve(agentDir ?? process.env.PI_CODING_AGENT_DIR ?? path.join(os.homedir(), ".pi", "agent"));
|
|
129
|
+
const state = gatewayProfileState(effectiveAgentDir);
|
|
130
|
+
writeGatewayErrorLog(state);
|
|
91
131
|
const child = spawn(process.execPath, [
|
|
92
132
|
"--disable-warning=ExperimentalWarning",
|
|
93
133
|
"--import", path.join(packageRoot, "scripts", "register-typescript-loader.mjs"),
|
|
@@ -97,14 +137,23 @@ async function ensureStarted() {
|
|
|
97
137
|
], { cwd: process.cwd(), env: { ...process.env,
|
|
98
138
|
PIAGENT_PINNED_TS_TRANSFORM_ROOT: path.join(effectiveAgentDir, "npm", "node_modules", "pi-mcp-adapter") },
|
|
99
139
|
detached: true, stdio: "ignore" });
|
|
140
|
+
let exit = null;
|
|
141
|
+
child.on("exit", (code, signal) => { exit = signal ? `signal ${signal}` : `exit code ${code}`; });
|
|
100
142
|
child.unref();
|
|
101
143
|
const deadline = Date.now() + 12_000;
|
|
102
144
|
while (Date.now() < deadline) {
|
|
103
145
|
const url = await currentLaunchUrl();
|
|
104
146
|
if (url) return url;
|
|
147
|
+
// A child that has already exited is never going to publish a URL, so
|
|
148
|
+
// waiting out the rest of the deadline only delays the report.
|
|
149
|
+
if (exit !== null) break;
|
|
105
150
|
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
106
151
|
}
|
|
107
|
-
|
|
152
|
+
const detail = [exit && `The gateway process ended with ${exit}.`, readGatewayErrorLog(state).trim()]
|
|
153
|
+
.filter(Boolean).join("\n");
|
|
154
|
+
throw new Error(detail
|
|
155
|
+
? `gateway-start-failed\n${detail}`
|
|
156
|
+
: "gateway-start-timeout\nThe gateway did not publish a URL within 12s and wrote nothing to stderr.");
|
|
108
157
|
}
|
|
109
158
|
|
|
110
159
|
async function serve() {
|
|
@@ -203,6 +252,10 @@ async function main() {
|
|
|
203
252
|
}
|
|
204
253
|
|
|
205
254
|
main().catch((error) => {
|
|
206
|
-
|
|
255
|
+
const message = `Piagent dashboard failed: ${error instanceof Error ? error.message : String(error)}\n`;
|
|
256
|
+
if (command === "serve") {
|
|
257
|
+
try { writeGatewayErrorLog(gatewayProfileState(agentDir), message); } catch { /* stderr still reports the failure. */ }
|
|
258
|
+
}
|
|
259
|
+
process.stderr.write(message);
|
|
207
260
|
process.exitCode = 1;
|
|
208
261
|
});
|
package/scripts/setup.sh
CHANGED
|
@@ -22,7 +22,7 @@ Options:
|
|
|
22
22
|
--project-only, --no-global Only initialize project, skip global install
|
|
23
23
|
--no-project Skip project initialization
|
|
24
24
|
--with-mcp / --no-mcp Install/skip Pi MCP adapter during global install (default: install)
|
|
25
|
-
--mcp-preset <minimal|core|popular|all|docs|browser|github|design|design-local|web>
|
|
25
|
+
--mcp-preset <minimal|core|popular|all|docs|browser|github|design|design-local|web|documents|google|google-mail|google-all>
|
|
26
26
|
Configure shared global MCP baseline when --with-mcp is enabled (default: core)
|
|
27
27
|
--with-subagents / --no-subagents
|
|
28
28
|
Install/skip pi-subagents during global install (default: install)
|
|
@@ -244,15 +244,11 @@ if [[ "$WITH_MCP" == false && "$MCP_PRESET_EXPLICIT" == true ]]; then
|
|
|
244
244
|
exit 2
|
|
245
245
|
fi
|
|
246
246
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if [[ "$DO_PROJECT" == true && -n "$PROJECT_PATH" && -f "$PROJECT_PATH/.pi/settings.json" ]]; then
|
|
254
|
-
local existing_source
|
|
255
|
-
existing_source="$(node --input-type=module - "$PROJECT_PATH/.pi/settings.json" <<'NODE'
|
|
247
|
+
# The pin a project has committed, or empty. It is authority over that project
|
|
248
|
+
# and nothing else -- one project may not decide what the machine runs.
|
|
249
|
+
project_pinned_source() {
|
|
250
|
+
[[ "$DO_PROJECT" == true && -n "$PROJECT_PATH" && -f "$PROJECT_PATH/.pi/settings.json" ]] || return 0
|
|
251
|
+
node --input-type=module - "$PROJECT_PATH/.pi/settings.json" <<'NODE'
|
|
256
252
|
import fs from "node:fs";
|
|
257
253
|
const settings = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
258
254
|
const source = Array.isArray(settings.packages)
|
|
@@ -260,30 +256,51 @@ const source = Array.isArray(settings.packages)
|
|
|
260
256
|
: undefined;
|
|
261
257
|
if (source) process.stdout.write(source);
|
|
262
258
|
NODE
|
|
263
|
-
|
|
264
|
-
if [[ -n "$existing_source" ]]; then
|
|
265
|
-
printf '%s\n' "$existing_source"
|
|
266
|
-
return
|
|
267
|
-
fi
|
|
268
|
-
fi
|
|
259
|
+
}
|
|
269
260
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
fi
|
|
261
|
+
# Running from an installed package rather than a checkout. The name and version
|
|
262
|
+
# on disk are exactly what a teammate resolves from the registry, so they are a
|
|
263
|
+
# portable source; the install path is not, and project settings are meant to be
|
|
264
|
+
# committed.
|
|
265
|
+
running_helper_source() {
|
|
266
|
+
[[ "$PLATFORM_ROOT" == */node_modules/* ]] || return 0
|
|
267
|
+
local installed_name installed_version
|
|
268
|
+
installed_name="$(node -e 'const fs=require("node:fs"); process.stdout.write(JSON.parse(fs.readFileSync(process.argv[1], "utf8")).name ?? "");' "$PLATFORM_ROOT/package.json")"
|
|
269
|
+
installed_version="$(node -e 'const fs=require("node:fs"); process.stdout.write(JSON.parse(fs.readFileSync(process.argv[1], "utf8")).version ?? "");' "$PLATFORM_ROOT/package.json")"
|
|
270
|
+
if [[ -n "$installed_name" && "$installed_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?$ ]]; then
|
|
271
|
+
printf 'npm:%s@%s' "$installed_name" "$installed_version"
|
|
282
272
|
fi
|
|
273
|
+
}
|
|
283
274
|
|
|
275
|
+
local_path_source() {
|
|
284
276
|
echo "WARN: No exact package source provided." >&2
|
|
285
277
|
echo "WARN: using local platform path; do not commit project .pi/settings.json with this value for team rollout." >&2
|
|
286
|
-
printf '%s
|
|
278
|
+
printf '%s' "$PLATFORM_ROOT"
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
# One resolved value used to serve both consumers, and the two do not want the
|
|
282
|
+
# same answer. A project pin is what that project's team agreed to run; the
|
|
283
|
+
# global install is what every project on this machine gets. Reading the pin
|
|
284
|
+
# first meant the documented two-command install silently downgraded the
|
|
285
|
+
# machine-wide package to whatever one project happened to pin -- including
|
|
286
|
+
# projects the operator only meant to open once.
|
|
287
|
+
resolve_global_package_source() {
|
|
288
|
+
if [[ -n "$PACKAGE_SOURCE" ]]; then printf '%s\n' "$PACKAGE_SOURCE"; return; fi
|
|
289
|
+
local helper
|
|
290
|
+
helper="$(running_helper_source)"
|
|
291
|
+
if [[ -n "$helper" ]]; then printf '%s\n' "$helper"; return; fi
|
|
292
|
+
printf '%s\n' "$(local_path_source)"
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
resolve_project_package_source() {
|
|
296
|
+
if [[ -n "$PACKAGE_SOURCE" ]]; then printf '%s\n' "$PACKAGE_SOURCE"; return; fi
|
|
297
|
+
local pinned
|
|
298
|
+
pinned="$(project_pinned_source)"
|
|
299
|
+
if [[ -n "$pinned" ]]; then printf '%s\n' "$pinned"; return; fi
|
|
300
|
+
local helper
|
|
301
|
+
helper="$(running_helper_source)"
|
|
302
|
+
if [[ -n "$helper" ]]; then printf '%s\n' "$helper"; return; fi
|
|
303
|
+
printf '%s\n' "$(local_path_source)"
|
|
287
304
|
}
|
|
288
305
|
|
|
289
306
|
print_cmd() {
|
|
@@ -385,22 +402,41 @@ if [[ "$DO_PROJECT" == true ]]; then
|
|
|
385
402
|
PROJECT_PATH="$(cd "$PROJECT_PATH" && pwd)"
|
|
386
403
|
fi
|
|
387
404
|
|
|
388
|
-
|
|
405
|
+
GLOBAL_PACKAGE_SOURCE="$(resolve_global_package_source)"
|
|
406
|
+
PROJECT_PACKAGE_SOURCE="$(resolve_project_package_source)"
|
|
389
407
|
|
|
390
408
|
echo "Pi Agent Platform setup"
|
|
391
409
|
echo " platform: $PLATFORM_ROOT"
|
|
392
|
-
|
|
410
|
+
if [[ "$DO_GLOBAL" == true ]]; then
|
|
411
|
+
echo " globalPackageSource: $GLOBAL_PACKAGE_SOURCE"
|
|
412
|
+
fi
|
|
413
|
+
if [[ "$DO_PROJECT" == true ]]; then
|
|
414
|
+
echo " projectPackageSource: $PROJECT_PACKAGE_SOURCE"
|
|
415
|
+
fi
|
|
393
416
|
echo " globalInstall: $DO_GLOBAL"
|
|
394
417
|
echo " projectInit: $DO_PROJECT"
|
|
395
418
|
if [[ "$DO_PROJECT" == true ]]; then
|
|
396
419
|
echo " project: $PROJECT_PATH"
|
|
397
420
|
echo " profile: $PROFILE_INPUT"
|
|
398
421
|
fi
|
|
422
|
+
|
|
423
|
+
# The operator asked for both scopes and the project wants a different build
|
|
424
|
+
# than the one about to be installed machine-wide. Neither choice is wrong, so
|
|
425
|
+
# neither is overridden -- but a divergence they cannot see is how the old
|
|
426
|
+
# silent downgrade went unnoticed.
|
|
427
|
+
if [[ "$DO_GLOBAL" == true && "$DO_PROJECT" == true && "$GLOBAL_PACKAGE_SOURCE" != "$PROJECT_PACKAGE_SOURCE" ]]; then
|
|
428
|
+
echo
|
|
429
|
+
echo "NOTE: this project pins a different package source than the global install." >&2
|
|
430
|
+
echo "NOTE: global: $GLOBAL_PACKAGE_SOURCE" >&2
|
|
431
|
+
echo "NOTE: project: $PROJECT_PACKAGE_SOURCE" >&2
|
|
432
|
+
echo "NOTE: the project keeps its pin and the machine keeps the running helper." >&2
|
|
433
|
+
echo "NOTE: pass --package-source <source> to make both the same." >&2
|
|
434
|
+
fi
|
|
399
435
|
echo
|
|
400
436
|
|
|
401
437
|
if [[ "$DO_GLOBAL" == true ]]; then
|
|
402
438
|
ensure_pi_cli
|
|
403
|
-
install_args=("$PLATFORM_ROOT/scripts/install-global.sh" "--package-source" "$
|
|
439
|
+
install_args=("$PLATFORM_ROOT/scripts/install-global.sh" "--package-source" "$GLOBAL_PACKAGE_SOURCE")
|
|
404
440
|
# The opt-out has to be passed explicitly. The installer defaults MCP on, so
|
|
405
441
|
# saying nothing here would install it against the operator's choice.
|
|
406
442
|
# Subagents also needs an explicit opt-out because the installer now preserves
|
|
@@ -432,7 +468,7 @@ if [[ "$DO_GLOBAL" == true ]]; then
|
|
|
432
468
|
fi
|
|
433
469
|
|
|
434
470
|
if [[ "$DO_PROJECT" == true ]]; then
|
|
435
|
-
init_args=("$PLATFORM_ROOT/scripts/init-project.sh" "$PROJECT_PATH" "--profile" "$PROFILE_INPUT" "--package-source" "$
|
|
471
|
+
init_args=("$PLATFORM_ROOT/scripts/init-project.sh" "$PROJECT_PATH" "--profile" "$PROFILE_INPUT" "--package-source" "$PROJECT_PACKAGE_SOURCE")
|
|
436
472
|
if [[ "$FORCE_PROFILE" == true ]]; then
|
|
437
473
|
init_args+=("--force-profile")
|
|
438
474
|
fi
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Archives GitHub repository traffic before GitHub discards it.
|
|
3
|
+
//
|
|
4
|
+
// The traffic API only serves a rolling 14-day window. Anything older is gone
|
|
5
|
+
// for good -- there is no endpoint that returns it and no way to reconstruct
|
|
6
|
+
// it. So the counts are merged into CSV files that keep growing, and a run that
|
|
7
|
+
// happens at least every 14 days loses nothing.
|
|
8
|
+
//
|
|
9
|
+
// Merge rule: a row is keyed by its day and the newest fetch wins. Today's
|
|
10
|
+
// bucket is still filling while the run happens, so it is always partial; the
|
|
11
|
+
// next run overwrites it with the settled value. Once a day leaves the window
|
|
12
|
+
// it stops being returned at all, and the stored row is final.
|
|
13
|
+
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
const EXIT_USAGE = 2;
|
|
18
|
+
const EXIT_FORBIDDEN = 3;
|
|
19
|
+
|
|
20
|
+
const SERIES = [
|
|
21
|
+
{ endpoint: "clones", file: "clones.csv", columns: ["date", "count", "uniques"] },
|
|
22
|
+
{ endpoint: "views", file: "views.csv", columns: ["date", "count", "uniques"] }
|
|
23
|
+
];
|
|
24
|
+
// Referrers and paths are a 14-day rollup, not a daily series: the API gives no
|
|
25
|
+
// per-day breakdown, so a row is only meaningful next to the day it was read.
|
|
26
|
+
const ROLLUPS = [
|
|
27
|
+
{ endpoint: "popular/referrers", file: "referrers.csv", key: "referrer", columns: ["snapshot_date", "referrer", "count", "uniques"] },
|
|
28
|
+
{ endpoint: "popular/paths", file: "paths.csv", key: "path", columns: ["snapshot_date", "path", "title", "count", "uniques"] }
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function fail(message, code = EXIT_USAGE) {
|
|
32
|
+
console.error(`FAIL: ${message}`);
|
|
33
|
+
process.exit(code);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function parseArguments(argv) {
|
|
37
|
+
const options = { repository: "", out: "traffic", dryRun: false, today: "" };
|
|
38
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
39
|
+
const argument = argv[index];
|
|
40
|
+
switch (argument) {
|
|
41
|
+
case "--repository": case "--repo": {
|
|
42
|
+
options.repository = argv[index += 1] ?? "";
|
|
43
|
+
if (!/^[^/\s]+\/[^/\s]+$/.test(options.repository)) fail("--repository needs owner/name");
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
case "--out": {
|
|
47
|
+
options.out = argv[index += 1] ?? "";
|
|
48
|
+
if (!options.out) fail("--out needs a directory");
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
// The archive is written from a snapshot date, and a test cannot wait a
|
|
52
|
+
// day to prove the merge. Injecting it keeps the assertions exact.
|
|
53
|
+
case "--today": {
|
|
54
|
+
options.today = argv[index += 1] ?? "";
|
|
55
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(options.today)) fail("--today needs YYYY-MM-DD");
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case "--dry-run": options.dryRun = true; break;
|
|
59
|
+
case "-h": case "--help": options.help = true; break;
|
|
60
|
+
default: fail(`unknown argument: ${argument}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return options;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function csvEscape(value) {
|
|
67
|
+
const text = String(value ?? "");
|
|
68
|
+
return /[",\n\r]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function parseCsv(text) {
|
|
72
|
+
const rows = [];
|
|
73
|
+
let row = [], field = "", quoted = false;
|
|
74
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
75
|
+
const character = text[index];
|
|
76
|
+
if (quoted) {
|
|
77
|
+
if (character === '"' && text[index + 1] === '"') { field += '"'; index += 1; }
|
|
78
|
+
else if (character === '"') quoted = false;
|
|
79
|
+
else field += character;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (character === '"') quoted = true;
|
|
83
|
+
else if (character === ",") { row.push(field); field = ""; }
|
|
84
|
+
else if (character === "\n") { row.push(field); rows.push(row); row = []; field = ""; }
|
|
85
|
+
else if (character !== "\r") field += character;
|
|
86
|
+
}
|
|
87
|
+
if (field.length || row.length) { row.push(field); rows.push(row); }
|
|
88
|
+
return rows.filter((entry) => entry.some((cell) => cell !== ""));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function readTable(file, columns) {
|
|
92
|
+
if (!fs.existsSync(file)) return [];
|
|
93
|
+
const rows = parseCsv(fs.readFileSync(file, "utf8"));
|
|
94
|
+
const header = rows.shift();
|
|
95
|
+
if (!header || header.join(",") !== columns.join(",")) {
|
|
96
|
+
fail(`${file} has header [${header?.join(", ") ?? "none"}], expected [${columns.join(", ")}]`);
|
|
97
|
+
}
|
|
98
|
+
return rows.map((row) => Object.fromEntries(columns.map((column, index) => [column, row[index] ?? ""])));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function writeTable(file, columns, rows) {
|
|
102
|
+
const lines = [columns.join(",")];
|
|
103
|
+
for (const row of rows) lines.push(columns.map((column) => csvEscape(row[column])).join(","));
|
|
104
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
105
|
+
fs.writeFileSync(file, `${lines.join("\n")}\n`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Existing rows plus fetched rows, keyed by `key`. The fetched value wins,
|
|
110
|
+
* because it is the newer reading of the same day. Rows outside the API window
|
|
111
|
+
* are untouched -- that is the whole point of the archive.
|
|
112
|
+
*/
|
|
113
|
+
export function mergeByKey(existing, fetched, key) {
|
|
114
|
+
const merged = new Map(existing.map((row) => [row[key], row]));
|
|
115
|
+
for (const row of fetched) merged.set(row[key], row);
|
|
116
|
+
return [...merged.values()].sort((a, b) => String(a[key]).localeCompare(String(b[key])));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Rollup rows for one snapshot date, replacing any earlier run on that date. */
|
|
120
|
+
export function mergeRollup(existing, fetched, snapshotDate) {
|
|
121
|
+
const kept = existing.filter((row) => row.snapshot_date !== snapshotDate);
|
|
122
|
+
return [...kept, ...fetched].sort((a, b) =>
|
|
123
|
+
String(a.snapshot_date).localeCompare(String(b.snapshot_date))
|
|
124
|
+
|| Number(b.count) - Number(a.count));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function fetchTraffic(repository, endpoint, token) {
|
|
128
|
+
const url = `https://api.github.com/repos/${repository}/traffic/${endpoint}`;
|
|
129
|
+
const response = await fetch(url, {
|
|
130
|
+
headers: {
|
|
131
|
+
accept: "application/vnd.github+json",
|
|
132
|
+
authorization: `Bearer ${token}`,
|
|
133
|
+
"user-agent": "piagent-traffic-snapshot",
|
|
134
|
+
"x-github-api-version": "2022-11-28"
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
if (response.status === 403 || response.status === 404) {
|
|
138
|
+
// The distinction matters: this is the one failure an operator cannot debug
|
|
139
|
+
// from the status code, because the traffic API answers 403 for a token
|
|
140
|
+
// that is valid but lacks push access -- which the default workflow token
|
|
141
|
+
// may or may not have, depending on repository settings.
|
|
142
|
+
fail(`the token cannot read ${endpoint} for ${repository} (HTTP ${response.status}).\n`
|
|
143
|
+
+ " Repository traffic needs push access. If this ran in GitHub Actions with the\n"
|
|
144
|
+
+ " default GITHUB_TOKEN, create a fine-grained personal access token with the\n"
|
|
145
|
+
+ " 'Administration: read' permission on this repository, store it as the\n"
|
|
146
|
+
+ " TRAFFIC_TOKEN secret, and re-run. Nothing was written.", EXIT_FORBIDDEN);
|
|
147
|
+
}
|
|
148
|
+
if (!response.ok) fail(`GET ${url} returned HTTP ${response.status}`);
|
|
149
|
+
return response.json();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function snapshot(options, token, fetcher = fetchTraffic) {
|
|
153
|
+
const today = options.today || new Date().toISOString().slice(0, 10);
|
|
154
|
+
const summary = { repository: options.repository, snapshotDate: today, files: {} };
|
|
155
|
+
|
|
156
|
+
// Read every existing file before fetching anything. A corrupt archive is a
|
|
157
|
+
// reason to stop, and stopping before the network call keeps the failure
|
|
158
|
+
// about the file rather than about whatever the API happened to answer.
|
|
159
|
+
const existing = new Map();
|
|
160
|
+
for (const target of [...SERIES, ...ROLLUPS]) {
|
|
161
|
+
existing.set(target.file, readTable(path.join(options.out, target.file), target.columns));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const series of SERIES) {
|
|
165
|
+
const payload = await fetcher(options.repository, series.endpoint, token);
|
|
166
|
+
const fetched = (payload[series.endpoint] ?? []).map((entry) => ({
|
|
167
|
+
date: String(entry.timestamp ?? "").slice(0, 10),
|
|
168
|
+
count: String(entry.count ?? 0),
|
|
169
|
+
uniques: String(entry.uniques ?? 0)
|
|
170
|
+
})).filter((row) => row.date);
|
|
171
|
+
const file = path.join(options.out, series.file);
|
|
172
|
+
const merged = mergeByKey(existing.get(series.file), fetched, "date");
|
|
173
|
+
if (!options.dryRun) writeTable(file, series.columns, merged);
|
|
174
|
+
summary.files[series.file] = { fetchedDays: fetched.length, totalDays: merged.length };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const rollup of ROLLUPS) {
|
|
178
|
+
const payload = await fetcher(options.repository, rollup.endpoint, token);
|
|
179
|
+
const fetched = (Array.isArray(payload) ? payload : []).map((entry) => ({
|
|
180
|
+
snapshot_date: today,
|
|
181
|
+
[rollup.key]: String(entry[rollup.key] ?? entry.referrer ?? entry.path ?? ""),
|
|
182
|
+
title: String(entry.title ?? ""),
|
|
183
|
+
count: String(entry.count ?? 0),
|
|
184
|
+
uniques: String(entry.uniques ?? 0)
|
|
185
|
+
}));
|
|
186
|
+
const file = path.join(options.out, rollup.file);
|
|
187
|
+
const merged = mergeRollup(existing.get(rollup.file), fetched, today);
|
|
188
|
+
if (!options.dryRun) writeTable(file, rollup.columns, merged);
|
|
189
|
+
summary.files[rollup.file] = { fetchedRows: fetched.length, totalRows: merged.length };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return summary;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const invokedDirectly = process.argv[1]
|
|
196
|
+
&& path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname);
|
|
197
|
+
|
|
198
|
+
if (invokedDirectly) {
|
|
199
|
+
const options = parseArguments(process.argv.slice(2));
|
|
200
|
+
if (options.help) {
|
|
201
|
+
console.log(`Usage:
|
|
202
|
+
node scripts/traffic-snapshot.mjs --repository <owner/name> [--out <dir>] [--dry-run]
|
|
203
|
+
|
|
204
|
+
Archives GitHub traffic (clones, views, referrers, paths) into growing CSV files.
|
|
205
|
+
The API only keeps 14 days, so run this at least fortnightly or the gap is
|
|
206
|
+
permanent. Reads the token from TRAFFIC_TOKEN, else GITHUB_TOKEN.`);
|
|
207
|
+
process.exit(0);
|
|
208
|
+
}
|
|
209
|
+
if (!options.repository) fail("--repository is required");
|
|
210
|
+
const token = process.env.TRAFFIC_TOKEN || process.env.GITHUB_TOKEN;
|
|
211
|
+
if (!token) fail("set TRAFFIC_TOKEN or GITHUB_TOKEN");
|
|
212
|
+
const summary = await snapshot(options, token);
|
|
213
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
214
|
+
}
|
|
@@ -18,11 +18,27 @@ export const PINNED_EXTERNAL_TRANSFORM = Object.freeze({
|
|
|
18
18
|
async function pinnedTransformRoot(url) {
|
|
19
19
|
const configured = process.env.PIAGENT_PINNED_TS_TRANSFORM_ROOT;
|
|
20
20
|
if (!configured || process.versions.node !== PINNED_EXTERNAL_TRANSFORM.nodeVersion) return false;
|
|
21
|
-
|
|
21
|
+
// This asks one question: is the file being loaded inside the pinned adapter?
|
|
22
|
+
// A configured root that is absent, unreadable or not that adapter answers
|
|
23
|
+
// "no". It used to throw instead, and the throw escaped `load` on the first
|
|
24
|
+
// `.ts` file of any kind -- so a machine without pi-mcp-adapter installed
|
|
25
|
+
// could not start the gateway at all, for a reason that had nothing to do
|
|
26
|
+
// with the file it died on. Answering "no" falls back to `strip`, which is
|
|
27
|
+
// the stricter of the two modes: this can only refuse a transform, never
|
|
28
|
+
// grant one.
|
|
29
|
+
let root;
|
|
30
|
+
let metadata;
|
|
31
|
+
try {
|
|
32
|
+
root = await fs.realpath(configured);
|
|
33
|
+
metadata = JSON.parse(await fs.readFile(path.join(root, "package.json"), "utf8"));
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if (metadata?.name !== PINNED_EXTERNAL_TRANSFORM.packageName
|
|
38
|
+
|| metadata?.version !== PINNED_EXTERNAL_TRANSFORM.packageVersion) return false;
|
|
39
|
+
const target = await fs.realpath(fileURLToPath(url));
|
|
22
40
|
const relative = path.relative(root, target);
|
|
23
|
-
|
|
24
|
-
const metadata = JSON.parse(await fs.readFile(path.join(root, "package.json"), "utf8"));
|
|
25
|
-
return metadata.name === PINNED_EXTERNAL_TRANSFORM.packageName && metadata.version === PINNED_EXTERNAL_TRANSFORM.packageVersion;
|
|
41
|
+
return Boolean(relative) && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
26
42
|
}
|
|
27
43
|
|
|
28
44
|
export async function load(url, context, nextLoad) {
|