pinokiod 5.3.6 → 5.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -48,6 +48,8 @@ const REGISTRY_PING_PNG = Buffer.from(
|
|
|
48
48
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMBAAObF+6bAAAAAElFTkSuQmCC',
|
|
49
49
|
'base64'
|
|
50
50
|
)
|
|
51
|
+
const DEFAULT_REGISTRY_URL = 'https://beta.pinokio.co'
|
|
52
|
+
const DEFAULT_REGISTRY_CONNECT_URL = `${DEFAULT_REGISTRY_URL}/connect/pinokio`
|
|
51
53
|
|
|
52
54
|
const ex = fn => (req, res, next) => {
|
|
53
55
|
Promise.resolve(fn(req, res, next)).catch(next);
|
|
@@ -1106,9 +1108,9 @@ class Server {
|
|
|
1106
1108
|
result.snapshotFooterEnabled = !!(options && options.snapshotFooterEnabled)
|
|
1107
1109
|
result.hasSnapshots = !!(options && options.hasSnapshots)
|
|
1108
1110
|
result.pendingSnapshotId = options && options.pendingSnapshotId ? String(options.pendingSnapshotId) : null
|
|
1109
|
-
const
|
|
1110
|
-
result.
|
|
1111
|
-
if (!
|
|
1111
|
+
const registryEnabled = await this.isRegistryEnabled().catch(() => false)
|
|
1112
|
+
result.registryEnabled = registryEnabled
|
|
1113
|
+
if (!registryEnabled) {
|
|
1112
1114
|
result.pendingSnapshotId = null
|
|
1113
1115
|
}
|
|
1114
1116
|
// if (!this.kernel.proto.config) {
|
|
@@ -1131,28 +1133,8 @@ class Server {
|
|
|
1131
1133
|
}
|
|
1132
1134
|
return { editorUrl, prevUrl }
|
|
1133
1135
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
if (value === false) return false
|
|
1137
|
-
if (value == null) return null
|
|
1138
|
-
const str = String(value).trim().toLowerCase()
|
|
1139
|
-
if (!str) return null
|
|
1140
|
-
if (["1", "true", "yes", "on", "enable", "enabled"].includes(str)) return true
|
|
1141
|
-
if (["0", "false", "no", "off", "disable", "disabled"].includes(str)) return false
|
|
1142
|
-
return null
|
|
1143
|
-
}
|
|
1144
|
-
async isRegistryBetaEnabled() {
|
|
1145
|
-
let raw = null
|
|
1146
|
-
try {
|
|
1147
|
-
const env = await Environment.get(this.kernel.homedir, this.kernel)
|
|
1148
|
-
if (env && Object.prototype.hasOwnProperty.call(env, "PINOKIO_REGISTRY_BETA")) {
|
|
1149
|
-
raw = env.PINOKIO_REGISTRY_BETA
|
|
1150
|
-
}
|
|
1151
|
-
} catch (_) {}
|
|
1152
|
-
if (raw == null && process.env.PINOKIO_REGISTRY_BETA != null) {
|
|
1153
|
-
raw = process.env.PINOKIO_REGISTRY_BETA
|
|
1154
|
-
}
|
|
1155
|
-
return this.parseRegistryBetaValue(raw) === true
|
|
1136
|
+
async isRegistryEnabled() {
|
|
1137
|
+
return true
|
|
1156
1138
|
}
|
|
1157
1139
|
async updateEnvironmentVars(updates) {
|
|
1158
1140
|
await Environment.init({}, this.kernel)
|
|
@@ -1194,6 +1176,9 @@ class Server {
|
|
|
1194
1176
|
await this.updateEnvironmentVars(updates).catch(() => {})
|
|
1195
1177
|
}
|
|
1196
1178
|
}
|
|
1179
|
+
if (!url) {
|
|
1180
|
+
url = DEFAULT_REGISTRY_URL
|
|
1181
|
+
}
|
|
1197
1182
|
return { url, apiKey }
|
|
1198
1183
|
}
|
|
1199
1184
|
async getRegistryConnectUrl() {
|
|
@@ -1229,6 +1214,11 @@ class Server {
|
|
|
1229
1214
|
}
|
|
1230
1215
|
}
|
|
1231
1216
|
}
|
|
1217
|
+
if (!value) {
|
|
1218
|
+
const registry = await this.getRegistryConfig().catch(() => ({ url: null }))
|
|
1219
|
+
const baseUrl = registry && registry.url ? String(registry.url).replace(/\/$/, "") : ""
|
|
1220
|
+
value = baseUrl ? `${baseUrl}/connect/pinokio` : DEFAULT_REGISTRY_CONNECT_URL
|
|
1221
|
+
}
|
|
1232
1222
|
if (value && source === "env-endpoint") {
|
|
1233
1223
|
await this.updateEnvironmentVars({
|
|
1234
1224
|
PINOKIO_REGISTRY_CONNECT_URL: value,
|
|
@@ -1271,9 +1261,9 @@ class Server {
|
|
|
1271
1261
|
let hasSnapshots = false
|
|
1272
1262
|
let pendingSnapshotId = null
|
|
1273
1263
|
const debugLogs = []
|
|
1274
|
-
const
|
|
1264
|
+
const registryEnabled = await this.isRegistryEnabled().catch(() => false)
|
|
1275
1265
|
const shouldTreatPending = (entry) => {
|
|
1276
|
-
if (!
|
|
1266
|
+
if (!registryEnabled || !entry || entry.decision) return false
|
|
1277
1267
|
const status = entry.sync && entry.sync.status ? String(entry.sync.status) : "local"
|
|
1278
1268
|
if (status === "published" || status === "imported") return false
|
|
1279
1269
|
return true
|
|
@@ -4616,7 +4606,7 @@ class Server {
|
|
|
4616
4606
|
})
|
|
4617
4607
|
}))
|
|
4618
4608
|
this.app.get("/checkpoints", ex(async (req, res) => {
|
|
4619
|
-
const
|
|
4609
|
+
const registryEnabled = await this.isRegistryEnabled().catch(() => false)
|
|
4620
4610
|
const peerAccess = await this.composePeerAccessPayload()
|
|
4621
4611
|
const list = this.getPeers()
|
|
4622
4612
|
const history = this.kernel.git && this.kernel.git.history ? this.kernel.git.history : { version: "1", apps: {} }
|
|
@@ -4819,7 +4809,7 @@ class Server {
|
|
|
4819
4809
|
autoInstall,
|
|
4820
4810
|
importError,
|
|
4821
4811
|
checkpointsDir,
|
|
4822
|
-
|
|
4812
|
+
registryEnabled,
|
|
4823
4813
|
portal: this.portal,
|
|
4824
4814
|
logo: this.logo,
|
|
4825
4815
|
theme: this.theme,
|
|
@@ -4827,19 +4817,37 @@ class Server {
|
|
|
4827
4817
|
list,
|
|
4828
4818
|
})
|
|
4829
4819
|
}))
|
|
4830
|
-
this.app.get("/checkpoints/
|
|
4831
|
-
const registryBetaEnabled = await this.isRegistryBetaEnabled().catch(() => false)
|
|
4820
|
+
this.app.get("/checkpoints/registry", ex(async (req, res) => {
|
|
4832
4821
|
const registryConnectUrl = await this.getRegistryConnectUrl().catch(() => null)
|
|
4833
4822
|
const registryUrlPrefill = typeof req.query.registry_url === "string" ? req.query.registry_url.trim() : ""
|
|
4834
|
-
res.render("
|
|
4835
|
-
registryBetaEnabled,
|
|
4823
|
+
res.render("checkpoints_registry", {
|
|
4836
4824
|
registryConnectUrl,
|
|
4837
4825
|
registryUrlPrefill,
|
|
4838
4826
|
theme: this.theme,
|
|
4839
4827
|
agent: req.agent,
|
|
4840
4828
|
})
|
|
4841
4829
|
}))
|
|
4842
|
-
|
|
4830
|
+
this.app.post("/checkpoints/registry", ex(async (req, res) => {
|
|
4831
|
+
const connectRaw = Object.prototype.hasOwnProperty.call(req.body || {}, "connectUrl")
|
|
4832
|
+
? req.body.connectUrl
|
|
4833
|
+
: (Object.prototype.hasOwnProperty.call(req.body || {}, "registry_url")
|
|
4834
|
+
? req.body.registry_url
|
|
4835
|
+
: (Object.prototype.hasOwnProperty.call(req.body || {}, "connectEndpoint")
|
|
4836
|
+
? req.body.connectEndpoint
|
|
4837
|
+
: (Object.prototype.hasOwnProperty.call(req.query || {}, "connectUrl")
|
|
4838
|
+
? req.query.connectUrl
|
|
4839
|
+
: (Object.prototype.hasOwnProperty.call(req.query || {}, "registry_url")
|
|
4840
|
+
? req.query.registry_url
|
|
4841
|
+
: (Object.prototype.hasOwnProperty.call(req.query || {}, "connectEndpoint")
|
|
4842
|
+
? req.query.connectEndpoint
|
|
4843
|
+
: "")))))
|
|
4844
|
+
const connectUrl = connectRaw != null ? String(connectRaw).trim() : ""
|
|
4845
|
+
await this.updateEnvironmentVars({
|
|
4846
|
+
PINOKIO_REGISTRY_CONNECT_URL: connectUrl,
|
|
4847
|
+
PINOKIO_REGISTRY_CONNECT_ENDPOINT: ""
|
|
4848
|
+
})
|
|
4849
|
+
res.json({ ok: true, connectUrl: connectUrl || null })
|
|
4850
|
+
}))
|
|
4843
4851
|
this.app.get("/registry/ping.png", ex(async (_req, res) => {
|
|
4844
4852
|
res.setHeader('Content-Type', 'image/png')
|
|
4845
4853
|
res.setHeader('Cache-Control', 'no-store')
|
|
@@ -4852,7 +4860,7 @@ class Server {
|
|
|
4852
4860
|
const returnRaw = typeof req.query.return === 'string' ? req.query.return.trim() : ''
|
|
4853
4861
|
const successRaw = typeof req.query.success === 'string' ? req.query.success.trim() : ''
|
|
4854
4862
|
const appSlug = typeof req.query.app === 'string' ? req.query.app.trim() : ''
|
|
4855
|
-
const registryEnabled = await this.
|
|
4863
|
+
const registryEnabled = await this.isRegistryEnabled().catch(() => false)
|
|
4856
4864
|
|
|
4857
4865
|
let returnUrl = null
|
|
4858
4866
|
if (returnRaw) {
|
|
@@ -5018,49 +5026,9 @@ class Server {
|
|
|
5018
5026
|
res.json({ linked: !!apiKey, url: baseUrl })
|
|
5019
5027
|
}))
|
|
5020
5028
|
|
|
5021
|
-
this.app.post("/checkpoints/registry_beta", ex(async (req, res) => {
|
|
5022
|
-
const rawValue = Object.prototype.hasOwnProperty.call(req.body || {}, "enabled")
|
|
5023
|
-
? req.body.enabled
|
|
5024
|
-
: (Object.prototype.hasOwnProperty.call(req.query || {}, "enabled") ? req.query.enabled : null)
|
|
5025
|
-
const hasConnectField = Object.prototype.hasOwnProperty.call(req.body || {}, "connectUrl")
|
|
5026
|
-
|| Object.prototype.hasOwnProperty.call(req.body || {}, "connectEndpoint")
|
|
5027
|
-
|| Object.prototype.hasOwnProperty.call(req.body || {}, "registry_url")
|
|
5028
|
-
|| Object.prototype.hasOwnProperty.call(req.query || {}, "connectUrl")
|
|
5029
|
-
|| Object.prototype.hasOwnProperty.call(req.query || {}, "connectEndpoint")
|
|
5030
|
-
|| Object.prototype.hasOwnProperty.call(req.query || {}, "registry_url")
|
|
5031
|
-
const connectRaw = Object.prototype.hasOwnProperty.call(req.body || {}, "connectUrl")
|
|
5032
|
-
? req.body.connectUrl
|
|
5033
|
-
: (Object.prototype.hasOwnProperty.call(req.body || {}, "registry_url")
|
|
5034
|
-
? req.body.registry_url
|
|
5035
|
-
: (Object.prototype.hasOwnProperty.call(req.body || {}, "connectEndpoint")
|
|
5036
|
-
? req.body.connectEndpoint
|
|
5037
|
-
: (Object.prototype.hasOwnProperty.call(req.query || {}, "connectUrl")
|
|
5038
|
-
? req.query.connectUrl
|
|
5039
|
-
: (Object.prototype.hasOwnProperty.call(req.query || {}, "registry_url")
|
|
5040
|
-
? req.query.registry_url
|
|
5041
|
-
: (Object.prototype.hasOwnProperty.call(req.query || {}, "connectEndpoint")
|
|
5042
|
-
? req.query.connectEndpoint
|
|
5043
|
-
: null)))))
|
|
5044
|
-
const connectUrl = connectRaw != null ? String(connectRaw).trim() : ""
|
|
5045
|
-
const parsed = this.parseRegistryBetaValue(rawValue)
|
|
5046
|
-
if (parsed == null) {
|
|
5047
|
-
res.status(400).json({ ok: false, error: "Invalid enabled value" })
|
|
5048
|
-
return
|
|
5049
|
-
}
|
|
5050
|
-
const updates = {
|
|
5051
|
-
PINOKIO_REGISTRY_BETA: parsed ? "1" : ""
|
|
5052
|
-
}
|
|
5053
|
-
if (hasConnectField) {
|
|
5054
|
-
updates.PINOKIO_REGISTRY_CONNECT_URL = connectUrl
|
|
5055
|
-
updates.PINOKIO_REGISTRY_CONNECT_ENDPOINT = ""
|
|
5056
|
-
}
|
|
5057
|
-
await this.updateEnvironmentVars(updates)
|
|
5058
|
-
res.json({ ok: true, enabled: parsed, connectUrl: connectUrl || null })
|
|
5059
|
-
}))
|
|
5060
|
-
|
|
5061
5029
|
this.app.post("/checkpoints/publish", ex(async (req, res) => {
|
|
5062
|
-
const
|
|
5063
|
-
if (!
|
|
5030
|
+
const registryEnabled = await this.isRegistryEnabled().catch(() => false)
|
|
5031
|
+
if (!registryEnabled) {
|
|
5064
5032
|
res.status(404).json({ ok: false, error: "Not found" })
|
|
5065
5033
|
return
|
|
5066
5034
|
}
|
|
@@ -5130,8 +5098,8 @@ class Server {
|
|
|
5130
5098
|
}))
|
|
5131
5099
|
|
|
5132
5100
|
this.app.post("/checkpoints/decision", ex(async (req, res) => {
|
|
5133
|
-
const
|
|
5134
|
-
if (!
|
|
5101
|
+
const registryEnabled = await this.isRegistryEnabled().catch(() => false)
|
|
5102
|
+
if (!registryEnabled) {
|
|
5135
5103
|
res.status(404).json({ ok: false, error: "Not found" })
|
|
5136
5104
|
return
|
|
5137
5105
|
}
|
|
@@ -5161,13 +5129,13 @@ class Server {
|
|
|
5161
5129
|
res.json({ ok: false })
|
|
5162
5130
|
return
|
|
5163
5131
|
}
|
|
5164
|
-
const
|
|
5132
|
+
const registryEnabled = await this.isRegistryEnabled().catch(() => false)
|
|
5165
5133
|
const publishRaw = typeof req.query.publish === 'string' ? req.query.publish.trim().toLowerCase() : ''
|
|
5166
|
-
const wantPublish =
|
|
5134
|
+
const wantPublish = registryEnabled && (publishRaw === '1' || publishRaw === 'true' || publishRaw === 'cloud')
|
|
5167
5135
|
const root = this.kernel.path("api", name)
|
|
5168
5136
|
const repos = await this.kernel.git.repos(root)
|
|
5169
5137
|
const created = await this.kernel.git.appendWorkspaceSnapshot(name, repos)
|
|
5170
|
-
if (
|
|
5138
|
+
if (registryEnabled && created && created.remoteKey && created.id) {
|
|
5171
5139
|
const existingDecision = this.kernel.git.getCheckpointDecision(created.remoteKey, created.id)
|
|
5172
5140
|
if (!existingDecision) {
|
|
5173
5141
|
await this.kernel.git.setCheckpointDecision(created.remoteKey, created.id, "pending").catch(() => {})
|
package/server/views/app.ejs
CHANGED
|
@@ -3702,12 +3702,12 @@ body.dark .snapshot-footer-input input {
|
|
|
3702
3702
|
<iframe class='selected' src="<%=editor_tab%>"></iframe>
|
|
3703
3703
|
<% } %>
|
|
3704
3704
|
</main>
|
|
3705
|
-
<% const
|
|
3705
|
+
<% const registryEnabledValue = typeof registryEnabled === "undefined" ? false : registryEnabled; %>
|
|
3706
3706
|
<% const snapshotFooterAllowed = typeof snapshotFooterEnabled === "undefined" ? false : snapshotFooterEnabled; %>
|
|
3707
3707
|
<% if (snapshotFooterAllowed) { %>
|
|
3708
|
-
<div class='snapshot-footer hidden' data-workspace="<%=name%>" data-pending-snapshot-id="<%= pendingSnapshotId ? pendingSnapshotId : '' %>" data-registry-
|
|
3709
|
-
<div class="snapshot-footer-save <%=
|
|
3710
|
-
<div class='caption' title="<%=
|
|
3708
|
+
<div class='snapshot-footer hidden' data-workspace="<%=name%>" data-pending-snapshot-id="<%= pendingSnapshotId ? pendingSnapshotId : '' %>" data-registry-enabled="<%= registryEnabledValue ? '1' : '0' %>">
|
|
3709
|
+
<div class="snapshot-footer-save <%= registryEnabledValue && pendingSnapshotId ? 'hidden' : '' %>">
|
|
3710
|
+
<div class='caption' title="<%= registryEnabledValue ? 'Saves a tiny JSON checkpoint of exact commit hashes (no files). You can publish it after saving.' : 'Saves a tiny JSON checkpoint of exact commit hashes (no files).' %>">
|
|
3711
3711
|
Snapshot installed modules information and restore later
|
|
3712
3712
|
</div>
|
|
3713
3713
|
<div class="snapshot-footer-actions" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center;">
|
|
@@ -3718,7 +3718,7 @@ body.dark .snapshot-footer-input input {
|
|
|
3718
3718
|
<button type="button" class="btn snapshot-btn-dismiss"><i class="fa-solid fa-circle-xmark"></i> Dismiss</button>
|
|
3719
3719
|
</div>
|
|
3720
3720
|
</div>
|
|
3721
|
-
<% if (
|
|
3721
|
+
<% if (registryEnabledValue) { %>
|
|
3722
3722
|
<div class="snapshot-footer-publish <%= pendingSnapshotId ? '' : 'hidden' %>" aria-live="polite">
|
|
3723
3723
|
<div class="snapshot-footer-publish-text">Local save success! Publish to the registry?</div>
|
|
3724
3724
|
<div class="snapshot-footer-actions" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center;">
|
|
@@ -9731,7 +9731,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
9731
9731
|
const publishText = publishPanel ? publishPanel.querySelector(".snapshot-footer-publish-text") : null
|
|
9732
9732
|
const publishActions = publishPanel ? publishPanel.querySelector(".snapshot-footer-actions") : null
|
|
9733
9733
|
if (!saveBtn) return
|
|
9734
|
-
const
|
|
9734
|
+
const registryEnabled = footer.getAttribute("data-registry-enabled") === "1"
|
|
9735
9735
|
const workspace = footer.getAttribute("data-workspace")
|
|
9736
9736
|
if (!workspace) {
|
|
9737
9737
|
footer.classList.add("hidden")
|
|
@@ -9780,7 +9780,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
9780
9780
|
}
|
|
9781
9781
|
}
|
|
9782
9782
|
const showPublishPanel = () => {
|
|
9783
|
-
if (!
|
|
9783
|
+
if (!registryEnabled) {
|
|
9784
9784
|
footer.classList.add("hidden")
|
|
9785
9785
|
return
|
|
9786
9786
|
}
|
|
@@ -9795,7 +9795,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
9795
9795
|
const hasSnapshots = !!(status && status.hasSnapshots)
|
|
9796
9796
|
pendingSnapshotId = status && status.pendingSnapshotId ? String(status.pendingSnapshotId) : ""
|
|
9797
9797
|
footer.setAttribute("data-pending-snapshot-id", pendingSnapshotId)
|
|
9798
|
-
const shouldShow = !hasSnapshots || (
|
|
9798
|
+
const shouldShow = !hasSnapshots || (registryEnabled && pendingSnapshotId)
|
|
9799
9799
|
if (!shouldShow) {
|
|
9800
9800
|
setDismissedClass(false)
|
|
9801
9801
|
footer.classList.add("hidden")
|
|
@@ -10194,8 +10194,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
10194
10194
|
}
|
|
10195
10195
|
|
|
10196
10196
|
saveBtn.addEventListener("click", handleSave)
|
|
10197
|
-
if (
|
|
10198
|
-
if (
|
|
10197
|
+
if (registryEnabled && publishBtn) publishBtn.addEventListener("click", handlePublish)
|
|
10198
|
+
if (registryEnabled && laterBtn) laterBtn.addEventListener("click", handleLater)
|
|
10199
10199
|
if (dismissBtn) dismissBtn.addEventListener("click", handleDismiss)
|
|
10200
10200
|
})
|
|
10201
10201
|
</script>
|
|
@@ -403,12 +403,12 @@ body.dark .backup-loading .backup-loading-text {
|
|
|
403
403
|
window.backupItems = <%- JSON.stringify(items || []) %>;
|
|
404
404
|
window.backupAutoInstall = <%- JSON.stringify(autoInstall || null) %>;
|
|
405
405
|
window.backupImportError = <%- JSON.stringify(importError || null) %>;
|
|
406
|
-
window.
|
|
406
|
+
window.registryEnabled = <%- JSON.stringify(!!registryEnabled) %>;
|
|
407
407
|
document.addEventListener("DOMContentLoaded", () => {
|
|
408
408
|
const items = Array.isArray(window.backupItems) ? window.backupItems : []
|
|
409
409
|
const autoInstall = window.backupAutoInstall && typeof window.backupAutoInstall === "object" ? window.backupAutoInstall : null
|
|
410
410
|
const importError = typeof window.backupImportError === "string" && window.backupImportError.trim() ? window.backupImportError.trim() : null
|
|
411
|
-
const
|
|
411
|
+
const registryEnabled = window.registryEnabled === true
|
|
412
412
|
|
|
413
413
|
const escapeHtml = (value) => {
|
|
414
414
|
const str = value == null ? '' : String(value)
|
|
@@ -509,7 +509,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
509
509
|
if (when) metaParts.push(when)
|
|
510
510
|
const metaText = metaParts.join(" · ")
|
|
511
511
|
const syncStatus = snap && snap.sync && snap.sync.status ? String(snap.sync.status) : null
|
|
512
|
-
const publishMarkup =
|
|
512
|
+
const publishMarkup = registryEnabled
|
|
513
513
|
? (syncStatus === "published"
|
|
514
514
|
? `<span class="badge">Cloud saved</span>`
|
|
515
515
|
: `<button type="button" class="url-modal-button confirm snapshot-publish" data-snapshot-id="${snap.id}">Save to Cloud</button>`)
|
|
@@ -1070,12 +1070,17 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
1070
1070
|
<div class='content backups-container'>
|
|
1071
1071
|
<h2>Checkpoints</h2>
|
|
1072
1072
|
<p class="subtitle">Local version history for workspaces on this machine.</p>
|
|
1073
|
-
<div style="margin: 10px 0 20px 0;">
|
|
1073
|
+
<div style="margin: 10px 0 20px 0; display:flex; gap:10px; flex-wrap:wrap;">
|
|
1074
1074
|
<% if (checkpointsDir) { %>
|
|
1075
1075
|
<button type="button" class="btn" data-filepath="<%= checkpointsDir %>">
|
|
1076
1076
|
<i class="fa-solid fa-folder-open"></i> Open checkpoints folder
|
|
1077
1077
|
</button>
|
|
1078
1078
|
<% } %>
|
|
1079
|
+
<% if (registryEnabled) { %>
|
|
1080
|
+
<a class="btn" href="/checkpoints/registry">
|
|
1081
|
+
<i class="fa-solid fa-link"></i> Registry settings
|
|
1082
|
+
</a>
|
|
1083
|
+
<% } %>
|
|
1079
1084
|
</div>
|
|
1080
1085
|
<% if (items && items.length > 0) { %>
|
|
1081
1086
|
<% items.forEach((item) => { %>
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<style>
|
|
7
|
+
body {
|
|
8
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
justify-content: center;
|
|
12
|
+
min-height: 100vh;
|
|
13
|
+
margin: 0;
|
|
14
|
+
background: #f5f5f5;
|
|
15
|
+
color: #0f172a;
|
|
16
|
+
}
|
|
17
|
+
.card {
|
|
18
|
+
background: white;
|
|
19
|
+
padding: 32px;
|
|
20
|
+
border-radius: 12px;
|
|
21
|
+
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.08);
|
|
22
|
+
max-width: 520px;
|
|
23
|
+
width: calc(100% - 32px);
|
|
24
|
+
}
|
|
25
|
+
h1 {
|
|
26
|
+
margin: 0 0 8px;
|
|
27
|
+
font-size: 22px;
|
|
28
|
+
}
|
|
29
|
+
.subtitle {
|
|
30
|
+
margin: 0 0 16px;
|
|
31
|
+
color: #64748b;
|
|
32
|
+
font-size: 14px;
|
|
33
|
+
}
|
|
34
|
+
label {
|
|
35
|
+
display: block;
|
|
36
|
+
font-weight: 600;
|
|
37
|
+
font-size: 13px;
|
|
38
|
+
margin-bottom: 6px;
|
|
39
|
+
}
|
|
40
|
+
input[type="url"] {
|
|
41
|
+
width: 100%;
|
|
42
|
+
padding: 10px 12px;
|
|
43
|
+
border-radius: 8px;
|
|
44
|
+
border: 1px solid #e2e8f0;
|
|
45
|
+
font-size: 14px;
|
|
46
|
+
box-sizing: border-box;
|
|
47
|
+
}
|
|
48
|
+
.hint {
|
|
49
|
+
margin-top: 6px;
|
|
50
|
+
font-size: 12px;
|
|
51
|
+
color: #94a3b8;
|
|
52
|
+
}
|
|
53
|
+
.actions {
|
|
54
|
+
margin-top: 16px;
|
|
55
|
+
display: flex;
|
|
56
|
+
gap: 10px;
|
|
57
|
+
flex-wrap: wrap;
|
|
58
|
+
}
|
|
59
|
+
.btn {
|
|
60
|
+
border: none;
|
|
61
|
+
border-radius: 8px;
|
|
62
|
+
padding: 10px 16px;
|
|
63
|
+
font-size: 14px;
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
text-decoration: none;
|
|
66
|
+
display: inline-flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
justify-content: center;
|
|
69
|
+
}
|
|
70
|
+
.btn-primary {
|
|
71
|
+
background: #2563eb;
|
|
72
|
+
color: white;
|
|
73
|
+
}
|
|
74
|
+
.btn-secondary {
|
|
75
|
+
background: #e2e8f0;
|
|
76
|
+
color: #0f172a;
|
|
77
|
+
}
|
|
78
|
+
.message {
|
|
79
|
+
margin-top: 12px;
|
|
80
|
+
font-size: 13px;
|
|
81
|
+
min-height: 18px;
|
|
82
|
+
}
|
|
83
|
+
.message.success {
|
|
84
|
+
color: #166534;
|
|
85
|
+
}
|
|
86
|
+
.message.error {
|
|
87
|
+
color: #991b1b;
|
|
88
|
+
}
|
|
89
|
+
</style>
|
|
90
|
+
</head>
|
|
91
|
+
<body>
|
|
92
|
+
<div class="card">
|
|
93
|
+
<h1>Registry Settings</h1>
|
|
94
|
+
<div class="subtitle">Set the connect URL used for linking and published checkpoints.</div>
|
|
95
|
+
<form id="registry-form">
|
|
96
|
+
<label for="registry-connect-url">Registry connect URL</label>
|
|
97
|
+
<input
|
|
98
|
+
id="registry-connect-url"
|
|
99
|
+
type="url"
|
|
100
|
+
placeholder="https://your-registry/connect/pinokio"
|
|
101
|
+
value="<%= registryUrlPrefill || registryConnectUrl || '' %>"
|
|
102
|
+
autocomplete="off"
|
|
103
|
+
/>
|
|
104
|
+
<div class="hint">Example: https://your-registry/connect/pinokio</div>
|
|
105
|
+
<div class="actions">
|
|
106
|
+
<button class="btn btn-primary" type="submit">Save</button>
|
|
107
|
+
<a class="btn btn-secondary" href="/checkpoints">Back</a>
|
|
108
|
+
</div>
|
|
109
|
+
</form>
|
|
110
|
+
<div id="registry-message" class="message" aria-live="polite"></div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<script>
|
|
114
|
+
const form = document.getElementById("registry-form");
|
|
115
|
+
const input = document.getElementById("registry-connect-url");
|
|
116
|
+
const message = document.getElementById("registry-message");
|
|
117
|
+
|
|
118
|
+
const setMessage = (text, isError) => {
|
|
119
|
+
if (!message) return;
|
|
120
|
+
message.textContent = text || "";
|
|
121
|
+
message.classList.toggle("error", !!isError);
|
|
122
|
+
message.classList.toggle("success", !isError && !!text);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
if (form) {
|
|
126
|
+
form.addEventListener("submit", async (event) => {
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
setMessage("");
|
|
129
|
+
const connectUrl = input && input.value != null ? String(input.value).trim() : "";
|
|
130
|
+
const submitBtn = form.querySelector("button[type='submit']");
|
|
131
|
+
if (submitBtn) submitBtn.disabled = true;
|
|
132
|
+
try {
|
|
133
|
+
const res = await fetch("/checkpoints/registry", {
|
|
134
|
+
method: "POST",
|
|
135
|
+
headers: { "Content-Type": "application/json", "Accept": "application/json" },
|
|
136
|
+
body: JSON.stringify({ connectUrl })
|
|
137
|
+
});
|
|
138
|
+
const payload = res.ok ? await res.json().catch(() => null) : null;
|
|
139
|
+
if (!payload || !payload.ok) {
|
|
140
|
+
setMessage("Could not save registry settings.", true);
|
|
141
|
+
} else {
|
|
142
|
+
setMessage("Saved.", false);
|
|
143
|
+
}
|
|
144
|
+
} catch (_) {
|
|
145
|
+
setMessage("Could not save registry settings.", true);
|
|
146
|
+
} finally {
|
|
147
|
+
if (submitBtn) submitBtn.disabled = false;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
</script>
|
|
152
|
+
</body>
|
|
153
|
+
</html>
|
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
<html>
|
|
2
|
-
<head>
|
|
3
|
-
<meta charset="UTF-8" />
|
|
4
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
5
|
-
<style>
|
|
6
|
-
:root {
|
|
7
|
-
--bg: #f5f5f5;
|
|
8
|
-
--card: #ffffff;
|
|
9
|
-
--text: #0f172a;
|
|
10
|
-
--muted: #64748b;
|
|
11
|
-
--border: rgba(15, 23, 42, 0.08);
|
|
12
|
-
--btn: #111827;
|
|
13
|
-
--btn-text: #ffffff;
|
|
14
|
-
--btn-secondary: #e2e8f0;
|
|
15
|
-
--btn-secondary-text: #0f172a;
|
|
16
|
-
--badge-on: #16a34a;
|
|
17
|
-
--badge-off: #dc2626;
|
|
18
|
-
}
|
|
19
|
-
body.dark {
|
|
20
|
-
--bg: #0b1120;
|
|
21
|
-
--card: #111827;
|
|
22
|
-
--text: #e2e8f0;
|
|
23
|
-
--muted: #94a3b8;
|
|
24
|
-
--border: rgba(148, 163, 184, 0.2);
|
|
25
|
-
--btn: #e2e8f0;
|
|
26
|
-
--btn-text: #0b1120;
|
|
27
|
-
--btn-secondary: #1f2937;
|
|
28
|
-
--btn-secondary-text: #e2e8f0;
|
|
29
|
-
--badge-on: #22c55e;
|
|
30
|
-
--badge-off: #f87171;
|
|
31
|
-
}
|
|
32
|
-
body {
|
|
33
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
34
|
-
background: var(--bg);
|
|
35
|
-
color: var(--text);
|
|
36
|
-
display: flex;
|
|
37
|
-
align-items: center;
|
|
38
|
-
justify-content: center;
|
|
39
|
-
min-height: 100vh;
|
|
40
|
-
margin: 0;
|
|
41
|
-
padding: 24px;
|
|
42
|
-
}
|
|
43
|
-
.card {
|
|
44
|
-
background: var(--card);
|
|
45
|
-
border: 1px solid var(--border);
|
|
46
|
-
border-radius: 16px;
|
|
47
|
-
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.08);
|
|
48
|
-
padding: 28px;
|
|
49
|
-
max-width: 520px;
|
|
50
|
-
width: 100%;
|
|
51
|
-
}
|
|
52
|
-
.title {
|
|
53
|
-
font-size: 22px;
|
|
54
|
-
font-weight: 700;
|
|
55
|
-
margin: 0 0 6px 0;
|
|
56
|
-
}
|
|
57
|
-
.subtitle {
|
|
58
|
-
margin: 0 0 18px 0;
|
|
59
|
-
color: var(--muted);
|
|
60
|
-
font-size: 14px;
|
|
61
|
-
}
|
|
62
|
-
.status-row {
|
|
63
|
-
display: flex;
|
|
64
|
-
align-items: center;
|
|
65
|
-
gap: 12px;
|
|
66
|
-
margin-bottom: 18px;
|
|
67
|
-
}
|
|
68
|
-
.field {
|
|
69
|
-
display: flex;
|
|
70
|
-
flex-direction: column;
|
|
71
|
-
gap: 6px;
|
|
72
|
-
margin-bottom: 18px;
|
|
73
|
-
}
|
|
74
|
-
.field label {
|
|
75
|
-
font-size: 13px;
|
|
76
|
-
color: var(--muted);
|
|
77
|
-
text-transform: uppercase;
|
|
78
|
-
letter-spacing: 0.08em;
|
|
79
|
-
}
|
|
80
|
-
.field input {
|
|
81
|
-
border: 1px solid var(--border);
|
|
82
|
-
background: transparent;
|
|
83
|
-
color: var(--text);
|
|
84
|
-
padding: 10px 12px;
|
|
85
|
-
border-radius: 10px;
|
|
86
|
-
font-size: 14px;
|
|
87
|
-
}
|
|
88
|
-
.field-hint {
|
|
89
|
-
font-size: 12px;
|
|
90
|
-
color: var(--muted);
|
|
91
|
-
}
|
|
92
|
-
.status-label {
|
|
93
|
-
font-size: 13px;
|
|
94
|
-
color: var(--muted);
|
|
95
|
-
text-transform: uppercase;
|
|
96
|
-
letter-spacing: 0.08em;
|
|
97
|
-
}
|
|
98
|
-
.status-badge {
|
|
99
|
-
padding: 6px 12px;
|
|
100
|
-
border-radius: 999px;
|
|
101
|
-
font-weight: 600;
|
|
102
|
-
font-size: 12px;
|
|
103
|
-
border: 1px solid transparent;
|
|
104
|
-
}
|
|
105
|
-
.status-badge.enabled {
|
|
106
|
-
color: var(--badge-on);
|
|
107
|
-
border-color: rgba(22, 163, 74, 0.3);
|
|
108
|
-
background: rgba(22, 163, 74, 0.12);
|
|
109
|
-
}
|
|
110
|
-
.status-badge.disabled {
|
|
111
|
-
color: var(--badge-off);
|
|
112
|
-
border-color: rgba(220, 38, 38, 0.3);
|
|
113
|
-
background: rgba(220, 38, 38, 0.12);
|
|
114
|
-
}
|
|
115
|
-
.actions {
|
|
116
|
-
display: flex;
|
|
117
|
-
gap: 10px;
|
|
118
|
-
flex-wrap: wrap;
|
|
119
|
-
}
|
|
120
|
-
.btn {
|
|
121
|
-
border: none;
|
|
122
|
-
border-radius: 10px;
|
|
123
|
-
padding: 10px 16px;
|
|
124
|
-
font-weight: 600;
|
|
125
|
-
cursor: pointer;
|
|
126
|
-
transition: transform 0.05s ease, opacity 0.15s ease;
|
|
127
|
-
}
|
|
128
|
-
.btn:disabled {
|
|
129
|
-
opacity: 0.6;
|
|
130
|
-
cursor: not-allowed;
|
|
131
|
-
}
|
|
132
|
-
.btn:active {
|
|
133
|
-
transform: translateY(1px);
|
|
134
|
-
}
|
|
135
|
-
.btn-primary {
|
|
136
|
-
background: var(--btn);
|
|
137
|
-
color: var(--btn-text);
|
|
138
|
-
}
|
|
139
|
-
.btn-secondary {
|
|
140
|
-
background: var(--btn-secondary);
|
|
141
|
-
color: var(--btn-secondary-text);
|
|
142
|
-
}
|
|
143
|
-
.note {
|
|
144
|
-
margin-top: 16px;
|
|
145
|
-
font-size: 13px;
|
|
146
|
-
color: var(--muted);
|
|
147
|
-
}
|
|
148
|
-
.message {
|
|
149
|
-
margin-top: 10px;
|
|
150
|
-
font-size: 13px;
|
|
151
|
-
min-height: 18px;
|
|
152
|
-
}
|
|
153
|
-
.message.success {
|
|
154
|
-
color: var(--badge-on);
|
|
155
|
-
}
|
|
156
|
-
.message.error {
|
|
157
|
-
color: var(--badge-off);
|
|
158
|
-
}
|
|
159
|
-
</style>
|
|
160
|
-
</head>
|
|
161
|
-
<body class="<%= theme %>" data-agent="<%= agent %>">
|
|
162
|
-
<div class="card">
|
|
163
|
-
<div class="title">Registry Beta</div>
|
|
164
|
-
<div class="subtitle">Enable or disable registry publish features for this Pinokio install.</div>
|
|
165
|
-
<div class="status-row">
|
|
166
|
-
<div class="status-label">Status</div>
|
|
167
|
-
<div
|
|
168
|
-
id="registry-beta-status"
|
|
169
|
-
class="status-badge <%= registryBetaEnabled ? 'enabled' : 'disabled' %>"
|
|
170
|
-
data-enabled="<%= registryBetaEnabled ? '1' : '0' %>"
|
|
171
|
-
>
|
|
172
|
-
<%= registryBetaEnabled ? 'Enabled' : 'Disabled' %>
|
|
173
|
-
</div>
|
|
174
|
-
</div>
|
|
175
|
-
<div class="field">
|
|
176
|
-
<label for="registry-connect-url">Registry connect URL</label>
|
|
177
|
-
<input
|
|
178
|
-
id="registry-connect-url"
|
|
179
|
-
type="url"
|
|
180
|
-
placeholder="http://localhost:3000/connect/pinokio"
|
|
181
|
-
value="<%= registryUrlPrefill || registryConnectUrl || '' %>"
|
|
182
|
-
autocomplete="off"
|
|
183
|
-
/>
|
|
184
|
-
<div class="field-hint">Used for connect prompts and published links.</div>
|
|
185
|
-
</div>
|
|
186
|
-
<div class="actions">
|
|
187
|
-
<button id="registry-beta-enable" class="btn btn-primary" type="button">Enable</button>
|
|
188
|
-
<button id="registry-beta-disable" class="btn btn-secondary" type="button">Disable</button>
|
|
189
|
-
</div>
|
|
190
|
-
<div class="note">Writes PINOKIO_REGISTRY_BETA and PINOKIO_REGISTRY_CONNECT_URL in ENVIRONMENT. Reload other pages to see changes.</div>
|
|
191
|
-
<div id="registry-beta-message" class="message" aria-live="polite"></div>
|
|
192
|
-
</div>
|
|
193
|
-
<script>
|
|
194
|
-
document.addEventListener("DOMContentLoaded", () => {
|
|
195
|
-
const statusEl = document.getElementById("registry-beta-status")
|
|
196
|
-
const messageEl = document.getElementById("registry-beta-message")
|
|
197
|
-
const enableBtn = document.getElementById("registry-beta-enable")
|
|
198
|
-
const disableBtn = document.getElementById("registry-beta-disable")
|
|
199
|
-
const connectInput = document.getElementById("registry-connect-url")
|
|
200
|
-
if (!statusEl || !enableBtn || !disableBtn) return
|
|
201
|
-
|
|
202
|
-
let enabled = statusEl.getAttribute("data-enabled") === "1"
|
|
203
|
-
|
|
204
|
-
const setMessage = (text, isError) => {
|
|
205
|
-
if (!messageEl) return
|
|
206
|
-
messageEl.textContent = text || ""
|
|
207
|
-
messageEl.classList.toggle("error", !!isError)
|
|
208
|
-
messageEl.classList.toggle("success", !isError && !!text)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const setState = (next) => {
|
|
212
|
-
enabled = !!next
|
|
213
|
-
statusEl.textContent = enabled ? "Enabled" : "Disabled"
|
|
214
|
-
statusEl.setAttribute("data-enabled", enabled ? "1" : "0")
|
|
215
|
-
statusEl.classList.toggle("enabled", enabled)
|
|
216
|
-
statusEl.classList.toggle("disabled", !enabled)
|
|
217
|
-
enableBtn.textContent = enabled ? "Save" : "Enable"
|
|
218
|
-
enableBtn.disabled = false
|
|
219
|
-
disableBtn.disabled = !enabled
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const update = async (next, includeEndpoint) => {
|
|
223
|
-
setMessage("")
|
|
224
|
-
enableBtn.disabled = true
|
|
225
|
-
disableBtn.disabled = true
|
|
226
|
-
try {
|
|
227
|
-
const connectUrl = connectInput ? String(connectInput.value || "").trim() : ""
|
|
228
|
-
const res = await fetch("/checkpoints/registry_beta", {
|
|
229
|
-
method: "POST",
|
|
230
|
-
headers: {
|
|
231
|
-
"Content-Type": "application/json",
|
|
232
|
-
"Accept": "application/json"
|
|
233
|
-
},
|
|
234
|
-
body: JSON.stringify({
|
|
235
|
-
enabled: next ? "1" : "0",
|
|
236
|
-
...(includeEndpoint ? { connectUrl } : {})
|
|
237
|
-
})
|
|
238
|
-
})
|
|
239
|
-
const payload = res.ok ? await res.json().catch(() => null) : null
|
|
240
|
-
if (!payload || !payload.ok) {
|
|
241
|
-
throw new Error("Update failed")
|
|
242
|
-
}
|
|
243
|
-
setState(!!payload.enabled)
|
|
244
|
-
setMessage("Updated.")
|
|
245
|
-
} catch (_) {
|
|
246
|
-
setState(enabled)
|
|
247
|
-
setMessage("Could not update setting.", true)
|
|
248
|
-
} finally {
|
|
249
|
-
enableBtn.disabled = enabled
|
|
250
|
-
disableBtn.disabled = !enabled
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
setState(enabled)
|
|
255
|
-
enableBtn.addEventListener("click", () => update(true, true))
|
|
256
|
-
disableBtn.addEventListener("click", () => update(false, true))
|
|
257
|
-
})
|
|
258
|
-
</script>
|
|
259
|
-
</body>
|
|
260
|
-
</html>
|