@instafy/cli 0.4.1 → 0.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/README.md +2 -0
- package/dist/bind/bin/instafy-bind.mjs +56 -8
- package/dist/bind/src/check.mjs +38 -4
- package/dist/bind/src/server.mjs +4 -0
- package/dist/bind/src/starters/compare/app.js +13 -6
- package/dist/cli.js +964 -369
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1330,6 +1330,8 @@ var require_redaction = __commonJS({
|
|
|
1330
1330
|
const opaqueCandidatePattern = /[A-Za-z0-9][A-Za-z0-9_\-+/=~]{18,}[A-Za-z0-9=]/g;
|
|
1331
1331
|
const digitCodePattern = /(?<![\d.,][ \u00a0]?)(?:\d{6,8}|\d{3}[ \u00a0]\d{3}|\d{4}[ \u00a0]\d{4})(?![\d]|[.,]\d)/g;
|
|
1332
1332
|
const codeWordPattern = /(?<![\p{L}])(?:codes?|kode|otp|passcode|pin|tan|2fa|mfa|(?:best(?:ä|ae)tigungs|sicherheits|verifizierungs|verifikations|zugangs|anmelde|freigabe|authentifizierungs|wiederherstellungs|einmal)code)(?![\p{L}])/iu;
|
|
1333
|
+
const DIGIT_CODE_SOURCE = digitCodePattern.source;
|
|
1334
|
+
const CODE_WORD_SOURCE = codeWordPattern.source;
|
|
1333
1335
|
const NEARBY_CHARACTERS = 120;
|
|
1334
1336
|
const CODE_NEARBY_CHARACTERS = 32;
|
|
1335
1337
|
const HEX_NEARBY_CHARACTERS = 32;
|
|
@@ -1424,21 +1426,21 @@ var require_redaction = __commonJS({
|
|
|
1424
1426
|
return TEXT_MASK;
|
|
1425
1427
|
});
|
|
1426
1428
|
}
|
|
1427
|
-
text2 = text2.replace(digitCodePattern, (match, offset,
|
|
1428
|
-
const before =
|
|
1429
|
+
text2 = text2.replace(digitCodePattern, (match, offset, whole2) => {
|
|
1430
|
+
const before = whole2.slice(Math.max(0, offset - CODE_NEARBY_CHARACTERS), offset);
|
|
1429
1431
|
if (!codeWordPattern.test(before)) return match;
|
|
1430
1432
|
masked += 1;
|
|
1431
1433
|
matches += 1;
|
|
1432
1434
|
return TEXT_MASK;
|
|
1433
1435
|
});
|
|
1434
1436
|
opaqueCandidatePattern.lastIndex = 0;
|
|
1435
|
-
text2 = text2.replace(opaqueCandidatePattern, (match, offset,
|
|
1437
|
+
text2 = text2.replace(opaqueCandidatePattern, (match, offset, whole2) => {
|
|
1436
1438
|
const hexLike = uuidPattern.test(match) || longHexPattern.test(match);
|
|
1437
1439
|
if (!hexLike && !looksRandom3(match)) return match;
|
|
1438
1440
|
if (!strict && !credentialContext) {
|
|
1439
|
-
if (hexLike && identifierLabelPattern.test(
|
|
1441
|
+
if (hexLike && identifierLabelPattern.test(whole2.slice(Math.max(0, offset - 16), offset))) return match;
|
|
1440
1442
|
const window = hexLike ? HEX_NEARBY_CHARACTERS : NEARBY_CHARACTERS;
|
|
1441
|
-
const nearby =
|
|
1443
|
+
const nearby = whole2.slice(Math.max(0, offset - window), offset) + " " + whole2.slice(offset + match.length, offset + match.length + window);
|
|
1442
1444
|
if (!credentialProximityPattern.test(nearby)) return match;
|
|
1443
1445
|
}
|
|
1444
1446
|
count(match);
|
|
@@ -1461,8 +1463,8 @@ var require_redaction = __commonJS({
|
|
|
1461
1463
|
return value.length >= 16 && (looksRandom3(value, 16) || uuidPattern.test(value) || /^[0-9a-f]{16,}$/i.test(value));
|
|
1462
1464
|
}
|
|
1463
1465
|
function redactPairs(value, onMask) {
|
|
1464
|
-
return value.replace(/([^&#?=/]+)=([^&#]*)/g, (
|
|
1465
|
-
if (!sensitiveUrlValue(key, rawValue)) return
|
|
1466
|
+
return value.replace(/([^&#?=/]+)=([^&#]*)/g, (whole2, key, rawValue) => {
|
|
1467
|
+
if (!sensitiveUrlValue(key, rawValue)) return whole2;
|
|
1466
1468
|
onMask(decodeComponent(rawValue));
|
|
1467
1469
|
return `${key}=${URL_MASK}`;
|
|
1468
1470
|
});
|
|
@@ -1556,6 +1558,9 @@ var require_redaction = __commonJS({
|
|
|
1556
1558
|
CREDENTIAL_CONTEXT_SOURCE,
|
|
1557
1559
|
PLACEHOLDER_SOURCE,
|
|
1558
1560
|
MASKED_TEXT_SOURCE,
|
|
1561
|
+
DIGIT_CODE_SOURCE,
|
|
1562
|
+
CODE_WORD_SOURCE,
|
|
1563
|
+
CODE_NEARBY_CHARACTERS,
|
|
1559
1564
|
SECRET_SCREEN_MESSAGE,
|
|
1560
1565
|
hasCredentialWord: hasCredentialWord2,
|
|
1561
1566
|
looksRandom: looksRandom3,
|
|
@@ -1807,8 +1812,8 @@ var require_linkOnly = __commonJS({
|
|
|
1807
1812
|
if (url.username || url.password || url.port || url.hash) return { error: "has user info, a port or a fragment" };
|
|
1808
1813
|
if (url.hostname.includes(MARK) || !hostOnSite(hostOfUrl2(url), patterns)) return { error: "is not on the site's own hosts" };
|
|
1809
1814
|
const placeholderIn = (text2) => {
|
|
1810
|
-
const
|
|
1811
|
-
if (
|
|
1815
|
+
const whole2 = new RegExp(`^${MARK}(\\d+)x$`).exec(text2);
|
|
1816
|
+
if (whole2) return { name: names[Number(whole2[1])] };
|
|
1812
1817
|
return text2.includes(MARK) ? void 0 : null;
|
|
1813
1818
|
};
|
|
1814
1819
|
const segments = [];
|
|
@@ -1965,8 +1970,8 @@ var require_linkOnly = __commonJS({
|
|
|
1965
1970
|
for (const run of decoded(text2).split(HOST_BREAK)) {
|
|
1966
1971
|
if (names.size > MAX_EMBEDDED_NAMES) break;
|
|
1967
1972
|
if (!run) continue;
|
|
1968
|
-
const
|
|
1969
|
-
if (
|
|
1973
|
+
const whole2 = embeddedName(run);
|
|
1974
|
+
if (whole2) names.add(whole2);
|
|
1970
1975
|
for (const piece of run.split(NOT_NAME_CHARACTER)) {
|
|
1971
1976
|
const name = piece && piece !== run ? embeddedName(piece) : null;
|
|
1972
1977
|
if (name) names.add(name);
|
|
@@ -6795,7 +6800,9 @@ async function tailTunnelLogs(options) {
|
|
|
6795
6800
|
|
|
6796
6801
|
// src/preview.ts
|
|
6797
6802
|
import { spawn as spawn6 } from "node:child_process";
|
|
6803
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
6798
6804
|
import fs12 from "node:fs";
|
|
6805
|
+
import net5 from "node:net";
|
|
6799
6806
|
import os7 from "node:os";
|
|
6800
6807
|
import path13 from "node:path";
|
|
6801
6808
|
import kleur8 from "kleur";
|
|
@@ -7110,6 +7117,7 @@ var MAX_HEADER_BYTES = 128 * 1024;
|
|
|
7110
7117
|
|
|
7111
7118
|
// src/web-read.ts
|
|
7112
7119
|
var redaction = __toESM(require_redaction(), 1);
|
|
7120
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
7113
7121
|
import dns2 from "node:dns";
|
|
7114
7122
|
import http3 from "node:http";
|
|
7115
7123
|
import https from "node:https";
|
|
@@ -8110,12 +8118,109 @@ function loadWebFixtures(folder) {
|
|
|
8110
8118
|
};
|
|
8111
8119
|
}
|
|
8112
8120
|
|
|
8121
|
+
// src/web-budget.ts
|
|
8122
|
+
var ResearchBudgetError = class extends Error {
|
|
8123
|
+
};
|
|
8124
|
+
function invalidResponse() {
|
|
8125
|
+
throw new ResearchBudgetError("The controller returned an invalid research allowance. No further pages were loaded.");
|
|
8126
|
+
}
|
|
8127
|
+
function object(value) {
|
|
8128
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8129
|
+
}
|
|
8130
|
+
function whole(value, min, max) {
|
|
8131
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= min && value <= max;
|
|
8132
|
+
}
|
|
8133
|
+
function readBudget(value) {
|
|
8134
|
+
if (!object(value) || value.scope !== "this task" || !whole(value.loadsPerTurn, 1, 200) || !whole(value.loadsPerHost, 1, value.loadsPerTurn) || !whole(value.loadsUsed, 0, value.loadsPerTurn) || value.loadsLeft !== value.loadsPerTurn - value.loadsUsed || value.hostSpacingSeconds !== 2 || !object(value.hosts) || Object.keys(value.hosts).length > 1200) invalidResponse();
|
|
8135
|
+
for (const entry of Object.values(value.hosts)) {
|
|
8136
|
+
if (!object(entry) || !whole(entry.loads, 0, value.loadsPerHost) || entry.lastAt !== void 0 && (typeof entry.lastAt !== "number" || !Number.isFinite(entry.lastAt))) invalidResponse();
|
|
8137
|
+
}
|
|
8138
|
+
return value;
|
|
8139
|
+
}
|
|
8140
|
+
function sharedResearchBudget(env, fetcher = globalThis.fetch) {
|
|
8141
|
+
const enabled = env.INSTAFY_RESEARCH_SHARED?.trim();
|
|
8142
|
+
const rawUrl = env.INSTAFY_RESEARCH_CONTROLLER_URL?.trim();
|
|
8143
|
+
const token = env.INSTAFY_RESEARCH_ACCESS_TOKEN?.trim();
|
|
8144
|
+
if (enabled === void 0 && rawUrl === void 0 && token === void 0) return null;
|
|
8145
|
+
if (enabled !== "1" || !rawUrl || !token || /[\r\n]/.test(token)) {
|
|
8146
|
+
throw new ResearchBudgetError("This task requires a shared research allowance, but its runtime binding is incomplete. No pages were loaded.");
|
|
8147
|
+
}
|
|
8148
|
+
let url;
|
|
8149
|
+
try {
|
|
8150
|
+
url = new URL(rawUrl);
|
|
8151
|
+
if (!/^https?:$/.test(url.protocol) || url.username || url.password || url.search || url.hash || url.pathname !== "/") throw new Error();
|
|
8152
|
+
} catch {
|
|
8153
|
+
throw new ResearchBudgetError("The task's research controller binding is invalid. No pages were loaded.");
|
|
8154
|
+
}
|
|
8155
|
+
return new SharedResearchBudget(new URL("/agent/research-budget", url.origin), token, fetcher);
|
|
8156
|
+
}
|
|
8157
|
+
var SharedResearchBudget = class {
|
|
8158
|
+
constructor(endpoint, token, fetcher) {
|
|
8159
|
+
this.endpoint = endpoint;
|
|
8160
|
+
this.token = token;
|
|
8161
|
+
this.fetcher = fetcher;
|
|
8162
|
+
this.current = null;
|
|
8163
|
+
}
|
|
8164
|
+
async request(body, remainingMs = 5e3) {
|
|
8165
|
+
const deadline = Date.now() + Math.max(1, Math.min(5e3, body?.remainingMs ?? remainingMs));
|
|
8166
|
+
for (let attempt2 = 0; attempt2 < 2; attempt2 += 1) {
|
|
8167
|
+
const abort = new AbortController();
|
|
8168
|
+
const timer = setTimeout(() => abort.abort(), Math.max(1, deadline - Date.now()));
|
|
8169
|
+
try {
|
|
8170
|
+
let response;
|
|
8171
|
+
try {
|
|
8172
|
+
response = await this.fetcher(this.endpoint.href, {
|
|
8173
|
+
method: body ? "POST" : "GET",
|
|
8174
|
+
headers: {
|
|
8175
|
+
Authorization: `Bearer ${this.token}`,
|
|
8176
|
+
Accept: "application/json",
|
|
8177
|
+
...body ? { "Content-Type": "application/json" } : {}
|
|
8178
|
+
},
|
|
8179
|
+
...body ? { body: JSON.stringify(body) } : {},
|
|
8180
|
+
redirect: "error",
|
|
8181
|
+
signal: abort.signal
|
|
8182
|
+
});
|
|
8183
|
+
} catch {
|
|
8184
|
+
if (attempt2 === 0 && !abort.signal.aborted && Date.now() < deadline) continue;
|
|
8185
|
+
throw new ResearchBudgetError("The task's shared research allowance is unavailable. No further pages were loaded; do not switch to a local allowance.");
|
|
8186
|
+
}
|
|
8187
|
+
if (!response.ok) {
|
|
8188
|
+
throw new ResearchBudgetError(`The controller refused the shared research allowance (HTTP ${response.status}). No further pages were loaded.`);
|
|
8189
|
+
}
|
|
8190
|
+
let value;
|
|
8191
|
+
try {
|
|
8192
|
+
value = await response.json();
|
|
8193
|
+
} catch {
|
|
8194
|
+
invalidResponse();
|
|
8195
|
+
}
|
|
8196
|
+
if (!object(value)) invalidResponse();
|
|
8197
|
+
return value;
|
|
8198
|
+
} finally {
|
|
8199
|
+
clearTimeout(timer);
|
|
8200
|
+
}
|
|
8201
|
+
}
|
|
8202
|
+
throw new ResearchBudgetError("The task's shared research allowance is unavailable.");
|
|
8203
|
+
}
|
|
8204
|
+
async status(remainingMs = 5e3) {
|
|
8205
|
+
const response = await this.request(void 0, remainingMs);
|
|
8206
|
+
this.current = readBudget(response.budget);
|
|
8207
|
+
return this.current;
|
|
8208
|
+
}
|
|
8209
|
+
async reserve(loadId, requestId, host, remainingMs) {
|
|
8210
|
+
const response = await this.request({ loadId, requestId, host, remainingMs: Math.max(0, Math.min(6e5, Math.trunc(remainingMs))) });
|
|
8211
|
+
const budget = readBudget(response.budget);
|
|
8212
|
+
if (typeof response.allowed !== "boolean" || !whole(response.waitMs, 0, 6e5) || (response.allowed ? response.code !== void 0 : !["turn_limit", "host_limit", "time_limit"].includes(String(response.code)))) invalidResponse();
|
|
8213
|
+
this.current = budget;
|
|
8214
|
+
return { allowed: response.allowed, ...response.code ? { code: response.code } : {}, waitMs: response.waitMs, budget };
|
|
8215
|
+
}
|
|
8216
|
+
};
|
|
8217
|
+
|
|
8113
8218
|
// src/web-read.ts
|
|
8114
8219
|
var ROBOTS_AGENTS = ["*", "instafy-agent", ...AI_AGENTS];
|
|
8115
8220
|
var LIMITS = Object.freeze({
|
|
8116
8221
|
urlsPerCall: 6,
|
|
8117
|
-
loadsPerTurn:
|
|
8118
|
-
loadsPerHost:
|
|
8222
|
+
loadsPerTurn: 100,
|
|
8223
|
+
loadsPerHost: 25,
|
|
8119
8224
|
hostSpacingMs: 2e3,
|
|
8120
8225
|
callMs: 6e4,
|
|
8121
8226
|
pageTimeoutMs: 2e4,
|
|
@@ -8377,6 +8482,7 @@ var TELL = {
|
|
|
8377
8482
|
http_error: "The site answered with an error. Use another source rather than retrying.",
|
|
8378
8483
|
unreachable: "The site did not answer. Use another source rather than retrying.",
|
|
8379
8484
|
timeout: "The site did not answer in time. Use another source rather than retrying.",
|
|
8485
|
+
budget_unavailable: "Research stopped because its shared allowance could not be checked. Keep the evidence already collected; further fresh reads require controller approval.",
|
|
8380
8486
|
needs_browser: "This page builds its text with JavaScript, so a plain read sees almost nothing. Use another source, or ask for a visible browser turn for this page.",
|
|
8381
8487
|
no_reuse: `This site's terms bar reusing its content. Summarize it in your own words, cite "seen <date> on <host>" and link back; never copy its reviews, photos or price lists, and keep its content out of anything published or shared by public link (links only there).`
|
|
8382
8488
|
};
|
|
@@ -8499,7 +8605,7 @@ async function readOne(rawUrl, options, ctx) {
|
|
|
8499
8605
|
status: "skipped",
|
|
8500
8606
|
code,
|
|
8501
8607
|
message,
|
|
8502
|
-
tell: TELL[code]
|
|
8608
|
+
tell: ctx.sharedBudget?.current && code !== "time_limit" ? code === "turn_limit" ? `This task's shared allowance of ${ctx.sharedBudget.current.loadsPerTurn} page loads is used. Work with the evidence already collected, or ask the person.` : `This task has used its ${ctx.sharedBudget.current.loadsPerHost} page loads for this site. Use another source.` : TELL[code]
|
|
8503
8609
|
});
|
|
8504
8610
|
let sibling = wwwSibling(requestedUrl);
|
|
8505
8611
|
const fail = (code, message, extra2 = {}) => ({
|
|
@@ -8531,15 +8637,24 @@ async function readOne(rawUrl, options, ctx) {
|
|
|
8531
8637
|
return present(page, cached, true, options, ctx.guard);
|
|
8532
8638
|
}
|
|
8533
8639
|
}
|
|
8534
|
-
if (
|
|
8535
|
-
return skip("
|
|
8536
|
-
|
|
8537
|
-
|
|
8538
|
-
|
|
8640
|
+
if (ctx.sharedBudget) {
|
|
8641
|
+
if (ctx.deadline - ctx.now() < 1e3) return skip("time_limit", "this call reached its time limit before this URL");
|
|
8642
|
+
const budget = await ctx.sharedBudget.status(ctx.deadline - ctx.now());
|
|
8643
|
+
if (budget.loadsLeft === 0) return skip("turn_limit", `this task already loaded ${budget.loadsUsed} pages (limit ${budget.loadsPerTurn})`);
|
|
8644
|
+
const used = budget.hosts[firstSite]?.loads ?? 0;
|
|
8645
|
+
if (used >= budget.loadsPerHost) return skip("host_limit", `this task already read ${firstSite} ${used} times (limit ${budget.loadsPerHost})`);
|
|
8646
|
+
} else {
|
|
8647
|
+
if (earlier.loads >= LIMITS.loadsPerTurn) {
|
|
8648
|
+
return skip("turn_limit", `this turn already loaded ${earlier.loads} pages (limit ${LIMITS.loadsPerTurn})`);
|
|
8649
|
+
}
|
|
8650
|
+
if (earlier.hostLoads >= LIMITS.loadsPerHost) {
|
|
8651
|
+
return skip("host_limit", `this turn already read ${firstSite} ${earlier.hostLoads} times (limit ${LIMITS.loadsPerHost})`);
|
|
8652
|
+
}
|
|
8539
8653
|
}
|
|
8540
8654
|
if (ctx.deadline - ctx.now() < 1e3) return skip("time_limit", "this call reached its time limit before this URL");
|
|
8541
8655
|
const redirects = [];
|
|
8542
8656
|
const chainSites = /* @__PURE__ */ new Set();
|
|
8657
|
+
const loadId = randomUUID2();
|
|
8543
8658
|
let current = new URL(requested);
|
|
8544
8659
|
let response = null;
|
|
8545
8660
|
for (let hop = 0; ; hop += 1) {
|
|
@@ -8564,7 +8679,21 @@ async function readOne(rawUrl, options, ctx) {
|
|
|
8564
8679
|
const robots = await robotsVerdict(current, ctx);
|
|
8565
8680
|
if ("failed" in robots) return fail(robots.failed, via + robots.message, hopExtra);
|
|
8566
8681
|
if (robots.refused) return stop("robots", via + robots.refused, host2, hopExtra);
|
|
8567
|
-
if (
|
|
8682
|
+
if (ctx.sharedBudget) {
|
|
8683
|
+
const remainingMs = ctx.deadline - ctx.now();
|
|
8684
|
+
if (remainingMs < 1e3) return skip("time_limit", `${via}this call reached its time limit before reserving a load`, hopExtra);
|
|
8685
|
+
const reservation = await ctx.sharedBudget.reserve(loadId, randomUUID2(), site, remainingMs - 1e3);
|
|
8686
|
+
if (!reservation.allowed) {
|
|
8687
|
+
const code = reservation.code;
|
|
8688
|
+
const budget = reservation.budget;
|
|
8689
|
+
const message = code === "turn_limit" ? `this task already loaded ${budget.loadsUsed} pages (limit ${budget.loadsPerTurn})` : code === "host_limit" ? `this task already read ${site} ${budget.hosts[site]?.loads ?? 0} times (limit ${budget.loadsPerHost})` : `no time left in this call for the next paced read of ${host2}`;
|
|
8690
|
+
return skip(code, via + message, hopExtra);
|
|
8691
|
+
}
|
|
8692
|
+
if (reservation.waitMs + 1e3 > ctx.deadline - ctx.now()) {
|
|
8693
|
+
return skip("time_limit", `${via}this call reached its time limit while reserving the next paced read`, hopExtra);
|
|
8694
|
+
}
|
|
8695
|
+
if (reservation.waitMs > 0) await ctx.sleep(reservation.waitMs);
|
|
8696
|
+
} else if (!chainSites.has(site)) {
|
|
8568
8697
|
const now = ctx.now();
|
|
8569
8698
|
const reservation = await ctx.turn.update((state) => {
|
|
8570
8699
|
if (hop === 0 && state.loads >= LIMITS.loadsPerTurn) {
|
|
@@ -8775,6 +8904,7 @@ async function webRead(options, deps = {}) {
|
|
|
8775
8904
|
now,
|
|
8776
8905
|
sleep: deps.sleep ?? realSleep,
|
|
8777
8906
|
turn,
|
|
8907
|
+
sharedBudget: sharedResearchBudget(env, deps.budgetFetch),
|
|
8778
8908
|
cache: new Cache(cacheDir === null ? null : apart(cacheDir)),
|
|
8779
8909
|
guard,
|
|
8780
8910
|
viewer,
|
|
@@ -8782,32 +8912,38 @@ async function webRead(options, deps = {}) {
|
|
|
8782
8912
|
robotsMemo: /* @__PURE__ */ new Map()
|
|
8783
8913
|
};
|
|
8784
8914
|
const pages = [];
|
|
8785
|
-
|
|
8786
|
-
|
|
8915
|
+
let interruptedBudget = null;
|
|
8916
|
+
for (const [index, url] of urls.entries()) {
|
|
8917
|
+
let page;
|
|
8918
|
+
try {
|
|
8919
|
+
page = await readOne(url, { maxChars, links, fresh: Boolean(options.fresh), terms }, ctx);
|
|
8920
|
+
} catch (error) {
|
|
8921
|
+
if (!(error instanceof ResearchBudgetError) || !ctx.sharedBudget?.current || !pages.length) throw error;
|
|
8922
|
+
interruptedBudget = ctx.sharedBudget.current;
|
|
8923
|
+
pages.push(...urls.slice(index).map((remaining) => ({
|
|
8924
|
+
...basePage(remaining, new Date(now()).toISOString(), viewer),
|
|
8925
|
+
code: "budget_unavailable",
|
|
8926
|
+
message: error.message,
|
|
8927
|
+
tell: TELL.budget_unavailable
|
|
8928
|
+
})));
|
|
8929
|
+
break;
|
|
8930
|
+
}
|
|
8787
8931
|
if (page.status === "failed" && unanswered(page.code, page)) {
|
|
8788
8932
|
const key = unansweredKey(new URL(url));
|
|
8789
|
-
await turn.update((
|
|
8790
|
-
(
|
|
8933
|
+
await turn.update((state) => {
|
|
8934
|
+
(state.unanswered ??= {})[key] = now();
|
|
8791
8935
|
});
|
|
8792
8936
|
}
|
|
8793
8937
|
pages.push(page);
|
|
8794
8938
|
}
|
|
8795
|
-
const
|
|
8939
|
+
const budget = interruptedBudget ? lastKnownBudget(interruptedBudget, warnings) : ctx.sharedBudget ? await finalSharedBudget(ctx.sharedBudget, deadline - now(), warnings) : await localBudget(turn, scope.label);
|
|
8796
8940
|
return {
|
|
8797
8941
|
ok: true,
|
|
8798
8942
|
viewer,
|
|
8799
8943
|
userAgent: USER_AGENT,
|
|
8800
8944
|
notice: NOTICE,
|
|
8801
8945
|
pages,
|
|
8802
|
-
budget
|
|
8803
|
-
scope: scope.label,
|
|
8804
|
-
loadsUsed: state.loads,
|
|
8805
|
-
loadsLeft: Math.max(0, LIMITS.loadsPerTurn - state.loads),
|
|
8806
|
-
loadsPerTurn: LIMITS.loadsPerTurn,
|
|
8807
|
-
loadsPerHost: LIMITS.loadsPerHost,
|
|
8808
|
-
hostSpacingSeconds: LIMITS.hostSpacingMs / 1e3,
|
|
8809
|
-
hosts: state.hosts
|
|
8810
|
-
},
|
|
8946
|
+
budget,
|
|
8811
8947
|
guards: {
|
|
8812
8948
|
linkOnly: {
|
|
8813
8949
|
sites: guard.list.sites.length,
|
|
@@ -8821,6 +8957,63 @@ async function webRead(options, deps = {}) {
|
|
|
8821
8957
|
warnings
|
|
8822
8958
|
};
|
|
8823
8959
|
}
|
|
8960
|
+
function reportBudget(budget) {
|
|
8961
|
+
return { ...budget, hosts: Object.fromEntries(Object.entries(budget.hosts).map(([host, value]) => [host, value.loads])) };
|
|
8962
|
+
}
|
|
8963
|
+
function lastKnownBudget(budget, warnings) {
|
|
8964
|
+
warnings.push("The shared research allowance could not be refreshed. Budget counts are the last validated controller snapshot and may have changed; another fresh read still requires controller approval.");
|
|
8965
|
+
return { ...reportBudget(budget), lastKnown: true };
|
|
8966
|
+
}
|
|
8967
|
+
async function finalSharedBudget(shared, remainingMs, warnings) {
|
|
8968
|
+
try {
|
|
8969
|
+
if (remainingMs <= 0) throw new ResearchBudgetError("The call deadline elapsed before the shared research allowance could be refreshed.");
|
|
8970
|
+
return reportBudget(await shared.status(remainingMs));
|
|
8971
|
+
} catch (error) {
|
|
8972
|
+
if (!(error instanceof ResearchBudgetError) || !shared.current) throw error;
|
|
8973
|
+
return lastKnownBudget(shared.current, warnings);
|
|
8974
|
+
}
|
|
8975
|
+
}
|
|
8976
|
+
async function localBudget(turn, scope) {
|
|
8977
|
+
return turn.update((state) => ({
|
|
8978
|
+
scope,
|
|
8979
|
+
loadsUsed: state.loads,
|
|
8980
|
+
loadsLeft: Math.max(0, LIMITS.loadsPerTurn - state.loads),
|
|
8981
|
+
loadsPerTurn: LIMITS.loadsPerTurn,
|
|
8982
|
+
loadsPerHost: LIMITS.loadsPerHost,
|
|
8983
|
+
hostSpacingSeconds: LIMITS.hostSpacingMs / 1e3,
|
|
8984
|
+
hosts: Object.fromEntries(Object.entries(state.hosts).map(([host, value]) => [host, value.loads]))
|
|
8985
|
+
}));
|
|
8986
|
+
}
|
|
8987
|
+
async function webBudget(deps = {}) {
|
|
8988
|
+
const env = deps.env ?? process.env;
|
|
8989
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
8990
|
+
const shared = sharedResearchBudget(env, deps.budgetFetch);
|
|
8991
|
+
if (shared) return { budget: reportBudget(await shared.status()) };
|
|
8992
|
+
const fixtureFolder = webFixturesFolder(env, cwd);
|
|
8993
|
+
const fixtures = fixtureFolder ? loadWebFixtures(fixtureFolder) : null;
|
|
8994
|
+
const dirs = webDirs("web-read", env, cwd);
|
|
8995
|
+
const stateDir = deps.stateDir ?? dirs.stateDir;
|
|
8996
|
+
const scope = turnScope(env);
|
|
8997
|
+
const turn = new TurnStore(
|
|
8998
|
+
fixtures ? path11.join(stateDir, "fixtures", sha256(fixtures.folder).slice(0, 16)) : stateDir,
|
|
8999
|
+
scope,
|
|
9000
|
+
deps.now ?? Date.now
|
|
9001
|
+
);
|
|
9002
|
+
return { budget: await localBudget(turn, scope.label) };
|
|
9003
|
+
}
|
|
9004
|
+
async function runWebBudgetCli(opts, io = { out: console.log, err: console.error }, deps = {}) {
|
|
9005
|
+
try {
|
|
9006
|
+
const report = await webBudget(deps);
|
|
9007
|
+
const b = report.budget;
|
|
9008
|
+
io.out(opts.json ? JSON.stringify(report, null, 1) : `${b.loadsLeft} page loads left in ${b.scope} (${b.loadsUsed} of ${b.loadsPerTurn} used; ${b.loadsPerHost} per site, ${b.hostSpacingSeconds} s apart).`);
|
|
9009
|
+
return WEB_READ_EXIT_OK;
|
|
9010
|
+
} catch (error) {
|
|
9011
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9012
|
+
if (opts.json) io.out(JSON.stringify({ ok: false, exitCode: WEB_READ_EXIT_INTERNAL, error: message }));
|
|
9013
|
+
else io.err(message);
|
|
9014
|
+
return WEB_READ_EXIT_INTERNAL;
|
|
9015
|
+
}
|
|
9016
|
+
}
|
|
8824
9017
|
function clamp(value, min, max) {
|
|
8825
9018
|
return Math.min(max, Math.max(min, Math.trunc(value)));
|
|
8826
9019
|
}
|
|
@@ -8848,13 +9041,13 @@ function formatWebRead(report) {
|
|
|
8848
9041
|
}
|
|
8849
9042
|
out.push("");
|
|
8850
9043
|
}
|
|
8851
|
-
out.push(`${report.budget.loadsUsed} of ${report.budget.loadsPerTurn} page loads used in ${report.budget.scope}.`);
|
|
9044
|
+
out.push(`${report.budget.lastKnown ? "Last known controller count: " : ""}${report.budget.loadsUsed} of ${report.budget.loadsPerTurn} page loads used in ${report.budget.scope}.`);
|
|
8852
9045
|
for (const warning of report.warnings) out.push(`warning: ${warning}`);
|
|
8853
9046
|
out.push(report.notice);
|
|
8854
9047
|
return out.join("\n");
|
|
8855
9048
|
}
|
|
8856
9049
|
async function runWebReadCli(urls, opts, io = { out: console.log, err: console.error }, deps = {}) {
|
|
8857
|
-
const
|
|
9050
|
+
const whole2 = (raw, name, min, max) => {
|
|
8858
9051
|
if (raw === void 0) return void 0;
|
|
8859
9052
|
const n = Number(raw);
|
|
8860
9053
|
if (!Number.isInteger(n) || n < min || n > max) throw new WebReadUsageError(`${name} must be a whole number from ${min} to ${max}.`);
|
|
@@ -8865,8 +9058,8 @@ async function runWebReadCli(urls, opts, io = { out: console.log, err: console.e
|
|
|
8865
9058
|
{
|
|
8866
9059
|
urls,
|
|
8867
9060
|
find: opts.find,
|
|
8868
|
-
maxChars:
|
|
8869
|
-
links:
|
|
9061
|
+
maxChars: whole2(opts.maxChars, "--max-chars", LIMITS.minChars, LIMITS.maxChars),
|
|
9062
|
+
links: whole2(opts.links, "--links", 0, LIMITS.maxLinks),
|
|
8870
9063
|
fresh: opts.fresh
|
|
8871
9064
|
},
|
|
8872
9065
|
deps
|
|
@@ -8876,7 +9069,7 @@ async function runWebReadCli(urls, opts, io = { out: console.log, err: console.e
|
|
|
8876
9069
|
} catch (error) {
|
|
8877
9070
|
const usage = error instanceof WebReadUsageError;
|
|
8878
9071
|
const code = usage ? WEB_READ_EXIT_USAGE : WEB_READ_EXIT_INTERNAL;
|
|
8879
|
-
const message = usage || error instanceof LinkOnlyListUnavailableError || error instanceof WebFixturesError ? error.message : `web read failed
|
|
9072
|
+
const message = usage || error instanceof LinkOnlyListUnavailableError || error instanceof WebFixturesError || error instanceof ResearchBudgetError ? error.message : `web read failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
8880
9073
|
if (opts.json) io.out(JSON.stringify({ ok: false, exitCode: code, error: message }));
|
|
8881
9074
|
else io.err(message);
|
|
8882
9075
|
return code;
|
|
@@ -8887,10 +9080,13 @@ function registerWebCommand(program2) {
|
|
|
8887
9080
|
"Read public web pages as untrusted data with their source (robots.txt, link-only sites and stop rules enforced), search for links to read (Octo's turns only), and give the person links to link-only sites (guided mode)"
|
|
8888
9081
|
);
|
|
8889
9082
|
web.command("read").description(
|
|
8890
|
-
`Read 1-6 public pages over plain HTTP and print their text, links and structured data with provenance (exit 2: nothing read, 64: bad options).
|
|
9083
|
+
`Read 1-6 public pages over plain HTTP and print their text, links and structured data with provenance (exit 2: nothing read, 64: bad options). Uses the task's shared allowance; standalone: ${LIMITS.loadsPerTurn} loads, ${LIMITS.loadsPerHost} per site, ${LIMITS.hostSpacingMs / 1e3} s apart. Stopped URLs (login, CAPTCHA, paywall, 401/403/429, download, secret screen, robots.txt, link-only, private address) are never read another way.`
|
|
8891
9084
|
).argument("<url...>", "http(s) URLs to read").option("--find <terms>", 'Return only the passages that mention these words ("quoted phrases" kept whole)').option("--max-chars <n>", `Most characters of text per page (default ${LIMITS.defaultChars}, at most ${LIMITS.maxChars})`).option("--links <n>", `Most links per page (default ${LIMITS.defaultLinks}, at most ${LIMITS.maxLinks})`).option("--fresh", "Read again even if this workspace's cache has the page (never re-reads a stopped URL)").option("--json", "Output JSON").action(async (urls, opts) => {
|
|
8892
9085
|
process.exitCode = await runWebReadCli(urls, opts);
|
|
8893
9086
|
});
|
|
9087
|
+
web.command("budget").description("Show this task's remaining shared page allowance without spending it (or the standalone session allowance)").option("--json", "Output JSON").action(async (opts) => {
|
|
9088
|
+
process.exitCode = await runWebBudgetCli(opts);
|
|
9089
|
+
});
|
|
8894
9090
|
return web;
|
|
8895
9091
|
}
|
|
8896
9092
|
|
|
@@ -9314,6 +9510,18 @@ async function readPageDescription(frame) {
|
|
|
9314
9510
|
const raw = await frame.evaluate(() => document.querySelector('meta[name="description" i]')?.getAttribute("content") ?? null).catch(() => null);
|
|
9315
9511
|
return cleanDescription(raw);
|
|
9316
9512
|
}
|
|
9513
|
+
var READY_MARK_ATTRIBUTE = "data-instafy-ready";
|
|
9514
|
+
var READY_MARK_WAIT_MS = 3e4;
|
|
9515
|
+
async function waitForReadyMark(frame, timeoutMs) {
|
|
9516
|
+
const mark = await frame.evaluate((name) => document.documentElement?.getAttribute(name) ?? null, READY_MARK_ATTRIBUTE).catch(() => null);
|
|
9517
|
+
if (mark === null) return "none";
|
|
9518
|
+
if (mark === "true") return "ready";
|
|
9519
|
+
if (timeoutMs <= 0) return "late";
|
|
9520
|
+
return frame.waitForFunction((name) => document.documentElement?.getAttribute(name) === "true", READY_MARK_ATTRIBUTE, {
|
|
9521
|
+
timeout: timeoutMs,
|
|
9522
|
+
polling: 100
|
|
9523
|
+
}).then(() => "ready").catch(() => "late");
|
|
9524
|
+
}
|
|
9317
9525
|
async function pageThumbnail(page, context) {
|
|
9318
9526
|
const cdp = await context.newCDPSession(page);
|
|
9319
9527
|
try {
|
|
@@ -9519,10 +9727,19 @@ async function checkViewport(browser, target, vp, siteOrigin, outDir, timeoutMs,
|
|
|
9519
9727
|
(ours(u) ? errors : warnings).push(`HTTP ${s}: ${shortUrl(u)}`);
|
|
9520
9728
|
});
|
|
9521
9729
|
await page.goto(target.pageUrl, { waitUntil: "load", timeout: timeoutMs });
|
|
9522
|
-
|
|
9523
|
-
|
|
9524
|
-
|
|
9525
|
-
|
|
9730
|
+
const siteFrame = () => target.framed ? page.frames().find((f) => f !== page.mainFrame() && isSiteUrl(String(f.url()), siteOrigin)) : page.mainFrame();
|
|
9731
|
+
const readiness = await waitForReadyMark(
|
|
9732
|
+
siteFrame() ?? page.mainFrame(),
|
|
9733
|
+
Math.max(0, Math.min(READY_MARK_WAIT_MS, timeoutMs) - (Date.now() - t0))
|
|
9734
|
+
);
|
|
9735
|
+
if (readiness === "none") {
|
|
9736
|
+
await page.waitForLoadState("networkidle", { timeout: Math.min(8e3, timeoutMs) }).catch(() => {
|
|
9737
|
+
warnings.push("the network was still busy after 8 s (screenshot taken anyway)");
|
|
9738
|
+
});
|
|
9739
|
+
} else if (readiness === "late") {
|
|
9740
|
+
warnings.push(`the page still said it was drawing (${READY_MARK_ATTRIBUTE}) after ${Math.round((Date.now() - t0) / 1e3)} s; screenshot taken anyway`);
|
|
9741
|
+
}
|
|
9742
|
+
const frame = siteFrame();
|
|
9526
9743
|
let framed = "ok";
|
|
9527
9744
|
let snap = { text: 0, media: 0, elements: 0, title: "" };
|
|
9528
9745
|
if (refused) framed = "refused";
|
|
@@ -9557,7 +9774,7 @@ async function checkViewport(browser, target, vp, siteOrigin, outDir, timeoutMs,
|
|
|
9557
9774
|
let thumbnail;
|
|
9558
9775
|
if (takeThumbnail) {
|
|
9559
9776
|
const [firstUnstopped] = unstopped;
|
|
9560
|
-
thumbnail = firstUnstopped ? { skipped: `a redirect led to ${firstUnstopped[0].slice(0, 200)} (${firstUnstopped[1]}), which a picture never shows` } : blank.blank ? { skipped: "the page looked blank" } : await pageThumbnail(page, context).then(async (shot) => "image" in shot ? { image: shot.image, description: await readPageDescription(page.mainFrame()) } : shot).catch((error) => ({ skipped: `the picture could not be taken: ${firstLine(error)}` }));
|
|
9777
|
+
thumbnail = firstUnstopped ? { skipped: `a redirect led to ${firstUnstopped[0].slice(0, 200)} (${firstUnstopped[1]}), which a picture never shows` } : blank.blank ? { skipped: "the page looked blank" } : readiness === "late" ? { skipped: "the page never said it was ready, so the card keeps its picture" } : await pageThumbnail(page, context).then(async (shot) => "image" in shot ? { image: shot.image, description: await readPageDescription(page.mainFrame()) } : shot).catch((error) => ({ skipped: `the picture could not be taken: ${firstLine(error)}` }));
|
|
9561
9778
|
}
|
|
9562
9779
|
return {
|
|
9563
9780
|
name: vp.name,
|
|
@@ -9599,7 +9816,9 @@ async function runSiteCheck(opts) {
|
|
|
9599
9816
|
const port = parsePreviewPort(opts.port, env);
|
|
9600
9817
|
const sitePath = parseCheckPath(opts.path);
|
|
9601
9818
|
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
9602
|
-
const viewports = VIEWPORTS.filter(
|
|
9819
|
+
const viewports = VIEWPORTS.filter(
|
|
9820
|
+
(v) => !opts.phone && !opts.desktop ? true : opts.phone && v.name === "phone" || opts.desktop && v.name === "desktop"
|
|
9821
|
+
);
|
|
9603
9822
|
let outDir = null;
|
|
9604
9823
|
if (opts.outDir !== void 0) {
|
|
9605
9824
|
outDir = path12.resolve(opts.outDir);
|
|
@@ -9713,12 +9932,14 @@ async function runPreviewCheckCli(opts, io = { out: console.log, err: console.er
|
|
|
9713
9932
|
const port = parsePreviewPort(opts.port, env);
|
|
9714
9933
|
const sitePath = parseCheckPath(opts.path);
|
|
9715
9934
|
const wantsThumbnail = Boolean(io.uploadThumbnail && io.hasOpenPreview?.(port, sitePath));
|
|
9935
|
+
const takenAt = Date.now();
|
|
9716
9936
|
const result = await runSiteCheck({
|
|
9717
9937
|
port,
|
|
9718
9938
|
path: opts.path,
|
|
9719
9939
|
thumbnail: wantsThumbnail,
|
|
9720
9940
|
proxyServer: io.proxyServer,
|
|
9721
9941
|
phone: opts.phone,
|
|
9942
|
+
desktop: opts.desktop,
|
|
9722
9943
|
framed: opts.framed,
|
|
9723
9944
|
workspacePlaywright: opts.workspacePlaywright,
|
|
9724
9945
|
outDir: opts.out,
|
|
@@ -9733,7 +9954,9 @@ async function runPreviewCheckCli(opts, io = { out: console.log, err: console.er
|
|
|
9733
9954
|
}
|
|
9734
9955
|
if (wantsThumbnail && result.thumbnail && io.uploadThumbnail) {
|
|
9735
9956
|
const shot = result.thumbnail;
|
|
9736
|
-
|
|
9957
|
+
if ("skipped" in shot) await io.pictureTried?.({ port, path: sitePath, takenAt }).catch(() => {
|
|
9958
|
+
});
|
|
9959
|
+
const outcome = "skipped" in shot ? { status: "skipped", reason: shot.skipped } : await io.uploadThumbnail({ port, path: sitePath, image: shot.image, description: shot.description, takenAt }).catch((error) => ({ status: "failed", reason: firstLine(error) }));
|
|
9737
9960
|
if (outcome) {
|
|
9738
9961
|
result.report.thumbnail = outcome.status;
|
|
9739
9962
|
if (outcome.reason) result.report.thumbnailReason = outcome.reason;
|
|
@@ -9753,7 +9976,7 @@ async function runPreviewCheckCli(opts, io = { out: console.log, err: console.er
|
|
|
9753
9976
|
function registerPreviewCheckCommand(preview, card = {}) {
|
|
9754
9977
|
return preview.command("check").description(
|
|
9755
9978
|
"Load a local site in a headless browser and report its title, errors, failed requests and a blank page; writes desktop and phone screenshots, and refreshes the picture on the card of a preview open on that port (exit 1: problems, 2: nothing answering, 3: port not allowed, 4: no browser, 64: bad options, 70: the check itself failed)"
|
|
9756
|
-
).option("--port <port>", "Local port the site listens on (1024-65535)").option("--path <path>", "Path to load (default /); a ?query and #hash are fine").option("--phone", "Only the phone size (390\xD7844); default: desktop 1280\xD7800 and phone").option("--framed", "Load the site inside a frame on another origin, and report a site that refuses to be framed").option("--workspace-playwright", "Also look for Playwright in this project's node_modules (runs the project's own copy)").option("--out <dir>", "Folder for desktop.png and phone.png (default: a new private folder under the temp folder each run; the report names it)").option("--timeout <ms>", "Page load timeout in ms (default 30000)").option("--wait <ms>", "Extra settle time after load in ms (default 600)").option("--json", "Output JSON").action(async (opts) => {
|
|
9979
|
+
).option("--port <port>", "Local port the site listens on (1024-65535)").option("--path <path>", "Path to load (default /); a ?query and #hash are fine").option("--phone", "Only the phone size (390\xD7844); default: desktop 1280\xD7800 and phone").option("--desktop", "Only the desktop size (1280\xD7800), the one the card's picture is taken from").option("--framed", "Load the site inside a frame on another origin, and report a site that refuses to be framed").option("--workspace-playwright", "Also look for Playwright in this project's node_modules (runs the project's own copy)").option("--out <dir>", "Folder for desktop.png and phone.png (default: a new private folder under the temp folder each run; the report names it)").option("--timeout <ms>", "Page load timeout in ms (default 30000)").option("--wait <ms>", "Extra settle time after load in ms (default 600)").option("--json", "Output JSON").action(async (opts) => {
|
|
9757
9980
|
process.exitCode = await runPreviewCheckCli(opts, { out: console.log, err: console.error, ...card });
|
|
9758
9981
|
});
|
|
9759
9982
|
}
|
|
@@ -9767,6 +9990,8 @@ function thumbnailBudgetMs(env = process.env) {
|
|
|
9767
9990
|
}
|
|
9768
9991
|
var NETWORK_IDLE_MS = 6e3;
|
|
9769
9992
|
var SETTLE_MS = 1e3;
|
|
9993
|
+
var READY_SETTLE_MS = 300;
|
|
9994
|
+
var SHOT_RESERVE_MS = 3e3;
|
|
9770
9995
|
var MAX_THUMBNAIL_FILE_BYTES = 512 * 1024;
|
|
9771
9996
|
var MAX_HTML_BYTES = 512 * 1024;
|
|
9772
9997
|
var MIN_SIDE = 64;
|
|
@@ -9923,7 +10148,8 @@ function errorLine(error) {
|
|
|
9923
10148
|
async function captureSiteThumbnail(options) {
|
|
9924
10149
|
const env = options.env ?? process.env;
|
|
9925
10150
|
const budget = options.budgetMs ?? thumbnailBudgetMs(env);
|
|
9926
|
-
const
|
|
10151
|
+
const began = Date.now();
|
|
10152
|
+
let deadline = began + budget;
|
|
9927
10153
|
const host = options.host === "::1" ? "[::1]" : "127.0.0.1";
|
|
9928
10154
|
const siteOrigin = `http://${host}:${options.port}`;
|
|
9929
10155
|
const left = () => Math.max(0, deadline - Date.now());
|
|
@@ -9985,9 +10211,18 @@ async function captureSiteThumbnail(options) {
|
|
|
9985
10211
|
if (response && response.status() >= 400) {
|
|
9986
10212
|
return { ok: false, status: "skipped", reason: `the page answered HTTP ${response.status()}` };
|
|
9987
10213
|
}
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
10214
|
+
const mark = await waitForReadyMark(page.mainFrame(), 0);
|
|
10215
|
+
if (mark === "none") {
|
|
10216
|
+
await page.waitForLoadState("networkidle", { timeout: Math.max(1, Math.min(NETWORK_IDLE_MS, left())) }).catch(() => {
|
|
10217
|
+
});
|
|
10218
|
+
await page.waitForTimeout(Math.min(SETTLE_MS, left()));
|
|
10219
|
+
} else {
|
|
10220
|
+
deadline = Math.max(deadline, began + (options.readyWaitMs ?? READY_MARK_WAIT_MS));
|
|
10221
|
+
if (mark === "late" && await waitForReadyMark(page.mainFrame(), Math.max(0, left() - SHOT_RESERVE_MS)) !== "ready") {
|
|
10222
|
+
return { ok: false, status: "skipped", reason: `the page still said it was drawing after ${Math.round((Date.now() - began) / 1e3)} s` };
|
|
10223
|
+
}
|
|
10224
|
+
await page.waitForTimeout(Math.min(READY_SETTLE_MS, left()));
|
|
10225
|
+
}
|
|
9991
10226
|
const description = await readPageDescription(page.mainFrame());
|
|
9992
10227
|
const shot = await pageThumbnail(page, context);
|
|
9993
10228
|
if ("skipped" in shot) return { ok: false, status: "skipped", reason: shot.skipped };
|
|
@@ -10002,7 +10237,16 @@ async function captureSiteThumbnail(options) {
|
|
|
10002
10237
|
};
|
|
10003
10238
|
})();
|
|
10004
10239
|
const outOfTime = new Promise((resolve) => {
|
|
10005
|
-
|
|
10240
|
+
const arm = () => {
|
|
10241
|
+
timer = setTimeout(() => {
|
|
10242
|
+
if (Date.now() < deadline) {
|
|
10243
|
+
arm();
|
|
10244
|
+
return;
|
|
10245
|
+
}
|
|
10246
|
+
resolve({ ok: false, status: "failed", reason: `the picture took longer than ${Math.round((deadline - began) / 1e3)} s` });
|
|
10247
|
+
}, Math.max(0, deadline - Date.now()));
|
|
10248
|
+
};
|
|
10249
|
+
arm();
|
|
10006
10250
|
});
|
|
10007
10251
|
try {
|
|
10008
10252
|
return await Promise.race([work.catch((error) => ({ ok: false, status: "failed", reason: errorLine(error) })), outOfTime]);
|
|
@@ -10026,6 +10270,12 @@ var MAX_PATH_LENGTH2 = 512;
|
|
|
10026
10270
|
var THUMBNAIL_UPLOAD_TIMEOUT_MS = 15e3;
|
|
10027
10271
|
var STATE_LOCK_WAIT_MS = 12e4;
|
|
10028
10272
|
var STATE_LOCK_STALE_MS = 10 * 6e4;
|
|
10273
|
+
var PORT_PLACEHOLDER = "{port}";
|
|
10274
|
+
var PORT_TAKEN_HINT = "Run it with --start and without --port, so this site gets a free port of its own, or pass --replace to show this site on that port instead.";
|
|
10275
|
+
var PICKED_PORT_TAKEN_HINT = "Pass --replace if this site replaces that one (this same site under a new title, for example); otherwise give this site a title of its own.";
|
|
10276
|
+
var AUTO_PORT_FIRST = 2e4;
|
|
10277
|
+
var AUTO_PORT_COUNT = 1e4;
|
|
10278
|
+
var AUTO_PORT_TRIES = 64;
|
|
10029
10279
|
function instafyDir() {
|
|
10030
10280
|
return path13.join(os7.homedir(), ".instafy");
|
|
10031
10281
|
}
|
|
@@ -10156,10 +10406,10 @@ function lockHolderIsGone(lockFile) {
|
|
|
10156
10406
|
return false;
|
|
10157
10407
|
}
|
|
10158
10408
|
}
|
|
10159
|
-
async function withStateLock(fn) {
|
|
10409
|
+
async function withStateLock(fn, waitMs = STATE_LOCK_WAIT_MS) {
|
|
10160
10410
|
const lockFile = previewStateLockFile();
|
|
10161
10411
|
fs12.mkdirSync(instafyDir(), { recursive: true });
|
|
10162
|
-
const deadline = Date.now() +
|
|
10412
|
+
const deadline = Date.now() + waitMs;
|
|
10163
10413
|
for (; ; ) {
|
|
10164
10414
|
try {
|
|
10165
10415
|
const fd = fs12.openSync(lockFile, "wx", 384);
|
|
@@ -10249,6 +10499,12 @@ async function controllerRequest(params) {
|
|
|
10249
10499
|
PREVIEW_EXIT_PORT_NOT_ALLOWED
|
|
10250
10500
|
);
|
|
10251
10501
|
}
|
|
10502
|
+
if (response.status === 409 && code === "port_in_use") {
|
|
10503
|
+
throw new PreviewCommandError(
|
|
10504
|
+
`${extractControllerErrorMessage(text2) ?? "Another site's preview holds this port."} ${params.portTakenHint ?? PORT_TAKEN_HINT}`,
|
|
10505
|
+
PREVIEW_EXIT_PORT_NOT_ALLOWED
|
|
10506
|
+
);
|
|
10507
|
+
}
|
|
10252
10508
|
const message = extractControllerErrorMessage(text2) ?? response.statusText;
|
|
10253
10509
|
throw new Error(`Preview request failed (${response.status}): ${message}`);
|
|
10254
10510
|
}
|
|
@@ -10457,12 +10713,12 @@ function listeningSocketInodes(procRoot, port) {
|
|
|
10457
10713
|
continue;
|
|
10458
10714
|
}
|
|
10459
10715
|
for (const line of text2.split("\n").slice(1)) {
|
|
10460
|
-
const
|
|
10461
|
-
if (
|
|
10462
|
-
const local =
|
|
10716
|
+
const fields2 = line.trim().split(/\s+/);
|
|
10717
|
+
if (fields2.length < 10) continue;
|
|
10718
|
+
const local = fields2[1] ?? "";
|
|
10463
10719
|
const portHex = local.slice(local.lastIndexOf(":") + 1);
|
|
10464
|
-
if (
|
|
10465
|
-
inodes.add(
|
|
10720
|
+
if (fields2[3] === "0A" && Number.parseInt(portHex, 16) === port && fields2[9] !== "0") {
|
|
10721
|
+
inodes.add(fields2[9]);
|
|
10466
10722
|
}
|
|
10467
10723
|
}
|
|
10468
10724
|
}
|
|
@@ -10638,8 +10894,159 @@ async function startPreviewSite(params) {
|
|
|
10638
10894
|
await new Promise((resolve) => setTimeout(resolve, START_POLL_MS));
|
|
10639
10895
|
}
|
|
10640
10896
|
}
|
|
10897
|
+
function withPort(argv, port) {
|
|
10898
|
+
return argv.map((arg) => arg.split(PORT_PLACEHOLDER).join(String(port)));
|
|
10899
|
+
}
|
|
10900
|
+
function sameArgv(a, b) {
|
|
10901
|
+
return a.length === b.length && a.every((arg, index) => arg === b[index]);
|
|
10902
|
+
}
|
|
10903
|
+
function sameStart(a, b) {
|
|
10904
|
+
return Boolean(a && Array.isArray(a.argv) && a.cwd === b.cwd && sameArgv(a.argv, b.argv));
|
|
10905
|
+
}
|
|
10906
|
+
function preferredSitePort(start) {
|
|
10907
|
+
const digest2 = createHash2("sha256").update(`${start.cwd}\0${start.argv.join("\0")}`).digest();
|
|
10908
|
+
return AUTO_PORT_FIRST + digest2.readUInt32BE(0) % AUTO_PORT_COUNT;
|
|
10909
|
+
}
|
|
10910
|
+
async function portIsFree(port) {
|
|
10911
|
+
for (const host of ["127.0.0.1", "0.0.0.0", "::1", "::"]) {
|
|
10912
|
+
const result = await new Promise((resolve) => {
|
|
10913
|
+
const server = net5.createServer();
|
|
10914
|
+
server.once(
|
|
10915
|
+
"error",
|
|
10916
|
+
(error) => resolve(error.code === "EADDRNOTAVAIL" || error.code === "EAFNOSUPPORT" ? "absent" : "taken")
|
|
10917
|
+
);
|
|
10918
|
+
server.listen({ port, host, ...host === "::" ? { ipv6Only: false } : {} }, () => server.close(() => resolve("free")));
|
|
10919
|
+
});
|
|
10920
|
+
if (result === "taken") return false;
|
|
10921
|
+
}
|
|
10922
|
+
return true;
|
|
10923
|
+
}
|
|
10924
|
+
function startedSiteRuns(entry) {
|
|
10925
|
+
return Boolean(entry.startedPid && entry.startedPid > 0 && entry.startedCommand) && readProcessCommandLine(entry.startedPid) === entry.startedCommand;
|
|
10926
|
+
}
|
|
10927
|
+
async function siteOnPort(params) {
|
|
10928
|
+
const { port, start, root, projectId: projectId2, previews, procRoot } = params;
|
|
10929
|
+
if (await portIsFree(port)) return "none";
|
|
10930
|
+
if (previews.some(
|
|
10931
|
+
(entry) => entry.projectId === projectId2 && entry.localPort === port && sameStart(entry.start, start) && startedSiteRuns(entry)
|
|
10932
|
+
)) {
|
|
10933
|
+
return "same";
|
|
10934
|
+
}
|
|
10935
|
+
const listening = detectListeningProcess(port, procRoot);
|
|
10936
|
+
if (listening && workspaceRelativeDir(root, listening.cwd) === start.cwd && sameArgv(listening.argv, withPort(start.argv, port))) {
|
|
10937
|
+
return "same";
|
|
10938
|
+
}
|
|
10939
|
+
return "other";
|
|
10940
|
+
}
|
|
10941
|
+
function portTakenError(port, previews, projectId2) {
|
|
10942
|
+
const holder = previews.find((entry) => entry.projectId === projectId2 && entry.localPort === port && entry.title);
|
|
10943
|
+
const what = holder ? `Port ${port} already shows '${holder.title}'.` : `Port ${port} is in use by another server.`;
|
|
10944
|
+
return new PreviewCommandError(`${what} Leave out --port: --start picks a free port for this site.`, PREVIEW_EXIT_PORT_NOT_ALLOWED);
|
|
10945
|
+
}
|
|
10946
|
+
async function startOrFindSite(params) {
|
|
10947
|
+
const { explicitPort, start, root, projectId: projectId2, title, sitePath, procRoot, log } = params;
|
|
10948
|
+
const { previews } = readStateFile2();
|
|
10949
|
+
const onPort = (port) => siteOnPort({ port, start, root, projectId: projectId2, previews, procRoot });
|
|
10950
|
+
const launch = async (port) => {
|
|
10951
|
+
const serve = { argv: withPort(start.argv, port), cwd: start.cwd };
|
|
10952
|
+
const started = await startPreviewSite({ port, sitePath, serve, root, timeoutMs: params.timeoutMs });
|
|
10953
|
+
log(`Started the site on port ${port} (pid ${started.pid}); log: ${displayPath(started.logFile)}`);
|
|
10954
|
+
return { port, start, started: { ...started, command: readProcessCommandLine(started.pid) } };
|
|
10955
|
+
};
|
|
10956
|
+
if (explicitPort !== null) {
|
|
10957
|
+
const found = await onPort(explicitPort);
|
|
10958
|
+
if (found === "same") return { port: explicitPort, start, started: null };
|
|
10959
|
+
if (found === "other") throw portTakenError(explicitPort, previews, projectId2);
|
|
10960
|
+
return launch(explicitPort);
|
|
10961
|
+
}
|
|
10962
|
+
const tried = /* @__PURE__ */ new Set();
|
|
10963
|
+
const known = previews.find((entry) => entry.projectId === projectId2 && sameStart(entry.start, start));
|
|
10964
|
+
if (known) {
|
|
10965
|
+
tried.add(known.localPort);
|
|
10966
|
+
const found = await onPort(known.localPort);
|
|
10967
|
+
if (found === "same") return { port: known.localPort, start, started: null };
|
|
10968
|
+
if (found === "none") return launch(known.localPort);
|
|
10969
|
+
}
|
|
10970
|
+
const reserved = reservedPreviewPorts();
|
|
10971
|
+
const held = new Set(
|
|
10972
|
+
previews.filter((entry) => entry.projectId !== projectId2 || !sameStart(entry.start, start)).map((entry) => entry.localPort)
|
|
10973
|
+
);
|
|
10974
|
+
for (const [port, holder] of await params.openPorts()) {
|
|
10975
|
+
if (holder !== title) held.add(port);
|
|
10976
|
+
}
|
|
10977
|
+
const first = preferredSitePort(start);
|
|
10978
|
+
for (let index = 0; index < AUTO_PORT_TRIES; index += 1) {
|
|
10979
|
+
const port = AUTO_PORT_FIRST + (first - AUTO_PORT_FIRST + index) % AUTO_PORT_COUNT;
|
|
10980
|
+
if (tried.has(port) || reserved.has(port) || held.has(port)) continue;
|
|
10981
|
+
tried.add(port);
|
|
10982
|
+
const found = await onPort(port);
|
|
10983
|
+
if (found === "same") return { port, start, started: null };
|
|
10984
|
+
if (found === "none") return launch(port);
|
|
10985
|
+
}
|
|
10986
|
+
throw new PreviewCommandError(
|
|
10987
|
+
`No free port was found for the site: ${tried.size} ports from ${first} are taken. Stop a site this space no longer needs (instafy preview close <previewId>), then retry.`,
|
|
10988
|
+
PREVIEW_EXIT_NOT_LISTENING
|
|
10989
|
+
);
|
|
10990
|
+
}
|
|
10991
|
+
async function stopSite(pid, command) {
|
|
10992
|
+
if (!pid || pid <= 0 || !command) return false;
|
|
10993
|
+
const ours = () => readProcessCommandLine(pid) === command;
|
|
10994
|
+
if (!ours()) return false;
|
|
10995
|
+
const signal = (name) => {
|
|
10996
|
+
try {
|
|
10997
|
+
process.kill(-pid, name);
|
|
10998
|
+
} catch {
|
|
10999
|
+
try {
|
|
11000
|
+
process.kill(pid, name);
|
|
11001
|
+
} catch {
|
|
11002
|
+
}
|
|
11003
|
+
}
|
|
11004
|
+
};
|
|
11005
|
+
signal("SIGTERM");
|
|
11006
|
+
if (!await waitForProcessExit(pid, 4e3) && ours()) signal("SIGKILL");
|
|
11007
|
+
return true;
|
|
11008
|
+
}
|
|
11009
|
+
function siteRecordFor(site, previews, port, projectId2) {
|
|
11010
|
+
if (site?.started) {
|
|
11011
|
+
return { start: site.start, startedPid: site.started.pid, ...site.started.command ? { startedCommand: site.started.command } : {} };
|
|
11012
|
+
}
|
|
11013
|
+
const earlier = previews.find(
|
|
11014
|
+
(entry) => entry.projectId === projectId2 && entry.localPort === port && (!site || sameStart(entry.start, site.start)) && startedSiteRuns(entry)
|
|
11015
|
+
);
|
|
11016
|
+
if (earlier) return { start: earlier.start, startedPid: earlier.startedPid, startedCommand: earlier.startedCommand };
|
|
11017
|
+
return site ? { start: site.start } : {};
|
|
11018
|
+
}
|
|
11019
|
+
async function openPreviewPorts(params) {
|
|
11020
|
+
const ports = /* @__PURE__ */ new Map();
|
|
11021
|
+
let rows = [];
|
|
11022
|
+
try {
|
|
11023
|
+
const listed = await controllerRequest({
|
|
11024
|
+
controllerUrl: params.controllerUrl,
|
|
11025
|
+
auth: params.auth,
|
|
11026
|
+
method: "GET",
|
|
11027
|
+
route: previewsRoute(params.projectId),
|
|
11028
|
+
retryCommand: "instafy preview list",
|
|
11029
|
+
notFoundMeansUnavailable: true,
|
|
11030
|
+
timeoutMs: 1e4
|
|
11031
|
+
});
|
|
11032
|
+
rows = Array.isArray(listed.body) ? listed.body : [];
|
|
11033
|
+
} catch (error) {
|
|
11034
|
+
if (error instanceof PreviewCommandError && error.exitCode === PREVIEW_EXIT_UNAVAILABLE) throw error;
|
|
11035
|
+
return ports;
|
|
11036
|
+
}
|
|
11037
|
+
const runtimeId = readEnv("RUNTIME_ID");
|
|
11038
|
+
for (const raw of rows) {
|
|
11039
|
+
if (!raw || typeof raw !== "object") continue;
|
|
11040
|
+
const record2 = raw;
|
|
11041
|
+
const onRuntime = stringField(record2, "runtimeId");
|
|
11042
|
+
if (runtimeId && onRuntime && onRuntime !== runtimeId) continue;
|
|
11043
|
+
const port = numberField(record2, "port");
|
|
11044
|
+
if (port) ports.set(port, stringField(record2, "title") ?? "");
|
|
11045
|
+
}
|
|
11046
|
+
return ports;
|
|
11047
|
+
}
|
|
10641
11048
|
async function createAndStartClient(params) {
|
|
10642
|
-
const { opts, port, title, sitePath, projectId: projectId2, retryCommand, auth, controllerUrl, serverUrl, log, serve } = params;
|
|
11049
|
+
const { opts, port, title, sitePath, projectId: projectId2, retryCommand, auth, controllerUrl, serverUrl, log, serve, site, portPicked } = params;
|
|
10643
11050
|
const conversationId = opts.conversationId?.trim() || void 0;
|
|
10644
11051
|
const created = await controllerRequest({
|
|
10645
11052
|
controllerUrl,
|
|
@@ -10655,10 +11062,12 @@ async function createAndStartClient(params) {
|
|
|
10655
11062
|
title,
|
|
10656
11063
|
...opts.summary ? { summary: opts.summary } : {},
|
|
10657
11064
|
metadata: { source: "instafy-cli" },
|
|
10658
|
-
...serve ? { serve } : {}
|
|
11065
|
+
...serve ? { serve } : {},
|
|
11066
|
+
...opts.replace ? { replace: true } : {}
|
|
10659
11067
|
},
|
|
10660
11068
|
retryCommand,
|
|
10661
|
-
notFoundMeansUnavailable: true
|
|
11069
|
+
notFoundMeansUnavailable: true,
|
|
11070
|
+
...portPicked ? { portTakenHint: PICKED_PORT_TAKEN_HINT } : {}
|
|
10662
11071
|
});
|
|
10663
11072
|
const grant = created.body && !Array.isArray(created.body) ? created.body : {};
|
|
10664
11073
|
const previewId = stringField(grant, "previewId");
|
|
@@ -10672,6 +11081,7 @@ async function createAndStartClient(params) {
|
|
|
10672
11081
|
const studioOrigin = parseStudioOrigin(grant["studioOrigin"]);
|
|
10673
11082
|
const state = readStateFile2();
|
|
10674
11083
|
const current = state.previews.find((preview) => preview.previewId === previewId) ?? null;
|
|
11084
|
+
const siteRecord = siteRecordFor(site, state.previews, port, projectId2);
|
|
10675
11085
|
let pid = null;
|
|
10676
11086
|
let binary = current?.binary;
|
|
10677
11087
|
try {
|
|
@@ -10735,47 +11145,88 @@ ${started.logTail}` : "";
|
|
|
10735
11145
|
startedAt: current?.pid === pid ? current.startedAt : (/* @__PURE__ */ new Date()).toISOString(),
|
|
10736
11146
|
...binary ? { binary } : {},
|
|
10737
11147
|
...hostSuffix ? { hostSuffix } : {},
|
|
10738
|
-
...studioOrigin ? { studioOrigin } : {}
|
|
11148
|
+
...studioOrigin ? { studioOrigin } : {},
|
|
11149
|
+
...siteRecord,
|
|
11150
|
+
...serve ? { siteDir: serve.cwd } : {},
|
|
11151
|
+
// The same preview opened again keeps its picture until a new one is uploaded.
|
|
11152
|
+
...current?.pictureAt ? { pictureAt: current.pictureAt } : {},
|
|
11153
|
+
...opts.thumbnail === false ? { noThumbnail: true } : {}
|
|
10739
11154
|
};
|
|
10740
11155
|
upsertPreviewState(entry);
|
|
10741
11156
|
return { grant, previewId, entry, current };
|
|
10742
11157
|
}
|
|
10743
11158
|
async function previewOpen(opts) {
|
|
10744
|
-
const
|
|
11159
|
+
const explicitPort = opts.port === void 0 || String(opts.port).trim() === "" ? null : parsePreviewPort(opts.port);
|
|
10745
11160
|
const title = parsePreviewTitle(opts.title);
|
|
10746
11161
|
const sitePath = parsePreviewPath(opts.path);
|
|
10747
11162
|
const summary = parsePreviewSummary(opts.summary);
|
|
10748
|
-
|
|
11163
|
+
const serveArgv = opts.serveCmd?.trim() ? parseExplicitServeCommand(opts.serveCmd) : null;
|
|
11164
|
+
if (opts.start && !serveArgv) {
|
|
11165
|
+
throw new PreviewCommandError('--start needs --serve-cmd "<command>" to start the site.', PREVIEW_EXIT_NOT_LISTENING);
|
|
11166
|
+
}
|
|
11167
|
+
if (explicitPort === null) {
|
|
11168
|
+
if (!opts.start) {
|
|
11169
|
+
throw new Error(`A port is required: --port <port>. Or start the site with --start and --serve-cmd, which picks a free port.`);
|
|
11170
|
+
}
|
|
11171
|
+
if (!serveArgv.some((arg) => arg.includes(PORT_PLACEHOLDER))) {
|
|
11172
|
+
throw new Error(
|
|
11173
|
+
`--serve-cmd needs ${PORT_PLACEHOLDER} where the site's port goes, e.g. --serve-cmd "python3 -m http.server ${PORT_PLACEHOLDER} --bind 127.0.0.1": --start picks the port.`
|
|
11174
|
+
);
|
|
11175
|
+
}
|
|
11176
|
+
}
|
|
10749
11177
|
const projectId2 = resolveProject({ project: opts.project });
|
|
10750
|
-
const retryCommand = `instafy preview open --port ${
|
|
11178
|
+
const retryCommand = `instafy preview open${explicitPort === null ? "" : ` --port ${explicitPort}`} --title ${shellQuote2(title)}`;
|
|
10751
11179
|
const { auth, controllerUrl } = resolveAuth(opts, retryCommand);
|
|
10752
11180
|
const serverUrl = cleanUrl(controllerUrl);
|
|
10753
11181
|
const log = (message) => console.error(kleur8.cyan(message));
|
|
10754
|
-
const
|
|
10755
|
-
|
|
10756
|
-
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
|
|
10761
|
-
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
|
|
11182
|
+
const start = opts.start ? resolvePreviewServe({ port: explicitPort ?? 0, serveCmd: opts.serveCmd, projectId: projectId2, cwd: opts.cwd }).serve : null;
|
|
11183
|
+
const { grant, previewId, entry, port, site, serve, restartNote } = await withStateLock(async () => {
|
|
11184
|
+
const site2 = start ? await startOrFindSite({
|
|
11185
|
+
explicitPort,
|
|
11186
|
+
start,
|
|
11187
|
+
root: resolveWorkspaceRoot(process.env, projectId2),
|
|
11188
|
+
projectId: projectId2,
|
|
11189
|
+
title,
|
|
11190
|
+
sitePath,
|
|
11191
|
+
timeoutMs: opts.startTimeoutMs,
|
|
11192
|
+
procRoot: opts.procRoot,
|
|
11193
|
+
openPorts: () => openPreviewPorts({ controllerUrl, auth, projectId: projectId2 }),
|
|
11194
|
+
log
|
|
11195
|
+
}) : null;
|
|
11196
|
+
const port2 = site2?.port ?? explicitPort;
|
|
11197
|
+
try {
|
|
11198
|
+
const probe = await probeLocalSite(port2, sitePath, void 0, RATHOLE_LOCAL_HOSTS);
|
|
11199
|
+
if (!probe.listening) {
|
|
11200
|
+
throw await notListeningError(port2, sitePath, probe);
|
|
10765
11201
|
}
|
|
10766
|
-
const
|
|
10767
|
-
|
|
10768
|
-
|
|
10769
|
-
|
|
11202
|
+
const { serve: serve2, note } = resolvePreviewServe({
|
|
11203
|
+
port: port2,
|
|
11204
|
+
serveCmd: opts.serveCmd?.split(PORT_PLACEHOLDER).join(String(port2)),
|
|
11205
|
+
projectId: projectId2,
|
|
11206
|
+
cwd: opts.cwd,
|
|
11207
|
+
procRoot: opts.procRoot
|
|
11208
|
+
});
|
|
11209
|
+
const created = await createAndStartClient({
|
|
11210
|
+
opts: { ...opts, summary },
|
|
11211
|
+
port: port2,
|
|
11212
|
+
title,
|
|
11213
|
+
sitePath,
|
|
11214
|
+
projectId: projectId2,
|
|
11215
|
+
retryCommand,
|
|
11216
|
+
auth,
|
|
11217
|
+
controllerUrl,
|
|
11218
|
+
serverUrl,
|
|
11219
|
+
log,
|
|
11220
|
+
serve: serve2,
|
|
11221
|
+
site: site2,
|
|
11222
|
+
portPicked: explicitPort === null
|
|
11223
|
+
});
|
|
11224
|
+
return { ...created, port: port2, site: site2, serve: serve2, restartNote: note };
|
|
11225
|
+
} catch (error) {
|
|
11226
|
+
if (site2?.started) await stopSite(site2.started.pid, site2.started.command ?? readProcessCommandLine(site2.started.pid));
|
|
11227
|
+
throw error;
|
|
10770
11228
|
}
|
|
10771
|
-
}
|
|
10772
|
-
if (!probe.listening) {
|
|
10773
|
-
throw await notListeningError(port, sitePath, probe);
|
|
10774
|
-
}
|
|
10775
|
-
const { serve, note: restartNote } = resolvePreviewServe({ port, serveCmd: opts.serveCmd, projectId: projectId2, cwd: opts.cwd });
|
|
10776
|
-
const { grant, previewId, entry } = await withStateLock(
|
|
10777
|
-
() => createAndStartClient({ opts: { ...opts, summary }, port, title, sitePath, projectId: projectId2, retryCommand, auth, controllerUrl, serverUrl, log, serve })
|
|
10778
|
-
);
|
|
11229
|
+
});
|
|
10779
11230
|
let ready;
|
|
10780
11231
|
try {
|
|
10781
11232
|
ready = await controllerRequest({
|
|
@@ -10805,8 +11256,10 @@ async function previewOpen(opts) {
|
|
|
10805
11256
|
thumbnail = { status: "skipped", reason: "the site is not live through the preview yet" };
|
|
10806
11257
|
} else {
|
|
10807
11258
|
if (!opts.thumbnailFile) log("Taking a picture of the site for its card\u2026");
|
|
11259
|
+
const takenAt = Date.now();
|
|
10808
11260
|
const attempt2 = opts.thumbnailFile ? readThumbnailFile(opts.thumbnailFile) : await captureSiteThumbnail({ port, path: sitePath, host: "127.0.0.1" });
|
|
10809
11261
|
thumbnail = "thumbnail" in attempt2 ? await uploadPreviewThumbnail({ controllerUrl, auth, projectId: projectId2, previewId, thumbnail: attempt2.thumbnail, retryCommand }) : { status: attempt2.status, reason: attempt2.reason };
|
|
11262
|
+
if (thumbnail.status === "uploaded" || !opts.thumbnailFile) await recordPictured(previewId, takenAt);
|
|
10810
11263
|
}
|
|
10811
11264
|
return {
|
|
10812
11265
|
previewId,
|
|
@@ -10819,7 +11272,7 @@ async function previewOpen(opts) {
|
|
|
10819
11272
|
viewerHostSuffix: parseViewerHostSuffix(grant["viewerHostSuffix"]) ?? null,
|
|
10820
11273
|
restartable,
|
|
10821
11274
|
...restartable ? {} : { restartNote: restartNote ?? "the server did not record a start command" },
|
|
10822
|
-
...opts.start ? { startedPid: started?.pid ?? null, logFile: started?.logFile ?? null } : {},
|
|
11275
|
+
...opts.start ? { startedPid: site?.started?.pid ?? null, logFile: site?.started?.logFile ?? null } : {},
|
|
10823
11276
|
thumbnail: thumbnail.status,
|
|
10824
11277
|
...thumbnail.reason ? { thumbnailReason: thumbnail.reason } : {}
|
|
10825
11278
|
};
|
|
@@ -10850,12 +11303,30 @@ async function uploadPreviewThumbnail(params) {
|
|
|
10850
11303
|
return { status: "failed", reason: message.slice(0, 300) };
|
|
10851
11304
|
}
|
|
10852
11305
|
}
|
|
11306
|
+
var PICTURE_RECORD_LOCK_WAIT_MS = 5e3;
|
|
11307
|
+
async function recordPictured(previewId, takenAt) {
|
|
11308
|
+
try {
|
|
11309
|
+
await withStateLock(async () => {
|
|
11310
|
+
const state = readStateFile2();
|
|
11311
|
+
const entry = state.previews.find((preview) => preview.previewId === previewId);
|
|
11312
|
+
if (!entry || (entry.pictureAt ?? 0) >= takenAt) return;
|
|
11313
|
+
entry.pictureAt = takenAt;
|
|
11314
|
+
writeStateFile2(state);
|
|
11315
|
+
}, PICTURE_RECORD_LOCK_WAIT_MS);
|
|
11316
|
+
} catch {
|
|
11317
|
+
}
|
|
11318
|
+
}
|
|
10853
11319
|
function openPreviewFor(port, sitePath) {
|
|
10854
|
-
|
|
11320
|
+
const entry = readStateFile2().previews.find((preview) => preview.localPort === port && preview.path === sitePath && preview.pid > 0);
|
|
11321
|
+
return entry && !entry.noThumbnail ? entry : null;
|
|
10855
11322
|
}
|
|
10856
11323
|
function hasOpenPreviewFor(port, sitePath) {
|
|
10857
11324
|
return openPreviewFor(port, sitePath) !== null;
|
|
10858
11325
|
}
|
|
11326
|
+
async function recordCheckPictureTried(input3) {
|
|
11327
|
+
const entry = openPreviewFor(input3.port, input3.path);
|
|
11328
|
+
if (entry) await recordPictured(entry.previewId, input3.takenAt);
|
|
11329
|
+
}
|
|
10859
11330
|
async function uploadCheckThumbnail(input3) {
|
|
10860
11331
|
const entry = openPreviewFor(input3.port, input3.path);
|
|
10861
11332
|
if (!entry) return null;
|
|
@@ -10870,9 +11341,13 @@ async function uploadCheckThumbnail(input3) {
|
|
|
10870
11341
|
if (cleanUrl(controllerUrl) !== cleanUrl(entry.controllerUrl)) {
|
|
10871
11342
|
return { status: "skipped", reason: "the preview was opened on another server" };
|
|
10872
11343
|
}
|
|
11344
|
+
const takenAt = input3.takenAt ?? Date.now();
|
|
10873
11345
|
const facts = sniffImage(input3.image);
|
|
10874
|
-
if (!facts)
|
|
10875
|
-
|
|
11346
|
+
if (!facts) {
|
|
11347
|
+
await recordPictured(entry.previewId, takenAt);
|
|
11348
|
+
return { status: "skipped", reason: "the picture is not a JPEG" };
|
|
11349
|
+
}
|
|
11350
|
+
const outcome = await uploadPreviewThumbnail({
|
|
10876
11351
|
controllerUrl,
|
|
10877
11352
|
auth,
|
|
10878
11353
|
projectId: entry.projectId,
|
|
@@ -10880,6 +11355,8 @@ async function uploadCheckThumbnail(input3) {
|
|
|
10880
11355
|
thumbnail: { image: input3.image, ...facts, source: "capture", description: input3.description },
|
|
10881
11356
|
retryCommand
|
|
10882
11357
|
});
|
|
11358
|
+
await recordPictured(entry.previewId, takenAt);
|
|
11359
|
+
return outcome;
|
|
10883
11360
|
}
|
|
10884
11361
|
function toPreviewSummary(raw) {
|
|
10885
11362
|
if (!raw || typeof raw !== "object") return null;
|
|
@@ -10914,9 +11391,13 @@ async function previewClose(opts) {
|
|
|
10914
11391
|
if (!previewId) {
|
|
10915
11392
|
throw new Error("Preview id is required. Use `instafy preview list` to find it.");
|
|
10916
11393
|
}
|
|
11394
|
+
let siteStopped = false;
|
|
10917
11395
|
const entry = await withStateLock(async () => {
|
|
10918
11396
|
const removed = removePreviewState(previewId);
|
|
10919
|
-
if (removed)
|
|
11397
|
+
if (removed) {
|
|
11398
|
+
await stopClient(removed);
|
|
11399
|
+
siteStopped = await stopSite(removed.startedPid, removed.startedCommand);
|
|
11400
|
+
}
|
|
10920
11401
|
return removed;
|
|
10921
11402
|
});
|
|
10922
11403
|
const projectId2 = entry?.projectId ?? resolveProject({ project: opts.project });
|
|
@@ -10927,11 +11408,14 @@ async function previewClose(opts) {
|
|
|
10927
11408
|
);
|
|
10928
11409
|
await closePreviewOnServer(controllerUrl, auth, projectId2, previewId);
|
|
10929
11410
|
} catch (error) {
|
|
10930
|
-
if (entry)
|
|
11411
|
+
if (entry) {
|
|
11412
|
+
const { startedPid: _pid, startedCommand: _command, ...rest } = entry;
|
|
11413
|
+
await withStateLock(async () => upsertPreviewState({ ...siteStopped ? rest : entry, pid: -1 }));
|
|
11414
|
+
}
|
|
10931
11415
|
throw error;
|
|
10932
11416
|
}
|
|
10933
11417
|
removeWorkdir(previewId);
|
|
10934
|
-
return { ok: true, previewId };
|
|
11418
|
+
return { ok: true, previewId, siteStopped };
|
|
10935
11419
|
}
|
|
10936
11420
|
function printPreviewOpen(result, json) {
|
|
10937
11421
|
if (json) {
|
|
@@ -10939,7 +11423,7 @@ function printPreviewOpen(result, json) {
|
|
|
10939
11423
|
return;
|
|
10940
11424
|
}
|
|
10941
11425
|
const label = result.status === "live" ? kleur8.green("Ready to preview") : result.status === "not_responding" ? kleur8.yellow("Not responding yet") : kleur8.yellow("Preview opened (the server did not report whether the site answers)");
|
|
10942
|
-
console.log(`${label}: ${result.title} (port ${result.port}).
|
|
11426
|
+
console.log(`${label}: ${result.title} (port ${result.port}). Its card is in the conversation.`);
|
|
10943
11427
|
if (result.thumbnail === "uploaded") {
|
|
10944
11428
|
console.log(kleur8.gray("The card shows a picture of the site; `instafy preview check` takes a new one after a change."));
|
|
10945
11429
|
} else {
|
|
@@ -10948,7 +11432,7 @@ function printPreviewOpen(result, json) {
|
|
|
10948
11432
|
if (result.startedPid) {
|
|
10949
11433
|
console.log(kleur8.gray(`Started the site in the background (pid ${result.startedPid}); log: ${displayPath(result.logFile ?? "")}`));
|
|
10950
11434
|
} else if (result.startedPid === null) {
|
|
10951
|
-
console.log(kleur8.gray("
|
|
11435
|
+
console.log(kleur8.gray("This site was already running on its port; nothing was started."));
|
|
10952
11436
|
}
|
|
10953
11437
|
if (result.restartable) {
|
|
10954
11438
|
console.log(kleur8.gray("Open starts the site again when the process that started it exits, or the space sleeps."));
|
|
@@ -11115,7 +11599,7 @@ init_config();
|
|
|
11115
11599
|
|
|
11116
11600
|
// src/git-wrapper.ts
|
|
11117
11601
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
11118
|
-
import { randomUUID as
|
|
11602
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
11119
11603
|
import fs13 from "node:fs";
|
|
11120
11604
|
import path14 from "node:path";
|
|
11121
11605
|
import kleur10 from "kleur";
|
|
@@ -11329,10 +11813,10 @@ function hideEmbeddedGitDirs(context, pathSpecs) {
|
|
|
11329
11813
|
fs13.mkdirSync(stagingBase, { recursive: true });
|
|
11330
11814
|
const renames = [];
|
|
11331
11815
|
for (const candidate of candidates) {
|
|
11332
|
-
let hidden = path14.join(stagingBase,
|
|
11816
|
+
let hidden = path14.join(stagingBase, randomUUID3());
|
|
11333
11817
|
for (let attempt2 = 0; attempt2 < 8; attempt2 += 1) {
|
|
11334
11818
|
if (!pathExists(hidden)) break;
|
|
11335
|
-
hidden = path14.join(stagingBase,
|
|
11819
|
+
hidden = path14.join(stagingBase, randomUUID3());
|
|
11336
11820
|
}
|
|
11337
11821
|
fs13.renameSync(candidate, hidden);
|
|
11338
11822
|
renames.push({ original: candidate, hidden });
|
|
@@ -11888,19 +12372,324 @@ function notSavedSentences(report, options) {
|
|
|
11888
12372
|
sentences.push(report.recoveryRef ? `Not saved: ${trimmed} (kept at ${report.recoveryRef})` : `Not saved: ${trimmed}`);
|
|
11889
12373
|
}
|
|
11890
12374
|
}
|
|
11891
|
-
return sentences;
|
|
11892
|
-
}
|
|
11893
|
-
function saveReportJson(report, notSaved) {
|
|
11894
|
-
return {
|
|
11895
|
-
rev: report.rev,
|
|
11896
|
-
baseRev: report.baseRev,
|
|
11897
|
-
gitSyncStatus: report.gitSyncStatus,
|
|
11898
|
-
recoveryRef: report.recoveryRef,
|
|
11899
|
-
conflictedPaths: report.conflictedPaths,
|
|
11900
|
-
rejectedPaths: report.rejectedPaths,
|
|
11901
|
-
unpushedRefs: report.unpushedRefs,
|
|
11902
|
-
notSaved
|
|
11903
|
-
};
|
|
12375
|
+
return sentences;
|
|
12376
|
+
}
|
|
12377
|
+
function saveReportJson(report, notSaved) {
|
|
12378
|
+
return {
|
|
12379
|
+
rev: report.rev,
|
|
12380
|
+
baseRev: report.baseRev,
|
|
12381
|
+
gitSyncStatus: report.gitSyncStatus,
|
|
12382
|
+
recoveryRef: report.recoveryRef,
|
|
12383
|
+
conflictedPaths: report.conflictedPaths,
|
|
12384
|
+
rejectedPaths: report.rejectedPaths,
|
|
12385
|
+
unpushedRefs: report.unpushedRefs,
|
|
12386
|
+
notSaved
|
|
12387
|
+
};
|
|
12388
|
+
}
|
|
12389
|
+
|
|
12390
|
+
// src/api.ts
|
|
12391
|
+
init_config();
|
|
12392
|
+
init_errors();
|
|
12393
|
+
init_controller_fetch();
|
|
12394
|
+
import fs14 from "node:fs";
|
|
12395
|
+
function normalizeUrl5(raw) {
|
|
12396
|
+
const value = (raw ?? "").trim();
|
|
12397
|
+
if (!value) return "http://127.0.0.1:8788";
|
|
12398
|
+
return value.replace(/\/$/, "");
|
|
12399
|
+
}
|
|
12400
|
+
function normalizeToken6(raw) {
|
|
12401
|
+
if (!raw) return null;
|
|
12402
|
+
const trimmed = raw.trim();
|
|
12403
|
+
return trimmed.length ? trimmed : null;
|
|
12404
|
+
}
|
|
12405
|
+
function resolveBearerTokenWithSource(options) {
|
|
12406
|
+
const cwd = process.cwd();
|
|
12407
|
+
const profile = resolveActiveProfileName({ cwd });
|
|
12408
|
+
const stored = resolveConfiguredAccessToken({ profile, cwd });
|
|
12409
|
+
const explicit = normalizeToken6(options.accessToken);
|
|
12410
|
+
if (explicit) {
|
|
12411
|
+
return { token: explicit, source: "explicit", profile, runtimeControllerUrl: null };
|
|
12412
|
+
}
|
|
12413
|
+
const runtimeCredential = resolveRuntimeControllerCredential();
|
|
12414
|
+
if (runtimeCredential) {
|
|
12415
|
+
return {
|
|
12416
|
+
token: runtimeCredential.token,
|
|
12417
|
+
source: "env",
|
|
12418
|
+
profile: null,
|
|
12419
|
+
runtimeControllerUrl: runtimeCredential.controllerUrl
|
|
12420
|
+
};
|
|
12421
|
+
}
|
|
12422
|
+
const envKeys = [
|
|
12423
|
+
"INSTAFY_ACCESS_TOKEN",
|
|
12424
|
+
"SUPABASE_ACCESS_TOKEN"
|
|
12425
|
+
];
|
|
12426
|
+
for (const key of envKeys) {
|
|
12427
|
+
const value = normalizeToken6(process.env[key]);
|
|
12428
|
+
if (value) {
|
|
12429
|
+
return { token: value, source: "env", profile, runtimeControllerUrl: null };
|
|
12430
|
+
}
|
|
12431
|
+
}
|
|
12432
|
+
if (stored) {
|
|
12433
|
+
return { token: stored, source: "config", profile, runtimeControllerUrl: null };
|
|
12434
|
+
}
|
|
12435
|
+
return { token: null, source: "none", profile, runtimeControllerUrl: null };
|
|
12436
|
+
}
|
|
12437
|
+
function parseKeyValue(raw) {
|
|
12438
|
+
const trimmed = raw.trim();
|
|
12439
|
+
if (!trimmed) {
|
|
12440
|
+
throw new Error("Key/value pair cannot be empty");
|
|
12441
|
+
}
|
|
12442
|
+
const equals = trimmed.indexOf("=");
|
|
12443
|
+
if (equals === -1) {
|
|
12444
|
+
return { key: trimmed, value: "" };
|
|
12445
|
+
}
|
|
12446
|
+
const key = trimmed.slice(0, equals).trim();
|
|
12447
|
+
const value = trimmed.slice(equals + 1).trim();
|
|
12448
|
+
if (!key) {
|
|
12449
|
+
throw new Error(`Invalid pair "${raw}" (missing key)`);
|
|
12450
|
+
}
|
|
12451
|
+
return { key, value };
|
|
12452
|
+
}
|
|
12453
|
+
function parseHeader(raw) {
|
|
12454
|
+
const trimmed = raw.trim();
|
|
12455
|
+
if (!trimmed) {
|
|
12456
|
+
throw new Error("Header cannot be empty");
|
|
12457
|
+
}
|
|
12458
|
+
const colon = trimmed.indexOf(":");
|
|
12459
|
+
if (colon !== -1) {
|
|
12460
|
+
const key = trimmed.slice(0, colon).trim();
|
|
12461
|
+
const value = trimmed.slice(colon + 1).trim();
|
|
12462
|
+
if (!key) {
|
|
12463
|
+
throw new Error(`Invalid header "${raw}" (missing name)`);
|
|
12464
|
+
}
|
|
12465
|
+
return { key, value };
|
|
12466
|
+
}
|
|
12467
|
+
return parseKeyValue(trimmed);
|
|
12468
|
+
}
|
|
12469
|
+
function parseJsonBody(options) {
|
|
12470
|
+
if (options.jsonFile) {
|
|
12471
|
+
const raw = fs14.readFileSync(options.jsonFile, "utf8");
|
|
12472
|
+
const parsed = JSON.parse(raw);
|
|
12473
|
+
return JSON.stringify(parsed);
|
|
12474
|
+
}
|
|
12475
|
+
if (options.json) {
|
|
12476
|
+
const parsed = JSON.parse(options.json);
|
|
12477
|
+
return JSON.stringify(parsed);
|
|
12478
|
+
}
|
|
12479
|
+
return void 0;
|
|
12480
|
+
}
|
|
12481
|
+
function buildRequestUrl(options, runtimeControllerUrl) {
|
|
12482
|
+
const selectedControllerUrl = options.controllerUrl ?? runtimeControllerUrl;
|
|
12483
|
+
const base = new URL(
|
|
12484
|
+
`${normalizeUrl5(resolveControllerUrl({ controllerUrl: selectedControllerUrl ?? null }))}/`
|
|
12485
|
+
);
|
|
12486
|
+
if (base.protocol !== "http:" && base.protocol !== "https:") {
|
|
12487
|
+
throw new Error("The configured controller URL must use http or https.");
|
|
12488
|
+
}
|
|
12489
|
+
if (base.username || base.password) {
|
|
12490
|
+
throw new Error("The configured controller URL must not contain embedded credentials.");
|
|
12491
|
+
}
|
|
12492
|
+
if (runtimeControllerUrl) {
|
|
12493
|
+
resolveRuntimeBoundControllerUrl(
|
|
12494
|
+
{ controllerUrl: runtimeControllerUrl },
|
|
12495
|
+
base.toString()
|
|
12496
|
+
);
|
|
12497
|
+
}
|
|
12498
|
+
const rawPath = options.path.trim();
|
|
12499
|
+
if (!rawPath) {
|
|
12500
|
+
throw new Error("Path is required");
|
|
12501
|
+
}
|
|
12502
|
+
const isAbsoluteReference = /^[a-z][a-z\d+.-]*:/i.test(rawPath) || rawPath.startsWith("//");
|
|
12503
|
+
const url = new URL(
|
|
12504
|
+
isAbsoluteReference ? rawPath : rawPath.startsWith("/") ? rawPath : `/${rawPath}`,
|
|
12505
|
+
base
|
|
12506
|
+
);
|
|
12507
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
12508
|
+
throw new Error("Controller API URLs must use http or https.");
|
|
12509
|
+
}
|
|
12510
|
+
if (url.username || url.password) {
|
|
12511
|
+
throw new Error("Controller API URLs must not contain embedded credentials.");
|
|
12512
|
+
}
|
|
12513
|
+
const crossOrigin = url.origin !== base.origin;
|
|
12514
|
+
if (crossOrigin && !options.allowCrossOrigin) {
|
|
12515
|
+
throw new Error(
|
|
12516
|
+
"Refusing a cross-origin controller API request. Set --server-url to that origin and use a relative path, or pass --allow-cross-origin only when you explicitly intend to send credentials and request data there."
|
|
12517
|
+
);
|
|
12518
|
+
}
|
|
12519
|
+
for (const pair of options.query ?? []) {
|
|
12520
|
+
const { key, value } = parseKeyValue(pair);
|
|
12521
|
+
url.searchParams.set(key, value);
|
|
12522
|
+
}
|
|
12523
|
+
return { url, crossOrigin };
|
|
12524
|
+
}
|
|
12525
|
+
function maybePrettyPrintJson(text2, pretty) {
|
|
12526
|
+
if (!pretty) return text2;
|
|
12527
|
+
if (!text2.trim()) return text2;
|
|
12528
|
+
try {
|
|
12529
|
+
return JSON.stringify(JSON.parse(text2), null, 2);
|
|
12530
|
+
} catch {
|
|
12531
|
+
return text2;
|
|
12532
|
+
}
|
|
12533
|
+
}
|
|
12534
|
+
async function executeControllerApiRequest(options, bodyOverride) {
|
|
12535
|
+
const resolved = resolveBearerTokenWithSource(options);
|
|
12536
|
+
const { url, crossOrigin } = buildRequestUrl(options, resolved.runtimeControllerUrl);
|
|
12537
|
+
const bearer = resolved.token;
|
|
12538
|
+
if (crossOrigin && (!bearer || resolved.source !== "explicit")) {
|
|
12539
|
+
throw new Error(
|
|
12540
|
+
"Cross-origin controller API requests require --allow-cross-origin and an explicit --access-token."
|
|
12541
|
+
);
|
|
12542
|
+
}
|
|
12543
|
+
const headers = new Headers();
|
|
12544
|
+
headers.set("accept", "application/json");
|
|
12545
|
+
for (const header of options.headers ?? []) {
|
|
12546
|
+
const { key, value } = parseHeader(header);
|
|
12547
|
+
headers.set(key, value);
|
|
12548
|
+
}
|
|
12549
|
+
const body = bodyOverride ?? parseJsonBody(options);
|
|
12550
|
+
if (body !== void 0 && !headers.has("content-type")) {
|
|
12551
|
+
headers.set("content-type", "application/json");
|
|
12552
|
+
}
|
|
12553
|
+
const init = {
|
|
12554
|
+
method: options.method,
|
|
12555
|
+
headers,
|
|
12556
|
+
body,
|
|
12557
|
+
redirect: "error"
|
|
12558
|
+
};
|
|
12559
|
+
const response = bearer ? (await fetchWithControllerAuth({
|
|
12560
|
+
url: url.toString(),
|
|
12561
|
+
init,
|
|
12562
|
+
accessToken: bearer,
|
|
12563
|
+
tokenSource: resolved.source,
|
|
12564
|
+
profile: resolved.profile,
|
|
12565
|
+
cwd: process.cwd()
|
|
12566
|
+
})).response : await fetch(url, init);
|
|
12567
|
+
const responseText = await response.text().catch(() => "");
|
|
12568
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
12569
|
+
const isJson = contentType.includes("application/json") || contentType.includes("+json");
|
|
12570
|
+
if (!response.ok) {
|
|
12571
|
+
if (response.status === 401 || response.status === 403) {
|
|
12572
|
+
throw formatAuthRejectedError({
|
|
12573
|
+
status: response.status,
|
|
12574
|
+
responseBody: responseText,
|
|
12575
|
+
retryCommand: "instafy login",
|
|
12576
|
+
advancedHint: "pass --access-token, or set INSTAFY_ACCESS_TOKEN / SUPABASE_ACCESS_TOKEN"
|
|
12577
|
+
});
|
|
12578
|
+
}
|
|
12579
|
+
const formattedBody = isJson ? maybePrettyPrintJson(responseText, true) : responseText;
|
|
12580
|
+
const prefix = `Request failed (${response.status} ${response.statusText})`;
|
|
12581
|
+
const suffix = formattedBody.trim() ? `: ${formattedBody}` : "";
|
|
12582
|
+
throw new Error(`${prefix}${suffix}`);
|
|
12583
|
+
}
|
|
12584
|
+
return { response, responseText, isJson };
|
|
12585
|
+
}
|
|
12586
|
+
async function requestControllerApiJson(options) {
|
|
12587
|
+
const body = options.jsonBody === void 0 ? void 0 : JSON.stringify(options.jsonBody);
|
|
12588
|
+
const { responseText, isJson } = await executeControllerApiRequest(
|
|
12589
|
+
{
|
|
12590
|
+
method: options.method,
|
|
12591
|
+
path: options.path,
|
|
12592
|
+
controllerUrl: options.controllerUrl,
|
|
12593
|
+
accessToken: options.accessToken,
|
|
12594
|
+
serviceToken: options.serviceToken,
|
|
12595
|
+
query: options.query,
|
|
12596
|
+
headers: options.headers,
|
|
12597
|
+
allowCrossOrigin: options.allowCrossOrigin
|
|
12598
|
+
},
|
|
12599
|
+
body
|
|
12600
|
+
);
|
|
12601
|
+
if (!isJson) {
|
|
12602
|
+
throw new Error("Expected JSON response from controller API");
|
|
12603
|
+
}
|
|
12604
|
+
return JSON.parse(responseText);
|
|
12605
|
+
}
|
|
12606
|
+
async function requestControllerApi(options) {
|
|
12607
|
+
const { responseText, isJson } = await executeControllerApiRequest(options);
|
|
12608
|
+
const pretty = options.pretty !== false;
|
|
12609
|
+
const formattedBody = isJson ? maybePrettyPrintJson(responseText, pretty) : responseText;
|
|
12610
|
+
if (formattedBody) console.log(formattedBody);
|
|
12611
|
+
}
|
|
12612
|
+
|
|
12613
|
+
// src/research-settings.ts
|
|
12614
|
+
init_project_manifest();
|
|
12615
|
+
var fields = [
|
|
12616
|
+
{ key: "maxPageLoads", label: "Fresh pages per task" },
|
|
12617
|
+
{ key: "maxPageLoadsPerSite", label: "Fresh pages per site per task" }
|
|
12618
|
+
];
|
|
12619
|
+
function integer(raw, label) {
|
|
12620
|
+
if (!/^[1-9]\d*$/.test(raw.trim()) || !Number.isSafeInteger(Number(raw))) {
|
|
12621
|
+
throw new Error(`${label} must be a positive whole number.`);
|
|
12622
|
+
}
|
|
12623
|
+
return Number(raw);
|
|
12624
|
+
}
|
|
12625
|
+
async function researchSettings(options) {
|
|
12626
|
+
const patch = {};
|
|
12627
|
+
for (const field of fields) {
|
|
12628
|
+
if (options[field.key] !== void 0) patch[field.key] = integer(options[field.key], field.label);
|
|
12629
|
+
}
|
|
12630
|
+
if (options.interactive) {
|
|
12631
|
+
if (options.json || Object.keys(patch).length) throw new Error("Use --interactive without --json or setting flags.");
|
|
12632
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY || process.env.CI === "true") {
|
|
12633
|
+
throw new Error("--interactive requires a terminal. Use --max-page-loads and --max-page-loads-per-site instead.");
|
|
12634
|
+
}
|
|
12635
|
+
}
|
|
12636
|
+
const project = [options.project, process.env.SPACE_ID, process.env.INSTAFY_SPACE_ID, process.env.PROJECT_ID, process.env.INSTAFY_PROJECT_ID].map((value) => value?.trim()).find(Boolean) ?? findProjectManifest(process.cwd()).manifest?.spaceId;
|
|
12637
|
+
if (!project) throw new Error("No space configured. Pass --space, set SPACE_ID, or run `instafy space init`.");
|
|
12638
|
+
const request = {
|
|
12639
|
+
path: `/projects/${encodeURIComponent(project)}/research-settings`,
|
|
12640
|
+
controllerUrl: options.controllerUrl,
|
|
12641
|
+
accessToken: options.accessToken
|
|
12642
|
+
};
|
|
12643
|
+
let settings = await requestControllerApiJson({ ...request, method: "GET" });
|
|
12644
|
+
if ((options.interactive || Object.keys(patch).length) && !settings.canManage) {
|
|
12645
|
+
throw new Error("Only space managers can change research settings.");
|
|
12646
|
+
}
|
|
12647
|
+
for (const field of fields) {
|
|
12648
|
+
if (!settings.limits?.[field.key] || !Number.isInteger(settings[field.key])) {
|
|
12649
|
+
throw new Error("This controller does not support research settings. Update it before changing the budget.");
|
|
12650
|
+
}
|
|
12651
|
+
}
|
|
12652
|
+
if (options.interactive) {
|
|
12653
|
+
const { isCancel, text: text2 } = await import("@clack/prompts");
|
|
12654
|
+
console.log("The parent and its helpers share one budget. Changes apply to new tasks; running tasks retain their allowance.");
|
|
12655
|
+
for (const field of fields) {
|
|
12656
|
+
const bounds = settings.limits[field.key];
|
|
12657
|
+
const entered = await text2({
|
|
12658
|
+
message: `${field.label} (${bounds.min}\u2013${bounds.max}; default ${bounds.default})`,
|
|
12659
|
+
defaultValue: String(settings[field.key]),
|
|
12660
|
+
placeholder: String(settings[field.key]),
|
|
12661
|
+
validate(raw) {
|
|
12662
|
+
try {
|
|
12663
|
+
const value2 = integer(raw || String(settings[field.key]), field.label);
|
|
12664
|
+
if (value2 < bounds.min || value2 > bounds.max) return `Enter a whole number from ${bounds.min} to ${bounds.max}.`;
|
|
12665
|
+
if (field.key === "maxPageLoadsPerSite" && value2 > (patch.maxPageLoads ?? settings.maxPageLoads)) return "The per-site limit cannot exceed the task limit.";
|
|
12666
|
+
} catch (error) {
|
|
12667
|
+
return error.message;
|
|
12668
|
+
}
|
|
12669
|
+
}
|
|
12670
|
+
});
|
|
12671
|
+
if (isCancel(entered)) throw new Error("Cancelled. No settings were changed.");
|
|
12672
|
+
const value = integer(String(entered), field.label);
|
|
12673
|
+
if (value !== settings[field.key]) patch[field.key] = value;
|
|
12674
|
+
}
|
|
12675
|
+
}
|
|
12676
|
+
for (const field of fields) {
|
|
12677
|
+
const value = patch[field.key];
|
|
12678
|
+
const bounds = settings.limits[field.key];
|
|
12679
|
+
if (value !== void 0 && (value < bounds.min || value > bounds.max)) throw new Error(`${field.label} must be between ${bounds.min} and ${bounds.max}.`);
|
|
12680
|
+
}
|
|
12681
|
+
if ((patch.maxPageLoadsPerSite ?? settings.maxPageLoadsPerSite) > (patch.maxPageLoads ?? settings.maxPageLoads)) {
|
|
12682
|
+
throw new Error("The per-site limit cannot exceed the task limit.");
|
|
12683
|
+
}
|
|
12684
|
+
if (Object.keys(patch).length) settings = await requestControllerApiJson({ ...request, method: "PATCH", jsonBody: patch });
|
|
12685
|
+
if (options.json) {
|
|
12686
|
+
console.log(JSON.stringify(settings, null, 2));
|
|
12687
|
+
return;
|
|
12688
|
+
}
|
|
12689
|
+
console.log(`Research budget for space ${project}`);
|
|
12690
|
+
for (const field of fields) console.log(`${field.label}: ${settings[field.key]}`);
|
|
12691
|
+
console.log("Shared by the parent and all helpers, including continued worker jobs. Cached reads are free. Changes apply to new tasks.");
|
|
12692
|
+
if (!settings.canManage) console.log("Read-only: only space managers can change this budget.");
|
|
11904
12693
|
}
|
|
11905
12694
|
|
|
11906
12695
|
// src/web-link.ts
|
|
@@ -11971,9 +12760,9 @@ function registerWebLinkCommand(web) {
|
|
|
11971
12760
|
// src/web-search.ts
|
|
11972
12761
|
var redaction2 = __toESM(require_redaction(), 1);
|
|
11973
12762
|
import dns3 from "node:dns";
|
|
11974
|
-
import
|
|
12763
|
+
import fs15 from "node:fs";
|
|
11975
12764
|
import https2 from "node:https";
|
|
11976
|
-
import
|
|
12765
|
+
import net6 from "node:net";
|
|
11977
12766
|
import path15 from "node:path";
|
|
11978
12767
|
import zlib2 from "node:zlib";
|
|
11979
12768
|
|
|
@@ -12180,7 +12969,7 @@ function compileProvider(id, raw) {
|
|
|
12180
12969
|
if (url.protocol !== "https:") return `${id}: request.url must be https`;
|
|
12181
12970
|
if (url.username || url.password || url.port && url.port !== "443" || url.hash) return `${id}: request.url must be a plain https address`;
|
|
12182
12971
|
const host = langInHost ? url.hostname.toLowerCase().replace(/^xx\./, "") : url.hostname.toLowerCase();
|
|
12183
|
-
if (
|
|
12972
|
+
if (net6.isIP(host.replace(/^\[|\]$/g, "")) || isLocalHostName(host)) return `${id}: request.url must name a public host`;
|
|
12184
12973
|
const never = NEVER_HOSTS.find((entry) => host === entry.host || host.endsWith(`.${entry.host}`));
|
|
12185
12974
|
if (never) return `${id}: ${never.host} is never used (${never.why})`;
|
|
12186
12975
|
if (/[{}]/.test(decodeURIComponent(url.href))) return `${id}: request.url may not hold placeholders`;
|
|
@@ -12303,9 +13092,9 @@ function compileProvider(id, raw) {
|
|
|
12303
13092
|
}
|
|
12304
13093
|
function resultLink(value) {
|
|
12305
13094
|
if (typeof value === "string" && value.startsWith("https://")) {
|
|
12306
|
-
const
|
|
13095
|
+
const fields2 = [...value.matchAll(/\{([\w-]+(?:\.[\w-]+)*)\}/g)];
|
|
12307
13096
|
const rest = value.replace(/\{([\w-]+(?:\.[\w-]+)*)\}/g, "");
|
|
12308
|
-
if (!
|
|
13097
|
+
if (!fields2.length || /[{}]/.test(rest)) return void 0;
|
|
12309
13098
|
try {
|
|
12310
13099
|
new URL(value.replace(/\{[^}]*\}/g, "x"));
|
|
12311
13100
|
} catch {
|
|
@@ -12363,8 +13152,8 @@ function loadSearchProviders(bundled = BUNDLED_SEARCH_PROVIDERS, copies = []) {
|
|
|
12363
13152
|
return { providers, chains, off: [...off].filter((id) => id in bundled.providers).sort(), keyEnvs: [...keyEnvs], warnings };
|
|
12364
13153
|
}
|
|
12365
13154
|
function fill(template, vars) {
|
|
12366
|
-
const
|
|
12367
|
-
if (
|
|
13155
|
+
const whole2 = /^\{([a-z]+)\}$/.exec(template);
|
|
13156
|
+
if (whole2 && whole2[1] === "count") return vars.count;
|
|
12368
13157
|
let missing = false;
|
|
12369
13158
|
const out = template.replace(/\{\{|\}\}|\{([a-z]+)(?::([a-z]+))?\}/g, (match, name, encoder) => {
|
|
12370
13159
|
if (!name) return match[0];
|
|
@@ -12453,7 +13242,7 @@ function sendOnce(request, target, timeoutMs) {
|
|
|
12453
13242
|
port: target.port,
|
|
12454
13243
|
method: request.method,
|
|
12455
13244
|
path: `${request.url.pathname}${request.url.search}`,
|
|
12456
|
-
servername:
|
|
13245
|
+
servername: net6.isIP(hostname) ? void 0 : hostname,
|
|
12457
13246
|
agent: false,
|
|
12458
13247
|
maxHeaderSize: 64 * 1024,
|
|
12459
13248
|
headers: { Host: request.url.host, ...request.headers, ...payload ? { "Content-Length": String(payload.length) } : {} }
|
|
@@ -12503,8 +13292,8 @@ function at(value, field) {
|
|
|
12503
13292
|
}
|
|
12504
13293
|
return current;
|
|
12505
13294
|
}
|
|
12506
|
-
function firstText(item,
|
|
12507
|
-
for (const field of
|
|
13295
|
+
function firstText(item, fields2) {
|
|
13296
|
+
for (const field of fields2) {
|
|
12508
13297
|
const value = at(item, field);
|
|
12509
13298
|
if (typeof value === "string" && value.trim()) return value;
|
|
12510
13299
|
}
|
|
@@ -12520,10 +13309,10 @@ function withoutTags(text2) {
|
|
|
12520
13309
|
return out;
|
|
12521
13310
|
}
|
|
12522
13311
|
function cleanText(raw, max) {
|
|
12523
|
-
const text2 = withoutTags(raw).replace(/&(#x[0-9a-f]{1,6}|#\d{1,7}|[a-z]{2,6}|#39);/gi, (
|
|
13312
|
+
const text2 = withoutTags(raw).replace(/&(#x[0-9a-f]{1,6}|#\d{1,7}|[a-z]{2,6}|#39);/gi, (whole2, name) => {
|
|
12524
13313
|
if (ENTITIES2[name.toLowerCase()]) return ENTITIES2[name.toLowerCase()];
|
|
12525
13314
|
const code = name[0] === "#" ? name[1]?.toLowerCase() === "x" ? parseInt(name.slice(2), 16) : parseInt(name.slice(1), 10) : NaN;
|
|
12526
|
-
return Number.isFinite(code) && code > 31 && code < 1114112 ? String.fromCodePoint(code) :
|
|
13315
|
+
return Number.isFinite(code) && code > 31 && code < 1114112 ? String.fromCodePoint(code) : whole2;
|
|
12527
13316
|
}).replace(/</g, "\u2039").replace(/[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u202a-\u202e\u2066-\u2069\ufeff]/g, " ").replace(/\s+/g, " ").trim();
|
|
12528
13317
|
const redacted = redaction2.redactText(text2).text;
|
|
12529
13318
|
return redacted.length > max ? `${redacted.slice(0, max - 1)}\u2026` : redacted;
|
|
@@ -12540,7 +13329,7 @@ function resultAddress(raw) {
|
|
|
12540
13329
|
if (url.username || url.password) return null;
|
|
12541
13330
|
if (url.port && url.port !== "80" && url.port !== "443") return null;
|
|
12542
13331
|
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
12543
|
-
if (
|
|
13332
|
+
if (net6.isIP(host) ? isPrivateAddress(host) : isLocalHostName(host)) return null;
|
|
12544
13333
|
url.hash = "";
|
|
12545
13334
|
return url;
|
|
12546
13335
|
}
|
|
@@ -12692,10 +13481,10 @@ var SearchState = class {
|
|
|
12692
13481
|
/** Removes turn and day files older than two days. */
|
|
12693
13482
|
prune() {
|
|
12694
13483
|
try {
|
|
12695
|
-
for (const name of
|
|
13484
|
+
for (const name of fs15.readdirSync(this.dir)) {
|
|
12696
13485
|
if (!/^(?:search-turn|day)-[\w-]+\.json$/.test(name)) continue;
|
|
12697
13486
|
const file = path15.join(this.dir, name);
|
|
12698
|
-
if (Date.now() -
|
|
13487
|
+
if (Date.now() - fs15.statSync(file).mtimeMs > 2 * 864e5) fs15.rmSync(file, { force: true });
|
|
12699
13488
|
}
|
|
12700
13489
|
} catch {
|
|
12701
13490
|
}
|
|
@@ -12710,7 +13499,7 @@ function liveTurn(env, now) {
|
|
|
12710
13499
|
}
|
|
12711
13500
|
function keyless(chain) {
|
|
12712
13501
|
const find = chain === DEFAULT_CHAIN ? 'find official sites with `instafy web search "<name>" --chain reference-search` (no key) and read them' : "read the official sites you know of";
|
|
12713
|
-
return `Go on without it: ${find}, and
|
|
13502
|
+
return `Go on without it: ${find}, and for reviews a reviewer's home page and the pages it links, with \`instafy web read\`. Then answer or build the page with what you read, each missing fact marked unknown with why. The person may send more pages with Send page to Octo.`;
|
|
12714
13503
|
}
|
|
12715
13504
|
function tellFor(status, chain) {
|
|
12716
13505
|
switch (status) {
|
|
@@ -13043,229 +13832,6 @@ function registerWebSearchCommand(web) {
|
|
|
13043
13832
|
return web;
|
|
13044
13833
|
}
|
|
13045
13834
|
|
|
13046
|
-
// src/api.ts
|
|
13047
|
-
init_config();
|
|
13048
|
-
init_errors();
|
|
13049
|
-
init_controller_fetch();
|
|
13050
|
-
import fs15 from "node:fs";
|
|
13051
|
-
function normalizeUrl5(raw) {
|
|
13052
|
-
const value = (raw ?? "").trim();
|
|
13053
|
-
if (!value) return "http://127.0.0.1:8788";
|
|
13054
|
-
return value.replace(/\/$/, "");
|
|
13055
|
-
}
|
|
13056
|
-
function normalizeToken6(raw) {
|
|
13057
|
-
if (!raw) return null;
|
|
13058
|
-
const trimmed = raw.trim();
|
|
13059
|
-
return trimmed.length ? trimmed : null;
|
|
13060
|
-
}
|
|
13061
|
-
function resolveBearerTokenWithSource(options) {
|
|
13062
|
-
const cwd = process.cwd();
|
|
13063
|
-
const profile = resolveActiveProfileName({ cwd });
|
|
13064
|
-
const stored = resolveConfiguredAccessToken({ profile, cwd });
|
|
13065
|
-
const explicit = normalizeToken6(options.accessToken);
|
|
13066
|
-
if (explicit) {
|
|
13067
|
-
return { token: explicit, source: "explicit", profile, runtimeControllerUrl: null };
|
|
13068
|
-
}
|
|
13069
|
-
const runtimeCredential = resolveRuntimeControllerCredential();
|
|
13070
|
-
if (runtimeCredential) {
|
|
13071
|
-
return {
|
|
13072
|
-
token: runtimeCredential.token,
|
|
13073
|
-
source: "env",
|
|
13074
|
-
profile: null,
|
|
13075
|
-
runtimeControllerUrl: runtimeCredential.controllerUrl
|
|
13076
|
-
};
|
|
13077
|
-
}
|
|
13078
|
-
const envKeys = [
|
|
13079
|
-
"INSTAFY_ACCESS_TOKEN",
|
|
13080
|
-
"SUPABASE_ACCESS_TOKEN"
|
|
13081
|
-
];
|
|
13082
|
-
for (const key of envKeys) {
|
|
13083
|
-
const value = normalizeToken6(process.env[key]);
|
|
13084
|
-
if (value) {
|
|
13085
|
-
return { token: value, source: "env", profile, runtimeControllerUrl: null };
|
|
13086
|
-
}
|
|
13087
|
-
}
|
|
13088
|
-
if (stored) {
|
|
13089
|
-
return { token: stored, source: "config", profile, runtimeControllerUrl: null };
|
|
13090
|
-
}
|
|
13091
|
-
return { token: null, source: "none", profile, runtimeControllerUrl: null };
|
|
13092
|
-
}
|
|
13093
|
-
function parseKeyValue(raw) {
|
|
13094
|
-
const trimmed = raw.trim();
|
|
13095
|
-
if (!trimmed) {
|
|
13096
|
-
throw new Error("Key/value pair cannot be empty");
|
|
13097
|
-
}
|
|
13098
|
-
const equals = trimmed.indexOf("=");
|
|
13099
|
-
if (equals === -1) {
|
|
13100
|
-
return { key: trimmed, value: "" };
|
|
13101
|
-
}
|
|
13102
|
-
const key = trimmed.slice(0, equals).trim();
|
|
13103
|
-
const value = trimmed.slice(equals + 1).trim();
|
|
13104
|
-
if (!key) {
|
|
13105
|
-
throw new Error(`Invalid pair "${raw}" (missing key)`);
|
|
13106
|
-
}
|
|
13107
|
-
return { key, value };
|
|
13108
|
-
}
|
|
13109
|
-
function parseHeader(raw) {
|
|
13110
|
-
const trimmed = raw.trim();
|
|
13111
|
-
if (!trimmed) {
|
|
13112
|
-
throw new Error("Header cannot be empty");
|
|
13113
|
-
}
|
|
13114
|
-
const colon = trimmed.indexOf(":");
|
|
13115
|
-
if (colon !== -1) {
|
|
13116
|
-
const key = trimmed.slice(0, colon).trim();
|
|
13117
|
-
const value = trimmed.slice(colon + 1).trim();
|
|
13118
|
-
if (!key) {
|
|
13119
|
-
throw new Error(`Invalid header "${raw}" (missing name)`);
|
|
13120
|
-
}
|
|
13121
|
-
return { key, value };
|
|
13122
|
-
}
|
|
13123
|
-
return parseKeyValue(trimmed);
|
|
13124
|
-
}
|
|
13125
|
-
function parseJsonBody(options) {
|
|
13126
|
-
if (options.jsonFile) {
|
|
13127
|
-
const raw = fs15.readFileSync(options.jsonFile, "utf8");
|
|
13128
|
-
const parsed = JSON.parse(raw);
|
|
13129
|
-
return JSON.stringify(parsed);
|
|
13130
|
-
}
|
|
13131
|
-
if (options.json) {
|
|
13132
|
-
const parsed = JSON.parse(options.json);
|
|
13133
|
-
return JSON.stringify(parsed);
|
|
13134
|
-
}
|
|
13135
|
-
return void 0;
|
|
13136
|
-
}
|
|
13137
|
-
function buildRequestUrl(options, runtimeControllerUrl) {
|
|
13138
|
-
const selectedControllerUrl = options.controllerUrl ?? runtimeControllerUrl;
|
|
13139
|
-
const base = new URL(
|
|
13140
|
-
`${normalizeUrl5(resolveControllerUrl({ controllerUrl: selectedControllerUrl ?? null }))}/`
|
|
13141
|
-
);
|
|
13142
|
-
if (base.protocol !== "http:" && base.protocol !== "https:") {
|
|
13143
|
-
throw new Error("The configured controller URL must use http or https.");
|
|
13144
|
-
}
|
|
13145
|
-
if (base.username || base.password) {
|
|
13146
|
-
throw new Error("The configured controller URL must not contain embedded credentials.");
|
|
13147
|
-
}
|
|
13148
|
-
if (runtimeControllerUrl) {
|
|
13149
|
-
resolveRuntimeBoundControllerUrl(
|
|
13150
|
-
{ controllerUrl: runtimeControllerUrl },
|
|
13151
|
-
base.toString()
|
|
13152
|
-
);
|
|
13153
|
-
}
|
|
13154
|
-
const rawPath = options.path.trim();
|
|
13155
|
-
if (!rawPath) {
|
|
13156
|
-
throw new Error("Path is required");
|
|
13157
|
-
}
|
|
13158
|
-
const isAbsoluteReference = /^[a-z][a-z\d+.-]*:/i.test(rawPath) || rawPath.startsWith("//");
|
|
13159
|
-
const url = new URL(
|
|
13160
|
-
isAbsoluteReference ? rawPath : rawPath.startsWith("/") ? rawPath : `/${rawPath}`,
|
|
13161
|
-
base
|
|
13162
|
-
);
|
|
13163
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
13164
|
-
throw new Error("Controller API URLs must use http or https.");
|
|
13165
|
-
}
|
|
13166
|
-
if (url.username || url.password) {
|
|
13167
|
-
throw new Error("Controller API URLs must not contain embedded credentials.");
|
|
13168
|
-
}
|
|
13169
|
-
const crossOrigin = url.origin !== base.origin;
|
|
13170
|
-
if (crossOrigin && !options.allowCrossOrigin) {
|
|
13171
|
-
throw new Error(
|
|
13172
|
-
"Refusing a cross-origin controller API request. Set --server-url to that origin and use a relative path, or pass --allow-cross-origin only when you explicitly intend to send credentials and request data there."
|
|
13173
|
-
);
|
|
13174
|
-
}
|
|
13175
|
-
for (const pair of options.query ?? []) {
|
|
13176
|
-
const { key, value } = parseKeyValue(pair);
|
|
13177
|
-
url.searchParams.set(key, value);
|
|
13178
|
-
}
|
|
13179
|
-
return { url, crossOrigin };
|
|
13180
|
-
}
|
|
13181
|
-
function maybePrettyPrintJson(text2, pretty) {
|
|
13182
|
-
if (!pretty) return text2;
|
|
13183
|
-
if (!text2.trim()) return text2;
|
|
13184
|
-
try {
|
|
13185
|
-
return JSON.stringify(JSON.parse(text2), null, 2);
|
|
13186
|
-
} catch {
|
|
13187
|
-
return text2;
|
|
13188
|
-
}
|
|
13189
|
-
}
|
|
13190
|
-
async function executeControllerApiRequest(options, bodyOverride) {
|
|
13191
|
-
const resolved = resolveBearerTokenWithSource(options);
|
|
13192
|
-
const { url, crossOrigin } = buildRequestUrl(options, resolved.runtimeControllerUrl);
|
|
13193
|
-
const bearer = resolved.token;
|
|
13194
|
-
if (crossOrigin && (!bearer || resolved.source !== "explicit")) {
|
|
13195
|
-
throw new Error(
|
|
13196
|
-
"Cross-origin controller API requests require --allow-cross-origin and an explicit --access-token."
|
|
13197
|
-
);
|
|
13198
|
-
}
|
|
13199
|
-
const headers = new Headers();
|
|
13200
|
-
headers.set("accept", "application/json");
|
|
13201
|
-
for (const header of options.headers ?? []) {
|
|
13202
|
-
const { key, value } = parseHeader(header);
|
|
13203
|
-
headers.set(key, value);
|
|
13204
|
-
}
|
|
13205
|
-
const body = bodyOverride ?? parseJsonBody(options);
|
|
13206
|
-
if (body !== void 0 && !headers.has("content-type")) {
|
|
13207
|
-
headers.set("content-type", "application/json");
|
|
13208
|
-
}
|
|
13209
|
-
const init = {
|
|
13210
|
-
method: options.method,
|
|
13211
|
-
headers,
|
|
13212
|
-
body,
|
|
13213
|
-
redirect: "error"
|
|
13214
|
-
};
|
|
13215
|
-
const response = bearer ? (await fetchWithControllerAuth({
|
|
13216
|
-
url: url.toString(),
|
|
13217
|
-
init,
|
|
13218
|
-
accessToken: bearer,
|
|
13219
|
-
tokenSource: resolved.source,
|
|
13220
|
-
profile: resolved.profile,
|
|
13221
|
-
cwd: process.cwd()
|
|
13222
|
-
})).response : await fetch(url, init);
|
|
13223
|
-
const responseText = await response.text().catch(() => "");
|
|
13224
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
13225
|
-
const isJson = contentType.includes("application/json") || contentType.includes("+json");
|
|
13226
|
-
if (!response.ok) {
|
|
13227
|
-
if (response.status === 401 || response.status === 403) {
|
|
13228
|
-
throw formatAuthRejectedError({
|
|
13229
|
-
status: response.status,
|
|
13230
|
-
responseBody: responseText,
|
|
13231
|
-
retryCommand: "instafy login",
|
|
13232
|
-
advancedHint: "pass --access-token, or set INSTAFY_ACCESS_TOKEN / SUPABASE_ACCESS_TOKEN"
|
|
13233
|
-
});
|
|
13234
|
-
}
|
|
13235
|
-
const formattedBody = isJson ? maybePrettyPrintJson(responseText, true) : responseText;
|
|
13236
|
-
const prefix = `Request failed (${response.status} ${response.statusText})`;
|
|
13237
|
-
const suffix = formattedBody.trim() ? `: ${formattedBody}` : "";
|
|
13238
|
-
throw new Error(`${prefix}${suffix}`);
|
|
13239
|
-
}
|
|
13240
|
-
return { response, responseText, isJson };
|
|
13241
|
-
}
|
|
13242
|
-
async function requestControllerApiJson(options) {
|
|
13243
|
-
const body = options.jsonBody === void 0 ? void 0 : JSON.stringify(options.jsonBody);
|
|
13244
|
-
const { responseText, isJson } = await executeControllerApiRequest(
|
|
13245
|
-
{
|
|
13246
|
-
method: options.method,
|
|
13247
|
-
path: options.path,
|
|
13248
|
-
controllerUrl: options.controllerUrl,
|
|
13249
|
-
accessToken: options.accessToken,
|
|
13250
|
-
serviceToken: options.serviceToken,
|
|
13251
|
-
query: options.query,
|
|
13252
|
-
headers: options.headers,
|
|
13253
|
-
allowCrossOrigin: options.allowCrossOrigin
|
|
13254
|
-
},
|
|
13255
|
-
body
|
|
13256
|
-
);
|
|
13257
|
-
if (!isJson) {
|
|
13258
|
-
throw new Error("Expected JSON response from controller API");
|
|
13259
|
-
}
|
|
13260
|
-
return JSON.parse(responseText);
|
|
13261
|
-
}
|
|
13262
|
-
async function requestControllerApi(options) {
|
|
13263
|
-
const { responseText, isJson } = await executeControllerApiRequest(options);
|
|
13264
|
-
const pretty = options.pretty !== false;
|
|
13265
|
-
const formattedBody = isJson ? maybePrettyPrintJson(responseText, pretty) : responseText;
|
|
13266
|
-
if (formattedBody) console.log(formattedBody);
|
|
13267
|
-
}
|
|
13268
|
-
|
|
13269
13835
|
// src/conversations.ts
|
|
13270
13836
|
init_project_manifest();
|
|
13271
13837
|
import kleur11 from "kleur";
|
|
@@ -14816,10 +15382,10 @@ function projectId(explicit) {
|
|
|
14816
15382
|
if (!value?.trim()) throw new Error("No space configured. Pass --space, set SPACE_ID, or run `instafy space init`.");
|
|
14817
15383
|
return uuid(value, "Space ID");
|
|
14818
15384
|
}
|
|
14819
|
-
function
|
|
15385
|
+
function object2(value, fields2, label) {
|
|
14820
15386
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object.`);
|
|
14821
15387
|
const record2 = value;
|
|
14822
|
-
if (Object.keys(record2).some((key) => !
|
|
15388
|
+
if (Object.keys(record2).some((key) => !fields2.includes(key))) throw new Error(`${label} contains an unsupported field.`);
|
|
14823
15389
|
return record2;
|
|
14824
15390
|
}
|
|
14825
15391
|
function text(value, label, max) {
|
|
@@ -14874,7 +15440,7 @@ async function readProposal(file) {
|
|
|
14874
15440
|
} catch {
|
|
14875
15441
|
throw new Error("Recommendation file must contain valid JSON.");
|
|
14876
15442
|
}
|
|
14877
|
-
const input3 =
|
|
15443
|
+
const input3 = object2(parsed, ["key", "title", "reason", "prompt", "message", "evidence"], "Recommendation");
|
|
14878
15444
|
const key = text(input3.key, "Recommendation key", 120);
|
|
14879
15445
|
if (!/^[a-z0-9][a-z0-9_-]{0,119}$/.test(key)) throw new Error("Recommendation key must be a lowercase slug using letters, digits, hyphens or underscores.");
|
|
14880
15446
|
if (!Array.isArray(input3.evidence) || input3.evidence.length < 1 || input3.evidence.length > 8) {
|
|
@@ -14887,7 +15453,7 @@ async function readProposal(file) {
|
|
|
14887
15453
|
prompt: text(input3.prompt, "Recommendation prompt", 4e3),
|
|
14888
15454
|
...input3.message === void 0 ? {} : { message: text(input3.message, "Recommendation message", 4e3) },
|
|
14889
15455
|
evidence: input3.evidence.map((value) => {
|
|
14890
|
-
const entry =
|
|
15456
|
+
const entry = object2(value, ["conversationId", "messageId"], "Evidence");
|
|
14891
15457
|
return {
|
|
14892
15458
|
conversationId: uuid(entry.conversationId, "Evidence conversationId"),
|
|
14893
15459
|
...entry.messageId === void 0 ? {} : { messageId: uuid(entry.messageId, "Evidence messageId") }
|
|
@@ -16440,7 +17006,7 @@ async function diagnosticsRunResult(options) {
|
|
|
16440
17006
|
|
|
16441
17007
|
// src/support.ts
|
|
16442
17008
|
import fs19 from "node:fs";
|
|
16443
|
-
import { randomUUID as
|
|
17009
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
16444
17010
|
import path20 from "node:path";
|
|
16445
17011
|
init_project_manifest();
|
|
16446
17012
|
var MAX_SUMMARY_CHARS = 500;
|
|
@@ -16733,7 +17299,7 @@ async function supportReport(options) {
|
|
|
16733
17299
|
printJson3(preview);
|
|
16734
17300
|
return;
|
|
16735
17301
|
}
|
|
16736
|
-
const clientRequestId = cleanText4(options.clientRequestId) ??
|
|
17302
|
+
const clientRequestId = cleanText4(options.clientRequestId) ?? randomUUID4();
|
|
16737
17303
|
if (!UUID2.test(clientRequestId)) {
|
|
16738
17304
|
throw new Error("--client-request-id must be a UUID.");
|
|
16739
17305
|
}
|
|
@@ -16948,7 +17514,7 @@ async function supportReply(options) {
|
|
|
16948
17514
|
`A follow-up must be at most ${MAX_MESSAGE_CHARS} characters and ${MAX_MESSAGE_BYTES} bytes.`
|
|
16949
17515
|
);
|
|
16950
17516
|
}
|
|
16951
|
-
const clientRequestId = cleanText4(options.clientRequestId) ??
|
|
17517
|
+
const clientRequestId = cleanText4(options.clientRequestId) ?? randomUUID4();
|
|
16952
17518
|
if (!UUID2.test(clientRequestId)) {
|
|
16953
17519
|
throw new Error("--client-request-id must be a UUID.");
|
|
16954
17520
|
}
|
|
@@ -18391,20 +18957,48 @@ function exitWithPreviewError(error) {
|
|
|
18391
18957
|
}
|
|
18392
18958
|
var previewCommand = program.command("preview").description("Look at sites running in this space, and show them in Studio as a private preview card");
|
|
18393
18959
|
failWithGroupHelp(previewCommand);
|
|
18394
|
-
registerPreviewCheckCommand(previewCommand, {
|
|
18395
|
-
|
|
18960
|
+
registerPreviewCheckCommand(previewCommand, {
|
|
18961
|
+
hasOpenPreview: hasOpenPreviewFor,
|
|
18962
|
+
uploadThumbnail: uploadCheckThumbnail,
|
|
18963
|
+
pictureTried: recordCheckPictureTried
|
|
18964
|
+
});
|
|
18965
|
+
var webCommand = registerWebSearchCommand(registerWebLinkCommand(registerWebCommand(program)));
|
|
18966
|
+
failWithGroupHelp(webCommand);
|
|
18967
|
+
var webSettingsCommand = webCommand.command("settings").description("Inspect or change the space's shared research budget for new tasks");
|
|
18968
|
+
addSpaceOption(webSettingsCommand, "Space UUID (defaults to .instafy/space.json or SPACE_ID)");
|
|
18969
|
+
addServerUrlOptions(webSettingsCommand);
|
|
18970
|
+
addAccessTokenOptions(webSettingsCommand, "Instafy access token");
|
|
18971
|
+
webSettingsCommand.option("--max-page-loads <n>", "Fresh pages allowed across the parent and all helpers").option("--max-page-loads-per-site <n>", "Fresh pages allowed from one site across the task").option("--interactive", "Interactively edit the shared research budget").option("--json", "Output JSON (noninteractive)").action(async (opts) => {
|
|
18972
|
+
try {
|
|
18973
|
+
await researchSettings({
|
|
18974
|
+
project: resolveSpaceIdOption(opts),
|
|
18975
|
+
controllerUrl: opts.serverUrl,
|
|
18976
|
+
accessToken: opts.accessToken,
|
|
18977
|
+
maxPageLoads: opts.maxPageLoads,
|
|
18978
|
+
maxPageLoadsPerSite: opts.maxPageLoadsPerSite,
|
|
18979
|
+
interactive: opts.interactive,
|
|
18980
|
+
json: opts.json
|
|
18981
|
+
});
|
|
18982
|
+
} catch (error) {
|
|
18983
|
+
console.error(kleur20.red(String(error)));
|
|
18984
|
+
process.exit(1);
|
|
18985
|
+
}
|
|
18986
|
+
});
|
|
18396
18987
|
var previewOpenCommand = previewCommand.command("open").description(
|
|
18397
|
-
"Open a private preview of a local port (exit 2: nothing listening, 3: port not allowed, 5: previews not available)"
|
|
18398
|
-
).
|
|
18988
|
+
"Open a private preview of a local port (exit 2: nothing listening, 3: port not allowed or another site's, 5: previews not available)"
|
|
18989
|
+
).option("--port <port>", "Local port the site listens on (1024-65535); with --start, leave it out and a free one is picked").requiredOption("--title <title>", "Card title (at most 120 characters)").option("--path <path>", "Path the preview opens at (default /)").option("--summary <text>", `One line of facts under the card's title, e.g. "Trip \xB7 3 days \xB7 14 places" (at most 80 characters); default: the page's own meta description`).option("--thumbnail <file>", "Use this picture on the card (JPEG, PNG or WebP, at most 512 KiB and 2000\xD72000) instead of taking one").option("--no-thumbnail", "Put no picture of the site on the card");
|
|
18399
18990
|
addSpaceOption(previewOpenCommand, "Space UUID (defaults to .instafy/space.json or SPACE_ID)");
|
|
18400
18991
|
addServerUrlOptions(previewOpenCommand);
|
|
18401
18992
|
addAccessTokenOptions(previewOpenCommand, "Instafy access token (defaults to the job token, then the saved `instafy login` token)");
|
|
18402
18993
|
previewOpenCommand.option("--conversation <id>", "Conversation for the card (only needed without a job token)").option("--rathole-bin <path>", "Path to rathole binary (or set RATHOLE_BIN)").option(
|
|
18403
18994
|
"--serve-cmd <command>",
|
|
18404
|
-
"How to start the site again when the process that started it exits
|
|
18995
|
+
"How to start the site, and to start it again when the process that started it exits or the space sleeps, run from the current directory (one command, no shell); {port} in it is the site's port; default: read from the process listening on the port"
|
|
18405
18996
|
).option(
|
|
18406
18997
|
"--start",
|
|
18407
|
-
"Start --serve-cmd in the background, in its own session, without Instafy credentials, logging to .instafy/agent/logs, and wait for the port (
|
|
18998
|
+
"Start --serve-cmd in the background, in its own session, without Instafy credentials, logging to .instafy/agent/logs, and wait for the port. Without --port it picks a free port (this same site's earlier one, when it has one) and puts it in {port}. A port where this same site already runs is used as it is; one where another site runs is refused"
|
|
18999
|
+
).option(
|
|
19000
|
+
"--replace",
|
|
19001
|
+
"Show this site on the port even when the port's open preview shows another site (another title or folder); without it that open is refused"
|
|
18408
19002
|
).option("--json", "Output JSON").action(async (opts) => {
|
|
18409
19003
|
try {
|
|
18410
19004
|
const result = await previewOpen({
|
|
@@ -18418,6 +19012,7 @@ previewOpenCommand.option("--conversation <id>", "Conversation for the card (onl
|
|
|
18418
19012
|
ratholeBin: opts.ratholeBin,
|
|
18419
19013
|
serveCmd: opts.serveCmd,
|
|
18420
19014
|
start: Boolean(opts.start),
|
|
19015
|
+
replace: Boolean(opts.replace),
|
|
18421
19016
|
summary: opts.summary,
|
|
18422
19017
|
thumbnail: opts.thumbnail !== false,
|
|
18423
19018
|
thumbnailFile: typeof opts.thumbnail === "string" ? opts.thumbnail : void 0
|
|
@@ -18443,7 +19038,7 @@ previewListCommand.option("--json", "Output JSON").action(async (opts) => {
|
|
|
18443
19038
|
exitWithPreviewError(error);
|
|
18444
19039
|
}
|
|
18445
19040
|
});
|
|
18446
|
-
var previewCloseCommand = previewCommand.command("close").description("Close a preview: stop its local client and revoke it").argument("<previewId>", "Preview ID (from `instafy preview list`)");
|
|
19041
|
+
var previewCloseCommand = previewCommand.command("close").description("Close a preview: stop its local client and the site `open --start` started for it, and revoke it").argument("<previewId>", "Preview ID (from `instafy preview list`)");
|
|
18447
19042
|
addSpaceOption(previewCloseCommand, "Space UUID (defaults to .instafy/space.json or SPACE_ID)");
|
|
18448
19043
|
addServerUrlOptions(previewCloseCommand);
|
|
18449
19044
|
addAccessTokenOptions(previewCloseCommand, "Instafy access token (defaults to the job token, then the saved `instafy login` token)");
|
|
@@ -18459,7 +19054,7 @@ previewCloseCommand.option("--json", "Output JSON").action(async (previewId, opt
|
|
|
18459
19054
|
console.log(JSON.stringify(result, null, 2));
|
|
18460
19055
|
return;
|
|
18461
19056
|
}
|
|
18462
|
-
console.log(kleur20.green(`Preview closed: ${result.previewId}`));
|
|
19057
|
+
console.log(kleur20.green(`Preview closed: ${result.previewId}${result.siteStopped ? "; its site was stopped" : ""}`));
|
|
18463
19058
|
} catch (error) {
|
|
18464
19059
|
exitWithPreviewError(error);
|
|
18465
19060
|
}
|