@skild/core 0.10.19 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -67,6 +67,10 @@ interface InstallRecord {
67
67
  sourceType: SourceType;
68
68
  installedAt: string;
69
69
  updatedAt?: string;
70
+ /**
71
+ * Derived at runtime from platform/scope/name.
72
+ * Stored install records omit this field to avoid machine-specific paths.
73
+ */
70
74
  installDir: string;
71
75
  contentHash: string;
72
76
  hasSkillMd: boolean;
@@ -96,7 +100,7 @@ interface LockEntry {
96
100
  registryUrl?: string;
97
101
  installedAt: string;
98
102
  updatedAt?: string;
99
- installDir: string;
103
+ installDir?: string;
100
104
  contentHash: string;
101
105
  }
102
106
  interface Lockfile {
package/dist/index.js CHANGED
@@ -95,6 +95,35 @@ function writeJsonFilePrivate(filePath, value) {
95
95
  } catch {
96
96
  }
97
97
  }
98
+ function toPersistedInstallRecord(record) {
99
+ const { installDir: _installDir, installedDependencies, ...rest } = record;
100
+ const sanitizedDeps = installedDependencies?.map((dep) => {
101
+ const { installDir: _depInstallDir, ...depRest } = dep;
102
+ return depRest;
103
+ });
104
+ if (!sanitizedDeps) return rest;
105
+ return { ...rest, installedDependencies: sanitizedDeps };
106
+ }
107
+ function hydrateInstallRecord(installDir, record) {
108
+ const hydrated = { ...record, installDir };
109
+ if (hydrated.installedDependencies?.length) {
110
+ hydrated.installedDependencies = hydrated.installedDependencies.map((dep) => {
111
+ if (dep.sourceType === "inline") return dep;
112
+ return { ...dep, installDir: getSkillInstallDir(hydrated.platform, hydrated.scope, dep.name) };
113
+ });
114
+ }
115
+ return hydrated;
116
+ }
117
+ function toPersistedLockEntry(entry) {
118
+ const { installDir: _installDir, ...rest } = entry;
119
+ return rest;
120
+ }
121
+ function sanitizeLockfile(lockfile) {
122
+ const entries = Object.fromEntries(
123
+ Object.entries(lockfile.entries).map(([name, entry]) => [name, toPersistedLockEntry(entry)])
124
+ );
125
+ return { ...lockfile, entries };
126
+ }
98
127
  function loadOrCreateGlobalConfig() {
99
128
  const filePath = getGlobalConfigPath();
100
129
  const existing = readJsonFile(filePath);
@@ -121,7 +150,7 @@ function loadLockfile(lockPath) {
121
150
  return readJsonFile(lockPath);
122
151
  }
123
152
  function saveLockfile(lockPath, lockfile) {
124
- writeJsonFile(lockPath, lockfile);
153
+ writeJsonFile(lockPath, sanitizeLockfile(lockfile));
125
154
  }
126
155
  function getLockPath(scope) {
127
156
  return scope === "project" ? getProjectLockPath() : getGlobalLockPath();
@@ -136,7 +165,7 @@ function loadOrCreateLockfile(scope) {
136
165
  }
137
166
  function upsertLockEntry(scope, entry) {
138
167
  const lockfile = loadOrCreateLockfile(scope);
139
- lockfile.entries[entry.name] = entry;
168
+ lockfile.entries[entry.name] = toPersistedLockEntry(entry);
140
169
  lockfile.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
141
170
  saveLockfile(getLockPath(scope), lockfile);
142
171
  }
@@ -148,12 +177,14 @@ function removeLockEntry(scope, name) {
148
177
  }
149
178
  function readInstallRecord(installDir) {
150
179
  const filePath = getSkillInstallRecordPath(installDir);
151
- return readJsonFile(filePath);
180
+ const record = readJsonFile(filePath);
181
+ if (!record) return null;
182
+ return hydrateInstallRecord(installDir, record);
152
183
  }
153
184
  function writeInstallRecord(installDir, record) {
154
185
  ensureDir(getSkillMetadataDir(installDir));
155
186
  const filePath = getSkillInstallRecordPath(installDir);
156
- writeJsonFile(filePath, record);
187
+ writeJsonFile(filePath, toPersistedInstallRecord(record));
157
188
  }
158
189
 
159
190
  // src/skill.ts
@@ -300,6 +331,7 @@ import fs6 from "fs";
300
331
  import os2 from "os";
301
332
  import path7 from "path";
302
333
  import degit from "degit";
334
+ import simpleGit from "simple-git";
303
335
 
304
336
  // src/source.ts
305
337
  import path6 from "path";
@@ -460,6 +492,81 @@ function resetTargetDir(targetPath) {
460
492
  removeDir(targetPath);
461
493
  fs6.mkdirSync(targetPath, { recursive: true });
462
494
  }
495
+ function splitSourceRef(source) {
496
+ const [base, ref] = source.split("#", 2);
497
+ return { base, ref: ref?.trim() || void 0 };
498
+ }
499
+ function parseGitCloneSpec(source) {
500
+ const trimmed = source.trim();
501
+ if (!trimmed) return null;
502
+ if (resolveLocalPath(trimmed)) return null;
503
+ const { base, ref } = splitSourceRef(trimmed);
504
+ const githubTreeWithPathMatch = base.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)/);
505
+ if (githubTreeWithPathMatch) {
506
+ const [, owner, repo, branch, subpath] = githubTreeWithPathMatch;
507
+ return { url: `https://github.com/${owner}/${repo}.git`, ref: branch, subpath };
508
+ }
509
+ const githubTreeMatch = base.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)$/);
510
+ if (githubTreeMatch) {
511
+ const [, owner, repo, branch] = githubTreeMatch;
512
+ return { url: `https://github.com/${owner}/${repo}.git`, ref: branch };
513
+ }
514
+ const githubRepoMatch = base.match(/github\.com\/([^/]+)\/([^/]+)/);
515
+ if (githubRepoMatch) {
516
+ const [, owner, repoRaw] = githubRepoMatch;
517
+ const repo = repoRaw.replace(/\.git$/, "");
518
+ return { url: `https://github.com/${owner}/${repo}.git`, ref };
519
+ }
520
+ const gitlabTreeWithPathMatch = base.match(/gitlab\.com\/([^/]+)\/([^/]+)\/-\/tree\/([^/]+)\/(.+)/);
521
+ if (gitlabTreeWithPathMatch) {
522
+ const [, owner, repo, branch, subpath] = gitlabTreeWithPathMatch;
523
+ return { url: `https://gitlab.com/${owner}/${repo}.git`, ref: branch, subpath };
524
+ }
525
+ const gitlabTreeMatch = base.match(/gitlab\.com\/([^/]+)\/([^/]+)\/-\/tree\/([^/]+)$/);
526
+ if (gitlabTreeMatch) {
527
+ const [, owner, repo, branch] = gitlabTreeMatch;
528
+ return { url: `https://gitlab.com/${owner}/${repo}.git`, ref: branch };
529
+ }
530
+ const gitlabRepoMatch = base.match(/gitlab\.com\/([^/]+)\/([^/]+)/);
531
+ if (gitlabRepoMatch) {
532
+ const [, owner, repoRaw] = gitlabRepoMatch;
533
+ const repo = repoRaw.replace(/\.git$/, "");
534
+ return { url: `https://gitlab.com/${owner}/${repo}.git`, ref };
535
+ }
536
+ const shorthandMatch = base.match(/^([^/]+)\/([^/]+)(?:\/(.+))?$/);
537
+ if (shorthandMatch && !base.includes(":") && !base.startsWith(".") && !base.startsWith("/")) {
538
+ const [, owner, repo, subpath] = shorthandMatch;
539
+ return { url: `https://github.com/${owner}/${repo}.git`, ref, subpath };
540
+ }
541
+ if (/^(https?:|git@|ssh:)/i.test(base)) {
542
+ return { url: base, ref };
543
+ }
544
+ return null;
545
+ }
546
+ async function cloneWithGit(spec, targetPath) {
547
+ const cloneOptions = spec.ref ? ["--depth", "1", "--branch", spec.ref] : ["--depth", "1"];
548
+ const git = simpleGit();
549
+ if (!spec.subpath) {
550
+ resetTargetDir(targetPath);
551
+ await git.clone(spec.url, targetPath, cloneOptions);
552
+ return spec.url;
553
+ }
554
+ const tempRoot = createTempDir(path7.join(os2.tmpdir(), "skild-git"), extractSkillName(spec.url));
555
+ try {
556
+ await git.clone(spec.url, tempRoot, cloneOptions);
557
+ const resolvedSubpath = path7.resolve(tempRoot, spec.subpath);
558
+ const resolvedRoot = path7.resolve(tempRoot);
559
+ if (!resolvedSubpath.startsWith(`${resolvedRoot}${path7.sep}`)) {
560
+ throw new SkildError("INVALID_SOURCE", `Subpath escapes repository root: ${spec.subpath}`, { subpath: spec.subpath });
561
+ }
562
+ ensureInstallableDir(resolvedSubpath);
563
+ resetTargetDir(targetPath);
564
+ copyDir(resolvedSubpath, targetPath);
565
+ return spec.url;
566
+ } finally {
567
+ removeDir(tempRoot);
568
+ }
569
+ }
463
570
  async function cloneRemote(degitSrc, targetPath, mode) {
464
571
  const emitter = degit(degitSrc, { force: true, verbose: false, mode });
465
572
  await emitter.clone(targetPath);
@@ -512,6 +619,14 @@ async function materializeSourceToDir(input) {
512
619
  copyDir(localPath, targetDir);
513
620
  return { sourceType: "local", materializedFrom: localPath };
514
621
  }
622
+ const gitSpec = parseGitCloneSpec(input.source);
623
+ if (gitSpec) {
624
+ try {
625
+ const materializedFrom2 = await cloneWithGit(gitSpec, targetDir);
626
+ return { sourceType, materializedFrom: materializedFrom2 };
627
+ } catch {
628
+ }
629
+ }
515
630
  const degitPath = toDegitPath(input.source);
516
631
  const materializedFrom = await cloneRemoteWithFallback(degitPath, targetDir);
517
632
  return { sourceType, materializedFrom };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skild/core",
3
- "version": "0.10.19",
3
+ "version": "0.11.0",
4
4
  "description": "Skild core library (headless) for installing, validating, and managing Agent Skills locally.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -20,6 +20,7 @@
20
20
  "degit": "^2.8.4",
21
21
  "js-yaml": "^4.1.0",
22
22
  "semver": "^7.6.3",
23
+ "simple-git": "^3.27.0",
23
24
  "tar": "^7.4.3"
24
25
  },
25
26
  "devDependencies": {