@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.
Files changed (3) hide show
  1. package/README.md +6 -3
  2. package/dist/index.js +410 -135
  3. 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` (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,18 +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
- 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$1 = [
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$1.map((line, i) => /* @__PURE__ */ jsxs(Text, { children: [
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", "references/onboarding-guide.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 }), showManifestFallback && manifestFormUrl ? /* @__PURE__ */ jsxs(Text, {
8852
- color: cliTheme.color.muted,
8853
- children: [" → ", /* @__PURE__ */ jsx(Text, {
8854
- color: cliTheme.color.accent,
8855
- children: manifestFormUrl
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 }), showInstallFallback && installationUrl ? /* @__PURE__ */ jsxs(Text, {
8884
- color: cliTheme.color.muted,
8885
- children: [" → ", /* @__PURE__ */ jsx(Text, {
8886
- color: cliTheme.color.accent,
8887
- children: installationUrl
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
- var { values, positionals } = parseArgs({
9736
- args: process.argv.slice(2),
9737
- allowPositionals: true,
9738
- options: {
9739
- name: {
9740
- type: "string",
9741
- short: "n"
9742
- },
9743
- agent: {
9744
- type: "string",
9745
- short: "a",
9746
- multiple: true
9747
- },
9748
- "api-url": { type: "string" },
9749
- dir: { type: "string" },
9750
- org: {
9751
- type: "string",
9752
- short: "o"
9753
- },
9754
- from: { type: "string" },
9755
- 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
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.30.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",
36
- "@moltnet/api-client": "0.1.0",
37
35
  "@moltnet/crypto-service": "0.1.0",
38
- "@themoltnet/design-system": "0.4.0",
39
- "@themoltnet/sdk": "0.89.0",
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",