@link-assistant/agent 0.0.9 → 0.0.12
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/EXAMPLES.md +36 -0
- package/MODELS.md +72 -24
- package/README.md +59 -2
- package/TOOLS.md +20 -0
- package/package.json +35 -2
- package/src/agent/agent.ts +68 -54
- package/src/auth/claude-oauth.ts +426 -0
- package/src/auth/index.ts +28 -26
- package/src/auth/plugins.ts +876 -0
- package/src/bun/index.ts +53 -43
- package/src/bus/global.ts +5 -5
- package/src/bus/index.ts +59 -53
- package/src/cli/bootstrap.js +12 -12
- package/src/cli/bootstrap.ts +6 -6
- package/src/cli/cmd/agent.ts +97 -92
- package/src/cli/cmd/auth.ts +469 -0
- package/src/cli/cmd/cmd.ts +2 -2
- package/src/cli/cmd/export.ts +41 -41
- package/src/cli/cmd/mcp.ts +144 -119
- package/src/cli/cmd/models.ts +30 -29
- package/src/cli/cmd/run.ts +269 -213
- package/src/cli/cmd/stats.ts +185 -146
- package/src/cli/error.ts +17 -13
- package/src/cli/ui.ts +39 -24
- package/src/command/index.ts +26 -26
- package/src/config/config.ts +528 -288
- package/src/config/markdown.ts +15 -15
- package/src/file/ripgrep.ts +201 -169
- package/src/file/time.ts +21 -18
- package/src/file/watcher.ts +51 -42
- package/src/file.ts +1 -1
- package/src/flag/flag.ts +26 -11
- package/src/format/formatter.ts +206 -162
- package/src/format/index.ts +61 -61
- package/src/global/index.ts +21 -21
- package/src/id/id.ts +47 -33
- package/src/index.js +346 -199
- package/src/json-standard/index.ts +67 -51
- package/src/mcp/index.ts +135 -128
- package/src/patch/index.ts +336 -267
- package/src/project/bootstrap.ts +15 -15
- package/src/project/instance.ts +43 -36
- package/src/project/project.ts +47 -47
- package/src/project/state.ts +37 -33
- package/src/provider/models-macro.ts +5 -5
- package/src/provider/models.ts +32 -32
- package/src/provider/opencode.js +19 -19
- package/src/provider/provider.ts +518 -277
- package/src/provider/transform.ts +143 -102
- package/src/server/project.ts +21 -21
- package/src/server/server.ts +111 -105
- package/src/session/agent.js +66 -60
- package/src/session/compaction.ts +136 -111
- package/src/session/index.ts +189 -156
- package/src/session/message-v2.ts +312 -268
- package/src/session/message.ts +73 -57
- package/src/session/processor.ts +180 -166
- package/src/session/prompt.ts +678 -533
- package/src/session/retry.ts +26 -23
- package/src/session/revert.ts +76 -62
- package/src/session/status.ts +26 -26
- package/src/session/summary.ts +97 -76
- package/src/session/system.ts +77 -63
- package/src/session/todo.ts +22 -16
- package/src/snapshot/index.ts +92 -76
- package/src/storage/storage.ts +157 -120
- package/src/tool/bash.ts +116 -106
- package/src/tool/batch.ts +73 -59
- package/src/tool/codesearch.ts +60 -53
- package/src/tool/edit.ts +319 -263
- package/src/tool/glob.ts +32 -28
- package/src/tool/grep.ts +72 -53
- package/src/tool/invalid.ts +7 -7
- package/src/tool/ls.ts +77 -64
- package/src/tool/multiedit.ts +30 -21
- package/src/tool/patch.ts +121 -94
- package/src/tool/read.ts +140 -122
- package/src/tool/registry.ts +38 -38
- package/src/tool/task.ts +93 -60
- package/src/tool/todo.ts +16 -16
- package/src/tool/tool.ts +45 -36
- package/src/tool/webfetch.ts +97 -74
- package/src/tool/websearch.ts +78 -64
- package/src/tool/write.ts +21 -15
- package/src/util/binary.ts +27 -19
- package/src/util/context.ts +8 -8
- package/src/util/defer.ts +7 -5
- package/src/util/error.ts +24 -19
- package/src/util/eventloop.ts +16 -10
- package/src/util/filesystem.ts +37 -33
- package/src/util/fn.ts +11 -8
- package/src/util/iife.ts +1 -1
- package/src/util/keybind.ts +44 -44
- package/src/util/lazy.ts +7 -7
- package/src/util/locale.ts +20 -16
- package/src/util/lock.ts +43 -38
- package/src/util/log.ts +95 -85
- package/src/util/queue.ts +8 -8
- package/src/util/rpc.ts +35 -23
- package/src/util/scrap.ts +4 -4
- package/src/util/signal.ts +5 -5
- package/src/util/timeout.ts +6 -6
- package/src/util/token.ts +2 -2
- package/src/util/wildcard.ts +38 -27
package/src/session/system.ts
CHANGED
|
@@ -1,138 +1,152 @@
|
|
|
1
|
-
import { Ripgrep } from
|
|
2
|
-
import { Global } from
|
|
3
|
-
import { Filesystem } from
|
|
4
|
-
import { Config } from
|
|
1
|
+
import { Ripgrep } from '../file/ripgrep';
|
|
2
|
+
import { Global } from '../global';
|
|
3
|
+
import { Filesystem } from '../util/filesystem';
|
|
4
|
+
import { Config } from '../config/config';
|
|
5
5
|
|
|
6
|
-
import { Instance } from
|
|
7
|
-
import path from
|
|
8
|
-
import os from
|
|
6
|
+
import { Instance } from '../project/instance';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import os from 'os';
|
|
9
9
|
|
|
10
|
-
import PROMPT_ANTHROPIC from
|
|
11
|
-
import PROMPT_ANTHROPIC_WITHOUT_TODO from
|
|
12
|
-
import PROMPT_POLARIS from
|
|
13
|
-
import PROMPT_BEAST from
|
|
14
|
-
import PROMPT_GEMINI from
|
|
15
|
-
import PROMPT_ANTHROPIC_SPOOF from
|
|
16
|
-
import PROMPT_SUMMARIZE from
|
|
17
|
-
import PROMPT_TITLE from
|
|
18
|
-
import PROMPT_CODEX from
|
|
19
|
-
import PROMPT_GROK_CODE from
|
|
10
|
+
import PROMPT_ANTHROPIC from './prompt/anthropic.txt';
|
|
11
|
+
import PROMPT_ANTHROPIC_WITHOUT_TODO from './prompt/qwen.txt';
|
|
12
|
+
import PROMPT_POLARIS from './prompt/polaris.txt';
|
|
13
|
+
import PROMPT_BEAST from './prompt/beast.txt';
|
|
14
|
+
import PROMPT_GEMINI from './prompt/gemini.txt';
|
|
15
|
+
import PROMPT_ANTHROPIC_SPOOF from './prompt/anthropic_spoof.txt';
|
|
16
|
+
import PROMPT_SUMMARIZE from './prompt/summarize.txt';
|
|
17
|
+
import PROMPT_TITLE from './prompt/title.txt';
|
|
18
|
+
import PROMPT_CODEX from './prompt/codex.txt';
|
|
19
|
+
import PROMPT_GROK_CODE from './prompt/grok-code.txt';
|
|
20
20
|
|
|
21
21
|
export namespace SystemPrompt {
|
|
22
22
|
export function header(providerID: string) {
|
|
23
|
-
if (providerID.includes(
|
|
24
|
-
|
|
23
|
+
if (providerID.includes('anthropic'))
|
|
24
|
+
return [PROMPT_ANTHROPIC_SPOOF.trim()];
|
|
25
|
+
return [];
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export function provider(modelID: string) {
|
|
28
|
-
if (modelID.includes(
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
if (modelID.includes('gpt-5')) return [PROMPT_CODEX];
|
|
30
|
+
if (
|
|
31
|
+
modelID.includes('gpt-') ||
|
|
32
|
+
modelID.includes('o1') ||
|
|
33
|
+
modelID.includes('o3')
|
|
34
|
+
)
|
|
35
|
+
return [PROMPT_BEAST];
|
|
36
|
+
if (modelID.includes('gemini-')) return [PROMPT_GEMINI];
|
|
37
|
+
if (modelID.includes('claude')) return [PROMPT_ANTHROPIC];
|
|
38
|
+
if (modelID.includes('polaris-alpha')) return [PROMPT_POLARIS];
|
|
39
|
+
if (modelID.includes('grok-code')) return [PROMPT_GROK_CODE];
|
|
40
|
+
return [PROMPT_ANTHROPIC_WITHOUT_TODO];
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
export async function environment() {
|
|
38
|
-
const project = Instance.project
|
|
44
|
+
const project = Instance.project;
|
|
39
45
|
return [
|
|
40
46
|
[
|
|
41
47
|
`Here is some useful information about the environment you are running in:`,
|
|
42
48
|
`<env>`,
|
|
43
49
|
` Working directory: ${Instance.directory}`,
|
|
44
|
-
` Is directory a git repo: ${project.vcs ===
|
|
50
|
+
` Is directory a git repo: ${project.vcs === 'git' ? 'yes' : 'no'}`,
|
|
45
51
|
` Platform: ${process.platform}`,
|
|
46
52
|
` Today's date: ${new Date().toDateString()}`,
|
|
47
53
|
`</env>`,
|
|
48
54
|
`<files>`,
|
|
49
55
|
` ${
|
|
50
|
-
project.vcs ===
|
|
56
|
+
project.vcs === 'git'
|
|
51
57
|
? await Ripgrep.tree({
|
|
52
58
|
cwd: Instance.directory,
|
|
53
59
|
limit: 200,
|
|
54
60
|
})
|
|
55
|
-
:
|
|
61
|
+
: ''
|
|
56
62
|
}`,
|
|
57
63
|
`</files>`,
|
|
58
|
-
].join(
|
|
59
|
-
]
|
|
64
|
+
].join('\n'),
|
|
65
|
+
];
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
const LOCAL_RULE_FILES = [
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
]
|
|
69
|
+
'AGENTS.md',
|
|
70
|
+
'CLAUDE.md',
|
|
71
|
+
'CONTEXT.md', // deprecated
|
|
72
|
+
];
|
|
67
73
|
const GLOBAL_RULE_FILES = [
|
|
68
|
-
path.join(Global.Path.config,
|
|
69
|
-
path.join(os.homedir(),
|
|
70
|
-
]
|
|
74
|
+
path.join(Global.Path.config, 'AGENTS.md'),
|
|
75
|
+
path.join(os.homedir(), '.claude', 'CLAUDE.md'),
|
|
76
|
+
];
|
|
71
77
|
|
|
72
78
|
export async function custom() {
|
|
73
|
-
const config = await Config.get()
|
|
74
|
-
const paths = new Set<string>()
|
|
79
|
+
const config = await Config.get();
|
|
80
|
+
const paths = new Set<string>();
|
|
75
81
|
|
|
76
82
|
for (const localRuleFile of LOCAL_RULE_FILES) {
|
|
77
|
-
const matches = await Filesystem.findUp(
|
|
83
|
+
const matches = await Filesystem.findUp(
|
|
84
|
+
localRuleFile,
|
|
85
|
+
Instance.directory,
|
|
86
|
+
Instance.worktree
|
|
87
|
+
);
|
|
78
88
|
if (matches.length > 0) {
|
|
79
|
-
matches.forEach((path) => paths.add(path))
|
|
80
|
-
break
|
|
89
|
+
matches.forEach((path) => paths.add(path));
|
|
90
|
+
break;
|
|
81
91
|
}
|
|
82
92
|
}
|
|
83
93
|
|
|
84
94
|
for (const globalRuleFile of GLOBAL_RULE_FILES) {
|
|
85
95
|
if (await Bun.file(globalRuleFile).exists()) {
|
|
86
|
-
paths.add(globalRuleFile)
|
|
87
|
-
break
|
|
96
|
+
paths.add(globalRuleFile);
|
|
97
|
+
break;
|
|
88
98
|
}
|
|
89
99
|
}
|
|
90
100
|
|
|
91
101
|
if (config.instructions) {
|
|
92
102
|
for (let instruction of config.instructions) {
|
|
93
|
-
if (instruction.startsWith(
|
|
94
|
-
instruction = path.join(os.homedir(), instruction.slice(2))
|
|
103
|
+
if (instruction.startsWith('~/')) {
|
|
104
|
+
instruction = path.join(os.homedir(), instruction.slice(2));
|
|
95
105
|
}
|
|
96
|
-
let matches: string[] = []
|
|
106
|
+
let matches: string[] = [];
|
|
97
107
|
if (path.isAbsolute(instruction)) {
|
|
98
108
|
matches = await Array.fromAsync(
|
|
99
109
|
new Bun.Glob(path.basename(instruction)).scan({
|
|
100
110
|
cwd: path.dirname(instruction),
|
|
101
111
|
absolute: true,
|
|
102
112
|
onlyFiles: true,
|
|
103
|
-
})
|
|
104
|
-
).catch(() => [])
|
|
113
|
+
})
|
|
114
|
+
).catch(() => []);
|
|
105
115
|
} else {
|
|
106
|
-
matches = await Filesystem.globUp(
|
|
116
|
+
matches = await Filesystem.globUp(
|
|
117
|
+
instruction,
|
|
118
|
+
Instance.directory,
|
|
119
|
+
Instance.worktree
|
|
120
|
+
).catch(() => []);
|
|
107
121
|
}
|
|
108
|
-
matches.forEach((path) => paths.add(path))
|
|
122
|
+
matches.forEach((path) => paths.add(path));
|
|
109
123
|
}
|
|
110
124
|
}
|
|
111
125
|
|
|
112
126
|
const found = Array.from(paths).map((p) =>
|
|
113
127
|
Bun.file(p)
|
|
114
128
|
.text()
|
|
115
|
-
.catch(() =>
|
|
116
|
-
.then((x) =>
|
|
117
|
-
)
|
|
118
|
-
return Promise.all(found).then((result) => result.filter(Boolean))
|
|
129
|
+
.catch(() => '')
|
|
130
|
+
.then((x) => 'Instructions from: ' + p + '\n' + x)
|
|
131
|
+
);
|
|
132
|
+
return Promise.all(found).then((result) => result.filter(Boolean));
|
|
119
133
|
}
|
|
120
134
|
|
|
121
135
|
export function summarize(providerID: string) {
|
|
122
136
|
switch (providerID) {
|
|
123
|
-
case
|
|
124
|
-
return [PROMPT_ANTHROPIC_SPOOF.trim(), PROMPT_SUMMARIZE]
|
|
137
|
+
case 'anthropic':
|
|
138
|
+
return [PROMPT_ANTHROPIC_SPOOF.trim(), PROMPT_SUMMARIZE];
|
|
125
139
|
default:
|
|
126
|
-
return [PROMPT_SUMMARIZE]
|
|
140
|
+
return [PROMPT_SUMMARIZE];
|
|
127
141
|
}
|
|
128
142
|
}
|
|
129
143
|
|
|
130
144
|
export function title(providerID: string) {
|
|
131
145
|
switch (providerID) {
|
|
132
|
-
case
|
|
133
|
-
return [PROMPT_ANTHROPIC_SPOOF.trim(), PROMPT_TITLE]
|
|
146
|
+
case 'anthropic':
|
|
147
|
+
return [PROMPT_ANTHROPIC_SPOOF.trim(), PROMPT_TITLE];
|
|
134
148
|
default:
|
|
135
|
-
return [PROMPT_TITLE]
|
|
149
|
+
return [PROMPT_TITLE];
|
|
136
150
|
}
|
|
137
151
|
}
|
|
138
152
|
}
|
package/src/session/todo.ts
CHANGED
|
@@ -1,36 +1,42 @@
|
|
|
1
|
-
import z from
|
|
2
|
-
import { Bus } from
|
|
3
|
-
import { Storage } from
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
import { Bus } from '../bus';
|
|
3
|
+
import { Storage } from '../storage/storage';
|
|
4
4
|
|
|
5
5
|
export namespace Todo {
|
|
6
6
|
export const Info = z
|
|
7
7
|
.object({
|
|
8
|
-
content: z.string().describe(
|
|
9
|
-
status: z
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
content: z.string().describe('Brief description of the task'),
|
|
9
|
+
status: z
|
|
10
|
+
.string()
|
|
11
|
+
.describe(
|
|
12
|
+
'Current status of the task: pending, in_progress, completed, cancelled'
|
|
13
|
+
),
|
|
14
|
+
priority: z
|
|
15
|
+
.string()
|
|
16
|
+
.describe('Priority level of the task: high, medium, low'),
|
|
17
|
+
id: z.string().describe('Unique identifier for the todo item'),
|
|
12
18
|
})
|
|
13
|
-
.meta({ ref:
|
|
14
|
-
export type Info = z.infer<typeof Info
|
|
19
|
+
.meta({ ref: 'Todo' });
|
|
20
|
+
export type Info = z.infer<typeof Info>;
|
|
15
21
|
|
|
16
22
|
export const Event = {
|
|
17
23
|
Updated: Bus.event(
|
|
18
|
-
|
|
24
|
+
'todo.updated',
|
|
19
25
|
z.object({
|
|
20
26
|
sessionID: z.string(),
|
|
21
27
|
todos: z.array(Info),
|
|
22
|
-
})
|
|
28
|
+
})
|
|
23
29
|
),
|
|
24
|
-
}
|
|
30
|
+
};
|
|
25
31
|
|
|
26
32
|
export async function update(input: { sessionID: string; todos: Info[] }) {
|
|
27
|
-
await Storage.write([
|
|
28
|
-
Bus.publish(Event.Updated, input)
|
|
33
|
+
await Storage.write(['todo', input.sessionID], input.todos);
|
|
34
|
+
Bus.publish(Event.Updated, input);
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
export async function get(sessionID: string) {
|
|
32
|
-
return Storage.read<Info[]>([
|
|
38
|
+
return Storage.read<Info[]>(['todo', sessionID])
|
|
33
39
|
.then((x) => x || [])
|
|
34
|
-
.catch(() => [])
|
|
40
|
+
.catch(() => []);
|
|
35
41
|
}
|
|
36
42
|
}
|
package/src/snapshot/index.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import { $ } from
|
|
2
|
-
import path from
|
|
3
|
-
import fs from
|
|
4
|
-
import { Log } from
|
|
5
|
-
import { Global } from
|
|
6
|
-
import z from
|
|
7
|
-
import { Config } from
|
|
8
|
-
import { Instance } from
|
|
1
|
+
import { $ } from 'bun';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import { Log } from '../util/log';
|
|
5
|
+
import { Global } from '../global';
|
|
6
|
+
import z from 'zod';
|
|
7
|
+
import { Config } from '../config/config';
|
|
8
|
+
import { Instance } from '../project/instance';
|
|
9
9
|
|
|
10
10
|
export namespace Snapshot {
|
|
11
|
-
const log = Log.create({ service:
|
|
11
|
+
const log = Log.create({ service: 'snapshot' });
|
|
12
12
|
|
|
13
13
|
export async function track() {
|
|
14
|
-
if (Instance.project.vcs !==
|
|
15
|
-
const cfg = await Config.get()
|
|
16
|
-
if (cfg.snapshot === false) return
|
|
17
|
-
const git = gitdir()
|
|
14
|
+
if (Instance.project.vcs !== 'git') return;
|
|
15
|
+
const cfg = await Config.get();
|
|
16
|
+
if (cfg.snapshot === false) return;
|
|
17
|
+
const git = gitdir();
|
|
18
18
|
if (await fs.mkdir(git, { recursive: true })) {
|
|
19
19
|
await $`git init`
|
|
20
20
|
.env({
|
|
@@ -23,125 +23,138 @@ export namespace Snapshot {
|
|
|
23
23
|
GIT_WORK_TREE: Instance.worktree,
|
|
24
24
|
})
|
|
25
25
|
.quiet()
|
|
26
|
-
.nothrow()
|
|
26
|
+
.nothrow();
|
|
27
27
|
// Configure git to not convert line endings on Windows
|
|
28
|
-
await $`git --git-dir ${git} config core.autocrlf false
|
|
29
|
-
|
|
28
|
+
await $`git --git-dir ${git} config core.autocrlf false`
|
|
29
|
+
.quiet()
|
|
30
|
+
.nothrow();
|
|
31
|
+
log.info('initialized');
|
|
30
32
|
}
|
|
31
|
-
await $`git --git-dir ${git} --work-tree ${Instance.worktree} add
|
|
32
|
-
const hash = await $`git --git-dir ${git} --work-tree ${Instance.worktree} write-tree`
|
|
33
|
+
await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`
|
|
33
34
|
.quiet()
|
|
34
35
|
.cwd(Instance.directory)
|
|
35
|
-
.nothrow()
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
.nothrow();
|
|
37
|
+
const hash =
|
|
38
|
+
await $`git --git-dir ${git} --work-tree ${Instance.worktree} write-tree`
|
|
39
|
+
.quiet()
|
|
40
|
+
.cwd(Instance.directory)
|
|
41
|
+
.nothrow()
|
|
42
|
+
.text();
|
|
43
|
+
log.info('tracking', { hash, cwd: Instance.directory, git });
|
|
44
|
+
return hash.trim();
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
export const Patch = z.object({
|
|
42
48
|
hash: z.string(),
|
|
43
49
|
files: z.string().array(),
|
|
44
|
-
})
|
|
45
|
-
export type Patch = z.infer<typeof Patch
|
|
50
|
+
});
|
|
51
|
+
export type Patch = z.infer<typeof Patch>;
|
|
46
52
|
|
|
47
53
|
export async function patch(hash: string): Promise<Patch> {
|
|
48
|
-
const git = gitdir()
|
|
49
|
-
await $`git --git-dir ${git} --work-tree ${Instance.worktree} add
|
|
54
|
+
const git = gitdir();
|
|
55
|
+
await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`
|
|
56
|
+
.quiet()
|
|
57
|
+
.cwd(Instance.directory)
|
|
58
|
+
.nothrow();
|
|
50
59
|
const result =
|
|
51
60
|
await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} diff --name-only ${hash} -- .`
|
|
52
61
|
.quiet()
|
|
53
62
|
.cwd(Instance.directory)
|
|
54
|
-
.nothrow()
|
|
63
|
+
.nothrow();
|
|
55
64
|
|
|
56
65
|
// If git diff fails, return empty patch
|
|
57
66
|
if (result.exitCode !== 0) {
|
|
58
|
-
log.warn(
|
|
59
|
-
return { hash, files: [] }
|
|
67
|
+
log.warn('failed to get diff', { hash, exitCode: result.exitCode });
|
|
68
|
+
return { hash, files: [] };
|
|
60
69
|
}
|
|
61
70
|
|
|
62
|
-
const files = result.text()
|
|
71
|
+
const files = result.text();
|
|
63
72
|
return {
|
|
64
73
|
hash,
|
|
65
74
|
files: files
|
|
66
75
|
.trim()
|
|
67
|
-
.split(
|
|
76
|
+
.split('\n')
|
|
68
77
|
.map((x) => x.trim())
|
|
69
78
|
.filter(Boolean)
|
|
70
79
|
.map((x) => path.join(Instance.worktree, x)),
|
|
71
|
-
}
|
|
80
|
+
};
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
export async function restore(snapshot: string) {
|
|
75
|
-
log.info(
|
|
76
|
-
const git = gitdir()
|
|
84
|
+
log.info('restore', { commit: snapshot });
|
|
85
|
+
const git = gitdir();
|
|
77
86
|
const result =
|
|
78
87
|
await $`git --git-dir ${git} --work-tree ${Instance.worktree} read-tree ${snapshot} && git --git-dir ${git} --work-tree ${Instance.worktree} checkout-index -a -f`
|
|
79
88
|
.quiet()
|
|
80
89
|
.cwd(Instance.worktree)
|
|
81
|
-
.nothrow()
|
|
90
|
+
.nothrow();
|
|
82
91
|
|
|
83
92
|
if (result.exitCode !== 0) {
|
|
84
|
-
log.error(
|
|
93
|
+
log.error('failed to restore snapshot', {
|
|
85
94
|
snapshot,
|
|
86
95
|
exitCode: result.exitCode,
|
|
87
96
|
stderr: result.stderr.toString(),
|
|
88
97
|
stdout: result.stdout.toString(),
|
|
89
|
-
})
|
|
98
|
+
});
|
|
90
99
|
}
|
|
91
100
|
}
|
|
92
101
|
|
|
93
102
|
export async function revert(patches: Patch[]) {
|
|
94
|
-
const files = new Set<string>()
|
|
95
|
-
const git = gitdir()
|
|
103
|
+
const files = new Set<string>();
|
|
104
|
+
const git = gitdir();
|
|
96
105
|
for (const item of patches) {
|
|
97
106
|
for (const file of item.files) {
|
|
98
|
-
if (files.has(file)) continue
|
|
99
|
-
log.info(
|
|
100
|
-
const result =
|
|
101
|
-
.
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
if (files.has(file)) continue;
|
|
108
|
+
log.info('reverting', { file, hash: item.hash });
|
|
109
|
+
const result =
|
|
110
|
+
await $`git --git-dir ${git} --work-tree ${Instance.worktree} checkout ${item.hash} -- ${file}`
|
|
111
|
+
.quiet()
|
|
112
|
+
.cwd(Instance.worktree)
|
|
113
|
+
.nothrow();
|
|
104
114
|
if (result.exitCode !== 0) {
|
|
105
|
-
const relativePath = path.relative(Instance.worktree, file)
|
|
115
|
+
const relativePath = path.relative(Instance.worktree, file);
|
|
106
116
|
const checkTree =
|
|
107
117
|
await $`git --git-dir ${git} --work-tree ${Instance.worktree} ls-tree ${item.hash} -- ${relativePath}`
|
|
108
118
|
.quiet()
|
|
109
119
|
.cwd(Instance.worktree)
|
|
110
|
-
.nothrow()
|
|
120
|
+
.nothrow();
|
|
111
121
|
if (checkTree.exitCode === 0 && checkTree.text().trim()) {
|
|
112
|
-
log.info(
|
|
122
|
+
log.info('file existed in snapshot but checkout failed, keeping', {
|
|
113
123
|
file,
|
|
114
|
-
})
|
|
124
|
+
});
|
|
115
125
|
} else {
|
|
116
|
-
log.info(
|
|
117
|
-
await fs.unlink(file).catch(() => {})
|
|
126
|
+
log.info('file did not exist in snapshot, deleting', { file });
|
|
127
|
+
await fs.unlink(file).catch(() => {});
|
|
118
128
|
}
|
|
119
129
|
}
|
|
120
|
-
files.add(file)
|
|
130
|
+
files.add(file);
|
|
121
131
|
}
|
|
122
132
|
}
|
|
123
133
|
}
|
|
124
134
|
|
|
125
135
|
export async function diff(hash: string) {
|
|
126
|
-
const git = gitdir()
|
|
127
|
-
await $`git --git-dir ${git} --work-tree ${Instance.worktree} add
|
|
136
|
+
const git = gitdir();
|
|
137
|
+
await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`
|
|
138
|
+
.quiet()
|
|
139
|
+
.cwd(Instance.directory)
|
|
140
|
+
.nothrow();
|
|
128
141
|
const result =
|
|
129
142
|
await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} diff ${hash} -- .`
|
|
130
143
|
.quiet()
|
|
131
144
|
.cwd(Instance.worktree)
|
|
132
|
-
.nothrow()
|
|
145
|
+
.nothrow();
|
|
133
146
|
|
|
134
147
|
if (result.exitCode !== 0) {
|
|
135
|
-
log.warn(
|
|
148
|
+
log.warn('failed to get diff', {
|
|
136
149
|
hash,
|
|
137
150
|
exitCode: result.exitCode,
|
|
138
151
|
stderr: result.stderr.toString(),
|
|
139
152
|
stdout: result.stdout.toString(),
|
|
140
|
-
})
|
|
141
|
-
return
|
|
153
|
+
});
|
|
154
|
+
return '';
|
|
142
155
|
}
|
|
143
156
|
|
|
144
|
-
return result.text().trim()
|
|
157
|
+
return result.text().trim();
|
|
145
158
|
}
|
|
146
159
|
|
|
147
160
|
export const FileDiff = z
|
|
@@ -153,45 +166,48 @@ export namespace Snapshot {
|
|
|
153
166
|
deletions: z.number(),
|
|
154
167
|
})
|
|
155
168
|
.meta({
|
|
156
|
-
ref:
|
|
157
|
-
})
|
|
158
|
-
export type FileDiff = z.infer<typeof FileDiff
|
|
159
|
-
export async function diffFull(
|
|
160
|
-
|
|
161
|
-
|
|
169
|
+
ref: 'FileDiff',
|
|
170
|
+
});
|
|
171
|
+
export type FileDiff = z.infer<typeof FileDiff>;
|
|
172
|
+
export async function diffFull(
|
|
173
|
+
from: string,
|
|
174
|
+
to: string
|
|
175
|
+
): Promise<FileDiff[]> {
|
|
176
|
+
const git = gitdir();
|
|
177
|
+
const result: FileDiff[] = [];
|
|
162
178
|
for await (const line of $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-renames --numstat ${from} ${to} -- .`
|
|
163
179
|
.quiet()
|
|
164
180
|
.cwd(Instance.directory)
|
|
165
181
|
.nothrow()
|
|
166
182
|
.lines()) {
|
|
167
|
-
if (!line) continue
|
|
168
|
-
const [additions, deletions, file] = line.split(
|
|
169
|
-
const isBinaryFile = additions ===
|
|
183
|
+
if (!line) continue;
|
|
184
|
+
const [additions, deletions, file] = line.split('\t');
|
|
185
|
+
const isBinaryFile = additions === '-' && deletions === '-';
|
|
170
186
|
const before = isBinaryFile
|
|
171
|
-
?
|
|
187
|
+
? ''
|
|
172
188
|
: await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} show ${from}:${file}`
|
|
173
189
|
.quiet()
|
|
174
190
|
.nothrow()
|
|
175
|
-
.text()
|
|
191
|
+
.text();
|
|
176
192
|
const after = isBinaryFile
|
|
177
|
-
?
|
|
193
|
+
? ''
|
|
178
194
|
: await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} show ${to}:${file}`
|
|
179
195
|
.quiet()
|
|
180
196
|
.nothrow()
|
|
181
|
-
.text()
|
|
197
|
+
.text();
|
|
182
198
|
result.push({
|
|
183
199
|
file,
|
|
184
200
|
before,
|
|
185
201
|
after,
|
|
186
202
|
additions: parseInt(additions),
|
|
187
203
|
deletions: parseInt(deletions),
|
|
188
|
-
})
|
|
204
|
+
});
|
|
189
205
|
}
|
|
190
|
-
return result
|
|
206
|
+
return result;
|
|
191
207
|
}
|
|
192
208
|
|
|
193
209
|
function gitdir() {
|
|
194
|
-
const project = Instance.project
|
|
195
|
-
return path.join(Global.Path.data,
|
|
210
|
+
const project = Instance.project;
|
|
211
|
+
return path.join(Global.Path.data, 'snapshot', project.id);
|
|
196
212
|
}
|
|
197
213
|
}
|