gitdog 0.0.0-stage → 0.1.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.
@@ -0,0 +1,55 @@
1
+ import { type Ui } from "./ui.js";
2
+ import type { Api } from "./client.js";
3
+ export declare const BOOLEAN_SETTINGS: readonly ["issueAssistantEnabled", "autoFormatIssue", "autoAnswerQuestions", "tagMaintainersForCodeChanges", "requireApproval", "discussionAssistantEnabled", "autoFormatDiscussion", "autoAnswerDiscussionQuestions", "tagMaintainersForDiscussionCodeChanges", "requireApprovalForDiscussions"];
4
+ export declare const STYLE_SETTINGS: readonly ["issueResponseStyle", "discussionResponseStyle"];
5
+ export type Settings = Record<(typeof BOOLEAN_SETTINGS)[number], boolean> & Partial<Record<(typeof STYLE_SETTINGS)[number], string>>;
6
+ export type Project = {
7
+ repository: string;
8
+ settings: Partial<Settings>;
9
+ };
10
+ export type ProjectConfig = {
11
+ version: 1;
12
+ defaultProject: string;
13
+ projects: Record<string, Project>;
14
+ };
15
+ export type Repository = {
16
+ _id: string;
17
+ githubRepo: string;
18
+ settings: Settings;
19
+ isActive: boolean;
20
+ [key: string]: unknown;
21
+ };
22
+ export declare function repositoryName(value: string): string;
23
+ export declare function aliasName(value: string): string;
24
+ export declare function validateSettings(value: unknown, complete?: boolean): Partial<Settings>;
25
+ export declare function validateProjectConfig(value: unknown): ProjectConfig;
26
+ export declare function findConfig(explicit?: string, cwd?: string): Promise<string | undefined>;
27
+ export declare class ProjectContext {
28
+ readonly api: Api;
29
+ readonly ui: Ui;
30
+ readonly selected?: string | undefined;
31
+ readonly configOption?: string | undefined;
32
+ constructor(api: Api, ui: Ui, selected?: string | undefined, configOption?: string | undefined);
33
+ config(): Promise<{
34
+ path: string;
35
+ value: ProjectConfig;
36
+ }>;
37
+ target(override?: string): Promise<string>;
38
+ repository(override?: string): Promise<Repository>;
39
+ available(reconnectRepositoryId?: string): Promise<Record<string, unknown>[]>;
40
+ connect(name: string, reconnectRepositoryId?: string): Promise<Repository>;
41
+ init(name?: string, alias?: string, force?: boolean): Promise<void>;
42
+ use(selection?: string, add?: string, list?: boolean): Promise<void>;
43
+ localProject(): Promise<{
44
+ path: string;
45
+ value: ProjectConfig;
46
+ alias: string;
47
+ project: Project;
48
+ }>;
49
+ configGet(key?: string, remote?: boolean): Promise<void>;
50
+ configSet(key: string, text: string): Promise<void>;
51
+ configPull(force?: boolean): Promise<void>;
52
+ configPush(dryRun?: boolean, yes?: boolean): Promise<void>;
53
+ }
54
+ export declare function chooseProject(ui: Ui, api: Api): Promise<string>;
55
+ export declare const limitParser: (value: string) => number;
@@ -0,0 +1,349 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { exists, readJson, writeJson } from "./storage.js";
5
+ import { asRecord, integer } from "./ui.js";
6
+ export const BOOLEAN_SETTINGS = [
7
+ "issueAssistantEnabled",
8
+ "autoFormatIssue",
9
+ "autoAnswerQuestions",
10
+ "tagMaintainersForCodeChanges",
11
+ "requireApproval",
12
+ "discussionAssistantEnabled",
13
+ "autoFormatDiscussion",
14
+ "autoAnswerDiscussionQuestions",
15
+ "tagMaintainersForDiscussionCodeChanges",
16
+ "requireApprovalForDiscussions",
17
+ ];
18
+ export const STYLE_SETTINGS = [
19
+ "issueResponseStyle",
20
+ "discussionResponseStyle",
21
+ ];
22
+ export function repositoryName(value) {
23
+ if (!/^[a-z\d](?:[a-z\d-]*[a-z\d])?\/[\w.-]+$/i.test(value) ||
24
+ value.endsWith("/.") ||
25
+ value.endsWith("/.."))
26
+ throw new Error("Use a GitHub repository name such as owner/repo.");
27
+ return value;
28
+ }
29
+ export function aliasName(value) {
30
+ if (!/^[a-z\d][a-z\d_-]{0,63}$/i.test(value) ||
31
+ ["__proto__", "constructor", "prototype"].includes(value))
32
+ throw new Error("Project aliases must use letters, numbers, underscores or hyphens (1–64 characters).");
33
+ return value;
34
+ }
35
+ export function validateSettings(value, complete = false) {
36
+ const record = asRecord(value);
37
+ if (!record)
38
+ throw new Error("Settings must be a JSON object.");
39
+ const settings = {};
40
+ for (const [key, item] of Object.entries(record)) {
41
+ if (BOOLEAN_SETTINGS.includes(key)) {
42
+ if (typeof item !== "boolean")
43
+ throw new Error(`${key} must be true or false.`);
44
+ }
45
+ else if (STYLE_SETTINGS.includes(key)) {
46
+ if (typeof item !== "string")
47
+ throw new Error(`${key} must be a string.`);
48
+ }
49
+ else
50
+ throw new Error(`Unknown setting: ${key}. Run gitdog config keys.`);
51
+ settings[key] = item;
52
+ }
53
+ if (complete &&
54
+ BOOLEAN_SETTINGS.some((key) => typeof settings[key] !== "boolean"))
55
+ throw new Error("The server returned incomplete project settings. Update Gitdog before applying settings.");
56
+ return settings;
57
+ }
58
+ export function validateProjectConfig(value) {
59
+ const config = asRecord(value);
60
+ const projects = asRecord(config?.projects);
61
+ if (config?.version !== 1 ||
62
+ typeof config.defaultProject !== "string" ||
63
+ !projects ||
64
+ !Object.hasOwn(projects, config.defaultProject))
65
+ throw new Error("Invalid gitdog.json. Run gitdog init in a new directory, or repair the project configuration.");
66
+ const result = {};
67
+ for (const [alias, value] of Object.entries(projects)) {
68
+ aliasName(alias);
69
+ const project = asRecord(value);
70
+ if (typeof project?.repository !== "string")
71
+ throw new Error(`Missing repository for ${alias}.`);
72
+ result[alias] = {
73
+ repository: repositoryName(project.repository),
74
+ settings: validateSettings(project.settings ?? {}),
75
+ };
76
+ }
77
+ return {
78
+ version: 1,
79
+ defaultProject: config.defaultProject,
80
+ projects: result,
81
+ };
82
+ }
83
+ export async function findConfig(explicit, cwd = process.cwd()) {
84
+ if (explicit)
85
+ return resolve(cwd, explicit);
86
+ let directory = resolve(cwd);
87
+ while (true) {
88
+ const path = join(directory, "gitdog.json");
89
+ if (await exists(path))
90
+ return path;
91
+ const parent = dirname(directory);
92
+ if (parent === directory)
93
+ return undefined;
94
+ directory = parent;
95
+ }
96
+ }
97
+ export class ProjectContext {
98
+ api;
99
+ ui;
100
+ selected;
101
+ configOption;
102
+ constructor(api, ui, selected, configOption) {
103
+ this.api = api;
104
+ this.ui = ui;
105
+ this.selected = selected;
106
+ this.configOption = configOption;
107
+ }
108
+ async config() {
109
+ const path = await findConfig(this.configOption);
110
+ if (!path)
111
+ throw new Error("No gitdog.json found. Run gitdog init, or pass --project owner/repo.");
112
+ const value = await readJson(path);
113
+ if (value === undefined)
114
+ throw new Error(`No project configuration at ${path}. Run gitdog init.`);
115
+ return { path, value: validateProjectConfig(value) };
116
+ }
117
+ async target(override) {
118
+ const selection = override ?? this.selected;
119
+ if (selection?.includes("/"))
120
+ return repositoryName(selection);
121
+ if (selection && /^[a-z0-9]{20,64}$/.test(selection)) {
122
+ const path = await findConfig(this.configOption);
123
+ const raw = path ? await readJson(path) : undefined;
124
+ if (!raw ||
125
+ !Object.hasOwn(validateProjectConfig(raw).projects, selection))
126
+ return selection;
127
+ }
128
+ const { value } = await this.config();
129
+ const alias = selection ?? value.defaultProject;
130
+ if (!Object.hasOwn(value.projects, alias))
131
+ throw new Error(`Unknown project alias ${alias}. Run gitdog use --list.`);
132
+ return value.projects[alias].repository;
133
+ }
134
+ async repository(override) {
135
+ const target = await this.target(override);
136
+ const repository = target.includes("/")
137
+ ? await this.api.call("query", "repositories/query:get", { owner: target.split("/")[0], repo: target.split("/")[1] })
138
+ : await this.api.call("query", "repositories/query:getById", { repositoryId: target });
139
+ if (!repository)
140
+ throw new Error(`Project ${target} is not connected or accessible. Run gitdog projects create ${target}.`);
141
+ return repository;
142
+ }
143
+ async available(reconnectRepositoryId) {
144
+ const repositories = [];
145
+ const seen = new Set();
146
+ let cursor;
147
+ for (let page = 0; page < 500; page++) {
148
+ const result = await this.api.call("action", "github/action:listUserRepositories", {
149
+ ...(cursor ? { cursor } : {}),
150
+ ...(reconnectRepositoryId ? { reconnectRepositoryId } : {}),
151
+ });
152
+ repositories.push(...result.repositories);
153
+ if (result.continueCursor === null)
154
+ return repositories;
155
+ if (seen.has(result.continueCursor))
156
+ throw new Error("GitHub returned a repeated repository page. Try again later.");
157
+ seen.add(result.continueCursor);
158
+ cursor = result.continueCursor;
159
+ }
160
+ throw new Error("Too many repository pages. Use projects available --cursor for explicit pagination.");
161
+ }
162
+ async connect(name, reconnectRepositoryId) {
163
+ repositoryName(name);
164
+ const [owner, repo] = name.split("/");
165
+ const existing = await this.api.call("query", "repositories/query:get", { owner, repo });
166
+ if (existing && !reconnectRepositoryId)
167
+ return existing;
168
+ const available = await this.available(reconnectRepositoryId);
169
+ const matches = available.filter((item) => String(item.fullName).toLowerCase() === name.toLowerCase());
170
+ if (matches.length !== 1)
171
+ throw new Error(`Repository ${name} is unavailable. Run gitdog github install, authorize it, then gitdog github recover. Check projects available.`);
172
+ const item = matches[0];
173
+ if (typeof item.installationId !== "number" || typeof item.id !== "number")
174
+ throw new Error("GitHub returned invalid repository identifiers.");
175
+ this.ui.info(`Connecting ${name} and scheduling repository indexing…`);
176
+ await this.api.call("action", "repositories/action:createAndIndex", {
177
+ installationId: item.installationId,
178
+ githubRepositoryId: item.id,
179
+ ...(reconnectRepositoryId ? { reconnectRepositoryId } : {}),
180
+ });
181
+ const result = await this.api.call("query", "repositories/query:get", { owner, repo });
182
+ if (!result)
183
+ throw new Error("The connection was accepted but is not readable yet. Check projects list before retrying.");
184
+ return result;
185
+ }
186
+ async init(name, alias = "default", force = false) {
187
+ this.ui.banner();
188
+ aliasName(alias);
189
+ const path = (await findConfig(this.configOption)) ??
190
+ join(process.cwd(), "gitdog.json");
191
+ const previous = await readJson(path);
192
+ if (previous !== undefined && !force)
193
+ throw new Error(`Project configuration already exists at ${path}. Use gitdog use --add, or --force to replace it.`);
194
+ if (!name) {
195
+ let detected;
196
+ try {
197
+ const { stdout } = await promisify(execFile)("git", ["remote", "get-url", "origin"], { timeout: 5000 });
198
+ const match = /^(?:git@github\.com:|https:\/\/github\.com\/|ssh:\/\/git@github\.com\/)([^\s]+?)\/?$/.exec(stdout.trim());
199
+ if (match)
200
+ detected = repositoryName(match[1].replace(/\.git$/, ""));
201
+ }
202
+ catch {
203
+ /* A project can be initialized outside a Git checkout. */
204
+ }
205
+ this.ui.info("1 / 3 Choose a GitHub repository");
206
+ name = await this.ui.ask("Repository (owner/repo)", detected);
207
+ }
208
+ this.ui.info("2 / 3 Connect your Gitdog project");
209
+ const repo = await this.connect(name);
210
+ const value = {
211
+ version: 1,
212
+ defaultProject: alias,
213
+ projects: {
214
+ [alias]: {
215
+ repository: repo.githubRepo,
216
+ settings: validateSettings(repo.settings, true),
217
+ },
218
+ },
219
+ };
220
+ this.ui.info("3 / 3 Save project settings");
221
+ await writeJson(path, value);
222
+ this.ui.output({
223
+ initialized: true,
224
+ project: repo.githubRepo,
225
+ alias,
226
+ config: path,
227
+ });
228
+ this.ui.info("✓ Ready. Try gitdog status, gitdog config get, or gitdog tickets list.");
229
+ }
230
+ async use(selection, add, list = false) {
231
+ const { path, value } = await this.config();
232
+ if (add) {
233
+ aliasName(add);
234
+ if (!selection)
235
+ throw new Error("Use gitdog use owner/repo --add alias.");
236
+ if (Object.hasOwn(value.projects, add))
237
+ throw new Error(`Alias ${add} already exists. Choose a new alias.`);
238
+ const repo = await this.repository(selection);
239
+ value.projects[add] = {
240
+ repository: repo.githubRepo,
241
+ settings: validateSettings(repo.settings, true),
242
+ };
243
+ value.defaultProject = add;
244
+ }
245
+ else if (selection) {
246
+ if (!Object.hasOwn(value.projects, selection))
247
+ throw new Error(`Unknown alias ${selection}. Use --add alias to add a project.`);
248
+ value.defaultProject = selection;
249
+ }
250
+ if (selection || add)
251
+ await writeJson(path, value);
252
+ this.ui.output(list
253
+ ? Object.entries(value.projects).map(([name, project]) => ({
254
+ name,
255
+ githubRepo: project.repository,
256
+ status: name === value.defaultProject ? "active" : "",
257
+ }))
258
+ : {
259
+ alias: value.defaultProject,
260
+ project: value.projects[value.defaultProject].repository,
261
+ });
262
+ }
263
+ async localProject() {
264
+ const { path, value } = await this.config();
265
+ const alias = this.selected ?? value.defaultProject;
266
+ const match = Object.hasOwn(value.projects, alias)
267
+ ? alias
268
+ : Object.keys(value.projects).find((key) => value.projects[key].repository.toLowerCase() ===
269
+ alias.toLowerCase());
270
+ if (!match)
271
+ throw new Error("This project has no local settings. Add it with gitdog use owner/repo --add alias.");
272
+ return { path, value, alias: match, project: value.projects[match] };
273
+ }
274
+ async configGet(key, remote = false) {
275
+ const settings = remote
276
+ ? (await this.repository()).settings
277
+ : (await this.localProject()).project.settings;
278
+ if (key && !Object.hasOwn(settings, key))
279
+ throw new Error(`Setting ${key} is not set.`);
280
+ this.ui.output(key ? settings[key] : settings);
281
+ }
282
+ async configSet(key, text) {
283
+ const value = BOOLEAN_SETTINGS.includes(key)
284
+ ? text === "true"
285
+ ? true
286
+ : text === "false"
287
+ ? false
288
+ : text
289
+ : text;
290
+ const patch = validateSettings({ [key]: value });
291
+ const local = await this.localProject();
292
+ local.project.settings = { ...local.project.settings, ...patch };
293
+ await writeJson(local.path, local.value);
294
+ this.ui.output({ saved: true, settings: patch });
295
+ this.ui.info("Saved locally. Run gitdog config push to apply.");
296
+ }
297
+ async configPull(force = false) {
298
+ const local = await this.localProject();
299
+ const settings = validateSettings((await this.repository(local.project.repository)).settings, true);
300
+ await this.ui.confirm("Replace local settings with server settings?", force);
301
+ local.project.settings = settings;
302
+ await writeJson(local.path, local.value);
303
+ this.ui.output({ pulled: true, settings });
304
+ }
305
+ async configPush(dryRun = false, yes = false) {
306
+ const local = await this.localProject();
307
+ const repo = await this.repository(local.project.repository);
308
+ const before = validateSettings(repo.settings, true);
309
+ const after = validateSettings({ ...before, ...local.project.settings }, true);
310
+ const changes = Object.entries(after)
311
+ .filter(([key, value]) => before[key] !== value)
312
+ .map(([key, value]) => ({
313
+ key,
314
+ before: before[key] ?? null,
315
+ after: value,
316
+ }));
317
+ if (!this.ui.json)
318
+ this.ui.output({ project: repo.githubRepo, changes });
319
+ if (dryRun || changes.length === 0) {
320
+ this.ui.output({
321
+ dryRun,
322
+ applied: false,
323
+ project: repo.githubRepo,
324
+ changes,
325
+ });
326
+ return;
327
+ }
328
+ await this.ui.confirm(`Apply ${changes.length} setting change(s) to ${repo.githubRepo}?`, yes);
329
+ await this.api.call("mutation", "repositories/mutation:updateSettings", {
330
+ repositoryId: repo._id,
331
+ settings: after,
332
+ });
333
+ const actual = validateSettings((await this.repository(local.project.repository)).settings, true);
334
+ if (Object.entries(after).some(([key, value]) => actual[key] !== value))
335
+ throw new Error("Settings changed during verification. Run config get --remote and compare before retrying.");
336
+ this.ui.output({
337
+ applied: true,
338
+ project: repo.githubRepo,
339
+ changes,
340
+ settings: actual,
341
+ });
342
+ }
343
+ }
344
+ export async function chooseProject(ui, api) {
345
+ const repos = await api.call("query", "repositories/query:list");
346
+ ui.output(repos);
347
+ return ui.ask("Repository to use (owner/repo)");
348
+ }
349
+ export const limitParser = (value) => integer(value, 1, 500);
@@ -0,0 +1,24 @@
1
+ export declare const DEFAULT_API_URL = "https://calm-hippopotamus-56.convex.cloud";
2
+ export declare const DEFAULT_SITE_URL = "https://gitdog.dev";
3
+ export type Tokens = {
4
+ token: string;
5
+ refreshToken: string;
6
+ };
7
+ export type Session = Tokens & {
8
+ apiUrl: string;
9
+ siteUrl: string;
10
+ };
11
+ export declare function configDirectory(): string;
12
+ export declare function readJson(path: string): Promise<unknown | undefined>;
13
+ export declare function writeJson(path: string, value: unknown, privateFile?: boolean, signal?: AbortSignal): Promise<void>;
14
+ export declare function validTokens(value: unknown): value is Tokens;
15
+ export declare function validateUrl(value: string, label: string): string;
16
+ export declare class SessionStore {
17
+ readonly path: string;
18
+ read(): Promise<Session | undefined>;
19
+ save(session: Session, signal?: AbortSignal): Promise<void>;
20
+ clear(): Promise<void>;
21
+ locked<T>(callback: () => Promise<T>, requestDeadline?: number, signal?: AbortSignal): Promise<T>;
22
+ }
23
+ export declare function tokenExpiresSoon(token: string): boolean;
24
+ export declare function exists(path: string): Promise<boolean>;
@@ -0,0 +1,147 @@
1
+ import { chmod, mkdir, open, readFile, rename, rm, stat, } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import { randomUUID } from "node:crypto";
5
+ import { asRecord } from "./ui.js";
6
+ export const DEFAULT_API_URL = "https://calm-hippopotamus-56.convex.cloud";
7
+ export const DEFAULT_SITE_URL = "https://gitdog.dev";
8
+ export function configDirectory() {
9
+ return (process.env.GITDOG_CONFIG_DIR ||
10
+ join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "gitdog"));
11
+ }
12
+ export async function readJson(path) {
13
+ try {
14
+ return JSON.parse(await readFile(path, "utf8"));
15
+ }
16
+ catch (error) {
17
+ if (error.code === "ENOENT")
18
+ return undefined;
19
+ throw new Error(`Cannot read ${path}. Check its permissions and JSON syntax.`);
20
+ }
21
+ }
22
+ export async function writeJson(path, value, privateFile = false, signal) {
23
+ signal?.throwIfAborted();
24
+ await mkdir(dirname(path), {
25
+ recursive: true,
26
+ mode: privateFile ? 0o700 : 0o755,
27
+ });
28
+ if (privateFile)
29
+ await chmod(dirname(path), 0o700);
30
+ const temporary = `${path}.${randomUUID()}.tmp`;
31
+ try {
32
+ const file = await open(temporary, "wx", privateFile ? 0o600 : 0o644);
33
+ try {
34
+ signal?.throwIfAborted();
35
+ await file.writeFile(`${JSON.stringify(value, null, 2)}\n`);
36
+ }
37
+ finally {
38
+ await file.close();
39
+ }
40
+ signal?.throwIfAborted();
41
+ await rename(temporary, path);
42
+ }
43
+ finally {
44
+ await rm(temporary, { force: true });
45
+ }
46
+ }
47
+ export function validTokens(value) {
48
+ const item = asRecord(value);
49
+ return (typeof item?.token === "string" &&
50
+ item.token.length > 0 &&
51
+ typeof item.refreshToken === "string" &&
52
+ item.refreshToken.length > 0);
53
+ }
54
+ export function validateUrl(value, label) {
55
+ let url;
56
+ try {
57
+ url = new URL(value);
58
+ }
59
+ catch {
60
+ throw new Error(`${label} must be an absolute URL.`);
61
+ }
62
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
63
+ if ((url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) ||
64
+ url.username ||
65
+ url.password ||
66
+ url.pathname !== "/" ||
67
+ url.search ||
68
+ url.hash) {
69
+ throw new Error(`${label} must be an HTTPS origin (HTTP is allowed for localhost development).`);
70
+ }
71
+ return url.origin;
72
+ }
73
+ export class SessionStore {
74
+ path = join(configDirectory(), "session.json");
75
+ async read() {
76
+ const value = await readJson(this.path);
77
+ if (value === undefined)
78
+ return undefined;
79
+ const item = asRecord(value);
80
+ if (!validTokens(value) ||
81
+ typeof item?.apiUrl !== "string" ||
82
+ typeof item.siteUrl !== "string")
83
+ throw new Error("Invalid saved session. Run gitdog logout, then gitdog login.");
84
+ return {
85
+ ...value,
86
+ apiUrl: validateUrl(item.apiUrl, "Saved API URL"),
87
+ siteUrl: validateUrl(item.siteUrl, "Saved site URL"),
88
+ };
89
+ }
90
+ async save(session, signal) {
91
+ await writeJson(this.path, session, true, signal);
92
+ }
93
+ async clear() {
94
+ await rm(this.path, { force: true });
95
+ }
96
+ async locked(callback, requestDeadline, signal) {
97
+ signal?.throwIfAborted();
98
+ await mkdir(dirname(this.path), { recursive: true, mode: 0o700 });
99
+ const path = `${this.path}.lock`;
100
+ const deadline = Math.min(Date.now() + 10_000, requestDeadline ?? Infinity);
101
+ let handle;
102
+ while (!handle) {
103
+ signal?.throwIfAborted();
104
+ if (Date.now() >= deadline)
105
+ throw new Error(`Authentication lock timed out. Check that another Gitdog process is not running; stale lock: ${path}.`);
106
+ try {
107
+ handle = await open(path, "wx", 0o600);
108
+ }
109
+ catch (error) {
110
+ if (error.code !== "EEXIST")
111
+ throw error;
112
+ if (Date.now() >= deadline)
113
+ throw new Error(`Another Gitdog process is updating authentication. Retry later; if it crashed, remove ${path}.`);
114
+ await new Promise((resolve) => setTimeout(resolve, Math.min(100, Math.max(1, deadline - Date.now()))));
115
+ }
116
+ }
117
+ try {
118
+ signal?.throwIfAborted();
119
+ return await callback();
120
+ }
121
+ finally {
122
+ await handle.close();
123
+ await rm(path, { force: true });
124
+ }
125
+ }
126
+ }
127
+ export function tokenExpiresSoon(token) {
128
+ try {
129
+ const payload = asRecord(JSON.parse(Buffer.from(token.split(".")[1], "base64url").toString("utf8")));
130
+ return (typeof payload?.exp !== "number" ||
131
+ payload.exp * 1000 < Date.now() + 60_000);
132
+ }
133
+ catch {
134
+ return true;
135
+ }
136
+ }
137
+ export async function exists(path) {
138
+ try {
139
+ await stat(path);
140
+ return true;
141
+ }
142
+ catch (error) {
143
+ if (error.code === "ENOENT")
144
+ return false;
145
+ throw error;
146
+ }
147
+ }
package/dist/ui.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ export declare const VERSION = "0.1.0";
2
+ export declare const DOG: string;
3
+ export declare function redactSecrets(value: string): string;
4
+ export declare function clean(value: string): string;
5
+ export declare class Ui {
6
+ readonly json: boolean;
7
+ constructor(json?: boolean);
8
+ color(text: string, code?: number): string;
9
+ banner(): void;
10
+ info(message: string): void;
11
+ output(value: unknown): void;
12
+ table(rows: Record<string, unknown>[], keys: string[]): void;
13
+ ask(question: string, fallback?: string): Promise<string>;
14
+ confirm(message: string, yes?: boolean): Promise<void>;
15
+ }
16
+ export declare function asRecord(value: unknown): Record<string, unknown> | undefined;
17
+ export declare function integer(value: string, minimum?: number, maximum?: number): number;