arcane-os 0.88.0 → 0.89.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +1 -1
  3. package/browser-runtime/ai/browser-decisions.mjs +31 -4
  4. package/browser-runtime/ai/core-local.mjs +105 -25
  5. package/browser-runtime/ai/decision-runtime.mjs +7 -0
  6. package/browser-runtime/ai/decision-worker.mjs +9 -1
  7. package/browser-runtime/core/client.mjs +6 -0
  8. package/browser-runtime/core/contracts.mjs +10 -0
  9. package/docs/reference/ai/browser-decisions.md +89 -7
  10. package/docs/reference/cli.md +10 -0
  11. package/docs/reference/core-client.md +59 -0
  12. package/docs/reference/core-native-packaging.md +57 -10
  13. package/docs/reference/core-repositories.md +47 -3
  14. package/docs/reference/inventory/package-api.json +15 -15
  15. package/docs/reference/inventory/runtime-components.json +1 -1
  16. package/docs/reference/inventory/runtime-modules.json +4 -4
  17. package/docs/reference/local-ai.md +86 -3
  18. package/docs/reference/mcp-stdio.md +76 -1
  19. package/docs/reference/native-app-control.md +17 -5
  20. package/docs/reference/protocols.md +14 -0
  21. package/docs/reference/runtime-modules.md +9 -3
  22. package/docs/reference/sdk-api.md +146 -23
  23. package/package.json +2 -2
  24. package/runtime/arcane/modules/RiskSignalAnalyzer.js +1 -5
  25. package/schemas/arcane-app.schema.json +13 -3
  26. package/schemas/arcane-package.schema.json +10 -1
  27. package/src/app-descriptor.mjs +6 -2
  28. package/src/cli/app-control.mjs +1 -1
  29. package/src/core/hosts/windows/ArcaneHost.cs +273 -5
  30. package/src/core/hosts/windows/ArcaneLauncher.cs +9 -0
  31. package/src/core/hosts/windows/ArcaneWindowControl.cs +7 -7
  32. package/src/core/repositories.mjs +11 -1
  33. package/src/core/services/local-ai.mjs +183 -22
  34. package/src/installed-sdk-runtime.mjs +2 -0
  35. package/src/local-ai/config.mjs +16 -0
  36. package/src/local-ai/dev.mjs +2 -2
  37. package/src/local-ai/discover.mjs +3 -0
  38. package/src/local-ai/install.mjs +130 -20
  39. package/src/local-ai/llama-execution-windows.ps1 +158 -0
  40. package/src/local-ai/llama-execution.mjs +169 -0
  41. package/src/local-ai/server.mjs +38 -8
  42. package/src/native/portable-layout.mjs +13 -3
  43. package/src/workspace-operation-lock.mjs +23 -10
  44. package/src/workspace-runtime.mjs +97 -1
@@ -237,7 +237,7 @@ function validateDocumentCatalog(value){
237
237
 
238
238
  function validateNativeWindow(value){
239
239
  if(value===undefined)return undefined;
240
- assertOnlyKeys(value,new Set(['width','height','resizable']),'descriptor.native.window');
240
+ assertOnlyKeys(value,new Set(['width','height','resizable','state']),'descriptor.native.window');
241
241
  for(const dimension of ['width','height']){
242
242
  if(value[dimension]!==undefined&&(!is.integer(value[dimension])||value[dimension]<=0)){
243
243
  fail(`descriptor.native.window.${dimension} must be a positive integer.`);
@@ -246,10 +246,14 @@ function validateNativeWindow(value){
246
246
  if(value.resizable!==undefined&&!is.boolean(value.resizable)){
247
247
  fail('descriptor.native.window.resizable must be a boolean.');
248
248
  }
249
+ if(value.state!==undefined&&!['normal','maximized','fullscreen'].includes(value.state)){
250
+ fail('descriptor.native.window.state must be normal, maximized, or fullscreen.');
251
+ }
249
252
  return completeValue({
250
253
  ...(value.width===undefined?{}:{width:value.width}),
251
254
  ...(value.height===undefined?{}:{height:value.height}),
252
- ...(value.resizable===undefined?{}:{resizable:value.resizable})
255
+ ...(value.resizable===undefined?{}:{resizable:value.resizable}),
256
+ ...(value.state===undefined?{}:{state:value.state})
253
257
  });
254
258
  }
255
259
 
@@ -18,7 +18,7 @@ an application or change its profile. --output selects the capture filename;
18
18
  the other Arcane commands retain their existing output-format option.
19
19
  Inspect --request reads the complete parameter object, including shadowPath and
20
20
  optional documentGeneration. It cannot be combined with --selector.
21
- Key --request selects Tab (optionally shiftKey:true), Enter, or Space and the
21
+ Key --request selects Tab (optionally shiftKey:true), Enter, Space, or Escape and the
22
22
  observed documentGeneration. Resize --request supplies native client width and
23
23
  height for a Normal window; its result includes the previous dimensions.
24
24
  `;
@@ -33,6 +33,7 @@ namespace Arcane.Core.Hosts.Windows
33
33
  public double? InitialClientWidth { get; set; }
34
34
  public double? InitialClientHeight { get; set; }
35
35
  public bool? Resizable { get; set; }
36
+ public string InitialWindowState { get; set; }
36
37
  }
37
38
 
38
39
  public static class ArcaneHost
@@ -130,6 +131,7 @@ namespace Arcane.Core.Hosts.Windows
130
131
  private bool browserProcessExited;
131
132
  private bool closing;
132
133
  private bool closeAllowed;
134
+ private WindowRestoreSnapshot fullscreenRestore;
133
135
 
134
136
  public ArcaneHostForm(ArcaneHostOptions options, Action<string> onDiagnostic,
135
137
  Action<Exception> onError = null)
@@ -138,6 +140,7 @@ namespace Arcane.Core.Hosts.Windows
138
140
  if (onDiagnostic == null) throw new ArgumentNullException("onDiagnostic");
139
141
  ValidateInitialDimension(options.InitialClientWidth, "InitialClientWidth");
140
142
  ValidateInitialDimension(options.InitialClientHeight, "InitialClientHeight");
143
+ if (options.InitialWindowState != null) ValidateWindowState(options.InitialWindowState);
141
144
  this.options = options;
142
145
  this.onDiagnostic = onDiagnostic;
143
146
  this.onError = onError;
@@ -160,9 +163,13 @@ namespace Arcane.Core.Hosts.Windows
160
163
 
161
164
  protected override void OnLoad(EventArgs args)
162
165
  {
163
- // Apply the initial choice once. A composing launcher's Load handler
164
- // can then restore its own saved state; later user sizing is untouched.
165
- if (!started && !closing) ApplyInitialWindowSize();
166
+ // Apply the initial choices before the composing launcher's Load
167
+ // handler. Later app/user sizing remains application-owned.
168
+ if (!started && !closing)
169
+ {
170
+ ApplyInitialWindowSize();
171
+ if (options.InitialWindowState != null) ApplyWindowState(options.InitialWindowState);
172
+ }
166
173
  base.OnLoad(args);
167
174
  if (started || closing) return;
168
175
  started = true;
@@ -202,6 +209,263 @@ namespace Arcane.Core.Hosts.Windows
202
209
  Math.Max(workArea.Top, Math.Min(Top, workArea.Bottom - Height)));
203
210
  }
204
211
 
212
+ private static void ValidateWindowState(string state)
213
+ {
214
+ if (state != "normal" && state != "maximized" && state != "fullscreen")
215
+ throw new ArgumentException("Select window state normal, maximized, or fullscreen.");
216
+ }
217
+
218
+ private FormWindowState CurrentWindowState()
219
+ {
220
+ if (!IsHandleCreated) return WindowState;
221
+ if (IsIconic(Handle)) return FormWindowState.Minimized;
222
+ return IsZoomed(Handle) ? FormWindowState.Maximized : FormWindowState.Normal;
223
+ }
224
+
225
+ private string WindowStateName()
226
+ {
227
+ FormWindowState state = CurrentWindowState();
228
+ if (state != FormWindowState.Normal) return state.ToString();
229
+ return fullscreenRestore != null && FormBorderStyle == FormBorderStyle.None
230
+ ? "Fullscreen" : "Normal";
231
+ }
232
+
233
+ private Dictionary<string, object> WindowStateRecord()
234
+ {
235
+ return new Dictionary<string, object>
236
+ {
237
+ { "platform", "windows" }, { "supported", true },
238
+ { "state", WindowStateName().ToLowerInvariant() }
239
+ };
240
+ }
241
+
242
+ private void ApplyWindowState(string state)
243
+ {
244
+ ValidateWindowState(state);
245
+ bool visible = IsHandleCreated && IsWindowVisible(Handle);
246
+ if (visible && state == WindowStateName().ToLowerInvariant()
247
+ && (state == "fullscreen" || fullscreenRestore == null)) return;
248
+ // Initial hidden configuration keeps ordinary startup behavior. A
249
+ // visible WinForms state setter can activate through ShowWindow.
250
+ WindowStateTransition transition = visible ? new WindowStateTransition(this) : null;
251
+ Exception mutationError = null;
252
+ try { ChangeWindowState(state); }
253
+ catch (Exception error)
254
+ {
255
+ mutationError = error;
256
+ throw;
257
+ }
258
+ finally
259
+ {
260
+ Exception cleanupError = transition == null ? null : transition.Release();
261
+ if (cleanupError != null)
262
+ {
263
+ if (mutationError != null)
264
+ throw new AggregateException("Window state change and activation-hook cleanup failed.",
265
+ mutationError, cleanupError);
266
+ throw cleanupError;
267
+ }
268
+ }
269
+ }
270
+
271
+ private void ChangeWindowState(string state)
272
+ {
273
+ if (state == "fullscreen")
274
+ {
275
+ Rectangle screen = Screen.FromControl(this).Bounds;
276
+ if (fullscreenRestore == null)
277
+ {
278
+ fullscreenRestore = new WindowRestoreSnapshot(
279
+ CurrentWindowState() == FormWindowState.Normal ? Bounds : RestoreBounds,
280
+ FormBorderStyle, MinimizeBox, MaximizeBox, ControlBox);
281
+ }
282
+ // Retain the normal frame and bounds before removing chrome.
283
+ // This affects this window only: no activation or TopMost.
284
+ WindowState = FormWindowState.Normal;
285
+ FormBorderStyle = FormBorderStyle.None;
286
+ Bounds = screen;
287
+ return;
288
+ }
289
+
290
+ if (fullscreenRestore != null)
291
+ {
292
+ // Restore normal geometry before maximizing so WinForms keeps
293
+ // the same restore bounds for a later Normal selection.
294
+ WindowState = FormWindowState.Normal;
295
+ FormBorderStyle = fullscreenRestore.BorderStyle;
296
+ MinimizeBox = fullscreenRestore.MinimizeBox;
297
+ MaximizeBox = fullscreenRestore.MaximizeBox;
298
+ ControlBox = fullscreenRestore.ControlBox;
299
+ Bounds = fullscreenRestore.Bounds;
300
+ fullscreenRestore = null;
301
+ }
302
+ WindowState = state == "maximized" ? FormWindowState.Maximized : FormWindowState.Normal;
303
+ }
304
+
305
+ private string WindowStateResponse(Dictionary<string, object> request, string id, bool apply)
306
+ {
307
+ Dictionary<string, object> previous = WindowStateRecord();
308
+ object parameters;
309
+ request.TryGetValue("parameters", out parameters);
310
+ Dictionary<string, object> response = new Dictionary<string, object>
311
+ {
312
+ { "protocol", "arcane/1" }, { "type", "response" }, { "id", id }
313
+ };
314
+ try
315
+ {
316
+ if (apply)
317
+ {
318
+ Dictionary<string, object> selection = parameters as Dictionary<string, object>;
319
+ object state;
320
+ if (selection == null || !selection.TryGetValue("state", out state) || !(state is string))
321
+ throw new ArgumentException("Supply a window state selection.");
322
+ ApplyWindowState((string)state);
323
+ }
324
+ response["ok"] = true;
325
+ response["result"] = WindowStateRecord();
326
+ }
327
+ catch (Exception error)
328
+ {
329
+ Dictionary<string, object> record = (Dictionary<string, object>)ArcaneHost.ErrorRecord(error,
330
+ error is ArgumentException ? "INVALID_ARGUMENT" : "ARCANE_WINDOW_STATE_FAILED");
331
+ record["details"] = new Dictionary<string, object>
332
+ {
333
+ { "requested", parameters }, { "previous", previous }, { "actual", WindowStateRecord() }
334
+ };
335
+ response["ok"] = false;
336
+ response["error"] = record;
337
+ }
338
+ return ArcaneHost.Serializer().Serialize(response);
339
+ }
340
+
341
+ private sealed class WindowRestoreSnapshot
342
+ {
343
+ internal readonly Rectangle Bounds;
344
+ internal readonly FormBorderStyle BorderStyle;
345
+ internal readonly bool MinimizeBox;
346
+ internal readonly bool MaximizeBox;
347
+ internal readonly bool ControlBox;
348
+
349
+ internal WindowRestoreSnapshot(Rectangle bounds, FormBorderStyle borderStyle,
350
+ bool minimizeBox, bool maximizeBox, bool controlBox)
351
+ {
352
+ Bounds = bounds;
353
+ BorderStyle = borderStyle;
354
+ MinimizeBox = minimizeBox;
355
+ MaximizeBox = maximizeBox;
356
+ ControlBox = controlBox;
357
+ }
358
+ }
359
+
360
+ private sealed class WindowStateTransition
361
+ {
362
+ private const int WhCbt = 5;
363
+ private const int HcbtActivate = 5;
364
+ private const int HcbtSetFocus = 9;
365
+ private delegate IntPtr HookProcedure(int code, IntPtr target, IntPtr details);
366
+ // A failed native unhook must never retain a collected delegate.
367
+ private static readonly HookProcedure callback = BeforeWindowChange;
368
+ [ThreadStatic] private static WindowStateTransition current;
369
+ [ThreadStatic] private static List<WindowStateTransition> pendingCleanup;
370
+ private readonly ArcaneHostForm owner;
371
+ private readonly WindowStateTransition previous;
372
+ private IntPtr hook;
373
+ private bool active;
374
+
375
+ internal WindowStateTransition(ArcaneHostForm owner)
376
+ {
377
+ this.owner = owner;
378
+ previous = current;
379
+ hook = SetWindowsHookEx(WhCbt, callback, IntPtr.Zero, GetCurrentThreadId());
380
+ if (hook == IntPtr.Zero)
381
+ {
382
+ int code = Marshal.GetLastWin32Error();
383
+ System.ComponentModel.Win32Exception error = new System.ComponentModel.Win32Exception(code);
384
+ error.Data["operation"] = "SetWindowsHookExW";
385
+ error.Data["nativeErrorCode"] = code;
386
+ throw error;
387
+ }
388
+ active = true;
389
+ current = this;
390
+ }
391
+
392
+ private static IntPtr BeforeWindowChange(int code, IntPtr target, IntPtr details)
393
+ {
394
+ if (code == HcbtActivate || code == HcbtSetFocus)
395
+ {
396
+ for (WindowStateTransition transition = current; transition != null; transition = transition.previous)
397
+ {
398
+ if (!transition.active || !transition.owner.IsHandleCreated) continue;
399
+ IntPtr window = transition.owner.Handle;
400
+ if (target == window || (code == HcbtSetFocus && IsChild(window, target)))
401
+ return new IntPtr(1);
402
+ }
403
+ }
404
+ return CallNextHookEx(IntPtr.Zero, code, target, details);
405
+ }
406
+
407
+ internal Exception Release()
408
+ {
409
+ // Stop vetoing immediately, even if the native unhook fails.
410
+ active = false;
411
+ if (current == this) current = previous;
412
+ if (hook == IntPtr.Zero) return null;
413
+ if (UnhookWindowsHookEx(hook))
414
+ {
415
+ hook = IntPtr.Zero;
416
+ if (pendingCleanup != null) pendingCleanup.Remove(this);
417
+ return null;
418
+ }
419
+ int code = Marshal.GetLastWin32Error();
420
+ if (pendingCleanup == null) pendingCleanup = new List<WindowStateTransition>();
421
+ if (!pendingCleanup.Contains(this)) pendingCleanup.Add(this);
422
+ System.ComponentModel.Win32Exception error = new System.ComponentModel.Win32Exception(code);
423
+ error.Data["operation"] = "UnhookWindowsHookEx";
424
+ error.Data["nativeErrorCode"] = code;
425
+ return error;
426
+ }
427
+
428
+ internal static void ReleasePending(ArcaneHostForm owner)
429
+ {
430
+ if (pendingCleanup == null) return;
431
+ foreach (WindowStateTransition transition in pendingCleanup.ToArray())
432
+ {
433
+ if (transition.owner != owner) continue;
434
+ Exception error = transition.Release();
435
+ if (error != null) owner.Report(error);
436
+ }
437
+ }
438
+
439
+ [DllImport("user32.dll", EntryPoint = "SetWindowsHookExW", SetLastError = true)]
440
+ private static extern IntPtr SetWindowsHookEx(int kind, HookProcedure procedure, IntPtr module, uint threadId);
441
+
442
+ [DllImport("user32.dll", SetLastError = true)]
443
+ [return: MarshalAs(UnmanagedType.Bool)]
444
+ private static extern bool UnhookWindowsHookEx(IntPtr hook);
445
+
446
+ [DllImport("user32.dll")]
447
+ private static extern IntPtr CallNextHookEx(IntPtr hook, int code, IntPtr target, IntPtr details);
448
+
449
+ [DllImport("user32.dll")]
450
+ [return: MarshalAs(UnmanagedType.Bool)]
451
+ private static extern bool IsChild(IntPtr parent, IntPtr child);
452
+
453
+ [DllImport("kernel32.dll")]
454
+ private static extern uint GetCurrentThreadId();
455
+ }
456
+
457
+ [DllImport("user32.dll")]
458
+ [return: MarshalAs(UnmanagedType.Bool)]
459
+ private static extern bool IsWindowVisible(IntPtr window);
460
+
461
+ [DllImport("user32.dll")]
462
+ [return: MarshalAs(UnmanagedType.Bool)]
463
+ private static extern bool IsIconic(IntPtr window);
464
+
465
+ [DllImport("user32.dll")]
466
+ [return: MarshalAs(UnmanagedType.Bool)]
467
+ private static extern bool IsZoomed(IntPtr window);
468
+
205
469
  private async Task InitializeAsync()
206
470
  {
207
471
  try
@@ -296,12 +560,15 @@ namespace Arcane.Core.Hosts.Windows
296
560
  {
297
561
  if (documentGeneration != generation || closing)
298
562
  throw new InvalidOperationException("The requesting document is no longer connected.");
563
+ string method = EnvelopeString(envelope, "method");
299
564
  if (requestId != null && EnvelopeString(envelope, "protocol") == "arcane/1"
300
- && EnvelopeString(envelope, "method") == "window.setTheme")
565
+ && (method == "window.setTheme" || method == "window.state" || method == "window.setState"))
301
566
  {
302
567
  // This method belongs to this exact window, not the Core
303
568
  // child or a machine-wide appearance service.
304
- string response = WindowThemeResponse(envelope, requestId);
569
+ string response = method == "window.setTheme"
570
+ ? WindowThemeResponse(envelope, requestId)
571
+ : WindowStateResponse(envelope, requestId, method == "window.setState");
305
572
  pending.Enqueue(
306
573
  new CoreDelivery(documentGeneration, response)
307
574
  );
@@ -747,6 +1014,7 @@ namespace Arcane.Core.Hosts.Windows
747
1014
  if (bridge != null) bridge.Stop(new InvalidOperationException("The application window is closing."));
748
1015
  if (activeBridge != null) activeBridge.Stop(new InvalidOperationException("The application window is closing."));
749
1016
  StopAppControl();
1017
+ WindowStateTransition.ReleasePending(this);
750
1018
  shutdown = DrainAndCloseAsync();
751
1019
  shutdown.ContinueWith(ObserveUnexpectedFailure, TaskContinuationOptions.OnlyOnFaulted);
752
1020
  }
@@ -98,6 +98,7 @@ namespace Arcane.Core.Hosts.Windows
98
98
  InitialClientWidth = OptionalWindowDimension(windowOptions, "width"),
99
99
  InitialClientHeight = OptionalWindowDimension(windowOptions, "height"),
100
100
  Resizable = OptionalWindowResizable(windowOptions),
101
+ InitialWindowState = OptionalWindowState(windowOptions),
101
102
  IconPath = String.IsNullOrEmpty(icon) ? null : Path.Combine(directory, icon),
102
103
  ClassicClientSource = File.ReadAllText(Path.Combine(directory, RequiredString(client, "source"))),
103
104
  AppControlEndpoint = appControl == null ? null : RequiredString(appControl, "endpoint"),
@@ -210,6 +211,14 @@ namespace Arcane.Core.Hosts.Windows
210
211
  throw new FormatException("The native manifest needs a boolean at window.resizable.");
211
212
  }
212
213
 
214
+ private static string OptionalWindowState(Dictionary<string, object> window)
215
+ {
216
+ object value;
217
+ if (window == null || !window.TryGetValue("state", out value)) return null;
218
+ if (value is string) return (string)value;
219
+ throw new FormatException("The native manifest needs a string at window.state.");
220
+ }
221
+
213
222
  private static string QuoteArgument(string value)
214
223
  {
215
224
  // Windows command-line escaping is transport-local. The child
@@ -148,7 +148,7 @@ namespace Arcane.Core.Hosts.Windows
148
148
  {
149
149
  return new Dictionary<string, object>
150
150
  {
151
- { "title", Text }, { "state", WindowState.ToString() },
151
+ { "title", Text }, { "state", WindowStateName() },
152
152
  { "width", ClientSize.Width }, { "height", ClientSize.Height }
153
153
  };
154
154
  }
@@ -252,8 +252,8 @@ namespace Arcane.Core.Hosts.Windows
252
252
  {
253
253
  object value;
254
254
  string key = parameters.TryGetValue("key", out value) ? value as string : null;
255
- if (key != "Tab" && key != "Enter" && key != "Space")
256
- throw new ArgumentException("key must be Tab, Enter, or Space.");
255
+ if (key != "Tab" && key != "Enter" && key != "Space" && key != "Escape")
256
+ throw new ArgumentException("key must be Tab, Enter, Space, or Escape.");
257
257
  bool shiftKey = false;
258
258
  if (parameters.TryGetValue("shiftKey", out value))
259
259
  {
@@ -263,14 +263,14 @@ namespace Arcane.Core.Hosts.Windows
263
263
  if (shiftKey && key != "Tab") throw new ArgumentException("shiftKey is supported only with Tab.");
264
264
  Dictionary<string, object> down = new Dictionary<string, object>
265
265
  {
266
- { "type", key == "Tab" ? "rawKeyDown" : "keyDown" },
266
+ { "type", key == "Tab" || key == "Escape" ? "rawKeyDown" : "keyDown" },
267
267
  { "key", key == "Space" ? " " : key }, { "code", key },
268
- { "windowsVirtualKeyCode", key == "Tab" ? 9 : key == "Enter" ? 13 : 32 },
268
+ { "windowsVirtualKeyCode", key == "Tab" ? 9 : key == "Enter" ? 13 : key == "Escape" ? 27 : 32 },
269
269
  { "modifiers", shiftKey ? 8 : 0 }
270
270
  };
271
271
  Dictionary<string, object> up = new Dictionary<string, object>(down);
272
272
  up["type"] = "keyUp";
273
- if (key != "Tab")
273
+ if (key == "Enter" || key == "Space")
274
274
  {
275
275
  down["text"] = key == "Enter" ? "\r" : " ";
276
276
  down["unmodifiedText"] = down["text"];
@@ -345,7 +345,7 @@ namespace Arcane.Core.Hosts.Windows
345
345
 
346
346
  private void RequireAppControlNormalWindow()
347
347
  {
348
- if (WindowState == FormWindowState.Normal) return;
348
+ if (WindowStateName() == "Normal") return;
349
349
  InvalidOperationException error = new InvalidOperationException("App-control resize requires a Normal window.");
350
350
  error.Data["reason"] = "unsupported-window-state";
351
351
  error.Data["supportedWindowState"] = "Normal";
@@ -45,7 +45,7 @@ export function resolveArcaneDataPaths({dataRoot} = {}) {
45
45
 
46
46
  /** One connected checkout, composed into an application's own Core service. */
47
47
  export function createRepositoryWorkspace(
48
- {name, directory, dataRoot, remote, branch, longPaths, gitIdentity, onEvent, run = runProcess} = {}
48
+ {name, directory, dataRoot, remote, branch, longPaths, cloneIdentity = {}, gitIdentity, onEvent, run = runProcess} = {}
49
49
  ) {
50
50
  let selected = directory;
51
51
  if (selected === undefined) {
@@ -62,6 +62,15 @@ export function createRepositoryWorkspace(
62
62
  throw new TypeError('longPaths must be a boolean when supplied.');
63
63
  }
64
64
  if (!is.function(run)) throw new TypeError('run must implement the SDK process adapter.');
65
+ const {name: cloneName, email: cloneEmail} = cloneIdentity;
66
+ const cloneConfiguration = [];
67
+ for (const [field, value] of Object.entries({name: cloneName, email: cloneEmail})) {
68
+ if (value === undefined) continue;
69
+ if (!is.string(value) || !value.isWellFormed() || value.includes('\0')) {
70
+ throw new TypeError(`cloneIdentity.${field} must be text representable as a native process argument.`);
71
+ }
72
+ cloneConfiguration.push('--config', `user.${field}=${value}`);
73
+ }
65
74
  const execute = createGitIdentityRunner(run, gitIdentity);
66
75
  const repositoryDirectory = path.resolve(selected);
67
76
  const pending = new Set();
@@ -108,6 +117,7 @@ export function createRepositoryWorkspace(
108
117
  if (process.platform === 'win32' && longPaths !== undefined) {
109
118
  arguments_.push('--config', `core.longpaths=${longPaths}`);
110
119
  }
120
+ arguments_.push(...cloneConfiguration);
111
121
  if (branch !== undefined) arguments_.push('--branch', branch);
112
122
  arguments_.push('--', remote, repositoryDirectory);
113
123
  const result = await prepareRun(