@link-assistant/agent 0.0.8 → 0.0.11

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.
Files changed (104) hide show
  1. package/EXAMPLES.md +80 -1
  2. package/MODELS.md +72 -24
  3. package/README.md +95 -2
  4. package/TOOLS.md +20 -0
  5. package/package.json +36 -2
  6. package/src/agent/agent.ts +68 -54
  7. package/src/auth/claude-oauth.ts +426 -0
  8. package/src/auth/index.ts +28 -26
  9. package/src/auth/plugins.ts +876 -0
  10. package/src/bun/index.ts +53 -43
  11. package/src/bus/global.ts +5 -5
  12. package/src/bus/index.ts +59 -53
  13. package/src/cli/bootstrap.js +12 -12
  14. package/src/cli/bootstrap.ts +6 -6
  15. package/src/cli/cmd/agent.ts +97 -92
  16. package/src/cli/cmd/auth.ts +468 -0
  17. package/src/cli/cmd/cmd.ts +2 -2
  18. package/src/cli/cmd/export.ts +41 -41
  19. package/src/cli/cmd/mcp.ts +210 -53
  20. package/src/cli/cmd/models.ts +30 -29
  21. package/src/cli/cmd/run.ts +269 -213
  22. package/src/cli/cmd/stats.ts +185 -146
  23. package/src/cli/error.ts +17 -13
  24. package/src/cli/ui.ts +78 -0
  25. package/src/command/index.ts +26 -26
  26. package/src/config/config.ts +528 -288
  27. package/src/config/markdown.ts +15 -15
  28. package/src/file/ripgrep.ts +201 -169
  29. package/src/file/time.ts +21 -18
  30. package/src/file/watcher.ts +51 -42
  31. package/src/file.ts +1 -1
  32. package/src/flag/flag.ts +26 -11
  33. package/src/format/formatter.ts +206 -162
  34. package/src/format/index.ts +61 -61
  35. package/src/global/index.ts +21 -21
  36. package/src/id/id.ts +47 -33
  37. package/src/index.js +554 -332
  38. package/src/json-standard/index.ts +173 -0
  39. package/src/mcp/index.ts +135 -128
  40. package/src/patch/index.ts +336 -267
  41. package/src/project/bootstrap.ts +15 -15
  42. package/src/project/instance.ts +43 -36
  43. package/src/project/project.ts +47 -47
  44. package/src/project/state.ts +37 -33
  45. package/src/provider/models-macro.ts +5 -5
  46. package/src/provider/models.ts +32 -32
  47. package/src/provider/opencode.js +19 -19
  48. package/src/provider/provider.ts +518 -277
  49. package/src/provider/transform.ts +143 -102
  50. package/src/server/project.ts +21 -21
  51. package/src/server/server.ts +111 -105
  52. package/src/session/agent.js +66 -60
  53. package/src/session/compaction.ts +136 -111
  54. package/src/session/index.ts +189 -156
  55. package/src/session/message-v2.ts +312 -268
  56. package/src/session/message.ts +73 -57
  57. package/src/session/processor.ts +180 -166
  58. package/src/session/prompt.ts +678 -533
  59. package/src/session/retry.ts +26 -23
  60. package/src/session/revert.ts +76 -62
  61. package/src/session/status.ts +26 -26
  62. package/src/session/summary.ts +97 -76
  63. package/src/session/system.ts +77 -63
  64. package/src/session/todo.ts +22 -16
  65. package/src/snapshot/index.ts +92 -76
  66. package/src/storage/storage.ts +157 -120
  67. package/src/tool/bash.ts +116 -106
  68. package/src/tool/batch.ts +73 -59
  69. package/src/tool/codesearch.ts +60 -53
  70. package/src/tool/edit.ts +319 -263
  71. package/src/tool/glob.ts +32 -28
  72. package/src/tool/grep.ts +72 -53
  73. package/src/tool/invalid.ts +7 -7
  74. package/src/tool/ls.ts +77 -64
  75. package/src/tool/multiedit.ts +30 -21
  76. package/src/tool/patch.ts +121 -94
  77. package/src/tool/read.ts +140 -122
  78. package/src/tool/registry.ts +38 -38
  79. package/src/tool/task.ts +93 -60
  80. package/src/tool/todo.ts +16 -16
  81. package/src/tool/tool.ts +45 -36
  82. package/src/tool/webfetch.ts +97 -74
  83. package/src/tool/websearch.ts +78 -64
  84. package/src/tool/write.ts +21 -15
  85. package/src/util/binary.ts +27 -19
  86. package/src/util/context.ts +8 -8
  87. package/src/util/defer.ts +7 -5
  88. package/src/util/error.ts +24 -19
  89. package/src/util/eventloop.ts +16 -10
  90. package/src/util/filesystem.ts +37 -33
  91. package/src/util/fn.ts +11 -8
  92. package/src/util/iife.ts +1 -1
  93. package/src/util/keybind.ts +44 -44
  94. package/src/util/lazy.ts +7 -7
  95. package/src/util/locale.ts +20 -16
  96. package/src/util/lock.ts +43 -38
  97. package/src/util/log.ts +95 -85
  98. package/src/util/queue.ts +8 -8
  99. package/src/util/rpc.ts +35 -23
  100. package/src/util/scrap.ts +4 -4
  101. package/src/util/signal.ts +5 -5
  102. package/src/util/timeout.ts +6 -6
  103. package/src/util/token.ts +2 -2
  104. package/src/util/wildcard.ts +38 -27
@@ -1,138 +1,152 @@
1
- import { Ripgrep } from "../file/ripgrep"
2
- import { Global } from "../global"
3
- import { Filesystem } from "../util/filesystem"
4
- import { Config } from "../config/config"
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 "../project/instance"
7
- import path from "path"
8
- import os from "os"
6
+ import { Instance } from '../project/instance';
7
+ import path from 'path';
8
+ import os from 'os';
9
9
 
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"
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("anthropic")) return [PROMPT_ANTHROPIC_SPOOF.trim()]
24
- return []
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("gpt-5")) return [PROMPT_CODEX]
29
- if (modelID.includes("gpt-") || modelID.includes("o1") || modelID.includes("o3")) return [PROMPT_BEAST]
30
- if (modelID.includes("gemini-")) return [PROMPT_GEMINI]
31
- if (modelID.includes("claude")) return [PROMPT_ANTHROPIC]
32
- if (modelID.includes("polaris-alpha")) return [PROMPT_POLARIS]
33
- if (modelID.includes("grok-code")) return [PROMPT_GROK_CODE]
34
- return [PROMPT_ANTHROPIC_WITHOUT_TODO]
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 === "git" ? "yes" : "no"}`,
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 === "git"
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("\n"),
59
- ]
64
+ ].join('\n'),
65
+ ];
60
66
  }
61
67
 
62
68
  const LOCAL_RULE_FILES = [
63
- "AGENTS.md",
64
- "CLAUDE.md",
65
- "CONTEXT.md", // deprecated
66
- ]
69
+ 'AGENTS.md',
70
+ 'CLAUDE.md',
71
+ 'CONTEXT.md', // deprecated
72
+ ];
67
73
  const GLOBAL_RULE_FILES = [
68
- path.join(Global.Path.config, "AGENTS.md"),
69
- path.join(os.homedir(), ".claude", "CLAUDE.md"),
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(localRuleFile, Instance.directory, Instance.worktree)
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(instruction, Instance.directory, Instance.worktree).catch(() => [])
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) => "Instructions from: " + p + "\n" + 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 "anthropic":
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 "anthropic":
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
  }
@@ -1,36 +1,42 @@
1
- import z from "zod"
2
- import { Bus } from "../bus"
3
- import { Storage } from "../storage/storage"
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("Brief description of the task"),
9
- status: z.string().describe("Current status of the task: pending, in_progress, completed, cancelled"),
10
- priority: z.string().describe("Priority level of the task: high, medium, low"),
11
- id: z.string().describe("Unique identifier for the todo item"),
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: "Todo" })
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
- "todo.updated",
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(["todo", input.sessionID], input.todos)
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[]>(["todo", sessionID])
38
+ return Storage.read<Info[]>(['todo', sessionID])
33
39
  .then((x) => x || [])
34
- .catch(() => [])
40
+ .catch(() => []);
35
41
  }
36
42
  }
@@ -1,20 +1,20 @@
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"
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: "snapshot" })
11
+ const log = Log.create({ service: 'snapshot' });
12
12
 
13
13
  export async function track() {
14
- if (Instance.project.vcs !== "git") return
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`.quiet().nothrow()
29
- log.info("initialized")
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 .`.quiet().cwd(Instance.directory).nothrow()
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
- .text()
37
- log.info("tracking", { hash, cwd: Instance.directory, git })
38
- return hash.trim()
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 .`.quiet().cwd(Instance.directory).nothrow()
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("failed to get diff", { hash, exitCode: result.exitCode })
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("\n")
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("restore", { commit: snapshot })
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("failed to restore snapshot", {
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("reverting", { file, hash: item.hash })
100
- const result = await $`git --git-dir ${git} --work-tree ${Instance.worktree} checkout ${item.hash} -- ${file}`
101
- .quiet()
102
- .cwd(Instance.worktree)
103
- .nothrow()
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("file existed in snapshot but checkout failed, keeping", {
122
+ log.info('file existed in snapshot but checkout failed, keeping', {
113
123
  file,
114
- })
124
+ });
115
125
  } else {
116
- log.info("file did not exist in snapshot, deleting", { file })
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 .`.quiet().cwd(Instance.directory).nothrow()
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("failed to get diff", {
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: "FileDiff",
157
- })
158
- export type FileDiff = z.infer<typeof FileDiff>
159
- export async function diffFull(from: string, to: string): Promise<FileDiff[]> {
160
- const git = gitdir()
161
- const result: FileDiff[] = []
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("\t")
169
- const isBinaryFile = additions === "-" && deletions === "-"
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, "snapshot", project.id)
210
+ const project = Instance.project;
211
+ return path.join(Global.Path.data, 'snapshot', project.id);
196
212
  }
197
213
  }