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.
@@ -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
@@ -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
- let browserless = this.kernel.which('browserless')
15
- if (browserless) {
16
- return true
17
- } else {
18
- return false
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
- // let p = this.kernel.which("pterm")
20
- // console.log({ exists, p})
21
- // if (p) {
22
- let res = await this.kernel.exec({
23
- message: "pterm version terminal"
24
- }, ondata)
25
- let e = /pterm@([0-9.]+)/.exec(res.stdout)
26
- if (e && e.length > 0) {
27
- let v = e[1]
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
- // } else {
38
- // return false
39
- // }
29
+ return false
30
+ } catch (err) {
31
+ return false
32
+ }
40
33
  } else {
41
34
  return false
42
35
  }
@@ -106,8 +106,14 @@ class Cuda {
106
106
  }
107
107
  }
108
108
  env() {
109
- return {
110
- CUDA_HOME: this.kernel.bin.path("miniconda")
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() {
@@ -433,25 +433,27 @@ class Bin {
433
433
 
434
434
  let brew = []
435
435
  if (brew_exists) {
436
- start = false
437
- res = await this.exec({ message: `brew list -1`, conda: { skip: true } }, (stream) => {
438
- })
439
- lines = res.response.split(/[\r\n]+/).slice(0, -1) // ignore last line since it's the prompt
440
- let end = false
441
- for(let line of lines) {
442
- if (start) {
443
- if (/^\s*$/.test(line)) {
444
- end = true
445
- } else {
446
- if (!end) {
447
- let chunks = line.split(/\s+/).filter(x => x)
448
- brew = brew.concat(chunks)
449
- }
450
- }
451
- } else {
452
- if (/==>/.test(line)) {
453
- start = true
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("BREW CHECK", { homebrew: e, cltStatus })
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
- try {
127
- const result = await run(`pkgutil --pkg-info=${pkgId}`)
128
- const stdout = result && result.stdout ? result.stdout : ''
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
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "5.0.0",
3
+ "version": "5.0.2",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {