bet-cli 0.2.0 → 0.3.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.
@@ -3,7 +3,12 @@ import readline from "node:readline";
3
3
  import { Command } from "commander";
4
4
  import { readConfig, resolveRoots, writeConfig } from "../lib/config.js";
5
5
  import { normalizeAbsolute } from "../utils/paths.js";
6
- import { installUpdateCron, uninstallUpdateCron, parseCronSchedule, formatScheduleLabel } from "../lib/cron.js";
6
+ import {
7
+ installUpdateCron,
8
+ uninstallUpdateCron,
9
+ parseCronSchedule,
10
+ formatScheduleLabel,
11
+ } from "../lib/cron.js";
7
12
  import { scanRoots } from "../lib/scan.js";
8
13
  import { computeMetadata } from "../lib/metadata.js";
9
14
  import { getEffectiveIgnores, isPathIgnored } from "../lib/ignore.js";
@@ -30,16 +35,16 @@ export function willOverrideRoots(
30
35
  providedRootConfigs: RootConfig[] | undefined,
31
36
  configRoots: RootConfig[],
32
37
  ): boolean {
33
- return !!(
34
- providedRootConfigs !== undefined &&
35
- configRoots.length > 0
36
- );
38
+ return !!(providedRootConfigs !== undefined && configRoots.length > 0);
37
39
  }
38
40
 
39
41
  const DEFAULT_SLUG_PARENT_FOLDERS = ["src", "app"];
40
42
 
41
43
  export { DEFAULT_SLUG_PARENT_FOLDERS };
42
- export function projectSlug(pathName: string, slugParentFolders: string[]): string {
44
+ export function projectSlug(
45
+ pathName: string,
46
+ slugParentFolders: string[],
47
+ ): string {
43
48
  const folderName = path.basename(pathName);
44
49
  if (slugParentFolders.includes(folderName)) {
45
50
  return path.basename(path.dirname(pathName));
@@ -47,8 +52,14 @@ export function projectSlug(pathName: string, slugParentFolders: string[]): stri
47
52
  return folderName;
48
53
  }
49
54
 
50
- async function promptYesNo(question: string, defaultNo = true): Promise<boolean> {
51
- const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
55
+ async function promptYesNo(
56
+ question: string,
57
+ defaultNo = true,
58
+ ): Promise<boolean> {
59
+ const rl = readline.createInterface({
60
+ input: process.stdin,
61
+ output: process.stderr,
62
+ });
52
63
  return new Promise((resolve) => {
53
64
  const defaultHint = defaultNo ? "y/N" : "Y/n";
54
65
  rl.question(question + " [" + defaultHint + "] ", (answer) => {
@@ -69,166 +80,209 @@ export function registerUpdate(program: Command): void {
69
80
  .description("Scan roots and update the project index")
70
81
  .option("--roots <paths>", "Comma-separated list of roots to scan")
71
82
  .option("--force", "Allow overriding configured roots when not in TTY")
72
- .option("--cron [frequency]", "Run update on a schedule: Nm/Nh/Nd e.g. 5m, 1h, 2d (default 1h), or 0/false to disable")
73
- .action(async (options: { roots?: string; force?: boolean; cron?: boolean | string }) => {
74
- try {
75
- const config = await readConfig();
76
- const providedPaths = parseRoots(options.roots);
77
- const providedRootConfigs = providedPaths
78
- ? pathsToRootConfigs(providedPaths)
79
- : undefined;
80
- const configRoots = config.roots.length > 0 ? config.roots : undefined;
81
- const rootsToUse = providedRootConfigs ?? configRoots;
82
-
83
- if (!rootsToUse || rootsToUse.length === 0) {
84
- log.error("update failed: no roots specified");
85
- process.stderr.write(
86
- "Error: No roots specified. Please provide roots using --roots option.\n" +
87
- "Example: bet update --roots /path/to/your/code\n",
88
- );
89
- process.exitCode = 1;
90
- return;
91
- }
83
+ .option(
84
+ "--cron [frequency]",
85
+ "Run update on a schedule: Nm/Nh/Nd e.g. 5m, 1h, 2d (default 1h), or 0/false to disable",
86
+ )
87
+ .action(
88
+ async (options: {
89
+ roots?: string;
90
+ force?: boolean;
91
+ cron?: boolean | string;
92
+ }) => {
93
+ try {
94
+ const config = await readConfig();
95
+ const providedPaths = parseRoots(options.roots);
96
+ const providedRootConfigs = providedPaths
97
+ ? pathsToRootConfigs(providedPaths)
98
+ : undefined;
99
+ const configRoots =
100
+ config.roots.length > 0 ? config.roots : undefined;
101
+ const rootsToUse = providedRootConfigs ?? configRoots;
92
102
 
93
- const willOverride = willOverrideRoots(providedRootConfigs, config.roots);
94
-
95
- if (willOverride) {
96
- log.warn("--roots overrides configured roots", "configured:", configRoots!.map((r) => r.path).join(", "), "provided:", providedRootConfigs!.map((r) => r.path).join(", "));
97
- process.stderr.write(
98
- "Warning: --roots will override your configured roots.\n" +
99
- " Configured: " +
100
- configRoots!.map((r) => r.path).join(", ") +
101
- "\n Provided: " +
102
- providedRootConfigs!.map((r) => r.path).join(", ") +
103
- "\n",
104
- );
105
- if (!process.stdin.isTTY) {
106
- if (!options.force) {
107
- log.error("update failed: refusing to override roots without confirmation (use --force when not in TTY)");
103
+ if (!rootsToUse || rootsToUse.length === 0) {
104
+ log.error("update failed: no roots specified");
108
105
  process.stderr.write(
109
- "Error: Refusing to override without confirmation. Run interactively or use --force.\n",
106
+ "Error: No roots specified. Please provide roots using --roots option.\n" +
107
+ "Example: bet update --roots /path/to/your/code\n",
110
108
  );
111
109
  process.exitCode = 1;
112
110
  return;
113
111
  }
114
- } else {
115
- const confirmed = await promptYesNo("Continue?", true);
116
- if (!confirmed) {
117
- log.info("update aborted by user");
118
- process.stderr.write("Aborted.\n");
119
- return;
112
+
113
+ const willOverride = willOverrideRoots(
114
+ providedRootConfigs,
115
+ config.roots,
116
+ );
117
+
118
+ if (willOverride) {
119
+ log.warn(
120
+ "--roots overrides configured roots",
121
+ "configured:",
122
+ configRoots!.map((r) => r.path).join(", "),
123
+ "provided:",
124
+ providedRootConfigs!.map((r) => r.path).join(", "),
125
+ );
126
+ process.stderr.write(
127
+ "Warning: --roots will override your configured roots.\n" +
128
+ " Configured: " +
129
+ configRoots!.map((r) => r.path).join(", ") +
130
+ "\n Provided: " +
131
+ providedRootConfigs!.map((r) => r.path).join(", ") +
132
+ "\n",
133
+ );
134
+ if (!process.stdin.isTTY) {
135
+ if (!options.force) {
136
+ log.error(
137
+ "update failed: refusing to override roots without confirmation (use --force when not in TTY)",
138
+ );
139
+ process.stderr.write(
140
+ "Error: Refusing to override without confirmation. Run interactively or use --force.\n",
141
+ );
142
+ process.exitCode = 1;
143
+ return;
144
+ }
145
+ } else {
146
+ const confirmed = await promptYesNo("Continue?", true);
147
+ if (!confirmed) {
148
+ log.info("update aborted by user");
149
+ process.stderr.write("Aborted.\n");
150
+ return;
151
+ }
152
+ }
120
153
  }
121
- }
122
- }
123
154
 
124
- const rootsResolved = resolveRoots(rootsToUse);
125
- const rootPaths = rootsResolved.map((r) => r.path);
126
- log.info("update started", "roots=" + rootPaths.join(", "));
127
- log.debug("scanning roots", rootPaths.length, "root(s)");
128
- const ignores = getEffectiveIgnores(config);
129
- const candidates = await scanRoots(rootPaths, ignores);
130
- const ignoredPaths = config.ignoredPaths ?? [];
131
- const filteredCandidates = candidates.filter(
132
- (c) => !isPathIgnored(c.path, ignoredPaths),
133
- );
134
- log.debug("found", filteredCandidates.length, "candidate(s) after ignoring paths");
135
- const projects: Record<string, Project> = {};
136
-
137
- for (const candidate of filteredCandidates) {
138
- const hasGit = await isInsideGitRepo(candidate.path);
139
- const auto = await computeMetadata(candidate.path, hasGit, ignores);
140
- const slug = projectSlug(candidate.path, config.slugParentFolders ?? DEFAULT_SLUG_PARENT_FOLDERS);
141
- const existing = config.projects[candidate.path];
142
- const rootConfig = rootsResolved.find((r) => r.path === candidate.root);
143
- const rootName = rootConfig?.name ?? path.basename(candidate.root);
144
-
145
- const project: Project = {
146
- id: candidate.path,
147
- slug,
148
- name: slug,
149
- path: candidate.path,
150
- root: candidate.root,
151
- rootName,
152
- hasGit,
153
- hasReadme: candidate.hasReadme,
154
- auto,
155
- user: existing?.user,
156
- };
157
-
158
- projects[candidate.path] = project;
159
- }
155
+ const rootsResolved = resolveRoots(rootsToUse);
156
+ const rootPaths = rootsResolved.map((r) => r.path);
157
+ log.info("update started", "roots=" + rootPaths.join(", "));
158
+ log.debug("scanning roots", rootPaths.length, "root(s)");
159
+ const ignores = getEffectiveIgnores(config);
160
+ const candidates = await scanRoots(rootPaths, ignores);
161
+ const ignoredPaths = config.ignoredPaths ?? [];
162
+ const filteredCandidates = candidates.filter(
163
+ (c) => !isPathIgnored(c.path, ignoredPaths),
164
+ );
165
+ log.debug(
166
+ "found",
167
+ filteredCandidates.length,
168
+ "candidate(s) after ignoring paths",
169
+ );
170
+ const projects: Record<string, Project> = {};
171
+
172
+ for (const candidate of filteredCandidates) {
173
+ const hasGit = await isInsideGitRepo(candidate.path);
174
+ const auto = await computeMetadata(candidate.path, hasGit, ignores);
175
+ const slug = projectSlug(
176
+ candidate.path,
177
+ config.slugParentFolders ?? DEFAULT_SLUG_PARENT_FOLDERS,
178
+ );
179
+ const existing = config.projects[candidate.path];
180
+ const rootConfig = rootsResolved.find(
181
+ (r) => r.path === candidate.root,
182
+ );
183
+ const rootName = rootConfig?.name ?? path.basename(candidate.root);
184
+
185
+ const project: Project = {
186
+ id: candidate.path,
187
+ slug,
188
+ name: slug,
189
+ path: candidate.path,
190
+ root: candidate.root,
191
+ rootName,
192
+ hasGit,
193
+ hasReadme: candidate.hasReadme,
194
+ auto,
195
+ user: existing?.user,
196
+ };
197
+
198
+ projects[candidate.path] = project;
199
+ }
200
+
201
+ const nextConfig: Config = {
202
+ version: config.version ?? 1,
203
+ roots: rootsResolved,
204
+ projects,
205
+ updatedAt: new Date().toISOString(),
206
+ ...(config.ignores !== undefined && { ignores: config.ignores }),
207
+ ...(config.ignoredPaths !== undefined && {
208
+ ignoredPaths: config.ignoredPaths,
209
+ }),
210
+ ...(config.slugParentFolders !== undefined && {
211
+ slugParentFolders: config.slugParentFolders,
212
+ }),
213
+ };
214
+
215
+ await writeConfig(nextConfig);
216
+
217
+ const projectCount = Object.keys(projects).length;
218
+ const rootCount = rootsResolved.length;
219
+ log.info(
220
+ "update completed",
221
+ "projects=" + projectCount,
222
+ "roots=" + rootCount,
223
+ );
160
224
 
161
- const nextConfig: Config = {
162
- version: config.version ?? 1,
163
- roots: rootsResolved,
164
- projects,
165
- updatedAt: new Date().toISOString(),
166
- ...(config.ignores !== undefined && { ignores: config.ignores }),
167
- ...(config.ignoredPaths !== undefined && { ignoredPaths: config.ignoredPaths }),
168
- ...(config.slugParentFolders !== undefined && { slugParentFolders: config.slugParentFolders }),
169
- };
170
-
171
- await writeConfig(nextConfig);
172
-
173
- const projectCount = Object.keys(projects).length;
174
- const rootCount = rootsResolved.length;
175
- log.info("update completed", "projects=" + projectCount, "roots=" + rootCount);
176
-
177
- process.stdout.write(
178
- "Indexed " +
179
- projectCount +
180
- " projects from " +
181
- rootCount +
182
- " root(s).\n",
183
- );
184
-
185
- if (options.cron !== undefined && options.cron !== false) {
186
- const entryScriptPath = path.isAbsolute(process.argv[1] ?? "")
187
- ? process.argv[1]
188
- : path.resolve(process.cwd(), process.argv[1] ?? "dist/index.js");
189
- const cronOpt = options.cron;
190
- if (cronOpt === true) {
191
- const { wrapperPath, logPath } = await installUpdateCron({
192
- nodePath: process.execPath,
193
- entryScriptPath,
194
- schedule: "1h",
195
- });
196
- process.stdout.write("Installed cron for bet update (every hour).\n");
197
225
  process.stdout.write(
198
- ` Wrapper script: ${wrapperPath}\n Log file: ${logPath}\n To view or edit crontab: crontab -l / crontab -e\n`,
226
+ "Indexed " +
227
+ projectCount +
228
+ " projects from " +
229
+ rootCount +
230
+ " root(s).\n",
199
231
  );
200
- } else if (typeof cronOpt === "string") {
201
- const normalized = cronOpt.trim().toLowerCase();
202
- if (normalized === "0" || normalized === "false") {
203
- await uninstallUpdateCron();
204
- process.stdout.write("Removed cron for bet update.\n");
205
- } else {
206
- try {
207
- const parsed = parseCronSchedule(cronOpt);
232
+
233
+ if (options.cron !== undefined && options.cron !== false) {
234
+ const entryScriptPath = path.isAbsolute(process.argv[1] ?? "")
235
+ ? process.argv[1]
236
+ : path.resolve(process.cwd(), process.argv[1] ?? "dist/index.js");
237
+ const cronOpt = options.cron;
238
+ if (cronOpt === true) {
208
239
  const { wrapperPath, logPath } = await installUpdateCron({
209
240
  nodePath: process.execPath,
210
241
  entryScriptPath,
211
- schedule: cronOpt,
242
+ schedule: "1h",
212
243
  });
213
- const label = formatScheduleLabel(parsed);
214
- process.stdout.write(`Installed cron for bet update (${label}).\n`);
244
+ process.stdout.write(
245
+ "Installed cron for bet update (every hour).\n",
246
+ );
215
247
  process.stdout.write(
216
248
  ` Wrapper script: ${wrapperPath}\n Log file: ${logPath}\n To view or edit crontab: crontab -l / crontab -e\n`,
217
249
  );
218
- } catch (err) {
219
- const message = err instanceof Error ? err.message : String(err);
220
- log.error(err instanceof Error ? err : new Error(message));
221
- process.stderr.write(`Error: ${message}\n`);
222
- process.exitCode = 1;
250
+ } else if (typeof cronOpt === "string") {
251
+ const normalized = cronOpt.trim().toLowerCase();
252
+ if (normalized === "0" || normalized === "false") {
253
+ await uninstallUpdateCron();
254
+ process.stdout.write("Removed cron for bet update.\n");
255
+ } else {
256
+ try {
257
+ const parsed = parseCronSchedule(cronOpt);
258
+ const { wrapperPath, logPath } = await installUpdateCron({
259
+ nodePath: process.execPath,
260
+ entryScriptPath,
261
+ schedule: cronOpt,
262
+ });
263
+ const label = formatScheduleLabel(parsed);
264
+ process.stdout.write(
265
+ `Installed cron for bet update (${label}).\n`,
266
+ );
267
+ process.stdout.write(
268
+ ` Wrapper script: ${wrapperPath}\n Log file: ${logPath}\n To view or edit crontab: crontab -l / crontab -e\n`,
269
+ );
270
+ } catch (err) {
271
+ const message =
272
+ err instanceof Error ? err.message : String(err);
273
+ log.error(err instanceof Error ? err : new Error(message));
274
+ process.stderr.write(`Error: ${message}\n`);
275
+ process.exitCode = 1;
276
+ }
277
+ }
223
278
  }
224
279
  }
280
+ } catch (err) {
281
+ const message = err instanceof Error ? err.message : String(err);
282
+ log.error(err instanceof Error ? err : new Error(message));
283
+ process.stderr.write(`Error: ${message}\n`);
284
+ process.exitCode = 1;
225
285
  }
226
- }
227
- } catch (err) {
228
- const message = err instanceof Error ? err.message : String(err);
229
- log.error(err instanceof Error ? err : new Error(message));
230
- process.stderr.write(`Error: ${message}\n`);
231
- process.exitCode = 1;
232
- }
233
- });
286
+ },
287
+ );
234
288
  }
package/src/lib/config.ts CHANGED
@@ -68,11 +68,25 @@ function normalizeIgnoredPaths(parsed: unknown): string[] | undefined {
68
68
  return list.length === 0 ? undefined : list;
69
69
  }
70
70
 
71
+ function normalizeEditor(parsed: unknown): string | undefined {
72
+ if (typeof parsed !== "string") return undefined;
73
+ const trimmed = parsed.trim();
74
+ return trimmed.length === 0 ? undefined : trimmed;
75
+ }
76
+
71
77
  async function readAppConfig(): Promise<AppConfig> {
72
78
  try {
73
79
  const raw = await fs.readFile(CONFIG_PATH, "utf8");
74
- const parsed = JSON.parse(raw) as { version?: number; roots?: unknown; ignores?: unknown; ignoredPaths?: unknown; slugParentFolders?: unknown };
80
+ const parsed = JSON.parse(raw) as {
81
+ version?: number;
82
+ roots?: unknown;
83
+ editor?: unknown;
84
+ ignores?: unknown;
85
+ ignoredPaths?: unknown;
86
+ slugParentFolders?: unknown;
87
+ };
75
88
  const roots = normalizeRoots(parsed.roots ?? []);
89
+ const editor = normalizeEditor(parsed.editor);
76
90
  const ignores = normalizeIgnores(parsed.ignores);
77
91
  const ignoredPaths = normalizeIgnoredPaths(parsed.ignoredPaths);
78
92
  const slugParentFolders = normalizeSlugParentFolders(parsed.slugParentFolders);
@@ -80,6 +94,7 @@ async function readAppConfig(): Promise<AppConfig> {
80
94
  ...DEFAULT_APP_CONFIG,
81
95
  version: parsed.version ?? 1,
82
96
  roots,
97
+ ...(editor !== undefined && { editor }),
83
98
  ...(ignores !== undefined && { ignores }),
84
99
  ...(ignoredPaths !== undefined && { ignoredPaths }),
85
100
  ...(slugParentFolders !== undefined && { slugParentFolders }),
@@ -146,6 +161,7 @@ export async function writeConfig(config: Config): Promise<void> {
146
161
  const appConfig: AppConfig = {
147
162
  version: config.version,
148
163
  roots: config.roots,
164
+ ...(config.editor !== undefined && { editor: config.editor }),
149
165
  ...(config.ignores !== undefined && { ignores: config.ignores }),
150
166
  ...(config.ignoredPaths !== undefined && { ignoredPaths: config.ignoredPaths }),
151
167
  ...(config.slugParentFolders !== undefined && { slugParentFolders: config.slugParentFolders }),
@@ -0,0 +1,131 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ export type LaunchCommand = {
4
+ command: string;
5
+ args: string[];
6
+ };
7
+
8
+ function tokenizeCommand(input: string): string[] {
9
+ const tokens: string[] = [];
10
+ let current = "";
11
+ let inSingle = false;
12
+ let inDouble = false;
13
+ let escaped = false;
14
+
15
+ for (const char of input) {
16
+ if (escaped) {
17
+ current += char;
18
+ escaped = false;
19
+ continue;
20
+ }
21
+
22
+ if (char === "\\") {
23
+ escaped = true;
24
+ continue;
25
+ }
26
+
27
+ if (char === "'" && !inDouble) {
28
+ inSingle = !inSingle;
29
+ continue;
30
+ }
31
+
32
+ if (char === '"' && !inSingle) {
33
+ inDouble = !inDouble;
34
+ continue;
35
+ }
36
+
37
+ if (!inSingle && !inDouble && /\s/.test(char)) {
38
+ if (current.length > 0) {
39
+ tokens.push(current);
40
+ current = "";
41
+ }
42
+ continue;
43
+ }
44
+
45
+ current += char;
46
+ }
47
+
48
+ if (escaped || inSingle || inDouble) {
49
+ throw new Error("Invalid editor command in config.");
50
+ }
51
+
52
+ if (current.length > 0) {
53
+ tokens.push(current);
54
+ }
55
+
56
+ return tokens;
57
+ }
58
+
59
+ export function parseEditorCommand(editor: string): LaunchCommand {
60
+ const trimmed = editor.trim();
61
+ if (!trimmed) {
62
+ throw new Error("Config editor must not be empty.");
63
+ }
64
+
65
+ const tokens = tokenizeCommand(trimmed);
66
+ if (tokens.length === 0) {
67
+ throw new Error("Config editor must not be empty.");
68
+ }
69
+
70
+ const [command, ...args] = tokens;
71
+ return { command, args };
72
+ }
73
+
74
+ export function getSystemOpenCommand(
75
+ targetPath: string,
76
+ platform: NodeJS.Platform = process.platform,
77
+ ): LaunchCommand {
78
+ if (platform === "darwin") {
79
+ return { command: "open", args: [targetPath] };
80
+ }
81
+
82
+ if (platform === "win32") {
83
+ return { command: "cmd", args: ["/c", "start", "", targetPath] };
84
+ }
85
+
86
+ return { command: "xdg-open", args: [targetPath] };
87
+ }
88
+
89
+ function getEnvEditor(env: NodeJS.ProcessEnv): string | undefined {
90
+ const visual = env.VISUAL?.trim();
91
+ if (visual) return visual;
92
+
93
+ const editor = env.EDITOR?.trim();
94
+ if (editor) return editor;
95
+
96
+ return undefined;
97
+ }
98
+
99
+ function spawnDetached(command: string, args: string[]): Promise<void> {
100
+ return new Promise((resolve, reject) => {
101
+ const child = spawn(command, args, {
102
+ detached: true,
103
+ stdio: "ignore",
104
+ });
105
+
106
+ child.once("error", (error) => {
107
+ reject(error);
108
+ });
109
+
110
+ child.once("spawn", () => {
111
+ child.unref();
112
+ resolve();
113
+ });
114
+ });
115
+ }
116
+
117
+ export async function openProjectInEditor(
118
+ projectPath: string,
119
+ configuredEditor?: string,
120
+ env: NodeJS.ProcessEnv = process.env,
121
+ ): Promise<void> {
122
+ const preferredEditor = configuredEditor?.trim() || getEnvEditor(env);
123
+ if (preferredEditor) {
124
+ const parsed = parseEditorCommand(preferredEditor);
125
+ await spawnDetached(parsed.command, [...parsed.args, projectPath]);
126
+ return;
127
+ }
128
+
129
+ const fallback = getSystemOpenCommand(projectPath);
130
+ await spawnDetached(fallback.command, fallback.args);
131
+ }
package/src/lib/git.ts CHANGED
@@ -1,12 +1,12 @@
1
- import { execFile } from 'node:child_process';
2
- import { promisify } from 'node:util';
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
3
 
4
4
  const execFileAsync = promisify(execFile);
5
5
 
6
6
  async function runGit(cwd: string, args: string[]): Promise<string | null> {
7
7
  try {
8
- const { stdout } = await execFileAsync('git', ['-C', cwd, ...args], {
9
- encoding: 'utf8',
8
+ const { stdout } = await execFileAsync("git", ["-C", cwd, ...args], {
9
+ encoding: "utf8",
10
10
  });
11
11
  return stdout.trim();
12
12
  } catch {
@@ -14,18 +14,28 @@ async function runGit(cwd: string, args: string[]): Promise<string | null> {
14
14
  }
15
15
  }
16
16
 
17
- export async function getFirstCommitDate(cwd: string): Promise<string | undefined> {
18
- const output = await runGit(cwd, ['log', '--reverse', '--format=%cI', '-n', '1']);
17
+ export async function getFirstCommitDate(
18
+ cwd: string,
19
+ ): Promise<string | undefined> {
20
+ const output = await runGit(cwd, [
21
+ "log",
22
+ "--max-parents=0",
23
+ "--format=%cd",
24
+ "--date=iso-strict",
25
+ "HEAD",
26
+ ]);
19
27
  return output || undefined;
20
28
  }
21
29
 
22
- export async function getDirtyStatus(cwd: string): Promise<boolean | undefined> {
23
- const output = await runGit(cwd, ['status', '--porcelain']);
30
+ export async function getDirtyStatus(
31
+ cwd: string,
32
+ ): Promise<boolean | undefined> {
33
+ const output = await runGit(cwd, ["status", "--porcelain"]);
24
34
  if (output === null) return undefined;
25
35
  return output.length > 0;
26
36
  }
27
37
 
28
38
  export async function isInsideGitRepo(cwd: string): Promise<boolean> {
29
- const output = await runGit(cwd, ['rev-parse', '--is-inside-work-tree']);
30
- return output === 'true';
39
+ const output = await runGit(cwd, ["rev-parse", "--is-inside-work-tree"]);
40
+ return output === "true";
31
41
  }