codexapp 0.1.32 → 0.1.34
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/dist/assets/index-5eZebBiF.js +1442 -0
- package/dist/assets/index-zaSJxL5w.css +1 -0
- package/dist/index.html +2 -2
- package/dist-cli/index.js +786 -15
- package/dist-cli/index.js.map +1 -1
- package/package.json +2 -1
- package/dist/assets/index-Bpwp-Qy6.js +0 -48
- package/dist/assets/index-C-Tj2KL5.css +0 -1
package/dist-cli/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { createServer as createServer2 } from "http";
|
|
5
|
-
import { chmodSync, createWriteStream, existsSync as
|
|
5
|
+
import { chmodSync, createWriteStream, existsSync as existsSync3, mkdirSync } from "fs";
|
|
6
6
|
import { readFile as readFile3 } from "fs/promises";
|
|
7
7
|
import { homedir as homedir2, networkInterfaces } from "os";
|
|
8
8
|
import { join as join4 } from "path";
|
|
@@ -17,14 +17,15 @@ import qrcode from "qrcode-terminal";
|
|
|
17
17
|
// src/server/httpServer.ts
|
|
18
18
|
import { fileURLToPath } from "url";
|
|
19
19
|
import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join3 } from "path";
|
|
20
|
-
import { existsSync } from "fs";
|
|
20
|
+
import { existsSync as existsSync2 } from "fs";
|
|
21
21
|
import { writeFile as writeFile2, stat as stat3 } from "fs/promises";
|
|
22
22
|
import express from "express";
|
|
23
23
|
|
|
24
24
|
// src/server/codexAppServerBridge.ts
|
|
25
25
|
import { spawn } from "child_process";
|
|
26
26
|
import { randomBytes } from "crypto";
|
|
27
|
-
import { mkdtemp, readFile, readdir, rm, mkdir, stat } from "fs/promises";
|
|
27
|
+
import { mkdtemp, readFile, readdir, rm, mkdir, stat, lstat, readlink, symlink } from "fs/promises";
|
|
28
|
+
import { existsSync } from "fs";
|
|
28
29
|
import { request as httpsRequest } from "https";
|
|
29
30
|
import { homedir } from "os";
|
|
30
31
|
import { tmpdir } from "os";
|
|
@@ -188,7 +189,7 @@ async function ensureRepoHasInitialCommit(repoRoot) {
|
|
|
188
189
|
);
|
|
189
190
|
}
|
|
190
191
|
async function runCommandCapture(command, args, options = {}) {
|
|
191
|
-
return await new Promise((
|
|
192
|
+
return await new Promise((resolve2, reject) => {
|
|
192
193
|
const proc = spawn(command, args, {
|
|
193
194
|
cwd: options.cwd,
|
|
194
195
|
env: process.env,
|
|
@@ -205,7 +206,34 @@ async function runCommandCapture(command, args, options = {}) {
|
|
|
205
206
|
proc.on("error", reject);
|
|
206
207
|
proc.on("close", (code) => {
|
|
207
208
|
if (code === 0) {
|
|
208
|
-
|
|
209
|
+
resolve2(stdout.trim());
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
213
|
+
const suffix = details.length > 0 ? `: ${details}` : "";
|
|
214
|
+
reject(new Error(`Command failed (${command} ${args.join(" ")})${suffix}`));
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
async function runCommandWithOutput(command, args, options = {}) {
|
|
219
|
+
return await new Promise((resolve2, reject) => {
|
|
220
|
+
const proc = spawn(command, args, {
|
|
221
|
+
cwd: options.cwd,
|
|
222
|
+
env: process.env,
|
|
223
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
224
|
+
});
|
|
225
|
+
let stdout = "";
|
|
226
|
+
let stderr = "";
|
|
227
|
+
proc.stdout.on("data", (chunk) => {
|
|
228
|
+
stdout += chunk.toString();
|
|
229
|
+
});
|
|
230
|
+
proc.stderr.on("data", (chunk) => {
|
|
231
|
+
stderr += chunk.toString();
|
|
232
|
+
});
|
|
233
|
+
proc.on("error", reject);
|
|
234
|
+
proc.on("close", (code) => {
|
|
235
|
+
if (code === 0) {
|
|
236
|
+
resolve2(stdout.trim());
|
|
209
237
|
return;
|
|
210
238
|
}
|
|
211
239
|
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
@@ -271,7 +299,7 @@ async function fetchSkillsTree() {
|
|
|
271
299
|
if (skillsTreeCache && Date.now() - skillsTreeCache.fetchedAt < TREE_CACHE_TTL_MS) {
|
|
272
300
|
return skillsTreeCache.entries;
|
|
273
301
|
}
|
|
274
|
-
const resp = await ghFetch(
|
|
302
|
+
const resp = await ghFetch(`https://api.github.com/repos/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/git/trees/main?recursive=1`);
|
|
275
303
|
if (!resp.ok) throw new Error(`GitHub tree API returned ${resp.status}`);
|
|
276
304
|
const data = await resp.json();
|
|
277
305
|
const metaPattern = /^skills\/([^/]+)\/([^/]+)\/_meta\.json$/;
|
|
@@ -287,7 +315,7 @@ async function fetchSkillsTree() {
|
|
|
287
315
|
entries.push({
|
|
288
316
|
name: skillName,
|
|
289
317
|
owner,
|
|
290
|
-
url: `https://github.com/
|
|
318
|
+
url: `https://github.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/tree/main/skills/${owner}/${skillName}`
|
|
291
319
|
});
|
|
292
320
|
}
|
|
293
321
|
skillsTreeCache = { entries, fetchedAt: Date.now() };
|
|
@@ -299,7 +327,7 @@ async function fetchMetaBatch(entries) {
|
|
|
299
327
|
const batch = toFetch.slice(0, 50);
|
|
300
328
|
const results = await Promise.allSettled(
|
|
301
329
|
batch.map(async (e) => {
|
|
302
|
-
const rawUrl = `https://raw.githubusercontent.com/
|
|
330
|
+
const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${e.owner}/${e.name}/_meta.json`;
|
|
303
331
|
const resp = await fetch(rawUrl);
|
|
304
332
|
if (!resp.ok) return;
|
|
305
333
|
const meta = await resp.json();
|
|
@@ -325,6 +353,23 @@ function buildHubEntry(e) {
|
|
|
325
353
|
installed: false
|
|
326
354
|
};
|
|
327
355
|
}
|
|
356
|
+
var GITHUB_DEVICE_CLIENT_ID = "Iv1.b507a08c87ecfe98";
|
|
357
|
+
var DEFAULT_SKILLS_SYNC_REPO_NAME = "codexskills";
|
|
358
|
+
var SKILLS_SYNC_MANIFEST_PATH = "installed-skills.json";
|
|
359
|
+
var SYNC_UPSTREAM_SKILLS_OWNER = "OpenClawAndroid";
|
|
360
|
+
var SYNC_UPSTREAM_SKILLS_REPO = "skills";
|
|
361
|
+
var HUB_SKILLS_OWNER = "openclaw";
|
|
362
|
+
var HUB_SKILLS_REPO = "skills";
|
|
363
|
+
var startupSkillsSyncInitialized = false;
|
|
364
|
+
var startupSyncStatus = {
|
|
365
|
+
inProgress: false,
|
|
366
|
+
mode: "idle",
|
|
367
|
+
branch: getPreferredSyncBranch(),
|
|
368
|
+
lastAction: "not-started",
|
|
369
|
+
lastRunAtIso: "",
|
|
370
|
+
lastSuccessAtIso: "",
|
|
371
|
+
lastError: ""
|
|
372
|
+
};
|
|
328
373
|
async function scanInstalledSkillsFromDisk() {
|
|
329
374
|
const map = /* @__PURE__ */ new Map();
|
|
330
375
|
const skillsDir = getSkillsInstallDir();
|
|
@@ -343,6 +388,538 @@ async function scanInstalledSkillsFromDisk() {
|
|
|
343
388
|
}
|
|
344
389
|
return map;
|
|
345
390
|
}
|
|
391
|
+
function getSkillsSyncStatePath() {
|
|
392
|
+
return join(getCodexHomeDir(), "skills-sync.json");
|
|
393
|
+
}
|
|
394
|
+
async function readSkillsSyncState() {
|
|
395
|
+
try {
|
|
396
|
+
const raw = await readFile(getSkillsSyncStatePath(), "utf8");
|
|
397
|
+
const parsed = JSON.parse(raw);
|
|
398
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
399
|
+
} catch {
|
|
400
|
+
return {};
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async function writeSkillsSyncState(state) {
|
|
404
|
+
await writeFile(getSkillsSyncStatePath(), JSON.stringify(state), "utf8");
|
|
405
|
+
}
|
|
406
|
+
async function getGithubJson(url, token, method = "GET", body) {
|
|
407
|
+
const resp = await fetch(url, {
|
|
408
|
+
method,
|
|
409
|
+
headers: {
|
|
410
|
+
Accept: "application/vnd.github+json",
|
|
411
|
+
"Content-Type": "application/json",
|
|
412
|
+
Authorization: `Bearer ${token}`,
|
|
413
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
414
|
+
"User-Agent": "codex-web-local"
|
|
415
|
+
},
|
|
416
|
+
body: body ? JSON.stringify(body) : void 0
|
|
417
|
+
});
|
|
418
|
+
if (!resp.ok) {
|
|
419
|
+
const text = await resp.text();
|
|
420
|
+
throw new Error(`GitHub API ${method} ${url} failed (${resp.status}): ${text}`);
|
|
421
|
+
}
|
|
422
|
+
return await resp.json();
|
|
423
|
+
}
|
|
424
|
+
async function startGithubDeviceLogin() {
|
|
425
|
+
const resp = await fetch("https://github.com/login/device/code", {
|
|
426
|
+
method: "POST",
|
|
427
|
+
headers: {
|
|
428
|
+
Accept: "application/json",
|
|
429
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
430
|
+
"User-Agent": "codex-web-local"
|
|
431
|
+
},
|
|
432
|
+
body: new URLSearchParams({
|
|
433
|
+
client_id: GITHUB_DEVICE_CLIENT_ID,
|
|
434
|
+
scope: "repo read:user"
|
|
435
|
+
})
|
|
436
|
+
});
|
|
437
|
+
if (!resp.ok) {
|
|
438
|
+
throw new Error(`GitHub device flow init failed (${resp.status})`);
|
|
439
|
+
}
|
|
440
|
+
return await resp.json();
|
|
441
|
+
}
|
|
442
|
+
async function completeGithubDeviceLogin(deviceCode) {
|
|
443
|
+
const resp = await fetch("https://github.com/login/oauth/access_token", {
|
|
444
|
+
method: "POST",
|
|
445
|
+
headers: {
|
|
446
|
+
Accept: "application/json",
|
|
447
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
448
|
+
"User-Agent": "codex-web-local"
|
|
449
|
+
},
|
|
450
|
+
body: new URLSearchParams({
|
|
451
|
+
client_id: GITHUB_DEVICE_CLIENT_ID,
|
|
452
|
+
device_code: deviceCode,
|
|
453
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
454
|
+
})
|
|
455
|
+
});
|
|
456
|
+
if (!resp.ok) {
|
|
457
|
+
throw new Error(`GitHub token exchange failed (${resp.status})`);
|
|
458
|
+
}
|
|
459
|
+
const payload = await resp.json();
|
|
460
|
+
if (!payload.access_token) return { token: null, error: payload.error || "unknown_error" };
|
|
461
|
+
return { token: payload.access_token, error: null };
|
|
462
|
+
}
|
|
463
|
+
function isAndroidLikeRuntime() {
|
|
464
|
+
if (process.platform === "android") return true;
|
|
465
|
+
if (existsSync("/data/data/com.termux")) return true;
|
|
466
|
+
if (process.env.TERMUX_VERSION) return true;
|
|
467
|
+
const prefix = process.env.PREFIX?.toLowerCase() ?? "";
|
|
468
|
+
if (prefix.includes("/com.termux/")) return true;
|
|
469
|
+
const proot = process.env.PROOT_TMP_DIR?.toLowerCase() ?? "";
|
|
470
|
+
if (proot.length > 0) return true;
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
function getPreferredSyncBranch() {
|
|
474
|
+
return isAndroidLikeRuntime() ? "android" : "main";
|
|
475
|
+
}
|
|
476
|
+
function isUpstreamSkillsRepo(repoOwner, repoName) {
|
|
477
|
+
return repoOwner.toLowerCase() === SYNC_UPSTREAM_SKILLS_OWNER.toLowerCase() && repoName.toLowerCase() === SYNC_UPSTREAM_SKILLS_REPO.toLowerCase();
|
|
478
|
+
}
|
|
479
|
+
async function resolveGithubUsername(token) {
|
|
480
|
+
const user = await getGithubJson("https://api.github.com/user", token);
|
|
481
|
+
return user.login;
|
|
482
|
+
}
|
|
483
|
+
async function ensurePrivateForkFromUpstream(token, username, repoName) {
|
|
484
|
+
const repoUrl = `https://api.github.com/repos/${username}/${repoName}`;
|
|
485
|
+
let created = false;
|
|
486
|
+
const existing = await fetch(repoUrl, {
|
|
487
|
+
headers: {
|
|
488
|
+
Accept: "application/vnd.github+json",
|
|
489
|
+
Authorization: `Bearer ${token}`,
|
|
490
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
491
|
+
"User-Agent": "codex-web-local"
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
if (existing.ok) {
|
|
495
|
+
const details = await existing.json();
|
|
496
|
+
if (details.private === true) return;
|
|
497
|
+
await getGithubJson(repoUrl, token, "PATCH", { private: true });
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (existing.status !== 404) {
|
|
501
|
+
throw new Error(`Failed to check personal repo existence (${existing.status})`);
|
|
502
|
+
}
|
|
503
|
+
await getGithubJson(
|
|
504
|
+
"https://api.github.com/user/repos",
|
|
505
|
+
token,
|
|
506
|
+
"POST",
|
|
507
|
+
{ name: repoName, private: true, auto_init: false, description: "Codex skills private mirror sync" }
|
|
508
|
+
);
|
|
509
|
+
created = true;
|
|
510
|
+
let ready = false;
|
|
511
|
+
for (let i = 0; i < 20; i++) {
|
|
512
|
+
const check = await fetch(repoUrl, {
|
|
513
|
+
headers: {
|
|
514
|
+
Accept: "application/vnd.github+json",
|
|
515
|
+
Authorization: `Bearer ${token}`,
|
|
516
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
517
|
+
"User-Agent": "codex-web-local"
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
if (check.ok) {
|
|
521
|
+
ready = true;
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
await new Promise((resolve2) => setTimeout(resolve2, 1e3));
|
|
525
|
+
}
|
|
526
|
+
if (!ready) throw new Error("Private mirror repo was created but is not available yet");
|
|
527
|
+
if (!created) return;
|
|
528
|
+
const tmp = await mkdtemp(join(tmpdir(), "codex-skills-seed-"));
|
|
529
|
+
try {
|
|
530
|
+
const upstreamUrl = `https://github.com/${SYNC_UPSTREAM_SKILLS_OWNER}/${SYNC_UPSTREAM_SKILLS_REPO}.git`;
|
|
531
|
+
const branch = getPreferredSyncBranch();
|
|
532
|
+
try {
|
|
533
|
+
await runCommand("git", ["clone", "--depth", "1", "--single-branch", "--branch", branch, upstreamUrl, tmp]);
|
|
534
|
+
} catch {
|
|
535
|
+
await runCommand("git", ["clone", "--depth", "1", upstreamUrl, tmp]);
|
|
536
|
+
}
|
|
537
|
+
const privateRemote = toGitHubTokenRemote(username, repoName, token);
|
|
538
|
+
await runCommand("git", ["remote", "set-url", "origin", privateRemote], { cwd: tmp });
|
|
539
|
+
try {
|
|
540
|
+
await runCommand("git", ["checkout", "-B", branch], { cwd: tmp });
|
|
541
|
+
} catch {
|
|
542
|
+
}
|
|
543
|
+
await runCommand("git", ["push", "-u", "origin", `HEAD:${branch}`], { cwd: tmp });
|
|
544
|
+
} finally {
|
|
545
|
+
await rm(tmp, { recursive: true, force: true });
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
async function readRemoteSkillsManifest(token, repoOwner, repoName) {
|
|
549
|
+
const url = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/${SKILLS_SYNC_MANIFEST_PATH}`;
|
|
550
|
+
const resp = await fetch(url, {
|
|
551
|
+
headers: {
|
|
552
|
+
Accept: "application/vnd.github+json",
|
|
553
|
+
Authorization: `Bearer ${token}`,
|
|
554
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
555
|
+
"User-Agent": "codex-web-local"
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
if (resp.status === 404) return [];
|
|
559
|
+
if (!resp.ok) throw new Error(`Failed to read remote manifest (${resp.status})`);
|
|
560
|
+
const payload = await resp.json();
|
|
561
|
+
const content = payload.content ? Buffer.from(payload.content.replace(/\n/g, ""), "base64").toString("utf8") : "[]";
|
|
562
|
+
const parsed = JSON.parse(content);
|
|
563
|
+
if (!Array.isArray(parsed)) return [];
|
|
564
|
+
const skills = [];
|
|
565
|
+
for (const row of parsed) {
|
|
566
|
+
const item = asRecord(row);
|
|
567
|
+
const owner = typeof item?.owner === "string" ? item.owner : "";
|
|
568
|
+
const name = typeof item?.name === "string" ? item.name : "";
|
|
569
|
+
if (!name) continue;
|
|
570
|
+
skills.push({ ...owner ? { owner } : {}, name, enabled: item?.enabled !== false });
|
|
571
|
+
}
|
|
572
|
+
return skills;
|
|
573
|
+
}
|
|
574
|
+
async function writeRemoteSkillsManifest(token, repoOwner, repoName, skills) {
|
|
575
|
+
const url = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/${SKILLS_SYNC_MANIFEST_PATH}`;
|
|
576
|
+
let sha = "";
|
|
577
|
+
const existing = await fetch(url, {
|
|
578
|
+
headers: {
|
|
579
|
+
Accept: "application/vnd.github+json",
|
|
580
|
+
Authorization: `Bearer ${token}`,
|
|
581
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
582
|
+
"User-Agent": "codex-web-local"
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
if (existing.ok) {
|
|
586
|
+
const payload = await existing.json();
|
|
587
|
+
sha = payload.sha ?? "";
|
|
588
|
+
}
|
|
589
|
+
const content = Buffer.from(JSON.stringify(skills, null, 2), "utf8").toString("base64");
|
|
590
|
+
await getGithubJson(url, token, "PUT", {
|
|
591
|
+
message: "Update synced skills manifest",
|
|
592
|
+
content,
|
|
593
|
+
...sha ? { sha } : {}
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
function toGitHubTokenRemote(repoOwner, repoName, token) {
|
|
597
|
+
return `https://x-access-token:${encodeURIComponent(token)}@github.com/${repoOwner}/${repoName}.git`;
|
|
598
|
+
}
|
|
599
|
+
async function ensureSkillsWorkingTreeRepo(repoUrl, branch) {
|
|
600
|
+
const localDir = getSkillsInstallDir();
|
|
601
|
+
await mkdir(localDir, { recursive: true });
|
|
602
|
+
const gitDir = join(localDir, ".git");
|
|
603
|
+
let hasGitDir = false;
|
|
604
|
+
try {
|
|
605
|
+
hasGitDir = (await stat(gitDir)).isDirectory();
|
|
606
|
+
} catch {
|
|
607
|
+
hasGitDir = false;
|
|
608
|
+
}
|
|
609
|
+
if (!hasGitDir) {
|
|
610
|
+
await runCommand("git", ["init"], { cwd: localDir });
|
|
611
|
+
await runCommand("git", ["config", "user.email", "skills-sync@local"], { cwd: localDir });
|
|
612
|
+
await runCommand("git", ["config", "user.name", "Skills Sync"], { cwd: localDir });
|
|
613
|
+
await runCommand("git", ["add", "-A"], { cwd: localDir });
|
|
614
|
+
try {
|
|
615
|
+
await runCommand("git", ["commit", "-m", "Local skills snapshot before sync"], { cwd: localDir });
|
|
616
|
+
} catch {
|
|
617
|
+
}
|
|
618
|
+
await runCommand("git", ["branch", "-M", branch], { cwd: localDir });
|
|
619
|
+
try {
|
|
620
|
+
await runCommand("git", ["remote", "add", "origin", repoUrl], { cwd: localDir });
|
|
621
|
+
} catch {
|
|
622
|
+
await runCommand("git", ["remote", "set-url", "origin", repoUrl], { cwd: localDir });
|
|
623
|
+
}
|
|
624
|
+
await runCommand("git", ["fetch", "origin"], { cwd: localDir });
|
|
625
|
+
try {
|
|
626
|
+
await runCommand("git", ["merge", "--allow-unrelated-histories", "--no-edit", `origin/${branch}`], { cwd: localDir });
|
|
627
|
+
} catch {
|
|
628
|
+
}
|
|
629
|
+
return localDir;
|
|
630
|
+
}
|
|
631
|
+
await runCommand("git", ["remote", "set-url", "origin", repoUrl], { cwd: localDir });
|
|
632
|
+
await runCommand("git", ["fetch", "origin"], { cwd: localDir });
|
|
633
|
+
await resolveMergeConflictsByNewerCommit(localDir, branch);
|
|
634
|
+
try {
|
|
635
|
+
await runCommand("git", ["checkout", branch], { cwd: localDir });
|
|
636
|
+
} catch {
|
|
637
|
+
await resolveMergeConflictsByNewerCommit(localDir, branch);
|
|
638
|
+
await runCommand("git", ["checkout", "-B", branch], { cwd: localDir });
|
|
639
|
+
}
|
|
640
|
+
await resolveMergeConflictsByNewerCommit(localDir, branch);
|
|
641
|
+
const localMtimesBeforePull = await snapshotFileMtimes(localDir);
|
|
642
|
+
try {
|
|
643
|
+
await runCommand("git", ["stash", "push", "--include-untracked", "-m", "codex-skills-autostash"], { cwd: localDir });
|
|
644
|
+
} catch {
|
|
645
|
+
}
|
|
646
|
+
let pulledMtimes = /* @__PURE__ */ new Map();
|
|
647
|
+
try {
|
|
648
|
+
await runCommand("git", ["pull", "--no-rebase", "origin", branch], { cwd: localDir });
|
|
649
|
+
pulledMtimes = await snapshotFileMtimes(localDir);
|
|
650
|
+
} catch {
|
|
651
|
+
await resolveMergeConflictsByNewerCommit(localDir, branch);
|
|
652
|
+
pulledMtimes = await snapshotFileMtimes(localDir);
|
|
653
|
+
}
|
|
654
|
+
try {
|
|
655
|
+
await runCommand("git", ["stash", "pop"], { cwd: localDir });
|
|
656
|
+
} catch {
|
|
657
|
+
await resolveStashPopConflictsByFileTime(localDir, localMtimesBeforePull, pulledMtimes);
|
|
658
|
+
}
|
|
659
|
+
return localDir;
|
|
660
|
+
}
|
|
661
|
+
async function resolveMergeConflictsByNewerCommit(repoDir, branch) {
|
|
662
|
+
const unmerged = (await runCommandWithOutput("git", ["diff", "--name-only", "--diff-filter=U"], { cwd: repoDir })).split(/\r?\n/).map((row) => row.trim()).filter(Boolean);
|
|
663
|
+
if (unmerged.length === 0) return;
|
|
664
|
+
for (const path of unmerged) {
|
|
665
|
+
const oursTime = await getCommitTime(repoDir, "HEAD", path);
|
|
666
|
+
const theirsTime = await getCommitTime(repoDir, `origin/${branch}`, path);
|
|
667
|
+
if (theirsTime > oursTime) {
|
|
668
|
+
await runCommand("git", ["checkout", "--theirs", "--", path], { cwd: repoDir });
|
|
669
|
+
} else {
|
|
670
|
+
await runCommand("git", ["checkout", "--ours", "--", path], { cwd: repoDir });
|
|
671
|
+
}
|
|
672
|
+
await runCommand("git", ["add", "--", path], { cwd: repoDir });
|
|
673
|
+
}
|
|
674
|
+
const mergeHead = (await runCommandWithOutput("git", ["rev-parse", "-q", "--verify", "MERGE_HEAD"], { cwd: repoDir })).trim();
|
|
675
|
+
if (mergeHead) {
|
|
676
|
+
await runCommand("git", ["commit", "-m", "Auto-resolve skills merge by newer file"], { cwd: repoDir });
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
async function getCommitTime(repoDir, ref, path) {
|
|
680
|
+
try {
|
|
681
|
+
const output = (await runCommandWithOutput("git", ["log", "-1", "--format=%ct", ref, "--", path], { cwd: repoDir })).trim();
|
|
682
|
+
return output ? Number.parseInt(output, 10) : 0;
|
|
683
|
+
} catch {
|
|
684
|
+
return 0;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
async function resolveStashPopConflictsByFileTime(repoDir, localMtimesBeforePull, pulledMtimes) {
|
|
688
|
+
const unmerged = (await runCommandWithOutput("git", ["diff", "--name-only", "--diff-filter=U"], { cwd: repoDir })).split(/\r?\n/).map((row) => row.trim()).filter(Boolean);
|
|
689
|
+
if (unmerged.length === 0) return;
|
|
690
|
+
for (const path of unmerged) {
|
|
691
|
+
const localMtime = localMtimesBeforePull.get(path) ?? 0;
|
|
692
|
+
const pulledMtime = pulledMtimes.get(path) ?? 0;
|
|
693
|
+
const side = localMtime >= pulledMtime ? "--theirs" : "--ours";
|
|
694
|
+
await runCommand("git", ["checkout", side, "--", path], { cwd: repoDir });
|
|
695
|
+
await runCommand("git", ["add", "--", path], { cwd: repoDir });
|
|
696
|
+
}
|
|
697
|
+
const mergeHead = (await runCommandWithOutput("git", ["rev-parse", "-q", "--verify", "MERGE_HEAD"], { cwd: repoDir })).trim();
|
|
698
|
+
if (mergeHead) {
|
|
699
|
+
await runCommand("git", ["commit", "-m", "Auto-resolve stash-pop conflicts by file time"], { cwd: repoDir });
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
async function snapshotFileMtimes(dir) {
|
|
703
|
+
const mtimes = /* @__PURE__ */ new Map();
|
|
704
|
+
await walkFileMtimes(dir, dir, mtimes);
|
|
705
|
+
return mtimes;
|
|
706
|
+
}
|
|
707
|
+
async function walkFileMtimes(rootDir, currentDir, out) {
|
|
708
|
+
let entries;
|
|
709
|
+
try {
|
|
710
|
+
entries = await readdir(currentDir, { withFileTypes: true });
|
|
711
|
+
} catch {
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
for (const entry of entries) {
|
|
715
|
+
const entryName = String(entry.name);
|
|
716
|
+
if (entryName === ".git") continue;
|
|
717
|
+
const absolutePath = join(currentDir, entryName);
|
|
718
|
+
const relativePath = absolutePath.slice(rootDir.length + 1);
|
|
719
|
+
if (entry.isDirectory()) {
|
|
720
|
+
await walkFileMtimes(rootDir, absolutePath, out);
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
if (!entry.isFile()) continue;
|
|
724
|
+
try {
|
|
725
|
+
const info = await stat(absolutePath);
|
|
726
|
+
out.set(relativePath, info.mtimeMs);
|
|
727
|
+
} catch {
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
async function syncInstalledSkillsFolderToRepo(token, repoOwner, repoName, _installedMap) {
|
|
732
|
+
const remoteUrl = toGitHubTokenRemote(repoOwner, repoName, token);
|
|
733
|
+
const branch = getPreferredSyncBranch();
|
|
734
|
+
const repoDir = await ensureSkillsWorkingTreeRepo(remoteUrl, branch);
|
|
735
|
+
const addPaths = ["."];
|
|
736
|
+
void _installedMap;
|
|
737
|
+
await runCommand("git", ["config", "user.email", "skills-sync@local"], { cwd: repoDir });
|
|
738
|
+
await runCommand("git", ["config", "user.name", "Skills Sync"], { cwd: repoDir });
|
|
739
|
+
await runCommand("git", ["add", ...addPaths], { cwd: repoDir });
|
|
740
|
+
const status = (await runCommandWithOutput("git", ["status", "--porcelain"], { cwd: repoDir })).trim();
|
|
741
|
+
if (!status) return;
|
|
742
|
+
await runCommand("git", ["commit", "-m", "Sync installed skills folder and manifest"], { cwd: repoDir });
|
|
743
|
+
await runCommand("git", ["push", "origin", `HEAD:${branch}`], { cwd: repoDir });
|
|
744
|
+
}
|
|
745
|
+
async function pullInstalledSkillsFolderFromRepo(token, repoOwner, repoName, _localSkillsDir) {
|
|
746
|
+
const remoteUrl = toGitHubTokenRemote(repoOwner, repoName, token);
|
|
747
|
+
const branch = getPreferredSyncBranch();
|
|
748
|
+
await ensureSkillsWorkingTreeRepo(remoteUrl, branch);
|
|
749
|
+
}
|
|
750
|
+
async function bootstrapSkillsFromUpstreamIntoLocal(_localSkillsDir) {
|
|
751
|
+
void _localSkillsDir;
|
|
752
|
+
const repoUrl = `https://github.com/${SYNC_UPSTREAM_SKILLS_OWNER}/${SYNC_UPSTREAM_SKILLS_REPO}.git`;
|
|
753
|
+
const branch = getPreferredSyncBranch();
|
|
754
|
+
await ensureSkillsWorkingTreeRepo(repoUrl, branch);
|
|
755
|
+
}
|
|
756
|
+
async function collectLocalSyncedSkills(appServer) {
|
|
757
|
+
const state = await readSkillsSyncState();
|
|
758
|
+
const owners = { ...state.installedOwners ?? {} };
|
|
759
|
+
const tree = await fetchSkillsTree();
|
|
760
|
+
const uniqueOwnerByName = /* @__PURE__ */ new Map();
|
|
761
|
+
const ambiguousNames = /* @__PURE__ */ new Set();
|
|
762
|
+
for (const entry of tree) {
|
|
763
|
+
if (ambiguousNames.has(entry.name)) continue;
|
|
764
|
+
const existingOwner = uniqueOwnerByName.get(entry.name);
|
|
765
|
+
if (!existingOwner) {
|
|
766
|
+
uniqueOwnerByName.set(entry.name, entry.owner);
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
if (existingOwner !== entry.owner) {
|
|
770
|
+
uniqueOwnerByName.delete(entry.name);
|
|
771
|
+
ambiguousNames.add(entry.name);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
const skills = await appServer.rpc("skills/list", {});
|
|
775
|
+
const seen = /* @__PURE__ */ new Set();
|
|
776
|
+
const synced = [];
|
|
777
|
+
let ownersChanged = false;
|
|
778
|
+
for (const entry of skills.data ?? []) {
|
|
779
|
+
for (const skill of entry.skills ?? []) {
|
|
780
|
+
const name = typeof skill.name === "string" ? skill.name : "";
|
|
781
|
+
if (!name || seen.has(name)) continue;
|
|
782
|
+
seen.add(name);
|
|
783
|
+
let owner = owners[name];
|
|
784
|
+
if (!owner) {
|
|
785
|
+
owner = uniqueOwnerByName.get(name) ?? "";
|
|
786
|
+
if (owner) {
|
|
787
|
+
owners[name] = owner;
|
|
788
|
+
ownersChanged = true;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
synced.push({ ...owner ? { owner } : {}, name, enabled: skill.enabled !== false });
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (ownersChanged) {
|
|
795
|
+
await writeSkillsSyncState({ ...state, installedOwners: owners });
|
|
796
|
+
}
|
|
797
|
+
synced.sort((a, b) => `${a.owner ?? ""}/${a.name}`.localeCompare(`${b.owner ?? ""}/${b.name}`));
|
|
798
|
+
return synced;
|
|
799
|
+
}
|
|
800
|
+
async function autoPushSyncedSkills(appServer) {
|
|
801
|
+
const state = await readSkillsSyncState();
|
|
802
|
+
if (!state.githubToken || !state.repoOwner || !state.repoName) return;
|
|
803
|
+
if (isUpstreamSkillsRepo(state.repoOwner, state.repoName)) {
|
|
804
|
+
throw new Error("Refusing to push to upstream skills repository");
|
|
805
|
+
}
|
|
806
|
+
const local = await collectLocalSyncedSkills(appServer);
|
|
807
|
+
const installedMap = await scanInstalledSkillsFromDisk();
|
|
808
|
+
await writeRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName, local);
|
|
809
|
+
await syncInstalledSkillsFolderToRepo(state.githubToken, state.repoOwner, state.repoName, installedMap);
|
|
810
|
+
}
|
|
811
|
+
async function ensureCodexAgentsSymlinkToSkillsAgents() {
|
|
812
|
+
const codexHomeDir = getCodexHomeDir();
|
|
813
|
+
const skillsAgentsPath = join(codexHomeDir, "skills", "AGENTS.md");
|
|
814
|
+
const codexAgentsPath = join(codexHomeDir, "AGENTS.md");
|
|
815
|
+
await mkdir(join(codexHomeDir, "skills"), { recursive: true });
|
|
816
|
+
let copiedFromCodex = false;
|
|
817
|
+
try {
|
|
818
|
+
const codexAgentsStat = await lstat(codexAgentsPath);
|
|
819
|
+
if (codexAgentsStat.isFile() || codexAgentsStat.isSymbolicLink()) {
|
|
820
|
+
const content = await readFile(codexAgentsPath, "utf8");
|
|
821
|
+
await writeFile(skillsAgentsPath, content, "utf8");
|
|
822
|
+
copiedFromCodex = true;
|
|
823
|
+
} else {
|
|
824
|
+
await rm(codexAgentsPath, { force: true, recursive: true });
|
|
825
|
+
}
|
|
826
|
+
} catch {
|
|
827
|
+
}
|
|
828
|
+
if (!copiedFromCodex) {
|
|
829
|
+
try {
|
|
830
|
+
const skillsAgentsStat = await stat(skillsAgentsPath);
|
|
831
|
+
if (!skillsAgentsStat.isFile()) {
|
|
832
|
+
await rm(skillsAgentsPath, { force: true, recursive: true });
|
|
833
|
+
await writeFile(skillsAgentsPath, "", "utf8");
|
|
834
|
+
}
|
|
835
|
+
} catch {
|
|
836
|
+
await writeFile(skillsAgentsPath, "", "utf8");
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
const relativeTarget = join("skills", "AGENTS.md");
|
|
840
|
+
try {
|
|
841
|
+
const current = await lstat(codexAgentsPath);
|
|
842
|
+
if (current.isSymbolicLink()) {
|
|
843
|
+
const existingTarget = await readlink(codexAgentsPath);
|
|
844
|
+
if (existingTarget === relativeTarget) return;
|
|
845
|
+
}
|
|
846
|
+
await rm(codexAgentsPath, { force: true, recursive: true });
|
|
847
|
+
} catch {
|
|
848
|
+
}
|
|
849
|
+
await symlink(relativeTarget, codexAgentsPath);
|
|
850
|
+
}
|
|
851
|
+
async function initializeSkillsSyncOnStartup(appServer) {
|
|
852
|
+
if (startupSkillsSyncInitialized) return;
|
|
853
|
+
startupSkillsSyncInitialized = true;
|
|
854
|
+
startupSyncStatus.inProgress = true;
|
|
855
|
+
startupSyncStatus.lastRunAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
856
|
+
startupSyncStatus.lastError = "";
|
|
857
|
+
startupSyncStatus.branch = getPreferredSyncBranch();
|
|
858
|
+
try {
|
|
859
|
+
const state = await readSkillsSyncState();
|
|
860
|
+
const localSkillsDir = getSkillsInstallDir();
|
|
861
|
+
if (!state.githubToken) {
|
|
862
|
+
await ensureCodexAgentsSymlinkToSkillsAgents();
|
|
863
|
+
if (!isAndroidLikeRuntime()) {
|
|
864
|
+
startupSyncStatus.mode = "idle";
|
|
865
|
+
startupSyncStatus.lastAction = "skip-upstream-non-android";
|
|
866
|
+
startupSyncStatus.lastSuccessAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
startupSyncStatus.mode = "unauthenticated-bootstrap";
|
|
870
|
+
startupSyncStatus.lastAction = "pull-upstream";
|
|
871
|
+
await bootstrapSkillsFromUpstreamIntoLocal(localSkillsDir);
|
|
872
|
+
try {
|
|
873
|
+
await appServer.rpc("skills/list", { forceReload: true });
|
|
874
|
+
} catch {
|
|
875
|
+
}
|
|
876
|
+
startupSyncStatus.lastSuccessAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
877
|
+
startupSyncStatus.lastAction = "pull-upstream-complete";
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
startupSyncStatus.mode = "authenticated-fork-sync";
|
|
881
|
+
startupSyncStatus.lastAction = "ensure-private-fork";
|
|
882
|
+
const username = state.githubUsername || await resolveGithubUsername(state.githubToken);
|
|
883
|
+
const repoName = DEFAULT_SKILLS_SYNC_REPO_NAME;
|
|
884
|
+
await ensurePrivateForkFromUpstream(state.githubToken, username, repoName);
|
|
885
|
+
const nextState = { ...state, githubUsername: username, repoOwner: username, repoName };
|
|
886
|
+
await writeSkillsSyncState(nextState);
|
|
887
|
+
startupSyncStatus.lastAction = "pull-private-fork";
|
|
888
|
+
await pullInstalledSkillsFolderFromRepo(state.githubToken, username, repoName, localSkillsDir);
|
|
889
|
+
try {
|
|
890
|
+
await appServer.rpc("skills/list", { forceReload: true });
|
|
891
|
+
} catch {
|
|
892
|
+
}
|
|
893
|
+
startupSyncStatus.lastAction = "push-private-fork";
|
|
894
|
+
await autoPushSyncedSkills(appServer);
|
|
895
|
+
startupSyncStatus.lastSuccessAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
896
|
+
startupSyncStatus.lastAction = "startup-sync-complete";
|
|
897
|
+
} catch (error) {
|
|
898
|
+
startupSyncStatus.lastError = getErrorMessage(error, "startup-sync-failed");
|
|
899
|
+
startupSyncStatus.lastAction = "startup-sync-failed";
|
|
900
|
+
} finally {
|
|
901
|
+
startupSyncStatus.inProgress = false;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
async function finalizeGithubLoginAndSync(token, username, appServer) {
|
|
905
|
+
const repoName = DEFAULT_SKILLS_SYNC_REPO_NAME;
|
|
906
|
+
await ensurePrivateForkFromUpstream(token, username, repoName);
|
|
907
|
+
const current = await readSkillsSyncState();
|
|
908
|
+
await writeSkillsSyncState({
|
|
909
|
+
...current,
|
|
910
|
+
githubToken: token,
|
|
911
|
+
githubUsername: username,
|
|
912
|
+
repoOwner: username,
|
|
913
|
+
repoName
|
|
914
|
+
});
|
|
915
|
+
const localDir = getSkillsInstallDir();
|
|
916
|
+
await pullInstalledSkillsFolderFromRepo(token, username, repoName, localDir);
|
|
917
|
+
try {
|
|
918
|
+
await appServer.rpc("skills/list", { forceReload: true });
|
|
919
|
+
} catch {
|
|
920
|
+
}
|
|
921
|
+
await autoPushSyncedSkills(appServer);
|
|
922
|
+
}
|
|
346
923
|
async function searchSkillsHub(allEntries, query, limit, sort, installedMap) {
|
|
347
924
|
const q = query.toLowerCase().trim();
|
|
348
925
|
let filtered = q ? allEntries.filter((s) => {
|
|
@@ -1010,6 +1587,7 @@ function createCodexBridgeMiddleware() {
|
|
|
1010
1587
|
}
|
|
1011
1588
|
return threadSearchIndexPromise;
|
|
1012
1589
|
}
|
|
1590
|
+
void initializeSkillsSyncOnStartup(appServer);
|
|
1013
1591
|
const middleware = async (req, res, next) => {
|
|
1014
1592
|
try {
|
|
1015
1593
|
if (!req.url) {
|
|
@@ -1340,6 +1918,188 @@ function createCodexBridgeMiddleware() {
|
|
|
1340
1918
|
}
|
|
1341
1919
|
return;
|
|
1342
1920
|
}
|
|
1921
|
+
if (req.method === "GET" && url.pathname === "/codex-api/skills-sync/status") {
|
|
1922
|
+
const state = await readSkillsSyncState();
|
|
1923
|
+
setJson(res, 200, {
|
|
1924
|
+
data: {
|
|
1925
|
+
loggedIn: Boolean(state.githubToken),
|
|
1926
|
+
githubUsername: state.githubUsername ?? "",
|
|
1927
|
+
repoOwner: state.repoOwner ?? "",
|
|
1928
|
+
repoName: state.repoName ?? "",
|
|
1929
|
+
configured: Boolean(state.githubToken && state.repoOwner && state.repoName),
|
|
1930
|
+
startup: {
|
|
1931
|
+
inProgress: startupSyncStatus.inProgress,
|
|
1932
|
+
mode: startupSyncStatus.mode,
|
|
1933
|
+
branch: startupSyncStatus.branch,
|
|
1934
|
+
lastAction: startupSyncStatus.lastAction,
|
|
1935
|
+
lastRunAtIso: startupSyncStatus.lastRunAtIso,
|
|
1936
|
+
lastSuccessAtIso: startupSyncStatus.lastSuccessAtIso,
|
|
1937
|
+
lastError: startupSyncStatus.lastError
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
});
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/start-login") {
|
|
1944
|
+
try {
|
|
1945
|
+
const started = await startGithubDeviceLogin();
|
|
1946
|
+
setJson(res, 200, { data: started });
|
|
1947
|
+
} catch (error) {
|
|
1948
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to start GitHub login") });
|
|
1949
|
+
}
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/token-login") {
|
|
1953
|
+
try {
|
|
1954
|
+
const payload = asRecord(await readJsonBody(req));
|
|
1955
|
+
const token = typeof payload?.token === "string" ? payload.token.trim() : "";
|
|
1956
|
+
if (!token) {
|
|
1957
|
+
setJson(res, 400, { error: "Missing GitHub token" });
|
|
1958
|
+
return;
|
|
1959
|
+
}
|
|
1960
|
+
const username = await resolveGithubUsername(token);
|
|
1961
|
+
await finalizeGithubLoginAndSync(token, username, appServer);
|
|
1962
|
+
setJson(res, 200, { ok: true, data: { githubUsername: username } });
|
|
1963
|
+
} catch (error) {
|
|
1964
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to login with GitHub token") });
|
|
1965
|
+
}
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/logout") {
|
|
1969
|
+
try {
|
|
1970
|
+
const state = await readSkillsSyncState();
|
|
1971
|
+
await writeSkillsSyncState({
|
|
1972
|
+
...state,
|
|
1973
|
+
githubToken: void 0,
|
|
1974
|
+
githubUsername: void 0,
|
|
1975
|
+
repoOwner: void 0,
|
|
1976
|
+
repoName: void 0
|
|
1977
|
+
});
|
|
1978
|
+
setJson(res, 200, { ok: true });
|
|
1979
|
+
} catch (error) {
|
|
1980
|
+
setJson(res, 500, { error: getErrorMessage(error, "Failed to logout GitHub") });
|
|
1981
|
+
}
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/complete-login") {
|
|
1985
|
+
try {
|
|
1986
|
+
const payload = asRecord(await readJsonBody(req));
|
|
1987
|
+
const deviceCode = typeof payload?.deviceCode === "string" ? payload.deviceCode : "";
|
|
1988
|
+
if (!deviceCode) {
|
|
1989
|
+
setJson(res, 400, { error: "Missing deviceCode" });
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1992
|
+
const result = await completeGithubDeviceLogin(deviceCode);
|
|
1993
|
+
if (!result.token) {
|
|
1994
|
+
setJson(res, 200, { ok: false, pending: result.error === "authorization_pending", error: result.error || "login_failed" });
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
const token = result.token;
|
|
1998
|
+
const username = await resolveGithubUsername(token);
|
|
1999
|
+
await finalizeGithubLoginAndSync(token, username, appServer);
|
|
2000
|
+
setJson(res, 200, { ok: true, data: { githubUsername: username } });
|
|
2001
|
+
} catch (error) {
|
|
2002
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to complete GitHub login") });
|
|
2003
|
+
}
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/push") {
|
|
2007
|
+
try {
|
|
2008
|
+
const state = await readSkillsSyncState();
|
|
2009
|
+
if (!state.githubToken || !state.repoOwner || !state.repoName) {
|
|
2010
|
+
setJson(res, 400, { error: "Skills sync is not configured yet" });
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
if (isUpstreamSkillsRepo(state.repoOwner, state.repoName)) {
|
|
2014
|
+
setJson(res, 400, { error: "Refusing to push to upstream repository" });
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
const local = await collectLocalSyncedSkills(appServer);
|
|
2018
|
+
const installedMap = await scanInstalledSkillsFromDisk();
|
|
2019
|
+
await writeRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName, local);
|
|
2020
|
+
await syncInstalledSkillsFolderToRepo(state.githubToken, state.repoOwner, state.repoName, installedMap);
|
|
2021
|
+
setJson(res, 200, { ok: true, data: { synced: local.length } });
|
|
2022
|
+
} catch (error) {
|
|
2023
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to push synced skills") });
|
|
2024
|
+
}
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/pull") {
|
|
2028
|
+
try {
|
|
2029
|
+
const state = await readSkillsSyncState();
|
|
2030
|
+
if (!state.githubToken || !state.repoOwner || !state.repoName) {
|
|
2031
|
+
const localDir2 = await detectUserSkillsDir(appServer);
|
|
2032
|
+
await bootstrapSkillsFromUpstreamIntoLocal(localDir2);
|
|
2033
|
+
try {
|
|
2034
|
+
await appServer.rpc("skills/list", { forceReload: true });
|
|
2035
|
+
} catch {
|
|
2036
|
+
}
|
|
2037
|
+
setJson(res, 200, { ok: true, data: { synced: 0, source: "upstream" } });
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
const remote = await readRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName);
|
|
2041
|
+
const tree = await fetchSkillsTree();
|
|
2042
|
+
const uniqueOwnerByName = /* @__PURE__ */ new Map();
|
|
2043
|
+
const ambiguousNames = /* @__PURE__ */ new Set();
|
|
2044
|
+
for (const entry of tree) {
|
|
2045
|
+
if (ambiguousNames.has(entry.name)) continue;
|
|
2046
|
+
const existingOwner = uniqueOwnerByName.get(entry.name);
|
|
2047
|
+
if (!existingOwner) {
|
|
2048
|
+
uniqueOwnerByName.set(entry.name, entry.owner);
|
|
2049
|
+
continue;
|
|
2050
|
+
}
|
|
2051
|
+
if (existingOwner !== entry.owner) {
|
|
2052
|
+
uniqueOwnerByName.delete(entry.name);
|
|
2053
|
+
ambiguousNames.add(entry.name);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
const localDir = await detectUserSkillsDir(appServer);
|
|
2057
|
+
await pullInstalledSkillsFolderFromRepo(state.githubToken, state.repoOwner, state.repoName, localDir);
|
|
2058
|
+
const installerScript = "/Users/igor/.cursor/skills/.system/skill-installer/scripts/install-skill-from-github.py";
|
|
2059
|
+
const localSkills = await scanInstalledSkillsFromDisk();
|
|
2060
|
+
for (const skill of remote) {
|
|
2061
|
+
const owner = skill.owner || uniqueOwnerByName.get(skill.name) || "";
|
|
2062
|
+
if (!owner) {
|
|
2063
|
+
continue;
|
|
2064
|
+
}
|
|
2065
|
+
if (!localSkills.has(skill.name)) {
|
|
2066
|
+
await runCommand("python3", [
|
|
2067
|
+
installerScript,
|
|
2068
|
+
"--repo",
|
|
2069
|
+
`${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
|
|
2070
|
+
"--path",
|
|
2071
|
+
`skills/${owner}/${skill.name}`,
|
|
2072
|
+
"--dest",
|
|
2073
|
+
localDir,
|
|
2074
|
+
"--method",
|
|
2075
|
+
"git"
|
|
2076
|
+
]);
|
|
2077
|
+
}
|
|
2078
|
+
const skillPath = join(localDir, skill.name);
|
|
2079
|
+
await appServer.rpc("skills/config/write", { path: skillPath, enabled: skill.enabled });
|
|
2080
|
+
}
|
|
2081
|
+
const remoteNames = new Set(remote.map((row) => row.name));
|
|
2082
|
+
for (const [name, localInfo] of localSkills.entries()) {
|
|
2083
|
+
if (!remoteNames.has(name)) {
|
|
2084
|
+
await rm(localInfo.path.replace(/\/SKILL\.md$/, ""), { recursive: true, force: true });
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
const nextOwners = {};
|
|
2088
|
+
for (const item of remote) {
|
|
2089
|
+
const owner = item.owner || uniqueOwnerByName.get(item.name) || "";
|
|
2090
|
+
if (owner) nextOwners[item.name] = owner;
|
|
2091
|
+
}
|
|
2092
|
+
await writeSkillsSyncState({ ...state, installedOwners: nextOwners });
|
|
2093
|
+
try {
|
|
2094
|
+
await appServer.rpc("skills/list", { forceReload: true });
|
|
2095
|
+
} catch {
|
|
2096
|
+
}
|
|
2097
|
+
setJson(res, 200, { ok: true, data: { synced: remote.length } });
|
|
2098
|
+
} catch (error) {
|
|
2099
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to pull synced skills") });
|
|
2100
|
+
}
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
1343
2103
|
if (req.method === "GET" && url.pathname === "/codex-api/skills-hub/readme") {
|
|
1344
2104
|
try {
|
|
1345
2105
|
const owner = url.searchParams.get("owner") || "";
|
|
@@ -1348,7 +2108,7 @@ function createCodexBridgeMiddleware() {
|
|
|
1348
2108
|
setJson(res, 400, { error: "Missing owner or name" });
|
|
1349
2109
|
return;
|
|
1350
2110
|
}
|
|
1351
|
-
const rawUrl = `https://raw.githubusercontent.com/
|
|
2111
|
+
const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${owner}/${name}/SKILL.md`;
|
|
1352
2112
|
const resp = await fetch(rawUrl);
|
|
1353
2113
|
if (!resp.ok) throw new Error(`Failed to fetch SKILL.md: ${resp.status}`);
|
|
1354
2114
|
const content = await resp.text();
|
|
@@ -1373,7 +2133,7 @@ function createCodexBridgeMiddleware() {
|
|
|
1373
2133
|
await runCommand("python3", [
|
|
1374
2134
|
installerScript,
|
|
1375
2135
|
"--repo",
|
|
1376
|
-
|
|
2136
|
+
`${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
|
|
1377
2137
|
"--path",
|
|
1378
2138
|
skillPathInRepo,
|
|
1379
2139
|
"--dest",
|
|
@@ -1383,6 +2143,10 @@ function createCodexBridgeMiddleware() {
|
|
|
1383
2143
|
]);
|
|
1384
2144
|
const skillDir = join(installDest, name);
|
|
1385
2145
|
await ensureInstalledSkillIsValid(appServer, skillDir);
|
|
2146
|
+
const syncState = await readSkillsSyncState();
|
|
2147
|
+
const nextOwners = { ...syncState.installedOwners ?? {}, [name]: owner };
|
|
2148
|
+
await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
|
|
2149
|
+
await autoPushSyncedSkills(appServer);
|
|
1386
2150
|
setJson(res, 200, { ok: true, path: skillDir });
|
|
1387
2151
|
} catch (error) {
|
|
1388
2152
|
setJson(res, 502, { error: getErrorMessage(error, "Failed to install skill") });
|
|
@@ -1400,6 +2164,13 @@ function createCodexBridgeMiddleware() {
|
|
|
1400
2164
|
return;
|
|
1401
2165
|
}
|
|
1402
2166
|
await rm(target, { recursive: true, force: true });
|
|
2167
|
+
if (name) {
|
|
2168
|
+
const syncState = await readSkillsSyncState();
|
|
2169
|
+
const nextOwners = { ...syncState.installedOwners ?? {} };
|
|
2170
|
+
delete nextOwners[name];
|
|
2171
|
+
await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
|
|
2172
|
+
}
|
|
2173
|
+
await autoPushSyncedSkills(appServer);
|
|
1403
2174
|
try {
|
|
1404
2175
|
await appServer.rpc("skills/list", { forceReload: true });
|
|
1405
2176
|
} catch {
|
|
@@ -1979,7 +2750,7 @@ function createServer(options = {}) {
|
|
|
1979
2750
|
res.status(404).json({ error: "File not found." });
|
|
1980
2751
|
}
|
|
1981
2752
|
});
|
|
1982
|
-
const hasFrontendAssets =
|
|
2753
|
+
const hasFrontendAssets = existsSync2(spaEntryFile);
|
|
1983
2754
|
if (hasFrontendAssets) {
|
|
1984
2755
|
app.use(express.static(distDir));
|
|
1985
2756
|
}
|
|
@@ -2086,7 +2857,7 @@ function resolveCodexCommand() {
|
|
|
2086
2857
|
return "codex";
|
|
2087
2858
|
}
|
|
2088
2859
|
const userCandidate = join4(getUserNpmPrefix(), "bin", "codex");
|
|
2089
|
-
if (
|
|
2860
|
+
if (existsSync3(userCandidate) && canRun(userCandidate, ["--version"])) {
|
|
2090
2861
|
return userCandidate;
|
|
2091
2862
|
}
|
|
2092
2863
|
const prefix = process.env.PREFIX?.trim();
|
|
@@ -2094,7 +2865,7 @@ function resolveCodexCommand() {
|
|
|
2094
2865
|
return null;
|
|
2095
2866
|
}
|
|
2096
2867
|
const candidate = join4(prefix, "bin", "codex");
|
|
2097
|
-
if (
|
|
2868
|
+
if (existsSync3(candidate) && canRun(candidate, ["--version"])) {
|
|
2098
2869
|
return candidate;
|
|
2099
2870
|
}
|
|
2100
2871
|
return null;
|
|
@@ -2104,7 +2875,7 @@ function resolveCloudflaredCommand() {
|
|
|
2104
2875
|
return "cloudflared";
|
|
2105
2876
|
}
|
|
2106
2877
|
const localCandidate = join4(homedir2(), ".local", "bin", "cloudflared");
|
|
2107
|
-
if (
|
|
2878
|
+
if (existsSync3(localCandidate) && canRun(localCandidate, ["--version"])) {
|
|
2108
2879
|
return localCandidate;
|
|
2109
2880
|
}
|
|
2110
2881
|
return null;
|
|
@@ -2199,7 +2970,7 @@ async function resolveCloudflaredForTunnel() {
|
|
|
2199
2970
|
}
|
|
2200
2971
|
function hasCodexAuth() {
|
|
2201
2972
|
const codexHome = process.env.CODEX_HOME?.trim() || join4(homedir2(), ".codex");
|
|
2202
|
-
return
|
|
2973
|
+
return existsSync3(join4(codexHome, "auth.json"));
|
|
2203
2974
|
}
|
|
2204
2975
|
function ensureCodexInstalled() {
|
|
2205
2976
|
let codexCommand = resolveCodexCommand();
|