@phpsandbox/sdk 0.1.6 → 0.1.10

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 (59) hide show
  1. package/README.md +42 -50
  2. package/dist/beacon/index.d.ts +1 -7
  3. package/dist/beacon/index.js +14 -13
  4. package/dist/beacon/navigator.d.ts +8 -36
  5. package/dist/beacon/navigator.js +1 -62
  6. package/dist/browser/phpsandbox-sdk.esm.js +547 -691
  7. package/dist/browser/phpsandbox-sdk.esm.min.js +23 -23
  8. package/dist/browser/phpsandbox-sdk.iife.js +547 -691
  9. package/dist/browser/phpsandbox-sdk.iife.min.js +35 -35
  10. package/dist/composer.d.ts +10 -8
  11. package/dist/composer.js +18 -18
  12. package/dist/errors/index.d.ts +43 -8
  13. package/dist/errors/index.js +120 -14
  14. package/dist/feedback.js +1 -2
  15. package/dist/filesystem.d.ts +0 -17
  16. package/dist/filesystem.js +2 -21
  17. package/dist/git.d.ts +1 -5
  18. package/dist/git.js +6 -8
  19. package/dist/index.d.ts +66 -156
  20. package/dist/index.js +172 -289
  21. package/dist/integrations.d.ts +0 -2
  22. package/dist/integrations.js +5 -11
  23. package/dist/log.d.ts +0 -2
  24. package/dist/lsp.js +1 -1
  25. package/dist/metrics.d.ts +3 -3
  26. package/dist/ports.d.ts +1 -1
  27. package/dist/publications.d.ts +4 -54
  28. package/dist/publications.js +25 -103
  29. package/dist/runtime/rest.js +34 -17
  30. package/dist/runtime.d.ts +68 -0
  31. package/dist/runtime.js +46 -0
  32. package/dist/sandbox-integrations.d.ts +1 -2
  33. package/dist/sandbox-integrations.js +11 -14
  34. package/dist/schemas/publications.d.ts +0 -6
  35. package/dist/schemas/publications.js +0 -1
  36. package/dist/servers.d.ts +4 -2
  37. package/dist/servers.js +5 -8
  38. package/dist/shell.d.ts +2 -14
  39. package/dist/shell.js +1 -21
  40. package/dist/socket/index.d.ts +6 -7
  41. package/dist/socket/index.js +39 -25
  42. package/dist/types.d.ts +0 -12
  43. package/dist/types.js +1 -15
  44. package/docs/api-summary.md +28 -28
  45. package/docs/cdn-usage.md +43 -55
  46. package/docs/getting-started.md +43 -26
  47. package/examples/README.md +5 -35
  48. package/examples/basic.ts +9 -18
  49. package/examples/get.ts +2 -2
  50. package/examples/git-merge-smoke.ts +2 -3
  51. package/examples/laravel-load-test.ts +31 -36
  52. package/examples/notebook-readonly-smoke.ts +12 -26
  53. package/examples/publication-provider-label.ts +1 -1
  54. package/examples/publish-cloudflare-container.ts +2 -2
  55. package/examples/publish-laravel-cloud.ts +2 -2
  56. package/examples/publish-ssh-agent.ts +2 -2
  57. package/package.json +4 -3
  58. package/dist/container.d.ts +0 -65
  59. package/dist/container.js +0 -30
@@ -1,5 +1,5 @@
1
- import { ComposerCredentials, type Action, type Client, type NotebookInstance } from './index.js';
2
- import { ProcessResult, type ProcessOutputChunk, type ProcessOutputSource, type ProcessResultData } from './shell.js';
1
+ import type { Action, Client, ComposerCredentialInfo, ComposerCredentialInput, ComposerCredentialType, NotebookInstance } from './index.js';
2
+ import { type ProcessOutputChunk, type ProcessOutputSource, type ProcessResult } from './shell.js';
3
3
  export interface ComposerEventData {
4
4
  'composer.output': {
5
5
  id: string;
@@ -23,7 +23,7 @@ export interface ComposerActions {
23
23
  command: ComposerCommand;
24
24
  args: Argument;
25
25
  options: Options;
26
- }, ProcessResultData>;
26
+ }, ProcessResult>;
27
27
  'composer.kill': Action<{
28
28
  id: string;
29
29
  }, boolean>;
@@ -32,6 +32,13 @@ export interface ComposerActions {
32
32
  export type ComposerCommand = 'install' | 'update' | 'require' | 'remove' | 'search' | 'show' | 'dump-autoload';
33
33
  export type Argument<Name extends string = string, Type extends string | string[] = string | string[]> = Record<Name, Type>;
34
34
  export type Options<Name extends string = string, Type extends string | string[] | boolean = string | string[] | boolean> = Record<Name, Type>;
35
+ declare class ComposerCredentials {
36
+ private readonly client;
37
+ private readonly notebookId;
38
+ constructor(client: Client, notebookId: string);
39
+ set(input: ComposerCredentialInput): Promise<ComposerCredentialInfo>;
40
+ remove(type: ComposerCredentialType, url?: string): Promise<void>;
41
+ }
35
42
  export declare class ComposerProcess {
36
43
  readonly id: string;
37
44
  private readonly okra;
@@ -48,11 +55,6 @@ export default class Composer {
48
55
  readonly credentials: ComposerCredentials;
49
56
  constructor(okra: NotebookInstance, client: Client);
50
57
  run(command: ComposerCommand, args?: Argument, options?: Options): ComposerProcess;
51
- dumpAutoload(options?: Options): ComposerProcess;
52
- install(options?: Options): ComposerProcess;
53
- update(args: Argument<'packages'>, options?: Options): ComposerProcess;
54
- require(packages: Argument<'packages'>, options?: Options): ComposerProcess;
55
- remove(args: Argument<'packages'>, options?: Options): ComposerProcess;
56
58
  packages(): Promise<ComposerPackage[]>;
57
59
  }
58
60
  export {};
package/dist/composer.js CHANGED
@@ -1,6 +1,21 @@
1
- import { ComposerCredentials } from './index.js';
2
- import { ProcessReadable, ProcessResult, } from './shell.js';
1
+ import { ProcessReadable, } from './shell.js';
3
2
  import { nanoid } from 'nanoid';
3
+ class ComposerCredentials {
4
+ constructor(client, notebookId) {
5
+ this.client = client;
6
+ this.notebookId = notebookId;
7
+ }
8
+ async set(input) {
9
+ const response = await this.client.put(`/notebook/${this.notebookId}/composer/credentials`, input);
10
+ return response.data;
11
+ }
12
+ async remove(type, url) {
13
+ const params = new URLSearchParams({ type });
14
+ if (url)
15
+ params.set('url', url);
16
+ await this.client.delete(`/notebook/${this.notebookId}/composer/credentials?${params.toString()}`);
17
+ }
18
+ }
4
19
  export class ComposerProcess {
5
20
  constructor(id, okra, command, args, options) {
6
21
  this.id = id;
@@ -39,7 +54,7 @@ export class ComposerProcess {
39
54
  };
40
55
  this.completion = this.okra.invoke('composer.invoke', { id, command, args, options }).then((result) => {
41
56
  finish();
42
- return new ProcessResult(result.output, result.stdout, result.stderr, result.exitCode);
57
+ return result;
43
58
  }).catch((error) => {
44
59
  fail(error);
45
60
  throw error;
@@ -60,21 +75,6 @@ export default class Composer {
60
75
  run(command, args = {}, options = {}) {
61
76
  return new ComposerProcess(nanoid(), this.okra, command, args, options);
62
77
  }
63
- dumpAutoload(options = {}) {
64
- return this.run('dump-autoload', {}, options);
65
- }
66
- install(options = {}) {
67
- return this.run('install', {}, options);
68
- }
69
- update(args, options = {}) {
70
- return this.run('update', args, options);
71
- }
72
- require(packages, options = {}) {
73
- return this.run('require', packages, options);
74
- }
75
- remove(args, options = {}) {
76
- return this.run('remove', args, options);
77
- }
78
78
  packages() {
79
79
  return this.okra.invoke('composer.packages');
80
80
  }
@@ -1,14 +1,49 @@
1
+ export type RemoteErrorSource = 'core' | 'runtime';
2
+ declare const remoteErrorCodes: readonly ["ActionNotFound", "AuthenticationRequired", "BadGateway", "BadRequest", "Conflict", "FileAlreadyExists", "FileIsDirectory", "FileNotDirectory", "FileNotFound", "FilesystemUnavailable", "InternalError", "InvalidArgument", "InvalidConfiguration", "InvalidRequest", "InvalidResponse", "InvalidServiceCommand", "MethodNotAllowed", "NotebookInitializationFailed", "NotebookUnavailable", "NotFound", "PayloadTooLarge", "PermissionDenied", "RateLimited", "RequestFailed", "RuntimeOperationFailed", "ServiceNotFound", "ServiceUnavailable", "UnprocessableEntity", "ValidationFailed", "WorkspaceUnavailable"];
3
+ export type RemoteErrorCode = typeof remoteErrorCodes[number];
4
+ export type TransportErrorCode = 'ConnectionFailed' | 'ConnectionTimeout' | 'InvalidResponse' | 'SendTimeout';
5
+ export interface ValidationErrorDetails {
6
+ errors: Record<string, string[]>;
7
+ }
8
+ export interface RemoteErrorOptions<TCode extends RemoteErrorCode = RemoteErrorCode, TDetails = unknown> {
9
+ source: RemoteErrorSource;
10
+ status: number;
11
+ code: TCode;
12
+ details?: TDetails;
13
+ cause?: unknown;
14
+ }
15
+ export declare class PHPSandboxError extends Error {
16
+ readonly cause?: unknown;
17
+ constructor(message: string, cause?: unknown);
18
+ }
19
+ export declare class RemoteError<TCode extends RemoteErrorCode = RemoteErrorCode, TDetails = unknown> extends PHPSandboxError {
20
+ readonly source: RemoteErrorSource;
21
+ readonly status: number;
22
+ readonly code: TCode;
23
+ readonly details: TDetails | undefined;
24
+ constructor(message: string, options: RemoteErrorOptions<TCode, TDetails>);
25
+ static is(error: unknown): error is RemoteError;
26
+ static is<TCode extends RemoteErrorCode>(error: unknown, code: TCode): error is RemoteError<TCode>;
27
+ static isValidation(error: unknown): error is RemoteError<'ValidationFailed', ValidationErrorDetails> & {
28
+ readonly details: ValidationErrorDetails;
29
+ };
30
+ is<TExpectedCode extends RemoteErrorCode>(code: TExpectedCode): this is this & RemoteError<TExpectedCode>;
31
+ }
32
+ export declare class TransportError<TCode extends TransportErrorCode = TransportErrorCode> extends PHPSandboxError {
33
+ readonly code: TCode;
34
+ constructor(message: string, code: TCode, cause?: unknown);
35
+ static is(error: unknown): error is TransportError;
36
+ static is<TCode extends TransportErrorCode>(error: unknown, code: TCode): error is TransportError<TCode>;
37
+ is<TExpectedCode extends TransportErrorCode>(code: TExpectedCode): this is this & TransportError<TExpectedCode>;
38
+ }
39
+ export declare function remoteError(source: RemoteErrorSource, payload: unknown, cause?: unknown): RemoteError;
1
40
  export declare class PromiseTimeoutError extends Error {
2
41
  time: number;
3
42
  constructor(message: string, time: number);
4
43
  }
5
- export declare class SendTimeoutError extends PromiseTimeoutError {
44
+ export declare class SendTimeoutError extends TransportError<'SendTimeout'> {
45
+ readonly time: number;
6
46
  static fromPromiseTimeoutError(error: PromiseTimeoutError): SendTimeoutError;
47
+ constructor(message: string, time: number, cause?: unknown);
7
48
  }
8
- export declare class NotebookUnavailableError extends Error {
9
- readonly raw: object;
10
- constructor(message?: string, raw?: object);
11
- }
12
- export declare class UnsupportedRuntimeTransportError extends Error {
13
- constructor(message: string);
14
- }
49
+ export {};
@@ -1,24 +1,130 @@
1
- export class PromiseTimeoutError extends Error {
2
- constructor(message, time) {
1
+ const remoteErrorCodes = [
2
+ 'ActionNotFound',
3
+ 'AuthenticationRequired',
4
+ 'BadGateway',
5
+ 'BadRequest',
6
+ 'Conflict',
7
+ 'FileAlreadyExists',
8
+ 'FileIsDirectory',
9
+ 'FileNotDirectory',
10
+ 'FileNotFound',
11
+ 'FilesystemUnavailable',
12
+ 'InternalError',
13
+ 'InvalidArgument',
14
+ 'InvalidConfiguration',
15
+ 'InvalidRequest',
16
+ 'InvalidResponse',
17
+ 'InvalidServiceCommand',
18
+ 'MethodNotAllowed',
19
+ 'NotebookInitializationFailed',
20
+ 'NotebookUnavailable',
21
+ 'NotFound',
22
+ 'PayloadTooLarge',
23
+ 'PermissionDenied',
24
+ 'RateLimited',
25
+ 'RequestFailed',
26
+ 'RuntimeOperationFailed',
27
+ 'ServiceNotFound',
28
+ 'ServiceUnavailable',
29
+ 'UnprocessableEntity',
30
+ 'ValidationFailed',
31
+ 'WorkspaceUnavailable',
32
+ ];
33
+ export class PHPSandboxError extends Error {
34
+ constructor(message, cause) {
3
35
  super(message);
4
- this.time = time;
36
+ this.name = new.target.name;
37
+ this.cause = cause;
5
38
  }
6
39
  }
7
- export class SendTimeoutError extends PromiseTimeoutError {
8
- static fromPromiseTimeoutError(error) {
9
- return new SendTimeoutError(error.message, error.time);
40
+ export class RemoteError extends PHPSandboxError {
41
+ constructor(message, options) {
42
+ super(message, options.cause);
43
+ this.source = options.source;
44
+ this.status = options.status;
45
+ this.code = options.code;
46
+ this.details = options.details;
47
+ }
48
+ static is(error, code) {
49
+ return error instanceof RemoteError && (code === undefined || error.code === code);
50
+ }
51
+ static isValidation(error) {
52
+ return RemoteError.is(error, 'ValidationFailed') && validationDetails(error.details) !== undefined;
53
+ }
54
+ is(code) {
55
+ return this.code === code;
10
56
  }
11
57
  }
12
- export class NotebookUnavailableError extends Error {
13
- constructor(message = 'Notebook is no longer available', raw = {}) {
14
- super(message);
15
- this.raw = raw;
16
- this.name = 'NotebookUnavailableError';
58
+ export class TransportError extends PHPSandboxError {
59
+ constructor(message, code, cause) {
60
+ super(message, cause);
61
+ this.code = code;
62
+ }
63
+ static is(error, code) {
64
+ return error instanceof TransportError && (code === undefined || error.code === code);
65
+ }
66
+ is(code) {
67
+ return this.code === code;
68
+ }
69
+ }
70
+ export function remoteError(source, payload, cause) {
71
+ const error = record(payload);
72
+ const status = positiveInteger(error?.status);
73
+ const code = nonEmptyString(error?.code);
74
+ const message = nonEmptyString(error?.message);
75
+ if (status === undefined || !isRemoteErrorCode(code) || message === undefined) {
76
+ throw new TransportError('PHPSandbox returned an invalid error response.', 'InvalidResponse', cause ?? payload);
77
+ }
78
+ return new RemoteError(message, {
79
+ source,
80
+ status,
81
+ code,
82
+ details: error?.details,
83
+ cause,
84
+ });
85
+ }
86
+ function isRemoteErrorCode(value) {
87
+ return typeof value === 'string' && remoteErrorCodes.includes(value);
88
+ }
89
+ function positiveInteger(value) {
90
+ return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : undefined;
91
+ }
92
+ function nonEmptyString(value) {
93
+ return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
94
+ }
95
+ function record(value) {
96
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
97
+ ? value
98
+ : undefined;
99
+ }
100
+ function stringArrayRecord(value) {
101
+ const candidate = record(value);
102
+ if (candidate === undefined) {
103
+ return undefined;
17
104
  }
105
+ const entries = Object.entries(candidate);
106
+ if (!entries.every(([, messages]) => Array.isArray(messages) && messages.every((message) => typeof message === 'string'))) {
107
+ return undefined;
108
+ }
109
+ return Object.fromEntries(entries);
110
+ }
111
+ function validationDetails(value) {
112
+ const details = record(value);
113
+ const errors = stringArrayRecord(details?.errors);
114
+ return errors === undefined ? undefined : { errors };
18
115
  }
19
- export class UnsupportedRuntimeTransportError extends Error {
20
- constructor(message) {
116
+ export class PromiseTimeoutError extends Error {
117
+ constructor(message, time) {
21
118
  super(message);
22
- this.name = 'UnsupportedRuntimeTransportError';
119
+ this.time = time;
120
+ }
121
+ }
122
+ export class SendTimeoutError extends TransportError {
123
+ static fromPromiseTimeoutError(error) {
124
+ return new SendTimeoutError(error.message, error.time, error);
125
+ }
126
+ constructor(message, time, cause) {
127
+ super(message, 'SendTimeout', cause);
128
+ this.time = time;
23
129
  }
24
130
  }
package/dist/feedback.js CHANGED
@@ -21,8 +21,7 @@ export class Feedback {
21
21
  if (value !== undefined)
22
22
  query.set(name, String(value));
23
23
  }
24
- const response = await this.client.get(`${this.path()}${query.size === 0 ? '' : `?${query.toString()}`}`);
25
- return response.data;
24
+ return this.client.get(`${this.path()}${query.size === 0 ? '' : `?${query.toString()}`}`);
26
25
  }
27
26
  async get(id) {
28
27
  const response = await this.client.get(this.path(`/${encodeURIComponent(id)}`));
@@ -1,12 +1,7 @@
1
1
  import type { Action, Disposable, NotebookInstance } from './index.js';
2
- import { ErrorEvent } from './types.js';
3
2
  export interface FileResult {
4
3
  name: string;
5
4
  directory: string;
6
- relativePath: string;
7
- /**
8
- * Same value as `relativePath`.
9
- */
10
5
  path: string;
11
6
  }
12
7
  export declare enum FileType {
@@ -470,18 +465,6 @@ type PrefixKey<K extends keyof FilesystemEventData> = `${K}.${string}`;
470
465
  export type FilesystemEvents = FilesystemEventData & {
471
466
  [K in keyof FilesystemEventData as PrefixKey<K>]: FilesystemEventData[K];
472
467
  };
473
- export declare enum FilesystemErrorType {
474
- Unavailable = "Unavailable",
475
- NoPermissions = "NoPermissions",
476
- FileExists = "FileExists",
477
- FileNotFound = "FileNotFound",
478
- FileIsADirectory = "FileIsADirectory",
479
- FileNotADirectory = "FileNotADirectory"
480
- }
481
- export declare class FilesystemError extends ErrorEvent {
482
- readonly name: FilesystemErrorType;
483
- constructor(inner: ErrorEvent, name: FilesystemErrorType);
484
- }
485
468
  declare class FilesystemSubscription {
486
469
  private disposable;
487
470
  private onDispose;
@@ -1,5 +1,4 @@
1
- import { UnsupportedRuntimeTransportError } from './errors/index.js';
2
- import { ErrorEvent } from './types.js';
1
+ import { PHPSandboxError } from './errors/index.js';
3
2
  import { nanoid } from 'nanoid';
4
3
  export var FileType;
5
4
  (function (FileType) {
@@ -39,21 +38,6 @@ export var FileChangeType;
39
38
  FileChangeType[FileChangeType["ADDED"] = 2] = "ADDED";
40
39
  FileChangeType[FileChangeType["DELETED"] = 3] = "DELETED";
41
40
  })(FileChangeType || (FileChangeType = {}));
42
- export var FilesystemErrorType;
43
- (function (FilesystemErrorType) {
44
- FilesystemErrorType["Unavailable"] = "Unavailable";
45
- FilesystemErrorType["NoPermissions"] = "NoPermissions";
46
- FilesystemErrorType["FileExists"] = "FileExists";
47
- FilesystemErrorType["FileNotFound"] = "FileNotFound";
48
- FilesystemErrorType["FileIsADirectory"] = "FileIsADirectory";
49
- FilesystemErrorType["FileNotADirectory"] = "FileNotADirectory";
50
- })(FilesystemErrorType || (FilesystemErrorType = {}));
51
- export class FilesystemError extends ErrorEvent {
52
- constructor(inner, name) {
53
- super(inner.code, inner.message);
54
- this.name = name;
55
- }
56
- }
57
41
  class FilesystemSubscription {
58
42
  constructor(disposable, onDispose) {
59
43
  this.disposable = disposable;
@@ -209,7 +193,7 @@ export class Filesystem {
209
193
  if (this.okra.runtimeTransport === 'rest') {
210
194
  return new ReadableStream({
211
195
  start(controller) {
212
- controller.error(new UnsupportedRuntimeTransportError('Following files requires the realtime transport.'));
196
+ controller.error(new PHPSandboxError('Following files requires the realtime transport.'));
213
197
  },
214
198
  });
215
199
  }
@@ -439,9 +423,6 @@ export class Filesystem {
439
423
  return this.okra.invoke('fs.tree', { path });
440
424
  }
441
425
  handleError(e) {
442
- if (e instanceof ErrorEvent && 'name' in e.raw) {
443
- throw new FilesystemError(e, e.raw.name);
444
- }
445
426
  throw e;
446
427
  }
447
428
  }
package/dist/git.d.ts CHANGED
@@ -62,7 +62,6 @@ export interface GitTargetConfiguration {
62
62
  branch?: string;
63
63
  setDefault?: boolean;
64
64
  }
65
- export type CreateGitTargetInput = GitTargetConfiguration;
66
65
  export interface GitSyncExistingTargetInput {
67
66
  direction?: GitSyncDirection;
68
67
  author?: GitSyncAuthor;
@@ -175,8 +174,6 @@ export interface GitResolveResult {
175
174
  ref?: string;
176
175
  message: string;
177
176
  }
178
- export interface GitEvents {
179
- }
180
177
  export interface GitActions {
181
178
  'git.checkpoint': Action<{
182
179
  author: string;
@@ -239,10 +236,9 @@ export declare class GitTargetApi {
239
236
  private readonly notebookId;
240
237
  private readonly client;
241
238
  constructor(notebookId: string, client: Client);
242
- create(input: CreateGitTargetInput): Promise<GitTarget>;
239
+ create(input: GitTargetConfiguration): Promise<GitTarget>;
243
240
  list(): Promise<GitTarget[]>;
244
241
  get(target: GitTargetReference): Promise<GitTarget>;
245
- sync(target: GitTargetReference, input?: GitSyncExistingTargetInput): Promise<GitTarget>;
246
242
  }
247
243
  export declare class GitTarget {
248
244
  readonly data: GitTargetData;
package/dist/git.js CHANGED
@@ -40,8 +40,9 @@ export default class Git {
40
40
  revert(input) {
41
41
  return this.okra.invoke('git.revert', input);
42
42
  }
43
- credentials() {
44
- return this.okra.gitCredentials();
43
+ async credentials() {
44
+ const response = await this.client.post(`/notebook/${this.okra.data.id}/git/credentials`);
45
+ return response.data;
45
46
  }
46
47
  restore(ref) {
47
48
  return this.okra.invoke('git.restore', { ref });
@@ -96,10 +97,6 @@ export class GitTargetApi {
96
97
  const response = await this.client.get(`/notebook/${this.notebookId}/git/targets/${encodeURIComponent(gitTargetId(target))}`);
97
98
  return new GitTarget(response.data, this.notebookId, this.client);
98
99
  }
99
- async sync(target, input = {}) {
100
- const response = await this.client.post(`/notebook/${this.notebookId}/git/targets/${encodeURIComponent(gitTargetId(target))}/sync`, omitUndefined({ ...input }));
101
- return new GitTarget(response.data, this.notebookId, this.client);
102
- }
103
100
  }
104
101
  export class GitTarget {
105
102
  constructor(data, notebookId, client) {
@@ -110,8 +107,9 @@ export class GitTarget {
110
107
  get id() {
111
108
  return this.data.id;
112
109
  }
113
- sync(input = {}) {
114
- return new GitTargetApi(this.notebookId, this.client).sync(this, input);
110
+ async sync(input = {}) {
111
+ const response = await this.client.post(`/notebook/${this.notebookId}/git/targets/${encodeURIComponent(this.id)}/sync`, omitUndefined({ ...input }));
112
+ return new GitTarget(response.data, this.notebookId, this.client);
115
113
  }
116
114
  }
117
115
  function gitTargetId(target) {