@mercury-fw/cli 0.29.4 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # @mercury-fw/cli
2
2
 
3
+ ## 0.31.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6ae8133: - `mfw local-packages <folder>` makes an app install `@mercury-fw/*` packages from local tarballs (`bun pm pack`) instead of the registry, transitive dependencies included; `--off` goes back to the registry.
8
+ - `mfw e2e` runs end-to-end tests against an app's real model through its REPL: cases of turns with checks on the tool calls and the answer, repeated runs, results kept in `e2e/results/`.
9
+ - Test files declare their cases with `e2e()` from `@mercury-fw/cli/e2e`.
10
+ - A new app's Dockerfile copies `.packs/` when present, and its `.gitignore` leaves out `.packs/` and `e2e/results/`.
11
+ - In the REPL, `/dump` after a confirmation no longer writes the turn before it.
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies [6ae8133]
16
+ - @mercury-fw/core@0.31.0
17
+
18
+ ## 0.30.0
19
+
20
+ ### Minor Changes
21
+
22
+ - 238d4dc: - `mfw create` runs `bun install` in the new app, showing its output as it goes (`--no-install` to skip it).
23
+ - `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.
24
+ - `mfw create` adds `origin` when given one, in the wizard or with `--git-remote`; nothing is pushed.
25
+ - The message at the end of `mfw create` lists what each step did and only the steps left to run.
26
+
27
+ ### Patch Changes
28
+
29
+ - @mercury-fw/core@0.30.0
30
+
3
31
  ## 0.29.4
4
32
 
5
33
  ### Patch Changes
package/README.md CHANGED
@@ -20,6 +20,8 @@
20
20
  - [`mfw credentials set <plugin> [--from <dir>] [--print]`](#mfw-credentials-set-plugin---from-dir---print)
21
21
  - [`mfw credentials reset <plugin>`](#mfw-credentials-reset-plugin)
22
22
  - [`mfw google-chat set-key <key-file> [--subscription <name>]`](#mfw-google-chat-set-key-key-file---subscription-name)
23
+ - [`mfw local-packages <folder>` / `mfw local-packages --off`](#mfw-local-packages-folder--mfw-local-packages---off)
24
+ - [`mfw e2e [tests...] [--repeat N]`](#mfw-e2e-tests---repeat-n)
23
25
  - [`mfw upgrade`](#mfw-upgrade)
24
26
  - [Help](#help)
25
27
 
@@ -44,9 +46,12 @@ Writes a new Mercury app into `<folder>`, which has to be missing or empty; the
44
46
 
45
47
  - the app name (the `name` in `package.json`, defaulting to the folder's);
46
48
  - 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).
49
+ - which channels and which tool plugins to include (none is fine: the REPL always works);
50
+ - the git remote for `origin`, which you can leave empty (most of the time it doesn't exist yet when you scaffold).
48
51
 
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.
52
+ 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.
53
+
54
+ 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
55
 
51
56
  | Flag | |
52
57
  |---|---|
@@ -55,6 +60,9 @@ It writes the `mercury.config.ts` for that selection, the persona, the service a
55
60
  | `--role <text>` | Completes "You are <name>, …" (default `an internal assistant`). |
56
61
  | `--channels <ids>` | Comma-separated: `google-chat`, `http`. |
57
62
  | `--plugins <ids>` | Comma-separated: `jira`, `bitbucket`, `atlassian-admin`. |
63
+ | `--git-remote <url>` | The repository's `origin`, taken as typed. |
64
+ | `--no-install` | Don't run `bun install`. |
65
+ | `--no-git` | Don't create the repository. |
58
66
  | `-y`, `--yes` | No questions: the flags, and the defaults for the rest. |
59
67
 
60
68
  ```bash
@@ -206,6 +214,88 @@ mfw google-chat set-key key.json --subscription projects/my-project/subscription
206
214
  mfw google-chat set-key new-key.json
207
215
  ```
208
216
 
217
+ ### `mfw local-packages <folder>` / `mfw local-packages --off`
218
+
219
+ Makes the app install `@mercury-fw/*` packages from local tarballs instead of the registry: the way to try a framework or plugin change in a real app before it's published. `<folder>` holds the `.tgz` files `bun pm pack` writes, one per package; the command copies them into the app's `.packs/`, points each package at its tarball with `overrides` in `package.json`, and runs `bun install`.
220
+
221
+ Overrides, and not the dependencies themselves, because a packed package names the packages it depends on by version, and the registry has those versions too: only an override sends them to the tarballs as well (a packed `@mercury-fw/core` depends on `@mercury-fw/plugin-types`, for example). The Dockerfile `mfw create` writes copies `.packs/` before installing, so the image gets the same packages (an app created before 0.31.0 needs `COPY --chown=mercury:mercury .pack[s] ./.packs/` added after the line copying `package.json`); `.gitignore` leaves it out of the repository.
222
+
223
+ Run it again after packing anew: it replaces the tarballs and the overrides of the previous run, and leaves alone the overrides you wrote yourself. `--off` takes the app back to the registry: the overrides it wrote and `.packs/` go, then `bun install`. After either, `mfw start` rebuilds the image with the packages now installed.
224
+
225
+ ```bash
226
+ mfw local-packages ../mercury-fw/apps/testbed/.packs
227
+ mfw local-packages --off
228
+ ```
229
+
230
+ ### `mfw e2e [tests...] [--repeat N]`
231
+
232
+ Runs end-to-end tests against the app's real model: each test case sends its turns to the app's REPL in the container, as `mfw repl` would, and checks what each turn did, the tool calls with their inputs and results, and the answer. It's how you find out whether the model actually uses a plugin the way its skill says, at the first try, with the app's own model, configuration, wiki and credentials. That's also why it doesn't belong in CI: the results depend on the model, on what's in the vault and in Qdrant, and on accounts a CI runner shouldn't have.
233
+
234
+ Without arguments it runs every `e2e/*.e2e.ts` in the app; with files, those (they can live anywhere). It prints every check of every run, keeps everything (each turn's calls and answer, each check) in `e2e/results/<time>/`, which `.gitignore` leaves out, and exits 1 when a case didn't pass. The app must be built and its `.env` filled in, as for `mfw repl`. `--repeat N` runs each case N times, overriding its own `repeat`.
235
+
236
+ #### Writing a test
237
+
238
+ A test is a TypeScript file whose default export is `e2e({ … })`, from `@mercury-fw/cli/e2e` (every app has the CLI among its devDependencies, so the types are there):
239
+
240
+ ```ts
241
+ import { e2e } from "@mercury-fw/cli/e2e";
242
+
243
+ export default e2e({
244
+ // What the app must have, by catalog id: checked before anything runs.
245
+ plugins: ["jira"],
246
+ channels: [],
247
+ cases: [
248
+ {
249
+ name: "project key, at the first try",
250
+ turns: ["On Jira, what is the project key of Customer Support?"],
251
+ repeat: 3, // the model isn't deterministic
252
+ minPasses: 3, // how many runs must pass; default every one
253
+ check: (run, expect) => {
254
+ expect.everyCall("jiraCommand", (c) => String((c.input as { command: string }).command).includes("--select "));
255
+ expect.noFailedCalls();
256
+ expect.callCount({ max: 3 });
257
+ expect.answer(/\bCS\b/);
258
+ },
259
+ },
260
+ ],
261
+ });
262
+ ```
263
+
264
+ Each run of a case gets a fresh REPL session. `check` receives the run, `run.turns` in order and `run.last`, each turn with:
265
+
266
+ - `calls`: the tool calls, each with `tool`, `input`, `output`, `ok` (false for a failed call, or one that never got a result) and `pending` (an irreversible command staged for confirmation: it worked, and its output holds the token);
267
+ - `answer`: the turn's final text (what the model wrote; a list `present` adds afterwards isn't part of it);
268
+ - `seconds`: how long it took, for the report.
269
+
270
+ The `expect` helpers record a named check each and never stop the others, so a failed run shows every check that failed:
271
+
272
+ | Helper | Passes when |
273
+ |---|---|
274
+ | `call(tool, match?)` | at least one call to `tool` (matching `match`, when given) |
275
+ | `everyCall(tool, match)` | there are calls to `tool`, and every one matches |
276
+ | `noFailedCalls()` | no call failed |
277
+ | `callCount({ min?, max? }, tool?)` | the number of calls, to any tool or to `tool`, is within the bounds |
278
+ | `answer(text \| regex)` | the last answer contains the text, or matches |
279
+ | `answerNot(text \| regex)` | it doesn't |
280
+ | `that(label, condition)` | `condition` is true |
281
+
282
+ The call helpers look at every turn of the run, the answer helpers at the last one; each takes an optional label as its last argument. A case that records no check fails: it proves nothing.
283
+
284
+ A turn can be a function of the one before, for a follow-up or a confirmation: `(previous) => …` returns the next message from `previous.calls` and `previous.answer` (an irreversible command comes back as a call whose output holds the pending confirmation's token, and sending the token as the next turn confirms it). Each turn is one line, as the REPL reads them.
285
+
286
+ `before`, `after` and `check` also get `cli(command)`, which runs `sh -c command` in the app's container, outside the model, and resolves to its exit code and output: to prepare data (an issue to work on, a note in the wiki with the vault CLI), to check what a turn changed (`jira issue get KEY --select fields.assignee.displayName` after an assignment), to clean up. `after` runs even when the run or the check failed. A test that changes an external system has to clean up after itself, so start from read-only ones.
287
+
288
+ #### What it can and can't test
289
+
290
+ It can test how the model uses a plugin through its skill (the commands and flags it picks, rejected or failed calls, how many calls it takes), what the answer says or must not say, what a turn changed in an external system, the confirmation of an irreversible command, conversations over several turns, several plugins working together, and what the wiki and memory make of a conversation.
291
+
292
+ It can't test how a channel shows a turn (Google Chat's cards, the HTTP channel's events: the REPL runs the same turn, without them), the background jobs (the nightly wiki review, idle-session capture), or the exact wording of an answer: checks look for properties, and `repeat` with `minPasses` says how steady the behaviour is. Durations are in the report and in `e2e/results/`, never a check.
293
+
294
+ ```bash
295
+ mfw e2e
296
+ mfw e2e e2e/jira.e2e.ts --repeat 3
297
+ ```
298
+
209
299
  ### `mfw upgrade`
210
300
 
211
301
  Installs the registry's latest `mfw` globally (`bun add -g @mercury-fw/cli@<latest>`) when it's newer than the one running, and says it's already the latest otherwise; a registry that doesn't answer exits 1. It's about the global `mfw` only: an app's framework, its own CLI included, moves with `bun update` in the app. `mfw create` run by a stale global `mfw` creates the app with the latest one anyway, and says to run this.
@@ -74,6 +74,19 @@ export declare function appCommands(app: App, deps: AppDeps): {
74
74
  googleChatSetKey: (keyFile: string, { subscription }: {
75
75
  subscription?: string;
76
76
  }) => Promise<number>;
77
+ /** Makes the app install the packages in `from`'s tarballs (`bun pm
78
+ * pack`) instead of the registry's: copied into `.packs/` (which the
79
+ * image copies too), overridden in the manifest, then installed.
80
+ * Everything is checked before anything is written. */
81
+ localPackages: (from: string) => Promise<number>;
82
+ /** Runs e2e tests (`tests`, or the app's `e2e/*.e2e.ts`) against the app's
83
+ * REPL in its container, keeping the turns and checks in
84
+ * `e2e/results/<time>/`; returns 1 when a case didn't pass. */
85
+ e2e: (tests: string[], { repeat }: {
86
+ repeat?: number;
87
+ }) => Promise<number>;
88
+ /** Undoes `localPackages`: the app installs from the registry again. */
89
+ localPackagesOff: () => Promise<number>;
77
90
  };
78
91
  /** The real deps: docker on the user's terminal, questions on `input`
79
92
  * (stdin by default). */
@@ -0,0 +1,26 @@
1
+ /**
2
+ * What `mfw local-packages` writes: the app's `package.json` with `overrides`
3
+ * pointing packages at local tarballs (`bun pm pack`) copied into `.packs/`,
4
+ * which the image copies before `bun install`. Overrides, and not the
5
+ * dependencies themselves, because a packed package names its siblings by
6
+ * version, which the registry has too: only an override sends those
7
+ * transitive dependencies to the tarballs as well.
8
+ */
9
+ /** The tarballs' folder inside the app, as the Dockerfile copies it. */
10
+ export declare const LOCAL_PACKS_DIR = ".packs";
11
+ type Manifest = Record<string, unknown> & {
12
+ overrides?: Record<string, string>;
13
+ };
14
+ /** `pkg` with each of `packs` (a package name and its tarball's file name in
15
+ * `.packs/`) as an override, in place of any left by an earlier run. */
16
+ export declare function withLocalOverrides(pkg: Manifest, packs: Array<{
17
+ name: string;
18
+ file: string;
19
+ }>): Manifest;
20
+ /** `pkg` without the overrides this command writes; without `overrides` at
21
+ * all when none of the user's own is left. */
22
+ export declare function withoutLocalOverrides(pkg: Manifest): Manifest;
23
+ /** The package name in a `bun pm pack` tarball, read from its
24
+ * `package/package.json`. */
25
+ export declare function packageNameOf(tarball: string): Promise<string>;
26
+ export {};
@@ -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,73 @@
1
+ /**
2
+ * The shape of an e2e test, as a test file imports it
3
+ * (`import { e2e } from "@mercury-fw/cli/e2e"`): the plugins and channels the
4
+ * app must have, and cases of turns sent to the app's real model through its
5
+ * REPL, each with checks on the calls it made and the answer it gave.
6
+ * `mfw e2e` runs them (`runner.ts`).
7
+ */
8
+ import type { Call, TurnData } from "./dump.ts";
9
+ export type { Call } from "./dump.ts";
10
+ /** One turn as a check sees it: its calls, its answer, how long it took. */
11
+ export type Turn = TurnData & {
12
+ seconds: number;
13
+ };
14
+ /** A case's run: every turn in order, and the last one. */
15
+ export type Run = {
16
+ turns: Turn[];
17
+ last: Turn;
18
+ };
19
+ /** Runs `command` in the app's container (`sh -c`), outside the model:
20
+ * preparing data, reading what a turn changed, cleaning up. */
21
+ export type Cli = (command: string) => Promise<{
22
+ code: number;
23
+ output: string;
24
+ }>;
25
+ /** What `before`, `after` and `check` can reach besides the run. */
26
+ export type Context = {
27
+ cli: Cli;
28
+ };
29
+ /** The checks a case makes. Call helpers look at every turn's calls, answer
30
+ * helpers at the last answer; each records a named check and never throws,
31
+ * so a case reports every failure at once. */
32
+ export type Expect = {
33
+ /** At least one call to `tool`, matching `match` when given. */
34
+ call(tool: string, match?: (call: Call) => boolean, label?: string): void;
35
+ /** Every call to `tool` matches `match` (and there is at least one). */
36
+ everyCall(tool: string, match: (call: Call) => boolean, label?: string): void;
37
+ /** No call failed or went without a result. */
38
+ noFailedCalls(label?: string): void;
39
+ /** The number of calls, to every tool or to `tool`, within the bounds. */
40
+ callCount(bounds: {
41
+ min?: number;
42
+ max?: number;
43
+ }, tool?: string, label?: string): void;
44
+ /** The answer contains `pattern`, or matches it. */
45
+ answer(pattern: string | RegExp, label?: string): void;
46
+ /** The answer doesn't contain `pattern`, or doesn't match it. */
47
+ answerNot(pattern: string | RegExp, label?: string): void;
48
+ /** Anything else. */
49
+ that(label: string, condition: boolean): void;
50
+ };
51
+ /** One case: the turns sent, in one REPL session, and the checks on them. */
52
+ export type E2eCase = {
53
+ name: string;
54
+ /** A message, or a function of the turn before (a follow-up, a confirmation token). */
55
+ turns: Array<string | ((previous: Turn) => string)>;
56
+ /** How many times to run it (the model isn't deterministic); default 1. */
57
+ repeat?: number;
58
+ /** How many runs must pass; default every one. */
59
+ minPasses?: number;
60
+ before?: (ctx: Context) => unknown;
61
+ after?: (ctx: Context) => unknown;
62
+ check: (run: Run, expect: Expect, ctx: Context) => unknown;
63
+ };
64
+ /** An e2e test: what the app must have, and its cases. */
65
+ export type E2eTest = {
66
+ /** Catalog ids of the tool plugins the app must have (`jira`, …). */
67
+ plugins?: string[];
68
+ /** Catalog ids of the channels the app must have (`http`, …). */
69
+ channels?: string[];
70
+ cases: E2eCase[];
71
+ };
72
+ /** Declares an e2e test: the identity, there for the types. */
73
+ export declare function e2e(test: E2eTest): E2eTest;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * A turn read out of the file the REPL's `/dump` writes: the AI SDK's step
3
+ * results for the last turn, whose `content` parts are the tool calls, their
4
+ * results (or errors) and the model's text. Read as data, not through the
5
+ * SDK's types: only the parts listed here matter.
6
+ */
7
+ /** One tool call as a check sees it. `ok` is false when the call failed or
8
+ * never got a result; a result without an `ok` of its own counts as worked.
9
+ * `pending` is an irreversible command staged for confirmation, which worked
10
+ * (its output has the token) though it reports `ok: false`. */
11
+ export type Call = {
12
+ tool: string;
13
+ input: unknown;
14
+ output: unknown;
15
+ ok: boolean;
16
+ pending: boolean;
17
+ };
18
+ /** What a turn did: its tool calls in order, and its final text. */
19
+ export type TurnData = {
20
+ calls: Call[];
21
+ answer: string;
22
+ };
23
+ /** The calls and the answer in `dump`, the parsed content of a `/dump` file. */
24
+ export declare function turnFromDump(dump: unknown): TurnData;
@@ -0,0 +1,12 @@
1
+ import type { Expect, Run } from "./define.ts";
2
+ /** One check's outcome; `detail` says what was found when it failed. */
3
+ export type Check = {
4
+ label: string;
5
+ ok: boolean;
6
+ detail?: string;
7
+ };
8
+ /** An `expect` bound to `run`, and the checks it recorded so far. */
9
+ export declare function createExpect(run: Run): {
10
+ expect: Expect;
11
+ checks: () => Check[];
12
+ };
@@ -0,0 +1,9 @@
1
+ import type { E2eTest } from "./define.ts";
2
+ /** The files to run: `named` resolved from `cwd`, or, when none is named,
3
+ * the app's `e2e/*.e2e.ts` in name order. */
4
+ export declare function findTests(named: string[], { appDir, cwd }: {
5
+ appDir: string;
6
+ cwd: string;
7
+ }): string[];
8
+ /** The test `file` exports as default; throws naming the file when it isn't one. */
9
+ export declare function loadTest(file: string): Promise<E2eTest>;
@@ -0,0 +1,42 @@
1
+ import type { Context, E2eTest, Turn } from "./define.ts";
2
+ import { type Check } from "./expect.ts";
3
+ /** A REPL session in the app: `turn` sends one line and resolves with what
4
+ * `/dump` wrote for it and what the REPL printed meanwhile. */
5
+ export type Session = {
6
+ turn: (line: string) => Promise<{
7
+ dump: unknown;
8
+ output: string;
9
+ }>;
10
+ close: () => Promise<void>;
11
+ };
12
+ export type RunnerDeps = {
13
+ openSession: () => Promise<Session>;
14
+ cli: Context["cli"];
15
+ /** The app's dependencies, to check the test's plugins and channels against. */
16
+ appPackages: Record<string, string>;
17
+ print: (line: string) => void;
18
+ /** Milliseconds, for each turn's duration. */
19
+ now: () => number;
20
+ writeReport: (report: CaseReport[]) => Promise<void>;
21
+ };
22
+ /** One run of a case, as the report keeps it. */
23
+ export type RunReport = {
24
+ ok: boolean;
25
+ turns: Turn[];
26
+ checks: Check[];
27
+ };
28
+ export type CaseReport = {
29
+ file: string;
30
+ case: string;
31
+ passed: boolean;
32
+ runs: RunReport[];
33
+ };
34
+ /** Runs the tests in `tests` (each with the file it came from); returns the
35
+ * exit code: 0 when every case passed enough runs. `repeat` overrides each
36
+ * case's own. */
37
+ export declare function runE2e(tests: Array<{
38
+ file: string;
39
+ test: E2eTest;
40
+ }>, opts: {
41
+ repeat?: number;
42
+ }, deps: RunnerDeps): Promise<number>;
@@ -0,0 +1,17 @@
1
+ import type { Session } from "./runner.ts";
2
+ export type ReplSessionOptions = {
3
+ /** The command that starts the REPL. */
4
+ argv: string[];
5
+ cwd: string;
6
+ /** Where the dumps are, on this side, and as the REPL sees the same folder. */
7
+ hostDir: string;
8
+ replDir: string;
9
+ /** Prefix of this session's dump files, unique among sessions sharing the folder. */
10
+ name: string;
11
+ /** How long one turn may take. */
12
+ timeoutMs: number;
13
+ };
14
+ /** Starts the REPL and returns the session over it once the REPL is ready
15
+ * (its first prompt is out), so a turn's time is only the turn's; throws
16
+ * when the REPL exits or doesn't get there in time. */
17
+ export declare function openReplSession(opts: ReplSessionOptions): Promise<Session>;
@@ -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.31.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -27,6 +27,11 @@
27
27
  "mercury-fw-source": "./src/main.ts",
28
28
  "types": "./dist/src/main.d.ts",
29
29
  "default": "./src/main.ts"
30
+ },
31
+ "./e2e": {
32
+ "mercury-fw-source": "./src/e2e/define.ts",
33
+ "types": "./dist/src/e2e/define.d.ts",
34
+ "default": "./src/e2e/define.ts"
30
35
  }
31
36
  },
32
37
  "scripts": {
@@ -36,16 +41,16 @@
36
41
  "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
42
  "dependencies": {
38
43
  "@clack/prompts": "^1.8.1",
39
- "@mercury-fw/core": "0.29.4",
44
+ "@mercury-fw/core": "0.31.0",
40
45
  "commander": "^15.0.0"
41
46
  },
42
47
  "devDependencies": {
43
48
  "@mercury-fw/channel-google-chat": "0.1.3",
44
49
  "@mercury-fw/channel-http": "0.1.0",
45
- "@mercury-fw/formatter": "0.29.4",
50
+ "@mercury-fw/formatter": "0.31.0",
46
51
  "@mercury-fw/plugin-atlassian-admin": "0.1.0",
47
52
  "@mercury-fw/plugin-bitbucket": "0.1.0",
48
- "@mercury-fw/plugin-jira": "0.3.0",
53
+ "@mercury-fw/plugin-jira": "0.4.1",
49
54
  "@mercury-fw/typescript-config": "*",
50
55
  "@types/bun": "^1.4.2",
51
56
  "typescript": "^6.0.3"