@mercury-fw/cli 0.29.3 → 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 +20 -0
- package/README.md +8 -2
- package/dist/src/args.d.ts +9 -0
- package/dist/src/finish.d.ts +52 -0
- package/dist/src/main.d.ts +3 -1
- package/dist/src/wizard.d.ts +3 -1
- package/package.json +3 -3
- package/src/args.ts +12 -1
- package/src/finish.ts +144 -0
- package/src/main.ts +35 -15
- package/src/program.ts +4 -1
- package/src/wizard.ts +15 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
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
|
+
|
|
16
|
+
## 0.29.4
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- Updated dependencies [63f3da2]
|
|
21
|
+
- @mercury-fw/core@0.29.4
|
|
22
|
+
|
|
3
23
|
## 0.29.3
|
|
4
24
|
|
|
5
25
|
### 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.
|
|
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
|
package/dist/src/args.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/src/main.d.ts
CHANGED
|
@@ -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>;
|
package/dist/src/wizard.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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`)
|
|
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 =
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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.
|
|
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 {
|