mergestorm 0.3.64 → 0.3.67
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 +28 -9
- package/dist/api.d.ts +77 -11
- package/dist/api.js +228 -6
- package/dist/automation-catalog.d.ts +9 -1
- package/dist/automation-catalog.js +2 -0
- package/dist/cli.js +21 -19
- package/dist/client/index.d.ts +3 -2
- package/dist/client/index.js +3 -2
- package/dist/commands/account.d.ts +3 -0
- package/dist/commands/account.js +75 -0
- package/dist/commands/browse.d.ts +1 -9
- package/dist/commands/browse.js +10 -8
- package/dist/commands/dismiss.d.ts +0 -2
- package/dist/commands/dismiss.js +2 -2
- package/dist/commands/help.js +3 -2
- package/dist/commands/issue.d.ts +24 -6
- package/dist/commands/issue.js +157 -31
- package/dist/commands/login.js +46 -14
- package/dist/commands/logout.js +6 -7
- package/dist/commands/memory.d.ts +2 -3
- package/dist/commands/memory.js +8 -6
- package/dist/commands/registry.d.ts +1 -0
- package/dist/commands/registry.js +35 -11
- package/dist/commands/review-client.d.ts +0 -3
- package/dist/commands/review-client.js +3 -3
- package/dist/commands/review.d.ts +1 -1
- package/dist/commands/review.js +1 -1
- package/dist/commands/settings.d.ts +3 -5
- package/dist/commands/settings.js +5 -3
- package/dist/commands/stack.d.ts +19 -5
- package/dist/commands/stack.js +88 -4
- package/dist/commands/whoami.js +14 -10
- package/dist/config.d.ts +55 -2
- package/dist/config.js +264 -13
- package/dist/errors.d.ts +1 -1
- package/dist/gh.d.ts +0 -4
- package/dist/gh.js +4 -4
- package/dist/git.d.ts +1 -1
- package/dist/git.js +2 -2
- package/dist/memory-api.d.ts +1 -0
- package/dist/memory-api.js +2 -2
- package/dist/node-version.d.ts +0 -2
- package/dist/node-version.js +1 -1
- package/dist/pr-body.d.ts +4 -0
- package/dist/pr-body.js +15 -0
- package/dist/repo-identity.d.ts +2 -1
- package/dist/repo-identity.js +8 -4
- package/dist/server-publish.d.ts +1 -24
- package/dist/server-publish.js +9 -9
- package/dist/stack-blockers.d.ts +12 -38
- package/dist/stack-blockers.js +57 -33
- package/dist/stack-dto.d.ts +3 -0
- package/dist/stack-dto.js +15 -2
- package/dist/stack-meta.d.ts +2 -2
- package/dist/stack-meta.js +1 -1
- package/dist/stack-watch-loop.d.ts +2 -13
- package/dist/stack-watch-loop.js +12 -12
- package/dist/submit-pr-base.d.ts +0 -1
- package/dist/submit-pr-base.js +1 -1
- package/dist/ui/ansi.d.ts +0 -10
- package/dist/ui/ansi.js +2 -2
- package/dist/ui/banner.d.ts +3 -1
- package/dist/ui/banner.js +13 -3
- package/dist/ui/box.d.ts +0 -2
- package/dist/ui/box.js +1 -1
- package/dist/ui/prompt.d.ts +0 -11
- package/dist/ui/prompt.js +7 -7
- package/dist/ui/tabs.d.ts +2 -2
- package/dist/ui/tabs.js +1 -1
- package/dist/ui/width.d.ts +0 -9
- package/dist/ui/width.js +3 -3
- package/dist/web-api.d.ts +17 -0
- package/dist/web-api.js +30 -0
- package/package.json +1 -1
- package/skill/SKILL.md +4 -0
- package/skill/mergestorm-bounce-watch/SKILL.md +1 -1
package/dist/commands/whoami.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { getMe } from "../api.js";
|
|
2
|
-
import { apiBase, configPath, loadConfig, resolveApiKey } from "../config.js";
|
|
1
|
+
import { accountLabel, getMe } from "../api.js";
|
|
2
|
+
import { apiBase, configPath, listAccounts, loadConfig, resolveApiKey, settleAccountName } from "../config.js";
|
|
3
3
|
import { CommandError } from "../errors.js";
|
|
4
4
|
import { ansi } from "../ui/ansi.js";
|
|
5
5
|
import { present } from "../ui/present.js";
|
|
6
|
-
import { canBrowse, openTabsBrowser } from "./browse.js";
|
|
7
6
|
export async function cmdWhoami(args, opts = {}) {
|
|
8
7
|
const asJson = args.includes("--json");
|
|
9
8
|
const cfg = await loadConfig();
|
|
@@ -11,15 +10,17 @@ export async function cmdWhoami(args, opts = {}) {
|
|
|
11
10
|
if (!key) {
|
|
12
11
|
throw new CommandError("Not logged in. Run `mergestorm login` (agents: `mergestorm login --start`, then `--finish`).");
|
|
13
12
|
}
|
|
14
|
-
if (!asJson && canBrowse()) {
|
|
15
|
-
await openTabsBrowser("status");
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
13
|
const me = await getMe(cfg);
|
|
19
14
|
if (!me) {
|
|
20
15
|
throw new CommandError("Live account details unavailable. Could not verify account with /me.");
|
|
21
16
|
}
|
|
17
|
+
const account = (await settleAccountName(cfg, me)) ?? accountLabel(me.account);
|
|
18
|
+
const accounts = (await listAccounts()).map((a) => a.name);
|
|
22
19
|
const payload = {
|
|
20
|
+
account,
|
|
21
|
+
accounts,
|
|
22
|
+
github_login: me.account?.github_login ?? null,
|
|
23
|
+
email: me.account?.email ?? null,
|
|
23
24
|
key_prefix: me.key.prefix,
|
|
24
25
|
key_name: me.key.name ?? null,
|
|
25
26
|
plan_key: me.plan_key ?? null,
|
|
@@ -37,9 +38,12 @@ export async function cmdWhoami(args, opts = {}) {
|
|
|
37
38
|
console.log(text);
|
|
38
39
|
return;
|
|
39
40
|
}
|
|
40
|
-
const lines = [
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
const lines = [` Account ${account ? ansi.bold(account) : "unknown (API too old to say)"}`];
|
|
42
|
+
lines.push(` Key ${ansi.bold(String(payload.key_prefix))}${payload.key_name ? ` (${payload.key_name})` : ""}`);
|
|
43
|
+
const others = accounts.filter((name) => name !== account);
|
|
44
|
+
if (others.length > 0) {
|
|
45
|
+
lines.push(` Others ${others.join(", ")} ${ansi.dim("· mergestorm account use <name>")}`);
|
|
46
|
+
}
|
|
43
47
|
if (payload.plan_key) {
|
|
44
48
|
lines.push(` Plan ${payload.plan_label_key ?? payload.plan_key}`);
|
|
45
49
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,16 +1,69 @@
|
|
|
1
1
|
export declare const DEFAULT_API = "https://api.mergestorm.ai";
|
|
2
|
+
export declare const ACCOUNT_LIMIT = 3;
|
|
2
3
|
export type Config = {
|
|
3
4
|
apiKey?: string;
|
|
4
5
|
apiBase?: string;
|
|
5
6
|
ignoreEnv?: boolean;
|
|
7
|
+
account?: string;
|
|
8
|
+
};
|
|
9
|
+
export type StoredAccount = {
|
|
10
|
+
apiKey: string;
|
|
11
|
+
apiBase?: string;
|
|
12
|
+
};
|
|
13
|
+
export type AccountStore = {
|
|
14
|
+
accounts: Record<string, StoredAccount>;
|
|
15
|
+
activeAccount?: string;
|
|
16
|
+
apiBase?: string;
|
|
17
|
+
ignoreEnv?: boolean;
|
|
18
|
+
};
|
|
19
|
+
type StoreFile = AccountStore & {
|
|
20
|
+
apiKey?: string;
|
|
21
|
+
};
|
|
22
|
+
export type AccountSummary = {
|
|
23
|
+
name: string;
|
|
24
|
+
active: boolean;
|
|
25
|
+
keyPrefix: string;
|
|
26
|
+
apiBase: string;
|
|
27
|
+
};
|
|
28
|
+
export type NamedMe = {
|
|
29
|
+
account?: {
|
|
30
|
+
github_login?: string | null;
|
|
31
|
+
email?: string | null;
|
|
32
|
+
} | null;
|
|
6
33
|
};
|
|
7
34
|
export declare function mergestormHome(env?: NodeJS.ProcessEnv): string;
|
|
8
35
|
export declare function configPath(): string;
|
|
36
|
+
export declare function provisionalAccountName(apiKey: string): string;
|
|
37
|
+
export declare function accountNameFromMe(me: NamedMe | null | undefined): string | null;
|
|
38
|
+
export declare function serializeStore(store: AccountStore): StoreFile;
|
|
39
|
+
export declare function migrateStore(raw: unknown): {
|
|
40
|
+
store: AccountStore;
|
|
41
|
+
migrated: boolean;
|
|
42
|
+
};
|
|
43
|
+
export declare function writeStore(store: AccountStore): Promise<void>;
|
|
44
|
+
export declare function readStore(): Promise<AccountStore>;
|
|
45
|
+
export declare function withAccount<T>(name: string | undefined, fn: () => Promise<T>): Promise<T>;
|
|
46
|
+
export declare function requestedAccount(store: AccountStore, env?: NodeJS.ProcessEnv): string | undefined;
|
|
47
|
+
export declare function selectAccountName(store: AccountStore, requested: string | undefined): string | undefined;
|
|
48
|
+
export declare function configFor(store: AccountStore, name: string | undefined): Config;
|
|
9
49
|
export declare function loadConfig(): Promise<Config>;
|
|
10
|
-
export declare function
|
|
11
|
-
export declare function
|
|
50
|
+
export declare function accountLimitMessage(store: AccountStore): string;
|
|
51
|
+
export declare function saveAccount(name: string, account: StoredAccount, opts?: {
|
|
52
|
+
activate?: boolean;
|
|
53
|
+
}): Promise<AccountStore>;
|
|
54
|
+
export declare function useAccount(name: string): Promise<AccountStore>;
|
|
55
|
+
export declare function removeAccount(name: string): Promise<AccountStore>;
|
|
56
|
+
export declare function renameAccount(from: string, to: string): Promise<AccountStore>;
|
|
57
|
+
export declare function summarizeAccounts(store: AccountStore): AccountSummary[];
|
|
58
|
+
export declare function listAccounts(): Promise<AccountSummary[]>;
|
|
59
|
+
export declare function settleAccountName(cfg: Config, me: NamedMe | null | undefined): Promise<string | undefined>;
|
|
60
|
+
export declare function splitAccountFlag(args: readonly string[]): {
|
|
61
|
+
account?: string;
|
|
62
|
+
args: string[];
|
|
63
|
+
};
|
|
12
64
|
export declare function apiBase(cfg: Config): string;
|
|
13
65
|
export declare function resolveChatId(cfg: Config): string | undefined;
|
|
14
66
|
export declare function resolveApiKey(cfg: Config): string | undefined;
|
|
15
67
|
/** Short display form of a stored key (prefix only). */
|
|
16
68
|
export declare function keyDisplay(key: string | undefined): string | null;
|
|
69
|
+
export {};
|
package/dist/config.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { CommandError } from "./errors.js";
|
|
4
5
|
export const DEFAULT_API = "https://api.mergestorm.ai";
|
|
6
|
+
export const ACCOUNT_LIMIT = 3;
|
|
7
|
+
const KEY_PREFIX_LENGTH = "msk_live_".length + 4;
|
|
5
8
|
export function mergestormHome(env = process.env) {
|
|
6
9
|
const override = env.MERGESTORM_HOME?.trim();
|
|
7
10
|
return override ? path.resolve(override) : path.join(homedir(), ".mergestorm");
|
|
@@ -9,25 +12,273 @@ export function mergestormHome(env = process.env) {
|
|
|
9
12
|
export function configPath() {
|
|
10
13
|
return path.join(mergestormHome(), "config.json");
|
|
11
14
|
}
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
export function provisionalAccountName(apiKey) {
|
|
16
|
+
return apiKey.slice(0, KEY_PREFIX_LENGTH);
|
|
17
|
+
}
|
|
18
|
+
export function accountNameFromMe(me) {
|
|
19
|
+
const login = me?.account?.github_login?.trim();
|
|
20
|
+
if (login)
|
|
21
|
+
return login;
|
|
22
|
+
const email = me?.account?.email?.trim();
|
|
23
|
+
return email || null;
|
|
24
|
+
}
|
|
25
|
+
function cleanString(value) {
|
|
26
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
27
|
+
}
|
|
28
|
+
function compact(store) {
|
|
29
|
+
const out = { accounts: store.accounts };
|
|
30
|
+
if (store.activeAccount && store.accounts[store.activeAccount])
|
|
31
|
+
out.activeAccount = store.activeAccount;
|
|
32
|
+
if (store.apiBase)
|
|
33
|
+
out.apiBase = store.apiBase;
|
|
34
|
+
if (store.ignoreEnv)
|
|
35
|
+
out.ignoreEnv = true;
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
function storedAccount(apiKey, apiBase) {
|
|
39
|
+
return apiBase ? { apiKey, apiBase } : { apiKey };
|
|
40
|
+
}
|
|
41
|
+
export function serializeStore(store) {
|
|
42
|
+
const out = compact(store);
|
|
43
|
+
const active = out.activeAccount ? out.accounts[out.activeAccount] : undefined;
|
|
44
|
+
if (!active)
|
|
45
|
+
return out;
|
|
46
|
+
out.apiKey = active.apiKey;
|
|
47
|
+
if (active.apiBase)
|
|
48
|
+
out.apiBase = active.apiBase;
|
|
49
|
+
else
|
|
50
|
+
delete out.apiBase;
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
export function migrateStore(raw) {
|
|
54
|
+
const obj = raw && typeof raw === "object" ? raw : {};
|
|
55
|
+
const ignoreEnv = obj.ignoreEnv === true ? true : undefined;
|
|
56
|
+
const apiBase = cleanString(obj.apiBase);
|
|
57
|
+
const apiKey = cleanString(obj.apiKey);
|
|
58
|
+
if (obj.accounts && typeof obj.accounts === "object") {
|
|
59
|
+
const accounts = {};
|
|
60
|
+
for (const [name, value] of Object.entries(obj.accounts)) {
|
|
61
|
+
const entry = value && typeof value === "object" ? value : {};
|
|
62
|
+
const key = cleanString(entry.apiKey);
|
|
63
|
+
if (!key)
|
|
64
|
+
continue;
|
|
65
|
+
accounts[name] = storedAccount(key, cleanString(entry.apiBase));
|
|
66
|
+
}
|
|
67
|
+
const requested = cleanString(obj.activeAccount);
|
|
68
|
+
let activeAccount = requested && Object.hasOwn(accounts, requested) ? requested : Object.keys(accounts)[0];
|
|
69
|
+
let migrated = false;
|
|
70
|
+
if (apiKey) {
|
|
71
|
+
const owner = Object.keys(accounts).find((name) => accounts[name].apiKey === apiKey);
|
|
72
|
+
if (!owner) {
|
|
73
|
+
if (Object.keys(accounts).length < ACCOUNT_LIMIT) {
|
|
74
|
+
const name = provisionalAccountName(apiKey);
|
|
75
|
+
accounts[name] = storedAccount(apiKey, apiBase);
|
|
76
|
+
activeAccount = name;
|
|
77
|
+
migrated = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else if (owner !== activeAccount) {
|
|
81
|
+
activeAccount = owner;
|
|
82
|
+
migrated = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else if (activeAccount) {
|
|
86
|
+
migrated = true;
|
|
87
|
+
}
|
|
88
|
+
const store = compact({ accounts, activeAccount, apiBase: activeAccount ? undefined : apiBase, ignoreEnv });
|
|
89
|
+
return { store, migrated };
|
|
16
90
|
}
|
|
17
|
-
|
|
18
|
-
return {};
|
|
91
|
+
if (!apiKey) {
|
|
92
|
+
return { store: compact({ accounts: {}, apiBase, ignoreEnv }), migrated: Object.keys(obj).length > 0 };
|
|
19
93
|
}
|
|
94
|
+
const name = provisionalAccountName(apiKey);
|
|
95
|
+
return {
|
|
96
|
+
store: compact({ accounts: { [name]: storedAccount(apiKey, apiBase) }, activeAccount: name, ignoreEnv }),
|
|
97
|
+
migrated: true,
|
|
98
|
+
};
|
|
20
99
|
}
|
|
21
|
-
export async function
|
|
22
|
-
const
|
|
23
|
-
await mkdir(
|
|
24
|
-
await writeFile(
|
|
100
|
+
export async function writeStore(store) {
|
|
101
|
+
const file = configPath();
|
|
102
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
103
|
+
await writeFile(file, JSON.stringify(serializeStore(store), null, 2) + "\n", {
|
|
25
104
|
encoding: "utf8",
|
|
26
105
|
mode: 0o600,
|
|
27
106
|
});
|
|
28
107
|
}
|
|
29
|
-
export async function
|
|
30
|
-
|
|
108
|
+
export async function readStore() {
|
|
109
|
+
let raw;
|
|
110
|
+
try {
|
|
111
|
+
raw = JSON.parse(await readFile(configPath(), "utf8"));
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return { accounts: {} };
|
|
115
|
+
}
|
|
116
|
+
const { store, migrated } = migrateStore(raw);
|
|
117
|
+
if (migrated) {
|
|
118
|
+
try {
|
|
119
|
+
await writeStore(store);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return store;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return store;
|
|
126
|
+
}
|
|
127
|
+
function accountNames(store) {
|
|
128
|
+
return Object.keys(store.accounts);
|
|
129
|
+
}
|
|
130
|
+
function unknownAccountMessage(name, store) {
|
|
131
|
+
const names = accountNames(store);
|
|
132
|
+
if (names.length === 0) {
|
|
133
|
+
return `No account named "${name}". Nobody is logged in; run \`mergestorm login\`.`;
|
|
134
|
+
}
|
|
135
|
+
return `No account named "${name}". Accounts: ${names.join(", ")}.`;
|
|
136
|
+
}
|
|
137
|
+
let accountOverride;
|
|
138
|
+
export async function withAccount(name, fn) {
|
|
139
|
+
if (!name)
|
|
140
|
+
return fn();
|
|
141
|
+
const previous = accountOverride;
|
|
142
|
+
accountOverride = name;
|
|
143
|
+
try {
|
|
144
|
+
return await fn();
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
accountOverride = previous;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export function requestedAccount(store, env = process.env) {
|
|
151
|
+
if (accountOverride)
|
|
152
|
+
return accountOverride;
|
|
153
|
+
if (store.ignoreEnv)
|
|
154
|
+
return undefined;
|
|
155
|
+
return env.MERGESTORM_ACCOUNT?.trim() || undefined;
|
|
156
|
+
}
|
|
157
|
+
export function selectAccountName(store, requested) {
|
|
158
|
+
if (!requested)
|
|
159
|
+
return store.activeAccount;
|
|
160
|
+
if (Object.hasOwn(store.accounts, requested))
|
|
161
|
+
return requested;
|
|
162
|
+
throw new CommandError(unknownAccountMessage(requested, store));
|
|
163
|
+
}
|
|
164
|
+
export function configFor(store, name) {
|
|
165
|
+
const account = name ? store.accounts[name] : undefined;
|
|
166
|
+
const cfg = {};
|
|
167
|
+
if (account && name) {
|
|
168
|
+
cfg.apiKey = account.apiKey;
|
|
169
|
+
cfg.account = name;
|
|
170
|
+
}
|
|
171
|
+
const base = account?.apiBase ?? store.apiBase;
|
|
172
|
+
if (base)
|
|
173
|
+
cfg.apiBase = base;
|
|
174
|
+
if (store.ignoreEnv)
|
|
175
|
+
cfg.ignoreEnv = true;
|
|
176
|
+
return cfg;
|
|
177
|
+
}
|
|
178
|
+
export async function loadConfig() {
|
|
179
|
+
const store = await readStore();
|
|
180
|
+
return configFor(store, selectAccountName(store, requestedAccount(store)));
|
|
181
|
+
}
|
|
182
|
+
export function accountLimitMessage(store) {
|
|
183
|
+
return `Already ${ACCOUNT_LIMIT} accounts: ${accountNames(store).join(", ")}. Remove one with \`mergestorm account remove <name>\` first.`;
|
|
184
|
+
}
|
|
185
|
+
export async function saveAccount(name, account, opts = {}) {
|
|
186
|
+
const store = await readStore();
|
|
187
|
+
if (!Object.hasOwn(store.accounts, name) && accountNames(store).length >= ACCOUNT_LIMIT) {
|
|
188
|
+
throw new CommandError(accountLimitMessage(store), 1, "account_limit");
|
|
189
|
+
}
|
|
190
|
+
store.accounts[name] = storedAccount(account.apiKey, account.apiBase);
|
|
191
|
+
if (opts.activate || !store.activeAccount)
|
|
192
|
+
store.activeAccount = name;
|
|
193
|
+
await writeStore(store);
|
|
194
|
+
return store;
|
|
195
|
+
}
|
|
196
|
+
export async function useAccount(name) {
|
|
197
|
+
const store = await readStore();
|
|
198
|
+
if (!Object.hasOwn(store.accounts, name))
|
|
199
|
+
throw new CommandError(unknownAccountMessage(name, store));
|
|
200
|
+
store.activeAccount = name;
|
|
201
|
+
await writeStore(store);
|
|
202
|
+
return store;
|
|
203
|
+
}
|
|
204
|
+
export async function removeAccount(name) {
|
|
205
|
+
const store = await readStore();
|
|
206
|
+
if (!Object.hasOwn(store.accounts, name))
|
|
207
|
+
throw new CommandError(unknownAccountMessage(name, store));
|
|
208
|
+
delete store.accounts[name];
|
|
209
|
+
if (store.activeAccount === name)
|
|
210
|
+
store.activeAccount = accountNames(store)[0];
|
|
211
|
+
await writeStore(store);
|
|
212
|
+
return store;
|
|
213
|
+
}
|
|
214
|
+
export async function renameAccount(from, to) {
|
|
215
|
+
const store = await readStore();
|
|
216
|
+
const account = store.accounts[from];
|
|
217
|
+
if (!account || from === to)
|
|
218
|
+
return store;
|
|
219
|
+
if (Object.hasOwn(store.accounts, to)) {
|
|
220
|
+
throw new CommandError(`An account named "${to}" already exists, so ${from} keeps its name.`);
|
|
221
|
+
}
|
|
222
|
+
delete store.accounts[from];
|
|
223
|
+
store.accounts[to] = account;
|
|
224
|
+
if (store.activeAccount === from)
|
|
225
|
+
store.activeAccount = to;
|
|
226
|
+
await writeStore(store);
|
|
227
|
+
return store;
|
|
228
|
+
}
|
|
229
|
+
export function summarizeAccounts(store) {
|
|
230
|
+
return Object.entries(store.accounts).map(([name, account]) => ({
|
|
231
|
+
name,
|
|
232
|
+
active: name === store.activeAccount,
|
|
233
|
+
keyPrefix: provisionalAccountName(account.apiKey),
|
|
234
|
+
apiBase: (account.apiBase ?? store.apiBase ?? DEFAULT_API).replace(/\/$/, ""),
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
export async function listAccounts() {
|
|
238
|
+
return summarizeAccounts(await readStore());
|
|
239
|
+
}
|
|
240
|
+
export async function settleAccountName(cfg, me) {
|
|
241
|
+
if (!cfg.account || !cfg.apiKey || resolveApiKey(cfg) !== cfg.apiKey)
|
|
242
|
+
return cfg.account;
|
|
243
|
+
if (cfg.account !== provisionalAccountName(cfg.apiKey))
|
|
244
|
+
return cfg.account;
|
|
245
|
+
const name = accountNameFromMe(me);
|
|
246
|
+
if (!name || name === cfg.account)
|
|
247
|
+
return cfg.account;
|
|
248
|
+
try {
|
|
249
|
+
await renameAccount(cfg.account, name);
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return cfg.account;
|
|
253
|
+
}
|
|
254
|
+
return name;
|
|
255
|
+
}
|
|
256
|
+
export function splitAccountFlag(args) {
|
|
257
|
+
const rest = [];
|
|
258
|
+
let account;
|
|
259
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
260
|
+
const arg = args[i];
|
|
261
|
+
let value;
|
|
262
|
+
if (arg === "--account") {
|
|
263
|
+
value = args[i + 1];
|
|
264
|
+
if (value === undefined || value.startsWith("-"))
|
|
265
|
+
value = "";
|
|
266
|
+
else
|
|
267
|
+
i += 1;
|
|
268
|
+
}
|
|
269
|
+
else if (arg.startsWith("--account=")) {
|
|
270
|
+
value = arg.slice("--account=".length);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
rest.push(arg);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (!value.trim()) {
|
|
277
|
+
throw new CommandError("--account needs an account name. `mergestorm account list` shows them.", 1, "usage");
|
|
278
|
+
}
|
|
279
|
+
account = value.trim();
|
|
280
|
+
}
|
|
281
|
+
return account ? { account, args: rest } : { args: rest };
|
|
31
282
|
}
|
|
32
283
|
export function apiBase(cfg) {
|
|
33
284
|
const fromEnv = cfg.ignoreEnv ? undefined : process.env.MERGESTORM_API_URL?.trim();
|
package/dist/errors.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Stable machine codes for CommandError control flow.
|
|
3
3
|
* UX copy in `message` may change freely; match on `code`, never on message text.
|
|
4
4
|
*/
|
|
5
|
-
export type CommandErrorCode = "auth_invalid" | "api_timeout" | "not_a_repo" | "not_found" | "missing_api_key" | "registered_parent" | "implicit_stack" | "usage" | "review_quota" | "review_failed" | "review_timeout" | "rate_limited" | "cyclone_not_installed" | "pr_merged" | "pr_closed" | "not_stacked" | "draft" | "reserved_branch" | "supabase_unconfigured" | "cyclone_not_connected" | "cyclone_permission_missing" | "busy" | "adopt_failed" | "github_unreadable" | "stack_attention" | "head_changed" | "base_changed" | "tree_mismatch" | "protected_branch" | "payload_too_large" | "unsupported_publication" | "publication_failed" | "git_failed" | "unknown_label" | "invalid_input" | "memory_off" | "memory_full" | "memory_unavailable";
|
|
5
|
+
export type CommandErrorCode = "auth_invalid" | "api_timeout" | "not_a_repo" | "not_found" | "missing_api_key" | "registered_parent" | "implicit_stack" | "usage" | "review_quota" | "review_failed" | "review_timeout" | "rate_limited" | "cyclone_not_installed" | "pr_merged" | "pr_closed" | "not_stacked" | "draft" | "reserved_branch" | "supabase_unconfigured" | "cyclone_not_connected" | "account_limit" | "surge_other_account" | "cyclone_permission_missing" | "busy" | "adopt_failed" | "github_unreadable" | "stack_attention" | "head_changed" | "base_changed" | "tree_mismatch" | "protected_branch" | "payload_too_large" | "unsupported_publication" | "publication_failed" | "git_failed" | "unknown_label" | "invalid_input" | "memory_off" | "memory_full" | "memory_similar" | "memory_unavailable" | "web_unavailable" | "issue_not_approved" | "issue_review_unavailable";
|
|
6
6
|
/** Stable one-shot exit codes for machine callers of `mergestorm review`. */
|
|
7
7
|
export declare const REVIEW_EXIT: {
|
|
8
8
|
readonly success: 0;
|
package/dist/gh.d.ts
CHANGED
|
@@ -10,7 +10,6 @@ export type GhResult = {
|
|
|
10
10
|
};
|
|
11
11
|
export type GhRunner = (args: string[], input?: string) => GhResult;
|
|
12
12
|
export type GithubApi = "graphql" | "rest";
|
|
13
|
-
export declare const GITHUB_API_ENV = "MERGESTORM_GITHUB_API";
|
|
14
13
|
export declare function githubApiFromEnv(env?: NodeJS.ProcessEnv): GithubApi;
|
|
15
14
|
export declare function isGraphqlEndpointBlocked(text: string): boolean;
|
|
16
15
|
export declare class GraphqlBlockedError extends CommandError {
|
|
@@ -34,7 +33,6 @@ export declare function findOpenPrNumber(owner: string, repo: string, headBranch
|
|
|
34
33
|
api?: GithubApi;
|
|
35
34
|
gh?: GhRunner;
|
|
36
35
|
}): number | null;
|
|
37
|
-
export declare function findOpenPrNumberViaRest(owner: string, repo: string, headBranch: string, gh: GhRunner): number | null;
|
|
38
36
|
export type NewPr = {
|
|
39
37
|
base: string;
|
|
40
38
|
head: string;
|
|
@@ -63,13 +61,11 @@ type GraphqlSpawn = (command: string, args: string[], options: {
|
|
|
63
61
|
encoding: "utf8";
|
|
64
62
|
input: string;
|
|
65
63
|
}) => GraphqlSpawnResult;
|
|
66
|
-
export declare const CREATE_PR_BATCH_SIZE = 5;
|
|
67
64
|
export declare function ghGraphql(request: GraphqlRequest, cwd?: string, spawn?: GraphqlSpawn): GraphqlResponse;
|
|
68
65
|
export declare function chunkCreatePrs<T>(items: readonly T[], size?: number): T[][];
|
|
69
66
|
export declare function buildCreatePrsMutation(repositoryId: string, prs: readonly NewPr[]): GraphqlRequest;
|
|
70
67
|
export declare function fetchRepositoryId(owner: string, repo: string, graphql: GraphqlRunner): string;
|
|
71
68
|
export declare function createPrsOnRepository(repositoryId: string, prs: readonly NewPr[], graphql: GraphqlRunner): number[];
|
|
72
|
-
export declare function createPrsViaRest(owner: string, repo: string, prs: readonly NewPr[], gh: GhRunner, opened?: readonly number[]): number[];
|
|
73
69
|
export declare function createPrs(input: {
|
|
74
70
|
owner: string;
|
|
75
71
|
repo: string;
|
package/dist/gh.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { CommandError } from "./errors.js";
|
|
3
|
-
|
|
3
|
+
const GITHUB_API_ENV = "MERGESTORM_GITHUB_API";
|
|
4
4
|
export function githubApiFromEnv(env = process.env) {
|
|
5
5
|
return env[GITHUB_API_ENV]?.trim().toLowerCase() === "rest" ? "rest" : "graphql";
|
|
6
6
|
}
|
|
@@ -83,7 +83,7 @@ function findOpenPrNumberViaGraphql(owner, repo, headBranch, gh) {
|
|
|
83
83
|
}
|
|
84
84
|
return firstPrNumber(res.stdout);
|
|
85
85
|
}
|
|
86
|
-
|
|
86
|
+
function findOpenPrNumberViaRest(owner, repo, headBranch, gh) {
|
|
87
87
|
const res = gh([
|
|
88
88
|
"api",
|
|
89
89
|
"-X",
|
|
@@ -111,7 +111,7 @@ function firstPrNumber(stdout) {
|
|
|
111
111
|
throw new CommandError(`Failed to parse PR list response: ${e instanceof Error ? e.message : String(e)}`);
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
|
-
|
|
114
|
+
const CREATE_PR_BATCH_SIZE = 5;
|
|
115
115
|
export function ghGraphql(request, cwd = process.cwd(), spawn = spawnSync) {
|
|
116
116
|
const result = spawn("gh", ["api", "graphql", "--input", "-"], {
|
|
117
117
|
cwd,
|
|
@@ -231,7 +231,7 @@ function restErrorText(res) {
|
|
|
231
231
|
return body.message.trim();
|
|
232
232
|
return res.stderr;
|
|
233
233
|
}
|
|
234
|
-
|
|
234
|
+
function createPrsViaRest(owner, repo, prs, gh, opened = []) {
|
|
235
235
|
const numbers = [...opened];
|
|
236
236
|
for (const pr of prs.slice(numbers.length)) {
|
|
237
237
|
const res = gh(["api", "-X", "POST", `repos/${owner}/${repo}/pulls`, "--input", "-"], JSON.stringify({ title: pr.title, head: pr.head, base: pr.base, body: pr.body }));
|
package/dist/git.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare function git(args: string[], cwd?: string): string;
|
|
1
|
+
export declare function git(args: string[], cwd?: string, env?: NodeJS.ProcessEnv): string;
|
|
2
2
|
/** Parse `origin` remote into GitHub owner/repo when possible. */
|
|
3
3
|
export declare function parseGithubOriginRepo(cwd?: string): {
|
|
4
4
|
owner: string;
|
package/dist/git.js
CHANGED
|
@@ -2,9 +2,9 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
3
|
import { readFile, realpath, stat } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
export function git(args, cwd = process.cwd()) {
|
|
5
|
+
export function git(args, cwd = process.cwd(), env) {
|
|
6
6
|
try {
|
|
7
|
-
const result = spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
7
|
+
const result = spawnSync("git", args, { cwd, encoding: "utf8", env });
|
|
8
8
|
if (result.status !== 0) {
|
|
9
9
|
throw new Error(result.stderr?.trim() || `git ${args.join(" ")} failed`);
|
|
10
10
|
}
|
package/dist/memory-api.d.ts
CHANGED
package/dist/memory-api.js
CHANGED
|
@@ -19,7 +19,7 @@ export function isMemoryKind(value) {
|
|
|
19
19
|
export function isMemoryScope(value) {
|
|
20
20
|
return typeof value === "string" && MEMORY_SCOPES.includes(value);
|
|
21
21
|
}
|
|
22
|
-
const KNOWN_CODES = new Set(["invalid_input", "not_found", "memory_off", "memory_full", "memory_unavailable", "payload_too_large"]);
|
|
22
|
+
const KNOWN_CODES = new Set(["invalid_input", "not_found", "memory_off", "memory_full", "memory_similar", "memory_unavailable", "payload_too_large"]);
|
|
23
23
|
function memoryFailure(status, body, retryAfterSeconds, action) {
|
|
24
24
|
const row = (body && typeof body === "object" ? body : {});
|
|
25
25
|
if (status === 429 || row.error === "rate_limited") {
|
|
@@ -52,7 +52,7 @@ export function searchMemories(input, cfg) {
|
|
|
52
52
|
return call(cfg, `/api/v1/memories/search${query({ query: input.query, repo: input.repo, limit: input.limit, chat_id: input.chatId })}`, {}, [200], "Could not search memories");
|
|
53
53
|
}
|
|
54
54
|
export function saveMemory(input, cfg) {
|
|
55
|
-
const json = { text: input.text, kind: input.kind, ...(input.repo ? { repo: input.repo } : {}), ...(input.chatId ? { chat_id: input.chatId } : {}) };
|
|
55
|
+
const json = { text: input.text, kind: input.kind, ...(input.repo ? { repo: input.repo } : {}), ...(input.chatId ? { chat_id: input.chatId } : {}), ...(input.keepBoth ? { keep_both: true } : {}) };
|
|
56
56
|
return call(cfg, "/api/v1/memories", { method: "POST", json }, [200, 201], "Could not save the memory");
|
|
57
57
|
}
|
|
58
58
|
export function updateMemory(id, change, cfg) {
|
package/dist/node-version.d.ts
CHANGED
package/dist/node-version.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Mergestorm CLI requires Node 22+ (matches package.json engines). */
|
|
2
|
-
|
|
2
|
+
const MIN_NODE_MAJOR = 22;
|
|
3
3
|
/** Parse major version from a Node version string (e.g. "22.22.3" → 22). */
|
|
4
4
|
export function nodeMajor(version = process.versions.node) {
|
|
5
5
|
const major = Number.parseInt(version.split(".")[0] ?? "", 10);
|
package/dist/pr-body.d.ts
CHANGED
|
@@ -6,3 +6,7 @@
|
|
|
6
6
|
* Turn `git log -1 --format=%B` output into a non-empty PR description.
|
|
7
7
|
*/
|
|
8
8
|
export declare function buildPrBodyFromCommit(fullMessage: string): string;
|
|
9
|
+
export declare function linkedIssueNumbers(repo: {
|
|
10
|
+
owner: string;
|
|
11
|
+
repo: string;
|
|
12
|
+
}, ...texts: string[]): number[];
|
package/dist/pr-body.js
CHANGED
|
@@ -49,6 +49,21 @@ export function buildPrBodyFromCommit(fullMessage) {
|
|
|
49
49
|
}
|
|
50
50
|
return parts.join("\n") + "\n";
|
|
51
51
|
}
|
|
52
|
+
const LINKED_ISSUE_RE = /(?<![\w/#.-])(?:fix(?:es|ed)?|close[sd]?|resolve[sd]?)\s*:?\s+(?:([\w.-]+)\/([\w.-]+))?#(\d+)\b/gi;
|
|
53
|
+
export function linkedIssueNumbers(repo, ...texts) {
|
|
54
|
+
const numbers = new Set();
|
|
55
|
+
const same = `${repo.owner}/${repo.repo}`.toLowerCase();
|
|
56
|
+
for (const text of texts) {
|
|
57
|
+
for (const match of text.matchAll(LINKED_ISSUE_RE)) {
|
|
58
|
+
if (match[1] && `${match[1]}/${match[2]}`.toLowerCase() !== same)
|
|
59
|
+
continue;
|
|
60
|
+
const n = Number(match[3]);
|
|
61
|
+
if (Number.isSafeInteger(n) && n > 0)
|
|
62
|
+
numbers.add(n);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return [...numbers].sort((a, b) => a - b);
|
|
66
|
+
}
|
|
52
67
|
function uniqueTrailers(trailers) {
|
|
53
68
|
const seen = new Set();
|
|
54
69
|
const out = [];
|
package/dist/repo-identity.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** Classify git "not a repo" failures once at the boundary; callers match on code. */
|
|
2
|
+
export declare function isNotARepoGitError(detail: string): boolean;
|
|
1
3
|
export declare function gitTopLevel(cwd?: string): string;
|
|
2
4
|
/**
|
|
3
5
|
* Each worktree has its own authoring state.
|
|
@@ -5,5 +7,4 @@ export declare function gitTopLevel(cwd?: string): string;
|
|
|
5
7
|
*/
|
|
6
8
|
export declare function stableRepoId(cwd?: string): string;
|
|
7
9
|
export declare function cliStacksRoot(): string;
|
|
8
|
-
export declare function repoStateDir(cwd?: string, stacksRoot?: string): string;
|
|
9
10
|
export declare function stackMetaPath(cwd?: string, stacksRoot?: string): string;
|
package/dist/repo-identity.js
CHANGED
|
@@ -5,16 +5,20 @@ import { mergestormHome } from "./config.js";
|
|
|
5
5
|
import { CommandError } from "./errors.js";
|
|
6
6
|
import { git } from "./git.js";
|
|
7
7
|
/** Classify git "not a repo" failures once at the boundary; callers match on code. */
|
|
8
|
+
export function isNotARepoGitError(detail) {
|
|
9
|
+
return /not a git repository|gitfile does not point to a valid repository/i.test(detail);
|
|
10
|
+
}
|
|
11
|
+
const classifierEnv = { ...process.env, LC_ALL: "C" };
|
|
8
12
|
function rethrowIfNotARepo(err) {
|
|
9
13
|
const detail = err instanceof Error ? err.message : String(err);
|
|
10
|
-
if (
|
|
14
|
+
if (isNotARepoGitError(detail)) {
|
|
11
15
|
throw new CommandError(detail, 1, "not_a_repo");
|
|
12
16
|
}
|
|
13
17
|
throw err;
|
|
14
18
|
}
|
|
15
19
|
export function gitTopLevel(cwd = process.cwd()) {
|
|
16
20
|
try {
|
|
17
|
-
return realpathSync(git(["rev-parse", "--show-toplevel"], cwd).trim());
|
|
21
|
+
return realpathSync(git(["rev-parse", "--show-toplevel"], cwd, classifierEnv).trim());
|
|
18
22
|
}
|
|
19
23
|
catch (err) {
|
|
20
24
|
rethrowIfNotARepo(err);
|
|
@@ -27,7 +31,7 @@ export function gitTopLevel(cwd = process.cwd()) {
|
|
|
27
31
|
export function stableRepoId(cwd = process.cwd()) {
|
|
28
32
|
let raw;
|
|
29
33
|
try {
|
|
30
|
-
raw = git(["rev-parse", "--git-dir"], cwd).trim();
|
|
34
|
+
raw = git(["rev-parse", "--git-dir"], cwd, classifierEnv).trim();
|
|
31
35
|
}
|
|
32
36
|
catch (err) {
|
|
33
37
|
rethrowIfNotARepo(err);
|
|
@@ -41,7 +45,7 @@ export function stableRepoId(cwd = process.cwd()) {
|
|
|
41
45
|
export function cliStacksRoot() {
|
|
42
46
|
return path.join(mergestormHome(), "stacks");
|
|
43
47
|
}
|
|
44
|
-
|
|
48
|
+
function repoStateDir(cwd = process.cwd(), stacksRoot = cliStacksRoot()) {
|
|
45
49
|
return path.join(stacksRoot, stableRepoId(cwd));
|
|
46
50
|
}
|
|
47
51
|
export function stackMetaPath(cwd = process.cwd(), stacksRoot = cliStacksRoot()) {
|