bet-cli 0.1.2 → 0.1.3
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/README.md +21 -1
- package/dist/commands/completion.js +2 -0
- package/dist/commands/ignore.js +78 -0
- package/dist/commands/update.js +139 -80
- package/dist/index.js +3 -1
- package/dist/lib/config.js +27 -0
- package/dist/lib/cron.js +115 -6
- package/dist/lib/ignore.js +9 -0
- package/dist/lib/log-dir.js +22 -0
- package/dist/lib/logger.js +80 -0
- package/dist/lib/metadata.js +2 -3
- package/dist/lib/scan.js +5 -10
- package/package.json +1 -1
- package/src/commands/completion.ts +2 -0
- package/src/commands/ignore.ts +93 -0
- package/src/commands/update.ts +80 -17
- package/src/index.ts +3 -1
- package/src/lib/config.ts +28 -1
- package/src/lib/cron.ts +152 -9
- package/src/lib/ignore.ts +13 -0
- package/src/lib/log-dir.ts +26 -0
- package/src/lib/logger.ts +92 -0
- package/src/lib/metadata.ts +2 -3
- package/src/lib/scan.ts +5 -10
- package/src/lib/types.ts +6 -0
- package/tests/config.test.ts +175 -1
- package/tests/cron.test.ts +243 -0
- package/tests/ignore.test.ts +179 -0
- package/tests/metadata.test.ts +3 -2
- package/tests/scan.test.ts +5 -4
- package/tests/update.test.ts +20 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { getLogDir, getLogFilePath } from "./log-dir.js";
|
|
3
|
+
const LEVELS = ["debug", "info", "warn", "error"];
|
|
4
|
+
const LEVEL_ORDER = {
|
|
5
|
+
debug: 0,
|
|
6
|
+
info: 1,
|
|
7
|
+
warn: 2,
|
|
8
|
+
error: 3,
|
|
9
|
+
};
|
|
10
|
+
function parseLogLevel() {
|
|
11
|
+
const raw = process.env.BET_LOG_LEVEL?.trim().toLowerCase();
|
|
12
|
+
if (raw && LEVELS.includes(raw)) {
|
|
13
|
+
return raw;
|
|
14
|
+
}
|
|
15
|
+
return "info";
|
|
16
|
+
}
|
|
17
|
+
let minLevelOrder = LEVEL_ORDER[parseLogLevel()];
|
|
18
|
+
let fileStream = null;
|
|
19
|
+
function ensureStream() {
|
|
20
|
+
if (fileStream != null) {
|
|
21
|
+
return fileStream;
|
|
22
|
+
}
|
|
23
|
+
const logDir = getLogDir();
|
|
24
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
25
|
+
const logPath = getLogFilePath();
|
|
26
|
+
fileStream = fs.createWriteStream(logPath, { flags: "a" });
|
|
27
|
+
return fileStream;
|
|
28
|
+
}
|
|
29
|
+
function formatLine(level, message, stack) {
|
|
30
|
+
const ts = new Date().toISOString();
|
|
31
|
+
let line = `${ts} ${level.toUpperCase()} ${message}`;
|
|
32
|
+
if (stack) {
|
|
33
|
+
line += "\n" + stack;
|
|
34
|
+
}
|
|
35
|
+
return line + "\n";
|
|
36
|
+
}
|
|
37
|
+
function shouldLog(level) {
|
|
38
|
+
return LEVEL_ORDER[level] >= minLevelOrder;
|
|
39
|
+
}
|
|
40
|
+
function write(level, message, stack) {
|
|
41
|
+
if (!shouldLog(level)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const line = formatLine(level, message, stack);
|
|
45
|
+
try {
|
|
46
|
+
ensureStream().write(line);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Avoid throwing from logger; best-effort only
|
|
50
|
+
}
|
|
51
|
+
if (process.stderr.isTTY) {
|
|
52
|
+
process.stderr.write(line);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export const log = {
|
|
56
|
+
debug(msg, ...args) {
|
|
57
|
+
const message = args.length > 0 ? `${msg} ${args.map(String).join(" ")}` : msg;
|
|
58
|
+
write("debug", message);
|
|
59
|
+
},
|
|
60
|
+
info(msg, ...args) {
|
|
61
|
+
const message = args.length > 0 ? `${msg} ${args.map(String).join(" ")}` : msg;
|
|
62
|
+
write("info", message);
|
|
63
|
+
},
|
|
64
|
+
warn(msg, ...args) {
|
|
65
|
+
const message = args.length > 0 ? `${msg} ${args.map(String).join(" ")}` : msg;
|
|
66
|
+
write("warn", message);
|
|
67
|
+
},
|
|
68
|
+
error(errOrMsg, ...args) {
|
|
69
|
+
if (errOrMsg instanceof Error) {
|
|
70
|
+
const message = errOrMsg.message;
|
|
71
|
+
const stack = errOrMsg.stack;
|
|
72
|
+
write("error", message, stack);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const message = args.length > 0
|
|
76
|
+
? `${String(errOrMsg)} ${args.map(String).join(" ")}`
|
|
77
|
+
: String(errOrMsg);
|
|
78
|
+
write("error", message);
|
|
79
|
+
},
|
|
80
|
+
};
|
package/dist/lib/metadata.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import fg from 'fast-glob';
|
|
2
|
-
import { DEFAULT_IGNORES } from './ignore.js';
|
|
3
2
|
import { getDirtyStatus, getFirstCommitDate } from './git.js';
|
|
4
3
|
import { readReadmeDescription } from './readme.js';
|
|
5
|
-
export async function computeMetadata(projectPath, hasGit) {
|
|
4
|
+
export async function computeMetadata(projectPath, hasGit, ignores) {
|
|
6
5
|
const nowIso = new Date().toISOString();
|
|
7
6
|
const entries = await fg('**/*', {
|
|
8
7
|
cwd: projectPath,
|
|
9
8
|
dot: true,
|
|
10
9
|
onlyFiles: true,
|
|
11
10
|
followSymbolicLinks: false,
|
|
12
|
-
ignore:
|
|
11
|
+
ignore: ignores,
|
|
13
12
|
stats: true,
|
|
14
13
|
});
|
|
15
14
|
let oldest;
|
package/dist/lib/scan.js
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import fg from "fast-glob";
|
|
3
|
-
import { DEFAULT_IGNORES } from "./ignore.js";
|
|
4
3
|
import { isSubpath } from "../utils/paths.js";
|
|
5
4
|
import { isInsideGitRepo } from "./git.js";
|
|
6
|
-
const README_PATTERNS = [
|
|
7
|
-
"**/README.md",
|
|
8
|
-
"**/readme.md",
|
|
9
|
-
"**/Readme.md",
|
|
10
|
-
"**/README.MD",
|
|
11
|
-
];
|
|
5
|
+
const README_PATTERNS = ["**/README.md", "**/readme.txt"];
|
|
12
6
|
function resolveProjectRoot(matchPath) {
|
|
13
7
|
const container = path.dirname(matchPath);
|
|
14
8
|
// if (
|
|
@@ -36,11 +30,11 @@ function addCandidate(map, projectPath, root, flags) {
|
|
|
36
30
|
}
|
|
37
31
|
map.set(projectPath, next);
|
|
38
32
|
}
|
|
39
|
-
export async function scanRoots(roots) {
|
|
33
|
+
export async function scanRoots(roots, ignores) {
|
|
40
34
|
const candidates = new Map();
|
|
41
35
|
for (const root of roots) {
|
|
42
36
|
// Exclude .git/** from ignores when scanning for .git directories
|
|
43
|
-
const gitIgnores =
|
|
37
|
+
const gitIgnores = ignores.filter((pattern) => pattern !== "**/.git/**");
|
|
44
38
|
const gitMatches = await fg("**/.git", {
|
|
45
39
|
cwd: root,
|
|
46
40
|
dot: true,
|
|
@@ -61,7 +55,8 @@ export async function scanRoots(roots) {
|
|
|
61
55
|
dot: true,
|
|
62
56
|
onlyFiles: true,
|
|
63
57
|
followSymbolicLinks: false,
|
|
64
|
-
ignore:
|
|
58
|
+
ignore: ignores,
|
|
59
|
+
caseSensitiveMatch: false,
|
|
65
60
|
});
|
|
66
61
|
for (const match of readmeMatches) {
|
|
67
62
|
const absMatch = path.join(root, match);
|
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@ const SUBCOMMANDS = [
|
|
|
11
11
|
"path",
|
|
12
12
|
"shell",
|
|
13
13
|
"completion",
|
|
14
|
+
"ignore",
|
|
14
15
|
];
|
|
15
16
|
|
|
16
17
|
function zshScript(): string {
|
|
@@ -27,6 +28,7 @@ _bet() {
|
|
|
27
28
|
'path:Print project path'
|
|
28
29
|
'shell:Print shell integration'
|
|
29
30
|
'completion:Print shell completion script'
|
|
31
|
+
'ignore:Manage ignored project paths'
|
|
30
32
|
)
|
|
31
33
|
|
|
32
34
|
if (( CURRENT == 2 )); then
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { readConfig, writeConfig } from "../lib/config.js";
|
|
3
|
+
import { normalizeAbsolute, isSubpath } from "../utils/paths.js";
|
|
4
|
+
|
|
5
|
+
function isPathUnderRoot(filePath: string, rootPath: string): boolean {
|
|
6
|
+
return filePath === rootPath || isSubpath(filePath, rootPath);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isPathUnderAnyRoot(filePath: string, rootPaths: string[]): boolean {
|
|
10
|
+
return rootPaths.some((rootPath) => isPathUnderRoot(filePath, rootPath));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function registerIgnore(program: Command): void {
|
|
14
|
+
const ignoreCmd = program
|
|
15
|
+
.command("ignore")
|
|
16
|
+
.description("Manage ignored project paths (excluded from index)");
|
|
17
|
+
|
|
18
|
+
ignoreCmd
|
|
19
|
+
.command("add [filepath]")
|
|
20
|
+
.description("Add a path to the ignore list (must be under a configured root)")
|
|
21
|
+
.option("--this", "Ignore the current folder")
|
|
22
|
+
.action(async (filepath: string | undefined, options: { this?: boolean }) => {
|
|
23
|
+
const pathToAdd = options.this ? process.cwd() : filepath;
|
|
24
|
+
if (pathToAdd === undefined || pathToAdd === "") {
|
|
25
|
+
process.stderr.write("Error: Provide a path or use --this to ignore the current folder.\n");
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const normalized = normalizeAbsolute(pathToAdd);
|
|
30
|
+
const config = await readConfig();
|
|
31
|
+
|
|
32
|
+
if (!config.roots.length) {
|
|
33
|
+
process.stderr.write("Error: No roots configured. Add roots first (e.g. bet update --roots /path/to/code).\n");
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const rootPaths = config.roots.map((r) => r.path);
|
|
39
|
+
if (!isPathUnderAnyRoot(normalized, rootPaths)) {
|
|
40
|
+
process.stderr.write(
|
|
41
|
+
`Error: Path must be under a configured root.\n Path: ${normalized}\n Roots: ${rootPaths.join(", ")}\n`,
|
|
42
|
+
);
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const ignoredPaths = config.ignoredPaths ?? [];
|
|
48
|
+
if (ignoredPaths.includes(normalized)) {
|
|
49
|
+
process.stdout.write(`Already ignored: ${normalized}\n`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const nextIgnoredPaths = [...ignoredPaths, normalized];
|
|
54
|
+
await writeConfig({
|
|
55
|
+
...config,
|
|
56
|
+
ignoredPaths: nextIgnoredPaths,
|
|
57
|
+
});
|
|
58
|
+
process.stdout.write(`Ignored: ${normalized}\n`);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
ignoreCmd
|
|
62
|
+
.command("rm <filepath>")
|
|
63
|
+
.description("Remove a path from the ignore list")
|
|
64
|
+
.action(async (filepath: string) => {
|
|
65
|
+
const normalized = normalizeAbsolute(filepath);
|
|
66
|
+
const config = await readConfig();
|
|
67
|
+
const ignoredPaths = config.ignoredPaths ?? [];
|
|
68
|
+
const index = ignoredPaths.indexOf(normalized);
|
|
69
|
+
|
|
70
|
+
if (index === -1) {
|
|
71
|
+
process.stdout.write(`Not in ignore list: ${normalized}\n`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const nextIgnoredPaths = ignoredPaths.filter((_, i) => i !== index);
|
|
76
|
+
await writeConfig({
|
|
77
|
+
...config,
|
|
78
|
+
ignoredPaths: nextIgnoredPaths.length > 0 ? nextIgnoredPaths : undefined,
|
|
79
|
+
});
|
|
80
|
+
process.stdout.write(`Removed from ignore list: ${normalized}\n`);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
ignoreCmd
|
|
84
|
+
.command("list")
|
|
85
|
+
.description("List ignored paths")
|
|
86
|
+
.action(async () => {
|
|
87
|
+
const config = await readConfig();
|
|
88
|
+
const ignoredPaths = config.ignoredPaths ?? [];
|
|
89
|
+
for (const p of ignoredPaths) {
|
|
90
|
+
process.stdout.write(`${p}\n`);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
package/src/commands/update.ts
CHANGED
|
@@ -3,11 +3,13 @@ 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 {
|
|
6
|
+
import { installUpdateCron, uninstallUpdateCron, parseCronSchedule, formatScheduleLabel } from "../lib/cron.js";
|
|
7
7
|
import { scanRoots } from "../lib/scan.js";
|
|
8
8
|
import { computeMetadata } from "../lib/metadata.js";
|
|
9
|
+
import { getEffectiveIgnores, isPathIgnored } from "../lib/ignore.js";
|
|
9
10
|
import { Config, Project, RootConfig } from "../lib/types.js";
|
|
10
11
|
import { isInsideGitRepo } from "../lib/git.js";
|
|
12
|
+
import { log } from "../lib/logger.js";
|
|
11
13
|
|
|
12
14
|
function parseRoots(value?: string): string[] | undefined {
|
|
13
15
|
if (!value) return undefined;
|
|
@@ -34,9 +36,12 @@ export function willOverrideRoots(
|
|
|
34
36
|
);
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
const DEFAULT_SLUG_PARENT_FOLDERS = ["src", "app"];
|
|
40
|
+
|
|
41
|
+
export { DEFAULT_SLUG_PARENT_FOLDERS };
|
|
42
|
+
export function projectSlug(pathName: string, slugParentFolders: string[]): string {
|
|
38
43
|
const folderName = path.basename(pathName);
|
|
39
|
-
if (folderName
|
|
44
|
+
if (slugParentFolders.includes(folderName)) {
|
|
40
45
|
return path.basename(path.dirname(pathName));
|
|
41
46
|
}
|
|
42
47
|
return folderName;
|
|
@@ -64,8 +69,9 @@ export function registerUpdate(program: Command): void {
|
|
|
64
69
|
.description("Scan roots and update the project index")
|
|
65
70
|
.option("--roots <paths>", "Comma-separated list of roots to scan")
|
|
66
71
|
.option("--force", "Allow overriding configured roots when not in TTY")
|
|
67
|
-
.option("--cron", "
|
|
68
|
-
.action(async (options: { roots?: string; force?: boolean; cron?: boolean }) => {
|
|
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 {
|
|
69
75
|
const config = await readConfig();
|
|
70
76
|
const providedPaths = parseRoots(options.roots);
|
|
71
77
|
const providedRootConfigs = providedPaths
|
|
@@ -75,6 +81,7 @@ export function registerUpdate(program: Command): void {
|
|
|
75
81
|
const rootsToUse = providedRootConfigs ?? configRoots;
|
|
76
82
|
|
|
77
83
|
if (!rootsToUse || rootsToUse.length === 0) {
|
|
84
|
+
log.error("update failed: no roots specified");
|
|
78
85
|
process.stderr.write(
|
|
79
86
|
"Error: No roots specified. Please provide roots using --roots option.\n" +
|
|
80
87
|
"Example: bet update --roots /path/to/your/code\n",
|
|
@@ -86,6 +93,7 @@ export function registerUpdate(program: Command): void {
|
|
|
86
93
|
const willOverride = willOverrideRoots(providedRootConfigs, config.roots);
|
|
87
94
|
|
|
88
95
|
if (willOverride) {
|
|
96
|
+
log.warn("--roots overrides configured roots", "configured:", configRoots!.map((r) => r.path).join(", "), "provided:", providedRootConfigs!.map((r) => r.path).join(", "));
|
|
89
97
|
process.stderr.write(
|
|
90
98
|
"Warning: --roots will override your configured roots.\n" +
|
|
91
99
|
" Configured: " +
|
|
@@ -96,6 +104,7 @@ export function registerUpdate(program: Command): void {
|
|
|
96
104
|
);
|
|
97
105
|
if (!process.stdin.isTTY) {
|
|
98
106
|
if (!options.force) {
|
|
107
|
+
log.error("update failed: refusing to override roots without confirmation (use --force when not in TTY)");
|
|
99
108
|
process.stderr.write(
|
|
100
109
|
"Error: Refusing to override without confirmation. Run interactively or use --force.\n",
|
|
101
110
|
);
|
|
@@ -105,6 +114,7 @@ export function registerUpdate(program: Command): void {
|
|
|
105
114
|
} else {
|
|
106
115
|
const confirmed = await promptYesNo("Continue?", true);
|
|
107
116
|
if (!confirmed) {
|
|
117
|
+
log.info("update aborted by user");
|
|
108
118
|
process.stderr.write("Aborted.\n");
|
|
109
119
|
return;
|
|
110
120
|
}
|
|
@@ -113,13 +123,21 @@ export function registerUpdate(program: Command): void {
|
|
|
113
123
|
|
|
114
124
|
const rootsResolved = resolveRoots(rootsToUse);
|
|
115
125
|
const rootPaths = rootsResolved.map((r) => r.path);
|
|
116
|
-
|
|
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");
|
|
117
135
|
const projects: Record<string, Project> = {};
|
|
118
136
|
|
|
119
|
-
for (const candidate of
|
|
137
|
+
for (const candidate of filteredCandidates) {
|
|
120
138
|
const hasGit = await isInsideGitRepo(candidate.path);
|
|
121
|
-
const auto = await computeMetadata(candidate.path, hasGit);
|
|
122
|
-
const slug = projectSlug(candidate.path);
|
|
139
|
+
const auto = await computeMetadata(candidate.path, hasGit, ignores);
|
|
140
|
+
const slug = projectSlug(candidate.path, config.slugParentFolders ?? DEFAULT_SLUG_PARENT_FOLDERS);
|
|
123
141
|
const existing = config.projects[candidate.path];
|
|
124
142
|
const rootConfig = rootsResolved.find((r) => r.path === candidate.root);
|
|
125
143
|
const rootName = rootConfig?.name ?? path.basename(candidate.root);
|
|
@@ -144,27 +162,72 @@ export function registerUpdate(program: Command): void {
|
|
|
144
162
|
version: config.version ?? 1,
|
|
145
163
|
roots: rootsResolved,
|
|
146
164
|
projects,
|
|
165
|
+
...(config.ignores !== undefined && { ignores: config.ignores }),
|
|
166
|
+
...(config.ignoredPaths !== undefined && { ignoredPaths: config.ignoredPaths }),
|
|
167
|
+
...(config.slugParentFolders !== undefined && { slugParentFolders: config.slugParentFolders }),
|
|
147
168
|
};
|
|
148
169
|
|
|
149
170
|
await writeConfig(nextConfig);
|
|
150
171
|
|
|
172
|
+
const projectCount = Object.keys(projects).length;
|
|
173
|
+
const rootCount = rootsResolved.length;
|
|
174
|
+
log.info("update completed", "projects=" + projectCount, "roots=" + rootCount);
|
|
175
|
+
|
|
151
176
|
process.stdout.write(
|
|
152
177
|
"Indexed " +
|
|
153
|
-
|
|
178
|
+
projectCount +
|
|
154
179
|
" projects from " +
|
|
155
|
-
|
|
180
|
+
rootCount +
|
|
156
181
|
" root(s).\n",
|
|
157
182
|
);
|
|
158
183
|
|
|
159
|
-
if (options.cron) {
|
|
184
|
+
if (options.cron !== undefined && options.cron !== false) {
|
|
160
185
|
const entryScriptPath = path.isAbsolute(process.argv[1] ?? "")
|
|
161
186
|
? process.argv[1]
|
|
162
187
|
: path.resolve(process.cwd(), process.argv[1] ?? "dist/index.js");
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
188
|
+
const cronOpt = options.cron;
|
|
189
|
+
if (cronOpt === true) {
|
|
190
|
+
const { wrapperPath, logPath } = await installUpdateCron({
|
|
191
|
+
nodePath: process.execPath,
|
|
192
|
+
entryScriptPath,
|
|
193
|
+
schedule: "1h",
|
|
194
|
+
});
|
|
195
|
+
process.stdout.write("Installed cron for bet update (every hour).\n");
|
|
196
|
+
process.stdout.write(
|
|
197
|
+
` Wrapper script: ${wrapperPath}\n Log file: ${logPath}\n To view or edit crontab: crontab -l / crontab -e\n`,
|
|
198
|
+
);
|
|
199
|
+
} else if (typeof cronOpt === "string") {
|
|
200
|
+
const normalized = cronOpt.trim().toLowerCase();
|
|
201
|
+
if (normalized === "0" || normalized === "false") {
|
|
202
|
+
await uninstallUpdateCron();
|
|
203
|
+
process.stdout.write("Removed cron for bet update.\n");
|
|
204
|
+
} else {
|
|
205
|
+
try {
|
|
206
|
+
const parsed = parseCronSchedule(cronOpt);
|
|
207
|
+
const { wrapperPath, logPath } = await installUpdateCron({
|
|
208
|
+
nodePath: process.execPath,
|
|
209
|
+
entryScriptPath,
|
|
210
|
+
schedule: cronOpt,
|
|
211
|
+
});
|
|
212
|
+
const label = formatScheduleLabel(parsed);
|
|
213
|
+
process.stdout.write(`Installed cron for bet update (${label}).\n`);
|
|
214
|
+
process.stdout.write(
|
|
215
|
+
` Wrapper script: ${wrapperPath}\n Log file: ${logPath}\n To view or edit crontab: crontab -l / crontab -e\n`,
|
|
216
|
+
);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
219
|
+
log.error(err instanceof Error ? err : new Error(message));
|
|
220
|
+
process.stderr.write(`Error: ${message}\n`);
|
|
221
|
+
process.exitCode = 1;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} catch (err) {
|
|
227
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
228
|
+
log.error(err instanceof Error ? err : new Error(message));
|
|
229
|
+
process.stderr.write(`Error: ${message}\n`);
|
|
230
|
+
process.exitCode = 1;
|
|
168
231
|
}
|
|
169
232
|
});
|
|
170
233
|
}
|
package/src/index.ts
CHANGED
|
@@ -8,13 +8,14 @@ import { registerGo } from "./commands/go.js";
|
|
|
8
8
|
import { registerPath } from "./commands/path.js";
|
|
9
9
|
import { registerShell } from "./commands/shell.js";
|
|
10
10
|
import { registerCompletion } from "./commands/completion.js";
|
|
11
|
+
import { registerIgnore } from "./commands/ignore.js";
|
|
11
12
|
|
|
12
13
|
const program = new Command();
|
|
13
14
|
|
|
14
15
|
program
|
|
15
16
|
.name("bet")
|
|
16
17
|
.description("Explore and jump between local projects.")
|
|
17
|
-
.version("0.1.
|
|
18
|
+
.version("0.1.3");
|
|
18
19
|
|
|
19
20
|
registerUpdate(program);
|
|
20
21
|
registerList(program);
|
|
@@ -24,5 +25,6 @@ registerGo(program);
|
|
|
24
25
|
registerPath(program);
|
|
25
26
|
registerShell(program);
|
|
26
27
|
registerCompletion(program);
|
|
28
|
+
registerIgnore(program);
|
|
27
29
|
|
|
28
30
|
program.parseAsync(process.argv);
|
package/src/lib/config.ts
CHANGED
|
@@ -50,15 +50,39 @@ function normalizeRoots(parsedRoots: unknown): RootConfig[] {
|
|
|
50
50
|
return result;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
function normalizeIgnores(parsed: unknown): string[] | undefined {
|
|
54
|
+
if (!Array.isArray(parsed)) return undefined;
|
|
55
|
+
const list = parsed.filter((x): x is string => typeof x === "string");
|
|
56
|
+
return list;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeSlugParentFolders(parsed: unknown): string[] | undefined {
|
|
60
|
+
if (!Array.isArray(parsed)) return undefined;
|
|
61
|
+
const list = parsed.filter((x): x is string => typeof x === "string" && x.trim() !== "").map((x) => x.trim());
|
|
62
|
+
return list.length === 0 ? undefined : list;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeIgnoredPaths(parsed: unknown): string[] | undefined {
|
|
66
|
+
if (!Array.isArray(parsed)) return undefined;
|
|
67
|
+
const list = parsed.filter((x): x is string => typeof x === "string").map((x) => normalizeAbsolute(x));
|
|
68
|
+
return list.length === 0 ? undefined : list;
|
|
69
|
+
}
|
|
70
|
+
|
|
53
71
|
async function readAppConfig(): Promise<AppConfig> {
|
|
54
72
|
try {
|
|
55
73
|
const raw = await fs.readFile(CONFIG_PATH, "utf8");
|
|
56
|
-
const parsed = JSON.parse(raw) as { version?: number; roots?: unknown };
|
|
74
|
+
const parsed = JSON.parse(raw) as { version?: number; roots?: unknown; ignores?: unknown; ignoredPaths?: unknown; slugParentFolders?: unknown };
|
|
57
75
|
const roots = normalizeRoots(parsed.roots ?? []);
|
|
76
|
+
const ignores = normalizeIgnores(parsed.ignores);
|
|
77
|
+
const ignoredPaths = normalizeIgnoredPaths(parsed.ignoredPaths);
|
|
78
|
+
const slugParentFolders = normalizeSlugParentFolders(parsed.slugParentFolders);
|
|
58
79
|
return {
|
|
59
80
|
...DEFAULT_APP_CONFIG,
|
|
60
81
|
version: parsed.version ?? 1,
|
|
61
82
|
roots,
|
|
83
|
+
...(ignores !== undefined && { ignores }),
|
|
84
|
+
...(ignoredPaths !== undefined && { ignoredPaths }),
|
|
85
|
+
...(slugParentFolders !== undefined && { slugParentFolders }),
|
|
62
86
|
};
|
|
63
87
|
} catch (error) {
|
|
64
88
|
return { ...DEFAULT_APP_CONFIG };
|
|
@@ -120,6 +144,9 @@ export async function writeConfig(config: Config): Promise<void> {
|
|
|
120
144
|
const appConfig: AppConfig = {
|
|
121
145
|
version: config.version,
|
|
122
146
|
roots: config.roots,
|
|
147
|
+
...(config.ignores !== undefined && { ignores: config.ignores }),
|
|
148
|
+
...(config.ignoredPaths !== undefined && { ignoredPaths: config.ignoredPaths }),
|
|
149
|
+
...(config.slugParentFolders !== undefined && { slugParentFolders: config.slugParentFolders }),
|
|
123
150
|
};
|
|
124
151
|
const projectsConfig: ProjectsConfig = {
|
|
125
152
|
projects: config.projects,
|