pinokiod 3.101.0 → 3.103.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "3.101.0",
3
+ "version": "3.103.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -83,6 +83,23 @@ class Server {
83
83
  this.kernel.version = this.version
84
84
  this.upload = multer();
85
85
  this.cf = new Cloudflare()
86
+ this.virtualEnvCache = new Map()
87
+ this.gitStatusIgnorePatterns = [
88
+ /^node_modules\//,
89
+ /^vendor\//,
90
+ /^\.venv\//,
91
+ /^venv\//,
92
+ /^\.virtualenv\//,
93
+ /^env\//,
94
+ /^__pycache__\//,
95
+ /^build\//,
96
+ /^dist\//,
97
+ /^tmp\//,
98
+ /^\.cache\//,
99
+ /^\.mypy_cache\//,
100
+ /^\.pytest_cache\//,
101
+ /^\.git\//
102
+ ]
86
103
 
87
104
  // sometimes the C:\Windows\System32 is not in PATH, need to add
88
105
  let platform = os.platform()
@@ -829,6 +846,7 @@ class Server {
829
846
  execUrl: "/api/" + name,
830
847
  git_monitor_url: `/gitcommit/HEAD/${name}`,
831
848
  git_history_url: `/info/git/HEAD/${name}`,
849
+ git_status_url: `/info/gitstatus/${name}`,
832
850
  git_push_url: `/run/scripts/git/push.json?cwd=${encodeURIComponent(this.kernel.path('api', name))}`,
833
851
  git_create_url: `/run/scripts/git/create.json?cwd=${encodeURIComponent(this.kernel.path('api', name))}`
834
852
  // rawpath,
@@ -869,6 +887,371 @@ class Server {
869
887
  let check = !!relative && !relative.startsWith('..') && !path.isAbsolute(relative);
870
888
  return check
871
889
  }
890
+ async discoverVirtualEnvDirs(dir) {
891
+ const cacheKey = path.resolve(dir)
892
+ const cached = this.virtualEnvCache.get(cacheKey)
893
+ const now = Date.now()
894
+ if (cached && cached.timestamp && (now - cached.timestamp) < 60000) {
895
+ return cached.dirs
896
+ }
897
+
898
+ const normalizePath = (p) => p.replace(/\\/g, '/').replace(/\/+/g, '/')
899
+ const ignored = new Set()
900
+ const autoExclude = new Set()
901
+ const seen = new Set()
902
+ const stack = [{ abs: dir, rel: '', depth: 0 }]
903
+ const maxDepth = 6
904
+
905
+ const shouldIgnoreRelative = (relative) => {
906
+ if (!relative) {
907
+ return false
908
+ }
909
+ const normalized = normalizePath(relative)
910
+ return this.gitStatusIgnorePatterns.some((regex) => regex.test(normalized) || regex.test(`${normalized}/`))
911
+ }
912
+
913
+ while (stack.length > 0) {
914
+ const { abs, rel, depth } = stack.pop()
915
+ const normalizedAbs = normalizePath(abs)
916
+ if (seen.has(normalizedAbs)) {
917
+ continue
918
+ }
919
+ seen.add(normalizedAbs)
920
+
921
+ let stats
922
+ try {
923
+ stats = await fs.promises.stat(abs)
924
+ } catch (err) {
925
+ continue
926
+ }
927
+ if (!stats.isDirectory()) {
928
+ continue
929
+ }
930
+
931
+ const relPath = rel ? normalizePath(rel) : ''
932
+ if (relPath && shouldIgnoreRelative(relPath)) {
933
+ ignored.add(relPath)
934
+ const relSegments = relPath.split('/')
935
+ const lastSegment = relSegments[relSegments.length - 1]
936
+ if (lastSegment && ['node_modules', '.venv', 'venv', '.virtualenv', 'env'].includes(lastSegment)) {
937
+ autoExclude.add(relPath)
938
+ }
939
+ continue
940
+ }
941
+
942
+ const entries = await fs.promises.readdir(abs, { withFileTypes: true }).catch(() => [])
943
+ let hasPyvenvCfg = false
944
+ let hasExecutables = false
945
+ let hasSitePackages = false
946
+ let hasInclude = false
947
+ let hasNestedGit = false
948
+
949
+ for (const entry of entries) {
950
+ if (entry.name === '.git') {
951
+ let treatAsGit = false
952
+ if (entry.isDirectory && entry.isDirectory()) {
953
+ treatAsGit = true
954
+ } else if (entry.isFile && entry.isFile()) {
955
+ treatAsGit = true
956
+ } else if (entry.isSymbolicLink && entry.isSymbolicLink()) {
957
+ treatAsGit = true
958
+ }
959
+ if (treatAsGit) {
960
+ hasNestedGit = true
961
+ }
962
+ }
963
+ if (entry.isFile() && entry.name === 'pyvenv.cfg') {
964
+ hasPyvenvCfg = true
965
+ }
966
+ if (!entry.isDirectory()) {
967
+ continue
968
+ }
969
+ const lower = entry.name.toLowerCase()
970
+ if (lower === 'include') {
971
+ hasInclude = true
972
+ }
973
+ if (lower === 'bin' || lower === 'scripts') {
974
+ const execEntries = await fs.promises.readdir(path.join(abs, entry.name)).catch(() => [])
975
+ if (execEntries.some((name) => /^activate(\..*)?$/i.test(name) || /^python(\d*(\.\d+)*)?(\.exe)?$/i.test(name))) {
976
+ hasExecutables = true
977
+ }
978
+ }
979
+ if (lower === 'site-packages') {
980
+ hasSitePackages = true
981
+ }
982
+ if (lower === 'lib' || lower === 'lib64') {
983
+ const libPath = path.join(abs, entry.name)
984
+ const libEntries = await fs.promises.readdir(libPath, { withFileTypes: true }).catch(() => [])
985
+ for (const libEntry of libEntries) {
986
+ if (!libEntry.isDirectory()) {
987
+ continue
988
+ }
989
+ if (/^python\d+(\.\d+)?$/i.test(libEntry.name)) {
990
+ const sitePackages = path.join(libPath, libEntry.name, 'site-packages')
991
+ try {
992
+ const siteStats = await fs.promises.stat(sitePackages)
993
+ if (siteStats.isDirectory()) {
994
+ hasSitePackages = true
995
+ break
996
+ }
997
+ } catch (err) {}
998
+ }
999
+ if (libEntry.name === 'site-packages') {
1000
+ hasSitePackages = true
1001
+ break
1002
+ }
1003
+ }
1004
+ }
1005
+ }
1006
+
1007
+ const looksLikeVenv = hasPyvenvCfg || (hasExecutables && (hasSitePackages || hasInclude))
1008
+ if (looksLikeVenv && relPath) {
1009
+ ignored.add(relPath)
1010
+ autoExclude.add(relPath)
1011
+ continue
1012
+ }
1013
+
1014
+ if (hasNestedGit && relPath) {
1015
+ ignored.add(relPath)
1016
+ autoExclude.add(relPath)
1017
+ continue
1018
+ }
1019
+
1020
+ if (depth >= maxDepth) {
1021
+ continue
1022
+ }
1023
+
1024
+ for (const entry of entries) {
1025
+ if (!entry.isDirectory()) {
1026
+ continue
1027
+ }
1028
+ const childRel = rel ? `${rel}/${entry.name}` : entry.name
1029
+ stack.push({ abs: path.join(abs, entry.name), rel: childRel, depth: depth + 1 })
1030
+ }
1031
+ }
1032
+
1033
+ this.virtualEnvCache.set(cacheKey, { dirs: ignored, timestamp: now })
1034
+ if (autoExclude.size > 0) {
1035
+ try {
1036
+ await this.syncGitInfoExclude(dir, autoExclude)
1037
+ } catch (error) {
1038
+ console.warn('syncGitInfoExclude failed', dir, error)
1039
+ }
1040
+ }
1041
+ return ignored
1042
+ }
1043
+ async syncGitInfoExclude(dir, prefixes) {
1044
+ if (!prefixes || prefixes.size === 0) {
1045
+ return
1046
+ }
1047
+
1048
+ const gitdir = path.join(dir, '.git')
1049
+ let gitStats
1050
+ try {
1051
+ gitStats = await fs.promises.stat(gitdir)
1052
+ } catch (error) {
1053
+ return
1054
+ }
1055
+ if (!gitStats.isDirectory()) {
1056
+ return
1057
+ }
1058
+
1059
+ const infoDir = path.join(gitdir, 'info')
1060
+ await fs.promises.mkdir(infoDir, { recursive: true })
1061
+
1062
+ const excludePath = path.join(infoDir, 'exclude')
1063
+ let existing = ''
1064
+ try {
1065
+ existing = await fs.promises.readFile(excludePath, 'utf8')
1066
+ } catch (error) {}
1067
+
1068
+ const markerStart = '# >>> pinokiod auto-ignore >>>'
1069
+ const markerEnd = '# <<< pinokiod auto-ignore <<<'
1070
+ const lines = existing.split(/\r?\n/)
1071
+ const preserved = []
1072
+ const managed = new Set()
1073
+ let inBlock = false
1074
+
1075
+ for (const rawLine of lines) {
1076
+ const line = rawLine.trimEnd()
1077
+ if (line === markerStart) {
1078
+ inBlock = true
1079
+ continue
1080
+ }
1081
+ if (line === markerEnd) {
1082
+ inBlock = false
1083
+ continue
1084
+ }
1085
+ if (inBlock) {
1086
+ const entry = rawLine.trim()
1087
+ if (entry && !entry.startsWith('#')) {
1088
+ managed.add(entry)
1089
+ }
1090
+ continue
1091
+ }
1092
+ preserved.push(rawLine)
1093
+ }
1094
+
1095
+ const beforeSize = managed.size
1096
+ for (const prefix of prefixes) {
1097
+ if (!prefix) {
1098
+ continue
1099
+ }
1100
+ const normalized = prefix.replace(/\\/g, '/').replace(/\/+/g, '/')
1101
+ if (!normalized || normalized === '.git' || normalized.startsWith('.git/')) {
1102
+ continue
1103
+ }
1104
+ const withSlash = normalized.endsWith('/') ? normalized : `${normalized}/`
1105
+ managed.add(withSlash)
1106
+ }
1107
+
1108
+ if (managed.size === beforeSize && existing.includes(markerStart)) {
1109
+ return
1110
+ }
1111
+
1112
+ const sortedEntries = Array.from(managed)
1113
+ .filter(Boolean)
1114
+ .sort((a, b) => a.localeCompare(b))
1115
+
1116
+ const blocks = []
1117
+ const preservedContent = preserved.join('\n').trimEnd()
1118
+ if (preservedContent) {
1119
+ blocks.push(preservedContent)
1120
+ }
1121
+ if (sortedEntries.length > 0) {
1122
+ blocks.push([markerStart, ...sortedEntries, markerEnd].join('\n'))
1123
+ }
1124
+
1125
+ const finalContent = blocks.join('\n\n')
1126
+ await fs.promises.writeFile(excludePath, finalContent ? `${finalContent}\n` : '', 'utf8')
1127
+ }
1128
+ async getRepoHeadStatus(repoRelPath) {
1129
+ const repoParam = repoRelPath || ""
1130
+ const dir = repoParam ? this.kernel.path("api", repoParam) : this.kernel.path("api")
1131
+
1132
+ if (!dir) {
1133
+ return { changes: [], git_commit_url: null }
1134
+ }
1135
+
1136
+ const normalizePath = (p) => p.replace(/\\/g, '/').replace(/\/+/g, '/')
1137
+ const ignoredPrefixes = await this.discoverVirtualEnvDirs(dir)
1138
+
1139
+ const shouldIncludePath = (relativePath) => {
1140
+ if (!relativePath) {
1141
+ return true
1142
+ }
1143
+ const normalized = normalizePath(relativePath)
1144
+ if (this.gitStatusIgnorePatterns && this.gitStatusIgnorePatterns.some((regex) => regex.test(normalized) || regex.test(`${normalized}/`))) {
1145
+ return false
1146
+ }
1147
+ for (const prefix of ignoredPrefixes) {
1148
+ if (normalized === prefix || normalized.startsWith(`${prefix}/`)) {
1149
+ return false
1150
+ }
1151
+ }
1152
+ if (normalized.includes('/site-packages/')) {
1153
+ return false
1154
+ }
1155
+ if (normalized.includes('/Scripts/')) {
1156
+ return false
1157
+ }
1158
+ if (normalized.includes('/bin/activate')) {
1159
+ return false
1160
+ }
1161
+ return true
1162
+ }
1163
+
1164
+ let statusMatrix = await git.statusMatrix({ dir, fs })
1165
+ statusMatrix = statusMatrix.filter(Boolean)
1166
+
1167
+ const changes = []
1168
+ for (const [filepath, head, workdir, stage] of statusMatrix) {
1169
+ if (!shouldIncludePath(filepath)) {
1170
+ continue
1171
+ }
1172
+ if (head === workdir && head === stage) {
1173
+ continue
1174
+ }
1175
+ const absolutePath = path.join(dir, filepath)
1176
+ let stats
1177
+ try {
1178
+ stats = await fs.promises.stat(absolutePath)
1179
+ } catch (error) {
1180
+ stats = null
1181
+ }
1182
+ if (stats && stats.isDirectory()) {
1183
+ continue
1184
+ }
1185
+
1186
+ const status = Util.classifyChange(head, workdir, stage)
1187
+ if (!status) {
1188
+ continue
1189
+ }
1190
+
1191
+ const webpath = "/asset/" + path.relative(this.kernel.homedir, absolutePath)
1192
+
1193
+ changes.push({
1194
+ ref: 'HEAD',
1195
+ webpath,
1196
+ file: normalizePath(filepath),
1197
+ path: absolutePath,
1198
+ diffpath: `/gitdiff/HEAD/${repoParam}/${normalizePath(filepath)}`,
1199
+ status,
1200
+ })
1201
+ }
1202
+
1203
+ const repoHistoryUrl = repoParam ? `/info/git/HEAD/${repoParam}` : null
1204
+
1205
+ return {
1206
+ changes,
1207
+ git_commit_url: `/run/scripts/git/commit.json?cwd=${dir}&callback_target=parent&callback=$location.href`,
1208
+ git_history_url: repoHistoryUrl,
1209
+ }
1210
+ }
1211
+ async computeWorkspaceGitStatus(workspaceName) {
1212
+ const workspacePath = this.kernel.path("api", workspaceName)
1213
+ const repos = await this.kernel.git.repos(workspacePath)
1214
+
1215
+ // await Util.ignore_subrepos(workspacePath, repos)
1216
+
1217
+ const statuses = []
1218
+ for (const repo of repos) {
1219
+ const repoParam = repo.gitParentRelPath || workspaceName
1220
+ try {
1221
+ const { changes, git_commit_url, git_history_url } = await this.getRepoHeadStatus(repoParam)
1222
+ const historyUrl = git_history_url || (repoParam ? `/info/git/HEAD/${repoParam}` : `/info/git/HEAD/${workspaceName}`)
1223
+ statuses.push({
1224
+ name: repo.name,
1225
+ main: repo.main,
1226
+ gitParentRelPath: repo.gitParentRelPath,
1227
+ repoParam,
1228
+ changeCount: changes.length,
1229
+ changes,
1230
+ git_commit_url,
1231
+ git_history_url: historyUrl,
1232
+ url: repo.url || null,
1233
+ })
1234
+ } catch (error) {
1235
+ console.error('computeWorkspaceGitStatus error', repoParam, error)
1236
+ const historyUrl = repoParam ? `/info/git/HEAD/${repoParam}` : `/info/git/HEAD/${workspaceName}`
1237
+ statuses.push({
1238
+ name: repo.name,
1239
+ main: repo.main,
1240
+ gitParentRelPath: repo.gitParentRelPath,
1241
+ repoParam,
1242
+ changeCount: 0,
1243
+ changes: [],
1244
+ git_commit_url: null,
1245
+ git_history_url: historyUrl,
1246
+ url: repo.url || null,
1247
+ error: error ? String(error.message || error) : 'unknown',
1248
+ })
1249
+ }
1250
+ }
1251
+
1252
+ const totalChanges = statuses.reduce((sum, repo) => sum + (repo.changeCount || 0), 0)
1253
+ return { totalChanges, repos: statuses }
1254
+ }
872
1255
  async render(req, res, pathComponents, meta) {
873
1256
  let base_path = req.base || this.kernel.path("api")
874
1257
  let full_filepath = path.resolve(base_path, ...pathComponents)
@@ -5417,35 +5800,8 @@ class Server {
5417
5800
  let d = Date.now()
5418
5801
  let changes = []
5419
5802
  if (req.params.ref === "HEAD") {
5420
- try {
5421
- let statusMatrix = await git.statusMatrix({ dir, fs });
5422
- statusMatrix = statusMatrix.filter(Boolean);
5423
- for (const [filepath, head, workdir, stage] of statusMatrix) {
5424
- if (head !== workdir || head !== stage) {
5425
- const fullPath = path.join(dir, filepath);
5426
- let relpath = path.relative(this.kernel.homedir, fullPath)
5427
- let webpath = "/asset/" + relpath
5428
- let rel_filepath = path.relative(this.kernel.path("api"), fullPath)
5429
-
5430
- const stats = await fs.promises.stat(fullPath)
5431
- if (stats.isDirectory()) {
5432
- continue
5433
- }
5434
-
5435
-
5436
- changes.push({
5437
- ref: req.params.ref,
5438
- webpath,
5439
- file: filepath,
5440
- path: fullPath,
5441
- diffpath: `/gitdiff/${req.params.ref}/${req.params[0]}/${filepath}`,
5442
- status: Util.classifyChange(head, workdir, stage),
5443
- });
5444
- }
5445
- }
5446
- } catch (err) {
5447
- // console.log("git status matrix error 1", err)
5448
- }
5803
+ const { changes: headChanges, git_commit_url } = await this.getRepoHeadStatus(req.params[0])
5804
+ return res.json({ git_commit_url, changes: headChanges })
5449
5805
  } else {
5450
5806
  try {
5451
5807
  let ref = req.params.ref
@@ -5591,6 +5947,15 @@ class Server {
5591
5947
  let response = await this.getGit(req.params.ref, req.params[0])
5592
5948
  res.json(response)
5593
5949
  }))
5950
+ this.app.get("/info/gitstatus/:name", ex(async (req, res) => {
5951
+ try {
5952
+ const data = await this.computeWorkspaceGitStatus(req.params.name)
5953
+ res.json(data)
5954
+ } catch (error) {
5955
+ console.error('[git-status] compute error', req.params.name, error)
5956
+ res.status(500).json({ totalChanges: 0, repos: [], error: error ? String(error.message || error) : 'unknown' })
5957
+ }
5958
+ }))
5594
5959
  this.app.get("/git/:ref/*", ex(async (req, res) => {
5595
5960
 
5596
5961
  let { requirements, install_required, requirements_pending, error } = await this.kernel.bin.check({
@@ -5904,7 +6269,7 @@ class Server {
5904
6269
  let c = this.kernel.path("api", req.params.name)
5905
6270
  let repos = await this.kernel.git.repos(c)
5906
6271
 
5907
- await Util.ignore_subrepos(c, repos)
6272
+ // await Util.ignore_subrepos(c, repos)
5908
6273
 
5909
6274
  // check if these are in the existing .git
5910
6275
  //
@@ -6239,7 +6604,7 @@ class Server {
6239
6604
  title: name,
6240
6605
  url: gitRemote,
6241
6606
  //redirect_uri: "http://localhost:3001/apps/redirect?git=" + gitRemote,
6242
- redirect_uri: "https://app.pinokio.co/apps/redirect?git=" + gitRemote,
6607
+ redirect_uri: "https://pinokio.co/apps/redirect?git=" + gitRemote,
6243
6608
  platform: this.kernel.platform,
6244
6609
  theme: this.theme,
6245
6610
  agent: req.agent,