@themoltnet/legreffier 0.32.0 → 0.32.2
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 +141 -13
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -7707,12 +7707,19 @@ function buildGhTokenRule() {
|
|
|
7707
7707
|
"",
|
|
7708
7708
|
"> **STRICT RULE — read this before every `gh` call.**",
|
|
7709
7709
|
">",
|
|
7710
|
-
"> When `GIT_CONFIG_GLOBAL` is set (matches `.moltnet/<agent>/gitconfig`),
|
|
7711
|
-
"> **MUST NOT** run bare `gh <command>`. You **MUST** prefix
|
|
7712
|
-
"> with a `GH_TOKEN` resolved from an **absolute path** to
|
|
7713
|
-
"> Running bare `gh` silently falls back to the human personal
|
|
7714
|
-
"> attributes the action to the wrong identity — this is a
|
|
7715
|
-
"> not a warning.",
|
|
7710
|
+
"> When `GIT_CONFIG_GLOBAL` is set (matches `.moltnet/<agent>/gitconfig`), the",
|
|
7711
|
+
"> default is: you **MUST NOT** run bare `gh <command>`. You **MUST** prefix",
|
|
7712
|
+
"> every `gh` call with a `GH_TOKEN` resolved from an **absolute path** to",
|
|
7713
|
+
"> `moltnet.json`. Running bare `gh` silently falls back to the human personal",
|
|
7714
|
+
"> token and attributes the action to the wrong identity — this is a",
|
|
7715
|
+
"> correctness bug, not a warning.",
|
|
7716
|
+
">",
|
|
7717
|
+
"> **Exception — `human` authorship mode**: when `MOLTNET_COMMIT_AUTHORSHIP=human`",
|
|
7718
|
+
"> in `.moltnet/<agent>/env`, `gh pr ...` and `gh issue ...` **must** run bare",
|
|
7719
|
+
"> (no `GH_TOKEN`) so the PR/issue appears as authored by the human. All other",
|
|
7720
|
+
"> `gh` calls (including `gh api repos/.../contents/...`) still require the agent",
|
|
7721
|
+
"> token. `git push` is not a `gh` call and always uses the agent token via the",
|
|
7722
|
+
"> gitconfig-configured credential helper.",
|
|
7716
7723
|
"",
|
|
7717
7724
|
"## The only correct form",
|
|
7718
7725
|
"",
|
|
@@ -7749,7 +7756,8 @@ function buildGhTokenRule() {
|
|
|
7749
7756
|
"",
|
|
7750
7757
|
"## Forbidden patterns",
|
|
7751
7758
|
"",
|
|
7752
|
-
"- `gh <command>` — bare, no `GH_TOKEN`. **Never
|
|
7759
|
+
"- `gh <command>` — bare, no `GH_TOKEN`. **Never** (except the `human` mode",
|
|
7760
|
+
" write-op carve-out for `gh pr` / `gh issue` described in the header above).",
|
|
7753
7761
|
"- `GH_TOKEN=$(... --credentials \"$(dirname \"$GIT_CONFIG_GLOBAL\")/moltnet.json\") gh ...`",
|
|
7754
7762
|
" — uses the raw relative path. Breaks in worktrees.",
|
|
7755
7763
|
"- `GH_TOKEN=$(... --credentials \"./moltnet.json\") gh ...` — relative. Breaks.",
|
|
@@ -8084,6 +8092,28 @@ async function writeEnvFile(opts) {
|
|
|
8084
8092
|
await writeFile(envPath, outputLines.join("\n") + "\n", "utf-8");
|
|
8085
8093
|
}
|
|
8086
8094
|
/**
|
|
8095
|
+
* Update a single managed key in an existing env file.
|
|
8096
|
+
* If the key exists, its value is replaced; otherwise appended.
|
|
8097
|
+
*/
|
|
8098
|
+
async function updateEnvVar(envDir, key, value) {
|
|
8099
|
+
const envPath = join(envDir, "env");
|
|
8100
|
+
let content;
|
|
8101
|
+
try {
|
|
8102
|
+
content = await readFile(envPath, "utf-8");
|
|
8103
|
+
} catch {
|
|
8104
|
+
content = "";
|
|
8105
|
+
}
|
|
8106
|
+
const line = `${key}=${q(value)}`;
|
|
8107
|
+
const keyPrefix = `${key}=`;
|
|
8108
|
+
const lines = content === "" ? [] : content.split("\n");
|
|
8109
|
+
const index = lines.findIndex((l) => l.startsWith(keyPrefix));
|
|
8110
|
+
if (index !== -1) {
|
|
8111
|
+
lines[index] = line;
|
|
8112
|
+
content = lines.join("\n");
|
|
8113
|
+
} else content = content.endsWith("\n") ? content + line + "\n" : content + "\n" + line + "\n";
|
|
8114
|
+
await writeFile(envPath, content, "utf-8");
|
|
8115
|
+
}
|
|
8116
|
+
/**
|
|
8087
8117
|
* Resolve the human operator's git identity from global git config.
|
|
8088
8118
|
* Must be called BEFORE GIT_CONFIG_GLOBAL is set (so it reads the
|
|
8089
8119
|
* human's config, not the agent's).
|
|
@@ -8529,10 +8559,41 @@ async function writeTokenCache(cachePath, token, expiresAt) {
|
|
|
8529
8559
|
} catch {}
|
|
8530
8560
|
}
|
|
8531
8561
|
/**
|
|
8532
|
-
*
|
|
8533
|
-
*
|
|
8534
|
-
*
|
|
8562
|
+
* List all installations of this GitHub App and return the one whose
|
|
8563
|
+
* `account.login` matches the given owner (case-insensitive).
|
|
8564
|
+
*
|
|
8565
|
+
* Uses the App JWT (not an installation token), so it works even when
|
|
8566
|
+
* `installation_id` is missing or stale.
|
|
8535
8567
|
*/
|
|
8568
|
+
async function findInstallationForOwner(opts) {
|
|
8569
|
+
const privateKeyPem = await readFile(opts.privateKeyPath, "utf-8");
|
|
8570
|
+
const jwt = createAppJWT(opts.appId, privateKeyPem);
|
|
8571
|
+
const ownerLower = opts.owner.toLowerCase();
|
|
8572
|
+
let nextUrl = "https://api.github.com/app/installations?per_page=100";
|
|
8573
|
+
let pageCount = 0;
|
|
8574
|
+
const MAX_PAGES = 10;
|
|
8575
|
+
while (nextUrl && pageCount < MAX_PAGES) {
|
|
8576
|
+
pageCount++;
|
|
8577
|
+
const res = await fetch(nextUrl, { headers: {
|
|
8578
|
+
Authorization: `Bearer ${jwt}`,
|
|
8579
|
+
Accept: "application/vnd.github+json",
|
|
8580
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
8581
|
+
} });
|
|
8582
|
+
if (!res.ok) throw new Error(`GitHub API error listing installations (${res.status}): ${await res.text()}`);
|
|
8583
|
+
const match = (await res.json()).find((i) => i.account?.login.toLowerCase() === ownerLower);
|
|
8584
|
+
if (match) return { installationId: String(match.id) };
|
|
8585
|
+
const linkHeader = res.headers.get("link");
|
|
8586
|
+
nextUrl = linkHeader ? parseNextLinkHeader(linkHeader) : null;
|
|
8587
|
+
}
|
|
8588
|
+
return null;
|
|
8589
|
+
}
|
|
8590
|
+
function parseNextLinkHeader(header) {
|
|
8591
|
+
for (const part of header.split(",")) {
|
|
8592
|
+
const match = part.match(/<([^>]+)>\s*;\s*rel="next"/);
|
|
8593
|
+
if (match) return match[1];
|
|
8594
|
+
}
|
|
8595
|
+
return null;
|
|
8596
|
+
}
|
|
8536
8597
|
async function getInstallationToken(opts) {
|
|
8537
8598
|
const cachePath = join(dirname(opts.privateKeyPath), "gh-token-cache.json");
|
|
8538
8599
|
const cached = await readTokenCache(cachePath);
|
|
@@ -9554,6 +9615,62 @@ async function runPortDiaryPhase(opts) {
|
|
|
9554
9615
|
};
|
|
9555
9616
|
}
|
|
9556
9617
|
//#endregion
|
|
9618
|
+
//#region src/phases/portResolveInstallation.ts
|
|
9619
|
+
/**
|
|
9620
|
+
* Resolve the correct `installation_id` for the target owner.
|
|
9621
|
+
*
|
|
9622
|
+
* When porting a config across orgs the source `installation_id` is scoped
|
|
9623
|
+
* to the original account. This phase uses the App JWT to list all
|
|
9624
|
+
* installations and find the one matching the target owner, then updates
|
|
9625
|
+
* `moltnet.json` if it differs.
|
|
9626
|
+
*/
|
|
9627
|
+
async function runPortResolveInstallationPhase(opts) {
|
|
9628
|
+
const { targetDir, config, currentRepo } = opts;
|
|
9629
|
+
if (!currentRepo) return {
|
|
9630
|
+
status: "skipped",
|
|
9631
|
+
message: "unable to determine target repo — skipping installation_id resolution",
|
|
9632
|
+
installationId: config.github?.installation_id ?? ""
|
|
9633
|
+
};
|
|
9634
|
+
if (!config.github?.app_id || !config.github?.private_key_path) return {
|
|
9635
|
+
status: "skipped",
|
|
9636
|
+
message: "github.app_id or private_key_path missing — cannot resolve",
|
|
9637
|
+
installationId: config.github?.installation_id ?? ""
|
|
9638
|
+
};
|
|
9639
|
+
const targetOwner = currentRepo.split("/")[0];
|
|
9640
|
+
let result;
|
|
9641
|
+
try {
|
|
9642
|
+
result = await findInstallationForOwner({
|
|
9643
|
+
appId: config.github.app_id,
|
|
9644
|
+
privateKeyPath: config.github.private_key_path,
|
|
9645
|
+
owner: targetOwner
|
|
9646
|
+
});
|
|
9647
|
+
} catch (err) {
|
|
9648
|
+
return {
|
|
9649
|
+
status: "skipped",
|
|
9650
|
+
message: `could not list app installations: ${err.message}`,
|
|
9651
|
+
installationId: config.github?.installation_id ?? ""
|
|
9652
|
+
};
|
|
9653
|
+
}
|
|
9654
|
+
if (!result) return {
|
|
9655
|
+
status: "not-installed",
|
|
9656
|
+
message: `GitHub App is not installed on ${targetOwner} — install it first`,
|
|
9657
|
+
installationId: config.github?.installation_id ?? ""
|
|
9658
|
+
};
|
|
9659
|
+
const oldId = config.github.installation_id;
|
|
9660
|
+
if (oldId === result.installationId) return {
|
|
9661
|
+
status: "unchanged",
|
|
9662
|
+
message: `installation_id ${oldId} already matches ${targetOwner}`,
|
|
9663
|
+
installationId: oldId
|
|
9664
|
+
};
|
|
9665
|
+
await updateConfigSection("github", { installation_id: result.installationId }, targetDir);
|
|
9666
|
+
if (opts.envPrefix) await updateEnvVar(targetDir, `${opts.envPrefix}_GITHUB_APP_INSTALLATION_ID`, result.installationId);
|
|
9667
|
+
return {
|
|
9668
|
+
status: "updated",
|
|
9669
|
+
message: `installation_id updated: ${oldId || "(empty)"} → ${result.installationId} (${targetOwner})`,
|
|
9670
|
+
installationId: result.installationId
|
|
9671
|
+
};
|
|
9672
|
+
}
|
|
9673
|
+
//#endregion
|
|
9557
9674
|
//#region src/phases/portRewrite.ts
|
|
9558
9675
|
/**
|
|
9559
9676
|
* Rewrite absolute paths in the ported `moltnet.json` so they point to
|
|
@@ -9777,6 +9894,16 @@ function PortApp({ name, agents, sourceDir, targetRepoDir, diaryMode, apiUrl })
|
|
|
9777
9894
|
});
|
|
9778
9895
|
filesWritten.push(rewriteResult.gitConfigPath);
|
|
9779
9896
|
filesWritten.push(join(targetDir, "env"));
|
|
9897
|
+
setPhase("resolving_installation");
|
|
9898
|
+
const currentRepo = detectCurrentRepo(targetRepoDir);
|
|
9899
|
+
const prefix = toEnvPrefix(name);
|
|
9900
|
+
const resolveResult = await runPortResolveInstallationPhase({
|
|
9901
|
+
targetDir,
|
|
9902
|
+
config: await readConfig(targetDir) ?? config,
|
|
9903
|
+
currentRepo: currentRepo ?? void 0,
|
|
9904
|
+
envPrefix: prefix
|
|
9905
|
+
});
|
|
9906
|
+
if (resolveResult.status === "not-installed" || resolveResult.status === "skipped") warnings.push(resolveResult.message);
|
|
9780
9907
|
setPhase("diary");
|
|
9781
9908
|
const diaryResult = await runPortDiaryPhase({
|
|
9782
9909
|
targetDir,
|
|
@@ -9787,14 +9914,14 @@ function PortApp({ name, agents, sourceDir, targetRepoDir, diaryMode, apiUrl })
|
|
|
9787
9914
|
const adapterOpts = {
|
|
9788
9915
|
repoDir: targetRepoDir,
|
|
9789
9916
|
agentName: name,
|
|
9790
|
-
prefix
|
|
9917
|
+
prefix,
|
|
9791
9918
|
mcpUrl: config.endpoints?.mcp ?? apiUrl.replace("://api.", "://mcp.") + "/mcp",
|
|
9792
9919
|
clientId: config.oauth2.client_id,
|
|
9793
9920
|
clientSecret: config.oauth2.client_secret,
|
|
9794
9921
|
appSlug: config.github?.app_slug ?? "",
|
|
9795
9922
|
appId: config.github?.app_id ?? "",
|
|
9796
9923
|
pemPath: join(targetDir, basename(config.github?.private_key_path ?? "")),
|
|
9797
|
-
installationId:
|
|
9924
|
+
installationId: resolveResult.installationId
|
|
9798
9925
|
};
|
|
9799
9926
|
for (const agentType of agents) {
|
|
9800
9927
|
const adapter = adapters[agentType];
|
|
@@ -9810,7 +9937,7 @@ function PortApp({ name, agents, sourceDir, targetRepoDir, diaryMode, apiUrl })
|
|
|
9810
9937
|
setPhase("verifying");
|
|
9811
9938
|
const verifyResult = await runPortVerifyInstallationPhase({
|
|
9812
9939
|
config: await readConfig(targetDir) ?? config,
|
|
9813
|
-
currentRepo:
|
|
9940
|
+
currentRepo: currentRepo ?? void 0
|
|
9814
9941
|
});
|
|
9815
9942
|
if (verifyResult.status !== "ok") warnings.push(verifyResult.message);
|
|
9816
9943
|
setSummary({
|
|
@@ -9852,6 +9979,7 @@ function PortApp({ name, agents, sourceDir, targetRepoDir, diaryMode, apiUrl })
|
|
|
9852
9979
|
validating: `Validating source .moltnet/${name}...`,
|
|
9853
9980
|
copying: `Copying private material...`,
|
|
9854
9981
|
rewriting: `Rewriting paths in moltnet.json...`,
|
|
9982
|
+
resolving_installation: `Resolving GitHub App installation for target org...`,
|
|
9855
9983
|
diary: `Configuring diary (${diaryMode})...`,
|
|
9856
9984
|
agent_setup: `Installing agent files for ${agents.join(", ")}...`,
|
|
9857
9985
|
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.2",
|
|
4
4
|
"description": "LeGreffier — attribution and measured memory for AI coding agents.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
@@ -32,11 +32,11 @@
|
|
|
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",
|
|
37
|
-
"@
|
|
38
|
-
"@themoltnet/github-agent": "0.23.
|
|
39
|
-
"@themoltnet/sdk": "0.
|
|
36
|
+
"@moltnet/crypto-service": "0.1.0",
|
|
37
|
+
"@themoltnet/github-agent": "0.23.2",
|
|
38
|
+
"@themoltnet/sdk": "0.91.0",
|
|
39
|
+
"@themoltnet/design-system": "0.6.0"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"dev": "vite build --watch",
|