Skip to content

Commit afa5109

Browse files
committed
fix: support --from <date> when ejecting
1 parent fe51987 commit afa5109

7 files changed

Lines changed: 64 additions & 22 deletions

File tree

CLAUDE.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@ skilld search "query" # Search indexed docs
3535
skilld search "query" -p nuxt # Search filtered by package
3636
skilld cache # Clean expired LLM cache entries
3737
skilld add owner/repo # Add pre-authored skills from git repo
38-
skilld eject vue # Eject skill (portable, no symlinks)
39-
skilld eject vue --name vue # Eject with custom skill dir name
40-
skilld eject vue --out ./dir/ # Eject to custom path
38+
skilld eject vue # Eject skill (portable, no symlinks)
39+
skilld eject vue --name vue # Eject with custom skill dir name
40+
skilld eject vue --out ./dir/ # Eject to custom path
41+
skilld eject vue --from 2025-07-01 # Only releases/issues since date
4142
```
4243

4344
## Architecture

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,9 @@ skilld eject vue --name vue
173173

174174
# Eject to a custom path
175175
skilld eject vue --out ./skills/vue/
176+
177+
# Only collect releases/issues/discussions since a date
178+
skilld eject vue --from 2025-07-01
176179
```
177180

178181
The ejected skill contains `SKILL.md` plus a `references/` directory with docs, issues, and releases as real files. Share it via `skilld add owner/repo` — consumers get fully functional skills with no LLM cost.
@@ -188,6 +191,7 @@ The ejected skill contains `SKILL.md` plus a `references/` directory with docs,
188191
| `--model` | `-m` | config default | LLM model for skill generation (sonnet, haiku, opus, etc.) |
189192
| `--name` | `-n` | | Custom skill directory name (eject only) |
190193
| `--out` | `-o` | | Output directory path override (eject only) |
194+
| `--from` | | | Collect releases/issues/discussions from this date (YYYY-MM-DD, eject only) |
191195
| `--debug` | | `false` | Save raw LLM output to logs/ for each section |
192196

193197
## The Landscape

src/commands/sync-shared.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,8 @@ export async function fetchAndCacheResources(opts: {
273273
version: string
274274
useCache: boolean
275275
features?: FeaturesConfig
276+
/** Lower-bound date for release/issue/discussion collection (ISO date) */
277+
from?: string
276278
onProgress: (message: string) => void
277279
}): Promise<FetchResult> {
278280
const { packageName, resolved, version, useCache, onProgress } = opts
@@ -441,7 +443,7 @@ export async function fetchAndCacheResources(opts: {
441443
const gh = parseGitHubUrl(resolved.repoUrl)
442444
if (gh) {
443445
onProgress('Fetching issues via GitHub API')
444-
const issues = await fetchGitHubIssues(gh.owner, gh.repo, 30, resolved.releasedAt).catch(() => [])
446+
const issues = await fetchGitHubIssues(gh.owner, gh.repo, 30, resolved.releasedAt, opts.from).catch(() => [])
445447
if (issues.length > 0) {
446448
onProgress(`Caching ${issues.length} issues`)
447449
writeToCache(packageName, version, issues.map(issue => ({
@@ -469,7 +471,7 @@ export async function fetchAndCacheResources(opts: {
469471
const gh = parseGitHubUrl(resolved.repoUrl)
470472
if (gh) {
471473
onProgress('Fetching discussions via GitHub API')
472-
const discussions = await fetchGitHubDiscussions(gh.owner, gh.repo, 20, resolved.releasedAt).catch(() => [])
474+
const discussions = await fetchGitHubDiscussions(gh.owner, gh.repo, 20, resolved.releasedAt, opts.from).catch(() => [])
473475
if (discussions.length > 0) {
474476
onProgress(`Caching ${discussions.length} discussions`)
475477
writeToCache(packageName, version, discussions.map(d => ({
@@ -497,7 +499,7 @@ export async function fetchAndCacheResources(opts: {
497499
const gh = parseGitHubUrl(resolved.repoUrl)
498500
if (gh) {
499501
onProgress('Fetching releases via GitHub API')
500-
const releaseDocs = await fetchReleaseNotes(gh.owner, gh.repo, version, resolved.gitRef, packageName).catch(() => [])
502+
const releaseDocs = await fetchReleaseNotes(gh.owner, gh.repo, version, resolved.gitRef, packageName, opts.from).catch(() => [])
501503

502504
// Fetch blog releases into same releases/ dir
503505
let blogDocs: Array<{ path: string, content: string }> = []

src/commands/sync.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { FeaturesConfig } from '../core/config.ts'
33
import type { ProjectState } from '../core/skills.ts'
44
import type { GitSkillSource } from '../sources/git-skills.ts'
55
import type { ResolveAttempt } from '../sources/index.ts'
6-
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
6+
import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
77
import * as p from '@clack/prompts'
88
import { defineCommand } from 'citty'
99
import { join, relative, resolve } from 'pathe'
@@ -214,6 +214,8 @@ export interface SyncOptions {
214214
eject?: boolean | string
215215
/** Override the computed skill directory name */
216216
name?: string
217+
/** Lower-bound date for release/issue/discussion collection (ISO date, e.g. "2025-07-01") */
218+
from?: string
217219
}
218220

219221
export async function syncCommand(state: ProjectState, opts: SyncOptions): Promise<void> {
@@ -502,6 +504,7 @@ interface SyncConfig {
502504
mode?: 'add' | 'update'
503505
eject?: boolean | string
504506
name?: string
507+
from?: string
505508
}
506509

507510
async function syncSinglePackage(packageName: string, config: SyncConfig): Promise<void> {
@@ -664,6 +667,7 @@ async function syncSinglePackage(packageName: string, config: SyncConfig): Promi
664667
version,
665668
useCache,
666669
features,
670+
from: config.from,
667671
onProgress: msg => resSpin.message(msg),
668672
})
669673
const resParts: string[] = []
@@ -682,13 +686,8 @@ async function syncSinglePackage(packageName: string, config: SyncConfig): Promi
682686
for (const w of resources.warnings)
683687
p.log.warn(`\x1B[33m${w}\x1B[0m`)
684688

685-
// Create symlinks or copy files (eject mode)
686-
if (config.eject) {
687-
ejectReferences(skillDir, packageName, cwd, version, resources.docsType, features)
688-
}
689-
else {
690-
linkAllReferences(skillDir, packageName, cwd, version, resources.docsType, undefined, features)
691-
}
689+
// Create symlinks (LLM needs .skilld/ to read docs, even in eject mode)
690+
linkAllReferences(skillDir, packageName, cwd, version, resources.docsType, undefined, features)
692691

693692
// ── Phase 2: Search index (skip in eject mode — not portable) ──
694693
if (features.search && !config.eject) {
@@ -792,6 +791,14 @@ async function syncSinglePackage(packageName: string, config: SyncConfig): Promi
792791
}
793792
}
794793

794+
// Eject: clean up transient .skilld/ symlinks → copy as real files
795+
if (isEject) {
796+
const skilldDir = join(skillDir, '.skilld')
797+
if (existsSync(skilldDir) && !config.debug)
798+
rmSync(skilldDir, { recursive: true, force: true })
799+
ejectReferences(skillDir, packageName, cwd, version, resources.docsType, features)
800+
}
801+
795802
// Skip agent integration in eject mode (no symlinks, no gitignore, no instructions)
796803
if (!isEject) {
797804
// Link shared dir to per-agent dirs
@@ -995,6 +1002,10 @@ export const ejectCommandDef = defineCommand({
9951002
alias: 'o',
9961003
description: 'Output directory path override',
9971004
},
1005+
from: {
1006+
type: 'string',
1007+
description: 'Collect releases/issues/discussions from this date onward (YYYY-MM-DD)',
1008+
},
9981009
...sharedArgs,
9991010
},
10001011
async run({ args }) {
@@ -1021,6 +1032,7 @@ export const ejectCommandDef = defineCommand({
10211032
debug: args.debug,
10221033
eject: args.out || true,
10231034
name: args.name,
1035+
from: args.from,
10241036
})
10251037
},
10261038
})

src/sources/discussions.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,14 +107,16 @@ export async function fetchGitHubDiscussions(
107107
repo: string,
108108
limit = 20,
109109
releasedAt?: string,
110+
fromDate?: string,
110111
): Promise<GitHubDiscussion[]> {
111112
if (!isGhAvailable())
112113
return []
113114

114115
// GraphQL discussions endpoint doesn't support date filtering,
115116
// so we fetch latest N and filter client-side. Skip entirely
116117
// if the cutoff is in the past — results would be empty anyway.
117-
if (releasedAt) {
118+
// (Skip this check when fromDate is set — we'll filter client-side below)
119+
if (!fromDate && releasedAt) {
118120
const cutoff = new Date(releasedAt)
119121
cutoff.setMonth(cutoff.getMonth() + 6)
120122
if (cutoff < new Date())
@@ -139,12 +141,14 @@ export async function fetchGitHubDiscussions(
139141
if (!Array.isArray(nodes))
140142
return []
141143

144+
const fromTs = fromDate ? new Date(fromDate).getTime() : null
142145
const discussions = nodes
143146
.filter((d: any) => d.author && !BOT_USERS.has(d.author.login))
144147
.filter((d: any) => {
145148
const cat = (d.category?.name || '').toLowerCase()
146149
return !LOW_VALUE_CATEGORIES.has(cat)
147150
})
151+
.filter((d: any) => !fromTs || new Date(d.createdAt).getTime() >= fromTs)
148152
.map((d: any) => {
149153
// Process answer — tag maintainer status
150154
let answer: string | undefined

src/sources/issues.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -289,10 +289,17 @@ function fetchIssuesByState(
289289
state: 'open' | 'closed',
290290
count: number,
291291
releasedAt?: string,
292+
fromDate?: string,
292293
): GitHubIssue[] {
293294
const fetchCount = Math.min(count * 3, 100)
294295
let datePart = ''
295-
if (state === 'closed') {
296+
if (fromDate) {
297+
// Explicit lower bound: only issues from this date onward
298+
datePart = state === 'closed'
299+
? `+closed:>=${fromDate}`
300+
: `+created:>=${fromDate}`
301+
}
302+
else if (state === 'closed') {
296303
if (releasedAt) {
297304
// For older versions, include issues closed up to 6 months after release
298305
const date = new Date(releasedAt)
@@ -459,6 +466,7 @@ export async function fetchGitHubIssues(
459466
repo: string,
460467
limit = 30,
461468
releasedAt?: string,
469+
fromDate?: string,
462470
): Promise<GitHubIssue[]> {
463471
if (!isGhAvailable())
464472
return []
@@ -468,8 +476,8 @@ export async function fetchGitHubIssues(
468476

469477
try {
470478
// Fetch more than needed so type quotas have a pool to draw from
471-
const open = fetchIssuesByState(owner, repo, 'open', Math.min(openCount * 2, 100), releasedAt)
472-
const closed = fetchIssuesByState(owner, repo, 'closed', Math.min(closedCount * 2, 50), releasedAt)
479+
const open = fetchIssuesByState(owner, repo, 'open', Math.min(openCount * 2, 100), releasedAt, fromDate)
480+
const closed = fetchIssuesByState(owner, repo, 'closed', Math.min(closedCount * 2, 50), releasedAt, fromDate)
473481
const all = [...open, ...closed]
474482
const selected = applyTypeQuotas(all, limit)
475483
enrichWithComments(owner, repo, selected)

src/sources/releases.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,12 @@ async function fetchAllReleases(owner: string, repo: string): Promise<GitHubRele
143143
* Falls back to generic tags (v1.2.3) only if no package-specific found.
144144
* If installedVersion is provided, filters out releases newer than it.
145145
*/
146-
export function selectReleases(releases: GitHubRelease[], packageName?: string, installedVersion?: string): GitHubRelease[] {
146+
export function selectReleases(releases: GitHubRelease[], packageName?: string, installedVersion?: string, fromDate?: string): GitHubRelease[] {
147147
// Check if this looks like a monorepo (has package-prefixed tags)
148148
const hasMonorepoTags = packageName && releases.some(r => tagMatchesPackage(r.tag, packageName))
149149
const installedSv = installedVersion ? parseSemver(installedVersion) : null
150150
const installedIsPrerelease = installedVersion ? isPrerelease(installedVersion) : false
151+
const fromTs = fromDate ? new Date(fromDate).getTime() : null
151152

152153
const filtered = releases.filter((r) => {
153154
const ver = extractVersion(r.tag, hasMonorepoTags ? packageName : undefined)
@@ -162,6 +163,13 @@ export function selectReleases(releases: GitHubRelease[], packageName?: string,
162163
if (hasMonorepoTags && packageName && !tagMatchesPackage(r.tag, packageName))
163164
return false
164165

166+
// Date lower bound: skip releases published before fromDate
167+
if (fromTs) {
168+
const pubDate = r.publishedAt || r.createdAt
169+
if (pubDate && new Date(pubDate).getTime() < fromTs)
170+
return false
171+
}
172+
165173
// Prerelease handling: include only when installed is also prerelease and same major.minor
166174
if (r.prerelease) {
167175
if (!installedIsPrerelease || !installedSv)
@@ -176,15 +184,17 @@ export function selectReleases(releases: GitHubRelease[], packageName?: string,
176184
return true
177185
})
178186

179-
return filtered
187+
const sorted = filtered
180188
.sort((a, b) => {
181189
const verA = extractVersion(a.tag, hasMonorepoTags ? packageName : undefined)
182190
const verB = extractVersion(b.tag, hasMonorepoTags ? packageName : undefined)
183191
if (!verA || !verB)
184192
return 0
185193
return compareSemver(parseSemver(verB)!, parseSemver(verA)!)
186194
})
187-
.slice(0, 20)
195+
196+
// No cap when fromDate is set — include all matching releases
197+
return fromDate ? sorted : sorted.slice(0, 20)
188198
}
189199

190200
/**
@@ -311,9 +321,10 @@ export async function fetchReleaseNotes(
311321
installedVersion: string,
312322
gitRef?: string,
313323
packageName?: string,
324+
fromDate?: string,
314325
): Promise<CachedDoc[]> {
315326
const releases = await fetchAllReleases(owner, repo)
316-
const selected = selectReleases(releases, packageName, installedVersion)
327+
const selected = selectReleases(releases, packageName, installedVersion, fromDate)
317328

318329
if (selected.length > 0) {
319330
// Detect changelog-redirect pattern: short stubs that just link to CHANGELOG.md

0 commit comments

Comments
 (0)