nugit-cli 0.0.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.
@@ -0,0 +1,211 @@
1
+ import { spawn } from "child_process";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import {
5
+ buildStartEnv,
6
+ expandUserPath,
7
+ getConfigPath,
8
+ inferMonorepoRootFromCli,
9
+ loadEnvFile,
10
+ mergeNugitPath,
11
+ readUserConfig,
12
+ writeUserConfig
13
+ } from "./user-config.js";
14
+ import { findGitRoot, parseRepoFullName } from "./nugit-stack.js";
15
+ import { getRepoFullNameFromGitRoot } from "./git-info.js";
16
+ import { questionLine } from "./stack-view/prompt-line.js";
17
+
18
+ /**
19
+ * @param {object} opts
20
+ * @param {string} [opts.installRoot]
21
+ * @param {string} [opts.envFile]
22
+ * @param {string} [opts.workingDirectory]
23
+ */
24
+ export function runConfigInit(opts) {
25
+ const root = path.resolve(
26
+ expandUserPath(opts.installRoot || inferMonorepoRootFromCli())
27
+ );
28
+ const defaultEnv = path.join(root, ".env");
29
+ const envFile = expandUserPath(opts.envFile || defaultEnv);
30
+ const cfg = { installRoot: root, envFile };
31
+ if (opts.workingDirectory) {
32
+ cfg.workingDirectory = expandUserPath(opts.workingDirectory);
33
+ }
34
+ writeUserConfig(cfg);
35
+ console.error(`Wrote ${getConfigPath()}`);
36
+ console.error(` installRoot: ${root}`);
37
+ console.error(` envFile: ${envFile}`);
38
+ if (!fs.existsSync(envFile)) {
39
+ console.error(` (env file does not exist yet — create it or run: nugit config set env-file <path>)`);
40
+ }
41
+ if (cfg.workingDirectory) {
42
+ console.error(` workingDirectory: ${cfg.workingDirectory}`);
43
+ }
44
+ }
45
+
46
+ export function runConfigShow() {
47
+ const c = readUserConfig();
48
+ console.log(JSON.stringify(c, null, 2));
49
+ }
50
+
51
+ /**
52
+ * @param {string} key
53
+ * @param {string} value
54
+ */
55
+ export function runConfigSet(key, value) {
56
+ const c = readUserConfig();
57
+ const k = key.toLowerCase().replace(/_/g, "-");
58
+ if (k === "install-root") {
59
+ c.installRoot = path.resolve(expandUserPath(value));
60
+ } else if (k === "env-file") {
61
+ c.envFile = expandUserPath(value);
62
+ } else if (k === "working-directory" || k === "cwd") {
63
+ c.workingDirectory = expandUserPath(value);
64
+ } else if (k === "stack-discovery-mode") {
65
+ c.stackDiscovery = { ...(c.stackDiscovery || {}), mode: value };
66
+ } else if (k === "stack-discovery-max-open-prs") {
67
+ c.stackDiscovery = {
68
+ ...(c.stackDiscovery || {}),
69
+ maxOpenPrs: Number.parseInt(value, 10)
70
+ };
71
+ } else if (k === "stack-discovery-fetch-concurrency") {
72
+ c.stackDiscovery = {
73
+ ...(c.stackDiscovery || {}),
74
+ fetchConcurrency: Number.parseInt(value, 10)
75
+ };
76
+ } else if (k === "stack-discovery-background") {
77
+ c.stackDiscovery = {
78
+ ...(c.stackDiscovery || {}),
79
+ background: value === "true" || value === "1"
80
+ };
81
+ } else if (k === "stack-discovery-lazy-first-pass-max-prs") {
82
+ c.stackDiscovery = {
83
+ ...(c.stackDiscovery || {}),
84
+ lazyFirstPassMaxPrs: Number.parseInt(value, 10)
85
+ };
86
+ } else {
87
+ throw new Error(
88
+ `Unknown key "${key}". Use: install-root | env-file | working-directory | stack-discovery-mode | stack-discovery-max-open-prs | stack-discovery-fetch-concurrency | stack-discovery-background | stack-discovery-lazy-first-pass-max-prs`
89
+ );
90
+ }
91
+ writeUserConfig(c);
92
+ console.error(`Updated ${getConfigPath()}`);
93
+ }
94
+
95
+ /**
96
+ * Shell-escape for single-quoted POSIX strings.
97
+ * @param {string} s
98
+ */
99
+ function shellQuoteExport(s) {
100
+ return `'${String(s).replace(/'/g, `'\\''`)}'`;
101
+ }
102
+
103
+ /**
104
+ * Print eval-able export lines (bash/zsh).
105
+ * @param {'bash' | 'fish'} style
106
+ */
107
+ export function runEnvExport(style = "bash") {
108
+ const cfg = readUserConfig();
109
+ if (!cfg.installRoot || !cfg.envFile) {
110
+ throw new Error("Run `nugit config init` first (or set install-root and env-file).");
111
+ }
112
+ const root = path.resolve(expandUserPath(cfg.installRoot));
113
+ const { vars, pathUsed } = loadEnvFile(cfg.envFile);
114
+ const merged = mergeNugitPath(
115
+ {
116
+ ...process.env,
117
+ ...vars,
118
+ NUGIT_MONOREPO_ROOT: root,
119
+ NUGIT_ENV_FILE: pathUsed
120
+ },
121
+ root
122
+ );
123
+
124
+ if (style === "fish") {
125
+ for (const [k, v] of Object.entries(vars)) {
126
+ console.log(`set -gx ${k} ${JSON.stringify(v)}`);
127
+ }
128
+ console.log(`set -gx NUGIT_MONOREPO_ROOT ${JSON.stringify(root)}`);
129
+ console.log(`set -gx NUGIT_ENV_FILE ${JSON.stringify(pathUsed)}`);
130
+ if (merged.PATH && merged.PATH !== process.env.PATH) {
131
+ console.log(`set -gx PATH ${JSON.stringify(merged.PATH)}`);
132
+ }
133
+ return;
134
+ }
135
+
136
+ for (const [k, v] of Object.entries(vars)) {
137
+ console.log(`export ${k}=${shellQuoteExport(v)}`);
138
+ }
139
+ console.log(`export NUGIT_MONOREPO_ROOT=${shellQuoteExport(root)}`);
140
+ console.log(`export NUGIT_ENV_FILE=${shellQuoteExport(pathUsed)}`);
141
+ if (merged.PATH && merged.PATH !== process.env.PATH) {
142
+ console.log(`export PATH=${shellQuoteExport(merged.PATH)}`);
143
+ }
144
+ }
145
+
146
+ /**
147
+ * @param {object} opts
148
+ * @param {string} [opts.command] shell command string for -lc
149
+ */
150
+ export function runStart(opts) {
151
+ const cfg = readUserConfig();
152
+ if (!cfg.installRoot || !cfg.envFile) {
153
+ throw new Error(
154
+ "No saved config. Run:\n nugit config init\n # then: nugit start"
155
+ );
156
+ }
157
+ const env = buildStartEnv(cfg);
158
+ const cwd = cfg.workingDirectory
159
+ ? expandUserPath(cfg.workingDirectory)
160
+ : process.cwd();
161
+ const shell = process.env.SHELL || "/bin/bash";
162
+ const cmd = opts.command;
163
+ const args = cmd ? ["-lc", cmd] : ["-i"];
164
+ const child = spawn(shell, args, {
165
+ env,
166
+ cwd,
167
+ stdio: "inherit"
168
+ });
169
+ child.on("exit", (code, signal) => {
170
+ if (signal) {
171
+ process.exit(1);
172
+ }
173
+ process.exit(code ?? 0);
174
+ });
175
+ }
176
+
177
+ /**
178
+ * Interactive menu when stdin/stdout are TTY. Options 1–2 run then exit; 3 spawns the shell (same as plain `nugit start` without a menu).
179
+ */
180
+ export async function runStartHub() {
181
+ console.error("nugit start — choose:");
182
+ console.error(" 1) Stack view");
183
+ console.error(" 2) Split a PR");
184
+ console.error(" 3) Open shell");
185
+ const ans = (await questionLine("Enter 1–3 [3]: ")).trim();
186
+ const choice = ans || "3";
187
+ if (choice === "1") {
188
+ const { runStackViewCommand } = await import("./stack-view/run-stack-view.js");
189
+ await runStackViewCommand({});
190
+ process.exit(0);
191
+ }
192
+ if (choice === "2") {
193
+ const raw = (await questionLine("PR number to split: ")).trim();
194
+ const n = Number.parseInt(raw, 10);
195
+ if (!Number.isFinite(n) || n < 1) {
196
+ console.error("Invalid PR number.");
197
+ process.exit(1);
198
+ }
199
+ const root = findGitRoot();
200
+ if (!root) {
201
+ console.error("Not inside a git repository.");
202
+ process.exit(1);
203
+ }
204
+ const repoFull = getRepoFullNameFromGitRoot(root);
205
+ const { owner, repo: repoName } = parseRepoFullName(repoFull);
206
+ const { runSplitCommand } = await import("./split-view/run-split.js");
207
+ await runSplitCommand({ root, owner, repo: repoName, prNumber: n });
208
+ process.exit(0);
209
+ }
210
+ runStart({});
211
+ }