@veris-ai/daytona-opencode 0.1.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/.opencode/plugin/daytona/core/logger.d.ts +15 -0
- package/.opencode/plugin/daytona/core/logger.js +65 -0
- package/.opencode/plugin/daytona/core/logger.js.map +1 -0
- package/.opencode/plugin/daytona/core/logger.ts +71 -0
- package/.opencode/plugin/daytona/core/project-data-storage.d.ts +63 -0
- package/.opencode/plugin/daytona/core/project-data-storage.js +213 -0
- package/.opencode/plugin/daytona/core/project-data-storage.js.map +1 -0
- package/.opencode/plugin/daytona/core/project-data-storage.ts +234 -0
- package/.opencode/plugin/daytona/core/session-manager.d.ts +74 -0
- package/.opencode/plugin/daytona/core/session-manager.js +441 -0
- package/.opencode/plugin/daytona/core/session-manager.js.map +1 -0
- package/.opencode/plugin/daytona/core/session-manager.ts +471 -0
- package/.opencode/plugin/daytona/core/toast.d.ts +47 -0
- package/.opencode/plugin/daytona/core/toast.js +70 -0
- package/.opencode/plugin/daytona/core/toast.js.map +1 -0
- package/.opencode/plugin/daytona/core/toast.ts +96 -0
- package/.opencode/plugin/daytona/core/types.d.ts +53 -0
- package/.opencode/plugin/daytona/core/types.js +12 -0
- package/.opencode/plugin/daytona/core/types.js.map +1 -0
- package/.opencode/plugin/daytona/core/types.ts +72 -0
- package/.opencode/plugin/daytona/git/host-git-manager.d.ts +58 -0
- package/.opencode/plugin/daytona/git/host-git-manager.js +342 -0
- package/.opencode/plugin/daytona/git/host-git-manager.js.map +1 -0
- package/.opencode/plugin/daytona/git/host-git-manager.ts +372 -0
- package/.opencode/plugin/daytona/git/index.d.ts +5 -0
- package/.opencode/plugin/daytona/git/index.js +6 -0
- package/.opencode/plugin/daytona/git/index.js.map +1 -0
- package/.opencode/plugin/daytona/git/index.ts +6 -0
- package/.opencode/plugin/daytona/git/sandbox-git-manager.d.ts +22 -0
- package/.opencode/plugin/daytona/git/sandbox-git-manager.js +80 -0
- package/.opencode/plugin/daytona/git/sandbox-git-manager.js.map +1 -0
- package/.opencode/plugin/daytona/git/sandbox-git-manager.ts +92 -0
- package/.opencode/plugin/daytona/git/session-git-manager.d.ts +54 -0
- package/.opencode/plugin/daytona/git/session-git-manager.js +184 -0
- package/.opencode/plugin/daytona/git/session-git-manager.js.map +1 -0
- package/.opencode/plugin/daytona/git/session-git-manager.ts +203 -0
- package/.opencode/plugin/daytona/index.d.ts +125 -0
- package/.opencode/plugin/daytona/index.js +60 -0
- package/.opencode/plugin/daytona/index.js.map +1 -0
- package/.opencode/plugin/daytona/index.ts +73 -0
- package/.opencode/plugin/daytona/plugins/custom-tools.d.ts +122 -0
- package/.opencode/plugin/daytona/plugins/custom-tools.js +18 -0
- package/.opencode/plugin/daytona/plugins/custom-tools.js.map +1 -0
- package/.opencode/plugin/daytona/plugins/custom-tools.ts +21 -0
- package/.opencode/plugin/daytona/plugins/session-events.d.ts +10 -0
- package/.opencode/plugin/daytona/plugins/session-events.js +63 -0
- package/.opencode/plugin/daytona/plugins/session-events.js.map +1 -0
- package/.opencode/plugin/daytona/plugins/session-events.ts +61 -0
- package/.opencode/plugin/daytona/plugins/system-transform.d.ts +10 -0
- package/.opencode/plugin/daytona/plugins/system-transform.js +38 -0
- package/.opencode/plugin/daytona/plugins/system-transform.js.map +1 -0
- package/.opencode/plugin/daytona/plugins/system-transform.ts +43 -0
- package/.opencode/plugin/daytona/tools/bash.d.ts +19 -0
- package/.opencode/plugin/daytona/tools/bash.js +42 -0
- package/.opencode/plugin/daytona/tools/bash.js.map +1 -0
- package/.opencode/plugin/daytona/tools/bash.ts +51 -0
- package/.opencode/plugin/daytona/tools/edit.d.ts +21 -0
- package/.opencode/plugin/daytona/tools/edit.js +33 -0
- package/.opencode/plugin/daytona/tools/edit.js.map +1 -0
- package/.opencode/plugin/daytona/tools/edit.ts +44 -0
- package/.opencode/plugin/daytona/tools/get-preview-url.d.ts +17 -0
- package/.opencode/plugin/daytona/tools/get-preview-url.js +17 -0
- package/.opencode/plugin/daytona/tools/get-preview-url.js.map +1 -0
- package/.opencode/plugin/daytona/tools/get-preview-url.ts +26 -0
- package/.opencode/plugin/daytona/tools/git-sync.d.ts +12 -0
- package/.opencode/plugin/daytona/tools/git-sync.js +26 -0
- package/.opencode/plugin/daytona/tools/git-sync.js.map +1 -0
- package/.opencode/plugin/daytona/tools/git-sync.ts +36 -0
- package/.opencode/plugin/daytona/tools/glob.d.ts +17 -0
- package/.opencode/plugin/daytona/tools/glob.js +21 -0
- package/.opencode/plugin/daytona/tools/glob.js.map +1 -0
- package/.opencode/plugin/daytona/tools/glob.ts +30 -0
- package/.opencode/plugin/daytona/tools/grep.d.ts +17 -0
- package/.opencode/plugin/daytona/tools/grep.js +29 -0
- package/.opencode/plugin/daytona/tools/grep.js.map +1 -0
- package/.opencode/plugin/daytona/tools/grep.ts +39 -0
- package/.opencode/plugin/daytona/tools/ls.d.ts +17 -0
- package/.opencode/plugin/daytona/tools/ls.js +22 -0
- package/.opencode/plugin/daytona/tools/ls.js.map +1 -0
- package/.opencode/plugin/daytona/tools/ls.ts +32 -0
- package/.opencode/plugin/daytona/tools/multiedit.d.ts +25 -0
- package/.opencode/plugin/daytona/tools/multiedit.js +37 -0
- package/.opencode/plugin/daytona/tools/multiedit.js.map +1 -0
- package/.opencode/plugin/daytona/tools/multiedit.ts +54 -0
- package/.opencode/plugin/daytona/tools/read.d.ts +17 -0
- package/.opencode/plugin/daytona/tools/read.js +18 -0
- package/.opencode/plugin/daytona/tools/read.js.map +1 -0
- package/.opencode/plugin/daytona/tools/read.ts +27 -0
- package/.opencode/plugin/daytona/tools/veris-receipt.d.ts +26 -0
- package/.opencode/plugin/daytona/tools/veris-receipt.js +76 -0
- package/.opencode/plugin/daytona/tools/veris-receipt.js.map +1 -0
- package/.opencode/plugin/daytona/tools/veris-receipt.ts +103 -0
- package/.opencode/plugin/daytona/tools/write.d.ts +19 -0
- package/.opencode/plugin/daytona/tools/write.js +18 -0
- package/.opencode/plugin/daytona/tools/write.js.map +1 -0
- package/.opencode/plugin/daytona/tools/write.ts +27 -0
- package/.opencode/plugin/daytona/tools.d.ts +119 -0
- package/.opencode/plugin/daytona/tools.js +35 -0
- package/.opencode/plugin/daytona/tools.js.map +1 -0
- package/.opencode/plugin/daytona/tools.ts +45 -0
- package/.opencode/plugin/index.d.ts +9 -0
- package/.opencode/plugin/index.js +10 -0
- package/.opencode/plugin/index.js.map +1 -0
- package/.opencode/plugin/index.ts +11 -0
- package/README.md +87 -0
- package/package.json +52 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Daytona Platforms Inc.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import type { LogLevel } from './types';
|
|
6
|
+
export declare function setLogFilePath(path: string): void;
|
|
7
|
+
declare class Logger {
|
|
8
|
+
private get logFile();
|
|
9
|
+
log(message: string, level?: LogLevel): void;
|
|
10
|
+
info(message: string): void;
|
|
11
|
+
error(message: string): void;
|
|
12
|
+
warn(message: string): void;
|
|
13
|
+
}
|
|
14
|
+
export declare const logger: Logger;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Daytona Platforms Inc.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Logger class for handling plugin logging
|
|
7
|
+
*/
|
|
8
|
+
import { appendFileSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'fs';
|
|
9
|
+
import { dirname } from 'path';
|
|
10
|
+
import { LOG_LEVEL_INFO, LOG_LEVEL_ERROR, LOG_LEVEL_WARN } from './types';
|
|
11
|
+
let logFilePath;
|
|
12
|
+
export function setLogFilePath(path) {
|
|
13
|
+
logFilePath = path;
|
|
14
|
+
}
|
|
15
|
+
class Logger {
|
|
16
|
+
get logFile() {
|
|
17
|
+
if (!logFilePath)
|
|
18
|
+
throw new Error('Logger file path not set. Call setLogFilePath(path) before use.');
|
|
19
|
+
return logFilePath;
|
|
20
|
+
}
|
|
21
|
+
log(message, level = LOG_LEVEL_INFO) {
|
|
22
|
+
// Ensure log directory exists
|
|
23
|
+
try {
|
|
24
|
+
mkdirSync(dirname(this.logFile), { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
// Directory may already exist, ignore
|
|
28
|
+
}
|
|
29
|
+
// Trim by byte length (not characters) so the 1MB target holds for non-ASCII logs
|
|
30
|
+
try {
|
|
31
|
+
const stats = statSync(this.logFile);
|
|
32
|
+
const maxSize = 3 * 1024 * 1024;
|
|
33
|
+
const keepSize = 1024 * 1024;
|
|
34
|
+
if (stats.size > maxSize) {
|
|
35
|
+
const buffer = readFileSync(this.logFile);
|
|
36
|
+
const trimmed = buffer.subarray(buffer.length - keepSize);
|
|
37
|
+
// Drop partial first line so we don't start mid-log
|
|
38
|
+
const firstNewline = trimmed.indexOf('\n');
|
|
39
|
+
writeFileSync(this.logFile, firstNewline >= 0 ? trimmed.subarray(firstNewline + 1) : trimmed);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
// File may not exist yet, ignore
|
|
44
|
+
}
|
|
45
|
+
const timestamp = new Date().toISOString();
|
|
46
|
+
const logEntry = `[${timestamp}] [${level}] ${message}\n`;
|
|
47
|
+
try {
|
|
48
|
+
appendFileSync(this.logFile, logEntry);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
// Best-effort logging: never let a write failure crash the caller
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
info(message) {
|
|
55
|
+
this.log(message, LOG_LEVEL_INFO);
|
|
56
|
+
}
|
|
57
|
+
error(message) {
|
|
58
|
+
this.log(message, LOG_LEVEL_ERROR);
|
|
59
|
+
}
|
|
60
|
+
warn(message) {
|
|
61
|
+
this.log(message, LOG_LEVEL_WARN);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export const logger = new Logger();
|
|
65
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["logger.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AAEH,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,IAAI,CAAA;AACrF,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAE9B,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAEzE,IAAI,WAA+B,CAAA;AAEnC,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,WAAW,GAAG,IAAI,CAAA;AACpB,CAAC;AAED,MAAM,MAAM;IACV,IAAY,OAAO;QACjB,IAAI,CAAC,WAAW;YAAE,MAAM,IAAI,KAAK,CAAC,iEAAiE,CAAC,CAAA;QACpG,OAAO,WAAW,CAAA;IACpB,CAAC;IAED,GAAG,CAAC,OAAe,EAAE,QAAkB,cAAc;QACnD,8BAA8B;QAC9B,IAAI,CAAC;YACH,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QACvD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,sCAAsC;QACxC,CAAC;QACD,kFAAkF;QAClF,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;YACpC,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAA;YAC/B,MAAM,QAAQ,GAAG,IAAI,GAAG,IAAI,CAAA;YAC5B,IAAI,KAAK,CAAC,IAAI,GAAG,OAAO,EAAE,CAAC;gBACzB,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;gBACzC,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAA;gBACzD,oDAAoD;gBACpD,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;gBAC1C,aAAa,CAAC,IAAI,CAAC,OAAO,EAAE,YAAY,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAA;YAC/F,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,iCAAiC;QACnC,CAAC;QACD,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;QAC1C,MAAM,QAAQ,GAAG,IAAI,SAAS,MAAM,KAAK,KAAK,OAAO,IAAI,CAAA;QACzD,IAAI,CAAC;YACH,cAAc,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;QACxC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,kEAAkE;QACpE,CAAC;IACH,CAAC;IAED,IAAI,CAAC,OAAe;QAClB,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,cAAc,CAAC,CAAA;IACnC,CAAC;IAED,KAAK,CAAC,OAAe;QACnB,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,eAAe,CAAC,CAAA;IACpC,CAAC;IAED,IAAI,CAAC,OAAe;QAClB,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,cAAc,CAAC,CAAA;IACnC,CAAC;CACF;AAED,MAAM,CAAC,MAAM,MAAM,GAAG,IAAI,MAAM,EAAE,CAAA"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Daytona Platforms Inc.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Logger class for handling plugin logging
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { appendFileSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'fs'
|
|
11
|
+
import { dirname } from 'path'
|
|
12
|
+
import type { LogLevel } from './types'
|
|
13
|
+
import { LOG_LEVEL_INFO, LOG_LEVEL_ERROR, LOG_LEVEL_WARN } from './types'
|
|
14
|
+
|
|
15
|
+
let logFilePath: string | undefined
|
|
16
|
+
|
|
17
|
+
export function setLogFilePath(path: string) {
|
|
18
|
+
logFilePath = path
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class Logger {
|
|
22
|
+
private get logFile() {
|
|
23
|
+
if (!logFilePath) throw new Error('Logger file path not set. Call setLogFilePath(path) before use.')
|
|
24
|
+
return logFilePath
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
log(message: string, level: LogLevel = LOG_LEVEL_INFO): void {
|
|
28
|
+
// Ensure log directory exists
|
|
29
|
+
try {
|
|
30
|
+
mkdirSync(dirname(this.logFile), { recursive: true })
|
|
31
|
+
} catch (err) {
|
|
32
|
+
// Directory may already exist, ignore
|
|
33
|
+
}
|
|
34
|
+
// Trim by byte length (not characters) so the 1MB target holds for non-ASCII logs
|
|
35
|
+
try {
|
|
36
|
+
const stats = statSync(this.logFile)
|
|
37
|
+
const maxSize = 3 * 1024 * 1024
|
|
38
|
+
const keepSize = 1024 * 1024
|
|
39
|
+
if (stats.size > maxSize) {
|
|
40
|
+
const buffer = readFileSync(this.logFile)
|
|
41
|
+
const trimmed = buffer.subarray(buffer.length - keepSize)
|
|
42
|
+
// Drop partial first line so we don't start mid-log
|
|
43
|
+
const firstNewline = trimmed.indexOf('\n')
|
|
44
|
+
writeFileSync(this.logFile, firstNewline >= 0 ? trimmed.subarray(firstNewline + 1) : trimmed)
|
|
45
|
+
}
|
|
46
|
+
} catch (err) {
|
|
47
|
+
// File may not exist yet, ignore
|
|
48
|
+
}
|
|
49
|
+
const timestamp = new Date().toISOString()
|
|
50
|
+
const logEntry = `[${timestamp}] [${level}] ${message}\n`
|
|
51
|
+
try {
|
|
52
|
+
appendFileSync(this.logFile, logEntry)
|
|
53
|
+
} catch (err) {
|
|
54
|
+
// Best-effort logging: never let a write failure crash the caller
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
info(message: string): void {
|
|
59
|
+
this.log(message, LOG_LEVEL_INFO)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
error(message: string): void {
|
|
63
|
+
this.log(message, LOG_LEVEL_ERROR)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
warn(message: string): void {
|
|
67
|
+
this.log(message, LOG_LEVEL_WARN)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const logger = new Logger()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Daytona Platforms Inc.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import type { ProjectSessionData, SessionInfo } from './types';
|
|
6
|
+
export declare class ProjectDataStorage {
|
|
7
|
+
private readonly storageDir;
|
|
8
|
+
constructor(storageDir: string);
|
|
9
|
+
/**
|
|
10
|
+
* Get the file path for a project's session data.
|
|
11
|
+
* encodeURIComponent gives a reversible, collision-free encoding that also
|
|
12
|
+
* strips path separators, so a projectId can't traverse outside storageDir
|
|
13
|
+
* and distinct ids can't collide onto the same file.
|
|
14
|
+
*/
|
|
15
|
+
private getProjectFilePath;
|
|
16
|
+
/**
|
|
17
|
+
* List known project IDs from storage, decoded to the canonical form used by callers.
|
|
18
|
+
* Filenames that can't be decoded (e.g. hand-created files with invalid percent escapes)
|
|
19
|
+
* are skipped: exposing them would return an id that can't round-trip through
|
|
20
|
+
* getProjectFilePath, causing subsequent load/save/remove to silently target the wrong file.
|
|
21
|
+
*/
|
|
22
|
+
private listProjectIds;
|
|
23
|
+
/**
|
|
24
|
+
* Load project session data from disk
|
|
25
|
+
*/
|
|
26
|
+
load(projectId: string): ProjectSessionData | null;
|
|
27
|
+
/**
|
|
28
|
+
* Get a session for a project. If not found in the requested project, search all other
|
|
29
|
+
* projects on disk and, if found, migrate it into the requested project.
|
|
30
|
+
*/
|
|
31
|
+
getSession(projectId: string, worktree: string, sessionId: string): SessionInfo | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Read-only lookup of a session across all project files. Unlike getSession, this never
|
|
34
|
+
* migrates or writes, so it is safe to use on the delete path.
|
|
35
|
+
*/
|
|
36
|
+
findSession(sessionId: string): {
|
|
37
|
+
projectId: string;
|
|
38
|
+
worktree: string;
|
|
39
|
+
session: SessionInfo;
|
|
40
|
+
} | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Save project session data to disk.
|
|
43
|
+
*
|
|
44
|
+
* `storageKey` identifies WHICH FILE on disk to write (round-trips through
|
|
45
|
+
* getProjectFilePath). `projectData.projectId` is the CANONICAL id written into
|
|
46
|
+
* the JSON body — kept separate so callers who reached a file via a filename-derived
|
|
47
|
+
* key (e.g. findSession → removeSession for legacy files) don't clobber the
|
|
48
|
+
* pre-existing canonical id with the storage key.
|
|
49
|
+
*/
|
|
50
|
+
save(storageKey: string, projectData: ProjectSessionData): void;
|
|
51
|
+
/**
|
|
52
|
+
* Get branch number for a sandbox
|
|
53
|
+
*/
|
|
54
|
+
getBranchNumberForSandbox(projectId: string, sandboxId: string): number | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Update a single session in the project file
|
|
57
|
+
*/
|
|
58
|
+
updateSession(projectId: string, worktree: string, sessionId: string, sandboxId: string, branchNumber?: number): void;
|
|
59
|
+
/**
|
|
60
|
+
* Remove a session from the project file
|
|
61
|
+
*/
|
|
62
|
+
removeSession(projectId: string, worktree: string, sessionId: string): void;
|
|
63
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Daytona Platforms Inc.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Handles file storage operations for project session data
|
|
7
|
+
* Stores data per-project in ~/.local/share/opencode/storage/daytona/{projectId}.json
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { logger } from './logger';
|
|
12
|
+
export class ProjectDataStorage {
|
|
13
|
+
storageDir;
|
|
14
|
+
constructor(storageDir) {
|
|
15
|
+
this.storageDir = storageDir;
|
|
16
|
+
// Ensure storage directory exists
|
|
17
|
+
if (!existsSync(this.storageDir)) {
|
|
18
|
+
mkdirSync(this.storageDir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get the file path for a project's session data.
|
|
23
|
+
* encodeURIComponent gives a reversible, collision-free encoding that also
|
|
24
|
+
* strips path separators, so a projectId can't traverse outside storageDir
|
|
25
|
+
* and distinct ids can't collide onto the same file.
|
|
26
|
+
*/
|
|
27
|
+
getProjectFilePath(projectId) {
|
|
28
|
+
return join(this.storageDir, `${encodeURIComponent(projectId)}.json`);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* List known project IDs from storage, decoded to the canonical form used by callers.
|
|
32
|
+
* Filenames that can't be decoded (e.g. hand-created files with invalid percent escapes)
|
|
33
|
+
* are skipped: exposing them would return an id that can't round-trip through
|
|
34
|
+
* getProjectFilePath, causing subsequent load/save/remove to silently target the wrong file.
|
|
35
|
+
*/
|
|
36
|
+
listProjectIds() {
|
|
37
|
+
try {
|
|
38
|
+
const ids = [];
|
|
39
|
+
for (const name of readdirSync(this.storageDir)) {
|
|
40
|
+
if (!name.endsWith('.json'))
|
|
41
|
+
continue;
|
|
42
|
+
const encoded = name.slice(0, -'.json'.length);
|
|
43
|
+
try {
|
|
44
|
+
ids.push(decodeURIComponent(encoded));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
logger.warn(`Skipping project data file with undecodable name: ${name}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return ids;
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
logger.error(`Failed to list project data files: ${err}`);
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Load project session data from disk
|
|
59
|
+
*/
|
|
60
|
+
load(projectId) {
|
|
61
|
+
const filePath = this.getProjectFilePath(projectId);
|
|
62
|
+
try {
|
|
63
|
+
if (existsSync(filePath)) {
|
|
64
|
+
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
logger.error(`Failed to load project data for ${projectId}: ${err}`);
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Get a session for a project. If not found in the requested project, search all other
|
|
74
|
+
* projects on disk and, if found, migrate it into the requested project.
|
|
75
|
+
*/
|
|
76
|
+
getSession(projectId, worktree, sessionId) {
|
|
77
|
+
const current = this.load(projectId);
|
|
78
|
+
const currentSession = current?.sessions?.[sessionId];
|
|
79
|
+
if (currentSession) {
|
|
80
|
+
return currentSession;
|
|
81
|
+
}
|
|
82
|
+
// Look in other projects and migrate if found.
|
|
83
|
+
for (const otherProjectId of this.listProjectIds()) {
|
|
84
|
+
if (otherProjectId === projectId)
|
|
85
|
+
continue;
|
|
86
|
+
const otherData = this.load(otherProjectId);
|
|
87
|
+
const found = otherData?.sessions?.[sessionId];
|
|
88
|
+
if (!found)
|
|
89
|
+
continue;
|
|
90
|
+
const destination = current ?? {
|
|
91
|
+
projectId,
|
|
92
|
+
worktree,
|
|
93
|
+
sessions: {},
|
|
94
|
+
};
|
|
95
|
+
// Write the destination first and confirm it landed on disk, so a write
|
|
96
|
+
// failure can never delete the source before the copy is safely persisted.
|
|
97
|
+
destination.sessions[sessionId] = found;
|
|
98
|
+
// Prefer the worktree for the project we're actually operating on.
|
|
99
|
+
destination.worktree = worktree;
|
|
100
|
+
this.save(projectId, destination);
|
|
101
|
+
if (!this.load(projectId)?.sessions?.[sessionId]) {
|
|
102
|
+
logger.error(`Migration of session ${sessionId} to project ${projectId} did not persist; leaving source intact`);
|
|
103
|
+
return found;
|
|
104
|
+
}
|
|
105
|
+
// Destination is safe; now remove from the source (best effort).
|
|
106
|
+
try {
|
|
107
|
+
delete otherData.sessions[sessionId];
|
|
108
|
+
this.save(otherProjectId, otherData);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
logger.warn(`Failed to remove session ${sessionId} from project ${otherProjectId}: ${err}`);
|
|
112
|
+
}
|
|
113
|
+
logger.info(`Migrated session ${sessionId} from project ${otherProjectId} to project ${projectId}`);
|
|
114
|
+
return found;
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Read-only lookup of a session across all project files. Unlike getSession, this never
|
|
120
|
+
* migrates or writes, so it is safe to use on the delete path.
|
|
121
|
+
*/
|
|
122
|
+
findSession(sessionId) {
|
|
123
|
+
for (const projectId of this.listProjectIds()) {
|
|
124
|
+
const data = this.load(projectId);
|
|
125
|
+
const session = data?.sessions?.[sessionId];
|
|
126
|
+
if (session && data) {
|
|
127
|
+
// Return the filename-derived projectId (the value that maps back to the file we
|
|
128
|
+
// just loaded), NOT data.projectId. The delete cleanup path passes this value to
|
|
129
|
+
// removeSession → load → getProjectFilePath; if we returned the raw canonical id
|
|
130
|
+
// and it doesn't round-trip identically (e.g. legacy files with a different
|
|
131
|
+
// sanitization scheme), the cleanup would silently target a different file and
|
|
132
|
+
// leave the stale mapping behind.
|
|
133
|
+
return { projectId, worktree: data.worktree, session };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Save project session data to disk.
|
|
140
|
+
*
|
|
141
|
+
* `storageKey` identifies WHICH FILE on disk to write (round-trips through
|
|
142
|
+
* getProjectFilePath). `projectData.projectId` is the CANONICAL id written into
|
|
143
|
+
* the JSON body — kept separate so callers who reached a file via a filename-derived
|
|
144
|
+
* key (e.g. findSession → removeSession for legacy files) don't clobber the
|
|
145
|
+
* pre-existing canonical id with the storage key.
|
|
146
|
+
*/
|
|
147
|
+
save(storageKey, projectData) {
|
|
148
|
+
const filePath = this.getProjectFilePath(storageKey);
|
|
149
|
+
try {
|
|
150
|
+
writeFileSync(filePath, JSON.stringify(projectData, null, 2));
|
|
151
|
+
logger.info(`Saved project data for ${projectData.projectId}`);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
logger.error(`Failed to save project data for ${projectData.projectId} at ${filePath}: ${err}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Get branch number for a sandbox
|
|
159
|
+
*/
|
|
160
|
+
getBranchNumberForSandbox(projectId, sandboxId) {
|
|
161
|
+
const projectData = this.load(projectId);
|
|
162
|
+
if (!projectData) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
const session = Object.values(projectData.sessions).find((s) => s.sandboxId === sandboxId);
|
|
166
|
+
return session?.branchNumber;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Update a single session in the project file
|
|
170
|
+
*/
|
|
171
|
+
updateSession(projectId, worktree, sessionId, sandboxId, branchNumber) {
|
|
172
|
+
const projectData = this.load(projectId) || {
|
|
173
|
+
projectId,
|
|
174
|
+
worktree,
|
|
175
|
+
sessions: {},
|
|
176
|
+
};
|
|
177
|
+
const now = Date.now();
|
|
178
|
+
if (!projectData.sessions[sessionId]) {
|
|
179
|
+
projectData.sessions[sessionId] = {
|
|
180
|
+
sandboxId,
|
|
181
|
+
...(branchNumber !== undefined ? { branchNumber } : {}),
|
|
182
|
+
created: now,
|
|
183
|
+
lastAccessed: now,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
projectData.sessions[sessionId].sandboxId = sandboxId;
|
|
188
|
+
projectData.sessions[sessionId].lastAccessed = now;
|
|
189
|
+
// Only update branch number if it wasn't set before
|
|
190
|
+
if (projectData.sessions[sessionId].branchNumber === undefined) {
|
|
191
|
+
if (branchNumber !== undefined) {
|
|
192
|
+
projectData.sessions[sessionId].branchNumber = branchNumber;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Refresh worktree from the current call (state that can change over time), but
|
|
197
|
+
// NEVER refresh projectData.projectId — that's identity, set at creation, and
|
|
198
|
+
// preserving it is the whole point of save()'s two-parameter API.
|
|
199
|
+
projectData.worktree = worktree;
|
|
200
|
+
this.save(projectId, projectData);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Remove a session from the project file
|
|
204
|
+
*/
|
|
205
|
+
removeSession(projectId, worktree, sessionId) {
|
|
206
|
+
const projectData = this.load(projectId);
|
|
207
|
+
if (projectData && projectData.sessions[sessionId]) {
|
|
208
|
+
delete projectData.sessions[sessionId];
|
|
209
|
+
this.save(projectId, projectData);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=project-data-storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-data-storage.js","sourceRoot":"","sources":["project-data-storage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Daytona Platforms Inc.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Handles file storage operations for project session data
|
|
8
|
+
* Stores data per-project in ~/.local/share/opencode/storage/daytona/{projectId}.json
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs'
|
|
12
|
+
import { join } from 'path'
|
|
13
|
+
import { logger } from './logger'
|
|
14
|
+
import type { ProjectSessionData, SessionInfo } from './types'
|
|
15
|
+
|
|
16
|
+
export class ProjectDataStorage {
|
|
17
|
+
private readonly storageDir: string
|
|
18
|
+
|
|
19
|
+
constructor(storageDir: string) {
|
|
20
|
+
this.storageDir = storageDir
|
|
21
|
+
|
|
22
|
+
// Ensure storage directory exists
|
|
23
|
+
if (!existsSync(this.storageDir)) {
|
|
24
|
+
mkdirSync(this.storageDir, { recursive: true })
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get the file path for a project's session data.
|
|
30
|
+
* encodeURIComponent gives a reversible, collision-free encoding that also
|
|
31
|
+
* strips path separators, so a projectId can't traverse outside storageDir
|
|
32
|
+
* and distinct ids can't collide onto the same file.
|
|
33
|
+
*/
|
|
34
|
+
private getProjectFilePath(projectId: string): string {
|
|
35
|
+
return join(this.storageDir, `${encodeURIComponent(projectId)}.json`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* List known project IDs from storage, decoded to the canonical form used by callers.
|
|
40
|
+
* Filenames that can't be decoded (e.g. hand-created files with invalid percent escapes)
|
|
41
|
+
* are skipped: exposing them would return an id that can't round-trip through
|
|
42
|
+
* getProjectFilePath, causing subsequent load/save/remove to silently target the wrong file.
|
|
43
|
+
*/
|
|
44
|
+
private listProjectIds(): string[] {
|
|
45
|
+
try {
|
|
46
|
+
const ids: string[] = []
|
|
47
|
+
for (const name of readdirSync(this.storageDir)) {
|
|
48
|
+
if (!name.endsWith('.json')) continue
|
|
49
|
+
const encoded = name.slice(0, -'.json'.length)
|
|
50
|
+
try {
|
|
51
|
+
ids.push(decodeURIComponent(encoded))
|
|
52
|
+
} catch {
|
|
53
|
+
logger.warn(`Skipping project data file with undecodable name: ${name}`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return ids
|
|
57
|
+
} catch (err) {
|
|
58
|
+
logger.error(`Failed to list project data files: ${err}`)
|
|
59
|
+
return []
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Load project session data from disk
|
|
65
|
+
*/
|
|
66
|
+
load(projectId: string): ProjectSessionData | null {
|
|
67
|
+
const filePath = this.getProjectFilePath(projectId)
|
|
68
|
+
try {
|
|
69
|
+
if (existsSync(filePath)) {
|
|
70
|
+
return JSON.parse(readFileSync(filePath, 'utf-8')) as ProjectSessionData
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
logger.error(`Failed to load project data for ${projectId}: ${err}`)
|
|
74
|
+
}
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get a session for a project. If not found in the requested project, search all other
|
|
80
|
+
* projects on disk and, if found, migrate it into the requested project.
|
|
81
|
+
*/
|
|
82
|
+
getSession(projectId: string, worktree: string, sessionId: string): SessionInfo | undefined {
|
|
83
|
+
const current = this.load(projectId)
|
|
84
|
+
const currentSession = current?.sessions?.[sessionId]
|
|
85
|
+
if (currentSession) {
|
|
86
|
+
return currentSession
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Look in other projects and migrate if found.
|
|
90
|
+
for (const otherProjectId of this.listProjectIds()) {
|
|
91
|
+
if (otherProjectId === projectId) continue
|
|
92
|
+
|
|
93
|
+
const otherData = this.load(otherProjectId)
|
|
94
|
+
const found = otherData?.sessions?.[sessionId]
|
|
95
|
+
if (!found) continue
|
|
96
|
+
|
|
97
|
+
const destination: ProjectSessionData = current ?? {
|
|
98
|
+
projectId,
|
|
99
|
+
worktree,
|
|
100
|
+
sessions: {},
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Write the destination first and confirm it landed on disk, so a write
|
|
104
|
+
// failure can never delete the source before the copy is safely persisted.
|
|
105
|
+
destination.sessions[sessionId] = found
|
|
106
|
+
// Prefer the worktree for the project we're actually operating on.
|
|
107
|
+
destination.worktree = worktree
|
|
108
|
+
this.save(projectId, destination)
|
|
109
|
+
|
|
110
|
+
if (!this.load(projectId)?.sessions?.[sessionId]) {
|
|
111
|
+
logger.error(`Migration of session ${sessionId} to project ${projectId} did not persist; leaving source intact`)
|
|
112
|
+
return found
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Destination is safe; now remove from the source (best effort).
|
|
116
|
+
try {
|
|
117
|
+
delete otherData!.sessions[sessionId]
|
|
118
|
+
this.save(otherProjectId, otherData!)
|
|
119
|
+
} catch (err) {
|
|
120
|
+
logger.warn(`Failed to remove session ${sessionId} from project ${otherProjectId}: ${err}`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
logger.info(`Migrated session ${sessionId} from project ${otherProjectId} to project ${projectId}`)
|
|
124
|
+
return found
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return undefined
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Read-only lookup of a session across all project files. Unlike getSession, this never
|
|
132
|
+
* migrates or writes, so it is safe to use on the delete path.
|
|
133
|
+
*/
|
|
134
|
+
findSession(sessionId: string): { projectId: string; worktree: string; session: SessionInfo } | undefined {
|
|
135
|
+
for (const projectId of this.listProjectIds()) {
|
|
136
|
+
const data = this.load(projectId)
|
|
137
|
+
const session = data?.sessions?.[sessionId]
|
|
138
|
+
if (session && data) {
|
|
139
|
+
// Return the filename-derived projectId (the value that maps back to the file we
|
|
140
|
+
// just loaded), NOT data.projectId. The delete cleanup path passes this value to
|
|
141
|
+
// removeSession → load → getProjectFilePath; if we returned the raw canonical id
|
|
142
|
+
// and it doesn't round-trip identically (e.g. legacy files with a different
|
|
143
|
+
// sanitization scheme), the cleanup would silently target a different file and
|
|
144
|
+
// leave the stale mapping behind.
|
|
145
|
+
return { projectId, worktree: data.worktree, session }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return undefined
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Save project session data to disk.
|
|
153
|
+
*
|
|
154
|
+
* `storageKey` identifies WHICH FILE on disk to write (round-trips through
|
|
155
|
+
* getProjectFilePath). `projectData.projectId` is the CANONICAL id written into
|
|
156
|
+
* the JSON body — kept separate so callers who reached a file via a filename-derived
|
|
157
|
+
* key (e.g. findSession → removeSession for legacy files) don't clobber the
|
|
158
|
+
* pre-existing canonical id with the storage key.
|
|
159
|
+
*/
|
|
160
|
+
save(storageKey: string, projectData: ProjectSessionData): void {
|
|
161
|
+
const filePath = this.getProjectFilePath(storageKey)
|
|
162
|
+
try {
|
|
163
|
+
writeFileSync(filePath, JSON.stringify(projectData, null, 2))
|
|
164
|
+
logger.info(`Saved project data for ${projectData.projectId}`)
|
|
165
|
+
} catch (err) {
|
|
166
|
+
logger.error(`Failed to save project data for ${projectData.projectId} at ${filePath}: ${err}`)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get branch number for a sandbox
|
|
172
|
+
*/
|
|
173
|
+
getBranchNumberForSandbox(projectId: string, sandboxId: string): number | undefined {
|
|
174
|
+
const projectData = this.load(projectId)
|
|
175
|
+
if (!projectData) {
|
|
176
|
+
return undefined
|
|
177
|
+
}
|
|
178
|
+
const session = Object.values(projectData.sessions).find((s) => s.sandboxId === sandboxId)
|
|
179
|
+
return session?.branchNumber
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Update a single session in the project file
|
|
184
|
+
*/
|
|
185
|
+
updateSession(
|
|
186
|
+
projectId: string,
|
|
187
|
+
worktree: string,
|
|
188
|
+
sessionId: string,
|
|
189
|
+
sandboxId: string,
|
|
190
|
+
branchNumber?: number,
|
|
191
|
+
): void {
|
|
192
|
+
const projectData = this.load(projectId) || {
|
|
193
|
+
projectId,
|
|
194
|
+
worktree,
|
|
195
|
+
sessions: {},
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const now = Date.now()
|
|
199
|
+
if (!projectData.sessions[sessionId]) {
|
|
200
|
+
projectData.sessions[sessionId] = {
|
|
201
|
+
sandboxId,
|
|
202
|
+
...(branchNumber !== undefined ? { branchNumber } : {}),
|
|
203
|
+
created: now,
|
|
204
|
+
lastAccessed: now,
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
projectData.sessions[sessionId].sandboxId = sandboxId
|
|
208
|
+
projectData.sessions[sessionId].lastAccessed = now
|
|
209
|
+
// Only update branch number if it wasn't set before
|
|
210
|
+
if (projectData.sessions[sessionId].branchNumber === undefined) {
|
|
211
|
+
if (branchNumber !== undefined) {
|
|
212
|
+
projectData.sessions[sessionId].branchNumber = branchNumber
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Refresh worktree from the current call (state that can change over time), but
|
|
218
|
+
// NEVER refresh projectData.projectId — that's identity, set at creation, and
|
|
219
|
+
// preserving it is the whole point of save()'s two-parameter API.
|
|
220
|
+
projectData.worktree = worktree
|
|
221
|
+
this.save(projectId, projectData)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Remove a session from the project file
|
|
226
|
+
*/
|
|
227
|
+
removeSession(projectId: string, worktree: string, sessionId: string): void {
|
|
228
|
+
const projectData = this.load(projectId)
|
|
229
|
+
if (projectData && projectData.sessions[sessionId]) {
|
|
230
|
+
delete projectData.sessions[sessionId]
|
|
231
|
+
this.save(projectId, projectData)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|