pinokiod 5.1.5 → 5.1.11
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/fs/download_worker.js +158 -0
- package/kernel/api/fs/index.js +95 -91
- package/kernel/api/index.js +3 -0
- package/kernel/bin/index.js +5 -2
- package/kernel/environment.js +19 -2
- package/kernel/git.js +972 -1
- package/kernel/index.js +65 -30
- package/kernel/peer.js +1 -2
- package/kernel/plugin.js +0 -8
- package/kernel/procs.js +92 -36
- package/kernel/prototype.js +45 -22
- package/kernel/shells.js +30 -6
- package/kernel/sysinfo.js +33 -13
- package/kernel/util.js +61 -24
- package/kernel/workspace_status.js +131 -7
- package/package.json +1 -1
- package/pipe/index.js +1 -1
- package/server/index.js +1173 -348
- package/server/public/create-launcher.js +157 -2
- package/server/public/install.js +135 -41
- package/server/public/style.css +32 -1
- package/server/public/tab-link-popover.js +45 -14
- package/server/public/terminal-settings.js +51 -35
- package/server/public/urldropdown.css +89 -3
- package/server/socket.js +12 -7
- package/server/views/agents.ejs +4 -3
- package/server/views/app.ejs +798 -30
- package/server/views/bootstrap.ejs +2 -1
- package/server/views/checkpoints.ejs +1014 -0
- package/server/views/checkpoints_registry_beta.ejs +260 -0
- package/server/views/columns.ejs +4 -4
- package/server/views/connect.ejs +1 -0
- package/server/views/d.ejs +74 -4
- package/server/views/download.ejs +28 -28
- package/server/views/editor.ejs +4 -5
- package/server/views/env_editor.ejs +1 -1
- package/server/views/file_explorer.ejs +1 -1
- package/server/views/index.ejs +3 -1
- package/server/views/init/index.ejs +2 -1
- package/server/views/install.ejs +2 -1
- package/server/views/net.ejs +9 -7
- package/server/views/network.ejs +15 -14
- package/server/views/pro.ejs +5 -2
- package/server/views/prototype/index.ejs +2 -1
- package/server/views/registry_link.ejs +76 -0
- package/server/views/rows.ejs +4 -4
- package/server/views/screenshots.ejs +1 -0
- package/server/views/settings.ejs +1 -0
- package/server/views/shell.ejs +4 -6
- package/server/views/terminal.ejs +528 -38
- package/server/views/tools.ejs +1 -0
- package/undefined/logs/dev/plugin/cursor-agent.git/pinokio.js/1764297248545 +0 -45
- package/undefined/logs/dev/plugin/cursor-agent.git/pinokio.js/events +0 -4
- package/undefined/logs/dev/plugin/cursor-agent.git/pinokio.js/latest +0 -45
package/server/index.js
CHANGED
|
@@ -18,7 +18,7 @@ const cors = require('cors');
|
|
|
18
18
|
const path = require("path")
|
|
19
19
|
const fs = require('fs');
|
|
20
20
|
const os = require('os')
|
|
21
|
-
const { fork,
|
|
21
|
+
const { fork, execFile } = require('child_process');
|
|
22
22
|
const semver = require('semver')
|
|
23
23
|
const fse = require('fs-extra')
|
|
24
24
|
const QRCode = require('qrcode')
|
|
@@ -68,18 +68,6 @@ const Setup = require("../kernel/bin/setup")
|
|
|
68
68
|
return (str.endsWith('\n') ? str : str + '\n').replace(/\r\n/g, '\n');
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
this.gitEnv = (repoPath) => {
|
|
72
|
-
const gitBin = this.kernel.bin && this.kernel.bin.git ? this.kernel.bin.git : null
|
|
73
|
-
if (gitBin && typeof gitBin.env === 'function') {
|
|
74
|
-
const env = gitBin.env(repoPath)
|
|
75
|
-
if (this.kernel.git && typeof this.kernel.git.clearStaleLock === "function" && repoPath) {
|
|
76
|
-
this.kernel.git.clearStaleLock(repoPath).catch(() => {})
|
|
77
|
-
}
|
|
78
|
-
return env
|
|
79
|
-
}
|
|
80
|
-
return {}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
71
|
class Server {
|
|
84
72
|
constructor(config) {
|
|
85
73
|
this.tabs = {}
|
|
@@ -103,7 +91,6 @@ class Server {
|
|
|
103
91
|
this.kernel.version = this.version
|
|
104
92
|
this.upload = multer();
|
|
105
93
|
this.cf = new Cloudflare()
|
|
106
|
-
this.virtualEnvCache = new Map()
|
|
107
94
|
this.gitStatusIgnorePatterns = [
|
|
108
95
|
/(^|\/)node_modules\//,
|
|
109
96
|
// /(^|\/)vendor\//,
|
|
@@ -121,9 +108,28 @@ class Server {
|
|
|
121
108
|
/(^|\/)\.pytest_cache\//,
|
|
122
109
|
/(^|\/)\.git\//
|
|
123
110
|
]
|
|
111
|
+
this.apiGitRefreshTimer = null
|
|
124
112
|
this.workspaceStatus = new WorkspaceStatusManager({
|
|
125
113
|
enableWatchers: process.env.PINOKIO_DISABLE_WATCH === '1' ? false : true,
|
|
126
114
|
fallbackIntervalMs: 60000,
|
|
115
|
+
onEvent: (workspaceName, events) => {
|
|
116
|
+
if (!this.kernel.homedir) return
|
|
117
|
+
if (!events || events.length === 0) return
|
|
118
|
+
const apiRoot = this.kernel.path('api')
|
|
119
|
+
const topLevel = events.some(({ path: evtPath, type }) => {
|
|
120
|
+
if (!evtPath) return false
|
|
121
|
+
const rel = path.relative(apiRoot, evtPath)
|
|
122
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return false
|
|
123
|
+
const parts = rel.split(path.sep)
|
|
124
|
+
return parts.length === 1 && (type === 'create' || type === 'delete')
|
|
125
|
+
})
|
|
126
|
+
if (topLevel) {
|
|
127
|
+
if (this.apiGitRefreshTimer) clearTimeout(this.apiGitRefreshTimer)
|
|
128
|
+
this.apiGitRefreshTimer = setTimeout(() => {
|
|
129
|
+
this.kernel.git.repos(apiRoot).catch(() => {})
|
|
130
|
+
}, 1000)
|
|
131
|
+
}
|
|
132
|
+
},
|
|
127
133
|
})
|
|
128
134
|
|
|
129
135
|
// sometimes the C:\Windows\System32 is not in PATH, need to add
|
|
@@ -857,6 +863,7 @@ class Server {
|
|
|
857
863
|
}
|
|
858
864
|
|
|
859
865
|
async chrome(req, res, type, options) {
|
|
866
|
+
console.log("Chrome")
|
|
860
867
|
|
|
861
868
|
let d = Date.now()
|
|
862
869
|
console.time("bin check")
|
|
@@ -1092,6 +1099,13 @@ class Server {
|
|
|
1092
1099
|
git_fork_url: `/run/scripts/git/fork.json?cwd=${encodeURIComponent(this.kernel.path('api', name))}`
|
|
1093
1100
|
// rawpath,
|
|
1094
1101
|
}
|
|
1102
|
+
result.hasSnapshots = !!(options && options.hasSnapshots)
|
|
1103
|
+
result.pendingSnapshotId = options && options.pendingSnapshotId ? String(options.pendingSnapshotId) : null
|
|
1104
|
+
const registryBetaEnabled = await this.isRegistryBetaEnabled().catch(() => false)
|
|
1105
|
+
result.registryBetaEnabled = registryBetaEnabled
|
|
1106
|
+
if (!registryBetaEnabled) {
|
|
1107
|
+
result.pendingSnapshotId = null
|
|
1108
|
+
}
|
|
1095
1109
|
// if (!this.kernel.proto.config) {
|
|
1096
1110
|
// await this.kernel.proto.init()
|
|
1097
1111
|
// }
|
|
@@ -1112,259 +1126,279 @@ class Server {
|
|
|
1112
1126
|
}
|
|
1113
1127
|
return { editorUrl, prevUrl }
|
|
1114
1128
|
}
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
if (
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
return shell_id
|
|
1125
|
-
}
|
|
1126
|
-
is_subpath(parent, child) {
|
|
1127
|
-
const relative = path.relative(parent, child);
|
|
1128
|
-
let check = !!relative && !relative.startsWith('..') && !path.isAbsolute(relative);
|
|
1129
|
-
return check
|
|
1129
|
+
parseRegistryBetaValue(value) {
|
|
1130
|
+
if (value === true) return true
|
|
1131
|
+
if (value === false) return false
|
|
1132
|
+
if (value == null) return null
|
|
1133
|
+
const str = String(value).trim().toLowerCase()
|
|
1134
|
+
if (!str) return null
|
|
1135
|
+
if (["1", "true", "yes", "on", "enable", "enabled"].includes(str)) return true
|
|
1136
|
+
if (["0", "false", "no", "off", "disable", "disabled"].includes(str)) return false
|
|
1137
|
+
return null
|
|
1130
1138
|
}
|
|
1131
|
-
async
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
const normalizePath = (p) => p.replace(/\\/g, '/').replace(/\/+/g, '/')
|
|
1140
|
-
const ignored = new Set()
|
|
1141
|
-
const autoExclude = new Set()
|
|
1142
|
-
const seen = new Set()
|
|
1143
|
-
const stack = [{ abs: dir, rel: '', depth: 0 }]
|
|
1144
|
-
const maxDepth = 6
|
|
1145
|
-
|
|
1146
|
-
const shouldIgnoreRelative = (relative) => {
|
|
1147
|
-
if (!relative) {
|
|
1148
|
-
return false
|
|
1139
|
+
async isRegistryBetaEnabled() {
|
|
1140
|
+
let raw = null
|
|
1141
|
+
try {
|
|
1142
|
+
const env = await Environment.get(this.kernel.homedir, this.kernel)
|
|
1143
|
+
if (env && Object.prototype.hasOwnProperty.call(env, "PINOKIO_REGISTRY_BETA")) {
|
|
1144
|
+
raw = env.PINOKIO_REGISTRY_BETA
|
|
1149
1145
|
}
|
|
1150
|
-
|
|
1151
|
-
|
|
1146
|
+
} catch (_) {}
|
|
1147
|
+
if (raw == null && process.env.PINOKIO_REGISTRY_BETA != null) {
|
|
1148
|
+
raw = process.env.PINOKIO_REGISTRY_BETA
|
|
1152
1149
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1150
|
+
return this.parseRegistryBetaValue(raw) === true
|
|
1151
|
+
}
|
|
1152
|
+
async updateEnvironmentVars(updates) {
|
|
1153
|
+
await Environment.init({}, this.kernel)
|
|
1154
|
+
const fullpath = path.resolve(this.kernel.homedir, "ENVIRONMENT")
|
|
1155
|
+
await Util.update_env(fullpath, updates)
|
|
1156
|
+
}
|
|
1157
|
+
async getRegistryConfig() {
|
|
1158
|
+
let url = null
|
|
1159
|
+
let apiKey = null
|
|
1160
|
+
try {
|
|
1161
|
+
const env = await Environment.get(this.kernel.homedir, this.kernel)
|
|
1162
|
+
if (env && env.PINOKIO_REGISTRY_URL) {
|
|
1163
|
+
const v = String(env.PINOKIO_REGISTRY_URL).trim()
|
|
1164
|
+
if (v) url = v
|
|
1167
1165
|
}
|
|
1168
|
-
if (
|
|
1169
|
-
|
|
1166
|
+
if (env && env.PINOKIO_REGISTRY_API_KEY) {
|
|
1167
|
+
const v = String(env.PINOKIO_REGISTRY_API_KEY).trim()
|
|
1168
|
+
if (v) apiKey = v
|
|
1170
1169
|
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
const
|
|
1177
|
-
if (
|
|
1178
|
-
|
|
1170
|
+
} catch (_) {}
|
|
1171
|
+
if ((url == null || apiKey == null) && this.kernel && this.kernel.store) {
|
|
1172
|
+
const registry = this.kernel.store.get('registry') || {}
|
|
1173
|
+
const updates = {}
|
|
1174
|
+
if (url == null && registry.url) {
|
|
1175
|
+
const v = String(registry.url).trim()
|
|
1176
|
+
if (v) {
|
|
1177
|
+
url = v
|
|
1178
|
+
updates.PINOKIO_REGISTRY_URL = v
|
|
1179
1179
|
}
|
|
1180
|
-
continue
|
|
1181
1180
|
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
let hasInclude = false
|
|
1188
|
-
let hasNestedGit = false
|
|
1189
|
-
|
|
1190
|
-
for (const entry of entries) {
|
|
1191
|
-
if (entry.name === '.git') {
|
|
1192
|
-
let treatAsGit = false
|
|
1193
|
-
if (entry.isDirectory && entry.isDirectory()) {
|
|
1194
|
-
treatAsGit = true
|
|
1195
|
-
} else if (entry.isFile && entry.isFile()) {
|
|
1196
|
-
treatAsGit = true
|
|
1197
|
-
} else if (entry.isSymbolicLink && entry.isSymbolicLink()) {
|
|
1198
|
-
treatAsGit = true
|
|
1199
|
-
}
|
|
1200
|
-
if (treatAsGit) {
|
|
1201
|
-
hasNestedGit = true
|
|
1202
|
-
}
|
|
1181
|
+
if (apiKey == null && registry.apiKey) {
|
|
1182
|
+
const v = String(registry.apiKey).trim()
|
|
1183
|
+
if (v) {
|
|
1184
|
+
apiKey = v
|
|
1185
|
+
updates.PINOKIO_REGISTRY_API_KEY = v
|
|
1203
1186
|
}
|
|
1204
|
-
if (entry.isFile() && entry.name === 'pyvenv.cfg') {
|
|
1205
|
-
hasPyvenvCfg = true
|
|
1206
|
-
}
|
|
1207
|
-
if (!entry.isDirectory()) {
|
|
1208
|
-
continue
|
|
1209
|
-
}
|
|
1210
|
-
const lower = entry.name.toLowerCase()
|
|
1211
|
-
if (lower === 'include') {
|
|
1212
|
-
hasInclude = true
|
|
1213
|
-
}
|
|
1214
|
-
if (lower === 'bin' || lower === 'scripts') {
|
|
1215
|
-
const execEntries = await fs.promises.readdir(path.join(abs, entry.name)).catch(() => [])
|
|
1216
|
-
if (execEntries.some((name) => /^activate(\..*)?$/i.test(name) || /^python(\d*(\.\d+)*)?(\.exe)?$/i.test(name))) {
|
|
1217
|
-
hasExecutables = true
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
if (lower === 'site-packages') {
|
|
1221
|
-
hasSitePackages = true
|
|
1222
|
-
}
|
|
1223
|
-
if (lower === 'lib' || lower === 'lib64') {
|
|
1224
|
-
const libPath = path.join(abs, entry.name)
|
|
1225
|
-
const libEntries = await fs.promises.readdir(libPath, { withFileTypes: true }).catch(() => [])
|
|
1226
|
-
for (const libEntry of libEntries) {
|
|
1227
|
-
if (!libEntry.isDirectory()) {
|
|
1228
|
-
continue
|
|
1229
|
-
}
|
|
1230
|
-
if (/^python\d+(\.\d+)?$/i.test(libEntry.name)) {
|
|
1231
|
-
const sitePackages = path.join(libPath, libEntry.name, 'site-packages')
|
|
1232
|
-
try {
|
|
1233
|
-
const siteStats = await fs.promises.stat(sitePackages)
|
|
1234
|
-
if (siteStats.isDirectory()) {
|
|
1235
|
-
hasSitePackages = true
|
|
1236
|
-
break
|
|
1237
|
-
}
|
|
1238
|
-
} catch (err) {}
|
|
1239
|
-
}
|
|
1240
|
-
if (libEntry.name === 'site-packages') {
|
|
1241
|
-
hasSitePackages = true
|
|
1242
|
-
break
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
const looksLikeVenv = hasPyvenvCfg || (hasExecutables && (hasSitePackages || hasInclude))
|
|
1249
|
-
if (looksLikeVenv && relPath) {
|
|
1250
|
-
ignored.add(relPath)
|
|
1251
|
-
autoExclude.add(relPath)
|
|
1252
|
-
continue
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
if (hasNestedGit && relPath) {
|
|
1256
|
-
ignored.add(relPath)
|
|
1257
|
-
autoExclude.add(relPath)
|
|
1258
|
-
continue
|
|
1259
1187
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
continue
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
for (const entry of entries) {
|
|
1266
|
-
if (!entry.isDirectory()) {
|
|
1267
|
-
continue
|
|
1268
|
-
}
|
|
1269
|
-
const childRel = rel ? `${rel}/${entry.name}` : entry.name
|
|
1270
|
-
stack.push({ abs: path.join(abs, entry.name), rel: childRel, depth: depth + 1 })
|
|
1188
|
+
if (Object.keys(updates).length) {
|
|
1189
|
+
await this.updateEnvironmentVars(updates).catch(() => {})
|
|
1271
1190
|
}
|
|
1272
1191
|
}
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1192
|
+
return { url, apiKey }
|
|
1193
|
+
}
|
|
1194
|
+
async getRegistryConnectUrl() {
|
|
1195
|
+
let raw = null
|
|
1196
|
+
let source = null
|
|
1197
|
+
try {
|
|
1198
|
+
const env = await Environment.get(this.kernel.homedir, this.kernel)
|
|
1199
|
+
if (env && Object.prototype.hasOwnProperty.call(env, "PINOKIO_REGISTRY_CONNECT_URL")) {
|
|
1200
|
+
raw = env.PINOKIO_REGISTRY_CONNECT_URL
|
|
1201
|
+
source = "env-url"
|
|
1202
|
+
} else if (env && Object.prototype.hasOwnProperty.call(env, "PINOKIO_REGISTRY_CONNECT_ENDPOINT")) {
|
|
1203
|
+
raw = env.PINOKIO_REGISTRY_CONNECT_ENDPOINT
|
|
1204
|
+
source = "env-endpoint"
|
|
1280
1205
|
}
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1206
|
+
} catch (_) {}
|
|
1207
|
+
if (raw == null && process.env.PINOKIO_REGISTRY_CONNECT_URL != null) {
|
|
1208
|
+
raw = process.env.PINOKIO_REGISTRY_CONNECT_URL
|
|
1209
|
+
source = "process-url"
|
|
1210
|
+
} else if (raw == null && process.env.PINOKIO_REGISTRY_CONNECT_ENDPOINT != null) {
|
|
1211
|
+
raw = process.env.PINOKIO_REGISTRY_CONNECT_ENDPOINT
|
|
1212
|
+
source = "process-endpoint"
|
|
1213
|
+
}
|
|
1214
|
+
let value = raw != null ? String(raw).trim() : ""
|
|
1215
|
+
if (!value && this.kernel && this.kernel.store) {
|
|
1216
|
+
const registry = this.kernel.store.get('registry') || {}
|
|
1217
|
+
if (registry.connectUrl) {
|
|
1218
|
+
value = String(registry.connectUrl).trim()
|
|
1219
|
+
if (value) {
|
|
1220
|
+
await this.updateEnvironmentVars({
|
|
1221
|
+
PINOKIO_REGISTRY_CONNECT_URL: value,
|
|
1222
|
+
PINOKIO_REGISTRY_CONNECT_ENDPOINT: ""
|
|
1223
|
+
}).catch(() => {})
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
if (value && source === "env-endpoint") {
|
|
1228
|
+
await this.updateEnvironmentVars({
|
|
1229
|
+
PINOKIO_REGISTRY_CONNECT_URL: value,
|
|
1230
|
+
PINOKIO_REGISTRY_CONNECT_ENDPOINT: ""
|
|
1231
|
+
}).catch(() => {})
|
|
1232
|
+
}
|
|
1233
|
+
return value ? value : null
|
|
1283
1234
|
}
|
|
1284
|
-
async
|
|
1285
|
-
if (!
|
|
1286
|
-
|
|
1235
|
+
async getRegistryViewUrl(hash) {
|
|
1236
|
+
if (!hash) return null
|
|
1237
|
+
const registry = await this.getRegistryConfig().catch(() => ({ url: null, apiKey: null }))
|
|
1238
|
+
const baseUrlRaw = registry && registry.url ? String(registry.url) : ""
|
|
1239
|
+
const baseUrl = baseUrlRaw ? baseUrlRaw.replace(/\/$/, "") : ""
|
|
1240
|
+
const connectUrl = await this.getRegistryConnectUrl().catch(() => null)
|
|
1241
|
+
let uiOrigin = null
|
|
1242
|
+
if (connectUrl) {
|
|
1243
|
+
try {
|
|
1244
|
+
uiOrigin = new URL(connectUrl).origin
|
|
1245
|
+
} catch (_) {}
|
|
1287
1246
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
gitStats = await fs.promises.stat(gitdir)
|
|
1293
|
-
} catch (error) {
|
|
1294
|
-
return
|
|
1247
|
+
if (!uiOrigin && baseUrlRaw) {
|
|
1248
|
+
try {
|
|
1249
|
+
uiOrigin = new URL(baseUrlRaw).origin
|
|
1250
|
+
} catch (_) {}
|
|
1295
1251
|
}
|
|
1296
|
-
if (!
|
|
1297
|
-
|
|
1252
|
+
if (!uiOrigin || !baseUrl) return null
|
|
1253
|
+
let appSlug = null
|
|
1254
|
+
try {
|
|
1255
|
+
const res = await axios.get(`${baseUrl}/checkpoints/${encodeURIComponent(String(hash))}`, {
|
|
1256
|
+
headers: { Accept: "application/json" },
|
|
1257
|
+
timeout: 10000,
|
|
1258
|
+
})
|
|
1259
|
+
appSlug = res && res.data && res.data.app && res.data.app.slug ? String(res.data.app.slug) : null
|
|
1260
|
+
} catch (_) {}
|
|
1261
|
+
if (!appSlug) return null
|
|
1262
|
+
const origin = uiOrigin.replace(/\/$/, "")
|
|
1263
|
+
return `${origin}/apps/${encodeURIComponent(appSlug)}/checkpoints/${encodeURIComponent(String(hash))}`
|
|
1264
|
+
}
|
|
1265
|
+
async getSnapshotStatus(name) {
|
|
1266
|
+
let hasSnapshots = false
|
|
1267
|
+
let pendingSnapshotId = null
|
|
1268
|
+
const debugLogs = []
|
|
1269
|
+
const registryBetaEnabled = await this.isRegistryBetaEnabled().catch(() => false)
|
|
1270
|
+
const shouldTreatPending = (entry) => {
|
|
1271
|
+
if (!registryBetaEnabled || !entry || entry.decision) return false
|
|
1272
|
+
const status = entry.sync && entry.sync.status ? String(entry.sync.status) : "local"
|
|
1273
|
+
if (status === "published" || status === "imported") return false
|
|
1274
|
+
return true
|
|
1298
1275
|
}
|
|
1299
|
-
|
|
1300
|
-
const infoDir = path.join(gitdir, 'info')
|
|
1301
|
-
await fs.promises.mkdir(infoDir, { recursive: true })
|
|
1302
|
-
|
|
1303
|
-
const excludePath = path.join(infoDir, 'exclude')
|
|
1304
|
-
let existing = ''
|
|
1305
1276
|
try {
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
inBlock = false
|
|
1324
|
-
continue
|
|
1325
|
-
}
|
|
1326
|
-
if (inBlock) {
|
|
1327
|
-
const entry = rawLine.trim()
|
|
1328
|
-
if (entry && !entry.startsWith('#')) {
|
|
1329
|
-
managed.add(entry)
|
|
1277
|
+
const apiRoot = this.kernel.path("api", name)
|
|
1278
|
+
let remoteKey = null
|
|
1279
|
+
let remoteEntry = null
|
|
1280
|
+
try {
|
|
1281
|
+
const mainRemote = await git.getConfig({
|
|
1282
|
+
fs,
|
|
1283
|
+
http,
|
|
1284
|
+
dir: apiRoot,
|
|
1285
|
+
path: 'remote.origin.url'
|
|
1286
|
+
})
|
|
1287
|
+
if (mainRemote) {
|
|
1288
|
+
remoteKey = this.kernel.git.normalizeRemote(mainRemote)
|
|
1289
|
+
const apps = this.kernel.git && this.kernel.git.history && this.kernel.git.history.apps ? this.kernel.git.history.apps : {}
|
|
1290
|
+
remoteEntry = remoteKey && apps[remoteKey] ? apps[remoteKey] : null
|
|
1291
|
+
debugLogs.push({ stage: "remote-derived", mainRemote, remoteKey, remoteEntryFound: !!remoteEntry })
|
|
1292
|
+
} else {
|
|
1293
|
+
debugLogs.push({ stage: "remote-derived", mainRemote: null })
|
|
1330
1294
|
}
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1295
|
+
} catch (e) {
|
|
1296
|
+
debugLogs.push({ stage: "remote-derived-error", error: e && e.message ? e.message : String(e) })
|
|
1297
|
+
}
|
|
1298
|
+
if (remoteEntry) {
|
|
1299
|
+
const activeRaw = this.kernel.git && this.kernel.git.activeSnapshot && this.kernel.git.activeSnapshot[name]
|
|
1300
|
+
const active = typeof activeRaw === "object" && activeRaw !== null ? activeRaw.id : activeRaw
|
|
1301
|
+
if (active) {
|
|
1302
|
+
hasSnapshots = true
|
|
1303
|
+
debugLogs.push({ stage: "active-snapshot", active })
|
|
1304
|
+
const activeEntry = remoteEntry && Array.isArray(remoteEntry.checkpoints)
|
|
1305
|
+
? remoteEntry.checkpoints.find((c) => c && String(c.id) === String(active))
|
|
1306
|
+
: null
|
|
1307
|
+
if (activeEntry && activeEntry.decision === "pending") {
|
|
1308
|
+
pendingSnapshotId = String(active)
|
|
1309
|
+
} else if (activeEntry && shouldTreatPending(activeEntry)) {
|
|
1310
|
+
pendingSnapshotId = String(active)
|
|
1311
|
+
await this.kernel.git.setCheckpointDecision(remoteKey, String(active), "pending").catch(() => {})
|
|
1312
|
+
}
|
|
1313
|
+
} else {
|
|
1314
|
+
const repos = await this.kernel.git.repos(apiRoot)
|
|
1315
|
+
const current = []
|
|
1316
|
+
for (const repo of repos) {
|
|
1317
|
+
if (!repo) continue
|
|
1318
|
+
const rel = repo.gitRelPath ? path.dirname(repo.gitRelPath) : null
|
|
1319
|
+
const normalizedPath = rel && rel !== "" && rel !== "." ? rel : "."
|
|
1320
|
+
const repoPath = this.kernel.path("api", name, normalizedPath === "." ? "" : normalizedPath)
|
|
1321
|
+
let commit = null
|
|
1322
|
+
try {
|
|
1323
|
+
const headFile = path.join(repoPath, ".git", "HEAD")
|
|
1324
|
+
const headRefRaw = await fs.promises.readFile(headFile, "utf8").catch(() => null)
|
|
1325
|
+
const headRef = headRefRaw ? headRefRaw.trim() : null
|
|
1326
|
+
if (headRef) {
|
|
1327
|
+
const refMatch = headRef.match(/^ref: (.+)$/)
|
|
1328
|
+
if (refMatch) {
|
|
1329
|
+
const refPath = path.join(repoPath, ".git", refMatch[1])
|
|
1330
|
+
const loose = await fs.promises.readFile(refPath, "utf8").catch(() => null)
|
|
1331
|
+
if (loose && loose.trim()) {
|
|
1332
|
+
commit = loose.trim()
|
|
1333
|
+
} else {
|
|
1334
|
+
// Try packed-refs
|
|
1335
|
+
try {
|
|
1336
|
+
const packed = await fs.promises.readFile(path.join(repoPath, ".git", "packed-refs"), "utf8")
|
|
1337
|
+
const lines = packed.split(/\r?\n/)
|
|
1338
|
+
for (const line of lines) {
|
|
1339
|
+
if (!line || line.startsWith('#') || line.startsWith('^')) continue
|
|
1340
|
+
const [sha, refname] = line.split(' ')
|
|
1341
|
+
if (refname && refname.trim() === refMatch[1]) {
|
|
1342
|
+
commit = sha.trim()
|
|
1343
|
+
break
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
} catch (_) {}
|
|
1347
|
+
}
|
|
1348
|
+
} else {
|
|
1349
|
+
commit = headRef
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
} catch (_) {}
|
|
1353
|
+
current.push({
|
|
1354
|
+
path: normalizedPath,
|
|
1355
|
+
remote: repo && repo.url ? this.kernel.git.canonicalRepoUrl(repo.url) : null,
|
|
1356
|
+
commit
|
|
1357
|
+
})
|
|
1358
|
+
}
|
|
1359
|
+
const snaps = await this.kernel.git.listSnapshotsForRemote(remoteKey)
|
|
1360
|
+
const snapNorm = snaps.map((snap) => this.kernel.git.normalizeReposArray(snap.repos || []))
|
|
1361
|
+
const currNorm = this.kernel.git.normalizeReposArray(current)
|
|
1362
|
+
const matchIndex = snapNorm.findIndex((snapRepos) => JSON.stringify(snapRepos) === JSON.stringify(currNorm))
|
|
1363
|
+
hasSnapshots = matchIndex >= 0
|
|
1364
|
+
if (hasSnapshots) {
|
|
1365
|
+
const matched = snaps[matchIndex]
|
|
1366
|
+
if (matched && matched.decision === "pending" && matched.id != null) {
|
|
1367
|
+
pendingSnapshotId = String(matched.id)
|
|
1368
|
+
} else if (matched && matched.id != null && shouldTreatPending(matched)) {
|
|
1369
|
+
pendingSnapshotId = String(matched.id)
|
|
1370
|
+
await this.kernel.git.setCheckpointDecision(remoteKey, String(matched.id), "pending").catch(() => {})
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
debugLogs.push({
|
|
1374
|
+
stage: "compare",
|
|
1375
|
+
current: currNorm,
|
|
1376
|
+
snaps: snapNorm.map((sr, idx) => ({ id: snaps[idx] && snaps[idx].id, repos: sr })),
|
|
1377
|
+
match: hasSnapshots
|
|
1378
|
+
})
|
|
1379
|
+
}
|
|
1380
|
+
} else {
|
|
1381
|
+
hasSnapshots = false
|
|
1382
|
+
debugLogs.push({ stage: "no-remote-entry" })
|
|
1344
1383
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
const blocks = []
|
|
1358
|
-
const preservedContent = preserved.join('\n').trimEnd()
|
|
1359
|
-
if (preservedContent) {
|
|
1360
|
-
blocks.push(preservedContent)
|
|
1361
|
-
}
|
|
1362
|
-
if (sortedEntries.length > 0) {
|
|
1363
|
-
blocks.push([markerStart, ...sortedEntries, markerEnd].join('\n'))
|
|
1384
|
+
} catch (_) {}
|
|
1385
|
+
return { hasSnapshots, pendingSnapshotId, debugLogs }
|
|
1386
|
+
}
|
|
1387
|
+
get_shell_id(name, i, rendered) {
|
|
1388
|
+
let shell_id
|
|
1389
|
+
if (rendered.id) {
|
|
1390
|
+
shell_id = encodeURIComponent(`${name}_${rendered.id}`)
|
|
1391
|
+
} else {
|
|
1392
|
+
let hash = crypto.createHash('md5').update(JSON.stringify(rendered)).digest('hex')
|
|
1393
|
+
//shell_id = encodeURIComponent(`${name}_${i}_session_${hash}`)
|
|
1394
|
+
shell_id = encodeURIComponent(`${name}_session_${hash}`)
|
|
1364
1395
|
}
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1396
|
+
return shell_id
|
|
1397
|
+
}
|
|
1398
|
+
is_subpath(parent, child) {
|
|
1399
|
+
const relative = path.relative(parent, child);
|
|
1400
|
+
let check = !!relative && !relative.startsWith('..') && !path.isAbsolute(relative);
|
|
1401
|
+
return check
|
|
1368
1402
|
}
|
|
1369
1403
|
async getRepoHeadStatus(repoRelPath) {
|
|
1370
1404
|
const repoParam = repoRelPath || ""
|
|
@@ -1374,122 +1408,180 @@ class Server {
|
|
|
1374
1408
|
return { changes: [], git_commit_url: null }
|
|
1375
1409
|
}
|
|
1376
1410
|
|
|
1377
|
-
|
|
1378
|
-
|
|
1411
|
+
let repo_exists = await this.exists(dir)
|
|
1412
|
+
if (!repo_exists) {
|
|
1413
|
+
return { changes: [], git_commit_url: null }
|
|
1414
|
+
}
|
|
1379
1415
|
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1416
|
+
let gitIgnoreEngine = null
|
|
1417
|
+
let workspaceName = null
|
|
1418
|
+
let workspaceRelBase = ''
|
|
1419
|
+
try {
|
|
1420
|
+
if (this.workspaceStatus && this.kernel && typeof this.kernel.path === 'function') {
|
|
1421
|
+
if (repoParam && repoParam.length > 0) {
|
|
1422
|
+
const parts = repoParam.split('/')
|
|
1423
|
+
workspaceName = parts[0]
|
|
1424
|
+
const workspaceRoot = this.kernel.path("api", workspaceName)
|
|
1425
|
+
await this.workspaceStatus.ensureGitIgnoreEngine(workspaceName, workspaceRoot)
|
|
1426
|
+
gitIgnoreEngine = this.workspaceStatus.gitIgnoreEngines && this.workspaceStatus.gitIgnoreEngines.get
|
|
1427
|
+
? this.workspaceStatus.gitIgnoreEngines.get(workspaceName) || null
|
|
1428
|
+
: null
|
|
1429
|
+
if (parts.length > 1) {
|
|
1430
|
+
workspaceRelBase = parts.slice(1).join('/')
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1391
1433
|
}
|
|
1434
|
+
} catch (_) {
|
|
1392
1435
|
}
|
|
1393
|
-
if (normalized.includes('/site-packages/')) {
|
|
1394
|
-
return false
|
|
1395
|
-
}
|
|
1396
|
-
if (normalized.includes('/Scripts/')) {
|
|
1397
|
-
return false
|
|
1398
|
-
}
|
|
1399
|
-
if (normalized.includes('/bin/activate')) {
|
|
1400
|
-
return false
|
|
1401
|
-
}
|
|
1402
|
-
return true
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
let statusMatrix = await git.statusMatrix({ dir, fs })
|
|
1406
|
-
statusMatrix = statusMatrix.filter(Boolean)
|
|
1407
1436
|
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1437
|
+
const normalizePath = (p) => p.replace(/\\/g, '/').replace(/\/+/g, '/')
|
|
1438
|
+
const shouldIncludePath = (relativePath) => {
|
|
1439
|
+
if (!relativePath) {
|
|
1440
|
+
return true
|
|
1441
|
+
}
|
|
1442
|
+
const normalized = normalizePath(relativePath)
|
|
1443
|
+
if (this.gitStatusIgnorePatterns && this.gitStatusIgnorePatterns.some((regex) => regex.test(normalized) || regex.test(`${normalized}/`))) {
|
|
1444
|
+
return false
|
|
1445
|
+
}
|
|
1446
|
+
if (normalized.includes('/site-packages/')) {
|
|
1447
|
+
return false
|
|
1448
|
+
}
|
|
1449
|
+
if (normalized.includes('/Scripts/')) {
|
|
1450
|
+
return false
|
|
1451
|
+
}
|
|
1452
|
+
if (normalized.includes('/bin/activate')) {
|
|
1453
|
+
return false
|
|
1454
|
+
}
|
|
1455
|
+
return true
|
|
1422
1456
|
}
|
|
1423
|
-
}
|
|
1424
1457
|
|
|
1425
|
-
|
|
1426
|
-
for (const [filepath, head, workdir, stage] of statusMatrix) {
|
|
1427
|
-
if (!shouldIncludePath(filepath)) {
|
|
1428
|
-
continue
|
|
1429
|
-
}
|
|
1430
|
-
if (head === workdir && head === stage) {
|
|
1431
|
-
continue
|
|
1432
|
-
}
|
|
1433
|
-
const absolutePath = path.join(dir, filepath)
|
|
1434
|
-
let stats
|
|
1458
|
+
let stdout = ""
|
|
1435
1459
|
try {
|
|
1436
|
-
|
|
1460
|
+
const env = this.kernel && this.kernel.envs
|
|
1461
|
+
? this.kernel.envs
|
|
1462
|
+
: (this.kernel && this.kernel.bin && typeof this.kernel.bin.envs === 'function'
|
|
1463
|
+
? this.kernel.bin.envs(process.env)
|
|
1464
|
+
: process.env)
|
|
1465
|
+
stdout = await new Promise((resolve) => {
|
|
1466
|
+
execFile(
|
|
1467
|
+
'git',
|
|
1468
|
+
['status', '--porcelain=v1', '--untracked-files=all'],
|
|
1469
|
+
{
|
|
1470
|
+
cwd: dir,
|
|
1471
|
+
env,
|
|
1472
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
1473
|
+
},
|
|
1474
|
+
(err, out) => {
|
|
1475
|
+
if (err) {
|
|
1476
|
+
console.warn('[git] status failed', dir, err && err.message ? err.message : err)
|
|
1477
|
+
resolve("")
|
|
1478
|
+
return
|
|
1479
|
+
}
|
|
1480
|
+
resolve(out || "")
|
|
1481
|
+
}
|
|
1482
|
+
)
|
|
1483
|
+
})
|
|
1437
1484
|
} catch (error) {
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
if (stats && stats.isDirectory()) {
|
|
1441
|
-
continue
|
|
1485
|
+
console.warn('[git] status threw', dir, error && error.message ? error.message : error)
|
|
1486
|
+
stdout = ""
|
|
1442
1487
|
}
|
|
1488
|
+
const lines = stdout.split(/\r?\n/).filter((line) => line && line.trim().length > 0)
|
|
1443
1489
|
|
|
1444
|
-
const
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1490
|
+
const changes = []
|
|
1491
|
+
for (const line of lines) {
|
|
1492
|
+
if (line.length < 3) {
|
|
1493
|
+
continue
|
|
1494
|
+
}
|
|
1495
|
+
const x = line[0]
|
|
1496
|
+
const y = line[1]
|
|
1497
|
+
if (x === '!' && y === '!') {
|
|
1498
|
+
continue
|
|
1499
|
+
}
|
|
1500
|
+
let rest = line.slice(3)
|
|
1501
|
+
let filepath
|
|
1502
|
+
const renameIdx = rest.indexOf(' -> ')
|
|
1503
|
+
if (renameIdx !== -1) {
|
|
1504
|
+
filepath = rest.slice(renameIdx + 4)
|
|
1505
|
+
} else {
|
|
1506
|
+
filepath = rest
|
|
1507
|
+
}
|
|
1508
|
+
if (!filepath) {
|
|
1509
|
+
continue
|
|
1510
|
+
}
|
|
1511
|
+
if (gitIgnoreEngine && workspaceName) {
|
|
1512
|
+
const workspaceRelative = workspaceRelBase ? `${workspaceRelBase}/${filepath}` : filepath
|
|
1513
|
+
const normalizedWorkspaceRel = normalizePath(workspaceRelative)
|
|
1514
|
+
if (gitIgnoreEngine.ignores(normalizedWorkspaceRel)) {
|
|
1455
1515
|
continue
|
|
1456
1516
|
}
|
|
1457
|
-
} catch (_) {
|
|
1458
|
-
// fall through if comparison fails
|
|
1459
1517
|
}
|
|
1460
|
-
|
|
1518
|
+
if (!shouldIncludePath(filepath)) {
|
|
1519
|
+
continue
|
|
1520
|
+
}
|
|
1521
|
+
const normalizedFile = normalizePath(filepath)
|
|
1522
|
+
const absolutePath = path.join(dir, filepath)
|
|
1523
|
+
let stats
|
|
1524
|
+
try {
|
|
1525
|
+
stats = await fs.promises.stat(absolutePath)
|
|
1526
|
+
} catch (error) {
|
|
1527
|
+
stats = null
|
|
1528
|
+
}
|
|
1529
|
+
if (stats && stats.isDirectory()) {
|
|
1530
|
+
continue
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
let status
|
|
1534
|
+
if (x === '?' && y === '?') {
|
|
1535
|
+
status = 'new (untracked)'
|
|
1536
|
+
} else if (x === 'R' || y === 'R') {
|
|
1537
|
+
status = 'renamed'
|
|
1538
|
+
} else if (x === 'C' || y === 'C') {
|
|
1539
|
+
status = 'copied'
|
|
1540
|
+
} else if (x === 'A' || y === 'A') {
|
|
1541
|
+
status = 'added (staged)'
|
|
1542
|
+
} else if (x === 'D' || y === 'D') {
|
|
1543
|
+
status = 'deleted'
|
|
1544
|
+
} else if (x === 'M' || y === 'M') {
|
|
1545
|
+
if (x === 'M' && y === 'M') {
|
|
1546
|
+
status = 'modified (staged + unstaged)'
|
|
1547
|
+
} else if (x === 'M') {
|
|
1548
|
+
status = 'modified (staged)'
|
|
1549
|
+
} else {
|
|
1550
|
+
status = 'modified (unstaged)'
|
|
1551
|
+
}
|
|
1552
|
+
} else {
|
|
1553
|
+
status = `unknown (${x}${y})`
|
|
1554
|
+
}
|
|
1461
1555
|
|
|
1462
|
-
|
|
1556
|
+
const webpath = "/asset/" + path.relative(this.kernel.homedir, absolutePath)
|
|
1463
1557
|
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1558
|
+
changes.push({
|
|
1559
|
+
ref: 'HEAD',
|
|
1560
|
+
webpath,
|
|
1561
|
+
file: normalizedFile,
|
|
1562
|
+
path: absolutePath,
|
|
1563
|
+
diffpath: `/gitdiff/HEAD/${repoParam}/${normalizedFile}`,
|
|
1564
|
+
status,
|
|
1565
|
+
})
|
|
1566
|
+
}
|
|
1473
1567
|
|
|
1474
|
-
|
|
1568
|
+
const repoHistoryUrl = repoParam ? `/info/git/HEAD/${repoParam}` : null
|
|
1475
1569
|
|
|
1476
|
-
|
|
1477
|
-
|
|
1570
|
+
const forkUrl = `/run/scripts/git/fork.json?cwd=${encodeURIComponent(dir)}`
|
|
1571
|
+
const pushUrl = `/run/scripts/git/push.json?cwd=${encodeURIComponent(dir)}`
|
|
1478
1572
|
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1573
|
+
return {
|
|
1574
|
+
changes,
|
|
1575
|
+
git_commit_url: `/run/scripts/git/commit.json?cwd=${dir}&callback_target=parent&callback=$location.href`,
|
|
1576
|
+
git_history_url: repoHistoryUrl,
|
|
1577
|
+
git_fork_url: forkUrl,
|
|
1578
|
+
git_push_url: pushUrl,
|
|
1579
|
+
}
|
|
1486
1580
|
}
|
|
1487
1581
|
async computeWorkspaceGitStatus(workspaceName) {
|
|
1488
1582
|
const workspacePath = this.kernel.path("api", workspaceName)
|
|
1489
1583
|
const repos = await this.kernel.git.repos(workspacePath)
|
|
1490
1584
|
|
|
1491
|
-
// await Util.ignore_subrepos(workspacePath, repos)
|
|
1492
|
-
|
|
1493
1585
|
const statuses = []
|
|
1494
1586
|
for (const repo of repos) {
|
|
1495
1587
|
const repoParam = repo.gitParentRelPath || workspaceName
|
|
@@ -4234,6 +4326,7 @@ class Server {
|
|
|
4234
4326
|
let str = await Environment.ENV("system", this.kernel.homedir, this.kernel)
|
|
4235
4327
|
await fs.promises.writeFile(path.resolve(this.kernel.homedir, "ENVIRONMENT"), str)
|
|
4236
4328
|
}
|
|
4329
|
+
this.workspaceStatus.ensureWatcher('api', this.kernel.path('api')).catch(() => {})
|
|
4237
4330
|
}
|
|
4238
4331
|
|
|
4239
4332
|
|
|
@@ -4517,6 +4610,634 @@ class Server {
|
|
|
4517
4610
|
list,
|
|
4518
4611
|
})
|
|
4519
4612
|
}))
|
|
4613
|
+
this.app.get("/checkpoints", ex(async (req, res) => {
|
|
4614
|
+
const registryBetaEnabled = await this.isRegistryBetaEnabled().catch(() => false)
|
|
4615
|
+
const peerAccess = await this.composePeerAccessPayload()
|
|
4616
|
+
const list = this.getPeers()
|
|
4617
|
+
const history = this.kernel.git && this.kernel.git.history ? this.kernel.git.history : { version: "1", apps: {} }
|
|
4618
|
+
|
|
4619
|
+
const normalizeSha256Digest = (raw) => {
|
|
4620
|
+
if (!raw) return null
|
|
4621
|
+
const str = String(raw).trim()
|
|
4622
|
+
if (!str) return null
|
|
4623
|
+
const m = str.match(/^sha256:([0-9a-f]{64})$/i)
|
|
4624
|
+
if (m) return `sha256:${m[1].toLowerCase()}`
|
|
4625
|
+
const m2 = str.match(/^([0-9a-f]{64})$/i)
|
|
4626
|
+
if (m2) return `sha256:${m2[1].toLowerCase()}`
|
|
4627
|
+
return null
|
|
4628
|
+
}
|
|
4629
|
+
|
|
4630
|
+
const importHash = normalizeSha256Digest(typeof req.query.hash === "string" ? req.query.hash : "")
|
|
4631
|
+
const importRegistryRaw = typeof req.query.registry === "string" ? req.query.registry.trim() : ""
|
|
4632
|
+
let autoInstall = null
|
|
4633
|
+
let importError = null
|
|
4634
|
+
if (importHash) {
|
|
4635
|
+
let registryUrl = importRegistryRaw
|
|
4636
|
+
if (!registryUrl) {
|
|
4637
|
+
try {
|
|
4638
|
+
const registry = await this.getRegistryConfig()
|
|
4639
|
+
registryUrl = registry && registry.url ? String(registry.url) : ""
|
|
4640
|
+
} catch (_) {}
|
|
4641
|
+
}
|
|
4642
|
+
registryUrl = (registryUrl || "").replace(/\/$/, "")
|
|
4643
|
+
if (!registryUrl) {
|
|
4644
|
+
importError = "Missing registry URL"
|
|
4645
|
+
} else {
|
|
4646
|
+
|
|
4647
|
+
try {
|
|
4648
|
+
const response = await axios.get(`${registryUrl}/checkpoints/${encodeURIComponent(importHash)}`, {
|
|
4649
|
+
headers: { Accept: "application/json" },
|
|
4650
|
+
timeout: 15000,
|
|
4651
|
+
})
|
|
4652
|
+
const detail = response && response.data ? response.data : null
|
|
4653
|
+
const checkpoint = detail && detail.checkpoint && typeof detail.checkpoint === "object" ? detail.checkpoint : null
|
|
4654
|
+
if (!checkpoint) throw new Error("Invalid checkpoint payload")
|
|
4655
|
+
|
|
4656
|
+
const hashed = this.kernel.git.hashCheckpoint(checkpoint)
|
|
4657
|
+
if (!hashed || !hashed.canonical || !hashed.digest) throw new Error("Invalid checkpoint payload")
|
|
4658
|
+
if (String(hashed.digest) !== importHash) throw new Error("Checkpoint hash mismatch")
|
|
4659
|
+
|
|
4660
|
+
// Persist commit metadata (subject/authorName/committedAt) returned by the registry for richer UI.
|
|
4661
|
+
const commitInfo = detail && detail.commitInfo && typeof detail.commitInfo === "object" ? detail.commitInfo : null
|
|
4662
|
+
if (commitInfo && hashed.canonical && Array.isArray(hashed.canonical.repos)) {
|
|
4663
|
+
for (const repo of hashed.canonical.repos) {
|
|
4664
|
+
if (!repo || !repo.repo || !repo.commit) continue
|
|
4665
|
+
const key = `${repo.repo}@${repo.commit}`
|
|
4666
|
+
const info = commitInfo[key]
|
|
4667
|
+
if (!info || typeof info !== "object") continue
|
|
4668
|
+
try {
|
|
4669
|
+
this.kernel.git.upsertCommitMeta(repo.repo, repo.commit, {
|
|
4670
|
+
subject: info.subject || null,
|
|
4671
|
+
authorName: info.authorName || null,
|
|
4672
|
+
committedAt: info.committedAt || null,
|
|
4673
|
+
})
|
|
4674
|
+
} catch (_) {}
|
|
4675
|
+
}
|
|
4676
|
+
}
|
|
4677
|
+
|
|
4678
|
+
const rootRemote = hashed.canonical.root
|
|
4679
|
+
const remoteEntry = this.kernel.git.ensureApp(rootRemote)
|
|
4680
|
+
if (!remoteEntry) throw new Error("Invalid checkpoint root")
|
|
4681
|
+
const { remoteKey, entry } = remoteEntry
|
|
4682
|
+
|
|
4683
|
+
let snapshotId = null
|
|
4684
|
+
if (entry && Array.isArray(entry.checkpoints)) {
|
|
4685
|
+
const existing = entry.checkpoints.find((c) => c && String(c.hash) === importHash && c.id != null)
|
|
4686
|
+
if (existing) snapshotId = String(existing.id)
|
|
4687
|
+
}
|
|
4688
|
+
|
|
4689
|
+
if (!snapshotId) {
|
|
4690
|
+
snapshotId = String(Date.now())
|
|
4691
|
+
await this.kernel.git.writeCheckpointPayload(remoteKey, rootRemote, {
|
|
4692
|
+
checkpoint: hashed.canonical,
|
|
4693
|
+
id: snapshotId,
|
|
4694
|
+
visibility: "public",
|
|
4695
|
+
system: null,
|
|
4696
|
+
})
|
|
4697
|
+
} else {
|
|
4698
|
+
const filePath = this.kernel.git.checkpointFilePath(importHash)
|
|
4699
|
+
if (filePath) {
|
|
4700
|
+
try {
|
|
4701
|
+
await fs.promises.access(filePath, fs.constants.F_OK)
|
|
4702
|
+
} catch (_) {
|
|
4703
|
+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true }).catch(() => {})
|
|
4704
|
+
await fs.promises.writeFile(filePath, JSON.stringify(hashed.canonical, null, 2))
|
|
4705
|
+
}
|
|
4706
|
+
}
|
|
4707
|
+
}
|
|
4708
|
+
|
|
4709
|
+
await this.kernel.git.setCheckpointSync(remoteKey, snapshotId, {
|
|
4710
|
+
status: "imported",
|
|
4711
|
+
at: Date.now(),
|
|
4712
|
+
source: registryUrl,
|
|
4713
|
+
hash: importHash,
|
|
4714
|
+
}).catch(() => {})
|
|
4715
|
+
autoInstall = { remoteKey, snapshotId, hash: importHash }
|
|
4716
|
+
} catch (err) {
|
|
4717
|
+
importError = err && err.message ? err.message : String(err)
|
|
4718
|
+
}
|
|
4719
|
+
}
|
|
4720
|
+
}
|
|
4721
|
+
|
|
4722
|
+
const apps = history && history.apps ? history.apps : {}
|
|
4723
|
+
const apiRoot = this.kernel.path("api")
|
|
4724
|
+
const checkpointsDir = (this.kernel && this.kernel.git && typeof this.kernel.git.checkpointsDir === "function")
|
|
4725
|
+
? this.kernel.git.checkpointsDir()
|
|
4726
|
+
: null
|
|
4727
|
+
const foldersByRemote = {}
|
|
4728
|
+
try {
|
|
4729
|
+
const entries = await fs.promises.readdir(apiRoot, { withFileTypes: true })
|
|
4730
|
+
for (const entry of entries) {
|
|
4731
|
+
if (!entry || !entry.isDirectory()) continue
|
|
4732
|
+
const folderName = entry.name
|
|
4733
|
+
const workspaceRoot = this.kernel.path("api", folderName)
|
|
4734
|
+
let remote = null
|
|
4735
|
+
try {
|
|
4736
|
+
remote = await git.getConfig({
|
|
4737
|
+
fs,
|
|
4738
|
+
http,
|
|
4739
|
+
dir: workspaceRoot,
|
|
4740
|
+
path: 'remote.origin.url'
|
|
4741
|
+
})
|
|
4742
|
+
} catch (_) {}
|
|
4743
|
+
if (!remote) continue
|
|
4744
|
+
let headHash = null
|
|
4745
|
+
try {
|
|
4746
|
+
const head = await this.kernel.git.getHead(workspaceRoot)
|
|
4747
|
+
headHash = head && head.hash ? head.hash : null
|
|
4748
|
+
} catch (_) {}
|
|
4749
|
+
const remoteKey = this.kernel.git.normalizeRemote(remote)
|
|
4750
|
+
if (!remoteKey) continue
|
|
4751
|
+
if (!foldersByRemote[remoteKey]) foldersByRemote[remoteKey] = []
|
|
4752
|
+
foldersByRemote[remoteKey].push({ name: folderName, head: headHash })
|
|
4753
|
+
}
|
|
4754
|
+
} catch (_) {}
|
|
4755
|
+
const items = []
|
|
4756
|
+
for (const [remoteKey, entry] of Object.entries(apps)) {
|
|
4757
|
+
const folders = Array.isArray(foldersByRemote[remoteKey])
|
|
4758
|
+
? foldersByRemote[remoteKey].slice().sort((a, b) => a.name.localeCompare(b.name))
|
|
4759
|
+
: []
|
|
4760
|
+
const snapshots = await this.kernel.git.listSnapshotsForRemote(remoteKey)
|
|
4761
|
+
const installedBySnapshot = {}
|
|
4762
|
+
for (const snap of snapshots) {
|
|
4763
|
+
const mainRepo = snap.repos.find((repo) => repo && repo.path === ".")
|
|
4764
|
+
if (!mainRepo || !mainRepo.commit) continue
|
|
4765
|
+
const matches = folders.filter((f) => f.head && f.head === mainRepo.commit).map((f) => f.name)
|
|
4766
|
+
if (matches.length > 0) {
|
|
4767
|
+
installedBySnapshot[snap.id] = matches
|
|
4768
|
+
}
|
|
4769
|
+
}
|
|
4770
|
+
let icon = null
|
|
4771
|
+
let title = null
|
|
4772
|
+
let description = ""
|
|
4773
|
+
if (this.kernel && this.kernel.api && typeof this.kernel.api.meta === "function") {
|
|
4774
|
+
for (const snap of snapshots) {
|
|
4775
|
+
const installedFolders = installedBySnapshot[snap.id]
|
|
4776
|
+
if (installedFolders && installedFolders.length > 0) {
|
|
4777
|
+
const folderName = installedFolders[0]
|
|
4778
|
+
try {
|
|
4779
|
+
const meta = await this.kernel.api.meta(folderName)
|
|
4780
|
+
if (meta && typeof meta === "object") {
|
|
4781
|
+
if (meta.icon) {
|
|
4782
|
+
icon = meta.icon
|
|
4783
|
+
}
|
|
4784
|
+
if (typeof meta.title === "string" && meta.title.trim()) {
|
|
4785
|
+
title = meta.title.trim()
|
|
4786
|
+
}
|
|
4787
|
+
if (typeof meta.description === "string" && meta.description.trim()) {
|
|
4788
|
+
description = meta.description.trim()
|
|
4789
|
+
}
|
|
4790
|
+
}
|
|
4791
|
+
} catch (_) {}
|
|
4792
|
+
break
|
|
4793
|
+
}
|
|
4794
|
+
}
|
|
4795
|
+
}
|
|
4796
|
+
items.push({
|
|
4797
|
+
remoteKey,
|
|
4798
|
+
remoteUrl: entry.remote || remoteKey,
|
|
4799
|
+
displayName: entry.remote || remoteKey,
|
|
4800
|
+
folders,
|
|
4801
|
+
snapshots,
|
|
4802
|
+
installedBySnapshot,
|
|
4803
|
+
icon,
|
|
4804
|
+
title,
|
|
4805
|
+
description,
|
|
4806
|
+
})
|
|
4807
|
+
}
|
|
4808
|
+
items.sort((a, b) => a.displayName.localeCompare(b.displayName))
|
|
4809
|
+
res.render("checkpoints", {
|
|
4810
|
+
current_host: this.kernel.peer.host,
|
|
4811
|
+
...peerAccess,
|
|
4812
|
+
history,
|
|
4813
|
+
items,
|
|
4814
|
+
autoInstall,
|
|
4815
|
+
importError,
|
|
4816
|
+
checkpointsDir,
|
|
4817
|
+
registryBetaEnabled,
|
|
4818
|
+
portal: this.portal,
|
|
4819
|
+
logo: this.logo,
|
|
4820
|
+
theme: this.theme,
|
|
4821
|
+
agent: req.agent,
|
|
4822
|
+
list,
|
|
4823
|
+
})
|
|
4824
|
+
}))
|
|
4825
|
+
this.app.get("/checkpoints/registry_beta", ex(async (req, res) => {
|
|
4826
|
+
const registryBetaEnabled = await this.isRegistryBetaEnabled().catch(() => false)
|
|
4827
|
+
const registryConnectUrl = await this.getRegistryConnectUrl().catch(() => null)
|
|
4828
|
+
const registryUrlPrefill = typeof req.query.registry_url === "string" ? req.query.registry_url.trim() : ""
|
|
4829
|
+
res.render("checkpoints_registry_beta", {
|
|
4830
|
+
registryBetaEnabled,
|
|
4831
|
+
registryConnectUrl,
|
|
4832
|
+
registryUrlPrefill,
|
|
4833
|
+
theme: this.theme,
|
|
4834
|
+
agent: req.agent,
|
|
4835
|
+
})
|
|
4836
|
+
}))
|
|
4837
|
+
|
|
4838
|
+
// Registry linking endpoint
|
|
4839
|
+
this.app.get("/registry/link", ex(async (req, res) => {
|
|
4840
|
+
const { token, registry, app: appOrigin, next: nextRaw } = req.query
|
|
4841
|
+
|
|
4842
|
+
if (!token || !registry) {
|
|
4843
|
+
res.status(400).render("registry_link", {
|
|
4844
|
+
ok: false,
|
|
4845
|
+
message: "Missing token or registry parameters",
|
|
4846
|
+
registryUrl: registry ? String(registry) : null,
|
|
4847
|
+
})
|
|
4848
|
+
return
|
|
4849
|
+
}
|
|
4850
|
+
|
|
4851
|
+
const registryUrl = String(registry)
|
|
4852
|
+
let connectUrl = null
|
|
4853
|
+
let redirectUrl = null
|
|
4854
|
+
try {
|
|
4855
|
+
if (appOrigin) {
|
|
4856
|
+
const u = new URL(String(appOrigin))
|
|
4857
|
+
u.pathname = "/connect/pinokio"
|
|
4858
|
+
u.search = ""
|
|
4859
|
+
u.hash = ""
|
|
4860
|
+
connectUrl = u.toString().replace(/\/$/, "")
|
|
4861
|
+
}
|
|
4862
|
+
} catch (_) {}
|
|
4863
|
+
|
|
4864
|
+
if (connectUrl) {
|
|
4865
|
+
try {
|
|
4866
|
+
const u = new URL(connectUrl)
|
|
4867
|
+
u.searchParams.set("linked", "1")
|
|
4868
|
+
redirectUrl = u.toString()
|
|
4869
|
+
} catch (_) {}
|
|
4870
|
+
}
|
|
4871
|
+
|
|
4872
|
+
if (appOrigin && nextRaw) {
|
|
4873
|
+
try {
|
|
4874
|
+
const base = new URL(String(appOrigin))
|
|
4875
|
+
const nextVal = String(nextRaw)
|
|
4876
|
+
const candidate = new URL(nextVal, base.toString())
|
|
4877
|
+
if (candidate.origin === base.origin) redirectUrl = candidate.toString()
|
|
4878
|
+
} catch (_) {}
|
|
4879
|
+
}
|
|
4880
|
+
|
|
4881
|
+
try {
|
|
4882
|
+
// Exchange token for API key at the specified registry
|
|
4883
|
+
const response = await axios.post(
|
|
4884
|
+
`${registryUrl}/registry/exchange`,
|
|
4885
|
+
{ token: String(token) },
|
|
4886
|
+
{ headers: { 'Content-Type': 'application/json' } }
|
|
4887
|
+
)
|
|
4888
|
+
|
|
4889
|
+
const { apiKey, registryUrl: confirmedUrl } = response.data
|
|
4890
|
+
|
|
4891
|
+
const envUpdates = {
|
|
4892
|
+
PINOKIO_REGISTRY_API_KEY: apiKey,
|
|
4893
|
+
PINOKIO_REGISTRY_URL: confirmedUrl || registryUrl
|
|
4894
|
+
}
|
|
4895
|
+
if (connectUrl) {
|
|
4896
|
+
envUpdates.PINOKIO_REGISTRY_CONNECT_URL = connectUrl
|
|
4897
|
+
envUpdates.PINOKIO_REGISTRY_CONNECT_ENDPOINT = ""
|
|
4898
|
+
}
|
|
4899
|
+
await this.updateEnvironmentVars(envUpdates)
|
|
4900
|
+
|
|
4901
|
+
if (redirectUrl) {
|
|
4902
|
+
res.redirect(redirectUrl)
|
|
4903
|
+
return
|
|
4904
|
+
}
|
|
4905
|
+
|
|
4906
|
+
res.render("registry_link", { ok: true, registryUrl: confirmedUrl || registryUrl })
|
|
4907
|
+
} catch (error) {
|
|
4908
|
+
console.log("Error", error)
|
|
4909
|
+
if (connectUrl) {
|
|
4910
|
+
try {
|
|
4911
|
+
const u = new URL(connectUrl)
|
|
4912
|
+
u.searchParams.set("error", "1")
|
|
4913
|
+
const msg = error && error.message ? String(error.message) : String(error)
|
|
4914
|
+
u.searchParams.set("message", msg.slice(0, 240))
|
|
4915
|
+
res.redirect(u.toString())
|
|
4916
|
+
return
|
|
4917
|
+
} catch (_) {}
|
|
4918
|
+
}
|
|
4919
|
+
res.status(500).render("registry_link", {
|
|
4920
|
+
ok: false,
|
|
4921
|
+
message: error && error.message ? error.message : String(error),
|
|
4922
|
+
registryUrl,
|
|
4923
|
+
})
|
|
4924
|
+
}
|
|
4925
|
+
}))
|
|
4926
|
+
|
|
4927
|
+
this.app.get("/api/registry/status", ex(async (req, res) => {
|
|
4928
|
+
const registry = await this.getRegistryConfig()
|
|
4929
|
+
const baseUrl = registry && registry.url ? String(registry.url).replace(/\/$/, '') : null
|
|
4930
|
+
const apiKey = registry && registry.apiKey ? String(registry.apiKey) : null
|
|
4931
|
+
res.json({ linked: !!apiKey, url: baseUrl })
|
|
4932
|
+
}))
|
|
4933
|
+
|
|
4934
|
+
this.app.post("/checkpoints/registry_beta", ex(async (req, res) => {
|
|
4935
|
+
const rawValue = Object.prototype.hasOwnProperty.call(req.body || {}, "enabled")
|
|
4936
|
+
? req.body.enabled
|
|
4937
|
+
: (Object.prototype.hasOwnProperty.call(req.query || {}, "enabled") ? req.query.enabled : null)
|
|
4938
|
+
const hasConnectField = Object.prototype.hasOwnProperty.call(req.body || {}, "connectUrl")
|
|
4939
|
+
|| Object.prototype.hasOwnProperty.call(req.body || {}, "connectEndpoint")
|
|
4940
|
+
|| Object.prototype.hasOwnProperty.call(req.body || {}, "registry_url")
|
|
4941
|
+
|| Object.prototype.hasOwnProperty.call(req.query || {}, "connectUrl")
|
|
4942
|
+
|| Object.prototype.hasOwnProperty.call(req.query || {}, "connectEndpoint")
|
|
4943
|
+
|| Object.prototype.hasOwnProperty.call(req.query || {}, "registry_url")
|
|
4944
|
+
const connectRaw = Object.prototype.hasOwnProperty.call(req.body || {}, "connectUrl")
|
|
4945
|
+
? req.body.connectUrl
|
|
4946
|
+
: (Object.prototype.hasOwnProperty.call(req.body || {}, "registry_url")
|
|
4947
|
+
? req.body.registry_url
|
|
4948
|
+
: (Object.prototype.hasOwnProperty.call(req.body || {}, "connectEndpoint")
|
|
4949
|
+
? req.body.connectEndpoint
|
|
4950
|
+
: (Object.prototype.hasOwnProperty.call(req.query || {}, "connectUrl")
|
|
4951
|
+
? req.query.connectUrl
|
|
4952
|
+
: (Object.prototype.hasOwnProperty.call(req.query || {}, "registry_url")
|
|
4953
|
+
? req.query.registry_url
|
|
4954
|
+
: (Object.prototype.hasOwnProperty.call(req.query || {}, "connectEndpoint")
|
|
4955
|
+
? req.query.connectEndpoint
|
|
4956
|
+
: null)))))
|
|
4957
|
+
const connectUrl = connectRaw != null ? String(connectRaw).trim() : ""
|
|
4958
|
+
const parsed = this.parseRegistryBetaValue(rawValue)
|
|
4959
|
+
if (parsed == null) {
|
|
4960
|
+
res.status(400).json({ ok: false, error: "Invalid enabled value" })
|
|
4961
|
+
return
|
|
4962
|
+
}
|
|
4963
|
+
const updates = {
|
|
4964
|
+
PINOKIO_REGISTRY_BETA: parsed ? "1" : ""
|
|
4965
|
+
}
|
|
4966
|
+
if (hasConnectField) {
|
|
4967
|
+
updates.PINOKIO_REGISTRY_CONNECT_URL = connectUrl
|
|
4968
|
+
updates.PINOKIO_REGISTRY_CONNECT_ENDPOINT = ""
|
|
4969
|
+
}
|
|
4970
|
+
await this.updateEnvironmentVars(updates)
|
|
4971
|
+
res.json({ ok: true, enabled: parsed, connectUrl: connectUrl || null })
|
|
4972
|
+
}))
|
|
4973
|
+
|
|
4974
|
+
this.app.post("/checkpoints/publish", ex(async (req, res) => {
|
|
4975
|
+
const registryBetaEnabled = await this.isRegistryBetaEnabled().catch(() => false)
|
|
4976
|
+
if (!registryBetaEnabled) {
|
|
4977
|
+
res.status(404).json({ ok: false, error: "Not found" })
|
|
4978
|
+
return
|
|
4979
|
+
}
|
|
4980
|
+
const snapshotRaw = typeof req.query.snapshotId === 'string' || typeof req.query.snapshotId === 'number' ? req.query.snapshotId : ''
|
|
4981
|
+
const snapshotId = snapshotRaw === 'latest' ? 'latest' : String(snapshotRaw || '')
|
|
4982
|
+
if (!snapshotId) {
|
|
4983
|
+
res.status(400).json({ ok: false, error: "Missing snapshotId" })
|
|
4984
|
+
return
|
|
4985
|
+
}
|
|
4986
|
+
const payload = await this.kernel.git.readCheckpointPayload(snapshotId)
|
|
4987
|
+
if (!payload || !payload.app || !payload.hash) {
|
|
4988
|
+
res.status(404).json({ ok: false, error: "Snapshot not found" })
|
|
4989
|
+
return
|
|
4990
|
+
}
|
|
4991
|
+
|
|
4992
|
+
const registry = await this.getRegistryConfig()
|
|
4993
|
+
const baseUrl = registry && registry.url ? String(registry.url).replace(/\/$/, '') : null
|
|
4994
|
+
const apiKey = registry && registry.apiKey ? String(registry.apiKey) : null
|
|
4995
|
+
const connectUrl = await this.getRegistryConnectUrl().catch(() => null)
|
|
4996
|
+
|
|
4997
|
+
if (!baseUrl || !apiKey) {
|
|
4998
|
+
await this.kernel.git.setCheckpointSync(payload.app, snapshotId, { status: "needs_link", at: Date.now() }).catch(() => {})
|
|
4999
|
+
res.json({ ok: true, publish: { ok: false, code: "not_linked", connectUrl } })
|
|
5000
|
+
return
|
|
5001
|
+
}
|
|
5002
|
+
|
|
5003
|
+
try {
|
|
5004
|
+
await this.kernel.git.setCheckpointSync(payload.app, snapshotId, { status: "syncing", at: Date.now() })
|
|
5005
|
+
const filePath = this.kernel.git.checkpointFilePath(payload.hash)
|
|
5006
|
+
const checkpoint = filePath ? JSON.parse(await fs.promises.readFile(filePath, "utf8")) : null
|
|
5007
|
+
const system = payload.system && typeof payload.system === "object" ? payload.system : {
|
|
5008
|
+
platform: payload.platform || null,
|
|
5009
|
+
arch: payload.arch || null,
|
|
5010
|
+
gpu: payload.gpu || null,
|
|
5011
|
+
ram: typeof payload.ram === "number" ? payload.ram : null,
|
|
5012
|
+
vram: typeof payload.vram === "number" ? payload.vram : null,
|
|
5013
|
+
}
|
|
5014
|
+
const response = await axios.post(
|
|
5015
|
+
`${baseUrl}/checkpoints`,
|
|
5016
|
+
{
|
|
5017
|
+
hash: String(payload.hash),
|
|
5018
|
+
visibility: "public",
|
|
5019
|
+
checkpoint,
|
|
5020
|
+
system,
|
|
5021
|
+
},
|
|
5022
|
+
{ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` } }
|
|
5023
|
+
)
|
|
5024
|
+
const remoteId = response && response.data
|
|
5025
|
+
? (response.data.checkpoint && response.data.checkpoint.id ? response.data.checkpoint.id : (response.data.id ? response.data.id : null))
|
|
5026
|
+
: null
|
|
5027
|
+
const viewUrl = await this.getRegistryViewUrl(payload.hash)
|
|
5028
|
+
await this.kernel.git.setCheckpointSync(payload.app, snapshotId, { status: "published", at: Date.now(), remoteId })
|
|
5029
|
+
await this.kernel.git.setCheckpointDecision(payload.app, snapshotId, "published").catch(() => {})
|
|
5030
|
+
res.json({ ok: true, publish: { ok: true, remoteId, hash: payload.hash, url: viewUrl } })
|
|
5031
|
+
} catch (error) {
|
|
5032
|
+
const status = error && error.response && error.response.status ? Number(error.response.status) : null
|
|
5033
|
+
const message = error && error.message ? error.message : String(error)
|
|
5034
|
+
if (status === 401 || status === 403) {
|
|
5035
|
+
await this.updateEnvironmentVars({ PINOKIO_REGISTRY_API_KEY: "" }).catch(() => {})
|
|
5036
|
+
await this.kernel.git.setCheckpointSync(payload.app, snapshotId, { status: "needs_link", at: Date.now(), error: message }).catch(() => {})
|
|
5037
|
+
res.json({ ok: true, publish: { ok: false, code: "not_linked", connectUrl } })
|
|
5038
|
+
return
|
|
5039
|
+
}
|
|
5040
|
+
await this.kernel.git.setCheckpointSync(payload.app, snapshotId, { status: "error", at: Date.now(), error: message }).catch(() => {})
|
|
5041
|
+
res.json({ ok: true, publish: { ok: false, code: "error", error: message } })
|
|
5042
|
+
}
|
|
5043
|
+
}))
|
|
5044
|
+
|
|
5045
|
+
this.app.post("/checkpoints/decision", ex(async (req, res) => {
|
|
5046
|
+
const registryBetaEnabled = await this.isRegistryBetaEnabled().catch(() => false)
|
|
5047
|
+
if (!registryBetaEnabled) {
|
|
5048
|
+
res.status(404).json({ ok: false, error: "Not found" })
|
|
5049
|
+
return
|
|
5050
|
+
}
|
|
5051
|
+
const snapshotRaw = typeof req.query.snapshotId === 'string' || typeof req.query.snapshotId === 'number' ? req.query.snapshotId : ''
|
|
5052
|
+
const snapshotId = snapshotRaw === 'latest' ? 'latest' : String(snapshotRaw || '')
|
|
5053
|
+
const decisionRaw = typeof req.query.decision === 'string' ? req.query.decision.trim().toLowerCase() : ''
|
|
5054
|
+
if (!snapshotId || !decisionRaw) {
|
|
5055
|
+
res.status(400).json({ ok: false, error: "Missing parameters" })
|
|
5056
|
+
return
|
|
5057
|
+
}
|
|
5058
|
+
if (decisionRaw !== "later") {
|
|
5059
|
+
res.status(400).json({ ok: false, error: "Invalid decision" })
|
|
5060
|
+
return
|
|
5061
|
+
}
|
|
5062
|
+
const payload = await this.kernel.git.readCheckpointPayload(snapshotId)
|
|
5063
|
+
if (!payload || !payload.app) {
|
|
5064
|
+
res.status(404).json({ ok: false, error: "Snapshot not found" })
|
|
5065
|
+
return
|
|
5066
|
+
}
|
|
5067
|
+
const ok = await this.kernel.git.setCheckpointDecision(payload.app, snapshotId, decisionRaw)
|
|
5068
|
+
res.json({ ok: !!ok })
|
|
5069
|
+
}))
|
|
5070
|
+
|
|
5071
|
+
this.app.post("/checkpoints/snapshot", ex(async (req, res) => {
|
|
5072
|
+
const name = typeof req.query.workspace === 'string' ? req.query.workspace.trim() : ''
|
|
5073
|
+
if (!name) {
|
|
5074
|
+
res.json({ ok: false })
|
|
5075
|
+
return
|
|
5076
|
+
}
|
|
5077
|
+
const registryBetaEnabled = await this.isRegistryBetaEnabled().catch(() => false)
|
|
5078
|
+
const publishRaw = typeof req.query.publish === 'string' ? req.query.publish.trim().toLowerCase() : ''
|
|
5079
|
+
const wantPublish = registryBetaEnabled && (publishRaw === '1' || publishRaw === 'true' || publishRaw === 'cloud')
|
|
5080
|
+
const root = this.kernel.path("api", name)
|
|
5081
|
+
const repos = await this.kernel.git.repos(root)
|
|
5082
|
+
const created = await this.kernel.git.appendWorkspaceSnapshot(name, repos)
|
|
5083
|
+
if (registryBetaEnabled && created && created.remoteKey && created.id) {
|
|
5084
|
+
const existingDecision = this.kernel.git.getCheckpointDecision(created.remoteKey, created.id)
|
|
5085
|
+
if (!existingDecision) {
|
|
5086
|
+
await this.kernel.git.setCheckpointDecision(created.remoteKey, created.id, "pending").catch(() => {})
|
|
5087
|
+
}
|
|
5088
|
+
}
|
|
5089
|
+
if (created && created.remoteKey && created.id && created.hash) {
|
|
5090
|
+
if (wantPublish) {
|
|
5091
|
+
const registry = await this.getRegistryConfig()
|
|
5092
|
+
const baseUrl = registry && registry.url ? String(registry.url).replace(/\/$/, '') : null
|
|
5093
|
+
const apiKey = registry && registry.apiKey ? String(registry.apiKey) : null
|
|
5094
|
+
const connectUrl = await this.getRegistryConnectUrl().catch(() => null)
|
|
5095
|
+
if (!baseUrl || !apiKey) {
|
|
5096
|
+
await this.kernel.git.setCheckpointSync(created.remoteKey, created.id, { status: "needs_link", at: Date.now() }).catch(() => {})
|
|
5097
|
+
res.json({ ok: true, created: created || null, publish: { ok: false, code: "not_linked", connectUrl } })
|
|
5098
|
+
return
|
|
5099
|
+
}
|
|
5100
|
+
try {
|
|
5101
|
+
await this.kernel.git.setCheckpointSync(created.remoteKey, created.id, { status: "syncing", at: Date.now() })
|
|
5102
|
+
const snapshot = await this.kernel.git.readCheckpointPayload(created.id)
|
|
5103
|
+
const filePath = this.kernel.git.checkpointFilePath(created.hash)
|
|
5104
|
+
const checkpoint = filePath ? JSON.parse(await fs.promises.readFile(filePath, "utf8")) : null
|
|
5105
|
+
const system = snapshot && snapshot.system && typeof snapshot.system === "object" ? snapshot.system : null
|
|
5106
|
+
const response = await axios.post(
|
|
5107
|
+
`${baseUrl}/checkpoints`,
|
|
5108
|
+
{
|
|
5109
|
+
hash: String(created.hash),
|
|
5110
|
+
visibility: "public",
|
|
5111
|
+
checkpoint,
|
|
5112
|
+
system,
|
|
5113
|
+
},
|
|
5114
|
+
{ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` } }
|
|
5115
|
+
)
|
|
5116
|
+
const remoteId = response && response.data
|
|
5117
|
+
? (response.data.checkpoint && response.data.checkpoint.id ? response.data.checkpoint.id : (response.data.id ? response.data.id : null))
|
|
5118
|
+
: null
|
|
5119
|
+
const viewUrl = await this.getRegistryViewUrl(created.hash)
|
|
5120
|
+
await this.kernel.git.setCheckpointSync(created.remoteKey, created.id, { status: "published", at: Date.now(), remoteId })
|
|
5121
|
+
await this.kernel.git.setCheckpointDecision(created.remoteKey, created.id, "published").catch(() => {})
|
|
5122
|
+
res.json({ ok: true, created: created || null, publish: { ok: true, remoteId, hash: created.hash, url: viewUrl } })
|
|
5123
|
+
return
|
|
5124
|
+
} catch (error) {
|
|
5125
|
+
const status = error && error.response && error.response.status ? Number(error.response.status) : null
|
|
5126
|
+
const message = error && error.message ? error.message : String(error)
|
|
5127
|
+
if (status === 401 || status === 403) {
|
|
5128
|
+
await this.updateEnvironmentVars({ PINOKIO_REGISTRY_API_KEY: "" }).catch(() => {})
|
|
5129
|
+
await this.kernel.git.setCheckpointSync(created.remoteKey, created.id, { status: "needs_link", at: Date.now(), error: message }).catch(() => {})
|
|
5130
|
+
res.json({ ok: true, created: created || null, publish: { ok: false, code: "not_linked", connectUrl } })
|
|
5131
|
+
return
|
|
5132
|
+
}
|
|
5133
|
+
await this.kernel.git.setCheckpointSync(created.remoteKey, created.id, { status: "error", at: Date.now(), error: message }).catch(() => {})
|
|
5134
|
+
res.json({ ok: true, created: created || null, publish: { ok: false, code: "error", error: message } })
|
|
5135
|
+
return
|
|
5136
|
+
}
|
|
5137
|
+
}
|
|
5138
|
+
}
|
|
5139
|
+
res.json({ ok: true, created: created || null })
|
|
5140
|
+
}))
|
|
5141
|
+
this.app.post("/checkpoints/install", ex(async (req, res) => {
|
|
5142
|
+
const remote = typeof req.body.remote === 'string' ? req.body.remote.trim() : ''
|
|
5143
|
+
const folder = typeof req.body.folder === 'string' ? req.body.folder.trim() : ''
|
|
5144
|
+
const snapshotRaw = typeof req.body.snapshotId === 'string' || typeof req.body.snapshotId === 'number' ? req.body.snapshotId : ''
|
|
5145
|
+
const snapshotId = snapshotRaw === 'latest' ? 'latest' : String(snapshotRaw)
|
|
5146
|
+
if (!remote || !folder || (!snapshotRaw && snapshotRaw !== 0)) {
|
|
5147
|
+
res.status(400).json({ ok: false, error: "Missing parameters" })
|
|
5148
|
+
return
|
|
5149
|
+
}
|
|
5150
|
+
if (folder.includes('/') || folder.includes('\\')) {
|
|
5151
|
+
res.status(400).json({ ok: false, error: "Invalid folder name" })
|
|
5152
|
+
return
|
|
5153
|
+
}
|
|
5154
|
+
const apiRoot = this.kernel.path("api")
|
|
5155
|
+
const targetPath = this.kernel.path("api", folder)
|
|
5156
|
+
let exists = false
|
|
5157
|
+
try {
|
|
5158
|
+
await fs.promises.access(targetPath, fs.constants.F_OK)
|
|
5159
|
+
exists = true
|
|
5160
|
+
} catch (_) {}
|
|
5161
|
+
if (exists) {
|
|
5162
|
+
res.json({ ok: false, code: "exists", error: "Folder already exists" })
|
|
5163
|
+
return
|
|
5164
|
+
}
|
|
5165
|
+
await fs.promises.mkdir(apiRoot, { recursive: true }).catch(() => {})
|
|
5166
|
+
if (snapshotId === 'latest') {
|
|
5167
|
+
const safeRemote = remote.replace(/"/g, '\\"')
|
|
5168
|
+
try {
|
|
5169
|
+
await this.kernel.exec({
|
|
5170
|
+
message: [`git clone "${safeRemote}" "${folder}"`],
|
|
5171
|
+
path: apiRoot
|
|
5172
|
+
}, () => {})
|
|
5173
|
+
} catch (err) {
|
|
5174
|
+
await fs.promises.rm(targetPath, { recursive: true, force: true }).catch(() => {})
|
|
5175
|
+
res.json({ ok: false, error: err && err.message ? err.message : 'Clone failed' })
|
|
5176
|
+
return
|
|
5177
|
+
}
|
|
5178
|
+
res.json({ ok: true, redirect: `/p/${encodeURIComponent(folder)}` })
|
|
5179
|
+
return
|
|
5180
|
+
}
|
|
5181
|
+
if (snapshotId !== 'latest' && String(snapshotId).trim() === "") {
|
|
5182
|
+
res.status(400).json({ ok: false, error: "Invalid snapshot" })
|
|
5183
|
+
return
|
|
5184
|
+
}
|
|
5185
|
+
const found = await this.kernel.git.findSnapshotByRemote(remote, snapshotId)
|
|
5186
|
+
if (!found || !found.snapshot) {
|
|
5187
|
+
res.status(404).json({ ok: false, error: "Snapshot not found" })
|
|
5188
|
+
return
|
|
5189
|
+
}
|
|
5190
|
+
const { remoteKey, snapshot } = found
|
|
5191
|
+
let addedSnapshot = false
|
|
5192
|
+
let ok = false
|
|
5193
|
+
try {
|
|
5194
|
+
ok = await this.kernel.git.downloadMainFromSnapshot(folder, snapshot.id, remote)
|
|
5195
|
+
} catch (_) {
|
|
5196
|
+
ok = false
|
|
5197
|
+
}
|
|
5198
|
+
if (!ok) {
|
|
5199
|
+
await fs.promises.rm(targetPath, { recursive: true, force: true }).catch(() => {})
|
|
5200
|
+
res.json({ ok: false, error: "Failed to download snapshot" })
|
|
5201
|
+
return
|
|
5202
|
+
}
|
|
5203
|
+
if (this.kernel.git && this.kernel.git.activeSnapshot) {
|
|
5204
|
+
this.kernel.git.activeSnapshot[folder] = { id: snapshot.id, remoteKey }
|
|
5205
|
+
}
|
|
5206
|
+
res.json({ ok: true, redirect: `/p/${encodeURIComponent(folder)}` })
|
|
5207
|
+
}))
|
|
5208
|
+
this.app.get("/checkpoints/restore/:workspace/:snapshotId", ex(async (req, res) => {
|
|
5209
|
+
const workspace = typeof req.params.workspace === 'string' ? req.params.workspace : ''
|
|
5210
|
+
const snapshotId = req.params.snapshotId
|
|
5211
|
+
const ok = await this.kernel.git.downloadMainFromSnapshot(workspace, snapshotId)
|
|
5212
|
+
if (ok) {
|
|
5213
|
+
// Mark this workspace as being in "restore from snapshot" mode so that
|
|
5214
|
+
// as the install script clones additional repos, they can be pinned to
|
|
5215
|
+
// the commits recorded in checkpoints.json. Then send the user to the
|
|
5216
|
+
// normal workspace page so installation happens through the usual UI.
|
|
5217
|
+
if (this.kernel.git && this.kernel.git.activeSnapshot) {
|
|
5218
|
+
const found = await this.kernel.git.findSnapshotForFolder(workspace, snapshotId)
|
|
5219
|
+
const remoteKey = found && found.remoteKey ? found.remoteKey : null
|
|
5220
|
+
this.kernel.git.activeSnapshot[workspace] = { id: snapshotId, remoteKey }
|
|
5221
|
+
}
|
|
5222
|
+
res.redirect(`/p/${workspace}`)
|
|
5223
|
+
} else {
|
|
5224
|
+
res.redirect("/backups")
|
|
5225
|
+
}
|
|
5226
|
+
}))
|
|
5227
|
+
this.app.post("/checkpoints/restore/:workspace/:snapshotId", ex(async (req, res) => {
|
|
5228
|
+
const workspace = typeof req.params.workspace === 'string' ? req.params.workspace : ''
|
|
5229
|
+
const snapshotId = req.params.snapshotId
|
|
5230
|
+
const ok = await this.kernel.git.downloadMainFromSnapshot(workspace, snapshotId)
|
|
5231
|
+
let meta = null
|
|
5232
|
+
if (ok) {
|
|
5233
|
+
try {
|
|
5234
|
+
meta = await this.kernel.api.meta(workspace)
|
|
5235
|
+
} catch (_) {
|
|
5236
|
+
meta = null
|
|
5237
|
+
}
|
|
5238
|
+
}
|
|
5239
|
+
res.json({ ok: !!ok, meta })
|
|
5240
|
+
}))
|
|
4520
5241
|
this.app.get("/agents", ex(async (req, res) => {
|
|
4521
5242
|
let pluginMenu = []
|
|
4522
5243
|
try {
|
|
@@ -7411,10 +8132,31 @@ class Server {
|
|
|
7411
8132
|
}))
|
|
7412
8133
|
this.app.get("/info/gitstatus/:name", ex(async (req, res) => {
|
|
7413
8134
|
try {
|
|
7414
|
-
const
|
|
8135
|
+
const workspaceName = req.params.name
|
|
8136
|
+
const workspaceRoot = this.kernel.path("api", workspaceName)
|
|
8137
|
+
const forceRefresh = req.query && (req.query.force === '1' || req.query.force === 'true')
|
|
8138
|
+
|
|
8139
|
+
if (forceRefresh && this.workspaceStatus && typeof this.workspaceStatus.markDirty === 'function') {
|
|
8140
|
+
this.workspaceStatus.markDirty(workspaceName)
|
|
8141
|
+
}
|
|
8142
|
+
|
|
8143
|
+
if (forceRefresh) {
|
|
8144
|
+
const data = await this.computeWorkspaceGitStatus(workspaceName)
|
|
8145
|
+
if (this.workspaceStatus && this.workspaceStatus.cache instanceof Map) {
|
|
8146
|
+
this.workspaceStatus.cache.set(workspaceName, {
|
|
8147
|
+
dirty: false,
|
|
8148
|
+
inflight: null,
|
|
8149
|
+
data,
|
|
8150
|
+
updatedAt: Date.now()
|
|
8151
|
+
})
|
|
8152
|
+
}
|
|
8153
|
+
res.json(data)
|
|
8154
|
+
return
|
|
8155
|
+
}
|
|
8156
|
+
|
|
7415
8157
|
const data = await this.workspaceStatus.getStatus(
|
|
7416
|
-
|
|
7417
|
-
() => this.computeWorkspaceGitStatus(
|
|
8158
|
+
workspaceName,
|
|
8159
|
+
() => this.computeWorkspaceGitStatus(workspaceName),
|
|
7418
8160
|
workspaceRoot
|
|
7419
8161
|
)
|
|
7420
8162
|
res.json(data)
|
|
@@ -7818,7 +8560,11 @@ class Server {
|
|
|
7818
8560
|
req.launcher_root = launcher.launcher_root
|
|
7819
8561
|
config = await this.processMenu(name, config)
|
|
7820
8562
|
} catch(e) {
|
|
7821
|
-
config
|
|
8563
|
+
if (config) {
|
|
8564
|
+
config.menu = []
|
|
8565
|
+
} else {
|
|
8566
|
+
config = { menu: [] }
|
|
8567
|
+
}
|
|
7822
8568
|
err = e.stack
|
|
7823
8569
|
}
|
|
7824
8570
|
await this.renderMenu(req, uri, name, config, [])
|
|
@@ -8334,16 +9080,20 @@ class Server {
|
|
|
8334
9080
|
})
|
|
8335
9081
|
}))
|
|
8336
9082
|
this.app.get("/p/:name/dev", ex(async (req, res) => {
|
|
8337
|
-
await this.
|
|
9083
|
+
const { hasSnapshots, pendingSnapshotId } = await this.getSnapshotStatus(req.params.name)
|
|
9084
|
+
await this.chrome(req, res, "browse", { hasSnapshots, pendingSnapshotId })
|
|
8338
9085
|
}))
|
|
8339
9086
|
this.app.get("/p/:name/files", ex(async (req, res) => {
|
|
8340
|
-
await this.
|
|
9087
|
+
const { hasSnapshots, pendingSnapshotId } = await this.getSnapshotStatus(req.params.name)
|
|
9088
|
+
await this.chrome(req, res, "files", { hasSnapshots, pendingSnapshotId })
|
|
8341
9089
|
}))
|
|
8342
9090
|
this.app.get("/p/:name/browse", ex(async (req, res) => {
|
|
8343
|
-
await this.
|
|
9091
|
+
const { hasSnapshots, pendingSnapshotId } = await this.getSnapshotStatus(req.params.name)
|
|
9092
|
+
await this.chrome(req, res, "browse", { hasSnapshots, pendingSnapshotId })
|
|
8344
9093
|
}))
|
|
8345
9094
|
this.app.get("/p/:name", ex(async (req, res) => {
|
|
8346
|
-
await this.
|
|
9095
|
+
const { hasSnapshots, pendingSnapshotId } = await this.getSnapshotStatus(req.params.name)
|
|
9096
|
+
await this.chrome(req, res, "run", { hasSnapshots, pendingSnapshotId })
|
|
8347
9097
|
}))
|
|
8348
9098
|
this.app.post("/pinokio/delete", ex(async (req, res) => {
|
|
8349
9099
|
try {
|
|
@@ -8481,6 +9231,55 @@ class Server {
|
|
|
8481
9231
|
let queryStr = new URLSearchParams(req.query).toString()
|
|
8482
9232
|
res.redirect("/home?mode=download&" + queryStr)
|
|
8483
9233
|
}))
|
|
9234
|
+
this.app.post("/pinokio/install/exists", ex(async (req, res) => {
|
|
9235
|
+
if (!this.kernel || !this.kernel.homedir) {
|
|
9236
|
+
return res.status(500).json({ error: "Pinokio home directory not set" })
|
|
9237
|
+
}
|
|
9238
|
+
|
|
9239
|
+
const body = req.body && typeof req.body === "object" ? req.body : {}
|
|
9240
|
+
const folderName = typeof body.folderName === "string" ? body.folderName.trim() : ""
|
|
9241
|
+
|
|
9242
|
+
if (!folderName) {
|
|
9243
|
+
return res.status(400).json({ error: "folderName is required" })
|
|
9244
|
+
}
|
|
9245
|
+
|
|
9246
|
+
if (folderName === "." || folderName === ".." || /[\\/]/.test(folderName) || folderName.includes("\0")) {
|
|
9247
|
+
return res.status(400).json({ error: "Invalid folderName" })
|
|
9248
|
+
}
|
|
9249
|
+
|
|
9250
|
+
let sanitizedPath = null
|
|
9251
|
+
if (typeof body.relativePath === "string") {
|
|
9252
|
+
let trimmed = body.relativePath.trim()
|
|
9253
|
+
if (trimmed) {
|
|
9254
|
+
trimmed = trimmed.replace(/^~[\\/]?/, "").replace(/^[\\/]+/, "")
|
|
9255
|
+
if (trimmed) {
|
|
9256
|
+
const segments = trimmed.split(/[\\/]+/).filter(Boolean)
|
|
9257
|
+
if (segments.length > 0 && !segments.some((segment) => segment === "." || segment === "..")) {
|
|
9258
|
+
sanitizedPath = segments.join("/")
|
|
9259
|
+
}
|
|
9260
|
+
}
|
|
9261
|
+
}
|
|
9262
|
+
}
|
|
9263
|
+
|
|
9264
|
+
if (!sanitizedPath) {
|
|
9265
|
+
sanitizedPath = "api"
|
|
9266
|
+
}
|
|
9267
|
+
|
|
9268
|
+
const home = path.resolve(this.kernel.homedir)
|
|
9269
|
+
const target = path.resolve(home, sanitizedPath, folderName)
|
|
9270
|
+
const homeWithSep = home.endsWith(path.sep) ? home : home + path.sep
|
|
9271
|
+
|
|
9272
|
+
if (!target.startsWith(homeWithSep)) {
|
|
9273
|
+
return res.status(400).json({ error: "Invalid install destination" })
|
|
9274
|
+
}
|
|
9275
|
+
|
|
9276
|
+
const exists = await fs.promises
|
|
9277
|
+
.access(target, fs.constants.F_OK)
|
|
9278
|
+
.then(() => true)
|
|
9279
|
+
.catch(() => false)
|
|
9280
|
+
|
|
9281
|
+
res.json({ exists })
|
|
9282
|
+
}))
|
|
8484
9283
|
this.app.post("/pinokio/install", ex((req, res) => {
|
|
8485
9284
|
req.session.requirements = req.body.requirements
|
|
8486
9285
|
req.session.callback = req.body.callback
|
|
@@ -8588,6 +9387,32 @@ class Server {
|
|
|
8588
9387
|
}
|
|
8589
9388
|
|
|
8590
9389
|
}))
|
|
9390
|
+
this.app.post("/create-upload", this.upload.any(), ex(async (req, res) => {
|
|
9391
|
+
try {
|
|
9392
|
+
const files = Array.isArray(req.files) ? req.files : [];
|
|
9393
|
+
if (!files.length) {
|
|
9394
|
+
return res.status(400).json({ error: "No files provided" });
|
|
9395
|
+
}
|
|
9396
|
+
const token = crypto.randomBytes(16).toString("hex");
|
|
9397
|
+
const baseDir = this.kernel.path("tmp", "create", token);
|
|
9398
|
+
await fs.promises.mkdir(baseDir, { recursive: true });
|
|
9399
|
+
const stored = [];
|
|
9400
|
+
for (const file of files) {
|
|
9401
|
+
if (!file || !file.buffer) continue;
|
|
9402
|
+
const filename = path.basename(file.originalname || "file");
|
|
9403
|
+
const target = path.resolve(baseDir, filename);
|
|
9404
|
+
await fs.promises.writeFile(target, file.buffer);
|
|
9405
|
+
stored.push({ name: filename, size: file.size });
|
|
9406
|
+
}
|
|
9407
|
+
if (!stored.length) {
|
|
9408
|
+
return res.status(400).json({ error: "No valid files provided" });
|
|
9409
|
+
}
|
|
9410
|
+
res.json({ uploadToken: token, files: stored });
|
|
9411
|
+
} catch (error) {
|
|
9412
|
+
console.log("create-upload error", error);
|
|
9413
|
+
res.status(500).json({ error: error.message || "Upload failed" });
|
|
9414
|
+
}
|
|
9415
|
+
}))
|
|
8591
9416
|
/*
|
|
8592
9417
|
SYNTAX
|
|
8593
9418
|
fs.uri(<bin|api>, path)
|