@llblab/pi-telegram 0.51.3 → 0.51.5
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/AGENTS.md +2 -2
- package/CHANGELOG.md +10 -0
- package/README.md +2 -2
- package/dist/lib/bus-follower.d.ts +2 -0
- package/dist/lib/bus-follower.js +55 -2
- package/dist/lib/bus-leader.d.ts +21 -0
- package/dist/lib/bus-leader.js +144 -1
- package/dist/lib/bus.d.ts +9 -0
- package/dist/lib/bus.js +19 -0
- package/dist/lib/commands.d.ts +13 -0
- package/dist/lib/commands.js +29 -6
- package/dist/lib/extension.js +16 -0
- package/dist/lib/locks.js +5 -1
- package/dist/lib/polling.js +6 -2
- package/dist/lib/skills.d.ts +7 -1
- package/dist/lib/skills.js +32 -7
- package/dist/lib/threads.d.ts +7 -0
- package/dist/lib/threads.js +13 -5
- package/dist/package.json +3 -8
- package/docs/multi-instance-bus.md +2 -2
- package/docs/public-api.md +1 -1
- package/lib/bus-follower.ts +79 -1
- package/lib/bus-leader.ts +197 -0
- package/lib/bus.ts +33 -0
- package/lib/commands.ts +38 -6
- package/lib/extension.ts +15 -0
- package/lib/locks.ts +6 -1
- package/lib/polling.ts +10 -2
- package/lib/skills.ts +43 -7
- package/lib/threads.ts +19 -5
- package/package.json +3 -8
- package/scripts/build-dist.mjs +103 -32
package/lib/commands.ts
CHANGED
|
@@ -2440,6 +2440,19 @@ export interface TelegramSessionActionAssemblyDeps {
|
|
|
2440
2440
|
};
|
|
2441
2441
|
getProfileName: () => string | undefined;
|
|
2442
2442
|
ownsPersistence: () => boolean;
|
|
2443
|
+
/**
|
|
2444
|
+
* Registered-follower port. A follower cannot persist leader-owned state, so
|
|
2445
|
+
* its Workspace Thread intent is published and claimed by the leader over
|
|
2446
|
+
* authenticated generation-fenced bus RPC.
|
|
2447
|
+
*/
|
|
2448
|
+
follower?: {
|
|
2449
|
+
instanceId: string;
|
|
2450
|
+
isRegisteredFor: (target: { chatId: number; threadId?: number }) => boolean;
|
|
2451
|
+
requestSessionReplacement: (
|
|
2452
|
+
operation: "publish" | "settle",
|
|
2453
|
+
intent: TelegramSessionReplacementIntent,
|
|
2454
|
+
) => Promise<boolean>;
|
|
2455
|
+
};
|
|
2443
2456
|
sendResult: (
|
|
2444
2457
|
target: { chatId: number; threadId?: number },
|
|
2445
2458
|
html: string,
|
|
@@ -2478,7 +2491,13 @@ export function createTelegramSessionActionAssembly(
|
|
|
2478
2491
|
sendUserMessage: deps.sendUserMessage,
|
|
2479
2492
|
notifyResult(target, result) { return sendTerminalResult(target, result); },
|
|
2480
2493
|
async prepareReplacement(ctx, updateId, target) {
|
|
2481
|
-
|
|
2494
|
+
const follower = !deps.ownsPersistence() && typeof target.threadId === "number" &&
|
|
2495
|
+
deps.follower?.isRegisteredFor(target)
|
|
2496
|
+
? deps.follower
|
|
2497
|
+
: undefined;
|
|
2498
|
+
// Follower memory is not authority; reread the leader-published snapshot.
|
|
2499
|
+
if (follower && deps.store.refresh) await deps.store.refresh();
|
|
2500
|
+
else await deps.store.load();
|
|
2482
2501
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
2483
2502
|
const binding = typeof target.threadId === "number"
|
|
2484
2503
|
? deps.store.getWorkspaceBindingByTarget(target)
|
|
@@ -2488,7 +2507,7 @@ export function createTelegramSessionActionAssembly(
|
|
|
2488
2507
|
throw new Error("Telegram session replacement binding is unavailable.");
|
|
2489
2508
|
}
|
|
2490
2509
|
const createdAtMs = now();
|
|
2491
|
-
|
|
2510
|
+
const intent: TelegramSessionReplacementIntent = {
|
|
2492
2511
|
continuity: binding ? "workspace-thread" : "classic-chat",
|
|
2493
2512
|
cwd: binding?.cwd ?? ctx.cwd,
|
|
2494
2513
|
profileName: deps.getProfileName() ?? "default",
|
|
@@ -2501,7 +2520,11 @@ export function createTelegramSessionActionAssembly(
|
|
|
2501
2520
|
? { threadName: binding.manualThreadName ?? binding.threadName } : {}),
|
|
2502
2521
|
createdAtMs,
|
|
2503
2522
|
expiresAtMs: createdAtMs + deps.handoffTtlMs,
|
|
2504
|
-
|
|
2523
|
+
...(follower ? { sourceInstanceId: follower.instanceId } : {}),
|
|
2524
|
+
};
|
|
2525
|
+
if (!await (follower
|
|
2526
|
+
? follower.requestSessionReplacement("publish", intent)
|
|
2527
|
+
: deps.store.commitSessionReplacementIntent(intent, deps.ownsPersistence))) {
|
|
2505
2528
|
throw new Error("Telegram session replacement intent was not persisted.");
|
|
2506
2529
|
}
|
|
2507
2530
|
},
|
|
@@ -2514,11 +2537,20 @@ export function createTelegramSessionActionAssembly(
|
|
|
2514
2537
|
return {
|
|
2515
2538
|
async getIntent() { await deps.store.refresh?.(); return deps.store.getSessionReplacementIntent(); },
|
|
2516
2539
|
hasSuccessorContinuity(intent) {
|
|
2517
|
-
|
|
2518
|
-
|
|
2540
|
+
if (intent.continuity === "classic-chat") return true;
|
|
2541
|
+
if (deps.store.getWorkspaceBindingByTarget(intent.target, sessionId)?.cwd !==
|
|
2542
|
+
intent.cwd) return false;
|
|
2543
|
+
// A follower successor claims only after its own re-registration is live.
|
|
2544
|
+
return intent.sourceInstanceId === undefined || deps.ownsPersistence() ||
|
|
2545
|
+
deps.follower?.isRegisteredFor(intent.target) === true;
|
|
2519
2546
|
},
|
|
2520
2547
|
editSuccess(intent) { return deps.sendResult(intent.target, "<b>🆕 New session started.</b>"); },
|
|
2521
|
-
clearIntent(intent) {
|
|
2548
|
+
async clearIntent(intent) {
|
|
2549
|
+
if (intent.sourceInstanceId === undefined || deps.ownsPersistence()) {
|
|
2550
|
+
return deps.store.removeSessionReplacementIntent(intent, deps.ownsPersistence);
|
|
2551
|
+
}
|
|
2552
|
+
return await deps.follower?.requestSessionReplacement("settle", intent) ?? false;
|
|
2553
|
+
},
|
|
2522
2554
|
profileName: deps.getProfileName() ?? "default",
|
|
2523
2555
|
cwd: ctx.cwd,
|
|
2524
2556
|
sessionId,
|
package/lib/extension.ts
CHANGED
|
@@ -65,6 +65,7 @@ const telegramBusProtocolIdentity =
|
|
|
65
65
|
Bus.TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
66
66
|
Bus.TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
|
|
67
67
|
Bus.TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
|
|
68
|
+
Bus.TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
|
|
68
69
|
]);
|
|
69
70
|
|
|
70
71
|
// --- Extension Runtime ---
|
|
@@ -244,6 +245,20 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
244
245
|
store: threadStore,
|
|
245
246
|
getProfileName: configStore.getActiveProfileName,
|
|
246
247
|
ownsPersistence: lockRuntime.owns,
|
|
248
|
+
follower: {
|
|
249
|
+
instanceId: telegramInstanceId,
|
|
250
|
+
isRegisteredFor(target) {
|
|
251
|
+
const registered = telegramBusFollowerRegistrationState.getTarget();
|
|
252
|
+
return telegramBusFollowerRegistrationState.isRegistered() &&
|
|
253
|
+
registered?.chatId === target.chatId &&
|
|
254
|
+
registered.threadId === target.threadId;
|
|
255
|
+
},
|
|
256
|
+
async requestSessionReplacement(operation, intent) {
|
|
257
|
+
const request = telegramBusFollowerRegistration.requestSessionReplacement;
|
|
258
|
+
if (!request) return false;
|
|
259
|
+
return await request(operation, intent);
|
|
260
|
+
},
|
|
261
|
+
},
|
|
247
262
|
async sendResult(target, html) {
|
|
248
263
|
const delivery = await Delivery.sendTelegramView(
|
|
249
264
|
{ text: html, parseMode: "html", replyMarkup: { inline_keyboard: [] } },
|
package/lib/locks.ts
CHANGED
|
@@ -1515,7 +1515,12 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1515
1515
|
ctx,
|
|
1516
1516
|
state.lock,
|
|
1517
1517
|
);
|
|
1518
|
-
if (!isCurrent()
|
|
1518
|
+
if (!isCurrent()) return;
|
|
1519
|
+
if (!restored) {
|
|
1520
|
+
deps.recordRuntimeEvent?.("lock", "Telegram follower auto-connect did not restore this session.",
|
|
1521
|
+
{ phase: "follower-auto-connect-unavailable" });
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1519
1524
|
deps.onTransportAvailabilityChanged?.();
|
|
1520
1525
|
deps.updateStatus(ctx);
|
|
1521
1526
|
deps.recordRuntimeEvent?.(
|
package/lib/polling.ts
CHANGED
|
@@ -1180,8 +1180,16 @@ export function createTelegramThreadAwarePollingPorts<TContext, TOwner>(
|
|
|
1180
1180
|
ctx: TContext,
|
|
1181
1181
|
owner: TOwner,
|
|
1182
1182
|
): Promise<boolean | undefined> => {
|
|
1183
|
-
if (!(await refreshFollowerState()))
|
|
1184
|
-
|
|
1183
|
+
if (!(await refreshFollowerState())) {
|
|
1184
|
+
deps.recordEvent("bus", "Telegram follower auto-connect skipped: Threaded Mode is not enabled.",
|
|
1185
|
+
{ phase: "follower-auto-connect-skip", reason: "thread-mode-unavailable" });
|
|
1186
|
+
return undefined;
|
|
1187
|
+
}
|
|
1188
|
+
if (!deps.hasRememberedWorkspaceBinding?.(ctx)) {
|
|
1189
|
+
deps.recordEvent("bus", "Telegram follower auto-connect skipped: no binding for this session.",
|
|
1190
|
+
{ phase: "follower-auto-connect-skip", reason: "session-binding-unavailable" });
|
|
1191
|
+
return undefined;
|
|
1192
|
+
}
|
|
1185
1193
|
return deps.restoreFollowerWithLeader?.(ctx, owner);
|
|
1186
1194
|
};
|
|
1187
1195
|
return {
|
package/lib/skills.ts
CHANGED
|
@@ -4,26 +4,62 @@
|
|
|
4
4
|
* Owns source-checkout skill contribution; installed packages use their manifest
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
8
9
|
import { fileURLToPath } from "node:url";
|
|
9
10
|
|
|
10
11
|
import type { ExtensionAPI } from "./pi.ts";
|
|
12
|
+
import { resolveAgentDir } from "./paths.ts";
|
|
11
13
|
|
|
12
14
|
const TELEGRAM_SKILLS_MODULE_PATH = fileURLToPath(import.meta.url);
|
|
13
15
|
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
+
export function getTelegramExtensionPackageRoot(modulePath: string): string {
|
|
17
|
+
let current = dirname(modulePath);
|
|
18
|
+
while (true) {
|
|
19
|
+
if (existsSync(join(current, "package.json"))) {
|
|
20
|
+
const parent = dirname(current);
|
|
21
|
+
if (basename(current) === "dist" && existsSync(join(parent, "package.json"))) {
|
|
22
|
+
return parent;
|
|
23
|
+
}
|
|
24
|
+
return current;
|
|
25
|
+
}
|
|
26
|
+
const parent = dirname(current);
|
|
27
|
+
if (parent === current) return dirname(modulePath);
|
|
28
|
+
current = parent;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface TelegramRawExtensionCheckoutOptions {
|
|
33
|
+
agentDir?: string;
|
|
34
|
+
cwd?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function isRawTelegramExtensionCheckout(
|
|
38
|
+
modulePath: string,
|
|
39
|
+
options: TelegramRawExtensionCheckoutOptions = {},
|
|
40
|
+
): boolean {
|
|
41
|
+
const packageRoot = resolve(getTelegramExtensionPackageRoot(modulePath));
|
|
42
|
+
const agentDir = resolve(options.agentDir ?? resolveAgentDir());
|
|
43
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
44
|
+
return dirname(packageRoot) === join(agentDir, "extensions") ||
|
|
45
|
+
dirname(packageRoot) === join(cwd, ".pi", "extensions");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const TELEGRAM_SKILLS_PATH = join(
|
|
49
|
+
getTelegramExtensionPackageRoot(TELEGRAM_SKILLS_MODULE_PATH),
|
|
50
|
+
"skills",
|
|
16
51
|
);
|
|
17
52
|
|
|
18
53
|
export function registerTelegramSkillDiscovery(
|
|
19
54
|
pi: Pick<ExtensionAPI, "on">,
|
|
20
55
|
modulePath = TELEGRAM_SKILLS_MODULE_PATH,
|
|
56
|
+
options: TelegramRawExtensionCheckoutOptions = {},
|
|
21
57
|
): boolean {
|
|
22
|
-
//
|
|
23
|
-
// packages
|
|
24
|
-
if (
|
|
58
|
+
// Pi auto-discovers extension entrypoints but not their manifest Skills.
|
|
59
|
+
// Manifest-loaded packages own Skills and filters through `pi.skills`.
|
|
60
|
+
if (!isRawTelegramExtensionCheckout(modulePath, options)) return false;
|
|
25
61
|
pi.on("resources_discover", () => ({
|
|
26
|
-
skillPaths: [
|
|
62
|
+
skillPaths: [join(getTelegramExtensionPackageRoot(modulePath), "skills")],
|
|
27
63
|
}));
|
|
28
64
|
return true;
|
|
29
65
|
}
|
package/lib/threads.ts
CHANGED
|
@@ -371,6 +371,12 @@ export interface TelegramSessionReplacementIntent {
|
|
|
371
371
|
threadName?: string;
|
|
372
372
|
createdAtMs: number;
|
|
373
373
|
expiresAtMs: number;
|
|
374
|
+
/**
|
|
375
|
+
* Present only when the leader published the intent for a registered
|
|
376
|
+
* follower. Successor re-key and settlement must come from this exact
|
|
377
|
+
* follower runtime or its authenticated same-process session handoff.
|
|
378
|
+
*/
|
|
379
|
+
sourceInstanceId?: string;
|
|
374
380
|
}
|
|
375
381
|
|
|
376
382
|
export interface TelegramTopicTargetFile {
|
|
@@ -1329,7 +1335,7 @@ function cloneSessionReplacementIntent(
|
|
|
1329
1335
|
return { ...intent, target: { ...intent.target } };
|
|
1330
1336
|
}
|
|
1331
1337
|
|
|
1332
|
-
function
|
|
1338
|
+
export function normalizeTelegramSessionReplacementIntent(
|
|
1333
1339
|
value: unknown,
|
|
1334
1340
|
): TelegramSessionReplacementIntent | undefined {
|
|
1335
1341
|
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
@@ -1346,7 +1352,10 @@ function normalizeSessionReplacementIntent(
|
|
|
1346
1352
|
typeof record.messageId !== "number" || !Number.isSafeInteger(record.messageId) ||
|
|
1347
1353
|
typeof record.createdAtMs !== "number" || !Number.isSafeInteger(record.createdAtMs) ||
|
|
1348
1354
|
typeof record.expiresAtMs !== "number" || !Number.isSafeInteger(record.expiresAtMs) ||
|
|
1349
|
-
record.expiresAtMs <= record.createdAtMs
|
|
1355
|
+
record.expiresAtMs <= record.createdAtMs ||
|
|
1356
|
+
(record.sourceInstanceId !== undefined &&
|
|
1357
|
+
(typeof record.sourceInstanceId !== "string" || !record.sourceInstanceId ||
|
|
1358
|
+
record.sourceInstanceId.length > 256))
|
|
1350
1359
|
) return undefined;
|
|
1351
1360
|
const continuity = record.continuity === "workspace-thread" ||
|
|
1352
1361
|
record.continuity === "classic-chat"
|
|
@@ -1370,6 +1379,8 @@ function normalizeSessionReplacementIntent(
|
|
|
1370
1379
|
...(typeof record.threadName === "string" ? { threadName: record.threadName } : {}),
|
|
1371
1380
|
createdAtMs: record.createdAtMs,
|
|
1372
1381
|
expiresAtMs: record.expiresAtMs,
|
|
1382
|
+
...(typeof record.sourceInstanceId === "string"
|
|
1383
|
+
? { sourceInstanceId: record.sourceInstanceId } : {}),
|
|
1373
1384
|
};
|
|
1374
1385
|
}
|
|
1375
1386
|
|
|
@@ -1680,7 +1691,7 @@ function parseTopicTargetFile(value: unknown): TelegramTopicTargetFile {
|
|
|
1680
1691
|
return normalized ? [normalized] : [];
|
|
1681
1692
|
})
|
|
1682
1693
|
: [],
|
|
1683
|
-
sessionReplacement:
|
|
1694
|
+
sessionReplacement: normalizeTelegramSessionReplacementIntent(file.sessionReplacement),
|
|
1684
1695
|
reservations: Array.isArray(file.reservations)
|
|
1685
1696
|
? file.reservations.flatMap((reservation) => {
|
|
1686
1697
|
const normalized = normalizeReservation(reservation);
|
|
@@ -2510,7 +2521,7 @@ export function createTelegramTopicTargetStore(
|
|
|
2510
2521
|
: undefined;
|
|
2511
2522
|
},
|
|
2512
2523
|
async commitSessionReplacementIntent(intent, isCurrent) {
|
|
2513
|
-
const next =
|
|
2524
|
+
const next = normalizeTelegramSessionReplacementIntent(intent);
|
|
2514
2525
|
if (!next || !isCurrent()) return false;
|
|
2515
2526
|
await loadFromDisk();
|
|
2516
2527
|
if (!isCurrent()) return false;
|
|
@@ -2925,7 +2936,10 @@ export function createTelegramTopicTargetStore(
|
|
|
2925
2936
|
replacement.expiresAtMs > getNowMs() &&
|
|
2926
2937
|
replacement.profileName === (getTelegramProfile() ?? "default") &&
|
|
2927
2938
|
replacement.cwd === normalizedCwd &&
|
|
2928
|
-
replacement.sourceSessionId !== normalizedSessionId
|
|
2939
|
+
replacement.sourceSessionId !== normalizedSessionId &&
|
|
2940
|
+
(replacement.sourceInstanceId === undefined ||
|
|
2941
|
+
replacement.sourceInstanceId === instanceId ||
|
|
2942
|
+
replacement.sourceInstanceId === previousInstanceId)
|
|
2929
2943
|
) {
|
|
2930
2944
|
const sourceEntry = Array.from(workspaceBindings.entries()).find(([, binding]) =>
|
|
2931
2945
|
binding.cwd === normalizedCwd &&
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-telegram",
|
|
3
|
-
"version": "0.51.
|
|
3
|
+
"version": "0.51.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -31,12 +31,13 @@
|
|
|
31
31
|
"test:verbose": "node --experimental-strip-types --test --test-reporter=spec tests/*.test.ts",
|
|
32
32
|
"typecheck": "tsc --noEmit",
|
|
33
33
|
"build": "node scripts/build-dist.mjs",
|
|
34
|
+
"build:check": "node scripts/build-dist.mjs --check",
|
|
34
35
|
"prepack": "npm run build",
|
|
35
36
|
"check": "node -e \"await import('./dist/pi-telegram/index.js'); console.log('pi-telegram: extension import ok')\"",
|
|
36
37
|
"audit": "npm audit --omit=peer",
|
|
37
38
|
"audit:host": "npm audit",
|
|
38
39
|
"pack:check": "npm pack --dry-run",
|
|
39
|
-
"validate": "npm run build && npm run typecheck && npm test && npm run audit && npm run check && npm run pack:check"
|
|
40
|
+
"validate": "npm run build:check && npm run typecheck && npm test && npm run audit && npm run check && npm run pack:check"
|
|
40
41
|
},
|
|
41
42
|
"files": [
|
|
42
43
|
"index.ts",
|
|
@@ -102,15 +103,9 @@
|
|
|
102
103
|
"extensions": [
|
|
103
104
|
"./dist/pi-telegram/index.js"
|
|
104
105
|
],
|
|
105
|
-
"sourceExtensions": [
|
|
106
|
-
"./index.ts"
|
|
107
|
-
],
|
|
108
106
|
"skills": [
|
|
109
107
|
"./dist/skills"
|
|
110
108
|
],
|
|
111
|
-
"sourceSkills": [
|
|
112
|
-
"./skills"
|
|
113
|
-
],
|
|
114
109
|
"image": "https://raw.githubusercontent.com/llblab/pi-telegram/main/screenshot.png"
|
|
115
110
|
},
|
|
116
111
|
"peerDependencies": {
|
package/scripts/build-dist.mjs
CHANGED
|
@@ -1,44 +1,115 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Builds the distributive JavaScript, declarations, Pi entrypoint,
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* The build replaces `dist`, compiles with `tsconfig.build.json`, then copies
|
|
8
|
-
* package-owned runtime assets. It does not publish, pack, or modify sources.
|
|
4
|
+
* Builds or verifies the distributive JavaScript, declarations, Pi entrypoint,
|
|
5
|
+
* Skills, and runtime assets without exposing a partial tree.
|
|
9
6
|
*/
|
|
10
7
|
|
|
11
8
|
import { spawnSync } from "node:child_process";
|
|
12
|
-
import {
|
|
13
|
-
|
|
9
|
+
import {
|
|
10
|
+
cpSync,
|
|
11
|
+
existsSync,
|
|
12
|
+
mkdirSync,
|
|
13
|
+
mkdtempSync,
|
|
14
|
+
readFileSync,
|
|
15
|
+
readdirSync,
|
|
16
|
+
renameSync,
|
|
17
|
+
rmSync,
|
|
18
|
+
writeFileSync,
|
|
19
|
+
} from "node:fs";
|
|
20
|
+
import { join, relative } from "node:path";
|
|
21
|
+
|
|
22
|
+
const DIST_DIR = "dist";
|
|
23
|
+
const checkOnly = process.argv.includes("--check");
|
|
14
24
|
|
|
15
25
|
function run(command, args) {
|
|
16
26
|
const result = spawnSync(command, args, { stdio: "inherit" });
|
|
17
27
|
if (result.error) throw result.error;
|
|
18
|
-
if (result.status !== 0)
|
|
28
|
+
if (result.status !== 0) {
|
|
29
|
+
throw new Error(`${command} exited with status ${result.status ?? "unknown"}.`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function listFiles(root, current = root) {
|
|
34
|
+
return readdirSync(current, { withFileTypes: true }).flatMap((entry) => {
|
|
35
|
+
const path = join(current, entry.name);
|
|
36
|
+
return entry.isDirectory() ? listFiles(root, path) : [relative(root, path)];
|
|
37
|
+
}).sort();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeTextFiles(root) {
|
|
41
|
+
const textSuffixes = [".js", ".ts", ".mjs", ".json", ".md"];
|
|
42
|
+
for (const path of listFiles(root)) {
|
|
43
|
+
if (!textSuffixes.some((suffix) => path.endsWith(suffix))) continue;
|
|
44
|
+
const absolutePath = join(root, path);
|
|
45
|
+
const source = readFileSync(absolutePath, "utf8");
|
|
46
|
+
const normalized = source.replace(/\r\n?/g, "\n");
|
|
47
|
+
if (normalized !== source) writeFileSync(absolutePath, normalized, "utf8");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function assertTreesEqual(expectedRoot, actualRoot) {
|
|
52
|
+
if (!existsSync(expectedRoot)) {
|
|
53
|
+
throw new Error(`${expectedRoot} is missing; run npm run build.`);
|
|
54
|
+
}
|
|
55
|
+
const expectedFiles = listFiles(expectedRoot);
|
|
56
|
+
const actualFiles = listFiles(actualRoot);
|
|
57
|
+
if (JSON.stringify(expectedFiles) !== JSON.stringify(actualFiles)) {
|
|
58
|
+
throw new Error("dist file inventory is stale; run npm run build.");
|
|
59
|
+
}
|
|
60
|
+
for (const path of expectedFiles) {
|
|
61
|
+
if (!readFileSync(join(expectedRoot, path)).equals(readFileSync(join(actualRoot, path)))) {
|
|
62
|
+
throw new Error(`dist/${path} is stale; run npm run build.`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function replaceDist(candidate) {
|
|
68
|
+
const backup = `.dist-backup-${process.pid}-${Date.now()}`;
|
|
69
|
+
const hadDist = existsSync(DIST_DIR);
|
|
70
|
+
if (hadDist) renameSync(DIST_DIR, backup);
|
|
71
|
+
try {
|
|
72
|
+
renameSync(candidate, DIST_DIR);
|
|
73
|
+
if (hadDist) rmSync(backup, { recursive: true, force: true });
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (hadDist && existsSync(backup) && !existsSync(DIST_DIR)) {
|
|
76
|
+
renameSync(backup, DIST_DIR);
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
19
80
|
}
|
|
20
81
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
cpSync("
|
|
39
|
-
cpSync(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
run(process.execPath, ["--check", join(
|
|
82
|
+
const candidate = mkdtempSync(join(process.cwd(), ".dist-build-"));
|
|
83
|
+
try {
|
|
84
|
+
run(process.execPath, [
|
|
85
|
+
join("node_modules", "typescript", "bin", "tsc"),
|
|
86
|
+
"-p",
|
|
87
|
+
"tsconfig.build.json",
|
|
88
|
+
"--outDir",
|
|
89
|
+
candidate,
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
mkdirSync(join(candidate, "pi-telegram"), { recursive: true });
|
|
93
|
+
writeFileSync(
|
|
94
|
+
join(candidate, "pi-telegram", "index.js"),
|
|
95
|
+
'export { default } from "../index.js";\n',
|
|
96
|
+
"utf8",
|
|
97
|
+
);
|
|
98
|
+
normalizeTextFiles(candidate);
|
|
99
|
+
cpSync("skills", join(candidate, "skills"), { recursive: true });
|
|
100
|
+
cpSync("package.json", join(candidate, "package.json"));
|
|
101
|
+
cpSync(
|
|
102
|
+
join("lib", "generative-app-worker.mjs"),
|
|
103
|
+
join(candidate, "lib", "generative-app-worker.mjs"),
|
|
104
|
+
);
|
|
105
|
+
run(process.execPath, ["--check", join(candidate, "pi-telegram", "index.js")]);
|
|
106
|
+
|
|
107
|
+
if (checkOnly) {
|
|
108
|
+
assertTreesEqual(DIST_DIR, candidate);
|
|
109
|
+
console.log("pi-telegram: dist is current");
|
|
110
|
+
} else {
|
|
111
|
+
replaceDist(candidate);
|
|
112
|
+
}
|
|
113
|
+
} finally {
|
|
114
|
+
rmSync(candidate, { recursive: true, force: true });
|
|
115
|
+
}
|