gh-stack 0.17.2 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +256 -13
- package/package.json +9 -8
- package/src/commands/add.ts +131 -3
- package/src/commands/config.ts +133 -0
- package/src/commands/init.ts +4 -0
- package/src/commands/join.ts +877 -0
- package/src/commands/learn.ts +295 -36
- package/src/commands/merge.ts +263 -5
- package/src/commands/restack.ts +954 -211
- package/src/commands/submit.ts +257 -51
- package/src/commands/sync.ts +145 -145
- package/src/commands/undo.ts +11 -0
- package/src/commands/update-prs.ts +87 -15
- package/src/index.ts +155 -125
- package/src/lib/advance.ts +45 -9
- package/src/lib/chain.ts +20 -0
- package/src/lib/collapse-guard.ts +325 -0
- package/src/lib/config.ts +259 -0
- package/src/lib/git.ts +98 -22
- package/src/lib/github.ts +83 -5
- package/src/lib/metadata.ts +34 -7
- package/src/lib/output.ts +64 -14
- package/src/lib/paths.ts +1 -1
- package/src/lib/remote-guard.ts +281 -0
- package/src/lib/run-log.ts +117 -0
- package/src/lib/using-gh-stack.md +140 -5
- package/src/types.ts +62 -2
package/src/lib/git.ts
CHANGED
|
@@ -143,30 +143,92 @@ export async function isAncestor(ancestor: string, descendant: string): Promise<
|
|
|
143
143
|
return exitCode === 0;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
146
|
+
export type PushResult =
|
|
147
|
+
| { ok: true }
|
|
148
|
+
| {
|
|
149
|
+
ok: false;
|
|
150
|
+
/**
|
|
151
|
+
* `not-integrated`: origin's tip was fetched but isn't in the local
|
|
152
|
+
* branch's history (--force-if-includes refused). `stale-lease`: origin
|
|
153
|
+
* moved since the last fetch. `rejected`: anything else.
|
|
154
|
+
*/
|
|
155
|
+
reason: "not-integrated" | "stale-lease" | "rejected";
|
|
156
|
+
stderr: string;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The one way gh-stack force-pushes a branch (restack, sync, merge's advance,
|
|
161
|
+
* submit, join). Sets upstream, and works whether or not the branch exists on
|
|
162
|
+
* origin yet.
|
|
163
|
+
*
|
|
164
|
+
* By default it's `--force-with-lease --force-if-includes`: the lease refuses
|
|
165
|
+
* if origin moved since the last fetch, and --force-if-includes refuses if the
|
|
166
|
+
* fetched tip was never brought into the local branch. That second check is
|
|
167
|
+
* #28's root cause (a collapse's squash commit fetched, never pulled, then
|
|
168
|
+
* overwritten), covered here for every command.
|
|
169
|
+
*
|
|
170
|
+
* `expect` gives the lease an explicit remote sha instead. Use it only for a
|
|
171
|
+
* tip whose content was checked first (see lib/remote-guard.ts): git makes
|
|
172
|
+
* --force-if-includes a no-op then, and the push still fails if origin moved
|
|
173
|
+
* off that sha.
|
|
174
|
+
*/
|
|
175
|
+
export async function forcePushWithLease(
|
|
176
|
+
branch: string,
|
|
177
|
+
opts: { expect?: string } = {},
|
|
178
|
+
): Promise<PushResult> {
|
|
179
|
+
const lease = opts.expect
|
|
180
|
+
? `--force-with-lease=refs/heads/${branch}:${opts.expect}`
|
|
181
|
+
: "--force-with-lease";
|
|
182
|
+
const { exitCode, stderr } = await $`git push -u ${lease} --force-if-includes origin ${branch}`
|
|
183
|
+
.nothrow()
|
|
184
|
+
.quiet();
|
|
185
|
+
if (exitCode === 0) return { ok: true };
|
|
186
|
+
const text = stderr.toString();
|
|
187
|
+
const reason = text.includes("remote ref updated since checkout")
|
|
188
|
+
? "not-integrated"
|
|
189
|
+
: text.includes("stale info")
|
|
190
|
+
? "stale-lease"
|
|
191
|
+
: "rejected";
|
|
192
|
+
return { ok: false, reason, stderr: text };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Why a force-push was refused, and what to do, in gh-stack terms (plain lines). */
|
|
196
|
+
export function pushFailureHint(branch: string, result: PushResult): string[] {
|
|
197
|
+
if (result.ok) return [];
|
|
198
|
+
switch (result.reason) {
|
|
199
|
+
case "not-integrated":
|
|
200
|
+
return [
|
|
201
|
+
`origin/${branch} has commits you fetched but never brought into local ${branch}, so git refused to overwrite them (--force-if-includes).`,
|
|
202
|
+
`That's how a collapsed PR's squash commit gets lost (#28). See them: git log ${branch}..origin/${branch}`,
|
|
203
|
+
`Bring them in (git pull --rebase origin ${branch}, or cherry-pick), then retry.`,
|
|
204
|
+
`To drop them on purpose: gh-stack restack --overwrite-remote`,
|
|
205
|
+
];
|
|
206
|
+
case "stale-lease":
|
|
207
|
+
return [
|
|
208
|
+
`origin/${branch} moved since your last fetch, so the lease refused.`,
|
|
209
|
+
`Look at what's there first: git fetch origin ${branch} && git log ${branch}..origin/${branch}`,
|
|
210
|
+
];
|
|
211
|
+
default:
|
|
212
|
+
return [
|
|
213
|
+
result.stderr
|
|
214
|
+
.trim()
|
|
215
|
+
.split("\n")
|
|
216
|
+
.find((l) => l.includes("rejected") || l.startsWith("error")) ?? result.stderr.trim(),
|
|
217
|
+
];
|
|
159
218
|
}
|
|
160
219
|
}
|
|
161
220
|
|
|
162
221
|
/**
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
222
|
+
* Fetch commits from origin without moving any ref: no remote-tracking ref
|
|
223
|
+
* update (`--refmap=`) and no FETCH_HEAD. The tracking refs are what
|
|
224
|
+
* `--force-with-lease` and `--force-if-includes` compare against, so a check
|
|
225
|
+
* that needs origin's objects must never advance them.
|
|
167
226
|
*/
|
|
168
|
-
export async function
|
|
169
|
-
|
|
227
|
+
export async function fetchObjectsOnly(refs: string[]): Promise<boolean> {
|
|
228
|
+
if (refs.length === 0) return true;
|
|
229
|
+
const { exitCode } = await $`git fetch --quiet --no-write-fetch-head --refmap= origin ${refs}`
|
|
230
|
+
.nothrow()
|
|
231
|
+
.quiet();
|
|
170
232
|
return exitCode === 0;
|
|
171
233
|
}
|
|
172
234
|
|
|
@@ -264,10 +326,10 @@ export async function deleteTagsMatching(pattern: string): Promise<void> {
|
|
|
264
326
|
}
|
|
265
327
|
|
|
266
328
|
/**
|
|
267
|
-
*
|
|
329
|
+
* Whether a ref (branch, tag, remote-tracking ref, or any revision) resolves.
|
|
268
330
|
*/
|
|
269
|
-
export async function
|
|
270
|
-
const { exitCode } = await $`git rev-parse --verify ${
|
|
331
|
+
export async function refExists(ref: string): Promise<boolean> {
|
|
332
|
+
const { exitCode } = await $`git rev-parse --verify --quiet ${ref}`.nothrow().quiet();
|
|
271
333
|
return exitCode === 0;
|
|
272
334
|
}
|
|
273
335
|
|
|
@@ -346,6 +408,20 @@ export async function allLocalBranches(): Promise<string[]> {
|
|
|
346
408
|
return result.trim().split("\n").filter(Boolean);
|
|
347
409
|
}
|
|
348
410
|
|
|
411
|
+
/**
|
|
412
|
+
* Map every local branch to its tip SHA, in one call.
|
|
413
|
+
*/
|
|
414
|
+
export async function localBranchTips(): Promise<Map<string, string>> {
|
|
415
|
+
const result =
|
|
416
|
+
await $`git for-each-ref --format=${"%(refname:short) %(objectname)"} refs/heads`.text();
|
|
417
|
+
const tips = new Map<string, string>();
|
|
418
|
+
for (const line of result.trim().split("\n").filter(Boolean)) {
|
|
419
|
+
const [name, sha] = line.split(" ");
|
|
420
|
+
if (name && sha) tips.set(name, sha);
|
|
421
|
+
}
|
|
422
|
+
return tips;
|
|
423
|
+
}
|
|
424
|
+
|
|
349
425
|
/**
|
|
350
426
|
* Determine the trunk branch name. Prefers "main", falls back to "master"
|
|
351
427
|
* if only that exists locally. Defaults to "main" when neither is present.
|
package/src/lib/github.ts
CHANGED
|
@@ -40,6 +40,26 @@ export async function getPrNumber(branch: string): Promise<number | null> {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Find the open PR whose head is `branch`, for adopting a PR opened outside
|
|
45
|
+
* gh-stack. Unlike getPrNumber, returns null unless there is exactly one match
|
|
46
|
+
* — a fork with the same branch name must never be adopted by accident.
|
|
47
|
+
*/
|
|
48
|
+
export async function findOpenPrForBranch(branch: string): Promise<number | null> {
|
|
49
|
+
try {
|
|
50
|
+
const out =
|
|
51
|
+
await $`gh pr list --head ${branch} --state open --json number --jq '.[].number'`.text();
|
|
52
|
+
const nums = out
|
|
53
|
+
.trim()
|
|
54
|
+
.split(/\s+/)
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.map((n) => parseInt(n, 10));
|
|
57
|
+
return nums.length === 1 && !isNaN(nums[0]!) ? nums[0]! : null;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
43
63
|
/**
|
|
44
64
|
* Get PR info for a specific PR number.
|
|
45
65
|
*/
|
|
@@ -183,17 +203,75 @@ export async function getPrBody(prNumber: number): Promise<string | null> {
|
|
|
183
203
|
* squash-merge lands on this branch. Compare `headRefOid` against the live ref
|
|
184
204
|
* (`getBranchRefSha`) to detect that staleness — see `merge.ts`.
|
|
185
205
|
*/
|
|
186
|
-
|
|
206
|
+
/** A PR's merge state, plus what merging it left behind. */
|
|
207
|
+
export interface PrMergeResult {
|
|
187
208
|
state: string;
|
|
188
209
|
mergeable: string;
|
|
189
210
|
mergeStateStatus: string;
|
|
190
211
|
headRefName: string;
|
|
212
|
+
/** The PR's head; for a merged PR, its head when it merged. */
|
|
191
213
|
headRefOid: string;
|
|
192
|
-
|
|
214
|
+
/** The branch it targets (or merged into). */
|
|
215
|
+
baseRefName: string;
|
|
216
|
+
/** The commit merging it created on its base (for a squash merge, the squash commit). */
|
|
217
|
+
mergeCommit: string | null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function getPrMergeState(prNumber: number): Promise<PrMergeResult | null> {
|
|
193
221
|
try {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
222
|
+
const data = JSON.parse(
|
|
223
|
+
(
|
|
224
|
+
await $`gh pr view ${prNumber} --json state,mergeable,mergeStateStatus,headRefName,headRefOid,baseRefName,mergeCommit`.text()
|
|
225
|
+
).trim(),
|
|
226
|
+
);
|
|
227
|
+
return { ...data, mergeCommit: data.mergeCommit?.oid ?? null };
|
|
228
|
+
} catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** A commit as a patch (`git format-patch` style), from the GitHub API. Null if unavailable. */
|
|
234
|
+
export async function getCommitPatch(sha: string): Promise<string | null> {
|
|
235
|
+
const nwo = await getRepoNwo();
|
|
236
|
+
if (!nwo) return null;
|
|
237
|
+
const { stdout, exitCode } =
|
|
238
|
+
await $`gh api -H ${"Accept: application/vnd.github.patch"} repos/${nwo}/commits/${sha}`
|
|
239
|
+
.nothrow()
|
|
240
|
+
.quiet();
|
|
241
|
+
const text = stdout.toString();
|
|
242
|
+
return exitCode === 0 && text.trim() ? text : null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The files a commit changed (path, status, blob sha after the commit), from
|
|
247
|
+
* the GitHub API. Null if unavailable, or if GitHub truncated the list (it
|
|
248
|
+
* returns at most 300 files): a partial list can't prove anything.
|
|
249
|
+
*/
|
|
250
|
+
export async function getCommitFiles(
|
|
251
|
+
sha: string,
|
|
252
|
+
): Promise<
|
|
253
|
+
{ filename: string; status: string; sha: string | null; before?: string | null }[] | null
|
|
254
|
+
> {
|
|
255
|
+
const nwo = await getRepoNwo();
|
|
256
|
+
if (!nwo) return null;
|
|
257
|
+
const { stdout, exitCode } =
|
|
258
|
+
await $`gh api repos/${nwo}/commits/${sha} --jq ${"[.files[] | {filename, status, sha}]"}`
|
|
259
|
+
.nothrow()
|
|
260
|
+
.quiet();
|
|
261
|
+
if (exitCode !== 0) return null;
|
|
262
|
+
try {
|
|
263
|
+
const files: { filename: string; status: string; sha: string | null }[] = JSON.parse(
|
|
264
|
+
stdout.toString(),
|
|
265
|
+
);
|
|
266
|
+
if (!Array.isArray(files) || files.length === 0 || files.length >= 300) return null;
|
|
267
|
+
// The API gives no blob from before the commit, except that an added
|
|
268
|
+
// file had none; a removed file has no blob after it.
|
|
269
|
+
return files.map((f) => ({
|
|
270
|
+
filename: f.filename,
|
|
271
|
+
status: f.status,
|
|
272
|
+
sha: f.status === "removed" ? null : f.sha,
|
|
273
|
+
before: f.status === "added" ? null : undefined,
|
|
274
|
+
}));
|
|
197
275
|
} catch {
|
|
198
276
|
return null;
|
|
199
277
|
}
|
package/src/lib/metadata.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// • A stack can never silently vanish — on write, a stale stack file is moved
|
|
12
12
|
// to `deleted/` (a recoverable tombstone), never just unlinked.
|
|
13
13
|
import type { StackMetadata, Stack, Branch, RestackState } from "../types.ts";
|
|
14
|
+
import { RUN_STATE_VERSION } from "../types.ts";
|
|
14
15
|
import { mkdir, readdir, rename, unlink, stat } from "node:fs/promises";
|
|
15
16
|
import {
|
|
16
17
|
ghStackDir,
|
|
@@ -605,7 +606,7 @@ export function getOrderedBranches(stack: Stack): string[] {
|
|
|
605
606
|
*/
|
|
606
607
|
export function getChildren(stack: Stack, branchName: string): string[] {
|
|
607
608
|
return Object.entries(stack.branches)
|
|
608
|
-
.filter(([
|
|
609
|
+
.filter(([, b]) => b.parent === branchName)
|
|
609
610
|
.map(([name]) => name);
|
|
610
611
|
}
|
|
611
612
|
|
|
@@ -655,23 +656,49 @@ export async function updateLastBranch(
|
|
|
655
656
|
// ── Restack state (for --resume) ──
|
|
656
657
|
|
|
657
658
|
/**
|
|
658
|
-
* Save
|
|
659
|
+
* Save a run's state (resume point + action log). Written after every step, so
|
|
660
|
+
* it goes through a temp file and a rename: a kill mid-write leaves the
|
|
661
|
+
* previous state, never a truncated file.
|
|
659
662
|
*/
|
|
660
663
|
export async function saveRestackState(state: RestackState): Promise<void> {
|
|
661
664
|
await ensureDirs();
|
|
662
|
-
|
|
665
|
+
const path = await restackStateFile();
|
|
666
|
+
const tmp = `${path}.tmp`;
|
|
667
|
+
await Bun.write(tmp, JSON.stringify(state, null, 2) + "\n");
|
|
668
|
+
await rename(tmp, path);
|
|
663
669
|
}
|
|
664
670
|
|
|
665
671
|
/**
|
|
666
|
-
* Load
|
|
667
|
-
*
|
|
672
|
+
* Load the saved run state. Returns null if there is none. A file that can't
|
|
673
|
+
* be parsed, or was written by another version of gh-stack, throws
|
|
674
|
+
* InvalidRunStateError: it is never guessed at or upgraded.
|
|
668
675
|
*/
|
|
669
676
|
export async function loadRestackState(): Promise<RestackState | null> {
|
|
670
|
-
const
|
|
677
|
+
const path = await restackStateFile();
|
|
678
|
+
const file = Bun.file(path);
|
|
671
679
|
if (!(await file.exists())) {
|
|
672
680
|
return null;
|
|
673
681
|
}
|
|
674
|
-
|
|
682
|
+
let raw: Partial<RestackState>;
|
|
683
|
+
try {
|
|
684
|
+
raw = await file.json();
|
|
685
|
+
} catch {
|
|
686
|
+
throw new InvalidRunStateError(path, "it isn't valid JSON");
|
|
687
|
+
}
|
|
688
|
+
if (raw?.version !== RUN_STATE_VERSION) {
|
|
689
|
+
throw new InvalidRunStateError(path, "it was written by an older gh-stack");
|
|
690
|
+
}
|
|
691
|
+
return raw as RestackState;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/** The saved run state exists but can't be used (unreadable, or another version's shape). */
|
|
695
|
+
export class InvalidRunStateError extends Error {
|
|
696
|
+
constructor(
|
|
697
|
+
readonly path: string,
|
|
698
|
+
readonly reason: string,
|
|
699
|
+
) {
|
|
700
|
+
super(`The saved run state at ${path} can't be used: ${reason}.`);
|
|
701
|
+
}
|
|
675
702
|
}
|
|
676
703
|
|
|
677
704
|
/**
|
package/src/lib/output.ts
CHANGED
|
@@ -7,17 +7,40 @@
|
|
|
7
7
|
// - no ANSI colors (picocolors auto-disables via NO_COLOR; we also strip
|
|
8
8
|
// any residual escapes defensively)
|
|
9
9
|
//
|
|
10
|
-
// Interactive prompts (text, confirm, select) are
|
|
10
|
+
// Interactive prompts (text, confirm, select) are unchanged by plain mode —
|
|
11
11
|
// plain mode is about output, not interactivity. Agents already pair --plain
|
|
12
|
-
// with --yes/GH_STACK_YES so the interactive paths aren't reached.
|
|
12
|
+
// with --yes/GH_STACK_YES so the interactive paths aren't reached. If one IS
|
|
13
|
+
// reached with no TTY on stdin, nothing can ever answer it, so we fail fast
|
|
14
|
+
// instead of blocking forever (#33).
|
|
13
15
|
import * as clack from "@clack/prompts";
|
|
14
16
|
|
|
15
17
|
let _plain = false;
|
|
18
|
+
let stderrOnly = false;
|
|
16
19
|
|
|
17
20
|
export function setPlain(enabled: boolean): void {
|
|
18
21
|
_plain = enabled;
|
|
19
22
|
}
|
|
20
23
|
|
|
24
|
+
/**
|
|
25
|
+
* `--json` mode: plain output, all of it on stderr, so stdout carries only the
|
|
26
|
+
* JSON document the command writes itself.
|
|
27
|
+
*/
|
|
28
|
+
export function setStderrOnly(): void {
|
|
29
|
+
_plain = true;
|
|
30
|
+
stderrOnly = true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Plain-mode line: stdout normally, stderr in --json mode. */
|
|
34
|
+
function say(line: string): void {
|
|
35
|
+
if (stderrOnly) console.error(line);
|
|
36
|
+
else console.log(line);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A raw line of output (tree drawings, spacing), routed like the log calls. */
|
|
40
|
+
export function print(line = ""): void {
|
|
41
|
+
say(_plain ? strip(line) : line);
|
|
42
|
+
}
|
|
43
|
+
|
|
21
44
|
export function isPlain(): boolean {
|
|
22
45
|
return _plain;
|
|
23
46
|
}
|
|
@@ -27,16 +50,43 @@ function strip(s: string | undefined): string {
|
|
|
27
50
|
return (s ?? "").replace(ANSI_RE, "");
|
|
28
51
|
}
|
|
29
52
|
|
|
30
|
-
// ── Interactive prompts
|
|
53
|
+
// ── Interactive prompts ──
|
|
31
54
|
export const isCancel = clack.isCancel;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Exit non-zero instead of prompting when stdin isn't a TTY. A prompt there
|
|
58
|
+
* can never be answered, and it would otherwise hang (holding the repo) past
|
|
59
|
+
* the caller's timeout. (#33)
|
|
60
|
+
*/
|
|
61
|
+
function ensureInteractive(message: string): void {
|
|
62
|
+
if (process.stdin.isTTY) return;
|
|
63
|
+
console.error(
|
|
64
|
+
`gh-stack: refusing to prompt — stdin is not a TTY.\n` +
|
|
65
|
+
` Prompt: ${strip(message)}\n` +
|
|
66
|
+
` Pass --yes (or set GH_STACK_YES=1) to confirm automatically.`,
|
|
67
|
+
);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const text: typeof clack.text = (opts) => {
|
|
72
|
+
ensureInteractive(opts.message);
|
|
73
|
+
return clack.text(opts);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const confirm: typeof clack.confirm = (opts) => {
|
|
77
|
+
ensureInteractive(opts.message);
|
|
78
|
+
return clack.confirm(opts);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const select: typeof clack.select = (opts) => {
|
|
82
|
+
ensureInteractive(opts.message);
|
|
83
|
+
return clack.select(opts);
|
|
84
|
+
};
|
|
35
85
|
|
|
36
86
|
// ── Banner / error ──
|
|
37
87
|
export function intro(message?: string): void {
|
|
38
88
|
if (_plain) {
|
|
39
|
-
if (message)
|
|
89
|
+
if (message) say(strip(message));
|
|
40
90
|
} else {
|
|
41
91
|
clack.intro(message);
|
|
42
92
|
}
|
|
@@ -44,7 +94,7 @@ export function intro(message?: string): void {
|
|
|
44
94
|
|
|
45
95
|
export function outro(message?: string): void {
|
|
46
96
|
if (_plain) {
|
|
47
|
-
if (message)
|
|
97
|
+
if (message) say(strip(message));
|
|
48
98
|
} else {
|
|
49
99
|
clack.outro(message);
|
|
50
100
|
}
|
|
@@ -60,11 +110,11 @@ export function cancel(message?: string): void {
|
|
|
60
110
|
|
|
61
111
|
// ── log.* ──
|
|
62
112
|
export const log = {
|
|
63
|
-
info: (msg: string) => (_plain ?
|
|
64
|
-
success: (msg: string) => (_plain ?
|
|
113
|
+
info: (msg: string) => (_plain ? say(strip(msg)) : clack.log.info(msg)),
|
|
114
|
+
success: (msg: string) => (_plain ? say(strip(msg)) : clack.log.success(msg)),
|
|
65
115
|
warn: (msg: string) => (_plain ? console.warn(strip(msg)) : clack.log.warn(msg)),
|
|
66
116
|
error: (msg: string) => (_plain ? console.error(strip(msg)) : clack.log.error(msg)),
|
|
67
|
-
step: (msg: string) => (_plain ?
|
|
117
|
+
step: (msg: string) => (_plain ? say(strip(msg)) : clack.log.step(msg)),
|
|
68
118
|
};
|
|
69
119
|
|
|
70
120
|
// ── Spinner ──
|
|
@@ -78,13 +128,13 @@ export function spinner(): Spinner {
|
|
|
78
128
|
if (!_plain) return clack.spinner() as Spinner;
|
|
79
129
|
return {
|
|
80
130
|
start: (msg) => {
|
|
81
|
-
if (msg)
|
|
131
|
+
if (msg) say(strip(msg));
|
|
82
132
|
},
|
|
83
133
|
stop: (msg) => {
|
|
84
|
-
if (msg)
|
|
134
|
+
if (msg) say(strip(msg));
|
|
85
135
|
},
|
|
86
136
|
message: (msg) => {
|
|
87
|
-
if (msg)
|
|
137
|
+
if (msg) say(strip(msg));
|
|
88
138
|
},
|
|
89
139
|
};
|
|
90
140
|
}
|
package/src/lib/paths.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// archived/<stack>.json merged/closed stacks
|
|
7
7
|
// deleted/<stack>.json tombstones (recoverable)
|
|
8
8
|
// snapshots/<ts>__<stack>.json append-only pre-operation branch tips
|
|
9
|
-
// restack-state.json
|
|
9
|
+
// restack-state.json last restack/sync/join run: resume point + action log
|
|
10
10
|
//
|
|
11
11
|
// Everything lives inside `.git/` and is never committed — gh-stack is a
|
|
12
12
|
// single-developer tool, so there's no need to share metadata across clones.
|