nugit-cli 0.0.1 → 0.1.1
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/package.json +1 -1
- package/src/api-client.js +10 -23
- package/src/github-device-flow.js +1 -1
- package/src/github-oauth-client-id.js +11 -0
- package/src/github-pr-social.js +42 -0
- package/src/github-rest.js +149 -6
- package/src/nugit-config.js +84 -0
- package/src/nugit-stack.js +40 -257
- package/src/nugit.js +104 -647
- package/src/review-hub/review-autoapprove.js +95 -0
- package/src/review-hub/review-hub-back.js +10 -0
- package/src/review-hub/review-hub-ink.js +169 -0
- package/src/review-hub/run-review-hub.js +131 -0
- package/src/services/repo-branches.js +151 -0
- package/src/services/stack-inference.js +90 -0
- package/src/split-view/run-split.js +14 -76
- package/src/split-view/split-ink.js +2 -2
- package/src/stack-infer-from-prs.js +71 -0
- package/src/stack-view/diff-line-map.js +62 -0
- package/src/stack-view/fetch-pr-data.js +104 -4
- package/src/stack-view/infer-chains-to-pick-stacks.js +80 -0
- package/src/stack-view/ink-app.js +3 -421
- package/src/stack-view/loader.js +19 -93
- package/src/stack-view/loading-ink.js +2 -0
- package/src/stack-view/merge-alternate-pick-stacks.js +245 -0
- package/src/stack-view/patch-preview-merge.js +108 -0
- package/src/stack-view/remote-infer-doc.js +76 -0
- package/src/stack-view/repo-picker-back.js +10 -0
- package/src/stack-view/run-stack-view.js +508 -150
- package/src/stack-view/run-view-entry.js +115 -0
- package/src/stack-view/sgr-mouse.js +56 -0
- package/src/stack-view/stack-branch-graph.js +95 -0
- package/src/stack-view/stack-pick-graph.js +93 -0
- package/src/stack-view/stack-pick-ink.js +308 -0
- package/src/stack-view/stack-pick-layout.js +19 -0
- package/src/stack-view/stack-pick-sort.js +188 -0
- package/src/stack-view/stack-picker-graph-pane.js +118 -0
- package/src/stack-view/terminal-fullscreen.js +7 -0
- package/src/stack-view/tree-ascii.js +73 -0
- package/src/stack-view/view-md-plain.js +23 -0
- package/src/stack-view/view-repo-picker-ink.js +293 -0
- package/src/stack-view/view-tui-sequential.js +126 -0
- package/src/tui/pages/home.js +122 -0
- package/src/tui/pages/repo-actions.js +81 -0
- package/src/tui/pages/repo-branches.js +259 -0
- package/src/tui/pages/viewer.js +2129 -0
- package/src/tui/router.js +40 -0
- package/src/tui/run-tui.js +281 -0
- package/src/utilities/loading.js +37 -0
- package/src/utilities/terminal.js +31 -0
- package/src/cli-output.js +0 -228
- package/src/nugit-start.js +0 -211
- package/src/stack-discover.js +0 -284
- package/src/stack-discovery-config.js +0 -91
- package/src/stack-extra-commands.js +0 -353
- package/src/stack-graph.js +0 -214
- package/src/stack-helpers.js +0 -58
- package/src/stack-propagate.js +0 -422
package/src/nugit.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import fs from "fs";
|
|
3
2
|
import chalk from "chalk";
|
|
4
3
|
import { Command } from "commander";
|
|
5
4
|
import {
|
|
@@ -7,146 +6,102 @@ import {
|
|
|
7
6
|
pollDeviceFlow,
|
|
8
7
|
pollDeviceFlowUntilComplete,
|
|
9
8
|
savePat,
|
|
10
|
-
|
|
11
|
-
listOpenPullsInRepo,
|
|
12
|
-
fetchRemoteStackJson,
|
|
13
|
-
getPull,
|
|
14
|
-
authMe,
|
|
15
|
-
createPullRequest,
|
|
16
|
-
getRepoMetadata
|
|
9
|
+
authMe
|
|
17
10
|
} from "./api-client.js";
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
readStackFile,
|
|
21
|
-
writeStackFile,
|
|
22
|
-
createInitialStackDoc,
|
|
23
|
-
validateStackDoc,
|
|
24
|
-
parseRepoFullName,
|
|
25
|
-
nextStackPosition,
|
|
26
|
-
stackEntryFromGithubPull,
|
|
27
|
-
stackJsonPath,
|
|
28
|
-
parseStackAddPrNumbers
|
|
29
|
-
} from "./nugit-stack.js";
|
|
30
|
-
import { runStackPropagate } from "./stack-propagate.js";
|
|
31
|
-
import { registerStackExtraCommands } from "./stack-extra-commands.js";
|
|
32
|
-
import { runStackViewCommand } from "./stack-view/run-stack-view.js";
|
|
33
|
-
import {
|
|
34
|
-
printJson,
|
|
35
|
-
formatWhoamiHuman,
|
|
36
|
-
formatPrSearchHuman,
|
|
37
|
-
formatOpenPullsHuman,
|
|
38
|
-
formatStackDocHuman,
|
|
39
|
-
formatStackEnrichHuman,
|
|
40
|
-
formatStacksListHuman,
|
|
41
|
-
formatPrCreatedHuman,
|
|
42
|
-
formatPatOkHuman
|
|
43
|
-
} from "./cli-output.js";
|
|
11
|
+
import { findGitRoot, parseRepoFullName } from "./nugit-stack.js";
|
|
12
|
+
import { runNugitViewEntry } from "./stack-view/run-view-entry.js";
|
|
44
13
|
import { getRepoFullNameFromGitRoot } from "./git-info.js";
|
|
45
|
-
import { discoverStacksInRepo } from "./stack-discover.js";
|
|
46
|
-
import { getStackDiscoveryOpts, effectiveMaxOpenPrs } from "./stack-discovery-config.js";
|
|
47
14
|
import {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
} from "./
|
|
53
|
-
import { runSplitCommand } from "./split-view/run-split.js";
|
|
54
|
-
import { getConfigPath } from "./user-config.js";
|
|
15
|
+
runConfigInit,
|
|
16
|
+
runConfigShow,
|
|
17
|
+
runConfigSet,
|
|
18
|
+
runEnvExport
|
|
19
|
+
} from "./nugit-config.js";
|
|
55
20
|
import { openInBrowser } from "./open-browser.js";
|
|
56
21
|
import {
|
|
57
22
|
writeStoredGithubToken,
|
|
58
23
|
clearStoredGithubToken,
|
|
59
24
|
getGithubTokenPath
|
|
60
25
|
} from "./token-store.js";
|
|
61
|
-
import {
|
|
62
|
-
|
|
63
|
-
runConfigShow,
|
|
64
|
-
runConfigSet,
|
|
65
|
-
runEnvExport,
|
|
66
|
-
runStart,
|
|
67
|
-
runStartHub
|
|
68
|
-
} from "./nugit-start.js";
|
|
26
|
+
import { getConfigPath } from "./user-config.js";
|
|
27
|
+
import { runNugitTui } from "./tui/run-tui.js";
|
|
69
28
|
|
|
70
29
|
const program = new Command();
|
|
71
|
-
program.name("nugit").description("Nugit
|
|
30
|
+
program.name("nugit").description("Nugit — browse and review stacked GitHub PRs");
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Default: TTY → launch the TUI; non-TTY scripting flags supported
|
|
34
|
+
//
|
|
35
|
+
// nugit → TUI (home screen)
|
|
36
|
+
// nugit --repo owner/repo → TUI starting from that repo's stack viewer
|
|
37
|
+
// nugit --no-tui --repo o/r → print stack summary (non-interactive)
|
|
38
|
+
// nugit --review-hub → TUI review hub
|
|
39
|
+
// nugit --no-tui --review-hub → print sorted repo list
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
72
41
|
|
|
73
42
|
program
|
|
74
|
-
.
|
|
75
|
-
.
|
|
76
|
-
|
|
77
|
-
)
|
|
78
|
-
.option("--
|
|
79
|
-
.option("--user <github-login>", "Override created_by metadata")
|
|
43
|
+
.option("--no-tui", "Non-interactive: print stack summary or repo list to stdout")
|
|
44
|
+
.option("--repo <owner/repo>", "Skip home screen and open this repo directly")
|
|
45
|
+
.option("--ref <branch>", "Branch or SHA for --repo (default: GitHub default branch)")
|
|
46
|
+
.option("--review-hub", "Open review hub instead of home screen")
|
|
47
|
+
.option("--auto-apply", "With --review-hub: auto-approve allowlisted PRs when stale approval + merge-only delta")
|
|
80
48
|
.action(async (opts) => {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const me = await authMe();
|
|
89
|
-
user = me.login;
|
|
90
|
-
if (!user) {
|
|
91
|
-
throw new Error("Could not resolve login; pass --user or set NUGIT_USER_TOKEN / STACKPR_USER_TOKEN");
|
|
49
|
+
const tty = process.stdin.isTTY && process.stdout.isTTY;
|
|
50
|
+
|
|
51
|
+
if (opts.reviewHub) {
|
|
52
|
+
if (!opts.tui || !tty) {
|
|
53
|
+
const { runReviewHub } = await import("./review-hub/run-review-hub.js");
|
|
54
|
+
await runReviewHub({ noTui: true, autoApply: !!opts.autoApply });
|
|
55
|
+
return;
|
|
92
56
|
}
|
|
93
|
-
|
|
57
|
+
await runNugitTui({ startAt: "review_hub" });
|
|
58
|
+
return;
|
|
94
59
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
cleared = prev.prs.length;
|
|
102
|
-
}
|
|
103
|
-
} catch {
|
|
104
|
-
/* ignore parse errors */
|
|
60
|
+
|
|
61
|
+
if (opts.repo) {
|
|
62
|
+
const repoFull = String(opts.repo);
|
|
63
|
+
if (!opts.tui || !tty) {
|
|
64
|
+
await runNugitViewEntry(repoFull, opts.ref, { noTui: true });
|
|
65
|
+
return;
|
|
105
66
|
}
|
|
67
|
+
await runNugitTui({ startRepo: repoFull, startRef: opts.ref });
|
|
68
|
+
return;
|
|
106
69
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
70
|
+
|
|
71
|
+
if (tty) {
|
|
72
|
+
await runNugitTui({});
|
|
73
|
+
} else {
|
|
74
|
+
program.outputHelp();
|
|
112
75
|
}
|
|
113
|
-
console.error(
|
|
114
|
-
"Add PRs with `nugit stack add --pr N [N...]` (bottom→top), then `nugit stack propagate` (auto-commits this file on the tip if needed)."
|
|
115
|
-
);
|
|
116
76
|
});
|
|
117
77
|
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// auth
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
118
82
|
const auth = new Command("auth").description("GitHub authentication");
|
|
119
83
|
|
|
120
84
|
auth
|
|
121
85
|
.command("login")
|
|
122
86
|
.description(
|
|
123
|
-
"OAuth device flow: opens browser (pre-filled code), waits for approval, saves token to ~/.config/nugit/github-token.
|
|
87
|
+
"OAuth device flow: opens browser (pre-filled code), waits for approval, saves token to ~/.config/nugit/github-token. " +
|
|
88
|
+
"Uses bundled OAuth App client id unless GITHUB_OAUTH_CLIENT_ID is set."
|
|
124
89
|
)
|
|
125
90
|
.option("--no-browser", "Do not launch a browser (open the printed URL yourself)", false)
|
|
126
|
-
.option(
|
|
127
|
-
"--no-wait",
|
|
128
|
-
"Only request a device code and print instructions (use nugit auth poll --device-code …)",
|
|
129
|
-
false
|
|
130
|
-
)
|
|
91
|
+
.option("--no-wait", "Only request a device code and print instructions (use nugit auth poll --device-code …)", false)
|
|
131
92
|
.option("--json", "With --no-wait: raw device response. Otherwise: { login, token_path } after save", false)
|
|
132
93
|
.action(async (opts) => {
|
|
133
94
|
const result = await startDeviceFlow();
|
|
134
95
|
const deviceCode = result.device_code;
|
|
135
|
-
if (!deviceCode || typeof deviceCode !== "string")
|
|
136
|
-
throw new Error("GitHub did not return device_code");
|
|
137
|
-
}
|
|
96
|
+
if (!deviceCode || typeof deviceCode !== "string") throw new Error("GitHub did not return device_code");
|
|
138
97
|
const interval = Number(result.interval) || 5;
|
|
139
98
|
|
|
140
99
|
if (opts.noWait) {
|
|
141
100
|
if (opts.json) {
|
|
142
|
-
|
|
101
|
+
console.log(JSON.stringify(result, null, 2));
|
|
143
102
|
} else if (result.verification_uri && result.user_code) {
|
|
144
|
-
console.error(
|
|
145
|
-
|
|
146
|
-
);
|
|
147
|
-
console.error(
|
|
148
|
-
`Then: ${chalk.bold(`nugit auth poll --device-code ${deviceCode}`)} (poll every ${interval}s)`
|
|
149
|
-
);
|
|
103
|
+
console.error(`Open ${result.verification_uri} and enter code ${chalk.bold(String(result.user_code))}`);
|
|
104
|
+
console.error(`Then: ${chalk.bold(`nugit auth poll --device-code ${deviceCode}`)} (poll every ${interval}s)`);
|
|
150
105
|
}
|
|
151
106
|
return;
|
|
152
107
|
}
|
|
@@ -154,9 +109,7 @@ auth
|
|
|
154
109
|
const baseUri = String(result.verification_uri || "https://github.com/login/device");
|
|
155
110
|
const userCode = result.user_code != null ? String(result.user_code) : "";
|
|
156
111
|
const sep = baseUri.includes("?") ? "&" : "?";
|
|
157
|
-
const verifyUrl = userCode
|
|
158
|
-
? `${baseUri}${sep}user_code=${encodeURIComponent(userCode)}`
|
|
159
|
-
: baseUri;
|
|
112
|
+
const verifyUrl = userCode ? `${baseUri}${sep}user_code=${encodeURIComponent(userCode)}` : baseUri;
|
|
160
113
|
|
|
161
114
|
if (opts.noBrowser) {
|
|
162
115
|
console.error(`Open in your browser:\n ${chalk.blue.underline(verifyUrl)}`);
|
|
@@ -168,30 +121,18 @@ auth
|
|
|
168
121
|
console.error(`Could not open a browser. Open:\n ${chalk.blue.underline(verifyUrl)}`);
|
|
169
122
|
}
|
|
170
123
|
}
|
|
171
|
-
if (userCode) {
|
|
172
|
-
console.error(`If prompted, code: ${chalk.bold(userCode)}`);
|
|
173
|
-
}
|
|
124
|
+
if (userCode) console.error(`If prompted, code: ${chalk.bold(userCode)}`);
|
|
174
125
|
console.error(chalk.dim("\nWaiting for you to authorize on GitHub…\n"));
|
|
175
126
|
|
|
176
127
|
const final = await pollDeviceFlowUntilComplete(deviceCode, interval);
|
|
177
|
-
if (!final.access_token)
|
|
178
|
-
throw new Error("No access_token from GitHub");
|
|
179
|
-
}
|
|
128
|
+
if (!final.access_token) throw new Error("No access_token from GitHub");
|
|
180
129
|
const me = await savePat(final.access_token);
|
|
181
130
|
writeStoredGithubToken(final.access_token);
|
|
182
131
|
const tokenPath = getGithubTokenPath();
|
|
183
132
|
if (opts.json) {
|
|
184
|
-
|
|
185
|
-
login: me.login,
|
|
186
|
-
token_path: tokenPath,
|
|
187
|
-
saved: true
|
|
188
|
-
});
|
|
133
|
+
console.log(JSON.stringify({ login: me.login, token_path: tokenPath, saved: true }, null, 2));
|
|
189
134
|
} else {
|
|
190
|
-
console.error(
|
|
191
|
-
chalk.green(
|
|
192
|
-
`\nSigned in as ${chalk.bold(String(me.login))}. Token saved to ${chalk.cyan(tokenPath)}`
|
|
193
|
-
)
|
|
194
|
-
);
|
|
135
|
+
console.error(chalk.green(`\nSigned in as ${chalk.bold(String(me.login))}. Token saved to ${chalk.cyan(tokenPath)}`));
|
|
195
136
|
console.error(
|
|
196
137
|
chalk.dim(
|
|
197
138
|
"Future `nugit` runs will use this token automatically. " +
|
|
@@ -203,9 +144,7 @@ auth
|
|
|
203
144
|
|
|
204
145
|
auth
|
|
205
146
|
.command("poll")
|
|
206
|
-
.description(
|
|
207
|
-
"Complete GitHub device flow (polls until authorized); or --once for a single poll"
|
|
208
|
-
)
|
|
147
|
+
.description("Complete GitHub device flow (polls until authorized); or --once for a single poll")
|
|
209
148
|
.requiredOption("--device-code <code>", "device_code from nugit auth login")
|
|
210
149
|
.option("--interval <sec>", "Initial poll interval from login response", "5")
|
|
211
150
|
.option("--once", "Single poll only (manual retry)", false)
|
|
@@ -220,16 +159,9 @@ auth
|
|
|
220
159
|
return;
|
|
221
160
|
}
|
|
222
161
|
if (result.access_token) {
|
|
223
|
-
|
|
162
|
+
await savePat(result.access_token);
|
|
224
163
|
writeStoredGithubToken(result.access_token);
|
|
225
|
-
|
|
226
|
-
console.error(
|
|
227
|
-
`\nToken saved to ${chalk.cyan(getGithubTokenPath())} (signed in as ${chalk.bold(String(me.login))}).`
|
|
228
|
-
);
|
|
229
|
-
console.error(
|
|
230
|
-
chalk.dim("Optional — use env instead of file:\n") +
|
|
231
|
-
` export NUGIT_USER_TOKEN=${t}\n # or: export STACKPR_USER_TOKEN=${t}`
|
|
232
|
-
);
|
|
164
|
+
console.error(`\nToken saved to ${chalk.cyan(getGithubTokenPath())} (signed in as ${chalk.bold(String((await authMe()).login))}).`);
|
|
233
165
|
}
|
|
234
166
|
});
|
|
235
167
|
|
|
@@ -249,13 +181,10 @@ auth
|
|
|
249
181
|
.action(async (opts) => {
|
|
250
182
|
const result = await savePat(opts.token);
|
|
251
183
|
if (opts.json) {
|
|
252
|
-
|
|
184
|
+
console.log(JSON.stringify(result, null, 2));
|
|
253
185
|
} else {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (result.access_token) {
|
|
257
|
-
const t = JSON.stringify(result.access_token);
|
|
258
|
-
console.error(`\nexport NUGIT_USER_TOKEN=${t}\n# or: export STACKPR_USER_TOKEN=${t}`);
|
|
186
|
+
const login = result.login || result.me?.login;
|
|
187
|
+
console.log(login ? chalk.green(`Token valid — signed in as ${chalk.bold(String(login))}`) : chalk.green("Token valid"));
|
|
259
188
|
}
|
|
260
189
|
});
|
|
261
190
|
|
|
@@ -266,562 +195,90 @@ auth
|
|
|
266
195
|
.action(async (opts) => {
|
|
267
196
|
const me = await authMe();
|
|
268
197
|
if (opts.json) {
|
|
269
|
-
|
|
198
|
+
console.log(JSON.stringify(me, null, 2));
|
|
270
199
|
} else {
|
|
271
|
-
|
|
200
|
+
const login = me && typeof me.login === "string" ? me.login : "(unknown)";
|
|
201
|
+
console.log(login);
|
|
272
202
|
}
|
|
273
203
|
});
|
|
274
204
|
|
|
275
205
|
program.addCommand(auth);
|
|
276
206
|
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// config
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
|
|
277
211
|
const config = new Command("config").description(
|
|
278
|
-
"Persist
|
|
212
|
+
"Persist install root + .env path for `eval \"$(nugit env)\"`"
|
|
279
213
|
);
|
|
280
214
|
|
|
281
215
|
config
|
|
282
216
|
.command("init")
|
|
283
|
-
.description(
|
|
284
|
-
"Write ~/.config/nugit/config.json (defaults: this repo root + <root>/.env)"
|
|
285
|
-
)
|
|
217
|
+
.description("Write ~/.config/nugit/config.json (defaults: this repo root + <root>/.env)")
|
|
286
218
|
.option("--install-root <path>", "Nugit monorepo root (contains scripts/ and cli/)")
|
|
287
219
|
.option("--env-file <path>", "Dotenv file to load (default: <install-root>/.env)")
|
|
288
|
-
.option(
|
|
289
|
-
"--working-directory <path>",
|
|
290
|
-
"Default cwd when running `nugit start` (optional)"
|
|
291
|
-
)
|
|
220
|
+
.option("--working-directory <path>", "Default cwd when running `nugit start` (optional)")
|
|
292
221
|
.action(async (opts) => {
|
|
293
|
-
runConfigInit({
|
|
294
|
-
installRoot: opts.installRoot,
|
|
295
|
-
envFile: opts.envFile,
|
|
296
|
-
workingDirectory: opts.workingDirectory
|
|
297
|
-
});
|
|
222
|
+
runConfigInit({ installRoot: opts.installRoot, envFile: opts.envFile, workingDirectory: opts.workingDirectory });
|
|
298
223
|
});
|
|
299
224
|
|
|
300
225
|
config
|
|
301
226
|
.command("show")
|
|
302
227
|
.description("Print saved config JSON")
|
|
303
|
-
.action(() =>
|
|
304
|
-
runConfigShow();
|
|
305
|
-
});
|
|
228
|
+
.action(() => runConfigShow());
|
|
306
229
|
|
|
307
230
|
config
|
|
308
231
|
.command("path")
|
|
309
232
|
.description("Print path to config.json")
|
|
310
|
-
.action(() =>
|
|
311
|
-
console.log(getConfigPath());
|
|
312
|
-
});
|
|
233
|
+
.action(() => console.log(getConfigPath()));
|
|
313
234
|
|
|
314
235
|
config
|
|
315
236
|
.command("set")
|
|
316
237
|
.description("Set install-root, env-file, or working-directory")
|
|
317
238
|
.argument("<key>", "install-root | env-file | working-directory")
|
|
318
239
|
.argument("<value>", "path")
|
|
319
|
-
.action((key, value) =>
|
|
320
|
-
runConfigSet(key, value);
|
|
321
|
-
});
|
|
240
|
+
.action((key, value) => runConfigSet(key, value));
|
|
322
241
|
|
|
323
242
|
program.addCommand(config);
|
|
324
243
|
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// env
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
|
|
325
248
|
program
|
|
326
|
-
.command("
|
|
327
|
-
.description(
|
|
328
|
-
|
|
329
|
-
)
|
|
330
|
-
.option(
|
|
331
|
-
"-c, --command <string>",
|
|
332
|
-
"Run one command via shell -lc instead of opening an interactive shell"
|
|
333
|
-
)
|
|
334
|
-
.option(
|
|
335
|
-
"--shell",
|
|
336
|
-
"Open the configured shell immediately (skip the TTY hub menu: stack view / split / shell)",
|
|
337
|
-
false
|
|
338
|
-
)
|
|
249
|
+
.command("env")
|
|
250
|
+
.description("Print export lines from saved config — bash/zsh: eval \"$(nugit env)\"")
|
|
251
|
+
.option("--fish", "Emit fish `set -gx` instead of sh export", false)
|
|
339
252
|
.action(async (opts) => {
|
|
340
|
-
|
|
341
|
-
runStart({ command: opts.command });
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
const tty = process.stdin.isTTY && process.stdout.isTTY;
|
|
345
|
-
if (opts.shell || !tty) {
|
|
346
|
-
runStart({});
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
await runStartHub();
|
|
253
|
+
runEnvExport(opts.fish ? "fish" : "bash");
|
|
350
254
|
});
|
|
351
255
|
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
// split
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
|
|
352
260
|
program
|
|
353
261
|
.command("split")
|
|
354
262
|
.description(
|
|
355
|
-
"Split one PR into layered branches and new GitHub PRs (TUI assigns files to layers
|
|
263
|
+
"Split one PR into layered branches and new GitHub PRs (TUI assigns files to layers)"
|
|
356
264
|
)
|
|
357
265
|
.requiredOption("--pr <n>", "PR number to split")
|
|
358
266
|
.option("--dry-run", "Materialize local branches only; do not push or create PRs", false)
|
|
359
267
|
.option("--remote <name>", "Git remote name", "origin")
|
|
360
268
|
.action(async (opts) => {
|
|
361
269
|
const root = findGitRoot();
|
|
362
|
-
if (!root)
|
|
363
|
-
throw new Error("Not inside a git repository");
|
|
364
|
-
}
|
|
270
|
+
if (!root) throw new Error("Not inside a git repository");
|
|
365
271
|
const repoFull = getRepoFullNameFromGitRoot(root);
|
|
366
272
|
const { owner, repo: repoName } = parseRepoFullName(repoFull);
|
|
367
273
|
const n = Number.parseInt(String(opts.pr), 10);
|
|
368
|
-
if (!Number.isFinite(n) || n < 1)
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
await runSplitCommand({
|
|
372
|
-
root,
|
|
373
|
-
owner,
|
|
374
|
-
repo: repoName,
|
|
375
|
-
prNumber: n,
|
|
376
|
-
dryRun: opts.dryRun,
|
|
377
|
-
remote: opts.remote
|
|
378
|
-
});
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
program
|
|
382
|
-
.command("env")
|
|
383
|
-
.description(
|
|
384
|
-
"Print export lines from saved config — bash/zsh: eval \"$(nugit env)\""
|
|
385
|
-
)
|
|
386
|
-
.option("--fish", "Emit fish `set -gx` instead of sh export", false)
|
|
387
|
-
.action(async (opts) => {
|
|
388
|
-
runEnvExport(opts.fish ? "fish" : "bash");
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
const prs = new Command("prs").description("Pull requests");
|
|
392
|
-
|
|
393
|
-
prs
|
|
394
|
-
.command("list")
|
|
395
|
-
.description(
|
|
396
|
-
"List open PRs in the repo (default: origin from cwd), paginated — use numbers with nugit stack add. Use --mine for only your PRs."
|
|
397
|
-
)
|
|
398
|
-
.option("--repo <owner/repo>", "Repository (default: github.com remote from current git repo)")
|
|
399
|
-
.option("--mine", "Only PRs authored by you (GitHub search)", false)
|
|
400
|
-
.option("--page <n>", "Page number (1-based)", "1")
|
|
401
|
-
.option("--per-page <n>", "Results per page (max 100)", "20")
|
|
402
|
-
.option("--json", "Print raw API response", false)
|
|
403
|
-
.action(async (opts) => {
|
|
404
|
-
const page = Number.parseInt(String(opts.page), 10) || 1;
|
|
405
|
-
const perPage = Math.min(100, Math.max(1, Number.parseInt(String(opts.perPage), 10) || 20));
|
|
406
|
-
const root = findGitRoot();
|
|
407
|
-
const repoFull =
|
|
408
|
-
opts.repo || (root ? getRepoFullNameFromGitRoot(root) : null);
|
|
409
|
-
if (!repoFull) {
|
|
410
|
-
throw new Error("Pass --repo owner/repo or run inside a git clone with a github.com origin");
|
|
411
|
-
}
|
|
412
|
-
const { owner, repo } = parseRepoFullName(repoFull);
|
|
413
|
-
|
|
414
|
-
if (opts.mine) {
|
|
415
|
-
const result = await listMyPulls({
|
|
416
|
-
repo: repoFull,
|
|
417
|
-
page,
|
|
418
|
-
perPage
|
|
419
|
-
});
|
|
420
|
-
if (opts.json) {
|
|
421
|
-
printJson(result);
|
|
422
|
-
} else {
|
|
423
|
-
console.log(
|
|
424
|
-
formatPrSearchHuman(/** @type {{ total_count?: number, items?: unknown[] }} */ (result), {
|
|
425
|
-
page,
|
|
426
|
-
perPage
|
|
427
|
-
})
|
|
428
|
-
);
|
|
429
|
-
}
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const result = await listOpenPullsInRepo(owner, repo, { page, perPage });
|
|
434
|
-
if (opts.json) {
|
|
435
|
-
printJson(result);
|
|
436
|
-
} else {
|
|
437
|
-
console.log(
|
|
438
|
-
formatOpenPullsHuman(
|
|
439
|
-
/** @type {{ pulls: unknown[], page: number, per_page: number, repo_full_name: string, has_more: boolean }} */ (
|
|
440
|
-
result
|
|
441
|
-
)
|
|
442
|
-
)
|
|
443
|
-
);
|
|
444
|
-
}
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
prs
|
|
448
|
-
.command("create")
|
|
449
|
-
.description("Open a GitHub PR (push the head branch first). Repo defaults to origin.")
|
|
450
|
-
.requiredOption("--head <branch>", "Head branch name (exists on GitHub)")
|
|
451
|
-
.requiredOption("--title <title>", "PR title")
|
|
452
|
-
.option("--base <branch>", "Base branch (default: repo default_branch from GitHub)")
|
|
453
|
-
.option("--body <markdown>", "PR body")
|
|
454
|
-
.option("--repo <owner/repo>", "Override; default from git remote origin")
|
|
455
|
-
.option("--draft", "Create as draft", false)
|
|
456
|
-
.option("--json", "Print full API response", false)
|
|
457
|
-
.action(async (opts) => {
|
|
458
|
-
const root = findGitRoot();
|
|
459
|
-
if (!opts.repo && !root) {
|
|
460
|
-
throw new Error("Not in a git repo: pass --repo owner/repo or run from a clone");
|
|
461
|
-
}
|
|
462
|
-
const repoFull = opts.repo || getRepoFullNameFromGitRoot(root);
|
|
463
|
-
const { owner, repo } = parseRepoFullName(repoFull);
|
|
464
|
-
let base = opts.base;
|
|
465
|
-
if (!base) {
|
|
466
|
-
const meta = await getRepoMetadata(owner, repo);
|
|
467
|
-
base = meta.default_branch;
|
|
468
|
-
if (!base) {
|
|
469
|
-
throw new Error("Could not determine default branch; pass --base");
|
|
470
|
-
}
|
|
471
|
-
console.error(`Using base branch: ${base}`);
|
|
472
|
-
}
|
|
473
|
-
const created = await createPullRequest(owner, repo, {
|
|
474
|
-
title: opts.title,
|
|
475
|
-
head: opts.head,
|
|
476
|
-
base,
|
|
477
|
-
body: opts.body,
|
|
478
|
-
draft: opts.draft
|
|
479
|
-
});
|
|
480
|
-
if (opts.json) {
|
|
481
|
-
printJson(created);
|
|
482
|
-
} else {
|
|
483
|
-
console.log(formatPrCreatedHuman(/** @type {Record<string, unknown>} */ (created)));
|
|
484
|
-
}
|
|
485
|
-
if (created.number) {
|
|
486
|
-
console.error(`\nAdd to stack: nugit stack add --pr ${created.number}`);
|
|
487
|
-
}
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
program.addCommand(prs);
|
|
491
|
-
|
|
492
|
-
const stack = new Command("stack").description("Local .nugit/stack.json");
|
|
493
|
-
|
|
494
|
-
stack
|
|
495
|
-
.command("show")
|
|
496
|
-
.description("Print local .nugit/stack.json")
|
|
497
|
-
.option("--json", "Raw JSON", false)
|
|
498
|
-
.action(async (opts) => {
|
|
499
|
-
const root = findGitRoot();
|
|
500
|
-
if (!root) {
|
|
501
|
-
throw new Error("Not inside a git repository");
|
|
502
|
-
}
|
|
503
|
-
const doc = readStackFile(root);
|
|
504
|
-
if (!doc) {
|
|
505
|
-
throw new Error("No .nugit/stack.json in this repo");
|
|
506
|
-
}
|
|
507
|
-
validateStackDoc(doc);
|
|
508
|
-
if (opts.json) {
|
|
509
|
-
printJson(doc);
|
|
510
|
-
} else {
|
|
511
|
-
console.log(formatStackDocHuman(/** @type {Record<string, unknown>} */ (doc)));
|
|
512
|
-
}
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
stack
|
|
516
|
-
.command("fetch")
|
|
517
|
-
.description("Fetch .nugit/stack.json from GitHub (needs token)")
|
|
518
|
-
.option("--repo <owner/repo>", "Default: git remote origin when run inside a repo")
|
|
519
|
-
.option("--ref <ref>", "branch or sha", "")
|
|
520
|
-
.option("--json", "Raw JSON", false)
|
|
521
|
-
.action(async (opts) => {
|
|
522
|
-
const root = findGitRoot();
|
|
523
|
-
const repoFull =
|
|
524
|
-
opts.repo || (root ? getRepoFullNameFromGitRoot(root) : null);
|
|
525
|
-
if (!repoFull) {
|
|
526
|
-
throw new Error("Pass --repo owner/repo or run from a git clone with github.com origin");
|
|
527
|
-
}
|
|
528
|
-
const doc = await fetchRemoteStackJson(repoFull, opts.ref || undefined);
|
|
529
|
-
validateStackDoc(doc);
|
|
530
|
-
if (opts.json) {
|
|
531
|
-
printJson(doc);
|
|
532
|
-
} else {
|
|
533
|
-
console.log(formatStackDocHuman(/** @type {Record<string, unknown>} */ (doc)));
|
|
534
|
-
}
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
stack
|
|
538
|
-
.command("enrich")
|
|
539
|
-
.description("Print stack with PR titles from GitHub (local file + API)")
|
|
540
|
-
.option("--json", "Raw JSON", false)
|
|
541
|
-
.action(async (opts) => {
|
|
542
|
-
const root = findGitRoot();
|
|
543
|
-
if (!root) {
|
|
544
|
-
throw new Error("Not inside a git repository");
|
|
545
|
-
}
|
|
546
|
-
const doc = readStackFile(root);
|
|
547
|
-
if (!doc) {
|
|
548
|
-
throw new Error("No .nugit/stack.json");
|
|
549
|
-
}
|
|
550
|
-
validateStackDoc(doc);
|
|
551
|
-
const { owner, repo } = parseRepoFullName(doc.repo_full_name);
|
|
552
|
-
const ordered = [...doc.prs].sort((a, b) => a.position - b.position);
|
|
553
|
-
const out = [];
|
|
554
|
-
for (const pr of ordered) {
|
|
555
|
-
try {
|
|
556
|
-
const g = await getPull(owner, repo, pr.pr_number);
|
|
557
|
-
out.push({
|
|
558
|
-
...pr,
|
|
559
|
-
title: g.title,
|
|
560
|
-
html_url: g.html_url,
|
|
561
|
-
state: g.state
|
|
562
|
-
});
|
|
563
|
-
} catch (e) {
|
|
564
|
-
out.push({ ...pr, error: String(e.message || e) });
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
if (opts.json) {
|
|
568
|
-
printJson({ ...doc, prs: out });
|
|
569
|
-
} else {
|
|
570
|
-
console.log(
|
|
571
|
-
formatStackEnrichHuman(/** @type {Record<string, unknown>} */ (doc), /** @type {Record<string, unknown>[]} */ (out))
|
|
572
|
-
);
|
|
573
|
-
}
|
|
574
|
-
});
|
|
575
|
-
|
|
576
|
-
stack
|
|
577
|
-
.command("list")
|
|
578
|
-
.description(
|
|
579
|
-
"Discover nugit stacks in the repo: scan open PR heads for .nugit/stack.json, dedupe by stack tip (for review / triage)"
|
|
580
|
-
)
|
|
581
|
-
.option("--repo <owner/repo>", "Default: github.com origin from cwd")
|
|
582
|
-
.option(
|
|
583
|
-
"--max-open-prs <n>",
|
|
584
|
-
"Max open PRs to scan (0 = all pages). Default: config / discovery mode"
|
|
585
|
-
)
|
|
586
|
-
.option(
|
|
587
|
-
"--fetch-concurrency <n>",
|
|
588
|
-
"Parallel GitHub API calls. Default: config (see stack-discovery-fetch-concurrency)"
|
|
589
|
-
)
|
|
590
|
-
.option(
|
|
591
|
-
"--full",
|
|
592
|
-
"Full scan for lazy mode (same as NUGIT_STACK_DISCOVERY_FULL=1)",
|
|
593
|
-
false
|
|
594
|
-
)
|
|
595
|
-
.option("--no-enrich", "Skip loading PR titles from the API (faster)", false)
|
|
596
|
-
.option("--json", "Machine-readable result", false)
|
|
597
|
-
.action(async (opts) => {
|
|
598
|
-
const root = findGitRoot();
|
|
599
|
-
const repoFull =
|
|
600
|
-
opts.repo || (root ? getRepoFullNameFromGitRoot(root) : null);
|
|
601
|
-
if (!repoFull) {
|
|
602
|
-
throw new Error(
|
|
603
|
-
"Pass --repo owner/repo or run inside a git clone with github.com origin"
|
|
604
|
-
);
|
|
605
|
-
}
|
|
606
|
-
const { owner, repo } = parseRepoFullName(repoFull);
|
|
607
|
-
const discovery = getStackDiscoveryOpts();
|
|
608
|
-
const maxOpenPrs =
|
|
609
|
-
opts.maxOpenPrs != null && String(opts.maxOpenPrs).length
|
|
610
|
-
? Number.parseInt(String(opts.maxOpenPrs), 10)
|
|
611
|
-
: effectiveMaxOpenPrs(discovery, opts.full);
|
|
612
|
-
const fetchConcurrency =
|
|
613
|
-
opts.fetchConcurrency != null && String(opts.fetchConcurrency).length
|
|
614
|
-
? Math.max(1, Math.min(32, Number.parseInt(String(opts.fetchConcurrency), 10) || 8))
|
|
615
|
-
: discovery.fetchConcurrency;
|
|
616
|
-
const result = await discoverStacksInRepo(owner, repo, {
|
|
617
|
-
maxOpenPrs: Number.isNaN(maxOpenPrs) ? discovery.maxOpenPrs : maxOpenPrs,
|
|
618
|
-
enrich: !opts.noEnrich,
|
|
619
|
-
fetchConcurrency
|
|
620
|
-
});
|
|
621
|
-
if (opts.json) {
|
|
622
|
-
printJson(result);
|
|
623
|
-
} else {
|
|
624
|
-
console.log(formatStacksListHuman(result));
|
|
625
|
-
}
|
|
274
|
+
if (!Number.isFinite(n) || n < 1) throw new Error("Invalid --pr");
|
|
275
|
+
const { runSplitCommand } = await import("./split-view/run-split.js");
|
|
276
|
+
await runSplitCommand({ root, owner, repo: repoName, prNumber: n, dryRun: opts.dryRun, remote: opts.remote });
|
|
626
277
|
});
|
|
627
278
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
.option("--repo <owner/repo>", "Default: github.com origin from cwd")
|
|
632
|
-
.option("--max-open-prs <n>", "Max open PRs to scan (default: config)")
|
|
633
|
-
.option("--no-enrich", "Skip PR title fetch", false)
|
|
634
|
-
.action(async (opts) => {
|
|
635
|
-
const root = findGitRoot();
|
|
636
|
-
if (!root) {
|
|
637
|
-
throw new Error("Not inside a git repository");
|
|
638
|
-
}
|
|
639
|
-
const repoFull =
|
|
640
|
-
opts.repo || (root ? getRepoFullNameFromGitRoot(root) : null);
|
|
641
|
-
if (!repoFull) {
|
|
642
|
-
throw new Error("Pass --repo owner/repo or run from a clone with github.com origin");
|
|
643
|
-
}
|
|
644
|
-
const { owner, repo } = parseRepoFullName(repoFull);
|
|
645
|
-
const discovery = getStackDiscoveryOpts();
|
|
646
|
-
const max =
|
|
647
|
-
opts.maxOpenPrs != null && String(opts.maxOpenPrs).length
|
|
648
|
-
? Number.parseInt(String(opts.maxOpenPrs), 10)
|
|
649
|
-
: discovery.maxOpenPrs;
|
|
650
|
-
const result = await discoverStacksInRepo(owner, repo, {
|
|
651
|
-
maxOpenPrs: Number.isNaN(max) ? discovery.maxOpenPrs : max,
|
|
652
|
-
enrich: !opts.noEnrich,
|
|
653
|
-
fetchConcurrency: discovery.fetchConcurrency
|
|
654
|
-
});
|
|
655
|
-
writeStackIndex(root, result);
|
|
656
|
-
console.error(`Wrote ${root}/.nugit/stack-index.json (${result.stacks_found} stack(s))`);
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
stack
|
|
660
|
-
.command("graph")
|
|
661
|
-
.description("Print compiled stack graph from stack-index.json + .nugit/stack-history.jsonl")
|
|
662
|
-
.option("--live", "Rediscover from GitHub if index missing", false)
|
|
663
|
-
.option("--json", "Machine-readable", false)
|
|
664
|
-
.action(async (opts) => {
|
|
665
|
-
const root = findGitRoot();
|
|
666
|
-
if (!root) {
|
|
667
|
-
throw new Error("Not inside a git repository");
|
|
668
|
-
}
|
|
669
|
-
const repoFull = getRepoFullNameFromGitRoot(root);
|
|
670
|
-
let discovered = tryLoadStackIndex(root, repoFull);
|
|
671
|
-
if (!discovered && opts.live) {
|
|
672
|
-
const { owner, repo } = parseRepoFullName(repoFull);
|
|
673
|
-
const d = getStackDiscoveryOpts();
|
|
674
|
-
discovered = await discoverStacksInRepo(owner, repo, {
|
|
675
|
-
maxOpenPrs: d.maxOpenPrs,
|
|
676
|
-
enrich: false,
|
|
677
|
-
fetchConcurrency: d.fetchConcurrency
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
if (!discovered) {
|
|
681
|
-
throw new Error("No stack-index.json — run: nugit stack index (or use --live)");
|
|
682
|
-
}
|
|
683
|
-
const hist = readStackHistoryLines(root);
|
|
684
|
-
const graph = compileStackGraph(discovered, hist);
|
|
685
|
-
if (opts.json) {
|
|
686
|
-
printJson(graph);
|
|
687
|
-
} else {
|
|
688
|
-
console.log(JSON.stringify(graph, null, 2));
|
|
689
|
-
}
|
|
690
|
-
});
|
|
691
|
-
|
|
692
|
-
stack
|
|
693
|
-
.command("add")
|
|
694
|
-
.description("Append one or more PRs to the stack (metadata from GitHub), bottom→top order")
|
|
695
|
-
.requiredOption(
|
|
696
|
-
"--pr <n...>",
|
|
697
|
-
"Pull request number(s): stack order bottom first — space- or comma-separated, or repeat --pr"
|
|
698
|
-
)
|
|
699
|
-
.option("--json", "Print entries as JSON", false)
|
|
700
|
-
.action(async (opts) => {
|
|
701
|
-
const root = findGitRoot();
|
|
702
|
-
if (!root) {
|
|
703
|
-
throw new Error("Not inside a git repository");
|
|
704
|
-
}
|
|
705
|
-
const doc = readStackFile(root);
|
|
706
|
-
if (!doc) {
|
|
707
|
-
throw new Error("No .nugit/stack.json — run nugit init first");
|
|
708
|
-
}
|
|
709
|
-
validateStackDoc(doc);
|
|
710
|
-
const prNums = parseStackAddPrNumbers(opts.pr);
|
|
711
|
-
const { owner, repo } = parseRepoFullName(doc.repo_full_name);
|
|
712
|
-
/** @type {ReturnType<typeof stackEntryFromGithubPull>[]} */
|
|
713
|
-
const added = [];
|
|
714
|
-
for (const prNum of prNums) {
|
|
715
|
-
if (doc.prs.some((p) => p.pr_number === prNum)) {
|
|
716
|
-
throw new Error(`PR #${prNum} is already in the stack`);
|
|
717
|
-
}
|
|
718
|
-
const pull = await getPull(owner, repo, prNum);
|
|
719
|
-
const position = nextStackPosition(doc.prs);
|
|
720
|
-
const entry = stackEntryFromGithubPull(pull, position);
|
|
721
|
-
doc.prs.push(entry);
|
|
722
|
-
added.push(entry);
|
|
723
|
-
}
|
|
724
|
-
writeStackFile(root, doc);
|
|
725
|
-
if (opts.json) {
|
|
726
|
-
printJson(added.length === 1 ? added[0] : added);
|
|
727
|
-
} else {
|
|
728
|
-
for (const e of added) {
|
|
729
|
-
console.log(
|
|
730
|
-
chalk.bold(`PR #${e.pr_number}`) +
|
|
731
|
-
chalk.dim(` ${e.head_branch} ← ${e.base_branch}`)
|
|
732
|
-
);
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
console.error(
|
|
736
|
-
`\nUpdated stack (${doc.prs.length} PRs). Run \`nugit stack propagate --push\` to commit the stack on the tip if needed, write prefix metadata on each head, merge lower→upper, and push.`
|
|
737
|
-
);
|
|
738
|
-
});
|
|
739
|
-
|
|
740
|
-
function addPropagateOptions(cmd) {
|
|
741
|
-
return cmd
|
|
742
|
-
.option(
|
|
743
|
-
"-m, --message <msg>",
|
|
744
|
-
"Commit message for each branch",
|
|
745
|
-
"nugit: propagate stack metadata"
|
|
746
|
-
)
|
|
747
|
-
.option("--push", "Run git push for each head after committing", false)
|
|
748
|
-
.option("--dry-run", "Print git actions without changing branches or committing", false)
|
|
749
|
-
.option("--remote <name>", "Remote name (default: origin)", "origin")
|
|
750
|
-
.option(
|
|
751
|
-
"--no-merge-lower",
|
|
752
|
-
"Do not merge each lower stacked head into the current head before writing stack.json (can break PR chains; not recommended)",
|
|
753
|
-
false
|
|
754
|
-
)
|
|
755
|
-
.option(
|
|
756
|
-
"--no-bootstrap",
|
|
757
|
-
"Do not auto-commit a dirty .nugit/stack.json on the tip before propagating (you must commit it yourself)",
|
|
758
|
-
false
|
|
759
|
-
);
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
stack
|
|
763
|
-
.command("view")
|
|
764
|
-
.description(
|
|
765
|
-
"Interactive stack viewer (GitHub API): PR chain, comments, open links, reply, request reviewers"
|
|
766
|
-
)
|
|
767
|
-
.option("--no-tui", "Print stack + comment counts to stdout (no Ink UI)", false)
|
|
768
|
-
.option("--repo <owner/repo>", "With --ref: load stack from GitHub instead of local file")
|
|
769
|
-
.option("--ref <branch>", "Branch/sha for .nugit/stack.json on GitHub")
|
|
770
|
-
.option("--file <path>", "Path to stack.json (skip local .nugit lookup)")
|
|
771
|
-
.action(async (opts) => {
|
|
772
|
-
await runStackViewCommand({
|
|
773
|
-
noTui: opts.noTui,
|
|
774
|
-
repo: opts.repo,
|
|
775
|
-
ref: opts.ref,
|
|
776
|
-
file: opts.file
|
|
777
|
-
});
|
|
778
|
-
});
|
|
779
|
-
|
|
780
|
-
addPropagateOptions(
|
|
781
|
-
stack
|
|
782
|
-
.command("propagate")
|
|
783
|
-
.description(
|
|
784
|
-
"Commit .nugit/stack.json on each stacked head: prs prefix through that layer, plus layer (with tip). Auto-commits tip stack file if it is the only dirty path; merges each lower head into the next so PRs stay mergeable."
|
|
785
|
-
)
|
|
786
|
-
).action(async (opts) => {
|
|
787
|
-
const root = findGitRoot();
|
|
788
|
-
if (!root) {
|
|
789
|
-
throw new Error("Not inside a git repository");
|
|
790
|
-
}
|
|
791
|
-
await runStackPropagate({
|
|
792
|
-
root,
|
|
793
|
-
message: opts.message,
|
|
794
|
-
push: opts.push,
|
|
795
|
-
dryRun: opts.dryRun,
|
|
796
|
-
remote: opts.remote,
|
|
797
|
-
noMergeLower: opts.noMergeLower,
|
|
798
|
-
bootstrapCommit: !opts.noBootstrap
|
|
799
|
-
});
|
|
800
|
-
});
|
|
801
|
-
|
|
802
|
-
addPropagateOptions(
|
|
803
|
-
stack
|
|
804
|
-
.command("commit")
|
|
805
|
-
.description("Alias for `nugit stack propagate` — prefix stack metadata on each stacked head")
|
|
806
|
-
).action(async (opts) => {
|
|
807
|
-
const root = findGitRoot();
|
|
808
|
-
if (!root) {
|
|
809
|
-
throw new Error("Not inside a git repository");
|
|
810
|
-
}
|
|
811
|
-
await runStackPropagate({
|
|
812
|
-
root,
|
|
813
|
-
message: opts.message,
|
|
814
|
-
push: opts.push,
|
|
815
|
-
dryRun: opts.dryRun,
|
|
816
|
-
remote: opts.remote,
|
|
817
|
-
noMergeLower: opts.noMergeLower,
|
|
818
|
-
bootstrapCommit: !opts.noBootstrap
|
|
819
|
-
});
|
|
820
|
-
});
|
|
821
|
-
|
|
822
|
-
registerStackExtraCommands(stack);
|
|
823
|
-
|
|
824
|
-
program.addCommand(stack);
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// Parse
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
825
282
|
|
|
826
283
|
program.parseAsync().catch((error) => {
|
|
827
284
|
console.error(error.message || error);
|