arcane-os 0.91.0 → 0.93.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.
- package/CHANGELOG.md +33 -0
- package/README.md +1 -1
- package/browser-runtime/core/client.mjs +18 -0
- package/browser-runtime/core/contracts.mjs +20 -0
- package/browser-runtime/core/native-diagnostics.mjs +217 -0
- package/docs/reference/core/arcane-api.md +32 -0
- package/docs/reference/core/arcane-events.md +25 -0
- package/docs/reference/core-client.md +99 -0
- package/docs/reference/core-repositories.md +87 -4
- package/docs/reference/git-text-snapshot.md +47 -3
- package/docs/reference/inventory/package-api.json +7 -7
- package/docs/reference/native-diagnostics.md +167 -0
- package/docs/reference/sdk-api.md +52 -7
- package/package.json +3 -1
- package/src/core/app-control.mjs +1 -0
- package/src/core/hosts/windows/ArcaneAppControl.cs +1 -1
- package/src/core/hosts/windows/ArcaneHost.cs +167 -0
- package/src/core/hosts/windows/ArcaneLauncher.cs +1 -0
- package/src/core/hosts/windows/ArcaneNativeDiagnostics.cs +137 -0
- package/src/core/hosts/windows/ArcaneNotifications.cs +822 -0
- package/src/core/hosts/windows/ArcaneWindowControl.cs +3 -2
- package/src/core/repositories.mjs +60 -3
- package/src/core-native-diagnostics-source.mjs +13 -0
- package/src/git-text-snapshot.mjs +25 -5
- package/src/import-map.mjs +1 -0
- package/src/process.mjs +25 -1
- package/src/repository.mjs +25 -1
- package/src/windows-git-executable.mjs +103 -0
|
@@ -38,7 +38,7 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
38
38
|
{
|
|
39
39
|
if (appControl == null) return;
|
|
40
40
|
appControlScript = await browser.AddScriptToExecuteOnDocumentCreatedAsync(
|
|
41
|
-
"globalThis[" + AppControlDocumentKey + "]
|
|
41
|
+
"globalThis[" + AppControlDocumentKey + "] ??= globalThis.crypto.randomUUID();");
|
|
42
42
|
if (closing) return;
|
|
43
43
|
browser.NavigationStarting += AppControlNavigationStarting;
|
|
44
44
|
browser.NavigationCompleted += AppControlNavigationCompleted;
|
|
@@ -140,7 +140,7 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
140
140
|
{ "url", webView.Source == null ? null : webView.Source.AbsoluteUri },
|
|
141
141
|
{ "documentGeneration", appControlGeneration }, { "ready", AppControlDocumentReady() },
|
|
142
142
|
{ "window", AppControlWindow() },
|
|
143
|
-
{ "operations", new string[] { "status", "inspect", "capture", "act", "key", "resize" } }
|
|
143
|
+
{ "operations", new string[] { "status", "diagnostics", "inspect", "capture", "act", "key", "resize" } }
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
146
|
|
|
@@ -158,6 +158,7 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
158
158
|
{
|
|
159
159
|
cancellation.ThrowIfCancellationRequested();
|
|
160
160
|
if (method == "app.control.status") return AppControlStatus();
|
|
161
|
+
if (method == "app.control.diagnostics") return nativeDiagnostics.Snapshot(parameters);
|
|
161
162
|
if (method != "app.control.inspect" && method != "app.control.capture" && method != "app.control.act"
|
|
162
163
|
&& method != "app.control.key" && method != "app.control.resize")
|
|
163
164
|
throw new ArgumentException("Unknown app-control method: " + method);
|
|
@@ -5,7 +5,7 @@ import Is from 'strong-type';
|
|
|
5
5
|
import {ArcaneError, throwIfAborted} from '../errors.mjs';
|
|
6
6
|
import {runProcess} from '../process.mjs';
|
|
7
7
|
import {captureRepositoryRemote, captureRepositoryTarget, repositoryConfiguration, pullRepositoryTarget, pushRepositoryTarget,
|
|
8
|
-
repositoryPushArguments, repositoryStatus} from '../repository.mjs';
|
|
8
|
+
repositoryHeadArguments, repositoryHeadState, repositoryPushArguments, repositoryStatus} from '../repository.mjs';
|
|
9
9
|
import {createGitIdentityRunner} from '../git-identity.mjs';
|
|
10
10
|
|
|
11
11
|
export {readGitIdentity} from '../git-identity.mjs';
|
|
@@ -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, remoteBase, branch, longPaths, cloneIdentity = {}, gitIdentity, onEvent, run = runProcess} = {}
|
|
48
|
+
{name, directory, dataRoot, remote, remoteBase, branch, initialBranch, longPaths, cloneIdentity = {}, gitIdentity, onEvent, run = runProcess} = {}
|
|
49
49
|
) {
|
|
50
50
|
let selected = directory;
|
|
51
51
|
if (selected === undefined) {
|
|
@@ -59,6 +59,12 @@ export function createRepositoryWorkspace(
|
|
|
59
59
|
if (remote !== undefined) requireString(remote, 'remote');
|
|
60
60
|
const remoteArgument = captureRepositoryRemote(remote, remoteBase);
|
|
61
61
|
if (branch !== undefined) requireString(branch, 'branch');
|
|
62
|
+
if (initialBranch !== undefined) {
|
|
63
|
+
requireString(initialBranch, 'initialBranch');
|
|
64
|
+
if (!initialBranch.isWellFormed() || initialBranch.includes('\0')) {
|
|
65
|
+
throw new TypeError('initialBranch must be text representable as a native process argument.');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
62
68
|
if (longPaths !== undefined && !is.boolean(longPaths)) {
|
|
63
69
|
throw new TypeError('longPaths must be a boolean when supplied.');
|
|
64
70
|
}
|
|
@@ -126,9 +132,60 @@ export function createRepositoryWorkspace(
|
|
|
126
132
|
arguments_,
|
|
127
133
|
{cwd: path.dirname(repositoryDirectory), signal, onEvent}
|
|
128
134
|
);
|
|
135
|
+
let outputs;
|
|
136
|
+
if (initialBranch !== undefined) {
|
|
137
|
+
const options = {cwd: repositoryDirectory, signal, onEvent};
|
|
138
|
+
outputs = [];
|
|
139
|
+
function completed(operation, observation) {
|
|
140
|
+
outputs.push({operation, stdout: observation.stdout, stderr: observation.stderr,
|
|
141
|
+
code: observation.code, signal: observation.signal ?? null});
|
|
142
|
+
}
|
|
143
|
+
completed('clone', result);
|
|
144
|
+
try {
|
|
145
|
+
throwIfAborted(signal);
|
|
146
|
+
const advertised = await prepareRun('git', ['ls-remote', '--symref', '--', remoteArgument],
|
|
147
|
+
{cwd: path.dirname(repositoryDirectory), signal, onEvent});
|
|
148
|
+
completed('observeRemote', advertised);
|
|
149
|
+
throwIfAborted(signal);
|
|
150
|
+
if (advertised.code !== 0) {
|
|
151
|
+
throw new ArcaneError('ARCANE_OPERATION_FAILED', 'Git remote observation failed.', {details: advertised});
|
|
152
|
+
}
|
|
153
|
+
if (advertised.stdout === '') {
|
|
154
|
+
const state = await prepareRun('git', [...repositoryHeadArguments], options);
|
|
155
|
+
completed('observeHead', state);
|
|
156
|
+
throwIfAborted(signal);
|
|
157
|
+
if (state.code !== 0) {
|
|
158
|
+
throw new ArcaneError('ARCANE_OPERATION_FAILED', 'Git branch state observation failed.', {details: state});
|
|
159
|
+
}
|
|
160
|
+
if (repositoryHeadState(state.stdout, state).unborn) {
|
|
161
|
+
const references = await prepareRun('git', ['for-each-ref', '--format=%(refname)'], options);
|
|
162
|
+
completed('observeRefs', references);
|
|
163
|
+
throwIfAborted(signal);
|
|
164
|
+
if (references.code !== 0) {
|
|
165
|
+
throw new ArcaneError('ARCANE_OPERATION_FAILED', 'Git local ref observation failed.', {details: references});
|
|
166
|
+
}
|
|
167
|
+
// Git templates or hooks can supply local refs even
|
|
168
|
+
// when the remote and this checkout's HEAD are unborn.
|
|
169
|
+
if (references.stdout === '') {
|
|
170
|
+
const selection = await prepareRun('git', ['symbolic-ref', 'HEAD', `refs/heads/${initialBranch}`], options);
|
|
171
|
+
completed('selectInitialBranch', selection);
|
|
172
|
+
throwIfAborted(signal);
|
|
173
|
+
if (selection.code !== 0) {
|
|
174
|
+
throw new ArcaneError('ARCANE_OPERATION_FAILED', 'Git initial branch selection failed.', {details: selection});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} catch (cause) {
|
|
180
|
+
throw new ArcaneError(cause?.code ?? 'ARCANE_REPOSITORY_PREPARE_FAILED',
|
|
181
|
+
cause instanceof Error ? cause.message : String(cause),
|
|
182
|
+
{cause, details: {directory: repositoryDirectory, cloned: true, outputs}, exitCode: cause?.exitCode});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
129
185
|
throwIfAborted(signal);
|
|
130
186
|
opened = true;
|
|
131
|
-
return {directory: repositoryDirectory, cloned: true, stdout: result.stdout, stderr: result.stderr
|
|
187
|
+
return {directory: repositoryDirectory, cloned: true, stdout: result.stdout, stderr: result.stderr,
|
|
188
|
+
...(outputs === undefined ? {} : {outputs})};
|
|
132
189
|
}
|
|
133
190
|
|
|
134
191
|
function accept(operation, signal) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {readFile} from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
/** Project the one canonical diagnostic observer into a document-start script. */
|
|
4
|
+
export async function createNativeDiagnosticsSource() {
|
|
5
|
+
const source = await readFile(new URL('../browser-runtime/core/native-diagnostics.mjs', import.meta.url), 'utf8');
|
|
6
|
+
return `(function installArcaneNativeDiagnostics(global){
|
|
7
|
+
'use strict';
|
|
8
|
+
if(global.top!==undefined&&global.top!==global)return;
|
|
9
|
+
${source.replace(/^export (?=(?:async )?function )/gmu, '')}
|
|
10
|
+
installNativeDiagnostics(global);
|
|
11
|
+
})(globalThis);
|
|
12
|
+
`;
|
|
13
|
+
}
|
|
@@ -15,11 +15,12 @@ const snapshotRef = 'refs/arcane/text-snapshot';
|
|
|
15
15
|
* One application service owns each dedicated cache directory. Construction
|
|
16
16
|
* performs no I/O; refresh coalesces callers and close drains accepted work.
|
|
17
17
|
*/
|
|
18
|
-
export function createGitTextSnapshot({cacheDirectory, remote, remoteBase, ref, selectPath, gitIdentity, onEvent, run = runProcess} = {}) {
|
|
18
|
+
export function createGitTextSnapshot({cacheDirectory, remote, remoteBase, ref, selectPath, allowUnborn = false, gitIdentity, onEvent, run = runProcess} = {}) {
|
|
19
19
|
for (const [name, value] of Object.entries({cacheDirectory, remote, ref})) {
|
|
20
20
|
if (!is.string(value) || value === '') throw new TypeError(`${name} must be a nonempty string.`);
|
|
21
21
|
}
|
|
22
22
|
if (!is.function(selectPath)) throw new TypeError('selectPath must be a function.');
|
|
23
|
+
if (!is.boolean(allowUnborn)) throw new TypeError('allowUnborn must be a boolean.');
|
|
23
24
|
const remoteArgument = captureRepositoryRemote(remote, remoteBase);
|
|
24
25
|
const execute = createGitIdentityRunner(run, gitIdentity);
|
|
25
26
|
const directory = path.resolve(cacheDirectory);
|
|
@@ -70,11 +71,28 @@ export function createGitTextSnapshot({cacheDirectory, remote, remoteBase, ref,
|
|
|
70
71
|
try {
|
|
71
72
|
await events.send({type: 'git.snapshot.refreshing', message: 'Refreshing the selected repository revision.', data: {ref}});
|
|
72
73
|
await prepare(events);
|
|
74
|
+
if (allowUnborn) {
|
|
75
|
+
// No ref pattern or --exit-code: a successful complete empty
|
|
76
|
+
// advertisement is distinct from a missing selected ref.
|
|
77
|
+
const advertised = await git(['ls-remote', '--symref', '--', remoteArgument], {onEvent: events.send});
|
|
78
|
+
if (advertised.code !== 0) {
|
|
79
|
+
throw new ArcaneError(ERROR_CODES.operationFailed, 'Git remote observation failed.', {details: advertised});
|
|
80
|
+
}
|
|
81
|
+
if (advertised.stdout === '') {
|
|
82
|
+
const result = {revision: null, files: [], unborn: true};
|
|
83
|
+
await events.send({type: 'git.snapshot.completed', message: 'The remote advertises no revision.',
|
|
84
|
+
data: {revision: null, unborn: true, reused: retained?.unborn === true}});
|
|
85
|
+
await events.drain();
|
|
86
|
+
retained = result;
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
73
90
|
await git(['fetch', '--no-tags', '--no-write-fetch-head', '--', remoteArgument, `+${ref}:${snapshotRef}`], {onEvent: events.send});
|
|
74
91
|
const selected = await git(['rev-parse', '--verify', `${snapshotRef}^{commit}`], {onEvent: events.send});
|
|
75
92
|
const revision = selected.stdout.trim();
|
|
76
93
|
if (retained?.revision === revision) {
|
|
77
|
-
await events.send({type: 'git.snapshot.completed', message: 'The selected repository revision is unchanged.',
|
|
94
|
+
await events.send({type: 'git.snapshot.completed', message: 'The selected repository revision is unchanged.',
|
|
95
|
+
data: {revision, reused: true, ...(allowUnborn ? {unborn: false} : {})}});
|
|
78
96
|
await events.drain();
|
|
79
97
|
return retained;
|
|
80
98
|
}
|
|
@@ -125,8 +143,9 @@ export function createGitTextSnapshot({cacheDirectory, remote, remoteBase, ref,
|
|
|
125
143
|
}
|
|
126
144
|
});
|
|
127
145
|
}
|
|
128
|
-
const result = {revision, files: reader.finish()};
|
|
129
|
-
await events.send({type: 'git.snapshot.completed', message: 'Repository text snapshot is ready.',
|
|
146
|
+
const result = {revision, files: reader.finish(), ...(allowUnborn ? {unborn: false} : {})};
|
|
147
|
+
await events.send({type: 'git.snapshot.completed', message: 'Repository text snapshot is ready.',
|
|
148
|
+
data: {revision, reused: false, ...(allowUnborn ? {unborn: false} : {})}});
|
|
130
149
|
await events.drain();
|
|
131
150
|
retained = result;
|
|
132
151
|
return result;
|
|
@@ -156,7 +175,8 @@ export function createGitTextSnapshot({cacheDirectory, remote, remoteBase, ref,
|
|
|
156
175
|
accepted.then(settled, settled);
|
|
157
176
|
}
|
|
158
177
|
return waitForSnapshot(active, signal).then(function copyResult(snapshot) {
|
|
159
|
-
return {revision: snapshot.revision, files: snapshot.files.map(function copyFile(file) { return {...file}; })
|
|
178
|
+
return {revision: snapshot.revision, files: snapshot.files.map(function copyFile(file) { return {...file}; }),
|
|
179
|
+
...(allowUnborn ? {unborn: snapshot.unborn} : {})};
|
|
160
180
|
});
|
|
161
181
|
}
|
|
162
182
|
|
package/src/import-map.mjs
CHANGED
|
@@ -44,6 +44,7 @@ const SDK_BROWSER_SELF_IMPORTS=new Map([
|
|
|
44
44
|
['arcane-os/websocket-client','sdk/websocket-client.mjs'],
|
|
45
45
|
['arcane-os/core/client','sdk/core/client.mjs'],
|
|
46
46
|
['arcane-os/core/contracts','sdk/core/contracts.mjs'],
|
|
47
|
+
['arcane-os/core/native-diagnostics','sdk/core/native-diagnostics.mjs'],
|
|
47
48
|
['arcane-os/document-acquisition','sdk/document-acquisition.mjs'],
|
|
48
49
|
['arcane-os/logging','sdk/logging.mjs'],
|
|
49
50
|
['arcane-os/browser-device','sdk/browser-device.mjs'],
|
package/src/process.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import {spawn} from 'node:child_process';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import {ArcaneError,ERROR_CODES,throwIfAborted} from './errors.mjs';
|
|
5
5
|
import {createEventQueue} from './event-queue.mjs';
|
|
6
|
+
import {windowsGitEnvironment} from './windows-git-executable.mjs';
|
|
6
7
|
|
|
7
8
|
const is = new Is(false);
|
|
8
9
|
|
|
@@ -195,6 +196,29 @@ export async function runProcess(command,args=[],{
|
|
|
195
196
|
});
|
|
196
197
|
throwIfAborted(signal);
|
|
197
198
|
|
|
199
|
+
let childEnvironment=env?{...process.env,...env}:process.env;
|
|
200
|
+
if(process.platform==='win32'&&command==='git'){
|
|
201
|
+
try{
|
|
202
|
+
childEnvironment=await windowsGitEnvironment(childEnvironment,{
|
|
203
|
+
run:runProcess,signal,
|
|
204
|
+
onEvent:function discoveryEvent(event){
|
|
205
|
+
return events.send({
|
|
206
|
+
...event,type:event.type.replace(/^process\./u,'process.git.discovery.')
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}catch(error){
|
|
211
|
+
throwIfAborted(signal);
|
|
212
|
+
if(events.error)throw events.error;
|
|
213
|
+
await events.send({
|
|
214
|
+
type:'process.git.discovery.unavailable',
|
|
215
|
+
message:'Registered Git discovery was unavailable; using the existing command environment.',
|
|
216
|
+
data:{error}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
throwIfAborted(signal);
|
|
220
|
+
}
|
|
221
|
+
|
|
198
222
|
return new Promise((resolve,reject)=>{
|
|
199
223
|
let stdout=outputEncodings.stdout!==null&&outputSelected(captureOutput,'stdout')?'':null;
|
|
200
224
|
let stderr=outputEncodings.stderr!==null&&outputSelected(captureOutput,'stderr')?'':null;
|
|
@@ -369,7 +393,7 @@ export async function runProcess(command,args=[],{
|
|
|
369
393
|
try{
|
|
370
394
|
child=spawn(executable,executableArgs,{
|
|
371
395
|
cwd,
|
|
372
|
-
env:
|
|
396
|
+
env:childEnvironment,
|
|
373
397
|
shell:false,
|
|
374
398
|
detached:process.platform!=='win32',
|
|
375
399
|
windowsHide:true,
|
package/src/repository.mjs
CHANGED
|
@@ -85,6 +85,25 @@ export function repositoryPushArguments(selection) {
|
|
|
85
85
|
: ['push', '--no-follow-tags', '--', selection.remoteArgument, `HEAD:${selection.target.ref}`];
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// Both configuration and new-clone preparation consume Git's explicit initial
|
|
89
|
+
// state. NUL framing keeps unusual working filenames outside these headers.
|
|
90
|
+
export const repositoryHeadArguments = [
|
|
91
|
+
'--no-optional-locks', 'status', '--porcelain=v2', '-z', '--branch', '--untracked-files=no'
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
export function repositoryHeadState(output, details) {
|
|
95
|
+
const prefix = '# branch.oid ';
|
|
96
|
+
const record = output.split('\0').find(function branchCommit(value) {
|
|
97
|
+
return value.startsWith(prefix);
|
|
98
|
+
});
|
|
99
|
+
const revision = record?.slice(prefix.length);
|
|
100
|
+
if (!revision) {
|
|
101
|
+
throw new ArcaneError('ARCANE_REPOSITORY_HEAD_UNAVAILABLE',
|
|
102
|
+
'Git did not report its current commit or initial branch state.', {details});
|
|
103
|
+
}
|
|
104
|
+
return revision === '(initial)' ? {revision: null, unborn: true} : {revision, unborn: false};
|
|
105
|
+
}
|
|
106
|
+
|
|
88
107
|
/** Observe an existing workspace without initializing or refreshing it. */
|
|
89
108
|
export async function repositoryConfiguration({workspaceRoot = process.cwd(), signal, onEvent, run = runProcess} = {}) {
|
|
90
109
|
const repositoryRoot = path.resolve(workspaceRoot);
|
|
@@ -147,7 +166,12 @@ export async function repositoryConfiguration({workspaceRoot = process.cwd(), si
|
|
|
147
166
|
upstream.urls = remotes.get(`remote.${remoteName}.url`) ?? [];
|
|
148
167
|
upstream.pushUrls = remotes.get(`remote.${remoteName}.pushurl`) ?? [];
|
|
149
168
|
}
|
|
150
|
-
|
|
169
|
+
const state = await readGitText([...repositoryHeadArguments], options);
|
|
170
|
+
if (state.result.code !== 0) throw processFailure(state, 'Git branch state observation failed.');
|
|
171
|
+
let headState;
|
|
172
|
+
try { headState = repositoryHeadState(state.output, state.result); }
|
|
173
|
+
catch (error) { error.rawStdout = state.rawStdout; throw error; }
|
|
174
|
+
return {repositoryRoot, headRef, ...headState, origin, upstream};
|
|
151
175
|
}
|
|
152
176
|
|
|
153
177
|
export async function repositoryStatus({
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
2
|
+
import {stat} from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {throwIfAborted} from './errors.mjs';
|
|
5
|
+
|
|
6
|
+
const is=new Is(false);
|
|
7
|
+
let installedDirectory=null;
|
|
8
|
+
|
|
9
|
+
// Git for Windows records its chosen location for third-party integrations.
|
|
10
|
+
// Read both installation scopes/views in one owned Windows process. No guessed
|
|
11
|
+
// installation paths or changes to the registry/environment are needed.
|
|
12
|
+
const READ_INSTALLATIONS=String.raw`
|
|
13
|
+
$ErrorActionPreference = 'Stop'
|
|
14
|
+
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
|
15
|
+
$installations = [System.Collections.Generic.List[string]]::new()
|
|
16
|
+
$views = @([Microsoft.Win32.RegistryView]::Registry32)
|
|
17
|
+
if ([Environment]::Is64BitOperatingSystem) {
|
|
18
|
+
$views = @([Microsoft.Win32.RegistryView]::Registry64, [Microsoft.Win32.RegistryView]::Registry32)
|
|
19
|
+
}
|
|
20
|
+
foreach ($hive in @([Microsoft.Win32.RegistryHive]::CurrentUser, [Microsoft.Win32.RegistryHive]::LocalMachine)) {
|
|
21
|
+
foreach ($view in $views) {
|
|
22
|
+
$base = [Microsoft.Win32.RegistryKey]::OpenBaseKey($hive, $view)
|
|
23
|
+
try {
|
|
24
|
+
$key = $base.OpenSubKey('Software\GitForWindows')
|
|
25
|
+
if ($null -ne $key) {
|
|
26
|
+
try {
|
|
27
|
+
$location = $key.GetValue('InstallPath')
|
|
28
|
+
if ($location -is [string] -and $location.Length -gt 0 -and -not $installations.Contains($location)) {
|
|
29
|
+
$installations.Add($location)
|
|
30
|
+
}
|
|
31
|
+
} finally { $key.Dispose() }
|
|
32
|
+
}
|
|
33
|
+
} finally { $base.Dispose() }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
ConvertTo-Json -InputObject @($installations.ToArray()) -Compress
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
function environmentKey(environment,name){
|
|
40
|
+
return Object.keys(environment).sort().find(function matchingKey(key){
|
|
41
|
+
return key.toLowerCase()===name;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function appendWindowsGitDirectory(environment,directory){
|
|
46
|
+
const keys=Object.keys(environment).filter(function pathKey(key){
|
|
47
|
+
return key.toLowerCase()==='path';
|
|
48
|
+
}).sort();
|
|
49
|
+
const selectedKey=keys[0]??'Path';
|
|
50
|
+
// Node chooses the first case-insensitive key before omitting undefined.
|
|
51
|
+
// libuv uses the parent's PATH if the child has none; null becomes 'null'.
|
|
52
|
+
const selected=environment[selectedKey];
|
|
53
|
+
const original=selected===undefined
|
|
54
|
+
?process.env[environmentKey(process.env,'path')]??'':selected;
|
|
55
|
+
const result={...environment};
|
|
56
|
+
for(const key of keys)delete result[key];
|
|
57
|
+
const originalPath=`${original}`;
|
|
58
|
+
result[selectedKey]=`${originalPath}${originalPath?';':''}"${directory}"`;
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function installedGitExists(directory){
|
|
63
|
+
try{
|
|
64
|
+
return (await stat(path.win32.join(directory,'git.exe'))).isFile();
|
|
65
|
+
}catch(error){
|
|
66
|
+
if(error.code==='ENOENT'||error.code==='ENOTDIR')return false;
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function windowsGitEnvironment(environment,{run,signal,onEvent}={}){
|
|
72
|
+
throwIfAborted(signal);
|
|
73
|
+
const retained=installedDirectory;
|
|
74
|
+
if(retained){
|
|
75
|
+
const available=await installedGitExists(retained);
|
|
76
|
+
throwIfAborted(signal);
|
|
77
|
+
if(available)return appendWindowsGitDirectory(environment,retained);
|
|
78
|
+
if(installedDirectory===retained)installedDirectory=null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const systemRoot=process.env[environmentKey(process.env,'systemroot')];
|
|
82
|
+
if(!systemRoot)return environment;
|
|
83
|
+
const powershell=path.win32.join(systemRoot,'System32','WindowsPowerShell','v1.0','powershell.exe');
|
|
84
|
+
const result=await run(powershell,['-NoProfile','-NonInteractive','-Command',READ_INSTALLATIONS],{
|
|
85
|
+
signal,onEvent,emitOutputEvents:false
|
|
86
|
+
});
|
|
87
|
+
throwIfAborted(signal);
|
|
88
|
+
const locations=JSON.parse(result.stdout);
|
|
89
|
+
if(!is.array(locations))throw new TypeError('Git installation discovery did not return a location list.');
|
|
90
|
+
for(const location of locations){
|
|
91
|
+
if(!is.string(location)||!path.win32.isAbsolute(location))continue;
|
|
92
|
+
const directory=path.win32.join(location,'cmd');
|
|
93
|
+
const available=await installedGitExists(directory);
|
|
94
|
+
throwIfAborted(signal);
|
|
95
|
+
if(available){
|
|
96
|
+
// Only successful, still-present discovery is retained. Concurrent
|
|
97
|
+
// first callers own separate lookups and cancellation independently.
|
|
98
|
+
installedDirectory=directory;
|
|
99
|
+
return appendWindowsGitDirectory(environment,directory);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return environment;
|
|
103
|
+
}
|