pinokiod 5.0.0 → 5.0.2
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/kernel/api/app/index.js +167 -0
- package/kernel/api/index.js +4 -0
- package/kernel/api/process/index.js +21 -0
- package/kernel/bin/browserless.js +15 -5
- package/kernel/bin/cli.js +13 -20
- package/kernel/bin/cuda.js +8 -2
- package/kernel/bin/index.js +24 -20
- package/kernel/bin/xcode-tools.js +29 -9
- package/kernel/environment.js +4 -1
- package/kernel/git.js +67 -1
- package/kernel/scripts/git/commit_files +31 -0
- package/kernel/scripts/git/reset_files +21 -0
- package/package.json +1 -1
- package/server/index.js +4 -61
- package/server/public/htmlmodal.js +12 -0
- package/server/views/app.ejs +561 -67
- package/server/views/index.ejs +4 -1
package/kernel/api/app/index.js
CHANGED
|
@@ -221,6 +221,26 @@ AppAPI.prototype.manualLaunchHtml = function manualLaunchHtml(appName) {
|
|
|
221
221
|
`
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
AppAPI.prototype.buildManualConfirmActions = function buildManualConfirmActions(appName) {
|
|
225
|
+
return [
|
|
226
|
+
{
|
|
227
|
+
id: 'reveal',
|
|
228
|
+
label: 'Open in Finder',
|
|
229
|
+
type: 'submit',
|
|
230
|
+
variant: 'secondary',
|
|
231
|
+
close: false
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
id: 'confirm-open',
|
|
235
|
+
label: `I've opened ${appName}`,
|
|
236
|
+
type: 'submit',
|
|
237
|
+
variant: 'primary',
|
|
238
|
+
primary: true,
|
|
239
|
+
close: false
|
|
240
|
+
}
|
|
241
|
+
]
|
|
242
|
+
}
|
|
243
|
+
|
|
224
244
|
AppAPI.prototype.handleInstallFlow = async function handleInstallFlow({ req, ondata, kernel, launcher, params }) {
|
|
225
245
|
const installUrl = params.install
|
|
226
246
|
if (!installUrl) {
|
|
@@ -380,6 +400,153 @@ AppAPI.prototype.waitForInstall = async function waitForInstall({ req, ondata, k
|
|
|
380
400
|
return null
|
|
381
401
|
}
|
|
382
402
|
|
|
403
|
+
AppAPI.prototype.waitForManualConfirmation = async function waitForManualConfirmation({ req, ondata, kernel, entry, appName, modalId }) {
|
|
404
|
+
const safeName = escapeHtml(entry.name || appName)
|
|
405
|
+
const readyId = modalId || `app-ready:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`
|
|
406
|
+
const payload = {
|
|
407
|
+
title: entry.name || appName,
|
|
408
|
+
html: this.manualLaunchHtml(entry.name || appName),
|
|
409
|
+
status: { text: `Open ${safeName} once from Finder, then confirm below.` },
|
|
410
|
+
actions: this.buildManualConfirmActions(entry.name || appName),
|
|
411
|
+
actionsAlign: 'end',
|
|
412
|
+
await: true,
|
|
413
|
+
dismissible: false
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
while (true) {
|
|
417
|
+
const response = modalId
|
|
418
|
+
? await this.htmlModal.update(this.modalRequest(req, readyId, payload), ondata, kernel)
|
|
419
|
+
: await this.htmlModal.open(this.modalRequest(req, readyId, payload), ondata, kernel)
|
|
420
|
+
if (!response) {
|
|
421
|
+
continue
|
|
422
|
+
}
|
|
423
|
+
if (response.action === 'reveal') {
|
|
424
|
+
await this.openInExplorer(entry.path, kernel)
|
|
425
|
+
payload.status = {
|
|
426
|
+
text: `Opened Finder at ${safeName}. After approving it, click "I've opened it".`
|
|
427
|
+
}
|
|
428
|
+
continue
|
|
429
|
+
}
|
|
430
|
+
if (['cancel', 'close', 'dismissed'].includes(response.action)) {
|
|
431
|
+
await this.htmlModal.close(this.modalRequest(req, readyId, {}), ondata, kernel)
|
|
432
|
+
throw new Error('Wait cancelled by user')
|
|
433
|
+
}
|
|
434
|
+
if (response.action === 'confirm-open') {
|
|
435
|
+
await this.htmlModal.update(
|
|
436
|
+
this.modalRequest(req, readyId, {
|
|
437
|
+
status: { text: `${safeName} is ready.`, variant: 'success' },
|
|
438
|
+
actions: [],
|
|
439
|
+
dismissible: true
|
|
440
|
+
}),
|
|
441
|
+
ondata,
|
|
442
|
+
kernel
|
|
443
|
+
)
|
|
444
|
+
await this.htmlModal.close(this.modalRequest(req, readyId, {}), ondata, kernel)
|
|
445
|
+
return entry
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
AppAPI.prototype.waitForAppPresence = async function waitForAppPresence(req, ondata, kernel) {
|
|
451
|
+
const params = req.params || {}
|
|
452
|
+
const appName = params.app || params.name || params.id
|
|
453
|
+
if (!appName) {
|
|
454
|
+
throw new Error('process.wait requires params.app or params.id when using app presence mode')
|
|
455
|
+
}
|
|
456
|
+
const launcher = this.ensureService(kernel)
|
|
457
|
+
const pollInterval = Number(params.installPollIntervalMs || params.installPollInterval) > 0
|
|
458
|
+
? Number(params.installPollIntervalMs || params.installPollInterval)
|
|
459
|
+
: DEFAULT_INSTALL_INTERVAL
|
|
460
|
+
const timeout = Number(params.installTimeoutMs || params.installTimeout) > 0
|
|
461
|
+
? Number(params.installTimeoutMs || params.installTimeout)
|
|
462
|
+
: DEFAULT_INSTALL_TIMEOUT
|
|
463
|
+
|
|
464
|
+
let entry = null
|
|
465
|
+
if (params.id) {
|
|
466
|
+
try {
|
|
467
|
+
entry = await launcher.info({ id: params.id, refresh: params.refresh })
|
|
468
|
+
} catch (_) {
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (!entry) {
|
|
472
|
+
const match = await launcher.findMatch(appName, { force: false })
|
|
473
|
+
if (match && match.entry) {
|
|
474
|
+
entry = match.entry
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (entry) {
|
|
479
|
+
return entry
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const installUrl = params.install
|
|
483
|
+
if (!installUrl) {
|
|
484
|
+
const error = new Error(`Application "${appName}" was not found and no install URL was provided`)
|
|
485
|
+
error.code = 'APP_NOT_FOUND'
|
|
486
|
+
throw error
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const modalId = `app-install:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`
|
|
490
|
+
const actions = this.buildInstallActions(appName, installUrl)
|
|
491
|
+
|
|
492
|
+
await this.htmlModal.open(
|
|
493
|
+
this.modalRequest(req, modalId, {
|
|
494
|
+
title: `Install ${appName}`,
|
|
495
|
+
html: this.installIntroHtml(appName, installUrl),
|
|
496
|
+
status: { text: `Waiting for ${escapeHtml(appName)} to be installed...`, waiting: true },
|
|
497
|
+
actions,
|
|
498
|
+
dismissible: false
|
|
499
|
+
}),
|
|
500
|
+
ondata,
|
|
501
|
+
kernel
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
entry = await this.waitForInstall({
|
|
505
|
+
req,
|
|
506
|
+
ondata,
|
|
507
|
+
kernel,
|
|
508
|
+
launcher,
|
|
509
|
+
appName,
|
|
510
|
+
modalId,
|
|
511
|
+
pollInterval,
|
|
512
|
+
timeout,
|
|
513
|
+
actions
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
if (!entry) {
|
|
517
|
+
await this.htmlModal.update(
|
|
518
|
+
this.modalRequest(req, modalId, {
|
|
519
|
+
status: { text: `Still cannot find ${escapeHtml(appName)}. Please complete the installation and try again.`, variant: 'error' },
|
|
520
|
+
actions: this.buildInstallActions(appName, installUrl, [{
|
|
521
|
+
id: 'close',
|
|
522
|
+
label: 'Close',
|
|
523
|
+
type: 'submit',
|
|
524
|
+
variant: 'secondary',
|
|
525
|
+
close: true
|
|
526
|
+
}]),
|
|
527
|
+
await: true
|
|
528
|
+
}),
|
|
529
|
+
ondata,
|
|
530
|
+
kernel
|
|
531
|
+
)
|
|
532
|
+
await this.htmlModal.close(this.modalRequest(req, modalId, {}), ondata, kernel)
|
|
533
|
+
throw new Error(`Timed out waiting for ${appName} to be installed`)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const needsManualLaunch = await this.requiresManualLaunch(entry, kernel)
|
|
537
|
+
await this.htmlModal.update(
|
|
538
|
+
this.modalRequest(req, modalId, {
|
|
539
|
+
status: { text: `${escapeHtml(entry.name || appName)} detected.`, variant: 'success' },
|
|
540
|
+
actions: [],
|
|
541
|
+
dismissible: true
|
|
542
|
+
}),
|
|
543
|
+
ondata,
|
|
544
|
+
kernel
|
|
545
|
+
)
|
|
546
|
+
await this.htmlModal.close(this.modalRequest(req, modalId, {}), ondata, kernel)
|
|
547
|
+
return entry
|
|
548
|
+
}
|
|
549
|
+
|
|
383
550
|
AppAPI.prototype.requiresManualLaunch = async function requiresManualLaunch(entry, kernel) {
|
|
384
551
|
if (!entry || kernel.platform !== 'darwin') {
|
|
385
552
|
return false
|
package/kernel/api/index.js
CHANGED
|
@@ -1498,6 +1498,10 @@ class Api {
|
|
|
1498
1498
|
// let keypath = path.resolve(this.kernel.homedir, "key.json")
|
|
1499
1499
|
// this.kernel.keys = (await this.loader.load(keypath)).resolved
|
|
1500
1500
|
|
|
1501
|
+
// ensure gitconfig carries required defaults before any shell commands run
|
|
1502
|
+
if (this.kernel.git && typeof this.kernel.git.ensureDefaults === "function") {
|
|
1503
|
+
await this.kernel.git.ensureDefaults()
|
|
1504
|
+
}
|
|
1501
1505
|
// init shell before running just to make sure the environment variables are fresh
|
|
1502
1506
|
await this.kernel.shell.init()
|
|
1503
1507
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const waitOn = require('wait-on');
|
|
2
2
|
const axios = require('axios');
|
|
3
|
+
const AppAPI = require('../app');
|
|
3
4
|
async function checkUrlStatus(url, message, interval, ondata) {
|
|
4
5
|
try {
|
|
5
6
|
console.log({ check: url, interval })
|
|
@@ -30,6 +31,9 @@ async function waitForUrl(url, message, interval, ondata) {
|
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
33
|
class Process {
|
|
34
|
+
constructor() {
|
|
35
|
+
this.appApi = new AppAPI()
|
|
36
|
+
}
|
|
33
37
|
// async start(req, ondata, kernel) {
|
|
34
38
|
// /*
|
|
35
39
|
// req := {
|
|
@@ -181,6 +185,19 @@ class Process {
|
|
|
181
185
|
|
|
182
186
|
or
|
|
183
187
|
|
|
188
|
+
params := {
|
|
189
|
+
app: <APP NAME OR ID>,
|
|
190
|
+
install: <INSTALL URL>,
|
|
191
|
+
installTimeoutMs: (optional) how long to wait for detection,
|
|
192
|
+
installPollIntervalMs: (optional) how often to poll for detection
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
If the app is already present, return immediately. If missing and install is
|
|
196
|
+
provided, show the install modal and poll until the app is detected, then
|
|
197
|
+
return (no launch). If missing and no install, throw.
|
|
198
|
+
|
|
199
|
+
or
|
|
200
|
+
|
|
184
201
|
|
|
185
202
|
params := {
|
|
186
203
|
on: <wait-on condition https://github.com/jeffbski/wait-on>,
|
|
@@ -197,6 +214,10 @@ class Process {
|
|
|
197
214
|
*/
|
|
198
215
|
let ms
|
|
199
216
|
if (req.params) {
|
|
217
|
+
if (req.params.app || req.params.id || req.params.name) {
|
|
218
|
+
await this.appApi.waitForAppPresence(req, ondata, kernel)
|
|
219
|
+
return
|
|
220
|
+
}
|
|
200
221
|
// Display modal
|
|
201
222
|
if (req.params.sec || req.params.min) {
|
|
202
223
|
// Wait
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
|
|
1
4
|
class Browserless {
|
|
2
5
|
description = "The headless Chrome/Chromium driver"
|
|
3
6
|
async uninstall(req, ondata) {
|
|
@@ -11,12 +14,19 @@ class Browserless {
|
|
|
11
14
|
}, ondata)
|
|
12
15
|
}
|
|
13
16
|
async installed() {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
const base = this.kernel.path("bin/npm")
|
|
18
|
+
const candidates = this.kernel.platform === "win32"
|
|
19
|
+
? ["browserless.cmd", "browserless.ps1"]
|
|
20
|
+
: ["browserless"]
|
|
21
|
+
for (const name of candidates) {
|
|
22
|
+
try {
|
|
23
|
+
await fs.promises.access(path.join(base, name), fs.constants.F_OK)
|
|
24
|
+
return true
|
|
25
|
+
} catch (_) {
|
|
26
|
+
// keep checking other candidates
|
|
27
|
+
}
|
|
19
28
|
}
|
|
29
|
+
return false
|
|
20
30
|
}
|
|
21
31
|
}
|
|
22
32
|
module.exports = Browserless
|
package/kernel/bin/cli.js
CHANGED
|
@@ -16,27 +16,20 @@ class CLI {
|
|
|
16
16
|
exists = await Util.exists(this.kernel.path("bin/npm/bin/pterm"))
|
|
17
17
|
}
|
|
18
18
|
if (exists) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
let coerced = semver.coerce(v)
|
|
29
|
-
if (semver.satisfies(coerced, this.version)) {
|
|
30
|
-
return true
|
|
31
|
-
} else {
|
|
32
|
-
return false
|
|
33
|
-
}
|
|
34
|
-
} else {
|
|
35
|
-
return false
|
|
19
|
+
try {
|
|
20
|
+
const moduleRoot = this.kernel.platform === "win32"
|
|
21
|
+
? this.kernel.path("bin/npm/node_modules")
|
|
22
|
+
: this.kernel.path("bin/npm/lib/node_modules")
|
|
23
|
+
const pkgPath = require.resolve("pterm/package.json", { paths: [moduleRoot] })
|
|
24
|
+
const { version } = require(pkgPath)
|
|
25
|
+
const coerced = semver.coerce(version)
|
|
26
|
+
if (coerced && semver.satisfies(coerced, this.version)) {
|
|
27
|
+
return true
|
|
36
28
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
29
|
+
return false
|
|
30
|
+
} catch (err) {
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
40
33
|
} else {
|
|
41
34
|
return false
|
|
42
35
|
}
|
package/kernel/bin/cuda.js
CHANGED
|
@@ -106,8 +106,14 @@ class Cuda {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
env() {
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
if (this.kernel.platform === 'win32') {
|
|
110
|
+
return {
|
|
111
|
+
CUDA_HOME: this.kernel.bin.path("miniconda/Library")
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
return {
|
|
115
|
+
CUDA_HOME: this.kernel.bin.path("miniconda")
|
|
116
|
+
}
|
|
111
117
|
}
|
|
112
118
|
}
|
|
113
119
|
// env() {
|
package/kernel/bin/index.js
CHANGED
|
@@ -433,25 +433,27 @@ class Bin {
|
|
|
433
433
|
|
|
434
434
|
let brew = []
|
|
435
435
|
if (brew_exists) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
436
|
+
const cellarPath = path.resolve(brew_path, "Cellar")
|
|
437
|
+
let usedCellar = false
|
|
438
|
+
try {
|
|
439
|
+
const entries = await fs.promises.readdir(cellarPath, { withFileTypes: true })
|
|
440
|
+
brew = entries
|
|
441
|
+
.filter((entry) => entry.isDirectory())
|
|
442
|
+
.map((entry) => entry.name)
|
|
443
|
+
usedCellar = true
|
|
444
|
+
} catch (err) {
|
|
445
|
+
console.log("[brew] cellar scan failed, falling back to brew list -1", err)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (!usedCellar) {
|
|
449
|
+
start = false
|
|
450
|
+
res = await this.exec({ message: `brew list -1`, conda: { skip: true } }, () => { })
|
|
451
|
+
lines = res.response.split(/[\r\n]+/).slice(0, -1) // ignore last line since it's the prompt
|
|
452
|
+
const parsed = lines
|
|
453
|
+
.map((raw) => raw.trim())
|
|
454
|
+
.filter((line) => line.length > 0 && !/^==>/.test(line))
|
|
455
|
+
for (const line of parsed) {
|
|
456
|
+
brew = brew.concat(line.split(/\s+/).filter(Boolean))
|
|
455
457
|
}
|
|
456
458
|
}
|
|
457
459
|
}
|
|
@@ -463,9 +465,11 @@ class Bin {
|
|
|
463
465
|
const cltStatus = await detectCommandLineTools({
|
|
464
466
|
exec: (params) => this.exec(params, () => {})
|
|
465
467
|
})
|
|
466
|
-
console.log(
|
|
468
|
+
console.log({ cltStatus })
|
|
467
469
|
this.brew_installed = e && cltStatus.valid
|
|
468
470
|
|
|
471
|
+
console.log("brew_installed", this.brew_installed)
|
|
472
|
+
|
|
469
473
|
}
|
|
470
474
|
|
|
471
475
|
if (this.platform === "win32") {
|
|
@@ -2,6 +2,11 @@ const fs = require('fs')
|
|
|
2
2
|
const semver = require('semver')
|
|
3
3
|
|
|
4
4
|
const MIN_CLT_VERSION = '13.0'
|
|
5
|
+
const PREFERRED_PKG_IDS = [
|
|
6
|
+
'com.apple.pkg.CLTools_Executables',
|
|
7
|
+
'com.apple.pkg.DeveloperToolsCLI',
|
|
8
|
+
'com.apple.pkg.Xcode'
|
|
9
|
+
]
|
|
5
10
|
const PACKAGE_MATCHERS = [
|
|
6
11
|
/^com\.apple\.pkg\.CLTools_/,
|
|
7
12
|
/^com\.apple\.pkg\.DeveloperToolsCLI$/,
|
|
@@ -104,7 +109,28 @@ async function detectCommandLineTools({ exec }) {
|
|
|
104
109
|
return status
|
|
105
110
|
}
|
|
106
111
|
|
|
112
|
+
async function pkgInfoFor(run, pkgId) {
|
|
113
|
+
try {
|
|
114
|
+
const result = await run(`pkgutil --pkg-info=${pkgId}`)
|
|
115
|
+
const stdout = result && result.stdout ? result.stdout : ''
|
|
116
|
+
const match = /version:\s*([^\n]+)/i.exec(stdout)
|
|
117
|
+
if (match) {
|
|
118
|
+
return { pkgId, version: match[1].trim() }
|
|
119
|
+
}
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.log(`[CLT] pkgutil --pkg-info ${pkgId} failed`, err)
|
|
122
|
+
}
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
125
|
+
|
|
107
126
|
async function readPkgInfo(run) {
|
|
127
|
+
for (const pkgId of PREFERRED_PKG_IDS) {
|
|
128
|
+
const info = await pkgInfoFor(run, pkgId)
|
|
129
|
+
if (info) {
|
|
130
|
+
return info
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
108
134
|
let candidates = []
|
|
109
135
|
try {
|
|
110
136
|
const listResult = await run('pkgutil --pkgs')
|
|
@@ -123,15 +149,9 @@ async function readPkgInfo(run) {
|
|
|
123
149
|
console.log('[CLT] pkg candidates:', candidates)
|
|
124
150
|
|
|
125
151
|
for (const pkgId of candidates) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const match = /version:\s*([^\n]+)/i.exec(stdout)
|
|
130
|
-
if (match) {
|
|
131
|
-
return { pkgId, version: match[1].trim() }
|
|
132
|
-
}
|
|
133
|
-
} catch (err) {
|
|
134
|
-
console.log(`[CLT] pkgutil --pkg-info ${pkgId} failed`, err)
|
|
152
|
+
const info = await pkgInfoFor(run, pkgId)
|
|
153
|
+
if (info) {
|
|
154
|
+
return info
|
|
135
155
|
}
|
|
136
156
|
}
|
|
137
157
|
return null
|
package/kernel/environment.js
CHANGED
|
@@ -628,7 +628,10 @@ const init = async (options, kernel) => {
|
|
|
628
628
|
"/CLAUDE.md",
|
|
629
629
|
"/GEMINI.md",
|
|
630
630
|
"/QWEN.md",
|
|
631
|
-
"/.geminiignore"
|
|
631
|
+
"/.geminiignore",
|
|
632
|
+
".clinerules",
|
|
633
|
+
".cursorrules",
|
|
634
|
+
".windsurfrules"
|
|
632
635
|
]
|
|
633
636
|
|
|
634
637
|
const missingEntries = entriesToEnsure.filter(entry => !existingEntries.has(entry))
|
package/kernel/git.js
CHANGED
|
@@ -29,9 +29,11 @@ class Git {
|
|
|
29
29
|
"push",
|
|
30
30
|
"create",
|
|
31
31
|
"commit",
|
|
32
|
+
"commit_files",
|
|
32
33
|
"checkout",
|
|
33
34
|
"reset_commit",
|
|
34
|
-
"reset_file"
|
|
35
|
+
"reset_file",
|
|
36
|
+
"reset_files"
|
|
35
37
|
]
|
|
36
38
|
await Promise.all(scripts.map((name) => {
|
|
37
39
|
const src = path.resolve(__dirname, `scripts/git/${name}`)
|
|
@@ -39,6 +41,70 @@ class Git {
|
|
|
39
41
|
return fs.promises.copyFile(src, dest)
|
|
40
42
|
}))
|
|
41
43
|
}
|
|
44
|
+
async ensureDefaults(homeOverride) {
|
|
45
|
+
const home = homeOverride || this.kernel.homedir
|
|
46
|
+
if (!home) return
|
|
47
|
+
|
|
48
|
+
const gitconfigPath = path.resolve(home, "gitconfig")
|
|
49
|
+
const templatePath = path.resolve(__dirname, "gitconfig_template")
|
|
50
|
+
const templateConfig = ini.parse(await fs.promises.readFile(templatePath, "utf8"))
|
|
51
|
+
const required = [
|
|
52
|
+
{ section: "init", key: "defaultBranch", value: "main" },
|
|
53
|
+
{ section: "push", key: "autoSetupRemote", value: true },
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
await fs.promises.access(gitconfigPath, fs.constants.F_OK)
|
|
58
|
+
} catch (_) {
|
|
59
|
+
await fs.promises.copyFile(templatePath, gitconfigPath)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let config
|
|
64
|
+
let dirty = false
|
|
65
|
+
try {
|
|
66
|
+
const content = await fs.promises.readFile(gitconfigPath, "utf8")
|
|
67
|
+
config = ini.parse(content)
|
|
68
|
+
} catch (_) {
|
|
69
|
+
config = {}
|
|
70
|
+
dirty = true
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const [section, tplSection] of Object.entries(templateConfig)) {
|
|
74
|
+
if (typeof tplSection !== "object" || tplSection === null) continue
|
|
75
|
+
if (!config[section]) {
|
|
76
|
+
config[section] = { ...tplSection }
|
|
77
|
+
dirty = true
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
for (const [key, value] of Object.entries(tplSection)) {
|
|
81
|
+
if (!Object.prototype.hasOwnProperty.call(config[section], key)) {
|
|
82
|
+
config[section][key] = value
|
|
83
|
+
dirty = true
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const { section, key, value } of required) {
|
|
89
|
+
if (!config[section]) {
|
|
90
|
+
config[section] = {}
|
|
91
|
+
}
|
|
92
|
+
const current = config[section][key]
|
|
93
|
+
if (String(current) !== String(value)) {
|
|
94
|
+
config[section][key] = value
|
|
95
|
+
dirty = true
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (config['credential "helperselector"']) {
|
|
100
|
+
delete config['credential "helperselector"']
|
|
101
|
+
dirty = true
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (dirty) {
|
|
105
|
+
await fs.promises.writeFile(gitconfigPath, ini.stringify(config))
|
|
106
|
+
}
|
|
107
|
+
}
|
|
42
108
|
async findGitDirs(dir, results = []) {
|
|
43
109
|
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
44
110
|
for (const entry of entries) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"run": [{
|
|
3
|
+
"when": "{{args.paths && args.paths.length > 0}}",
|
|
4
|
+
"method": "shell.run",
|
|
5
|
+
"params": {
|
|
6
|
+
"path": "{{args.cwd}}",
|
|
7
|
+
"message": [
|
|
8
|
+
"git add -- {{args.paths}}"
|
|
9
|
+
]
|
|
10
|
+
}
|
|
11
|
+
}, {
|
|
12
|
+
"when": "{{args.untracked_paths && args.untracked_paths.length > 0}}",
|
|
13
|
+
"method": "shell.run",
|
|
14
|
+
"params": {
|
|
15
|
+
"path": "{{args.cwd}}",
|
|
16
|
+
"message": [
|
|
17
|
+
"git add -- {{args.untracked_paths}}"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
}, {
|
|
21
|
+
"when": "{{(args.paths && args.paths.length > 0) || (args.untracked_paths && args.untracked_paths.length > 0)}}",
|
|
22
|
+
"method": "shell.run",
|
|
23
|
+
"params": {
|
|
24
|
+
"chain": true,
|
|
25
|
+
"path": "{{args.cwd}}",
|
|
26
|
+
"message": [
|
|
27
|
+
"git commit -m \"{{args.message}}\""
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
}]
|
|
31
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"run": [{
|
|
3
|
+
"when": "{{args.untracked_paths && args.untracked_paths.length > 0}}",
|
|
4
|
+
"method": "shell.run",
|
|
5
|
+
"params": {
|
|
6
|
+
"path": "{{args.cwd}}",
|
|
7
|
+
"message": [
|
|
8
|
+
"git clean -f -- {{args.untracked_paths}}"
|
|
9
|
+
]
|
|
10
|
+
}
|
|
11
|
+
}, {
|
|
12
|
+
"when": "{{args.tracked_paths && args.tracked_paths.length > 0}}",
|
|
13
|
+
"method": "shell.run",
|
|
14
|
+
"params": {
|
|
15
|
+
"path": "{{args.cwd}}",
|
|
16
|
+
"message": [
|
|
17
|
+
"git restore -- {{args.tracked_paths}}"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
}]
|
|
21
|
+
}
|