@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.
Files changed (2) hide show
  1. package/dist/index.js +301 -11
  2. 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", "references/onboarding-guide.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: process.argv.slice(2),
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 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");
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
- if (!fromDir) {
9806
- process.stderr.write("Error: legreffier port requires --from <path/to/source/.moltnet/<agent>>\n");
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(fromDir).isDirectory()) {
9829
- process.stderr.write(`Error: --from "${fromDir}" is not a directory\n`);
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 "${fromDir}" does not exist\n`);
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: fromDir,
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.30.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
- "@themoltnet/github-agent": "0.23.0"
40
+ "@moltnet/crypto-service": "0.1.0"
41
41
  },
42
42
  "scripts": {
43
43
  "dev": "vite build --watch",