Skip to content

Commit a5f10a5

Browse files
committed
feat(app): add GitHub star prompt and migrate app data paths
Increase release-worthy user value by adding a usage-based GitHub star prompt while migrating runtime/cache paths from skills-app to skiller with legacy settings compatibility. Made-with: Cursor
1 parent c296070 commit a5f10a5

11 files changed

Lines changed: 190 additions & 16 deletions

File tree

src/main/install.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ export async function installSkillFromGit(
184184
): Promise<string> {
185185
const tempDir = join(
186186
tmpdir(),
187-
`skills-app-install-${Date.now()}-${Math.random().toString(36).slice(2)}`,
187+
`skiller-install-${Date.now()}-${Math.random().toString(36).slice(2)}`,
188188
);
189189
await simpleGit().clone(repoUrl, tempDir);
190190
if (ref && ref.trim()) {

src/main/marketplace/cache.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ import type { MarketplaceSkill } from "../marketplace-types";
1717
function appCacheDir(): string {
1818
const h = homedir();
1919
if (process.platform === "darwin") {
20-
return join(h, "Library", "Caches", "skills-app");
20+
return join(h, "Library", "Caches", "skiller");
2121
}
2222
if (process.platform === "win32") {
23-
return join(h, "AppData", "Local", "skills-app");
23+
return join(h, "AppData", "Local", "skiller");
2424
}
2525
const xdg = process.env.XDG_CACHE_HOME;
26-
if (xdg) return join(xdg, "skills-app");
27-
return join(h, ".cache", "skills-app");
26+
if (xdg) return join(xdg, "skiller");
27+
return join(h, ".cache", "skiller");
2828
}
2929

3030
export function marketplaceCacheDbPath(): string {

src/main/marketplace/install-from-marketplace.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export async function installFromMarketplace(
5757

5858
const tempDir = join(
5959
tmpdir(),
60-
`skills-app-marketplace-${Date.now()}-${Math.random().toString(36).slice(2)}`,
60+
`skiller-marketplace-${Date.now()}-${Math.random().toString(36).slice(2)}`,
6161
);
6262

6363
await simpleGit().clone(repoUrl, tempDir);

src/main/projects.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ export async function installSkillToProjectFromGit(
276276
): Promise<string> {
277277
const tempDir = join(
278278
tmpdir(),
279-
`skills-app-project-install-${Date.now()}-${Math.random().toString(36).slice(2)}`,
279+
`skiller-project-install-${Date.now()}-${Math.random().toString(36).slice(2)}`,
280280
);
281281
await simpleGit().clone(repoUrl, tempDir);
282282
if (ref && ref.trim()) {

src/main/repos.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
rmSync,
88
statSync,
99
} from "node:fs";
10-
import { homedir } from "node:os";
1110
import { basename, join } from "node:path";
1211
import { parse as parseToml } from "@iarna/toml";
1312
import simpleGit from "simple-git";
@@ -24,6 +23,7 @@ import type { AgentConfig } from "./types";
2423
import { discoverSkillDirs } from "./scanner";
2524
import { detectAgents, loadAgentConfigs } from "./registry";
2625
import { getAgentsDir } from "./paths";
26+
import { appDataRootPath } from "./settings";
2727

2828
type SkillsManifest = {
2929
name?: string;
@@ -54,7 +54,7 @@ export function localDirId(path: string): string {
5454
}
5555

5656
export function reposDir(): string {
57-
return join(homedir(), ".skills-app", "repos");
57+
return join(appDataRootPath(), "repos");
5858
}
5959

6060
function parseManifest(repoPath: string): SkillsManifest {

src/main/settings.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,25 @@ import { homedir } from "node:os";
44
import { parse as parseToml, stringify as stringifyToml } from "@iarna/toml";
55
import type { AppSettingsJson, RepoEntryJson } from "../shared/rpc-schema";
66

7+
const APP_DATA_DIR = ".skiller";
8+
const LEGACY_APP_DATA_DIR = ".skills-app";
9+
10+
export function appDataRootPath(): string {
11+
return join(homedir(), APP_DATA_DIR);
12+
}
13+
14+
function legacySettingsPath(): string {
15+
return join(homedir(), LEGACY_APP_DATA_DIR, "config.toml");
16+
}
17+
718
export function settingsPath(): string {
8-
return join(homedir(), ".skills-app", "config.toml");
19+
return join(appDataRootPath(), "config.toml");
920
}
1021

1122
export function readSettings(): AppSettingsJson {
12-
const path = settingsPath();
23+
const currentPath = settingsPath();
24+
const legacyPath = legacySettingsPath();
25+
const path = existsSync(currentPath) ? currentPath : legacyPath;
1326
if (!existsSync(path)) return {};
1427
try {
1528
const raw = readFileSync(path, "utf-8");

src/main/update.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { existsSync, rmSync } from "node:fs";
2-
import { homedir } from "node:os";
32
import { basename, join } from "node:path";
43
import { tmpdir } from "node:os";
54
import simpleGit from "simple-git";
@@ -10,6 +9,7 @@ import { writeProvenance } from "./provenance";
109
import { discoverSkillDirs, scanAllSkills } from "./scanner";
1110
import type { SkillCandidate } from "./scanner";
1211
import type { UpdateAllResult, UpdateProgress } from "./skill-types";
12+
import { appDataRootPath } from "./settings";
1313

1414
function persistentClonePath(repoUrl: string): string {
1515
const name = repoUrl
@@ -18,7 +18,7 @@ function persistentClonePath(repoUrl: string): string {
1818
.split("/")
1919
.pop()
2020
?.replace(/\.git$/, "") ?? "repo";
21-
return join(homedir(), ".skills-app", "repos", name);
21+
return join(appDataRootPath(), "repos", name);
2222
}
2323

2424
export class RepoSession {
@@ -42,7 +42,7 @@ export class RepoSession {
4242
}
4343
const temp = join(
4444
tmpdir(),
45-
`skills-app-update-${Date.now()}-${Math.random().toString(36).slice(2)}`,
45+
`skiller-update-${Date.now()}-${Math.random().toString(36).slice(2)}`,
4646
);
4747
await simpleGit().clone(repoUrl, temp);
4848
const candidates = discoverSkillDirs(temp);

src/mainview/App.tsx

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,19 @@ import OnboardingWizard from './components/OnboardingWizard'
1313
import { useTheme } from './hooks/useTheme'
1414
import CloseConfirmDialog from './components/CloseConfirmDialog'
1515

16+
const GITHUB_REPO_URL =
17+
'https://github.com/beautyfree/skiller-skills-desktop-manager'
18+
const STAR_PROMPT_MIN_LAUNCHES = 3
19+
const STAR_PROMPT_MIN_AGE_MS = 24 * 60 * 60 * 1000
20+
const STAR_PROMPT_RESHOW_LAUNCHES = 3
21+
const STAR_PROMPT_RESHOW_MS = 3 * 24 * 60 * 60 * 1000
22+
1623
function AppInner() {
1724
const queryClient = useQueryClient()
1825
const { i18n } = useTranslation()
1926
useTheme()
2027
const [closeDialogOpen, setCloseDialogOpen] = useState(false)
28+
const [showGithubStarPrompt, setShowGithubStarPrompt] = useState(false)
2129
// Onboarding shows on very first launch (or when user explicitly replays it
2230
// from Settings). Guarded by a localStorage flag — we respect privacy mode
2331
// by falling back to "not done" if storage throws, which still shows once.
@@ -92,6 +100,104 @@ function AppInner() {
92100
.catch(() => {})
93101
}, []) // eslint-disable-line react-hooks/exhaustive-deps
94102

103+
// Ask for a GitHub star once after meaningful usage cadence.
104+
useEffect(() => {
105+
let cancelled = false
106+
void invoke('read_settings')
107+
.then(async (settings) => {
108+
const existing = settings.github_star_prompt ?? {}
109+
const now = Date.now()
110+
const nowIso = new Date(now).toISOString()
111+
const firstSeenAt = existing.first_seen_at ?? nowIso
112+
const launchCount = Math.max(0, existing.launch_count ?? 0) + 1
113+
const hasClicked = Boolean(existing.cta_clicked_at)
114+
const dismissedAt = existing.dismissed_at
115+
const dismissedLaunchCount = Math.max(
116+
0,
117+
existing.dismissed_launch_count ?? 0,
118+
)
119+
const firstSeenMs = Date.parse(firstSeenAt)
120+
const ageMs = Number.isFinite(firstSeenMs) ? now - firstSeenMs : 0
121+
const reachedInitialCadence =
122+
launchCount >= STAR_PROMPT_MIN_LAUNCHES &&
123+
ageMs >= STAR_PROMPT_MIN_AGE_MS
124+
125+
let shouldPrompt = false
126+
if (!hasClicked && reachedInitialCadence) {
127+
if (!dismissedAt) {
128+
shouldPrompt = true
129+
} else {
130+
const dismissedMs = Date.parse(dismissedAt)
131+
const dismissedAgeMs = Number.isFinite(dismissedMs)
132+
? now - dismissedMs
133+
: 0
134+
const launchesSinceDismiss = Math.max(
135+
0,
136+
launchCount - dismissedLaunchCount,
137+
)
138+
shouldPrompt =
139+
launchesSinceDismiss >= STAR_PROMPT_RESHOW_LAUNCHES ||
140+
dismissedAgeMs >= STAR_PROMPT_RESHOW_MS
141+
}
142+
}
143+
144+
const nextSettings = {
145+
...settings,
146+
github_star_prompt: {
147+
...existing,
148+
first_seen_at: firstSeenAt,
149+
launch_count: launchCount,
150+
},
151+
}
152+
153+
await invoke('write_settings', { settings: nextSettings })
154+
if (!cancelled && shouldPrompt) setShowGithubStarPrompt(true)
155+
})
156+
.catch(() => {})
157+
return () => {
158+
cancelled = true
159+
}
160+
}, [])
161+
162+
const handleGithubStarDismiss = () => {
163+
setShowGithubStarPrompt(false)
164+
void invoke('read_settings')
165+
.then((settings) => {
166+
const existing = settings.github_star_prompt ?? {}
167+
const launchCount = Math.max(0, existing.launch_count ?? 0)
168+
return invoke('write_settings', {
169+
settings: {
170+
...settings,
171+
github_star_prompt: {
172+
...existing,
173+
dismissed_at: new Date().toISOString(),
174+
dismissed_launch_count: launchCount,
175+
dismiss_count: Math.max(0, existing.dismiss_count ?? 0) + 1,
176+
},
177+
},
178+
})
179+
})
180+
.catch(() => {})
181+
}
182+
183+
const handleGithubStarClick = () => {
184+
setShowGithubStarPrompt(false)
185+
void invoke('open_external', { url: GITHUB_REPO_URL })
186+
void invoke('read_settings')
187+
.then((settings) =>
188+
invoke('write_settings', {
189+
settings: {
190+
...settings,
191+
github_star_prompt: {
192+
...(settings.github_star_prompt ?? {}),
193+
cta_clicked_at: new Date().toISOString(),
194+
},
195+
},
196+
}),
197+
)
198+
.catch(() => {})
199+
}
200+
95201
useEffect(() => {
96202
let cancelled = false
97203
let unlisten: (() => void) | undefined
@@ -142,7 +248,15 @@ function AppInner() {
142248
{/* Electrobun / file URLs often use .../index.html as the pathname; SPA routes are /. */}
143249
<Route path="/index.html" element={<Navigate to="/" replace />} />
144250
<Route path="/main/index.html" element={<Navigate to="/" replace />} />
145-
<Route element={<Layout />}>
251+
<Route
252+
element={
253+
<Layout
254+
showGithubStarPrompt={showGithubStarPrompt}
255+
onDismissGithubStarPrompt={handleGithubStarDismiss}
256+
onGithubStarPromptCta={handleGithubStarClick}
257+
/>
258+
}
259+
>
146260
<Route index element={<Dashboard />} />
147261
<Route path="skills" element={<SkillsManager />} />
148262
<Route path="marketplace" element={<Marketplace />} />

src/mainview/components/Layout.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,21 @@ const TITLE_BAR_DRAG_HEIGHT = 36
6161
*/
6262
const DRAG_CLASSES = 'app-drag electrobun-webkit-app-region-drag'
6363

64-
export default function Layout() {
64+
type LayoutProps = {
65+
showGithubStarPrompt?: boolean
66+
onDismissGithubStarPrompt?: () => void
67+
onGithubStarPromptCta?: () => void
68+
}
69+
70+
export default function Layout(props: LayoutProps) {
71+
return <LayoutInner {...props} />
72+
}
73+
74+
function LayoutInner({
75+
showGithubStarPrompt = false,
76+
onDismissGithubStarPrompt,
77+
onGithubStarPromptCta,
78+
}: LayoutProps) {
6579
const { t } = useTranslation()
6680
const [importMode, setImportMode] = useState<'git' | 'local' | null>(null)
6781
const [importLocalPath, setImportLocalPath] = useState<string | null>(null)
@@ -330,6 +344,26 @@ export default function Layout() {
330344
</main>
331345

332346
<footer className="flex h-7 shrink-0 items-center justify-end gap-3 pl-2 pr-2 text-[11px] text-muted-foreground/50">
347+
{showGithubStarPrompt && (
348+
<div className="mr-auto flex items-center gap-2 rounded-md border border-border/80 bg-card/85 px-2 py-1 text-[11px] text-foreground">
349+
<span>{t('layout.starPromptText')}</span>
350+
<button
351+
type="button"
352+
className="font-medium text-primary transition-colors hover:text-primary/80"
353+
onClick={onGithubStarPromptCta}
354+
>
355+
{t('layout.starPromptAction')}
356+
</button>
357+
<button
358+
type="button"
359+
className="text-muted-foreground transition-colors hover:text-foreground"
360+
onClick={onDismissGithubStarPrompt}
361+
aria-label={t('layout.starPromptDismiss')}
362+
>
363+
{t('layout.starPromptDismiss')}
364+
</button>
365+
</div>
366+
)}
333367
<button
334368
type="button"
335369
className="transition-colors hover:text-muted-foreground/85"

src/mainview/i18n/en.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ const en = {
1212
footerDocs: "Documentation",
1313
footerRepo: "GitHub",
1414
footerFork: "Fork on GitHub",
15+
starPromptText: "Enjoying Skiller?",
16+
starPromptAction: "Star on GitHub",
17+
starPromptDismiss: "Not now",
1518
},
1619

1720
sidebar: {

0 commit comments

Comments
 (0)