pinokiod 5.1.10 → 5.1.17
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 +1169 -350
- 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/1764335557118 +0 -45
- package/undefined/logs/dev/plugin/cursor-agent.git/pinokio.js/1764335834126 +0 -45
- package/undefined/logs/dev/plugin/cursor-agent.git/pinokio.js/events +0 -12
- 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 || ""
|
|
@@ -1379,123 +1413,175 @@ class Server {
|
|
|
1379
1413
|
return { changes: [], git_commit_url: null }
|
|
1380
1414
|
}
|
|
1381
1415
|
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
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
|
+
}
|
|
1397
1433
|
}
|
|
1434
|
+
} catch (_) {
|
|
1398
1435
|
}
|
|
1399
|
-
if (normalized.includes('/site-packages/')) {
|
|
1400
|
-
return false
|
|
1401
|
-
}
|
|
1402
|
-
if (normalized.includes('/Scripts/')) {
|
|
1403
|
-
return false
|
|
1404
|
-
}
|
|
1405
|
-
if (normalized.includes('/bin/activate')) {
|
|
1406
|
-
return false
|
|
1407
|
-
}
|
|
1408
|
-
return true
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
let statusMatrix = await git.statusMatrix({ dir, fs })
|
|
1412
|
-
statusMatrix = statusMatrix.filter(Boolean)
|
|
1413
1436
|
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
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
|
|
1428
1456
|
}
|
|
1429
|
-
}
|
|
1430
1457
|
|
|
1431
|
-
|
|
1432
|
-
for (const [filepath, head, workdir, stage] of statusMatrix) {
|
|
1433
|
-
if (!shouldIncludePath(filepath)) {
|
|
1434
|
-
continue
|
|
1435
|
-
}
|
|
1436
|
-
if (head === workdir && head === stage) {
|
|
1437
|
-
continue
|
|
1438
|
-
}
|
|
1439
|
-
const absolutePath = path.join(dir, filepath)
|
|
1440
|
-
let stats
|
|
1458
|
+
let stdout = ""
|
|
1441
1459
|
try {
|
|
1442
|
-
|
|
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
|
+
})
|
|
1443
1484
|
} catch (error) {
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
if (stats && stats.isDirectory()) {
|
|
1447
|
-
continue
|
|
1485
|
+
console.warn('[git] status threw', dir, error && error.message ? error.message : error)
|
|
1486
|
+
stdout = ""
|
|
1448
1487
|
}
|
|
1488
|
+
const lines = stdout.split(/\r?\n/).filter((line) => line && line.trim().length > 0)
|
|
1449
1489
|
|
|
1450
|
-
const
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
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)) {
|
|
1461
1515
|
continue
|
|
1462
1516
|
}
|
|
1463
|
-
} catch (_) {
|
|
1464
|
-
// fall through if comparison fails
|
|
1465
1517
|
}
|
|
1466
|
-
|
|
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
|
+
}
|
|
1467
1555
|
|
|
1468
|
-
|
|
1556
|
+
const webpath = "/asset/" + path.relative(this.kernel.homedir, absolutePath)
|
|
1469
1557
|
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1558
|
+
changes.push({
|
|
1559
|
+
ref: 'HEAD',
|
|
1560
|
+
webpath,
|
|
1561
|
+
file: normalizedFile,
|
|
1562
|
+
path: absolutePath,
|
|
1563
|
+
diffpath: `/gitdiff/HEAD/${repoParam}/${normalizedFile}`,
|
|
1564
|
+
status,
|
|
1565
|
+
})
|
|
1566
|
+
}
|
|
1479
1567
|
|
|
1480
|
-
|
|
1568
|
+
const repoHistoryUrl = repoParam ? `/info/git/HEAD/${repoParam}` : null
|
|
1481
1569
|
|
|
1482
|
-
|
|
1483
|
-
|
|
1570
|
+
const forkUrl = `/run/scripts/git/fork.json?cwd=${encodeURIComponent(dir)}`
|
|
1571
|
+
const pushUrl = `/run/scripts/git/push.json?cwd=${encodeURIComponent(dir)}`
|
|
1484
1572
|
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
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
|
+
}
|
|
1492
1580
|
}
|
|
1493
1581
|
async computeWorkspaceGitStatus(workspaceName) {
|
|
1494
1582
|
const workspacePath = this.kernel.path("api", workspaceName)
|
|
1495
1583
|
const repos = await this.kernel.git.repos(workspacePath)
|
|
1496
1584
|
|
|
1497
|
-
// await Util.ignore_subrepos(workspacePath, repos)
|
|
1498
|
-
|
|
1499
1585
|
const statuses = []
|
|
1500
1586
|
for (const repo of repos) {
|
|
1501
1587
|
const repoParam = repo.gitParentRelPath || workspaceName
|
|
@@ -4240,6 +4326,7 @@ class Server {
|
|
|
4240
4326
|
let str = await Environment.ENV("system", this.kernel.homedir, this.kernel)
|
|
4241
4327
|
await fs.promises.writeFile(path.resolve(this.kernel.homedir, "ENVIRONMENT"), str)
|
|
4242
4328
|
}
|
|
4329
|
+
this.workspaceStatus.ensureWatcher('api', this.kernel.path('api')).catch(() => {})
|
|
4243
4330
|
}
|
|
4244
4331
|
|
|
4245
4332
|
|
|
@@ -4523,6 +4610,634 @@ class Server {
|
|
|
4523
4610
|
list,
|
|
4524
4611
|
})
|
|
4525
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
|
+
}))
|
|
4526
5241
|
this.app.get("/agents", ex(async (req, res) => {
|
|
4527
5242
|
let pluginMenu = []
|
|
4528
5243
|
try {
|
|
@@ -7417,10 +8132,31 @@ class Server {
|
|
|
7417
8132
|
}))
|
|
7418
8133
|
this.app.get("/info/gitstatus/:name", ex(async (req, res) => {
|
|
7419
8134
|
try {
|
|
7420
|
-
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
|
+
|
|
7421
8157
|
const data = await this.workspaceStatus.getStatus(
|
|
7422
|
-
|
|
7423
|
-
() => this.computeWorkspaceGitStatus(
|
|
8158
|
+
workspaceName,
|
|
8159
|
+
() => this.computeWorkspaceGitStatus(workspaceName),
|
|
7424
8160
|
workspaceRoot
|
|
7425
8161
|
)
|
|
7426
8162
|
res.json(data)
|
|
@@ -7824,7 +8560,11 @@ class Server {
|
|
|
7824
8560
|
req.launcher_root = launcher.launcher_root
|
|
7825
8561
|
config = await this.processMenu(name, config)
|
|
7826
8562
|
} catch(e) {
|
|
7827
|
-
config
|
|
8563
|
+
if (config) {
|
|
8564
|
+
config.menu = []
|
|
8565
|
+
} else {
|
|
8566
|
+
config = { menu: [] }
|
|
8567
|
+
}
|
|
7828
8568
|
err = e.stack
|
|
7829
8569
|
}
|
|
7830
8570
|
await this.renderMenu(req, uri, name, config, [])
|
|
@@ -8340,16 +9080,20 @@ class Server {
|
|
|
8340
9080
|
})
|
|
8341
9081
|
}))
|
|
8342
9082
|
this.app.get("/p/:name/dev", ex(async (req, res) => {
|
|
8343
|
-
await this.
|
|
9083
|
+
const { hasSnapshots, pendingSnapshotId } = await this.getSnapshotStatus(req.params.name)
|
|
9084
|
+
await this.chrome(req, res, "browse", { hasSnapshots, pendingSnapshotId })
|
|
8344
9085
|
}))
|
|
8345
9086
|
this.app.get("/p/:name/files", ex(async (req, res) => {
|
|
8346
|
-
await this.
|
|
9087
|
+
const { hasSnapshots, pendingSnapshotId } = await this.getSnapshotStatus(req.params.name)
|
|
9088
|
+
await this.chrome(req, res, "files", { hasSnapshots, pendingSnapshotId })
|
|
8347
9089
|
}))
|
|
8348
9090
|
this.app.get("/p/:name/browse", ex(async (req, res) => {
|
|
8349
|
-
await this.
|
|
9091
|
+
const { hasSnapshots, pendingSnapshotId } = await this.getSnapshotStatus(req.params.name)
|
|
9092
|
+
await this.chrome(req, res, "browse", { hasSnapshots, pendingSnapshotId })
|
|
8350
9093
|
}))
|
|
8351
9094
|
this.app.get("/p/:name", ex(async (req, res) => {
|
|
8352
|
-
await this.
|
|
9095
|
+
const { hasSnapshots, pendingSnapshotId } = await this.getSnapshotStatus(req.params.name)
|
|
9096
|
+
await this.chrome(req, res, "run", { hasSnapshots, pendingSnapshotId })
|
|
8353
9097
|
}))
|
|
8354
9098
|
this.app.post("/pinokio/delete", ex(async (req, res) => {
|
|
8355
9099
|
try {
|
|
@@ -8487,6 +9231,55 @@ class Server {
|
|
|
8487
9231
|
let queryStr = new URLSearchParams(req.query).toString()
|
|
8488
9232
|
res.redirect("/home?mode=download&" + queryStr)
|
|
8489
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
|
+
}))
|
|
8490
9283
|
this.app.post("/pinokio/install", ex((req, res) => {
|
|
8491
9284
|
req.session.requirements = req.body.requirements
|
|
8492
9285
|
req.session.callback = req.body.callback
|
|
@@ -8594,6 +9387,32 @@ class Server {
|
|
|
8594
9387
|
}
|
|
8595
9388
|
|
|
8596
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
|
+
}))
|
|
8597
9416
|
/*
|
|
8598
9417
|
SYNTAX
|
|
8599
9418
|
fs.uri(<bin|api>, path)
|