@themoltnet/legreffier 0.30.0 → 0.32.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 +6 -3
- package/dist/index.js +410 -135
- package/package.json +6 -7
package/README.md
CHANGED
|
@@ -36,16 +36,19 @@ and across agents.
|
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
38
|
# Run directly (no install needed)
|
|
39
|
-
npx @themoltnet/legreffier --name my-agent
|
|
39
|
+
npx @themoltnet/legreffier init --name my-agent
|
|
40
40
|
|
|
41
41
|
# Or install globally
|
|
42
42
|
npm install -g @themoltnet/legreffier
|
|
43
|
-
legreffier --name my-agent
|
|
43
|
+
legreffier init --name my-agent
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
Every invocation requires an explicit subcommand (`init`, `setup`, `port`,
|
|
47
|
+
or `github`). Run `legreffier --help` to see them all.
|
|
48
|
+
|
|
46
49
|
### Subcommands
|
|
47
50
|
|
|
48
|
-
#### `legreffier init`
|
|
51
|
+
#### `legreffier init`
|
|
49
52
|
|
|
50
53
|
Full onboarding: identity, GitHub App, git signing, agent setup.
|
|
51
54
|
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { parseArgs, parseEnv } from "node:util";
|
|
4
|
-
import { Box, Text, render, useApp, useInput } from "ink";
|
|
2
|
+
import { defineCommand, runMain } from "citty";
|
|
5
3
|
import { execFileSync, execSync } from "node:child_process";
|
|
6
|
-
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { basename, dirname, isAbsolute, join } from "node:path";
|
|
5
|
+
import { Box, Text, render, useApp, useInput } from "ink";
|
|
7
6
|
import { useEffect, useReducer, useRef, useState } from "react";
|
|
8
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
-
import figlet from "figlet";
|
|
10
8
|
import { createSign } from "node:crypto";
|
|
11
9
|
import { createHash, randomBytes } from "crypto";
|
|
12
10
|
import { access, chmod, copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
13
11
|
import { homedir } from "node:os";
|
|
14
12
|
import { parse, stringify } from "smol-toml";
|
|
13
|
+
import { parseEnv } from "node:util";
|
|
15
14
|
import open from "open";
|
|
15
|
+
import { statSync } from "node:fs";
|
|
16
16
|
//#region src/github-token.ts
|
|
17
17
|
var MOLTNET_GITCONFIG_RE = /\.moltnet\/([^/]+)\/gitconfig$/;
|
|
18
18
|
function resolveAgentName(nameFlag, gitConfigGlobal) {
|
|
@@ -44,6 +44,146 @@ function printGitHubToken(agentName, dir) {
|
|
|
44
44
|
process.stdout.write(token);
|
|
45
45
|
}
|
|
46
46
|
//#endregion
|
|
47
|
+
//#region src/ui/types.ts
|
|
48
|
+
var SUPPORTED_AGENTS = ["claude", "codex"];
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/commands/shared.ts
|
|
51
|
+
/**
|
|
52
|
+
* Regex for valid agent names: 2-39 chars, lowercase alphanumerics and
|
|
53
|
+
* hyphens, must start and end with an alphanumeric. Matches the server-side
|
|
54
|
+
* validation so a client-side failure gives immediate feedback rather than
|
|
55
|
+
* a REST 400 after a keypair has been generated.
|
|
56
|
+
*/
|
|
57
|
+
var AGENT_NAME_RE = /^[a-z0-9][a-z0-9-]{0,37}[a-z0-9]$/;
|
|
58
|
+
var DEFAULT_API_URL = "https://api.themolt.net";
|
|
59
|
+
/**
|
|
60
|
+
* Common argument definitions shared across subcommands. Kept as plain
|
|
61
|
+
* objects (not a merged record) so individual commands can pick the flags
|
|
62
|
+
* they actually accept instead of inheriting every flag.
|
|
63
|
+
*/
|
|
64
|
+
var commonArgs = {
|
|
65
|
+
name: {
|
|
66
|
+
type: "string",
|
|
67
|
+
description: "Agent name (2-39 lowercase alphanumerics or hyphens, e.g. `jobi`)",
|
|
68
|
+
alias: "n",
|
|
69
|
+
valueHint: "agent-name"
|
|
70
|
+
},
|
|
71
|
+
agent: {
|
|
72
|
+
type: "string",
|
|
73
|
+
description: "Agent client to configure (repeatable: --agent claude --agent codex). Accepts: claude, codex.",
|
|
74
|
+
alias: "a",
|
|
75
|
+
valueHint: "claude|codex"
|
|
76
|
+
},
|
|
77
|
+
"api-url": {
|
|
78
|
+
type: "string",
|
|
79
|
+
description: "MoltNet API base URL (default: $MOLTNET_API_URL or https://api.themolt.net)",
|
|
80
|
+
valueHint: "url"
|
|
81
|
+
},
|
|
82
|
+
dir: {
|
|
83
|
+
type: "string",
|
|
84
|
+
description: "Target repository root (default: current working directory)",
|
|
85
|
+
valueHint: "path"
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Collect repeated `--agent` / `-a` values from rawArgs.
|
|
90
|
+
*
|
|
91
|
+
* Citty wraps Node's `parseArgs` without `multiple: true`, which means
|
|
92
|
+
* repeating `--agent claude --agent codex` keeps only the last value. The
|
|
93
|
+
* hand-rolled CLI supported repeats and users' docs rely on that shape, so
|
|
94
|
+
* we walk `rawArgs` ourselves to rebuild the full list before validating it
|
|
95
|
+
* against the supported-agent allowlist.
|
|
96
|
+
*/
|
|
97
|
+
function collectAgents(rawArgs) {
|
|
98
|
+
const out = [];
|
|
99
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
100
|
+
const token = rawArgs[i];
|
|
101
|
+
if (token === "--") break;
|
|
102
|
+
let value;
|
|
103
|
+
if (token === "--agent" || token === "-a") {
|
|
104
|
+
value = rawArgs[i + 1];
|
|
105
|
+
i++;
|
|
106
|
+
} else if (token.startsWith("--agent=")) value = token.slice(8);
|
|
107
|
+
else if (token.startsWith("-a=")) value = token.slice(3);
|
|
108
|
+
if (value === void 0) continue;
|
|
109
|
+
if (!SUPPORTED_AGENTS.includes(value)) throw new CliValidationError(`Unsupported agent: ${value}. Supported: ${SUPPORTED_AGENTS.join(", ")}`);
|
|
110
|
+
out.push(value);
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Validate a `--name` arg. Throws with the same user-facing message as the
|
|
116
|
+
* previous hand-rolled CLI so scripts and docs referencing it keep working.
|
|
117
|
+
*/
|
|
118
|
+
function requireAgentName(name) {
|
|
119
|
+
if (typeof name !== "string" || name.length === 0) throw new CliValidationError("--name is required");
|
|
120
|
+
if (!AGENT_NAME_RE.test(name)) throw new CliValidationError(`Invalid agent name: "${name}". Must be 2-39 lowercase alphanumeric characters or hyphens, starting and ending with a letter or digit.`);
|
|
121
|
+
return name;
|
|
122
|
+
}
|
|
123
|
+
/** Resolve the target repo dir, defaulting to CWD. */
|
|
124
|
+
function resolveDir(dir) {
|
|
125
|
+
if (typeof dir === "string" && dir.length > 0) return dir;
|
|
126
|
+
return process.cwd();
|
|
127
|
+
}
|
|
128
|
+
/** Resolve the API URL: --api-url flag > $MOLTNET_API_URL > default. */
|
|
129
|
+
function resolveApiUrl(apiUrl) {
|
|
130
|
+
if (typeof apiUrl === "string" && apiUrl.length > 0) return apiUrl;
|
|
131
|
+
return process.env["MOLTNET_API_URL"] ?? DEFAULT_API_URL;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Thrown by shared validators (`requireAgentName`, `collectAgents`,
|
|
135
|
+
* `validatePortFromArg` adapters, etc.) when the user passed a bad flag
|
|
136
|
+
* value. `withCleanErrors` catches these and prints a single-line
|
|
137
|
+
* "Error: <msg>" on stderr + exit 1, instead of letting citty dump the
|
|
138
|
+
* full stack via its default `console.error(error, "\n")` handler.
|
|
139
|
+
*/
|
|
140
|
+
var CliValidationError = class extends Error {
|
|
141
|
+
name = "CliValidationError";
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Wrap a command handler so `CliValidationError`s print a single clean
|
|
145
|
+
* line and exit 1. Unexpected errors (bugs, TypeErrors, network failures)
|
|
146
|
+
* still bubble up with their full stack so we can debug them.
|
|
147
|
+
*/
|
|
148
|
+
function withCleanErrors(handler) {
|
|
149
|
+
return async (ctx) => {
|
|
150
|
+
try {
|
|
151
|
+
await handler(ctx);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
if (err instanceof CliValidationError) {
|
|
154
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
throw err;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
var githubCommand = defineCommand({
|
|
162
|
+
meta: {
|
|
163
|
+
name: "github",
|
|
164
|
+
description: "GitHub-related helpers (token minting)"
|
|
165
|
+
},
|
|
166
|
+
subCommands: { token: defineCommand({
|
|
167
|
+
meta: {
|
|
168
|
+
name: "token",
|
|
169
|
+
description: "Print a short-lived installation token for the agent GitHub App (reads .moltnet/<agent>/moltnet.json)."
|
|
170
|
+
},
|
|
171
|
+
args: {
|
|
172
|
+
name: commonArgs.name,
|
|
173
|
+
dir: commonArgs.dir
|
|
174
|
+
},
|
|
175
|
+
run: withCleanErrors(({ args }) => {
|
|
176
|
+
let agentName;
|
|
177
|
+
try {
|
|
178
|
+
agentName = resolveAgentName(typeof args.name === "string" ? args.name : void 0, process.env["GIT_CONFIG_GLOBAL"]);
|
|
179
|
+
} catch (err) {
|
|
180
|
+
throw new CliValidationError(err instanceof Error ? err.message : String(err));
|
|
181
|
+
}
|
|
182
|
+
printGitHubToken(agentName, resolveDir(args.dir));
|
|
183
|
+
})
|
|
184
|
+
}) }
|
|
185
|
+
});
|
|
186
|
+
//#endregion
|
|
47
187
|
//#region ../../libs/design-system/src/tokens.ts
|
|
48
188
|
/**
|
|
49
189
|
* MoltNet Design Tokens
|
|
@@ -327,7 +467,7 @@ var QUILL_LINES = [
|
|
|
327
467
|
" ╲───────╲───────╲",
|
|
328
468
|
" ◆"
|
|
329
469
|
];
|
|
330
|
-
var WORDMARK
|
|
470
|
+
var WORDMARK = [
|
|
331
471
|
" __ __ ___ _ _____ _ _ ___ _____",
|
|
332
472
|
"| \\/ |/ _ \\| ||_ _|| \\| | __|_ _|",
|
|
333
473
|
"| |\\/| | (_) | |__| | | .` | _| | | ",
|
|
@@ -374,7 +514,7 @@ function CliHero({ animated = false }) {
|
|
|
374
514
|
color: glowColor,
|
|
375
515
|
children: HALO_TOP
|
|
376
516
|
}),
|
|
377
|
-
WORDMARK
|
|
517
|
+
WORDMARK.map((line, i) => /* @__PURE__ */ jsxs(Text, { children: [
|
|
378
518
|
/* @__PURE__ */ jsx(Text, {
|
|
379
519
|
color: glowColor,
|
|
380
520
|
children: " · · │ "
|
|
@@ -435,7 +575,6 @@ function CliHero({ animated = false }) {
|
|
|
435
575
|
})
|
|
436
576
|
});
|
|
437
577
|
}
|
|
438
|
-
figlet.textSync("MOLTNET", { font: "slant" });
|
|
439
578
|
//#endregion
|
|
440
579
|
//#region ../../libs/design-system/src/cli/CliSpinner.tsx
|
|
441
580
|
var FRAMES = [
|
|
@@ -7514,7 +7653,7 @@ var SKILLS = [
|
|
|
7514
7653
|
},
|
|
7515
7654
|
{
|
|
7516
7655
|
name: "legreffier-onboarding",
|
|
7517
|
-
files: ["SKILL.md"
|
|
7656
|
+
files: ["SKILL.md"]
|
|
7518
7657
|
}
|
|
7519
7658
|
];
|
|
7520
7659
|
async function downloadSkillFiles(skill) {
|
|
@@ -7944,6 +8083,63 @@ async function writeEnvFile(opts) {
|
|
|
7944
8083
|
}
|
|
7945
8084
|
await writeFile(envPath, outputLines.join("\n") + "\n", "utf-8");
|
|
7946
8085
|
}
|
|
8086
|
+
/**
|
|
8087
|
+
* Resolve the human operator's git identity from global git config.
|
|
8088
|
+
* Must be called BEFORE GIT_CONFIG_GLOBAL is set (so it reads the
|
|
8089
|
+
* human's config, not the agent's).
|
|
8090
|
+
*
|
|
8091
|
+
* Returns `"Name <email>"` or `null` if either is missing.
|
|
8092
|
+
*/
|
|
8093
|
+
function resolveHumanGitIdentity() {
|
|
8094
|
+
try {
|
|
8095
|
+
const name = execFileSync("git", [
|
|
8096
|
+
"config",
|
|
8097
|
+
"--global",
|
|
8098
|
+
"user.name"
|
|
8099
|
+
], {
|
|
8100
|
+
encoding: "utf-8",
|
|
8101
|
+
env: {
|
|
8102
|
+
...process.env,
|
|
8103
|
+
GIT_CONFIG_GLOBAL: void 0
|
|
8104
|
+
}
|
|
8105
|
+
}).trim();
|
|
8106
|
+
const email = execFileSync("git", [
|
|
8107
|
+
"config",
|
|
8108
|
+
"--global",
|
|
8109
|
+
"user.email"
|
|
8110
|
+
], {
|
|
8111
|
+
encoding: "utf-8",
|
|
8112
|
+
env: {
|
|
8113
|
+
...process.env,
|
|
8114
|
+
GIT_CONFIG_GLOBAL: void 0
|
|
8115
|
+
}
|
|
8116
|
+
}).trim();
|
|
8117
|
+
return name && email ? `${name} <${email}>` : null;
|
|
8118
|
+
} catch {
|
|
8119
|
+
return null;
|
|
8120
|
+
}
|
|
8121
|
+
}
|
|
8122
|
+
/**
|
|
8123
|
+
* Append MOLTNET_HUMAN_GIT_IDENTITY and optionally MOLTNET_COMMIT_AUTHORSHIP
|
|
8124
|
+
* to an existing env file if not already present.
|
|
8125
|
+
* Preserves existing content — only appends new vars.
|
|
8126
|
+
*/
|
|
8127
|
+
async function appendAuthorshipVars(envDir, humanGitIdentity, commitAuthorship) {
|
|
8128
|
+
const envPath = join(envDir, "env");
|
|
8129
|
+
let existing = "";
|
|
8130
|
+
try {
|
|
8131
|
+
existing = await readFile(envPath, "utf-8");
|
|
8132
|
+
} catch {
|
|
8133
|
+
return;
|
|
8134
|
+
}
|
|
8135
|
+
const lines = [];
|
|
8136
|
+
const hasVar = (content, key) => new RegExp(`^${key}=`, "m").test(content);
|
|
8137
|
+
if (humanGitIdentity && !hasVar(existing, "MOLTNET_HUMAN_GIT_IDENTITY")) lines.push(`MOLTNET_HUMAN_GIT_IDENTITY=${q(humanGitIdentity)}`);
|
|
8138
|
+
if (commitAuthorship && !hasVar(existing, "MOLTNET_COMMIT_AUTHORSHIP")) lines.push(`MOLTNET_COMMIT_AUTHORSHIP=${q(commitAuthorship)}`);
|
|
8139
|
+
if (lines.length === 0) return;
|
|
8140
|
+
const suffix = lines.join("\n") + "\n";
|
|
8141
|
+
await writeFile(envPath, existing.endsWith("\n") ? existing + suffix : existing + "\n" + suffix, "utf-8");
|
|
8142
|
+
}
|
|
7947
8143
|
//#endregion
|
|
7948
8144
|
//#region src/state.ts
|
|
7949
8145
|
function getStatePath(configDir) {
|
|
@@ -8048,6 +8244,7 @@ async function runAgentSetupPhase(opts) {
|
|
|
8048
8244
|
pemPath,
|
|
8049
8245
|
installationId
|
|
8050
8246
|
});
|
|
8247
|
+
await appendAuthorshipVars(configDir, opts.humanGitIdentity ?? resolveHumanGitIdentity(), opts.commitAuthorship);
|
|
8051
8248
|
await clearState(configDir);
|
|
8052
8249
|
}
|
|
8053
8250
|
//#endregion
|
|
@@ -8606,9 +8803,6 @@ async function runInstallationPhase(opts) {
|
|
|
8606
8803
|
};
|
|
8607
8804
|
}
|
|
8608
8805
|
//#endregion
|
|
8609
|
-
//#region src/ui/types.ts
|
|
8610
|
-
var SUPPORTED_AGENTS = ["claude", "codex"];
|
|
8611
|
-
//#endregion
|
|
8612
8806
|
//#region src/ui/AgentSelect.tsx
|
|
8613
8807
|
var AGENTS = [
|
|
8614
8808
|
{
|
|
@@ -8848,12 +9042,26 @@ function ProgressPhase({ state, name, showManifestFallback, showInstallFallback
|
|
|
8848
9042
|
flexDirection: "column",
|
|
8849
9043
|
children: steps.githubApp === "running" ? /* @__PURE__ */ jsxs(Box, {
|
|
8850
9044
|
flexDirection: "column",
|
|
8851
|
-
children: [/* @__PURE__ */ jsx(CliSpinner, { label: githubAppSpinnerLabel }),
|
|
8852
|
-
|
|
8853
|
-
children: [
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
9045
|
+
children: [/* @__PURE__ */ jsx(CliSpinner, { label: githubAppSpinnerLabel }), manifestFormUrl ? /* @__PURE__ */ jsxs(Box, {
|
|
9046
|
+
flexDirection: "column",
|
|
9047
|
+
children: [
|
|
9048
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
9049
|
+
color: cliTheme.color.text,
|
|
9050
|
+
children: [" ", "Confirm the GitHub App creation in your browser:"]
|
|
9051
|
+
}),
|
|
9052
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
9053
|
+
color: cliTheme.color.accent,
|
|
9054
|
+
children: [
|
|
9055
|
+
" ",
|
|
9056
|
+
"→ ",
|
|
9057
|
+
manifestFormUrl
|
|
9058
|
+
]
|
|
9059
|
+
}),
|
|
9060
|
+
showManifestFallback ? /* @__PURE__ */ jsxs(Text, {
|
|
9061
|
+
color: cliTheme.color.muted,
|
|
9062
|
+
children: [" ", "Browser didn't open? Copy the link above."]
|
|
9063
|
+
}) : null
|
|
9064
|
+
]
|
|
8857
9065
|
}) : null]
|
|
8858
9066
|
}) : /* @__PURE__ */ jsx(CliStatusLine, {
|
|
8859
9067
|
label: "Create GitHub App",
|
|
@@ -8880,12 +9088,26 @@ function ProgressPhase({ state, name, showManifestFallback, showInstallFallback
|
|
|
8880
9088
|
children: [
|
|
8881
9089
|
steps.installation === "running" ? /* @__PURE__ */ jsxs(Box, {
|
|
8882
9090
|
flexDirection: "column",
|
|
8883
|
-
children: [/* @__PURE__ */ jsx(CliSpinner, { label: installationSpinnerLabel }),
|
|
8884
|
-
|
|
8885
|
-
children: [
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
9091
|
+
children: [/* @__PURE__ */ jsx(CliSpinner, { label: installationSpinnerLabel }), installationUrl ? /* @__PURE__ */ jsxs(Box, {
|
|
9092
|
+
flexDirection: "column",
|
|
9093
|
+
children: [
|
|
9094
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
9095
|
+
color: cliTheme.color.text,
|
|
9096
|
+
children: [" ", "Install the GitHub App on your account/org:"]
|
|
9097
|
+
}),
|
|
9098
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
9099
|
+
color: cliTheme.color.accent,
|
|
9100
|
+
children: [
|
|
9101
|
+
" ",
|
|
9102
|
+
"→ ",
|
|
9103
|
+
installationUrl
|
|
9104
|
+
]
|
|
9105
|
+
}),
|
|
9106
|
+
showInstallFallback ? /* @__PURE__ */ jsxs(Text, {
|
|
9107
|
+
color: cliTheme.color.muted,
|
|
9108
|
+
children: [" ", "Browser didn't open? Copy the link above."]
|
|
9109
|
+
}) : null
|
|
9110
|
+
]
|
|
8889
9111
|
}) : null]
|
|
8890
9112
|
}) : /* @__PURE__ */ jsx(CliStatusLine, {
|
|
8891
9113
|
label: "GitHub App installation",
|
|
@@ -9043,6 +9265,75 @@ function InitApp({ name, agents: agentsProp, apiUrl, dir = process.cwd(), org })
|
|
|
9043
9265
|
});
|
|
9044
9266
|
}
|
|
9045
9267
|
//#endregion
|
|
9268
|
+
//#region src/commands/init.tsx
|
|
9269
|
+
var initCommand = defineCommand({
|
|
9270
|
+
meta: {
|
|
9271
|
+
name: "init",
|
|
9272
|
+
description: "Create a new agent identity and wire it into this repository"
|
|
9273
|
+
},
|
|
9274
|
+
args: {
|
|
9275
|
+
name: {
|
|
9276
|
+
...commonArgs.name,
|
|
9277
|
+
required: true
|
|
9278
|
+
},
|
|
9279
|
+
agent: commonArgs.agent,
|
|
9280
|
+
"api-url": commonArgs["api-url"],
|
|
9281
|
+
dir: commonArgs.dir,
|
|
9282
|
+
org: {
|
|
9283
|
+
type: "string",
|
|
9284
|
+
description: "GitHub organization to install the App on (optional)",
|
|
9285
|
+
alias: "o",
|
|
9286
|
+
valueHint: "github-org"
|
|
9287
|
+
}
|
|
9288
|
+
},
|
|
9289
|
+
run: withCleanErrors(({ args, rawArgs }) => {
|
|
9290
|
+
const name = requireAgentName(args.name);
|
|
9291
|
+
const agents = collectAgents(rawArgs);
|
|
9292
|
+
const apiUrl = resolveApiUrl(args["api-url"]);
|
|
9293
|
+
const dir = resolveDir(args.dir);
|
|
9294
|
+
const org = typeof args.org === "string" ? args.org : void 0;
|
|
9295
|
+
render(/* @__PURE__ */ jsx(InitApp, {
|
|
9296
|
+
name,
|
|
9297
|
+
agents: agents.length > 0 ? agents : void 0,
|
|
9298
|
+
apiUrl,
|
|
9299
|
+
dir,
|
|
9300
|
+
org
|
|
9301
|
+
}));
|
|
9302
|
+
})
|
|
9303
|
+
});
|
|
9304
|
+
//#endregion
|
|
9305
|
+
//#region src/phases/portArgs.ts
|
|
9306
|
+
/**
|
|
9307
|
+
* Validate the raw `--from` argument passed to `legreffier port` before
|
|
9308
|
+
* any filesystem access.
|
|
9309
|
+
*
|
|
9310
|
+
* `--from` must be:
|
|
9311
|
+
* - non-empty
|
|
9312
|
+
* - an absolute path (no `~`, no relative paths, no bare repo names)
|
|
9313
|
+
*
|
|
9314
|
+
* The help text for `port` documents this as a hard requirement because
|
|
9315
|
+
* the port pipeline rewrites paths embedded in `moltnet.json` and
|
|
9316
|
+
* `gitconfig`, and those rewrites only round-trip correctly when the
|
|
9317
|
+
* source is an absolute path. Accepting a relative path here silently
|
|
9318
|
+
* produces broken output in git worktrees (different CWD than the main
|
|
9319
|
+
* worktree root), so we fail fast instead of letting the port run.
|
|
9320
|
+
*/
|
|
9321
|
+
function validatePortFromArg(fromDir) {
|
|
9322
|
+
if (typeof fromDir !== "string" || fromDir.length === 0) return {
|
|
9323
|
+
ok: false,
|
|
9324
|
+
error: "legreffier port requires --from <repo-root>/.moltnet/<agent>"
|
|
9325
|
+
};
|
|
9326
|
+
if (fromDir.startsWith("~")) return {
|
|
9327
|
+
ok: false,
|
|
9328
|
+
error: `--from "${fromDir}" uses "~" which is not expanded. Pass an absolute path (e.g. "\$HOME/code/other-repo/.moltnet/<agent>").`
|
|
9329
|
+
};
|
|
9330
|
+
if (!isAbsolute(fromDir)) return {
|
|
9331
|
+
ok: false,
|
|
9332
|
+
error: `--from "${fromDir}" must be an absolute path (e.g. /Users/me/code/other-repo/.moltnet/<agent>). Relative paths break inside git worktrees where the CWD differs from the main worktree root.`
|
|
9333
|
+
};
|
|
9334
|
+
return { ok: true };
|
|
9335
|
+
}
|
|
9336
|
+
//#endregion
|
|
9046
9337
|
//#region src/phases/portValidate.ts
|
|
9047
9338
|
/**
|
|
9048
9339
|
* Validate a source `.moltnet/<agent>/` directory for porting.
|
|
@@ -9325,6 +9616,7 @@ async function runPortRewritePhase(opts) {
|
|
|
9325
9616
|
pemPath: newPem,
|
|
9326
9617
|
installationId: config.github.installation_id
|
|
9327
9618
|
});
|
|
9619
|
+
await appendAuthorshipVars(targetDir, resolveHumanGitIdentity());
|
|
9328
9620
|
return {
|
|
9329
9621
|
configPath: join(targetDir, "moltnet.json"),
|
|
9330
9622
|
rewrittenFields,
|
|
@@ -9617,6 +9909,68 @@ function PortApp({ name, agents, sourceDir, targetRepoDir, diaryMode, apiUrl })
|
|
|
9617
9909
|
});
|
|
9618
9910
|
}
|
|
9619
9911
|
//#endregion
|
|
9912
|
+
//#region src/commands/port.tsx
|
|
9913
|
+
var DIARY_MODES = [
|
|
9914
|
+
"new",
|
|
9915
|
+
"reuse",
|
|
9916
|
+
"skip"
|
|
9917
|
+
];
|
|
9918
|
+
function assertDirectory(label, path) {
|
|
9919
|
+
try {
|
|
9920
|
+
if (!statSync(path).isDirectory()) throw new CliValidationError(`${label} "${path}" is not a directory`);
|
|
9921
|
+
} catch (err) {
|
|
9922
|
+
if (err instanceof CliValidationError) throw err;
|
|
9923
|
+
throw new CliValidationError(`${label} "${path}" does not exist`);
|
|
9924
|
+
}
|
|
9925
|
+
}
|
|
9926
|
+
var portCommand = defineCommand({
|
|
9927
|
+
meta: {
|
|
9928
|
+
name: "port",
|
|
9929
|
+
description: "Copy an existing agent from another repository into this one"
|
|
9930
|
+
},
|
|
9931
|
+
args: {
|
|
9932
|
+
name: {
|
|
9933
|
+
...commonArgs.name,
|
|
9934
|
+
required: true
|
|
9935
|
+
},
|
|
9936
|
+
from: {
|
|
9937
|
+
type: "string",
|
|
9938
|
+
description: "Absolute path to the source agent directory (must be `<repo-root>/.moltnet/<agent-name>` and contain moltnet.json + gitconfig)",
|
|
9939
|
+
required: true,
|
|
9940
|
+
valueHint: "repo-root/.moltnet/agent-name"
|
|
9941
|
+
},
|
|
9942
|
+
agent: commonArgs.agent,
|
|
9943
|
+
"api-url": commonArgs["api-url"],
|
|
9944
|
+
dir: commonArgs.dir,
|
|
9945
|
+
diary: {
|
|
9946
|
+
type: "enum",
|
|
9947
|
+
description: "How to handle the diary in the new repo: `new` creates a fresh diary, `reuse` reuses the source diary ID, `skip` leaves MOLTNET_DIARY_ID unset",
|
|
9948
|
+
options: [...DIARY_MODES],
|
|
9949
|
+
default: "new",
|
|
9950
|
+
valueHint: "new|reuse|skip"
|
|
9951
|
+
}
|
|
9952
|
+
},
|
|
9953
|
+
run: withCleanErrors(({ args, rawArgs }) => {
|
|
9954
|
+
const name = requireAgentName(args.name);
|
|
9955
|
+
const agents = collectAgents(rawArgs);
|
|
9956
|
+
const apiUrl = resolveApiUrl(args["api-url"]);
|
|
9957
|
+
const dir = resolveDir(args.dir);
|
|
9958
|
+
const fromValidation = validatePortFromArg(args.from);
|
|
9959
|
+
if (!fromValidation.ok) throw new CliValidationError(fromValidation.error);
|
|
9960
|
+
const absoluteFromDir = args.from;
|
|
9961
|
+
assertDirectory("--dir", dir);
|
|
9962
|
+
assertDirectory("--from", absoluteFromDir);
|
|
9963
|
+
render(/* @__PURE__ */ jsx(PortApp, {
|
|
9964
|
+
name,
|
|
9965
|
+
agents: agents.length > 0 ? agents : ["claude"],
|
|
9966
|
+
sourceDir: absoluteFromDir,
|
|
9967
|
+
targetRepoDir: dir,
|
|
9968
|
+
diaryMode: args.diary,
|
|
9969
|
+
apiUrl
|
|
9970
|
+
}));
|
|
9971
|
+
})
|
|
9972
|
+
});
|
|
9973
|
+
//#endregion
|
|
9620
9974
|
//#region src/SetupApp.tsx
|
|
9621
9975
|
function SetupApp({ name, agents: agentsProp, apiUrl, dir }) {
|
|
9622
9976
|
const { exit } = useApp();
|
|
@@ -9732,118 +10086,39 @@ function SetupApp({ name, agents: agentsProp, apiUrl, dir }) {
|
|
|
9732
10086
|
}
|
|
9733
10087
|
//#endregion
|
|
9734
10088
|
//#region src/index.tsx
|
|
9735
|
-
|
|
9736
|
-
|
|
9737
|
-
|
|
9738
|
-
|
|
9739
|
-
|
|
9740
|
-
|
|
9741
|
-
|
|
9742
|
-
|
|
9743
|
-
|
|
9744
|
-
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
|
|
9753
|
-
|
|
9754
|
-
|
|
9755
|
-
|
|
10089
|
+
runMain(defineCommand({
|
|
10090
|
+
meta: {
|
|
10091
|
+
name: "legreffier",
|
|
10092
|
+
description: "LeGreffier — attribution and measured memory for AI coding agents"
|
|
10093
|
+
},
|
|
10094
|
+
subCommands: {
|
|
10095
|
+
init: initCommand,
|
|
10096
|
+
setup: defineCommand({
|
|
10097
|
+
meta: {
|
|
10098
|
+
name: "setup",
|
|
10099
|
+
description: "Install LeGreffier skills and MCP config into an existing repo"
|
|
10100
|
+
},
|
|
10101
|
+
args: {
|
|
10102
|
+
name: {
|
|
10103
|
+
...commonArgs.name,
|
|
10104
|
+
required: true
|
|
10105
|
+
},
|
|
10106
|
+
agent: commonArgs.agent,
|
|
10107
|
+
"api-url": commonArgs["api-url"],
|
|
10108
|
+
dir: commonArgs.dir
|
|
10109
|
+
},
|
|
10110
|
+
run: withCleanErrors(({ args, rawArgs }) => {
|
|
10111
|
+
render(/* @__PURE__ */ jsx(SetupApp, {
|
|
10112
|
+
name: requireAgentName(args.name),
|
|
10113
|
+
agents: collectAgents(rawArgs),
|
|
10114
|
+
apiUrl: resolveApiUrl(args["api-url"]),
|
|
10115
|
+
dir: resolveDir(args.dir)
|
|
10116
|
+
}));
|
|
10117
|
+
})
|
|
10118
|
+
}),
|
|
10119
|
+
port: portCommand,
|
|
10120
|
+
github: githubCommand
|
|
9756
10121
|
}
|
|
9757
|
-
});
|
|
9758
|
-
var subcommand = positionals[0] ?? "init";
|
|
9759
|
-
var name = values["name"];
|
|
9760
|
-
var agentFlags = values["agent"] ?? [];
|
|
9761
|
-
var apiUrl = values["api-url"] ?? process.env.MOLTNET_API_URL ?? "https://api.themolt.net";
|
|
9762
|
-
var dir = values["dir"] ?? process.cwd();
|
|
9763
|
-
var org = values["org"];
|
|
9764
|
-
var fromDir = values["from"];
|
|
9765
|
-
var diaryModeArg = values["diary"];
|
|
9766
|
-
if (diaryModeArg !== void 0 && subcommand !== "port") {
|
|
9767
|
-
process.stderr.write(`Error: --diary is only valid for \`legreffier port\` (got subcommand "${subcommand}")\n`);
|
|
9768
|
-
process.exit(1);
|
|
9769
|
-
}
|
|
9770
|
-
if (subcommand === "github" && positionals[1] === "token") try {
|
|
9771
|
-
printGitHubToken(resolveAgentName(name, process.env.GIT_CONFIG_GLOBAL), dir);
|
|
9772
|
-
process.exit(0);
|
|
9773
|
-
} catch (err) {
|
|
9774
|
-
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
9775
|
-
process.exit(1);
|
|
9776
|
-
}
|
|
9777
|
-
if (!name) {
|
|
9778
|
-
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>]";
|
|
9779
|
-
process.stderr.write(usage + "\n");
|
|
9780
|
-
process.exit(1);
|
|
9781
|
-
}
|
|
9782
|
-
if (!/^[a-z0-9][a-z0-9-]{0,37}[a-z0-9]$/.test(name)) {
|
|
9783
|
-
process.stderr.write(`Invalid agent name: "${name}". Must be 2-39 lowercase alphanumeric characters or hyphens, starting and ending with a letter or digit.\n`);
|
|
9784
|
-
process.exit(1);
|
|
9785
|
-
}
|
|
9786
|
-
for (const a of agentFlags) if (!SUPPORTED_AGENTS.includes(a)) {
|
|
9787
|
-
process.stderr.write(`Unsupported agent: ${a}. Supported: ${SUPPORTED_AGENTS.join(", ")}\n`);
|
|
9788
|
-
process.exit(1);
|
|
9789
|
-
}
|
|
9790
|
-
var agents = agentFlags;
|
|
9791
|
-
if (subcommand === "setup") render(/* @__PURE__ */ jsx(SetupApp, {
|
|
9792
|
-
name,
|
|
9793
|
-
agents,
|
|
9794
|
-
apiUrl,
|
|
9795
|
-
dir
|
|
9796
|
-
}));
|
|
9797
|
-
else if (subcommand === "init") render(/* @__PURE__ */ jsx(InitApp, {
|
|
9798
|
-
name,
|
|
9799
|
-
agents: agents.length > 0 ? agents : void 0,
|
|
9800
|
-
apiUrl,
|
|
9801
|
-
dir,
|
|
9802
|
-
org
|
|
9803
10122
|
}));
|
|
9804
|
-
else if (subcommand === "port") {
|
|
9805
|
-
if (!fromDir) {
|
|
9806
|
-
process.stderr.write("Error: legreffier port requires --from <path/to/source/.moltnet/<agent>>\n");
|
|
9807
|
-
process.exit(1);
|
|
9808
|
-
}
|
|
9809
|
-
const resolvedDiaryMode = diaryModeArg ?? "new";
|
|
9810
|
-
if (![
|
|
9811
|
-
"new",
|
|
9812
|
-
"reuse",
|
|
9813
|
-
"skip"
|
|
9814
|
-
].includes(resolvedDiaryMode)) {
|
|
9815
|
-
process.stderr.write(`Error: --diary must be one of: new, reuse, skip (got "${resolvedDiaryMode}")\n`);
|
|
9816
|
-
process.exit(1);
|
|
9817
|
-
}
|
|
9818
|
-
try {
|
|
9819
|
-
if (!statSync(dir).isDirectory()) {
|
|
9820
|
-
process.stderr.write(`Error: --dir "${dir}" is not a directory\n`);
|
|
9821
|
-
process.exit(1);
|
|
9822
|
-
}
|
|
9823
|
-
} catch {
|
|
9824
|
-
process.stderr.write(`Error: --dir "${dir}" does not exist\n`);
|
|
9825
|
-
process.exit(1);
|
|
9826
|
-
}
|
|
9827
|
-
try {
|
|
9828
|
-
if (!statSync(fromDir).isDirectory()) {
|
|
9829
|
-
process.stderr.write(`Error: --from "${fromDir}" is not a directory\n`);
|
|
9830
|
-
process.exit(1);
|
|
9831
|
-
}
|
|
9832
|
-
} catch {
|
|
9833
|
-
process.stderr.write(`Error: --from "${fromDir}" does not exist\n`);
|
|
9834
|
-
process.exit(1);
|
|
9835
|
-
}
|
|
9836
|
-
render(/* @__PURE__ */ jsx(PortApp, {
|
|
9837
|
-
name,
|
|
9838
|
-
agents: agents.length > 0 ? agents : ["claude"],
|
|
9839
|
-
sourceDir: fromDir,
|
|
9840
|
-
targetRepoDir: dir,
|
|
9841
|
-
diaryMode: resolvedDiaryMode,
|
|
9842
|
-
apiUrl
|
|
9843
|
-
}));
|
|
9844
|
-
} else {
|
|
9845
|
-
process.stderr.write(`Unknown subcommand: ${subcommand}. Use "init", "setup", or "port".\n`);
|
|
9846
|
-
process.exit(1);
|
|
9847
|
-
}
|
|
9848
10123
|
//#endregion
|
|
9849
10124
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/legreffier",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "LeGreffier — attribution and measured memory for AI coding agents.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
@@ -20,24 +20,23 @@
|
|
|
20
20
|
"node": ">=22"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"
|
|
23
|
+
"citty": "^0.2.2",
|
|
24
24
|
"ink": "^6.8.0",
|
|
25
25
|
"open": "^10.1.2",
|
|
26
26
|
"react": "^19.0.0",
|
|
27
27
|
"smol-toml": "^1.6.1"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@types/figlet": "^1.7.0",
|
|
31
30
|
"@types/node": "^20.11.0",
|
|
32
31
|
"@types/react": "^19.0.0",
|
|
33
32
|
"typescript": "^5.3.3",
|
|
34
33
|
"vite": "^8.0.0",
|
|
35
34
|
"vitest": "^3.0.0",
|
|
36
|
-
"@moltnet/api-client": "0.1.0",
|
|
37
35
|
"@moltnet/crypto-service": "0.1.0",
|
|
38
|
-
"@
|
|
39
|
-
"@themoltnet/
|
|
40
|
-
"@themoltnet/github-agent": "0.23.0"
|
|
36
|
+
"@moltnet/api-client": "0.1.0",
|
|
37
|
+
"@themoltnet/design-system": "0.5.1",
|
|
38
|
+
"@themoltnet/github-agent": "0.23.0",
|
|
39
|
+
"@themoltnet/sdk": "0.89.0"
|
|
41
40
|
},
|
|
42
41
|
"scripts": {
|
|
43
42
|
"dev": "vite build --watch",
|