replen 1.0.0 → 1.0.1
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/dist/commands.js +83 -0
- package/dist/discover-projects.js +182 -47
- package/dist/discover-roots.js +169 -0
- package/dist/first-ingest.js +140 -0
- package/dist/index.js +33 -4
- package/dist/init.js +8 -0
- package/dist/inject-instruction.js +85 -66
- package/dist/mcp-setup.js +162 -63
- package/dist/sync-projects.js +148 -24
- package/package.json +4 -1
package/dist/sync-projects.js
CHANGED
|
@@ -1,27 +1,34 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Orchestrates project discovery + server registration. Tries layered
|
|
2
|
+
// inference strategies (cwd / Claude Code config / hardcoded roots /
|
|
3
|
+
// interactive prompt) until at least one git repo is found, then walks
|
|
4
|
+
// each root recursively to find every repo, then POSTs to
|
|
5
|
+
// /api/projects/bulk for upsert.
|
|
6
|
+
//
|
|
7
|
+
// Persists the user's chosen root(s) to ~/.replen/config.json so future
|
|
8
|
+
// `npx replen sync-projects` runs use the same set without re-prompting.
|
|
9
|
+
import { readConfig, writeConfig } from "./config.js";
|
|
5
10
|
import { discoverProjects } from "./discover-projects.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
import { promptForRoot, rootsFromClaudeJson, rootsFromConfig, rootsFromCwdWalkUp, rootsFromEnv, rootsFromFlag, rootsFromHardcoded, } from "./discover-roots.js";
|
|
12
|
+
export async function syncDiscoveredProjects({ token, base, explicitRoots = [], }) {
|
|
13
|
+
const { result, source, prompted } = await resolveAndWalk(explicitRoots);
|
|
14
|
+
// Tell the user what we did, regardless of outcome.
|
|
15
|
+
reportDiscovery(result, source);
|
|
16
|
+
if (result.projects.length === 0) {
|
|
17
|
+
console.log(` · No git repos with GitHub remotes were found. Run \`npx replen sync-projects --root <path>\` to point at a specific dir, or set REPLEN_PROJECT_ROOTS=...`);
|
|
10
18
|
return { discovered: 0, created: 0, updated: 0 };
|
|
11
19
|
}
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
// Persist ONLY when the user came in via the interactive prompt.
|
|
21
|
+
// Flags and env vars are deliberate per-invocation overrides — saving
|
|
22
|
+
// them to config creates a confusing dual source of truth and, worse,
|
|
23
|
+
// can lock subsequent commands (like `inject` with no args) into the
|
|
24
|
+
// narrow scope the user chose for a one-off `--root` use.
|
|
25
|
+
if (prompted) {
|
|
26
|
+
await persistRoots(result.scannedRoots);
|
|
18
27
|
}
|
|
19
|
-
if (projects.length > 3)
|
|
20
|
-
console.log(` … and ${projects.length - 3} more`);
|
|
21
28
|
const payload = {
|
|
22
|
-
projects: projects.map((p) => ({
|
|
29
|
+
projects: result.projects.map((p) => ({
|
|
23
30
|
slug: p.slug,
|
|
24
|
-
githubFullName: p.githubFullName,
|
|
31
|
+
githubFullName: p.githubFullName, // null is impossible here — we filter above
|
|
25
32
|
name: p.name,
|
|
26
33
|
tags: p.tags,
|
|
27
34
|
primaryLanguage: p.primaryLanguage ?? undefined,
|
|
@@ -41,12 +48,12 @@ export async function syncDiscoveredProjects({ token, base, }) {
|
|
|
41
48
|
catch (e) {
|
|
42
49
|
console.warn(` ✗ Failed to reach ${base}/api/projects/bulk: ${e.message}`);
|
|
43
50
|
console.warn(` Skipping project registration. Run \`npx replen sync-projects\` later to retry.`);
|
|
44
|
-
return { discovered: projects.length, created: 0, updated: 0 };
|
|
51
|
+
return { discovered: result.projects.length, created: 0, updated: 0 };
|
|
45
52
|
}
|
|
46
53
|
if (!res.ok) {
|
|
47
54
|
const text = await res.text().catch(() => "");
|
|
48
55
|
console.warn(` ✗ Project registration failed (HTTP ${res.status}): ${text.slice(0, 200)}`);
|
|
49
|
-
return { discovered: projects.length, created: 0, updated: 0 };
|
|
56
|
+
return { discovered: result.projects.length, created: 0, updated: 0 };
|
|
50
57
|
}
|
|
51
58
|
const body = (await res.json());
|
|
52
59
|
const created = body.created ?? 0;
|
|
@@ -60,12 +67,129 @@ export async function syncDiscoveredProjects({ token, base, }) {
|
|
|
60
67
|
console.log(` ✓ Registered with Replen: ${parts.join(", ")}`);
|
|
61
68
|
}
|
|
62
69
|
else {
|
|
63
|
-
console.log(` · All ${projects.length} projects already up to date with Replen`);
|
|
70
|
+
console.log(` · All ${result.projects.length} projects already up to date with Replen`);
|
|
64
71
|
}
|
|
65
|
-
return { discovered: projects.length, created, updated };
|
|
72
|
+
return { discovered: result.projects.length, created, updated };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Try discovery strategies in priority order. The first strategy that
|
|
76
|
+
* yields ≥1 project's worth of repos wins; if none do, falls back to
|
|
77
|
+
* the interactive prompt.
|
|
78
|
+
*
|
|
79
|
+
* Returns the walked result, the strategy that supplied the roots,
|
|
80
|
+
* and a flag indicating whether the interactive prompt was used.
|
|
81
|
+
*/
|
|
82
|
+
async function resolveAndWalk(explicitRoots) {
|
|
83
|
+
// Strategies that take precedence and short-circuit on hit.
|
|
84
|
+
const ordered = [
|
|
85
|
+
{ source: "flag", roots: () => rootsFromFlag(explicitRoots) },
|
|
86
|
+
{ source: "env", roots: () => rootsFromEnv() },
|
|
87
|
+
{ source: "config", roots: () => rootsFromConfig() },
|
|
88
|
+
];
|
|
89
|
+
for (const { source, roots } of ordered) {
|
|
90
|
+
const r = await roots();
|
|
91
|
+
if (r.length > 0) {
|
|
92
|
+
const result = discoverProjects(r);
|
|
93
|
+
return { result, source, prompted: false };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Inference strategies combine: walk the union of cwd / Claude / hardcoded.
|
|
97
|
+
// Any one of them surfacing repos is enough; we union to maximise the
|
|
98
|
+
// chance of catching the user's repos no matter where they live.
|
|
99
|
+
const inferred = Array.from(new Set([
|
|
100
|
+
...rootsFromCwdWalkUp(),
|
|
101
|
+
...rootsFromClaudeJson(),
|
|
102
|
+
...rootsFromHardcoded(),
|
|
103
|
+
]));
|
|
104
|
+
if (inferred.length > 0) {
|
|
105
|
+
const result = discoverProjects(inferred);
|
|
106
|
+
if (result.projects.length > 0) {
|
|
107
|
+
return { result, source: pickInferredSource(inferred), prompted: false };
|
|
108
|
+
}
|
|
109
|
+
// Inferred roots existed but contained no GitHub repos — fall
|
|
110
|
+
// through to prompt rather than reporting empty.
|
|
111
|
+
}
|
|
112
|
+
// Last resort: ask.
|
|
113
|
+
const chosen = await promptForRoot();
|
|
114
|
+
if (!chosen) {
|
|
115
|
+
return {
|
|
116
|
+
result: { projects: [], nonGithubSkipped: 0, scannedRoots: inferred },
|
|
117
|
+
source: "prompt",
|
|
118
|
+
prompted: true,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const result = discoverProjects([chosen]);
|
|
122
|
+
return { result, source: "prompt", prompted: true };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* If the inferred-roots union turned up projects, pick the most
|
|
126
|
+
* specific source for the user-facing message. Order matches priority:
|
|
127
|
+
* cwd is most specific, hardcoded least.
|
|
128
|
+
*/
|
|
129
|
+
function pickInferredSource(roots) {
|
|
130
|
+
if (rootsFromCwdWalkUp().some((r) => roots.includes(r)))
|
|
131
|
+
return "cwd";
|
|
132
|
+
if (rootsFromClaudeJson().some((r) => roots.includes(r)))
|
|
133
|
+
return "claude-json";
|
|
134
|
+
return "hardcoded";
|
|
135
|
+
}
|
|
136
|
+
function reportDiscovery(result, source) {
|
|
137
|
+
const sourceLabel = {
|
|
138
|
+
flag: "from --root flag",
|
|
139
|
+
env: "from REPLEN_PROJECT_ROOTS env",
|
|
140
|
+
config: "from saved config",
|
|
141
|
+
cwd: "via cwd walk-up",
|
|
142
|
+
"claude-json": "via Claude Code's tracked projects",
|
|
143
|
+
hardcoded: "via conventional ~/github, ~/code, ~/projects",
|
|
144
|
+
prompt: "you specified",
|
|
145
|
+
};
|
|
146
|
+
console.log(` · Scanning ${result.scannedRoots.length} root(s) (${sourceLabel[source]}):`);
|
|
147
|
+
for (const r of result.scannedRoots.slice(0, 5)) {
|
|
148
|
+
console.log(` ${r}`);
|
|
149
|
+
}
|
|
150
|
+
if (result.scannedRoots.length > 5) {
|
|
151
|
+
console.log(` … and ${result.scannedRoots.length - 5} more`);
|
|
152
|
+
}
|
|
153
|
+
if (result.projects.length === 0)
|
|
154
|
+
return;
|
|
155
|
+
console.log(` ✓ Found ${result.projects.length} git repo(s) with GitHub remotes:`);
|
|
156
|
+
for (const p of result.projects.slice(0, 5)) {
|
|
157
|
+
const sampleTags = p.tags.slice(0, 3).join(", ") || "(no auto-tags)";
|
|
158
|
+
const localHint = pathHint(p);
|
|
159
|
+
console.log(` • ${p.githubFullName} ${localHint} → ${sampleTags}`);
|
|
160
|
+
}
|
|
161
|
+
if (result.projects.length > 5) {
|
|
162
|
+
console.log(` … and ${result.projects.length - 5} more`);
|
|
163
|
+
}
|
|
164
|
+
if (result.nonGithubSkipped > 0) {
|
|
165
|
+
console.log(` · Skipped ${result.nonGithubSkipped} local repo(s) without a GitHub remote (nothing to match against)`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** Render "(at ~/projects/drone)" only when the dirname differs from the
|
|
169
|
+
* repo name — i.e. only when there's actual signal worth showing. */
|
|
170
|
+
function pathHint(p) {
|
|
171
|
+
const repoName = (p.githubFullName ?? "").split("/")[1] ?? "";
|
|
172
|
+
const dirName = p.localPath.split("/").pop() ?? "";
|
|
173
|
+
if (!repoName || dirName.toLowerCase() === repoName.toLowerCase())
|
|
174
|
+
return "";
|
|
175
|
+
// Compress home prefix for readability.
|
|
176
|
+
const home = process.env.HOME ?? "";
|
|
177
|
+
const display = home && p.localPath.startsWith(home) ? "~" + p.localPath.slice(home.length) : p.localPath;
|
|
178
|
+
return ` (at ${display})`;
|
|
179
|
+
}
|
|
180
|
+
async function persistRoots(roots) {
|
|
181
|
+
const cfg = await readConfig();
|
|
182
|
+
if (!cfg)
|
|
183
|
+
return;
|
|
184
|
+
// Don't overwrite if config already had a roots list — leave the
|
|
185
|
+
// user's previous choice alone unless they re-prompt.
|
|
186
|
+
if (cfg.projectRoots && cfg.projectRoots.length > 0)
|
|
187
|
+
return;
|
|
188
|
+
await writeConfig({ ...cfg, projectRoots: roots });
|
|
66
189
|
}
|
|
67
190
|
// Returns just the discovered list without sending. Useful for the
|
|
68
191
|
// CLI's `replen list-projects` subcommand (preview mode).
|
|
69
|
-
export function previewDiscovery() {
|
|
70
|
-
|
|
192
|
+
export async function previewDiscovery(explicitRoots = []) {
|
|
193
|
+
const { result } = await resolveAndWalk(explicitRoots);
|
|
194
|
+
return result;
|
|
71
195
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "replen",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Make your AI coding tools smarter. One command, no API keys. Replen scouts the OSS firehose against your projects and surfaces drop-in libraries, ideas to port, and dead deps to swap — the match decision happens inside your AI tool's session on your subscription tokens. 1-3 actionable matches a month, by design.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -39,5 +39,8 @@
|
|
|
39
39
|
"@types/node": "^22.10.5",
|
|
40
40
|
"tsx": "^4.19.2",
|
|
41
41
|
"typescript": "^5.7.2"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"smol-toml": "^1.6.1"
|
|
42
45
|
}
|
|
43
46
|
}
|