bet-cli 0.1.1 → 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 +31 -1
- package/dist/commands/completion.js +110 -0
- package/dist/commands/ignore.js +78 -0
- package/dist/commands/update.js +139 -80
- package/dist/index.js +5 -1
- package/dist/lib/completion.js +16 -0
- 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 +127 -0
- package/src/commands/ignore.ts +93 -0
- package/src/commands/update.ts +80 -17
- package/src/index.ts +5 -1
- package/src/lib/completion.ts +16 -0
- 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
package/README.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
```
|
|
2
|
+
██
|
|
3
|
+
██
|
|
4
|
+
████████████
|
|
5
|
+
██
|
|
6
|
+
████████
|
|
7
|
+
╲╲
|
|
8
|
+
╲╲
|
|
9
|
+
```
|
|
10
|
+
|
|
1
11
|
# bet
|
|
2
12
|
|
|
3
13
|
Keep your house in order. Explore and jump between local projects.
|
|
@@ -77,12 +87,21 @@ After that:
|
|
|
77
87
|
- `bet go <slug>` will `cd` your current shell into the project
|
|
78
88
|
- `bet list` / `bet search` can also “select and jump” in interactive mode
|
|
79
89
|
|
|
90
|
+
**Project name autocompletion:** To complete project names (slugs) when using `bet go`, `bet path`, or `bet info`, add to your rc file:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
eval "$(bet completion zsh)" # zsh
|
|
94
|
+
eval "$(bet completion bash)" # bash
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Then Tab-complete the slug argument after `bet go `, `bet path `, or `bet info `.
|
|
98
|
+
|
|
80
99
|
## Core commands
|
|
81
100
|
|
|
82
101
|
- **`bet update`**: Scan configured roots and rebuild the project index.
|
|
83
102
|
- **First-time setup**: `bet update --roots "$HOME/code,$HOME/work"`
|
|
84
103
|
- If you pass `--roots` when you already have roots in config, you will be warned and must confirm (or use `--force` when not in a TTY).
|
|
85
|
-
- **Optional**: `--cron`
|
|
104
|
+
- **Optional**: `--cron [frequency]` — install a `crontab` entry that runs `bet update` on a schedule. Use Nm (1–59), Nh (1–24), or Nd (1–31), e.g. `--cron 5m`, `--cron 1h` (default if omitted), `--cron 2d`. Use `--cron 0` or `--cron false` to remove the cron. Cron stdout/stderr are appended to `~/.config/bet/cron-update.log`; structured logs go to the main log file (see [Logging](#logging)).
|
|
86
105
|
- **`bet list`**: List indexed projects (interactive by default).
|
|
87
106
|
- **`--plain`**: non-interactive output
|
|
88
107
|
- **`--json`**: machine-readable output
|
|
@@ -94,6 +113,7 @@ After that:
|
|
|
94
113
|
- **`--print`**: print selected path only (no shell `cd`)
|
|
95
114
|
- **`--no-enter`**: do not run the project’s `onEnter` hook (if configured)
|
|
96
115
|
- **`bet shell`**: Print the shell integration snippet (see above).
|
|
116
|
+
- **`bet completion [bash|zsh]`**: Print shell completion script for project name autocompletion (see above).
|
|
97
117
|
|
|
98
118
|
## Config & data files
|
|
99
119
|
|
|
@@ -101,10 +121,20 @@ bet stores its data in:
|
|
|
101
121
|
|
|
102
122
|
- **Config dir**: `~/.config/bet/` (or `$XDG_CONFIG_HOME/bet/`)
|
|
103
123
|
- **Roots**: `config.json` — each root is `{ "path": "/absolute/path", "name": "display-name" }`. The name defaults to the top folder name and is used when listing/grouping projects.
|
|
124
|
+
- **slugParentFolders** (optional): In `config.json`, an array of folder names. When a discovered project path ends in one of these (e.g. `src` or `app`), the project slug is taken from the parent directory name instead. Default in code is `["src", "app"]` when the key is not set.
|
|
104
125
|
- **Project index**: `projects.json`
|
|
105
126
|
|
|
106
127
|
These are plain JSON files—easy to inspect, back up, or edit.
|
|
107
128
|
|
|
129
|
+
### Logging
|
|
130
|
+
|
|
131
|
+
bet writes a structured log file for debugging, especially when `bet update` runs from cron:
|
|
132
|
+
|
|
133
|
+
- **macOS**: `~/Library/Logs/bet/bet.log`
|
|
134
|
+
- **Linux**: `~/.local/state/bet/bet.log` (or `$XDG_STATE_HOME/bet/bet.log`)
|
|
135
|
+
|
|
136
|
+
Each line is timestamped and includes a level (`DEBUG`, `INFO`, `WARN`, `ERROR`). Set `BET_LOG_LEVEL=debug` for verbose output when troubleshooting (e.g. in your cron environment). When run from cron, stdout/stderr are also captured in `~/.config/bet/cron-update.log`; the main log file is the structured, level-based log.
|
|
137
|
+
|
|
108
138
|
### Advanced filtering with `--json`
|
|
109
139
|
|
|
110
140
|
You can combine `bet list --json` with [jq](https://stedolan.github.io/jq/) for powerful, scriptable project filtering. Here are some practical examples:
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { getProjectSlugs } from "../lib/completion.js";
|
|
2
|
+
const SLUG_COMMANDS = ["go", "path", "info"];
|
|
3
|
+
const SUBCOMMANDS = [
|
|
4
|
+
"update",
|
|
5
|
+
"list",
|
|
6
|
+
"search",
|
|
7
|
+
"info",
|
|
8
|
+
"go",
|
|
9
|
+
"path",
|
|
10
|
+
"shell",
|
|
11
|
+
"completion",
|
|
12
|
+
"ignore",
|
|
13
|
+
];
|
|
14
|
+
function zshScript() {
|
|
15
|
+
const slugCommandsPattern = SLUG_COMMANDS.join("|");
|
|
16
|
+
return `#compdef bet
|
|
17
|
+
_bet() {
|
|
18
|
+
local -a subcommands
|
|
19
|
+
subcommands=(
|
|
20
|
+
'update:Scan roots and rebuild project index'
|
|
21
|
+
'list:List projects'
|
|
22
|
+
'search:Fuzzy-search projects'
|
|
23
|
+
'info:Show project details'
|
|
24
|
+
'go:Jump to a project'
|
|
25
|
+
'path:Print project path'
|
|
26
|
+
'shell:Print shell integration'
|
|
27
|
+
'completion:Print shell completion script'
|
|
28
|
+
'ignore:Manage ignored project paths'
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if (( CURRENT == 2 )); then
|
|
32
|
+
_describe 'bet commands' subcommands
|
|
33
|
+
return
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
if [[ "${slugCommandsPattern}" == *"\$words[2]"* ]]; then
|
|
37
|
+
if (( CURRENT == 3 )); then
|
|
38
|
+
local -a slugs
|
|
39
|
+
slugs=(\$(command bet completion --list 2>/dev/null))
|
|
40
|
+
_describe 'project' slugs
|
|
41
|
+
fi
|
|
42
|
+
return
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
_default
|
|
46
|
+
}
|
|
47
|
+
compdef _bet bet
|
|
48
|
+
`;
|
|
49
|
+
}
|
|
50
|
+
function bashScript() {
|
|
51
|
+
const subcommandsList = SUBCOMMANDS.join(" ");
|
|
52
|
+
const cw = "COMP_WORDS";
|
|
53
|
+
const cc = "COMP_CWORD";
|
|
54
|
+
const dollar = "$";
|
|
55
|
+
return `_bet_completions() {
|
|
56
|
+
local cur="${dollar}{${cw}[${cc}]}"
|
|
57
|
+
|
|
58
|
+
if (( ${cc} == 1 )); then
|
|
59
|
+
COMPREPLY=(\$(compgen -W "${subcommandsList}" -- "${dollar}cur"))
|
|
60
|
+
return
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
local cmd="${dollar}{${cw}[1]}"
|
|
64
|
+
if [[ "${dollar}cmd" == "go" || "${dollar}cmd" == "path" || "${dollar}cmd" == "info" ]]; then
|
|
65
|
+
if (( ${cc} == 2 )); then
|
|
66
|
+
local slugs
|
|
67
|
+
slugs=\$(command bet completion --list 2>/dev/null)
|
|
68
|
+
COMPREPLY=(\$(compgen -W "${dollar}slugs" -- "${dollar}cur"))
|
|
69
|
+
fi
|
|
70
|
+
fi
|
|
71
|
+
}
|
|
72
|
+
complete -F _bet_completions bet
|
|
73
|
+
`;
|
|
74
|
+
}
|
|
75
|
+
export function registerCompletion(program) {
|
|
76
|
+
program
|
|
77
|
+
.command("completion [shell]")
|
|
78
|
+
.description("Print shell completion script for project name autocompletion")
|
|
79
|
+
.option("--list", "Print project slugs only (for use by completion script)")
|
|
80
|
+
.option("--prefix <prefix>", "Filter slugs by prefix")
|
|
81
|
+
.action(async (shell, options) => {
|
|
82
|
+
if (options.list) {
|
|
83
|
+
let slugs = await getProjectSlugs();
|
|
84
|
+
if (options.prefix?.length) {
|
|
85
|
+
const p = options.prefix.toLowerCase();
|
|
86
|
+
slugs = slugs.filter((s) => s.toLowerCase().startsWith(p));
|
|
87
|
+
}
|
|
88
|
+
for (const slug of slugs) {
|
|
89
|
+
process.stdout.write(`${slug}\n`);
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const sh = shell?.toLowerCase() ?? "";
|
|
94
|
+
if (sh === "zsh") {
|
|
95
|
+
process.stdout.write(zshScript());
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (sh === "bash") {
|
|
99
|
+
process.stdout.write(bashScript());
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (shell !== undefined) {
|
|
103
|
+
process.stderr.write(`Unknown shell "${shell}". Use bash or zsh.\n`);
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
process.stderr.write("Usage: bet completion [bash|zsh]\n eval \"$(bet completion zsh)\"\n");
|
|
108
|
+
process.exitCode = 1;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { readConfig, writeConfig } from "../lib/config.js";
|
|
2
|
+
import { normalizeAbsolute, isSubpath } from "../utils/paths.js";
|
|
3
|
+
function isPathUnderRoot(filePath, rootPath) {
|
|
4
|
+
return filePath === rootPath || isSubpath(filePath, rootPath);
|
|
5
|
+
}
|
|
6
|
+
function isPathUnderAnyRoot(filePath, rootPaths) {
|
|
7
|
+
return rootPaths.some((rootPath) => isPathUnderRoot(filePath, rootPath));
|
|
8
|
+
}
|
|
9
|
+
export function registerIgnore(program) {
|
|
10
|
+
const ignoreCmd = program
|
|
11
|
+
.command("ignore")
|
|
12
|
+
.description("Manage ignored project paths (excluded from index)");
|
|
13
|
+
ignoreCmd
|
|
14
|
+
.command("add [filepath]")
|
|
15
|
+
.description("Add a path to the ignore list (must be under a configured root)")
|
|
16
|
+
.option("--this", "Ignore the current folder")
|
|
17
|
+
.action(async (filepath, options) => {
|
|
18
|
+
const pathToAdd = options.this ? process.cwd() : filepath;
|
|
19
|
+
if (pathToAdd === undefined || pathToAdd === "") {
|
|
20
|
+
process.stderr.write("Error: Provide a path or use --this to ignore the current folder.\n");
|
|
21
|
+
process.exitCode = 1;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const normalized = normalizeAbsolute(pathToAdd);
|
|
25
|
+
const config = await readConfig();
|
|
26
|
+
if (!config.roots.length) {
|
|
27
|
+
process.stderr.write("Error: No roots configured. Add roots first (e.g. bet update --roots /path/to/code).\n");
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const rootPaths = config.roots.map((r) => r.path);
|
|
32
|
+
if (!isPathUnderAnyRoot(normalized, rootPaths)) {
|
|
33
|
+
process.stderr.write(`Error: Path must be under a configured root.\n Path: ${normalized}\n Roots: ${rootPaths.join(", ")}\n`);
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const ignoredPaths = config.ignoredPaths ?? [];
|
|
38
|
+
if (ignoredPaths.includes(normalized)) {
|
|
39
|
+
process.stdout.write(`Already ignored: ${normalized}\n`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const nextIgnoredPaths = [...ignoredPaths, normalized];
|
|
43
|
+
await writeConfig({
|
|
44
|
+
...config,
|
|
45
|
+
ignoredPaths: nextIgnoredPaths,
|
|
46
|
+
});
|
|
47
|
+
process.stdout.write(`Ignored: ${normalized}\n`);
|
|
48
|
+
});
|
|
49
|
+
ignoreCmd
|
|
50
|
+
.command("rm <filepath>")
|
|
51
|
+
.description("Remove a path from the ignore list")
|
|
52
|
+
.action(async (filepath) => {
|
|
53
|
+
const normalized = normalizeAbsolute(filepath);
|
|
54
|
+
const config = await readConfig();
|
|
55
|
+
const ignoredPaths = config.ignoredPaths ?? [];
|
|
56
|
+
const index = ignoredPaths.indexOf(normalized);
|
|
57
|
+
if (index === -1) {
|
|
58
|
+
process.stdout.write(`Not in ignore list: ${normalized}\n`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const nextIgnoredPaths = ignoredPaths.filter((_, i) => i !== index);
|
|
62
|
+
await writeConfig({
|
|
63
|
+
...config,
|
|
64
|
+
ignoredPaths: nextIgnoredPaths.length > 0 ? nextIgnoredPaths : undefined,
|
|
65
|
+
});
|
|
66
|
+
process.stdout.write(`Removed from ignore list: ${normalized}\n`);
|
|
67
|
+
});
|
|
68
|
+
ignoreCmd
|
|
69
|
+
.command("list")
|
|
70
|
+
.description("List ignored paths")
|
|
71
|
+
.action(async () => {
|
|
72
|
+
const config = await readConfig();
|
|
73
|
+
const ignoredPaths = config.ignoredPaths ?? [];
|
|
74
|
+
for (const p of ignoredPaths) {
|
|
75
|
+
process.stdout.write(`${p}\n`);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
package/dist/commands/update.js
CHANGED
|
@@ -2,10 +2,12 @@ import path from "node:path";
|
|
|
2
2
|
import readline from "node:readline";
|
|
3
3
|
import { readConfig, resolveRoots, writeConfig } from "../lib/config.js";
|
|
4
4
|
import { normalizeAbsolute } from "../utils/paths.js";
|
|
5
|
-
import {
|
|
5
|
+
import { installUpdateCron, uninstallUpdateCron, parseCronSchedule, formatScheduleLabel } from "../lib/cron.js";
|
|
6
6
|
import { scanRoots } from "../lib/scan.js";
|
|
7
7
|
import { computeMetadata } from "../lib/metadata.js";
|
|
8
|
+
import { getEffectiveIgnores, isPathIgnored } from "../lib/ignore.js";
|
|
8
9
|
import { isInsideGitRepo } from "../lib/git.js";
|
|
10
|
+
import { log } from "../lib/logger.js";
|
|
9
11
|
function parseRoots(value) {
|
|
10
12
|
if (!value)
|
|
11
13
|
return undefined;
|
|
@@ -24,9 +26,11 @@ export function willOverrideRoots(providedRootConfigs, configRoots) {
|
|
|
24
26
|
return !!(providedRootConfigs !== undefined &&
|
|
25
27
|
configRoots.length > 0);
|
|
26
28
|
}
|
|
27
|
-
|
|
29
|
+
const DEFAULT_SLUG_PARENT_FOLDERS = ["src", "app"];
|
|
30
|
+
export { DEFAULT_SLUG_PARENT_FOLDERS };
|
|
31
|
+
export function projectSlug(pathName, slugParentFolders) {
|
|
28
32
|
const folderName = path.basename(pathName);
|
|
29
|
-
if (folderName
|
|
33
|
+
if (slugParentFolders.includes(folderName)) {
|
|
30
34
|
return path.basename(path.dirname(pathName));
|
|
31
35
|
}
|
|
32
36
|
return folderName;
|
|
@@ -52,89 +56,144 @@ export function registerUpdate(program) {
|
|
|
52
56
|
.description("Scan roots and update the project index")
|
|
53
57
|
.option("--roots <paths>", "Comma-separated list of roots to scan")
|
|
54
58
|
.option("--force", "Allow overriding configured roots when not in TTY")
|
|
55
|
-
.option("--cron", "
|
|
59
|
+
.option("--cron [frequency]", "Run update on a schedule: Nm/Nh/Nd e.g. 5m, 1h, 2d (default 1h), or 0/false to disable")
|
|
56
60
|
.action(async (options) => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (willOverride) {
|
|
72
|
-
process.stderr.write("Warning: --roots will override your configured roots.\n" +
|
|
73
|
-
" Configured: " +
|
|
74
|
-
configRoots.map((r) => r.path).join(", ") +
|
|
75
|
-
"\n Provided: " +
|
|
76
|
-
providedRootConfigs.map((r) => r.path).join(", ") +
|
|
77
|
-
"\n");
|
|
78
|
-
if (!process.stdin.isTTY) {
|
|
79
|
-
if (!options.force) {
|
|
80
|
-
process.stderr.write("Error: Refusing to override without confirmation. Run interactively or use --force.\n");
|
|
81
|
-
process.exitCode = 1;
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
61
|
+
try {
|
|
62
|
+
const config = await readConfig();
|
|
63
|
+
const providedPaths = parseRoots(options.roots);
|
|
64
|
+
const providedRootConfigs = providedPaths
|
|
65
|
+
? pathsToRootConfigs(providedPaths)
|
|
66
|
+
: undefined;
|
|
67
|
+
const configRoots = config.roots.length > 0 ? config.roots : undefined;
|
|
68
|
+
const rootsToUse = providedRootConfigs ?? configRoots;
|
|
69
|
+
if (!rootsToUse || rootsToUse.length === 0) {
|
|
70
|
+
log.error("update failed: no roots specified");
|
|
71
|
+
process.stderr.write("Error: No roots specified. Please provide roots using --roots option.\n" +
|
|
72
|
+
"Example: bet update --roots /path/to/your/code\n");
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
return;
|
|
84
75
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
76
|
+
const willOverride = willOverrideRoots(providedRootConfigs, config.roots);
|
|
77
|
+
if (willOverride) {
|
|
78
|
+
log.warn("--roots overrides configured roots", "configured:", configRoots.map((r) => r.path).join(", "), "provided:", providedRootConfigs.map((r) => r.path).join(", "));
|
|
79
|
+
process.stderr.write("Warning: --roots will override your configured roots.\n" +
|
|
80
|
+
" Configured: " +
|
|
81
|
+
configRoots.map((r) => r.path).join(", ") +
|
|
82
|
+
"\n Provided: " +
|
|
83
|
+
providedRootConfigs.map((r) => r.path).join(", ") +
|
|
84
|
+
"\n");
|
|
85
|
+
if (!process.stdin.isTTY) {
|
|
86
|
+
if (!options.force) {
|
|
87
|
+
log.error("update failed: refusing to override roots without confirmation (use --force when not in TTY)");
|
|
88
|
+
process.stderr.write("Error: Refusing to override without confirmation. Run interactively or use --force.\n");
|
|
89
|
+
process.exitCode = 1;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const confirmed = await promptYesNo("Continue?", true);
|
|
95
|
+
if (!confirmed) {
|
|
96
|
+
log.info("update aborted by user");
|
|
97
|
+
process.stderr.write("Aborted.\n");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
90
100
|
}
|
|
91
101
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
path
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
102
|
+
const rootsResolved = resolveRoots(rootsToUse);
|
|
103
|
+
const rootPaths = rootsResolved.map((r) => r.path);
|
|
104
|
+
log.info("update started", "roots=" + rootPaths.join(", "));
|
|
105
|
+
log.debug("scanning roots", rootPaths.length, "root(s)");
|
|
106
|
+
const ignores = getEffectiveIgnores(config);
|
|
107
|
+
const candidates = await scanRoots(rootPaths, ignores);
|
|
108
|
+
const ignoredPaths = config.ignoredPaths ?? [];
|
|
109
|
+
const filteredCandidates = candidates.filter((c) => !isPathIgnored(c.path, ignoredPaths));
|
|
110
|
+
log.debug("found", filteredCandidates.length, "candidate(s) after ignoring paths");
|
|
111
|
+
const projects = {};
|
|
112
|
+
for (const candidate of filteredCandidates) {
|
|
113
|
+
const hasGit = await isInsideGitRepo(candidate.path);
|
|
114
|
+
const auto = await computeMetadata(candidate.path, hasGit, ignores);
|
|
115
|
+
const slug = projectSlug(candidate.path, config.slugParentFolders ?? DEFAULT_SLUG_PARENT_FOLDERS);
|
|
116
|
+
const existing = config.projects[candidate.path];
|
|
117
|
+
const rootConfig = rootsResolved.find((r) => r.path === candidate.root);
|
|
118
|
+
const rootName = rootConfig?.name ?? path.basename(candidate.root);
|
|
119
|
+
const project = {
|
|
120
|
+
id: candidate.path,
|
|
121
|
+
slug,
|
|
122
|
+
name: slug,
|
|
123
|
+
path: candidate.path,
|
|
124
|
+
root: candidate.root,
|
|
125
|
+
rootName,
|
|
126
|
+
hasGit,
|
|
127
|
+
hasReadme: candidate.hasReadme,
|
|
128
|
+
auto,
|
|
129
|
+
user: existing?.user,
|
|
130
|
+
};
|
|
131
|
+
projects[candidate.path] = project;
|
|
132
|
+
}
|
|
133
|
+
const nextConfig = {
|
|
134
|
+
version: config.version ?? 1,
|
|
135
|
+
roots: rootsResolved,
|
|
136
|
+
projects,
|
|
137
|
+
...(config.ignores !== undefined && { ignores: config.ignores }),
|
|
138
|
+
...(config.ignoredPaths !== undefined && { ignoredPaths: config.ignoredPaths }),
|
|
139
|
+
...(config.slugParentFolders !== undefined && { slugParentFolders: config.slugParentFolders }),
|
|
115
140
|
};
|
|
116
|
-
|
|
141
|
+
await writeConfig(nextConfig);
|
|
142
|
+
const projectCount = Object.keys(projects).length;
|
|
143
|
+
const rootCount = rootsResolved.length;
|
|
144
|
+
log.info("update completed", "projects=" + projectCount, "roots=" + rootCount);
|
|
145
|
+
process.stdout.write("Indexed " +
|
|
146
|
+
projectCount +
|
|
147
|
+
" projects from " +
|
|
148
|
+
rootCount +
|
|
149
|
+
" root(s).\n");
|
|
150
|
+
if (options.cron !== undefined && options.cron !== false) {
|
|
151
|
+
const entryScriptPath = path.isAbsolute(process.argv[1] ?? "")
|
|
152
|
+
? process.argv[1]
|
|
153
|
+
: path.resolve(process.cwd(), process.argv[1] ?? "dist/index.js");
|
|
154
|
+
const cronOpt = options.cron;
|
|
155
|
+
if (cronOpt === true) {
|
|
156
|
+
const { wrapperPath, logPath } = await installUpdateCron({
|
|
157
|
+
nodePath: process.execPath,
|
|
158
|
+
entryScriptPath,
|
|
159
|
+
schedule: "1h",
|
|
160
|
+
});
|
|
161
|
+
process.stdout.write("Installed cron for bet update (every hour).\n");
|
|
162
|
+
process.stdout.write(` Wrapper script: ${wrapperPath}\n Log file: ${logPath}\n To view or edit crontab: crontab -l / crontab -e\n`);
|
|
163
|
+
}
|
|
164
|
+
else if (typeof cronOpt === "string") {
|
|
165
|
+
const normalized = cronOpt.trim().toLowerCase();
|
|
166
|
+
if (normalized === "0" || normalized === "false") {
|
|
167
|
+
await uninstallUpdateCron();
|
|
168
|
+
process.stdout.write("Removed cron for bet update.\n");
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
try {
|
|
172
|
+
const parsed = parseCronSchedule(cronOpt);
|
|
173
|
+
const { wrapperPath, logPath } = await installUpdateCron({
|
|
174
|
+
nodePath: process.execPath,
|
|
175
|
+
entryScriptPath,
|
|
176
|
+
schedule: cronOpt,
|
|
177
|
+
});
|
|
178
|
+
const label = formatScheduleLabel(parsed);
|
|
179
|
+
process.stdout.write(`Installed cron for bet update (${label}).\n`);
|
|
180
|
+
process.stdout.write(` Wrapper script: ${wrapperPath}\n Log file: ${logPath}\n To view or edit crontab: crontab -l / crontab -e\n`);
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
184
|
+
log.error(err instanceof Error ? err : new Error(message));
|
|
185
|
+
process.stderr.write(`Error: ${message}\n`);
|
|
186
|
+
process.exitCode = 1;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
117
191
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
await writeConfig(nextConfig);
|
|
124
|
-
process.stdout.write("Indexed " +
|
|
125
|
-
Object.keys(projects).length +
|
|
126
|
-
" projects from " +
|
|
127
|
-
rootsResolved.length +
|
|
128
|
-
" root(s).\n");
|
|
129
|
-
if (options.cron) {
|
|
130
|
-
const entryScriptPath = path.isAbsolute(process.argv[1] ?? "")
|
|
131
|
-
? process.argv[1]
|
|
132
|
-
: path.resolve(process.cwd(), process.argv[1] ?? "dist/index.js");
|
|
133
|
-
await installHourlyUpdateCron({
|
|
134
|
-
nodePath: process.execPath,
|
|
135
|
-
entryScriptPath,
|
|
136
|
-
});
|
|
137
|
-
process.stdout.write("Installed hourly cron job for bet update.\n");
|
|
192
|
+
catch (err) {
|
|
193
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
194
|
+
log.error(err instanceof Error ? err : new Error(message));
|
|
195
|
+
process.stderr.write(`Error: ${message}\n`);
|
|
196
|
+
process.exitCode = 1;
|
|
138
197
|
}
|
|
139
198
|
});
|
|
140
199
|
}
|
package/dist/index.js
CHANGED
|
@@ -7,11 +7,13 @@ import { registerInfo } from "./commands/info.js";
|
|
|
7
7
|
import { registerGo } from "./commands/go.js";
|
|
8
8
|
import { registerPath } from "./commands/path.js";
|
|
9
9
|
import { registerShell } from "./commands/shell.js";
|
|
10
|
+
import { registerCompletion } from "./commands/completion.js";
|
|
11
|
+
import { registerIgnore } from "./commands/ignore.js";
|
|
10
12
|
const program = new Command();
|
|
11
13
|
program
|
|
12
14
|
.name("bet")
|
|
13
15
|
.description("Explore and jump between local projects.")
|
|
14
|
-
.version("0.1.
|
|
16
|
+
.version("0.1.2");
|
|
15
17
|
registerUpdate(program);
|
|
16
18
|
registerList(program);
|
|
17
19
|
registerSearch(program);
|
|
@@ -19,4 +21,6 @@ registerInfo(program);
|
|
|
19
21
|
registerGo(program);
|
|
20
22
|
registerPath(program);
|
|
21
23
|
registerShell(program);
|
|
24
|
+
registerCompletion(program);
|
|
25
|
+
registerIgnore(program);
|
|
22
26
|
program.parseAsync(process.argv);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { readConfig } from "./config.js";
|
|
2
|
+
import { listProjects } from "./projects.js";
|
|
3
|
+
/**
|
|
4
|
+
* Returns project slugs for shell completion. On missing config or error,
|
|
5
|
+
* returns an empty array so the completion path can exit 0 with no output.
|
|
6
|
+
*/
|
|
7
|
+
export async function getProjectSlugs() {
|
|
8
|
+
try {
|
|
9
|
+
const config = await readConfig();
|
|
10
|
+
const projects = listProjects(config);
|
|
11
|
+
return projects.map((p) => p.slug);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
}
|
package/dist/lib/config.js
CHANGED
|
@@ -42,15 +42,39 @@ function normalizeRoots(parsedRoots) {
|
|
|
42
42
|
}
|
|
43
43
|
return result;
|
|
44
44
|
}
|
|
45
|
+
function normalizeIgnores(parsed) {
|
|
46
|
+
if (!Array.isArray(parsed))
|
|
47
|
+
return undefined;
|
|
48
|
+
const list = parsed.filter((x) => typeof x === "string");
|
|
49
|
+
return list;
|
|
50
|
+
}
|
|
51
|
+
function normalizeSlugParentFolders(parsed) {
|
|
52
|
+
if (!Array.isArray(parsed))
|
|
53
|
+
return undefined;
|
|
54
|
+
const list = parsed.filter((x) => typeof x === "string" && x.trim() !== "").map((x) => x.trim());
|
|
55
|
+
return list.length === 0 ? undefined : list;
|
|
56
|
+
}
|
|
57
|
+
function normalizeIgnoredPaths(parsed) {
|
|
58
|
+
if (!Array.isArray(parsed))
|
|
59
|
+
return undefined;
|
|
60
|
+
const list = parsed.filter((x) => typeof x === "string").map((x) => normalizeAbsolute(x));
|
|
61
|
+
return list.length === 0 ? undefined : list;
|
|
62
|
+
}
|
|
45
63
|
async function readAppConfig() {
|
|
46
64
|
try {
|
|
47
65
|
const raw = await fs.readFile(CONFIG_PATH, "utf8");
|
|
48
66
|
const parsed = JSON.parse(raw);
|
|
49
67
|
const roots = normalizeRoots(parsed.roots ?? []);
|
|
68
|
+
const ignores = normalizeIgnores(parsed.ignores);
|
|
69
|
+
const ignoredPaths = normalizeIgnoredPaths(parsed.ignoredPaths);
|
|
70
|
+
const slugParentFolders = normalizeSlugParentFolders(parsed.slugParentFolders);
|
|
50
71
|
return {
|
|
51
72
|
...DEFAULT_APP_CONFIG,
|
|
52
73
|
version: parsed.version ?? 1,
|
|
53
74
|
roots,
|
|
75
|
+
...(ignores !== undefined && { ignores }),
|
|
76
|
+
...(ignoredPaths !== undefined && { ignoredPaths }),
|
|
77
|
+
...(slugParentFolders !== undefined && { slugParentFolders }),
|
|
54
78
|
};
|
|
55
79
|
}
|
|
56
80
|
catch (error) {
|
|
@@ -108,6 +132,9 @@ export async function writeConfig(config) {
|
|
|
108
132
|
const appConfig = {
|
|
109
133
|
version: config.version,
|
|
110
134
|
roots: config.roots,
|
|
135
|
+
...(config.ignores !== undefined && { ignores: config.ignores }),
|
|
136
|
+
...(config.ignoredPaths !== undefined && { ignoredPaths: config.ignoredPaths }),
|
|
137
|
+
...(config.slugParentFolders !== undefined && { slugParentFolders: config.slugParentFolders }),
|
|
111
138
|
};
|
|
112
139
|
const projectsConfig = {
|
|
113
140
|
projects: config.projects,
|