@themoltnet/legreffier 0.28.1 → 0.29.0
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 +39 -0
- package/dist/index.js +963 -59
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -55,6 +55,43 @@ legreffier setup --name my-agent --agent codex
|
|
|
55
55
|
legreffier setup --name my-agent --agent claude --agent codex
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
#### `legreffier port`
|
|
59
|
+
|
|
60
|
+
Port an existing agent identity into a new repository — **reuses** the
|
|
61
|
+
cryptographic identity, GitHub App, SSH keys, and gitconfig instead of
|
|
62
|
+
creating a new agent. Use this when the same agent should operate in
|
|
63
|
+
multiple repos under one GitHub App installation.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
legreffier port \
|
|
67
|
+
--name my-agent \
|
|
68
|
+
--from /path/to/source-repo/.moltnet/my-agent \
|
|
69
|
+
[--dir /path/to/target-repo] \
|
|
70
|
+
[--agent claude] [--agent codex] \
|
|
71
|
+
[--diary new|reuse|skip]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Phases:
|
|
75
|
+
|
|
76
|
+
1. **validate** — dry-run `repairConfig` on the source + presence checks
|
|
77
|
+
for PEM, ssh keys, `installation_id`, `client_id/secret`.
|
|
78
|
+
2. **copy** — copies `moltnet.json`, the PEM, ssh keys (mode 0600 for
|
|
79
|
+
private material), and `allowed_signers` if present.
|
|
80
|
+
3. **rewrite** — rewrites absolute paths in `moltnet.json` to the target
|
|
81
|
+
repo and regenerates `gitconfig` + `env`.
|
|
82
|
+
4. **diary** — `reuse` carries `MOLTNET_DIARY_ID` from the source, `new`
|
|
83
|
+
strips it so the agent creates a fresh per-repo diary on activation,
|
|
84
|
+
`skip` leaves the env file untouched.
|
|
85
|
+
5. **agent_setup** — writes per-agent MCP config, skills, settings, and
|
|
86
|
+
rules for each `--agent` (defaults to `claude`).
|
|
87
|
+
6. **verify** — warning-only check that the GitHub App installation can
|
|
88
|
+
reach the current repo (detected from `git remote get-url origin`).
|
|
89
|
+
If the repo is out of scope, you'll see a link to the installation
|
|
90
|
+
settings page.
|
|
91
|
+
|
|
92
|
+
**Identity guard:** if the target already has a `.moltnet/<name>/` with
|
|
93
|
+
a different `identity_id`, port refuses to overwrite it.
|
|
94
|
+
|
|
58
95
|
### Options
|
|
59
96
|
|
|
60
97
|
| Flag | Description | Default |
|
|
@@ -63,6 +100,8 @@ legreffier setup --name my-agent --agent claude --agent codex
|
|
|
63
100
|
| `--agent, -a` | Agent type(s) to configure (repeatable) | Interactive prompt |
|
|
64
101
|
| `--api-url` | MoltNet API URL | `https://api.themolt.net` |
|
|
65
102
|
| `--dir` | Repository directory for config files | Current working directory |
|
|
103
|
+
| `--from` | (port) Source `.moltnet/<name>` dir | — |
|
|
104
|
+
| `--diary` | (port) Diary handling: new/reuse/skip | `new` |
|
|
66
105
|
|
|
67
106
|
Supported agents: `claude`, `codex`.
|
|
68
107
|
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { statSync } from "node:fs";
|
|
3
|
+
import { parseArgs, parseEnv } from "node:util";
|
|
3
4
|
import { Box, Text, render, useApp, useInput } from "ink";
|
|
4
|
-
import { execFileSync } from "node:child_process";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
5
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
6
|
+
import { basename, dirname, join } from "node:path";
|
|
6
7
|
import { useEffect, useReducer, useRef, useState } from "react";
|
|
7
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
9
|
import figlet from "figlet";
|
|
10
|
+
import { createSign } from "node:crypto";
|
|
9
11
|
import { createHash, randomBytes } from "crypto";
|
|
10
|
-
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
12
|
+
import { access, chmod, copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
11
13
|
import { homedir } from "node:os";
|
|
12
14
|
import { parse, stringify } from "smol-toml";
|
|
13
15
|
import open from "open";
|
|
@@ -7192,6 +7194,13 @@ async function writeMcpConfig(mcpConfig, dir) {
|
|
|
7192
7194
|
}
|
|
7193
7195
|
//#endregion
|
|
7194
7196
|
//#region ../../libs/sdk/src/credentials.ts
|
|
7197
|
+
/**
|
|
7198
|
+
* Derive the MCP URL from an API URL.
|
|
7199
|
+
* e.g. "https://api.themolt.net" → "https://mcp.themolt.net/mcp"
|
|
7200
|
+
*/
|
|
7201
|
+
function deriveMcpUrl(apiUrl) {
|
|
7202
|
+
return apiUrl.replace("://api.", "://mcp.") + "/mcp";
|
|
7203
|
+
}
|
|
7195
7204
|
function getConfigDir() {
|
|
7196
7205
|
return join(homedir(), ".config", "moltnet");
|
|
7197
7206
|
}
|
|
@@ -7241,6 +7250,119 @@ async function updateConfigSection(section, data, configDir) {
|
|
|
7241
7250
|
await writeConfig(config, configDir);
|
|
7242
7251
|
}
|
|
7243
7252
|
//#endregion
|
|
7253
|
+
//#region ../../libs/sdk/src/repair.ts
|
|
7254
|
+
/**
|
|
7255
|
+
* Validate and optionally repair a MoltNet config.
|
|
7256
|
+
*
|
|
7257
|
+
* Checks required fields, detects stale file paths, and migrates
|
|
7258
|
+
* `credentials.json` to `moltnet.json` when found.
|
|
7259
|
+
*
|
|
7260
|
+
* Pass `dryRun: true` to report issues without writing changes.
|
|
7261
|
+
*/
|
|
7262
|
+
async function repairConfig(opts) {
|
|
7263
|
+
const dir = opts?.configDir ?? getConfigDir();
|
|
7264
|
+
const issues = [];
|
|
7265
|
+
let config = await tryReadJson(join(dir, "moltnet.json"));
|
|
7266
|
+
if (!config) {
|
|
7267
|
+
const legacy = await tryReadJson(join(dir, "credentials.json"));
|
|
7268
|
+
if (legacy) {
|
|
7269
|
+
config = legacy;
|
|
7270
|
+
issues.push({
|
|
7271
|
+
field: "file",
|
|
7272
|
+
problem: "using deprecated credentials.json — will migrate to moltnet.json",
|
|
7273
|
+
action: "migrate"
|
|
7274
|
+
});
|
|
7275
|
+
if (!opts?.dryRun) await writeConfig(config, dir);
|
|
7276
|
+
} else return {
|
|
7277
|
+
issues: [],
|
|
7278
|
+
config: null
|
|
7279
|
+
};
|
|
7280
|
+
}
|
|
7281
|
+
validateConfig(config, issues);
|
|
7282
|
+
await checkFilePaths(config, issues);
|
|
7283
|
+
if (!config.endpoints.mcp && config.endpoints.api) {
|
|
7284
|
+
config.endpoints.mcp = deriveMcpUrl(config.endpoints.api);
|
|
7285
|
+
issues.push({
|
|
7286
|
+
field: "endpoints.mcp",
|
|
7287
|
+
problem: "missing — derived from API endpoint",
|
|
7288
|
+
action: "fixed"
|
|
7289
|
+
});
|
|
7290
|
+
}
|
|
7291
|
+
if (issues.some((i) => i.action === "fixed") && !opts?.dryRun) await writeConfig(config, dir);
|
|
7292
|
+
return {
|
|
7293
|
+
issues,
|
|
7294
|
+
config
|
|
7295
|
+
};
|
|
7296
|
+
}
|
|
7297
|
+
function validateConfig(config, issues) {
|
|
7298
|
+
if (!config.identity_id) issues.push({
|
|
7299
|
+
field: "identity_id",
|
|
7300
|
+
problem: "missing",
|
|
7301
|
+
action: "warning"
|
|
7302
|
+
});
|
|
7303
|
+
if (!config.keys.public_key) issues.push({
|
|
7304
|
+
field: "keys.public_key",
|
|
7305
|
+
problem: "missing",
|
|
7306
|
+
action: "warning"
|
|
7307
|
+
});
|
|
7308
|
+
if (!config.keys.private_key) issues.push({
|
|
7309
|
+
field: "keys.private_key",
|
|
7310
|
+
problem: "missing",
|
|
7311
|
+
action: "warning"
|
|
7312
|
+
});
|
|
7313
|
+
if (config.keys.public_key && !config.keys.public_key.startsWith("ed25519:")) issues.push({
|
|
7314
|
+
field: "keys.public_key",
|
|
7315
|
+
problem: "missing 'ed25519:' prefix",
|
|
7316
|
+
action: "warning"
|
|
7317
|
+
});
|
|
7318
|
+
if (!config.endpoints.api) issues.push({
|
|
7319
|
+
field: "endpoints.api",
|
|
7320
|
+
problem: "missing",
|
|
7321
|
+
action: "warning"
|
|
7322
|
+
});
|
|
7323
|
+
if (config.github?.app_id && !/^\d+$/.test(config.github.app_id)) issues.push({
|
|
7324
|
+
field: "github.app_id",
|
|
7325
|
+
problem: `not a numeric GitHub App ID (got "${config.github.app_id}") — likely the slug; refetch via \`moltnet config init-from-env\` or re-run \`legreffier\``,
|
|
7326
|
+
action: "warning"
|
|
7327
|
+
});
|
|
7328
|
+
}
|
|
7329
|
+
async function checkFilePaths(config, issues) {
|
|
7330
|
+
const checks = [];
|
|
7331
|
+
if (config.ssh?.private_key_path) checks.push({
|
|
7332
|
+
field: "ssh.private_key_path",
|
|
7333
|
+
path: config.ssh.private_key_path
|
|
7334
|
+
});
|
|
7335
|
+
if (config.ssh?.public_key_path) checks.push({
|
|
7336
|
+
field: "ssh.public_key_path",
|
|
7337
|
+
path: config.ssh.public_key_path
|
|
7338
|
+
});
|
|
7339
|
+
if (config.git?.config_path) checks.push({
|
|
7340
|
+
field: "git.config_path",
|
|
7341
|
+
path: config.git.config_path
|
|
7342
|
+
});
|
|
7343
|
+
if (config.github?.private_key_path) checks.push({
|
|
7344
|
+
field: "github.private_key_path",
|
|
7345
|
+
path: config.github.private_key_path
|
|
7346
|
+
});
|
|
7347
|
+
for (const { field, path } of checks) try {
|
|
7348
|
+
await access(path);
|
|
7349
|
+
} catch {
|
|
7350
|
+
issues.push({
|
|
7351
|
+
field,
|
|
7352
|
+
problem: `file not found: ${path}`,
|
|
7353
|
+
action: "warning"
|
|
7354
|
+
});
|
|
7355
|
+
}
|
|
7356
|
+
}
|
|
7357
|
+
async function tryReadJson(path) {
|
|
7358
|
+
try {
|
|
7359
|
+
const content = await readFile(path, "utf-8");
|
|
7360
|
+
return JSON.parse(content);
|
|
7361
|
+
} catch {
|
|
7362
|
+
return null;
|
|
7363
|
+
}
|
|
7364
|
+
}
|
|
7365
|
+
//#endregion
|
|
7244
7366
|
//#region ../../libs/sdk/src/ssh.ts
|
|
7245
7367
|
/**
|
|
7246
7368
|
* Export the agent's Ed25519 key pair as OpenSSH key files.
|
|
@@ -7424,6 +7546,20 @@ function buildGhTokenRule(agentName) {
|
|
|
7424
7546
|
"The token is cached locally (~1 hour lifetime, 5-min expiry buffer),",
|
|
7425
7547
|
"so repeated calls are fast after the first API hit.",
|
|
7426
7548
|
"",
|
|
7549
|
+
"## Worktree warning",
|
|
7550
|
+
"",
|
|
7551
|
+
`\`GIT_CONFIG_GLOBAL\` may be a **relative path** (e.g. \`.moltnet/${agentName}/gitconfig\`).`,
|
|
7552
|
+
"In git worktrees the CWD differs from the main worktree root, so `$(dirname \"$GIT_CONFIG_GLOBAL\")`",
|
|
7553
|
+
"resolves incorrectly and `no credentials found` is printed — the command then falls back to your",
|
|
7554
|
+
"personal `gh` token silently.",
|
|
7555
|
+
"",
|
|
7556
|
+
"**Always resolve to an absolute path first:**",
|
|
7557
|
+
"",
|
|
7558
|
+
"```bash",
|
|
7559
|
+
"CREDS=\"$(cd \"$(dirname \"$GIT_CONFIG_GLOBAL\")\" && pwd)/moltnet.json\"",
|
|
7560
|
+
"GH_TOKEN=$(npx @themoltnet/cli github token --credentials \"$CREDS\") gh <command>",
|
|
7561
|
+
"```",
|
|
7562
|
+
"",
|
|
7427
7563
|
"## Allowed `gh` subcommands",
|
|
7428
7564
|
"",
|
|
7429
7565
|
"The GitHub App only has these permissions:",
|
|
@@ -7517,6 +7653,28 @@ function buildCodexRules(_agentName) {
|
|
|
7517
7653
|
" decision = \"allow\",",
|
|
7518
7654
|
")",
|
|
7519
7655
|
"",
|
|
7656
|
+
"# GitHub CLI — read-only subcommands (write ops prompt the user)",
|
|
7657
|
+
"prefix_rule(",
|
|
7658
|
+
" pattern = [\"gh\", \"pr\", \"view\"],",
|
|
7659
|
+
" decision = \"allow\",",
|
|
7660
|
+
")",
|
|
7661
|
+
"prefix_rule(",
|
|
7662
|
+
" pattern = [\"gh\", \"pr\", \"list\"],",
|
|
7663
|
+
" decision = \"allow\",",
|
|
7664
|
+
")",
|
|
7665
|
+
"prefix_rule(",
|
|
7666
|
+
" pattern = [\"gh\", \"issue\", \"view\"],",
|
|
7667
|
+
" decision = \"allow\",",
|
|
7668
|
+
")",
|
|
7669
|
+
"prefix_rule(",
|
|
7670
|
+
" pattern = [\"gh\", \"issue\", \"list\"],",
|
|
7671
|
+
" decision = \"allow\",",
|
|
7672
|
+
")",
|
|
7673
|
+
"prefix_rule(",
|
|
7674
|
+
" pattern = [\"gh\", \"repo\", \"view\"],",
|
|
7675
|
+
" decision = \"allow\",",
|
|
7676
|
+
")",
|
|
7677
|
+
"",
|
|
7520
7678
|
"# Worktree symlink creation",
|
|
7521
7679
|
"prefix_rule(",
|
|
7522
7680
|
" pattern = [\"ln\", \"-s\"],",
|
|
@@ -7559,7 +7717,7 @@ function toEnvPrefix(agentName) {
|
|
|
7559
7717
|
return agentName.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
7560
7718
|
}
|
|
7561
7719
|
/** Merge agent env vars into .claude/settings.local.json, preserving existing entries. */
|
|
7562
|
-
async function writeSettingsLocal({ repoDir, agentName,
|
|
7720
|
+
async function writeSettingsLocal({ repoDir, agentName, appId, pemPath, installationId, clientId, clientSecret }) {
|
|
7563
7721
|
const dir = join(repoDir, ".claude");
|
|
7564
7722
|
await mkdir(dir, { recursive: true });
|
|
7565
7723
|
const filePath = join(dir, "settings.local.json");
|
|
@@ -7582,7 +7740,7 @@ async function writeSettingsLocal({ repoDir, agentName, appSlug, pemPath, instal
|
|
|
7582
7740
|
},
|
|
7583
7741
|
env: {
|
|
7584
7742
|
...existing.env,
|
|
7585
|
-
[`${prefix}_GITHUB_APP_ID`]:
|
|
7743
|
+
[`${prefix}_GITHUB_APP_ID`]: appId,
|
|
7586
7744
|
[`${prefix}_GITHUB_APP_PRIVATE_KEY_PATH`]: pemPath,
|
|
7587
7745
|
[`${prefix}_GITHUB_APP_INSTALLATION_ID`]: installationId,
|
|
7588
7746
|
[`${prefix}_CLIENT_ID`]: clientId,
|
|
@@ -7612,7 +7770,7 @@ var ClaudeAdapter = class {
|
|
|
7612
7770
|
await writeSettingsLocal({
|
|
7613
7771
|
repoDir: opts.repoDir,
|
|
7614
7772
|
agentName: opts.agentName,
|
|
7615
|
-
|
|
7773
|
+
appId: opts.appId,
|
|
7616
7774
|
pemPath: opts.pemPath,
|
|
7617
7775
|
installationId: opts.installationId,
|
|
7618
7776
|
clientId: opts.clientId,
|
|
@@ -7672,6 +7830,13 @@ var adapters = {
|
|
|
7672
7830
|
};
|
|
7673
7831
|
//#endregion
|
|
7674
7832
|
//#region src/env-file.ts
|
|
7833
|
+
/**
|
|
7834
|
+
* Parse a dotenv-format string using Node.js built-in `util.parseEnv`.
|
|
7835
|
+
* Handles quoting, comments, and blank lines.
|
|
7836
|
+
*/
|
|
7837
|
+
function parseEnvFile(content) {
|
|
7838
|
+
return parseEnv(content);
|
|
7839
|
+
}
|
|
7675
7840
|
function q(v) {
|
|
7676
7841
|
return `'${v.replace(/'/g, "'\\''")}'`;
|
|
7677
7842
|
}
|
|
@@ -7686,10 +7851,11 @@ async function writeEnvFile(opts) {
|
|
|
7686
7851
|
const managedEntries = [
|
|
7687
7852
|
[`${opts.prefix}_CLIENT_ID`, q(opts.clientId)],
|
|
7688
7853
|
[`${opts.prefix}_CLIENT_SECRET`, q(opts.clientSecret)],
|
|
7689
|
-
[`${opts.prefix}_GITHUB_APP_ID`, q(opts.
|
|
7854
|
+
[`${opts.prefix}_GITHUB_APP_ID`, q(opts.appId)],
|
|
7690
7855
|
[`${opts.prefix}_GITHUB_APP_PRIVATE_KEY_PATH`, q(opts.pemPath)],
|
|
7691
7856
|
[`${opts.prefix}_GITHUB_APP_INSTALLATION_ID`, q(opts.installationId)],
|
|
7692
|
-
["GIT_CONFIG_GLOBAL", q(`.moltnet/${opts.agentName}/gitconfig`)]
|
|
7857
|
+
["GIT_CONFIG_GLOBAL", q(`.moltnet/${opts.agentName}/gitconfig`)],
|
|
7858
|
+
["MOLTNET_AGENT_NAME", q(opts.agentName)]
|
|
7693
7859
|
];
|
|
7694
7860
|
const managedKeys = new Set(managedEntries.map(([k]) => k));
|
|
7695
7861
|
let existingLines = [];
|
|
@@ -7746,7 +7912,7 @@ async function clearState(configDir) {
|
|
|
7746
7912
|
//#endregion
|
|
7747
7913
|
//#region src/phases/agentSetup.ts
|
|
7748
7914
|
async function runAgentSetupPhase(opts) {
|
|
7749
|
-
const { apiUrl, repoDir, configDir, agentName, agentTypes, publicKey, fingerprint, appSlug, pemPath, installationId, identityId, clientId, clientSecret, dispatch } = opts;
|
|
7915
|
+
const { apiUrl, repoDir, configDir, agentName, agentTypes, publicKey, fingerprint, appId, appSlug, pemPath, installationId, identityId, clientId, clientSecret, org, dispatch } = opts;
|
|
7750
7916
|
dispatch({
|
|
7751
7917
|
type: "phase",
|
|
7752
7918
|
phase: "agent_setup"
|
|
@@ -7769,10 +7935,11 @@ async function runAgentSetupPhase(opts) {
|
|
|
7769
7935
|
mcp: apiUrl.replace("://api.", "://mcp.") + "/mcp"
|
|
7770
7936
|
},
|
|
7771
7937
|
github: {
|
|
7772
|
-
app_id:
|
|
7938
|
+
app_id: appId,
|
|
7773
7939
|
app_slug: appSlug,
|
|
7774
7940
|
installation_id: installationId,
|
|
7775
|
-
private_key_path: pemPath
|
|
7941
|
+
private_key_path: pemPath,
|
|
7942
|
+
...org ? { org } : {}
|
|
7776
7943
|
}
|
|
7777
7944
|
}, configDir);
|
|
7778
7945
|
const prefix = toEnvPrefix(agentName);
|
|
@@ -7784,6 +7951,7 @@ async function runAgentSetupPhase(opts) {
|
|
|
7784
7951
|
clientId,
|
|
7785
7952
|
clientSecret,
|
|
7786
7953
|
appSlug,
|
|
7954
|
+
appId,
|
|
7787
7955
|
pemPath,
|
|
7788
7956
|
installationId
|
|
7789
7957
|
};
|
|
@@ -7819,7 +7987,7 @@ async function runAgentSetupPhase(opts) {
|
|
|
7819
7987
|
prefix,
|
|
7820
7988
|
clientId,
|
|
7821
7989
|
clientSecret,
|
|
7822
|
-
|
|
7990
|
+
appId,
|
|
7823
7991
|
pemPath,
|
|
7824
7992
|
installationId
|
|
7825
7993
|
});
|
|
@@ -7878,35 +8046,6 @@ async function suggestAppNames(appName) {
|
|
|
7878
8046
|
available: await checkAppNameAvailable(name)
|
|
7879
8047
|
})))).filter((r) => r.available).map((r) => r.name);
|
|
7880
8048
|
}
|
|
7881
|
-
/**
|
|
7882
|
-
* Look up GitHub bot user and derive noreply email.
|
|
7883
|
-
* Tries <appSlug>[bot] first (exists post-installation), then falls back to
|
|
7884
|
-
* plain <appSlug> (exists right after app creation, pre-installation).
|
|
7885
|
-
*
|
|
7886
|
-
* Retries with exponential backoff because GitHub's public /users API
|
|
7887
|
-
* may not index a newly created app account immediately.
|
|
7888
|
-
*/
|
|
7889
|
-
async function lookupBotUser(appSlug, { maxRetries = 5, baseDelayMs = 2e3 } = {}) {
|
|
7890
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
7891
|
-
for (const username of [`${appSlug}[bot]`, appSlug]) {
|
|
7892
|
-
const res = await fetch(`https://api.github.com/users/${encodeURIComponent(username)}`, { headers: GITHUB_HEADERS });
|
|
7893
|
-
if (res.ok) {
|
|
7894
|
-
const data = await res.json();
|
|
7895
|
-
return {
|
|
7896
|
-
id: data.id,
|
|
7897
|
-
email: `${data.id}+${data.login}@users.noreply.github.com`
|
|
7898
|
-
};
|
|
7899
|
-
}
|
|
7900
|
-
}
|
|
7901
|
-
if (attempt < maxRetries) {
|
|
7902
|
-
const delayMs = baseDelayMs * 2 ** attempt;
|
|
7903
|
-
await new Promise((resolve) => {
|
|
7904
|
-
setTimeout(resolve, delayMs);
|
|
7905
|
-
});
|
|
7906
|
-
}
|
|
7907
|
-
}
|
|
7908
|
-
throw new Error(`GitHub user lookup failed for app "${appSlug}"`);
|
|
7909
|
-
}
|
|
7910
8049
|
/** Write GitHub App PEM to <configDir>/<appSlug>.pem (mode 0o600). */
|
|
7911
8050
|
async function writePem(pem, appSlug, configDir) {
|
|
7912
8051
|
await mkdir(configDir, { recursive: true });
|
|
@@ -7954,6 +8093,7 @@ async function runGithubAppPhase(opts) {
|
|
|
7954
8093
|
appSlug: existingConfig.github.app_slug ?? ""
|
|
7955
8094
|
});
|
|
7956
8095
|
return {
|
|
8096
|
+
appId: existingConfig.github.app_id,
|
|
7957
8097
|
appSlug: existingConfig.github.app_slug ?? "",
|
|
7958
8098
|
pemPath: existingConfig.github.private_key_path,
|
|
7959
8099
|
installationId: existingConfig.github.installation_id,
|
|
@@ -7972,6 +8112,7 @@ async function runGithubAppPhase(opts) {
|
|
|
7972
8112
|
appSlug: existingState.appSlug
|
|
7973
8113
|
});
|
|
7974
8114
|
return {
|
|
8115
|
+
appId: existingState.appId,
|
|
7975
8116
|
appSlug: existingState.appSlug,
|
|
7976
8117
|
pemPath,
|
|
7977
8118
|
installationId: "",
|
|
@@ -8023,6 +8164,7 @@ async function runGithubAppPhase(opts) {
|
|
|
8023
8164
|
status: "done"
|
|
8024
8165
|
});
|
|
8025
8166
|
return {
|
|
8167
|
+
appId: ghCreds.appId,
|
|
8026
8168
|
appSlug: ghCreds.appSlug,
|
|
8027
8169
|
pemPath,
|
|
8028
8170
|
installationId: "",
|
|
@@ -8030,6 +8172,136 @@ async function runGithubAppPhase(opts) {
|
|
|
8030
8172
|
};
|
|
8031
8173
|
}
|
|
8032
8174
|
//#endregion
|
|
8175
|
+
//#region ../github-agent/src/bot-user.ts
|
|
8176
|
+
var GITHUB_API_BASE_URL = "https://api.github.com";
|
|
8177
|
+
/**
|
|
8178
|
+
* Look up the shadow bot user associated with a GitHub App.
|
|
8179
|
+
* Every GitHub App gets a bot user account (`<slug>[bot]`).
|
|
8180
|
+
* This endpoint is public — no authentication required.
|
|
8181
|
+
*
|
|
8182
|
+
* Tries `<appSlug>[bot]` first (exists post-installation), then falls
|
|
8183
|
+
* back to plain `<appSlug>` (exists right after app creation, pre-install).
|
|
8184
|
+
*
|
|
8185
|
+
* @returns The bot user ID and login
|
|
8186
|
+
*/
|
|
8187
|
+
async function lookupBotUser(appSlug, opts = {}) {
|
|
8188
|
+
const { apiBaseUrl = GITHUB_API_BASE_URL, maxRetries = 0, baseDelayMs = 2e3 } = opts;
|
|
8189
|
+
const headers = {
|
|
8190
|
+
Accept: "application/vnd.github+json",
|
|
8191
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
8192
|
+
};
|
|
8193
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
8194
|
+
for (const username of [`${appSlug}[bot]`, appSlug]) {
|
|
8195
|
+
const url = `${apiBaseUrl}/users/${encodeURIComponent(username)}`;
|
|
8196
|
+
const response = await fetch(url, { headers });
|
|
8197
|
+
if (response.ok) {
|
|
8198
|
+
const data = await response.json();
|
|
8199
|
+
return {
|
|
8200
|
+
id: data.id,
|
|
8201
|
+
login: data.login
|
|
8202
|
+
};
|
|
8203
|
+
}
|
|
8204
|
+
}
|
|
8205
|
+
if (attempt < maxRetries) {
|
|
8206
|
+
const delayMs = baseDelayMs * 2 ** attempt;
|
|
8207
|
+
await new Promise((resolve) => {
|
|
8208
|
+
setTimeout(resolve, delayMs);
|
|
8209
|
+
});
|
|
8210
|
+
}
|
|
8211
|
+
}
|
|
8212
|
+
throw new Error(`GitHub user lookup failed for app "${appSlug}"`);
|
|
8213
|
+
}
|
|
8214
|
+
/**
|
|
8215
|
+
* Build the GitHub noreply email for a bot user.
|
|
8216
|
+
* Format: `<bot-user-id>+<slug>[bot]@users.noreply.github.com`
|
|
8217
|
+
*/
|
|
8218
|
+
function buildBotEmail(botUserId, appSlug) {
|
|
8219
|
+
return `${botUserId}+${appSlug}[bot]@users.noreply.github.com`;
|
|
8220
|
+
}
|
|
8221
|
+
//#endregion
|
|
8222
|
+
//#region ../github-agent/src/token.ts
|
|
8223
|
+
/**
|
|
8224
|
+
* Create a JWT signed with the GitHub App's RSA private key.
|
|
8225
|
+
*/
|
|
8226
|
+
function createAppJWT(appId, privateKeyPem) {
|
|
8227
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
8228
|
+
const header = Buffer.from(JSON.stringify({
|
|
8229
|
+
alg: "RS256",
|
|
8230
|
+
typ: "JWT"
|
|
8231
|
+
})).toString("base64url");
|
|
8232
|
+
const payload = Buffer.from(JSON.stringify({
|
|
8233
|
+
iss: appId,
|
|
8234
|
+
iat: now - 60,
|
|
8235
|
+
exp: now + 600
|
|
8236
|
+
})).toString("base64url");
|
|
8237
|
+
const sign = createSign("RSA-SHA256");
|
|
8238
|
+
sign.update(`${header}.${payload}`);
|
|
8239
|
+
return `${header}.${payload}.${sign.sign(privateKeyPem, "base64url")}`;
|
|
8240
|
+
}
|
|
8241
|
+
/** Minimum remaining validity before we consider a cached token expired. */
|
|
8242
|
+
var EXPIRY_BUFFER_MS = 300 * 1e3;
|
|
8243
|
+
/**
|
|
8244
|
+
* Read a cached token from disk. Returns null if missing, corrupt, or expired.
|
|
8245
|
+
*/
|
|
8246
|
+
async function readTokenCache(cachePath) {
|
|
8247
|
+
try {
|
|
8248
|
+
const raw = await readFile(cachePath, "utf-8");
|
|
8249
|
+
const cached = JSON.parse(raw);
|
|
8250
|
+
if (!cached.token || !cached.expires_at) return null;
|
|
8251
|
+
const expiresAt = new Date(cached.expires_at).getTime();
|
|
8252
|
+
if (Date.now() + EXPIRY_BUFFER_MS >= expiresAt) return null;
|
|
8253
|
+
return {
|
|
8254
|
+
token: cached.token,
|
|
8255
|
+
expiresAt: cached.expires_at
|
|
8256
|
+
};
|
|
8257
|
+
} catch {
|
|
8258
|
+
return null;
|
|
8259
|
+
}
|
|
8260
|
+
}
|
|
8261
|
+
/**
|
|
8262
|
+
* Write a token to the cache file (best-effort).
|
|
8263
|
+
*/
|
|
8264
|
+
async function writeTokenCache(cachePath, token, expiresAt) {
|
|
8265
|
+
const cache = {
|
|
8266
|
+
token,
|
|
8267
|
+
expires_at: expiresAt
|
|
8268
|
+
};
|
|
8269
|
+
try {
|
|
8270
|
+
await writeFile(cachePath, JSON.stringify(cache), { mode: 384 });
|
|
8271
|
+
} catch {}
|
|
8272
|
+
}
|
|
8273
|
+
/**
|
|
8274
|
+
* Exchange a GitHub App JWT for an installation access token.
|
|
8275
|
+
* Uses a file-based cache next to the private key to avoid
|
|
8276
|
+
* hitting the GitHub API on every call.
|
|
8277
|
+
*/
|
|
8278
|
+
async function getInstallationToken(opts) {
|
|
8279
|
+
const cachePath = join(dirname(opts.privateKeyPath), "gh-token-cache.json");
|
|
8280
|
+
const cached = await readTokenCache(cachePath);
|
|
8281
|
+
if (cached) return cached;
|
|
8282
|
+
const privateKeyPem = await readFile(opts.privateKeyPath, "utf-8");
|
|
8283
|
+
const jwt = createAppJWT(opts.appId, privateKeyPem);
|
|
8284
|
+
const response = await fetch(`https://api.github.com/app/installations/${opts.installationId}/access_tokens`, {
|
|
8285
|
+
method: "POST",
|
|
8286
|
+
headers: {
|
|
8287
|
+
Authorization: `Bearer ${jwt}`,
|
|
8288
|
+
Accept: "application/vnd.github+json",
|
|
8289
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
8290
|
+
}
|
|
8291
|
+
});
|
|
8292
|
+
if (!response.ok) {
|
|
8293
|
+
const body = await response.text();
|
|
8294
|
+
throw new Error(`GitHub API error (${response.status}): ${body}`);
|
|
8295
|
+
}
|
|
8296
|
+
const data = await response.json();
|
|
8297
|
+
const result = {
|
|
8298
|
+
token: data.token,
|
|
8299
|
+
expiresAt: data.expires_at
|
|
8300
|
+
};
|
|
8301
|
+
await writeTokenCache(cachePath, result.token, result.expiresAt);
|
|
8302
|
+
return result;
|
|
8303
|
+
}
|
|
8304
|
+
//#endregion
|
|
8033
8305
|
//#region src/phases/gitSetup.ts
|
|
8034
8306
|
async function runGitSetupPhase(opts) {
|
|
8035
8307
|
const { configDir, agentName, appSlug, dispatch } = opts;
|
|
@@ -8051,18 +8323,17 @@ async function runGitSetupPhase(opts) {
|
|
|
8051
8323
|
status: "running"
|
|
8052
8324
|
});
|
|
8053
8325
|
const { privatePath } = await exportSSHKey({ configDir });
|
|
8054
|
-
const
|
|
8055
|
-
const gitConfigPath = await writeGitConfig({
|
|
8056
|
-
configDir,
|
|
8057
|
-
name: agentName,
|
|
8058
|
-
email: botUser.email,
|
|
8059
|
-
sshKeyPath: privatePath
|
|
8060
|
-
});
|
|
8326
|
+
const email = buildBotEmail((await lookupBotUser(appSlug, { maxRetries: 5 })).id, appSlug);
|
|
8061
8327
|
await updateConfigSection("git", {
|
|
8062
8328
|
name: agentName,
|
|
8063
|
-
email
|
|
8329
|
+
email,
|
|
8064
8330
|
signing: true,
|
|
8065
|
-
config_path:
|
|
8331
|
+
config_path: await writeGitConfig({
|
|
8332
|
+
configDir,
|
|
8333
|
+
name: agentName,
|
|
8334
|
+
email,
|
|
8335
|
+
sshKeyPath: privatePath
|
|
8336
|
+
})
|
|
8066
8337
|
}, configDir);
|
|
8067
8338
|
dispatch({
|
|
8068
8339
|
type: "step",
|
|
@@ -8073,7 +8344,7 @@ async function runGitSetupPhase(opts) {
|
|
|
8073
8344
|
//#endregion
|
|
8074
8345
|
//#region src/phases/identity.ts
|
|
8075
8346
|
async function runIdentityPhase(opts) {
|
|
8076
|
-
const { apiUrl, agentName, configDir, dispatch } = opts;
|
|
8347
|
+
const { apiUrl, agentName, configDir, org, dispatch } = opts;
|
|
8077
8348
|
const existingConfig = await readConfig(configDir);
|
|
8078
8349
|
const existingState = await readState(configDir);
|
|
8079
8350
|
if (existingConfig?.keys?.public_key && existingConfig?.oauth2?.client_id) {
|
|
@@ -8180,7 +8451,8 @@ async function runIdentityPhase(opts) {
|
|
|
8180
8451
|
const started = await startOnboarding(apiUrl, {
|
|
8181
8452
|
publicKey: kp.publicKey,
|
|
8182
8453
|
fingerprint: kp.fingerprint,
|
|
8183
|
-
agentName
|
|
8454
|
+
agentName,
|
|
8455
|
+
...org ? { org } : {}
|
|
8184
8456
|
});
|
|
8185
8457
|
await writeState({
|
|
8186
8458
|
workflowId: started.workflowId,
|
|
@@ -8572,7 +8844,7 @@ function ProgressPhase({ state, name, showManifestFallback, showInstallFallback
|
|
|
8572
8844
|
]
|
|
8573
8845
|
});
|
|
8574
8846
|
}
|
|
8575
|
-
function InitApp({ name, agents: agentsProp, apiUrl, dir = process.cwd() }) {
|
|
8847
|
+
function InitApp({ name, agents: agentsProp, apiUrl, dir = process.cwd(), org }) {
|
|
8576
8848
|
const { exit } = useApp();
|
|
8577
8849
|
const [state, dispatch] = useReducer(uiReducer, {
|
|
8578
8850
|
phase: "disclaimer",
|
|
@@ -8614,6 +8886,7 @@ function InitApp({ name, agents: agentsProp, apiUrl, dir = process.cwd() }) {
|
|
|
8614
8886
|
apiUrl,
|
|
8615
8887
|
agentName: name,
|
|
8616
8888
|
configDir,
|
|
8889
|
+
org,
|
|
8617
8890
|
dispatch
|
|
8618
8891
|
});
|
|
8619
8892
|
const githubApp = await runGithubAppPhase({
|
|
@@ -8648,12 +8921,14 @@ function InitApp({ name, agents: agentsProp, apiUrl, dir = process.cwd() }) {
|
|
|
8648
8921
|
agentTypes: selectedAgents,
|
|
8649
8922
|
publicKey: identity.publicKey,
|
|
8650
8923
|
fingerprint: identity.fingerprint,
|
|
8924
|
+
appId: githubApp.appId,
|
|
8651
8925
|
appSlug: githubApp.appSlug,
|
|
8652
8926
|
pemPath: githubApp.pemPath,
|
|
8653
8927
|
installationId: installation.installationId || githubApp.installationId,
|
|
8654
8928
|
identityId: installation.identityId,
|
|
8655
8929
|
clientId: installation.clientId || identity.clientId,
|
|
8656
8930
|
clientSecret: installation.clientSecret || identity.clientSecret,
|
|
8931
|
+
org,
|
|
8657
8932
|
dispatch
|
|
8658
8933
|
});
|
|
8659
8934
|
const mcpUrl = apiUrl.replace("://api.", "://mcp.") + "/mcp";
|
|
@@ -8707,6 +8982,580 @@ function InitApp({ name, agents: agentsProp, apiUrl, dir = process.cwd() }) {
|
|
|
8707
8982
|
});
|
|
8708
8983
|
}
|
|
8709
8984
|
//#endregion
|
|
8985
|
+
//#region src/phases/portValidate.ts
|
|
8986
|
+
/**
|
|
8987
|
+
* Validate a source `.moltnet/<agent>/` directory for porting.
|
|
8988
|
+
*
|
|
8989
|
+
* Runs the generic `repairConfig({ dryRun: true })` checks, then adds
|
|
8990
|
+
* port-specific blocking checks:
|
|
8991
|
+
* - `identity_id`, `keys.fingerprint`, `oauth2.client_id/secret`
|
|
8992
|
+
* - `github.app_id` present and numeric, `github.app_slug`, `github.installation_id`
|
|
8993
|
+
* - `ssh.private_key_path`, `ssh.public_key_path`, `git.config_path` set
|
|
8994
|
+
* - `github.private_key_path` set
|
|
8995
|
+
* - All four absolute paths (ssh priv/pub, git config, github pem) exist on disk
|
|
8996
|
+
*
|
|
8997
|
+
* Throws if `moltnet.json` is missing or unreadable — nothing to port.
|
|
8998
|
+
*/
|
|
8999
|
+
async function runPortValidatePhase(opts) {
|
|
9000
|
+
const { sourceDir } = opts;
|
|
9001
|
+
const config = await readConfig(sourceDir);
|
|
9002
|
+
if (!config) throw new Error(`No moltnet.json found in ${sourceDir} — nothing to port. Run \`legreffier\` on a repo first to create a source identity.`);
|
|
9003
|
+
const { issues: baseIssues } = await repairConfig({
|
|
9004
|
+
configDir: sourceDir,
|
|
9005
|
+
dryRun: true
|
|
9006
|
+
});
|
|
9007
|
+
const issues = [...baseIssues];
|
|
9008
|
+
if (!config.oauth2?.client_id) issues.push({
|
|
9009
|
+
field: "oauth2.client_id",
|
|
9010
|
+
problem: "missing — required for port",
|
|
9011
|
+
action: "warning"
|
|
9012
|
+
});
|
|
9013
|
+
if (!config.oauth2?.client_secret) issues.push({
|
|
9014
|
+
field: "oauth2.client_secret",
|
|
9015
|
+
problem: "missing — required for port",
|
|
9016
|
+
action: "warning"
|
|
9017
|
+
});
|
|
9018
|
+
if (!config.keys?.fingerprint) issues.push({
|
|
9019
|
+
field: "keys.fingerprint",
|
|
9020
|
+
problem: "missing — required for port",
|
|
9021
|
+
action: "warning"
|
|
9022
|
+
});
|
|
9023
|
+
if (!config.github?.app_id) issues.push({
|
|
9024
|
+
field: "github.app_id",
|
|
9025
|
+
problem: "missing — required for port",
|
|
9026
|
+
action: "warning"
|
|
9027
|
+
});
|
|
9028
|
+
if (!config.github?.app_slug) issues.push({
|
|
9029
|
+
field: "github.app_slug",
|
|
9030
|
+
problem: "missing — required for port (used for PEM filename and bot lookup)",
|
|
9031
|
+
action: "warning"
|
|
9032
|
+
});
|
|
9033
|
+
if (!config.github?.installation_id) issues.push({
|
|
9034
|
+
field: "github.installation_id",
|
|
9035
|
+
problem: "missing — required for port",
|
|
9036
|
+
action: "warning"
|
|
9037
|
+
});
|
|
9038
|
+
if (!config.github?.private_key_path) issues.push({
|
|
9039
|
+
field: "github.private_key_path",
|
|
9040
|
+
problem: "missing — required for port",
|
|
9041
|
+
action: "warning"
|
|
9042
|
+
});
|
|
9043
|
+
if (!config.ssh?.private_key_path) issues.push({
|
|
9044
|
+
field: "ssh.private_key_path",
|
|
9045
|
+
problem: "missing — required for port",
|
|
9046
|
+
action: "warning"
|
|
9047
|
+
});
|
|
9048
|
+
if (!config.ssh?.public_key_path) issues.push({
|
|
9049
|
+
field: "ssh.public_key_path",
|
|
9050
|
+
problem: "missing — required for port",
|
|
9051
|
+
action: "warning"
|
|
9052
|
+
});
|
|
9053
|
+
if (!config.git?.config_path) issues.push({
|
|
9054
|
+
field: "git.config_path",
|
|
9055
|
+
problem: "missing — required for port",
|
|
9056
|
+
action: "warning"
|
|
9057
|
+
});
|
|
9058
|
+
return {
|
|
9059
|
+
config,
|
|
9060
|
+
issues,
|
|
9061
|
+
canProceed: issues.filter((i) => i.action === "warning").length === 0
|
|
9062
|
+
};
|
|
9063
|
+
}
|
|
9064
|
+
/** Check whether a file is readable. Used by portCopy for optional files. */
|
|
9065
|
+
async function fileExists(path) {
|
|
9066
|
+
try {
|
|
9067
|
+
await access(path);
|
|
9068
|
+
return true;
|
|
9069
|
+
} catch {
|
|
9070
|
+
return false;
|
|
9071
|
+
}
|
|
9072
|
+
}
|
|
9073
|
+
//#endregion
|
|
9074
|
+
//#region src/phases/portCopy.ts
|
|
9075
|
+
/**
|
|
9076
|
+
* Copy private material from a source `.moltnet/<agent>/` into the target.
|
|
9077
|
+
*
|
|
9078
|
+
* Copies:
|
|
9079
|
+
* - `moltnet.json` (0600) — will later be rewritten in P3 with absolute paths
|
|
9080
|
+
* - GitHub App PEM (0600) — at `<sourceDir>/<appSlug>.pem` by convention
|
|
9081
|
+
* - SSH private key (0600) and public key (0644)
|
|
9082
|
+
* - `allowed_signers` if present (0644) — optional, warning only
|
|
9083
|
+
*
|
|
9084
|
+
* Assumes `runPortValidatePhase` has been run and `canProceed` was true,
|
|
9085
|
+
* so required fields and files are known to exist.
|
|
9086
|
+
*/
|
|
9087
|
+
async function runPortCopyPhase(opts) {
|
|
9088
|
+
const { sourceDir, targetDir, config } = opts;
|
|
9089
|
+
const copied = [];
|
|
9090
|
+
const warnings = [];
|
|
9091
|
+
await mkdir(targetDir, { recursive: true });
|
|
9092
|
+
const targetConfig = join(targetDir, "moltnet.json");
|
|
9093
|
+
await copyFile(join(sourceDir, "moltnet.json"), targetConfig);
|
|
9094
|
+
await chmod(targetConfig, 384);
|
|
9095
|
+
copied.push(targetConfig);
|
|
9096
|
+
if (!config.github?.private_key_path) throw new Error("github.private_key_path missing — run portValidate first");
|
|
9097
|
+
const targetPem = join(targetDir, basename(config.github.private_key_path));
|
|
9098
|
+
await copyFile(config.github.private_key_path, targetPem);
|
|
9099
|
+
await chmod(targetPem, 384);
|
|
9100
|
+
copied.push(targetPem);
|
|
9101
|
+
if (!config.ssh?.private_key_path || !config.ssh?.public_key_path) throw new Error("ssh key paths missing — run portValidate first");
|
|
9102
|
+
const sshDir = join(targetDir, "ssh");
|
|
9103
|
+
await mkdir(sshDir, { recursive: true });
|
|
9104
|
+
const targetSshPriv = join(sshDir, basename(config.ssh.private_key_path));
|
|
9105
|
+
await copyFile(config.ssh.private_key_path, targetSshPriv);
|
|
9106
|
+
await chmod(targetSshPriv, 384);
|
|
9107
|
+
copied.push(targetSshPriv);
|
|
9108
|
+
const targetSshPub = join(sshDir, basename(config.ssh.public_key_path));
|
|
9109
|
+
await copyFile(config.ssh.public_key_path, targetSshPub);
|
|
9110
|
+
await chmod(targetSshPub, 420);
|
|
9111
|
+
copied.push(targetSshPub);
|
|
9112
|
+
const sourceAllowed = join(dirname(config.ssh.private_key_path), "allowed_signers");
|
|
9113
|
+
if (await fileExists(sourceAllowed)) {
|
|
9114
|
+
const targetAllowed = join(sshDir, "allowed_signers");
|
|
9115
|
+
await copyFile(sourceAllowed, targetAllowed);
|
|
9116
|
+
await chmod(targetAllowed, 420);
|
|
9117
|
+
copied.push(targetAllowed);
|
|
9118
|
+
} else warnings.push(`allowed_signers not found at ${sourceAllowed} — skipping (optional)`);
|
|
9119
|
+
return {
|
|
9120
|
+
copied,
|
|
9121
|
+
warnings
|
|
9122
|
+
};
|
|
9123
|
+
}
|
|
9124
|
+
//#endregion
|
|
9125
|
+
//#region src/phases/portDiary.ts
|
|
9126
|
+
/**
|
|
9127
|
+
* Standard UUID v4-ish shape. Diary IDs are server-issued UUIDs; anything
|
|
9128
|
+
* else in this field is either stale data, a mis-parsed env line, or a
|
|
9129
|
+
* crafted injection attempt (e.g. embedded newlines) and must be rejected
|
|
9130
|
+
* before we echo it into the target env file.
|
|
9131
|
+
*/
|
|
9132
|
+
var DIARY_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9133
|
+
/**
|
|
9134
|
+
* Read `MOLTNET_DIARY_ID` from a source env file.
|
|
9135
|
+
* Returns null if the file or key is absent, or if the stored value is
|
|
9136
|
+
* not a valid UUID (defensive: never propagate malformed data).
|
|
9137
|
+
*/
|
|
9138
|
+
async function readSourceDiaryId(sourceDir) {
|
|
9139
|
+
try {
|
|
9140
|
+
const raw = parseEnvFile(await readFile(join(sourceDir, "env"), "utf-8")).MOLTNET_DIARY_ID;
|
|
9141
|
+
if (!raw || !DIARY_ID_RE.test(raw)) return null;
|
|
9142
|
+
return raw;
|
|
9143
|
+
} catch {
|
|
9144
|
+
return null;
|
|
9145
|
+
}
|
|
9146
|
+
}
|
|
9147
|
+
/**
|
|
9148
|
+
* Apply the chosen diary mode to the target env file:
|
|
9149
|
+
*
|
|
9150
|
+
* - `reuse`: persist the source MOLTNET_DIARY_ID in the target env
|
|
9151
|
+
* - `new`: strip MOLTNET_DIARY_ID from the target env (legreffier skill
|
|
9152
|
+
* will resolve it at session start via `diaries_list` / create)
|
|
9153
|
+
* - `skip`: leave the target env untouched
|
|
9154
|
+
*
|
|
9155
|
+
* Assumes the target env file has already been written by `portRewrite`.
|
|
9156
|
+
*/
|
|
9157
|
+
async function runPortDiaryPhase(opts) {
|
|
9158
|
+
const { targetDir, mode, sourceDiaryId } = opts;
|
|
9159
|
+
const envPath = join(targetDir, "env");
|
|
9160
|
+
if (mode === "skip") return {
|
|
9161
|
+
mode,
|
|
9162
|
+
diaryId: null,
|
|
9163
|
+
modified: false
|
|
9164
|
+
};
|
|
9165
|
+
let content = "";
|
|
9166
|
+
try {
|
|
9167
|
+
content = await readFile(envPath, "utf-8");
|
|
9168
|
+
} catch {
|
|
9169
|
+
return {
|
|
9170
|
+
mode,
|
|
9171
|
+
diaryId: null,
|
|
9172
|
+
modified: false
|
|
9173
|
+
};
|
|
9174
|
+
}
|
|
9175
|
+
const lines = content.split("\n");
|
|
9176
|
+
const diaryLineRe = /^\s*MOLTNET_DIARY_ID\s*=/;
|
|
9177
|
+
const filtered = lines.filter((l) => !diaryLineRe.test(l));
|
|
9178
|
+
const strippedExisting = filtered.length !== lines.length;
|
|
9179
|
+
if (mode === "reuse") {
|
|
9180
|
+
if (!sourceDiaryId) return {
|
|
9181
|
+
mode,
|
|
9182
|
+
diaryId: null,
|
|
9183
|
+
modified: false
|
|
9184
|
+
};
|
|
9185
|
+
if (!DIARY_ID_RE.test(sourceDiaryId)) throw new Error(`invalid sourceDiaryId: ${JSON.stringify(sourceDiaryId)} — expected UUID`);
|
|
9186
|
+
const diaryLine = `MOLTNET_DIARY_ID='${sourceDiaryId}'`;
|
|
9187
|
+
const gitCfgIdx = filtered.findIndex((l) => /^\s*GIT_CONFIG_GLOBAL\s*=/.test(l));
|
|
9188
|
+
if (gitCfgIdx >= 0) filtered.splice(gitCfgIdx + 1, 0, diaryLine);
|
|
9189
|
+
else filtered.unshift(diaryLine);
|
|
9190
|
+
await writeFile(envPath, filtered.join("\n"), { mode: 384 });
|
|
9191
|
+
return {
|
|
9192
|
+
mode,
|
|
9193
|
+
diaryId: sourceDiaryId,
|
|
9194
|
+
modified: true
|
|
9195
|
+
};
|
|
9196
|
+
}
|
|
9197
|
+
if (strippedExisting) await writeFile(envPath, filtered.join("\n"), { mode: 384 });
|
|
9198
|
+
return {
|
|
9199
|
+
mode,
|
|
9200
|
+
diaryId: null,
|
|
9201
|
+
modified: strippedExisting
|
|
9202
|
+
};
|
|
9203
|
+
}
|
|
9204
|
+
//#endregion
|
|
9205
|
+
//#region src/phases/portRewrite.ts
|
|
9206
|
+
/**
|
|
9207
|
+
* Rewrite absolute paths in the ported `moltnet.json` so they point to
|
|
9208
|
+
* target locations, then regenerate the gitconfig and env file.
|
|
9209
|
+
*
|
|
9210
|
+
* Assumes `portCopy` already placed files at:
|
|
9211
|
+
* - `<targetDir>/moltnet.json`
|
|
9212
|
+
* - `<targetDir>/<appSlug>.pem`
|
|
9213
|
+
* - `<targetDir>/ssh/<basename(ssh.private_key_path)>`
|
|
9214
|
+
* - `<targetDir>/ssh/<basename(ssh.public_key_path)>`
|
|
9215
|
+
*
|
|
9216
|
+
* The ported config still has the *source* absolute paths — this phase
|
|
9217
|
+
* rewrites them to the target absolute paths, then writes the gitconfig
|
|
9218
|
+
* (which needs the new ssh key path) and the env file (which needs the
|
|
9219
|
+
* new PEM path).
|
|
9220
|
+
*/
|
|
9221
|
+
async function runPortRewritePhase(opts) {
|
|
9222
|
+
const { targetDir, agentName, config } = opts;
|
|
9223
|
+
if (!config.ssh || !config.git || !config.github) throw new Error("config missing ssh/git/github sections — run portValidate first");
|
|
9224
|
+
const newSshPriv = join(targetDir, "ssh", basename(config.ssh.private_key_path));
|
|
9225
|
+
const newSshPub = join(targetDir, "ssh", basename(config.ssh.public_key_path));
|
|
9226
|
+
const newPem = join(targetDir, basename(config.github.private_key_path));
|
|
9227
|
+
const newGitConfig = join(targetDir, "gitconfig");
|
|
9228
|
+
await updateConfigSection("ssh", {
|
|
9229
|
+
private_key_path: newSshPriv,
|
|
9230
|
+
public_key_path: newSshPub
|
|
9231
|
+
}, targetDir);
|
|
9232
|
+
await updateConfigSection("github", {
|
|
9233
|
+
app_id: config.github.app_id,
|
|
9234
|
+
app_slug: config.github.app_slug,
|
|
9235
|
+
installation_id: config.github.installation_id,
|
|
9236
|
+
private_key_path: newPem,
|
|
9237
|
+
...config.github.org ? { org: config.github.org } : {}
|
|
9238
|
+
}, targetDir);
|
|
9239
|
+
await updateConfigSection("git", {
|
|
9240
|
+
name: config.git.name,
|
|
9241
|
+
email: config.git.email,
|
|
9242
|
+
signing: config.git.signing,
|
|
9243
|
+
config_path: newGitConfig
|
|
9244
|
+
}, targetDir);
|
|
9245
|
+
const rewrittenFields = [
|
|
9246
|
+
"ssh.private_key_path",
|
|
9247
|
+
"ssh.public_key_path",
|
|
9248
|
+
"github.private_key_path",
|
|
9249
|
+
"git.config_path"
|
|
9250
|
+
];
|
|
9251
|
+
await writeGitConfig({
|
|
9252
|
+
configDir: targetDir,
|
|
9253
|
+
name: config.git.name,
|
|
9254
|
+
email: config.git.email,
|
|
9255
|
+
sshKeyPath: newSshPriv
|
|
9256
|
+
});
|
|
9257
|
+
await writeEnvFile({
|
|
9258
|
+
envDir: targetDir,
|
|
9259
|
+
agentName,
|
|
9260
|
+
prefix: toEnvPrefix(agentName),
|
|
9261
|
+
clientId: config.oauth2.client_id,
|
|
9262
|
+
clientSecret: config.oauth2.client_secret,
|
|
9263
|
+
appId: config.github.app_id,
|
|
9264
|
+
pemPath: newPem,
|
|
9265
|
+
installationId: config.github.installation_id
|
|
9266
|
+
});
|
|
9267
|
+
return {
|
|
9268
|
+
configPath: join(targetDir, "moltnet.json"),
|
|
9269
|
+
rewrittenFields,
|
|
9270
|
+
gitConfigPath: newGitConfig,
|
|
9271
|
+
envDir: targetDir
|
|
9272
|
+
};
|
|
9273
|
+
}
|
|
9274
|
+
//#endregion
|
|
9275
|
+
//#region src/phases/portVerifyInstallation.ts
|
|
9276
|
+
/**
|
|
9277
|
+
* Warning-only check: can the ported GitHub App installation reach the
|
|
9278
|
+
* repo the port command is running against?
|
|
9279
|
+
*
|
|
9280
|
+
* Mints an installation token via github-agent, then calls
|
|
9281
|
+
* GET /installation/repositories. Never blocks — returns a warning
|
|
9282
|
+
* object the TUI renders. Any failure (bad token, network, missing
|
|
9283
|
+
* currentRepo) is downgraded to a warning.
|
|
9284
|
+
*/
|
|
9285
|
+
async function runPortVerifyInstallationPhase(opts) {
|
|
9286
|
+
const { config, currentRepo, apiBaseUrl = "https://api.github.com" } = opts;
|
|
9287
|
+
if (!currentRepo) return {
|
|
9288
|
+
status: "warning",
|
|
9289
|
+
message: "unable to determine current repo (git remote missing) — skipping installation scope check"
|
|
9290
|
+
};
|
|
9291
|
+
if (!config.github?.app_id || !config.github?.installation_id || !config.github?.private_key_path) return {
|
|
9292
|
+
status: "warning",
|
|
9293
|
+
message: "github.app_id / installation_id / private_key_path missing",
|
|
9294
|
+
currentRepo
|
|
9295
|
+
};
|
|
9296
|
+
let token;
|
|
9297
|
+
try {
|
|
9298
|
+
token = (await getInstallationToken({
|
|
9299
|
+
appId: config.github.app_id,
|
|
9300
|
+
privateKeyPath: config.github.private_key_path,
|
|
9301
|
+
installationId: config.github.installation_id
|
|
9302
|
+
})).token;
|
|
9303
|
+
} catch (err) {
|
|
9304
|
+
return {
|
|
9305
|
+
status: "warning",
|
|
9306
|
+
message: `could not mint installation token: ${err.message}`,
|
|
9307
|
+
currentRepo
|
|
9308
|
+
};
|
|
9309
|
+
}
|
|
9310
|
+
const accessible = [];
|
|
9311
|
+
let nextUrl = `${apiBaseUrl}/installation/repositories?per_page=100`;
|
|
9312
|
+
let pageCount = 0;
|
|
9313
|
+
const MAX_PAGES = 20;
|
|
9314
|
+
while (nextUrl && pageCount < MAX_PAGES) {
|
|
9315
|
+
pageCount++;
|
|
9316
|
+
let res;
|
|
9317
|
+
try {
|
|
9318
|
+
res = await fetch(nextUrl, { headers: {
|
|
9319
|
+
Accept: "application/vnd.github+json",
|
|
9320
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
9321
|
+
Authorization: `Bearer ${token}`
|
|
9322
|
+
} });
|
|
9323
|
+
} catch (err) {
|
|
9324
|
+
return {
|
|
9325
|
+
status: "warning",
|
|
9326
|
+
message: `installation check network error: ${err.message}`,
|
|
9327
|
+
currentRepo
|
|
9328
|
+
};
|
|
9329
|
+
}
|
|
9330
|
+
if (!res.ok) return {
|
|
9331
|
+
status: "warning",
|
|
9332
|
+
message: `installation check failed (${res.status})`,
|
|
9333
|
+
currentRepo
|
|
9334
|
+
};
|
|
9335
|
+
const data = await res.json();
|
|
9336
|
+
if (data.repository_selection === "all") return {
|
|
9337
|
+
status: "ok",
|
|
9338
|
+
message: "installation has access to all repos on the account",
|
|
9339
|
+
currentRepo,
|
|
9340
|
+
repositorySelection: "all"
|
|
9341
|
+
};
|
|
9342
|
+
for (const r of data.repositories) accessible.push(r.full_name);
|
|
9343
|
+
if (accessible.includes(currentRepo)) break;
|
|
9344
|
+
nextUrl = parseNextLink(res.headers.get("link"));
|
|
9345
|
+
}
|
|
9346
|
+
if (accessible.includes(currentRepo)) return {
|
|
9347
|
+
status: "ok",
|
|
9348
|
+
message: `installation has access to ${currentRepo}`,
|
|
9349
|
+
currentRepo,
|
|
9350
|
+
repositorySelection: "selected",
|
|
9351
|
+
accessibleRepos: accessible
|
|
9352
|
+
};
|
|
9353
|
+
const truncated = pageCount >= MAX_PAGES && nextUrl !== null;
|
|
9354
|
+
const truncatedNote = truncated ? " (scan truncated after " + MAX_PAGES + " pages — result may be stale)" : "";
|
|
9355
|
+
return {
|
|
9356
|
+
status: "repo-not-in-scope",
|
|
9357
|
+
message: `installation is scoped to ${accessible.length}${truncated ? "+" : ""} repo(s) but does not include ${currentRepo}. Add the repo at https://github.com/settings/installations/${config.github.installation_id}` + truncatedNote,
|
|
9358
|
+
currentRepo,
|
|
9359
|
+
repositorySelection: "selected",
|
|
9360
|
+
accessibleRepos: accessible
|
|
9361
|
+
};
|
|
9362
|
+
}
|
|
9363
|
+
/**
|
|
9364
|
+
* Parse the `Link` header for a `rel="next"` URL. Returns null if absent.
|
|
9365
|
+
* GitHub's Link header format:
|
|
9366
|
+
* <https://api.github.com/...?page=2>; rel="next", <...>; rel="last"
|
|
9367
|
+
*/
|
|
9368
|
+
function parseNextLink(header) {
|
|
9369
|
+
if (!header) return null;
|
|
9370
|
+
for (const part of header.split(",")) {
|
|
9371
|
+
const match = part.match(/<([^>]+)>\s*;\s*rel="next"/);
|
|
9372
|
+
if (match) return match[1];
|
|
9373
|
+
}
|
|
9374
|
+
return null;
|
|
9375
|
+
}
|
|
9376
|
+
//#endregion
|
|
9377
|
+
//#region src/PortApp.tsx
|
|
9378
|
+
/** Read `owner/repo` from `git remote get-url origin`. Returns null on any failure. */
|
|
9379
|
+
function detectCurrentRepo(repoDir) {
|
|
9380
|
+
try {
|
|
9381
|
+
const match = execSync("git remote get-url origin", {
|
|
9382
|
+
cwd: repoDir,
|
|
9383
|
+
encoding: "utf-8",
|
|
9384
|
+
stdio: [
|
|
9385
|
+
"ignore",
|
|
9386
|
+
"pipe",
|
|
9387
|
+
"ignore"
|
|
9388
|
+
]
|
|
9389
|
+
}).trim().match(/github\.com[:/]([^/]+\/[^/]+?)(\.git)?$/);
|
|
9390
|
+
return match ? match[1] : null;
|
|
9391
|
+
} catch {
|
|
9392
|
+
return null;
|
|
9393
|
+
}
|
|
9394
|
+
}
|
|
9395
|
+
function PortApp({ name, agents, sourceDir, targetRepoDir, diaryMode, apiUrl }) {
|
|
9396
|
+
const { exit } = useApp();
|
|
9397
|
+
const [phase, setPhase] = useState("validating");
|
|
9398
|
+
const [error, setError] = useState();
|
|
9399
|
+
const [summary, setSummary] = useState(null);
|
|
9400
|
+
useEffect(() => {
|
|
9401
|
+
(async () => {
|
|
9402
|
+
try {
|
|
9403
|
+
const targetDir = join(targetRepoDir, ".moltnet", name);
|
|
9404
|
+
const filesWritten = [];
|
|
9405
|
+
const warnings = [];
|
|
9406
|
+
setPhase("validating");
|
|
9407
|
+
const { config, issues, canProceed } = await runPortValidatePhase({ sourceDir });
|
|
9408
|
+
if (!canProceed) throw new Error("source .moltnet is not portable: " + issues.map((i) => `${i.field} (${i.problem})`).join(", "));
|
|
9409
|
+
const existing = await readConfig(targetDir);
|
|
9410
|
+
if (existing?.identity_id && existing.identity_id !== config.identity_id) throw new Error(`target ${targetDir} already has a different identity_id (${existing.identity_id}); refusing to overwrite`);
|
|
9411
|
+
setPhase("copying");
|
|
9412
|
+
const copyResult = await runPortCopyPhase({
|
|
9413
|
+
sourceDir,
|
|
9414
|
+
targetDir,
|
|
9415
|
+
config
|
|
9416
|
+
});
|
|
9417
|
+
filesWritten.push(...copyResult.copied);
|
|
9418
|
+
warnings.push(...copyResult.warnings);
|
|
9419
|
+
setPhase("rewriting");
|
|
9420
|
+
const rewriteResult = await runPortRewritePhase({
|
|
9421
|
+
targetDir,
|
|
9422
|
+
agentName: name,
|
|
9423
|
+
config
|
|
9424
|
+
});
|
|
9425
|
+
filesWritten.push(rewriteResult.gitConfigPath);
|
|
9426
|
+
filesWritten.push(join(targetDir, "env"));
|
|
9427
|
+
setPhase("diary");
|
|
9428
|
+
const diaryResult = await runPortDiaryPhase({
|
|
9429
|
+
targetDir,
|
|
9430
|
+
mode: diaryMode,
|
|
9431
|
+
sourceDiaryId: await readSourceDiaryId(sourceDir)
|
|
9432
|
+
});
|
|
9433
|
+
setPhase("agent_setup");
|
|
9434
|
+
const adapterOpts = {
|
|
9435
|
+
repoDir: targetRepoDir,
|
|
9436
|
+
agentName: name,
|
|
9437
|
+
prefix: toEnvPrefix(name),
|
|
9438
|
+
mcpUrl: config.endpoints?.mcp ?? apiUrl.replace("://api.", "://mcp.") + "/mcp",
|
|
9439
|
+
clientId: config.oauth2.client_id,
|
|
9440
|
+
clientSecret: config.oauth2.client_secret,
|
|
9441
|
+
appSlug: config.github?.app_slug ?? "",
|
|
9442
|
+
appId: config.github?.app_id ?? "",
|
|
9443
|
+
pemPath: join(targetDir, basename(config.github?.private_key_path ?? "")),
|
|
9444
|
+
installationId: config.github?.installation_id ?? ""
|
|
9445
|
+
};
|
|
9446
|
+
for (const agentType of agents) {
|
|
9447
|
+
const adapter = adapters[agentType];
|
|
9448
|
+
await adapter.writeMcpConfig(adapterOpts);
|
|
9449
|
+
filesWritten.push(`${agentType}: MCP config`);
|
|
9450
|
+
await adapter.writeSkills(targetRepoDir);
|
|
9451
|
+
filesWritten.push(`${agentType}: skills`);
|
|
9452
|
+
await adapter.writeSettings(adapterOpts);
|
|
9453
|
+
filesWritten.push(`${agentType}: settings`);
|
|
9454
|
+
await adapter.writeRules(adapterOpts);
|
|
9455
|
+
filesWritten.push(`${agentType}: gh token rule`);
|
|
9456
|
+
}
|
|
9457
|
+
setPhase("verifying");
|
|
9458
|
+
const verifyResult = await runPortVerifyInstallationPhase({
|
|
9459
|
+
config: await readConfig(targetDir) ?? config,
|
|
9460
|
+
currentRepo: detectCurrentRepo(targetRepoDir) ?? void 0
|
|
9461
|
+
});
|
|
9462
|
+
if (verifyResult.status !== "ok") warnings.push(verifyResult.message);
|
|
9463
|
+
setSummary({
|
|
9464
|
+
agentName: name,
|
|
9465
|
+
filesWritten,
|
|
9466
|
+
warnings,
|
|
9467
|
+
validationIssues: issues,
|
|
9468
|
+
diaryMode,
|
|
9469
|
+
diaryId: diaryResult.diaryId,
|
|
9470
|
+
installMessage: verifyResult.message,
|
|
9471
|
+
installStatus: verifyResult.status
|
|
9472
|
+
});
|
|
9473
|
+
setPhase("done");
|
|
9474
|
+
setTimeout(() => exit(), 3e3);
|
|
9475
|
+
} catch (err) {
|
|
9476
|
+
setError(toErrorMessage(err));
|
|
9477
|
+
setPhase("error");
|
|
9478
|
+
setTimeout(() => exit(/* @__PURE__ */ new Error("Port failed")), 3e3);
|
|
9479
|
+
}
|
|
9480
|
+
})();
|
|
9481
|
+
}, []);
|
|
9482
|
+
if (phase === "error") return /* @__PURE__ */ jsxs(Box, {
|
|
9483
|
+
flexDirection: "column",
|
|
9484
|
+
paddingY: 1,
|
|
9485
|
+
children: [/* @__PURE__ */ jsx(CliHero, {}), /* @__PURE__ */ jsx(Box, {
|
|
9486
|
+
borderStyle: "round",
|
|
9487
|
+
borderColor: cliTheme.color.error,
|
|
9488
|
+
paddingX: 2,
|
|
9489
|
+
paddingY: 1,
|
|
9490
|
+
children: /* @__PURE__ */ jsx(Text, {
|
|
9491
|
+
color: cliTheme.color.error,
|
|
9492
|
+
bold: true,
|
|
9493
|
+
children: "* Port failed: " + (error ?? "unknown error")
|
|
9494
|
+
})
|
|
9495
|
+
})]
|
|
9496
|
+
});
|
|
9497
|
+
if (phase !== "done") {
|
|
9498
|
+
const labels = {
|
|
9499
|
+
validating: `Validating source .moltnet/${name}...`,
|
|
9500
|
+
copying: `Copying private material...`,
|
|
9501
|
+
rewriting: `Rewriting paths in moltnet.json...`,
|
|
9502
|
+
diary: `Configuring diary (${diaryMode})...`,
|
|
9503
|
+
agent_setup: `Installing agent files for ${agents.join(", ")}...`,
|
|
9504
|
+
verifying: `Verifying GitHub App installation scope...`
|
|
9505
|
+
};
|
|
9506
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
9507
|
+
flexDirection: "column",
|
|
9508
|
+
paddingY: 1,
|
|
9509
|
+
children: [/* @__PURE__ */ jsx(CliHero, {}), /* @__PURE__ */ jsx(CliSpinner, { label: labels[phase] })]
|
|
9510
|
+
});
|
|
9511
|
+
}
|
|
9512
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
9513
|
+
flexDirection: "column",
|
|
9514
|
+
paddingY: 1,
|
|
9515
|
+
children: [
|
|
9516
|
+
/* @__PURE__ */ jsx(CliHero, {}),
|
|
9517
|
+
/* @__PURE__ */ jsxs(Box, {
|
|
9518
|
+
flexDirection: "column",
|
|
9519
|
+
marginBottom: 1,
|
|
9520
|
+
children: [
|
|
9521
|
+
/* @__PURE__ */ jsx(Text, {
|
|
9522
|
+
color: cliTheme.color.success,
|
|
9523
|
+
bold: true,
|
|
9524
|
+
children: `Ported ${name} to ${targetRepoDir}`
|
|
9525
|
+
}),
|
|
9526
|
+
/* @__PURE__ */ jsx(Text, {
|
|
9527
|
+
color: cliTheme.color.muted,
|
|
9528
|
+
children: ` diary: ${summary?.diaryMode}${summary?.diaryId ? ` (${summary.diaryId})` : ""}`
|
|
9529
|
+
}),
|
|
9530
|
+
/* @__PURE__ */ jsx(Text, {
|
|
9531
|
+
color: cliTheme.color.muted,
|
|
9532
|
+
children: ` installation: ${summary?.installStatus}`
|
|
9533
|
+
}),
|
|
9534
|
+
summary?.filesWritten.map((f, i) => /* @__PURE__ */ jsx(Text, {
|
|
9535
|
+
color: cliTheme.color.muted,
|
|
9536
|
+
children: " * " + f
|
|
9537
|
+
}, i))
|
|
9538
|
+
]
|
|
9539
|
+
}),
|
|
9540
|
+
summary && summary.warnings.length > 0 && /* @__PURE__ */ jsxs(Box, {
|
|
9541
|
+
borderStyle: "round",
|
|
9542
|
+
borderColor: cliTheme.color.warning,
|
|
9543
|
+
paddingX: 2,
|
|
9544
|
+
paddingY: 0,
|
|
9545
|
+
flexDirection: "column",
|
|
9546
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
9547
|
+
color: cliTheme.color.warning,
|
|
9548
|
+
bold: true,
|
|
9549
|
+
children: "Warnings:"
|
|
9550
|
+
}), summary.warnings.map((w, i) => /* @__PURE__ */ jsx(Text, {
|
|
9551
|
+
color: cliTheme.color.warning,
|
|
9552
|
+
children: " ! " + w
|
|
9553
|
+
}, i))]
|
|
9554
|
+
})
|
|
9555
|
+
]
|
|
9556
|
+
});
|
|
9557
|
+
}
|
|
9558
|
+
//#endregion
|
|
8710
9559
|
//#region src/SetupApp.tsx
|
|
8711
9560
|
function SetupApp({ name, agents: agentsProp, apiUrl, dir }) {
|
|
8712
9561
|
const { exit } = useApp();
|
|
@@ -8732,6 +9581,7 @@ function SetupApp({ name, agents: agentsProp, apiUrl, dir }) {
|
|
|
8732
9581
|
clientId: config.oauth2.client_id,
|
|
8733
9582
|
clientSecret: config.oauth2.client_secret,
|
|
8734
9583
|
appSlug: config.github?.app_slug ?? config.github?.app_id ?? "",
|
|
9584
|
+
appId: config.github?.app_id ?? "",
|
|
8735
9585
|
pemPath: config.github?.private_key_path ?? "",
|
|
8736
9586
|
installationId: config.github?.installation_id ?? ""
|
|
8737
9587
|
};
|
|
@@ -8835,7 +9685,13 @@ var { values, positionals } = parseArgs({
|
|
|
8835
9685
|
multiple: true
|
|
8836
9686
|
},
|
|
8837
9687
|
"api-url": { type: "string" },
|
|
8838
|
-
dir: { type: "string" }
|
|
9688
|
+
dir: { type: "string" },
|
|
9689
|
+
org: {
|
|
9690
|
+
type: "string",
|
|
9691
|
+
short: "o"
|
|
9692
|
+
},
|
|
9693
|
+
from: { type: "string" },
|
|
9694
|
+
diary: { type: "string" }
|
|
8839
9695
|
}
|
|
8840
9696
|
});
|
|
8841
9697
|
var subcommand = positionals[0] ?? "init";
|
|
@@ -8843,6 +9699,13 @@ var name = values["name"];
|
|
|
8843
9699
|
var agentFlags = values["agent"] ?? [];
|
|
8844
9700
|
var apiUrl = values["api-url"] ?? process.env.MOLTNET_API_URL ?? "https://api.themolt.net";
|
|
8845
9701
|
var dir = values["dir"] ?? process.cwd();
|
|
9702
|
+
var org = values["org"];
|
|
9703
|
+
var fromDir = values["from"];
|
|
9704
|
+
var diaryModeArg = values["diary"];
|
|
9705
|
+
if (diaryModeArg !== void 0 && subcommand !== "port") {
|
|
9706
|
+
process.stderr.write(`Error: --diary is only valid for \`legreffier port\` (got subcommand "${subcommand}")\n`);
|
|
9707
|
+
process.exit(1);
|
|
9708
|
+
}
|
|
8846
9709
|
if (subcommand === "github" && positionals[1] === "token") try {
|
|
8847
9710
|
printGitHubToken(resolveAgentName(name, process.env.GIT_CONFIG_GLOBAL), dir);
|
|
8848
9711
|
process.exit(0);
|
|
@@ -8851,7 +9714,7 @@ if (subcommand === "github" && positionals[1] === "token") try {
|
|
|
8851
9714
|
process.exit(1);
|
|
8852
9715
|
}
|
|
8853
9716
|
if (!name) {
|
|
8854
|
-
const usage = subcommand === "setup" ? "Usage: legreffier setup --name <agent-name> [--agent claude] [--agent codex] [--dir <path>]" : "Usage: legreffier [init] --name <agent-name> [--agent claude] [--agent codex] [--api-url <url>] [--dir <path>]";
|
|
9717
|
+
const usage = subcommand === "setup" ? "Usage: legreffier setup --name <agent-name> [--agent claude] [--agent codex] [--dir <path>]" : subcommand === "port" ? "Usage: legreffier port --name <agent-name> --from <path/to/source/.moltnet/<agent>> [--agent claude] [--agent codex] [--dir <target-repo>] [--diary new|reuse|skip]" : "Usage: legreffier [init] --name <agent-name> [--agent claude] [--agent codex] [--api-url <url>] [--dir <path>] [--org <github-org>]";
|
|
8855
9718
|
process.stderr.write(usage + "\n");
|
|
8856
9719
|
process.exit(1);
|
|
8857
9720
|
}
|
|
@@ -8874,10 +9737,51 @@ else if (subcommand === "init") render(/* @__PURE__ */ jsx(InitApp, {
|
|
|
8874
9737
|
name,
|
|
8875
9738
|
agents: agents.length > 0 ? agents : void 0,
|
|
8876
9739
|
apiUrl,
|
|
8877
|
-
dir
|
|
9740
|
+
dir,
|
|
9741
|
+
org
|
|
8878
9742
|
}));
|
|
8879
|
-
else {
|
|
8880
|
-
|
|
9743
|
+
else if (subcommand === "port") {
|
|
9744
|
+
if (!fromDir) {
|
|
9745
|
+
process.stderr.write("Error: legreffier port requires --from <path/to/source/.moltnet/<agent>>\n");
|
|
9746
|
+
process.exit(1);
|
|
9747
|
+
}
|
|
9748
|
+
const resolvedDiaryMode = diaryModeArg ?? "new";
|
|
9749
|
+
if (![
|
|
9750
|
+
"new",
|
|
9751
|
+
"reuse",
|
|
9752
|
+
"skip"
|
|
9753
|
+
].includes(resolvedDiaryMode)) {
|
|
9754
|
+
process.stderr.write(`Error: --diary must be one of: new, reuse, skip (got "${resolvedDiaryMode}")\n`);
|
|
9755
|
+
process.exit(1);
|
|
9756
|
+
}
|
|
9757
|
+
try {
|
|
9758
|
+
if (!statSync(dir).isDirectory()) {
|
|
9759
|
+
process.stderr.write(`Error: --dir "${dir}" is not a directory\n`);
|
|
9760
|
+
process.exit(1);
|
|
9761
|
+
}
|
|
9762
|
+
} catch {
|
|
9763
|
+
process.stderr.write(`Error: --dir "${dir}" does not exist\n`);
|
|
9764
|
+
process.exit(1);
|
|
9765
|
+
}
|
|
9766
|
+
try {
|
|
9767
|
+
if (!statSync(fromDir).isDirectory()) {
|
|
9768
|
+
process.stderr.write(`Error: --from "${fromDir}" is not a directory\n`);
|
|
9769
|
+
process.exit(1);
|
|
9770
|
+
}
|
|
9771
|
+
} catch {
|
|
9772
|
+
process.stderr.write(`Error: --from "${fromDir}" does not exist\n`);
|
|
9773
|
+
process.exit(1);
|
|
9774
|
+
}
|
|
9775
|
+
render(/* @__PURE__ */ jsx(PortApp, {
|
|
9776
|
+
name,
|
|
9777
|
+
agents: agents.length > 0 ? agents : ["claude"],
|
|
9778
|
+
sourceDir: fromDir,
|
|
9779
|
+
targetRepoDir: dir,
|
|
9780
|
+
diaryMode: resolvedDiaryMode,
|
|
9781
|
+
apiUrl
|
|
9782
|
+
}));
|
|
9783
|
+
} else {
|
|
9784
|
+
process.stderr.write(`Unknown subcommand: ${subcommand}. Use "init", "setup", or "port".\n`);
|
|
8881
9785
|
process.exit(1);
|
|
8882
9786
|
}
|
|
8883
9787
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/legreffier",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "LeGreffier — one-command accountable AI agent setup",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"ink": "^6.8.0",
|
|
25
25
|
"open": "^10.1.2",
|
|
26
26
|
"react": "^19.0.0",
|
|
27
|
-
"smol-toml": "^1.6.
|
|
27
|
+
"smol-toml": "^1.6.1"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@types/figlet": "^1.7.0",
|
|
@@ -33,10 +33,11 @@
|
|
|
33
33
|
"typescript": "^5.3.3",
|
|
34
34
|
"vite": "^8.0.0",
|
|
35
35
|
"vitest": "^3.0.0",
|
|
36
|
-
"@moltnet/crypto-service": "0.1.0",
|
|
37
|
-
"@themoltnet/design-system": "0.3.2",
|
|
38
36
|
"@moltnet/api-client": "0.1.0",
|
|
39
|
-
"@themoltnet/
|
|
37
|
+
"@themoltnet/design-system": "0.3.2",
|
|
38
|
+
"@moltnet/crypto-service": "0.1.0",
|
|
39
|
+
"@themoltnet/github-agent": "0.23.0",
|
|
40
|
+
"@themoltnet/sdk": "0.88.0"
|
|
40
41
|
},
|
|
41
42
|
"scripts": {
|
|
42
43
|
"dev": "vite build --watch",
|