Skip to content

Commit 4ba2ca3

Browse files
committed
Make interface custom and typesafe
1 parent a536702 commit 4ba2ca3

File tree

6 files changed

+125
-101
lines changed

6 files changed

+125
-101
lines changed

src/cmd/upgrade.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
import { UpgradeCommand } from "@cliffy/command/upgrade";
22
import { JsrProvider } from "@cliffy/command/upgrade/provider/jsr";
3-
import {
4-
JSR_ENTRY_NAME,
5-
SAW_AS_LATEST_VERSION,
6-
VT_MINIMUM_FLAGS,
7-
} from "~/consts.ts";
3+
import { JSR_ENTRY_NAME, VT_MINIMUM_FLAGS } from "~/consts.ts";
84
import manifest from "../../deno.json" with { type: "json" };
95
import { colors } from "@cliffy/ansi/colors";
10-
import { vtState } from "~/vt/VTState.ts";
6+
import { vtCheckCache } from "~/vt/VTCheckCache.ts";
117

128
const provider = new JsrProvider({ package: JSR_ENTRY_NAME });
139

@@ -16,17 +12,20 @@ export async function registerOutdatedWarning() {
1612
const list = await provider.getVersions(JSR_ENTRY_NAME);
1713
const currentVersion = manifest.version;
1814
if (list.latest !== currentVersion) {
19-
const lastSawAsLatestVersion = await vtState.getItem(SAW_AS_LATEST_VERSION);
15+
const lastSawAsLatestVersion = await vtCheckCache
16+
.getLastSawAsLatestVersion();
2017
if (lastSawAsLatestVersion !== list.latest) {
2118
addEventListener("unload", async () => {
2219
// The last thing logged
2320
if (Deno.args.includes("upgrade")) return; // Don't show when they are upgrading
2421

25-
await vtState.setItem(SAW_AS_LATEST_VERSION, currentVersion);
22+
await vtCheckCache.setLastSawAsLatestVersion(currentVersion);
2623
console.log(
27-
`A new version of vt is available: ${colors.bold(
28-
list.latest,
29-
)}! Run \`${colors.bold("vt upgrade")}\` to update.`,
24+
`A new version of vt is available: ${
25+
colors.bold(
26+
list.latest,
27+
)
28+
}! Run \`${colors.bold("vt upgrade")}\` to update.`,
3029
);
3130
});
3231
}

src/consts.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const META_IGNORE_FILE_NAME = ".vtignore";
2121
export const GLOBAL_VT_CONFIG_PATH = join(xdg.config(), PROGRAM_NAME);
2222
export const GLOBAL_VT_META_FILE_PATH = join(
2323
GLOBAL_VT_CONFIG_PATH,
24-
"state.json",
24+
"upgrade-status.json",
2525
);
2626

2727
export const DEFAULT_WRAP_WIDTH = 80;

src/vt/VTCheckCache.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type z from "zod";
2+
import {
3+
AUTH_CACHE_LOCALSTORE_ENTRY,
4+
GLOBAL_VT_META_FILE_PATH,
5+
SAW_AS_LATEST_VERSION,
6+
} from "../consts.ts";
7+
import { VTCheckCacheFile } from "~/vt/vt/schemas.ts";
8+
9+
/**
10+
* Cheap singleton that takes the place of localStorage but writes to a
11+
* file within the global configuration directory.
12+
*
13+
* Used for caching the results of authentication and upgrade checks.
14+
*/
15+
class VTCheckCache {
16+
async #read() {
17+
try {
18+
const text = await Deno.readTextFile(GLOBAL_VT_META_FILE_PATH);
19+
const json = JSON.parse(text);
20+
return VTCheckCacheFile.parse(json);
21+
} catch {
22+
return {};
23+
}
24+
}
25+
async getAuthChecked() {
26+
return (await this.#read())[AUTH_CACHE_LOCALSTORE_ENTRY];
27+
}
28+
async getLastSawAsLatestVersion() {
29+
return (await this.#read())[SAW_AS_LATEST_VERSION];
30+
}
31+
async setAuthCheckedToNow() {
32+
return await this.setItem(
33+
AUTH_CACHE_LOCALSTORE_ENTRY,
34+
new Date().toISOString(),
35+
);
36+
}
37+
async setLastSawAsLatestVersion(version: string) {
38+
return await this.setItem(SAW_AS_LATEST_VERSION, version);
39+
}
40+
async setItem(key: keyof z.infer<typeof VTCheckCacheFile>, value: string) {
41+
const before = await this.#read();
42+
await Deno.writeTextFile(
43+
GLOBAL_VT_META_FILE_PATH,
44+
JSON.stringify({
45+
...before,
46+
[key]: value,
47+
}),
48+
);
49+
}
50+
}
51+
52+
export const vtCheckCache = new VTCheckCache();

src/vt/VTState.ts

Lines changed: 0 additions & 37 deletions
This file was deleted.

src/vt/vt/schemas.ts

Lines changed: 57 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { join } from "@std/path";
22
import { z } from "zod";
33
import {
4+
AUTH_CACHE_LOCALSTORE_ENTRY,
45
DEFAULT_EDITOR_TEMPLATE,
56
GLOBAL_VT_CONFIG_PATH,
67
META_IGNORE_FILE_NAME,
8+
SAW_AS_LATEST_VERSION,
79
} from "~/consts.ts";
810

911
/**
@@ -12,57 +14,68 @@ import {
1214
* Contains required metadata for operations that require context about the val
1315
* town directory you are in, like the Val that it represents.
1416
*/
15-
export const VTStateSchema = z.object({
16-
project: z.object({
17-
id: z.string().uuid(),
18-
}).optional(),
19-
val: z.object({
20-
id: z.string().uuid(),
21-
// Project -> Val migration: This lets old meta.jsons that have a project.id
22-
// still parse, and we transform them below. That means this ID is always
23-
// populated.
24-
}).catch({ id: "" }),
25-
branch: z.object({
26-
id: z.string().uuid(),
27-
version: z.number().gte(0),
28-
}),
29-
lastRun: z.object({
30-
pid: z.number().gte(0),
31-
time: z.string().refine((val) => !isNaN(Date.parse(val)), {}),
32-
}),
33-
}).transform((data) => {
34-
const result = { ...data };
35-
if (data.project) {
36-
result.val = structuredClone(data.project);
37-
delete result.project;
38-
}
39-
return result;
40-
}); // Silently inject the Val field, to prepare for future migration
17+
export const VTStateSchema = z
18+
.object({
19+
project: z
20+
.object({
21+
id: z.string().uuid(),
22+
})
23+
.optional(),
24+
val: z
25+
.object({
26+
id: z.string().uuid(),
27+
// Project -> Val migration: This lets old meta.jsons that have a project.id
28+
// still parse, and we transform them below. That means this ID is always
29+
// populated.
30+
})
31+
.catch({ id: "" }),
32+
branch: z.object({
33+
id: z.string().uuid(),
34+
version: z.number().gte(0),
35+
}),
36+
lastRun: z.object({
37+
pid: z.number().gte(0),
38+
time: z.string().refine((val) => !isNaN(Date.parse(val)), {}),
39+
}),
40+
})
41+
.transform((data) => {
42+
const result = { ...data };
43+
if (data.project) {
44+
result.val = structuredClone(data.project);
45+
delete result.project;
46+
}
47+
return result;
48+
}); // Silently inject the Val field, to prepare for future migration
4149

4250
/**
4351
* JSON schema for the config.yaml file for configuration storage.
4452
*/
4553
export const VTConfigSchema = z.object({
46-
apiKey: z.string()
54+
apiKey: z
55+
.string()
4756
.refine((val) => val === null || val.length === 32 || val.length === 33, {
4857
message: "API key must be 32-33 characters long when provided",
4958
})
5059
.nullable(),
51-
globalIgnoreFiles: z.preprocess(
52-
(input) => {
60+
globalIgnoreFiles: z
61+
.preprocess((input) => {
5362
if (typeof input === "string") {
54-
return input.split(",").map((s) => s.trim()).filter(Boolean);
63+
return input
64+
.split(",")
65+
.map((s) => s.trim())
66+
.filter(Boolean);
5567
}
5668
return input;
57-
},
58-
z.array(z.string()),
59-
).optional(),
60-
dangerousOperations: z.object({
61-
confirmation: z.union([
62-
z.boolean(),
63-
z.enum(["true", "false"]).transform((val) => val === "true"),
64-
]),
65-
}).optional(),
69+
}, z.array(z.string()))
70+
.optional(),
71+
dangerousOperations: z
72+
.object({
73+
confirmation: z.union([
74+
z.boolean(),
75+
z.enum(["true", "false"]).transform((val) => val === "true"),
76+
]),
77+
})
78+
.optional(),
6679
editorTemplate: z.string().optional(), // a Val URI
6780
});
6881

@@ -74,3 +87,8 @@ export const DefaultVTConfig: z.infer<typeof VTConfigSchema> = {
7487
},
7588
editorTemplate: DEFAULT_EDITOR_TEMPLATE,
7689
};
90+
91+
export const VTCheckCacheFile = z.object({
92+
[SAW_AS_LATEST_VERSION]: z.string().optional(),
93+
[AUTH_CACHE_LOCALSTORE_ENTRY]: z.coerce.date().optional(),
94+
});

vt.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,20 @@
22
import "@std/dotenv/load";
33
import { ensureGlobalVtConfig, globalConfig } from "~/vt/VTConfig.ts";
44
import { onboardFlow } from "~/cmd/flows/onboard.ts";
5-
import {
6-
API_KEY_KEY,
7-
AUTH_CACHE_LOCALSTORE_ENTRY,
8-
AUTH_CACHE_TTL,
9-
} from "~/consts.ts";
5+
import { API_KEY_KEY, AUTH_CACHE_TTL } from "~/consts.ts";
106
import { colors } from "@cliffy/ansi/colors";
117
import sdk from "~/sdk.ts";
128
import { registerOutdatedWarning } from "~/cmd/upgrade.ts";
13-
import { vtState } from "~/vt/VTState.ts";
9+
import { vtCheckCache } from "~/vt/VTCheckCache.ts";
1410

1511
await ensureGlobalVtConfig();
1612

1713
async function isApiKeyValid(): Promise<boolean> {
1814
// Since we run this on every invocation of vt, it makes sense to only check
1915
// if the api key is still valid every so often.
20-
21-
const lastAuthAt = await vtState.getItem(AUTH_CACHE_LOCALSTORE_ENTRY);
16+
const lastAuthAt = await vtCheckCache.getAuthChecked();
2217
const hoursSinceLastAuth = lastAuthAt
23-
? new Date().getTime() - new Date(lastAuthAt).getTime()
18+
? new Date().getTime() - lastAuthAt.getTime()
2419
: Infinity;
2520
if (hoursSinceLastAuth < AUTH_CACHE_TTL) return true;
2621

@@ -32,10 +27,7 @@ async function isApiKeyValid(): Promise<boolean> {
3227
});
3328

3429
if (resp.ok) {
35-
await vtState.setItem(
36-
AUTH_CACHE_LOCALSTORE_ENTRY,
37-
new Date().toISOString(),
38-
);
30+
await vtCheckCache.setAuthCheckedToNow();
3931
return true;
4032
}
4133

0 commit comments

Comments
 (0)