@askexenow/exe-os 0.9.61 → 0.9.63
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/deploy/stack-manifests/v0.9.json +62 -13
- package/dist/bin/backfill-conversations.js +210 -2
- package/dist/bin/backfill-responses.js +210 -2
- package/dist/bin/backfill-vectors.js +47 -2
- package/dist/bin/cleanup-stale-review-tasks.js +210 -2
- package/dist/bin/cli.js +488 -25
- package/dist/bin/exe-agent-config.js +2 -2
- package/dist/bin/exe-agent.js +2 -2
- package/dist/bin/exe-assign.js +210 -2
- package/dist/bin/exe-boot.js +47 -2
- package/dist/bin/exe-call.js +2 -2
- package/dist/bin/exe-cloud.js +2 -2
- package/dist/bin/exe-dispatch.js +210 -2
- package/dist/bin/exe-doctor.js +2691 -1212
- package/dist/bin/exe-export-behaviors.js +210 -2
- package/dist/bin/exe-forget.js +264 -2
- package/dist/bin/exe-gateway.js +210 -2
- package/dist/bin/exe-heartbeat.js +212 -4
- package/dist/bin/exe-kill.js +210 -2
- package/dist/bin/exe-launch-agent.js +210 -2
- package/dist/bin/exe-link.js +47 -2
- package/dist/bin/exe-new-employee.js +113 -3
- package/dist/bin/exe-pending-messages.js +210 -2
- package/dist/bin/exe-pending-notifications.js +210 -2
- package/dist/bin/exe-pending-reviews.js +210 -2
- package/dist/bin/exe-rename.js +210 -2
- package/dist/bin/exe-review.js +210 -2
- package/dist/bin/exe-search.js +234 -3
- package/dist/bin/exe-session-cleanup.js +210 -2
- package/dist/bin/exe-settings.js +2 -2
- package/dist/bin/exe-start-codex.js +322 -4
- package/dist/bin/exe-start-opencode.js +336 -4
- package/dist/bin/exe-status.js +210 -2
- package/dist/bin/exe-team.js +210 -2
- package/dist/bin/git-sweep.js +210 -2
- package/dist/bin/graph-backfill.js +210 -2
- package/dist/bin/graph-export.js +210 -2
- package/dist/bin/install.js +113 -3
- package/dist/bin/intercom-check.js +210 -2
- package/dist/bin/scan-tasks.js +210 -2
- package/dist/bin/setup.js +47 -2
- package/dist/bin/shard-migrate.js +210 -2
- package/dist/bin/stack-update.js +158 -13
- package/dist/bin/update.js +2 -2
- package/dist/gateway/index.js +210 -2
- package/dist/hooks/bug-report-worker.js +321 -44
- package/dist/hooks/codex-stop-task-finalizer.js +210 -2
- package/dist/hooks/commit-complete.js +210 -2
- package/dist/hooks/error-recall.js +234 -3
- package/dist/hooks/exe-heartbeat-hook.js +2 -2
- package/dist/hooks/ingest-worker.js +2 -2
- package/dist/hooks/ingest.js +210 -2
- package/dist/hooks/instructions-loaded.js +210 -2
- package/dist/hooks/notification.js +210 -2
- package/dist/hooks/post-compact.js +210 -2
- package/dist/hooks/post-tool-combined.js +234 -3
- package/dist/hooks/pre-compact.js +446 -111
- package/dist/hooks/pre-tool-use.js +210 -2
- package/dist/hooks/prompt-submit.js +234 -3
- package/dist/hooks/session-end.js +347 -124
- package/dist/hooks/session-start.js +236 -5
- package/dist/hooks/stop.js +210 -2
- package/dist/hooks/subagent-stop.js +210 -2
- package/dist/hooks/summary-worker.js +171 -74
- package/dist/index.js +210 -2
- package/dist/lib/agent-config.js +2 -2
- package/dist/lib/cloud-sync.js +47 -2
- package/dist/lib/config.js +2 -2
- package/dist/lib/consolidation.js +2 -2
- package/dist/lib/database.js +47 -2
- package/dist/lib/db-daemon-client.js +2 -2
- package/dist/lib/db.js +47 -2
- package/dist/lib/device-registry.js +47 -2
- package/dist/lib/embedder.js +2 -2
- package/dist/lib/employee-templates.js +2 -2
- package/dist/lib/employees.js +2 -2
- package/dist/lib/exe-daemon-client.js +2 -2
- package/dist/lib/exe-daemon.js +592 -10
- package/dist/lib/hybrid-search.js +234 -3
- package/dist/lib/identity.js +2 -2
- package/dist/lib/license.js +2 -2
- package/dist/lib/messaging.js +2 -2
- package/dist/lib/reminders.js +2 -2
- package/dist/lib/schedules.js +47 -2
- package/dist/lib/skill-learning.js +2 -2
- package/dist/lib/store.js +210 -2
- package/dist/lib/task-router.js +2 -2
- package/dist/lib/tasks.js +2 -2
- package/dist/lib/tmux-routing.js +2 -2
- package/dist/lib/token-spend.js +2 -2
- package/dist/mcp/server.js +608 -10
- package/dist/mcp/tools/complete-reminder.js +2 -2
- package/dist/mcp/tools/create-reminder.js +2 -2
- package/dist/mcp/tools/create-task.js +2 -2
- package/dist/mcp/tools/deactivate-behavior.js +2 -2
- package/dist/mcp/tools/list-reminders.js +2 -2
- package/dist/mcp/tools/list-tasks.js +2 -2
- package/dist/mcp/tools/send-message.js +2 -2
- package/dist/mcp/tools/update-task.js +2 -2
- package/dist/runtime/index.js +210 -2
- package/dist/tui/App.js +210 -2
- package/package.json +3 -2
- package/stack.release.json +23 -7
|
@@ -256,8 +256,8 @@ var init_config = __esm({
|
|
|
256
256
|
rerankerAutoTrigger: {
|
|
257
257
|
enabled: true,
|
|
258
258
|
broadQueryMinCardinality: 5e4,
|
|
259
|
-
fetchTopK:
|
|
260
|
-
returnTopK:
|
|
259
|
+
fetchTopK: 200,
|
|
260
|
+
returnTopK: 20
|
|
261
261
|
}
|
|
262
262
|
},
|
|
263
263
|
graphRagEnabled: true,
|
|
@@ -2506,6 +2506,51 @@ async function ensureSchema() {
|
|
|
2506
2506
|
VALUES (new.rowid, new.content_text, new.sender_name, new.agent_response);
|
|
2507
2507
|
END;
|
|
2508
2508
|
`);
|
|
2509
|
+
await client.executeMultiple(`
|
|
2510
|
+
CREATE TABLE IF NOT EXISTS memory_cards (
|
|
2511
|
+
id TEXT PRIMARY KEY,
|
|
2512
|
+
memory_id TEXT NOT NULL,
|
|
2513
|
+
agent_id TEXT NOT NULL,
|
|
2514
|
+
session_id TEXT NOT NULL,
|
|
2515
|
+
project_name TEXT,
|
|
2516
|
+
timestamp TEXT NOT NULL,
|
|
2517
|
+
card_type TEXT NOT NULL,
|
|
2518
|
+
subject TEXT,
|
|
2519
|
+
predicate TEXT,
|
|
2520
|
+
object TEXT,
|
|
2521
|
+
content TEXT NOT NULL,
|
|
2522
|
+
source_ref TEXT,
|
|
2523
|
+
confidence REAL DEFAULT 0.6,
|
|
2524
|
+
active INTEGER DEFAULT 1,
|
|
2525
|
+
created_at TEXT NOT NULL
|
|
2526
|
+
);
|
|
2527
|
+
|
|
2528
|
+
CREATE INDEX IF NOT EXISTS idx_memory_cards_agent
|
|
2529
|
+
ON memory_cards(agent_id, active, timestamp);
|
|
2530
|
+
|
|
2531
|
+
CREATE INDEX IF NOT EXISTS idx_memory_cards_memory
|
|
2532
|
+
ON memory_cards(memory_id);
|
|
2533
|
+
|
|
2534
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memory_cards_fts
|
|
2535
|
+
USING fts5(content, subject, predicate, object, content='memory_cards', content_rowid='rowid');
|
|
2536
|
+
|
|
2537
|
+
CREATE TRIGGER IF NOT EXISTS memory_cards_fts_ai AFTER INSERT ON memory_cards BEGIN
|
|
2538
|
+
INSERT INTO memory_cards_fts(rowid, content, subject, predicate, object)
|
|
2539
|
+
VALUES (new.rowid, new.content, new.subject, new.predicate, new.object);
|
|
2540
|
+
END;
|
|
2541
|
+
|
|
2542
|
+
CREATE TRIGGER IF NOT EXISTS memory_cards_fts_ad AFTER DELETE ON memory_cards BEGIN
|
|
2543
|
+
INSERT INTO memory_cards_fts(memory_cards_fts, rowid, content, subject, predicate, object)
|
|
2544
|
+
VALUES('delete', old.rowid, old.content, old.subject, old.predicate, old.object);
|
|
2545
|
+
END;
|
|
2546
|
+
|
|
2547
|
+
CREATE TRIGGER IF NOT EXISTS memory_cards_fts_au AFTER UPDATE ON memory_cards BEGIN
|
|
2548
|
+
INSERT INTO memory_cards_fts(memory_cards_fts, rowid, content, subject, predicate, object)
|
|
2549
|
+
VALUES('delete', old.rowid, old.content, old.subject, old.predicate, old.object);
|
|
2550
|
+
INSERT INTO memory_cards_fts(rowid, content, subject, predicate, object)
|
|
2551
|
+
VALUES (new.rowid, new.content, new.subject, new.predicate, new.object);
|
|
2552
|
+
END;
|
|
2553
|
+
`);
|
|
2509
2554
|
try {
|
|
2510
2555
|
await client.execute({
|
|
2511
2556
|
sql: `ALTER TABLE memories ADD COLUMN tier INTEGER DEFAULT 3`,
|
|
@@ -3323,6 +3368,164 @@ ${p.content}`).join("\n\n");
|
|
|
3323
3368
|
}
|
|
3324
3369
|
});
|
|
3325
3370
|
|
|
3371
|
+
// src/lib/memory-cards.ts
|
|
3372
|
+
var memory_cards_exports = {};
|
|
3373
|
+
__export(memory_cards_exports, {
|
|
3374
|
+
extractMemoryCards: () => extractMemoryCards,
|
|
3375
|
+
insertMemoryCardsForBatch: () => insertMemoryCardsForBatch,
|
|
3376
|
+
searchMemoryCards: () => searchMemoryCards
|
|
3377
|
+
});
|
|
3378
|
+
import { createHash as createHash2 } from "crypto";
|
|
3379
|
+
function stableId(memoryId, type, content) {
|
|
3380
|
+
return createHash2("sha256").update(`${memoryId}:${type}:${content}`).digest("hex").slice(0, 32);
|
|
3381
|
+
}
|
|
3382
|
+
function cleanText(text) {
|
|
3383
|
+
return text.replace(/```[\s\S]*?```/g, " ").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
3384
|
+
}
|
|
3385
|
+
function splitSentences(text) {
|
|
3386
|
+
return cleanText(text).split(/(?<=[.!?])\s+|\n+/).map((s) => s.trim()).filter((s) => s.length >= 24 && s.length <= MAX_SENTENCE_CHARS);
|
|
3387
|
+
}
|
|
3388
|
+
function inferCardType(sentence, toolName) {
|
|
3389
|
+
const lower = sentence.toLowerCase();
|
|
3390
|
+
if (toolName === "store_decision" || /\b(decided|decision|adr|approved|rejected)\b/.test(lower)) return "decision";
|
|
3391
|
+
if (/\b(prefers|preference|likes|dislikes|wants|doesn't want|does not want)\b/.test(lower)) return "preference";
|
|
3392
|
+
if (/\b(changed|updated|replaced|now|no longer|instead|supersedes)\b/.test(lower)) return "belief_update";
|
|
3393
|
+
if (toolName && ["Read", "Write", "Edit", "Bash"].includes(toolName)) return "code";
|
|
3394
|
+
if (/\b(meeting|deadline|shipped|launched|completed|failed|blocked|assigned|created)\b/.test(lower)) return "event";
|
|
3395
|
+
return "fact";
|
|
3396
|
+
}
|
|
3397
|
+
function extractSubject(sentence, agentId) {
|
|
3398
|
+
const explicit = sentence.match(/\b([A-Z][a-zA-Z0-9_-]{2,}(?:\s+[A-Z][a-zA-Z0-9_-]{2,})?)\b/);
|
|
3399
|
+
return explicit?.[1] ?? agentId;
|
|
3400
|
+
}
|
|
3401
|
+
function predicateFor(type) {
|
|
3402
|
+
switch (type) {
|
|
3403
|
+
case "preference":
|
|
3404
|
+
return "prefers";
|
|
3405
|
+
case "belief_update":
|
|
3406
|
+
return "updated";
|
|
3407
|
+
case "decision":
|
|
3408
|
+
return "decided";
|
|
3409
|
+
case "event":
|
|
3410
|
+
return "happened";
|
|
3411
|
+
case "code":
|
|
3412
|
+
return "implemented";
|
|
3413
|
+
default:
|
|
3414
|
+
return "states";
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
function extractMemoryCards(row) {
|
|
3418
|
+
const sentences = splitSentences(row.raw_text);
|
|
3419
|
+
const cards = [];
|
|
3420
|
+
for (const sentence of sentences) {
|
|
3421
|
+
const type = inferCardType(sentence, row.tool_name);
|
|
3422
|
+
const subject = extractSubject(sentence, row.agent_id);
|
|
3423
|
+
const content = sentence.length > MAX_SENTENCE_CHARS ? `${sentence.slice(0, MAX_SENTENCE_CHARS - 1)}\u2026` : sentence;
|
|
3424
|
+
cards.push({
|
|
3425
|
+
id: stableId(row.id, type, content),
|
|
3426
|
+
memory_id: row.id,
|
|
3427
|
+
agent_id: row.agent_id,
|
|
3428
|
+
session_id: row.session_id,
|
|
3429
|
+
project_name: row.project_name ?? null,
|
|
3430
|
+
timestamp: row.timestamp,
|
|
3431
|
+
card_type: type,
|
|
3432
|
+
subject,
|
|
3433
|
+
predicate: predicateFor(type),
|
|
3434
|
+
object: content,
|
|
3435
|
+
content,
|
|
3436
|
+
source_ref: row.id,
|
|
3437
|
+
confidence: type === "fact" ? 0.55 : 0.65
|
|
3438
|
+
});
|
|
3439
|
+
if (cards.length >= MAX_CARDS_PER_MEMORY) break;
|
|
3440
|
+
}
|
|
3441
|
+
return cards;
|
|
3442
|
+
}
|
|
3443
|
+
async function insertMemoryCardsForBatch(rows) {
|
|
3444
|
+
const cards = rows.flatMap(extractMemoryCards);
|
|
3445
|
+
if (cards.length === 0) return 0;
|
|
3446
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3447
|
+
const client = getClient();
|
|
3448
|
+
const stmts = cards.map((card) => ({
|
|
3449
|
+
sql: `INSERT OR IGNORE INTO memory_cards
|
|
3450
|
+
(id, memory_id, agent_id, session_id, project_name, timestamp, card_type,
|
|
3451
|
+
subject, predicate, object, content, source_ref, confidence, active, created_at)
|
|
3452
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)`,
|
|
3453
|
+
args: [
|
|
3454
|
+
card.id,
|
|
3455
|
+
card.memory_id,
|
|
3456
|
+
card.agent_id,
|
|
3457
|
+
card.session_id,
|
|
3458
|
+
card.project_name,
|
|
3459
|
+
card.timestamp,
|
|
3460
|
+
card.card_type,
|
|
3461
|
+
card.subject,
|
|
3462
|
+
card.predicate,
|
|
3463
|
+
card.object,
|
|
3464
|
+
card.content,
|
|
3465
|
+
card.source_ref,
|
|
3466
|
+
card.confidence,
|
|
3467
|
+
now
|
|
3468
|
+
]
|
|
3469
|
+
}));
|
|
3470
|
+
await client.batch(stmts, "write");
|
|
3471
|
+
return cards.length;
|
|
3472
|
+
}
|
|
3473
|
+
function buildMatchExpr(queryText) {
|
|
3474
|
+
const terms = queryText.toLowerCase().split(/\s+/).filter((t) => t.length >= 3).map((t) => t.replace(/[^a-z0-9_]/g, "")).filter((t) => t.length >= 3).slice(0, 12);
|
|
3475
|
+
if (terms.length === 0) return null;
|
|
3476
|
+
return terms.map((t) => `${t}*`).join(terms.length >= 3 ? " AND " : " OR ");
|
|
3477
|
+
}
|
|
3478
|
+
async function searchMemoryCards(queryText, agentId, options) {
|
|
3479
|
+
const limit = options?.limit ?? 10;
|
|
3480
|
+
const matchExpr = buildMatchExpr(queryText);
|
|
3481
|
+
if (!matchExpr) return [];
|
|
3482
|
+
let sql = `SELECT c.id, c.memory_id, c.agent_id, c.session_id, c.project_name,
|
|
3483
|
+
c.timestamp, c.card_type, c.content, c.source_ref, c.confidence
|
|
3484
|
+
FROM memory_cards c
|
|
3485
|
+
JOIN memory_cards_fts fts ON c.rowid = fts.rowid
|
|
3486
|
+
WHERE memory_cards_fts MATCH ?
|
|
3487
|
+
AND c.agent_id = ?
|
|
3488
|
+
AND COALESCE(c.active, 1) = 1`;
|
|
3489
|
+
const args = [matchExpr, agentId];
|
|
3490
|
+
if (options?.projectName) {
|
|
3491
|
+
sql += ` AND c.project_name = ?`;
|
|
3492
|
+
args.push(options.projectName);
|
|
3493
|
+
}
|
|
3494
|
+
if (options?.since) {
|
|
3495
|
+
sql += ` AND c.timestamp >= ?`;
|
|
3496
|
+
args.push(options.since);
|
|
3497
|
+
}
|
|
3498
|
+
sql += ` ORDER BY rank LIMIT ?`;
|
|
3499
|
+
args.push(limit);
|
|
3500
|
+
const result = await getClient().execute({ sql, args });
|
|
3501
|
+
return result.rows.map((row) => ({
|
|
3502
|
+
id: `card:${String(row.id)}`,
|
|
3503
|
+
agent_id: String(row.agent_id),
|
|
3504
|
+
agent_role: "memory_card",
|
|
3505
|
+
session_id: String(row.session_id),
|
|
3506
|
+
timestamp: String(row.timestamp),
|
|
3507
|
+
tool_name: `memory_card:${String(row.card_type)}`,
|
|
3508
|
+
project_name: row.project_name == null ? "" : String(row.project_name),
|
|
3509
|
+
has_error: false,
|
|
3510
|
+
raw_text: `[${String(row.card_type)}] ${String(row.content)}
|
|
3511
|
+
Source memory: ${String(row.source_ref ?? row.memory_id)}`,
|
|
3512
|
+
vector: [],
|
|
3513
|
+
importance: 6,
|
|
3514
|
+
status: "active",
|
|
3515
|
+
confidence: Number(row.confidence ?? 0.6),
|
|
3516
|
+
last_accessed: String(row.timestamp)
|
|
3517
|
+
}));
|
|
3518
|
+
}
|
|
3519
|
+
var MAX_CARDS_PER_MEMORY, MAX_SENTENCE_CHARS;
|
|
3520
|
+
var init_memory_cards = __esm({
|
|
3521
|
+
"src/lib/memory-cards.ts"() {
|
|
3522
|
+
"use strict";
|
|
3523
|
+
init_database();
|
|
3524
|
+
MAX_CARDS_PER_MEMORY = 6;
|
|
3525
|
+
MAX_SENTENCE_CHARS = 360;
|
|
3526
|
+
}
|
|
3527
|
+
});
|
|
3528
|
+
|
|
3326
3529
|
// src/lib/store.ts
|
|
3327
3530
|
init_memory();
|
|
3328
3531
|
init_database();
|
|
@@ -3877,6 +4080,11 @@ async function flushBatch() {
|
|
|
3877
4080
|
const globalClient = getClient();
|
|
3878
4081
|
const globalStmts = batch.map(buildStmt);
|
|
3879
4082
|
await globalClient.batch(globalStmts, "write");
|
|
4083
|
+
try {
|
|
4084
|
+
const { insertMemoryCardsForBatch: insertMemoryCardsForBatch2 } = await Promise.resolve().then(() => (init_memory_cards(), memory_cards_exports));
|
|
4085
|
+
await insertMemoryCardsForBatch2(batch);
|
|
4086
|
+
} catch {
|
|
4087
|
+
}
|
|
3880
4088
|
schedulePostWriteMemoryHygiene(batch.map((row) => row.id));
|
|
3881
4089
|
_pendingRecords.splice(0, batch.length);
|
|
3882
4090
|
try {
|
package/dist/bin/stack-update.js
CHANGED
|
@@ -20,12 +20,64 @@ function isMainModule(importMetaUrl) {
|
|
|
20
20
|
|
|
21
21
|
// src/lib/stack-update.ts
|
|
22
22
|
import { execFileSync } from "child_process";
|
|
23
|
-
import {
|
|
23
|
+
import { createVerify, verify as verifySignature } from "crypto";
|
|
24
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
24
25
|
import http from "http";
|
|
25
26
|
import https from "https";
|
|
26
27
|
import path from "path";
|
|
27
|
-
function
|
|
28
|
-
|
|
28
|
+
function isSignedEnvelope(value) {
|
|
29
|
+
return !!value && typeof value === "object" && "manifest" in value && "signature" in value;
|
|
30
|
+
}
|
|
31
|
+
function canonicalizeStackManifest(manifest) {
|
|
32
|
+
const clone = JSON.parse(JSON.stringify(manifest));
|
|
33
|
+
delete clone.signature;
|
|
34
|
+
return stableJson(clone);
|
|
35
|
+
}
|
|
36
|
+
function verifyStackManifestSignature(manifest, publicKeyPem) {
|
|
37
|
+
const signature = manifest.signature;
|
|
38
|
+
if (!signature) throw new Error("Stack manifest signature required but missing");
|
|
39
|
+
const payload = Buffer.from(canonicalizeStackManifest(manifest));
|
|
40
|
+
const sig = Buffer.from(signature.signature, "base64");
|
|
41
|
+
let ok = false;
|
|
42
|
+
if (signature.alg === "ed25519") {
|
|
43
|
+
ok = verifySignature(null, payload, publicKeyPem, sig);
|
|
44
|
+
} else if (signature.alg === "rsa-sha256") {
|
|
45
|
+
const verifier = createVerify("RSA-SHA256");
|
|
46
|
+
verifier.update(payload);
|
|
47
|
+
verifier.end();
|
|
48
|
+
ok = verifier.verify(publicKeyPem, sig);
|
|
49
|
+
} else {
|
|
50
|
+
throw new Error(`Unsupported stack manifest signature alg: ${signature.alg}`);
|
|
51
|
+
}
|
|
52
|
+
if (!ok) throw new Error("Stack manifest signature verification failed");
|
|
53
|
+
}
|
|
54
|
+
function stableJson(value) {
|
|
55
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
56
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
57
|
+
const obj = value;
|
|
58
|
+
return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${stableJson(obj[key])}`).join(",")}}`;
|
|
59
|
+
}
|
|
60
|
+
function findLatestBackupEnvFile(envFile) {
|
|
61
|
+
const backupDir = path.join(path.dirname(envFile), ".exe-stack-backups");
|
|
62
|
+
if (!existsSync(backupDir)) return null;
|
|
63
|
+
const backups = readdirSync(backupDir).filter((name) => name.startsWith("env-") && name.endsWith(".bak")).sort();
|
|
64
|
+
const latest = backups.at(-1);
|
|
65
|
+
return latest ? path.join(backupDir, latest) : null;
|
|
66
|
+
}
|
|
67
|
+
async function rollbackStackUpdate(options) {
|
|
68
|
+
const exec = options.exec ?? defaultExec;
|
|
69
|
+
const backupEnvFile = options.lockFile && existsSync(options.lockFile) ? JSON.parse(readFileSync(options.lockFile, "utf8")).backupEnvFile : void 0;
|
|
70
|
+
const rollbackEnv = backupEnvFile && existsSync(backupEnvFile) ? backupEnvFile : findLatestBackupEnvFile(options.envFile);
|
|
71
|
+
if (!rollbackEnv) throw new Error(`No stack backup env found beside ${options.envFile}`);
|
|
72
|
+
writeFileSync(options.envFile, readFileSync(rollbackEnv), { mode: 384 });
|
|
73
|
+
const composeArgs = ["compose", "--file", options.composeFile, "--env-file", options.envFile];
|
|
74
|
+
exec("docker", [...composeArgs, "up", "-d"]);
|
|
75
|
+
return { status: "rolled_back", targetVersion: "previous", changes: [], backupEnvFile: rollbackEnv, lockFile: options.lockFile ?? path.join(path.dirname(options.envFile), ".exe-stack-lock.json") };
|
|
76
|
+
}
|
|
77
|
+
function parseStackManifest(raw, publicKey) {
|
|
78
|
+
const parsedRaw = JSON.parse(raw);
|
|
79
|
+
const parsed = isSignedEnvelope(parsedRaw) ? { ...parsedRaw.manifest, signature: parsedRaw.signature } : parsedRaw;
|
|
80
|
+
if (publicKey) verifyStackManifestSignature(parsed, publicKey);
|
|
29
81
|
if (parsed.schemaVersion !== 1) throw new Error("Unsupported stack manifest schemaVersion");
|
|
30
82
|
if (!parsed.latest || !parsed.stacks || typeof parsed.stacks !== "object") {
|
|
31
83
|
throw new Error("Invalid stack manifest: latest and stacks are required");
|
|
@@ -43,9 +95,13 @@ function parseStackManifest(raw) {
|
|
|
43
95
|
}
|
|
44
96
|
return parsed;
|
|
45
97
|
}
|
|
46
|
-
async function loadStackManifest(ref, fetchText = defaultFetchText) {
|
|
47
|
-
if (/^https?:\/\//.test(ref)) return parseStackManifest(await
|
|
48
|
-
return parseStackManifest(readFileSync(ref, "utf8"));
|
|
98
|
+
async function loadStackManifest(ref, fetchText = defaultFetchText, publicKey, authToken) {
|
|
99
|
+
if (/^https?:\/\//.test(ref)) return parseStackManifest(await fetchTextWithAuth(ref, fetchText, authToken), publicKey);
|
|
100
|
+
return parseStackManifest(readFileSync(ref, "utf8"), publicKey);
|
|
101
|
+
}
|
|
102
|
+
async function fetchTextWithAuth(ref, fetchText, authToken) {
|
|
103
|
+
if (!authToken || fetchText !== defaultFetchText) return fetchText(ref);
|
|
104
|
+
return defaultFetchText(ref, authToken);
|
|
49
105
|
}
|
|
50
106
|
function parseEnv(raw) {
|
|
51
107
|
const env = /* @__PURE__ */ new Map();
|
|
@@ -111,14 +167,17 @@ Re-run with --allow-breaking ${missing.map((c) => c.id).join(",")}`
|
|
|
111
167
|
async function runStackUpdate(options) {
|
|
112
168
|
const exec = options.exec ?? defaultExec;
|
|
113
169
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
114
|
-
|
|
170
|
+
if (options.rollback) return rollbackStackUpdate(options);
|
|
171
|
+
const manifest = await loadStackManifest(options.manifestRef, options.fetchText, options.manifestPublicKey, options.manifestAuthToken);
|
|
115
172
|
const envRaw = readFileSync(options.envFile, "utf8");
|
|
116
173
|
const plan = createStackUpdatePlan(manifest, envRaw, options.targetVersion);
|
|
117
174
|
assertBreakingChangesAllowed(plan, options.allowedBreakingChangeIds ?? []);
|
|
118
175
|
const lockFile = options.lockFile ?? path.join(path.dirname(options.envFile), ".exe-stack-lock.json");
|
|
176
|
+
const previousVersion = readCurrentStackVersion(lockFile);
|
|
119
177
|
if (options.dryRun || plan.changes.length === 0) {
|
|
120
178
|
return { status: "planned", targetVersion: plan.targetVersion, changes: plan.changes, lockFile };
|
|
121
179
|
}
|
|
180
|
+
await postDeployAudit(options, "started", plan.targetVersion, previousVersion);
|
|
122
181
|
const backupDir = path.join(path.dirname(options.envFile), ".exe-stack-backups");
|
|
123
182
|
mkdirSync(backupDir, { recursive: true });
|
|
124
183
|
const stamp = now().toISOString().replace(/[:.]/g, "-");
|
|
@@ -134,7 +193,8 @@ async function runStackUpdate(options) {
|
|
|
134
193
|
exec("docker", [...composeArgs, "pull"]);
|
|
135
194
|
exec("docker", [...composeArgs, "up", "-d"]);
|
|
136
195
|
await verifyReleaseHealth(plan.release, options.healthRetries ?? 12, options.healthDelayMs ?? 5e3);
|
|
137
|
-
writeFileSync(lockFile, JSON.stringify({ stackVersion: plan.targetVersion, updatedAt: now().toISOString(), services: plan.release.services }, null, 2) + "\n");
|
|
196
|
+
writeFileSync(lockFile, JSON.stringify({ stackVersion: plan.targetVersion, updatedAt: now().toISOString(), backupEnvFile, services: plan.release.services }, null, 2) + "\n");
|
|
197
|
+
await postDeployAudit(options, "success", plan.targetVersion, previousVersion, void 0, { changes: plan.changes.length });
|
|
138
198
|
return { status: "updated", targetVersion: plan.targetVersion, changes: plan.changes, backupEnvFile, lockFile };
|
|
139
199
|
} catch (err) {
|
|
140
200
|
writeFileSync(options.envFile, envRaw, { mode: 384 });
|
|
@@ -143,9 +203,37 @@ async function runStackUpdate(options) {
|
|
|
143
203
|
} catch {
|
|
144
204
|
}
|
|
145
205
|
const reason = err instanceof Error ? err.message : String(err);
|
|
206
|
+
await postDeployAudit(options, "failed", plan.targetVersion, previousVersion, reason, { rollbackAttempted: true });
|
|
146
207
|
throw new Error(`Stack update failed and rollback was attempted: ${reason}`);
|
|
147
208
|
}
|
|
148
209
|
}
|
|
210
|
+
function readCurrentStackVersion(lockFile) {
|
|
211
|
+
if (!existsSync(lockFile)) return void 0;
|
|
212
|
+
try {
|
|
213
|
+
const parsed = JSON.parse(readFileSync(lockFile, "utf8"));
|
|
214
|
+
return parsed.stackVersion;
|
|
215
|
+
} catch {
|
|
216
|
+
return void 0;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async function postDeployAudit(options, status, stackVersion, previousVersion, error, metadata) {
|
|
220
|
+
if (!options.auditUrl) return;
|
|
221
|
+
const postJson = options.postJson ?? defaultPostJson;
|
|
222
|
+
try {
|
|
223
|
+
await postJson(options.auditUrl, {
|
|
224
|
+
stackVersion,
|
|
225
|
+
previousVersion,
|
|
226
|
+
status,
|
|
227
|
+
error,
|
|
228
|
+
metadata,
|
|
229
|
+
deviceId: options.deviceId,
|
|
230
|
+
licenseKey: options.licenseKey
|
|
231
|
+
}, options.manifestAuthToken);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
234
|
+
console.warn(`[stack-update] deploy audit failed: ${reason}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
149
237
|
async function verifyReleaseHealth(release, retries, delayMs) {
|
|
150
238
|
for (const [serviceName, service] of Object.entries(release.services)) {
|
|
151
239
|
if (!service.healthUrl) continue;
|
|
@@ -182,20 +270,43 @@ function httpStatus(urlString) {
|
|
|
182
270
|
function defaultExec(cmd, args, opts) {
|
|
183
271
|
execFileSync(cmd, args, { stdio: "inherit", cwd: opts?.cwd });
|
|
184
272
|
}
|
|
185
|
-
async function defaultFetchText(ref) {
|
|
186
|
-
const res = await fetch(ref
|
|
273
|
+
async function defaultFetchText(ref, authToken) {
|
|
274
|
+
const res = await fetch(ref, {
|
|
275
|
+
headers: authToken ? { authorization: `Bearer ${authToken}` } : void 0
|
|
276
|
+
});
|
|
187
277
|
if (!res.ok) throw new Error(`Failed to fetch ${ref}: HTTP ${res.status}`);
|
|
188
278
|
return res.text();
|
|
189
279
|
}
|
|
280
|
+
async function defaultPostJson(url, body, authToken) {
|
|
281
|
+
const res = await fetch(url, {
|
|
282
|
+
method: "POST",
|
|
283
|
+
headers: {
|
|
284
|
+
"content-type": "application/json",
|
|
285
|
+
...authToken ? { authorization: `Bearer ${authToken}` } : {}
|
|
286
|
+
},
|
|
287
|
+
body: JSON.stringify(body)
|
|
288
|
+
});
|
|
289
|
+
if (!res.ok) throw new Error(`Failed to POST ${url}: HTTP ${res.status}`);
|
|
290
|
+
}
|
|
190
291
|
function defaultStackPaths() {
|
|
191
292
|
const cwdCompose = path.resolve("docker-compose.yml");
|
|
192
293
|
const cwdEnv = path.resolve(".env");
|
|
193
294
|
return {
|
|
194
295
|
composeFile: process.env.EXE_STACK_COMPOSE_FILE || (existsSync(cwdCompose) ? cwdCompose : "/opt/exe-stack/docker-compose.yml"),
|
|
195
296
|
envFile: process.env.EXE_STACK_ENV_FILE || (existsSync(cwdEnv) ? cwdEnv : "/opt/exe-stack/.env"),
|
|
196
|
-
manifestRef: process.env.EXE_STACK_MANIFEST || "https://
|
|
297
|
+
manifestRef: process.env.EXE_STACK_MANIFEST || "https://update.askexe.com/stack-manifest.json",
|
|
298
|
+
auditUrl: process.env.EXE_STACK_AUDIT_URL || "https://update.askexe.com/v1/deploy-audits",
|
|
299
|
+
manifestAuthToken: process.env.EXE_STACK_UPDATE_TOKEN,
|
|
300
|
+
manifestPublicKey: loadDefaultPublicKey()
|
|
197
301
|
};
|
|
198
302
|
}
|
|
303
|
+
function loadDefaultPublicKey() {
|
|
304
|
+
if (process.env.EXE_STACK_PUBLIC_KEY) return process.env.EXE_STACK_PUBLIC_KEY;
|
|
305
|
+
if (process.env.EXE_STACK_PUBLIC_KEY_FILE && existsSync(process.env.EXE_STACK_PUBLIC_KEY_FILE)) {
|
|
306
|
+
return readFileSync(process.env.EXE_STACK_PUBLIC_KEY_FILE, "utf8");
|
|
307
|
+
}
|
|
308
|
+
return void 0;
|
|
309
|
+
}
|
|
199
310
|
|
|
200
311
|
// src/bin/stack-update.ts
|
|
201
312
|
function parseArgs(args) {
|
|
@@ -204,8 +315,14 @@ function parseArgs(args) {
|
|
|
204
315
|
manifestRef: defaults.manifestRef,
|
|
205
316
|
composeFile: defaults.composeFile,
|
|
206
317
|
envFile: defaults.envFile,
|
|
318
|
+
auditUrl: defaults.auditUrl,
|
|
319
|
+
manifestAuthToken: defaults.manifestAuthToken,
|
|
320
|
+
manifestPublicKey: defaults.manifestPublicKey,
|
|
321
|
+
deviceId: process.env.EXE_DEVICE_ID,
|
|
322
|
+
licenseKey: process.env.EXE_LICENSE_KEY,
|
|
207
323
|
dryRun: false,
|
|
208
324
|
check: false,
|
|
325
|
+
rollback: false,
|
|
209
326
|
yes: false,
|
|
210
327
|
allowedBreakingChangeIds: []
|
|
211
328
|
};
|
|
@@ -221,6 +338,18 @@ function parseArgs(args) {
|
|
|
221
338
|
else if (arg === "--env-file") opts.envFile = next();
|
|
222
339
|
else if (arg.startsWith("--env-file=")) opts.envFile = arg.split("=").slice(1).join("=");
|
|
223
340
|
else if (arg === "--lock-file") opts.lockFile = next();
|
|
341
|
+
else if (arg === "--public-key") opts.manifestPublicKey = readFileSync2(next(), "utf8");
|
|
342
|
+
else if (arg.startsWith("--public-key=")) opts.manifestPublicKey = readFileSync2(arg.split("=").slice(1).join("="), "utf8");
|
|
343
|
+
else if (arg === "--auth-token") opts.manifestAuthToken = next();
|
|
344
|
+
else if (arg.startsWith("--auth-token=")) opts.manifestAuthToken = arg.split("=").slice(1).join("=");
|
|
345
|
+
else if (arg === "--auth-token-env") opts.manifestAuthToken = process.env[next()] ?? "";
|
|
346
|
+
else if (arg === "--audit-url") opts.auditUrl = next();
|
|
347
|
+
else if (arg.startsWith("--audit-url=")) opts.auditUrl = arg.split("=").slice(1).join("=");
|
|
348
|
+
else if (arg === "--device-id") opts.deviceId = next();
|
|
349
|
+
else if (arg.startsWith("--device-id=")) opts.deviceId = arg.split("=").slice(1).join("=");
|
|
350
|
+
else if (arg === "--license-key") opts.licenseKey = next();
|
|
351
|
+
else if (arg.startsWith("--license-key=")) opts.licenseKey = arg.split("=").slice(1).join("=");
|
|
352
|
+
else if (arg === "--rollback") opts.rollback = true;
|
|
224
353
|
else if (arg === "--dry-run") opts.dryRun = true;
|
|
225
354
|
else if (arg === "--check") opts.check = true;
|
|
226
355
|
else if (arg === "--yes" || arg === "-y") opts.yes = true;
|
|
@@ -242,13 +371,20 @@ Usage:
|
|
|
242
371
|
exe-os stack-update [--manifest <path-or-url>] [--target <version>] [--yes]
|
|
243
372
|
|
|
244
373
|
Options:
|
|
245
|
-
--manifest <ref> Stack manifest JSON path or URL (default:
|
|
374
|
+
--manifest <ref> Stack manifest JSON path or URL (default: update.askexe.com)
|
|
246
375
|
--target <version> Stack version to install (default: manifest.latest)
|
|
247
376
|
--compose-file <path> docker-compose.yml path (default: ./docker-compose.yml or /opt/exe-stack/docker-compose.yml)
|
|
248
377
|
--env-file <path> .env path (default: ./.env or /opt/exe-stack/.env)
|
|
249
378
|
--lock-file <path> Lock file path (default: beside .env)
|
|
250
379
|
--check Print available changes only
|
|
251
380
|
--dry-run Plan only; do not run Docker
|
|
381
|
+
--public-key <path> PEM public key for signed manifest verification
|
|
382
|
+
--auth-token <token> Bearer token for private update manifest/audit API
|
|
383
|
+
--auth-token-env <name> Read bearer token from an environment variable
|
|
384
|
+
--audit-url <url> Deploy-audit endpoint (default: EXE_STACK_AUDIT_URL/update.askexe.com)
|
|
385
|
+
--device-id <id> Device ID to include in deploy audit
|
|
386
|
+
--license-key <key> License key to include in deploy audit
|
|
387
|
+
--rollback Restore latest backed-up .env and restart stack
|
|
252
388
|
--allow-breaking <ids> Confirm breaking changes, comma-separated
|
|
253
389
|
-y, --yes Non-interactive confirmation
|
|
254
390
|
`);
|
|
@@ -276,7 +412,16 @@ function printBreaking(changes) {
|
|
|
276
412
|
}
|
|
277
413
|
async function main() {
|
|
278
414
|
const opts = parseArgs(process.argv.slice(2));
|
|
279
|
-
|
|
415
|
+
if (opts.rollback) {
|
|
416
|
+
if (!opts.yes) {
|
|
417
|
+
console.error("Refusing to rollback without --yes.");
|
|
418
|
+
process.exit(2);
|
|
419
|
+
}
|
|
420
|
+
const result2 = await runStackUpdate(opts);
|
|
421
|
+
console.log(`\u2705 Stack rollback attempted using backup: ${result2.backupEnvFile ?? "latest backup"}`);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const manifest = await loadStackManifest(opts.manifestRef, void 0, opts.manifestPublicKey, opts.manifestAuthToken);
|
|
280
425
|
const envRaw = readFileSync2(opts.envFile, "utf8");
|
|
281
426
|
const plan = createStackUpdatePlan(manifest, envRaw, opts.targetVersion);
|
|
282
427
|
console.log(`Exe OS stack target: ${plan.targetVersion}`);
|