@mnstry/atelier 0.2.0-alpha.5 → 0.2.0-alpha.6
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 +15 -0
- package/README.md +36 -19
- package/contracts/atelier-repository-observation.v1.schema.json +163 -0
- package/docs/assurance-controls.md +4 -2
- package/docs/atelier-runtime.md +13 -2
- package/docs/atelier-sync.md +171 -0
- package/docs/blocks/claims.md +18 -13
- package/docs/blocks/will-not-do.md +9 -3
- package/docs/install.md +3 -3
- package/docs/release-engineering.md +11 -2
- package/docs/upgrade.md +17 -2
- package/fixtures/atelier-repository-observation/invalid/complete-with-blocker.v1.json +18 -0
- package/fixtures/atelier-repository-observation/valid/complete-local.v1.json +48 -0
- package/package.json +5 -1
- package/src/boundary/content-rules.mjs +9 -4
- package/src/boundary/policy.mjs +23 -23
- package/src/cli/run.mjs +18 -0
- package/src/commands/sync.mjs +100 -0
- package/src/contracts/corpus.mjs +6 -0
- package/src/index.mjs +29 -0
- package/src/project/config.mjs +27 -25
- package/src/runtime/git-adapter.mjs +189 -0
- package/src/runtime/local-state.mjs +439 -0
- package/src/runtime/repository-observation.mjs +491 -0
- package/src/runtime/supervisor.mjs +788 -0
package/src/project/config.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { spawnSync } from 'node:child_process'
|
|
4
|
+
import { sanitizedGitEnvironment } from '../runtime/git-adapter.mjs'
|
|
4
5
|
|
|
5
6
|
export const PROJECT_CONFIG_ARG_PREFIX = '--project-config='
|
|
6
7
|
export const PROJECT_CONFIG_ENV = 'MNSTRY_ATELIER_PROJECT_CONFIG'
|
|
@@ -195,25 +196,25 @@ function overlayRepoPath(overlay, repoName) {
|
|
|
195
196
|
return firstString(direct, repo.path, repo.localPath)
|
|
196
197
|
}
|
|
197
198
|
|
|
198
|
-
function resolveRepoPath({ repo, repoName, configDir, workspaceRoot, overlay, cliRepoPaths }) {
|
|
199
|
+
function resolveRepoPath({ repo, repoName, configDir, workspaceRoot, overlay, cliRepoPaths, gitExecutable, env }) {
|
|
199
200
|
if (repoName && cliRepoPaths.has(repoName)) return cliRepoPaths.get(repoName)
|
|
200
201
|
const overlayPath = overlayRepoPath(overlay, repoName)
|
|
201
202
|
const fromOverlay = resolvePathValue(overlayPath, configDir)
|
|
202
203
|
if (fromOverlay) return fromOverlay
|
|
203
204
|
const fromConfig = resolvePathValue(repo.path, configDir) || resolvePathValue(repo.path, workspaceRoot)
|
|
204
205
|
if (fromConfig) return fromConfig
|
|
205
|
-
return discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot })
|
|
206
|
+
return discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot, gitExecutable, env })
|
|
206
207
|
}
|
|
207
208
|
|
|
208
|
-
function repoPathSource({ repo, repoName, overlay, cliRepoPaths, workspaceRoot, configDir }) {
|
|
209
|
+
function repoPathSource({ repo, repoName, overlay, cliRepoPaths, workspaceRoot, configDir, gitExecutable, env }) {
|
|
209
210
|
if (repoName && cliRepoPaths.has(repoName)) return 'cli'
|
|
210
211
|
if (overlayRepoPath(overlay, repoName)) return 'local-overlay'
|
|
211
212
|
if (firstString(repo.path)) return 'tracked-config'
|
|
212
|
-
if (discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot })) return 'sibling-discovery'
|
|
213
|
+
if (discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot, gitExecutable, env })) return 'sibling-discovery'
|
|
213
214
|
return null
|
|
214
215
|
}
|
|
215
216
|
|
|
216
|
-
function discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot }) {
|
|
217
|
+
function discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot, gitExecutable = 'git', env = process.env }) {
|
|
217
218
|
if (!repoName) return null
|
|
218
219
|
const candidates = [
|
|
219
220
|
path.join(workspaceRoot, repoName),
|
|
@@ -222,14 +223,14 @@ function discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot }) {
|
|
|
222
223
|
]
|
|
223
224
|
for (const candidate of candidates) {
|
|
224
225
|
if (!fs.existsSync(candidate)) continue
|
|
225
|
-
if (repo.remote && !gitRemoteMatches(candidate, repo.remote)) continue
|
|
226
|
+
if (repo.remote && !gitRemoteMatches(candidate, repo.remote, { gitExecutable, env })) continue
|
|
226
227
|
return path.resolve(candidate)
|
|
227
228
|
}
|
|
228
229
|
return null
|
|
229
230
|
}
|
|
230
231
|
|
|
231
|
-
function gitRemoteMatches(repoPath, expected) {
|
|
232
|
-
const result = spawnSync(
|
|
232
|
+
function gitRemoteMatches(repoPath, expected, { gitExecutable = 'git', env = process.env } = {}) {
|
|
233
|
+
const result = spawnSync(gitExecutable, ['-C', repoPath, 'remote', 'get-url', 'origin'], { encoding: 'utf8', env: sanitizedGitEnvironment(env) })
|
|
233
234
|
if (result.status !== 0) return false
|
|
234
235
|
const actual = normalizeRemote(result.stdout.trim())
|
|
235
236
|
return actual === normalizeRemote(expected)
|
|
@@ -243,9 +244,9 @@ function normalizeRemote(value) {
|
|
|
243
244
|
.toLowerCase()
|
|
244
245
|
}
|
|
245
246
|
|
|
246
|
-
export function gitRemoteUrl(repoPath) {
|
|
247
|
+
export function gitRemoteUrl(repoPath, { gitExecutable = 'git', env = process.env } = {}) {
|
|
247
248
|
if (!repoPath || !fs.existsSync(repoPath)) return null
|
|
248
|
-
const result = spawnSync(
|
|
249
|
+
const result = spawnSync(gitExecutable, ['-C', repoPath, 'remote', 'get-url', 'origin'], { encoding: 'utf8', env: sanitizedGitEnvironment(env) })
|
|
249
250
|
return result.status === 0 ? firstString(result.stdout) : null
|
|
250
251
|
}
|
|
251
252
|
|
|
@@ -268,6 +269,7 @@ export function resolveProjectConfig({
|
|
|
268
269
|
env = process.env,
|
|
269
270
|
cwd = process.cwd(),
|
|
270
271
|
configArgPrefix = PROJECT_CONFIG_ARG_PREFIX,
|
|
272
|
+
gitExecutable = 'git',
|
|
271
273
|
configEnv = PROJECT_CONFIG_ENV,
|
|
272
274
|
defaults = {},
|
|
273
275
|
} = {}) {
|
|
@@ -373,7 +375,7 @@ export function resolveProjectConfig({
|
|
|
373
375
|
localOverlay,
|
|
374
376
|
repos: (Array.isArray(config.repos) ? config.repos : []).map((repo) => {
|
|
375
377
|
const repoName = firstString(repo.name) || (repo.path ? path.basename(repo.path) : null)
|
|
376
|
-
const repoPath = resolveRepoPath({ repo, repoName, configDir, workspaceRoot, overlay: localOverlay.overlay, cliRepoPaths })
|
|
378
|
+
const repoPath = resolveRepoPath({ repo, repoName, configDir, workspaceRoot, overlay: localOverlay.overlay, cliRepoPaths, gitExecutable, env })
|
|
377
379
|
const external = isExternalRepo(repo)
|
|
378
380
|
return {
|
|
379
381
|
...repo,
|
|
@@ -381,16 +383,16 @@ export function resolveProjectConfig({
|
|
|
381
383
|
path: repoPath,
|
|
382
384
|
external,
|
|
383
385
|
readBoundary: external ? null : firstString(repo.readBoundary) || 'team',
|
|
384
|
-
pathSource: repoPath ? repoPathSource({ repo, repoName, overlay: localOverlay.overlay, cliRepoPaths, workspaceRoot, configDir }) : null,
|
|
386
|
+
pathSource: repoPath ? repoPathSource({ repo, repoName, overlay: localOverlay.overlay, cliRepoPaths, workspaceRoot, configDir, gitExecutable, env }) : null,
|
|
385
387
|
}
|
|
386
388
|
}),
|
|
387
389
|
}
|
|
388
|
-
resolved.localState = ensureLocalState(resolved, { write: true })
|
|
390
|
+
resolved.localState = ensureLocalState(resolved, { write: true, gitExecutable, env })
|
|
389
391
|
return resolved
|
|
390
392
|
}
|
|
391
393
|
|
|
392
|
-
export function commandProject({ argv = process.argv.slice(2), env = process.env, cwd = process.cwd() } = {}) {
|
|
393
|
-
const project = resolveProjectConfig({ argv, env, cwd })
|
|
394
|
+
export function commandProject({ argv = process.argv.slice(2), env = process.env, cwd = process.cwd(), gitExecutable = 'git' } = {}) {
|
|
395
|
+
const project = resolveProjectConfig({ argv, env, cwd, gitExecutable })
|
|
394
396
|
// Fail closed at CLI entry, but only when a real config file was loaded; the
|
|
395
397
|
// defaults/no-file path resolves with an empty config that would spuriously
|
|
396
398
|
// fail document validation.
|
|
@@ -419,35 +421,35 @@ export function localStateRoot(projectOrDir) {
|
|
|
419
421
|
return path.join(configDir || process.cwd(), LOCAL_STATE_DIR)
|
|
420
422
|
}
|
|
421
423
|
|
|
422
|
-
function gitRootFor(dir) {
|
|
423
|
-
const result = spawnSync(
|
|
424
|
+
function gitRootFor(dir, { gitExecutable = 'git', env = process.env } = {}) {
|
|
425
|
+
const result = spawnSync(gitExecutable, ['-C', dir, 'rev-parse', '--show-toplevel'], { encoding: 'utf8', env: sanitizedGitEnvironment(env) })
|
|
424
426
|
return result.status === 0 ? result.stdout.trim() : null
|
|
425
427
|
}
|
|
426
428
|
|
|
427
|
-
function gitPrefixFor(dir) {
|
|
428
|
-
const result = spawnSync(
|
|
429
|
+
function gitPrefixFor(dir, { gitExecutable = 'git', env = process.env } = {}) {
|
|
430
|
+
const result = spawnSync(gitExecutable, ['-C', dir, 'rev-parse', '--show-prefix'], { encoding: 'utf8', env: sanitizedGitEnvironment(env) })
|
|
429
431
|
return result.status === 0 ? result.stdout.trim() : null
|
|
430
432
|
}
|
|
431
433
|
|
|
432
|
-
function isIgnoredByGit(gitRoot, rel) {
|
|
433
|
-
const result = spawnSync(
|
|
434
|
+
function isIgnoredByGit(gitRoot, rel, { gitExecutable = 'git', env = process.env } = {}) {
|
|
435
|
+
const result = spawnSync(gitExecutable, ['-C', gitRoot, 'check-ignore', '-q', rel], { encoding: 'utf8', env: sanitizedGitEnvironment(env) })
|
|
434
436
|
return result.status === 0
|
|
435
437
|
}
|
|
436
438
|
|
|
437
|
-
export function ensureLocalState(project, { write = false } = {}) {
|
|
439
|
+
export function ensureLocalState(project, { write = false, gitExecutable = 'git', env = process.env } = {}) {
|
|
438
440
|
const root = localStateRoot(project)
|
|
439
|
-
const gitRoot = gitRootFor(project.configDir)
|
|
441
|
+
const gitRoot = gitRootFor(project.configDir, { gitExecutable, env })
|
|
440
442
|
// Git owns the repository-relative spelling. Deriving it by comparing
|
|
441
443
|
// filesystem paths breaks when Windows exposes the cwd through an 8.3 short
|
|
442
444
|
// name (RUNNER~1) but Git reports the same root through its long name.
|
|
443
|
-
const gitPrefix = gitRoot ? gitPrefixFor(project.configDir) : null
|
|
445
|
+
const gitPrefix = gitRoot ? gitPrefixFor(project.configDir, { gitExecutable, env }) : null
|
|
444
446
|
const rel = gitRoot && gitPrefix !== null ? `${gitPrefix}${LOCAL_STATE_DIR}` : LOCAL_STATE_DIR
|
|
445
447
|
// A directory-only ignore rule such as `.atelier-local/` is not evaluated
|
|
446
448
|
// consistently by Git for an absent directory on every host. Probe a
|
|
447
449
|
// hypothetical child as well: it proves the directory rule before Atelier
|
|
448
450
|
// creates any local state, including on Git for Windows.
|
|
449
451
|
const ignored = gitRoot && gitPrefix !== null
|
|
450
|
-
? isIgnoredByGit(gitRoot, `${rel}/.atelier-ignore-probe
|
|
452
|
+
? isIgnoredByGit(gitRoot, `${rel}/.atelier-ignore-probe`, { gitExecutable, env }) || isIgnoredByGit(gitRoot, `${rel}/`, { gitExecutable, env }) || isIgnoredByGit(gitRoot, rel, { gitExecutable, env })
|
|
451
453
|
: gitRoot === null
|
|
452
454
|
const report = {
|
|
453
455
|
root,
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 30_000
|
|
6
|
+
const DEFAULT_MAX_BUFFER = 16 * 1024 * 1024
|
|
7
|
+
|
|
8
|
+
function executableNames(platform = process.platform, env = process.env) {
|
|
9
|
+
if (platform !== 'win32') return ['git']
|
|
10
|
+
const extensions = String(env.PATHEXT || '.EXE;.CMD;.BAT;.COM')
|
|
11
|
+
.split(';')
|
|
12
|
+
.map((value) => value.trim().toLowerCase())
|
|
13
|
+
.filter(Boolean)
|
|
14
|
+
return ['git.exe', ...extensions.map((extension) => `git${extension}`).filter((name) => name !== 'git.exe')]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isExecutable(file, platform = process.platform) {
|
|
18
|
+
try {
|
|
19
|
+
fs.accessSync(file, platform === 'win32' ? fs.constants.F_OK : fs.constants.X_OK)
|
|
20
|
+
return fs.statSync(file).isFile()
|
|
21
|
+
} catch {
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function canonicalExecutable(file) {
|
|
27
|
+
try {
|
|
28
|
+
return fs.realpathSync.native(file)
|
|
29
|
+
} catch {
|
|
30
|
+
return fs.realpathSync(file)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolve one exact Git executable without invoking a shell. The selected path
|
|
36
|
+
* is carried through every later operation so PATH changes cannot swap engines
|
|
37
|
+
* underneath a running supervisor.
|
|
38
|
+
*/
|
|
39
|
+
export function resolveGitExecutable({ env = process.env, platform = process.platform } = {}) {
|
|
40
|
+
const configured = String(env.ATELIER_GIT_PATH || '').trim()
|
|
41
|
+
if (configured) {
|
|
42
|
+
if (!path.isAbsolute(configured)) throw new Error('ATELIER_GIT_PATH must be an absolute path')
|
|
43
|
+
if (!isExecutable(configured, platform)) throw new Error(`configured Git executable is unavailable: ${configured}`)
|
|
44
|
+
return canonicalExecutable(configured)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const searchPath = String(env.PATH || '')
|
|
48
|
+
const delimiter = platform === 'win32' ? ';' : path.delimiter
|
|
49
|
+
const names = executableNames(platform, env)
|
|
50
|
+
for (const directory of searchPath.split(delimiter).filter(Boolean)) {
|
|
51
|
+
const cleanDir = directory.replace(/^"|"$/g, '')
|
|
52
|
+
for (const name of names) {
|
|
53
|
+
const candidate = path.join(cleanDir, name)
|
|
54
|
+
if (isExecutable(candidate, platform)) return canonicalExecutable(candidate)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
throw new Error('compatible system Git was not found on PATH')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function sanitizedGitEnvironment(env = process.env, allowPrompt = false) {
|
|
61
|
+
const next = { ...env }
|
|
62
|
+
for (const key of Object.keys(next)) {
|
|
63
|
+
if (key.toUpperCase().startsWith('GIT_')) delete next[key]
|
|
64
|
+
}
|
|
65
|
+
if (!allowPrompt) next.GIT_TERMINAL_PROMPT = '0'
|
|
66
|
+
next.GIT_OPTIONAL_LOCKS = next.GIT_OPTIONAL_LOCKS || '1'
|
|
67
|
+
next.GIT_NO_REPLACE_OBJECTS = '1'
|
|
68
|
+
return next
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function redactGitDiagnostic(value) {
|
|
72
|
+
return String(value || '')
|
|
73
|
+
.replace(/https?:\/\/[^\s]+/gi, (url) => sanitizeRemoteUrl(url))
|
|
74
|
+
.replace(/\b((?:password|passwd|token|secret|authorization|proxy-authorization|extraheader)\s*[=:]\s*)[^\s]+/gi, '$1[redacted]')
|
|
75
|
+
.replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, '$1 [redacted]')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class GitCommandError extends Error {
|
|
79
|
+
constructor(message, result) {
|
|
80
|
+
super(message)
|
|
81
|
+
this.name = 'GitCommandError'
|
|
82
|
+
this.result = result
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function runGit(gitExecutable, repoRoot, args, {
|
|
87
|
+
allowFailure = false,
|
|
88
|
+
allowPrompt = false,
|
|
89
|
+
env = process.env,
|
|
90
|
+
input = undefined,
|
|
91
|
+
timeout = DEFAULT_TIMEOUT_MS,
|
|
92
|
+
} = {}) {
|
|
93
|
+
if (!path.isAbsolute(gitExecutable)) throw new Error('Git executable must be an absolute path')
|
|
94
|
+
if (!Array.isArray(args) || args.some((value) => typeof value !== 'string')) {
|
|
95
|
+
throw new Error('Git arguments must be a string array')
|
|
96
|
+
}
|
|
97
|
+
const argv = repoRoot ? ['-C', repoRoot, ...args] : args
|
|
98
|
+
const child = spawnSync(gitExecutable, argv, {
|
|
99
|
+
encoding: 'utf8',
|
|
100
|
+
env: sanitizedGitEnvironment(env, allowPrompt),
|
|
101
|
+
input,
|
|
102
|
+
maxBuffer: DEFAULT_MAX_BUFFER,
|
|
103
|
+
shell: false,
|
|
104
|
+
timeout,
|
|
105
|
+
windowsHide: true,
|
|
106
|
+
})
|
|
107
|
+
const result = {
|
|
108
|
+
ok: !child.error && child.status === 0,
|
|
109
|
+
status: child.status,
|
|
110
|
+
signal: child.signal ?? null,
|
|
111
|
+
stdout: child.stdout || '',
|
|
112
|
+
stderr: redactGitDiagnostic(child.stderr || ''),
|
|
113
|
+
error: child.error?.message ? redactGitDiagnostic(child.error.message) : null,
|
|
114
|
+
executable: gitExecutable,
|
|
115
|
+
args: argv,
|
|
116
|
+
}
|
|
117
|
+
if (!result.ok && !allowFailure) {
|
|
118
|
+
const detail = result.error || result.stderr.trim() || `exit ${String(result.status)}`
|
|
119
|
+
throw new GitCommandError(`Git ${args[0] || 'command'} failed: ${detail}`, result)
|
|
120
|
+
}
|
|
121
|
+
return result
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function gitText(gitExecutable, repoRoot, args, options = {}) {
|
|
125
|
+
return runGit(gitExecutable, repoRoot, args, options).stdout.trim()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function parseGitVersion(text) {
|
|
129
|
+
const match = String(text || '').match(/git version (\d+)\.(\d+)\.(\d+)/i)
|
|
130
|
+
if (!match) return null
|
|
131
|
+
return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), text: match[0] }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function isSupportedGitVersion(version) {
|
|
135
|
+
return Boolean(version && (version.major > 2 || (version.major === 2 && version.minor >= 40)))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function inspectGitEngine(gitExecutable, { env = process.env } = {}) {
|
|
139
|
+
const result = runGit(gitExecutable, null, ['--version'], { env })
|
|
140
|
+
const version = parseGitVersion(result.stdout)
|
|
141
|
+
if (!version) throw new Error(`unrecognized Git version output from ${gitExecutable}`)
|
|
142
|
+
return {
|
|
143
|
+
executable: gitExecutable,
|
|
144
|
+
version: `${version.major}.${version.minor}.${version.patch}`,
|
|
145
|
+
supported: isSupportedGitVersion(version),
|
|
146
|
+
minimum: '2.40.0',
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function classifyRemoteAuthentication(url) {
|
|
151
|
+
const value = String(url || '').trim()
|
|
152
|
+
if (!value) return 'none'
|
|
153
|
+
if (/^(?:ssh:\/\/|[^/@\s]+@[^:/\s]+:)/i.test(value)) return 'ssh'
|
|
154
|
+
if (/^https?:\/\//i.test(value)) return 'https'
|
|
155
|
+
if (/^(?:file:\/\/|\.{0,2}[\\/]|[a-z]:[\\/]|[\\/])/i.test(value)) return 'local'
|
|
156
|
+
return 'unknown'
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Remote URLs are evidence, not authentication material. Git may carry HTTP
|
|
161
|
+
* credentials in user-info or query fragments, so local observations retain
|
|
162
|
+
* only the provider/repository address needed for diagnosis.
|
|
163
|
+
*/
|
|
164
|
+
export function sanitizeRemoteUrl(url) {
|
|
165
|
+
const value = String(url || '').trim()
|
|
166
|
+
const scpLike = value.match(/^[^/@\s]+@([^:/\s]+:.+)$/)
|
|
167
|
+
if (scpLike) return scpLike[1]
|
|
168
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(value)) return value
|
|
169
|
+
try {
|
|
170
|
+
const parsed = new URL(value)
|
|
171
|
+
parsed.username = ''
|
|
172
|
+
Reflect.set(parsed, 'password', '')
|
|
173
|
+
parsed.search = ''
|
|
174
|
+
parsed.hash = ''
|
|
175
|
+
return parsed.toString()
|
|
176
|
+
} catch {
|
|
177
|
+
return value.replace(/^((?:https?|ssh):\/\/)(?:[^/@:]+:)?[^/@]+@/i, '$1')
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function parseNullConfig(text) {
|
|
182
|
+
const entries = []
|
|
183
|
+
for (const record of String(text || '').split('\0').filter(Boolean)) {
|
|
184
|
+
const newline = record.indexOf('\n')
|
|
185
|
+
if (newline === -1) entries.push({ key: record, value: '' })
|
|
186
|
+
else entries.push({ key: record.slice(0, newline), value: record.slice(newline + 1) })
|
|
187
|
+
}
|
|
188
|
+
return entries
|
|
189
|
+
}
|