mergestorm 0.3.13 → 0.3.15
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/README.md +14 -15
- package/dist/api.d.ts +82 -5
- package/dist/api.js +166 -7
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +2 -2
- package/dist/commands/branches.d.ts +6 -2
- package/dist/commands/branches.js +29 -14
- package/dist/commands/browse.d.ts +39 -0
- package/dist/commands/browse.js +259 -0
- package/dist/commands/credits.js +7 -10
- package/dist/commands/help.d.ts +5 -0
- package/dist/commands/help.js +93 -0
- package/dist/commands/jobs.d.ts +11 -2
- package/dist/commands/jobs.js +64 -15
- package/dist/commands/login.js +4 -3
- package/dist/commands/logout.js +2 -1
- package/dist/commands/pr.d.ts +22 -0
- package/dist/commands/pr.js +127 -0
- package/dist/commands/queue.d.ts +12 -0
- package/dist/commands/queue.js +160 -0
- package/dist/commands/registry.js +93 -18
- package/dist/commands/review-client.d.ts +13 -0
- package/dist/commands/review-client.js +110 -0
- package/dist/commands/review.d.ts +6 -0
- package/dist/commands/review.js +18 -7
- package/dist/commands/settings.d.ts +17 -0
- package/dist/commands/settings.js +89 -0
- package/dist/commands/skill.d.ts +20 -0
- package/dist/commands/skill.js +145 -0
- package/dist/commands/stack.d.ts +2 -0
- package/dist/commands/stack.js +91 -57
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +30 -15
- package/dist/commands/whoami.d.ts +3 -1
- package/dist/commands/whoami.js +21 -7
- package/dist/errors.d.ts +1 -1
- package/dist/shell.js +39 -31
- package/dist/stack-dto.d.ts +82 -1
- package/dist/ui/ansi.d.ts +2 -0
- package/dist/ui/ansi.js +2 -0
- package/dist/ui/banner.d.ts +34 -2
- package/dist/ui/banner.js +163 -79
- package/dist/ui/findings.d.ts +2 -1
- package/dist/ui/findings.js +13 -7
- package/dist/ui/line-tabs.d.ts +27 -0
- package/dist/ui/line-tabs.js +170 -0
- package/dist/ui/logo.d.ts +10 -2
- package/dist/ui/logo.js +13 -9
- package/dist/ui/present.d.ts +10 -0
- package/dist/ui/present.js +13 -0
- package/dist/ui/prompt.d.ts +58 -1
- package/dist/ui/prompt.js +225 -42
- package/dist/ui/select.js +1 -1
- package/dist/ui/tabs.d.ts +125 -0
- package/dist/ui/tabs.js +480 -0
- package/dist/ui/usage.d.ts +4 -0
- package/dist/ui/usage.js +22 -2
- package/package.json +2 -1
- package/skill/SKILL.md +98 -0
- package/skill/mergestorm-pr-loop/SKILL.md +76 -0
package/dist/commands/review.js
CHANGED
|
@@ -3,7 +3,8 @@ import { loadConfig } from "../config.js";
|
|
|
3
3
|
import { CommandError, DetachedError, REVIEW_EXIT, isCommandErrorCode, rateLimitedMessage, } from "../errors.js";
|
|
4
4
|
import { discoverTrunk } from "../git-stack.js";
|
|
5
5
|
import { ansi } from "../ui/ansi.js";
|
|
6
|
-
import {
|
|
6
|
+
import { formatFindingsLines } from "../ui/findings.js";
|
|
7
|
+
import { present } from "../ui/present.js";
|
|
7
8
|
import { toReviewJobEnvelope, } from "../ui/job-envelope.js";
|
|
8
9
|
import { MAX_IDEMPOTENCY_KEY_CHARS, REVIEW_POLL_DEFAULT_TIMEOUT_MS, REVIEW_POLL_MAX_TRANSIENT_RETRIES, REVIEW_THREAD_SLUG_RE, ReviewPollTimeoutError, collectReviewInput, isTransientReviewPollError, isTransientReviewPollStatus, loadReviewContext, pollReview, submitReview, } from "./review-client.js";
|
|
9
10
|
export { REVIEW_POLL_MAX_TRANSIENT_RETRIES, isTransientReviewPollError, isTransientReviewPollStatus, };
|
|
@@ -11,6 +12,12 @@ export { REVIEW_POLL_MAX_TRANSIENT_RETRIES, isTransientReviewPollError, isTransi
|
|
|
11
12
|
export const VORTEX_ROUTER_MODES = ["off", "standard", "max", "manual"];
|
|
12
13
|
/** Standard/Max fleet caps; Manual is capped at Max. */
|
|
13
14
|
export const VORTEX_ROUTER_CAPS = { standard: 3, max: 5 };
|
|
15
|
+
/**
|
|
16
|
+
* Pinnable L0 lanes only. Keep this list byte-identical to
|
|
17
|
+
* `pinnableVortexSpecialistIds(VORTEX_SPECIALIST_IDS)` in review-findings
|
|
18
|
+
* (`cli-lockstep.test.ts`). Do not add `governance` (round-gated) or `seam`
|
|
19
|
+
* (phase-gated). Those L1 lanes are not local pins.
|
|
20
|
+
*/
|
|
14
21
|
export const VORTEX_SPECIALIST_IDS = [
|
|
15
22
|
"security",
|
|
16
23
|
"performance",
|
|
@@ -394,14 +401,17 @@ export async function cmdReview(args, opts = {}) {
|
|
|
394
401
|
printJson(row, { jobId });
|
|
395
402
|
return;
|
|
396
403
|
}
|
|
397
|
-
console.log(`\nVerdict: ${ansi.bold(row.verdict ?? "comment")}`);
|
|
398
404
|
const ran = row.specialists_run ?? row.findings?.specialists_run ?? [];
|
|
405
|
+
const result = [
|
|
406
|
+
`Verdict: ${ansi.bold(row.verdict ?? "comment")}`,
|
|
407
|
+
];
|
|
399
408
|
if (ran.length > 0)
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
409
|
+
result.push(ansi.dim(`Specialists: ${ran.join(", ")}`));
|
|
410
|
+
if (row.summary)
|
|
411
|
+
result.push(row.summary);
|
|
412
|
+
result.push(...formatFindingsLines(row.findings, {
|
|
403
413
|
emptyMessage: true,
|
|
404
|
-
});
|
|
414
|
+
}));
|
|
405
415
|
if (interactive && meBefore) {
|
|
406
416
|
const meAfter = await getMe(cfg);
|
|
407
417
|
if (meAfter) {
|
|
@@ -409,10 +419,11 @@ export async function cmdReview(args, opts = {}) {
|
|
|
409
419
|
const afterLeft = meAfter.usage.standard.remaining ?? 0;
|
|
410
420
|
const used = Math.max(0, beforeLeft - afterLeft);
|
|
411
421
|
if (used > 0) {
|
|
412
|
-
|
|
422
|
+
result.push(ansi.dim(`${used} credit${used === 1 ? "" : "s"} used · ${afterLeft} remaining`));
|
|
413
423
|
}
|
|
414
424
|
}
|
|
415
425
|
}
|
|
426
|
+
await present("Review", result);
|
|
416
427
|
}
|
|
417
428
|
catch (err) {
|
|
418
429
|
if (err instanceof DetachedError)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type SettingsPatch, type SettingsResponse, type SettingsWritableKey } from "../api.js";
|
|
2
|
+
/**
|
|
3
|
+
* CLI flag → Bearer settings key. on|off only. The connected flags are
|
|
4
|
+
* read-only on the API and deliberately have no flag here.
|
|
5
|
+
*/
|
|
6
|
+
export declare const SETTINGS_FLAGS: Record<string, SettingsWritableKey>;
|
|
7
|
+
export type SettingsArgs = {
|
|
8
|
+
json: boolean;
|
|
9
|
+
patch: SettingsPatch;
|
|
10
|
+
};
|
|
11
|
+
/** Parse `settings` argv. Accepts `--flag on|off` and `--flag=on|off`. */
|
|
12
|
+
export declare function parseSettingsArgs(args: string[]): SettingsArgs;
|
|
13
|
+
/** Static printout of the GET/PATCH response shape (rows match the Config tab). */
|
|
14
|
+
export declare function formatSettingsLines(settings: SettingsResponse): string[];
|
|
15
|
+
export declare function cmdSettings(args: string[], opts?: {
|
|
16
|
+
mode?: "oneshot" | "shell";
|
|
17
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { getSettings, patchSettings, } from "../api.js";
|
|
2
|
+
import { loadConfig } from "../config.js";
|
|
3
|
+
import { CommandError } from "../errors.js";
|
|
4
|
+
import { present } from "../ui/present.js";
|
|
5
|
+
import { buildConfigRows, canBrowse, openTabsBrowser } from "./browse.js";
|
|
6
|
+
/**
|
|
7
|
+
* CLI flag → Bearer settings key. on|off only. The connected flags are
|
|
8
|
+
* read-only on the API and deliberately have no flag here.
|
|
9
|
+
*/
|
|
10
|
+
export const SETTINGS_FLAGS = {
|
|
11
|
+
"--auto-review": "auto_review_enabled",
|
|
12
|
+
"--auto-patch": "auto_patch_enabled",
|
|
13
|
+
"--vortex-thinking": "vortex_show_thinking_traces",
|
|
14
|
+
"--repo-overview": "repo_overview_enabled",
|
|
15
|
+
"--review-unit-land": "review_unit_land_prs_enabled",
|
|
16
|
+
"--cyclone-review-unit-land": "cyclone_review_unit_land_prs_enabled",
|
|
17
|
+
"--vortex-seam": "vortex_seam_specialist_enabled",
|
|
18
|
+
};
|
|
19
|
+
/** Parse `settings` argv. Accepts `--flag on|off` and `--flag=on|off`. */
|
|
20
|
+
export function parseSettingsArgs(args) {
|
|
21
|
+
let json = false;
|
|
22
|
+
const patch = {};
|
|
23
|
+
for (let i = 0; i < args.length; i++) {
|
|
24
|
+
const arg = args[i];
|
|
25
|
+
if (arg === "--json") {
|
|
26
|
+
json = true;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const eq = arg.indexOf("=");
|
|
30
|
+
const flag = eq >= 0 ? arg.slice(0, eq) : arg;
|
|
31
|
+
const key = SETTINGS_FLAGS[flag];
|
|
32
|
+
if (!key) {
|
|
33
|
+
throw new CommandError(`Unknown settings flag: ${flag}. See \`mergestorm --help\`.`, 2, "usage");
|
|
34
|
+
}
|
|
35
|
+
const value = eq >= 0 ? arg.slice(eq + 1) : args[++i];
|
|
36
|
+
if (value !== "on" && value !== "off") {
|
|
37
|
+
throw new CommandError(`${flag} takes on or off.`, 2, "usage");
|
|
38
|
+
}
|
|
39
|
+
patch[key] = value === "on";
|
|
40
|
+
}
|
|
41
|
+
return { json, patch };
|
|
42
|
+
}
|
|
43
|
+
/** Static printout of the GET/PATCH response shape (rows match the Config tab). */
|
|
44
|
+
export function formatSettingsLines(settings) {
|
|
45
|
+
const rows = buildConfigRows(settings);
|
|
46
|
+
const labelWidth = Math.max(...rows.map((r) => r.label.length));
|
|
47
|
+
return rows.map((row) => {
|
|
48
|
+
const value = row.writable
|
|
49
|
+
? row.value
|
|
50
|
+
? "on"
|
|
51
|
+
: "off"
|
|
52
|
+
: row.value
|
|
53
|
+
? "connected"
|
|
54
|
+
: "not connected";
|
|
55
|
+
return ` ${row.label.padEnd(labelWidth + 2)}${value}`;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export async function cmdSettings(args, opts = {}) {
|
|
59
|
+
const { json, patch } = parseSettingsArgs(args);
|
|
60
|
+
const hasPatch = Object.keys(patch).length > 0;
|
|
61
|
+
// Bare `settings` on a TTY: the tabbed browser, opened on Config.
|
|
62
|
+
if (!hasPatch && !json && canBrowse()) {
|
|
63
|
+
await openTabsBrowser("config");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const cfg = await loadConfig();
|
|
67
|
+
let settings;
|
|
68
|
+
if (hasPatch) {
|
|
69
|
+
settings = await patchSettings(patch, cfg);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
const got = await getSettings(cfg);
|
|
73
|
+
if (!got) {
|
|
74
|
+
throw new CommandError("Settings are not available (offline or API too old). " +
|
|
75
|
+
"Try again or open https://mergestorm.ai/settings");
|
|
76
|
+
}
|
|
77
|
+
settings = got;
|
|
78
|
+
}
|
|
79
|
+
if (json) {
|
|
80
|
+
const text = JSON.stringify(settings, null, 2);
|
|
81
|
+
if (opts.mode === "shell") {
|
|
82
|
+
await present("Settings", text.split("\n"));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
console.log(text);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
await present("Settings", formatSettingsLines(settings));
|
|
89
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const SKILL_NAME = "mergestorm-review";
|
|
2
|
+
export declare const PR_LOOP_SKILL_NAME = "mergestorm-pr-loop";
|
|
3
|
+
export declare const SKILL_NAMES: readonly ["mergestorm-review", "mergestorm-pr-loop"];
|
|
4
|
+
export type SkillName = (typeof SKILL_NAMES)[number];
|
|
5
|
+
export declare const SKILL_USAGE = "usage:\n mergestorm skill install --claude|--cursor [--json]\n\n Copies mergestorm-review and mergestorm-pr-loop into this repo (git toplevel, else cwd):\n --claude .claude/skills/<name>/SKILL.md\n --cursor .cursor/skills/<name>/SKILL.md\n Pass both flags to write both trees. Overwrites existing copies.";
|
|
6
|
+
export type SkillTarget = "claude" | "cursor";
|
|
7
|
+
export type SkillInstallRequest = {
|
|
8
|
+
help: boolean;
|
|
9
|
+
json: boolean;
|
|
10
|
+
targets: SkillTarget[];
|
|
11
|
+
};
|
|
12
|
+
export declare function parseSkillArgs(args: string[]): SkillInstallRequest;
|
|
13
|
+
/** Prefer the git repo root so `mg skill install` from a subdir still lands in-project. */
|
|
14
|
+
export declare function skillInstallRoot(cwd?: string): string;
|
|
15
|
+
export declare function skillDestination(root: string, target: SkillTarget, name?: SkillName): string;
|
|
16
|
+
export declare function resolveBundledSkillPath(fromFile?: string, name?: SkillName): string;
|
|
17
|
+
export declare function writeSkillCopies(sourcePath: string, destinations: string[]): Promise<string[]>;
|
|
18
|
+
export declare function cmdSkill(args: string[], opts?: {
|
|
19
|
+
cwd?: string;
|
|
20
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { lstat, mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { CommandError, isCommandErrorCode } from "../errors.js";
|
|
6
|
+
import { gitTopLevel } from "../repo-identity.js";
|
|
7
|
+
import { present } from "../ui/present.js";
|
|
8
|
+
export const SKILL_NAME = "mergestorm-review";
|
|
9
|
+
export const PR_LOOP_SKILL_NAME = "mergestorm-pr-loop";
|
|
10
|
+
export const SKILL_NAMES = [SKILL_NAME, PR_LOOP_SKILL_NAME];
|
|
11
|
+
export const SKILL_USAGE = `usage:
|
|
12
|
+
mergestorm skill install --claude|--cursor [--json]
|
|
13
|
+
|
|
14
|
+
Copies mergestorm-review and mergestorm-pr-loop into this repo (git toplevel, else cwd):
|
|
15
|
+
--claude .claude/skills/<name>/SKILL.md
|
|
16
|
+
--cursor .cursor/skills/<name>/SKILL.md
|
|
17
|
+
Pass both flags to write both trees. Overwrites existing copies.`;
|
|
18
|
+
export function parseSkillArgs(args) {
|
|
19
|
+
const [sub, ...rest] = args;
|
|
20
|
+
if (!sub || sub === "-h" || sub === "--help" || sub === "help") {
|
|
21
|
+
return { help: true, json: false, targets: [] };
|
|
22
|
+
}
|
|
23
|
+
if (sub !== "install") {
|
|
24
|
+
throw new CommandError(`${SKILL_USAGE}\nunknown skill subcommand: ${sub}`, 1, "usage");
|
|
25
|
+
}
|
|
26
|
+
let json = false;
|
|
27
|
+
const targets = [];
|
|
28
|
+
for (const arg of rest) {
|
|
29
|
+
if (arg === "--json") {
|
|
30
|
+
json = true;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (arg === "--claude") {
|
|
34
|
+
if (!targets.includes("claude"))
|
|
35
|
+
targets.push("claude");
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (arg === "--cursor") {
|
|
39
|
+
if (!targets.includes("cursor"))
|
|
40
|
+
targets.push("cursor");
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (arg === "-h" || arg === "--help" || arg === "help") {
|
|
44
|
+
return { help: true, json: false, targets: [] };
|
|
45
|
+
}
|
|
46
|
+
throw new CommandError(`${SKILL_USAGE}\nunknown flag: ${arg}`, 1, "usage");
|
|
47
|
+
}
|
|
48
|
+
if (targets.length === 0) {
|
|
49
|
+
throw new CommandError(`${SKILL_USAGE}\npass --claude and/or --cursor`, 1, "usage");
|
|
50
|
+
}
|
|
51
|
+
return { help: false, json, targets };
|
|
52
|
+
}
|
|
53
|
+
/** Prefer the git repo root so `mg skill install` from a subdir still lands in-project. */
|
|
54
|
+
export function skillInstallRoot(cwd = process.cwd()) {
|
|
55
|
+
try {
|
|
56
|
+
return gitTopLevel(cwd);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
if (isCommandErrorCode(err, "not_a_repo"))
|
|
60
|
+
return path.resolve(cwd);
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function skillDestination(root, target, name = SKILL_NAME) {
|
|
65
|
+
if (target === "claude") {
|
|
66
|
+
return path.join(root, ".claude", "skills", name, "SKILL.md");
|
|
67
|
+
}
|
|
68
|
+
return path.join(root, ".cursor", "skills", name, "SKILL.md");
|
|
69
|
+
}
|
|
70
|
+
export function resolveBundledSkillPath(fromFile = import.meta.url, name = SKILL_NAME) {
|
|
71
|
+
const here = path.dirname(fileURLToPath(fromFile));
|
|
72
|
+
const packageRoot = path.resolve(here, "..", "..");
|
|
73
|
+
// The published package ships the review skill at skill/SKILL.md; extra
|
|
74
|
+
// skills sit beside it in per-name directories. The monorepo keeps every
|
|
75
|
+
// skill under skills/<name>/SKILL.md.
|
|
76
|
+
const candidates = name === SKILL_NAME
|
|
77
|
+
? [
|
|
78
|
+
path.join(packageRoot, "skill", "SKILL.md"),
|
|
79
|
+
path.join(packageRoot, "..", "..", "skills", name, "SKILL.md"),
|
|
80
|
+
]
|
|
81
|
+
: [
|
|
82
|
+
path.join(packageRoot, "skill", name, "SKILL.md"),
|
|
83
|
+
path.join(packageRoot, "..", "..", "skills", name, "SKILL.md"),
|
|
84
|
+
];
|
|
85
|
+
for (const candidate of candidates) {
|
|
86
|
+
if (existsSync(candidate))
|
|
87
|
+
return candidate;
|
|
88
|
+
}
|
|
89
|
+
throw new CommandError(`${name} skill is missing (looked in ${candidates.join(" and ")})`);
|
|
90
|
+
}
|
|
91
|
+
async function assertNoSymlink(dest) {
|
|
92
|
+
const parts = dest.split(path.sep);
|
|
93
|
+
const marker = parts.findIndex((p) => p === ".claude" || p === ".cursor");
|
|
94
|
+
const first = marker === -1 ? parts.length - 1 : marker;
|
|
95
|
+
for (let i = first; i < parts.length; i++) {
|
|
96
|
+
const candidate = parts.slice(0, i + 1).join(path.sep);
|
|
97
|
+
let stat;
|
|
98
|
+
try {
|
|
99
|
+
stat = await lstat(candidate);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
if (err.code === "ENOENT")
|
|
103
|
+
continue;
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
if (stat.isSymbolicLink()) {
|
|
107
|
+
throw new CommandError(`refusing to write skill through symlink: ${candidate}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export async function writeSkillCopies(sourcePath, destinations) {
|
|
112
|
+
const body = readFileSync(sourcePath, "utf8");
|
|
113
|
+
const written = [];
|
|
114
|
+
for (const dest of destinations) {
|
|
115
|
+
await assertNoSymlink(dest);
|
|
116
|
+
await mkdir(path.dirname(dest), { recursive: true });
|
|
117
|
+
await writeFile(dest, body, "utf8");
|
|
118
|
+
written.push(dest);
|
|
119
|
+
}
|
|
120
|
+
return written;
|
|
121
|
+
}
|
|
122
|
+
export async function cmdSkill(args, opts = {}) {
|
|
123
|
+
const req = parseSkillArgs(args);
|
|
124
|
+
if (req.help) {
|
|
125
|
+
await present("Skill", SKILL_USAGE.split("\n"));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const root = skillInstallRoot(opts.cwd);
|
|
129
|
+
const sources = {};
|
|
130
|
+
const written = [];
|
|
131
|
+
for (const name of SKILL_NAMES) {
|
|
132
|
+
const source = resolveBundledSkillPath(undefined, name);
|
|
133
|
+
sources[name] = source;
|
|
134
|
+
const destinations = req.targets.map((t) => skillDestination(root, t, name));
|
|
135
|
+
written.push(...(await writeSkillCopies(source, destinations)));
|
|
136
|
+
}
|
|
137
|
+
if (req.json) {
|
|
138
|
+
await present("Skill", JSON.stringify({ names: SKILL_NAMES, sources, written }, null, 2).split("\n"));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
await present("Skill", [
|
|
142
|
+
` Installed ${SKILL_NAMES.join(" and ")}`,
|
|
143
|
+
...written.map((dest) => ` ${dest}`),
|
|
144
|
+
]);
|
|
145
|
+
}
|
package/dist/commands/stack.d.ts
CHANGED
|
@@ -28,6 +28,8 @@ export declare function parseAdoptTarget(args: string[]): {
|
|
|
28
28
|
repo: string;
|
|
29
29
|
prNumber: number;
|
|
30
30
|
};
|
|
31
|
+
/** Empty + populated bodies for the TTY `stack list` panel. */
|
|
32
|
+
export declare function buildStackListLines(stacks: StackDto[]): string[];
|
|
31
33
|
/** Parse `stack create` argv into options. Exported for tests. */
|
|
32
34
|
export declare function parseStackCreateArgs(args: string[]): {
|
|
33
35
|
name?: string;
|
package/dist/commands/stack.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { adoptStack, ensureUpperPark, landNextStack, listStacks, restackStack,
|
|
1
|
+
import { adoptStack, ensureUpperPark, landNextStack, listStacks, restackStack, } from "../api.js";
|
|
2
2
|
import { loadConfig } from "../config.js";
|
|
3
3
|
import { CommandError } from "../errors.js";
|
|
4
4
|
import { createPr, findOpenPrNumber, requireGh } from "../gh.js";
|
|
@@ -9,6 +9,10 @@ import { gitTopLevel } from "../repo-identity.js";
|
|
|
9
9
|
import { activeLayers, appendLayer, emptyStackMeta, loadStackMeta, removeActiveStack, resetStackMeta, saveStackMeta, selectStackForCreate, } from "../stack-meta.js";
|
|
10
10
|
import { isMgParkBranch, planSubmitLayerBases } from "../submit-pr-base.js";
|
|
11
11
|
import { ansi } from "../ui/ansi.js";
|
|
12
|
+
import { runLineTabsBrowser } from "../ui/line-tabs.js";
|
|
13
|
+
import { present } from "../ui/present.js";
|
|
14
|
+
import { canBrowse } from "./browse.js";
|
|
15
|
+
import { openHelpBrowser } from "./help.js";
|
|
12
16
|
const STACK_USAGE = `usage:
|
|
13
17
|
mergestorm stack create [name] [--onto <branch>] [--trunk <branch>] [--extend] [--json]
|
|
14
18
|
mergestorm stack submit [--extend] [--json]
|
|
@@ -17,7 +21,6 @@ const STACK_USAGE = `usage:
|
|
|
17
21
|
mergestorm stack adopt <owner/repo>#<pr>
|
|
18
22
|
mergestorm stack restack <stack-id> [--json]
|
|
19
23
|
mergestorm stack land <stack-id> [--json]
|
|
20
|
-
mergestorm stack auto-promote on|off <stack-id> [--json]
|
|
21
24
|
|
|
22
25
|
stack submit opens PRs with a body generated from each layer tip commit.
|
|
23
26
|
Layers 1–2 use the git parent as GitHub base. Layer 3+ opens onto the
|
|
@@ -97,10 +100,35 @@ export function parseAdoptTarget(args) {
|
|
|
97
100
|
}
|
|
98
101
|
throw new CommandError(`usage: mergestorm stack adopt <owner/repo>#<pr>`);
|
|
99
102
|
}
|
|
103
|
+
const stackCmd = (name, blurb) => ` ${ansi.brightGreen(name.padEnd(26))}${blurb}`;
|
|
104
|
+
/** Empty + populated bodies for the TTY `stack list` panel. */
|
|
105
|
+
export function buildStackListLines(stacks) {
|
|
106
|
+
if (stacks.length === 0) {
|
|
107
|
+
return [
|
|
108
|
+
` ${ansi.bold("None yet")}`,
|
|
109
|
+
" No registered stacks for this key.",
|
|
110
|
+
"",
|
|
111
|
+
` ${ansi.bold("Author one")}`,
|
|
112
|
+
stackCmd("stack create", "new layer on this branch"),
|
|
113
|
+
" commit, then stack submit",
|
|
114
|
+
"",
|
|
115
|
+
` ${ansi.bold("Or import")}`,
|
|
116
|
+
stackCmd("stack adopt org/repo#12", "open GitHub chain"),
|
|
117
|
+
"",
|
|
118
|
+
ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"),
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
const lines = [];
|
|
122
|
+
for (const s of stacks) {
|
|
123
|
+
lines.push(...formatStackHuman(s));
|
|
124
|
+
lines.push("");
|
|
125
|
+
}
|
|
126
|
+
lines.push(ansi.dim(" create · submit · land <id> · q to close"));
|
|
127
|
+
return lines;
|
|
128
|
+
}
|
|
100
129
|
function formatStackHuman(s) {
|
|
101
130
|
const lines = [];
|
|
102
|
-
|
|
103
|
-
lines.push(` ${ansi.bold(`${s.owner}/${s.repo}`)} trunk=${s.trunkBranch}${auto}`);
|
|
131
|
+
lines.push(` ${ansi.bold(`${s.owner}/${s.repo}`)} trunk=${s.trunkBranch}`);
|
|
104
132
|
lines.push(` ${ansi.dim(s.id)}`);
|
|
105
133
|
if (s.layers.length === 0) {
|
|
106
134
|
lines.push(ansi.dim(" (no layers)"));
|
|
@@ -288,13 +316,16 @@ async function cmdStackCreate(args, deps = {}) {
|
|
|
288
316
|
}, null, 2));
|
|
289
317
|
return;
|
|
290
318
|
}
|
|
291
|
-
|
|
319
|
+
const created = [
|
|
320
|
+
ansi.brightGreen(` Created ${branch} (onto ${parentBranch})`),
|
|
321
|
+
];
|
|
292
322
|
if (registeredHit) {
|
|
293
|
-
|
|
323
|
+
created.push(ansi.dim(` extending registered stack ${registeredHit.stackId}` +
|
|
294
324
|
(registeredHit.prNumber > 0 ? ` (from #${registeredHit.prNumber})` : "")));
|
|
295
325
|
}
|
|
296
|
-
|
|
297
|
-
|
|
326
|
+
created.push(ansi.dim(` trunk: ${meta.trunk} · local stacks: ${meta.stacks.length} · active layers: ${layers.length}`));
|
|
327
|
+
created.push(ansi.dim(" authoring state: managed automatically outside the repository"));
|
|
328
|
+
await present("Stack", created);
|
|
298
329
|
}
|
|
299
330
|
export async function cmdStackSubmit(args, deps = {}) {
|
|
300
331
|
const { extend, asJson } = parseStackSubmitArgs(args);
|
|
@@ -495,22 +526,26 @@ export async function cmdStackSubmit(args, deps = {}) {
|
|
|
495
526
|
}, null, 2));
|
|
496
527
|
return;
|
|
497
528
|
}
|
|
529
|
+
const summary = [];
|
|
498
530
|
for (const row of opened) {
|
|
499
531
|
const verb = row.created ? "opened" : "exists";
|
|
500
|
-
|
|
532
|
+
summary.push(ansi.brightGreen(` #${row.prNumber} ${row.branch} → ${row.base} (${verb})`));
|
|
501
533
|
}
|
|
502
534
|
if (stackId) {
|
|
503
|
-
|
|
535
|
+
summary.push(ansi.brightGreen(` Registered stack ${stackId}`));
|
|
504
536
|
}
|
|
505
537
|
else {
|
|
506
|
-
|
|
538
|
+
summary.push(ansi.dim(" Stack registration returned no stack ID"));
|
|
507
539
|
}
|
|
508
|
-
|
|
540
|
+
summary.push(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"));
|
|
541
|
+
await present("Stack", summary);
|
|
509
542
|
}
|
|
510
543
|
async function cmdStackReset(args) {
|
|
511
544
|
parseStackResetArgs(args);
|
|
512
545
|
await resetStackMeta(process.cwd());
|
|
513
|
-
|
|
546
|
+
await present("Stack", [
|
|
547
|
+
ansi.brightGreen(" Cleared CLI-managed pre-submit stack state. Branches, PRs, and registered stacks were not changed."),
|
|
548
|
+
]);
|
|
514
549
|
}
|
|
515
550
|
async function cmdStackList(args) {
|
|
516
551
|
const asJson = args.includes("--json");
|
|
@@ -520,18 +555,26 @@ async function cmdStackList(args) {
|
|
|
520
555
|
console.log(JSON.stringify({ stacks }, null, 2));
|
|
521
556
|
return;
|
|
522
557
|
}
|
|
558
|
+
if (canBrowse()) {
|
|
559
|
+
await runLineTabsBrowser({
|
|
560
|
+
tabs: [{ id: "yours", label: "Stacks", lines: buildStackListLines(stacks) }],
|
|
561
|
+
});
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
523
564
|
if (stacks.length === 0) {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
565
|
+
await present("Stacks", [
|
|
566
|
+
ansi.dim(" No stacks yet. Author with `stack create` → commit → `stack submit`."),
|
|
567
|
+
ansi.dim(" Import an existing chain only: `stack adopt owner/repo#pr`"),
|
|
568
|
+
ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"),
|
|
569
|
+
]);
|
|
527
570
|
return;
|
|
528
571
|
}
|
|
572
|
+
const listed = [];
|
|
529
573
|
for (const s of stacks) {
|
|
530
|
-
|
|
531
|
-
console.log(line);
|
|
532
|
-
console.log("");
|
|
574
|
+
listed.push(...formatStackHuman(s), "");
|
|
533
575
|
}
|
|
534
|
-
|
|
576
|
+
listed.push(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"));
|
|
577
|
+
await present("Stacks", listed);
|
|
535
578
|
}
|
|
536
579
|
async function cmdStackAdopt(args) {
|
|
537
580
|
const asJson = args.includes("--json");
|
|
@@ -552,13 +595,16 @@ async function cmdStackAdopt(args) {
|
|
|
552
595
|
const data = body;
|
|
553
596
|
const stack = data.stack;
|
|
554
597
|
const chain = data.chain;
|
|
555
|
-
|
|
556
|
-
(
|
|
557
|
-
|
|
598
|
+
const imported = [
|
|
599
|
+
ansi.brightGreen(` Imported ${owner}/${repo}#${prNumber}` +
|
|
600
|
+
(stack?.id ? ` → ${stack.id}` : "") +
|
|
601
|
+
(Array.isArray(chain) ? ` (${chain.length} layer(s))` : "")),
|
|
602
|
+
];
|
|
558
603
|
if (stack?.trunkBranch) {
|
|
559
|
-
|
|
604
|
+
imported.push(ansi.dim(` trunk: ${stack.trunkBranch}`));
|
|
560
605
|
}
|
|
561
|
-
|
|
606
|
+
imported.push(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"));
|
|
607
|
+
await present("Stack", imported);
|
|
562
608
|
}
|
|
563
609
|
async function cmdStackRestack(args) {
|
|
564
610
|
const asJson = args.includes("--json");
|
|
@@ -570,7 +616,9 @@ async function cmdStackRestack(args) {
|
|
|
570
616
|
return;
|
|
571
617
|
}
|
|
572
618
|
const fromBranch = body.fromBranch;
|
|
573
|
-
|
|
619
|
+
await present("Stack", [
|
|
620
|
+
ansi.brightGreen(` Restack started` + (fromBranch ? ` from ${fromBranch}` : "") + ` (${id})`),
|
|
621
|
+
]);
|
|
574
622
|
}
|
|
575
623
|
async function cmdStackLand(args) {
|
|
576
624
|
const asJson = args.includes("--json");
|
|
@@ -586,39 +634,31 @@ async function cmdStackLand(args) {
|
|
|
586
634
|
const branch = body.mergedBranch;
|
|
587
635
|
const uNumber = body.uNumber;
|
|
588
636
|
if (typeof uNumber === "number" && uNumber > 0) {
|
|
589
|
-
|
|
590
|
-
(
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
637
|
+
await present("Stack", [
|
|
638
|
+
ansi.brightGreen(` Promoted` +
|
|
639
|
+
(pr != null ? ` #${pr}` : "") +
|
|
640
|
+
` into U${uNumber}` +
|
|
641
|
+
(branch ? ` (${branch})` : "") +
|
|
642
|
+
` on ${id}`),
|
|
643
|
+
]);
|
|
594
644
|
return;
|
|
595
645
|
}
|
|
596
|
-
|
|
597
|
-
(
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
const asJson = args.includes("--json");
|
|
603
|
-
const filtered = args.filter((a) => a !== "--json");
|
|
604
|
-
const mode = filtered[0]?.toLowerCase();
|
|
605
|
-
const id = requireStackId(filtered[1], "usage: mergestorm stack auto-promote on|off <stack-id>");
|
|
606
|
-
if (mode !== "on" && mode !== "off") {
|
|
607
|
-
throw new CommandError("usage: mergestorm stack auto-promote on|off <stack-id>");
|
|
608
|
-
}
|
|
609
|
-
const cfg = await loadConfig();
|
|
610
|
-
const body = await setStackAutoPromote(id, mode === "on", cfg);
|
|
611
|
-
if (asJson) {
|
|
612
|
-
console.log(JSON.stringify(body, null, 2));
|
|
613
|
-
return;
|
|
614
|
-
}
|
|
615
|
-
console.log(ansi.green(` Auto-promote ${mode} for ${id}`));
|
|
646
|
+
await present("Stack", [
|
|
647
|
+
ansi.brightGreen(` Landed` +
|
|
648
|
+
(pr != null ? ` #${pr}` : "") +
|
|
649
|
+
(branch ? ` (${branch})` : "") +
|
|
650
|
+
` on ${id}`),
|
|
651
|
+
]);
|
|
616
652
|
}
|
|
617
653
|
export async function cmdStack(args) {
|
|
618
654
|
const sub = args[0]?.toLowerCase();
|
|
619
655
|
const rest = args.slice(1);
|
|
620
656
|
// Help must exit 0 — print usage, do not throw CommandError.
|
|
621
657
|
if (!sub || sub === "-h" || sub === "--help" || sub === "help") {
|
|
658
|
+
if (canBrowse()) {
|
|
659
|
+
await openHelpBrowser("stacks");
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
622
662
|
console.log(STACK_USAGE);
|
|
623
663
|
return;
|
|
624
664
|
}
|
|
@@ -636,11 +676,5 @@ export async function cmdStack(args) {
|
|
|
636
676
|
return cmdStackRestack(rest);
|
|
637
677
|
if (sub === "land" || sub === "land-next")
|
|
638
678
|
return cmdStackLand(rest);
|
|
639
|
-
if (sub === "auto-promote" ||
|
|
640
|
-
sub === "autopromote" ||
|
|
641
|
-
sub === "auto-land" ||
|
|
642
|
-
sub === "autoland") {
|
|
643
|
-
return cmdStackAutoLand(rest);
|
|
644
|
-
}
|
|
645
679
|
throw new CommandError(`${STACK_USAGE}\nunknown stack subcommand: ${sub}`);
|
|
646
680
|
}
|
|
@@ -12,6 +12,8 @@ export type StatusOptions = {
|
|
|
12
12
|
interactive?: boolean;
|
|
13
13
|
/** Test seam; production uses the two-second poll interval. */
|
|
14
14
|
pollIntervalMs?: number;
|
|
15
|
+
/** Shell: route --json through the hold-to-read panel instead of console.log. */
|
|
16
|
+
mode?: "oneshot" | "shell";
|
|
15
17
|
};
|
|
16
18
|
export declare function parseStatusArgs(args: string[], defaults?: {
|
|
17
19
|
format: "json" | "pretty";
|