@mercury-fw/cli 0.29.4 → 0.30.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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @mercury-fw/cli
2
2
 
3
+ ## 0.30.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 238d4dc: - `mfw create` runs `bun install` in the new app, showing its output as it goes (`--no-install` to skip it).
8
+ - `mfw create` creates a git repository on `main` with a first commit, lockfile included (`--no-git` to skip it); inside another repository it is skipped, and when git refuses a step or is missing, the message reports why and lists the commands to finish by hand.
9
+ - `mfw create` adds `origin` when given one, in the wizard or with `--git-remote`; nothing is pushed.
10
+ - The message at the end of `mfw create` lists what each step did and only the steps left to run.
11
+
12
+ ### Patch Changes
13
+
14
+ - @mercury-fw/core@0.30.0
15
+
3
16
  ## 0.29.4
4
17
 
5
18
  ### Patch Changes
package/README.md CHANGED
@@ -44,9 +44,12 @@ Writes a new Mercury app into `<folder>`, which has to be missing or empty; the
44
44
 
45
45
  - the app name (the `name` in `package.json`, defaulting to the folder's);
46
46
  - the assistant's name and role, which become `persona/identity.md` ("You are Hermes, the platform team's release assistant.") next to a `persona/tone.md` to edit;
47
- - which channels and which tool plugins to include (none is fine: the REPL always works).
47
+ - which channels and which tool plugins to include (none is fine: the REPL always works);
48
+ - the git remote for `origin`, which you can leave empty (most of the time it doesn't exist yet when you scaffold).
48
49
 
49
- It writes the `mercury.config.ts` for that selection, the persona, the service and REPL entrypoints, a Dockerfile, a compose file with Qdrant, and an env example listing every variable the chosen pieces read. A plugin that hands over lists comes wrapped in the formatter with a starting rule, yours to change. Nothing is installed: run `bun install` in the new app.
50
+ It writes the `mercury.config.ts` for that selection, the persona, the service and REPL entrypoints, a Dockerfile, a compose file with Qdrant, and an env example listing every variable the chosen pieces read. A plugin that hands over lists comes wrapped in the formatter with a starting rule, yours to change.
51
+
52
+ Then it runs `bun install` in the app and creates a git repository on `main` with a first commit (`bun.lock` included), adding `origin` when you gave one; nothing is pushed. Each step is best effort and never undoes the ones before it: an install that fails still leaves the app committed, without the lockfile, and the closing message says what to run. The repository is skipped when the folder is already inside one (an app created in a monorepo belongs to it). Whether git can commit is git's call: when it refuses (no identity, a hook) or isn't installed, the message reports why and lists the commands to finish by hand. `bun install` prints its own output as it runs.
50
53
 
51
54
  | Flag | |
52
55
  |---|---|
@@ -55,6 +58,9 @@ It writes the `mercury.config.ts` for that selection, the persona, the service a
55
58
  | `--role <text>` | Completes "You are <name>, …" (default `an internal assistant`). |
56
59
  | `--channels <ids>` | Comma-separated: `google-chat`, `http`. |
57
60
  | `--plugins <ids>` | Comma-separated: `jira`, `bitbucket`, `atlassian-admin`. |
61
+ | `--git-remote <url>` | The repository's `origin`, taken as typed. |
62
+ | `--no-install` | Don't run `bun install`. |
63
+ | `--no-git` | Don't create the repository. |
58
64
  | `-y`, `--yes` | No questions: the flags, and the defaults for the rest. |
59
65
 
60
66
  ```bash
@@ -14,7 +14,13 @@ export type CreateArgs = {
14
14
  role?: string;
15
15
  channels?: string[];
16
16
  plugins?: string[];
17
+ /** The repository's origin, taken as typed. */
18
+ gitRemote?: string;
17
19
  yes: boolean;
20
+ /** Run `bun install` after writing (`--no-install` turns it off). */
21
+ install: boolean;
22
+ /** Create the repository with a first commit (`--no-git` turns it off). */
23
+ git: boolean;
18
24
  };
19
25
  /** `create`'s options as commander hands them over. */
20
26
  export type CreateOptions = {
@@ -23,7 +29,10 @@ export type CreateOptions = {
23
29
  role?: string;
24
30
  channels?: string;
25
31
  plugins?: string;
32
+ gitRemote?: string;
26
33
  yes?: boolean;
34
+ install?: boolean;
35
+ git?: boolean;
27
36
  };
28
37
  /** The answers in `folder` and `opts`, with only what was actually given. */
29
38
  export declare function toCreateArgs(folder: string, opts: CreateOptions): CreateArgs;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * What `mfw create` does once the app's files are written: `bun install`, then
3
+ * a git repository on `main` with a first commit and, when given, its origin
4
+ * (never pushed: that stays the user's). Every step is best effort: a failure
5
+ * never undoes what came before, and the report says what happened, which
6
+ * `finishMessage` turns into the command's last words, next steps included.
7
+ */
8
+ /** Runs `argv` in `cwd`; resolves with its exit code and its output (stdout
9
+ * and stderr together), or with an empty output when `live` sends it straight
10
+ * to the terminal. A missing binary resolves with a non-zero code. */
11
+ export type Run = (argv: string[], cwd: string, opts?: {
12
+ live?: boolean;
13
+ }) => Promise<{
14
+ code: number;
15
+ output: string;
16
+ }>;
17
+ /** Why git is skipped for an app created inside another repository: on
18
+ * purpose, so there's nothing to finish by hand. */
19
+ export declare const INSIDE_A_REPOSITORY = "the folder is already inside a git repository";
20
+ /** What to do after writing: `remote` is taken as typed. */
21
+ export type FinishOptions = {
22
+ install: boolean;
23
+ git: boolean;
24
+ remote?: string;
25
+ /** The first commit's message, one `-m` per paragraph. */
26
+ commitMessage: string[];
27
+ };
28
+ /** A step that ran (`done`), wasn't asked for (`off`), was skipped with a
29
+ * reason, or failed with the command's output. */
30
+ type Outcome = "done" | "off" | {
31
+ failed: string;
32
+ };
33
+ export type FinishReport = {
34
+ install: Outcome;
35
+ git: Outcome | {
36
+ skipped: string;
37
+ };
38
+ remote: Outcome;
39
+ };
40
+ /** The real runner: a child process with its output captured. */
41
+ export declare const spawnRun: Run;
42
+ /** Runs the steps after writing the app in `dir`; see the file's comment. */
43
+ export declare function finishApp(dir: string, opts: FinishOptions, run: Run): Promise<FinishReport>;
44
+ /** The command's closing output: where the app is, what the steps after
45
+ * writing did, and what's left to run. */
46
+ export declare function finishMessage(app: {
47
+ name: string;
48
+ dir: string;
49
+ remote?: string;
50
+ report: FinishReport;
51
+ }): string;
52
+ export {};
@@ -1,4 +1,5 @@
1
1
  import { type AppDeps } from "./app/commands.ts";
2
+ import { type Run } from "./finish.ts";
2
3
  /** Runs `argv` with stdio inherited and `env` on top of this process's
3
4
  * environment; returns its exit code. */
4
5
  export type Relaunch = (argv: string[], env: Record<string, string>, opts?: {
@@ -8,9 +9,10 @@ export type Relaunch = (argv: string[], env: Record<string, string>, opts?: {
8
9
  * the exit code, printing to stdout/stderr. `bin.ts` and `create-mercury-agent`
9
10
  * both call it; the app commands look for the app from `cwd` and run docker
10
11
  * through `deps`; `relaunch` is how `create` hands over to a newer CLI. */
11
- export declare function main(argv: string[], { cwd, deps, relaunch, globalInstall, }?: {
12
+ export declare function main(argv: string[], { cwd, deps, relaunch, globalInstall, run, }?: {
12
13
  cwd?: string;
13
14
  deps?: AppDeps;
14
15
  relaunch?: Relaunch;
15
16
  globalInstall?: boolean;
17
+ run?: Run;
16
18
  }): Promise<number>;
@@ -1,11 +1,13 @@
1
1
  import type { CreateArgs } from "./args.ts";
2
- /** The answers `renderApp` needs, apart from the package versions. */
2
+ /** The answers `renderApp` needs, apart from the package versions, and the
3
+ * repository's origin (none when absent). */
3
4
  export type Answers = {
4
5
  name: string;
5
6
  assistantName: string;
6
7
  role: string;
7
8
  channels: string[];
8
9
  plugins: string[];
10
+ gitRemote?: string;
9
11
  };
10
12
  export declare const DEFAULT_ASSISTANT_NAME = "Mercury";
11
13
  export declare const DEFAULT_ROLE = "an internal assistant";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mercury-fw/cli",
3
- "version": "0.29.4",
3
+ "version": "0.30.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,13 +36,13 @@
36
36
  "comment:shape": "The Mercury CLI (`mfw`). `mfw create <dir>` writes a new Mercury app from the template. The framework packages move in lockstep with it, so a new app gets them at the CLI's own version; plugins and channels at the registry's latest. The plugin and channel packages are devDependencies only for the catalog test. `main(argv)` is exported for create-mercury-agent.",
37
37
  "dependencies": {
38
38
  "@clack/prompts": "^1.8.1",
39
- "@mercury-fw/core": "0.29.4",
39
+ "@mercury-fw/core": "0.30.0",
40
40
  "commander": "^15.0.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@mercury-fw/channel-google-chat": "0.1.3",
44
44
  "@mercury-fw/channel-http": "0.1.0",
45
- "@mercury-fw/formatter": "0.29.4",
45
+ "@mercury-fw/formatter": "0.30.0",
46
46
  "@mercury-fw/plugin-atlassian-admin": "0.1.0",
47
47
  "@mercury-fw/plugin-bitbucket": "0.1.0",
48
48
  "@mercury-fw/plugin-jira": "0.3.0",
package/src/args.ts CHANGED
@@ -15,7 +15,13 @@ export type CreateArgs = {
15
15
  role?: string;
16
16
  channels?: string[];
17
17
  plugins?: string[];
18
+ /** The repository's origin, taken as typed. */
19
+ gitRemote?: string;
18
20
  yes: boolean;
21
+ /** Run `bun install` after writing (`--no-install` turns it off). */
22
+ install: boolean;
23
+ /** Create the repository with a first commit (`--no-git` turns it off). */
24
+ git: boolean;
19
25
  };
20
26
 
21
27
  /** `create`'s options as commander hands them over. */
@@ -25,7 +31,10 @@ export type CreateOptions = {
25
31
  role?: string;
26
32
  channels?: string;
27
33
  plugins?: string;
34
+ gitRemote?: string;
28
35
  yes?: boolean;
36
+ install?: boolean;
37
+ git?: boolean;
29
38
  };
30
39
 
31
40
  /** A comma-separated list, trimmed, empty items dropped. */
@@ -37,11 +46,13 @@ const list = (value: string): string[] =>
37
46
 
38
47
  /** The answers in `folder` and `opts`, with only what was actually given. */
39
48
  export function toCreateArgs(folder: string, opts: CreateOptions): CreateArgs {
40
- const args: CreateArgs = { dir: folder, yes: opts.yes ?? false };
49
+ const args: CreateArgs = { dir: folder, yes: opts.yes ?? false, install: opts.install ?? true, git: opts.git ?? true };
41
50
  if (opts.name !== undefined) args.name = opts.name;
42
51
  if (opts.assistantName !== undefined) args.assistantName = opts.assistantName;
43
52
  if (opts.role !== undefined) args.role = opts.role;
44
53
  if (opts.channels !== undefined) args.channels = list(opts.channels);
45
54
  if (opts.plugins !== undefined) args.plugins = list(opts.plugins);
55
+ // Blank is none, as in the wizard.
56
+ if (opts.gitRemote !== undefined && opts.gitRemote.trim() !== "") args.gitRemote = opts.gitRemote.trim();
46
57
  return args;
47
58
  }
package/src/finish.ts ADDED
@@ -0,0 +1,144 @@
1
+ /**
2
+ * What `mfw create` does once the app's files are written: `bun install`, then
3
+ * a git repository on `main` with a first commit and, when given, its origin
4
+ * (never pushed: that stays the user's). Every step is best effort: a failure
5
+ * never undoes what came before, and the report says what happened, which
6
+ * `finishMessage` turns into the command's last words, next steps included.
7
+ */
8
+
9
+ /** Runs `argv` in `cwd`; resolves with its exit code and its output (stdout
10
+ * and stderr together), or with an empty output when `live` sends it straight
11
+ * to the terminal. A missing binary resolves with a non-zero code. */
12
+ export type Run = (argv: string[], cwd: string, opts?: { live?: boolean }) => Promise<{ code: number; output: string }>;
13
+
14
+ /** Why git is skipped for an app created inside another repository: on
15
+ * purpose, so there's nothing to finish by hand. */
16
+ export const INSIDE_A_REPOSITORY = "the folder is already inside a git repository";
17
+
18
+ /** What to do after writing: `remote` is taken as typed. */
19
+ export type FinishOptions = {
20
+ install: boolean;
21
+ git: boolean;
22
+ remote?: string;
23
+ /** The first commit's message, one `-m` per paragraph. */
24
+ commitMessage: string[];
25
+ };
26
+
27
+ /** A step that ran (`done`), wasn't asked for (`off`), was skipped with a
28
+ * reason, or failed with the command's output. */
29
+ type Outcome = "done" | "off" | { failed: string };
30
+
31
+ export type FinishReport = {
32
+ install: Outcome;
33
+ git: Outcome | { skipped: string };
34
+ remote: Outcome;
35
+ };
36
+
37
+ /** The real runner: a child process with its output captured. */
38
+ export const spawnRun: Run = async (argv, cwd, opts) => {
39
+ try {
40
+ if (opts?.live) {
41
+ const proc = Bun.spawn(argv, { cwd, stdin: "ignore", stdout: "inherit", stderr: "inherit" });
42
+ return { code: await proc.exited, output: "" };
43
+ }
44
+ const proc = Bun.spawn(argv, { cwd, stdin: "ignore", stdout: "pipe", stderr: "pipe" });
45
+ const [stdout, stderr, code] = await Promise.all([
46
+ new Response(proc.stdout).text(),
47
+ new Response(proc.stderr).text(),
48
+ proc.exited,
49
+ ]);
50
+ return { code, output: stdout + stderr };
51
+ } catch (err) {
52
+ return { code: 127, output: err instanceof Error ? err.message : String(err) };
53
+ }
54
+ };
55
+
56
+ /** Runs the steps after writing the app in `dir`; see the file's comment. */
57
+ export async function finishApp(dir: string, opts: FinishOptions, run: Run): Promise<FinishReport> {
58
+ const report: FinishReport = { install: "off", git: "off", remote: "off" };
59
+ if (opts.install) {
60
+ // Live: an install can take a while, and its progress is worth seeing.
61
+ const installed = await run(["bun", "install"], dir, { live: true });
62
+ report.install = installed.code === 0 ? "done" : { failed: `exit code ${installed.code}` };
63
+ }
64
+ if (!opts.git) return report;
65
+
66
+ const skipped = await whyNoGit(dir, run);
67
+ if (skipped !== undefined) {
68
+ report.git = { skipped };
69
+ return report;
70
+ }
71
+ const commit = opts.commitMessage.flatMap((paragraph) => ["-m", paragraph]);
72
+ const steps: [label: string, argv: string[]][] = [
73
+ ["git init", ["git", "init", "-q", "-b", "main"]],
74
+ ["git add -A", ["git", "add", "-A"]],
75
+ ["git commit", ["git", "commit", "-q", ...commit]],
76
+ ];
77
+ for (const [label, argv] of steps) {
78
+ const step = await run(argv, dir);
79
+ if (step.code !== 0) {
80
+ report.git = { failed: `${label}: ${step.output.trim()}` };
81
+ return report;
82
+ }
83
+ }
84
+ report.git = "done";
85
+
86
+ if (opts.remote !== undefined) {
87
+ const added = await run(["git", "remote", "add", "origin", opts.remote], dir);
88
+ report.remote = added.code === 0 ? "done" : { failed: added.output.trim() };
89
+ }
90
+ return report;
91
+ }
92
+
93
+ /** Why the repository can't be created in `dir`, or undefined when it can.
94
+ * Whether git can commit (an identity, hooks, signing) is git's to say: the
95
+ * commit runs, and its refusal is reported as it is. */
96
+ async function whyNoGit(dir: string, run: Run): Promise<string | undefined> {
97
+ if ((await run(["git", "--version"], dir)).code !== 0) return "git isn't installed";
98
+ // An app created inside a monorepo belongs to that repository, not a nested one.
99
+ if ((await run(["git", "rev-parse", "--is-inside-work-tree"], dir)).code === 0) return INSIDE_A_REPOSITORY;
100
+ return undefined;
101
+ }
102
+
103
+ /** `outcome` as a status line's value, or undefined for a step that was off. */
104
+ function statusOf(outcome: FinishReport[keyof FinishReport], done: string): string | undefined {
105
+ if (outcome === "off") return undefined;
106
+ if (outcome === "done") return done;
107
+ if ("skipped" in outcome) return `skipped, ${outcome.skipped}`;
108
+ return `failed (${outcome.failed})`;
109
+ }
110
+
111
+ /** The command's closing output: where the app is, what the steps after
112
+ * writing did, and what's left to run. */
113
+ export function finishMessage(app: { name: string; dir: string; remote?: string; report: FinishReport }): string {
114
+ const { report } = app;
115
+ const insideRepository = typeof report.git === "object" && "skipped" in report.git && report.git.skipped === INSIDE_A_REPOSITORY;
116
+ // A repository that git didn't finish (not installed, a step refused) is
117
+ // finished by hand; one skipped on purpose isn't.
118
+ const byHand = report.git !== "done" && report.git !== "off" && !insideRepository;
119
+ const status = [
120
+ ["bun install", statusOf(report.install, "done")],
121
+ ["git", statusOf(report.git, "first commit on main")],
122
+ ["origin", insideRepository && app.remote !== undefined ? "not added, the app is in that repository" : statusOf(report.remote, app.remote ?? "")],
123
+ ].flatMap(([step, value]) => (value === undefined ? [] : [` ${step}: ${value}\n`]));
124
+
125
+ const next = [`cd ${app.dir}`];
126
+ if (report.install !== "done") next.push("bun install");
127
+ if (byHand) {
128
+ next.push("git init -b main", "git add -A", 'git commit -m "Scaffold with mfw create"');
129
+ if (app.remote !== undefined) next.push(`git remote add origin ${app.remote}`);
130
+ } else if (report.git === "done") {
131
+ if (report.install !== "done") next.push('git add bun.lock && git commit -m "Add bun.lock"');
132
+ if (app.remote !== undefined && report.remote !== "done") next.push(`git remote add origin ${app.remote}`);
133
+ }
134
+ next.push("cp .env.example .env # then fill it in", "mfw start # bunx mfw start, without a global mfw");
135
+ if (report.git === "done" && report.remote === "done") next.push("git push -u origin main");
136
+
137
+ return `Created ${app.name} in ${app.dir}
138
+ ${status.join("")}
139
+ Next:
140
+ ${next.map((line) => ` ${line}\n`).join("")}
141
+ Optional, to have mfw everywhere:
142
+ bun add -g @mercury-fw/cli
143
+ `;
144
+ }
package/src/main.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The `mfw` command. `create <folder>` writes a new Mercury app from the
3
3
  * template, asking what to put in it (or taking the answers from flags with
4
- * `--yes`); `bun install` in the new app is left to the user. The other
4
+ * `--yes`), then installs it and commits it to a new repository (`finish.ts`). The other
5
5
  * commands operate an existing app from inside its folder (see
6
6
  * `app/commands.ts`). The command line itself is declared in `program.ts`.
7
7
  */
@@ -16,6 +16,7 @@ import { appVersions, cliVersion, newerCli, registryFrom } from "./versions.ts";
16
16
  import { appCommands, terminalDeps, type AppDeps } from "./app/commands.ts";
17
17
  import { findApp, type App } from "./app/find-app.ts";
18
18
  import { askAnswers, DEFAULT_ASSISTANT_NAME, DEFAULT_ROLE, type Answers } from "./wizard.ts";
19
+ import { finishApp, finishMessage, spawnRun, type Run } from "./finish.ts";
19
20
  import { targetError, writeApp } from "./write.ts";
20
21
 
21
22
  /** The answers taken from the flags alone, defaults for the rest. */
@@ -26,6 +27,7 @@ function answersFromFlags(args: CreateArgs, defaultName: string): Answers {
26
27
  role: args.role ?? DEFAULT_ROLE,
27
28
  channels: args.channels ?? [],
28
29
  plugins: args.plugins ?? [],
30
+ ...(args.gitRemote !== undefined ? { gitRemote: args.gitRemote } : {}),
29
31
  };
30
32
  }
31
33
 
@@ -90,9 +92,18 @@ async function relaunchIfStale(rawArgs: string[], relaunch: Relaunch, globalInst
90
92
  return relaunch(["bunx", cli, "create", ...rawArgs], env);
91
93
  }
92
94
 
95
+ /** What's wrong with `--git-remote`, or undefined. It's taken as typed, but
96
+ * one starting with "-" would reach git as an option. */
97
+ function remoteError(args: CreateArgs): string | undefined {
98
+ if (args.gitRemote === undefined) return undefined;
99
+ if (!args.git) return "--git-remote needs the repository: drop --no-git";
100
+ if (args.gitRemote.startsWith("-")) return `--git-remote "${args.gitRemote}" isn't a remote`;
101
+ return undefined;
102
+ }
103
+
93
104
  /** `mfw create`: returns the exit code. `rawArgs` are the arguments after
94
105
  * `create` as typed, for a relaunch. */
95
- async function create(args: CreateArgs, rawArgs: string[], relaunch: Relaunch, globalInstall: boolean): Promise<number> {
106
+ async function create(args: CreateArgs, rawArgs: string[], relaunch: Relaunch, globalInstall: boolean, run: Run): Promise<number> {
96
107
  const relaunched = await relaunchIfStale(rawArgs, relaunch, globalInstall);
97
108
  if (relaunched !== undefined) return relaunched;
98
109
  // The folder is created in kebab case, only its own name: the parent path is
@@ -105,7 +116,10 @@ async function create(args: CreateArgs, rawArgs: string[], relaunch: Relaunch, g
105
116
  const dir = join(dirname(typed), folder);
106
117
  // What the command line already settles is checked before any question, so
107
118
  // the wizard is never answered for nothing.
108
- const early = targetError(dir) ?? selectionError(args.channels ?? [], args.plugins ?? []);
119
+ const early =
120
+ targetError(dir) ??
121
+ selectionError(args.channels ?? [], args.plugins ?? []) ??
122
+ remoteError(args);
109
123
  if (early !== undefined) {
110
124
  throw new Error(early);
111
125
  }
@@ -118,16 +132,21 @@ async function create(args: CreateArgs, rawArgs: string[], relaunch: Relaunch, g
118
132
  ).map((e) => e.package);
119
133
  const versions = await appVersions(chosen, { registry: registryFrom(process.env.MFW_REGISTRY) });
120
134
  writeApp(dir, renderApp({ ...answers, versions }));
121
- console.log(`Created ${answers.name} in ${dir}
122
-
123
- Next:
124
- cd ${dir}
125
- bun install
126
- cp .env.example .env # then fill it in
127
- mfw start # bunx mfw start, without a global mfw
128
-
129
- Optional, to have mfw everywhere:
130
- bun add -g @mercury-fw/cli`);
135
+ const none = (ids: string[]) => (ids.length > 0 ? ids.join(", ") : "none");
136
+ const report = await finishApp(
137
+ dir,
138
+ {
139
+ install: args.install,
140
+ git: args.git,
141
+ ...(answers.gitRemote !== undefined ? { remote: answers.gitRemote } : {}),
142
+ commitMessage: [
143
+ `Scaffold with mfw create ${cliVersion()}`,
144
+ `Channels: ${none(answers.channels)}\nPlugins: ${none(answers.plugins)}`,
145
+ ],
146
+ },
147
+ run,
148
+ );
149
+ process.stdout.write(finishMessage({ name: answers.name, dir, remote: answers.gitRemote, report }));
131
150
  return 0;
132
151
  }
133
152
 
@@ -203,13 +222,14 @@ export async function main(
203
222
  deps,
204
223
  relaunch = spawnRelaunch,
205
224
  globalInstall = isGlobalInstall(),
206
- }: { cwd?: string; deps?: AppDeps; relaunch?: Relaunch; globalInstall?: boolean } = {},
225
+ run = spawnRun,
226
+ }: { cwd?: string; deps?: AppDeps; relaunch?: Relaunch; globalInstall?: boolean; run?: Run } = {},
207
227
  ): Promise<number> {
208
228
  const handedOver = await handOverToAppCli(argv, cwd, relaunch);
209
229
  if (handedOver !== undefined) return handedOver;
210
230
  const rawCreateArgs = argv.slice(argv.indexOf("create") + 1);
211
231
  return runProgram(argv, {
212
- create: (args) => create(args, rawCreateArgs, relaunch, globalInstall),
232
+ create: (args) => create(args, rawCreateArgs, relaunch, globalInstall, run),
213
233
  upgrade: () => upgrade(relaunch),
214
234
  app: () => appCommands(findApp(cwd), deps ?? terminalDeps()),
215
235
  });
package/src/program.ts CHANGED
@@ -50,7 +50,7 @@ function buildProgram(handlers: ProgramHandlers, result: { code: number }): Comm
50
50
  .command("create")
51
51
  .summary("writes a new Mercury app")
52
52
  .description(
53
- "Writes a new Mercury app into <folder>, which has to be missing or empty (its own name is turned into kebab case). Without options it asks for the app name, the assistant's name and role, and which channels and tool plugins to include; then it writes mercury.config.ts for that selection, the persona (persona/identity.md, persona/tone.md), the service and REPL entrypoints, a Dockerfile, a compose file with Qdrant, and an env example listing every variable the app reads. Nothing is installed: run bun install in the new app.",
53
+ "Writes a new Mercury app into <folder>, which has to be missing or empty (its own name is turned into kebab case). Without options it asks for the app name, the assistant's name and role, and which channels and tool plugins to include; then it writes mercury.config.ts for that selection, the persona (persona/identity.md, persona/tone.md), the service and REPL entrypoints, a Dockerfile, a compose file with Qdrant, and an env example listing every variable the app reads. Then it runs bun install, and creates a git repository on main with a first commit, adding the origin when given (nothing is pushed).",
54
54
  )
55
55
  .argument("<folder>", "where to write the app")
56
56
  .option("--name <name>", "app name, as in package.json (default: the folder's name)")
@@ -61,6 +61,9 @@ function buildProgram(handlers: ProgramHandlers, result: { code: number }): Comm
61
61
  `comma-separated: ${CATALOG.filter((e) => e.kind === "channel").map((e) => e.id).join(", ")}`,
62
62
  )
63
63
  .option("--plugins <ids>", `comma-separated: ${CATALOG.filter((e) => e.kind === "tool").map((e) => e.id).join(", ")}`)
64
+ .option("--git-remote <url>", "the repository's origin, taken as typed (default: none)")
65
+ .option("--no-install", "don't run bun install")
66
+ .option("--no-git", "don't create the git repository")
64
67
  .option("-y, --yes", "don't ask: use the flags and the defaults")
65
68
  .addHelpText(
66
69
  "after",
package/src/wizard.ts CHANGED
@@ -9,13 +9,15 @@ import { CATALOG, type CatalogEntry } from "./catalog.ts";
9
9
  import { appNameError } from "./render.ts";
10
10
  import type { CreateArgs } from "./args.ts";
11
11
 
12
- /** The answers `renderApp` needs, apart from the package versions. */
12
+ /** The answers `renderApp` needs, apart from the package versions, and the
13
+ * repository's origin (none when absent). */
13
14
  export type Answers = {
14
15
  name: string;
15
16
  assistantName: string;
16
17
  role: string;
17
18
  channels: string[];
18
19
  plugins: string[];
20
+ gitRemote?: string;
19
21
  };
20
22
 
21
23
  export const DEFAULT_ASSISTANT_NAME = "Mercury";
@@ -67,6 +69,16 @@ export async function askAnswers(args: CreateArgs, dir: string): Promise<Answers
67
69
  if (channels === undefined) return cancelled();
68
70
  const plugins = await pick("tool", "Tool plugins (space to select)", args.plugins ?? []);
69
71
  if (plugins === undefined) return cancelled();
72
+ // Asked only when there will be a repository and the flag didn't say.
73
+ let gitRemote = args.gitRemote;
74
+ if (args.git && gitRemote === undefined) {
75
+ const typed = await p.text({
76
+ message: "Git remote for origin (empty: none, add it later)",
77
+ validate: (v) => (v?.trim().startsWith("-") ? "That's not a remote: git would read it as an option" : undefined),
78
+ });
79
+ if (p.isCancel(typed)) return cancelled();
80
+ if (typed && typed.trim()) gitRemote = typed.trim();
81
+ }
70
82
 
71
83
  p.note(
72
84
  [
@@ -74,12 +86,13 @@ export async function askAnswers(args: CreateArgs, dir: string): Promise<Answers
74
86
  `Assistant: You are ${assistantName}, ${role}.`,
75
87
  `Channels: ${channels.length > 0 ? channels.join(", ") : "none"}`,
76
88
  `Tool plugins: ${plugins.length > 0 ? plugins.join(", ") : "none"}`,
89
+ ...(args.git ? [`Origin: ${gitRemote ?? "none"}`] : []),
77
90
  ].join("\n"),
78
91
  "Summary",
79
92
  );
80
93
  const ok = await p.confirm({ message: `Create it in ${dir}?` });
81
94
  if (p.isCancel(ok) || !ok) return cancelled();
82
- return { name, assistantName, role, channels, plugins };
95
+ return { name, assistantName, role, channels, plugins, ...(gitRemote !== undefined ? { gitRemote } : {}) };
83
96
  }
84
97
 
85
98
  function cancelled(): undefined {