@watasu/sdk 0.1.5 → 0.1.24

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.
@@ -1,5 +1,6 @@
1
1
  import { withQuery } from './transport.js';
2
- import { FileNotFoundError, unsupported } from './errors.js';
2
+ import { FileNotFoundError, InvalidArgumentError } from './errors.js';
3
+ import { ProcessSocket, base64Encode } from './processSocket.js';
3
4
  export var FileType;
4
5
  (function (FileType) {
5
6
  /** Regular file. */
@@ -9,27 +10,133 @@ export var FileType;
9
10
  /** Symbolic link. */
10
11
  FileType["SYMLINK"] = "symlink";
11
12
  })(FileType || (FileType = {}));
13
+ /** Live filesystem watcher. Call `stop()` to close the local watch stream. */
14
+ export class WatchHandle {
15
+ socket;
16
+ done;
17
+ constructor(socket, events, onEvent, onExit) {
18
+ this.socket = socket;
19
+ this.done = this.pump(events, onEvent, onExit);
20
+ }
21
+ /** Stop watching the directory. */
22
+ stop() {
23
+ this.socket.close();
24
+ }
25
+ /** Alias for `stop`. */
26
+ close() {
27
+ this.stop();
28
+ }
29
+ /** Resolves when the watcher stream exits. */
30
+ wait() {
31
+ return this.done;
32
+ }
33
+ async pump(events, onEvent, onExit) {
34
+ let error;
35
+ try {
36
+ for await (const frame of events) {
37
+ if (frame.type !== 'events' || !Array.isArray(frame.events))
38
+ continue;
39
+ for (const item of frame.events) {
40
+ await onEvent(filesystemEvent(item));
41
+ }
42
+ }
43
+ }
44
+ catch (caught) {
45
+ error = caught instanceof Error ? caught : new Error(String(caught));
46
+ throw error;
47
+ }
48
+ finally {
49
+ await onExit?.(error);
50
+ }
51
+ }
52
+ }
53
+ /** Lazy filesystem watcher. Add listeners, then call `start()`. */
54
+ export class FilesystemWatcher {
55
+ dataPlane;
56
+ path;
57
+ opts;
58
+ handle;
59
+ listeners = [];
60
+ constructor(dataPlane, path, opts = {}) {
61
+ this.dataPlane = dataPlane;
62
+ this.path = path;
63
+ this.opts = opts;
64
+ }
65
+ async start(opts = {}) {
66
+ if (this.handle)
67
+ return;
68
+ const nextOpts = { ...this.opts, ...opts };
69
+ const socket = await new ProcessSocket(this.dataPlane.baseUrl, this.dataPlane.token, withQuery('/runtime/v1/files/watch', { path: this.path, recursive: nextOpts.recursive ?? false, include_entry: nextOpts.includeEntry }), nextOpts.requestTimeoutMs).connect();
70
+ this.handle = new WatchHandle(socket, socket, async (event) => {
71
+ for (const listener of this.listeners)
72
+ await listener(event);
73
+ }, nextOpts.onExit);
74
+ }
75
+ async stop() {
76
+ this.handle?.stop();
77
+ }
78
+ addEventListener(listener) {
79
+ this.listeners.push(listener);
80
+ return () => {
81
+ const index = this.listeners.indexOf(listener);
82
+ if (index === -1)
83
+ return false;
84
+ this.listeners.splice(index, 1);
85
+ return true;
86
+ };
87
+ }
88
+ wait() {
89
+ return this.handle?.wait() ?? Promise.resolve();
90
+ }
91
+ }
12
92
  /** Filesystem helper for a sandbox data-plane session. */
13
93
  export class Filesystem {
14
94
  dataPlane;
15
95
  constructor(dataPlane) {
16
96
  this.dataPlane = dataPlane;
17
97
  }
18
- /** Read a file as UTF-8 text, bytes, or a one-chunk async byte stream. */
19
98
  async read(path, opts = {}) {
20
99
  const bytes = await this.dataPlane.getBytes(withQuery('/runtime/v1/files', { path, gzip: opts.gzip }), opts);
21
100
  if (opts.format === 'bytes')
22
101
  return bytes;
102
+ if (opts.format === 'blob')
103
+ return new Blob([toArrayBuffer(bytes)]);
23
104
  if (opts.format === 'stream')
24
- return (async function* () { yield bytes; })();
105
+ return new ReadableStream({ start(controller) { controller.enqueue(bytes); controller.close(); } });
25
106
  return new TextDecoder().decode(bytes);
26
107
  }
27
- /** Write UTF-8 text or bytes to a file. */
28
- async write(path, data, opts = {}) {
29
- const body = typeof data === 'string' ? new TextEncoder().encode(data) : data;
30
- const payload = await this.dataPlane.putJson(withQuery('/runtime/v1/files', { path, gzip: opts.gzip }), body, opts);
108
+ /** Read a file as raw bytes. */
109
+ async readBytes(path, opts = {}) {
110
+ return this.read(path, { ...opts, format: 'bytes' });
111
+ }
112
+ async write(pathOrFiles, dataOrOpts, opts = {}) {
113
+ if (Array.isArray(pathOrFiles)) {
114
+ return this.writeFiles(pathOrFiles, dataOrOpts);
115
+ }
116
+ const body = await writeDataToBytes(dataOrOpts);
117
+ const payload = await this.dataPlane.putJson(withQuery('/runtime/v1/files', { path: pathOrFiles, gzip: opts.gzip }), body, opts);
31
118
  return entryInfo(payload.file ?? payload);
32
119
  }
120
+ /** Write raw bytes to a file. */
121
+ async writeBytes(path, data, opts = {}) {
122
+ return this.write(path, data, opts);
123
+ }
124
+ /** Write several files in one runtime API call. */
125
+ async writeFiles(files, opts = {}) {
126
+ if (files.length === 0)
127
+ return [];
128
+ const payload = await this.dataPlane.postJson('/runtime/v1/files/write_files', {
129
+ ...opts,
130
+ json: {
131
+ files: await Promise.all(files.map(async (file) => ({
132
+ path: file.path,
133
+ data_base64: base64Encode(await writeDataToBytes(file.data)),
134
+ }))),
135
+ },
136
+ });
137
+ const written = Array.isArray(payload.files) ? payload.files : [];
138
+ return written.map(entryInfo);
139
+ }
33
140
  /** List directory entries below `path`. */
34
141
  async list(path, opts = {}) {
35
142
  const payload = await this.dataPlane.getJson(withQuery('/runtime/v1/directories', { path, depth: opts.depth }), opts);
@@ -70,9 +177,61 @@ export class Filesystem {
70
177
  await this.dataPlane.postJson(withQuery('/runtime/v1/directories', { path }), opts);
71
178
  return true;
72
179
  }
73
- watchDir() {
74
- unsupported('sandbox.files.watchDir');
180
+ watchDir(path, onEvent, opts = {}) {
181
+ const watcher = new FilesystemWatcher(this.dataPlane, path, opts);
182
+ if (!onEvent)
183
+ return watcher;
184
+ watcher.addEventListener(onEvent);
185
+ return watcher.start().then(() => watcher);
186
+ }
187
+ }
188
+ async function writeDataToBytes(data) {
189
+ if (typeof data === 'string')
190
+ return new TextEncoder().encode(data);
191
+ if (data instanceof Uint8Array)
192
+ return data;
193
+ if (data instanceof ArrayBuffer)
194
+ return new Uint8Array(data);
195
+ if (typeof Blob !== 'undefined' && data instanceof Blob)
196
+ return new Uint8Array(await data.arrayBuffer());
197
+ if (isReadableStream(data))
198
+ return readStreamToBytes(data);
199
+ throw new InvalidArgumentError(`Unsupported file data type: ${Object.prototype.toString.call(data)}`);
200
+ }
201
+ function toArrayBuffer(bytes) {
202
+ const buffer = new ArrayBuffer(bytes.byteLength);
203
+ new Uint8Array(buffer).set(bytes);
204
+ return buffer;
205
+ }
206
+ function isReadableStream(value) {
207
+ return Boolean(value && typeof value === 'object' && typeof value.getReader === 'function');
208
+ }
209
+ async function readStreamToBytes(stream) {
210
+ const reader = stream.getReader();
211
+ const chunks = [];
212
+ let total = 0;
213
+ try {
214
+ for (;;) {
215
+ const { done, value } = await reader.read();
216
+ if (done)
217
+ break;
218
+ if (!(value instanceof Uint8Array)) {
219
+ throw new InvalidArgumentError('ReadableStream file data must yield Uint8Array chunks');
220
+ }
221
+ chunks.push(value);
222
+ total += value.byteLength;
223
+ }
224
+ }
225
+ finally {
226
+ reader.releaseLock();
75
227
  }
228
+ const bytes = new Uint8Array(total);
229
+ let offset = 0;
230
+ for (const chunk of chunks) {
231
+ bytes.set(chunk, offset);
232
+ offset += chunk.byteLength;
233
+ }
234
+ return bytes;
76
235
  }
77
236
  function entryInfo(value) {
78
237
  const item = value && typeof value === 'object' ? value : {};
@@ -96,3 +255,19 @@ function recordOfStrings(value) {
96
255
  return undefined;
97
256
  return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, String(item)]));
98
257
  }
258
+ function filesystemEvent(value) {
259
+ const item = value && typeof value === 'object' ? value : {};
260
+ return {
261
+ type: normalizeEventType(String(item.type ?? 'modify')),
262
+ path: String(item.path ?? ''),
263
+ entry: item.file && typeof item.file === 'object' ? entryInfo(item.file) : undefined,
264
+ raw: item,
265
+ };
266
+ }
267
+ function normalizeEventType(value) {
268
+ if (value === 'delete')
269
+ return 'remove';
270
+ if (value === 'modify')
271
+ return 'write';
272
+ return value;
273
+ }
package/dist/git.d.ts ADDED
@@ -0,0 +1,171 @@
1
+ import { DataPlaneClient } from './transport.js';
2
+ export interface GitCommandResult {
3
+ path?: string;
4
+ url?: string;
5
+ ref?: string;
6
+ branch?: string;
7
+ remote?: string;
8
+ name?: string;
9
+ value?: string;
10
+ branches?: string[];
11
+ currentBranch?: string;
12
+ stdout: string;
13
+ stderr: string;
14
+ command?: Record<string, unknown>;
15
+ raw: Record<string, unknown>;
16
+ }
17
+ export interface GitAuthOpts {
18
+ username?: string;
19
+ password?: string;
20
+ envs?: Record<string, string>;
21
+ user?: string;
22
+ cwd?: string;
23
+ timeout?: number;
24
+ timeoutMs?: number;
25
+ requestTimeoutMs?: number;
26
+ }
27
+ export interface GitCloneOpts extends GitAuthOpts {
28
+ path?: string;
29
+ branch?: string;
30
+ depth?: number;
31
+ recursive?: boolean;
32
+ submodules?: boolean;
33
+ dangerouslyStoreCredentials?: boolean;
34
+ }
35
+ export interface GitRequestOpts extends GitAuthOpts {
36
+ }
37
+ export interface GitInitOpts extends GitRequestOpts {
38
+ bare?: boolean;
39
+ initialBranch?: string;
40
+ }
41
+ export interface GitPullOpts extends GitAuthOpts {
42
+ branch?: string;
43
+ remote?: string;
44
+ }
45
+ export interface GitPushOpts extends GitAuthOpts {
46
+ branch?: string;
47
+ remote?: string;
48
+ setUpstream?: boolean;
49
+ }
50
+ export interface GitCredentialOpts extends GitRequestOpts {
51
+ host?: string;
52
+ protocol?: string;
53
+ }
54
+ export interface GitConfigureUserOpts extends GitRequestOpts {
55
+ scope?: 'global' | 'local';
56
+ path?: string;
57
+ }
58
+ export interface GitBranchOpts extends GitRequestOpts {
59
+ force?: boolean;
60
+ }
61
+ export interface GitAddOpts extends GitRequestOpts {
62
+ files?: string[];
63
+ all?: boolean;
64
+ }
65
+ export interface GitCommitOpts extends GitRequestOpts {
66
+ authorName?: string;
67
+ authorEmail?: string;
68
+ allowEmpty?: boolean;
69
+ }
70
+ export type GitResetMode = 'soft' | 'mixed' | 'hard' | 'merge' | 'keep';
71
+ export interface GitResetOpts extends GitRequestOpts {
72
+ mode?: GitResetMode;
73
+ target?: string;
74
+ paths?: string[];
75
+ }
76
+ export interface GitRestoreOpts extends GitRequestOpts {
77
+ paths: string[];
78
+ staged?: boolean;
79
+ worktree?: boolean;
80
+ source?: string;
81
+ }
82
+ export interface GitRemoteAddOpts extends GitRequestOpts {
83
+ fetch?: boolean;
84
+ overwrite?: boolean;
85
+ }
86
+ export interface GitConfigOpts extends GitRequestOpts {
87
+ scope?: 'global' | 'local';
88
+ path?: string;
89
+ }
90
+ export interface GitBranches {
91
+ path?: string;
92
+ branches: string[];
93
+ currentBranch?: string;
94
+ result: GitCommandResult;
95
+ }
96
+ export interface GitFileStatus {
97
+ name: string;
98
+ status: string;
99
+ indexStatus: string;
100
+ workingTreeStatus: string;
101
+ staged: boolean;
102
+ renamedFrom?: string;
103
+ }
104
+ export interface GitStatus {
105
+ currentBranch?: string;
106
+ upstream?: string;
107
+ ahead: number;
108
+ behind: number;
109
+ detached: boolean;
110
+ fileStatus: GitFileStatus[];
111
+ isClean: boolean;
112
+ hasChanges: boolean;
113
+ hasStaged: boolean;
114
+ hasUntracked: boolean;
115
+ hasConflicts: boolean;
116
+ totalCount: number;
117
+ stagedCount: number;
118
+ unstagedCount: number;
119
+ untrackedCount: number;
120
+ conflictCount: number;
121
+ result: GitCommandResult;
122
+ }
123
+ /** Git helper backed by sandbox data-plane routes. */
124
+ export declare class Git {
125
+ private readonly dataPlane;
126
+ constructor(dataPlane: DataPlaneClient);
127
+ /** Clone a repository into the sandbox. */
128
+ clone(url: string, opts?: GitCloneOpts): Promise<GitCommandResult>;
129
+ /** Store Git credentials in the sandbox credential helper. */
130
+ dangerouslyAuthenticate(opts: GitCredentialOpts & {
131
+ username: string;
132
+ password: string;
133
+ }): Promise<GitCommandResult>;
134
+ /** Configure Git author identity globally or for one repository. */
135
+ configureUser(name: string, email: string, opts?: GitConfigureUserOpts): Promise<GitCommandResult>;
136
+ /** Initialize a Git repository. */
137
+ init(path: string, opts?: GitInitOpts): Promise<GitCommandResult>;
138
+ /** Return parsed repository status for `path`. */
139
+ status(path: string, opts?: GitRequestOpts): Promise<GitStatus>;
140
+ /** Return branches and the current branch for `path`. */
141
+ branches(path: string, opts?: GitRequestOpts): Promise<GitBranches>;
142
+ /** Create and check out a new branch. */
143
+ createBranch(path: string, branch: string, opts?: GitRequestOpts): Promise<GitCommandResult>;
144
+ /** Delete a branch. */
145
+ deleteBranch(path: string, branch: string, opts?: GitBranchOpts): Promise<GitCommandResult>;
146
+ /** Stage files. Defaults to all files. */
147
+ add(path: string, opts?: GitAddOpts): Promise<GitCommandResult>;
148
+ /** Commit staged files. */
149
+ commit(path: string, message: string, opts?: GitCommitOpts): Promise<GitCommandResult>;
150
+ /** Reset the current HEAD to a specified state. */
151
+ reset(path: string, opts?: GitResetOpts): Promise<GitCommandResult>;
152
+ /** Restore working tree files or unstage changes. */
153
+ restore(path: string, opts: GitRestoreOpts): Promise<GitCommandResult>;
154
+ /** Pull the current branch with a fast-forward-only merge. */
155
+ pull(path: string, opts?: GitPullOpts): Promise<GitCommandResult>;
156
+ /** Push the current branch or a selected branch. */
157
+ push(path: string, opts?: GitPushOpts): Promise<GitCommandResult>;
158
+ /** Check out an arbitrary ref in a repository. */
159
+ checkout(path: string, ref: string, opts?: GitRequestOpts): Promise<GitCommandResult>;
160
+ /** Check out an existing branch in a repository. */
161
+ checkoutBranch(path: string, branch: string, opts?: GitRequestOpts): Promise<GitCommandResult>;
162
+ /** Add a remote. */
163
+ remoteAdd(path: string, name: string, url: string, opts?: GitRemoteAddOpts): Promise<GitCommandResult>;
164
+ /** Return a remote URL, or undefined when the remote does not exist. */
165
+ remoteGet(path: string, name: string, opts?: GitRequestOpts): Promise<string | undefined>;
166
+ /** Set a Git config value. */
167
+ setConfig(key: string, value: string, opts?: GitConfigOpts): Promise<GitCommandResult>;
168
+ /** Read a Git config value. */
169
+ getConfig(key: string, opts?: GitConfigOpts): Promise<string>;
170
+ private run;
171
+ }
package/dist/git.js ADDED
@@ -0,0 +1,277 @@
1
+ /** Git helper backed by sandbox data-plane routes. */
2
+ export class Git {
3
+ dataPlane;
4
+ constructor(dataPlane) {
5
+ this.dataPlane = dataPlane;
6
+ }
7
+ /** Clone a repository into the sandbox. */
8
+ async clone(url, opts = {}) {
9
+ return this.run('/runtime/v1/git/clone', {
10
+ url,
11
+ ...gitOpts(opts),
12
+ ...pick(opts, ['path', 'branch', 'depth', 'recursive', 'submodules', 'username', 'password']),
13
+ dangerously_store_credentials: opts.dangerouslyStoreCredentials,
14
+ }, opts);
15
+ }
16
+ /** Store Git credentials in the sandbox credential helper. */
17
+ async dangerouslyAuthenticate(opts) {
18
+ return this.run('/runtime/v1/git/dangerously_authenticate', {
19
+ ...gitOpts(opts),
20
+ username: opts.username,
21
+ password: opts.password,
22
+ host: opts.host,
23
+ protocol: opts.protocol,
24
+ }, opts);
25
+ }
26
+ /** Configure Git author identity globally or for one repository. */
27
+ async configureUser(name, email, opts = {}) {
28
+ return this.run('/runtime/v1/git/configure_user', {
29
+ ...gitOpts(opts),
30
+ name,
31
+ email,
32
+ scope: opts.scope,
33
+ path: opts.path,
34
+ }, opts);
35
+ }
36
+ /** Initialize a Git repository. */
37
+ async init(path, opts = {}) {
38
+ return this.run('/runtime/v1/git/init', {
39
+ path,
40
+ bare: opts.bare,
41
+ initial_branch: opts.initialBranch,
42
+ ...gitOpts(opts),
43
+ }, opts);
44
+ }
45
+ /** Return parsed repository status for `path`. */
46
+ async status(path, opts = {}) {
47
+ const result = await this.run('/runtime/v1/git/status', { path, ...gitOpts(opts) }, opts);
48
+ return parseGitStatus(result);
49
+ }
50
+ /** Return branches and the current branch for `path`. */
51
+ async branches(path, opts = {}) {
52
+ const result = await this.run('/runtime/v1/git/branches', { path, ...gitOpts(opts) }, opts);
53
+ return {
54
+ path: result.path,
55
+ branches: Array.isArray(result.raw.branches) ? result.raw.branches.map(String) : result.branches ?? [],
56
+ currentBranch: stringValue(result.raw.current_branch) ?? result.currentBranch,
57
+ result,
58
+ };
59
+ }
60
+ /** Create and check out a new branch. */
61
+ async createBranch(path, branch, opts = {}) {
62
+ return this.run('/runtime/v1/git/create_branch', { path, branch, ...gitOpts(opts) }, opts);
63
+ }
64
+ /** Delete a branch. */
65
+ async deleteBranch(path, branch, opts = {}) {
66
+ return this.run('/runtime/v1/git/delete_branch', { path, branch, force: opts.force, ...gitOpts(opts) }, opts);
67
+ }
68
+ /** Stage files. Defaults to all files. */
69
+ async add(path, opts = {}) {
70
+ return this.run('/runtime/v1/git/add', { path, files: opts.files, all: opts.all, ...gitOpts(opts) }, opts);
71
+ }
72
+ /** Commit staged files. */
73
+ async commit(path, message, opts = {}) {
74
+ return this.run('/runtime/v1/git/commit', {
75
+ path,
76
+ message,
77
+ author_name: opts.authorName,
78
+ author_email: opts.authorEmail,
79
+ allow_empty: opts.allowEmpty,
80
+ ...gitOpts(opts),
81
+ }, opts);
82
+ }
83
+ /** Reset the current HEAD to a specified state. */
84
+ async reset(path, opts = {}) {
85
+ return this.run('/runtime/v1/git/reset', {
86
+ path,
87
+ mode: opts.mode,
88
+ target: opts.target,
89
+ paths: opts.paths,
90
+ ...gitOpts(opts),
91
+ }, opts);
92
+ }
93
+ /** Restore working tree files or unstage changes. */
94
+ async restore(path, opts) {
95
+ return this.run('/runtime/v1/git/restore', {
96
+ path,
97
+ paths: opts.paths,
98
+ staged: opts.staged,
99
+ worktree: opts.worktree,
100
+ source: opts.source,
101
+ ...gitOpts(opts),
102
+ }, opts);
103
+ }
104
+ /** Pull the current branch with a fast-forward-only merge. */
105
+ async pull(path, opts = {}) {
106
+ return this.run('/runtime/v1/git/pull', { path, ...gitOpts(opts), ...pick(opts, ['remote', 'branch', 'username', 'password']) }, opts);
107
+ }
108
+ /** Push the current branch or a selected branch. */
109
+ async push(path, opts = {}) {
110
+ return this.run('/runtime/v1/git/push', {
111
+ path,
112
+ ...gitOpts(opts),
113
+ ...pick(opts, ['remote', 'branch', 'username', 'password']),
114
+ set_upstream: opts.setUpstream ?? true,
115
+ }, opts);
116
+ }
117
+ /** Check out an arbitrary ref in a repository. */
118
+ async checkout(path, ref, opts = {}) {
119
+ return this.run('/runtime/v1/git/checkout', { path, ref, ...gitOpts(opts) }, opts);
120
+ }
121
+ /** Check out an existing branch in a repository. */
122
+ async checkoutBranch(path, branch, opts = {}) {
123
+ return this.checkout(path, branch, opts);
124
+ }
125
+ /** Add a remote. */
126
+ async remoteAdd(path, name, url, opts = {}) {
127
+ return this.run('/runtime/v1/git/remote_add', {
128
+ path,
129
+ name,
130
+ url,
131
+ fetch: opts.fetch,
132
+ overwrite: opts.overwrite,
133
+ ...gitOpts(opts),
134
+ }, opts);
135
+ }
136
+ /** Return a remote URL, or undefined when the remote does not exist. */
137
+ async remoteGet(path, name, opts = {}) {
138
+ const result = await this.run('/runtime/v1/git/remote_get', {
139
+ path,
140
+ name,
141
+ ...gitOpts(opts),
142
+ }, opts);
143
+ return result.value ?? result.url;
144
+ }
145
+ /** Set a Git config value. */
146
+ async setConfig(key, value, opts = {}) {
147
+ return this.run('/runtime/v1/git/set_config', {
148
+ key,
149
+ value,
150
+ scope: opts.scope,
151
+ path: opts.path,
152
+ ...gitOpts(opts),
153
+ }, opts);
154
+ }
155
+ /** Read a Git config value. */
156
+ async getConfig(key, opts = {}) {
157
+ const result = await this.run('/runtime/v1/git/get_config', {
158
+ key,
159
+ scope: opts.scope,
160
+ path: opts.path,
161
+ ...gitOpts(opts),
162
+ }, opts);
163
+ return String(result.value ?? '');
164
+ }
165
+ async run(path, json, opts) {
166
+ const payload = await this.dataPlane.postJson(path, { json: compact(json), requestTimeoutMs: opts.requestTimeoutMs });
167
+ return gitResult(payload.git ?? payload);
168
+ }
169
+ }
170
+ function gitOpts(opts) {
171
+ return {
172
+ env_vars: opts.envs,
173
+ user: opts.user,
174
+ cwd: opts.cwd,
175
+ timeout_seconds: opts.timeout ?? (opts.timeoutMs === undefined ? undefined : Math.ceil(opts.timeoutMs / 1000)),
176
+ };
177
+ }
178
+ function pick(source, keys) {
179
+ const record = source;
180
+ return Object.fromEntries(keys.map((key) => [key, record[key]]));
181
+ }
182
+ function compact(value) {
183
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
184
+ }
185
+ function gitResult(value) {
186
+ const item = value && typeof value === 'object' ? value : {};
187
+ return {
188
+ path: stringValue(item.path),
189
+ url: stringValue(item.url),
190
+ ref: stringValue(item.ref),
191
+ branch: stringValue(item.branch),
192
+ remote: stringValue(item.remote),
193
+ name: stringValue(item.name),
194
+ value: stringValue(item.value),
195
+ branches: Array.isArray(item.branches) ? item.branches.map(String) : undefined,
196
+ currentBranch: stringValue(item.current_branch),
197
+ stdout: String(item.stdout ?? ''),
198
+ stderr: String(item.stderr ?? ''),
199
+ command: item.command && typeof item.command === 'object' ? item.command : undefined,
200
+ raw: item,
201
+ };
202
+ }
203
+ function parseGitStatus(result) {
204
+ const fileStatus = [];
205
+ let currentBranch;
206
+ let upstream;
207
+ let ahead = 0;
208
+ let behind = 0;
209
+ let detached = false;
210
+ for (const line of result.stdout.split(/\r?\n/).filter(Boolean)) {
211
+ if (line.startsWith('## ')) {
212
+ const branchLine = line.slice(3);
213
+ detached = branchLine.includes('HEAD') && branchLine.includes('no branch');
214
+ const [branchPart, trackingPart] = branchLine.split('...');
215
+ currentBranch = branchPart?.replace(/\s+\[.*\]$/, '') || undefined;
216
+ if (trackingPart) {
217
+ const match = trackingPart.match(/^([^\s[]+)(?:\s+\[(.*)\])?/);
218
+ upstream = match?.[1];
219
+ const details = match?.[2] ?? '';
220
+ ahead = numberFrom(details, /ahead\s+(\d+)/);
221
+ behind = numberFrom(details, /behind\s+(\d+)/);
222
+ }
223
+ continue;
224
+ }
225
+ const indexStatus = line[0] ?? ' ';
226
+ const workingTreeStatus = line[1] ?? ' ';
227
+ const path = line.slice(3);
228
+ const [name, renamedFrom] = path.includes(' -> ') ? path.split(' -> ').reverse() : [path, undefined];
229
+ const status = statusName(indexStatus, workingTreeStatus);
230
+ fileStatus.push({ name, status, indexStatus, workingTreeStatus, staged: indexStatus !== ' ' && indexStatus !== '?', renamedFrom });
231
+ }
232
+ const stagedCount = fileStatus.filter((item) => item.staged).length;
233
+ const untrackedCount = fileStatus.filter((item) => item.status === 'untracked').length;
234
+ const conflictCount = fileStatus.filter((item) => item.status === 'conflict').length;
235
+ const totalCount = fileStatus.length;
236
+ return {
237
+ currentBranch,
238
+ upstream,
239
+ ahead,
240
+ behind,
241
+ detached,
242
+ fileStatus,
243
+ isClean: totalCount === 0,
244
+ hasChanges: totalCount > 0,
245
+ hasStaged: stagedCount > 0,
246
+ hasUntracked: untrackedCount > 0,
247
+ hasConflicts: conflictCount > 0,
248
+ totalCount,
249
+ stagedCount,
250
+ unstagedCount: totalCount - stagedCount,
251
+ untrackedCount,
252
+ conflictCount,
253
+ result,
254
+ };
255
+ }
256
+ function numberFrom(value, pattern) {
257
+ const match = value.match(pattern);
258
+ return match ? Number(match[1]) : 0;
259
+ }
260
+ function statusName(indexStatus, workingTreeStatus) {
261
+ if (indexStatus === '?' && workingTreeStatus === '?')
262
+ return 'untracked';
263
+ if (indexStatus === 'U' || workingTreeStatus === 'U' || indexStatus === 'A' && workingTreeStatus === 'A')
264
+ return 'conflict';
265
+ if (indexStatus === 'D' || workingTreeStatus === 'D')
266
+ return 'deleted';
267
+ if (indexStatus === 'R')
268
+ return 'renamed';
269
+ if (indexStatus === 'A')
270
+ return 'added';
271
+ if (indexStatus === 'M' || workingTreeStatus === 'M')
272
+ return 'modified';
273
+ return 'changed';
274
+ }
275
+ function stringValue(value) {
276
+ return typeof value === 'string' ? value : undefined;
277
+ }