@themoltnet/legreffier 0.31.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.
Files changed (3) hide show
  1. package/README.md +6 -3
  2. package/dist/index.js +376 -391
  3. package/package.json +5 -6
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` (default)
51
+ #### `legreffier init`
49
52
 
50
53
  Full onboarding: identity, GitHub App, git signing, agent setup.
51
54
 
package/dist/index.js CHANGED
@@ -1,263 +1,18 @@
1
1
  #!/usr/bin/env node
2
- import { statSync } from "node:fs";
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
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";
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
15
+ import { statSync } from "node:fs";
261
16
  //#region src/github-token.ts
262
17
  var MOLTNET_GITCONFIG_RE = /\.moltnet\/([^/]+)\/gitconfig$/;
263
18
  function resolveAgentName(nameFlag, gitConfigGlobal) {
@@ -289,6 +44,146 @@ function printGitHubToken(agentName, dir) {
289
44
  process.stdout.write(token);
290
45
  }
291
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
292
187
  //#region ../../libs/design-system/src/tokens.ts
293
188
  /**
294
189
  * MoltNet Design Tokens
@@ -572,7 +467,7 @@ var QUILL_LINES = [
572
467
  " ╲───────╲───────╲",
573
468
  " ◆"
574
469
  ];
575
- var WORDMARK$1 = [
470
+ var WORDMARK = [
576
471
  " __ __ ___ _ _____ _ _ ___ _____",
577
472
  "| \\/ |/ _ \\| ||_ _|| \\| | __|_ _|",
578
473
  "| |\\/| | (_) | |__| | | .` | _| | | ",
@@ -619,7 +514,7 @@ function CliHero({ animated = false }) {
619
514
  color: glowColor,
620
515
  children: HALO_TOP
621
516
  }),
622
- WORDMARK$1.map((line, i) => /* @__PURE__ */ jsxs(Text, { children: [
517
+ WORDMARK.map((line, i) => /* @__PURE__ */ jsxs(Text, { children: [
623
518
  /* @__PURE__ */ jsx(Text, {
624
519
  color: glowColor,
625
520
  children: " · · │ "
@@ -680,7 +575,6 @@ function CliHero({ animated = false }) {
680
575
  })
681
576
  });
682
577
  }
683
- figlet.textSync("MOLTNET", { font: "slant" });
684
578
  //#endregion
685
579
  //#region ../../libs/design-system/src/cli/CliSpinner.tsx
686
580
  var FRAMES = [
@@ -8189,6 +8083,63 @@ async function writeEnvFile(opts) {
8189
8083
  }
8190
8084
  await writeFile(envPath, outputLines.join("\n") + "\n", "utf-8");
8191
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
+ }
8192
8143
  //#endregion
8193
8144
  //#region src/state.ts
8194
8145
  function getStatePath(configDir) {
@@ -8293,6 +8244,7 @@ async function runAgentSetupPhase(opts) {
8293
8244
  pemPath,
8294
8245
  installationId
8295
8246
  });
8247
+ await appendAuthorshipVars(configDir, opts.humanGitIdentity ?? resolveHumanGitIdentity(), opts.commitAuthorship);
8296
8248
  await clearState(configDir);
8297
8249
  }
8298
8250
  //#endregion
@@ -8851,9 +8803,6 @@ async function runInstallationPhase(opts) {
8851
8803
  };
8852
8804
  }
8853
8805
  //#endregion
8854
- //#region src/ui/types.ts
8855
- var SUPPORTED_AGENTS = ["claude", "codex"];
8856
- //#endregion
8857
8806
  //#region src/ui/AgentSelect.tsx
8858
8807
  var AGENTS = [
8859
8808
  {
@@ -9093,12 +9042,26 @@ function ProgressPhase({ state, name, showManifestFallback, showInstallFallback
9093
9042
  flexDirection: "column",
9094
9043
  children: steps.githubApp === "running" ? /* @__PURE__ */ jsxs(Box, {
9095
9044
  flexDirection: "column",
9096
- children: [/* @__PURE__ */ jsx(CliSpinner, { label: githubAppSpinnerLabel }), showManifestFallback && manifestFormUrl ? /* @__PURE__ */ jsxs(Text, {
9097
- color: cliTheme.color.muted,
9098
- children: [" → ", /* @__PURE__ */ jsx(Text, {
9099
- color: cliTheme.color.accent,
9100
- children: manifestFormUrl
9101
- })]
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
+ ]
9102
9065
  }) : null]
9103
9066
  }) : /* @__PURE__ */ jsx(CliStatusLine, {
9104
9067
  label: "Create GitHub App",
@@ -9125,12 +9088,26 @@ function ProgressPhase({ state, name, showManifestFallback, showInstallFallback
9125
9088
  children: [
9126
9089
  steps.installation === "running" ? /* @__PURE__ */ jsxs(Box, {
9127
9090
  flexDirection: "column",
9128
- children: [/* @__PURE__ */ jsx(CliSpinner, { label: installationSpinnerLabel }), showInstallFallback && installationUrl ? /* @__PURE__ */ jsxs(Text, {
9129
- color: cliTheme.color.muted,
9130
- children: [" → ", /* @__PURE__ */ jsx(Text, {
9131
- color: cliTheme.color.accent,
9132
- children: installationUrl
9133
- })]
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
+ ]
9134
9111
  }) : null]
9135
9112
  }) : /* @__PURE__ */ jsx(CliStatusLine, {
9136
9113
  label: "GitHub App installation",
@@ -9288,6 +9265,43 @@ function InitApp({ name, agents: agentsProp, apiUrl, dir = process.cwd(), org })
9288
9265
  });
9289
9266
  }
9290
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
9291
9305
  //#region src/phases/portArgs.ts
9292
9306
  /**
9293
9307
  * Validate the raw `--from` argument passed to `legreffier port` before
@@ -9602,6 +9616,7 @@ async function runPortRewritePhase(opts) {
9602
9616
  pemPath: newPem,
9603
9617
  installationId: config.github.installation_id
9604
9618
  });
9619
+ await appendAuthorshipVars(targetDir, resolveHumanGitIdentity());
9605
9620
  return {
9606
9621
  configPath: join(targetDir, "moltnet.json"),
9607
9622
  rewrittenFields,
@@ -9894,6 +9909,68 @@ function PortApp({ name, agents, sourceDir, targetRepoDir, diaryMode, apiUrl })
9894
9909
  });
9895
9910
  }
9896
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
9897
9974
  //#region src/SetupApp.tsx
9898
9975
  function SetupApp({ name, agents: agentsProp, apiUrl, dir }) {
9899
9976
  const { exit } = useApp();
@@ -10009,131 +10086,39 @@ function SetupApp({ name, agents: agentsProp, apiUrl, dir }) {
10009
10086
  }
10010
10087
  //#endregion
10011
10088
  //#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
- }
10023
- var { values, positionals } = parseArgs({
10024
- args: rawArgs,
10025
- allowPositionals: true,
10026
- options: {
10027
- name: {
10028
- type: "string",
10029
- short: "n"
10030
- },
10031
- agent: {
10032
- type: "string",
10033
- short: "a",
10034
- multiple: true
10035
- },
10036
- "api-url": { type: "string" },
10037
- dir: { type: "string" },
10038
- org: {
10039
- type: "string",
10040
- short: "o"
10041
- },
10042
- from: { type: "string" },
10043
- diary: { type: "string" }
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
10044
10121
  }
10045
- });
10046
- var subcommand = positionals[0] ?? "init";
10047
- var name = values["name"];
10048
- var agentFlags = values["agent"] ?? [];
10049
- var apiUrl = values["api-url"] ?? process.env.MOLTNET_API_URL ?? "https://api.themolt.net";
10050
- var dir = values["dir"] ?? process.cwd();
10051
- var org = values["org"];
10052
- var fromDir = values["from"];
10053
- var diaryModeArg = values["diary"];
10054
- if (diaryModeArg !== void 0 && subcommand !== "port") {
10055
- process.stderr.write(`Error: --diary is only valid for \`legreffier port\` (got subcommand "${subcommand}")\n`);
10056
- process.exit(1);
10057
- }
10058
- if (subcommand === "github" && positionals[1] === "token") try {
10059
- printGitHubToken(resolveAgentName(name, process.env.GIT_CONFIG_GLOBAL), dir);
10060
- process.exit(0);
10061
- } catch (err) {
10062
- process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
10063
- process.exit(1);
10064
- }
10065
- if (!name) {
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`);
10068
- process.exit(1);
10069
- }
10070
- if (!/^[a-z0-9][a-z0-9-]{0,37}[a-z0-9]$/.test(name)) {
10071
- 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`);
10072
- process.exit(1);
10073
- }
10074
- for (const a of agentFlags) if (!SUPPORTED_AGENTS.includes(a)) {
10075
- process.stderr.write(`Unsupported agent: ${a}. Supported: ${SUPPORTED_AGENTS.join(", ")}\n`);
10076
- process.exit(1);
10077
- }
10078
- var agents = agentFlags;
10079
- if (subcommand === "setup") render(/* @__PURE__ */ jsx(SetupApp, {
10080
- name,
10081
- agents,
10082
- apiUrl,
10083
- dir
10084
10122
  }));
10085
- else if (subcommand === "init") render(/* @__PURE__ */ jsx(InitApp, {
10086
- name,
10087
- agents: agents.length > 0 ? agents : void 0,
10088
- apiUrl,
10089
- dir,
10090
- org
10091
- }));
10092
- else if (subcommand === "port") {
10093
- const fromValidation = validatePortFromArg(fromDir);
10094
- if (!fromValidation.ok) {
10095
- process.stderr.write(`Error: ${fromValidation.error}\n`);
10096
- process.exit(1);
10097
- }
10098
- const absoluteFromDir = fromDir;
10099
- const resolvedDiaryMode = diaryModeArg ?? "new";
10100
- if (![
10101
- "new",
10102
- "reuse",
10103
- "skip"
10104
- ].includes(resolvedDiaryMode)) {
10105
- process.stderr.write(`Error: --diary must be one of: new, reuse, skip (got "${resolvedDiaryMode}")\n`);
10106
- process.exit(1);
10107
- }
10108
- try {
10109
- if (!statSync(dir).isDirectory()) {
10110
- process.stderr.write(`Error: --dir "${dir}" is not a directory\n`);
10111
- process.exit(1);
10112
- }
10113
- } catch {
10114
- process.stderr.write(`Error: --dir "${dir}" does not exist\n`);
10115
- process.exit(1);
10116
- }
10117
- try {
10118
- if (!statSync(absoluteFromDir).isDirectory()) {
10119
- process.stderr.write(`Error: --from "${absoluteFromDir}" is not a directory\n`);
10120
- process.exit(1);
10121
- }
10122
- } catch {
10123
- process.stderr.write(`Error: --from "${absoluteFromDir}" does not exist\n`);
10124
- process.exit(1);
10125
- }
10126
- render(/* @__PURE__ */ jsx(PortApp, {
10127
- name,
10128
- agents: agents.length > 0 ? agents : ["claude"],
10129
- sourceDir: absoluteFromDir,
10130
- targetRepoDir: dir,
10131
- diaryMode: resolvedDiaryMode,
10132
- apiUrl
10133
- }));
10134
- } else {
10135
- process.stderr.write(`Unknown subcommand: ${subcommand}. Use "init", "setup", or "port".\n`);
10136
- process.exit(1);
10137
- }
10138
10123
  //#endregion
10139
10124
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/legreffier",
3
- "version": "0.31.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
- "figlet": "^1.8.0",
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",
35
+ "@moltnet/crypto-service": "0.1.0",
36
36
  "@moltnet/api-client": "0.1.0",
37
- "@themoltnet/design-system": "0.4.0",
37
+ "@themoltnet/design-system": "0.5.1",
38
38
  "@themoltnet/github-agent": "0.23.0",
39
- "@themoltnet/sdk": "0.89.0",
40
- "@moltnet/crypto-service": "0.1.0"
39
+ "@themoltnet/sdk": "0.89.0"
41
40
  },
42
41
  "scripts": {
43
42
  "dev": "vite build --watch",