@themoltnet/legreffier 0.32.0 → 0.32.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/index.js +126 -6
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -8084,6 +8084,28 @@ async function writeEnvFile(opts) {
|
|
|
8084
8084
|
await writeFile(envPath, outputLines.join("\n") + "\n", "utf-8");
|
|
8085
8085
|
}
|
|
8086
8086
|
/**
|
|
8087
|
+
* Update a single managed key in an existing env file.
|
|
8088
|
+
* If the key exists, its value is replaced; otherwise appended.
|
|
8089
|
+
*/
|
|
8090
|
+
async function updateEnvVar(envDir, key, value) {
|
|
8091
|
+
const envPath = join(envDir, "env");
|
|
8092
|
+
let content;
|
|
8093
|
+
try {
|
|
8094
|
+
content = await readFile(envPath, "utf-8");
|
|
8095
|
+
} catch {
|
|
8096
|
+
content = "";
|
|
8097
|
+
}
|
|
8098
|
+
const line = `${key}=${q(value)}`;
|
|
8099
|
+
const keyPrefix = `${key}=`;
|
|
8100
|
+
const lines = content === "" ? [] : content.split("\n");
|
|
8101
|
+
const index = lines.findIndex((l) => l.startsWith(keyPrefix));
|
|
8102
|
+
if (index !== -1) {
|
|
8103
|
+
lines[index] = line;
|
|
8104
|
+
content = lines.join("\n");
|
|
8105
|
+
} else content = content.endsWith("\n") ? content + line + "\n" : content + "\n" + line + "\n";
|
|
8106
|
+
await writeFile(envPath, content, "utf-8");
|
|
8107
|
+
}
|
|
8108
|
+
/**
|
|
8087
8109
|
* Resolve the human operator's git identity from global git config.
|
|
8088
8110
|
* Must be called BEFORE GIT_CONFIG_GLOBAL is set (so it reads the
|
|
8089
8111
|
* human's config, not the agent's).
|
|
@@ -8529,10 +8551,41 @@ async function writeTokenCache(cachePath, token, expiresAt) {
|
|
|
8529
8551
|
} catch {}
|
|
8530
8552
|
}
|
|
8531
8553
|
/**
|
|
8532
|
-
*
|
|
8533
|
-
*
|
|
8534
|
-
*
|
|
8554
|
+
* List all installations of this GitHub App and return the one whose
|
|
8555
|
+
* `account.login` matches the given owner (case-insensitive).
|
|
8556
|
+
*
|
|
8557
|
+
* Uses the App JWT (not an installation token), so it works even when
|
|
8558
|
+
* `installation_id` is missing or stale.
|
|
8535
8559
|
*/
|
|
8560
|
+
async function findInstallationForOwner(opts) {
|
|
8561
|
+
const privateKeyPem = await readFile(opts.privateKeyPath, "utf-8");
|
|
8562
|
+
const jwt = createAppJWT(opts.appId, privateKeyPem);
|
|
8563
|
+
const ownerLower = opts.owner.toLowerCase();
|
|
8564
|
+
let nextUrl = "https://api.github.com/app/installations?per_page=100";
|
|
8565
|
+
let pageCount = 0;
|
|
8566
|
+
const MAX_PAGES = 10;
|
|
8567
|
+
while (nextUrl && pageCount < MAX_PAGES) {
|
|
8568
|
+
pageCount++;
|
|
8569
|
+
const res = await fetch(nextUrl, { headers: {
|
|
8570
|
+
Authorization: `Bearer ${jwt}`,
|
|
8571
|
+
Accept: "application/vnd.github+json",
|
|
8572
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
8573
|
+
} });
|
|
8574
|
+
if (!res.ok) throw new Error(`GitHub API error listing installations (${res.status}): ${await res.text()}`);
|
|
8575
|
+
const match = (await res.json()).find((i) => i.account?.login.toLowerCase() === ownerLower);
|
|
8576
|
+
if (match) return { installationId: String(match.id) };
|
|
8577
|
+
const linkHeader = res.headers.get("link");
|
|
8578
|
+
nextUrl = linkHeader ? parseNextLinkHeader(linkHeader) : null;
|
|
8579
|
+
}
|
|
8580
|
+
return null;
|
|
8581
|
+
}
|
|
8582
|
+
function parseNextLinkHeader(header) {
|
|
8583
|
+
for (const part of header.split(",")) {
|
|
8584
|
+
const match = part.match(/<([^>]+)>\s*;\s*rel="next"/);
|
|
8585
|
+
if (match) return match[1];
|
|
8586
|
+
}
|
|
8587
|
+
return null;
|
|
8588
|
+
}
|
|
8536
8589
|
async function getInstallationToken(opts) {
|
|
8537
8590
|
const cachePath = join(dirname(opts.privateKeyPath), "gh-token-cache.json");
|
|
8538
8591
|
const cached = await readTokenCache(cachePath);
|
|
@@ -9554,6 +9607,62 @@ async function runPortDiaryPhase(opts) {
|
|
|
9554
9607
|
};
|
|
9555
9608
|
}
|
|
9556
9609
|
//#endregion
|
|
9610
|
+
//#region src/phases/portResolveInstallation.ts
|
|
9611
|
+
/**
|
|
9612
|
+
* Resolve the correct `installation_id` for the target owner.
|
|
9613
|
+
*
|
|
9614
|
+
* When porting a config across orgs the source `installation_id` is scoped
|
|
9615
|
+
* to the original account. This phase uses the App JWT to list all
|
|
9616
|
+
* installations and find the one matching the target owner, then updates
|
|
9617
|
+
* `moltnet.json` if it differs.
|
|
9618
|
+
*/
|
|
9619
|
+
async function runPortResolveInstallationPhase(opts) {
|
|
9620
|
+
const { targetDir, config, currentRepo } = opts;
|
|
9621
|
+
if (!currentRepo) return {
|
|
9622
|
+
status: "skipped",
|
|
9623
|
+
message: "unable to determine target repo — skipping installation_id resolution",
|
|
9624
|
+
installationId: config.github?.installation_id ?? ""
|
|
9625
|
+
};
|
|
9626
|
+
if (!config.github?.app_id || !config.github?.private_key_path) return {
|
|
9627
|
+
status: "skipped",
|
|
9628
|
+
message: "github.app_id or private_key_path missing — cannot resolve",
|
|
9629
|
+
installationId: config.github?.installation_id ?? ""
|
|
9630
|
+
};
|
|
9631
|
+
const targetOwner = currentRepo.split("/")[0];
|
|
9632
|
+
let result;
|
|
9633
|
+
try {
|
|
9634
|
+
result = await findInstallationForOwner({
|
|
9635
|
+
appId: config.github.app_id,
|
|
9636
|
+
privateKeyPath: config.github.private_key_path,
|
|
9637
|
+
owner: targetOwner
|
|
9638
|
+
});
|
|
9639
|
+
} catch (err) {
|
|
9640
|
+
return {
|
|
9641
|
+
status: "skipped",
|
|
9642
|
+
message: `could not list app installations: ${err.message}`,
|
|
9643
|
+
installationId: config.github?.installation_id ?? ""
|
|
9644
|
+
};
|
|
9645
|
+
}
|
|
9646
|
+
if (!result) return {
|
|
9647
|
+
status: "not-installed",
|
|
9648
|
+
message: `GitHub App is not installed on ${targetOwner} — install it first`,
|
|
9649
|
+
installationId: config.github?.installation_id ?? ""
|
|
9650
|
+
};
|
|
9651
|
+
const oldId = config.github.installation_id;
|
|
9652
|
+
if (oldId === result.installationId) return {
|
|
9653
|
+
status: "unchanged",
|
|
9654
|
+
message: `installation_id ${oldId} already matches ${targetOwner}`,
|
|
9655
|
+
installationId: oldId
|
|
9656
|
+
};
|
|
9657
|
+
await updateConfigSection("github", { installation_id: result.installationId }, targetDir);
|
|
9658
|
+
if (opts.envPrefix) await updateEnvVar(targetDir, `${opts.envPrefix}_GITHUB_APP_INSTALLATION_ID`, result.installationId);
|
|
9659
|
+
return {
|
|
9660
|
+
status: "updated",
|
|
9661
|
+
message: `installation_id updated: ${oldId || "(empty)"} → ${result.installationId} (${targetOwner})`,
|
|
9662
|
+
installationId: result.installationId
|
|
9663
|
+
};
|
|
9664
|
+
}
|
|
9665
|
+
//#endregion
|
|
9557
9666
|
//#region src/phases/portRewrite.ts
|
|
9558
9667
|
/**
|
|
9559
9668
|
* Rewrite absolute paths in the ported `moltnet.json` so they point to
|
|
@@ -9777,6 +9886,16 @@ function PortApp({ name, agents, sourceDir, targetRepoDir, diaryMode, apiUrl })
|
|
|
9777
9886
|
});
|
|
9778
9887
|
filesWritten.push(rewriteResult.gitConfigPath);
|
|
9779
9888
|
filesWritten.push(join(targetDir, "env"));
|
|
9889
|
+
setPhase("resolving_installation");
|
|
9890
|
+
const currentRepo = detectCurrentRepo(targetRepoDir);
|
|
9891
|
+
const prefix = toEnvPrefix(name);
|
|
9892
|
+
const resolveResult = await runPortResolveInstallationPhase({
|
|
9893
|
+
targetDir,
|
|
9894
|
+
config: await readConfig(targetDir) ?? config,
|
|
9895
|
+
currentRepo: currentRepo ?? void 0,
|
|
9896
|
+
envPrefix: prefix
|
|
9897
|
+
});
|
|
9898
|
+
if (resolveResult.status === "not-installed" || resolveResult.status === "skipped") warnings.push(resolveResult.message);
|
|
9780
9899
|
setPhase("diary");
|
|
9781
9900
|
const diaryResult = await runPortDiaryPhase({
|
|
9782
9901
|
targetDir,
|
|
@@ -9787,14 +9906,14 @@ function PortApp({ name, agents, sourceDir, targetRepoDir, diaryMode, apiUrl })
|
|
|
9787
9906
|
const adapterOpts = {
|
|
9788
9907
|
repoDir: targetRepoDir,
|
|
9789
9908
|
agentName: name,
|
|
9790
|
-
prefix
|
|
9909
|
+
prefix,
|
|
9791
9910
|
mcpUrl: config.endpoints?.mcp ?? apiUrl.replace("://api.", "://mcp.") + "/mcp",
|
|
9792
9911
|
clientId: config.oauth2.client_id,
|
|
9793
9912
|
clientSecret: config.oauth2.client_secret,
|
|
9794
9913
|
appSlug: config.github?.app_slug ?? "",
|
|
9795
9914
|
appId: config.github?.app_id ?? "",
|
|
9796
9915
|
pemPath: join(targetDir, basename(config.github?.private_key_path ?? "")),
|
|
9797
|
-
installationId:
|
|
9916
|
+
installationId: resolveResult.installationId
|
|
9798
9917
|
};
|
|
9799
9918
|
for (const agentType of agents) {
|
|
9800
9919
|
const adapter = adapters[agentType];
|
|
@@ -9810,7 +9929,7 @@ function PortApp({ name, agents, sourceDir, targetRepoDir, diaryMode, apiUrl })
|
|
|
9810
9929
|
setPhase("verifying");
|
|
9811
9930
|
const verifyResult = await runPortVerifyInstallationPhase({
|
|
9812
9931
|
config: await readConfig(targetDir) ?? config,
|
|
9813
|
-
currentRepo:
|
|
9932
|
+
currentRepo: currentRepo ?? void 0
|
|
9814
9933
|
});
|
|
9815
9934
|
if (verifyResult.status !== "ok") warnings.push(verifyResult.message);
|
|
9816
9935
|
setSummary({
|
|
@@ -9852,6 +9971,7 @@ function PortApp({ name, agents, sourceDir, targetRepoDir, diaryMode, apiUrl })
|
|
|
9852
9971
|
validating: `Validating source .moltnet/${name}...`,
|
|
9853
9972
|
copying: `Copying private material...`,
|
|
9854
9973
|
rewriting: `Rewriting paths in moltnet.json...`,
|
|
9974
|
+
resolving_installation: `Resolving GitHub App installation for target org...`,
|
|
9855
9975
|
diary: `Configuring diary (${diaryMode})...`,
|
|
9856
9976
|
agent_setup: `Installing agent files for ${agents.join(", ")}...`,
|
|
9857
9977
|
verifying: `Verifying GitHub App installation scope...`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/legreffier",
|
|
3
|
-
"version": "0.32.
|
|
3
|
+
"version": "0.32.1",
|
|
4
4
|
"description": "LeGreffier — attribution and measured memory for AI coding agents.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
"typescript": "^5.3.3",
|
|
33
33
|
"vite": "^8.0.0",
|
|
34
34
|
"vitest": "^3.0.0",
|
|
35
|
-
"@moltnet/crypto-service": "0.1.0",
|
|
36
35
|
"@moltnet/api-client": "0.1.0",
|
|
36
|
+
"@moltnet/crypto-service": "0.1.0",
|
|
37
37
|
"@themoltnet/design-system": "0.5.1",
|
|
38
|
-
"@themoltnet/github-agent": "0.23.
|
|
38
|
+
"@themoltnet/github-agent": "0.23.1",
|
|
39
39
|
"@themoltnet/sdk": "0.89.0"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|