skillwatch 0.1.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/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Matt Blode
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # skillwatch
2
+
3
+ Daily macOS notifications when installed GitHub-backed skills have updates.
4
+
5
+ - Compares each installed skill's `skillFolderHash` with the current GitHub tree hash for that folder
6
+ - Installs a per-user LaunchAgent that runs once per day
7
+ - Deduplicates notifications until the update set changes
8
+
9
+ ## Requirements
10
+
11
+ - macOS
12
+ - Node.js 22 or newer
13
+ - An existing skills lock file at `~/.agents/.skill-lock.json` or `$XDG_STATE_HOME/skills/.skill-lock.json`
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npx skillwatch install
19
+ ```
20
+
21
+ Default schedule is daily at `09:00` local time.
22
+
23
+ If you want the command installed permanently:
24
+
25
+ ```bash
26
+ npm install -g skillwatch
27
+ skillwatch install
28
+ ```
29
+
30
+ ## Verify
31
+
32
+ ```bash
33
+ npx skillwatch check-now
34
+ ```
35
+
36
+ ## Uninstall
37
+
38
+ ```bash
39
+ npx skillwatch uninstall
40
+ ```
41
+
42
+ If you installed the CLI globally and want to remove that too:
43
+
44
+ ```bash
45
+ npm uninstall -g skillwatch
46
+ ```
47
+
48
+ This removes the LaunchAgent, installed support files, state, and logs. The global uninstall only removes the CLI package.
49
+
50
+ ## Custom Time
51
+
52
+ ```bash
53
+ npx skillwatch install --hour 14 --minute 30
54
+ ```
55
+
56
+ ## What Gets Installed
57
+
58
+ - LaunchAgent: `~/Library/LaunchAgents/com.mblode.skillwatch.plist`
59
+ - Checker: `~/Library/Application Support/skillwatch/checker.js`
60
+ - State: `~/Library/Application Support/skillwatch/state.json`
61
+ - Logs: `~/Library/Logs/skillwatch/`
62
+
63
+ ## Troubleshooting
64
+
65
+ - If you see `No skill lock file found`, run `skills` or `npx skills` first so the lock file exists.
66
+ - If your Node path changes later, rerun `npx skillwatch install` so the LaunchAgent points at the new `node` binary.
67
+ - This project is macOS-only and requires `launchctl`, `plutil`, and `osascript`.
68
+
69
+ ## Local Development
70
+
71
+ Use the repo wrapper if you are working from source:
72
+
73
+ ```bash
74
+ git clone https://github.com/mblode/update-skills.git
75
+ cd update-skills
76
+ npm install
77
+ ./install.sh
78
+ ```
79
+
80
+ `./install.sh` auto-builds `dist/` if needed and forwards to the built CLI.
81
+
82
+ Use the built CLI directly if you want to test the package output:
83
+
84
+ ```bash
85
+ npm run build
86
+ node dist/cli.js install
87
+ node dist/cli.js check-now
88
+ node dist/cli.js uninstall
89
+ ```
90
+
91
+ Install command options from `--help`:
92
+
93
+ ```text
94
+ --hour <0-23> Daily check hour, default 9
95
+ --minute <0-59> Daily check minute, default 0
96
+ ```
97
+
98
+ ## Development
99
+
100
+ ```bash
101
+ npm run validate
102
+ npm run pack:dry-run
103
+ ```
104
+
105
+ ## License
106
+
107
+ [MIT](LICENSE.md)
@@ -0,0 +1,225 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { existsSync } from "node:fs";
4
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
5
+ import { homedir } from "node:os";
6
+ import { dirname, join, resolve } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ //#region src/checker.ts
9
+ const log = (message) => {
10
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
11
+ console.log(`[${timestamp}] ${message}`);
12
+ };
13
+ const readJson = async (path, fallback) => {
14
+ try {
15
+ const content = await readFile(path, "utf8");
16
+ return JSON.parse(content);
17
+ } catch {
18
+ return fallback;
19
+ }
20
+ };
21
+ const writeJson = async (path, value) => {
22
+ await mkdir(dirname(path), { recursive: true });
23
+ await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
24
+ };
25
+ const normalizeSkillFolderPath = (skillPath) => skillPath.replaceAll("\\", "/").replace(/\/?SKILL\.md$/, "").replace(/\/$/, "");
26
+ const DEFAULT_STATE_DIR = join(homedir(), "Library", "Application Support", "skillwatch");
27
+ const ENTRYPOINT_PATH = fileURLToPath(import.meta.url);
28
+ const DEFAULT_STATE_PATH = join(DEFAULT_STATE_DIR, "state.json");
29
+ const DEFAULT_LOCK_PATH = process.env.XDG_STATE_HOME ? join(process.env.XDG_STATE_HOME, "skills", ".skill-lock.json") : join(homedir(), ".agents", ".skill-lock.json");
30
+ const STATE_PATH = process.env.SKILLS_CHECK_STATE_PATH || DEFAULT_STATE_PATH;
31
+ const LOCK_PATH = process.env.SKILLS_CHECK_LOCK_PATH || DEFAULT_LOCK_PATH;
32
+ const getRepoId = (entry) => {
33
+ if (typeof entry?.source === "string" && entry.source.includes("/")) return entry.source.replace(/\.git$/, "");
34
+ if (typeof entry?.sourceUrl === "string") {
35
+ const sshMatch = entry.sourceUrl.match(/^git@[^:]+:(.+?)(?:\.git)?$/);
36
+ if (sshMatch?.[1]) return sshMatch[1];
37
+ try {
38
+ const pathname = new URL(entry.sourceUrl).pathname.replace(/^\/+/, "").replace(/\.git$/, "");
39
+ if (pathname.includes("/")) return pathname;
40
+ } catch {}
41
+ }
42
+ return null;
43
+ };
44
+ const getGitHubToken = () => {
45
+ const envToken = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
46
+ if (envToken) return envToken;
47
+ const ghCandidates = [
48
+ process.env.GH_PATH,
49
+ "/opt/homebrew/bin/gh",
50
+ "/usr/local/bin/gh",
51
+ "gh"
52
+ ].filter(Boolean);
53
+ for (const candidate of ghCandidates) try {
54
+ const token = execFileSync(candidate, ["auth", "token"], {
55
+ encoding: "utf8",
56
+ stdio: [
57
+ "ignore",
58
+ "pipe",
59
+ "ignore"
60
+ ]
61
+ }).trim();
62
+ if (token) return token;
63
+ } catch {}
64
+ return null;
65
+ };
66
+ const fetchGitHubJson = async (url, token) => {
67
+ try {
68
+ const response = await fetch(url, { headers: {
69
+ Accept: "application/vnd.github.v3+json",
70
+ "User-Agent": "skillwatch",
71
+ ...token ? { Authorization: `Bearer ${token}` } : {}
72
+ } });
73
+ if (!response.ok) return null;
74
+ return await response.json();
75
+ } catch {
76
+ return null;
77
+ }
78
+ };
79
+ const fetchRepoTree = async (ownerRepo, token) => {
80
+ const branches = [
81
+ (await fetchGitHubJson(`https://api.github.com/repos/${ownerRepo}`, token))?.default_branch,
82
+ "main",
83
+ "master"
84
+ ].filter(Boolean);
85
+ const uniqueBranches = [...new Set(branches)];
86
+ for (const branch of uniqueBranches) {
87
+ const treeData = await fetchGitHubJson(`https://api.github.com/repos/${ownerRepo}/git/trees/${branch}?recursive=1`, token);
88
+ if (treeData) return treeData;
89
+ }
90
+ return null;
91
+ };
92
+ const findFolderHashInTree = (treeData, skillPath) => {
93
+ const folderPath = normalizeSkillFolderPath(skillPath);
94
+ if (!folderPath) return treeData?.sha ?? null;
95
+ if (!Array.isArray(treeData?.tree)) return null;
96
+ return treeData.tree.find((entry) => entry.type === "tree" && entry.path === folderPath)?.sha ?? null;
97
+ };
98
+ const groupUpdatesByRepo = (updates) => {
99
+ const grouped = /* @__PURE__ */ new Map();
100
+ for (const update of updates) {
101
+ const existing = grouped.get(update.repoId) ?? [];
102
+ existing.push(update.skillName);
103
+ grouped.set(update.repoId, existing);
104
+ }
105
+ return [...grouped.entries()].map(([repoId, skillNames]) => ({
106
+ repoId,
107
+ skillNames: skillNames.toSorted()
108
+ })).toSorted((a, b) => a.repoId.localeCompare(b.repoId));
109
+ };
110
+ const buildNotificationBody = (grouped) => {
111
+ const preview = grouped.slice(0, 3).map(({ repoId, skillNames }) => `${repoId} (${skillNames.length})`);
112
+ if (grouped.length > 3) preview.push(`+${grouped.length - 3} more`);
113
+ return preview.join(", ");
114
+ };
115
+ const buildSignature = (grouped) => createHash("sha256").update(JSON.stringify(grouped)).digest("hex");
116
+ const escapeAppleScript = (value) => value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
117
+ const sendNotification = (title, body) => {
118
+ execFileSync("/usr/bin/osascript", ["-e", `display notification "${escapeAppleScript(body)}" with title "${escapeAppleScript(title)}"`], { stdio: "ignore" });
119
+ };
120
+ const writeState = async (signature, grouped) => {
121
+ await writeJson(STATE_PATH, {
122
+ lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
123
+ lastNotifiedSignature: signature,
124
+ ...grouped ? { updates: grouped } : {}
125
+ });
126
+ };
127
+ const getTrackedSkills = async () => {
128
+ const lockFile = await readJson(LOCK_PATH, { skills: {} });
129
+ const tracked = [];
130
+ for (const [skillName, entry] of Object.entries(lockFile.skills ?? {})) {
131
+ if (entry?.sourceType !== "github") continue;
132
+ if (!entry.skillPath || !entry.skillFolderHash) continue;
133
+ const repoId = getRepoId(entry);
134
+ if (!repoId) continue;
135
+ tracked.push({
136
+ localHash: entry.skillFolderHash,
137
+ repoId,
138
+ skillName,
139
+ skillPath: entry.skillPath
140
+ });
141
+ }
142
+ return tracked;
143
+ };
144
+ const findUpdates = async () => {
145
+ const trackedSkills = await getTrackedSkills();
146
+ const token = getGitHubToken();
147
+ const updates = [];
148
+ const errors = [];
149
+ const treeCache = /* @__PURE__ */ new Map();
150
+ for (const tracked of trackedSkills) {
151
+ let treeData = treeCache.get(tracked.repoId);
152
+ if (treeData === void 0) {
153
+ treeData = await fetchRepoTree(tracked.repoId, token);
154
+ treeCache.set(tracked.repoId, treeData);
155
+ }
156
+ const remoteHash = treeData ? findFolderHashInTree(treeData, tracked.skillPath) : null;
157
+ if (!remoteHash) {
158
+ errors.push({
159
+ reason: "Could not fetch remote folder hash",
160
+ repoId: tracked.repoId,
161
+ skillName: tracked.skillName
162
+ });
163
+ continue;
164
+ }
165
+ if (remoteHash !== tracked.localHash) updates.push({
166
+ localHash: tracked.localHash,
167
+ remoteHash,
168
+ repoId: tracked.repoId,
169
+ skillName: tracked.skillName
170
+ });
171
+ }
172
+ return {
173
+ errors,
174
+ trackedSkills,
175
+ updates
176
+ };
177
+ };
178
+ const main = async () => {
179
+ log(`Using lock file: ${LOCK_PATH}`);
180
+ log(`Using state file: ${STATE_PATH}`);
181
+ if (!existsSync(LOCK_PATH)) {
182
+ log("No skill lock file found. Nothing to check.");
183
+ process.exitCode = 0;
184
+ return;
185
+ }
186
+ const { trackedSkills, updates, errors } = await findUpdates();
187
+ const grouped = groupUpdatesByRepo(updates);
188
+ log(`Tracked ${trackedSkills.length} GitHub-backed skill(s).`);
189
+ if (errors.length > 0) for (const error of errors) log(`Warning: ${error.repoId}/${error.skillName} - ${error.reason}`);
190
+ if (grouped.length === 0) {
191
+ log("No updates available.");
192
+ await writeState(null);
193
+ process.exitCode = 0;
194
+ return;
195
+ }
196
+ for (const repo of grouped) log(`Update available: ${repo.repoId} -> ${repo.skillNames.join(", ")}`);
197
+ const state = await readJson(STATE_PATH, {});
198
+ const signature = buildSignature(grouped);
199
+ if (state.lastNotifiedSignature === signature) {
200
+ log("Updates already notified. Skipping duplicate notification.");
201
+ await writeState(signature, grouped);
202
+ process.exitCode = 0;
203
+ return;
204
+ }
205
+ const title = "Skill updates available";
206
+ const body = buildNotificationBody(grouped);
207
+ sendNotification(title, body);
208
+ log(`Notification sent: ${body}`);
209
+ log("To update installed skills, run: npx skills update");
210
+ await writeState(signature, grouped);
211
+ };
212
+ const isExecutedDirectly = () => {
213
+ const [, entryArg] = process.argv;
214
+ return entryArg !== void 0 && resolve(entryArg) === ENTRYPOINT_PATH;
215
+ };
216
+ if (isExecutedDirectly()) try {
217
+ await main();
218
+ } catch (error) {
219
+ log(`Fatal error: ${error instanceof Error ? error.stack || error.message : String(error)}`);
220
+ process.exitCode = 1;
221
+ }
222
+ //#endregion
223
+ export { buildNotificationBody, buildSignature, findFolderHashInTree, getRepoId, groupUpdatesByRepo };
224
+
225
+ //# sourceMappingURL=checker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checker.js","names":[],"sources":["../src/checker.ts"],"sourcesContent":["import { execFileSync } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport { existsSync } from \"node:fs\";\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n// --- Types ---\n\ninterface LockFileEntry {\n sourceType?: string;\n source?: string;\n sourceUrl?: string;\n skillPath?: string;\n skillFolderHash?: string;\n}\n\ninterface LockFile {\n skills?: Record<string, LockFileEntry>;\n}\n\ninterface TrackedSkill {\n skillName: string;\n repoId: string;\n skillPath: string;\n localHash: string;\n}\n\ninterface UpdateResult {\n repoId: string;\n skillName: string;\n localHash: string;\n remoteHash: string;\n}\n\ninterface CheckError {\n repoId: string;\n skillName: string;\n reason: string;\n}\n\ninterface CheckResult {\n trackedSkills: TrackedSkill[];\n updates: UpdateResult[];\n errors: CheckError[];\n}\n\ninterface GroupedUpdate {\n repoId: string;\n skillNames: string[];\n}\n\ninterface GitHubTreeEntry {\n path: string;\n type: string;\n sha: string;\n}\n\ninterface GitHubTreeResponse {\n sha?: string;\n tree?: GitHubTreeEntry[];\n}\n\ninterface GitHubRepoResponse {\n default_branch?: string;\n}\n\n// --- Utilities ---\n\nconst log = (message: string): void => {\n const timestamp = new Date().toISOString();\n console.log(`[${timestamp}] ${message}`);\n};\n\nconst readJson = async <T>(path: string, fallback: T): Promise<T> => {\n try {\n const content = await readFile(path, \"utf8\");\n return JSON.parse(content) as T;\n } catch {\n return fallback;\n }\n};\n\nconst writeJson = async (path: string, value: unknown): Promise<void> => {\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, `${JSON.stringify(value, null, 2)}\\n`, \"utf8\");\n};\n\nconst normalizeSkillFolderPath = (skillPath: string): string =>\n skillPath\n .replaceAll(\"\\\\\", \"/\")\n .replace(/\\/?SKILL\\.md$/, \"\")\n .replace(/\\/$/, \"\");\n\n// --- Config ---\n\nconst DEFAULT_STATE_DIR = join(\n homedir(),\n \"Library\",\n \"Application Support\",\n \"skillwatch\"\n);\nconst ENTRYPOINT_PATH = fileURLToPath(import.meta.url);\nconst DEFAULT_STATE_PATH = join(DEFAULT_STATE_DIR, \"state.json\");\nconst DEFAULT_LOCK_PATH = process.env.XDG_STATE_HOME\n ? join(process.env.XDG_STATE_HOME, \"skills\", \".skill-lock.json\")\n : join(homedir(), \".agents\", \".skill-lock.json\");\n\nconst STATE_PATH = process.env.SKILLS_CHECK_STATE_PATH || DEFAULT_STATE_PATH;\nconst LOCK_PATH = process.env.SKILLS_CHECK_LOCK_PATH || DEFAULT_LOCK_PATH;\n\n// --- GitHub API helpers ---\n\nconst getRepoId = (entry: LockFileEntry): string | null => {\n if (typeof entry?.source === \"string\" && entry.source.includes(\"/\")) {\n return entry.source.replace(/\\.git$/, \"\");\n }\n\n if (typeof entry?.sourceUrl === \"string\") {\n const sshMatch = entry.sourceUrl.match(/^git@[^:]+:(.+?)(?:\\.git)?$/);\n if (sshMatch?.[1]) {\n return sshMatch[1];\n }\n\n try {\n const pathname = new URL(entry.sourceUrl).pathname\n .replace(/^\\/+/, \"\")\n .replace(/\\.git$/, \"\");\n if (pathname.includes(\"/\")) {\n return pathname;\n }\n } catch {\n // Ignore parse failures and fall through.\n }\n }\n\n return null;\n};\n\nconst getGitHubToken = (): string | null => {\n const envToken = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;\n if (envToken) {\n return envToken;\n }\n\n const ghCandidates = [\n process.env.GH_PATH,\n \"/opt/homebrew/bin/gh\",\n \"/usr/local/bin/gh\",\n \"gh\",\n ].filter(Boolean) as string[];\n\n for (const candidate of ghCandidates) {\n try {\n const token = execFileSync(candidate, [\"auth\", \"token\"], {\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim();\n\n if (token) {\n return token;\n }\n } catch {\n // Try the next candidate.\n }\n }\n\n return null;\n};\n\nconst fetchGitHubJson = async (\n url: string,\n token: string | null\n): Promise<unknown> => {\n try {\n const response = await fetch(url, {\n headers: {\n Accept: \"application/vnd.github.v3+json\",\n \"User-Agent\": \"skillwatch\",\n ...(token ? { Authorization: `Bearer ${token}` } : {}),\n },\n });\n\n if (!response.ok) {\n return null;\n }\n\n return await response.json();\n } catch {\n return null;\n }\n};\n\nconst fetchRepoTree = async (\n ownerRepo: string,\n token: string | null\n): Promise<GitHubTreeResponse | null> => {\n const repoData = (await fetchGitHubJson(\n `https://api.github.com/repos/${ownerRepo}`,\n token\n )) as GitHubRepoResponse | null;\n\n const branches = [repoData?.default_branch, \"main\", \"master\"].filter(\n Boolean\n ) as string[];\n const uniqueBranches = [...new Set(branches)];\n\n for (const branch of uniqueBranches) {\n const treeData = (await fetchGitHubJson(\n `https://api.github.com/repos/${ownerRepo}/git/trees/${branch}?recursive=1`,\n token\n )) as GitHubTreeResponse | null;\n\n if (treeData) {\n return treeData;\n }\n }\n\n return null;\n};\n\nconst findFolderHashInTree = (\n treeData: GitHubTreeResponse | null,\n skillPath: string\n): string | null => {\n const folderPath = normalizeSkillFolderPath(skillPath);\n\n if (!folderPath) {\n return treeData?.sha ?? null;\n }\n\n if (!Array.isArray(treeData?.tree)) {\n return null;\n }\n\n const folderEntry = treeData.tree.find(\n (entry: GitHubTreeEntry) =>\n entry.type === \"tree\" && entry.path === folderPath\n );\n return folderEntry?.sha ?? null;\n};\n\n// --- Notification helpers ---\n\nconst groupUpdatesByRepo = (updates: UpdateResult[]): GroupedUpdate[] => {\n const grouped = new Map<string, string[]>();\n\n for (const update of updates) {\n const existing = grouped.get(update.repoId) ?? [];\n existing.push(update.skillName);\n grouped.set(update.repoId, existing);\n }\n\n return [...grouped.entries()]\n .map(([repoId, skillNames]) => ({\n repoId,\n skillNames: skillNames.toSorted(),\n }))\n .toSorted((a, b) => a.repoId.localeCompare(b.repoId));\n};\n\nconst buildNotificationBody = (grouped: GroupedUpdate[]): string => {\n const preview = grouped\n .slice(0, 3)\n .map(({ repoId, skillNames }) => `${repoId} (${skillNames.length})`);\n\n if (grouped.length > 3) {\n preview.push(`+${grouped.length - 3} more`);\n }\n\n return preview.join(\", \");\n};\n\nconst buildSignature = (grouped: GroupedUpdate[]): string =>\n createHash(\"sha256\").update(JSON.stringify(grouped)).digest(\"hex\");\n\nconst escapeAppleScript = (value: string): string =>\n value.replaceAll(\"\\\\\", \"\\\\\\\\\").replaceAll('\"', '\\\\\"');\n\nconst sendNotification = (title: string, body: string): void => {\n execFileSync(\n \"/usr/bin/osascript\",\n [\n \"-e\",\n `display notification \"${escapeAppleScript(body)}\" with title \"${escapeAppleScript(title)}\"`,\n ],\n {\n stdio: \"ignore\",\n }\n );\n};\n\nconst writeState = async (\n signature: string | null,\n grouped?: GroupedUpdate[]\n): Promise<void> => {\n await writeJson(STATE_PATH, {\n lastCheckedAt: new Date().toISOString(),\n lastNotifiedSignature: signature,\n ...(grouped ? { updates: grouped } : {}),\n });\n};\n\n// --- Core checker logic ---\n\n// Exported for tests\nexport {\n buildNotificationBody,\n buildSignature,\n findFolderHashInTree,\n getRepoId,\n groupUpdatesByRepo,\n};\n\nconst getTrackedSkills = async (): Promise<TrackedSkill[]> => {\n const lockFile = await readJson<LockFile>(LOCK_PATH, { skills: {} });\n const tracked: TrackedSkill[] = [];\n\n for (const [skillName, entry] of Object.entries(lockFile.skills ?? {})) {\n if (entry?.sourceType !== \"github\") {\n continue;\n }\n\n if (!entry.skillPath || !entry.skillFolderHash) {\n continue;\n }\n\n const repoId = getRepoId(entry);\n if (!repoId) {\n continue;\n }\n\n tracked.push({\n localHash: entry.skillFolderHash,\n repoId,\n skillName,\n skillPath: entry.skillPath,\n });\n }\n\n return tracked;\n};\n\nconst findUpdates = async (): Promise<CheckResult> => {\n const trackedSkills = await getTrackedSkills();\n const token = getGitHubToken();\n const updates: UpdateResult[] = [];\n const errors: CheckError[] = [];\n const treeCache = new Map<\n string,\n Awaited<ReturnType<typeof fetchRepoTree>>\n >();\n\n for (const tracked of trackedSkills) {\n let treeData = treeCache.get(tracked.repoId);\n\n if (treeData === undefined) {\n treeData = await fetchRepoTree(tracked.repoId, token);\n treeCache.set(tracked.repoId, treeData);\n }\n\n const remoteHash = treeData\n ? findFolderHashInTree(treeData, tracked.skillPath)\n : null;\n\n if (!remoteHash) {\n errors.push({\n reason: \"Could not fetch remote folder hash\",\n repoId: tracked.repoId,\n skillName: tracked.skillName,\n });\n continue;\n }\n\n if (remoteHash !== tracked.localHash) {\n updates.push({\n localHash: tracked.localHash,\n remoteHash,\n repoId: tracked.repoId,\n skillName: tracked.skillName,\n });\n }\n }\n\n return { errors, trackedSkills, updates };\n};\n\nconst main = async (): Promise<void> => {\n log(`Using lock file: ${LOCK_PATH}`);\n log(`Using state file: ${STATE_PATH}`);\n\n if (!existsSync(LOCK_PATH)) {\n log(\"No skill lock file found. Nothing to check.\");\n process.exitCode = 0;\n return;\n }\n\n const { trackedSkills, updates, errors } = await findUpdates();\n const grouped: GroupedUpdate[] = groupUpdatesByRepo(updates);\n\n log(`Tracked ${trackedSkills.length} GitHub-backed skill(s).`);\n\n if (errors.length > 0) {\n for (const error of errors) {\n log(`Warning: ${error.repoId}/${error.skillName} - ${error.reason}`);\n }\n }\n\n if (grouped.length === 0) {\n log(\"No updates available.\");\n await writeState(null);\n process.exitCode = 0;\n return;\n }\n\n for (const repo of grouped) {\n log(`Update available: ${repo.repoId} -> ${repo.skillNames.join(\", \")}`);\n }\n\n const state = await readJson<{ lastNotifiedSignature?: string }>(\n STATE_PATH,\n {}\n );\n const signature = buildSignature(grouped);\n\n if (state.lastNotifiedSignature === signature) {\n log(\"Updates already notified. Skipping duplicate notification.\");\n await writeState(signature, grouped);\n process.exitCode = 0;\n return;\n }\n\n const title = \"Skill updates available\";\n const body = buildNotificationBody(grouped);\n\n sendNotification(title, body);\n log(`Notification sent: ${body}`);\n log(\"To update installed skills, run: npx skills update\");\n\n await writeState(signature, grouped);\n};\n\nconst isExecutedDirectly = (): boolean => {\n const [, entryArg] = process.argv;\n\n return entryArg !== undefined && resolve(entryArg) === ENTRYPOINT_PATH;\n};\n\nif (isExecutedDirectly()) {\n try {\n await main();\n } catch (error: unknown) {\n log(\n `Fatal error: ${error instanceof Error ? error.stack || error.message : String(error)}`\n );\n process.exitCode = 1;\n }\n}\n"],"mappings":"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"}
package/dist/cli.js ADDED
@@ -0,0 +1,218 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ //#region src/cli.ts
8
+ const { version, name } = await import("../package.json", { with: { type: "json" } }).then((m) => m.default);
9
+ const ENTRYPOINT_PATH = fileURLToPath(import.meta.url);
10
+ const PACKAGE_DIR = dirname(ENTRYPOINT_PATH);
11
+ const CHECKER_SOURCE_PATH = join(PACKAGE_DIR, "checker.js");
12
+ const COMMAND_NAME = name.split("/").at(-1) ?? "skillwatch";
13
+ const APP_DIR_NAME = "skillwatch";
14
+ const LEGACY_APP_DIR_NAME = "skills-update-notifier";
15
+ const fail = (message) => {
16
+ console.error(message);
17
+ process.exit(1);
18
+ };
19
+ const parseInteger = (value, flagName, min, max) => {
20
+ const parsed = Number.parseInt(value, 10);
21
+ if (!Number.isInteger(parsed) || parsed < min || parsed > max) fail(`${flagName} must be an integer between ${min} and ${max}.`);
22
+ return parsed;
23
+ };
24
+ const LABEL = "com.mblode.skillwatch";
25
+ const LEGACY_LABEL = "com.mblode.skills-check";
26
+ const getSupportDir = (dirName) => join(homedir(), "Library", "Application Support", dirName);
27
+ const getAppDir = () => getSupportDir(APP_DIR_NAME);
28
+ const getLegacyAppDir = () => getSupportDir(LEGACY_APP_DIR_NAME);
29
+ const getLogDirForName = (dirName) => join(homedir(), "Library", "Logs", dirName);
30
+ const getLogDir = () => getLogDirForName(APP_DIR_NAME);
31
+ const getLegacyLogDir = () => getLogDirForName(LEGACY_APP_DIR_NAME);
32
+ const getPlistTargetPath = (label = LABEL) => join(homedir(), "Library", "LaunchAgents", `${label}.plist`);
33
+ const assertMacOS = () => {
34
+ if (process.platform !== "darwin") fail(`${COMMAND_NAME} only supports macOS because it uses launchd and osascript.`);
35
+ };
36
+ const requireCommand = (command) => {
37
+ try {
38
+ execFileSync("which", [command], { stdio: "ignore" });
39
+ } catch {
40
+ fail(`Missing required command: ${command}`);
41
+ }
42
+ };
43
+ const getUid = () => {
44
+ if (typeof process.getuid !== "function") fail("Could not determine the current user id.");
45
+ return process.getuid();
46
+ };
47
+ const writePlist = async (templatePath, options, paths) => {
48
+ const plistTargetPath = getPlistTargetPath();
49
+ const content = (await readFile(templatePath, "utf8")).replaceAll("__NODE_BIN__", process.execPath).replaceAll("__SCRIPT_PATH__", paths.checkerTargetPath).replaceAll("__HOUR__", String(options.hour)).replaceAll("__MINUTE__", String(options.minute)).replaceAll("__STDOUT_PATH__", paths.stdoutPath).replaceAll("__STDERR_PATH__", paths.stderrPath);
50
+ await mkdir(dirname(plistTargetPath), { recursive: true });
51
+ await writeFile(plistTargetPath, content, "utf8");
52
+ };
53
+ const lintPlist = () => {
54
+ execFileSync("plutil", ["-lint", getPlistTargetPath()], { stdio: "ignore" });
55
+ };
56
+ const bootoutIfLoaded = (label = LABEL) => {
57
+ try {
58
+ execFileSync("launchctl", ["bootout", `gui/${getUid()}/${label}`], { stdio: "ignore" });
59
+ } catch {}
60
+ };
61
+ const bootstrapAgent = () => {
62
+ execFileSync("launchctl", [
63
+ "bootstrap",
64
+ `gui/${getUid()}`,
65
+ getPlistTargetPath()
66
+ ], { stdio: "inherit" });
67
+ };
68
+ const printUsage = () => {
69
+ console.log(`
70
+ ${name} ${version}
71
+
72
+ Usage:
73
+ ${COMMAND_NAME} install [--hour 9] [--minute 0]
74
+ ${COMMAND_NAME} uninstall
75
+ ${COMMAND_NAME} check-now
76
+ ${COMMAND_NAME} --help
77
+ ${COMMAND_NAME} --version
78
+
79
+ Commands:
80
+ install Install the daily LaunchAgent and checker script
81
+ uninstall Remove the LaunchAgent, checker script, and logs
82
+ check-now Run the checker immediately
83
+
84
+ Options for install:
85
+ --hour <0-23> Daily check hour, default 9
86
+ --minute <0-59> Daily check minute, default 0
87
+ `.trim());
88
+ };
89
+ const parseInstallOptions = (args) => {
90
+ const options = {
91
+ hour: parseInteger(process.env.CHECK_HOUR ?? "9", "CHECK_HOUR", 0, 23),
92
+ minute: parseInteger(process.env.CHECK_MINUTE ?? "0", "CHECK_MINUTE", 0, 59)
93
+ };
94
+ for (let index = 0; index < args.length; index += 1) {
95
+ const current = args[index];
96
+ if (current === "--help" || current === "-h") {
97
+ printUsage();
98
+ process.exit(0);
99
+ }
100
+ if (current === "--hour") {
101
+ index += 1;
102
+ options.hour = parseInteger(args[index] ?? "", "--hour", 0, 23);
103
+ continue;
104
+ }
105
+ if (current === "--minute") {
106
+ index += 1;
107
+ options.minute = parseInteger(args[index] ?? "", "--minute", 0, 59);
108
+ continue;
109
+ }
110
+ fail(`Unknown install option: ${current}`);
111
+ }
112
+ return options;
113
+ };
114
+ const installCommand = async (args) => {
115
+ assertMacOS();
116
+ requireCommand("launchctl");
117
+ requireCommand("plutil");
118
+ const options = parseInstallOptions(args);
119
+ const appDir = getAppDir();
120
+ const logDir = getLogDir();
121
+ const checkerTargetPath = join(appDir, "checker.js");
122
+ const plistTemplatePath = join(PACKAGE_DIR, "com.mblode.skillwatch.plist.template");
123
+ await mkdir(appDir, { recursive: true });
124
+ await mkdir(logDir, { recursive: true });
125
+ await copyFile(CHECKER_SOURCE_PATH, checkerTargetPath);
126
+ await writePlist(plistTemplatePath, options, {
127
+ checkerTargetPath,
128
+ stderrPath: join(logDir, "stderr.log"),
129
+ stdoutPath: join(logDir, "stdout.log")
130
+ });
131
+ lintPlist();
132
+ bootoutIfLoaded();
133
+ bootstrapAgent();
134
+ console.log(`
135
+ Installed ${COMMAND_NAME}.
136
+
137
+ Files:
138
+ agent: ${getPlistTargetPath()}
139
+ script: ${checkerTargetPath}
140
+ logs: ${logDir}
141
+
142
+ Check now:
143
+ npx ${COMMAND_NAME} check-now
144
+ launchctl kickstart -k gui/${getUid()}/${LABEL}
145
+ `.trim());
146
+ };
147
+ const uninstallCommand = async () => {
148
+ assertMacOS();
149
+ requireCommand("launchctl");
150
+ bootoutIfLoaded();
151
+ bootoutIfLoaded(LEGACY_LABEL);
152
+ await rm(getPlistTargetPath(), { force: true });
153
+ await rm(getPlistTargetPath(LEGACY_LABEL), { force: true });
154
+ await rm(getAppDir(), {
155
+ force: true,
156
+ recursive: true
157
+ });
158
+ await rm(getLegacyAppDir(), {
159
+ force: true,
160
+ recursive: true
161
+ });
162
+ await rm(getLogDir(), {
163
+ force: true,
164
+ recursive: true
165
+ });
166
+ await rm(getLegacyLogDir(), {
167
+ force: true,
168
+ recursive: true
169
+ });
170
+ console.log(`Uninstalled ${COMMAND_NAME}.`);
171
+ };
172
+ const checkNowCommand = () => {
173
+ assertMacOS();
174
+ const installedChecker = join(getAppDir(), "checker.js");
175
+ const checkerPath = existsSync(installedChecker) ? installedChecker : CHECKER_SOURCE_PATH;
176
+ execFileSync(process.execPath, [checkerPath], {
177
+ env: process.env,
178
+ stdio: "inherit"
179
+ });
180
+ };
181
+ const main = async () => {
182
+ const [rawCommand, ...rest] = process.argv.slice(2);
183
+ const command = rawCommand === "--uninstall" ? "uninstall" : rawCommand;
184
+ if (!command || command === "--help" || command === "-h" || command === "help") {
185
+ printUsage();
186
+ return;
187
+ }
188
+ if (command === "--version" || command === "-v" || command === "version") {
189
+ console.log(version);
190
+ return;
191
+ }
192
+ if (command === "install") {
193
+ await installCommand(rest);
194
+ return;
195
+ }
196
+ if (command === "uninstall") {
197
+ await uninstallCommand();
198
+ return;
199
+ }
200
+ if (command === "check-now") {
201
+ checkNowCommand();
202
+ return;
203
+ }
204
+ fail(`Unknown command: ${command}`);
205
+ };
206
+ const isExecutedDirectly = () => {
207
+ const [, entryArg] = process.argv;
208
+ return entryArg !== void 0 && resolve(entryArg) === ENTRYPOINT_PATH;
209
+ };
210
+ if (isExecutedDirectly()) try {
211
+ await main();
212
+ } catch (error) {
213
+ fail(error instanceof Error ? error.stack || error.message : String(error));
214
+ }
215
+ //#endregion
216
+ export { parseInteger };
217
+
218
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["import { execFileSync } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { copyFile, mkdir, readFile, rm, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst { version, name } = await import(\"../package.json\", {\n with: { type: \"json\" },\n}).then((m) => m.default);\n\nconst ENTRYPOINT_PATH = fileURLToPath(import.meta.url);\nconst PACKAGE_DIR = dirname(ENTRYPOINT_PATH);\nconst CHECKER_SOURCE_PATH = join(PACKAGE_DIR, \"checker.js\");\nconst COMMAND_NAME = name.split(\"/\").at(-1) ?? \"skillwatch\";\nconst APP_DIR_NAME = \"skillwatch\";\nconst LEGACY_APP_DIR_NAME = \"skills-update-notifier\";\n\n// --- Utilities ---\n\nconst fail = (message: string): never => {\n console.error(message);\n process.exit(1);\n};\n\nexport const parseInteger = (\n value: string,\n flagName: string,\n min: number,\n max: number\n): number => {\n const parsed = Number.parseInt(value, 10);\n\n if (!Number.isInteger(parsed) || parsed < min || parsed > max) {\n fail(`${flagName} must be an integer between ${min} and ${max}.`);\n }\n\n return parsed;\n};\n\n// --- LaunchAgent management ---\n\ninterface InstallOptions {\n hour: number;\n minute: number;\n}\n\nconst LABEL = \"com.mblode.skillwatch\";\nconst LEGACY_LABEL = \"com.mblode.skills-check\";\n\nconst getSupportDir = (dirName: string): string =>\n join(homedir(), \"Library\", \"Application Support\", dirName);\n\nconst getAppDir = (): string => getSupportDir(APP_DIR_NAME);\n\nconst getLegacyAppDir = (): string => getSupportDir(LEGACY_APP_DIR_NAME);\n\nconst getLogDirForName = (dirName: string): string =>\n join(homedir(), \"Library\", \"Logs\", dirName);\n\nconst getLogDir = (): string => getLogDirForName(APP_DIR_NAME);\n\nconst getLegacyLogDir = (): string => getLogDirForName(LEGACY_APP_DIR_NAME);\n\nconst getPlistTargetPath = (label = LABEL): string =>\n join(homedir(), \"Library\", \"LaunchAgents\", `${label}.plist`);\n\nconst assertMacOS = (): void => {\n if (process.platform !== \"darwin\") {\n fail(\n `${COMMAND_NAME} only supports macOS because it uses launchd and osascript.`\n );\n }\n};\n\nconst requireCommand = (command: string): void => {\n try {\n execFileSync(\"which\", [command], {\n stdio: \"ignore\",\n });\n } catch {\n fail(`Missing required command: ${command}`);\n }\n};\n\nconst getUid = (): number => {\n if (typeof process.getuid !== \"function\") {\n fail(\"Could not determine the current user id.\");\n }\n\n // Safe: guarded above, fail() returns never\n return (process.getuid as () => number)();\n};\n\nconst writePlist = async (\n templatePath: string,\n options: InstallOptions,\n paths: {\n checkerTargetPath: string;\n stdoutPath: string;\n stderrPath: string;\n }\n): Promise<void> => {\n const plistTargetPath = getPlistTargetPath();\n const template = await readFile(templatePath, \"utf8\");\n const content = template\n .replaceAll(\"__NODE_BIN__\", process.execPath)\n .replaceAll(\"__SCRIPT_PATH__\", paths.checkerTargetPath)\n .replaceAll(\"__HOUR__\", String(options.hour))\n .replaceAll(\"__MINUTE__\", String(options.minute))\n .replaceAll(\"__STDOUT_PATH__\", paths.stdoutPath)\n .replaceAll(\"__STDERR_PATH__\", paths.stderrPath);\n\n await mkdir(dirname(plistTargetPath), { recursive: true });\n await writeFile(plistTargetPath, content, \"utf8\");\n};\n\nconst lintPlist = (): void => {\n execFileSync(\"plutil\", [\"-lint\", getPlistTargetPath()], {\n stdio: \"ignore\",\n });\n};\n\nconst bootoutIfLoaded = (label = LABEL): void => {\n try {\n execFileSync(\"launchctl\", [\"bootout\", `gui/${getUid()}/${label}`], {\n stdio: \"ignore\",\n });\n } catch {\n // Ignore when the agent is not loaded.\n }\n};\n\nconst bootstrapAgent = (): void => {\n execFileSync(\n \"launchctl\",\n [\"bootstrap\", `gui/${getUid()}`, getPlistTargetPath()],\n {\n stdio: \"inherit\",\n }\n );\n};\n\n// --- CLI commands ---\n\nconst printUsage = (): void => {\n console.log(\n `\n${name} ${version}\n\nUsage:\n ${COMMAND_NAME} install [--hour 9] [--minute 0]\n ${COMMAND_NAME} uninstall\n ${COMMAND_NAME} check-now\n ${COMMAND_NAME} --help\n ${COMMAND_NAME} --version\n\nCommands:\n install Install the daily LaunchAgent and checker script\n uninstall Remove the LaunchAgent, checker script, and logs\n check-now Run the checker immediately\n\nOptions for install:\n --hour <0-23> Daily check hour, default 9\n --minute <0-59> Daily check minute, default 0\n`.trim()\n );\n};\n\nconst parseInstallOptions = (args: string[]): InstallOptions => {\n const options: InstallOptions = {\n hour: parseInteger(process.env.CHECK_HOUR ?? \"9\", \"CHECK_HOUR\", 0, 23),\n minute: parseInteger(\n process.env.CHECK_MINUTE ?? \"0\",\n \"CHECK_MINUTE\",\n 0,\n 59\n ),\n };\n\n for (let index = 0; index < args.length; index += 1) {\n const current = args[index];\n\n if (current === \"--help\" || current === \"-h\") {\n printUsage();\n process.exit(0);\n }\n\n if (current === \"--hour\") {\n index += 1;\n options.hour = parseInteger(args[index] ?? \"\", \"--hour\", 0, 23);\n continue;\n }\n\n if (current === \"--minute\") {\n index += 1;\n options.minute = parseInteger(args[index] ?? \"\", \"--minute\", 0, 59);\n continue;\n }\n\n fail(`Unknown install option: ${current}`);\n }\n\n return options;\n};\n\nconst installCommand = async (args: string[]): Promise<void> => {\n assertMacOS();\n requireCommand(\"launchctl\");\n requireCommand(\"plutil\");\n\n const options = parseInstallOptions(args);\n const appDir = getAppDir();\n const logDir = getLogDir();\n const checkerTargetPath = join(appDir, \"checker.js\");\n const plistTemplatePath = join(\n PACKAGE_DIR,\n \"com.mblode.skillwatch.plist.template\"\n );\n\n await mkdir(appDir, { recursive: true });\n await mkdir(logDir, { recursive: true });\n await copyFile(CHECKER_SOURCE_PATH, checkerTargetPath);\n await writePlist(plistTemplatePath, options, {\n checkerTargetPath,\n stderrPath: join(logDir, \"stderr.log\"),\n stdoutPath: join(logDir, \"stdout.log\"),\n });\n lintPlist();\n\n bootoutIfLoaded();\n bootstrapAgent();\n\n console.log(\n `\nInstalled ${COMMAND_NAME}.\n\nFiles:\n agent: ${getPlistTargetPath()}\n script: ${checkerTargetPath}\n logs: ${logDir}\n\nCheck now:\n npx ${COMMAND_NAME} check-now\n launchctl kickstart -k gui/${getUid()}/${LABEL}\n`.trim()\n );\n};\n\nconst uninstallCommand = async (): Promise<void> => {\n assertMacOS();\n requireCommand(\"launchctl\");\n\n bootoutIfLoaded();\n bootoutIfLoaded(LEGACY_LABEL);\n await rm(getPlistTargetPath(), { force: true });\n await rm(getPlistTargetPath(LEGACY_LABEL), { force: true });\n await rm(getAppDir(), { force: true, recursive: true });\n await rm(getLegacyAppDir(), { force: true, recursive: true });\n await rm(getLogDir(), { force: true, recursive: true });\n await rm(getLegacyLogDir(), { force: true, recursive: true });\n\n console.log(`Uninstalled ${COMMAND_NAME}.`);\n};\n\nconst checkNowCommand = (): void => {\n assertMacOS();\n const appDir = getAppDir();\n const installedChecker = join(appDir, \"checker.js\");\n const checkerPath = existsSync(installedChecker)\n ? installedChecker\n : CHECKER_SOURCE_PATH;\n\n execFileSync(process.execPath, [checkerPath], {\n env: process.env,\n stdio: \"inherit\",\n });\n};\n\nconst main = async (): Promise<void> => {\n const [rawCommand, ...rest] = process.argv.slice(2);\n const command = rawCommand === \"--uninstall\" ? \"uninstall\" : rawCommand;\n\n if (\n !command ||\n command === \"--help\" ||\n command === \"-h\" ||\n command === \"help\"\n ) {\n printUsage();\n return;\n }\n\n if (command === \"--version\" || command === \"-v\" || command === \"version\") {\n console.log(version);\n return;\n }\n\n if (command === \"install\") {\n await installCommand(rest);\n return;\n }\n\n if (command === \"uninstall\") {\n await uninstallCommand();\n return;\n }\n\n if (command === \"check-now\") {\n checkNowCommand();\n return;\n }\n\n fail(`Unknown command: ${command}`);\n};\n\nconst isExecutedDirectly = (): boolean => {\n const [, entryArg] = process.argv;\n\n return entryArg !== undefined && resolve(entryArg) === ENTRYPOINT_PATH;\n};\n\nif (isExecutedDirectly()) {\n try {\n await main();\n } catch (error: unknown) {\n fail(error instanceof Error ? error.stack || error.message : String(error));\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>Label</key>
6
+ <string>com.mblode.skillwatch</string>
7
+
8
+ <key>ProgramArguments</key>
9
+ <array>
10
+ <string>__NODE_BIN__</string>
11
+ <string>__SCRIPT_PATH__</string>
12
+ </array>
13
+
14
+ <key>StartCalendarInterval</key>
15
+ <dict>
16
+ <key>Hour</key>
17
+ <integer>__HOUR__</integer>
18
+ <key>Minute</key>
19
+ <integer>__MINUTE__</integer>
20
+ </dict>
21
+
22
+ <key>StandardOutPath</key>
23
+ <string>__STDOUT_PATH__</string>
24
+
25
+ <key>StandardErrorPath</key>
26
+ <string>__STDERR_PATH__</string>
27
+ </dict>
28
+ </plist>
@@ -0,0 +1,59 @@
1
+ //#region package.json
2
+ var package_default = {
3
+ name: "skillwatch",
4
+ version: "0.1.0",
5
+ description: "Daily macOS notifications when installed GitHub-backed skills have updates.",
6
+ keywords: [
7
+ "agent-skills",
8
+ "launchd",
9
+ "macos",
10
+ "notification",
11
+ "skills"
12
+ ],
13
+ homepage: "https://github.com/mblode/update-skills#readme",
14
+ bugs: { "url": "https://github.com/mblode/update-skills/issues" },
15
+ license: "MIT",
16
+ author: "Matt Blode",
17
+ repository: {
18
+ "type": "git",
19
+ "url": "git+https://github.com/mblode/update-skills.git"
20
+ },
21
+ bin: "./dist/cli.js",
22
+ files: [
23
+ "dist",
24
+ "README.md",
25
+ "LICENSE.md"
26
+ ],
27
+ type: "module",
28
+ publishConfig: { "access": "public" },
29
+ scripts: {
30
+ "build": "tsdown && cp com.mblode.skillwatch.plist.template dist/",
31
+ "dev": "tsdown --watch",
32
+ "typecheck": "tsc --noEmit",
33
+ "check": "ultracite check",
34
+ "fix": "ultracite fix",
35
+ "test": "vitest run",
36
+ "smoke": "SKILLS_CHECK_LOCK_PATH=/tmp/skillwatch-missing-lock.json SKILLS_CHECK_STATE_PATH=/tmp/skillwatch-state.json node dist/checker.js && node dist/cli.js --help && node dist/cli.js --version",
37
+ "validate": "npm run build && npm run typecheck && npm run check && npm run test && npm run smoke",
38
+ "pack:dry-run": "npm pack --dry-run",
39
+ "changeset": "changeset",
40
+ "release": "npm run build && changeset publish",
41
+ "prepare": "git rev-parse --is-inside-work-tree >/dev/null 2>&1 && lefthook install || true"
42
+ },
43
+ devDependencies: {
44
+ "@changesets/cli": "^2.29.0",
45
+ "@types/node": "^22.15.0",
46
+ "lefthook": "^2.1.4",
47
+ "oxfmt": "^0.41.0",
48
+ "oxlint": "^1.56.0",
49
+ "tsdown": "^0.12.0",
50
+ "typescript": "^5.8.0",
51
+ "ultracite": "^7.3.2",
52
+ "vitest": "^3.1.0"
53
+ },
54
+ engines: { "node": ">=22" }
55
+ };
56
+ //#endregion
57
+ export { package_default as default };
58
+
59
+ //# sourceMappingURL=package-D8pw1JVM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package-D8pw1JVM.js","names":[],"sources":["../package.json"],"sourcesContent":[""],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "skillwatch",
3
+ "version": "0.1.0",
4
+ "description": "Daily macOS notifications when installed GitHub-backed skills have updates.",
5
+ "keywords": [
6
+ "agent-skills",
7
+ "launchd",
8
+ "macos",
9
+ "notification",
10
+ "skills"
11
+ ],
12
+ "homepage": "https://github.com/mblode/update-skills#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/mblode/update-skills/issues"
15
+ },
16
+ "license": "MIT",
17
+ "author": "Matt Blode",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/mblode/update-skills.git"
21
+ },
22
+ "bin": "./dist/cli.js",
23
+ "files": [
24
+ "dist",
25
+ "README.md",
26
+ "LICENSE.md"
27
+ ],
28
+ "type": "module",
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "scripts": {
33
+ "build": "tsdown && cp com.mblode.skillwatch.plist.template dist/",
34
+ "dev": "tsdown --watch",
35
+ "typecheck": "tsc --noEmit",
36
+ "check": "ultracite check",
37
+ "fix": "ultracite fix",
38
+ "test": "vitest run",
39
+ "smoke": "SKILLS_CHECK_LOCK_PATH=/tmp/skillwatch-missing-lock.json SKILLS_CHECK_STATE_PATH=/tmp/skillwatch-state.json node dist/checker.js && node dist/cli.js --help && node dist/cli.js --version",
40
+ "validate": "npm run build && npm run typecheck && npm run check && npm run test && npm run smoke",
41
+ "pack:dry-run": "npm pack --dry-run",
42
+ "changeset": "changeset",
43
+ "release": "npm run build && changeset publish",
44
+ "prepare": "git rev-parse --is-inside-work-tree >/dev/null 2>&1 && lefthook install || true"
45
+ },
46
+ "devDependencies": {
47
+ "@changesets/cli": "^2.29.0",
48
+ "@types/node": "^22.15.0",
49
+ "lefthook": "^2.1.4",
50
+ "oxfmt": "^0.41.0",
51
+ "oxlint": "^1.56.0",
52
+ "tsdown": "^0.12.0",
53
+ "typescript": "^5.8.0",
54
+ "ultracite": "^7.3.2",
55
+ "vitest": "^3.1.0"
56
+ },
57
+ "engines": {
58
+ "node": ">=22"
59
+ }
60
+ }