forgemap 0.4.1-dev.67-4a4bf9d → 0.4.1-dev.69-291072b
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 +7 -1
- package/dist/bin/forgemap.mjs +177 -69
- package/dist/bin/forgemap.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -83,7 +83,8 @@ forgemap pick kirch # picker pre-filtered by fuzzy query
|
|
|
83
83
|
### Open the folder in the OS file manager
|
|
84
84
|
|
|
85
85
|
```bash
|
|
86
|
-
forgemap open kirchDev/laravel-pbac
|
|
86
|
+
forgemap open kirchDev/laravel-pbac # exact slug
|
|
87
|
+
forgemap open laravel # fuzzy single match → same folder
|
|
87
88
|
```
|
|
88
89
|
|
|
89
90
|
- **WSL** → launches `explorer.exe` against `\\wsl$\<distro>\…`, Explorer opens the folder
|
|
@@ -99,8 +100,13 @@ forgemap sync --forge work --query api # restrict scope
|
|
|
99
100
|
|
|
100
101
|
forgemap status # tree: branch / dirty / ahead↑ / behind↓ / last commit
|
|
101
102
|
forgemap status --format json # structured for jq + scripts
|
|
103
|
+
|
|
104
|
+
# --filter keeps only matching owners/forges. Repeatable, OR-combined.
|
|
105
|
+
forgemap status --format json --filter kirchDev --filter TitusKirch
|
|
102
106
|
```
|
|
103
107
|
|
|
108
|
+
`--filter` works on `status`, `sync` and `search`. It matches an **owner** or a **forge name** exactly (case-insensitive) — unlike `--query`, which is fuzzy.
|
|
109
|
+
|
|
104
110
|
All tree output (`status`, `search`, `import`) groups as `forge → owner → repo`. Network operations (`sync`, `import`, `cleanup`) run with a hard timeout and non-interactive SSH, so an unreachable host can never wedge a run.
|
|
105
111
|
|
|
106
112
|
### Adopt an existing layout — `import`
|
package/dist/bin/forgemap.mjs
CHANGED
|
@@ -672,6 +672,9 @@ const SSH_RE = /^git@([\w.-]+):([\w.-]+)\/([\w.-]+?)(?:\.git)?$/;
|
|
|
672
672
|
function stripGitSuffix(repo) {
|
|
673
673
|
return repo.endsWith(".git") ? repo.slice(0, -4) : repo;
|
|
674
674
|
}
|
|
675
|
+
function looksLikeSlug(input) {
|
|
676
|
+
return input.trim().includes("/");
|
|
677
|
+
}
|
|
675
678
|
function parseSlug(input) {
|
|
676
679
|
const trimmed = input.trim();
|
|
677
680
|
if (!trimmed) {
|
|
@@ -2050,6 +2053,111 @@ const importCommand = defineCommand({
|
|
|
2050
2053
|
});
|
|
2051
2054
|
}
|
|
2052
2055
|
});
|
|
2056
|
+
const REPO_FUSE_OPTIONS = {
|
|
2057
|
+
keys: ["slug", "owner", "repo"],
|
|
2058
|
+
threshold: 0.3,
|
|
2059
|
+
ignoreLocation: true
|
|
2060
|
+
};
|
|
2061
|
+
function createRepoFuse(repos) {
|
|
2062
|
+
return new Fuse(repos, REPO_FUSE_OPTIONS);
|
|
2063
|
+
}
|
|
2064
|
+
function matchRepos(repos, query, limit) {
|
|
2065
|
+
const fuse = createRepoFuse(repos);
|
|
2066
|
+
const results = fuse.search(query, limit ? { limit } : void 0);
|
|
2067
|
+
return results.map((r) => r.item);
|
|
2068
|
+
}
|
|
2069
|
+
async function promptRepoChoice(candidates) {
|
|
2070
|
+
const out = process.stdout;
|
|
2071
|
+
const realWrite = out.write;
|
|
2072
|
+
const saved = {
|
|
2073
|
+
rows: Object.getOwnPropertyDescriptor(out, "rows"),
|
|
2074
|
+
columns: Object.getOwnPropertyDescriptor(out, "columns"),
|
|
2075
|
+
isTTY: Object.getOwnPropertyDescriptor(out, "isTTY")
|
|
2076
|
+
};
|
|
2077
|
+
const fake = (key, value) => {
|
|
2078
|
+
Object.defineProperty(out, key, { configurable: true, value });
|
|
2079
|
+
};
|
|
2080
|
+
const restore = (key) => {
|
|
2081
|
+
if (saved[key]) Object.defineProperty(out, key, saved[key]);
|
|
2082
|
+
else delete out[key];
|
|
2083
|
+
};
|
|
2084
|
+
out.write = process.stderr.write.bind(process.stderr);
|
|
2085
|
+
fake("rows", process.stderr.rows ?? 24);
|
|
2086
|
+
fake("columns", process.stderr.columns ?? 80);
|
|
2087
|
+
fake("isTTY", true);
|
|
2088
|
+
let choice;
|
|
2089
|
+
try {
|
|
2090
|
+
choice = await consola.prompt("Select a repo", {
|
|
2091
|
+
type: "select",
|
|
2092
|
+
options: candidates.map((r) => ({
|
|
2093
|
+
label: `${colors.gray(`${r.forgeName}:`)}${r.slug}`,
|
|
2094
|
+
value: r.localPath,
|
|
2095
|
+
hint: r.localPath
|
|
2096
|
+
}))
|
|
2097
|
+
});
|
|
2098
|
+
} finally {
|
|
2099
|
+
out.write = realWrite;
|
|
2100
|
+
restore("rows");
|
|
2101
|
+
restore("columns");
|
|
2102
|
+
restore("isTTY");
|
|
2103
|
+
}
|
|
2104
|
+
return typeof choice === "string" && choice ? choice : void 0;
|
|
2105
|
+
}
|
|
2106
|
+
function canPrompt() {
|
|
2107
|
+
return Boolean(process.stdin.isTTY);
|
|
2108
|
+
}
|
|
2109
|
+
async function locateRepo(input, options) {
|
|
2110
|
+
const { config, configDir } = options;
|
|
2111
|
+
if (!input.trim() || looksLikeSlug(input)) {
|
|
2112
|
+
const resolved = resolveSlug(parseSlug(input), { config, configDir });
|
|
2113
|
+
return { kind: "slug", localPath: resolved.localPath };
|
|
2114
|
+
}
|
|
2115
|
+
const repos = options.repos ?? await scanRepos({ config, configDir });
|
|
2116
|
+
const candidates = matchRepos(repos, input);
|
|
2117
|
+
if (candidates.length === 0) return { kind: "none", query: input };
|
|
2118
|
+
if (candidates.length === 1) {
|
|
2119
|
+
return {
|
|
2120
|
+
kind: "match",
|
|
2121
|
+
localPath: candidates[0].localPath,
|
|
2122
|
+
repo: candidates[0]
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
return { kind: "ambiguous", query: input, candidates };
|
|
2126
|
+
}
|
|
2127
|
+
async function resolveRepoPath(input, options) {
|
|
2128
|
+
const outcome = await locateRepo(input, options);
|
|
2129
|
+
switch (outcome.kind) {
|
|
2130
|
+
case "slug":
|
|
2131
|
+
case "match":
|
|
2132
|
+
return outcome.localPath;
|
|
2133
|
+
case "none":
|
|
2134
|
+
consola.error(`No cloned repo matches "${outcome.query}".`);
|
|
2135
|
+
process.stderr.write(
|
|
2136
|
+
`${colors.dim("Pass an explicit <owner>/<repo> for a repo that is not cloned yet.")}
|
|
2137
|
+
`
|
|
2138
|
+
);
|
|
2139
|
+
return null;
|
|
2140
|
+
case "ambiguous": {
|
|
2141
|
+
if (canPrompt()) {
|
|
2142
|
+
return await promptRepoChoice(outcome.candidates) ?? null;
|
|
2143
|
+
}
|
|
2144
|
+
consola.error(
|
|
2145
|
+
`"${outcome.query}" matches ${outcome.candidates.length} cloned repos:`
|
|
2146
|
+
);
|
|
2147
|
+
for (const c of outcome.candidates) {
|
|
2148
|
+
process.stderr.write(
|
|
2149
|
+
` ${colors.cyan(`${c.forgeName}:${c.slug}`)} ${colors.dim(c.localPath)}
|
|
2150
|
+
`
|
|
2151
|
+
);
|
|
2152
|
+
}
|
|
2153
|
+
process.stderr.write(
|
|
2154
|
+
`${colors.dim("Narrow the query, pass an explicit <owner>/<repo>, or run `forgemap pick` to choose interactively.")}
|
|
2155
|
+
`
|
|
2156
|
+
);
|
|
2157
|
+
return null;
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2053
2161
|
function platformOpen(localPath) {
|
|
2054
2162
|
const distro = process.env.WSL_DISTRO_NAME;
|
|
2055
2163
|
if (distro) {
|
|
@@ -2069,7 +2177,7 @@ const openCommand = defineCommand({
|
|
|
2069
2177
|
args: {
|
|
2070
2178
|
slug: {
|
|
2071
2179
|
type: "positional",
|
|
2072
|
-
description: "owner/repo, forge:owner/repo, or
|
|
2180
|
+
description: "owner/repo, forge:owner/repo, full URL, or a fuzzy query matched against cloned repos",
|
|
2073
2181
|
required: true
|
|
2074
2182
|
},
|
|
2075
2183
|
config: {
|
|
@@ -2079,14 +2187,17 @@ const openCommand = defineCommand({
|
|
|
2079
2187
|
},
|
|
2080
2188
|
async run({ args }) {
|
|
2081
2189
|
const loaded = await loadForgeMapConfig({ configFile: args.config });
|
|
2082
|
-
const parsed = parseSlug(args.slug);
|
|
2083
2190
|
const configDir = loaded.configFile ? dirname(loaded.configFile) : loaded.cwd;
|
|
2084
|
-
const
|
|
2191
|
+
const localPath = await resolveRepoPath(args.slug, {
|
|
2085
2192
|
config: loaded.config,
|
|
2086
2193
|
configDir
|
|
2087
2194
|
});
|
|
2088
|
-
|
|
2089
|
-
|
|
2195
|
+
if (!localPath) {
|
|
2196
|
+
process.exitCode = 1;
|
|
2197
|
+
return;
|
|
2198
|
+
}
|
|
2199
|
+
const { cmd, args: cmdArgs } = platformOpen(localPath);
|
|
2200
|
+
consola.info(`Opening ${localPath}`);
|
|
2090
2201
|
const child = spawn(cmd, cmdArgs, {
|
|
2091
2202
|
stdio: "ignore",
|
|
2092
2203
|
detached: true
|
|
@@ -2113,7 +2224,7 @@ const pathCommand = defineCommand({
|
|
|
2113
2224
|
args: {
|
|
2114
2225
|
slug: {
|
|
2115
2226
|
type: "positional",
|
|
2116
|
-
description: "owner/repo, forge:owner/repo, or
|
|
2227
|
+
description: "owner/repo, forge:owner/repo, full URL, or a fuzzy query matched against cloned repos",
|
|
2117
2228
|
required: true
|
|
2118
2229
|
},
|
|
2119
2230
|
config: {
|
|
@@ -2123,13 +2234,16 @@ const pathCommand = defineCommand({
|
|
|
2123
2234
|
},
|
|
2124
2235
|
async run({ args }) {
|
|
2125
2236
|
const loaded = await loadForgeMapConfig({ configFile: args.config });
|
|
2126
|
-
const parsed = parseSlug(args.slug);
|
|
2127
2237
|
const configDir = loaded.configFile ? dirname(loaded.configFile) : loaded.cwd;
|
|
2128
|
-
const
|
|
2238
|
+
const localPath = await resolveRepoPath(args.slug, {
|
|
2129
2239
|
config: loaded.config,
|
|
2130
2240
|
configDir
|
|
2131
2241
|
});
|
|
2132
|
-
|
|
2242
|
+
if (!localPath) {
|
|
2243
|
+
process.exitCode = 1;
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
process.stdout.write(`${localPath}
|
|
2133
2247
|
`);
|
|
2134
2248
|
}
|
|
2135
2249
|
});
|
|
@@ -2153,17 +2267,7 @@ const pickCommand = defineCommand({
|
|
|
2153
2267
|
const loaded = await loadForgeMapConfig({ configFile: args.config });
|
|
2154
2268
|
const configDir = loaded.configFile ? dirname(loaded.configFile) : loaded.cwd;
|
|
2155
2269
|
const all = await scanRepos({ config: loaded.config, configDir });
|
|
2156
|
-
|
|
2157
|
-
if (args.query) {
|
|
2158
|
-
const fuse = new Fuse(all, {
|
|
2159
|
-
keys: ["slug", "owner", "repo"],
|
|
2160
|
-
threshold: 0.3,
|
|
2161
|
-
ignoreLocation: true
|
|
2162
|
-
});
|
|
2163
|
-
candidates = fuse.search(args.query).map((r) => r.item);
|
|
2164
|
-
} else {
|
|
2165
|
-
candidates = all;
|
|
2166
|
-
}
|
|
2270
|
+
const candidates = args.query ? matchRepos(all, args.query) : all;
|
|
2167
2271
|
if (candidates.length === 0) {
|
|
2168
2272
|
consola.error(
|
|
2169
2273
|
args.query ? `No repos match "${args.query}".` : "No repos found under the configured root."
|
|
@@ -2176,53 +2280,58 @@ const pickCommand = defineCommand({
|
|
|
2176
2280
|
`);
|
|
2177
2281
|
return;
|
|
2178
2282
|
}
|
|
2179
|
-
if (!
|
|
2283
|
+
if (!canPrompt()) {
|
|
2180
2284
|
consola.error(
|
|
2181
2285
|
"pick requires an interactive terminal. Use `forgemap search` for non-interactive output."
|
|
2182
2286
|
);
|
|
2183
2287
|
process.exitCode = 1;
|
|
2184
2288
|
return;
|
|
2185
2289
|
}
|
|
2186
|
-
const
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
rows: Object.getOwnPropertyDescriptor(out, "rows"),
|
|
2190
|
-
columns: Object.getOwnPropertyDescriptor(out, "columns"),
|
|
2191
|
-
isTTY: Object.getOwnPropertyDescriptor(out, "isTTY")
|
|
2192
|
-
};
|
|
2193
|
-
const fake = (key, value) => {
|
|
2194
|
-
Object.defineProperty(out, key, { configurable: true, value });
|
|
2195
|
-
};
|
|
2196
|
-
const restore = (key) => {
|
|
2197
|
-
if (saved[key]) Object.defineProperty(out, key, saved[key]);
|
|
2198
|
-
else delete out[key];
|
|
2199
|
-
};
|
|
2200
|
-
out.write = process.stderr.write.bind(process.stderr);
|
|
2201
|
-
fake("rows", process.stderr.rows ?? 24);
|
|
2202
|
-
fake("columns", process.stderr.columns ?? 80);
|
|
2203
|
-
fake("isTTY", true);
|
|
2204
|
-
let choice;
|
|
2205
|
-
try {
|
|
2206
|
-
choice = await consola.prompt("Select a repo", {
|
|
2207
|
-
type: "select",
|
|
2208
|
-
options: candidates.map((r) => ({
|
|
2209
|
-
label: `${colors.gray(`${r.forgeName}:`)}${r.slug}`,
|
|
2210
|
-
value: r.localPath,
|
|
2211
|
-
hint: r.localPath
|
|
2212
|
-
}))
|
|
2213
|
-
});
|
|
2214
|
-
} finally {
|
|
2215
|
-
out.write = realWrite;
|
|
2216
|
-
restore("rows");
|
|
2217
|
-
restore("columns");
|
|
2218
|
-
restore("isTTY");
|
|
2219
|
-
}
|
|
2220
|
-
if (typeof choice === "string" && choice) {
|
|
2221
|
-
realWrite.call(out, `${choice}
|
|
2290
|
+
const choice = await promptRepoChoice(candidates);
|
|
2291
|
+
if (choice) {
|
|
2292
|
+
process.stdout.write(`${choice}
|
|
2222
2293
|
`);
|
|
2223
2294
|
}
|
|
2224
2295
|
}
|
|
2225
2296
|
});
|
|
2297
|
+
const FLAG = "--filter";
|
|
2298
|
+
const filterArg = {
|
|
2299
|
+
type: "string",
|
|
2300
|
+
description: "Restrict to repos whose owner or forge name matches. Repeatable; a repo passes if it matches any value."
|
|
2301
|
+
};
|
|
2302
|
+
function collectFilterArgs(rawArgs) {
|
|
2303
|
+
const values = [];
|
|
2304
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
2305
|
+
const arg = rawArgs[i];
|
|
2306
|
+
if (arg === "--") break;
|
|
2307
|
+
if (arg === FLAG) {
|
|
2308
|
+
const next = rawArgs[i + 1];
|
|
2309
|
+
if (next !== void 0 && !next.startsWith("-")) {
|
|
2310
|
+
values.push(next);
|
|
2311
|
+
i++;
|
|
2312
|
+
}
|
|
2313
|
+
continue;
|
|
2314
|
+
}
|
|
2315
|
+
if (arg.startsWith(`${FLAG}=`)) values.push(arg.slice(FLAG.length + 1));
|
|
2316
|
+
}
|
|
2317
|
+
return values;
|
|
2318
|
+
}
|
|
2319
|
+
function normalizeFilters(value) {
|
|
2320
|
+
if (value === void 0) return [];
|
|
2321
|
+
const values = Array.isArray(value) ? value : [value];
|
|
2322
|
+
return values.map((v) => v.trim()).filter((v) => v.length > 0);
|
|
2323
|
+
}
|
|
2324
|
+
function resolveFilters(rawArgs, value) {
|
|
2325
|
+
const fromRawArgs = collectFilterArgs(rawArgs);
|
|
2326
|
+
return normalizeFilters(fromRawArgs.length > 0 ? fromRawArgs : value);
|
|
2327
|
+
}
|
|
2328
|
+
function filterRepos(repos, filters) {
|
|
2329
|
+
if (filters.length === 0) return repos;
|
|
2330
|
+
const wanted = new Set(filters.map((f) => f.toLowerCase()));
|
|
2331
|
+
return repos.filter(
|
|
2332
|
+
(r) => wanted.has(r.owner.toLowerCase()) || wanted.has(r.forgeName.toLowerCase())
|
|
2333
|
+
);
|
|
2334
|
+
}
|
|
2226
2335
|
function renderTree$1(repos) {
|
|
2227
2336
|
const byForge = /* @__PURE__ */ new Map();
|
|
2228
2337
|
for (const r of repos) {
|
|
@@ -2263,6 +2372,7 @@ const searchCommand = defineCommand({
|
|
|
2263
2372
|
description: "Output format: auto (default), pretty, path, or slug. auto picks pretty in a TTY, path when piped.",
|
|
2264
2373
|
default: "auto"
|
|
2265
2374
|
},
|
|
2375
|
+
filter: filterArg,
|
|
2266
2376
|
limit: {
|
|
2267
2377
|
type: "string",
|
|
2268
2378
|
description: "Maximum number of matches to print (default: unlimited)"
|
|
@@ -2272,19 +2382,13 @@ const searchCommand = defineCommand({
|
|
|
2272
2382
|
description: "Path to forgemap.config.ts (overrides walk-up discovery)"
|
|
2273
2383
|
}
|
|
2274
2384
|
},
|
|
2275
|
-
async run({ args }) {
|
|
2385
|
+
async run({ args, rawArgs }) {
|
|
2276
2386
|
const loaded = await loadForgeMapConfig({ configFile: args.config });
|
|
2277
2387
|
const configDir = loaded.configFile ? dirname(loaded.configFile) : loaded.cwd;
|
|
2278
|
-
const
|
|
2279
|
-
const
|
|
2280
|
-
keys: ["slug", "owner", "repo"],
|
|
2281
|
-
threshold: 0.3,
|
|
2282
|
-
ignoreLocation: true,
|
|
2283
|
-
includeScore: true
|
|
2284
|
-
});
|
|
2388
|
+
const scanned = await scanRepos({ config: loaded.config, configDir });
|
|
2389
|
+
const repos = filterRepos(scanned, resolveFilters(rawArgs, args.filter));
|
|
2285
2390
|
const limit = args.limit ? Number.parseInt(args.limit, 10) : void 0;
|
|
2286
|
-
const
|
|
2287
|
-
const items = results.map((r) => r.item);
|
|
2391
|
+
const items = matchRepos(repos, args.query, limit);
|
|
2288
2392
|
const allowed = ["auto", "pretty", "path", "slug"];
|
|
2289
2393
|
if (!allowed.includes(args.format)) {
|
|
2290
2394
|
consola.error(
|
|
@@ -2495,6 +2599,7 @@ const statusCommand = defineCommand({
|
|
|
2495
2599
|
type: "string",
|
|
2496
2600
|
description: "Restrict to a single forge alias"
|
|
2497
2601
|
},
|
|
2602
|
+
filter: filterArg,
|
|
2498
2603
|
query: {
|
|
2499
2604
|
type: "string",
|
|
2500
2605
|
description: "Fuzzy filter against <owner>/<repo>"
|
|
@@ -2509,7 +2614,7 @@ const statusCommand = defineCommand({
|
|
|
2509
2614
|
description: "Path to forgemap.config.ts (overrides walk-up discovery)"
|
|
2510
2615
|
}
|
|
2511
2616
|
},
|
|
2512
|
-
async run({ args }) {
|
|
2617
|
+
async run({ args, rawArgs }) {
|
|
2513
2618
|
if (!ALLOWED_FORMATS.includes(args.format)) {
|
|
2514
2619
|
consola.error(
|
|
2515
2620
|
`Invalid --format value "${args.format}". Allowed: ${ALLOWED_FORMATS.join(", ")}.`
|
|
@@ -2527,6 +2632,7 @@ const statusCommand = defineCommand({
|
|
|
2527
2632
|
if (args.forge) {
|
|
2528
2633
|
repos = repos.filter((r) => r.forgeName === args.forge);
|
|
2529
2634
|
}
|
|
2635
|
+
repos = filterRepos(repos, resolveFilters(rawArgs, args.filter));
|
|
2530
2636
|
if (args.query) {
|
|
2531
2637
|
const fuse = new Fuse(repos, {
|
|
2532
2638
|
keys: ["slug", "owner", "repo"],
|
|
@@ -2608,6 +2714,7 @@ const syncCommand = defineCommand({
|
|
|
2608
2714
|
type: "string",
|
|
2609
2715
|
description: "Restrict to a single forge alias"
|
|
2610
2716
|
},
|
|
2717
|
+
filter: filterArg,
|
|
2611
2718
|
query: {
|
|
2612
2719
|
type: "string",
|
|
2613
2720
|
description: "Fuzzy filter against <owner>/<repo>"
|
|
@@ -2622,7 +2729,7 @@ const syncCommand = defineCommand({
|
|
|
2622
2729
|
description: "Path to forgemap.config.ts (overrides walk-up discovery)"
|
|
2623
2730
|
}
|
|
2624
2731
|
},
|
|
2625
|
-
async run({ args }) {
|
|
2732
|
+
async run({ args, rawArgs }) {
|
|
2626
2733
|
const loaded = await loadForgeMapConfig({ configFile: args.config });
|
|
2627
2734
|
const configDir = loaded.configFile ? dirname(loaded.configFile) : loaded.cwd;
|
|
2628
2735
|
let repos = await scanReposCached({
|
|
@@ -2633,6 +2740,7 @@ const syncCommand = defineCommand({
|
|
|
2633
2740
|
if (args.forge) {
|
|
2634
2741
|
repos = repos.filter((r) => r.forgeName === args.forge);
|
|
2635
2742
|
}
|
|
2743
|
+
repos = filterRepos(repos, resolveFilters(rawArgs, args.filter));
|
|
2636
2744
|
if (args.query) {
|
|
2637
2745
|
const fuse = new Fuse(repos, {
|
|
2638
2746
|
keys: ["slug", "owner", "repo"],
|