codiedev 0.9.4 → 0.9.6-beta.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/dist/backup/run.d.ts +2 -0
- package/dist/backup/run.js +42 -1
- package/dist/hook.js +15 -13
- package/dist/repoResolver.d.ts +1 -1
- package/dist/repoResolver.js +1 -1
- package/dist/utils.d.ts +14 -0
- package/dist/utils.js +124 -0
- package/package.json +1 -1
package/dist/backup/run.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export interface SelectDeps {
|
|
|
3
3
|
readFile: (rel: string) => string;
|
|
4
4
|
sha256: (content: string) => string;
|
|
5
5
|
}
|
|
6
|
+
export declare function unionPaths(a: string[], b: string[]): string[];
|
|
6
7
|
export declare function selectBackupFiles(input: {
|
|
7
8
|
repoKey: string;
|
|
8
9
|
statusPaths: string[];
|
|
@@ -14,6 +15,7 @@ export declare function selectBackupFiles(input: {
|
|
|
14
15
|
};
|
|
15
16
|
import { readConfig } from "../utils";
|
|
16
17
|
import { loadCache, saveCache } from "./cache";
|
|
18
|
+
export declare function gitCandidatePaths(root: string): string[];
|
|
17
19
|
export declare function runBackup(cwd: string, hookScriptPath: string): void;
|
|
18
20
|
export interface BackupUploadDeps {
|
|
19
21
|
readConfig: typeof readConfig;
|
package/dist/backup/run.js
CHANGED
|
@@ -33,11 +33,29 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.unionPaths = unionPaths;
|
|
36
37
|
exports.selectBackupFiles = selectBackupFiles;
|
|
38
|
+
exports.gitCandidatePaths = gitCandidatePaths;
|
|
37
39
|
exports.runBackup = runBackup;
|
|
38
40
|
exports.runBackupUpload = runBackupUpload;
|
|
39
41
|
const match_1 = require("./match");
|
|
40
42
|
const cache_1 = require("./cache");
|
|
43
|
+
// Pure: merge two path lists into one, deduped, preserving first-seen order.
|
|
44
|
+
function unionPaths(a, b) {
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
const out = [];
|
|
47
|
+
for (const p of a)
|
|
48
|
+
if (!seen.has(p)) {
|
|
49
|
+
seen.add(p);
|
|
50
|
+
out.push(p);
|
|
51
|
+
}
|
|
52
|
+
for (const p of b)
|
|
53
|
+
if (!seen.has(p)) {
|
|
54
|
+
seen.add(p);
|
|
55
|
+
out.push(p);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
41
59
|
// Pure: from this turn's git-status paths, keep artifact files whose content
|
|
42
60
|
// hash changed since the cache. Returns the changed candidates + next cache.
|
|
43
61
|
function selectBackupFiles(input) {
|
|
@@ -95,6 +113,27 @@ function gitStatusPaths(root) {
|
|
|
95
113
|
return [];
|
|
96
114
|
}
|
|
97
115
|
}
|
|
116
|
+
// All git-tracked files (committed, regardless of dirty state). `git ls-files`
|
|
117
|
+
// emits bare NUL-separated paths — no "XY " status prefix — so we don't slice.
|
|
118
|
+
function gitTrackedPaths(root) {
|
|
119
|
+
try {
|
|
120
|
+
const out = (0, child_process_1.execFileSync)("git", ["-C", root, "ls-files", "-z"], {
|
|
121
|
+
encoding: "utf8",
|
|
122
|
+
});
|
|
123
|
+
return out.split("\0").filter(Boolean);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// The hook's candidate set: dirty/untracked files ∪ all tracked files. Feeding
|
|
130
|
+
// tracked files in is what lets a COMMITTED artifact (clean status) still get
|
|
131
|
+
// backed up — the hash cache in selectBackupFiles dedups so already-backed-up
|
|
132
|
+
// files don't re-upload. selectBackupFiles rejects non-artifact paths cheaply
|
|
133
|
+
// (by regex, no content read), so the full tracked list is fine to pass.
|
|
134
|
+
function gitCandidatePaths(root) {
|
|
135
|
+
return unionPaths(gitStatusPaths(root), gitTrackedPaths(root));
|
|
136
|
+
}
|
|
98
137
|
const sha256 = (s) => crypto.createHash("sha256").update(s, "utf8").digest("hex");
|
|
99
138
|
// Stop/sessionEnd hook entry. Best-effort, never throws.
|
|
100
139
|
function runBackup(cwd, hookScriptPath) {
|
|
@@ -108,7 +147,9 @@ function runBackup(cwd, hookScriptPath) {
|
|
|
108
147
|
if (!root)
|
|
109
148
|
return;
|
|
110
149
|
const repoKey = (0, utils_1.getGitRemoteUrl)(root) ?? root;
|
|
111
|
-
|
|
150
|
+
// Dirty files ∪ tracked files — so committed-but-unbacked artifacts are
|
|
151
|
+
// caught too, not just unsaved drafts. Hash cache dedups the rest.
|
|
152
|
+
const statusPaths = gitCandidatePaths(root);
|
|
112
153
|
if (statusPaths.length === 0)
|
|
113
154
|
return;
|
|
114
155
|
const prevCache = (0, cache_2.loadCache)();
|
package/dist/hook.js
CHANGED
|
@@ -236,31 +236,33 @@ async function main() {
|
|
|
236
236
|
if (!remoteUrl) {
|
|
237
237
|
remoteUrl = (0, utils_1.getGitRemoteUrl)(workspaceRoot);
|
|
238
238
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
|
|
239
|
+
// Capture-first: the transcript is the asset; the repo is just a tag. We do
|
|
240
|
+
// NOT bail when `remoteUrl` is null — that previously dropped EVERY session
|
|
241
|
+
// for users whose workspace couldn't resolve a remote (no git binary on lean
|
|
242
|
+
// Alpine images, no `origin`, or a submodule whose parent .git isn't mounted),
|
|
243
|
+
// with no error and a green `doctor`. Upload regardless; the backend accepts
|
|
244
|
+
// an unmatched capture (repoId null) and surfaces it for triage.
|
|
245
|
+
//
|
|
246
|
+
// Best-effort local match — backend re-resolves from `remoteUrl` against the
|
|
247
|
+
// company's current repo list, so a stale/empty local cache doesn't matter.
|
|
248
|
+
const matchedRepo = remoteUrl ? (0, utils_1.matchRepo)(remoteUrl, config.repos) : null;
|
|
247
249
|
const stats = mode === "codex"
|
|
248
250
|
? (0, utils_1.parseCodexStats)(transcriptContent)
|
|
249
251
|
: mode === "cursor"
|
|
250
252
|
? { messageCount: 0, toolCallCount: 0 }
|
|
251
253
|
: (0, utils_1.parseClaudeCodeStats)(transcriptContent);
|
|
252
254
|
const transcriptBase64 = Buffer.from(transcriptContent, "utf8").toString("base64");
|
|
253
|
-
const payload = {
|
|
255
|
+
const payload = (0, utils_1.buildCapturePayload)({
|
|
254
256
|
sessionId: session_id,
|
|
255
|
-
|
|
256
|
-
|
|
257
|
+
transcriptBase64,
|
|
258
|
+
remoteUrl,
|
|
259
|
+
matchedRepoId: matchedRepo?.repoId,
|
|
257
260
|
companyId: config.companyId,
|
|
258
|
-
transcript: transcriptBase64,
|
|
259
261
|
messageCount: stats.messageCount,
|
|
260
262
|
toolCallCount: stats.toolCallCount,
|
|
261
263
|
capturedAt: Date.now(),
|
|
262
264
|
source: mode,
|
|
263
|
-
};
|
|
265
|
+
});
|
|
264
266
|
// Fire the upload from a fully detached child and exit immediately. Awaiting
|
|
265
267
|
// it inline let Claude Code's SessionEnd shutdown window elapse and kill the
|
|
266
268
|
// still-running hook ("Hook cancelled") on large transcripts — even though
|
package/dist/repoResolver.d.ts
CHANGED
package/dist/repoResolver.js
CHANGED
|
@@ -30,7 +30,7 @@ function resolveRepoForCapture(input) {
|
|
|
30
30
|
if (hit)
|
|
31
31
|
return { repoId: hit._id, source: "provided" };
|
|
32
32
|
}
|
|
33
|
-
const parsed = parseRemoteUrl(remoteUrl);
|
|
33
|
+
const parsed = remoteUrl ? parseRemoteUrl(remoteUrl) : null;
|
|
34
34
|
if (!parsed) {
|
|
35
35
|
return { repoId: null, source: "unmatched" };
|
|
36
36
|
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface CodiedevConfig {
|
|
|
14
14
|
}
|
|
15
15
|
export declare function readConfig(): CodiedevConfig | null;
|
|
16
16
|
export declare function writeConfig(config: CodiedevConfig): void;
|
|
17
|
+
export declare function parseGitRemoteFromConfig(configText: string): string | null;
|
|
18
|
+
export declare function readGitRemoteFromConfig(startDir: string): string | null;
|
|
17
19
|
export declare function getGitRemoteUrl(cwd: string): string | null;
|
|
18
20
|
export declare function normalizeRepoUrl(url: string): string;
|
|
19
21
|
export declare function matchRepo(remoteUrl: string, repos: Array<{
|
|
@@ -25,6 +27,18 @@ export declare function matchRepo(remoteUrl: string, repos: Array<{
|
|
|
25
27
|
url: string;
|
|
26
28
|
fullName: string;
|
|
27
29
|
} | null;
|
|
30
|
+
export interface CapturePayloadInput {
|
|
31
|
+
sessionId?: string;
|
|
32
|
+
transcriptBase64: string;
|
|
33
|
+
remoteUrl: string | null;
|
|
34
|
+
matchedRepoId?: string;
|
|
35
|
+
companyId: string;
|
|
36
|
+
messageCount: number;
|
|
37
|
+
toolCallCount: number;
|
|
38
|
+
capturedAt: number;
|
|
39
|
+
source: string;
|
|
40
|
+
}
|
|
41
|
+
export declare function buildCapturePayload(input: CapturePayloadInput): Record<string, unknown>;
|
|
28
42
|
export declare function hashToken(token: string): string;
|
|
29
43
|
export declare function claudeCodeInstalled(): boolean;
|
|
30
44
|
export declare function codexInstalled(): boolean;
|
package/dist/utils.js
CHANGED
|
@@ -35,9 +35,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.readConfig = readConfig;
|
|
37
37
|
exports.writeConfig = writeConfig;
|
|
38
|
+
exports.parseGitRemoteFromConfig = parseGitRemoteFromConfig;
|
|
39
|
+
exports.readGitRemoteFromConfig = readGitRemoteFromConfig;
|
|
38
40
|
exports.getGitRemoteUrl = getGitRemoteUrl;
|
|
39
41
|
exports.normalizeRepoUrl = normalizeRepoUrl;
|
|
40
42
|
exports.matchRepo = matchRepo;
|
|
43
|
+
exports.buildCapturePayload = buildCapturePayload;
|
|
41
44
|
exports.hashToken = hashToken;
|
|
42
45
|
exports.claudeCodeInstalled = claudeCodeInstalled;
|
|
43
46
|
exports.codexInstalled = codexInstalled;
|
|
@@ -89,7 +92,115 @@ function writeConfig(config) {
|
|
|
89
92
|
}
|
|
90
93
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf8");
|
|
91
94
|
}
|
|
95
|
+
// Parse a remote URL out of git-config INI text WITHOUT invoking git. Prefers
|
|
96
|
+
// the "origin" remote; falls back to the first remote found. Returns null if
|
|
97
|
+
// there are no remote sections.
|
|
98
|
+
function parseGitRemoteFromConfig(configText) {
|
|
99
|
+
const remotes = {};
|
|
100
|
+
let current = null;
|
|
101
|
+
for (const raw of configText.split(/\r?\n/)) {
|
|
102
|
+
const line = raw.trim();
|
|
103
|
+
const remoteHeader = line.match(/^\[\s*remote\s+"([^"]+)"\s*\]$/);
|
|
104
|
+
if (remoteHeader) {
|
|
105
|
+
current = remoteHeader[1];
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (line.startsWith("[")) {
|
|
109
|
+
// Any other section header ends the current remote context, so a
|
|
110
|
+
// `url =` under e.g. [core] or [some-section] is never mistaken for one.
|
|
111
|
+
current = null;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (current) {
|
|
115
|
+
const kv = line.match(/^url\s*=\s*(.+)$/);
|
|
116
|
+
if (kv && remotes[current] === undefined) {
|
|
117
|
+
remotes[current] = kv[1].trim();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (remotes.origin)
|
|
122
|
+
return remotes.origin;
|
|
123
|
+
const first = Object.values(remotes)[0];
|
|
124
|
+
return first ?? null;
|
|
125
|
+
}
|
|
126
|
+
// Locate the git directory for `startDir`, walking up parent directories.
|
|
127
|
+
// Handles a `.git` directory (normal repo), a `.git` FILE containing
|
|
128
|
+
// `gitdir: <path>` (submodules, some devcontainer/worktree layouts), and stops
|
|
129
|
+
// at the filesystem root. Returns the resolved git dir path, or null.
|
|
130
|
+
function resolveGitDir(startDir) {
|
|
131
|
+
let dir = path.resolve(startDir);
|
|
132
|
+
// eslint-disable-next-line no-constant-condition
|
|
133
|
+
while (true) {
|
|
134
|
+
const dotGit = path.join(dir, ".git");
|
|
135
|
+
try {
|
|
136
|
+
const stat = fs.statSync(dotGit);
|
|
137
|
+
if (stat.isDirectory())
|
|
138
|
+
return dotGit;
|
|
139
|
+
if (stat.isFile()) {
|
|
140
|
+
const m = fs.readFileSync(dotGit, "utf8").match(/^gitdir:\s*(.+)\s*$/m);
|
|
141
|
+
if (m) {
|
|
142
|
+
const target = m[1].trim();
|
|
143
|
+
return path.isAbsolute(target) ? target : path.resolve(dir, target);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// No .git here — keep walking up.
|
|
149
|
+
}
|
|
150
|
+
const parent = path.dirname(dir);
|
|
151
|
+
if (parent === dir)
|
|
152
|
+
return null;
|
|
153
|
+
dir = parent;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Ordered list of config files that may hold the remotes for a git dir. Linked
|
|
157
|
+
// worktrees keep their remotes in the COMMON (main) git dir, pointed at by a
|
|
158
|
+
// `commondir` file, so we check that first, then the git dir's own config.
|
|
159
|
+
function gitConfigCandidates(gitDir) {
|
|
160
|
+
const candidates = [];
|
|
161
|
+
try {
|
|
162
|
+
const commondirFile = path.join(gitDir, "commondir");
|
|
163
|
+
if (fs.existsSync(commondirFile)) {
|
|
164
|
+
const rel = fs.readFileSync(commondirFile, "utf8").trim();
|
|
165
|
+
const commonDir = path.isAbsolute(rel) ? rel : path.resolve(gitDir, rel);
|
|
166
|
+
candidates.push(path.join(commonDir, "config"));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// ignore — fall through to the plain config below
|
|
171
|
+
}
|
|
172
|
+
candidates.push(path.join(gitDir, "config"));
|
|
173
|
+
return candidates;
|
|
174
|
+
}
|
|
175
|
+
// Resolve a repo's remote URL by reading .git/config directly — no `git`
|
|
176
|
+
// binary required. This is the primary path so lean container images (Alpine
|
|
177
|
+
// devcontainers without git) still capture sessions instead of silently
|
|
178
|
+
// dropping them.
|
|
179
|
+
function readGitRemoteFromConfig(startDir) {
|
|
180
|
+
const gitDir = resolveGitDir(startDir);
|
|
181
|
+
if (!gitDir)
|
|
182
|
+
return null;
|
|
183
|
+
for (const cfg of gitConfigCandidates(gitDir)) {
|
|
184
|
+
try {
|
|
185
|
+
const url = parseGitRemoteFromConfig(fs.readFileSync(cfg, "utf8"));
|
|
186
|
+
if (url)
|
|
187
|
+
return url;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// try the next candidate
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
92
195
|
function getGitRemoteUrl(cwd) {
|
|
196
|
+
// Primary: read .git/config directly. Works even when the `git` binary is
|
|
197
|
+
// absent (lean Alpine/devcontainer images), which previously made the
|
|
198
|
+
// SessionEnd hook silently drop every session.
|
|
199
|
+
const fromConfig = readGitRemoteFromConfig(cwd);
|
|
200
|
+
if (fromConfig)
|
|
201
|
+
return fromConfig;
|
|
202
|
+
// Fallback: ask git itself, for setups the direct read doesn't cover
|
|
203
|
+
// (insteadOf rewrites, includeIf'd remotes, unusual gitdir indirection).
|
|
93
204
|
try {
|
|
94
205
|
const url = (0, child_process_1.execSync)("git remote get-url origin", {
|
|
95
206
|
cwd,
|
|
@@ -136,6 +247,19 @@ function matchRepo(remoteUrl, repos) {
|
|
|
136
247
|
}
|
|
137
248
|
return null;
|
|
138
249
|
}
|
|
250
|
+
function buildCapturePayload(input) {
|
|
251
|
+
return {
|
|
252
|
+
sessionId: input.sessionId,
|
|
253
|
+
repoUrl: input.remoteUrl ?? "",
|
|
254
|
+
repoId: input.matchedRepoId,
|
|
255
|
+
companyId: input.companyId,
|
|
256
|
+
transcript: input.transcriptBase64,
|
|
257
|
+
messageCount: input.messageCount,
|
|
258
|
+
toolCallCount: input.toolCallCount,
|
|
259
|
+
capturedAt: input.capturedAt,
|
|
260
|
+
source: input.source,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
139
263
|
function hashToken(token) {
|
|
140
264
|
let hash = 0;
|
|
141
265
|
for (let i = 0; i < token.length; i++) {
|
package/package.json
CHANGED