@themoltnet/legreffier 0.30.0 → 0.31.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/dist/index.js +301 -11
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { statSync } from "node:fs";
|
|
|
3
3
|
import { parseArgs, parseEnv } from "node:util";
|
|
4
4
|
import { Box, Text, render, useApp, useInput } from "ink";
|
|
5
5
|
import { execFileSync, execSync } from "node:child_process";
|
|
6
|
-
import { basename, dirname, join } from "node:path";
|
|
6
|
+
import { basename, dirname, isAbsolute, join } from "node:path";
|
|
7
7
|
import { useEffect, useReducer, useRef, useState } from "react";
|
|
8
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
9
|
import figlet from "figlet";
|
|
@@ -13,6 +13,251 @@ import { access, chmod, copyFile, mkdir, readFile, rm, writeFile } from "node:fs
|
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
14
|
import { parse, stringify } from "smol-toml";
|
|
15
15
|
import open from "open";
|
|
16
|
+
//#region src/commands/help.ts
|
|
17
|
+
function formatFlag(flag) {
|
|
18
|
+
const head = flag.short ? `${flag.name}, ${flag.short}` : flag.name;
|
|
19
|
+
const value = flag.value ? ` ${flag.value}` : "";
|
|
20
|
+
const suffixParts = [];
|
|
21
|
+
if (flag.required) suffixParts.push("(required)");
|
|
22
|
+
if (flag.default !== void 0) suffixParts.push(`default: ${flag.default}`);
|
|
23
|
+
return ` ${head}${value}${suffixParts.length > 0 ? ` [${suffixParts.join(", ")}]` : ""}\n ${flag.description}`;
|
|
24
|
+
}
|
|
25
|
+
function printCommandHelp(help) {
|
|
26
|
+
const lines = [];
|
|
27
|
+
lines.push(`${help.command} — ${help.summary}`);
|
|
28
|
+
lines.push("");
|
|
29
|
+
lines.push(`Usage: ${help.usage}`);
|
|
30
|
+
lines.push("");
|
|
31
|
+
lines.push(help.description);
|
|
32
|
+
if (help.flags.length > 0) {
|
|
33
|
+
lines.push("");
|
|
34
|
+
lines.push("Flags:");
|
|
35
|
+
for (const flag of help.flags) lines.push(formatFlag(flag));
|
|
36
|
+
}
|
|
37
|
+
if (help.examples.length > 0) {
|
|
38
|
+
lines.push("");
|
|
39
|
+
lines.push("Examples:");
|
|
40
|
+
for (const ex of help.examples) {
|
|
41
|
+
lines.push(` # ${ex.description}`);
|
|
42
|
+
lines.push(` ${ex.command}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (help.notes && help.notes.length > 0) {
|
|
46
|
+
lines.push("");
|
|
47
|
+
lines.push("Notes:");
|
|
48
|
+
for (const note of help.notes) lines.push(` - ${note}`);
|
|
49
|
+
}
|
|
50
|
+
lines.push("");
|
|
51
|
+
process.stdout.write(lines.join("\n"));
|
|
52
|
+
}
|
|
53
|
+
function printRootHelp(commands) {
|
|
54
|
+
const lines = [];
|
|
55
|
+
lines.push("legreffier — LeGreffier CLI");
|
|
56
|
+
lines.push("");
|
|
57
|
+
lines.push("Usage: legreffier <command> [flags]");
|
|
58
|
+
lines.push("");
|
|
59
|
+
lines.push("Commands:");
|
|
60
|
+
const pad = Math.max(...commands.map((c) => c.command.length)) + 2;
|
|
61
|
+
for (const cmd of commands) lines.push(` ${cmd.command.padEnd(pad)}${cmd.summary}`);
|
|
62
|
+
lines.push("");
|
|
63
|
+
lines.push("Run `legreffier <command> --help` for command-specific help.");
|
|
64
|
+
lines.push("");
|
|
65
|
+
process.stdout.write(lines.join("\n"));
|
|
66
|
+
}
|
|
67
|
+
var COMMANDS = [
|
|
68
|
+
{
|
|
69
|
+
command: "init",
|
|
70
|
+
summary: "Create a new agent identity and wire it into this repository",
|
|
71
|
+
usage: "legreffier init --name <agent-name> [flags]",
|
|
72
|
+
description: "Runs the full 5-phase onboarding: generates an Ed25519 keypair, registers the agent on MoltNet, creates a GitHub App via manifest flow, writes the gitconfig with SSH signing, installs the GitHub App on selected repos, and writes the MCP config for your chosen agent clients.",
|
|
73
|
+
flags: [
|
|
74
|
+
{
|
|
75
|
+
name: "--name",
|
|
76
|
+
short: "-n",
|
|
77
|
+
value: "<agent-name>",
|
|
78
|
+
description: "Agent name (2-39 lowercase alphanumerics/hyphens, e.g. `jobi`)",
|
|
79
|
+
required: true
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "--agent",
|
|
83
|
+
short: "-a",
|
|
84
|
+
value: "claude|codex",
|
|
85
|
+
description: "Agent client to configure (repeatable). Default: no client config written."
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "--api-url",
|
|
89
|
+
value: "<url>",
|
|
90
|
+
description: "MoltNet API base URL",
|
|
91
|
+
default: "https://api.themolt.net"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "--dir",
|
|
95
|
+
value: "<path>",
|
|
96
|
+
description: "Target repository root",
|
|
97
|
+
default: "current working directory"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "--org",
|
|
101
|
+
short: "-o",
|
|
102
|
+
value: "<github-org>",
|
|
103
|
+
description: "GitHub organization to install the App on (optional)"
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
examples: [{
|
|
107
|
+
description: "Basic init for a new agent named `jobi`",
|
|
108
|
+
command: "legreffier init --name jobi --agent claude"
|
|
109
|
+
}, {
|
|
110
|
+
description: "Init against a local API",
|
|
111
|
+
command: "legreffier init --name jobi --agent claude --api-url http://localhost:3000"
|
|
112
|
+
}]
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
command: "setup",
|
|
116
|
+
summary: "Install LeGreffier skills and MCP config into an existing repo",
|
|
117
|
+
usage: "legreffier setup --name <agent-name> [flags]",
|
|
118
|
+
description: "For a repository that already has `.moltnet/<agent-name>/` credentials (e.g. after running `init` elsewhere), `setup` writes the MCP config, downloads skills, and configures your agent clients. Does not touch identity, keys, or GitHub App state.",
|
|
119
|
+
flags: [
|
|
120
|
+
{
|
|
121
|
+
name: "--name",
|
|
122
|
+
short: "-n",
|
|
123
|
+
value: "<agent-name>",
|
|
124
|
+
description: "Agent name (must already exist under `.moltnet/`)",
|
|
125
|
+
required: true
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: "--agent",
|
|
129
|
+
short: "-a",
|
|
130
|
+
value: "claude|codex",
|
|
131
|
+
description: "Agent client to configure (repeatable)"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: "--dir",
|
|
135
|
+
value: "<path>",
|
|
136
|
+
description: "Target repository root",
|
|
137
|
+
default: "current working directory"
|
|
138
|
+
}
|
|
139
|
+
],
|
|
140
|
+
examples: [{
|
|
141
|
+
description: "Install skills and MCP config for both Claude and Codex",
|
|
142
|
+
command: "legreffier setup --name jobi --agent claude --agent codex"
|
|
143
|
+
}]
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
command: "port",
|
|
147
|
+
summary: "Copy an existing agent from another repository into this one",
|
|
148
|
+
usage: "legreffier port --name <agent-name> --from <repo-root>/.moltnet/<agent-name> [flags]",
|
|
149
|
+
description: "Ports an existing agent identity (keypair, moltnet.json, gitconfig, GitHub App credentials) from a source repository into the current one. `--from` is strict: it must point to the exact `<repo-root>/.moltnet/<agent-name>` directory. The source repo is not modified.",
|
|
150
|
+
flags: [
|
|
151
|
+
{
|
|
152
|
+
name: "--name",
|
|
153
|
+
short: "-n",
|
|
154
|
+
value: "<agent-name>",
|
|
155
|
+
description: "Agent name to port (must exist under `--from`)",
|
|
156
|
+
required: true
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: "--from",
|
|
160
|
+
value: "<repo-root>/.moltnet/<agent-name>",
|
|
161
|
+
description: "Absolute path to the source agent directory. Strict format: must be `<repo-root>/.moltnet/<agent-name>` and contain moltnet.json + gitconfig.",
|
|
162
|
+
required: true
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: "--agent",
|
|
166
|
+
short: "-a",
|
|
167
|
+
value: "claude|codex",
|
|
168
|
+
description: "Agent client to configure in the target repo (repeatable)",
|
|
169
|
+
default: "claude"
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: "--dir",
|
|
173
|
+
value: "<path>",
|
|
174
|
+
description: "Target repository root",
|
|
175
|
+
default: "current working directory"
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: "--diary",
|
|
179
|
+
value: "new|reuse|skip",
|
|
180
|
+
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",
|
|
181
|
+
default: "new"
|
|
182
|
+
}
|
|
183
|
+
],
|
|
184
|
+
examples: [{
|
|
185
|
+
description: "Port agent `jobi` from a sibling repo",
|
|
186
|
+
command: "legreffier port --name jobi --from /Users/me/code/other-repo/.moltnet/jobi"
|
|
187
|
+
}, {
|
|
188
|
+
description: "Port and reuse the existing diary",
|
|
189
|
+
command: "legreffier port --name jobi --from /Users/me/code/other-repo/.moltnet/jobi --diary reuse"
|
|
190
|
+
}],
|
|
191
|
+
notes: ["The source repo is read-only; nothing there is modified.", "`--from` does not accept relative paths, `~`, or repo-name shorthands. Provide the full `.moltnet/<agent>` directory path."]
|
|
192
|
+
}
|
|
193
|
+
];
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region src/commands/resolveCommand.ts
|
|
196
|
+
/**
|
|
197
|
+
* Long-form option names that consume the next argument as a value.
|
|
198
|
+
* Used by `resolveHelpCommand` to skip over option values when scanning
|
|
199
|
+
* for the first positional subcommand.
|
|
200
|
+
*/
|
|
201
|
+
var VALUE_OPTIONS_LONG = new Set([
|
|
202
|
+
"--name",
|
|
203
|
+
"--agent",
|
|
204
|
+
"--api-url",
|
|
205
|
+
"--dir",
|
|
206
|
+
"--org",
|
|
207
|
+
"--from",
|
|
208
|
+
"--diary"
|
|
209
|
+
]);
|
|
210
|
+
/**
|
|
211
|
+
* Short-form option names that consume the next argument as a value.
|
|
212
|
+
* Kept in sync with `parseArgs` options in index.tsx.
|
|
213
|
+
*/
|
|
214
|
+
var VALUE_OPTIONS_SHORT = new Set([
|
|
215
|
+
"-n",
|
|
216
|
+
"-a",
|
|
217
|
+
"-o"
|
|
218
|
+
]);
|
|
219
|
+
function isValueOption(arg) {
|
|
220
|
+
if (VALUE_OPTIONS_LONG.has(arg)) return true;
|
|
221
|
+
if (VALUE_OPTIONS_SHORT.has(arg)) return true;
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Resolve which command's help to print for `legreffier <...> --help`.
|
|
226
|
+
*
|
|
227
|
+
* Scans `rawArgs` linearly, skipping option flags and their values, and
|
|
228
|
+
* returns the first positional argument that matches a known command. If
|
|
229
|
+
* no positional is found (or the positional is not a known command),
|
|
230
|
+
* returns `null` so the caller prints root help.
|
|
231
|
+
*
|
|
232
|
+
* Unlike `rawArgs.find((a) => !a.startsWith('-'))`, this correctly handles
|
|
233
|
+
* flags-before-subcommand orderings like:
|
|
234
|
+
*
|
|
235
|
+
* legreffier --name jobi port --help
|
|
236
|
+
*
|
|
237
|
+
* where the naive scan would return "jobi" instead of "port".
|
|
238
|
+
*
|
|
239
|
+
* Unknown positionals (typos, etc.) fall back to root help rather than
|
|
240
|
+
* silently matching nothing, so users see the full command list.
|
|
241
|
+
*/
|
|
242
|
+
function resolveHelpCommand(rawArgs, commands) {
|
|
243
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
244
|
+
const arg = rawArgs[i];
|
|
245
|
+
if (arg === void 0) continue;
|
|
246
|
+
if (arg === "--help" || arg === "-h") continue;
|
|
247
|
+
if (arg.startsWith("--") && arg.includes("=")) continue;
|
|
248
|
+
if (arg.startsWith("--")) {
|
|
249
|
+
if (isValueOption(arg)) i++;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (arg.startsWith("-") && arg.length > 1) {
|
|
253
|
+
if (isValueOption(arg)) i++;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
return commands.find((c) => c.command === arg) ?? null;
|
|
257
|
+
}
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
//#endregion
|
|
16
261
|
//#region src/github-token.ts
|
|
17
262
|
var MOLTNET_GITCONFIG_RE = /\.moltnet\/([^/]+)\/gitconfig$/;
|
|
18
263
|
function resolveAgentName(nameFlag, gitConfigGlobal) {
|
|
@@ -7514,7 +7759,7 @@ var SKILLS = [
|
|
|
7514
7759
|
},
|
|
7515
7760
|
{
|
|
7516
7761
|
name: "legreffier-onboarding",
|
|
7517
|
-
files: ["SKILL.md"
|
|
7762
|
+
files: ["SKILL.md"]
|
|
7518
7763
|
}
|
|
7519
7764
|
];
|
|
7520
7765
|
async function downloadSkillFiles(skill) {
|
|
@@ -9043,6 +9288,38 @@ function InitApp({ name, agents: agentsProp, apiUrl, dir = process.cwd(), org })
|
|
|
9043
9288
|
});
|
|
9044
9289
|
}
|
|
9045
9290
|
//#endregion
|
|
9291
|
+
//#region src/phases/portArgs.ts
|
|
9292
|
+
/**
|
|
9293
|
+
* Validate the raw `--from` argument passed to `legreffier port` before
|
|
9294
|
+
* any filesystem access.
|
|
9295
|
+
*
|
|
9296
|
+
* `--from` must be:
|
|
9297
|
+
* - non-empty
|
|
9298
|
+
* - an absolute path (no `~`, no relative paths, no bare repo names)
|
|
9299
|
+
*
|
|
9300
|
+
* The help text for `port` documents this as a hard requirement because
|
|
9301
|
+
* the port pipeline rewrites paths embedded in `moltnet.json` and
|
|
9302
|
+
* `gitconfig`, and those rewrites only round-trip correctly when the
|
|
9303
|
+
* source is an absolute path. Accepting a relative path here silently
|
|
9304
|
+
* produces broken output in git worktrees (different CWD than the main
|
|
9305
|
+
* worktree root), so we fail fast instead of letting the port run.
|
|
9306
|
+
*/
|
|
9307
|
+
function validatePortFromArg(fromDir) {
|
|
9308
|
+
if (typeof fromDir !== "string" || fromDir.length === 0) return {
|
|
9309
|
+
ok: false,
|
|
9310
|
+
error: "legreffier port requires --from <repo-root>/.moltnet/<agent>"
|
|
9311
|
+
};
|
|
9312
|
+
if (fromDir.startsWith("~")) return {
|
|
9313
|
+
ok: false,
|
|
9314
|
+
error: `--from "${fromDir}" uses "~" which is not expanded. Pass an absolute path (e.g. "\$HOME/code/other-repo/.moltnet/<agent>").`
|
|
9315
|
+
};
|
|
9316
|
+
if (!isAbsolute(fromDir)) return {
|
|
9317
|
+
ok: false,
|
|
9318
|
+
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.`
|
|
9319
|
+
};
|
|
9320
|
+
return { ok: true };
|
|
9321
|
+
}
|
|
9322
|
+
//#endregion
|
|
9046
9323
|
//#region src/phases/portValidate.ts
|
|
9047
9324
|
/**
|
|
9048
9325
|
* Validate a source `.moltnet/<agent>/` directory for porting.
|
|
@@ -9732,8 +10009,19 @@ function SetupApp({ name, agents: agentsProp, apiUrl, dir }) {
|
|
|
9732
10009
|
}
|
|
9733
10010
|
//#endregion
|
|
9734
10011
|
//#region src/index.tsx
|
|
10012
|
+
var rawArgs = process.argv.slice(2);
|
|
10013
|
+
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
10014
|
+
const help = resolveHelpCommand(rawArgs, COMMANDS);
|
|
10015
|
+
if (help) printCommandHelp(help);
|
|
10016
|
+
else printRootHelp(COMMANDS);
|
|
10017
|
+
process.exit(0);
|
|
10018
|
+
}
|
|
10019
|
+
if (rawArgs.length === 0) {
|
|
10020
|
+
printRootHelp(COMMANDS);
|
|
10021
|
+
process.exit(0);
|
|
10022
|
+
}
|
|
9735
10023
|
var { values, positionals } = parseArgs({
|
|
9736
|
-
args:
|
|
10024
|
+
args: rawArgs,
|
|
9737
10025
|
allowPositionals: true,
|
|
9738
10026
|
options: {
|
|
9739
10027
|
name: {
|
|
@@ -9775,8 +10063,8 @@ if (subcommand === "github" && positionals[1] === "token") try {
|
|
|
9775
10063
|
process.exit(1);
|
|
9776
10064
|
}
|
|
9777
10065
|
if (!name) {
|
|
9778
|
-
const
|
|
9779
|
-
process.stderr.write(
|
|
10066
|
+
const help = COMMANDS.find((c) => c.command === subcommand);
|
|
10067
|
+
process.stderr.write(`Error: --name is required.\n\nRun \`legreffier ${help ? help.command : "<command>"} --help\` for details.\n`);
|
|
9780
10068
|
process.exit(1);
|
|
9781
10069
|
}
|
|
9782
10070
|
if (!/^[a-z0-9][a-z0-9-]{0,37}[a-z0-9]$/.test(name)) {
|
|
@@ -9802,10 +10090,12 @@ else if (subcommand === "init") render(/* @__PURE__ */ jsx(InitApp, {
|
|
|
9802
10090
|
org
|
|
9803
10091
|
}));
|
|
9804
10092
|
else if (subcommand === "port") {
|
|
9805
|
-
|
|
9806
|
-
|
|
10093
|
+
const fromValidation = validatePortFromArg(fromDir);
|
|
10094
|
+
if (!fromValidation.ok) {
|
|
10095
|
+
process.stderr.write(`Error: ${fromValidation.error}\n`);
|
|
9807
10096
|
process.exit(1);
|
|
9808
10097
|
}
|
|
10098
|
+
const absoluteFromDir = fromDir;
|
|
9809
10099
|
const resolvedDiaryMode = diaryModeArg ?? "new";
|
|
9810
10100
|
if (![
|
|
9811
10101
|
"new",
|
|
@@ -9825,18 +10115,18 @@ else if (subcommand === "port") {
|
|
|
9825
10115
|
process.exit(1);
|
|
9826
10116
|
}
|
|
9827
10117
|
try {
|
|
9828
|
-
if (!statSync(
|
|
9829
|
-
process.stderr.write(`Error: --from "${
|
|
10118
|
+
if (!statSync(absoluteFromDir).isDirectory()) {
|
|
10119
|
+
process.stderr.write(`Error: --from "${absoluteFromDir}" is not a directory\n`);
|
|
9830
10120
|
process.exit(1);
|
|
9831
10121
|
}
|
|
9832
10122
|
} catch {
|
|
9833
|
-
process.stderr.write(`Error: --from "${
|
|
10123
|
+
process.stderr.write(`Error: --from "${absoluteFromDir}" does not exist\n`);
|
|
9834
10124
|
process.exit(1);
|
|
9835
10125
|
}
|
|
9836
10126
|
render(/* @__PURE__ */ jsx(PortApp, {
|
|
9837
10127
|
name,
|
|
9838
10128
|
agents: agents.length > 0 ? agents : ["claude"],
|
|
9839
|
-
sourceDir:
|
|
10129
|
+
sourceDir: absoluteFromDir,
|
|
9840
10130
|
targetRepoDir: dir,
|
|
9841
10131
|
diaryMode: resolvedDiaryMode,
|
|
9842
10132
|
apiUrl
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/legreffier",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "LeGreffier — attribution and measured memory for AI coding agents.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
@@ -34,10 +34,10 @@
|
|
|
34
34
|
"vite": "^8.0.0",
|
|
35
35
|
"vitest": "^3.0.0",
|
|
36
36
|
"@moltnet/api-client": "0.1.0",
|
|
37
|
-
"@moltnet/crypto-service": "0.1.0",
|
|
38
37
|
"@themoltnet/design-system": "0.4.0",
|
|
38
|
+
"@themoltnet/github-agent": "0.23.0",
|
|
39
39
|
"@themoltnet/sdk": "0.89.0",
|
|
40
|
-
"@
|
|
40
|
+
"@moltnet/crypto-service": "0.1.0"
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"dev": "vite build --watch",
|