austere-axi 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -10
- package/dist/austere.js +2 -2
- package/dist/catalog.json +1 -1
- package/dist/chrome/chrome.css +1 -1
- package/dist/chrome/chrome.js +4 -4
- package/dist/cli.mjs +788 -298
- package/dist/packs/mermaid/catalog.json +2 -2
- package/dist/packs/mermaid/pack.js +1 -1
- package/dist/packs/shadcn/catalog.json +429 -0
- package/dist/packs/shadcn/pack.js +69 -0
- package/dist/prompt.md +5 -4
- package/dist/spec.json +1 -1
- package/package.json +19 -3
- package/skills/austere-setup/SKILL.md +3 -4
- package/types/catalog.d.ts +82 -0
- package/types/pack-kit.d.ts +46 -0
- package/types/pack.d.ts +104 -0
package/dist/cli.mjs
CHANGED
|
@@ -608,7 +608,7 @@ var init_paths = __esm({
|
|
|
608
608
|
HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
609
609
|
BUNDLED = true;
|
|
610
610
|
ASSET_DIR = BUNDLED ? HERE : path.resolve(HERE, "../../dist");
|
|
611
|
-
VERSION = true ? "0.
|
|
611
|
+
VERSION = true ? "0.4.1" : JSON.parse(fs.readFileSync(path.resolve(HERE, "../../package.json"), "utf8")).version;
|
|
612
612
|
CLI_ENTRY = BUNDLED ? fileURLToPath(import.meta.url) : path.resolve(HERE, "main.js");
|
|
613
613
|
}
|
|
614
614
|
});
|
|
@@ -650,7 +650,10 @@ var init_packs_meta = __esm({
|
|
|
650
650
|
});
|
|
651
651
|
|
|
652
652
|
// src/shared/catalog.js
|
|
653
|
-
function searchCatalog(entries, query, { notesOf = (
|
|
653
|
+
function searchCatalog(entries, query, { notesOf = (
|
|
654
|
+
/** @type {(entry: any) => string[]} */
|
|
655
|
+
(() => [])
|
|
656
|
+
), limit = 8 } = {}) {
|
|
654
657
|
const q2 = String(query || "").trim().toLowerCase();
|
|
655
658
|
const terms = words(q2);
|
|
656
659
|
if (!terms.length) return [];
|
|
@@ -871,12 +874,6 @@ function setHubEnabled(enabled) {
|
|
|
871
874
|
function hubDbFile() {
|
|
872
875
|
return path3.join(stateDir(), "hub.db");
|
|
873
876
|
}
|
|
874
|
-
function hubUnsupported(version2 = process.versions.node) {
|
|
875
|
-
const [a, b] = version2.split(".").map(Number);
|
|
876
|
-
const [x, y] = HUB_MIN_NODE.split(".").map(Number);
|
|
877
|
-
if (a > x || a === x && b >= y) return null;
|
|
878
|
-
return `the hub needs Node ${HUB_MIN_NODE} or newer (for node:sqlite); this is ${version2}`;
|
|
879
|
-
}
|
|
880
877
|
async function loadSqlite() {
|
|
881
878
|
if (DatabaseSync) return DatabaseSync;
|
|
882
879
|
const emit2 = process.emitWarning;
|
|
@@ -901,6 +898,8 @@ function projectOf(file2) {
|
|
|
901
898
|
}
|
|
902
899
|
}
|
|
903
900
|
function itemStatus(entry, pending) {
|
|
901
|
+
if (entry.discarded) return "discarded";
|
|
902
|
+
if (entry.held) return "held";
|
|
904
903
|
if (pending) return "open";
|
|
905
904
|
if (entry.doneAt) return "addressed";
|
|
906
905
|
return "received";
|
|
@@ -912,7 +911,7 @@ function itemText(it2) {
|
|
|
912
911
|
return it2.text ?? it2.note ?? "";
|
|
913
912
|
}
|
|
914
913
|
async function registerWithHub(file2, title, id) {
|
|
915
|
-
if (!hubEnabled()
|
|
914
|
+
if (!hubEnabled()) return;
|
|
916
915
|
try {
|
|
917
916
|
const hub = await Hub.open();
|
|
918
917
|
try {
|
|
@@ -923,11 +922,10 @@ async function registerWithHub(file2, title, id) {
|
|
|
923
922
|
} catch {
|
|
924
923
|
}
|
|
925
924
|
}
|
|
926
|
-
var
|
|
925
|
+
var DatabaseSync, SCHEMA, Hub;
|
|
927
926
|
var init_hub = __esm({
|
|
928
927
|
"src/cli/hub.js"() {
|
|
929
928
|
init_paths();
|
|
930
|
-
HUB_MIN_NODE = "22.13.0";
|
|
931
929
|
DatabaseSync = null;
|
|
932
930
|
SCHEMA = `
|
|
933
931
|
PRAGMA journal_mode = WAL;
|
|
@@ -953,7 +951,8 @@ CREATE TABLE IF NOT EXISTS items (
|
|
|
953
951
|
target TEXT,
|
|
954
952
|
text TEXT,
|
|
955
953
|
status TEXT NOT NULL,
|
|
956
|
-
data TEXT NOT NULL
|
|
954
|
+
data TEXT NOT NULL,
|
|
955
|
+
author TEXT NOT NULL DEFAULT ''
|
|
957
956
|
);
|
|
958
957
|
CREATE INDEX IF NOT EXISTS items_artifact ON items(artifact_id, at);
|
|
959
958
|
CREATE TABLE IF NOT EXISTS replies (
|
|
@@ -971,6 +970,9 @@ CREATE TABLE IF NOT EXISTS replies (
|
|
|
971
970
|
constructor(db) {
|
|
972
971
|
this.db = db;
|
|
973
972
|
db.exec(SCHEMA);
|
|
973
|
+
if (!db.prepare("PRAGMA table_info(items)").all().some((c) => c.name === "author")) {
|
|
974
|
+
db.exec("ALTER TABLE items ADD COLUMN author TEXT NOT NULL DEFAULT ''");
|
|
975
|
+
}
|
|
974
976
|
this.q = {
|
|
975
977
|
upsertArtifact: db.prepare(`
|
|
976
978
|
INSERT INTO artifacts (id, file, project, title, created_at, updated_at, reviewed, status, ended_by)
|
|
@@ -982,9 +984,9 @@ CREATE TABLE IF NOT EXISTS replies (
|
|
|
982
984
|
status = COALESCE(excluded.status, artifacts.status),
|
|
983
985
|
ended_by = CASE WHEN excluded.status IS NULL THEN artifacts.ended_by ELSE excluded.ended_by END`),
|
|
984
986
|
upsertItem: db.prepare(`
|
|
985
|
-
INSERT INTO items (id, artifact_id, entry_id, at, kind, target, text, status, data)
|
|
986
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
987
|
-
ON CONFLICT(id) DO UPDATE SET status = excluded.status, text = excluded.text, data = excluded.data`),
|
|
987
|
+
INSERT INTO items (id, artifact_id, entry_id, at, kind, target, text, status, data, author)
|
|
988
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
989
|
+
ON CONFLICT(id) DO UPDATE SET status = excluded.status, text = excluded.text, data = excluded.data, author = excluded.author`),
|
|
988
990
|
upsertReply: db.prepare(`INSERT INTO replies (id, artifact_id, at, text) VALUES (?, ?, ?, ?) ON CONFLICT(id) DO NOTHING`),
|
|
989
991
|
artifact: db.prepare(`SELECT * FROM artifacts WHERE id = ?`)
|
|
990
992
|
};
|
|
@@ -1011,7 +1013,7 @@ CREATE TABLE IF NOT EXISTS replies (
|
|
|
1011
1013
|
const status = itemStatus(e, pending.has(e.id));
|
|
1012
1014
|
(e.items || []).forEach((it2, i) => {
|
|
1013
1015
|
const id = it2.id || `${e.id}.${i}`;
|
|
1014
|
-
this.q.upsertItem.run(id, s.id, e.id, e.at, it2.kind || "message", it2.target || it2.statement || null, itemText(it2), status, JSON.stringify(it2));
|
|
1016
|
+
this.q.upsertItem.run(id, s.id, e.id, e.at, it2.kind || "message", it2.target || it2.statement || null, itemText(it2), status, JSON.stringify(it2), e.author || "");
|
|
1015
1017
|
});
|
|
1016
1018
|
}
|
|
1017
1019
|
this.db.exec("COMMIT");
|
|
@@ -1030,29 +1032,154 @@ CREATE TABLE IF NOT EXISTS replies (
|
|
|
1030
1032
|
`SELECT a.*,
|
|
1031
1033
|
SUM(CASE WHEN i.status = 'open' THEN 1 ELSE 0 END) AS open,
|
|
1032
1034
|
SUM(CASE WHEN i.status = 'received' THEN 1 ELSE 0 END) AS received,
|
|
1033
|
-
SUM(CASE WHEN i.status = 'addressed' THEN 1 ELSE 0 END) AS addressed
|
|
1035
|
+
SUM(CASE WHEN i.status = 'addressed' THEN 1 ELSE 0 END) AS addressed,
|
|
1036
|
+
SUM(CASE WHEN i.status = 'held' THEN 1 ELSE 0 END) AS held
|
|
1034
1037
|
FROM artifacts a LEFT JOIN items i ON i.artifact_id = a.id
|
|
1035
1038
|
WHERE (? = '' OR a.title LIKE ? ESCAPE '\\' OR a.file LIKE ? ESCAPE '\\' OR a.project LIKE ? ESCAPE '\\')
|
|
1036
1039
|
AND (? = '' OR a.project = ?)
|
|
1037
1040
|
GROUP BY a.id ORDER BY a.updated_at DESC LIMIT ?`
|
|
1038
|
-
).all(q2, like, like, like, project, project, limit).map((r) => ({ ...r, open: r.open || 0, received: r.received || 0, addressed: r.addressed || 0 }));
|
|
1041
|
+
).all(q2, like, like, like, project, project, limit).map((r) => ({ ...r, open: r.open || 0, received: r.received || 0, addressed: r.addressed || 0, held: r.held || 0 }));
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Notes (annotations and messages) across all pages, newest first.
|
|
1045
|
+
* `q` matches note text, target and page title. `status`: "unanswered" (held, open and received), "answered", or "" for all.
|
|
1046
|
+
* `author`: a reviewer's name, "owner" for the owner's own notes, or "" for everyone. Discarded notes are left out.
|
|
1047
|
+
* @param {{ q?: string, status?: string, project?: string, author?: string, limit?: number }} [o]
|
|
1048
|
+
*/
|
|
1049
|
+
notes({ q: q2 = "", status = "", project = "", author = "", limit = 200 } = {}) {
|
|
1050
|
+
const anyAuthor = author === "" ? "all" : "one";
|
|
1051
|
+
const who = author === "owner" ? "" : author;
|
|
1052
|
+
const like = `%${q2.replace(/[%_\\]/g, (c) => `\\${c}`)}%`;
|
|
1053
|
+
return this.db.prepare(
|
|
1054
|
+
`SELECT i.id, i.artifact_id, i.at, i.kind, i.target, i.text, i.status, i.author,
|
|
1055
|
+
a.title, a.file, a.project
|
|
1056
|
+
FROM items i JOIN artifacts a ON a.id = i.artifact_id
|
|
1057
|
+
WHERE i.kind IN ('annotation', 'message') AND i.status != 'discarded'
|
|
1058
|
+
AND (? = 'all' OR i.author = ?)
|
|
1059
|
+
AND (? = '' OR i.text LIKE ? ESCAPE '\\' OR i.target LIKE ? ESCAPE '\\' OR a.title LIKE ? ESCAPE '\\')
|
|
1060
|
+
AND (? = '' OR a.project = ?)
|
|
1061
|
+
AND (? = '' OR (? = 'unanswered' AND i.status != 'addressed') OR (? = 'answered' AND i.status = 'addressed'))
|
|
1062
|
+
ORDER BY i.at DESC, i.id LIMIT ?`
|
|
1063
|
+
).all(anyAuthor, who, q2, like, like, like, project, project, status, status, status, limit);
|
|
1064
|
+
}
|
|
1065
|
+
/** Reviewer names with notes in the index (the owner's own notes are not listed). */
|
|
1066
|
+
authors() {
|
|
1067
|
+
return this.db.prepare(`SELECT DISTINCT author FROM items WHERE author != '' AND status != 'discarded' ORDER BY author COLLATE NOCASE`).all().map((r) => String(r.author));
|
|
1039
1068
|
}
|
|
1040
1069
|
stats() {
|
|
1041
1070
|
return this.db.prepare(
|
|
1042
1071
|
`SELECT (SELECT COUNT(*) FROM artifacts) AS artifacts, (SELECT COUNT(DISTINCT project) FROM artifacts) AS projects,
|
|
1043
|
-
(SELECT COUNT(*) FROM items WHERE status
|
|
1072
|
+
(SELECT COUNT(*) FROM items WHERE status IN ('open', 'received', 'held')) AS unanswered`
|
|
1044
1073
|
).get();
|
|
1045
1074
|
}
|
|
1046
1075
|
};
|
|
1047
1076
|
}
|
|
1048
1077
|
});
|
|
1049
1078
|
|
|
1050
|
-
// src/cli/
|
|
1079
|
+
// src/cli/invites.js
|
|
1051
1080
|
import crypto from "node:crypto";
|
|
1052
1081
|
import fs6 from "node:fs";
|
|
1053
1082
|
import path5 from "node:path";
|
|
1083
|
+
function safeEqual(a, b) {
|
|
1084
|
+
const x = crypto.createHash("sha256").update(String(a)).digest();
|
|
1085
|
+
const y = crypto.createHash("sha256").update(String(b)).digest();
|
|
1086
|
+
return crypto.timingSafeEqual(x, y);
|
|
1087
|
+
}
|
|
1088
|
+
function cleanName(raw) {
|
|
1089
|
+
const name = String(raw ?? "").replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
|
|
1090
|
+
if (!name) throw new Error('a reviewer name is required: --as "<name>"');
|
|
1091
|
+
if (name.length > 40) throw new Error("the reviewer name is longer than 40 characters");
|
|
1092
|
+
if (RESERVED.has(name.toLowerCase())) throw new Error(`"${name}" is reserved; pick another name`);
|
|
1093
|
+
return name;
|
|
1094
|
+
}
|
|
1095
|
+
function tokenFromCookie(header, name = COOKIE) {
|
|
1096
|
+
for (const part of String(header || "").split(";")) {
|
|
1097
|
+
const i = part.indexOf("=");
|
|
1098
|
+
if (i > 0 && part.slice(0, i).trim() === name) return part.slice(i + 1).trim();
|
|
1099
|
+
}
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1102
|
+
function inviteId(inv) {
|
|
1103
|
+
return inv.token.slice(0, 8);
|
|
1104
|
+
}
|
|
1105
|
+
function ownerKey(dir) {
|
|
1106
|
+
const file2 = path5.join(dir, "owner.key");
|
|
1107
|
+
try {
|
|
1108
|
+
const key2 = fs6.readFileSync(file2, "utf8").trim();
|
|
1109
|
+
if (key2.length >= 22) return key2;
|
|
1110
|
+
} catch {
|
|
1111
|
+
}
|
|
1112
|
+
const key = crypto.randomBytes(24).toString("base64url");
|
|
1113
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
1114
|
+
fs6.writeFileSync(file2, key + "\n", { mode: 384 });
|
|
1115
|
+
return key;
|
|
1116
|
+
}
|
|
1117
|
+
var COOKIE, RESERVED, InviteStore, OWNER_COOKIE;
|
|
1118
|
+
var init_invites = __esm({
|
|
1119
|
+
"src/cli/invites.js"() {
|
|
1120
|
+
COOKIE = "austere_rv";
|
|
1121
|
+
RESERVED = /* @__PURE__ */ new Set(["owner", "you", "agent", "me"]);
|
|
1122
|
+
InviteStore = class {
|
|
1123
|
+
/** @param {string} dir state dir */
|
|
1124
|
+
constructor(dir) {
|
|
1125
|
+
this.file = path5.join(dir, "invites.json");
|
|
1126
|
+
this.invites = [];
|
|
1127
|
+
try {
|
|
1128
|
+
const raw = JSON.parse(fs6.readFileSync(this.file, "utf8"));
|
|
1129
|
+
if (Array.isArray(raw.invites)) {
|
|
1130
|
+
this.invites = raw.invites.filter((i) => i && typeof i.token === "string" && i.token.length >= 22 && i.name && i.sessionId);
|
|
1131
|
+
}
|
|
1132
|
+
} catch {
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
save() {
|
|
1136
|
+
const tmp = `${this.file}.${process.pid}.tmp`;
|
|
1137
|
+
fs6.writeFileSync(tmp, JSON.stringify({ invites: this.invites }, null, 2) + "\n", { mode: 384 });
|
|
1138
|
+
fs6.renameSync(tmp, this.file);
|
|
1139
|
+
}
|
|
1140
|
+
/** @returns {Invite} */
|
|
1141
|
+
add({ name, sessionId: sessionId2, file: file2, auto = false }) {
|
|
1142
|
+
const clean = cleanName(name);
|
|
1143
|
+
if (this.invites.some((i) => i.sessionId === sessionId2 && i.name.toLowerCase() === clean.toLowerCase())) {
|
|
1144
|
+
throw new Error(`this page is already shared with "${clean}"; revoke that invite first (share revoke "${clean}")`);
|
|
1145
|
+
}
|
|
1146
|
+
const inv = { token: crypto.randomBytes(24).toString("base64url"), name: clean, sessionId: sessionId2, file: file2, auto: Boolean(auto), createdAt: Date.now() };
|
|
1147
|
+
this.invites.push(inv);
|
|
1148
|
+
this.save();
|
|
1149
|
+
return inv;
|
|
1150
|
+
}
|
|
1151
|
+
/** The invite for a token, or null. Compares every token, so timing does not show a near miss. */
|
|
1152
|
+
find(token) {
|
|
1153
|
+
if (typeof token !== "string" || !token) return null;
|
|
1154
|
+
let hit = null;
|
|
1155
|
+
for (const inv of this.invites) if (safeEqual(inv.token, token) && !hit) hit = inv;
|
|
1156
|
+
return hit;
|
|
1157
|
+
}
|
|
1158
|
+
list() {
|
|
1159
|
+
return [...this.invites];
|
|
1160
|
+
}
|
|
1161
|
+
/** Remove the invites a key names: a full token, its short id, or a reviewer name. Returns what was removed. */
|
|
1162
|
+
revoke(key) {
|
|
1163
|
+
const k = String(key || "").trim();
|
|
1164
|
+
if (!k) return [];
|
|
1165
|
+
const gone = this.invites.filter((i) => safeEqual(i.token, k) || k.length === 8 && inviteId(i) === k || i.name.toLowerCase() === k.toLowerCase());
|
|
1166
|
+
if (gone.length) {
|
|
1167
|
+
this.invites = this.invites.filter((i) => !gone.includes(i));
|
|
1168
|
+
this.save();
|
|
1169
|
+
}
|
|
1170
|
+
return gone;
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
OWNER_COOKIE = "austere_owner";
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
// src/cli/sessions.js
|
|
1178
|
+
import crypto2 from "node:crypto";
|
|
1179
|
+
import fs7 from "node:fs";
|
|
1180
|
+
import path6 from "node:path";
|
|
1054
1181
|
function sessionId(file2) {
|
|
1055
|
-
return
|
|
1182
|
+
return crypto2.createHash("sha1").update(file2).digest("hex").slice(0, 12);
|
|
1056
1183
|
}
|
|
1057
1184
|
function newId(prefix) {
|
|
1058
1185
|
return `${prefix}${Date.now().toString(36)}${(++seq).toString(36)}`;
|
|
@@ -1061,7 +1188,7 @@ function hydrate(s) {
|
|
|
1061
1188
|
return {
|
|
1062
1189
|
id: s.id,
|
|
1063
1190
|
file: s.file,
|
|
1064
|
-
title: s.title ||
|
|
1191
|
+
title: s.title || path6.basename(s.file),
|
|
1065
1192
|
createdAt: s.createdAt || Date.now(),
|
|
1066
1193
|
updatedAt: s.updatedAt || Date.now(),
|
|
1067
1194
|
status: s.status || "open",
|
|
@@ -1082,17 +1209,20 @@ var init_sessions = __esm({
|
|
|
1082
1209
|
MAX_TRANSCRIPT = 400;
|
|
1083
1210
|
seq = 0;
|
|
1084
1211
|
SessionStore = class {
|
|
1085
|
-
/**
|
|
1212
|
+
/**
|
|
1213
|
+
* @param {string} dir
|
|
1214
|
+
* @param {{ onFlush?: (session) => void }} [options] onFlush runs after a session is written (the hub mirrors it)
|
|
1215
|
+
*/
|
|
1086
1216
|
constructor(dir, { onFlush } = {}) {
|
|
1087
1217
|
this.onFlush = onFlush || null;
|
|
1088
|
-
this.dir =
|
|
1089
|
-
|
|
1218
|
+
this.dir = path6.join(dir, "sessions");
|
|
1219
|
+
fs7.mkdirSync(this.dir, { recursive: true });
|
|
1090
1220
|
this.sessions = /* @__PURE__ */ new Map();
|
|
1091
1221
|
this.timers = /* @__PURE__ */ new Map();
|
|
1092
|
-
for (const f of
|
|
1222
|
+
for (const f of fs7.readdirSync(this.dir)) {
|
|
1093
1223
|
if (!f.endsWith(".json")) continue;
|
|
1094
1224
|
try {
|
|
1095
|
-
const s = JSON.parse(
|
|
1225
|
+
const s = JSON.parse(fs7.readFileSync(path6.join(this.dir, f), "utf8"));
|
|
1096
1226
|
if (s && s.id && s.file) this.sessions.set(s.id, hydrate(s));
|
|
1097
1227
|
} catch {
|
|
1098
1228
|
}
|
|
@@ -1139,11 +1269,11 @@ var init_sessions = __esm({
|
|
|
1139
1269
|
pending: s.pending,
|
|
1140
1270
|
transcript: s.transcript.slice(-MAX_TRANSCRIPT)
|
|
1141
1271
|
};
|
|
1142
|
-
const file2 =
|
|
1272
|
+
const file2 = path6.join(this.dir, `${s.id}.json`);
|
|
1143
1273
|
const tmp = `${file2}.${process.pid}.tmp`;
|
|
1144
1274
|
try {
|
|
1145
|
-
|
|
1146
|
-
|
|
1275
|
+
fs7.writeFileSync(tmp, JSON.stringify(data));
|
|
1276
|
+
fs7.renameSync(tmp, file2);
|
|
1147
1277
|
} catch {
|
|
1148
1278
|
}
|
|
1149
1279
|
if (this.onFlush) {
|
|
@@ -1157,14 +1287,46 @@ var init_sessions = __esm({
|
|
|
1157
1287
|
for (const s of this.sessions.values()) if (this.timers.has(s.id)) this.flush(s);
|
|
1158
1288
|
}
|
|
1159
1289
|
// ── conversation ──
|
|
1160
|
-
/**
|
|
1161
|
-
|
|
1290
|
+
/**
|
|
1291
|
+
* Reviewer sent a batch of items. Returns the transcript entry.
|
|
1292
|
+
* `author` is a team reviewer's name (empty: the owner). `hold` keeps the entry out of
|
|
1293
|
+
* `pending` until the owner releases it.
|
|
1294
|
+
*/
|
|
1295
|
+
addUserBatch(s, items, { end = false, author = "", hold = false } = {}) {
|
|
1162
1296
|
const entry = { id: newId("u"), role: "user", at: Date.now(), items, end: end || void 0, seenAt: null, doneAt: null };
|
|
1297
|
+
if (author) entry.author = author;
|
|
1298
|
+
if (hold) entry.held = true;
|
|
1163
1299
|
s.transcript.push(entry);
|
|
1164
|
-
s.pending.push(entry.id);
|
|
1300
|
+
if (!hold) s.pending.push(entry.id);
|
|
1165
1301
|
this.touch(s);
|
|
1166
1302
|
return entry;
|
|
1167
1303
|
}
|
|
1304
|
+
/** Held entries, oldest first. */
|
|
1305
|
+
heldEntries(s) {
|
|
1306
|
+
return s.transcript.filter((e) => e.role === "user" && e.held);
|
|
1307
|
+
}
|
|
1308
|
+
/** The owner sends held notes on to the agent (`ids` omitted: all of them). Returns the entries released. */
|
|
1309
|
+
release(s, ids) {
|
|
1310
|
+
const want = ids ? new Set(ids) : null;
|
|
1311
|
+
const out = this.heldEntries(s).filter((e) => !want || want.has(e.id));
|
|
1312
|
+
for (const e of out) {
|
|
1313
|
+
e.held = false;
|
|
1314
|
+
s.pending.push(e.id);
|
|
1315
|
+
}
|
|
1316
|
+
if (out.length) this.touch(s);
|
|
1317
|
+
return out;
|
|
1318
|
+
}
|
|
1319
|
+
/** The owner drops held notes. They stay on record, marked discarded, and never reach the agent. */
|
|
1320
|
+
discard(s, ids) {
|
|
1321
|
+
const want = ids ? new Set(ids) : null;
|
|
1322
|
+
const out = this.heldEntries(s).filter((e) => !want || want.has(e.id));
|
|
1323
|
+
for (const e of out) {
|
|
1324
|
+
e.held = false;
|
|
1325
|
+
e.discarded = true;
|
|
1326
|
+
}
|
|
1327
|
+
if (out.length) this.touch(s);
|
|
1328
|
+
return out;
|
|
1329
|
+
}
|
|
1168
1330
|
addAgentReply(s, text) {
|
|
1169
1331
|
const now = Date.now();
|
|
1170
1332
|
for (const e of s.transcript) if (e.role === "user" && e.seenAt && !e.doneAt) e.doneAt = now;
|
|
@@ -1182,10 +1344,13 @@ var init_sessions = __esm({
|
|
|
1182
1344
|
*/
|
|
1183
1345
|
feedback(s, { all = false, peek = false } = {}) {
|
|
1184
1346
|
const pending = new Set(s.pending);
|
|
1185
|
-
const picked = s.transcript.filter(
|
|
1347
|
+
const picked = s.transcript.filter(
|
|
1348
|
+
(e) => e.role === "user" && !e.held && !e.discarded && (e.items || []).length && (all || pending.has(e.id) || !e.doneAt)
|
|
1349
|
+
);
|
|
1186
1350
|
const entries = picked.map((e) => ({
|
|
1187
1351
|
id: e.id,
|
|
1188
1352
|
at: e.at,
|
|
1353
|
+
author: e.author || void 0,
|
|
1189
1354
|
items: e.items,
|
|
1190
1355
|
status: pending.has(e.id) ? "open" : e.doneAt ? "addressed" : "received"
|
|
1191
1356
|
}));
|
|
@@ -1223,8 +1388,8 @@ var init_sessions = __esm({
|
|
|
1223
1388
|
});
|
|
1224
1389
|
|
|
1225
1390
|
// src/cli/export.js
|
|
1226
|
-
import
|
|
1227
|
-
import
|
|
1391
|
+
import fs8 from "node:fs";
|
|
1392
|
+
import path7 from "node:path";
|
|
1228
1393
|
function inlineScript(code) {
|
|
1229
1394
|
return `<script>${code.replace(/<\/script/gi, "<\\/script")}</script>`;
|
|
1230
1395
|
}
|
|
@@ -1234,12 +1399,12 @@ function exportHtml(html) {
|
|
|
1234
1399
|
if (art.program == null) throw new Error('no <script type="text/openui"> block');
|
|
1235
1400
|
let tags = "";
|
|
1236
1401
|
if (/\bMermaid\s*\(/.test(art.program)) {
|
|
1237
|
-
tags += inlineScript(
|
|
1402
|
+
tags += inlineScript(fs8.readFileSync(assetPath("mermaid.min.js"), "utf8")) + "\n";
|
|
1238
1403
|
}
|
|
1239
|
-
tags += inlineScript(
|
|
1240
|
-
for (const p of implicitPacks(art.program)) tags += "\n" + inlineScript(
|
|
1404
|
+
tags += inlineScript(fs8.readFileSync(assetPath("austere.js"), "utf8"));
|
|
1405
|
+
for (const p of implicitPacks(art.program)) tags += "\n" + inlineScript(fs8.readFileSync(path7.join(p.dir, "pack.js"), "utf8"));
|
|
1241
1406
|
const packs = pagePacks(html);
|
|
1242
|
-
for (const p of packs.installed) tags += "\n" + inlineScript(
|
|
1407
|
+
for (const p of packs.installed) tags += "\n" + inlineScript(fs8.readFileSync(path7.join(p.dir, "pack.js"), "utf8"));
|
|
1243
1408
|
for (const p of [...packs.missing, ...packs.mismatched]) {
|
|
1244
1409
|
notices.push(`Pack ${p.name}@${p.version} is not installed here, so the exported page loads it from the CDN (needs network).`);
|
|
1245
1410
|
}
|
|
@@ -1267,10 +1432,10 @@ __export(install_exports, {
|
|
|
1267
1432
|
removePack: () => removePack,
|
|
1268
1433
|
untar: () => untar
|
|
1269
1434
|
});
|
|
1270
|
-
import
|
|
1271
|
-
import
|
|
1435
|
+
import fs9 from "node:fs";
|
|
1436
|
+
import path8 from "node:path";
|
|
1272
1437
|
import zlib from "node:zlib";
|
|
1273
|
-
import
|
|
1438
|
+
import crypto3 from "node:crypto";
|
|
1274
1439
|
function registryUrl() {
|
|
1275
1440
|
const r = process.env.AUSTERE_AXI_NPM_REGISTRY || process.env.npm_config_registry || "https://registry.npmjs.org/";
|
|
1276
1441
|
return r.endsWith("/") ? r : r + "/";
|
|
@@ -1279,7 +1444,7 @@ function looksLikePath(spec) {
|
|
|
1279
1444
|
return /^(\.{1,2}([\\/]|$)|[\\/]|~[\\/]|[A-Za-z]:[\\/])/.test(spec) || /\.t(ar\.)?gz$/i.test(spec) || spec.includes("\\") || spec.includes("/") && !spec.startsWith("@");
|
|
1280
1445
|
}
|
|
1281
1446
|
function sri(alg, buf) {
|
|
1282
|
-
return `${alg}-${
|
|
1447
|
+
return `${alg}-${crypto3.createHash(alg).update(buf).digest("base64")}`;
|
|
1283
1448
|
}
|
|
1284
1449
|
function untar(buf) {
|
|
1285
1450
|
const files = /* @__PURE__ */ new Map();
|
|
@@ -1350,13 +1515,11 @@ async function fetchBuffer(url2, accept) {
|
|
|
1350
1515
|
try {
|
|
1351
1516
|
res = await fetch(url2, { headers: accept ? { accept } : {}, redirect: "follow" });
|
|
1352
1517
|
} catch (err) {
|
|
1353
|
-
const e = new Error(`could not reach ${url2} (${err.cause?.code || err.message})`);
|
|
1354
|
-
e.network = true;
|
|
1518
|
+
const e = Object.assign(new Error(`could not reach ${url2} (${err.cause?.code || err.message})`), { network: true });
|
|
1355
1519
|
throw e;
|
|
1356
1520
|
}
|
|
1357
1521
|
if (!res.ok) {
|
|
1358
|
-
const e = new Error(`${url2}: HTTP ${res.status}`);
|
|
1359
|
-
e.status = res.status;
|
|
1522
|
+
const e = Object.assign(new Error(`${url2}: HTTP ${res.status}`), { status: res.status });
|
|
1360
1523
|
throw e;
|
|
1361
1524
|
}
|
|
1362
1525
|
const len = Number(res.headers.get("content-length") || 0);
|
|
@@ -1386,7 +1549,7 @@ async function fromRegistry(spec) {
|
|
|
1386
1549
|
if (!["sha512", "sha384", "sha256"].includes(alg) || sri(alg, tgz) !== dist.integrity) {
|
|
1387
1550
|
throw new Error(`${name}@${version2}: the tarball does not match the registry's integrity hash`);
|
|
1388
1551
|
}
|
|
1389
|
-
} else if (dist.shasum &&
|
|
1552
|
+
} else if (dist.shasum && crypto3.createHash("sha1").update(tgz).digest("hex") !== dist.shasum) {
|
|
1390
1553
|
throw new Error(`${name}@${version2}: the tarball does not match the registry's shasum`);
|
|
1391
1554
|
}
|
|
1392
1555
|
return { ...readTarball(tgz, `${name}@${version2}`), expect: { name, version: version2 }, source: "npm", resolved: dist.tarball };
|
|
@@ -1406,15 +1569,15 @@ function readTarball(tgz, label) {
|
|
|
1406
1569
|
return { packJs, catalog, packageJson: files.get("package.json") || null };
|
|
1407
1570
|
}
|
|
1408
1571
|
function fromTarball(file2) {
|
|
1409
|
-
const abs =
|
|
1410
|
-
if (!
|
|
1411
|
-
return { ...readTarball(
|
|
1572
|
+
const abs = path8.resolve(file2.replace(/^~(?=[\\/])/, process.env.HOME || ""));
|
|
1573
|
+
if (!fs9.existsSync(abs)) throw new Error(`no such file: ${abs}`);
|
|
1574
|
+
return { ...readTarball(fs9.readFileSync(abs), path8.basename(abs)), source: "tarball", resolved: abs };
|
|
1412
1575
|
}
|
|
1413
1576
|
function fromFolder(dir) {
|
|
1414
|
-
const abs =
|
|
1415
|
-
if (!
|
|
1416
|
-
if (
|
|
1417
|
-
const read = (rel) =>
|
|
1577
|
+
const abs = path8.resolve(dir.replace(/^~(?=[\\/])/, process.env.HOME || ""));
|
|
1578
|
+
if (!fs9.existsSync(abs)) throw new Error(`no such folder: ${abs}`);
|
|
1579
|
+
if (fs9.statSync(abs).isFile()) return fromTarball(abs);
|
|
1580
|
+
const read = (rel) => fs9.existsSync(path8.join(abs, rel)) ? fs9.readFileSync(path8.join(abs, rel)) : null;
|
|
1418
1581
|
const packJs = read("dist/pack.js");
|
|
1419
1582
|
const catalog = read("dist/catalog.json");
|
|
1420
1583
|
if (!packJs || !catalog) throw new Error(`${abs} has no dist/pack.js and dist/catalog.json: run \`austere-axi components build ${dir}\` first`);
|
|
@@ -1444,21 +1607,23 @@ async function addPack(spec, { trust = false } = {}) {
|
|
|
1444
1607
|
if (!local) {
|
|
1445
1608
|
const { name } = spec.startsWith("@") || spec.includes("@") ? splitSpec(spec) : { name: spec };
|
|
1446
1609
|
if (!isFirstParty(name) && !trust) {
|
|
1447
|
-
|
|
1448
|
-
|
|
1610
|
+
throw Object.assign(
|
|
1611
|
+
new Error(
|
|
1612
|
+
`${name} is a third-party pack: its code would run inside your review pages, where it can read the page and add items to the feedback queue. Look at its source first (npm view ${name} repository), then rerun with --trust.`
|
|
1613
|
+
),
|
|
1614
|
+
{ code: "untrusted" }
|
|
1449
1615
|
);
|
|
1450
|
-
e.code = "untrusted";
|
|
1451
|
-
throw e;
|
|
1452
1616
|
}
|
|
1453
1617
|
}
|
|
1454
1618
|
const got = local ? fromFolder(spec) : await fromRegistry(spec);
|
|
1455
1619
|
if (got.source === "tarball" && !trust) {
|
|
1456
|
-
const peek = parseCatalog(got.catalog,
|
|
1457
|
-
|
|
1458
|
-
|
|
1620
|
+
const peek = parseCatalog(got.catalog, path8.basename(got.resolved));
|
|
1621
|
+
throw Object.assign(
|
|
1622
|
+
new Error(
|
|
1623
|
+
`${path8.basename(got.resolved)} (${peek.pack}) comes from a file, not the registry: its code would run inside your review pages. Rerun with --trust once you have looked at it.`
|
|
1624
|
+
),
|
|
1625
|
+
{ code: "untrusted" }
|
|
1459
1626
|
);
|
|
1460
|
-
e.code = "untrusted";
|
|
1461
|
-
throw e;
|
|
1462
1627
|
}
|
|
1463
1628
|
const label = got.expect ? `${got.expect.name}@${got.expect.version}` : spec;
|
|
1464
1629
|
const cat = parseCatalog(got.catalog, label);
|
|
@@ -1480,14 +1645,14 @@ async function addPack(spec, { trust = false } = {}) {
|
|
|
1480
1645
|
}
|
|
1481
1646
|
const lock = readLock();
|
|
1482
1647
|
const previous = lock.packs[cat.pack];
|
|
1483
|
-
const dir =
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
if (got.packageJson)
|
|
1648
|
+
const dir = path8.join(packsDir(), packDirName(cat.pack, cat.version));
|
|
1649
|
+
fs9.rmSync(dir, { recursive: true, force: true });
|
|
1650
|
+
fs9.mkdirSync(dir, { recursive: true });
|
|
1651
|
+
fs9.writeFileSync(path8.join(dir, "pack.js"), got.packJs);
|
|
1652
|
+
fs9.writeFileSync(path8.join(dir, "catalog.json"), got.catalog);
|
|
1653
|
+
if (got.packageJson) fs9.writeFileSync(path8.join(dir, "package.json"), got.packageJson);
|
|
1489
1654
|
if (previous && previous.version !== cat.version) {
|
|
1490
|
-
|
|
1655
|
+
fs9.rmSync(path8.join(packsDir(), packDirName(cat.pack, previous.version)), { recursive: true, force: true });
|
|
1491
1656
|
}
|
|
1492
1657
|
lock.packs[cat.pack] = {
|
|
1493
1658
|
version: cat.version,
|
|
@@ -1507,7 +1672,7 @@ function removePack(name) {
|
|
|
1507
1672
|
if (!entry) return null;
|
|
1508
1673
|
delete lock.packs[name];
|
|
1509
1674
|
writeLock(lock);
|
|
1510
|
-
|
|
1675
|
+
fs9.rmSync(path8.join(packsDir(), packDirName(name, entry.version)), { recursive: true, force: true });
|
|
1511
1676
|
return { name, version: entry.version };
|
|
1512
1677
|
}
|
|
1513
1678
|
var MAX_TARBALL, MAX_PACK_JS;
|
|
@@ -1527,8 +1692,8 @@ __export(pack_dev_exports, {
|
|
|
1527
1692
|
buildPackDir: () => buildPackDir,
|
|
1528
1693
|
newPack: () => newPack
|
|
1529
1694
|
});
|
|
1530
|
-
import
|
|
1531
|
-
import
|
|
1695
|
+
import fs10 from "node:fs";
|
|
1696
|
+
import path9 from "node:path";
|
|
1532
1697
|
import { createRequire } from "node:module";
|
|
1533
1698
|
import { pathToFileURL } from "node:url";
|
|
1534
1699
|
function packageJson(name) {
|
|
@@ -1553,7 +1718,7 @@ Components for [austere-axi](https://github.com/MandalorianBatman/austere-axi) r
|
|
|
1553
1718
|
npm install
|
|
1554
1719
|
npm run build # dist/pack.js + dist/catalog.json
|
|
1555
1720
|
npx austere-axi components add . # use it on this machine
|
|
1556
|
-
npx austere-axi components show
|
|
1721
|
+
npx austere-axi components show Milestones
|
|
1557
1722
|
\`\`\`
|
|
1558
1723
|
|
|
1559
1724
|
A page that uses it must declare it: \`austere-axi check --fix page.html\` writes the
|
|
@@ -1564,10 +1729,10 @@ Publish with \`npm publish\` after \`npm run build\`. Others install it with
|
|
|
1564
1729
|
`;
|
|
1565
1730
|
}
|
|
1566
1731
|
function newPack(dir, { name } = {}) {
|
|
1567
|
-
if (!dir) throw new Error("new needs a folder: austere-axi components new austere-pack-
|
|
1568
|
-
const abs =
|
|
1569
|
-
if (
|
|
1570
|
-
const pkgName = name ||
|
|
1732
|
+
if (!dir) throw new Error("new needs a folder: austere-axi components new austere-pack-milestones");
|
|
1733
|
+
const abs = path9.resolve(dir);
|
|
1734
|
+
if (fs10.existsSync(abs) && fs10.readdirSync(abs).length) throw new Error(`${abs} is not empty`);
|
|
1735
|
+
const pkgName = name || path9.basename(abs).toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
|
|
1571
1736
|
if (!isValidPackName(pkgName)) throw new Error(`${JSON.stringify(pkgName)} is not an npm package name; pass --name`);
|
|
1572
1737
|
const files = {
|
|
1573
1738
|
"package.json": JSON.stringify(packageJson(pkgName), null, 2) + "\n",
|
|
@@ -1576,13 +1741,13 @@ function newPack(dir, { name } = {}) {
|
|
|
1576
1741
|
...TEMPLATE
|
|
1577
1742
|
};
|
|
1578
1743
|
for (const [rel, text] of Object.entries(files)) {
|
|
1579
|
-
|
|
1580
|
-
|
|
1744
|
+
fs10.mkdirSync(path9.dirname(path9.join(abs, rel)), { recursive: true });
|
|
1745
|
+
fs10.writeFileSync(path9.join(abs, rel), text);
|
|
1581
1746
|
}
|
|
1582
1747
|
return { dir: abs, name: pkgName, files: Object.keys(files) };
|
|
1583
1748
|
}
|
|
1584
1749
|
async function findEsbuild(dir) {
|
|
1585
|
-
for (const base of [
|
|
1750
|
+
for (const base of [path9.join(dir, "package.json"), path9.join(process.cwd(), "package.json"), import.meta.url]) {
|
|
1586
1751
|
try {
|
|
1587
1752
|
const resolved = createRequire(base).resolve("esbuild");
|
|
1588
1753
|
return await import(pathToFileURL(resolved).href);
|
|
@@ -1592,8 +1757,8 @@ async function findEsbuild(dir) {
|
|
|
1592
1757
|
return null;
|
|
1593
1758
|
}
|
|
1594
1759
|
async function buildPackDir(dir = ".") {
|
|
1595
|
-
const abs =
|
|
1596
|
-
if (!
|
|
1760
|
+
const abs = path9.resolve(dir);
|
|
1761
|
+
if (!fs10.existsSync(path9.join(abs, "package.json"))) throw new Error(`${abs} has no package.json (\`austere-axi components new <dir>\` makes a pack)`);
|
|
1597
1762
|
const esbuild = await findEsbuild(abs);
|
|
1598
1763
|
if (!esbuild) throw new Error(`components build needs esbuild: run \`npm install -D esbuild\` in ${abs}`);
|
|
1599
1764
|
process.env.OPENUI_TELEMETRY_DISABLED ||= "1";
|
|
@@ -1620,12 +1785,12 @@ var init_pack_dev = __esm({
|
|
|
1620
1785
|
init_packs_meta();
|
|
1621
1786
|
TEMPLATE = {
|
|
1622
1787
|
"src/index.js": `// The pack: every component it adds, plus CSS the runtime puts on the page once.
|
|
1623
|
-
import
|
|
1788
|
+
import Milestones from "./Milestones.jsx";
|
|
1624
1789
|
import css from "./style.css";
|
|
1625
1790
|
|
|
1626
|
-
export default { components: [
|
|
1791
|
+
export default { components: [Milestones], css };
|
|
1627
1792
|
`,
|
|
1628
|
-
"src/
|
|
1793
|
+
"src/Milestones.jsx": `// One component per file. austereComponent is the same contract the core
|
|
1629
1794
|
// components follow: PascalCase name, zod props in positional order (append
|
|
1630
1795
|
// only once published), a one-line description, an example program.
|
|
1631
1796
|
// React, zod and "austere-axi/pack" come from the page's runtime; do not
|
|
@@ -1646,10 +1811,10 @@ function parseEvents(lines) {
|
|
|
1646
1811
|
});
|
|
1647
1812
|
}
|
|
1648
1813
|
|
|
1649
|
-
function
|
|
1814
|
+
function MilestonesView({ props }) {
|
|
1650
1815
|
const events = parseEvents(props.events);
|
|
1651
1816
|
return (
|
|
1652
|
-
<ol className="pack-
|
|
1817
|
+
<ol className="pack-milestones">
|
|
1653
1818
|
{events.map((e, i) => (
|
|
1654
1819
|
// data-austere-part makes each event its own annotation target.
|
|
1655
1820
|
<li key={i} data-austere-part={e.what || e.when}>
|
|
@@ -1663,20 +1828,20 @@ function TimelineView({ props }) {
|
|
|
1663
1828
|
}
|
|
1664
1829
|
|
|
1665
1830
|
export default austereComponent({
|
|
1666
|
-
name: "
|
|
1831
|
+
name: "Milestones",
|
|
1667
1832
|
// A core group (Layout Content Data Charts Forms Buttons) or a category of your own.
|
|
1668
1833
|
group: "Content",
|
|
1669
1834
|
props: z.object({
|
|
1670
1835
|
events: z.array(z.string()),
|
|
1671
1836
|
}),
|
|
1672
|
-
description: 'Vertical
|
|
1673
|
-
renderer:
|
|
1674
|
-
example: \`launch =
|
|
1837
|
+
description: 'Vertical milestones. events are "when | what | detail" strings (detail optional), oldest first. A click reaches the agent as the event.',
|
|
1838
|
+
renderer: MilestonesView,
|
|
1839
|
+
example: \`launch = Milestones(["Mon | Freeze | branch cut", "Wed | Canary | 5% of traffic", "Fri | GA"])\`,
|
|
1675
1840
|
});
|
|
1676
1841
|
`,
|
|
1677
1842
|
"src/style.css": `/* Use the page's theme variables (--austere-*, --openui-*) so the pack fits the
|
|
1678
1843
|
dark theme, and prefix every class with the pack's name. */
|
|
1679
|
-
.pack-
|
|
1844
|
+
.pack-milestones {
|
|
1680
1845
|
list-style: none;
|
|
1681
1846
|
margin: 0;
|
|
1682
1847
|
padding: 0 0 0 18px;
|
|
@@ -1686,13 +1851,13 @@ export default austereComponent({
|
|
|
1686
1851
|
font-size: 14px;
|
|
1687
1852
|
min-width: 0;
|
|
1688
1853
|
}
|
|
1689
|
-
.pack-
|
|
1854
|
+
.pack-milestones li {
|
|
1690
1855
|
position: relative;
|
|
1691
1856
|
display: grid;
|
|
1692
1857
|
gap: 2px;
|
|
1693
1858
|
overflow-wrap: anywhere;
|
|
1694
1859
|
}
|
|
1695
|
-
.pack-
|
|
1860
|
+
.pack-milestones li::before {
|
|
1696
1861
|
content: "";
|
|
1697
1862
|
position: absolute;
|
|
1698
1863
|
left: -24px;
|
|
@@ -1702,11 +1867,11 @@ export default austereComponent({
|
|
|
1702
1867
|
border-radius: 50%;
|
|
1703
1868
|
background: var(--austere-accent);
|
|
1704
1869
|
}
|
|
1705
|
-
.pack-
|
|
1870
|
+
.pack-milestones time {
|
|
1706
1871
|
font-size: 12px;
|
|
1707
1872
|
color: var(--austere-muted);
|
|
1708
1873
|
}
|
|
1709
|
-
.pack-
|
|
1874
|
+
.pack-milestones span {
|
|
1710
1875
|
color: var(--austere-muted);
|
|
1711
1876
|
}
|
|
1712
1877
|
`
|
|
@@ -1716,7 +1881,7 @@ export default austereComponent({
|
|
|
1716
1881
|
|
|
1717
1882
|
// src/cli/net.js
|
|
1718
1883
|
import { execFile } from "node:child_process";
|
|
1719
|
-
import
|
|
1884
|
+
import fs11 from "node:fs";
|
|
1720
1885
|
function run(bin, args) {
|
|
1721
1886
|
return new Promise((resolve) => {
|
|
1722
1887
|
execFile(bin, args, { timeout: 2500, windowsHide: true }, (err, stdout) => resolve(err ? null : stdout));
|
|
@@ -1727,7 +1892,7 @@ async function detectTailscale({ fresh = false } = {}) {
|
|
|
1727
1892
|
if (!fresh && Date.now() - cache.at < 3e4) return cache.value;
|
|
1728
1893
|
let value = null;
|
|
1729
1894
|
for (const bin of CANDIDATES) {
|
|
1730
|
-
if (bin.startsWith("/") && !
|
|
1895
|
+
if (bin.startsWith("/") && !fs11.existsSync(bin)) continue;
|
|
1731
1896
|
const out = await run(bin, ["status", "--json"]);
|
|
1732
1897
|
if (!out) continue;
|
|
1733
1898
|
try {
|
|
@@ -1751,6 +1916,19 @@ function allowedHosts(extra = []) {
|
|
|
1751
1916
|
for (const h of String(process.env.AUSTERE_AXI_ALLOWED_HOSTS || "").split(/\s+/)) if (h) set2.add(h.toLowerCase());
|
|
1752
1917
|
return set2;
|
|
1753
1918
|
}
|
|
1919
|
+
function linkUrl() {
|
|
1920
|
+
const raw = String(process.env.AUSTERE_AXI_LINK_URL || "").trim();
|
|
1921
|
+
if (!raw) return null;
|
|
1922
|
+
let u;
|
|
1923
|
+
try {
|
|
1924
|
+
u = new URL(raw);
|
|
1925
|
+
} catch {
|
|
1926
|
+
throw new Error(`AUSTERE_AXI_LINK_URL is not a URL: ${raw}`);
|
|
1927
|
+
}
|
|
1928
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") throw new Error(`AUSTERE_AXI_LINK_URL must be http or https: ${raw}`);
|
|
1929
|
+
if (u.pathname !== "/" || u.search || u.hash || u.username || u.password) throw new Error(`AUSTERE_AXI_LINK_URL must be just scheme, host and optional port, like https://austere.example.com: ${raw}`);
|
|
1930
|
+
return { base: u.origin, host: u.hostname.toLowerCase() };
|
|
1931
|
+
}
|
|
1754
1932
|
function hostOnly(hostHeader) {
|
|
1755
1933
|
const h = String(hostHeader || "").toLowerCase();
|
|
1756
1934
|
if (h.startsWith("[")) return h.slice(0, h.indexOf("]") + 1);
|
|
@@ -1775,7 +1953,7 @@ var init_net = __esm({
|
|
|
1775
1953
|
|
|
1776
1954
|
// src/cli/hub-page.js
|
|
1777
1955
|
import os2 from "node:os";
|
|
1778
|
-
import
|
|
1956
|
+
import path10 from "node:path";
|
|
1779
1957
|
function day(ms) {
|
|
1780
1958
|
const d = new Date(ms);
|
|
1781
1959
|
const today = /* @__PURE__ */ new Date();
|
|
@@ -1790,7 +1968,7 @@ function time3(ms) {
|
|
|
1790
1968
|
}
|
|
1791
1969
|
function tilde(p) {
|
|
1792
1970
|
const home = os2.homedir();
|
|
1793
|
-
return p === home || p.startsWith(home +
|
|
1971
|
+
return p === home || p.startsWith(home + path10.sep) ? "~" + p.slice(home.length) : p;
|
|
1794
1972
|
}
|
|
1795
1973
|
function counts(a) {
|
|
1796
1974
|
const waiting = a.open + a.received;
|
|
@@ -1824,13 +2002,13 @@ function hubPage(rows, { version: version2, q: q2, project, stats }) {
|
|
|
1824
2002
|
const body = [...days.entries()].map(
|
|
1825
2003
|
([d, items]) => `<h3>${escapeHtml(d)}</h3><ul>${items.map(
|
|
1826
2004
|
(a) => `<li><a href="/hub/open/${a.id}" title="${escapeHtml(a.file)}">
|
|
1827
|
-
<strong>${escapeHtml(a.title ||
|
|
1828
|
-
<span class="meta">${escapeHtml(
|
|
2005
|
+
<strong>${escapeHtml(a.title || path10.basename(a.file))}</strong>
|
|
2006
|
+
<span class="meta">${escapeHtml(path10.relative(proj, a.file) || path10.basename(a.file))} \xB7 ${time3(a.updated_at)} \xB7 ${state(a)}${counts(a) ? ` \xB7 ${counts(a)}` : ""}</span>
|
|
1829
2007
|
</a></li>`
|
|
1830
2008
|
).join("")}</ul>`
|
|
1831
2009
|
).join("");
|
|
1832
2010
|
const unanswered = list.reduce((n, a) => n + a.open + a.received, 0);
|
|
1833
|
-
return `<section><h2><a href="/?project=${encodeURIComponent(proj)}">${escapeHtml(
|
|
2011
|
+
return `<section><h2><a href="/?project=${encodeURIComponent(proj)}">${escapeHtml(path10.basename(proj) || proj)}</a> <small title="${escapeHtml(proj)}">${escapeHtml(
|
|
1834
2012
|
shortPath(proj)
|
|
1835
2013
|
)}</small>${unanswered ? ` <b class="open">${unanswered} unanswered</b>` : ""}</h2>${body}</section>`;
|
|
1836
2014
|
}).join("");
|
|
@@ -1839,7 +2017,42 @@ function hubPage(rows, { version: version2, q: q2, project, stats }) {
|
|
|
1839
2017
|
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1840
2018
|
<title>austere-axi hub</title><meta name="color-scheme" content="dark">
|
|
1841
2019
|
<style>
|
|
1842
|
-
|
|
2020
|
+
${STYLE}
|
|
2021
|
+
</style></head>
|
|
2022
|
+
<body><main>
|
|
2023
|
+
<header><h1>austere-axi hub<small>${escapeHtml(version2)}</small></h1><nav class="nav"><a href="/">Pages</a><a href="/notes">Notes</a></nav><span class="stats">${stats.artifacts} artifacts \xB7 ${stats.projects} projects${stats.unanswered ? ` \xB7 ${stats.unanswered} unanswered` : ""}</span></header>
|
|
2024
|
+
<form method="get" action="/">${project ? `<input type="hidden" name="project" value="${escapeHtml(project)}">` : ""}<input type="search" name="q" value="${escapeHtml(q2)}" placeholder="Search titles, files, projects" aria-label="Search"></form>
|
|
2025
|
+
${filter}${sections || `<p class="empty">${empty}</p>`}
|
|
2026
|
+
</main></body></html>`;
|
|
2027
|
+
}
|
|
2028
|
+
function notesPage(rows, { version: version2, q: q2, project, status, author = "", authors = [], stats }) {
|
|
2029
|
+
const items = rows.map((n) => {
|
|
2030
|
+
const waiting = n.status !== "addressed";
|
|
2031
|
+
const when = `${day(n.at)} ${time3(n.at)}`;
|
|
2032
|
+
return `<li class="n"><a href="/hub/open/${escapeHtml(n.artifact_id)}" title="${escapeHtml(n.file)}">
|
|
2033
|
+
<p class="note">${escapeHtml(n.text || "(no text)")}</p>
|
|
2034
|
+
<span class="note-t">${n.author ? `<b class="by">${escapeHtml(n.author)}</b> \xB7 ` : ""}${n.target ? `on ${escapeHtml(n.target)} \xB7 ` : ""}<strong>${escapeHtml(n.title || path10.basename(n.file))}</strong> \xB7 ${escapeHtml(path10.basename(n.project) || n.project)} \xB7 ${escapeHtml(when)} \xB7 <span class="st ${waiting ? "waiting" : "done"}">${STATUS_LABEL[n.status] || escapeHtml(n.status)}</span></span>
|
|
2035
|
+
</a></li>`;
|
|
2036
|
+
}).join("");
|
|
2037
|
+
const opt = (v, label) => `<option value="${v}"${status === v ? " selected" : ""}>${label}</option>`;
|
|
2038
|
+
const people = authors.length ? `<select name="author" aria-label="Author"><option value="">Everyone</option><option value="owner"${author === "owner" ? " selected" : ""}>Owner</option>${authors.map((a) => `<option value="${escapeHtml(a)}"${author === a ? " selected" : ""}>${escapeHtml(a)}</option>`).join("")}</select>` : "";
|
|
2039
|
+
const filter = project ? `<p class="filter">Project ${escapeHtml(tilde(project))} \xB7 <a href="/notes">all projects</a></p>` : "";
|
|
2040
|
+
const empty = q2 || project || author || status !== "all" ? "No notes match." : "No notes yet.";
|
|
2041
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2042
|
+
<title>Notes \xB7 austere-axi hub</title><meta name="color-scheme" content="dark">
|
|
2043
|
+
<style>
|
|
2044
|
+
${STYLE}</style></head>
|
|
2045
|
+
<body><main>
|
|
2046
|
+
<header><h1>austere-axi hub<small>${escapeHtml(version2)}</small></h1><nav class="nav"><a href="/">Pages</a><a href="/notes">Notes</a></nav><span class="stats">${rows.length} notes${stats.unanswered ? ` \xB7 ${stats.unanswered} unanswered` : ""}</span></header>
|
|
2047
|
+
<form class="f${authors.length ? " p" : ""}" method="get" action="/notes">${project ? `<input type="hidden" name="project" value="${escapeHtml(project)}">` : ""}<input type="search" name="q" value="${escapeHtml(q2)}" placeholder="Search notes, targets, page titles" aria-label="Search notes"><select name="status" aria-label="Status">${opt("all", "All")}${opt("unanswered", "Unanswered")}${opt("answered", "Answered")}</select>${people}<button type="submit">Filter</button></form>
|
|
2048
|
+
${filter}${items ? `<ul>${items}</ul>` : `<p class="empty">${empty}</p>`}
|
|
2049
|
+
</main></body></html>`;
|
|
2050
|
+
}
|
|
2051
|
+
var STYLE, STATUS_LABEL;
|
|
2052
|
+
var init_hub_page = __esm({
|
|
2053
|
+
"src/cli/hub-page.js"() {
|
|
2054
|
+
init_artifact();
|
|
2055
|
+
STYLE = `body{margin:0;background:#0b0b0d;color:#e8e8ec;font:14px/1.5 ui-sans-serif,system-ui,-apple-system,sans-serif}
|
|
1843
2056
|
main{max-width:860px;margin:0 auto;padding:32px 16px 64px}
|
|
1844
2057
|
header{display:flex;flex-wrap:wrap;gap:12px;align-items:baseline;justify-content:space-between}
|
|
1845
2058
|
h1{font-size:18px;font-weight:600;margin:0}h1 small{color:#77777f;font-weight:400;font-size:13px;margin-left:6px}
|
|
@@ -1855,16 +2068,14 @@ li strong{color:#a9c1ff;font-weight:600;overflow-wrap:anywhere}li>a:hover strong
|
|
|
1855
2068
|
.meta{color:#8a8a94;font-size:12px;overflow-wrap:anywhere}em{font-style:normal}em.live{color:#8fd19e}
|
|
1856
2069
|
b{font-weight:600}b.open{color:#f2c46d;font-size:12px}
|
|
1857
2070
|
.filter{color:#8a8a94;font-size:12.5px}.filter a{color:#a9c1ff}.empty{color:#8a8a94;margin-top:28px}
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
"src/cli/hub-page.js"() {
|
|
1867
|
-
init_artifact();
|
|
2071
|
+
.nav{display:flex;gap:14px;font-size:13px}.nav a{color:#a9c1ff;text-decoration:none}
|
|
2072
|
+
form.f{display:grid;grid-template-columns:minmax(0,1fr);gap:8px}button{background:#1c1c20;border:1px solid #2a2a30;border-radius:8px;color:#e8e8ec;padding:9px 12px;font:inherit}select{background:#141417;border:1px solid #2a2a30;border-radius:8px;color:#e8e8ec;padding:9px 12px;font:inherit;width:100%;box-sizing:border-box}
|
|
2073
|
+
.note{margin:0;overflow-wrap:anywhere;white-space:pre-wrap}.note-t{color:#8a8a94;font-size:12px}.st{font-size:12px}.st.waiting{color:#f2c46d}.st.done{color:#8fd19e}
|
|
2074
|
+
li.n{padding:12px 0}li.n>a{padding:0}
|
|
2075
|
+
b.by{color:#e8e8ec}
|
|
2076
|
+
@media(min-width:600px){form.f{grid-template-columns:minmax(0,1fr) 150px auto}form.f.p{grid-template-columns:minmax(0,1fr) 150px 150px auto}}
|
|
2077
|
+
`;
|
|
2078
|
+
STATUS_LABEL = { open: "unanswered", received: "unanswered", addressed: "answered", held: "held for the owner" };
|
|
1868
2079
|
}
|
|
1869
2080
|
});
|
|
1870
2081
|
|
|
@@ -1873,30 +2084,26 @@ var server_exports = {};
|
|
|
1873
2084
|
__export(server_exports, {
|
|
1874
2085
|
runServer: () => runServer
|
|
1875
2086
|
});
|
|
1876
|
-
import
|
|
2087
|
+
import fs12 from "node:fs";
|
|
1877
2088
|
import http2 from "node:http";
|
|
1878
|
-
import
|
|
1879
|
-
import
|
|
2089
|
+
import path11 from "node:path";
|
|
2090
|
+
import crypto4 from "node:crypto";
|
|
1880
2091
|
async function runServer({ port: port2, verbose = false, alsoListen = [] }) {
|
|
1881
2092
|
const log = (...a) => {
|
|
1882
2093
|
const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${a.join(" ")}`;
|
|
1883
2094
|
if (verbose) process.stderr.write(line + "\n");
|
|
1884
2095
|
try {
|
|
1885
|
-
|
|
2096
|
+
fs12.appendFileSync(path11.join(stateDir(), "server.log"), line + "\n");
|
|
1886
2097
|
} catch {
|
|
1887
2098
|
}
|
|
1888
2099
|
};
|
|
1889
2100
|
let hub = null;
|
|
1890
2101
|
const hubWanted = hubEnabled();
|
|
1891
2102
|
if (hubWanted) {
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
hub = await Hub.open();
|
|
1897
|
-
} catch (err) {
|
|
1898
|
-
log("hub-unavailable", String(err?.message || err));
|
|
1899
|
-
}
|
|
2103
|
+
try {
|
|
2104
|
+
hub = await Hub.open();
|
|
2105
|
+
} catch (err) {
|
|
2106
|
+
log("hub-unavailable", String(err?.message || err));
|
|
1900
2107
|
}
|
|
1901
2108
|
}
|
|
1902
2109
|
const store = new SessionStore(stateDir(), { onFlush: hub ? (s) => hub.syncSession(s) : void 0 });
|
|
@@ -1909,6 +2116,8 @@ async function runServer({ port: port2, verbose = false, alsoListen = [] }) {
|
|
|
1909
2116
|
}
|
|
1910
2117
|
}
|
|
1911
2118
|
}
|
|
2119
|
+
const invites = new InviteStore(stateDir());
|
|
2120
|
+
const ownerToken = ownerKey(stateDir());
|
|
1912
2121
|
const clients = /* @__PURE__ */ new Map();
|
|
1913
2122
|
const polls = /* @__PURE__ */ new Map();
|
|
1914
2123
|
const watchers = /* @__PURE__ */ new Map();
|
|
@@ -1917,6 +2126,7 @@ async function runServer({ port: port2, verbose = false, alsoListen = [] }) {
|
|
|
1917
2126
|
const byeAt = /* @__PURE__ */ new Map();
|
|
1918
2127
|
const browserSeen = /* @__PURE__ */ new Set();
|
|
1919
2128
|
let tailnet = null;
|
|
2129
|
+
const linkBase = linkUrl();
|
|
1920
2130
|
let lastActivity = Date.now();
|
|
1921
2131
|
const longGraceMs = msEnv("AUSTERE_AXI_DISCONNECT_GRACE_MS", 5 * 6e4);
|
|
1922
2132
|
const idleMs = hub ? 0 : msEnv("AUSTERE_AXI_IDLE_TIMEOUT_MS", 30 * 6e4);
|
|
@@ -1926,17 +2136,53 @@ async function runServer({ port: port2, verbose = false, alsoListen = [] }) {
|
|
|
1926
2136
|
function hostsAllowed() {
|
|
1927
2137
|
const extra = [];
|
|
1928
2138
|
if (tailnet) extra.push(tailnet.ip, tailnet.dnsName);
|
|
2139
|
+
if (linkBase) extra.push(linkBase.host);
|
|
1929
2140
|
for (const h of listeners.keys()) extra.push(h);
|
|
1930
2141
|
return allowedHosts(extra);
|
|
1931
2142
|
}
|
|
1932
|
-
function
|
|
1933
|
-
const local = `http://127.0.0.1:${port2}/s/${id}`;
|
|
1934
|
-
const out = { local };
|
|
1935
|
-
if (tailnet && listeners.has(tailnet.ip)) out.remote = `http://${tailnet.dnsName || tailnet.ip}:${port2}/s/${id}`;
|
|
2143
|
+
function remoteHost() {
|
|
1936
2144
|
const linkHost = process.env.AUSTERE_AXI_LINK_HOST;
|
|
1937
|
-
if (linkHost)
|
|
2145
|
+
if (linkHost) return linkHost;
|
|
2146
|
+
if (tailnet && listeners.has(tailnet.ip)) return tailnet.dnsName || tailnet.ip;
|
|
2147
|
+
const explicit = process.env.AUSTERE_AXI_HOST;
|
|
2148
|
+
if (explicit && !/^(0\.0\.0\.0|::|\*)$/.test(explicit) && listeners.has(explicit)) return explicit;
|
|
2149
|
+
return null;
|
|
2150
|
+
}
|
|
2151
|
+
function remoteBase() {
|
|
2152
|
+
if (linkBase) return linkBase.base;
|
|
2153
|
+
const host = remoteHost();
|
|
2154
|
+
return host ? `http://${host}:${port2}` : null;
|
|
2155
|
+
}
|
|
2156
|
+
function remoteUrl(id) {
|
|
2157
|
+
const base = remoteBase();
|
|
2158
|
+
return base ? `${base}/s/${id}` : null;
|
|
2159
|
+
}
|
|
2160
|
+
function links(id) {
|
|
2161
|
+
const out = { local: `http://127.0.0.1:${port2}/s/${id}` };
|
|
2162
|
+
const remote = remoteUrl(id);
|
|
2163
|
+
if (remote) out.remote = `${remote}?key=${ownerToken}`;
|
|
1938
2164
|
return out;
|
|
1939
2165
|
}
|
|
2166
|
+
function inviteLink(inv) {
|
|
2167
|
+
return `${remoteUrl(inv.sessionId)}?invite=${inv.token}`;
|
|
2168
|
+
}
|
|
2169
|
+
function isOwner(req) {
|
|
2170
|
+
if (isLoopback(req.socket.remoteAddress)) return true;
|
|
2171
|
+
const key = tokenFromCookie(req.headers.cookie, OWNER_COOKIE);
|
|
2172
|
+
return key != null && safeEqual(key, ownerToken);
|
|
2173
|
+
}
|
|
2174
|
+
function identify(req, s) {
|
|
2175
|
+
const token = tokenFromCookie(req.headers.cookie);
|
|
2176
|
+
if (token == null) return isOwner(req) ? OWNER : null;
|
|
2177
|
+
const inv = invites.find(token);
|
|
2178
|
+
if (!inv || inv.sessionId !== s.id) return null;
|
|
2179
|
+
return { role: "reviewer", name: inv.name, auto: inv.auto, token: inv.token };
|
|
2180
|
+
}
|
|
2181
|
+
function canSee(e, viewer) {
|
|
2182
|
+
if (viewer.role === "owner") return true;
|
|
2183
|
+
if (e.role === "user" && (e.held || e.discarded)) return e.author === viewer.name;
|
|
2184
|
+
return true;
|
|
2185
|
+
}
|
|
1940
2186
|
function send(res, status, body, headers = {}) {
|
|
1941
2187
|
const isText = typeof body === "string" || Buffer.isBuffer(body);
|
|
1942
2188
|
const payload = isText ? body : JSON.stringify(body);
|
|
@@ -1985,11 +2231,32 @@ data: ${JSON.stringify(data)}
|
|
|
1985
2231
|
broadcast(s.id, "presence", { presence: presence(s), status: s.status, endedBy: s.endedBy });
|
|
1986
2232
|
}
|
|
1987
2233
|
function broadcastTranscript(s, entries) {
|
|
1988
|
-
|
|
2234
|
+
const set2 = clients.get(s.id);
|
|
2235
|
+
if (!set2) return;
|
|
2236
|
+
for (const c of set2) {
|
|
2237
|
+
const visible = entries.filter((e) => canSee(e, c.viewer));
|
|
2238
|
+
if (visible.length) c.res.write(`event: transcript
|
|
2239
|
+
data: ${JSON.stringify({ entries: visible })}
|
|
2240
|
+
|
|
2241
|
+
`);
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
function dropReviewers(tokens) {
|
|
2245
|
+
for (const set2 of clients.values()) {
|
|
2246
|
+
for (const c of Array.from(set2)) {
|
|
2247
|
+
if (c.viewer.role !== "reviewer" || !tokens.has(c.viewer.token)) continue;
|
|
2248
|
+
c.res.write(`event: server
|
|
2249
|
+
data: ${JSON.stringify({ state: "revoked" })}
|
|
2250
|
+
|
|
2251
|
+
`);
|
|
2252
|
+
c.res.end();
|
|
2253
|
+
set2.delete(c);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
1989
2256
|
}
|
|
1990
2257
|
function readFileSafe(file2) {
|
|
1991
2258
|
try {
|
|
1992
|
-
return
|
|
2259
|
+
return fs12.readFileSync(file2, "utf8");
|
|
1993
2260
|
} catch {
|
|
1994
2261
|
return null;
|
|
1995
2262
|
}
|
|
@@ -2001,14 +2268,14 @@ data: ${JSON.stringify(data)}
|
|
|
2001
2268
|
const w = { html, program: art?.program ?? null, shell: art?.shell ?? null, timer: null };
|
|
2002
2269
|
watchers.set(s.id, w);
|
|
2003
2270
|
if (art?.title) s.title = art.title;
|
|
2004
|
-
|
|
2271
|
+
fs12.watchFile(s.file, { interval: 300 }, () => {
|
|
2005
2272
|
clearTimeout(w.timer);
|
|
2006
2273
|
w.timer = setTimeout(() => onFileChange(s, w), 120);
|
|
2007
2274
|
});
|
|
2008
2275
|
}
|
|
2009
2276
|
function unwatch(id) {
|
|
2010
2277
|
const s = store.get(id);
|
|
2011
|
-
if (s)
|
|
2278
|
+
if (s) fs12.unwatchFile(s.file);
|
|
2012
2279
|
watchers.delete(id);
|
|
2013
2280
|
}
|
|
2014
2281
|
function onFileChange(s, w) {
|
|
@@ -2050,7 +2317,7 @@ data: ${JSON.stringify(data)}
|
|
|
2050
2317
|
const items = [];
|
|
2051
2318
|
let end = false;
|
|
2052
2319
|
for (const e of entries) {
|
|
2053
|
-
for (const it2 of e.items || []) items.push(it2);
|
|
2320
|
+
for (const it2 of e.items || []) items.push(e.author ? { ...it2, author: e.author } : it2);
|
|
2054
2321
|
if (e.end) end = true;
|
|
2055
2322
|
}
|
|
2056
2323
|
const body = {
|
|
@@ -2111,6 +2378,7 @@ data: ${JSON.stringify(data)}
|
|
|
2111
2378
|
stateDir: stateDir(),
|
|
2112
2379
|
listen: Array.from(listeners.keys()),
|
|
2113
2380
|
tailnet: tailnet || void 0,
|
|
2381
|
+
remote: remoteBase() || void 0,
|
|
2114
2382
|
hub: Boolean(hub),
|
|
2115
2383
|
hubWanted
|
|
2116
2384
|
});
|
|
@@ -2122,6 +2390,7 @@ data: ${JSON.stringify(data)}
|
|
|
2122
2390
|
presence: presence(s2),
|
|
2123
2391
|
browsers: clientCount(s2.id),
|
|
2124
2392
|
pending: s2.pending.length,
|
|
2393
|
+
held: store.heldEntries(s2).length,
|
|
2125
2394
|
links: links(s2.id)
|
|
2126
2395
|
}))
|
|
2127
2396
|
});
|
|
@@ -2131,12 +2400,52 @@ data: ${JSON.stringify(data)}
|
|
|
2131
2400
|
setTimeout(() => shutdown("requested"), 50);
|
|
2132
2401
|
return;
|
|
2133
2402
|
}
|
|
2403
|
+
if (route === "share/list") {
|
|
2404
|
+
const withLinks = url2.searchParams.get("links") === "1";
|
|
2405
|
+
return send(res, 200, {
|
|
2406
|
+
invites: invites.list().map((i) => ({
|
|
2407
|
+
id: inviteId(i),
|
|
2408
|
+
name: i.name,
|
|
2409
|
+
file: i.file,
|
|
2410
|
+
auto: i.auto,
|
|
2411
|
+
createdAt: i.createdAt,
|
|
2412
|
+
link: withLinks ? inviteLink(i) : void 0
|
|
2413
|
+
}))
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2134
2416
|
if (req.method !== "POST") return send(res, 405, { error: "POST expected" });
|
|
2135
2417
|
const body = await readBody(req);
|
|
2418
|
+
if (route === "share/revoke") {
|
|
2419
|
+
const gone = invites.revoke(String(body.key || ""));
|
|
2420
|
+
if (!gone.length) return send(res, 404, { error: "no invite matches that name or id" });
|
|
2421
|
+
dropReviewers(new Set(gone.map((i) => i.token)));
|
|
2422
|
+
log("share-revoke", gone.map((i) => `${i.name}(${inviteId(i)})`).join(","));
|
|
2423
|
+
return send(res, 200, { revoked: gone.map((i) => ({ id: inviteId(i), name: i.name, file: i.file })) });
|
|
2424
|
+
}
|
|
2136
2425
|
const file2 = typeof body.file === "string" ? body.file : "";
|
|
2137
2426
|
if (!file2) return send(res, 400, { error: "file is required" });
|
|
2427
|
+
if (route === "share") {
|
|
2428
|
+
if (!fs12.existsSync(file2)) return send(res, 404, { error: `no such file: ${file2}` });
|
|
2429
|
+
await ensureTailnetListener();
|
|
2430
|
+
if (!remoteBase()) {
|
|
2431
|
+
return send(res, 409, {
|
|
2432
|
+
error: "no tailnet address: Tailscale is not running here and neither AUSTERE_AXI_HOST nor AUSTERE_AXI_LINK_URL is set, so a teammate could not open the link"
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
let inv;
|
|
2436
|
+
try {
|
|
2437
|
+
const s2 = store.ensure(file2);
|
|
2438
|
+
inv = invites.add({ name: body.name, sessionId: s2.id, file: s2.file, auto: !!body.auto });
|
|
2439
|
+
watch(s2);
|
|
2440
|
+
store.touch(s2);
|
|
2441
|
+
} catch (err) {
|
|
2442
|
+
return send(res, 400, { error: String(err.message || err) });
|
|
2443
|
+
}
|
|
2444
|
+
log("share", inv.name, inviteId(inv), inv.auto ? "auto" : "hold");
|
|
2445
|
+
return send(res, 200, { id: inviteId(inv), name: inv.name, auto: inv.auto, link: inviteLink(inv), file: inv.file });
|
|
2446
|
+
}
|
|
2138
2447
|
if (route === "open") {
|
|
2139
|
-
if (!
|
|
2448
|
+
if (!fs12.existsSync(file2)) return send(res, 404, { error: `no such file: ${file2}` });
|
|
2140
2449
|
await ensureTailnetListener();
|
|
2141
2450
|
const s2 = store.ensure(file2);
|
|
2142
2451
|
if (s2.status === "ended") {
|
|
@@ -2153,7 +2462,8 @@ data: ${JSON.stringify(data)}
|
|
|
2153
2462
|
session: publicSession(s2),
|
|
2154
2463
|
links: links(s2.id),
|
|
2155
2464
|
browsers: clientCount(s2.id),
|
|
2156
|
-
pending: s2.pending.length
|
|
2465
|
+
pending: s2.pending.length,
|
|
2466
|
+
held: store.heldEntries(s2).length
|
|
2157
2467
|
});
|
|
2158
2468
|
}
|
|
2159
2469
|
const s = store.byFile(file2);
|
|
@@ -2246,9 +2556,9 @@ data: ${JSON.stringify(data)}
|
|
|
2246
2556
|
}
|
|
2247
2557
|
const file2 = installedPackFile(spec.name, spec.version);
|
|
2248
2558
|
if (!file2) return send(res, 404, `pack ${spec.name}@${spec.version} is not installed (austere-axi components add ${spec.name}@${spec.version})`);
|
|
2249
|
-
|
|
2559
|
+
fs12.readFile(file2, (err, buf) => {
|
|
2250
2560
|
if (err) return send(res, 404, "not found");
|
|
2251
|
-
const actual = `sha384-${
|
|
2561
|
+
const actual = `sha384-${crypto4.createHash("sha384").update(buf).digest("base64")}`;
|
|
2252
2562
|
const locked = readLock().packs[spec.name]?.integrity;
|
|
2253
2563
|
const wanted = url2.searchParams.get("integrity");
|
|
2254
2564
|
if (locked && locked !== actual) {
|
|
@@ -2270,23 +2580,23 @@ data: ${JSON.stringify(data)}
|
|
|
2270
2580
|
}
|
|
2271
2581
|
function serveAsset(res, rel, url2) {
|
|
2272
2582
|
if (rel.startsWith("installed-packs/")) return serveInstalledPack(res, rel, url2);
|
|
2273
|
-
const clean =
|
|
2274
|
-
const file2 =
|
|
2275
|
-
if (!file2.startsWith(ASSET_DIR +
|
|
2276
|
-
|
|
2583
|
+
const clean = path11.normalize(rel).replace(/^(\.\.[/\\])+/, "");
|
|
2584
|
+
const file2 = path11.join(ASSET_DIR, clean);
|
|
2585
|
+
if (!file2.startsWith(ASSET_DIR + path11.sep)) return send(res, 404, "not found");
|
|
2586
|
+
fs12.stat(file2, (err, st2) => {
|
|
2277
2587
|
if (err || !st2.isFile()) return send(res, 404, "not found");
|
|
2278
2588
|
res.writeHead(200, {
|
|
2279
|
-
"content-type": MIME[
|
|
2589
|
+
"content-type": MIME[path11.extname(file2)] || "application/octet-stream",
|
|
2280
2590
|
"content-length": st2.size,
|
|
2281
2591
|
"cache-control": "no-cache",
|
|
2282
2592
|
"x-content-type-options": "nosniff"
|
|
2283
2593
|
});
|
|
2284
|
-
|
|
2594
|
+
fs12.createReadStream(file2).pipe(res);
|
|
2285
2595
|
});
|
|
2286
2596
|
}
|
|
2287
2597
|
function serveArtifact(req, res, s, rest) {
|
|
2288
|
-
const dir =
|
|
2289
|
-
const base =
|
|
2598
|
+
const dir = path11.dirname(s.file);
|
|
2599
|
+
const base = path11.basename(s.file);
|
|
2290
2600
|
if (rest === base || rest === "") {
|
|
2291
2601
|
const html = readFileSafe(s.file);
|
|
2292
2602
|
if (html == null) return send(res, 404, "artifact file is missing");
|
|
@@ -2299,28 +2609,29 @@ data: ${JSON.stringify(data)}
|
|
|
2299
2609
|
} catch {
|
|
2300
2610
|
return send(res, 400, "bad path");
|
|
2301
2611
|
}
|
|
2302
|
-
const file2 =
|
|
2303
|
-
if (!file2.startsWith(dir +
|
|
2612
|
+
const file2 = path11.resolve(dir, decoded);
|
|
2613
|
+
if (!file2.startsWith(dir + path11.sep) || decoded.split(/[/\\]/).some((p) => p.startsWith("."))) {
|
|
2304
2614
|
return send(res, 404, "not found");
|
|
2305
2615
|
}
|
|
2306
|
-
|
|
2616
|
+
fs12.stat(file2, (err, st2) => {
|
|
2307
2617
|
if (err || !st2.isFile() || st2.size > 64 * 1024 * 1024) return send(res, 404, "not found");
|
|
2308
2618
|
res.writeHead(200, {
|
|
2309
|
-
"content-type": MIME[
|
|
2619
|
+
"content-type": MIME[path11.extname(file2).toLowerCase()] || "application/octet-stream",
|
|
2310
2620
|
"content-length": st2.size,
|
|
2311
2621
|
"cache-control": "no-cache",
|
|
2312
2622
|
"x-content-type-options": "nosniff"
|
|
2313
2623
|
});
|
|
2314
|
-
|
|
2624
|
+
fs12.createReadStream(file2).pipe(res);
|
|
2315
2625
|
});
|
|
2316
2626
|
}
|
|
2317
|
-
function chromePage(s) {
|
|
2627
|
+
function chromePage(s, viewer) {
|
|
2318
2628
|
const boot = {
|
|
2319
2629
|
id: s.id,
|
|
2320
2630
|
title: s.title,
|
|
2321
|
-
file: s.file,
|
|
2322
|
-
name:
|
|
2323
|
-
|
|
2631
|
+
file: viewer.role === "owner" ? s.file : "",
|
|
2632
|
+
viewer: { role: viewer.role, name: viewer.name, auto: viewer.role === "reviewer" ? viewer.auto : void 0 },
|
|
2633
|
+
name: path11.basename(s.file),
|
|
2634
|
+
artifactUrl: `/s/${s.id}/a/${encodeURIComponent(path11.basename(s.file))}`,
|
|
2324
2635
|
version: VERSION
|
|
2325
2636
|
};
|
|
2326
2637
|
return `<!doctype html>
|
|
@@ -2343,10 +2654,20 @@ data: ${JSON.stringify(data)}
|
|
|
2343
2654
|
const project = (url2.searchParams.get("project") || "").slice(0, 1e3);
|
|
2344
2655
|
return hubPage(hub.list({ q: q2, project }), { version: VERSION, q: q2, project, stats: hub.stats() });
|
|
2345
2656
|
}
|
|
2657
|
+
function hubNotes(url2) {
|
|
2658
|
+
store.flushAll();
|
|
2659
|
+
const q2 = (url2.searchParams.get("q") || "").slice(0, 200);
|
|
2660
|
+
const project = (url2.searchParams.get("project") || "").slice(0, 1e3);
|
|
2661
|
+
const st2 = url2.searchParams.get("status");
|
|
2662
|
+
const status = st2 === "unanswered" || st2 === "answered" ? st2 : "all";
|
|
2663
|
+
const author = (url2.searchParams.get("author") || "").slice(0, 80);
|
|
2664
|
+
const rows = hub.notes({ q: q2, project, author, status: status === "all" ? "" : status });
|
|
2665
|
+
return notesPage(rows, { version: VERSION, q: q2, project, status, author, authors: hub.authors(), stats: hub.stats() });
|
|
2666
|
+
}
|
|
2346
2667
|
function hubOpen(res, id) {
|
|
2347
2668
|
const a = /^[a-f0-9]{12}$/.test(id) ? hub.artifact(id) : null;
|
|
2348
2669
|
if (!a) return send(res, 404, "not in the hub");
|
|
2349
|
-
if (!
|
|
2670
|
+
if (!fs12.existsSync(a.file)) return send(res, 410, `${a.file} no longer exists`);
|
|
2350
2671
|
const existed = store.byFile(a.file);
|
|
2351
2672
|
const s = store.ensure(a.file);
|
|
2352
2673
|
if (!existed) store.touch(s);
|
|
@@ -2369,9 +2690,23 @@ data: ${JSON.stringify(data)}
|
|
|
2369
2690
|
}
|
|
2370
2691
|
if (url2.pathname.startsWith("/api/")) return handleApi(req, res, url2);
|
|
2371
2692
|
if (url2.pathname.startsWith("/__austere/")) return serveAsset(res, url2.pathname.slice("/__austere/".length), url2);
|
|
2693
|
+
const key = url2.searchParams.get("key");
|
|
2694
|
+
if (key != null) {
|
|
2695
|
+
if (!safeEqual(key, ownerToken)) return send(res, 403, "This link is not valid.");
|
|
2696
|
+
url2.searchParams.delete("key");
|
|
2697
|
+
res.writeHead(302, {
|
|
2698
|
+
location: url2.pathname + url2.search,
|
|
2699
|
+
"set-cookie": `${OWNER_COOKIE}=${ownerToken}; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000`,
|
|
2700
|
+
"cache-control": "no-store"
|
|
2701
|
+
});
|
|
2702
|
+
return res.end();
|
|
2703
|
+
}
|
|
2704
|
+
const ownerOnly = url2.pathname === "/" || url2.pathname === "/notes" || url2.pathname.startsWith("/hub/");
|
|
2705
|
+
if (ownerOnly && !isOwner(req)) return send(res, 403, "Only the owner can open this. Ask the owner for a link to the page.");
|
|
2372
2706
|
if (url2.pathname === "/") {
|
|
2373
2707
|
return send(res, 200, hub ? hubHome(url2) : homePage(), { "content-type": "text/html; charset=utf-8" });
|
|
2374
2708
|
}
|
|
2709
|
+
if (hub && url2.pathname === "/notes") return send(res, 200, hubNotes(url2), { "content-type": "text/html; charset=utf-8" });
|
|
2375
2710
|
if (hub && url2.pathname.startsWith("/hub/open/")) return hubOpen(res, url2.pathname.slice("/hub/open/".length));
|
|
2376
2711
|
if (url2.pathname === "/favicon.ico") return send(res, 204, "");
|
|
2377
2712
|
const m = /^\/s\/([a-f0-9]{12})(\/.*)?$/.exec(url2.pathname);
|
|
@@ -2380,9 +2715,31 @@ data: ${JSON.stringify(data)}
|
|
|
2380
2715
|
if (!s) return send(res, 404, "no such session");
|
|
2381
2716
|
const sub = m[2] || "";
|
|
2382
2717
|
activity();
|
|
2718
|
+
const invite = url2.searchParams.get("invite");
|
|
2719
|
+
if (invite != null && (sub === "" || sub === "/")) {
|
|
2720
|
+
const inv = invites.find(invite);
|
|
2721
|
+
if (!inv || inv.sessionId !== s.id) return send(res, 403, "This invite link is not valid. It may have been revoked; ask for a new one.");
|
|
2722
|
+
res.writeHead(302, {
|
|
2723
|
+
location: `/s/${s.id}`,
|
|
2724
|
+
"set-cookie": `${COOKIE}=${inv.token}; Path=/s/${s.id}; HttpOnly; SameSite=Lax; Max-Age=2592000`,
|
|
2725
|
+
"cache-control": "no-store"
|
|
2726
|
+
});
|
|
2727
|
+
return res.end();
|
|
2728
|
+
}
|
|
2729
|
+
const who = identify(req, s);
|
|
2730
|
+
if (!who) {
|
|
2731
|
+
if (tokenFromCookie(req.headers.cookie) == null) return send(res, 403, "Ask the owner for a link to this page.");
|
|
2732
|
+
return send(res, 403, "This invite is no longer valid. Ask for a new link.", {
|
|
2733
|
+
"set-cookie": `${COOKIE}=; Path=/s/${s.id}; HttpOnly; SameSite=Lax; Max-Age=0`
|
|
2734
|
+
});
|
|
2735
|
+
}
|
|
2736
|
+
const reviewer = who.role === "reviewer";
|
|
2737
|
+
if (reviewer && !(sub === "" || sub === "/" || sub.startsWith("/a/") || sub === "/events" || sub === "/send" || sub === "/bye")) {
|
|
2738
|
+
return send(res, 403, { error: "reviewers can only read the page and add notes" });
|
|
2739
|
+
}
|
|
2383
2740
|
if (sub === "" || sub === "/") {
|
|
2384
2741
|
watch(s);
|
|
2385
|
-
return send(res, 200, chromePage(s), { "content-type": "text/html; charset=utf-8" });
|
|
2742
|
+
return send(res, 200, chromePage(s, who), { "content-type": "text/html; charset=utf-8" });
|
|
2386
2743
|
}
|
|
2387
2744
|
if (sub.startsWith("/a/")) return serveArtifact(req, res, s, sub.slice(3));
|
|
2388
2745
|
if (sub === "/events") {
|
|
@@ -2396,20 +2753,20 @@ data: ${JSON.stringify(data)}
|
|
|
2396
2753
|
res.write("retry: 2000\n\n");
|
|
2397
2754
|
const w = watchers.get(s.id);
|
|
2398
2755
|
const snapshot = {
|
|
2399
|
-
session: publicSession(s),
|
|
2756
|
+
session: reviewer ? { ...publicSession(s), file: "" } : publicSession(s),
|
|
2400
2757
|
presence: presence(s),
|
|
2401
|
-
transcript: s.transcript.slice(-200),
|
|
2758
|
+
transcript: s.transcript.filter((e) => !e.discarded && canSee(e, who)).slice(-200),
|
|
2402
2759
|
revision: s.revision,
|
|
2403
2760
|
program: w?.program ?? null,
|
|
2404
2761
|
thirdPartyPacks: thirdPartyPacks(w?.html),
|
|
2405
2762
|
version: VERSION,
|
|
2406
|
-
links: links(s.id)
|
|
2763
|
+
links: reviewer ? null : links(s.id)
|
|
2407
2764
|
};
|
|
2408
2765
|
res.write(`event: snapshot
|
|
2409
2766
|
data: ${JSON.stringify(snapshot)}
|
|
2410
2767
|
|
|
2411
2768
|
`);
|
|
2412
|
-
const client = { res };
|
|
2769
|
+
const client = { res, viewer: who };
|
|
2413
2770
|
if (!clients.has(s.id)) clients.set(s.id, /* @__PURE__ */ new Set());
|
|
2414
2771
|
clients.get(s.id).add(client);
|
|
2415
2772
|
browserSeen.add(s.id);
|
|
@@ -2426,13 +2783,19 @@ data: ${JSON.stringify(snapshot)}
|
|
|
2426
2783
|
if (req.method === "POST" && !checkOrigin(req)) return send(res, 403, { error: "cross-origin request refused" });
|
|
2427
2784
|
if (sub === "/send" && req.method === "POST") {
|
|
2428
2785
|
const body = await readBody(req, 4 * 1024 * 1024);
|
|
2429
|
-
const
|
|
2786
|
+
const end = !reviewer && !!body.end;
|
|
2787
|
+
let items = Array.isArray(body.items) ? body.items.filter((x) => x && typeof x === "object").slice(0, reviewer ? 50 : 200) : [];
|
|
2788
|
+
if (reviewer) items = items.filter((x) => REVIEWER_KINDS.has(x.kind));
|
|
2430
2789
|
if (s.status === "ended") return send(res, 409, { error: "session has ended" });
|
|
2431
|
-
if (!items.length && !
|
|
2432
|
-
for (const it2 of items)
|
|
2790
|
+
if (!items.length && !end) return send(res, 400, { error: "nothing to send" });
|
|
2791
|
+
for (const it2 of items) {
|
|
2792
|
+
delete it2.author;
|
|
2793
|
+
it2.id = reviewer || !it2.id ? newId("i") : it2.id;
|
|
2794
|
+
}
|
|
2795
|
+
const hold = reviewer && !who.auto;
|
|
2433
2796
|
let entry = null;
|
|
2434
|
-
if (items.length) entry = store.addUserBatch(s, items, { end:
|
|
2435
|
-
if (
|
|
2797
|
+
if (items.length) entry = store.addUserBatch(s, items, { end, author: who.name, hold });
|
|
2798
|
+
if (end) {
|
|
2436
2799
|
store.end(s, "user");
|
|
2437
2800
|
broadcast(s.id, "ended", { by: "user" });
|
|
2438
2801
|
}
|
|
@@ -2440,11 +2803,21 @@ data: ${JSON.stringify(snapshot)}
|
|
|
2440
2803
|
const poll = polls.get(s.id);
|
|
2441
2804
|
if (poll && !poll.done) {
|
|
2442
2805
|
if (s.pending.length) deliver(s, poll);
|
|
2443
|
-
else if (
|
|
2806
|
+
else if (end) finishPoll(s, poll, { status: "ended", endedBy: "user" });
|
|
2444
2807
|
}
|
|
2445
2808
|
broadcastPresence(s);
|
|
2446
2809
|
return send(res, 200, { ok: true, entry });
|
|
2447
2810
|
}
|
|
2811
|
+
if ((sub === "/release" || sub === "/discard") && req.method === "POST") {
|
|
2812
|
+
const body = await readBody(req);
|
|
2813
|
+
const ids = Array.isArray(body.ids) ? body.ids.map(String) : body.all ? null : [];
|
|
2814
|
+
const done = sub === "/release" ? store.release(s, ids) : store.discard(s, ids);
|
|
2815
|
+
if (done.length) broadcastTranscript(s, done);
|
|
2816
|
+
const poll = polls.get(s.id);
|
|
2817
|
+
if (sub === "/release" && poll && !poll.done && s.pending.length) deliver(s, poll);
|
|
2818
|
+
broadcastPresence(s);
|
|
2819
|
+
return send(res, 200, { ok: true, count: done.length });
|
|
2820
|
+
}
|
|
2448
2821
|
if (sub === "/end" && req.method === "POST") {
|
|
2449
2822
|
if (s.status !== "ended") {
|
|
2450
2823
|
store.end(s, "user");
|
|
@@ -2467,7 +2840,7 @@ data: ${JSON.stringify(snapshot)}
|
|
|
2467
2840
|
if (html == null) return send(res, 404, "artifact file is missing");
|
|
2468
2841
|
try {
|
|
2469
2842
|
const out = exportHtml(html);
|
|
2470
|
-
const name =
|
|
2843
|
+
const name = path11.basename(s.file).replace(/\.html?$/i, "") + ".export.html";
|
|
2471
2844
|
return send(res, 200, out.html, {
|
|
2472
2845
|
"content-type": "text/html; charset=utf-8",
|
|
2473
2846
|
"content-disposition": `attachment; filename="${name.replace(/"/g, "")}"`
|
|
@@ -2483,7 +2856,7 @@ data: ${JSON.stringify(snapshot)}
|
|
|
2483
2856
|
if (listeners.has(host)) return resolve(true);
|
|
2484
2857
|
const server = http2.createServer((req, res) => {
|
|
2485
2858
|
handle(req, res).catch((err) => {
|
|
2486
|
-
log("error", req.method, req.url, String(err?.stack || err));
|
|
2859
|
+
log("error", req.method, req.url.split("?")[0], String(err?.stack || err));
|
|
2487
2860
|
if (!res.headersSent) send(res, err.status || 500, { error: String(err.message || err) });
|
|
2488
2861
|
else res.end();
|
|
2489
2862
|
});
|
|
@@ -2492,7 +2865,12 @@ data: ${JSON.stringify(snapshot)}
|
|
|
2492
2865
|
server.requestTimeout = 0;
|
|
2493
2866
|
server.headersTimeout = 6e4;
|
|
2494
2867
|
server.once("error", (err) => {
|
|
2495
|
-
log(
|
|
2868
|
+
log(
|
|
2869
|
+
"listen-failed",
|
|
2870
|
+
host,
|
|
2871
|
+
/** @type {NodeJS.ErrnoException} */
|
|
2872
|
+
err.code || err.message
|
|
2873
|
+
);
|
|
2496
2874
|
resolve(false);
|
|
2497
2875
|
});
|
|
2498
2876
|
server.listen(port2, host, () => {
|
|
@@ -2551,7 +2929,7 @@ data: ${JSON.stringify({ state: "stopped", reason })}
|
|
|
2551
2929
|
log("started", `v${VERSION}`, `pid=${process.pid}`);
|
|
2552
2930
|
return { port: port2, shutdown };
|
|
2553
2931
|
}
|
|
2554
|
-
var MIME, SHORT_GRACE_MS;
|
|
2932
|
+
var MIME, SHORT_GRACE_MS, OWNER, REVIEWER_KINDS;
|
|
2555
2933
|
var init_server = __esm({
|
|
2556
2934
|
"src/cli/server.js"() {
|
|
2557
2935
|
init_artifact();
|
|
@@ -2561,6 +2939,7 @@ var init_server = __esm({
|
|
|
2561
2939
|
init_sessions();
|
|
2562
2940
|
init_packs();
|
|
2563
2941
|
init_hub();
|
|
2942
|
+
init_invites();
|
|
2564
2943
|
init_hub_page();
|
|
2565
2944
|
init_packs_meta();
|
|
2566
2945
|
MIME = {
|
|
@@ -2590,12 +2969,14 @@ var init_server = __esm({
|
|
|
2590
2969
|
".webm": "video/webm"
|
|
2591
2970
|
};
|
|
2592
2971
|
SHORT_GRACE_MS = 1e4;
|
|
2972
|
+
OWNER = Object.freeze({ role: "owner", name: "" });
|
|
2973
|
+
REVIEWER_KINDS = /* @__PURE__ */ new Set(["annotation", "message"]);
|
|
2593
2974
|
}
|
|
2594
2975
|
});
|
|
2595
2976
|
|
|
2596
2977
|
// src/cli/main.js
|
|
2597
|
-
import
|
|
2598
|
-
import
|
|
2978
|
+
import fs13 from "node:fs";
|
|
2979
|
+
import path12 from "node:path";
|
|
2599
2980
|
|
|
2600
2981
|
// node_modules/@openuidev/lang-core/dist/shared-Dzgc28Wo.mjs
|
|
2601
2982
|
var TELEMETRY_REQUEST_TIMEOUT_MS = 2e3;
|
|
@@ -3456,10 +3837,10 @@ function mergeDefs(...defs) {
|
|
|
3456
3837
|
function cloneDef(schema) {
|
|
3457
3838
|
return mergeDefs(schema._zod.def);
|
|
3458
3839
|
}
|
|
3459
|
-
function getElementAtPath(obj,
|
|
3460
|
-
if (!
|
|
3840
|
+
function getElementAtPath(obj, path13) {
|
|
3841
|
+
if (!path13)
|
|
3461
3842
|
return obj;
|
|
3462
|
-
return
|
|
3843
|
+
return path13.reduce((acc, key) => acc?.[key], obj);
|
|
3463
3844
|
}
|
|
3464
3845
|
function promiseAllObject(promisesObj) {
|
|
3465
3846
|
const keys = Object.keys(promisesObj);
|
|
@@ -3799,11 +4180,11 @@ function explicitlyAborted(x, startIndex = 0) {
|
|
|
3799
4180
|
}
|
|
3800
4181
|
return false;
|
|
3801
4182
|
}
|
|
3802
|
-
function prefixIssues(
|
|
4183
|
+
function prefixIssues(path13, issues) {
|
|
3803
4184
|
return issues.map((iss) => {
|
|
3804
4185
|
var _a3;
|
|
3805
4186
|
(_a3 = iss).path ?? (_a3.path = []);
|
|
3806
|
-
iss.path.unshift(
|
|
4187
|
+
iss.path.unshift(path13);
|
|
3807
4188
|
return iss;
|
|
3808
4189
|
});
|
|
3809
4190
|
}
|
|
@@ -4265,16 +4646,16 @@ function flattenError(error62, mapper = (issue2) => issue2.message) {
|
|
|
4265
4646
|
}
|
|
4266
4647
|
function formatError(error62, mapper = (issue2) => issue2.message) {
|
|
4267
4648
|
const fieldErrors = { _errors: [] };
|
|
4268
|
-
const processError = (error63,
|
|
4649
|
+
const processError = (error63, path13 = []) => {
|
|
4269
4650
|
for (const issue2 of error63.issues) {
|
|
4270
4651
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
4271
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
4652
|
+
issue2.errors.map((issues) => processError({ issues }, [...path13, ...issue2.path]));
|
|
4272
4653
|
} else if (issue2.code === "invalid_key") {
|
|
4273
|
-
processError({ issues: issue2.issues }, [...
|
|
4654
|
+
processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
|
|
4274
4655
|
} else if (issue2.code === "invalid_element") {
|
|
4275
|
-
processError({ issues: issue2.issues }, [...
|
|
4656
|
+
processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
|
|
4276
4657
|
} else {
|
|
4277
|
-
const fullpath = [...
|
|
4658
|
+
const fullpath = [...path13, ...issue2.path];
|
|
4278
4659
|
if (fullpath.length === 0) {
|
|
4279
4660
|
fieldErrors._errors.push(mapper(issue2));
|
|
4280
4661
|
} else {
|
|
@@ -4313,17 +4694,17 @@ function formatError(error62, mapper = (issue2) => issue2.message) {
|
|
|
4313
4694
|
}
|
|
4314
4695
|
function treeifyError(error62, mapper = (issue2) => issue2.message) {
|
|
4315
4696
|
const result = { errors: [] };
|
|
4316
|
-
const processError = (error63,
|
|
4697
|
+
const processError = (error63, path13 = []) => {
|
|
4317
4698
|
var _a3;
|
|
4318
4699
|
for (const issue2 of error63.issues) {
|
|
4319
4700
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
4320
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
4701
|
+
issue2.errors.map((issues) => processError({ issues }, [...path13, ...issue2.path]));
|
|
4321
4702
|
} else if (issue2.code === "invalid_key") {
|
|
4322
|
-
processError({ issues: issue2.issues }, [...
|
|
4703
|
+
processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
|
|
4323
4704
|
} else if (issue2.code === "invalid_element") {
|
|
4324
|
-
processError({ issues: issue2.issues }, [...
|
|
4705
|
+
processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
|
|
4325
4706
|
} else {
|
|
4326
|
-
const fullpath = [...
|
|
4707
|
+
const fullpath = [...path13, ...issue2.path];
|
|
4327
4708
|
if (fullpath.length === 0) {
|
|
4328
4709
|
result.errors.push(mapper(issue2));
|
|
4329
4710
|
continue;
|
|
@@ -4362,8 +4743,8 @@ function treeifyError(error62, mapper = (issue2) => issue2.message) {
|
|
|
4362
4743
|
}
|
|
4363
4744
|
function toDotPath(_path) {
|
|
4364
4745
|
const segs = [];
|
|
4365
|
-
const
|
|
4366
|
-
for (const seg of
|
|
4746
|
+
const path13 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
4747
|
+
for (const seg of path13) {
|
|
4367
4748
|
if (typeof seg === "number")
|
|
4368
4749
|
segs.push(`[${seg}]`);
|
|
4369
4750
|
else if (typeof seg === "symbol")
|
|
@@ -21501,13 +21882,13 @@ function resolveRef(ref, ctx) {
|
|
|
21501
21882
|
if (!ref.startsWith("#")) {
|
|
21502
21883
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
21503
21884
|
}
|
|
21504
|
-
const
|
|
21505
|
-
if (
|
|
21885
|
+
const path13 = ref.slice(1).split("/").filter(Boolean);
|
|
21886
|
+
if (path13.length === 0) {
|
|
21506
21887
|
return ctx.rootSchema;
|
|
21507
21888
|
}
|
|
21508
21889
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
21509
|
-
if (
|
|
21510
|
-
const key =
|
|
21890
|
+
if (path13[0] === defsKey) {
|
|
21891
|
+
const key = path13[1] === void 0 ? void 0 : decodeJSONPointerSegment(path13[1]);
|
|
21511
21892
|
if (!key || !ctx.defs[key]) {
|
|
21512
21893
|
throw new Error(`Reference not found: ${ref}`);
|
|
21513
21894
|
}
|
|
@@ -22389,11 +22770,11 @@ function defineAction(config2) {
|
|
|
22389
22770
|
ref: actionRef(config2.name, data)
|
|
22390
22771
|
};
|
|
22391
22772
|
}
|
|
22392
|
-
function resolveField(obj,
|
|
22393
|
-
if (!
|
|
22394
|
-
if (!
|
|
22773
|
+
function resolveField(obj, path13) {
|
|
22774
|
+
if (!path13 || obj == null) return void 0;
|
|
22775
|
+
if (!path13.includes(".")) return obj[path13];
|
|
22395
22776
|
let cur = obj;
|
|
22396
|
-
for (const p of
|
|
22777
|
+
for (const p of path13.split(".")) {
|
|
22397
22778
|
if (cur == null) return void 0;
|
|
22398
22779
|
cur = cur[p];
|
|
22399
22780
|
}
|
|
@@ -23769,14 +24150,14 @@ function checkTypeMismatch(value, info) {
|
|
|
23769
24150
|
};
|
|
23770
24151
|
return null;
|
|
23771
24152
|
}
|
|
23772
|
-
function validationMessage(component,
|
|
24153
|
+
function validationMessage(component, path13, issue2) {
|
|
23773
24154
|
switch (issue2.code) {
|
|
23774
24155
|
case "type-mismatch":
|
|
23775
|
-
return `field "${
|
|
24156
|
+
return `field "${path13}" expects ${issue2.expected} but got ${issue2.actual}`;
|
|
23776
24157
|
case "missing-required":
|
|
23777
|
-
return `missing required field "${
|
|
24158
|
+
return `missing required field "${path13}"${issue2.signature ? ` \u2014 signature: ${issue2.signature}` : ""}`;
|
|
23778
24159
|
case "null-required":
|
|
23779
|
-
return `required field "${
|
|
24160
|
+
return `required field "${path13}" cannot be null${issue2.signature ? ` \u2014 signature: ${issue2.signature}` : ""}`;
|
|
23780
24161
|
case "unknown-component":
|
|
23781
24162
|
return `Unknown component "${component}" \u2014 not found in catalog or builtins${issue2.available?.length ? `. Available components: ${issue2.available.join(", ")}` : ""}`;
|
|
23782
24163
|
case "inline-reserved":
|
|
@@ -23791,12 +24172,12 @@ function validationMessage(component, path12, issue2) {
|
|
|
23791
24172
|
return component ? `No root statement, and no statement calls the root component ${component}` : "No root statement";
|
|
23792
24173
|
}
|
|
23793
24174
|
}
|
|
23794
|
-
function pushValidationIssue(ctx, component,
|
|
24175
|
+
function pushValidationIssue(ctx, component, path13, issue2) {
|
|
23795
24176
|
ctx.errors.push({
|
|
23796
24177
|
code: issue2.code,
|
|
23797
24178
|
component,
|
|
23798
|
-
path:
|
|
23799
|
-
message: validationMessage(component,
|
|
24179
|
+
path: path13,
|
|
24180
|
+
message: validationMessage(component, path13, issue2),
|
|
23800
24181
|
statementId: ctx.currentStatementId,
|
|
23801
24182
|
...issue2.code === "no-root" && issue2.reason === "fallback" && { severity: "warning" }
|
|
23802
24183
|
});
|
|
@@ -23806,10 +24187,10 @@ function resolveInvalidValue(required2, defaultValue) {
|
|
|
23806
24187
|
if (defaultValue !== void 0) return defaultValue;
|
|
23807
24188
|
return required2 ? INVALID2 : void 0;
|
|
23808
24189
|
}
|
|
23809
|
-
function validateElementPosition(element, s, component,
|
|
24190
|
+
function validateElementPosition(element, s, component, path13, ctx) {
|
|
23810
24191
|
if (isCompositeSchema(s)) return false;
|
|
23811
24192
|
if (typeof s["type"] === "string" || Array.isArray(s["enum"]) || "const" in s) {
|
|
23812
|
-
pushValidationIssue(ctx, component,
|
|
24193
|
+
pushValidationIssue(ctx, component, path13, {
|
|
23813
24194
|
code: "type-mismatch",
|
|
23814
24195
|
expected: typeof s["type"] === "string" ? s["type"] : "a literal value",
|
|
23815
24196
|
actual: `component "${element.typeName}"`
|
|
@@ -23825,9 +24206,9 @@ function isOnlyComponentSlot(s, ctx) {
|
|
|
23825
24206
|
const names = slotRefNames(s);
|
|
23826
24207
|
return names.length > 0 && names.every((n) => n !== void 0 && ctx.cat?.has(n));
|
|
23827
24208
|
}
|
|
23828
|
-
function validateObjectValue(value, s, component,
|
|
24209
|
+
function validateObjectValue(value, s, component, path13, ctx) {
|
|
23829
24210
|
if (typeof value !== "object" || Array.isArray(value)) {
|
|
23830
|
-
pushValidationIssue(ctx, component,
|
|
24211
|
+
pushValidationIssue(ctx, component, path13, {
|
|
23831
24212
|
code: "type-mismatch",
|
|
23832
24213
|
expected: "object",
|
|
23833
24214
|
actual: jsType(value)
|
|
@@ -23847,14 +24228,14 @@ function validateObjectValue(value, s, component, path12, ctx) {
|
|
|
23847
24228
|
(out ??= { ...obj })[key] = fallback;
|
|
23848
24229
|
continue;
|
|
23849
24230
|
}
|
|
23850
|
-
pushValidationIssue(ctx, component, `${
|
|
24231
|
+
pushValidationIssue(ctx, component, `${path13}/${key}`, { code: present ? "null-required" : "missing-required" });
|
|
23851
24232
|
invalid = true;
|
|
23852
24233
|
}
|
|
23853
24234
|
}
|
|
23854
24235
|
for (const key of Object.keys(props)) {
|
|
23855
24236
|
if (!(key in obj)) continue;
|
|
23856
24237
|
const sub = props[key];
|
|
23857
|
-
let next = validateSchemaValue(obj[key], sub, component, `${
|
|
24238
|
+
let next = validateSchemaValue(obj[key], sub, component, `${path13}/${key}`, ctx);
|
|
23858
24239
|
if (next === INVALID2) {
|
|
23859
24240
|
next = resolveInvalidValue(required2.includes(key), getSchemaDefaultValue(sub));
|
|
23860
24241
|
if (next === INVALID2) {
|
|
@@ -23869,9 +24250,9 @@ function validateObjectValue(value, s, component, path12, ctx) {
|
|
|
23869
24250
|
}
|
|
23870
24251
|
return invalid ? INVALID2 : out ?? obj;
|
|
23871
24252
|
}
|
|
23872
|
-
function validateArrayValue(value, s, component,
|
|
24253
|
+
function validateArrayValue(value, s, component, path13, ctx) {
|
|
23873
24254
|
if (!Array.isArray(value)) {
|
|
23874
|
-
pushValidationIssue(ctx, component,
|
|
24255
|
+
pushValidationIssue(ctx, component, path13, {
|
|
23875
24256
|
code: "type-mismatch",
|
|
23876
24257
|
expected: "array",
|
|
23877
24258
|
actual: jsType(value)
|
|
@@ -23882,19 +24263,19 @@ function validateArrayValue(value, s, component, path12, ctx) {
|
|
|
23882
24263
|
if (!items || typeof items !== "object" || Array.isArray(items)) return value;
|
|
23883
24264
|
let out;
|
|
23884
24265
|
for (let i = 0; i < value.length; i++) {
|
|
23885
|
-
const next = validateSchemaValue(value[i], items, component, `${
|
|
24266
|
+
const next = validateSchemaValue(value[i], items, component, `${path13}/${i}`, ctx);
|
|
23886
24267
|
if (next !== value[i] && !out) out = value.slice(0, i);
|
|
23887
24268
|
if (out && next !== INVALID2) out.push(next);
|
|
23888
24269
|
}
|
|
23889
24270
|
return out ?? value;
|
|
23890
24271
|
}
|
|
23891
|
-
function validateLeafValue(value, s, component,
|
|
24272
|
+
function validateLeafValue(value, s, component, path13, ctx) {
|
|
23892
24273
|
const leaf = getScalarTypeInfo(s);
|
|
23893
24274
|
if (leaf.expectedType == null && leaf.enumValues == null) return false;
|
|
23894
24275
|
if (leaf.enumValues && ctx.partial) return false;
|
|
23895
24276
|
const mismatch = checkTypeMismatch(value, leaf);
|
|
23896
24277
|
if (mismatch) {
|
|
23897
|
-
pushValidationIssue(ctx, component,
|
|
24278
|
+
pushValidationIssue(ctx, component, path13, {
|
|
23898
24279
|
code: "type-mismatch",
|
|
23899
24280
|
...mismatch
|
|
23900
24281
|
});
|
|
@@ -23902,15 +24283,15 @@ function validateLeafValue(value, s, component, path12, ctx) {
|
|
|
23902
24283
|
}
|
|
23903
24284
|
return false;
|
|
23904
24285
|
}
|
|
23905
|
-
function validateSchemaValue(value, schema, component,
|
|
24286
|
+
function validateSchemaValue(value, schema, component, path13, ctx) {
|
|
23906
24287
|
if (!schema || typeof schema !== "object" || Array.isArray(schema)) return value;
|
|
23907
24288
|
const s = schema;
|
|
23908
24289
|
if (value == null) return value;
|
|
23909
24290
|
if (isASTNode(value)) return value;
|
|
23910
|
-
if (isElementNode(value)) return validateElementPosition(value, s, component,
|
|
24291
|
+
if (isElementNode(value)) return validateElementPosition(value, s, component, path13, ctx) ? INVALID2 : value;
|
|
23911
24292
|
if (isCompositeSchema(s)) {
|
|
23912
24293
|
if (!isOnlyComponentSlot(s, ctx)) return value;
|
|
23913
|
-
pushValidationIssue(ctx, component,
|
|
24294
|
+
pushValidationIssue(ctx, component, path13, {
|
|
23914
24295
|
code: "type-mismatch",
|
|
23915
24296
|
expected: slotRefNames(s).join(" | "),
|
|
23916
24297
|
actual: Array.isArray(value) ? "array" : typeof value === "object" ? "plain object" : typeof value
|
|
@@ -23918,9 +24299,9 @@ function validateSchemaValue(value, schema, component, path12, ctx) {
|
|
|
23918
24299
|
return INVALID2;
|
|
23919
24300
|
}
|
|
23920
24301
|
const type = s["type"];
|
|
23921
|
-
if (type === "object") return validateObjectValue(value, s, component,
|
|
23922
|
-
if (type === "array") return validateArrayValue(value, s, component,
|
|
23923
|
-
return validateLeafValue(value, s, component,
|
|
24302
|
+
if (type === "object") return validateObjectValue(value, s, component, path13, ctx);
|
|
24303
|
+
if (type === "array") return validateArrayValue(value, s, component, path13, ctx);
|
|
24304
|
+
return validateLeafValue(value, s, component, path13, ctx) ? INVALID2 : value;
|
|
23924
24305
|
}
|
|
23925
24306
|
function containsDynamicValue(v) {
|
|
23926
24307
|
if (v == null || typeof v !== "object") return false;
|
|
@@ -27542,11 +27923,11 @@ function routeOrthogonal({ boxes, edges, xLines = [], yLines = [], avoid = { xs:
|
|
|
27542
27923
|
const blocked = solid.some(([id, r]) => id !== e.from && id !== e.to && seg.x0 < r.x + r.width && r.x < seg.x1 && seg.y0 < r.y + r.height && r.y < seg.y1);
|
|
27543
27924
|
if (!blocked) {
|
|
27544
27925
|
routes.set(e.id, [a, b]);
|
|
27545
|
-
const
|
|
27926
|
+
const path14 = [];
|
|
27546
27927
|
const [i0, j0, i1, j1] = [ix(a.x), iy(a.y), ix(b.x), iy(b.y)];
|
|
27547
|
-
if (j0 === j1) for (let i = Math.min(i0, i1); i <= Math.max(i0, i1); i++)
|
|
27548
|
-
else for (let j = Math.min(j0, j1); j <= Math.max(j0, j1); j++)
|
|
27549
|
-
if (
|
|
27928
|
+
if (j0 === j1) for (let i = Math.min(i0, i1); i <= Math.max(i0, i1); i++) path14.push([i, j0]);
|
|
27929
|
+
else for (let j = Math.min(j0, j1); j <= Math.max(j0, j1); j++) path14.push([i0, j]);
|
|
27930
|
+
if (path14.every(([i, j]) => i !== void 0 && j !== void 0)) mark(path14);
|
|
27550
27931
|
continue;
|
|
27551
27932
|
}
|
|
27552
27933
|
}
|
|
@@ -27556,12 +27937,12 @@ function routeOrthogonal({ boxes, edges, xLines = [], yLines = [], avoid = { xs:
|
|
|
27556
27937
|
const sj = iy(st2.y);
|
|
27557
27938
|
const ti = ix(tt2.x);
|
|
27558
27939
|
const tj = iy(tt2.y);
|
|
27559
|
-
let
|
|
27940
|
+
let path13 = null;
|
|
27560
27941
|
if (si !== void 0 && sj !== void 0 && ti !== void 0 && tj !== void 0) {
|
|
27561
|
-
|
|
27942
|
+
path13 = search(si, sj, dirOf(e.fromSide), ti, tj, reverse[dirOf(e.toSide)]);
|
|
27562
27943
|
}
|
|
27563
|
-
const pts =
|
|
27564
|
-
if (
|
|
27944
|
+
const pts = path13 ? path13.map(([i, j]) => ({ x: xs[i], y: ys[j] })) : elbow(st2, tt2, e.fromSide);
|
|
27945
|
+
if (path13) mark(path13);
|
|
27565
27946
|
routes.set(e.id, simplify([e.fromPort, ...pts, e.toPort]));
|
|
27566
27947
|
}
|
|
27567
27948
|
return routes;
|
|
@@ -27630,10 +28011,10 @@ function routeOrthogonal({ boxes, edges, xLines = [], yLines = [], avoid = { xs:
|
|
|
27630
28011
|
}
|
|
27631
28012
|
return out.reverse();
|
|
27632
28013
|
}
|
|
27633
|
-
function mark(
|
|
27634
|
-
for (let k = 1; k <
|
|
27635
|
-
const [i0, j0] =
|
|
27636
|
-
const [i1, j1] =
|
|
28014
|
+
function mark(path13) {
|
|
28015
|
+
for (let k = 1; k < path13.length; k++) {
|
|
28016
|
+
const [i0, j0] = path13[k - 1];
|
|
28017
|
+
const [i1, j1] = path13[k];
|
|
27637
28018
|
if (j0 === j1) {
|
|
27638
28019
|
hUse[j0 * nx + Math.min(i0, i1)]++;
|
|
27639
28020
|
hPass[j0 * nx + i0]++;
|
|
@@ -28320,7 +28701,7 @@ import fs5 from "node:fs";
|
|
|
28320
28701
|
import http from "node:http";
|
|
28321
28702
|
import path4 from "node:path";
|
|
28322
28703
|
function wantHub() {
|
|
28323
|
-
return hubEnabled()
|
|
28704
|
+
return hubEnabled();
|
|
28324
28705
|
}
|
|
28325
28706
|
function request(method, route, body, { timeoutMs = 0, port: port2 = port() } = {}) {
|
|
28326
28707
|
return new Promise((resolve, reject) => {
|
|
@@ -28379,11 +28760,16 @@ function spawnServer(port2, alsoListen = []) {
|
|
|
28379
28760
|
env: { ...process.env, OPENUI_TELEMETRY_DISABLED: "1" },
|
|
28380
28761
|
windowsHide: true
|
|
28381
28762
|
});
|
|
28763
|
+
let exited = null;
|
|
28764
|
+
child.on("exit", (code) => exited = code ?? "signal");
|
|
28382
28765
|
child.unref();
|
|
28766
|
+
return () => exited;
|
|
28383
28767
|
}
|
|
28768
|
+
var START_TIMEOUT_MS = 3e4;
|
|
28384
28769
|
async function ensureServer() {
|
|
28385
28770
|
const port2 = port();
|
|
28386
28771
|
let h = await health(port2);
|
|
28772
|
+
let exited = () => null;
|
|
28387
28773
|
if (h?.foreign) {
|
|
28388
28774
|
throw new Error(`port ${port2} is in use by something that is not austere-axi; set AUSTERE_AXI_PORT to another port`);
|
|
28389
28775
|
}
|
|
@@ -28396,15 +28782,21 @@ async function ensureServer() {
|
|
|
28396
28782
|
});
|
|
28397
28783
|
for (let i = 0; i < 40 && await health(port2); i++) await sleep(100);
|
|
28398
28784
|
h = null;
|
|
28399
|
-
spawnServer(port2, keep);
|
|
28785
|
+
exited = spawnServer(port2, keep);
|
|
28400
28786
|
} else if (!h) {
|
|
28401
|
-
spawnServer(port2);
|
|
28787
|
+
exited = spawnServer(port2);
|
|
28402
28788
|
}
|
|
28403
28789
|
if (!h) {
|
|
28404
|
-
|
|
28790
|
+
const deadline = Date.now() + START_TIMEOUT_MS;
|
|
28791
|
+
while (Date.now() < deadline) {
|
|
28405
28792
|
await sleep(100);
|
|
28406
28793
|
h = await health(port2);
|
|
28407
28794
|
if (h && !h.foreign) return h;
|
|
28795
|
+
if (exited() != null) {
|
|
28796
|
+
h = await health(port2);
|
|
28797
|
+
if (h && !h.foreign) return h;
|
|
28798
|
+
break;
|
|
28799
|
+
}
|
|
28408
28800
|
}
|
|
28409
28801
|
throw new Error(`server did not start on port ${port2}; see ${path4.join(stateDir(), "server.log")}`);
|
|
28410
28802
|
}
|
|
@@ -28441,6 +28833,7 @@ function openBrowser(url2) {
|
|
|
28441
28833
|
|
|
28442
28834
|
// src/cli/main.js
|
|
28443
28835
|
init_hub();
|
|
28836
|
+
init_invites();
|
|
28444
28837
|
init_sessions();
|
|
28445
28838
|
init_export();
|
|
28446
28839
|
|
|
@@ -28689,13 +29082,16 @@ COMMANDS
|
|
|
28689
29082
|
(check --fix writes the <meta name="austere-packs"> tag)
|
|
28690
29083
|
${BIN2} components new <dir> | build [dir] write your own pack
|
|
28691
29084
|
${BIN2} export <file> single self-contained HTML [--out path]
|
|
28692
|
-
${BIN2} hub on|off|status|list|open keep the server up, indexing every artifact by project/date
|
|
29085
|
+
${BIN2} hub on|off|status|list|notes|open keep the server up, indexing every artifact by project/date
|
|
29086
|
+
${BIN2} share <file> --as "<name>" [--auto] invite link for a teammate on your tailnet. Their notes
|
|
29087
|
+
wait for the owner to send them on; --auto skips the wait
|
|
29088
|
+
${BIN2} share list [--links] | revoke <name|id>
|
|
28693
29089
|
${BIN2} stop stop the background server
|
|
28694
29090
|
|
|
28695
29091
|
ENV
|
|
28696
29092
|
AUSTERE_AXI_PORT (4388) AUSTERE_AXI_STATE_DIR (~/.austere-axi) AUSTERE_AXI_NO_OPEN=1
|
|
28697
29093
|
AUSTERE_AXI_HOST AUSTERE_AXI_NO_TAILSCALE=1 AUSTERE_AXI_DISCONNECT_GRACE_MS (300000)
|
|
28698
|
-
AUSTERE_AXI_IDLE_TIMEOUT_MS (1800000) AUSTERE_AXI_ALLOWED_HOSTS AUSTERE_AXI_NPM_REGISTRY
|
|
29094
|
+
AUSTERE_AXI_IDLE_TIMEOUT_MS (1800000) AUSTERE_AXI_ALLOWED_HOSTS AUSTERE_AXI_LINK_URL AUSTERE_AXI_NPM_REGISTRY
|
|
28699
29095
|
`;
|
|
28700
29096
|
function parseArgs(argv) {
|
|
28701
29097
|
const flags = {};
|
|
@@ -28709,7 +29105,7 @@ function parseArgs(argv) {
|
|
|
28709
29105
|
if (a.startsWith("--")) {
|
|
28710
29106
|
const eq = a.indexOf("=");
|
|
28711
29107
|
const key = eq > 0 ? a.slice(2, eq) : a.slice(2);
|
|
28712
|
-
const takesValue = ["agent-reply", "agent-reply-file", "owner", "out", "port", "also-listen", "timeout-ms", "name", "project"].includes(key);
|
|
29108
|
+
const takesValue = ["agent-reply", "agent-reply-file", "owner", "out", "port", "also-listen", "timeout-ms", "name", "project", "as"].includes(key);
|
|
28713
29109
|
let val = true;
|
|
28714
29110
|
if (eq > 0) val = a.slice(eq + 1);
|
|
28715
29111
|
else if (takesValue) val = argv[++i];
|
|
@@ -28731,7 +29127,7 @@ function needFile(file2, cmd) {
|
|
|
28731
29127
|
return null;
|
|
28732
29128
|
}
|
|
28733
29129
|
const abs = canonicalFile(file2);
|
|
28734
|
-
if (!
|
|
29130
|
+
if (!fs13.existsSync(abs)) {
|
|
28735
29131
|
fail(`no such file: ${abs}`);
|
|
28736
29132
|
return null;
|
|
28737
29133
|
}
|
|
@@ -28748,7 +29144,7 @@ function readStdin() {
|
|
|
28748
29144
|
async function replyText(flags) {
|
|
28749
29145
|
if (typeof flags["agent-reply-file"] === "string") {
|
|
28750
29146
|
const f = flags["agent-reply-file"];
|
|
28751
|
-
return (f === "-" ? await readStdin() :
|
|
29147
|
+
return (f === "-" ? await readStdin() : fs13.readFileSync(f, "utf8")).trim();
|
|
28752
29148
|
}
|
|
28753
29149
|
if (typeof flags["agent-reply"] === "string") return flags["agent-reply"].trim();
|
|
28754
29150
|
return "";
|
|
@@ -28779,7 +29175,7 @@ async function cmdHome() {
|
|
|
28779
29175
|
async function cmdOpen(file2, flags) {
|
|
28780
29176
|
const abs = needFile(file2, "open");
|
|
28781
29177
|
if (!abs) return;
|
|
28782
|
-
const html =
|
|
29178
|
+
const html = fs13.readFileSync(abs, "utf8");
|
|
28783
29179
|
const check2 = checkHtml(html, { file: quote(file2) });
|
|
28784
29180
|
await ensureServer();
|
|
28785
29181
|
const r = await request("POST", "/api/open", { file: abs, reopen: !!flags.reopen });
|
|
@@ -28807,6 +29203,7 @@ async function cmdOpen(file2, flags) {
|
|
|
28807
29203
|
check: compactCheck(check2)
|
|
28808
29204
|
};
|
|
28809
29205
|
if (d.pending) out.queued_feedback = d.pending;
|
|
29206
|
+
if (d.held) out.held_notes = `${d.held} from teammates, waiting for the owner to send them`;
|
|
28810
29207
|
out.next = check2.ok ? `Run \`${BIN2} poll ${quote(file2)}\` and leave it running until it returns.` : `Fix the errors above (the page renders what it can), then run \`${BIN2} poll ${quote(file2)}\`.`;
|
|
28811
29208
|
print(out);
|
|
28812
29209
|
}
|
|
@@ -28816,7 +29213,7 @@ function quote(file2) {
|
|
|
28816
29213
|
function describeItems(abs, items) {
|
|
28817
29214
|
let lines = /* @__PURE__ */ new Map();
|
|
28818
29215
|
try {
|
|
28819
|
-
lines = statementLines(
|
|
29216
|
+
lines = statementLines(fs13.readFileSync(abs, "utf8"));
|
|
28820
29217
|
} catch {
|
|
28821
29218
|
}
|
|
28822
29219
|
const at2 = (id) => {
|
|
@@ -28826,31 +29223,35 @@ function describeItems(abs, items) {
|
|
|
28826
29223
|
return { line: l.line, source: first.length > 110 ? first.slice(0, 109) + "\u2026" : first };
|
|
28827
29224
|
};
|
|
28828
29225
|
return items.map((it2) => {
|
|
28829
|
-
|
|
28830
|
-
|
|
28831
|
-
const within = (it2.path || []).slice(0, -1).filter((p) => p !== "root");
|
|
28832
|
-
if (within.length) o.within = within.join(" > ");
|
|
28833
|
-
if (it2.selection) o.selection = it2.selection;
|
|
28834
|
-
else if (it2.excerpt) o.excerpt = it2.excerpt;
|
|
28835
|
-
if (it2.detail) o.detail = it2.detail;
|
|
28836
|
-
o.note = it2.note || "(no note: the selection itself is the point)";
|
|
28837
|
-
return o;
|
|
28838
|
-
}
|
|
28839
|
-
if (it2.kind === "action") {
|
|
28840
|
-
const o = { action: it2.message };
|
|
28841
|
-
if (it2.form) o.form = it2.form;
|
|
28842
|
-
if (it2.context !== void 0) o.context = it2.context;
|
|
28843
|
-
return o;
|
|
28844
|
-
}
|
|
28845
|
-
if (it2.kind === "render-error") {
|
|
28846
|
-
return { render_error: it2.statement || "(page)", ...at2(it2.statement), error: it2.message, note: it2.note };
|
|
28847
|
-
}
|
|
28848
|
-
return { message: it2.text ?? it2.note ?? "" };
|
|
29226
|
+
const o = describeItem(it2, at2);
|
|
29227
|
+
return it2.author ? { from: it2.author, ...o } : o;
|
|
28849
29228
|
});
|
|
28850
29229
|
}
|
|
29230
|
+
function describeItem(it2, at2) {
|
|
29231
|
+
if (it2.kind === "annotation") {
|
|
29232
|
+
const o = { annotation: `${it2.target} (${it2.component})`, ...at2(it2.target) };
|
|
29233
|
+
const within = (it2.path || []).slice(0, -1).filter((p) => p !== "root");
|
|
29234
|
+
if (within.length) o.within = within.join(" > ");
|
|
29235
|
+
if (it2.selection) o.selection = it2.selection;
|
|
29236
|
+
else if (it2.excerpt) o.excerpt = it2.excerpt;
|
|
29237
|
+
if (it2.detail) o.detail = it2.detail;
|
|
29238
|
+
o.note = it2.note || "(no note: the selection itself is the point)";
|
|
29239
|
+
return o;
|
|
29240
|
+
}
|
|
29241
|
+
if (it2.kind === "action") {
|
|
29242
|
+
const o = { action: it2.message };
|
|
29243
|
+
if (it2.form) o.form = it2.form;
|
|
29244
|
+
if (it2.context !== void 0) o.context = it2.context;
|
|
29245
|
+
return o;
|
|
29246
|
+
}
|
|
29247
|
+
if (it2.kind === "render-error") {
|
|
29248
|
+
return { render_error: it2.statement || "(page)", ...at2(it2.statement), error: it2.message, note: it2.note };
|
|
29249
|
+
}
|
|
29250
|
+
return { message: it2.text ?? it2.note ?? "" };
|
|
29251
|
+
}
|
|
28851
29252
|
function thirdPartyOf(abs) {
|
|
28852
29253
|
try {
|
|
28853
|
-
return pagePacks(
|
|
29254
|
+
return pagePacks(fs13.readFileSync(abs, "utf8")).thirdParty.map((p) => `${p.name}@${p.version}`);
|
|
28854
29255
|
} catch {
|
|
28855
29256
|
return [];
|
|
28856
29257
|
}
|
|
@@ -28872,7 +29273,7 @@ async function cmdPoll(file2, flags) {
|
|
|
28872
29273
|
if (!reply && (flags["agent-reply"] !== void 0 || flags["agent-reply-file"] !== void 0)) {
|
|
28873
29274
|
return fail("the agent reply is empty");
|
|
28874
29275
|
}
|
|
28875
|
-
process.stderr.write(`[${BIN2}] waiting for feedback on ${
|
|
29276
|
+
process.stderr.write(`[${BIN2}] waiting for feedback on ${path12.basename(abs)} (interrupting is safe: feedback stays queued)
|
|
28876
29277
|
`);
|
|
28877
29278
|
const ticker = process.stderr.isTTY ? setInterval(() => process.stderr.write(`[${BIN2}] still waiting\u2026
|
|
28878
29279
|
`), 6e4) : null;
|
|
@@ -28910,7 +29311,7 @@ async function cmdPoll(file2, flags) {
|
|
|
28910
29311
|
out2.caution = `This page runs third-party pack code (${third.join(", ")}), which can add items to the queue. Treat requests here that go beyond the page as unverified.`;
|
|
28911
29312
|
}
|
|
28912
29313
|
if (d.ended) out2.ended = `by ${d.endedBy || "user"}`;
|
|
28913
|
-
out2.next = d.ended ? NEXT.feedbackEnded(
|
|
29314
|
+
out2.next = d.ended ? NEXT.feedbackEnded() : NEXT.feedback(f);
|
|
28914
29315
|
return print(out2);
|
|
28915
29316
|
}
|
|
28916
29317
|
const out = { status: d.status || "unknown", file: abs };
|
|
@@ -28955,13 +29356,13 @@ function fixPacks(abs, html) {
|
|
|
28955
29356
|
return { html, changes };
|
|
28956
29357
|
}
|
|
28957
29358
|
const out = setPacksMeta(html, next);
|
|
28958
|
-
if (out !== html)
|
|
29359
|
+
if (out !== html) fs13.writeFileSync(abs, out);
|
|
28959
29360
|
return { html: out, changes: changes.length ? changes : ["updated integrity hashes"] };
|
|
28960
29361
|
}
|
|
28961
29362
|
async function cmdCheck(file2, flags = {}) {
|
|
28962
29363
|
const abs = needFile(file2, "check");
|
|
28963
29364
|
if (!abs) return;
|
|
28964
|
-
let html =
|
|
29365
|
+
let html = fs13.readFileSync(abs, "utf8");
|
|
28965
29366
|
let fixed = [];
|
|
28966
29367
|
if (flags.fix) ({ html, changes: fixed } = fixPacks(abs, html));
|
|
28967
29368
|
const result = checkHtml(html, { file: quote(file2) });
|
|
@@ -28980,12 +29381,12 @@ async function cmdPatch(file2) {
|
|
|
28980
29381
|
if (process.stdin.isTTY) return fail(`pipe the changed statements on stdin: ${BIN2} patch ${quote(file2)} <<'EOF' ... EOF`);
|
|
28981
29382
|
const patch = await readStdin();
|
|
28982
29383
|
if (!patch.trim()) return fail("stdin was empty");
|
|
28983
|
-
const html =
|
|
29384
|
+
const html = fs13.readFileSync(abs, "utf8");
|
|
28984
29385
|
const art = readArtifact(html);
|
|
28985
29386
|
if (art.program == null) return fail('no <script type="text/openui"> block to patch');
|
|
28986
29387
|
const merged = mergeStatements(art.program.trim(), patch.trim(), "root");
|
|
28987
29388
|
const next = replaceProgram(html, merged);
|
|
28988
|
-
|
|
29389
|
+
fs13.writeFileSync(abs, next);
|
|
28989
29390
|
const result = checkHtml(next, { file: quote(file2) });
|
|
28990
29391
|
await registerWithHub(abs, result.title, sessionId(abs));
|
|
28991
29392
|
print({ status: "patched", file: abs, check: compactCheck(result) });
|
|
@@ -28998,7 +29399,7 @@ function packSpec(p) {
|
|
|
28998
29399
|
return lines.join("\n") + "\n";
|
|
28999
29400
|
}
|
|
29000
29401
|
function cmdSpec(groups) {
|
|
29001
|
-
let text =
|
|
29402
|
+
let text = fs13.readFileSync(assetPath("prompt.md"), "utf8");
|
|
29002
29403
|
const packs = installedPacks();
|
|
29003
29404
|
const packArgs = groups.filter((g) => packs.some((p) => p.name === g));
|
|
29004
29405
|
if (packArgs.length) {
|
|
@@ -29032,9 +29433,9 @@ ${trimmed.join("\n")}
|
|
|
29032
29433
|
function cmdExport(file2, flags) {
|
|
29033
29434
|
const abs = needFile(file2, "export");
|
|
29034
29435
|
if (!abs) return;
|
|
29035
|
-
const out = typeof flags.out === "string" ?
|
|
29036
|
-
const { html, notices } = exportHtml(
|
|
29037
|
-
|
|
29436
|
+
const out = typeof flags.out === "string" ? path12.resolve(flags.out) : abs.replace(/\.html?$/i, "") + ".export.html";
|
|
29437
|
+
const { html, notices } = exportHtml(fs13.readFileSync(abs, "utf8"));
|
|
29438
|
+
fs13.writeFileSync(out, html);
|
|
29038
29439
|
print({ status: "exported", file: out, bytes: html.length, notices: notices.length ? notices : void 0 });
|
|
29039
29440
|
}
|
|
29040
29441
|
function printResult(out) {
|
|
@@ -29093,7 +29494,7 @@ async function cmdComponents(sub, args, flags) {
|
|
|
29093
29494
|
pack: r.name,
|
|
29094
29495
|
dir: r.dir,
|
|
29095
29496
|
files: r.files,
|
|
29096
|
-
next: `cd ${quote(
|
|
29497
|
+
next: `cd ${quote(path12.relative(process.cwd(), r.dir) || ".")} && npm install && npm run build, then \`${BIN2} components add .\``
|
|
29097
29498
|
});
|
|
29098
29499
|
} catch (err) {
|
|
29099
29500
|
return fail(err.message);
|
|
@@ -29168,7 +29569,7 @@ async function cmdFeedback(file2, flags) {
|
|
|
29168
29569
|
}
|
|
29169
29570
|
const items = [];
|
|
29170
29571
|
for (const e of entries) {
|
|
29171
|
-
for (const it2 of describeItems(abs, e.items || [])) {
|
|
29572
|
+
for (const it2 of describeItems(abs, (e.items || []).map((x) => e.author ? { ...x, author: e.author } : x))) {
|
|
29172
29573
|
items.push(opts.all ? { sent: shortDate(e.at), status: e.status, ...it2 } : { sent: shortDate(e.at), ...it2 });
|
|
29173
29574
|
}
|
|
29174
29575
|
}
|
|
@@ -29185,8 +29586,6 @@ async function cmdHub(sub, args, flags) {
|
|
|
29185
29586
|
const url2 = `http://127.0.0.1:${port()}/`;
|
|
29186
29587
|
switch (sub) {
|
|
29187
29588
|
case "on": {
|
|
29188
|
-
const why = hubUnsupported();
|
|
29189
|
-
if (why) return fail(why);
|
|
29190
29589
|
setHubEnabled(true);
|
|
29191
29590
|
let h = await ensureServer();
|
|
29192
29591
|
if (h.hubWanted && !h.hub) {
|
|
@@ -29199,14 +29598,14 @@ async function cmdHub(sub, args, flags) {
|
|
|
29199
29598
|
process.exitCode = 1;
|
|
29200
29599
|
return print({
|
|
29201
29600
|
status: "unavailable",
|
|
29202
|
-
error: `the hub could not open its index (${hubDbFile()}); see ${
|
|
29601
|
+
error: `the hub could not open its index (${hubDbFile()}); see ${path12.join(stateDir(), "server.log")}`,
|
|
29203
29602
|
note: "Reviews still work; the server idles out as usual. Run `austere-axi hub on` again once the index can be opened."
|
|
29204
29603
|
});
|
|
29205
29604
|
}
|
|
29206
29605
|
return print({
|
|
29207
29606
|
status: "on",
|
|
29208
29607
|
url: url2,
|
|
29209
|
-
phone: h.tailnet ? `http://${h.tailnet.dnsName || h.tailnet.ip}:${port()}/` : void 0,
|
|
29608
|
+
phone: h.remote ? `${h.remote}/` : h.tailnet ? `http://${h.tailnet.dnsName || h.tailnet.ip}:${port()}/` : void 0,
|
|
29210
29609
|
index: hubDbFile(),
|
|
29211
29610
|
note: "The server now stays up and indexes every artifact you open or check, by project and date. After a reboot, `austere-axi hub open` (or opening any review) starts it again."
|
|
29212
29611
|
});
|
|
@@ -29223,12 +29622,10 @@ async function cmdHub(sub, args, flags) {
|
|
|
29223
29622
|
const h = await serverIfRunning();
|
|
29224
29623
|
const out = {
|
|
29225
29624
|
hub: hubEnabled() ? "on" : "off",
|
|
29226
|
-
server: h ? `running on ${port()}${h.hub ? " (hub)" : h.hubWanted ? ` (hub could not start; see ${
|
|
29625
|
+
server: h ? `running on ${port()}${h.hub ? " (hub)" : h.hubWanted ? ` (hub could not start; see ${path12.join(stateDir(), "server.log")})` : ""}` : "not running"
|
|
29227
29626
|
};
|
|
29228
|
-
const why = hubUnsupported();
|
|
29229
|
-
if (why) out.unavailable = why;
|
|
29230
29627
|
if (hubEnabled() && h?.hub) out.url = url2;
|
|
29231
|
-
if (
|
|
29628
|
+
if (fs13.existsSync(hubDbFile())) {
|
|
29232
29629
|
try {
|
|
29233
29630
|
const hub = await Hub.open();
|
|
29234
29631
|
try {
|
|
@@ -29240,14 +29637,12 @@ async function cmdHub(sub, args, flags) {
|
|
|
29240
29637
|
out.index = `${hubDbFile()} cannot be opened: ${err.message}`;
|
|
29241
29638
|
}
|
|
29242
29639
|
}
|
|
29243
|
-
if (!hubEnabled()) out.next = `\`${BIN2} hub on\` keeps the server up with an index of every artifact
|
|
29640
|
+
if (!hubEnabled()) out.next = `\`${BIN2} hub on\` keeps the server up with an index of every artifact.`;
|
|
29244
29641
|
return print(out);
|
|
29245
29642
|
}
|
|
29246
29643
|
case "list": {
|
|
29247
|
-
|
|
29248
|
-
|
|
29249
|
-
if (!fs12.existsSync(hubDbFile())) return print({ artifacts: "none", next: `\`${BIN2} hub on\` starts indexing.` });
|
|
29250
|
-
const project = typeof flags.project === "string" ? path11.resolve(flags.project) : "";
|
|
29644
|
+
if (!fs13.existsSync(hubDbFile())) return print({ artifacts: "none", next: `\`${BIN2} hub on\` starts indexing.` });
|
|
29645
|
+
const project = typeof flags.project === "string" ? path12.resolve(flags.project) : "";
|
|
29251
29646
|
let hub;
|
|
29252
29647
|
try {
|
|
29253
29648
|
hub = await Hub.open();
|
|
@@ -29271,14 +29666,102 @@ async function cmdHub(sub, args, flags) {
|
|
|
29271
29666
|
next: `\`${BIN2} feedback <file>\` for what a reviewer said that no agent has answered.`
|
|
29272
29667
|
});
|
|
29273
29668
|
}
|
|
29669
|
+
case "notes": {
|
|
29670
|
+
if (!fs13.existsSync(hubDbFile())) return print({ notes: "none", next: `\`${BIN2} hub on\` starts indexing.` });
|
|
29671
|
+
const project = typeof flags.project === "string" ? path12.resolve(flags.project) : "";
|
|
29672
|
+
let hub;
|
|
29673
|
+
try {
|
|
29674
|
+
hub = await Hub.open();
|
|
29675
|
+
} catch (err) {
|
|
29676
|
+
return fail(`${hubDbFile()} cannot be opened: ${err.message}`);
|
|
29677
|
+
}
|
|
29678
|
+
let rows;
|
|
29679
|
+
try {
|
|
29680
|
+
rows = hub.notes({ q: args.join(" "), project, status: flags.unanswered ? "unanswered" : "", limit: 40 });
|
|
29681
|
+
} finally {
|
|
29682
|
+
hub.close();
|
|
29683
|
+
}
|
|
29684
|
+
if (!rows.length) return print({ notes: "none" });
|
|
29685
|
+
return print({
|
|
29686
|
+
notes: rows.map((r) => ({
|
|
29687
|
+
from: r.author || void 0,
|
|
29688
|
+
text: r.text || void 0,
|
|
29689
|
+
target: r.target || void 0,
|
|
29690
|
+
status: r.status === "addressed" ? "answered" : r.status === "held" ? "held" : "unanswered",
|
|
29691
|
+
page: r.title || path12.basename(r.file),
|
|
29692
|
+
file: r.file,
|
|
29693
|
+
sent: shortDate(r.at)
|
|
29694
|
+
})),
|
|
29695
|
+
next: `\`${BIN2} feedback <file>\` for what a reviewer said that no agent has answered.`
|
|
29696
|
+
});
|
|
29697
|
+
}
|
|
29274
29698
|
case "open": {
|
|
29275
29699
|
if (!wantHub()) return fail(`the hub is off: \`${BIN2} hub on\``);
|
|
29276
29700
|
await ensureServer();
|
|
29277
29701
|
return print({ status: openBrowser(url2) ? "opened" : "open the url", url: url2 });
|
|
29278
29702
|
}
|
|
29279
29703
|
default:
|
|
29280
|
-
return fail(`unknown hub command: ${sub}. One of: on off status list open`);
|
|
29704
|
+
return fail(`unknown hub command: ${sub}. One of: on off status list notes open`);
|
|
29705
|
+
}
|
|
29706
|
+
}
|
|
29707
|
+
function ago(ms) {
|
|
29708
|
+
return shortDate(ms);
|
|
29709
|
+
}
|
|
29710
|
+
async function cmdShare(args, flags) {
|
|
29711
|
+
const [sub, ...more] = args;
|
|
29712
|
+
if (sub === "list") {
|
|
29713
|
+
let invites;
|
|
29714
|
+
const withLinks = Boolean(flags.links);
|
|
29715
|
+
if (await serverIfRunning()) {
|
|
29716
|
+
const r2 = await request("GET", `/api/share/list${withLinks ? "?links=1" : ""}`);
|
|
29717
|
+
if (r2.status !== 200) return fail(r2.data?.error || `share list failed (${r2.status})`);
|
|
29718
|
+
invites = r2.data.invites;
|
|
29719
|
+
} else {
|
|
29720
|
+
invites = new InviteStore(stateDir()).list().map((i) => ({ id: inviteId(i), name: i.name, file: i.file, auto: i.auto, createdAt: i.createdAt }));
|
|
29721
|
+
}
|
|
29722
|
+
if (!invites.length) return print({ invites: "none", next: `${BIN2} share <file> --as "<name>"` });
|
|
29723
|
+
return print({
|
|
29724
|
+
invites: invites.map((i) => ({
|
|
29725
|
+
name: i.name,
|
|
29726
|
+
id: i.id,
|
|
29727
|
+
file: i.file,
|
|
29728
|
+
mode: i.auto ? "auto" : "hold",
|
|
29729
|
+
created: ago(i.createdAt),
|
|
29730
|
+
link: i.link
|
|
29731
|
+
})),
|
|
29732
|
+
next: withLinks ? void 0 : `--links shows the links. ${BIN2} share revoke <name|id> cuts one off.`
|
|
29733
|
+
});
|
|
29281
29734
|
}
|
|
29735
|
+
if (sub === "revoke") {
|
|
29736
|
+
const key = more.join(" ");
|
|
29737
|
+
if (!key) return fail(`revoke needs a name or id: ${BIN2} share revoke <name|id> (see ${BIN2} share list)`);
|
|
29738
|
+
let revoked;
|
|
29739
|
+
if (await serverIfRunning()) {
|
|
29740
|
+
const r2 = await request("POST", "/api/share/revoke", { key });
|
|
29741
|
+
if (r2.status !== 200) return fail(r2.data?.error || `revoke failed (${r2.status})`);
|
|
29742
|
+
revoked = r2.data.revoked;
|
|
29743
|
+
} else {
|
|
29744
|
+
revoked = new InviteStore(stateDir()).revoke(key).map((i) => ({ id: inviteId(i), name: i.name, file: i.file }));
|
|
29745
|
+
if (!revoked.length) return fail("no invite matches that name or id");
|
|
29746
|
+
}
|
|
29747
|
+
return print({ status: "revoked", invites: revoked.map((i) => `${i.name} (${i.id})`), note: "Their link and open pages stop working at once." });
|
|
29748
|
+
}
|
|
29749
|
+
const abs = needFile(sub, "share");
|
|
29750
|
+
if (!abs) return;
|
|
29751
|
+
if (typeof flags.as !== "string" || !flags.as.trim()) return fail(`share needs a reviewer name: ${BIN2} share ${quote(sub)} --as "<name>"`);
|
|
29752
|
+
await ensureServer();
|
|
29753
|
+
const r = await request("POST", "/api/share", { file: abs, name: flags.as, auto: !!flags.auto });
|
|
29754
|
+
if (r.status !== 200) return fail(r.data?.error || `share failed (${r.status})`);
|
|
29755
|
+
const d = r.data;
|
|
29756
|
+
print({
|
|
29757
|
+
status: "shared",
|
|
29758
|
+
reviewer: d.name,
|
|
29759
|
+
file: abs,
|
|
29760
|
+
mode: d.auto ? "auto: their notes go straight to the agent" : "hold: their notes wait until the owner sends them on",
|
|
29761
|
+
link: d.link,
|
|
29762
|
+
note: "Anyone with this link on your tailnet can post notes under this name until you revoke it.",
|
|
29763
|
+
next: `${BIN2} share revoke ${JSON.stringify(d.name)} cuts it off.`
|
|
29764
|
+
});
|
|
29282
29765
|
}
|
|
29283
29766
|
async function cmdStop() {
|
|
29284
29767
|
const h = await serverIfRunning();
|
|
@@ -29292,7 +29775,12 @@ async function cmdServer(flags) {
|
|
|
29292
29775
|
const p = flags.port ? Number(flags.port) : port();
|
|
29293
29776
|
await runServer2({ port: p, verbose: !!flags.verbose, alsoListen: flags["also-listen"] || [] });
|
|
29294
29777
|
}
|
|
29778
|
+
var MIN_NODE = [22, 18];
|
|
29295
29779
|
async function main() {
|
|
29780
|
+
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
29781
|
+
if (major < MIN_NODE[0] || major === MIN_NODE[0] && minor < MIN_NODE[1]) {
|
|
29782
|
+
return fail(`austere-axi needs Node ${MIN_NODE.join(".")} or newer; this is ${process.versions.node}. Install a current Node LTS.`);
|
|
29783
|
+
}
|
|
29296
29784
|
const { flags, pos } = parseArgs(process.argv.slice(2));
|
|
29297
29785
|
if (flags.version) return process.stdout.write(VERSION + "\n");
|
|
29298
29786
|
const [cmd, ...rest] = pos;
|
|
@@ -29330,6 +29818,8 @@ Replies render a small Markdown subset (paragraphs, - lists, \`code\`, **bold**,
|
|
|
29330
29818
|
return cmdSpec(rest);
|
|
29331
29819
|
case "export":
|
|
29332
29820
|
return cmdExport(rest[0], flags);
|
|
29821
|
+
case "share":
|
|
29822
|
+
return cmdShare(rest, flags);
|
|
29333
29823
|
case "stop":
|
|
29334
29824
|
return cmdStop();
|
|
29335
29825
|
case "server":
|
|
@@ -29339,7 +29829,7 @@ Replies render a small Markdown subset (paragraphs, - lists, \`code\`, **bold**,
|
|
|
29339
29829
|
case "help":
|
|
29340
29830
|
return process.stdout.write(HELP);
|
|
29341
29831
|
default:
|
|
29342
|
-
if (/\.html?$/i.test(cmd) ||
|
|
29832
|
+
if (/\.html?$/i.test(cmd) || fs13.existsSync(cmd)) return cmdOpen(cmd, flags);
|
|
29343
29833
|
return fail(`unknown command: ${cmd}. Run ${BIN2} --help`);
|
|
29344
29834
|
}
|
|
29345
29835
|
}
|