@instafy/cli 0.4.1 → 0.5.0
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/server.mjs +4 -0
- package/dist/cli.js +909 -359
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1424,21 +1424,21 @@ var require_redaction = __commonJS({
|
|
|
1424
1424
|
return TEXT_MASK;
|
|
1425
1425
|
});
|
|
1426
1426
|
}
|
|
1427
|
-
text2 = text2.replace(digitCodePattern, (match, offset,
|
|
1428
|
-
const before =
|
|
1427
|
+
text2 = text2.replace(digitCodePattern, (match, offset, whole2) => {
|
|
1428
|
+
const before = whole2.slice(Math.max(0, offset - CODE_NEARBY_CHARACTERS), offset);
|
|
1429
1429
|
if (!codeWordPattern.test(before)) return match;
|
|
1430
1430
|
masked += 1;
|
|
1431
1431
|
matches += 1;
|
|
1432
1432
|
return TEXT_MASK;
|
|
1433
1433
|
});
|
|
1434
1434
|
opaqueCandidatePattern.lastIndex = 0;
|
|
1435
|
-
text2 = text2.replace(opaqueCandidatePattern, (match, offset,
|
|
1435
|
+
text2 = text2.replace(opaqueCandidatePattern, (match, offset, whole2) => {
|
|
1436
1436
|
const hexLike = uuidPattern.test(match) || longHexPattern.test(match);
|
|
1437
1437
|
if (!hexLike && !looksRandom3(match)) return match;
|
|
1438
1438
|
if (!strict && !credentialContext) {
|
|
1439
|
-
if (hexLike && identifierLabelPattern.test(
|
|
1439
|
+
if (hexLike && identifierLabelPattern.test(whole2.slice(Math.max(0, offset - 16), offset))) return match;
|
|
1440
1440
|
const window = hexLike ? HEX_NEARBY_CHARACTERS : NEARBY_CHARACTERS;
|
|
1441
|
-
const nearby =
|
|
1441
|
+
const nearby = whole2.slice(Math.max(0, offset - window), offset) + " " + whole2.slice(offset + match.length, offset + match.length + window);
|
|
1442
1442
|
if (!credentialProximityPattern.test(nearby)) return match;
|
|
1443
1443
|
}
|
|
1444
1444
|
count(match);
|
|
@@ -1461,8 +1461,8 @@ var require_redaction = __commonJS({
|
|
|
1461
1461
|
return value.length >= 16 && (looksRandom3(value, 16) || uuidPattern.test(value) || /^[0-9a-f]{16,}$/i.test(value));
|
|
1462
1462
|
}
|
|
1463
1463
|
function redactPairs(value, onMask) {
|
|
1464
|
-
return value.replace(/([^&#?=/]+)=([^&#]*)/g, (
|
|
1465
|
-
if (!sensitiveUrlValue(key, rawValue)) return
|
|
1464
|
+
return value.replace(/([^&#?=/]+)=([^&#]*)/g, (whole2, key, rawValue) => {
|
|
1465
|
+
if (!sensitiveUrlValue(key, rawValue)) return whole2;
|
|
1466
1466
|
onMask(decodeComponent(rawValue));
|
|
1467
1467
|
return `${key}=${URL_MASK}`;
|
|
1468
1468
|
});
|
|
@@ -1807,8 +1807,8 @@ var require_linkOnly = __commonJS({
|
|
|
1807
1807
|
if (url.username || url.password || url.port || url.hash) return { error: "has user info, a port or a fragment" };
|
|
1808
1808
|
if (url.hostname.includes(MARK) || !hostOnSite(hostOfUrl2(url), patterns)) return { error: "is not on the site's own hosts" };
|
|
1809
1809
|
const placeholderIn = (text2) => {
|
|
1810
|
-
const
|
|
1811
|
-
if (
|
|
1810
|
+
const whole2 = new RegExp(`^${MARK}(\\d+)x$`).exec(text2);
|
|
1811
|
+
if (whole2) return { name: names[Number(whole2[1])] };
|
|
1812
1812
|
return text2.includes(MARK) ? void 0 : null;
|
|
1813
1813
|
};
|
|
1814
1814
|
const segments = [];
|
|
@@ -1965,8 +1965,8 @@ var require_linkOnly = __commonJS({
|
|
|
1965
1965
|
for (const run of decoded(text2).split(HOST_BREAK)) {
|
|
1966
1966
|
if (names.size > MAX_EMBEDDED_NAMES) break;
|
|
1967
1967
|
if (!run) continue;
|
|
1968
|
-
const
|
|
1969
|
-
if (
|
|
1968
|
+
const whole2 = embeddedName(run);
|
|
1969
|
+
if (whole2) names.add(whole2);
|
|
1970
1970
|
for (const piece of run.split(NOT_NAME_CHARACTER)) {
|
|
1971
1971
|
const name = piece && piece !== run ? embeddedName(piece) : null;
|
|
1972
1972
|
if (name) names.add(name);
|
|
@@ -6795,7 +6795,9 @@ async function tailTunnelLogs(options) {
|
|
|
6795
6795
|
|
|
6796
6796
|
// src/preview.ts
|
|
6797
6797
|
import { spawn as spawn6 } from "node:child_process";
|
|
6798
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
6798
6799
|
import fs12 from "node:fs";
|
|
6800
|
+
import net5 from "node:net";
|
|
6799
6801
|
import os7 from "node:os";
|
|
6800
6802
|
import path13 from "node:path";
|
|
6801
6803
|
import kleur8 from "kleur";
|
|
@@ -7110,6 +7112,7 @@ var MAX_HEADER_BYTES = 128 * 1024;
|
|
|
7110
7112
|
|
|
7111
7113
|
// src/web-read.ts
|
|
7112
7114
|
var redaction = __toESM(require_redaction(), 1);
|
|
7115
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
7113
7116
|
import dns2 from "node:dns";
|
|
7114
7117
|
import http3 from "node:http";
|
|
7115
7118
|
import https from "node:https";
|
|
@@ -8110,12 +8113,109 @@ function loadWebFixtures(folder) {
|
|
|
8110
8113
|
};
|
|
8111
8114
|
}
|
|
8112
8115
|
|
|
8116
|
+
// src/web-budget.ts
|
|
8117
|
+
var ResearchBudgetError = class extends Error {
|
|
8118
|
+
};
|
|
8119
|
+
function invalidResponse() {
|
|
8120
|
+
throw new ResearchBudgetError("The controller returned an invalid research allowance. No further pages were loaded.");
|
|
8121
|
+
}
|
|
8122
|
+
function object(value) {
|
|
8123
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8124
|
+
}
|
|
8125
|
+
function whole(value, min, max) {
|
|
8126
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= min && value <= max;
|
|
8127
|
+
}
|
|
8128
|
+
function readBudget(value) {
|
|
8129
|
+
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();
|
|
8130
|
+
for (const entry of Object.values(value.hosts)) {
|
|
8131
|
+
if (!object(entry) || !whole(entry.loads, 0, value.loadsPerHost) || entry.lastAt !== void 0 && (typeof entry.lastAt !== "number" || !Number.isFinite(entry.lastAt))) invalidResponse();
|
|
8132
|
+
}
|
|
8133
|
+
return value;
|
|
8134
|
+
}
|
|
8135
|
+
function sharedResearchBudget(env, fetcher = globalThis.fetch) {
|
|
8136
|
+
const enabled = env.INSTAFY_RESEARCH_SHARED?.trim();
|
|
8137
|
+
const rawUrl = env.INSTAFY_RESEARCH_CONTROLLER_URL?.trim();
|
|
8138
|
+
const token = env.INSTAFY_RESEARCH_ACCESS_TOKEN?.trim();
|
|
8139
|
+
if (enabled === void 0 && rawUrl === void 0 && token === void 0) return null;
|
|
8140
|
+
if (enabled !== "1" || !rawUrl || !token || /[\r\n]/.test(token)) {
|
|
8141
|
+
throw new ResearchBudgetError("This task requires a shared research allowance, but its runtime binding is incomplete. No pages were loaded.");
|
|
8142
|
+
}
|
|
8143
|
+
let url;
|
|
8144
|
+
try {
|
|
8145
|
+
url = new URL(rawUrl);
|
|
8146
|
+
if (!/^https?:$/.test(url.protocol) || url.username || url.password || url.search || url.hash || url.pathname !== "/") throw new Error();
|
|
8147
|
+
} catch {
|
|
8148
|
+
throw new ResearchBudgetError("The task's research controller binding is invalid. No pages were loaded.");
|
|
8149
|
+
}
|
|
8150
|
+
return new SharedResearchBudget(new URL("/agent/research-budget", url.origin), token, fetcher);
|
|
8151
|
+
}
|
|
8152
|
+
var SharedResearchBudget = class {
|
|
8153
|
+
constructor(endpoint, token, fetcher) {
|
|
8154
|
+
this.endpoint = endpoint;
|
|
8155
|
+
this.token = token;
|
|
8156
|
+
this.fetcher = fetcher;
|
|
8157
|
+
this.current = null;
|
|
8158
|
+
}
|
|
8159
|
+
async request(body, remainingMs = 5e3) {
|
|
8160
|
+
const deadline = Date.now() + Math.max(1, Math.min(5e3, body?.remainingMs ?? remainingMs));
|
|
8161
|
+
for (let attempt2 = 0; attempt2 < 2; attempt2 += 1) {
|
|
8162
|
+
const abort = new AbortController();
|
|
8163
|
+
const timer = setTimeout(() => abort.abort(), Math.max(1, deadline - Date.now()));
|
|
8164
|
+
try {
|
|
8165
|
+
let response;
|
|
8166
|
+
try {
|
|
8167
|
+
response = await this.fetcher(this.endpoint.href, {
|
|
8168
|
+
method: body ? "POST" : "GET",
|
|
8169
|
+
headers: {
|
|
8170
|
+
Authorization: `Bearer ${this.token}`,
|
|
8171
|
+
Accept: "application/json",
|
|
8172
|
+
...body ? { "Content-Type": "application/json" } : {}
|
|
8173
|
+
},
|
|
8174
|
+
...body ? { body: JSON.stringify(body) } : {},
|
|
8175
|
+
redirect: "error",
|
|
8176
|
+
signal: abort.signal
|
|
8177
|
+
});
|
|
8178
|
+
} catch {
|
|
8179
|
+
if (attempt2 === 0 && !abort.signal.aborted && Date.now() < deadline) continue;
|
|
8180
|
+
throw new ResearchBudgetError("The task's shared research allowance is unavailable. No further pages were loaded; do not switch to a local allowance.");
|
|
8181
|
+
}
|
|
8182
|
+
if (!response.ok) {
|
|
8183
|
+
throw new ResearchBudgetError(`The controller refused the shared research allowance (HTTP ${response.status}). No further pages were loaded.`);
|
|
8184
|
+
}
|
|
8185
|
+
let value;
|
|
8186
|
+
try {
|
|
8187
|
+
value = await response.json();
|
|
8188
|
+
} catch {
|
|
8189
|
+
invalidResponse();
|
|
8190
|
+
}
|
|
8191
|
+
if (!object(value)) invalidResponse();
|
|
8192
|
+
return value;
|
|
8193
|
+
} finally {
|
|
8194
|
+
clearTimeout(timer);
|
|
8195
|
+
}
|
|
8196
|
+
}
|
|
8197
|
+
throw new ResearchBudgetError("The task's shared research allowance is unavailable.");
|
|
8198
|
+
}
|
|
8199
|
+
async status(remainingMs = 5e3) {
|
|
8200
|
+
const response = await this.request(void 0, remainingMs);
|
|
8201
|
+
this.current = readBudget(response.budget);
|
|
8202
|
+
return this.current;
|
|
8203
|
+
}
|
|
8204
|
+
async reserve(loadId, requestId, host, remainingMs) {
|
|
8205
|
+
const response = await this.request({ loadId, requestId, host, remainingMs: Math.max(0, Math.min(6e5, Math.trunc(remainingMs))) });
|
|
8206
|
+
const budget = readBudget(response.budget);
|
|
8207
|
+
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();
|
|
8208
|
+
this.current = budget;
|
|
8209
|
+
return { allowed: response.allowed, ...response.code ? { code: response.code } : {}, waitMs: response.waitMs, budget };
|
|
8210
|
+
}
|
|
8211
|
+
};
|
|
8212
|
+
|
|
8113
8213
|
// src/web-read.ts
|
|
8114
8214
|
var ROBOTS_AGENTS = ["*", "instafy-agent", ...AI_AGENTS];
|
|
8115
8215
|
var LIMITS = Object.freeze({
|
|
8116
8216
|
urlsPerCall: 6,
|
|
8117
|
-
loadsPerTurn:
|
|
8118
|
-
loadsPerHost:
|
|
8217
|
+
loadsPerTurn: 100,
|
|
8218
|
+
loadsPerHost: 25,
|
|
8119
8219
|
hostSpacingMs: 2e3,
|
|
8120
8220
|
callMs: 6e4,
|
|
8121
8221
|
pageTimeoutMs: 2e4,
|
|
@@ -8377,6 +8477,7 @@ var TELL = {
|
|
|
8377
8477
|
http_error: "The site answered with an error. Use another source rather than retrying.",
|
|
8378
8478
|
unreachable: "The site did not answer. Use another source rather than retrying.",
|
|
8379
8479
|
timeout: "The site did not answer in time. Use another source rather than retrying.",
|
|
8480
|
+
budget_unavailable: "Research stopped because its shared allowance could not be checked. Keep the evidence already collected; further fresh reads require controller approval.",
|
|
8380
8481
|
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
8482
|
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
8483
|
};
|
|
@@ -8499,7 +8600,7 @@ async function readOne(rawUrl, options, ctx) {
|
|
|
8499
8600
|
status: "skipped",
|
|
8500
8601
|
code,
|
|
8501
8602
|
message,
|
|
8502
|
-
tell: TELL[code]
|
|
8603
|
+
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
8604
|
});
|
|
8504
8605
|
let sibling = wwwSibling(requestedUrl);
|
|
8505
8606
|
const fail = (code, message, extra2 = {}) => ({
|
|
@@ -8531,15 +8632,24 @@ async function readOne(rawUrl, options, ctx) {
|
|
|
8531
8632
|
return present(page, cached, true, options, ctx.guard);
|
|
8532
8633
|
}
|
|
8533
8634
|
}
|
|
8534
|
-
if (
|
|
8535
|
-
return skip("
|
|
8536
|
-
|
|
8537
|
-
|
|
8538
|
-
|
|
8635
|
+
if (ctx.sharedBudget) {
|
|
8636
|
+
if (ctx.deadline - ctx.now() < 1e3) return skip("time_limit", "this call reached its time limit before this URL");
|
|
8637
|
+
const budget = await ctx.sharedBudget.status(ctx.deadline - ctx.now());
|
|
8638
|
+
if (budget.loadsLeft === 0) return skip("turn_limit", `this task already loaded ${budget.loadsUsed} pages (limit ${budget.loadsPerTurn})`);
|
|
8639
|
+
const used = budget.hosts[firstSite]?.loads ?? 0;
|
|
8640
|
+
if (used >= budget.loadsPerHost) return skip("host_limit", `this task already read ${firstSite} ${used} times (limit ${budget.loadsPerHost})`);
|
|
8641
|
+
} else {
|
|
8642
|
+
if (earlier.loads >= LIMITS.loadsPerTurn) {
|
|
8643
|
+
return skip("turn_limit", `this turn already loaded ${earlier.loads} pages (limit ${LIMITS.loadsPerTurn})`);
|
|
8644
|
+
}
|
|
8645
|
+
if (earlier.hostLoads >= LIMITS.loadsPerHost) {
|
|
8646
|
+
return skip("host_limit", `this turn already read ${firstSite} ${earlier.hostLoads} times (limit ${LIMITS.loadsPerHost})`);
|
|
8647
|
+
}
|
|
8539
8648
|
}
|
|
8540
8649
|
if (ctx.deadline - ctx.now() < 1e3) return skip("time_limit", "this call reached its time limit before this URL");
|
|
8541
8650
|
const redirects = [];
|
|
8542
8651
|
const chainSites = /* @__PURE__ */ new Set();
|
|
8652
|
+
const loadId = randomUUID2();
|
|
8543
8653
|
let current = new URL(requested);
|
|
8544
8654
|
let response = null;
|
|
8545
8655
|
for (let hop = 0; ; hop += 1) {
|
|
@@ -8564,7 +8674,21 @@ async function readOne(rawUrl, options, ctx) {
|
|
|
8564
8674
|
const robots = await robotsVerdict(current, ctx);
|
|
8565
8675
|
if ("failed" in robots) return fail(robots.failed, via + robots.message, hopExtra);
|
|
8566
8676
|
if (robots.refused) return stop("robots", via + robots.refused, host2, hopExtra);
|
|
8567
|
-
if (
|
|
8677
|
+
if (ctx.sharedBudget) {
|
|
8678
|
+
const remainingMs = ctx.deadline - ctx.now();
|
|
8679
|
+
if (remainingMs < 1e3) return skip("time_limit", `${via}this call reached its time limit before reserving a load`, hopExtra);
|
|
8680
|
+
const reservation = await ctx.sharedBudget.reserve(loadId, randomUUID2(), site, remainingMs - 1e3);
|
|
8681
|
+
if (!reservation.allowed) {
|
|
8682
|
+
const code = reservation.code;
|
|
8683
|
+
const budget = reservation.budget;
|
|
8684
|
+
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}`;
|
|
8685
|
+
return skip(code, via + message, hopExtra);
|
|
8686
|
+
}
|
|
8687
|
+
if (reservation.waitMs + 1e3 > ctx.deadline - ctx.now()) {
|
|
8688
|
+
return skip("time_limit", `${via}this call reached its time limit while reserving the next paced read`, hopExtra);
|
|
8689
|
+
}
|
|
8690
|
+
if (reservation.waitMs > 0) await ctx.sleep(reservation.waitMs);
|
|
8691
|
+
} else if (!chainSites.has(site)) {
|
|
8568
8692
|
const now = ctx.now();
|
|
8569
8693
|
const reservation = await ctx.turn.update((state) => {
|
|
8570
8694
|
if (hop === 0 && state.loads >= LIMITS.loadsPerTurn) {
|
|
@@ -8775,6 +8899,7 @@ async function webRead(options, deps = {}) {
|
|
|
8775
8899
|
now,
|
|
8776
8900
|
sleep: deps.sleep ?? realSleep,
|
|
8777
8901
|
turn,
|
|
8902
|
+
sharedBudget: sharedResearchBudget(env, deps.budgetFetch),
|
|
8778
8903
|
cache: new Cache(cacheDir === null ? null : apart(cacheDir)),
|
|
8779
8904
|
guard,
|
|
8780
8905
|
viewer,
|
|
@@ -8782,32 +8907,38 @@ async function webRead(options, deps = {}) {
|
|
|
8782
8907
|
robotsMemo: /* @__PURE__ */ new Map()
|
|
8783
8908
|
};
|
|
8784
8909
|
const pages = [];
|
|
8785
|
-
|
|
8786
|
-
|
|
8910
|
+
let interruptedBudget = null;
|
|
8911
|
+
for (const [index, url] of urls.entries()) {
|
|
8912
|
+
let page;
|
|
8913
|
+
try {
|
|
8914
|
+
page = await readOne(url, { maxChars, links, fresh: Boolean(options.fresh), terms }, ctx);
|
|
8915
|
+
} catch (error) {
|
|
8916
|
+
if (!(error instanceof ResearchBudgetError) || !ctx.sharedBudget?.current || !pages.length) throw error;
|
|
8917
|
+
interruptedBudget = ctx.sharedBudget.current;
|
|
8918
|
+
pages.push(...urls.slice(index).map((remaining) => ({
|
|
8919
|
+
...basePage(remaining, new Date(now()).toISOString(), viewer),
|
|
8920
|
+
code: "budget_unavailable",
|
|
8921
|
+
message: error.message,
|
|
8922
|
+
tell: TELL.budget_unavailable
|
|
8923
|
+
})));
|
|
8924
|
+
break;
|
|
8925
|
+
}
|
|
8787
8926
|
if (page.status === "failed" && unanswered(page.code, page)) {
|
|
8788
8927
|
const key = unansweredKey(new URL(url));
|
|
8789
|
-
await turn.update((
|
|
8790
|
-
(
|
|
8928
|
+
await turn.update((state) => {
|
|
8929
|
+
(state.unanswered ??= {})[key] = now();
|
|
8791
8930
|
});
|
|
8792
8931
|
}
|
|
8793
8932
|
pages.push(page);
|
|
8794
8933
|
}
|
|
8795
|
-
const
|
|
8934
|
+
const budget = interruptedBudget ? lastKnownBudget(interruptedBudget, warnings) : ctx.sharedBudget ? await finalSharedBudget(ctx.sharedBudget, deadline - now(), warnings) : await localBudget(turn, scope.label);
|
|
8796
8935
|
return {
|
|
8797
8936
|
ok: true,
|
|
8798
8937
|
viewer,
|
|
8799
8938
|
userAgent: USER_AGENT,
|
|
8800
8939
|
notice: NOTICE,
|
|
8801
8940
|
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
|
-
},
|
|
8941
|
+
budget,
|
|
8811
8942
|
guards: {
|
|
8812
8943
|
linkOnly: {
|
|
8813
8944
|
sites: guard.list.sites.length,
|
|
@@ -8821,6 +8952,63 @@ async function webRead(options, deps = {}) {
|
|
|
8821
8952
|
warnings
|
|
8822
8953
|
};
|
|
8823
8954
|
}
|
|
8955
|
+
function reportBudget(budget) {
|
|
8956
|
+
return { ...budget, hosts: Object.fromEntries(Object.entries(budget.hosts).map(([host, value]) => [host, value.loads])) };
|
|
8957
|
+
}
|
|
8958
|
+
function lastKnownBudget(budget, warnings) {
|
|
8959
|
+
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.");
|
|
8960
|
+
return { ...reportBudget(budget), lastKnown: true };
|
|
8961
|
+
}
|
|
8962
|
+
async function finalSharedBudget(shared, remainingMs, warnings) {
|
|
8963
|
+
try {
|
|
8964
|
+
if (remainingMs <= 0) throw new ResearchBudgetError("The call deadline elapsed before the shared research allowance could be refreshed.");
|
|
8965
|
+
return reportBudget(await shared.status(remainingMs));
|
|
8966
|
+
} catch (error) {
|
|
8967
|
+
if (!(error instanceof ResearchBudgetError) || !shared.current) throw error;
|
|
8968
|
+
return lastKnownBudget(shared.current, warnings);
|
|
8969
|
+
}
|
|
8970
|
+
}
|
|
8971
|
+
async function localBudget(turn, scope) {
|
|
8972
|
+
return turn.update((state) => ({
|
|
8973
|
+
scope,
|
|
8974
|
+
loadsUsed: state.loads,
|
|
8975
|
+
loadsLeft: Math.max(0, LIMITS.loadsPerTurn - state.loads),
|
|
8976
|
+
loadsPerTurn: LIMITS.loadsPerTurn,
|
|
8977
|
+
loadsPerHost: LIMITS.loadsPerHost,
|
|
8978
|
+
hostSpacingSeconds: LIMITS.hostSpacingMs / 1e3,
|
|
8979
|
+
hosts: Object.fromEntries(Object.entries(state.hosts).map(([host, value]) => [host, value.loads]))
|
|
8980
|
+
}));
|
|
8981
|
+
}
|
|
8982
|
+
async function webBudget(deps = {}) {
|
|
8983
|
+
const env = deps.env ?? process.env;
|
|
8984
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
8985
|
+
const shared = sharedResearchBudget(env, deps.budgetFetch);
|
|
8986
|
+
if (shared) return { budget: reportBudget(await shared.status()) };
|
|
8987
|
+
const fixtureFolder = webFixturesFolder(env, cwd);
|
|
8988
|
+
const fixtures = fixtureFolder ? loadWebFixtures(fixtureFolder) : null;
|
|
8989
|
+
const dirs = webDirs("web-read", env, cwd);
|
|
8990
|
+
const stateDir = deps.stateDir ?? dirs.stateDir;
|
|
8991
|
+
const scope = turnScope(env);
|
|
8992
|
+
const turn = new TurnStore(
|
|
8993
|
+
fixtures ? path11.join(stateDir, "fixtures", sha256(fixtures.folder).slice(0, 16)) : stateDir,
|
|
8994
|
+
scope,
|
|
8995
|
+
deps.now ?? Date.now
|
|
8996
|
+
);
|
|
8997
|
+
return { budget: await localBudget(turn, scope.label) };
|
|
8998
|
+
}
|
|
8999
|
+
async function runWebBudgetCli(opts, io = { out: console.log, err: console.error }, deps = {}) {
|
|
9000
|
+
try {
|
|
9001
|
+
const report = await webBudget(deps);
|
|
9002
|
+
const b = report.budget;
|
|
9003
|
+
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).`);
|
|
9004
|
+
return WEB_READ_EXIT_OK;
|
|
9005
|
+
} catch (error) {
|
|
9006
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9007
|
+
if (opts.json) io.out(JSON.stringify({ ok: false, exitCode: WEB_READ_EXIT_INTERNAL, error: message }));
|
|
9008
|
+
else io.err(message);
|
|
9009
|
+
return WEB_READ_EXIT_INTERNAL;
|
|
9010
|
+
}
|
|
9011
|
+
}
|
|
8824
9012
|
function clamp(value, min, max) {
|
|
8825
9013
|
return Math.min(max, Math.max(min, Math.trunc(value)));
|
|
8826
9014
|
}
|
|
@@ -8848,13 +9036,13 @@ function formatWebRead(report) {
|
|
|
8848
9036
|
}
|
|
8849
9037
|
out.push("");
|
|
8850
9038
|
}
|
|
8851
|
-
out.push(`${report.budget.loadsUsed} of ${report.budget.loadsPerTurn} page loads used in ${report.budget.scope}.`);
|
|
9039
|
+
out.push(`${report.budget.lastKnown ? "Last known controller count: " : ""}${report.budget.loadsUsed} of ${report.budget.loadsPerTurn} page loads used in ${report.budget.scope}.`);
|
|
8852
9040
|
for (const warning of report.warnings) out.push(`warning: ${warning}`);
|
|
8853
9041
|
out.push(report.notice);
|
|
8854
9042
|
return out.join("\n");
|
|
8855
9043
|
}
|
|
8856
9044
|
async function runWebReadCli(urls, opts, io = { out: console.log, err: console.error }, deps = {}) {
|
|
8857
|
-
const
|
|
9045
|
+
const whole2 = (raw, name, min, max) => {
|
|
8858
9046
|
if (raw === void 0) return void 0;
|
|
8859
9047
|
const n = Number(raw);
|
|
8860
9048
|
if (!Number.isInteger(n) || n < min || n > max) throw new WebReadUsageError(`${name} must be a whole number from ${min} to ${max}.`);
|
|
@@ -8865,8 +9053,8 @@ async function runWebReadCli(urls, opts, io = { out: console.log, err: console.e
|
|
|
8865
9053
|
{
|
|
8866
9054
|
urls,
|
|
8867
9055
|
find: opts.find,
|
|
8868
|
-
maxChars:
|
|
8869
|
-
links:
|
|
9056
|
+
maxChars: whole2(opts.maxChars, "--max-chars", LIMITS.minChars, LIMITS.maxChars),
|
|
9057
|
+
links: whole2(opts.links, "--links", 0, LIMITS.maxLinks),
|
|
8870
9058
|
fresh: opts.fresh
|
|
8871
9059
|
},
|
|
8872
9060
|
deps
|
|
@@ -8876,7 +9064,7 @@ async function runWebReadCli(urls, opts, io = { out: console.log, err: console.e
|
|
|
8876
9064
|
} catch (error) {
|
|
8877
9065
|
const usage = error instanceof WebReadUsageError;
|
|
8878
9066
|
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
|
|
9067
|
+
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
9068
|
if (opts.json) io.out(JSON.stringify({ ok: false, exitCode: code, error: message }));
|
|
8881
9069
|
else io.err(message);
|
|
8882
9070
|
return code;
|
|
@@ -8887,10 +9075,13 @@ function registerWebCommand(program2) {
|
|
|
8887
9075
|
"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
9076
|
);
|
|
8889
9077
|
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).
|
|
9078
|
+
`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
9079
|
).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
9080
|
process.exitCode = await runWebReadCli(urls, opts);
|
|
8893
9081
|
});
|
|
9082
|
+
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) => {
|
|
9083
|
+
process.exitCode = await runWebBudgetCli(opts);
|
|
9084
|
+
});
|
|
8894
9085
|
return web;
|
|
8895
9086
|
}
|
|
8896
9087
|
|
|
@@ -9314,6 +9505,18 @@ async function readPageDescription(frame) {
|
|
|
9314
9505
|
const raw = await frame.evaluate(() => document.querySelector('meta[name="description" i]')?.getAttribute("content") ?? null).catch(() => null);
|
|
9315
9506
|
return cleanDescription(raw);
|
|
9316
9507
|
}
|
|
9508
|
+
var READY_MARK_ATTRIBUTE = "data-instafy-ready";
|
|
9509
|
+
var READY_MARK_WAIT_MS = 3e4;
|
|
9510
|
+
async function waitForReadyMark(frame, timeoutMs) {
|
|
9511
|
+
const mark = await frame.evaluate((name) => document.documentElement?.getAttribute(name) ?? null, READY_MARK_ATTRIBUTE).catch(() => null);
|
|
9512
|
+
if (mark === null) return "none";
|
|
9513
|
+
if (mark === "true") return "ready";
|
|
9514
|
+
if (timeoutMs <= 0) return "late";
|
|
9515
|
+
return frame.waitForFunction((name) => document.documentElement?.getAttribute(name) === "true", READY_MARK_ATTRIBUTE, {
|
|
9516
|
+
timeout: timeoutMs,
|
|
9517
|
+
polling: 100
|
|
9518
|
+
}).then(() => "ready").catch(() => "late");
|
|
9519
|
+
}
|
|
9317
9520
|
async function pageThumbnail(page, context) {
|
|
9318
9521
|
const cdp = await context.newCDPSession(page);
|
|
9319
9522
|
try {
|
|
@@ -9519,10 +9722,19 @@ async function checkViewport(browser, target, vp, siteOrigin, outDir, timeoutMs,
|
|
|
9519
9722
|
(ours(u) ? errors : warnings).push(`HTTP ${s}: ${shortUrl(u)}`);
|
|
9520
9723
|
});
|
|
9521
9724
|
await page.goto(target.pageUrl, { waitUntil: "load", timeout: timeoutMs });
|
|
9522
|
-
|
|
9523
|
-
|
|
9524
|
-
|
|
9525
|
-
|
|
9725
|
+
const siteFrame = () => target.framed ? page.frames().find((f) => f !== page.mainFrame() && isSiteUrl(String(f.url()), siteOrigin)) : page.mainFrame();
|
|
9726
|
+
const readiness = await waitForReadyMark(
|
|
9727
|
+
siteFrame() ?? page.mainFrame(),
|
|
9728
|
+
Math.max(0, Math.min(READY_MARK_WAIT_MS, timeoutMs) - (Date.now() - t0))
|
|
9729
|
+
);
|
|
9730
|
+
if (readiness === "none") {
|
|
9731
|
+
await page.waitForLoadState("networkidle", { timeout: Math.min(8e3, timeoutMs) }).catch(() => {
|
|
9732
|
+
warnings.push("the network was still busy after 8 s (screenshot taken anyway)");
|
|
9733
|
+
});
|
|
9734
|
+
} else if (readiness === "late") {
|
|
9735
|
+
warnings.push(`the page still said it was drawing (${READY_MARK_ATTRIBUTE}) after ${Math.round((Date.now() - t0) / 1e3)} s; screenshot taken anyway`);
|
|
9736
|
+
}
|
|
9737
|
+
const frame = siteFrame();
|
|
9526
9738
|
let framed = "ok";
|
|
9527
9739
|
let snap = { text: 0, media: 0, elements: 0, title: "" };
|
|
9528
9740
|
if (refused) framed = "refused";
|
|
@@ -9557,7 +9769,7 @@ async function checkViewport(browser, target, vp, siteOrigin, outDir, timeoutMs,
|
|
|
9557
9769
|
let thumbnail;
|
|
9558
9770
|
if (takeThumbnail) {
|
|
9559
9771
|
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)}` }));
|
|
9772
|
+
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
9773
|
}
|
|
9562
9774
|
return {
|
|
9563
9775
|
name: vp.name,
|
|
@@ -9767,6 +9979,8 @@ function thumbnailBudgetMs(env = process.env) {
|
|
|
9767
9979
|
}
|
|
9768
9980
|
var NETWORK_IDLE_MS = 6e3;
|
|
9769
9981
|
var SETTLE_MS = 1e3;
|
|
9982
|
+
var READY_SETTLE_MS = 300;
|
|
9983
|
+
var SHOT_RESERVE_MS = 3e3;
|
|
9770
9984
|
var MAX_THUMBNAIL_FILE_BYTES = 512 * 1024;
|
|
9771
9985
|
var MAX_HTML_BYTES = 512 * 1024;
|
|
9772
9986
|
var MIN_SIDE = 64;
|
|
@@ -9923,7 +10137,8 @@ function errorLine(error) {
|
|
|
9923
10137
|
async function captureSiteThumbnail(options) {
|
|
9924
10138
|
const env = options.env ?? process.env;
|
|
9925
10139
|
const budget = options.budgetMs ?? thumbnailBudgetMs(env);
|
|
9926
|
-
const
|
|
10140
|
+
const began = Date.now();
|
|
10141
|
+
let deadline = began + budget;
|
|
9927
10142
|
const host = options.host === "::1" ? "[::1]" : "127.0.0.1";
|
|
9928
10143
|
const siteOrigin = `http://${host}:${options.port}`;
|
|
9929
10144
|
const left = () => Math.max(0, deadline - Date.now());
|
|
@@ -9985,9 +10200,18 @@ async function captureSiteThumbnail(options) {
|
|
|
9985
10200
|
if (response && response.status() >= 400) {
|
|
9986
10201
|
return { ok: false, status: "skipped", reason: `the page answered HTTP ${response.status()}` };
|
|
9987
10202
|
}
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
10203
|
+
const mark = await waitForReadyMark(page.mainFrame(), 0);
|
|
10204
|
+
if (mark === "none") {
|
|
10205
|
+
await page.waitForLoadState("networkidle", { timeout: Math.max(1, Math.min(NETWORK_IDLE_MS, left())) }).catch(() => {
|
|
10206
|
+
});
|
|
10207
|
+
await page.waitForTimeout(Math.min(SETTLE_MS, left()));
|
|
10208
|
+
} else {
|
|
10209
|
+
deadline = Math.max(deadline, began + (options.readyWaitMs ?? READY_MARK_WAIT_MS));
|
|
10210
|
+
if (mark === "late" && await waitForReadyMark(page.mainFrame(), Math.max(0, left() - SHOT_RESERVE_MS)) !== "ready") {
|
|
10211
|
+
return { ok: false, status: "skipped", reason: `the page still said it was drawing after ${Math.round((Date.now() - began) / 1e3)} s` };
|
|
10212
|
+
}
|
|
10213
|
+
await page.waitForTimeout(Math.min(READY_SETTLE_MS, left()));
|
|
10214
|
+
}
|
|
9991
10215
|
const description = await readPageDescription(page.mainFrame());
|
|
9992
10216
|
const shot = await pageThumbnail(page, context);
|
|
9993
10217
|
if ("skipped" in shot) return { ok: false, status: "skipped", reason: shot.skipped };
|
|
@@ -10002,7 +10226,16 @@ async function captureSiteThumbnail(options) {
|
|
|
10002
10226
|
};
|
|
10003
10227
|
})();
|
|
10004
10228
|
const outOfTime = new Promise((resolve) => {
|
|
10005
|
-
|
|
10229
|
+
const arm = () => {
|
|
10230
|
+
timer = setTimeout(() => {
|
|
10231
|
+
if (Date.now() < deadline) {
|
|
10232
|
+
arm();
|
|
10233
|
+
return;
|
|
10234
|
+
}
|
|
10235
|
+
resolve({ ok: false, status: "failed", reason: `the picture took longer than ${Math.round((deadline - began) / 1e3)} s` });
|
|
10236
|
+
}, Math.max(0, deadline - Date.now()));
|
|
10237
|
+
};
|
|
10238
|
+
arm();
|
|
10006
10239
|
});
|
|
10007
10240
|
try {
|
|
10008
10241
|
return await Promise.race([work.catch((error) => ({ ok: false, status: "failed", reason: errorLine(error) })), outOfTime]);
|
|
@@ -10026,6 +10259,12 @@ var MAX_PATH_LENGTH2 = 512;
|
|
|
10026
10259
|
var THUMBNAIL_UPLOAD_TIMEOUT_MS = 15e3;
|
|
10027
10260
|
var STATE_LOCK_WAIT_MS = 12e4;
|
|
10028
10261
|
var STATE_LOCK_STALE_MS = 10 * 6e4;
|
|
10262
|
+
var PORT_PLACEHOLDER = "{port}";
|
|
10263
|
+
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.";
|
|
10264
|
+
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.";
|
|
10265
|
+
var AUTO_PORT_FIRST = 2e4;
|
|
10266
|
+
var AUTO_PORT_COUNT = 1e4;
|
|
10267
|
+
var AUTO_PORT_TRIES = 64;
|
|
10029
10268
|
function instafyDir() {
|
|
10030
10269
|
return path13.join(os7.homedir(), ".instafy");
|
|
10031
10270
|
}
|
|
@@ -10249,6 +10488,12 @@ async function controllerRequest(params) {
|
|
|
10249
10488
|
PREVIEW_EXIT_PORT_NOT_ALLOWED
|
|
10250
10489
|
);
|
|
10251
10490
|
}
|
|
10491
|
+
if (response.status === 409 && code === "port_in_use") {
|
|
10492
|
+
throw new PreviewCommandError(
|
|
10493
|
+
`${extractControllerErrorMessage(text2) ?? "Another site's preview holds this port."} ${params.portTakenHint ?? PORT_TAKEN_HINT}`,
|
|
10494
|
+
PREVIEW_EXIT_PORT_NOT_ALLOWED
|
|
10495
|
+
);
|
|
10496
|
+
}
|
|
10252
10497
|
const message = extractControllerErrorMessage(text2) ?? response.statusText;
|
|
10253
10498
|
throw new Error(`Preview request failed (${response.status}): ${message}`);
|
|
10254
10499
|
}
|
|
@@ -10457,12 +10702,12 @@ function listeningSocketInodes(procRoot, port) {
|
|
|
10457
10702
|
continue;
|
|
10458
10703
|
}
|
|
10459
10704
|
for (const line of text2.split("\n").slice(1)) {
|
|
10460
|
-
const
|
|
10461
|
-
if (
|
|
10462
|
-
const local =
|
|
10705
|
+
const fields2 = line.trim().split(/\s+/);
|
|
10706
|
+
if (fields2.length < 10) continue;
|
|
10707
|
+
const local = fields2[1] ?? "";
|
|
10463
10708
|
const portHex = local.slice(local.lastIndexOf(":") + 1);
|
|
10464
|
-
if (
|
|
10465
|
-
inodes.add(
|
|
10709
|
+
if (fields2[3] === "0A" && Number.parseInt(portHex, 16) === port && fields2[9] !== "0") {
|
|
10710
|
+
inodes.add(fields2[9]);
|
|
10466
10711
|
}
|
|
10467
10712
|
}
|
|
10468
10713
|
}
|
|
@@ -10638,8 +10883,159 @@ async function startPreviewSite(params) {
|
|
|
10638
10883
|
await new Promise((resolve) => setTimeout(resolve, START_POLL_MS));
|
|
10639
10884
|
}
|
|
10640
10885
|
}
|
|
10886
|
+
function withPort(argv, port) {
|
|
10887
|
+
return argv.map((arg) => arg.split(PORT_PLACEHOLDER).join(String(port)));
|
|
10888
|
+
}
|
|
10889
|
+
function sameArgv(a, b) {
|
|
10890
|
+
return a.length === b.length && a.every((arg, index) => arg === b[index]);
|
|
10891
|
+
}
|
|
10892
|
+
function sameStart(a, b) {
|
|
10893
|
+
return Boolean(a && Array.isArray(a.argv) && a.cwd === b.cwd && sameArgv(a.argv, b.argv));
|
|
10894
|
+
}
|
|
10895
|
+
function preferredSitePort(start) {
|
|
10896
|
+
const digest2 = createHash2("sha256").update(`${start.cwd}\0${start.argv.join("\0")}`).digest();
|
|
10897
|
+
return AUTO_PORT_FIRST + digest2.readUInt32BE(0) % AUTO_PORT_COUNT;
|
|
10898
|
+
}
|
|
10899
|
+
async function portIsFree(port) {
|
|
10900
|
+
for (const host of ["127.0.0.1", "0.0.0.0", "::1", "::"]) {
|
|
10901
|
+
const result = await new Promise((resolve) => {
|
|
10902
|
+
const server = net5.createServer();
|
|
10903
|
+
server.once(
|
|
10904
|
+
"error",
|
|
10905
|
+
(error) => resolve(error.code === "EADDRNOTAVAIL" || error.code === "EAFNOSUPPORT" ? "absent" : "taken")
|
|
10906
|
+
);
|
|
10907
|
+
server.listen({ port, host, ...host === "::" ? { ipv6Only: false } : {} }, () => server.close(() => resolve("free")));
|
|
10908
|
+
});
|
|
10909
|
+
if (result === "taken") return false;
|
|
10910
|
+
}
|
|
10911
|
+
return true;
|
|
10912
|
+
}
|
|
10913
|
+
function startedSiteRuns(entry) {
|
|
10914
|
+
return Boolean(entry.startedPid && entry.startedPid > 0 && entry.startedCommand) && readProcessCommandLine(entry.startedPid) === entry.startedCommand;
|
|
10915
|
+
}
|
|
10916
|
+
async function siteOnPort(params) {
|
|
10917
|
+
const { port, start, root, projectId: projectId2, previews, procRoot } = params;
|
|
10918
|
+
if (await portIsFree(port)) return "none";
|
|
10919
|
+
if (previews.some(
|
|
10920
|
+
(entry) => entry.projectId === projectId2 && entry.localPort === port && sameStart(entry.start, start) && startedSiteRuns(entry)
|
|
10921
|
+
)) {
|
|
10922
|
+
return "same";
|
|
10923
|
+
}
|
|
10924
|
+
const listening = detectListeningProcess(port, procRoot);
|
|
10925
|
+
if (listening && workspaceRelativeDir(root, listening.cwd) === start.cwd && sameArgv(listening.argv, withPort(start.argv, port))) {
|
|
10926
|
+
return "same";
|
|
10927
|
+
}
|
|
10928
|
+
return "other";
|
|
10929
|
+
}
|
|
10930
|
+
function portTakenError(port, previews, projectId2) {
|
|
10931
|
+
const holder = previews.find((entry) => entry.projectId === projectId2 && entry.localPort === port && entry.title);
|
|
10932
|
+
const what = holder ? `Port ${port} already shows '${holder.title}'.` : `Port ${port} is in use by another server.`;
|
|
10933
|
+
return new PreviewCommandError(`${what} Leave out --port: --start picks a free port for this site.`, PREVIEW_EXIT_PORT_NOT_ALLOWED);
|
|
10934
|
+
}
|
|
10935
|
+
async function startOrFindSite(params) {
|
|
10936
|
+
const { explicitPort, start, root, projectId: projectId2, title, sitePath, procRoot, log } = params;
|
|
10937
|
+
const { previews } = readStateFile2();
|
|
10938
|
+
const onPort = (port) => siteOnPort({ port, start, root, projectId: projectId2, previews, procRoot });
|
|
10939
|
+
const launch = async (port) => {
|
|
10940
|
+
const serve = { argv: withPort(start.argv, port), cwd: start.cwd };
|
|
10941
|
+
const started = await startPreviewSite({ port, sitePath, serve, root, timeoutMs: params.timeoutMs });
|
|
10942
|
+
log(`Started the site on port ${port} (pid ${started.pid}); log: ${displayPath(started.logFile)}`);
|
|
10943
|
+
return { port, start, started: { ...started, command: readProcessCommandLine(started.pid) } };
|
|
10944
|
+
};
|
|
10945
|
+
if (explicitPort !== null) {
|
|
10946
|
+
const found = await onPort(explicitPort);
|
|
10947
|
+
if (found === "same") return { port: explicitPort, start, started: null };
|
|
10948
|
+
if (found === "other") throw portTakenError(explicitPort, previews, projectId2);
|
|
10949
|
+
return launch(explicitPort);
|
|
10950
|
+
}
|
|
10951
|
+
const tried = /* @__PURE__ */ new Set();
|
|
10952
|
+
const known = previews.find((entry) => entry.projectId === projectId2 && sameStart(entry.start, start));
|
|
10953
|
+
if (known) {
|
|
10954
|
+
tried.add(known.localPort);
|
|
10955
|
+
const found = await onPort(known.localPort);
|
|
10956
|
+
if (found === "same") return { port: known.localPort, start, started: null };
|
|
10957
|
+
if (found === "none") return launch(known.localPort);
|
|
10958
|
+
}
|
|
10959
|
+
const reserved = reservedPreviewPorts();
|
|
10960
|
+
const held = new Set(
|
|
10961
|
+
previews.filter((entry) => entry.projectId !== projectId2 || !sameStart(entry.start, start)).map((entry) => entry.localPort)
|
|
10962
|
+
);
|
|
10963
|
+
for (const [port, holder] of await params.openPorts()) {
|
|
10964
|
+
if (holder !== title) held.add(port);
|
|
10965
|
+
}
|
|
10966
|
+
const first = preferredSitePort(start);
|
|
10967
|
+
for (let index = 0; index < AUTO_PORT_TRIES; index += 1) {
|
|
10968
|
+
const port = AUTO_PORT_FIRST + (first - AUTO_PORT_FIRST + index) % AUTO_PORT_COUNT;
|
|
10969
|
+
if (tried.has(port) || reserved.has(port) || held.has(port)) continue;
|
|
10970
|
+
tried.add(port);
|
|
10971
|
+
const found = await onPort(port);
|
|
10972
|
+
if (found === "same") return { port, start, started: null };
|
|
10973
|
+
if (found === "none") return launch(port);
|
|
10974
|
+
}
|
|
10975
|
+
throw new PreviewCommandError(
|
|
10976
|
+
`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.`,
|
|
10977
|
+
PREVIEW_EXIT_NOT_LISTENING
|
|
10978
|
+
);
|
|
10979
|
+
}
|
|
10980
|
+
async function stopSite(pid, command) {
|
|
10981
|
+
if (!pid || pid <= 0 || !command) return false;
|
|
10982
|
+
const ours = () => readProcessCommandLine(pid) === command;
|
|
10983
|
+
if (!ours()) return false;
|
|
10984
|
+
const signal = (name) => {
|
|
10985
|
+
try {
|
|
10986
|
+
process.kill(-pid, name);
|
|
10987
|
+
} catch {
|
|
10988
|
+
try {
|
|
10989
|
+
process.kill(pid, name);
|
|
10990
|
+
} catch {
|
|
10991
|
+
}
|
|
10992
|
+
}
|
|
10993
|
+
};
|
|
10994
|
+
signal("SIGTERM");
|
|
10995
|
+
if (!await waitForProcessExit(pid, 4e3) && ours()) signal("SIGKILL");
|
|
10996
|
+
return true;
|
|
10997
|
+
}
|
|
10998
|
+
function siteRecordFor(site, previews, port, projectId2) {
|
|
10999
|
+
if (site?.started) {
|
|
11000
|
+
return { start: site.start, startedPid: site.started.pid, ...site.started.command ? { startedCommand: site.started.command } : {} };
|
|
11001
|
+
}
|
|
11002
|
+
const earlier = previews.find(
|
|
11003
|
+
(entry) => entry.projectId === projectId2 && entry.localPort === port && (!site || sameStart(entry.start, site.start)) && startedSiteRuns(entry)
|
|
11004
|
+
);
|
|
11005
|
+
if (earlier) return { start: earlier.start, startedPid: earlier.startedPid, startedCommand: earlier.startedCommand };
|
|
11006
|
+
return site ? { start: site.start } : {};
|
|
11007
|
+
}
|
|
11008
|
+
async function openPreviewPorts(params) {
|
|
11009
|
+
const ports = /* @__PURE__ */ new Map();
|
|
11010
|
+
let rows = [];
|
|
11011
|
+
try {
|
|
11012
|
+
const listed = await controllerRequest({
|
|
11013
|
+
controllerUrl: params.controllerUrl,
|
|
11014
|
+
auth: params.auth,
|
|
11015
|
+
method: "GET",
|
|
11016
|
+
route: previewsRoute(params.projectId),
|
|
11017
|
+
retryCommand: "instafy preview list",
|
|
11018
|
+
notFoundMeansUnavailable: true,
|
|
11019
|
+
timeoutMs: 1e4
|
|
11020
|
+
});
|
|
11021
|
+
rows = Array.isArray(listed.body) ? listed.body : [];
|
|
11022
|
+
} catch (error) {
|
|
11023
|
+
if (error instanceof PreviewCommandError && error.exitCode === PREVIEW_EXIT_UNAVAILABLE) throw error;
|
|
11024
|
+
return ports;
|
|
11025
|
+
}
|
|
11026
|
+
const runtimeId = readEnv("RUNTIME_ID");
|
|
11027
|
+
for (const raw of rows) {
|
|
11028
|
+
if (!raw || typeof raw !== "object") continue;
|
|
11029
|
+
const record2 = raw;
|
|
11030
|
+
const onRuntime = stringField(record2, "runtimeId");
|
|
11031
|
+
if (runtimeId && onRuntime && onRuntime !== runtimeId) continue;
|
|
11032
|
+
const port = numberField(record2, "port");
|
|
11033
|
+
if (port) ports.set(port, stringField(record2, "title") ?? "");
|
|
11034
|
+
}
|
|
11035
|
+
return ports;
|
|
11036
|
+
}
|
|
10641
11037
|
async function createAndStartClient(params) {
|
|
10642
|
-
const { opts, port, title, sitePath, projectId: projectId2, retryCommand, auth, controllerUrl, serverUrl, log, serve } = params;
|
|
11038
|
+
const { opts, port, title, sitePath, projectId: projectId2, retryCommand, auth, controllerUrl, serverUrl, log, serve, site, portPicked } = params;
|
|
10643
11039
|
const conversationId = opts.conversationId?.trim() || void 0;
|
|
10644
11040
|
const created = await controllerRequest({
|
|
10645
11041
|
controllerUrl,
|
|
@@ -10655,10 +11051,12 @@ async function createAndStartClient(params) {
|
|
|
10655
11051
|
title,
|
|
10656
11052
|
...opts.summary ? { summary: opts.summary } : {},
|
|
10657
11053
|
metadata: { source: "instafy-cli" },
|
|
10658
|
-
...serve ? { serve } : {}
|
|
11054
|
+
...serve ? { serve } : {},
|
|
11055
|
+
...opts.replace ? { replace: true } : {}
|
|
10659
11056
|
},
|
|
10660
11057
|
retryCommand,
|
|
10661
|
-
notFoundMeansUnavailable: true
|
|
11058
|
+
notFoundMeansUnavailable: true,
|
|
11059
|
+
...portPicked ? { portTakenHint: PICKED_PORT_TAKEN_HINT } : {}
|
|
10662
11060
|
});
|
|
10663
11061
|
const grant = created.body && !Array.isArray(created.body) ? created.body : {};
|
|
10664
11062
|
const previewId = stringField(grant, "previewId");
|
|
@@ -10672,6 +11070,7 @@ async function createAndStartClient(params) {
|
|
|
10672
11070
|
const studioOrigin = parseStudioOrigin(grant["studioOrigin"]);
|
|
10673
11071
|
const state = readStateFile2();
|
|
10674
11072
|
const current = state.previews.find((preview) => preview.previewId === previewId) ?? null;
|
|
11073
|
+
const siteRecord = siteRecordFor(site, state.previews, port, projectId2);
|
|
10675
11074
|
let pid = null;
|
|
10676
11075
|
let binary = current?.binary;
|
|
10677
11076
|
try {
|
|
@@ -10735,47 +11134,84 @@ ${started.logTail}` : "";
|
|
|
10735
11134
|
startedAt: current?.pid === pid ? current.startedAt : (/* @__PURE__ */ new Date()).toISOString(),
|
|
10736
11135
|
...binary ? { binary } : {},
|
|
10737
11136
|
...hostSuffix ? { hostSuffix } : {},
|
|
10738
|
-
...studioOrigin ? { studioOrigin } : {}
|
|
11137
|
+
...studioOrigin ? { studioOrigin } : {},
|
|
11138
|
+
...siteRecord
|
|
10739
11139
|
};
|
|
10740
11140
|
upsertPreviewState(entry);
|
|
10741
11141
|
return { grant, previewId, entry, current };
|
|
10742
11142
|
}
|
|
10743
11143
|
async function previewOpen(opts) {
|
|
10744
|
-
const
|
|
11144
|
+
const explicitPort = opts.port === void 0 || String(opts.port).trim() === "" ? null : parsePreviewPort(opts.port);
|
|
10745
11145
|
const title = parsePreviewTitle(opts.title);
|
|
10746
11146
|
const sitePath = parsePreviewPath(opts.path);
|
|
10747
11147
|
const summary = parsePreviewSummary(opts.summary);
|
|
10748
|
-
|
|
11148
|
+
const serveArgv = opts.serveCmd?.trim() ? parseExplicitServeCommand(opts.serveCmd) : null;
|
|
11149
|
+
if (opts.start && !serveArgv) {
|
|
11150
|
+
throw new PreviewCommandError('--start needs --serve-cmd "<command>" to start the site.', PREVIEW_EXIT_NOT_LISTENING);
|
|
11151
|
+
}
|
|
11152
|
+
if (explicitPort === null) {
|
|
11153
|
+
if (!opts.start) {
|
|
11154
|
+
throw new Error(`A port is required: --port <port>. Or start the site with --start and --serve-cmd, which picks a free port.`);
|
|
11155
|
+
}
|
|
11156
|
+
if (!serveArgv.some((arg) => arg.includes(PORT_PLACEHOLDER))) {
|
|
11157
|
+
throw new Error(
|
|
11158
|
+
`--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.`
|
|
11159
|
+
);
|
|
11160
|
+
}
|
|
11161
|
+
}
|
|
10749
11162
|
const projectId2 = resolveProject({ project: opts.project });
|
|
10750
|
-
const retryCommand = `instafy preview open --port ${
|
|
11163
|
+
const retryCommand = `instafy preview open${explicitPort === null ? "" : ` --port ${explicitPort}`} --title ${shellQuote2(title)}`;
|
|
10751
11164
|
const { auth, controllerUrl } = resolveAuth(opts, retryCommand);
|
|
10752
11165
|
const serverUrl = cleanUrl(controllerUrl);
|
|
10753
11166
|
const log = (message) => console.error(kleur8.cyan(message));
|
|
10754
|
-
const
|
|
10755
|
-
|
|
10756
|
-
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
|
|
10761
|
-
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
|
|
11167
|
+
const start = opts.start ? resolvePreviewServe({ port: explicitPort ?? 0, serveCmd: opts.serveCmd, projectId: projectId2, cwd: opts.cwd }).serve : null;
|
|
11168
|
+
const { grant, previewId, entry, port, site, serve, restartNote } = await withStateLock(async () => {
|
|
11169
|
+
const site2 = start ? await startOrFindSite({
|
|
11170
|
+
explicitPort,
|
|
11171
|
+
start,
|
|
11172
|
+
root: resolveWorkspaceRoot(process.env, projectId2),
|
|
11173
|
+
projectId: projectId2,
|
|
11174
|
+
title,
|
|
11175
|
+
sitePath,
|
|
11176
|
+
timeoutMs: opts.startTimeoutMs,
|
|
11177
|
+
procRoot: opts.procRoot,
|
|
11178
|
+
openPorts: () => openPreviewPorts({ controllerUrl, auth, projectId: projectId2 }),
|
|
11179
|
+
log
|
|
11180
|
+
}) : null;
|
|
11181
|
+
const port2 = site2?.port ?? explicitPort;
|
|
11182
|
+
try {
|
|
11183
|
+
const probe = await probeLocalSite(port2, sitePath, void 0, RATHOLE_LOCAL_HOSTS);
|
|
11184
|
+
if (!probe.listening) {
|
|
11185
|
+
throw await notListeningError(port2, sitePath, probe);
|
|
10765
11186
|
}
|
|
10766
|
-
const
|
|
10767
|
-
|
|
10768
|
-
|
|
10769
|
-
|
|
11187
|
+
const { serve: serve2, note } = resolvePreviewServe({
|
|
11188
|
+
port: port2,
|
|
11189
|
+
serveCmd: opts.serveCmd?.split(PORT_PLACEHOLDER).join(String(port2)),
|
|
11190
|
+
projectId: projectId2,
|
|
11191
|
+
cwd: opts.cwd,
|
|
11192
|
+
procRoot: opts.procRoot
|
|
11193
|
+
});
|
|
11194
|
+
const created = await createAndStartClient({
|
|
11195
|
+
opts: { ...opts, summary },
|
|
11196
|
+
port: port2,
|
|
11197
|
+
title,
|
|
11198
|
+
sitePath,
|
|
11199
|
+
projectId: projectId2,
|
|
11200
|
+
retryCommand,
|
|
11201
|
+
auth,
|
|
11202
|
+
controllerUrl,
|
|
11203
|
+
serverUrl,
|
|
11204
|
+
log,
|
|
11205
|
+
serve: serve2,
|
|
11206
|
+
site: site2,
|
|
11207
|
+
portPicked: explicitPort === null
|
|
11208
|
+
});
|
|
11209
|
+
return { ...created, port: port2, site: site2, serve: serve2, restartNote: note };
|
|
11210
|
+
} catch (error) {
|
|
11211
|
+
if (site2?.started) await stopSite(site2.started.pid, site2.started.command ?? readProcessCommandLine(site2.started.pid));
|
|
11212
|
+
throw error;
|
|
10770
11213
|
}
|
|
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
|
-
);
|
|
11214
|
+
});
|
|
10779
11215
|
let ready;
|
|
10780
11216
|
try {
|
|
10781
11217
|
ready = await controllerRequest({
|
|
@@ -10819,7 +11255,7 @@ async function previewOpen(opts) {
|
|
|
10819
11255
|
viewerHostSuffix: parseViewerHostSuffix(grant["viewerHostSuffix"]) ?? null,
|
|
10820
11256
|
restartable,
|
|
10821
11257
|
...restartable ? {} : { restartNote: restartNote ?? "the server did not record a start command" },
|
|
10822
|
-
...opts.start ? { startedPid: started?.pid ?? null, logFile: started?.logFile ?? null } : {},
|
|
11258
|
+
...opts.start ? { startedPid: site?.started?.pid ?? null, logFile: site?.started?.logFile ?? null } : {},
|
|
10823
11259
|
thumbnail: thumbnail.status,
|
|
10824
11260
|
...thumbnail.reason ? { thumbnailReason: thumbnail.reason } : {}
|
|
10825
11261
|
};
|
|
@@ -10914,9 +11350,13 @@ async function previewClose(opts) {
|
|
|
10914
11350
|
if (!previewId) {
|
|
10915
11351
|
throw new Error("Preview id is required. Use `instafy preview list` to find it.");
|
|
10916
11352
|
}
|
|
11353
|
+
let siteStopped = false;
|
|
10917
11354
|
const entry = await withStateLock(async () => {
|
|
10918
11355
|
const removed = removePreviewState(previewId);
|
|
10919
|
-
if (removed)
|
|
11356
|
+
if (removed) {
|
|
11357
|
+
await stopClient(removed);
|
|
11358
|
+
siteStopped = await stopSite(removed.startedPid, removed.startedCommand);
|
|
11359
|
+
}
|
|
10920
11360
|
return removed;
|
|
10921
11361
|
});
|
|
10922
11362
|
const projectId2 = entry?.projectId ?? resolveProject({ project: opts.project });
|
|
@@ -10927,11 +11367,14 @@ async function previewClose(opts) {
|
|
|
10927
11367
|
);
|
|
10928
11368
|
await closePreviewOnServer(controllerUrl, auth, projectId2, previewId);
|
|
10929
11369
|
} catch (error) {
|
|
10930
|
-
if (entry)
|
|
11370
|
+
if (entry) {
|
|
11371
|
+
const { startedPid: _pid, startedCommand: _command, ...rest } = entry;
|
|
11372
|
+
await withStateLock(async () => upsertPreviewState({ ...siteStopped ? rest : entry, pid: -1 }));
|
|
11373
|
+
}
|
|
10931
11374
|
throw error;
|
|
10932
11375
|
}
|
|
10933
11376
|
removeWorkdir(previewId);
|
|
10934
|
-
return { ok: true, previewId };
|
|
11377
|
+
return { ok: true, previewId, siteStopped };
|
|
10935
11378
|
}
|
|
10936
11379
|
function printPreviewOpen(result, json) {
|
|
10937
11380
|
if (json) {
|
|
@@ -10939,7 +11382,7 @@ function printPreviewOpen(result, json) {
|
|
|
10939
11382
|
return;
|
|
10940
11383
|
}
|
|
10941
11384
|
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}).
|
|
11385
|
+
console.log(`${label}: ${result.title} (port ${result.port}). Its card is in the conversation.`);
|
|
10943
11386
|
if (result.thumbnail === "uploaded") {
|
|
10944
11387
|
console.log(kleur8.gray("The card shows a picture of the site; `instafy preview check` takes a new one after a change."));
|
|
10945
11388
|
} else {
|
|
@@ -10948,7 +11391,7 @@ function printPreviewOpen(result, json) {
|
|
|
10948
11391
|
if (result.startedPid) {
|
|
10949
11392
|
console.log(kleur8.gray(`Started the site in the background (pid ${result.startedPid}); log: ${displayPath(result.logFile ?? "")}`));
|
|
10950
11393
|
} else if (result.startedPid === null) {
|
|
10951
|
-
console.log(kleur8.gray("
|
|
11394
|
+
console.log(kleur8.gray("This site was already running on its port; nothing was started."));
|
|
10952
11395
|
}
|
|
10953
11396
|
if (result.restartable) {
|
|
10954
11397
|
console.log(kleur8.gray("Open starts the site again when the process that started it exits, or the space sleeps."));
|
|
@@ -11115,7 +11558,7 @@ init_config();
|
|
|
11115
11558
|
|
|
11116
11559
|
// src/git-wrapper.ts
|
|
11117
11560
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
11118
|
-
import { randomUUID as
|
|
11561
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
11119
11562
|
import fs13 from "node:fs";
|
|
11120
11563
|
import path14 from "node:path";
|
|
11121
11564
|
import kleur10 from "kleur";
|
|
@@ -11329,10 +11772,10 @@ function hideEmbeddedGitDirs(context, pathSpecs) {
|
|
|
11329
11772
|
fs13.mkdirSync(stagingBase, { recursive: true });
|
|
11330
11773
|
const renames = [];
|
|
11331
11774
|
for (const candidate of candidates) {
|
|
11332
|
-
let hidden = path14.join(stagingBase,
|
|
11775
|
+
let hidden = path14.join(stagingBase, randomUUID3());
|
|
11333
11776
|
for (let attempt2 = 0; attempt2 < 8; attempt2 += 1) {
|
|
11334
11777
|
if (!pathExists(hidden)) break;
|
|
11335
|
-
hidden = path14.join(stagingBase,
|
|
11778
|
+
hidden = path14.join(stagingBase, randomUUID3());
|
|
11336
11779
|
}
|
|
11337
11780
|
fs13.renameSync(candidate, hidden);
|
|
11338
11781
|
renames.push({ original: candidate, hidden });
|
|
@@ -11888,19 +12331,324 @@ function notSavedSentences(report, options) {
|
|
|
11888
12331
|
sentences.push(report.recoveryRef ? `Not saved: ${trimmed} (kept at ${report.recoveryRef})` : `Not saved: ${trimmed}`);
|
|
11889
12332
|
}
|
|
11890
12333
|
}
|
|
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
|
-
};
|
|
12334
|
+
return sentences;
|
|
12335
|
+
}
|
|
12336
|
+
function saveReportJson(report, notSaved) {
|
|
12337
|
+
return {
|
|
12338
|
+
rev: report.rev,
|
|
12339
|
+
baseRev: report.baseRev,
|
|
12340
|
+
gitSyncStatus: report.gitSyncStatus,
|
|
12341
|
+
recoveryRef: report.recoveryRef,
|
|
12342
|
+
conflictedPaths: report.conflictedPaths,
|
|
12343
|
+
rejectedPaths: report.rejectedPaths,
|
|
12344
|
+
unpushedRefs: report.unpushedRefs,
|
|
12345
|
+
notSaved
|
|
12346
|
+
};
|
|
12347
|
+
}
|
|
12348
|
+
|
|
12349
|
+
// src/api.ts
|
|
12350
|
+
init_config();
|
|
12351
|
+
init_errors();
|
|
12352
|
+
init_controller_fetch();
|
|
12353
|
+
import fs14 from "node:fs";
|
|
12354
|
+
function normalizeUrl5(raw) {
|
|
12355
|
+
const value = (raw ?? "").trim();
|
|
12356
|
+
if (!value) return "http://127.0.0.1:8788";
|
|
12357
|
+
return value.replace(/\/$/, "");
|
|
12358
|
+
}
|
|
12359
|
+
function normalizeToken6(raw) {
|
|
12360
|
+
if (!raw) return null;
|
|
12361
|
+
const trimmed = raw.trim();
|
|
12362
|
+
return trimmed.length ? trimmed : null;
|
|
12363
|
+
}
|
|
12364
|
+
function resolveBearerTokenWithSource(options) {
|
|
12365
|
+
const cwd = process.cwd();
|
|
12366
|
+
const profile = resolveActiveProfileName({ cwd });
|
|
12367
|
+
const stored = resolveConfiguredAccessToken({ profile, cwd });
|
|
12368
|
+
const explicit = normalizeToken6(options.accessToken);
|
|
12369
|
+
if (explicit) {
|
|
12370
|
+
return { token: explicit, source: "explicit", profile, runtimeControllerUrl: null };
|
|
12371
|
+
}
|
|
12372
|
+
const runtimeCredential = resolveRuntimeControllerCredential();
|
|
12373
|
+
if (runtimeCredential) {
|
|
12374
|
+
return {
|
|
12375
|
+
token: runtimeCredential.token,
|
|
12376
|
+
source: "env",
|
|
12377
|
+
profile: null,
|
|
12378
|
+
runtimeControllerUrl: runtimeCredential.controllerUrl
|
|
12379
|
+
};
|
|
12380
|
+
}
|
|
12381
|
+
const envKeys = [
|
|
12382
|
+
"INSTAFY_ACCESS_TOKEN",
|
|
12383
|
+
"SUPABASE_ACCESS_TOKEN"
|
|
12384
|
+
];
|
|
12385
|
+
for (const key of envKeys) {
|
|
12386
|
+
const value = normalizeToken6(process.env[key]);
|
|
12387
|
+
if (value) {
|
|
12388
|
+
return { token: value, source: "env", profile, runtimeControllerUrl: null };
|
|
12389
|
+
}
|
|
12390
|
+
}
|
|
12391
|
+
if (stored) {
|
|
12392
|
+
return { token: stored, source: "config", profile, runtimeControllerUrl: null };
|
|
12393
|
+
}
|
|
12394
|
+
return { token: null, source: "none", profile, runtimeControllerUrl: null };
|
|
12395
|
+
}
|
|
12396
|
+
function parseKeyValue(raw) {
|
|
12397
|
+
const trimmed = raw.trim();
|
|
12398
|
+
if (!trimmed) {
|
|
12399
|
+
throw new Error("Key/value pair cannot be empty");
|
|
12400
|
+
}
|
|
12401
|
+
const equals = trimmed.indexOf("=");
|
|
12402
|
+
if (equals === -1) {
|
|
12403
|
+
return { key: trimmed, value: "" };
|
|
12404
|
+
}
|
|
12405
|
+
const key = trimmed.slice(0, equals).trim();
|
|
12406
|
+
const value = trimmed.slice(equals + 1).trim();
|
|
12407
|
+
if (!key) {
|
|
12408
|
+
throw new Error(`Invalid pair "${raw}" (missing key)`);
|
|
12409
|
+
}
|
|
12410
|
+
return { key, value };
|
|
12411
|
+
}
|
|
12412
|
+
function parseHeader(raw) {
|
|
12413
|
+
const trimmed = raw.trim();
|
|
12414
|
+
if (!trimmed) {
|
|
12415
|
+
throw new Error("Header cannot be empty");
|
|
12416
|
+
}
|
|
12417
|
+
const colon = trimmed.indexOf(":");
|
|
12418
|
+
if (colon !== -1) {
|
|
12419
|
+
const key = trimmed.slice(0, colon).trim();
|
|
12420
|
+
const value = trimmed.slice(colon + 1).trim();
|
|
12421
|
+
if (!key) {
|
|
12422
|
+
throw new Error(`Invalid header "${raw}" (missing name)`);
|
|
12423
|
+
}
|
|
12424
|
+
return { key, value };
|
|
12425
|
+
}
|
|
12426
|
+
return parseKeyValue(trimmed);
|
|
12427
|
+
}
|
|
12428
|
+
function parseJsonBody(options) {
|
|
12429
|
+
if (options.jsonFile) {
|
|
12430
|
+
const raw = fs14.readFileSync(options.jsonFile, "utf8");
|
|
12431
|
+
const parsed = JSON.parse(raw);
|
|
12432
|
+
return JSON.stringify(parsed);
|
|
12433
|
+
}
|
|
12434
|
+
if (options.json) {
|
|
12435
|
+
const parsed = JSON.parse(options.json);
|
|
12436
|
+
return JSON.stringify(parsed);
|
|
12437
|
+
}
|
|
12438
|
+
return void 0;
|
|
12439
|
+
}
|
|
12440
|
+
function buildRequestUrl(options, runtimeControllerUrl) {
|
|
12441
|
+
const selectedControllerUrl = options.controllerUrl ?? runtimeControllerUrl;
|
|
12442
|
+
const base = new URL(
|
|
12443
|
+
`${normalizeUrl5(resolveControllerUrl({ controllerUrl: selectedControllerUrl ?? null }))}/`
|
|
12444
|
+
);
|
|
12445
|
+
if (base.protocol !== "http:" && base.protocol !== "https:") {
|
|
12446
|
+
throw new Error("The configured controller URL must use http or https.");
|
|
12447
|
+
}
|
|
12448
|
+
if (base.username || base.password) {
|
|
12449
|
+
throw new Error("The configured controller URL must not contain embedded credentials.");
|
|
12450
|
+
}
|
|
12451
|
+
if (runtimeControllerUrl) {
|
|
12452
|
+
resolveRuntimeBoundControllerUrl(
|
|
12453
|
+
{ controllerUrl: runtimeControllerUrl },
|
|
12454
|
+
base.toString()
|
|
12455
|
+
);
|
|
12456
|
+
}
|
|
12457
|
+
const rawPath = options.path.trim();
|
|
12458
|
+
if (!rawPath) {
|
|
12459
|
+
throw new Error("Path is required");
|
|
12460
|
+
}
|
|
12461
|
+
const isAbsoluteReference = /^[a-z][a-z\d+.-]*:/i.test(rawPath) || rawPath.startsWith("//");
|
|
12462
|
+
const url = new URL(
|
|
12463
|
+
isAbsoluteReference ? rawPath : rawPath.startsWith("/") ? rawPath : `/${rawPath}`,
|
|
12464
|
+
base
|
|
12465
|
+
);
|
|
12466
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
12467
|
+
throw new Error("Controller API URLs must use http or https.");
|
|
12468
|
+
}
|
|
12469
|
+
if (url.username || url.password) {
|
|
12470
|
+
throw new Error("Controller API URLs must not contain embedded credentials.");
|
|
12471
|
+
}
|
|
12472
|
+
const crossOrigin = url.origin !== base.origin;
|
|
12473
|
+
if (crossOrigin && !options.allowCrossOrigin) {
|
|
12474
|
+
throw new Error(
|
|
12475
|
+
"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."
|
|
12476
|
+
);
|
|
12477
|
+
}
|
|
12478
|
+
for (const pair of options.query ?? []) {
|
|
12479
|
+
const { key, value } = parseKeyValue(pair);
|
|
12480
|
+
url.searchParams.set(key, value);
|
|
12481
|
+
}
|
|
12482
|
+
return { url, crossOrigin };
|
|
12483
|
+
}
|
|
12484
|
+
function maybePrettyPrintJson(text2, pretty) {
|
|
12485
|
+
if (!pretty) return text2;
|
|
12486
|
+
if (!text2.trim()) return text2;
|
|
12487
|
+
try {
|
|
12488
|
+
return JSON.stringify(JSON.parse(text2), null, 2);
|
|
12489
|
+
} catch {
|
|
12490
|
+
return text2;
|
|
12491
|
+
}
|
|
12492
|
+
}
|
|
12493
|
+
async function executeControllerApiRequest(options, bodyOverride) {
|
|
12494
|
+
const resolved = resolveBearerTokenWithSource(options);
|
|
12495
|
+
const { url, crossOrigin } = buildRequestUrl(options, resolved.runtimeControllerUrl);
|
|
12496
|
+
const bearer = resolved.token;
|
|
12497
|
+
if (crossOrigin && (!bearer || resolved.source !== "explicit")) {
|
|
12498
|
+
throw new Error(
|
|
12499
|
+
"Cross-origin controller API requests require --allow-cross-origin and an explicit --access-token."
|
|
12500
|
+
);
|
|
12501
|
+
}
|
|
12502
|
+
const headers = new Headers();
|
|
12503
|
+
headers.set("accept", "application/json");
|
|
12504
|
+
for (const header of options.headers ?? []) {
|
|
12505
|
+
const { key, value } = parseHeader(header);
|
|
12506
|
+
headers.set(key, value);
|
|
12507
|
+
}
|
|
12508
|
+
const body = bodyOverride ?? parseJsonBody(options);
|
|
12509
|
+
if (body !== void 0 && !headers.has("content-type")) {
|
|
12510
|
+
headers.set("content-type", "application/json");
|
|
12511
|
+
}
|
|
12512
|
+
const init = {
|
|
12513
|
+
method: options.method,
|
|
12514
|
+
headers,
|
|
12515
|
+
body,
|
|
12516
|
+
redirect: "error"
|
|
12517
|
+
};
|
|
12518
|
+
const response = bearer ? (await fetchWithControllerAuth({
|
|
12519
|
+
url: url.toString(),
|
|
12520
|
+
init,
|
|
12521
|
+
accessToken: bearer,
|
|
12522
|
+
tokenSource: resolved.source,
|
|
12523
|
+
profile: resolved.profile,
|
|
12524
|
+
cwd: process.cwd()
|
|
12525
|
+
})).response : await fetch(url, init);
|
|
12526
|
+
const responseText = await response.text().catch(() => "");
|
|
12527
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
12528
|
+
const isJson = contentType.includes("application/json") || contentType.includes("+json");
|
|
12529
|
+
if (!response.ok) {
|
|
12530
|
+
if (response.status === 401 || response.status === 403) {
|
|
12531
|
+
throw formatAuthRejectedError({
|
|
12532
|
+
status: response.status,
|
|
12533
|
+
responseBody: responseText,
|
|
12534
|
+
retryCommand: "instafy login",
|
|
12535
|
+
advancedHint: "pass --access-token, or set INSTAFY_ACCESS_TOKEN / SUPABASE_ACCESS_TOKEN"
|
|
12536
|
+
});
|
|
12537
|
+
}
|
|
12538
|
+
const formattedBody = isJson ? maybePrettyPrintJson(responseText, true) : responseText;
|
|
12539
|
+
const prefix = `Request failed (${response.status} ${response.statusText})`;
|
|
12540
|
+
const suffix = formattedBody.trim() ? `: ${formattedBody}` : "";
|
|
12541
|
+
throw new Error(`${prefix}${suffix}`);
|
|
12542
|
+
}
|
|
12543
|
+
return { response, responseText, isJson };
|
|
12544
|
+
}
|
|
12545
|
+
async function requestControllerApiJson(options) {
|
|
12546
|
+
const body = options.jsonBody === void 0 ? void 0 : JSON.stringify(options.jsonBody);
|
|
12547
|
+
const { responseText, isJson } = await executeControllerApiRequest(
|
|
12548
|
+
{
|
|
12549
|
+
method: options.method,
|
|
12550
|
+
path: options.path,
|
|
12551
|
+
controllerUrl: options.controllerUrl,
|
|
12552
|
+
accessToken: options.accessToken,
|
|
12553
|
+
serviceToken: options.serviceToken,
|
|
12554
|
+
query: options.query,
|
|
12555
|
+
headers: options.headers,
|
|
12556
|
+
allowCrossOrigin: options.allowCrossOrigin
|
|
12557
|
+
},
|
|
12558
|
+
body
|
|
12559
|
+
);
|
|
12560
|
+
if (!isJson) {
|
|
12561
|
+
throw new Error("Expected JSON response from controller API");
|
|
12562
|
+
}
|
|
12563
|
+
return JSON.parse(responseText);
|
|
12564
|
+
}
|
|
12565
|
+
async function requestControllerApi(options) {
|
|
12566
|
+
const { responseText, isJson } = await executeControllerApiRequest(options);
|
|
12567
|
+
const pretty = options.pretty !== false;
|
|
12568
|
+
const formattedBody = isJson ? maybePrettyPrintJson(responseText, pretty) : responseText;
|
|
12569
|
+
if (formattedBody) console.log(formattedBody);
|
|
12570
|
+
}
|
|
12571
|
+
|
|
12572
|
+
// src/research-settings.ts
|
|
12573
|
+
init_project_manifest();
|
|
12574
|
+
var fields = [
|
|
12575
|
+
{ key: "maxPageLoads", label: "Fresh pages per task" },
|
|
12576
|
+
{ key: "maxPageLoadsPerSite", label: "Fresh pages per site per task" }
|
|
12577
|
+
];
|
|
12578
|
+
function integer(raw, label) {
|
|
12579
|
+
if (!/^[1-9]\d*$/.test(raw.trim()) || !Number.isSafeInteger(Number(raw))) {
|
|
12580
|
+
throw new Error(`${label} must be a positive whole number.`);
|
|
12581
|
+
}
|
|
12582
|
+
return Number(raw);
|
|
12583
|
+
}
|
|
12584
|
+
async function researchSettings(options) {
|
|
12585
|
+
const patch = {};
|
|
12586
|
+
for (const field of fields) {
|
|
12587
|
+
if (options[field.key] !== void 0) patch[field.key] = integer(options[field.key], field.label);
|
|
12588
|
+
}
|
|
12589
|
+
if (options.interactive) {
|
|
12590
|
+
if (options.json || Object.keys(patch).length) throw new Error("Use --interactive without --json or setting flags.");
|
|
12591
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY || process.env.CI === "true") {
|
|
12592
|
+
throw new Error("--interactive requires a terminal. Use --max-page-loads and --max-page-loads-per-site instead.");
|
|
12593
|
+
}
|
|
12594
|
+
}
|
|
12595
|
+
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;
|
|
12596
|
+
if (!project) throw new Error("No space configured. Pass --space, set SPACE_ID, or run `instafy space init`.");
|
|
12597
|
+
const request = {
|
|
12598
|
+
path: `/projects/${encodeURIComponent(project)}/research-settings`,
|
|
12599
|
+
controllerUrl: options.controllerUrl,
|
|
12600
|
+
accessToken: options.accessToken
|
|
12601
|
+
};
|
|
12602
|
+
let settings = await requestControllerApiJson({ ...request, method: "GET" });
|
|
12603
|
+
if ((options.interactive || Object.keys(patch).length) && !settings.canManage) {
|
|
12604
|
+
throw new Error("Only space managers can change research settings.");
|
|
12605
|
+
}
|
|
12606
|
+
for (const field of fields) {
|
|
12607
|
+
if (!settings.limits?.[field.key] || !Number.isInteger(settings[field.key])) {
|
|
12608
|
+
throw new Error("This controller does not support research settings. Update it before changing the budget.");
|
|
12609
|
+
}
|
|
12610
|
+
}
|
|
12611
|
+
if (options.interactive) {
|
|
12612
|
+
const { isCancel, text: text2 } = await import("@clack/prompts");
|
|
12613
|
+
console.log("The parent and its helpers share one budget. Changes apply to new tasks; running tasks retain their allowance.");
|
|
12614
|
+
for (const field of fields) {
|
|
12615
|
+
const bounds = settings.limits[field.key];
|
|
12616
|
+
const entered = await text2({
|
|
12617
|
+
message: `${field.label} (${bounds.min}\u2013${bounds.max}; default ${bounds.default})`,
|
|
12618
|
+
defaultValue: String(settings[field.key]),
|
|
12619
|
+
placeholder: String(settings[field.key]),
|
|
12620
|
+
validate(raw) {
|
|
12621
|
+
try {
|
|
12622
|
+
const value2 = integer(raw || String(settings[field.key]), field.label);
|
|
12623
|
+
if (value2 < bounds.min || value2 > bounds.max) return `Enter a whole number from ${bounds.min} to ${bounds.max}.`;
|
|
12624
|
+
if (field.key === "maxPageLoadsPerSite" && value2 > (patch.maxPageLoads ?? settings.maxPageLoads)) return "The per-site limit cannot exceed the task limit.";
|
|
12625
|
+
} catch (error) {
|
|
12626
|
+
return error.message;
|
|
12627
|
+
}
|
|
12628
|
+
}
|
|
12629
|
+
});
|
|
12630
|
+
if (isCancel(entered)) throw new Error("Cancelled. No settings were changed.");
|
|
12631
|
+
const value = integer(String(entered), field.label);
|
|
12632
|
+
if (value !== settings[field.key]) patch[field.key] = value;
|
|
12633
|
+
}
|
|
12634
|
+
}
|
|
12635
|
+
for (const field of fields) {
|
|
12636
|
+
const value = patch[field.key];
|
|
12637
|
+
const bounds = settings.limits[field.key];
|
|
12638
|
+
if (value !== void 0 && (value < bounds.min || value > bounds.max)) throw new Error(`${field.label} must be between ${bounds.min} and ${bounds.max}.`);
|
|
12639
|
+
}
|
|
12640
|
+
if ((patch.maxPageLoadsPerSite ?? settings.maxPageLoadsPerSite) > (patch.maxPageLoads ?? settings.maxPageLoads)) {
|
|
12641
|
+
throw new Error("The per-site limit cannot exceed the task limit.");
|
|
12642
|
+
}
|
|
12643
|
+
if (Object.keys(patch).length) settings = await requestControllerApiJson({ ...request, method: "PATCH", jsonBody: patch });
|
|
12644
|
+
if (options.json) {
|
|
12645
|
+
console.log(JSON.stringify(settings, null, 2));
|
|
12646
|
+
return;
|
|
12647
|
+
}
|
|
12648
|
+
console.log(`Research budget for space ${project}`);
|
|
12649
|
+
for (const field of fields) console.log(`${field.label}: ${settings[field.key]}`);
|
|
12650
|
+
console.log("Shared by the parent and all helpers, including continued worker jobs. Cached reads are free. Changes apply to new tasks.");
|
|
12651
|
+
if (!settings.canManage) console.log("Read-only: only space managers can change this budget.");
|
|
11904
12652
|
}
|
|
11905
12653
|
|
|
11906
12654
|
// src/web-link.ts
|
|
@@ -11971,9 +12719,9 @@ function registerWebLinkCommand(web) {
|
|
|
11971
12719
|
// src/web-search.ts
|
|
11972
12720
|
var redaction2 = __toESM(require_redaction(), 1);
|
|
11973
12721
|
import dns3 from "node:dns";
|
|
11974
|
-
import
|
|
12722
|
+
import fs15 from "node:fs";
|
|
11975
12723
|
import https2 from "node:https";
|
|
11976
|
-
import
|
|
12724
|
+
import net6 from "node:net";
|
|
11977
12725
|
import path15 from "node:path";
|
|
11978
12726
|
import zlib2 from "node:zlib";
|
|
11979
12727
|
|
|
@@ -12180,7 +12928,7 @@ function compileProvider(id, raw) {
|
|
|
12180
12928
|
if (url.protocol !== "https:") return `${id}: request.url must be https`;
|
|
12181
12929
|
if (url.username || url.password || url.port && url.port !== "443" || url.hash) return `${id}: request.url must be a plain https address`;
|
|
12182
12930
|
const host = langInHost ? url.hostname.toLowerCase().replace(/^xx\./, "") : url.hostname.toLowerCase();
|
|
12183
|
-
if (
|
|
12931
|
+
if (net6.isIP(host.replace(/^\[|\]$/g, "")) || isLocalHostName(host)) return `${id}: request.url must name a public host`;
|
|
12184
12932
|
const never = NEVER_HOSTS.find((entry) => host === entry.host || host.endsWith(`.${entry.host}`));
|
|
12185
12933
|
if (never) return `${id}: ${never.host} is never used (${never.why})`;
|
|
12186
12934
|
if (/[{}]/.test(decodeURIComponent(url.href))) return `${id}: request.url may not hold placeholders`;
|
|
@@ -12303,9 +13051,9 @@ function compileProvider(id, raw) {
|
|
|
12303
13051
|
}
|
|
12304
13052
|
function resultLink(value) {
|
|
12305
13053
|
if (typeof value === "string" && value.startsWith("https://")) {
|
|
12306
|
-
const
|
|
13054
|
+
const fields2 = [...value.matchAll(/\{([\w-]+(?:\.[\w-]+)*)\}/g)];
|
|
12307
13055
|
const rest = value.replace(/\{([\w-]+(?:\.[\w-]+)*)\}/g, "");
|
|
12308
|
-
if (!
|
|
13056
|
+
if (!fields2.length || /[{}]/.test(rest)) return void 0;
|
|
12309
13057
|
try {
|
|
12310
13058
|
new URL(value.replace(/\{[^}]*\}/g, "x"));
|
|
12311
13059
|
} catch {
|
|
@@ -12363,8 +13111,8 @@ function loadSearchProviders(bundled = BUNDLED_SEARCH_PROVIDERS, copies = []) {
|
|
|
12363
13111
|
return { providers, chains, off: [...off].filter((id) => id in bundled.providers).sort(), keyEnvs: [...keyEnvs], warnings };
|
|
12364
13112
|
}
|
|
12365
13113
|
function fill(template, vars) {
|
|
12366
|
-
const
|
|
12367
|
-
if (
|
|
13114
|
+
const whole2 = /^\{([a-z]+)\}$/.exec(template);
|
|
13115
|
+
if (whole2 && whole2[1] === "count") return vars.count;
|
|
12368
13116
|
let missing = false;
|
|
12369
13117
|
const out = template.replace(/\{\{|\}\}|\{([a-z]+)(?::([a-z]+))?\}/g, (match, name, encoder) => {
|
|
12370
13118
|
if (!name) return match[0];
|
|
@@ -12453,7 +13201,7 @@ function sendOnce(request, target, timeoutMs) {
|
|
|
12453
13201
|
port: target.port,
|
|
12454
13202
|
method: request.method,
|
|
12455
13203
|
path: `${request.url.pathname}${request.url.search}`,
|
|
12456
|
-
servername:
|
|
13204
|
+
servername: net6.isIP(hostname) ? void 0 : hostname,
|
|
12457
13205
|
agent: false,
|
|
12458
13206
|
maxHeaderSize: 64 * 1024,
|
|
12459
13207
|
headers: { Host: request.url.host, ...request.headers, ...payload ? { "Content-Length": String(payload.length) } : {} }
|
|
@@ -12503,8 +13251,8 @@ function at(value, field) {
|
|
|
12503
13251
|
}
|
|
12504
13252
|
return current;
|
|
12505
13253
|
}
|
|
12506
|
-
function firstText(item,
|
|
12507
|
-
for (const field of
|
|
13254
|
+
function firstText(item, fields2) {
|
|
13255
|
+
for (const field of fields2) {
|
|
12508
13256
|
const value = at(item, field);
|
|
12509
13257
|
if (typeof value === "string" && value.trim()) return value;
|
|
12510
13258
|
}
|
|
@@ -12520,10 +13268,10 @@ function withoutTags(text2) {
|
|
|
12520
13268
|
return out;
|
|
12521
13269
|
}
|
|
12522
13270
|
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, (
|
|
13271
|
+
const text2 = withoutTags(raw).replace(/&(#x[0-9a-f]{1,6}|#\d{1,7}|[a-z]{2,6}|#39);/gi, (whole2, name) => {
|
|
12524
13272
|
if (ENTITIES2[name.toLowerCase()]) return ENTITIES2[name.toLowerCase()];
|
|
12525
13273
|
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) :
|
|
13274
|
+
return Number.isFinite(code) && code > 31 && code < 1114112 ? String.fromCodePoint(code) : whole2;
|
|
12527
13275
|
}).replace(/</g, "\u2039").replace(/[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u202a-\u202e\u2066-\u2069\ufeff]/g, " ").replace(/\s+/g, " ").trim();
|
|
12528
13276
|
const redacted = redaction2.redactText(text2).text;
|
|
12529
13277
|
return redacted.length > max ? `${redacted.slice(0, max - 1)}\u2026` : redacted;
|
|
@@ -12540,7 +13288,7 @@ function resultAddress(raw) {
|
|
|
12540
13288
|
if (url.username || url.password) return null;
|
|
12541
13289
|
if (url.port && url.port !== "80" && url.port !== "443") return null;
|
|
12542
13290
|
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
12543
|
-
if (
|
|
13291
|
+
if (net6.isIP(host) ? isPrivateAddress(host) : isLocalHostName(host)) return null;
|
|
12544
13292
|
url.hash = "";
|
|
12545
13293
|
return url;
|
|
12546
13294
|
}
|
|
@@ -12692,10 +13440,10 @@ var SearchState = class {
|
|
|
12692
13440
|
/** Removes turn and day files older than two days. */
|
|
12693
13441
|
prune() {
|
|
12694
13442
|
try {
|
|
12695
|
-
for (const name of
|
|
13443
|
+
for (const name of fs15.readdirSync(this.dir)) {
|
|
12696
13444
|
if (!/^(?:search-turn|day)-[\w-]+\.json$/.test(name)) continue;
|
|
12697
13445
|
const file = path15.join(this.dir, name);
|
|
12698
|
-
if (Date.now() -
|
|
13446
|
+
if (Date.now() - fs15.statSync(file).mtimeMs > 2 * 864e5) fs15.rmSync(file, { force: true });
|
|
12699
13447
|
}
|
|
12700
13448
|
} catch {
|
|
12701
13449
|
}
|
|
@@ -13043,229 +13791,6 @@ function registerWebSearchCommand(web) {
|
|
|
13043
13791
|
return web;
|
|
13044
13792
|
}
|
|
13045
13793
|
|
|
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
13794
|
// src/conversations.ts
|
|
13270
13795
|
init_project_manifest();
|
|
13271
13796
|
import kleur11 from "kleur";
|
|
@@ -14816,10 +15341,10 @@ function projectId(explicit) {
|
|
|
14816
15341
|
if (!value?.trim()) throw new Error("No space configured. Pass --space, set SPACE_ID, or run `instafy space init`.");
|
|
14817
15342
|
return uuid(value, "Space ID");
|
|
14818
15343
|
}
|
|
14819
|
-
function
|
|
15344
|
+
function object2(value, fields2, label) {
|
|
14820
15345
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object.`);
|
|
14821
15346
|
const record2 = value;
|
|
14822
|
-
if (Object.keys(record2).some((key) => !
|
|
15347
|
+
if (Object.keys(record2).some((key) => !fields2.includes(key))) throw new Error(`${label} contains an unsupported field.`);
|
|
14823
15348
|
return record2;
|
|
14824
15349
|
}
|
|
14825
15350
|
function text(value, label, max) {
|
|
@@ -14874,7 +15399,7 @@ async function readProposal(file) {
|
|
|
14874
15399
|
} catch {
|
|
14875
15400
|
throw new Error("Recommendation file must contain valid JSON.");
|
|
14876
15401
|
}
|
|
14877
|
-
const input3 =
|
|
15402
|
+
const input3 = object2(parsed, ["key", "title", "reason", "prompt", "message", "evidence"], "Recommendation");
|
|
14878
15403
|
const key = text(input3.key, "Recommendation key", 120);
|
|
14879
15404
|
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
15405
|
if (!Array.isArray(input3.evidence) || input3.evidence.length < 1 || input3.evidence.length > 8) {
|
|
@@ -14887,7 +15412,7 @@ async function readProposal(file) {
|
|
|
14887
15412
|
prompt: text(input3.prompt, "Recommendation prompt", 4e3),
|
|
14888
15413
|
...input3.message === void 0 ? {} : { message: text(input3.message, "Recommendation message", 4e3) },
|
|
14889
15414
|
evidence: input3.evidence.map((value) => {
|
|
14890
|
-
const entry =
|
|
15415
|
+
const entry = object2(value, ["conversationId", "messageId"], "Evidence");
|
|
14891
15416
|
return {
|
|
14892
15417
|
conversationId: uuid(entry.conversationId, "Evidence conversationId"),
|
|
14893
15418
|
...entry.messageId === void 0 ? {} : { messageId: uuid(entry.messageId, "Evidence messageId") }
|
|
@@ -16440,7 +16965,7 @@ async function diagnosticsRunResult(options) {
|
|
|
16440
16965
|
|
|
16441
16966
|
// src/support.ts
|
|
16442
16967
|
import fs19 from "node:fs";
|
|
16443
|
-
import { randomUUID as
|
|
16968
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
16444
16969
|
import path20 from "node:path";
|
|
16445
16970
|
init_project_manifest();
|
|
16446
16971
|
var MAX_SUMMARY_CHARS = 500;
|
|
@@ -16733,7 +17258,7 @@ async function supportReport(options) {
|
|
|
16733
17258
|
printJson3(preview);
|
|
16734
17259
|
return;
|
|
16735
17260
|
}
|
|
16736
|
-
const clientRequestId = cleanText4(options.clientRequestId) ??
|
|
17261
|
+
const clientRequestId = cleanText4(options.clientRequestId) ?? randomUUID4();
|
|
16737
17262
|
if (!UUID2.test(clientRequestId)) {
|
|
16738
17263
|
throw new Error("--client-request-id must be a UUID.");
|
|
16739
17264
|
}
|
|
@@ -16948,7 +17473,7 @@ async function supportReply(options) {
|
|
|
16948
17473
|
`A follow-up must be at most ${MAX_MESSAGE_CHARS} characters and ${MAX_MESSAGE_BYTES} bytes.`
|
|
16949
17474
|
);
|
|
16950
17475
|
}
|
|
16951
|
-
const clientRequestId = cleanText4(options.clientRequestId) ??
|
|
17476
|
+
const clientRequestId = cleanText4(options.clientRequestId) ?? randomUUID4();
|
|
16952
17477
|
if (!UUID2.test(clientRequestId)) {
|
|
16953
17478
|
throw new Error("--client-request-id must be a UUID.");
|
|
16954
17479
|
}
|
|
@@ -18392,19 +18917,43 @@ function exitWithPreviewError(error) {
|
|
|
18392
18917
|
var previewCommand = program.command("preview").description("Look at sites running in this space, and show them in Studio as a private preview card");
|
|
18393
18918
|
failWithGroupHelp(previewCommand);
|
|
18394
18919
|
registerPreviewCheckCommand(previewCommand, { hasOpenPreview: hasOpenPreviewFor, uploadThumbnail: uploadCheckThumbnail });
|
|
18395
|
-
|
|
18920
|
+
var webCommand = registerWebSearchCommand(registerWebLinkCommand(registerWebCommand(program)));
|
|
18921
|
+
failWithGroupHelp(webCommand);
|
|
18922
|
+
var webSettingsCommand = webCommand.command("settings").description("Inspect or change the space's shared research budget for new tasks");
|
|
18923
|
+
addSpaceOption(webSettingsCommand, "Space UUID (defaults to .instafy/space.json or SPACE_ID)");
|
|
18924
|
+
addServerUrlOptions(webSettingsCommand);
|
|
18925
|
+
addAccessTokenOptions(webSettingsCommand, "Instafy access token");
|
|
18926
|
+
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) => {
|
|
18927
|
+
try {
|
|
18928
|
+
await researchSettings({
|
|
18929
|
+
project: resolveSpaceIdOption(opts),
|
|
18930
|
+
controllerUrl: opts.serverUrl,
|
|
18931
|
+
accessToken: opts.accessToken,
|
|
18932
|
+
maxPageLoads: opts.maxPageLoads,
|
|
18933
|
+
maxPageLoadsPerSite: opts.maxPageLoadsPerSite,
|
|
18934
|
+
interactive: opts.interactive,
|
|
18935
|
+
json: opts.json
|
|
18936
|
+
});
|
|
18937
|
+
} catch (error) {
|
|
18938
|
+
console.error(kleur20.red(String(error)));
|
|
18939
|
+
process.exit(1);
|
|
18940
|
+
}
|
|
18941
|
+
});
|
|
18396
18942
|
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
|
-
).
|
|
18943
|
+
"Open a private preview of a local port (exit 2: nothing listening, 3: port not allowed or another site's, 5: previews not available)"
|
|
18944
|
+
).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
18945
|
addSpaceOption(previewOpenCommand, "Space UUID (defaults to .instafy/space.json or SPACE_ID)");
|
|
18400
18946
|
addServerUrlOptions(previewOpenCommand);
|
|
18401
18947
|
addAccessTokenOptions(previewOpenCommand, "Instafy access token (defaults to the job token, then the saved `instafy login` token)");
|
|
18402
18948
|
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
18949
|
"--serve-cmd <command>",
|
|
18404
|
-
"How to start the site again when the process that started it exits
|
|
18950
|
+
"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
18951
|
).option(
|
|
18406
18952
|
"--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 (
|
|
18953
|
+
"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"
|
|
18954
|
+
).option(
|
|
18955
|
+
"--replace",
|
|
18956
|
+
"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
18957
|
).option("--json", "Output JSON").action(async (opts) => {
|
|
18409
18958
|
try {
|
|
18410
18959
|
const result = await previewOpen({
|
|
@@ -18418,6 +18967,7 @@ previewOpenCommand.option("--conversation <id>", "Conversation for the card (onl
|
|
|
18418
18967
|
ratholeBin: opts.ratholeBin,
|
|
18419
18968
|
serveCmd: opts.serveCmd,
|
|
18420
18969
|
start: Boolean(opts.start),
|
|
18970
|
+
replace: Boolean(opts.replace),
|
|
18421
18971
|
summary: opts.summary,
|
|
18422
18972
|
thumbnail: opts.thumbnail !== false,
|
|
18423
18973
|
thumbnailFile: typeof opts.thumbnail === "string" ? opts.thumbnail : void 0
|
|
@@ -18443,7 +18993,7 @@ previewListCommand.option("--json", "Output JSON").action(async (opts) => {
|
|
|
18443
18993
|
exitWithPreviewError(error);
|
|
18444
18994
|
}
|
|
18445
18995
|
});
|
|
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`)");
|
|
18996
|
+
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
18997
|
addSpaceOption(previewCloseCommand, "Space UUID (defaults to .instafy/space.json or SPACE_ID)");
|
|
18448
18998
|
addServerUrlOptions(previewCloseCommand);
|
|
18449
18999
|
addAccessTokenOptions(previewCloseCommand, "Instafy access token (defaults to the job token, then the saved `instafy login` token)");
|
|
@@ -18459,7 +19009,7 @@ previewCloseCommand.option("--json", "Output JSON").action(async (previewId, opt
|
|
|
18459
19009
|
console.log(JSON.stringify(result, null, 2));
|
|
18460
19010
|
return;
|
|
18461
19011
|
}
|
|
18462
|
-
console.log(kleur20.green(`Preview closed: ${result.previewId}`));
|
|
19012
|
+
console.log(kleur20.green(`Preview closed: ${result.previewId}${result.siteStopped ? "; its site was stopped" : ""}`));
|
|
18463
19013
|
} catch (error) {
|
|
18464
19014
|
exitWithPreviewError(error);
|
|
18465
19015
|
}
|