@robbeverhelst/do 1.282.0 → 1.284.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/do.js +695 -16
- package/package.json +2 -1
package/dist/do.js
CHANGED
|
@@ -1038,7 +1038,7 @@ function flagBool(flags, name) {
|
|
|
1038
1038
|
|
|
1039
1039
|
// src/commands.ts
|
|
1040
1040
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
1041
|
-
import { basename, dirname as dirname2, join as
|
|
1041
|
+
import { basename as basename2, dirname as dirname2, join as join4 } from "path";
|
|
1042
1042
|
|
|
1043
1043
|
// ../../node_modules/drizzle-orm/entity.js
|
|
1044
1044
|
var entityKind = Symbol.for("drizzle:entityKind");
|
|
@@ -4191,6 +4191,7 @@ var contacts = pgTable("contacts", {
|
|
|
4191
4191
|
tenantId: text("tenant_id").notNull().references(() => tenants.id),
|
|
4192
4192
|
kind: contactKind("kind").notNull().default("person"),
|
|
4193
4193
|
email: text("email"),
|
|
4194
|
+
emailInvalidAt: timestamp("email_invalid_at", { withTimezone: true }),
|
|
4194
4195
|
phone: text("phone"),
|
|
4195
4196
|
company: text("company"),
|
|
4196
4197
|
website: text("website"),
|
|
@@ -5821,6 +5822,7 @@ var journalImportItems = pgTable("journal_import_items", {
|
|
|
5821
5822
|
body: text("body"),
|
|
5822
5823
|
rating: doublePrecision("rating"),
|
|
5823
5824
|
ratingQuestion: text("rating_question"),
|
|
5825
|
+
sleep: doublePrecision("sleep"),
|
|
5824
5826
|
highlight: text("highlight"),
|
|
5825
5827
|
milestoneCandidate: boolean("milestone_candidate").notNull().default(false),
|
|
5826
5828
|
health: boolean("health").notNull().default(false),
|
|
@@ -22235,7 +22237,14 @@ var setHealthLabAnalyteInputSchema = object({
|
|
|
22235
22237
|
|
|
22236
22238
|
// ../../packages/schema/src/contracts/files.ts
|
|
22237
22239
|
var FILE_TEXT_MAX = 1e6;
|
|
22238
|
-
var FILE_SOURCES = [
|
|
22240
|
+
var FILE_SOURCES = [
|
|
22241
|
+
"upload",
|
|
22242
|
+
"chat",
|
|
22243
|
+
"doccle",
|
|
22244
|
+
"doccle_zip",
|
|
22245
|
+
"scan",
|
|
22246
|
+
"game_save"
|
|
22247
|
+
];
|
|
22239
22248
|
var fileSourceSchema = union([
|
|
22240
22249
|
_enum2(FILE_SOURCES),
|
|
22241
22250
|
string2().regex(/^gmail:[A-Za-z0-9_-]{1,200}$/, "gmail:<message id>")
|
|
@@ -22466,6 +22475,7 @@ var journalImportItemInputSchema = object({
|
|
|
22466
22475
|
body: string2().trim().min(1).max(20000).nullish(),
|
|
22467
22476
|
rating: number2().min(1).max(10).nullish(),
|
|
22468
22477
|
ratingQuestion: string2().trim().min(1).max(80).nullish(),
|
|
22478
|
+
sleep: number2().min(0).max(24).nullish(),
|
|
22469
22479
|
highlight: string2().trim().min(1).max(200).nullish(),
|
|
22470
22480
|
milestoneCandidate: boolean3().default(false),
|
|
22471
22481
|
health: boolean3().default(false),
|
|
@@ -22494,6 +22504,7 @@ var journalImportItemSchema = object({
|
|
|
22494
22504
|
body: string2().nullable(),
|
|
22495
22505
|
rating: number2().nullable(),
|
|
22496
22506
|
ratingQuestion: string2().nullable(),
|
|
22507
|
+
sleep: number2().nullable().default(null),
|
|
22497
22508
|
highlight: string2().nullable(),
|
|
22498
22509
|
milestoneCandidate: boolean3(),
|
|
22499
22510
|
health: boolean3(),
|
|
@@ -22518,6 +22529,7 @@ var journalImportOverviewSchema = object({
|
|
|
22518
22529
|
ratings: number2().int(),
|
|
22519
22530
|
unclearRatings: array(string2()),
|
|
22520
22531
|
milestoneCandidates: number2().int(),
|
|
22532
|
+
sleep: number2().int().default(0),
|
|
22521
22533
|
health: number2().int(),
|
|
22522
22534
|
links: number2().int()
|
|
22523
22535
|
}),
|
|
@@ -23540,7 +23552,8 @@ var foodWeekSchema = object({
|
|
|
23540
23552
|
points: array(object({ day: civilDate, value: number2() })),
|
|
23541
23553
|
trend: array(object({ day: civilDate, value: number2() })),
|
|
23542
23554
|
latest: number2().nullable(),
|
|
23543
|
-
perWeek: number2().nullable()
|
|
23555
|
+
perWeek: number2().nullable(),
|
|
23556
|
+
source: metricSourceSchema.nullable()
|
|
23544
23557
|
}),
|
|
23545
23558
|
numbers: object({
|
|
23546
23559
|
completeDays: number2().int(),
|
|
@@ -24144,7 +24157,8 @@ var contactSchema = createSelectSchema(contacts, {
|
|
|
24144
24157
|
lastContactedAt: exports_coerce.date().nullable(),
|
|
24145
24158
|
userId: string2().nullable().default(null),
|
|
24146
24159
|
rhythmDays: number2().int().nullable().default(null),
|
|
24147
|
-
rhythmSnoozedUntil: string2().nullable().default(null)
|
|
24160
|
+
rhythmSnoozedUntil: string2().nullable().default(null),
|
|
24161
|
+
emailInvalidAt: exports_coerce.date().nullable().default(null)
|
|
24148
24162
|
});
|
|
24149
24163
|
var contactImportantDateSchema = object({
|
|
24150
24164
|
id: ulidSchema,
|
|
@@ -24188,7 +24202,7 @@ var createContactInputSchema = createInsertSchema(contacts, {
|
|
|
24188
24202
|
lastContactedAt: exports_coerce.date().nullish(),
|
|
24189
24203
|
rhythmDays: contactPeopleInputShape.rhythmDays,
|
|
24190
24204
|
rhythmSnoozedUntil: contactPeopleInputShape.rhythmSnoozedUntil
|
|
24191
|
-
}).omit({ tenantId: true, keepInTouchRuleId: true, userId: true }).extend({
|
|
24205
|
+
}).omit({ tenantId: true, keepInTouchRuleId: true, userId: true, emailInvalidAt: true }).extend({
|
|
24192
24206
|
title: string2().min(1),
|
|
24193
24207
|
parentId: ulidSchema.nullish(),
|
|
24194
24208
|
importantDates: array(contactImportantDateInputSchema).optional(),
|
|
@@ -24205,7 +24219,7 @@ var updateContactInputSchema = createUpdateSchema(contacts, {
|
|
|
24205
24219
|
lastContactedAt: exports_coerce.date().nullish(),
|
|
24206
24220
|
rhythmDays: contactPeopleInputShape.rhythmDays,
|
|
24207
24221
|
rhythmSnoozedUntil: contactPeopleInputShape.rhythmSnoozedUntil
|
|
24208
|
-
}).omit({ id: true, tenantId: true, keepInTouchRuleId: true, userId: true }).extend({
|
|
24222
|
+
}).omit({ id: true, tenantId: true, keepInTouchRuleId: true, userId: true, emailInvalidAt: true }).extend({
|
|
24209
24223
|
title: string2().min(1).optional(),
|
|
24210
24224
|
parentId: ulidSchema.nullish(),
|
|
24211
24225
|
importantDates: array(contactImportantDateInputSchema).optional(),
|
|
@@ -25296,6 +25310,7 @@ var runQuestSchema = object({
|
|
|
25296
25310
|
status: runQuestStatusSchema.default("active"),
|
|
25297
25311
|
objective: text5.nullish(),
|
|
25298
25312
|
hint: shortText.nullish(),
|
|
25313
|
+
reward: shortText.nullish(),
|
|
25299
25314
|
place: shortText.nullish(),
|
|
25300
25315
|
steps: array(object({
|
|
25301
25316
|
text: shortText.pipe(string2().min(1)),
|
|
@@ -25327,7 +25342,7 @@ var runChoiceSchema = object({
|
|
|
25327
25342
|
var runHomeSchema = object({
|
|
25328
25343
|
ref: ref.nullish(),
|
|
25329
25344
|
name,
|
|
25330
|
-
role: _enum2(["home", "collector", "storage", "planned"]).default("home"),
|
|
25345
|
+
role: _enum2(["home", "collector", "storage", "planned", "later"]).default("home"),
|
|
25331
25346
|
place: shortText.nullish(),
|
|
25332
25347
|
upgrades: array(object({ name, done: boolean3().default(false) }).strict()).max(40).optional(),
|
|
25333
25348
|
steward: shortText.nullish(),
|
|
@@ -25394,6 +25409,35 @@ var runInstallSchema = object({
|
|
|
25394
25409
|
lastBackup: isoDate.nullish(),
|
|
25395
25410
|
notes: text5.nullish()
|
|
25396
25411
|
}).strict();
|
|
25412
|
+
var SAVE_SCREENSHOT_MAX_BASE64 = 1500000;
|
|
25413
|
+
var runSaveFields = {
|
|
25414
|
+
game: _enum2(["skyrim"]).default("skyrim"),
|
|
25415
|
+
fileName: shortText.nullish(),
|
|
25416
|
+
saveNumber: number2().int().min(0).max(1e9),
|
|
25417
|
+
playerName: name,
|
|
25418
|
+
level: number2().int().min(0).max(1e4),
|
|
25419
|
+
location: shortText,
|
|
25420
|
+
race: shortText.nullish(),
|
|
25421
|
+
sex: _enum2(["male", "female"]).nullish(),
|
|
25422
|
+
playTimeMinutes: number2().int().min(0).max(1e4 * 60).nullish(),
|
|
25423
|
+
savedAt: string2().datetime({ offset: true }).nullish(),
|
|
25424
|
+
gameDate: object({
|
|
25425
|
+
text: shortText.pipe(string2().min(1)),
|
|
25426
|
+
hour: number2().min(0).max(24).nullish(),
|
|
25427
|
+
daysPassed: number2().min(0).max(1e6).nullish()
|
|
25428
|
+
}).strict().nullish(),
|
|
25429
|
+
plugins: array(shortText).max(600).optional(),
|
|
25430
|
+
stats: record(shortText, number2().int()).refine((r) => Object.keys(r).length <= 300, "at most 300 stats").optional()
|
|
25431
|
+
};
|
|
25432
|
+
var runSaveSchema = object({
|
|
25433
|
+
...runSaveFields,
|
|
25434
|
+
screenshotFileId: string2().min(1).max(64).nullish(),
|
|
25435
|
+
uploadedAt: string2().datetime({ offset: true })
|
|
25436
|
+
}).strict();
|
|
25437
|
+
var uploadRunSaveHeaderInputSchema = object({
|
|
25438
|
+
...runSaveFields,
|
|
25439
|
+
screenshot: string2().max(SAVE_SCREENSHOT_MAX_BASE64).regex(/^[A-Za-z0-9+/]+={0,2}$/, "must be base64").nullish()
|
|
25440
|
+
}).strict();
|
|
25397
25441
|
var runFacetSchema = object({
|
|
25398
25442
|
theme: runThemeSchema.default("generic"),
|
|
25399
25443
|
edition: shortText.nullish(),
|
|
@@ -25414,6 +25458,7 @@ var runFacetSchema = object({
|
|
|
25414
25458
|
achievements: runAchievementsSchema.nullish(),
|
|
25415
25459
|
rareFinds: runRareFindsSchema.nullish(),
|
|
25416
25460
|
install: runInstallSchema.nullish(),
|
|
25461
|
+
save: runSaveSchema.nullish(),
|
|
25417
25462
|
money: object({ goal: shortText.nullish(), purpose: shortText.nullish() }).strict().nullish(),
|
|
25418
25463
|
links: array(object({ label: name, url: string2().url().max(2000) }).strict()).max(50).optional()
|
|
25419
25464
|
}).strict();
|
|
@@ -25441,6 +25486,7 @@ var runValueSectionValues = [
|
|
|
25441
25486
|
"achievements",
|
|
25442
25487
|
"rareFinds",
|
|
25443
25488
|
"install",
|
|
25489
|
+
"save",
|
|
25444
25490
|
"money"
|
|
25445
25491
|
];
|
|
25446
25492
|
var runValueSectionSchema = _enum2(runValueSectionValues);
|
|
@@ -25544,6 +25590,12 @@ var runChangesResultSchema = object({
|
|
|
25544
25590
|
sessionId: string2(),
|
|
25545
25591
|
batch: runChangeBatchSchema
|
|
25546
25592
|
});
|
|
25593
|
+
var runSaveHeaderResultSchema = object({
|
|
25594
|
+
run: runDetailSchema,
|
|
25595
|
+
unchanged: boolean3(),
|
|
25596
|
+
sessionId: string2().nullable(),
|
|
25597
|
+
batch: runChangeBatchSchema.nullable()
|
|
25598
|
+
});
|
|
25547
25599
|
var createRunInputSchema = object({
|
|
25548
25600
|
id: ulidSchema,
|
|
25549
25601
|
gameId: ulidSchema,
|
|
@@ -26011,7 +26063,8 @@ var animalHandoverTripSchema = object({
|
|
|
26011
26063
|
houseNote: string2().nullable(),
|
|
26012
26064
|
holidayMode: animalHolidayModeSchema,
|
|
26013
26065
|
sharedUntil: string2().nullable(),
|
|
26014
|
-
agentNote: string2().nullable()
|
|
26066
|
+
agentNote: string2().nullable(),
|
|
26067
|
+
calendar: string2().nullable()
|
|
26015
26068
|
});
|
|
26016
26069
|
var animalHandoverAnimalSchema = object({
|
|
26017
26070
|
id: ulidSchema,
|
|
@@ -26028,6 +26081,7 @@ var animalHandoverAnimalSchema = object({
|
|
|
26028
26081
|
var animalHandoverContactSchema = object({
|
|
26029
26082
|
id: ulidSchema,
|
|
26030
26083
|
title: string2(),
|
|
26084
|
+
kind: _enum2(["vet", "member"]),
|
|
26031
26085
|
label: string2().nullable(),
|
|
26032
26086
|
phone: string2().nullable()
|
|
26033
26087
|
});
|
|
@@ -26042,6 +26096,7 @@ var animalHandoverSchema = object({
|
|
|
26042
26096
|
contacts: array(animalHandoverContactSchema),
|
|
26043
26097
|
supplies: array(animalHandoverSupplySchema),
|
|
26044
26098
|
habits: array(animalHandoverHabitSchema),
|
|
26099
|
+
sensors: boolean3(),
|
|
26045
26100
|
generatedAt: exports_coerce.date()
|
|
26046
26101
|
});
|
|
26047
26102
|
var animalHolidayModeInputSchema = animalHolidayModeSchema.partial().extend({ periodId: ulidSchema }).strict().refine((v) => Object.keys(v).length > 1, { message: "Turn at least one switch." });
|
|
@@ -28315,7 +28370,7 @@ function parseCapture2(input, now) {
|
|
|
28315
28370
|
|
|
28316
28371
|
// src/meta.ts
|
|
28317
28372
|
var CLI_NAME = "do";
|
|
28318
|
-
var VERSION = "1.
|
|
28373
|
+
var VERSION = "1.284.0";
|
|
28319
28374
|
var API_PREFIX = "/api/v1";
|
|
28320
28375
|
var DEFAULT_API_URL = "http://localhost:3001";
|
|
28321
28376
|
|
|
@@ -29799,6 +29854,41 @@ var HEALTH = /\b(symptoms?|stomach|nausea|headaches?|migraine|doctor|GP|medicati
|
|
|
29799
29854
|
var NOISE = /\b(PR monitor|GitHub daily|deploy(?:ment)? watch\w*|cron|heartbeat|sales prospects?)\b/i;
|
|
29800
29855
|
var MILESTONE = /\b(first|went live|is live|win:|moved in|arrived|wedding|engaged)\b/i;
|
|
29801
29856
|
var PR_REF = /\bPR\s*#\d+/gi;
|
|
29857
|
+
var SLEPT = /\bslept\s*~?\s*(\d{1,2})\s*h(?:\s*(\d{1,2}))?/i;
|
|
29858
|
+
var SLEEP_HOURS = /\bsleep\s*:?\s*~?\s*(\d{1,2}(?:\.\d+)?)\s*h\b/i;
|
|
29859
|
+
var atClock = (word) => [
|
|
29860
|
+
new RegExp(`\\b${word}\\b\\D{0,12}?(\\d{1,2})[:.h](\\d{2})`, "i"),
|
|
29861
|
+
new RegExp(`(\\d{1,2})[:.h](\\d{2})\\D{0,6}?\\b${word}\\b`, "i")
|
|
29862
|
+
];
|
|
29863
|
+
var BED_AT = atClock("(?:bed|asleep|lights out)");
|
|
29864
|
+
var WOKE_AT = atClock("(?:woke(?: up)?|wake(?: up)?|awake|got up|up at)");
|
|
29865
|
+
function sleepOf(raw) {
|
|
29866
|
+
const lines = raw.map(plain);
|
|
29867
|
+
for (const line of lines) {
|
|
29868
|
+
const slept = SLEPT.exec(line);
|
|
29869
|
+
if (slept)
|
|
29870
|
+
return Number(slept[1]) + Number(slept[2] ?? 0) / 60;
|
|
29871
|
+
const hours = SLEEP_HOURS.exec(line);
|
|
29872
|
+
if (hours)
|
|
29873
|
+
return Number(hours[1]);
|
|
29874
|
+
}
|
|
29875
|
+
const clock = (patterns) => {
|
|
29876
|
+
for (const line of lines) {
|
|
29877
|
+
for (const re of patterns) {
|
|
29878
|
+
const m = re.exec(line);
|
|
29879
|
+
if (m)
|
|
29880
|
+
return Number(m[1]) * 60 + Number(m[2]);
|
|
29881
|
+
}
|
|
29882
|
+
}
|
|
29883
|
+
return null;
|
|
29884
|
+
};
|
|
29885
|
+
const bed = clock(BED_AT);
|
|
29886
|
+
const woke = clock(WOKE_AT);
|
|
29887
|
+
if (bed === null || woke === null)
|
|
29888
|
+
return null;
|
|
29889
|
+
const minutes = (woke - bed + 24 * 60) % (24 * 60);
|
|
29890
|
+
return minutes >= 120 && minutes <= 16 * 60 ? Math.round(minutes / 60 * 100) / 100 : null;
|
|
29891
|
+
}
|
|
29802
29892
|
var TASK_DONE = /^\s*[-*]\s*\[x\]/i;
|
|
29803
29893
|
function section(text, heading) {
|
|
29804
29894
|
const lines = text.split(/\r?\n/);
|
|
@@ -29864,6 +29954,7 @@ function parseDailyNote(path, day, text) {
|
|
|
29864
29954
|
}
|
|
29865
29955
|
const milestoneLine = all.find((l) => MILESTONE.test(l) && !HEALTH.test(l) && !NOISE.test(l) && plain(l).length > 0);
|
|
29866
29956
|
const highlight = milestoneLine ? firstSentence(plain(milestoneLine), 80) : null;
|
|
29957
|
+
const sleep = sleepOf(all);
|
|
29867
29958
|
const prs = (text.match(PR_REF) ?? []).length;
|
|
29868
29959
|
const tasks = all.filter((l) => TASK_DONE.test(l)).length;
|
|
29869
29960
|
return {
|
|
@@ -29876,6 +29967,7 @@ function parseDailyNote(path, day, text) {
|
|
|
29876
29967
|
body,
|
|
29877
29968
|
rating,
|
|
29878
29969
|
ratingQuestion,
|
|
29970
|
+
...sleep !== null ? { sleep } : {},
|
|
29879
29971
|
highlight,
|
|
29880
29972
|
milestoneCandidate: Boolean(highlight),
|
|
29881
29973
|
health,
|
|
@@ -33305,6 +33397,34 @@ var COMMAND_SPECS = [
|
|
|
33305
33397
|
returns: "RunChangesResult",
|
|
33306
33398
|
notes: "POST /api/v1/runs/{id}/changes. Ops: set|merge on a one-value section (character, arc, nextSteps, achievements, install, \u2026), upsert|remove on a list section matched by name (quests, choices, homes, people, mounts, uniques, orderOfPlay, team, checklists, links), tick on a checklist. The batch joins the last session if it was active in the last 6 hours, else opens a new one. A change that leaves the run invalid writes nothing."
|
|
33307
33399
|
},
|
|
33400
|
+
{
|
|
33401
|
+
path: ["runs", "save-header"],
|
|
33402
|
+
summary: "Read a Skyrim save (read-only) and send its header and screenshot to a run.",
|
|
33403
|
+
args: [
|
|
33404
|
+
{ name: "id", required: true, description: "Run ULID." },
|
|
33405
|
+
{
|
|
33406
|
+
name: "save",
|
|
33407
|
+
required: true,
|
|
33408
|
+
description: "A .ess save, or the Saves folder (its newest save)."
|
|
33409
|
+
}
|
|
33410
|
+
],
|
|
33411
|
+
flags: [
|
|
33412
|
+
{
|
|
33413
|
+
name: "watch",
|
|
33414
|
+
type: "boolean",
|
|
33415
|
+
description: "Keep watching the Saves folder; send each new save once the game has written it."
|
|
33416
|
+
},
|
|
33417
|
+
{
|
|
33418
|
+
name: "interval",
|
|
33419
|
+
type: "string",
|
|
33420
|
+
description: "With --watch: seconds between looks (default 10)."
|
|
33421
|
+
},
|
|
33422
|
+
{ name: "dry-run", type: "boolean", description: "Print what the save says; send nothing." }
|
|
33423
|
+
],
|
|
33424
|
+
scopes: ["collections:write"],
|
|
33425
|
+
returns: "RunSaveHeaderResult",
|
|
33426
|
+
notes: "POST /api/v1/runs/{id}/save-header. The save sync (run tracker phase 4): name, level, location, race, play time, the in-game date, plugins and General Stats become the run's `save` section and its character's level, as one update with a receipt and Undo; the screenshot is a file on the run (the last one is dropped). The same save twice changes nothing. Needs a member's key (the screenshot is stored as theirs). The save itself is only read. See docs/agents/skyrim-save-sync.md."
|
|
33427
|
+
},
|
|
33308
33428
|
{
|
|
33309
33429
|
path: ["runs", "undo"],
|
|
33310
33430
|
summary: "Undo a run's latest chat update (or the one given).",
|
|
@@ -35288,6 +35408,534 @@ function renderEvents(data, mode) {
|
|
|
35288
35408
|
`);
|
|
35289
35409
|
}
|
|
35290
35410
|
|
|
35411
|
+
// src/save-sync.ts
|
|
35412
|
+
import { readFile, stat as stat2 } from "fs/promises";
|
|
35413
|
+
import { basename } from "path";
|
|
35414
|
+
|
|
35415
|
+
// ../../packages/skyrim-save/src/body.ts
|
|
35416
|
+
import { inflateSync } from "zlib";
|
|
35417
|
+
|
|
35418
|
+
// ../../packages/skyrim-save/src/reader.ts
|
|
35419
|
+
class SaveFormatError extends Error {
|
|
35420
|
+
name = "SaveFormatError";
|
|
35421
|
+
}
|
|
35422
|
+
|
|
35423
|
+
class Reader {
|
|
35424
|
+
bytes;
|
|
35425
|
+
view;
|
|
35426
|
+
offset;
|
|
35427
|
+
constructor(bytes, offset = 0) {
|
|
35428
|
+
this.bytes = bytes;
|
|
35429
|
+
this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
35430
|
+
this.offset = offset;
|
|
35431
|
+
}
|
|
35432
|
+
get remaining() {
|
|
35433
|
+
return this.bytes.byteLength - this.offset;
|
|
35434
|
+
}
|
|
35435
|
+
need(n, what) {
|
|
35436
|
+
if (n < 0 || this.offset + n > this.bytes.byteLength) {
|
|
35437
|
+
throw new SaveFormatError(`the save ends before its ${what} (at byte ${this.offset})`);
|
|
35438
|
+
}
|
|
35439
|
+
const at = this.offset;
|
|
35440
|
+
this.offset += n;
|
|
35441
|
+
return at;
|
|
35442
|
+
}
|
|
35443
|
+
u8(what) {
|
|
35444
|
+
return this.view.getUint8(this.need(1, what));
|
|
35445
|
+
}
|
|
35446
|
+
u16(what) {
|
|
35447
|
+
return this.view.getUint16(this.need(2, what), true);
|
|
35448
|
+
}
|
|
35449
|
+
u32(what) {
|
|
35450
|
+
return this.view.getUint32(this.need(4, what), true);
|
|
35451
|
+
}
|
|
35452
|
+
f32(what) {
|
|
35453
|
+
return this.view.getFloat32(this.need(4, what), true);
|
|
35454
|
+
}
|
|
35455
|
+
u64(what) {
|
|
35456
|
+
return this.view.getBigUint64(this.need(8, what), true);
|
|
35457
|
+
}
|
|
35458
|
+
take(n, what) {
|
|
35459
|
+
const at = this.need(n, what);
|
|
35460
|
+
return this.bytes.subarray(at, at + n);
|
|
35461
|
+
}
|
|
35462
|
+
wstring(what) {
|
|
35463
|
+
const len = this.u16(what);
|
|
35464
|
+
return decodeLatin1(this.take(len, what));
|
|
35465
|
+
}
|
|
35466
|
+
}
|
|
35467
|
+
function decodeLatin1(bytes) {
|
|
35468
|
+
let out = "";
|
|
35469
|
+
for (const b of bytes)
|
|
35470
|
+
out += String.fromCharCode(b);
|
|
35471
|
+
return out;
|
|
35472
|
+
}
|
|
35473
|
+
|
|
35474
|
+
// ../../packages/skyrim-save/src/lz4.ts
|
|
35475
|
+
function lz4DecompressBlock(src, size) {
|
|
35476
|
+
const out = new Uint8Array(size);
|
|
35477
|
+
let i = 0;
|
|
35478
|
+
let o = 0;
|
|
35479
|
+
while (i < src.length) {
|
|
35480
|
+
const token = src[i++];
|
|
35481
|
+
let literals = token >> 4;
|
|
35482
|
+
if (literals === 15) {
|
|
35483
|
+
let b;
|
|
35484
|
+
do {
|
|
35485
|
+
b = byteAt(src, i++);
|
|
35486
|
+
literals += b;
|
|
35487
|
+
} while (b === 255);
|
|
35488
|
+
}
|
|
35489
|
+
if (i + literals > src.length || o + literals > size) {
|
|
35490
|
+
throw new SaveFormatError("the save's body is cut short (LZ4 literals)");
|
|
35491
|
+
}
|
|
35492
|
+
out.set(src.subarray(i, i + literals), o);
|
|
35493
|
+
i += literals;
|
|
35494
|
+
o += literals;
|
|
35495
|
+
if (i >= src.length)
|
|
35496
|
+
break;
|
|
35497
|
+
const offset = byteAt(src, i) | byteAt(src, i + 1) << 8;
|
|
35498
|
+
i += 2;
|
|
35499
|
+
if (offset === 0 || offset > o)
|
|
35500
|
+
throw new SaveFormatError("a bad LZ4 match in the save's body");
|
|
35501
|
+
let length = (token & 15) + 4;
|
|
35502
|
+
if ((token & 15) === 15) {
|
|
35503
|
+
let b;
|
|
35504
|
+
do {
|
|
35505
|
+
b = byteAt(src, i++);
|
|
35506
|
+
length += b;
|
|
35507
|
+
} while (b === 255);
|
|
35508
|
+
}
|
|
35509
|
+
if (o + length > size)
|
|
35510
|
+
throw new SaveFormatError("the save's body is longer than it says");
|
|
35511
|
+
for (let k = 0;k < length; k++, o++)
|
|
35512
|
+
out[o] = out[o - offset];
|
|
35513
|
+
}
|
|
35514
|
+
if (o !== size)
|
|
35515
|
+
throw new SaveFormatError("the save's body is shorter than it says");
|
|
35516
|
+
return out;
|
|
35517
|
+
}
|
|
35518
|
+
function byteAt(src, i) {
|
|
35519
|
+
const b = src[i];
|
|
35520
|
+
if (b === undefined)
|
|
35521
|
+
throw new SaveFormatError("the save's body is cut short (LZ4)");
|
|
35522
|
+
return b;
|
|
35523
|
+
}
|
|
35524
|
+
|
|
35525
|
+
// ../../packages/skyrim-save/src/body.ts
|
|
35526
|
+
var SAVE_STAT_CATEGORIES = [
|
|
35527
|
+
"general",
|
|
35528
|
+
"quest",
|
|
35529
|
+
"combat",
|
|
35530
|
+
"magic",
|
|
35531
|
+
"crafting",
|
|
35532
|
+
"crime",
|
|
35533
|
+
"dlc"
|
|
35534
|
+
];
|
|
35535
|
+
var TAMRIEL_MONTHS = [
|
|
35536
|
+
"Morning Star",
|
|
35537
|
+
"Sun's Dawn",
|
|
35538
|
+
"First Seed",
|
|
35539
|
+
"Rain's Hand",
|
|
35540
|
+
"Second Seed",
|
|
35541
|
+
"Midyear",
|
|
35542
|
+
"Sun's Height",
|
|
35543
|
+
"Last Seed",
|
|
35544
|
+
"Hearthfire",
|
|
35545
|
+
"Frostfall",
|
|
35546
|
+
"Sun's Dusk",
|
|
35547
|
+
"Evening Star"
|
|
35548
|
+
];
|
|
35549
|
+
var CLOCK_GLOBALS = {
|
|
35550
|
+
year: 53,
|
|
35551
|
+
month: 54,
|
|
35552
|
+
day: 55,
|
|
35553
|
+
hour: 56,
|
|
35554
|
+
daysPassed: 57
|
|
35555
|
+
};
|
|
35556
|
+
var MISC_STATS = 0;
|
|
35557
|
+
var GLOBAL_VARIABLES = 3;
|
|
35558
|
+
var LIGHT_PLUGINS_FROM = 78;
|
|
35559
|
+
function decompressBody(bytes, header) {
|
|
35560
|
+
if (header.compression === "none")
|
|
35561
|
+
return bytes.subarray(header.bodyOffset);
|
|
35562
|
+
const r = new Reader(bytes, header.bodyOffset);
|
|
35563
|
+
const uncompressed = r.u32("uncompressed size");
|
|
35564
|
+
const compressed = r.u32("compressed size");
|
|
35565
|
+
if (uncompressed > 512 * 1024 * 1024)
|
|
35566
|
+
throw new SaveFormatError("the save's body is too big");
|
|
35567
|
+
const block = r.take(compressed, "compressed body");
|
|
35568
|
+
if (header.compression === "lz4")
|
|
35569
|
+
return lz4DecompressBlock(block, uncompressed);
|
|
35570
|
+
const out = new Uint8Array(inflateSync(block));
|
|
35571
|
+
if (out.byteLength !== uncompressed) {
|
|
35572
|
+
throw new SaveFormatError("the save's body is not the size it says");
|
|
35573
|
+
}
|
|
35574
|
+
return out;
|
|
35575
|
+
}
|
|
35576
|
+
function parseSaveBody(bytes, header) {
|
|
35577
|
+
const body = decompressBody(bytes, header);
|
|
35578
|
+
const r = new Reader(body);
|
|
35579
|
+
const formVersion = r.u8("form version");
|
|
35580
|
+
const pluginInfoSize = r.u32("plugin info size");
|
|
35581
|
+
const pluginsEnd = r.offset + pluginInfoSize;
|
|
35582
|
+
const plugins = [];
|
|
35583
|
+
const pluginCount = r.u8("plugin count");
|
|
35584
|
+
for (let i = 0;i < pluginCount; i++)
|
|
35585
|
+
plugins.push(r.wstring("plugin name"));
|
|
35586
|
+
const lightPlugins = [];
|
|
35587
|
+
if (header.version >= 12 && formVersion >= LIGHT_PLUGINS_FROM) {
|
|
35588
|
+
const lightCount = r.u16("light plugin count");
|
|
35589
|
+
for (let i = 0;i < lightCount; i++)
|
|
35590
|
+
lightPlugins.push(r.wstring("light plugin name"));
|
|
35591
|
+
}
|
|
35592
|
+
if (r.offset !== pluginsEnd) {
|
|
35593
|
+
throw new SaveFormatError("the save's plugin list is not the size it says");
|
|
35594
|
+
}
|
|
35595
|
+
const formIdArrayAt = r.u32("form id array offset");
|
|
35596
|
+
r.u32("unknown table offset");
|
|
35597
|
+
const globals1At = r.u32("global data table 1 offset");
|
|
35598
|
+
r.u32("global data table 2 offset");
|
|
35599
|
+
r.u32("change forms offset");
|
|
35600
|
+
r.u32("global data table 3 offset");
|
|
35601
|
+
const globals1Count = r.u32("global data table 1 count");
|
|
35602
|
+
const base = header.bodyOffset;
|
|
35603
|
+
const formIds = readFormIdArray(body, formIdArrayAt - base);
|
|
35604
|
+
const { globals, stats } = readGlobalData(body, globals1At - base, globals1Count, formIds);
|
|
35605
|
+
return { formVersion, plugins, lightPlugins, gameTime: clockFrom(globals), stats };
|
|
35606
|
+
}
|
|
35607
|
+
function readFormIdArray(body, at) {
|
|
35608
|
+
const r = new Reader(body, at);
|
|
35609
|
+
const count = r.u32("form id count");
|
|
35610
|
+
if (count * 4 > r.remaining)
|
|
35611
|
+
throw new SaveFormatError("the save's form id array is cut short");
|
|
35612
|
+
const out = new Uint32Array(count);
|
|
35613
|
+
for (let i = 0;i < count; i++)
|
|
35614
|
+
out[i] = r.u32("form id");
|
|
35615
|
+
return out;
|
|
35616
|
+
}
|
|
35617
|
+
function readGlobalData(body, at, count, formIds) {
|
|
35618
|
+
const r = new Reader(body, at);
|
|
35619
|
+
const globals = new Map;
|
|
35620
|
+
const stats = [];
|
|
35621
|
+
for (let i = 0;i < count; i++) {
|
|
35622
|
+
const type = r.u32("global data type");
|
|
35623
|
+
const length = r.u32("global data length");
|
|
35624
|
+
const g = new Reader(r.take(length, "global data"));
|
|
35625
|
+
if (type === MISC_STATS) {
|
|
35626
|
+
const n = g.u32("stat count");
|
|
35627
|
+
for (let k = 0;k < n; k++) {
|
|
35628
|
+
const name = g.wstring("stat name");
|
|
35629
|
+
const category = SAVE_STAT_CATEGORIES[g.u8("stat category")] ?? "general";
|
|
35630
|
+
const value = g.u32("stat value") | 0;
|
|
35631
|
+
if (value !== 0)
|
|
35632
|
+
stats.push({ name, category, value });
|
|
35633
|
+
}
|
|
35634
|
+
} else if (type === GLOBAL_VARIABLES) {
|
|
35635
|
+
const n = vsval(g);
|
|
35636
|
+
for (let k = 0;k < n; k++) {
|
|
35637
|
+
const id = refId(g, formIds);
|
|
35638
|
+
globals.set(id, g.f32("global value"));
|
|
35639
|
+
}
|
|
35640
|
+
}
|
|
35641
|
+
}
|
|
35642
|
+
return { globals, stats };
|
|
35643
|
+
}
|
|
35644
|
+
function clockFrom(globals) {
|
|
35645
|
+
const get = (id) => globals.get(id);
|
|
35646
|
+
const year = get(CLOCK_GLOBALS.year);
|
|
35647
|
+
const month = get(CLOCK_GLOBALS.month);
|
|
35648
|
+
const day = get(CLOCK_GLOBALS.day);
|
|
35649
|
+
const hour = get(CLOCK_GLOBALS.hour);
|
|
35650
|
+
const daysPassed = get(CLOCK_GLOBALS.daysPassed);
|
|
35651
|
+
if (year === undefined || month === undefined || day === undefined || hour === undefined) {
|
|
35652
|
+
return null;
|
|
35653
|
+
}
|
|
35654
|
+
return {
|
|
35655
|
+
year: Math.round(year),
|
|
35656
|
+
month: Math.round(month),
|
|
35657
|
+
day: Math.round(day),
|
|
35658
|
+
hour,
|
|
35659
|
+
daysPassed: daysPassed ?? 0
|
|
35660
|
+
};
|
|
35661
|
+
}
|
|
35662
|
+
function vsval(r) {
|
|
35663
|
+
const first = r.u8("vsval");
|
|
35664
|
+
const size = first & 3;
|
|
35665
|
+
if (size === 0)
|
|
35666
|
+
return first >> 2;
|
|
35667
|
+
if (size === 1)
|
|
35668
|
+
return (first | r.u8("vsval") << 8) >> 2;
|
|
35669
|
+
const rest = r.u16("vsval");
|
|
35670
|
+
return (first | rest << 8) >>> 2;
|
|
35671
|
+
}
|
|
35672
|
+
function refId(r, formIds) {
|
|
35673
|
+
const b0 = r.u8("ref id");
|
|
35674
|
+
const b1 = r.u8("ref id");
|
|
35675
|
+
const b2 = r.u8("ref id");
|
|
35676
|
+
const value = (b0 & 63) << 16 | b1 << 8 | b2;
|
|
35677
|
+
const type = b0 >> 6;
|
|
35678
|
+
if (type === 1)
|
|
35679
|
+
return value;
|
|
35680
|
+
if (type === 2)
|
|
35681
|
+
return (4278190080 | value) >>> 0;
|
|
35682
|
+
if (type === 0)
|
|
35683
|
+
return value === 0 ? 0 : formIds[value - 1] ?? 0;
|
|
35684
|
+
return 0;
|
|
35685
|
+
}
|
|
35686
|
+
function formatGameDate(time) {
|
|
35687
|
+
const month = TAMRIEL_MONTHS[time.month] ?? `month ${time.month + 1}`;
|
|
35688
|
+
return `${ordinal(time.day)} of ${month}, 4E ${time.year}`;
|
|
35689
|
+
}
|
|
35690
|
+
function ordinal(n) {
|
|
35691
|
+
const tens = n % 100;
|
|
35692
|
+
if (tens >= 11 && tens <= 13)
|
|
35693
|
+
return `${n}th`;
|
|
35694
|
+
const suffix = n % 10 === 1 ? "st" : n % 10 === 2 ? "nd" : n % 10 === 3 ? "rd" : "th";
|
|
35695
|
+
return `${n}${suffix}`;
|
|
35696
|
+
}
|
|
35697
|
+
// ../../packages/skyrim-save/src/header.ts
|
|
35698
|
+
var SAVE_MAGIC = "TESV_SAVEGAME";
|
|
35699
|
+
var SE_VERSION = 12;
|
|
35700
|
+
var FILETIME_UNIX_EPOCH = 116444736000000000n;
|
|
35701
|
+
function parseSaveHeader(bytes) {
|
|
35702
|
+
const r = new Reader(bytes);
|
|
35703
|
+
const magic = String.fromCharCode(...r.take(SAVE_MAGIC.length, "magic"));
|
|
35704
|
+
if (magic !== SAVE_MAGIC)
|
|
35705
|
+
throw new SaveFormatError("not a Skyrim save (no TESV_SAVEGAME magic)");
|
|
35706
|
+
const headerSize = r.u32("header size");
|
|
35707
|
+
const headerStart = r.offset;
|
|
35708
|
+
const version = r.u32("version");
|
|
35709
|
+
if (version < 7 || version > 64)
|
|
35710
|
+
throw new SaveFormatError(`unknown save version ${version}`);
|
|
35711
|
+
const saveNumber = r.u32("save number");
|
|
35712
|
+
const playerName = r.wstring("player name");
|
|
35713
|
+
const level = r.u32("level");
|
|
35714
|
+
const location = r.wstring("location");
|
|
35715
|
+
const gameDate = r.wstring("game date");
|
|
35716
|
+
const raceEditorId = r.wstring("race");
|
|
35717
|
+
const sexCode = r.u16("sex");
|
|
35718
|
+
const current = r.f32("experience");
|
|
35719
|
+
const toNextLevel = r.f32("experience to next level");
|
|
35720
|
+
const filetime = r.u64("save time");
|
|
35721
|
+
const width = r.u32("screenshot width");
|
|
35722
|
+
const height = r.u32("screenshot height");
|
|
35723
|
+
let compression = "none";
|
|
35724
|
+
if (version >= SE_VERSION) {
|
|
35725
|
+
const code = r.u16("compression");
|
|
35726
|
+
compression = code === 1 ? "zlib" : code === 2 ? "lz4" : "none";
|
|
35727
|
+
}
|
|
35728
|
+
if (r.offset - headerStart !== headerSize) {
|
|
35729
|
+
throw new SaveFormatError(`the header says ${headerSize} bytes but its fields take ${r.offset - headerStart}`);
|
|
35730
|
+
}
|
|
35731
|
+
if (width > 4096 || height > 4096) {
|
|
35732
|
+
throw new SaveFormatError(`a ${width}\xD7${height} screenshot is not a save's`);
|
|
35733
|
+
}
|
|
35734
|
+
const bytesPerPixel = version >= SE_VERSION ? 4 : 3;
|
|
35735
|
+
const pixels = r.take(width * height * bytesPerPixel, "screenshot");
|
|
35736
|
+
return {
|
|
35737
|
+
version,
|
|
35738
|
+
saveNumber,
|
|
35739
|
+
playerName,
|
|
35740
|
+
level,
|
|
35741
|
+
location,
|
|
35742
|
+
gameDate,
|
|
35743
|
+
playTimeMinutes: playTimeMinutes(gameDate),
|
|
35744
|
+
raceEditorId,
|
|
35745
|
+
race: raceName(raceEditorId),
|
|
35746
|
+
sex: sexCode === 1 ? "female" : "male",
|
|
35747
|
+
experience: { current, toNextLevel },
|
|
35748
|
+
savedAt: filetimeToIso(filetime),
|
|
35749
|
+
screenshot: { width, height, bytesPerPixel, pixels },
|
|
35750
|
+
compression,
|
|
35751
|
+
bodyOffset: r.offset
|
|
35752
|
+
};
|
|
35753
|
+
}
|
|
35754
|
+
function playTimeMinutes(gameDate) {
|
|
35755
|
+
const m = /^(\d+)\.(\d{1,2})\.(\d{1,2})$/.exec(gameDate.trim());
|
|
35756
|
+
if (!m)
|
|
35757
|
+
return null;
|
|
35758
|
+
return Number(m[1]) * 60 + Number(m[2]);
|
|
35759
|
+
}
|
|
35760
|
+
function raceName(editorId) {
|
|
35761
|
+
const base = editorId.replace(/Vampire$/, "").replace(/Race$/, "");
|
|
35762
|
+
return base.replace(/([a-z])([A-Z])/g, "$1 $2") || editorId;
|
|
35763
|
+
}
|
|
35764
|
+
function filetimeToIso(filetime) {
|
|
35765
|
+
if (filetime <= FILETIME_UNIX_EPOCH)
|
|
35766
|
+
return null;
|
|
35767
|
+
const ms = Number((filetime - FILETIME_UNIX_EPOCH) / 10000n);
|
|
35768
|
+
const date = new Date(ms);
|
|
35769
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
35770
|
+
}
|
|
35771
|
+
// ../../packages/skyrim-save/src/png.ts
|
|
35772
|
+
import { deflateSync } from "zlib";
|
|
35773
|
+
var SIGNATURE = Uint8Array.of(137, 80, 78, 71, 13, 10, 26, 10);
|
|
35774
|
+
function screenshotToPng(shot) {
|
|
35775
|
+
const { width, height, bytesPerPixel, pixels } = shot;
|
|
35776
|
+
if (width === 0 || height === 0)
|
|
35777
|
+
throw new RangeError("the save has no screenshot");
|
|
35778
|
+
const stride = width * bytesPerPixel;
|
|
35779
|
+
const raw = new Uint8Array((stride + 1) * height);
|
|
35780
|
+
for (let y = 0;y < height; y++) {
|
|
35781
|
+
raw.set(pixels.subarray(y * stride, (y + 1) * stride), y * (stride + 1) + 1);
|
|
35782
|
+
}
|
|
35783
|
+
const ihdr = new Uint8Array(13);
|
|
35784
|
+
const v = new DataView(ihdr.buffer);
|
|
35785
|
+
v.setUint32(0, width);
|
|
35786
|
+
v.setUint32(4, height);
|
|
35787
|
+
ihdr[8] = 8;
|
|
35788
|
+
ihdr[9] = bytesPerPixel === 4 ? 6 : 2;
|
|
35789
|
+
return concat([
|
|
35790
|
+
SIGNATURE,
|
|
35791
|
+
chunk("IHDR", ihdr),
|
|
35792
|
+
chunk("IDAT", new Uint8Array(deflateSync(raw))),
|
|
35793
|
+
chunk("IEND", new Uint8Array(0))
|
|
35794
|
+
]);
|
|
35795
|
+
}
|
|
35796
|
+
function chunk(type, data) {
|
|
35797
|
+
const out = new Uint8Array(12 + data.byteLength);
|
|
35798
|
+
const v = new DataView(out.buffer);
|
|
35799
|
+
v.setUint32(0, data.byteLength);
|
|
35800
|
+
for (let i = 0;i < 4; i++)
|
|
35801
|
+
out[4 + i] = type.charCodeAt(i);
|
|
35802
|
+
out.set(data, 8);
|
|
35803
|
+
v.setUint32(8 + data.byteLength, crc32(out.subarray(4, 8 + data.byteLength)));
|
|
35804
|
+
return out;
|
|
35805
|
+
}
|
|
35806
|
+
var table = null;
|
|
35807
|
+
function crc32(bytes) {
|
|
35808
|
+
if (!table) {
|
|
35809
|
+
table = new Uint32Array(256);
|
|
35810
|
+
for (let n = 0;n < 256; n++) {
|
|
35811
|
+
let c = n;
|
|
35812
|
+
for (let k = 0;k < 8; k++)
|
|
35813
|
+
c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
35814
|
+
table[n] = c >>> 0;
|
|
35815
|
+
}
|
|
35816
|
+
}
|
|
35817
|
+
let crc = 4294967295;
|
|
35818
|
+
for (const b of bytes)
|
|
35819
|
+
crc = table[(crc ^ b) & 255] ^ crc >>> 8;
|
|
35820
|
+
return (crc ^ 4294967295) >>> 0;
|
|
35821
|
+
}
|
|
35822
|
+
function concat(parts) {
|
|
35823
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.byteLength, 0));
|
|
35824
|
+
let at = 0;
|
|
35825
|
+
for (const p of parts) {
|
|
35826
|
+
out.set(p, at);
|
|
35827
|
+
at += p.byteLength;
|
|
35828
|
+
}
|
|
35829
|
+
return out;
|
|
35830
|
+
}
|
|
35831
|
+
// ../../packages/skyrim-save/src/save.ts
|
|
35832
|
+
function readSave(bytes) {
|
|
35833
|
+
const header = parseSaveHeader(bytes);
|
|
35834
|
+
let body = null;
|
|
35835
|
+
let bodyError = null;
|
|
35836
|
+
try {
|
|
35837
|
+
body = parseSaveBody(bytes, header);
|
|
35838
|
+
} catch (err) {
|
|
35839
|
+
if (!(err instanceof SaveFormatError))
|
|
35840
|
+
throw err;
|
|
35841
|
+
bodyError = err.message;
|
|
35842
|
+
}
|
|
35843
|
+
const shot = header.screenshot;
|
|
35844
|
+
return {
|
|
35845
|
+
header,
|
|
35846
|
+
body,
|
|
35847
|
+
bodyError,
|
|
35848
|
+
summary: summarize(header, body),
|
|
35849
|
+
screenshotPng: shot.width > 0 && shot.height > 0 ? screenshotToPng(shot) : null
|
|
35850
|
+
};
|
|
35851
|
+
}
|
|
35852
|
+
function summarize(header, body) {
|
|
35853
|
+
const time = body?.gameTime ?? null;
|
|
35854
|
+
return {
|
|
35855
|
+
version: header.version,
|
|
35856
|
+
saveNumber: header.saveNumber,
|
|
35857
|
+
playerName: header.playerName,
|
|
35858
|
+
level: header.level,
|
|
35859
|
+
location: header.location,
|
|
35860
|
+
race: header.race,
|
|
35861
|
+
sex: header.sex,
|
|
35862
|
+
playTimeMinutes: header.playTimeMinutes,
|
|
35863
|
+
savedAt: header.savedAt,
|
|
35864
|
+
gameDate: time ? {
|
|
35865
|
+
text: formatGameDate(time),
|
|
35866
|
+
hour: Math.round(time.hour * 100) / 100,
|
|
35867
|
+
daysPassed: Math.round(time.daysPassed * 100) / 100
|
|
35868
|
+
} : null,
|
|
35869
|
+
plugins: body?.plugins ?? [],
|
|
35870
|
+
lightPlugins: body?.lightPlugins ?? [],
|
|
35871
|
+
stats: Object.fromEntries((body?.stats ?? []).map((s) => [s.name, s.value]))
|
|
35872
|
+
};
|
|
35873
|
+
}
|
|
35874
|
+
// ../../packages/skyrim-save/src/newest.ts
|
|
35875
|
+
import { readdir, stat } from "fs/promises";
|
|
35876
|
+
import { join as join3 } from "path";
|
|
35877
|
+
async function newestSave(dir) {
|
|
35878
|
+
let best = null;
|
|
35879
|
+
for (const name of await readdir(dir)) {
|
|
35880
|
+
if (!name.toLowerCase().endsWith(".ess"))
|
|
35881
|
+
continue;
|
|
35882
|
+
const path = join3(dir, name);
|
|
35883
|
+
const info = await stat(path);
|
|
35884
|
+
if (!info.isFile())
|
|
35885
|
+
continue;
|
|
35886
|
+
if (!best || info.mtimeMs > best.at)
|
|
35887
|
+
best = { path, at: info.mtimeMs };
|
|
35888
|
+
}
|
|
35889
|
+
return best?.path ?? null;
|
|
35890
|
+
}
|
|
35891
|
+
// src/save-sync.ts
|
|
35892
|
+
async function resolveSave(path) {
|
|
35893
|
+
const info = await stat2(path).catch(() => null);
|
|
35894
|
+
if (!info)
|
|
35895
|
+
throw usageError(`no such file or folder: ${path}`);
|
|
35896
|
+
if (!info.isDirectory())
|
|
35897
|
+
return path;
|
|
35898
|
+
const newest = await newestSave(path);
|
|
35899
|
+
if (!newest)
|
|
35900
|
+
throw usageError(`no .ess save in ${path}`);
|
|
35901
|
+
return newest;
|
|
35902
|
+
}
|
|
35903
|
+
async function saveUpload(path) {
|
|
35904
|
+
const save = readSave(new Uint8Array(await readFile(path)));
|
|
35905
|
+
const { version: _version, lightPlugins, plugins, ...summary } = save.summary;
|
|
35906
|
+
return uploadRunSaveHeaderInputSchema.parse({
|
|
35907
|
+
...summary,
|
|
35908
|
+
fileName: basename(path),
|
|
35909
|
+
plugins: [...plugins, ...lightPlugins].slice(0, 600),
|
|
35910
|
+
screenshot: save.screenshotPng ? Buffer.from(save.screenshotPng).toString("base64") : null
|
|
35911
|
+
});
|
|
35912
|
+
}
|
|
35913
|
+
async function watchSaves(opts) {
|
|
35914
|
+
const interval = opts.intervalMs ?? 1e4;
|
|
35915
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
35916
|
+
let seen = null;
|
|
35917
|
+
let sent = null;
|
|
35918
|
+
while (!opts.signal?.aborted) {
|
|
35919
|
+
try {
|
|
35920
|
+
const path = await newestSave(opts.dir);
|
|
35921
|
+
if (path) {
|
|
35922
|
+
const info = await stat2(path);
|
|
35923
|
+
const mark = `${path}|${info.mtimeMs}|${info.size}`;
|
|
35924
|
+
if (mark === seen && mark !== sent) {
|
|
35925
|
+
await opts.onSave(path);
|
|
35926
|
+
sent = mark;
|
|
35927
|
+
}
|
|
35928
|
+
seen = mark;
|
|
35929
|
+
}
|
|
35930
|
+
} catch (err) {
|
|
35931
|
+
opts.onError?.(err);
|
|
35932
|
+
}
|
|
35933
|
+
if (opts.signal?.aborted)
|
|
35934
|
+
break;
|
|
35935
|
+
await sleep(interval);
|
|
35936
|
+
}
|
|
35937
|
+
}
|
|
35938
|
+
|
|
35291
35939
|
// src/today.ts
|
|
35292
35940
|
var OPEN_STATUSES = new Set(["todo", "in_progress"]);
|
|
35293
35941
|
function isTaskLike(value) {
|
|
@@ -35864,7 +36512,7 @@ var HANDLERS = {
|
|
|
35864
36512
|
});
|
|
35865
36513
|
if (out !== undefined) {
|
|
35866
36514
|
for (const file of data.files) {
|
|
35867
|
-
const target =
|
|
36515
|
+
const target = join4(out, file.path);
|
|
35868
36516
|
mkdirSync2(dirname2(target), { recursive: true });
|
|
35869
36517
|
writeFileSync2(target, file.markdown);
|
|
35870
36518
|
}
|
|
@@ -36050,7 +36698,7 @@ var HANDLERS = {
|
|
|
36050
36698
|
} catch (cause) {
|
|
36051
36699
|
throw usageError(`can't read ${path}: ${cause instanceof Error ? cause.message : cause}`);
|
|
36052
36700
|
}
|
|
36053
|
-
const name = flagString(flags, "title") ?? flagString(flags, "name") ??
|
|
36701
|
+
const name = flagString(flags, "title") ?? flagString(flags, "name") ?? basename2(path);
|
|
36054
36702
|
const form = new FormData;
|
|
36055
36703
|
form.append("nodeId", node);
|
|
36056
36704
|
const fields = {
|
|
@@ -36077,7 +36725,7 @@ var HANDLERS = {
|
|
|
36077
36725
|
"files download": async ({ args, flags, requireClient }) => {
|
|
36078
36726
|
const id = requireArg(args, 0, "file id");
|
|
36079
36727
|
const file = await requireClient().getBytes(`/files/${id}/download`);
|
|
36080
|
-
const out = flagString(flags, "out") ??
|
|
36728
|
+
const out = flagString(flags, "out") ?? basename2(file.name ?? id);
|
|
36081
36729
|
writeFileSync2(out, file.bytes);
|
|
36082
36730
|
return {
|
|
36083
36731
|
data: { id, saved: out, size: file.bytes.byteLength, type: file.contentType },
|
|
@@ -36144,7 +36792,7 @@ var HANDLERS = {
|
|
|
36144
36792
|
if (value !== undefined)
|
|
36145
36793
|
form.append(key, value);
|
|
36146
36794
|
}
|
|
36147
|
-
const name =
|
|
36795
|
+
const name = basename2(path);
|
|
36148
36796
|
const type = flagString(flags, "type") ?? Bun.file(name).type;
|
|
36149
36797
|
form.append("file", new File([Uint8Array.from(bytes)], name, { type }), name);
|
|
36150
36798
|
}
|
|
@@ -37501,7 +38149,7 @@ var HANDLERS = {
|
|
|
37501
38149
|
const since = flagString(flags, "since");
|
|
37502
38150
|
if (since !== undefined)
|
|
37503
38151
|
form.append("since", since);
|
|
37504
|
-
const name =
|
|
38152
|
+
const name = basename2(path);
|
|
37505
38153
|
form.append("file", new File([Uint8Array.from(bytes)], name, { type: "text/csv" }), name);
|
|
37506
38154
|
const route = flagBool(flags, "commit") ? "/money/imports" : "/money/imports/preview";
|
|
37507
38155
|
return { data: await requireClient().upload(route, form), render: renderRecord };
|
|
@@ -37705,6 +38353,37 @@ var HANDLERS = {
|
|
|
37705
38353
|
const input = parseInput(applyRunChangesInputSchema, body);
|
|
37706
38354
|
return { data: await requireClient().post(`/runs/${id}/changes`, input), render: renderRecord };
|
|
37707
38355
|
},
|
|
38356
|
+
"runs save-header": async ({ args, flags, io, requireClient }) => {
|
|
38357
|
+
const id = requireArg(args, 0, "run id");
|
|
38358
|
+
const target = requireArg(args, 1, "save file or Saves folder");
|
|
38359
|
+
const dryRun = flagBool(flags, "dry-run");
|
|
38360
|
+
const send = async (path) => {
|
|
38361
|
+
const input = await saveUpload(path);
|
|
38362
|
+
if (dryRun)
|
|
38363
|
+
return { ...input, screenshot: input.screenshot ? "(png)" : null };
|
|
38364
|
+
return requireClient().post(`/runs/${id}/save-header`, input);
|
|
38365
|
+
};
|
|
38366
|
+
if (!flagBool(flags, "watch")) {
|
|
38367
|
+
return { data: await send(await resolveSave(target)), render: renderRecord };
|
|
38368
|
+
}
|
|
38369
|
+
const seconds = Number(flagString(flags, "interval") ?? "10");
|
|
38370
|
+
if (!Number.isFinite(seconds) || seconds < 1)
|
|
38371
|
+
throw usageError("--interval is seconds, 1 or more");
|
|
38372
|
+
io.stderr(`watching ${target} for new saves (every ${seconds}s); Ctrl-C stops
|
|
38373
|
+
`);
|
|
38374
|
+
await watchSaves({
|
|
38375
|
+
dir: target,
|
|
38376
|
+
intervalMs: seconds * 1000,
|
|
38377
|
+
onSave: async (path) => {
|
|
38378
|
+
const result = await send(path);
|
|
38379
|
+
io.stderr(`${result.unchanged ? "already had" : dryRun ? "read" : "sent"} ${basename2(path)}
|
|
38380
|
+
`);
|
|
38381
|
+
},
|
|
38382
|
+
onError: (err) => io.stderr(`save sync: ${err.message}
|
|
38383
|
+
`)
|
|
38384
|
+
});
|
|
38385
|
+
return { data: null, render: () => "" };
|
|
38386
|
+
},
|
|
37708
38387
|
"runs undo": async ({ args, flags, requireClient }) => {
|
|
37709
38388
|
const id = requireArg(args, 0, "run id");
|
|
37710
38389
|
const batch = flagString(flags, "batch");
|
|
@@ -37894,7 +38573,7 @@ var HANDLERS = {
|
|
|
37894
38573
|
} catch (cause) {
|
|
37895
38574
|
throw usageError(`can't read ${path}: ${cause instanceof Error ? cause.message : cause}`);
|
|
37896
38575
|
}
|
|
37897
|
-
const name =
|
|
38576
|
+
const name = basename2(path);
|
|
37898
38577
|
const form = new FormData;
|
|
37899
38578
|
const textFile = flagString(flags, "text-file");
|
|
37900
38579
|
if (textFile)
|
|
@@ -38101,7 +38780,7 @@ var HANDLERS = {
|
|
|
38101
38780
|
form.append("sourceName", fields.sourceName);
|
|
38102
38781
|
if (fields.sourceEmail)
|
|
38103
38782
|
form.append("sourceEmail", fields.sourceEmail);
|
|
38104
|
-
const name =
|
|
38783
|
+
const name = basename2(path);
|
|
38105
38784
|
form.append("file", new File([Uint8Array.from(bytes)], name, { type: "text/plain" }), name);
|
|
38106
38785
|
return { data: await requireClient().upload("/family-tree", form), render: renderRecord };
|
|
38107
38786
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robbeverhelst/do",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.284.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "do — the agent-drivable CLI over the public /api/v1 surface",
|
|
6
6
|
"homepage": "https://github.com/robbeverhelst/do/tree/main/apps/cli",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@do/schema": "workspace:*",
|
|
36
|
+
"@do/skyrim-save": "workspace:*",
|
|
36
37
|
"@types/bun": "^1.4.3",
|
|
37
38
|
"@types/node": "^25.9.3",
|
|
38
39
|
"ulidx": "^2.4.1",
|