respawn-session 0.0.2 → 0.0.5
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 +87 -10
- package/package.json +1 -1
- package/src/agents/claude.ts +80 -8
- package/src/agents/codex.ts +25 -1
- package/src/agents/index.ts +5 -0
- package/src/agents/types.ts +5 -0
- package/src/cli.ts +26 -1
- package/src/commands/import.ts +98 -0
- package/src/commands/update.ts +26 -0
- package/src/git.ts +21 -0
package/README.md
CHANGED
|
@@ -17,56 +17,133 @@ bun --version
|
|
|
17
17
|
gh auth status
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
##
|
|
20
|
+
## Quick Start
|
|
21
21
|
|
|
22
|
-
Initialize
|
|
22
|
+
Initialize once on each machine. This creates `~/.respawn/index.json` and installs autosave Stop hooks for Claude Code and Codex:
|
|
23
23
|
|
|
24
24
|
```sh
|
|
25
25
|
respawn init
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
Work normally in Claude Code or Codex. `respawn init` makes sessions autosave when the agent stops. To save immediately from inside an active agent session, run:
|
|
29
29
|
|
|
30
30
|
```sh
|
|
31
31
|
respawn save
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
Resume the latest saved session for a branch:
|
|
35
35
|
|
|
36
36
|
```sh
|
|
37
|
-
respawn
|
|
37
|
+
respawn angela/fix-bugs
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
Resume
|
|
40
|
+
Resume from a PR that was tagged with `respawn tag`:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
respawn 123
|
|
44
|
+
respawn https://github.com/org/repo/pull/123
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Common Workflows
|
|
48
|
+
|
|
49
|
+
### Manual Branch Save
|
|
50
|
+
|
|
51
|
+
Inside an active Claude Code or Codex session:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
respawn save
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Later, from a clone or worktree for the same repo:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
respawn <branch>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Example:
|
|
41
64
|
|
|
42
65
|
```sh
|
|
43
66
|
respawn angela/fix-bugs
|
|
44
67
|
```
|
|
45
68
|
|
|
46
|
-
|
|
69
|
+
### Autosave
|
|
70
|
+
|
|
71
|
+
Run this once per machine:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
respawn init
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
After that, Claude Code and Codex Stop hooks run:
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
respawn autosave
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Autosave hashes the transcript and skips unchanged sessions, so repeated Stop events do not create duplicate gists.
|
|
84
|
+
|
|
85
|
+
### PR Tagging
|
|
86
|
+
|
|
87
|
+
Use this when you want a session to survive branch deletion after merge:
|
|
47
88
|
|
|
48
89
|
```sh
|
|
49
90
|
respawn tag
|
|
50
91
|
```
|
|
51
92
|
|
|
52
|
-
|
|
93
|
+
That writes or updates a hidden metadata comment on the current GitHub PR. The comment stores session pointers, not the transcript body. Transcripts still live in your private gists.
|
|
94
|
+
|
|
95
|
+
Later, resume from the PR:
|
|
53
96
|
|
|
54
97
|
```sh
|
|
55
98
|
respawn 123
|
|
56
99
|
respawn https://github.com/org/repo/pull/123
|
|
57
100
|
```
|
|
58
101
|
|
|
59
|
-
List
|
|
102
|
+
### List Saved Sessions
|
|
103
|
+
|
|
104
|
+
Show every saved session in your local index:
|
|
60
105
|
|
|
61
106
|
```sh
|
|
62
107
|
respawn list
|
|
63
108
|
```
|
|
64
109
|
|
|
110
|
+
### Import Existing Sessions
|
|
111
|
+
|
|
112
|
+
Backfill sessions that already exist on this machine:
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
respawn import
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Import scans Claude Code and Codex transcripts, groups them by their recorded cwd, and saves sessions whose cwd is still an available git worktree. It skips transcripts that are already in `~/.respawn/index.json` and skips deleted or non-git worktrees.
|
|
119
|
+
|
|
120
|
+
## Commands
|
|
121
|
+
|
|
122
|
+
| Command | What it does |
|
|
123
|
+
| --- | --- |
|
|
124
|
+
| `respawn init` | Creates the local index and installs autosave hooks |
|
|
125
|
+
| `respawn save` | Saves the active Claude Code or Codex transcript |
|
|
126
|
+
| `respawn autosave` | Saves only if the transcript changed |
|
|
127
|
+
| `respawn tag` | Saves and attaches session metadata to the current PR |
|
|
128
|
+
| `respawn import` | Backfills existing local Claude Code and Codex sessions |
|
|
129
|
+
| `respawn <branch>` | Restores the newest session for a branch |
|
|
130
|
+
| `respawn <pr-number>` | Restores the newest session from a tagged PR |
|
|
131
|
+
| `respawn <pr-url>` | Restores the newest session from a tagged PR URL |
|
|
132
|
+
| `respawn list` | Lists locally indexed sessions |
|
|
133
|
+
| `respawn version` | Prints the installed CLI version |
|
|
134
|
+
| `respawn update` | Updates the global npm install to the latest release |
|
|
135
|
+
|
|
136
|
+
If your installed version does not recognize `respawn update`, bootstrap once with:
|
|
137
|
+
|
|
138
|
+
```sh
|
|
139
|
+
npm install -g respawn-session@latest
|
|
140
|
+
```
|
|
141
|
+
|
|
65
142
|
## How It Works
|
|
66
143
|
|
|
67
144
|
`respawn save` detects the active agent in this order:
|
|
68
145
|
|
|
69
|
-
1. Claude Code via `CLAUDE_SESSION_ID`
|
|
146
|
+
1. Claude Code via `CLAUDE_SESSION_ID`, then `~/.claude/sessions/*.json`
|
|
70
147
|
2. Codex via `CODEX_TUI_SESSION_LOG_PATH`, `CODEX_SESSION_ID`, or the newest `~/.codex/sessions/**.jsonl` transcript for the current cwd
|
|
71
148
|
|
|
72
149
|
It then runs:
|
package/package.json
CHANGED
package/src/agents/claude.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import type { LocatedTranscript, LocateOptions } from "./types";
|
|
4
|
+
import type { ImportableTranscript, LocatedTranscript, LocateOptions } from "./types";
|
|
5
5
|
|
|
6
6
|
export function encodeClaudeProjectPath(cwd: string): string {
|
|
7
|
-
return cwd.
|
|
7
|
+
return cwd.replace(/[^A-Za-z0-9._-]/g, "-");
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export function transcriptPath(
|
|
@@ -27,14 +27,86 @@ export function locateTranscript(
|
|
|
27
27
|
): LocatedTranscript | null {
|
|
28
28
|
const env = options.env ?? process.env;
|
|
29
29
|
const sessionId = env.CLAUDE_SESSION_ID;
|
|
30
|
-
if (
|
|
30
|
+
if (sessionId) {
|
|
31
|
+
const path = transcriptPath(sessionId, options);
|
|
32
|
+
if (existsSync(path)) return { agent: "claude", path, sessionId };
|
|
33
|
+
}
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
if (!existsSync(path)) return null;
|
|
34
|
-
|
|
35
|
-
return { agent: "claude", path, sessionId };
|
|
35
|
+
return locateFromSessionRegistry(options);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export function resumeCmd(sessionId: string): string[] {
|
|
39
39
|
return ["claude", "--resume", sessionId];
|
|
40
40
|
}
|
|
41
|
+
|
|
42
|
+
export function listTranscripts(options: LocateOptions = {}): ImportableTranscript[] {
|
|
43
|
+
const home = options.home ?? homedir();
|
|
44
|
+
const sessionsDir = join(home, ".claude", "sessions");
|
|
45
|
+
if (!existsSync(sessionsDir)) return [];
|
|
46
|
+
|
|
47
|
+
return readdirSync(sessionsDir, { withFileTypes: true })
|
|
48
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
49
|
+
.map((entry) => readSessionRecord(join(sessionsDir, entry.name)))
|
|
50
|
+
.filter(
|
|
51
|
+
(record): record is Required<Pick<ClaudeSessionRecord, "sessionId" | "cwd">> &
|
|
52
|
+
ClaudeSessionRecord => Boolean(record?.sessionId && record.cwd),
|
|
53
|
+
)
|
|
54
|
+
.map((record): ImportableTranscript | null => {
|
|
55
|
+
const path = transcriptPath(record.sessionId, {
|
|
56
|
+
...options,
|
|
57
|
+
cwd: record.cwd,
|
|
58
|
+
});
|
|
59
|
+
if (!existsSync(path)) return null;
|
|
60
|
+
return {
|
|
61
|
+
agent: "claude" as const,
|
|
62
|
+
path,
|
|
63
|
+
sessionId: record.sessionId,
|
|
64
|
+
cwd: record.cwd,
|
|
65
|
+
savedAt: record.updatedAt
|
|
66
|
+
? new Date(record.updatedAt).toISOString()
|
|
67
|
+
: undefined,
|
|
68
|
+
};
|
|
69
|
+
})
|
|
70
|
+
.filter((transcript): transcript is ImportableTranscript => transcript !== null);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type ClaudeSessionRecord = {
|
|
74
|
+
sessionId?: string;
|
|
75
|
+
cwd?: string;
|
|
76
|
+
updatedAt?: number;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function locateFromSessionRegistry(
|
|
80
|
+
options: LocateOptions = {},
|
|
81
|
+
): LocatedTranscript | null {
|
|
82
|
+
const cwd = options.cwd ?? process.cwd();
|
|
83
|
+
const home = options.home ?? homedir();
|
|
84
|
+
const sessionsDir = join(home, ".claude", "sessions");
|
|
85
|
+
if (!existsSync(sessionsDir)) return null;
|
|
86
|
+
|
|
87
|
+
const records = readdirSync(sessionsDir, { withFileTypes: true })
|
|
88
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
89
|
+
.map((entry) => readSessionRecord(join(sessionsDir, entry.name)))
|
|
90
|
+
.filter(
|
|
91
|
+
(record): record is Required<Pick<ClaudeSessionRecord, "sessionId" | "cwd">> &
|
|
92
|
+
ClaudeSessionRecord => Boolean(record?.sessionId && record.cwd === cwd),
|
|
93
|
+
)
|
|
94
|
+
.sort((a, b) => (a.updatedAt ?? 0) - (b.updatedAt ?? 0));
|
|
95
|
+
|
|
96
|
+
for (const record of records.reverse()) {
|
|
97
|
+
const path = transcriptPath(record.sessionId, options);
|
|
98
|
+
if (existsSync(path)) {
|
|
99
|
+
return { agent: "claude", path, sessionId: record.sessionId };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function readSessionRecord(path: string): ClaudeSessionRecord | null {
|
|
107
|
+
try {
|
|
108
|
+
return JSON.parse(readFileSync(path, "utf8")) as ClaudeSessionRecord;
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
package/src/agents/codex.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { basename, join, relative } from "node:path";
|
|
4
|
-
import type { LocatedTranscript, LocateOptions } from "./types";
|
|
4
|
+
import type { ImportableTranscript, LocatedTranscript, LocateOptions } from "./types";
|
|
5
5
|
|
|
6
6
|
const uuidPattern =
|
|
7
7
|
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
@@ -54,6 +54,30 @@ export function resumeCmd(sessionId: string): string[] {
|
|
|
54
54
|
return ["codex", "resume", sessionId];
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
export function listTranscripts(options: LocateOptions = {}): ImportableTranscript[] {
|
|
58
|
+
const home = options.home ?? homedir();
|
|
59
|
+
const sessionsDir = codexSessionsDir(home);
|
|
60
|
+
if (!existsSync(sessionsDir)) return [];
|
|
61
|
+
|
|
62
|
+
return walkJsonl(sessionsDir)
|
|
63
|
+
.map((path): ImportableTranscript | null => {
|
|
64
|
+
const meta = readCodexMeta(path);
|
|
65
|
+
const sessionId = meta?.id ?? sessionIdFromCodexPath(path);
|
|
66
|
+
if (!sessionId || !meta?.cwd) return null;
|
|
67
|
+
|
|
68
|
+
const rel = relative(sessionsDir, path);
|
|
69
|
+
return {
|
|
70
|
+
agent: "codex" as const,
|
|
71
|
+
path,
|
|
72
|
+
sessionId,
|
|
73
|
+
cwd: meta.cwd,
|
|
74
|
+
relativePath: rel.startsWith("..") ? undefined : rel,
|
|
75
|
+
savedAt: statSync(path).mtime.toISOString(),
|
|
76
|
+
};
|
|
77
|
+
})
|
|
78
|
+
.filter((transcript): transcript is ImportableTranscript => transcript !== null);
|
|
79
|
+
}
|
|
80
|
+
|
|
57
81
|
function locatedFromPath(
|
|
58
82
|
path: string,
|
|
59
83
|
cwd: string,
|
package/src/agents/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LocatedTranscript, LocateOptions } from "./types";
|
|
2
|
+
import type { ImportableTranscript } from "./types";
|
|
2
3
|
import * as claude from "./claude";
|
|
3
4
|
import * as codex from "./codex";
|
|
4
5
|
|
|
@@ -11,6 +12,10 @@ export function locateActiveTranscript(
|
|
|
11
12
|
);
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
export function listAllTranscripts(options: LocateOptions = {}): ImportableTranscript[] {
|
|
16
|
+
return [...claude.listTranscripts(options), ...codex.listTranscripts(options)];
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
export function resumeCmd(agent: LocatedTranscript["agent"], sessionId: string): string[] {
|
|
15
20
|
if (agent === "claude") return claude.resumeCmd(sessionId);
|
|
16
21
|
return codex.resumeCmd(sessionId);
|
package/src/agents/types.ts
CHANGED
package/src/cli.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { initRespawn } from "./commands/init";
|
|
3
|
+
import { importSessions } from "./commands/import";
|
|
3
4
|
import { listSessions } from "./commands/list";
|
|
4
5
|
import { resumePrSession, resumeSession } from "./commands/resume";
|
|
5
6
|
import { saveSession } from "./commands/save";
|
|
6
7
|
import { tagCurrentPr } from "./commands/tag";
|
|
8
|
+
import { updateRespawn, versionText } from "./commands/update";
|
|
7
9
|
|
|
8
10
|
export type Route =
|
|
9
11
|
| { name: "help" }
|
|
@@ -11,7 +13,10 @@ export type Route =
|
|
|
11
13
|
| { name: "autosave" }
|
|
12
14
|
| { name: "list" }
|
|
13
15
|
| { name: "init" }
|
|
16
|
+
| { name: "import" }
|
|
14
17
|
| { name: "tag" }
|
|
18
|
+
| { name: "version" }
|
|
19
|
+
| { name: "update" }
|
|
15
20
|
| { name: "resume"; branch: string }
|
|
16
21
|
| { name: "resume-pr"; prRef: string };
|
|
17
22
|
|
|
@@ -20,12 +25,17 @@ export function route(args: string[]): Route {
|
|
|
20
25
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
21
26
|
return { name: "help" };
|
|
22
27
|
}
|
|
28
|
+
if (command === "version" || command === "--version" || command === "-v") {
|
|
29
|
+
return { name: "version" };
|
|
30
|
+
}
|
|
23
31
|
if (
|
|
24
32
|
command === "save" ||
|
|
25
33
|
command === "autosave" ||
|
|
26
34
|
command === "list" ||
|
|
27
35
|
command === "init" ||
|
|
28
|
-
command === "
|
|
36
|
+
command === "import" ||
|
|
37
|
+
command === "tag" ||
|
|
38
|
+
command === "update"
|
|
29
39
|
) {
|
|
30
40
|
return { name: command };
|
|
31
41
|
}
|
|
@@ -57,10 +67,22 @@ export async function main(args = Bun.argv.slice(2)): Promise<void> {
|
|
|
57
67
|
console.log(await initRespawn());
|
|
58
68
|
return;
|
|
59
69
|
}
|
|
70
|
+
if (selected.name === "import") {
|
|
71
|
+
console.log((await importSessions()).message);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
60
74
|
if (selected.name === "tag") {
|
|
61
75
|
console.log((await tagCurrentPr()).message);
|
|
62
76
|
return;
|
|
63
77
|
}
|
|
78
|
+
if (selected.name === "version") {
|
|
79
|
+
console.log(versionText());
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (selected.name === "update") {
|
|
83
|
+
console.log(await updateRespawn());
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
64
86
|
|
|
65
87
|
const result =
|
|
66
88
|
selected.name === "resume-pr"
|
|
@@ -85,6 +107,9 @@ function helpText(): string {
|
|
|
85
107
|
" respawn <pr-url|number>",
|
|
86
108
|
" respawn list",
|
|
87
109
|
" respawn init",
|
|
110
|
+
" respawn import",
|
|
111
|
+
" respawn version",
|
|
112
|
+
" respawn update",
|
|
88
113
|
].join("\n");
|
|
89
114
|
}
|
|
90
115
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { listAllTranscripts } from "../agents";
|
|
4
|
+
import type { ImportableTranscript } from "../agents/types";
|
|
5
|
+
import { gitInfoForCwd, type GitInfo } from "../git";
|
|
6
|
+
import {
|
|
7
|
+
defaultIndexPath,
|
|
8
|
+
readIndex,
|
|
9
|
+
recordSession,
|
|
10
|
+
type SavedSession,
|
|
11
|
+
} from "../index-file";
|
|
12
|
+
import { createGist } from "../storage/gist";
|
|
13
|
+
|
|
14
|
+
export type ImportResult = {
|
|
15
|
+
imported: number;
|
|
16
|
+
duplicates: number;
|
|
17
|
+
skipped: number;
|
|
18
|
+
message: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type ImportDeps = {
|
|
22
|
+
indexPath?: string;
|
|
23
|
+
listTranscripts?: typeof listAllTranscripts;
|
|
24
|
+
gitInfoForCwd?: typeof gitInfoForCwd;
|
|
25
|
+
createGist?: typeof createGist;
|
|
26
|
+
now?: () => Date;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export async function importSessions(
|
|
30
|
+
deps: ImportDeps = {},
|
|
31
|
+
): Promise<ImportResult> {
|
|
32
|
+
const indexPath = deps.indexPath ?? defaultIndexPath();
|
|
33
|
+
const transcripts = (deps.listTranscripts ?? listAllTranscripts)();
|
|
34
|
+
const gitInfo = deps.gitInfoForCwd ?? gitInfoForCwd;
|
|
35
|
+
const upload = deps.createGist ?? createGist;
|
|
36
|
+
let imported = 0;
|
|
37
|
+
let duplicates = 0;
|
|
38
|
+
let skipped = 0;
|
|
39
|
+
|
|
40
|
+
for (const transcript of transcripts) {
|
|
41
|
+
const info = await gitInfo(transcript.cwd);
|
|
42
|
+
if (!info) {
|
|
43
|
+
skipped += 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const transcriptHash = await hashFile(transcript.path);
|
|
48
|
+
if (await isDuplicate(indexPath, info, transcript, transcriptHash)) {
|
|
49
|
+
duplicates += 1;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const gistUrl = await upload(
|
|
54
|
+
transcript.path,
|
|
55
|
+
`respawn: ${info.repo}@${info.branch}`,
|
|
56
|
+
);
|
|
57
|
+
const session: SavedSession = {
|
|
58
|
+
repo: info.repo,
|
|
59
|
+
branch: info.branch,
|
|
60
|
+
gistUrl,
|
|
61
|
+
sessionId: transcript.sessionId,
|
|
62
|
+
sha: info.sha,
|
|
63
|
+
agent: transcript.agent,
|
|
64
|
+
savedAt: transcript.savedAt ?? (deps.now ?? (() => new Date()))().toISOString(),
|
|
65
|
+
relativePath: transcript.relativePath,
|
|
66
|
+
transcriptHash,
|
|
67
|
+
};
|
|
68
|
+
await recordSession(indexPath, session);
|
|
69
|
+
imported += 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
imported,
|
|
74
|
+
duplicates,
|
|
75
|
+
skipped,
|
|
76
|
+
message: `Imported ${imported} sessions, skipped ${duplicates} duplicates and ${skipped} unavailable worktrees`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function isDuplicate(
|
|
81
|
+
indexPath: string,
|
|
82
|
+
info: GitInfo,
|
|
83
|
+
transcript: ImportableTranscript,
|
|
84
|
+
transcriptHash: string,
|
|
85
|
+
): Promise<boolean> {
|
|
86
|
+
const index = await readIndex(indexPath);
|
|
87
|
+
return index.sessions.some(
|
|
88
|
+
(session) =>
|
|
89
|
+
session.repo === info.repo &&
|
|
90
|
+
session.branch === info.branch &&
|
|
91
|
+
session.agent === transcript.agent &&
|
|
92
|
+
session.transcriptHash === transcriptHash,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function hashFile(path: string): Promise<string> {
|
|
97
|
+
return createHash("sha256").update(await readFile(path)).digest("hex");
|
|
98
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import packageJson from "../../package.json" with { type: "json" };
|
|
2
|
+
import { runCommand, type RunCommand } from "../shell";
|
|
3
|
+
|
|
4
|
+
export const currentVersion = packageJson.version;
|
|
5
|
+
|
|
6
|
+
export function versionText(version = currentVersion): string {
|
|
7
|
+
return `respawn-session ${version}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type UpdateDeps = {
|
|
11
|
+
currentVersion?: string;
|
|
12
|
+
run?: RunCommand;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export async function updateRespawn(deps: UpdateDeps = {}): Promise<string> {
|
|
16
|
+
const version = deps.currentVersion ?? currentVersion;
|
|
17
|
+
const run = deps.run ?? runCommand;
|
|
18
|
+
const latest = (await run("npm", ["view", "respawn-session", "version"])).trim();
|
|
19
|
+
|
|
20
|
+
if (latest === version) {
|
|
21
|
+
return `respawn-session is already up to date at ${version}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
await run("npm", ["install", "-g", "respawn-session@latest"]);
|
|
25
|
+
return `Updated respawn-session ${version} -> ${latest}`;
|
|
26
|
+
}
|
package/src/git.ts
CHANGED
|
@@ -4,6 +4,12 @@ export async function currentRepo(run: RunCommand = runCommand): Promise<string>
|
|
|
4
4
|
return (await run("git", ["remote", "get-url", "origin"])).trim();
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
export type GitInfo = {
|
|
8
|
+
repo: string;
|
|
9
|
+
branch: string;
|
|
10
|
+
sha: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
7
13
|
export async function currentBranch(
|
|
8
14
|
run: RunCommand = runCommand,
|
|
9
15
|
): Promise<string> {
|
|
@@ -20,3 +26,18 @@ export async function checkoutBranch(
|
|
|
20
26
|
): Promise<void> {
|
|
21
27
|
await run("git", ["checkout", branch]);
|
|
22
28
|
}
|
|
29
|
+
|
|
30
|
+
export async function gitInfoForCwd(
|
|
31
|
+
cwd: string,
|
|
32
|
+
run: RunCommand = runCommand,
|
|
33
|
+
): Promise<GitInfo | null> {
|
|
34
|
+
try {
|
|
35
|
+
const repo = (await run("git", ["-C", cwd, "remote", "get-url", "origin"])).trim();
|
|
36
|
+
const branch = (await run("git", ["-C", cwd, "branch", "--show-current"])).trim();
|
|
37
|
+
const sha = (await run("git", ["-C", cwd, "rev-parse", "HEAD"])).trim();
|
|
38
|
+
if (!repo || !branch || !sha) return null;
|
|
39
|
+
return { repo, branch, sha };
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|