@lucassantana/sharekit
Advanced tools
+24
-15
| #!/usr/bin/env node | ||
| import kleur from "kleur"; | ||
| import { install, preview, rollback, init } from "./sharekit.js"; | ||
| const VERSION = "0.1.0"; | ||
| const USAGE = `${kleur.bold("sharekit")} v${VERSION} — share your AI coding setup | ||
| import kleur from 'kleur'; | ||
| import { install, preview, rollback, init } from './sharekit.js'; | ||
| const VERSION = '0.2.0'; | ||
| const USAGE = `${kleur.bold('sharekit')} v${VERSION} — share your AI coding setup | ||
| ${kleur.cyan("init")} [skill...] scaffold a publishable profile in ./sharekit-profile | ||
| ${kleur.cyan("install")} <user> fetch, preview, apply a profile | ||
| ${kleur.cyan("preview")} <user> show changes, apply nothing | ||
| ${kleur.cyan("rollback")} <user> restore the last backup | ||
| ${kleur.cyan('init')} [skill...] scaffold a publishable profile in ./sharekit-profile | ||
| ${kleur.cyan('install')} <user>[@<ref>] [opts] fetch, preview, apply a profile | ||
| --include-hooks also install settings.json with shell hooks | ||
| ${kleur.cyan('preview')} <user>[@<ref>] show changes, apply nothing | ||
| ${kleur.cyan('rollback')} <user> restore the last backup | ||
| Publish yours: a GitHub repo named ${kleur.cyan("sharekit-profile")} with a ${kleur.cyan("sharekit.toml")}. | ||
| Publish yours: a GitHub repo named ${kleur.cyan('sharekit-profile')} with a ${kleur.cyan('sharekit.toml')}. | ||
| Pin to a branch/tag: ${kleur.cyan('sharekit install user@v1.0')} or ${kleur.cyan('sharekit install user@stable')}. | ||
| `; | ||
@@ -18,9 +20,9 @@ const cmds = { install, preview, rollback }; | ||
| async function main() { | ||
| if (!cmd || cmd === "-h" || cmd === "--help") | ||
| if (!cmd || cmd === '-h' || cmd === '--help') | ||
| return void console.log(USAGE); | ||
| if (cmd === "-V" || cmd === "--version") | ||
| if (cmd === '-V' || cmd === '--version') | ||
| return void console.log(VERSION); | ||
| if (cmd === "init") { | ||
| if (cmd === 'init') { | ||
| console.log(); | ||
| init("./sharekit-profile", rest); | ||
| init('./sharekit-profile', rest); | ||
| return; | ||
@@ -35,6 +37,13 @@ } | ||
| if (!arg) { | ||
| console.error(kleur.red(`usage: sharekit ${cmd} <user>`)); | ||
| console.error(kleur.red(`usage: sharekit ${cmd} <user>${cmd === 'install' ? '[@<ref>]' : ''}`)); | ||
| process.exit(1); | ||
| } | ||
| await fn(arg); | ||
| // Parse options for install command | ||
| const opts = {}; | ||
| if (cmd === 'install') { | ||
| if (rest.includes('--include-hooks')) { | ||
| opts.includeHooks = true; | ||
| } | ||
| } | ||
| await fn(arg, opts); | ||
| } | ||
@@ -41,0 +50,0 @@ main().catch((e) => { |
+10
-3
@@ -1,2 +0,2 @@ | ||
| type Status = "new" | "changed" | "same"; | ||
| type Status = 'new' | 'changed' | 'same'; | ||
| interface PlanFile { | ||
@@ -9,3 +9,3 @@ tool: string; | ||
| } | ||
| export declare function fetchProfile(user: string): string; | ||
| export declare function fetchProfile(user: string, ref?: string, baseUrl?: string, cacheRoot?: string): string; | ||
| export declare function readManifest(profileDir: string): { | ||
@@ -18,3 +18,10 @@ name: string; | ||
| export declare function printPlan(files: PlanFile[], manifest: ReturnType<typeof readManifest>): void; | ||
| export declare function install(user: string): Promise<void>; | ||
| export declare function applyProfile(files: PlanFile[], user: string, includeHooks?: boolean): { | ||
| backupDir: string; | ||
| filesWritten: number; | ||
| }; | ||
| export declare function restoreBackup(user: string): void; | ||
| export declare function install(user: string, opts?: { | ||
| includeHooks?: boolean; | ||
| }): Promise<void>; | ||
| export declare function preview(user: string): Promise<void>; | ||
@@ -21,0 +28,0 @@ export declare function rollback(user: string): Promise<void>; |
+193
-81
@@ -1,20 +0,22 @@ | ||
| import * as fs from "node:fs"; | ||
| import * as path from "node:path"; | ||
| import * as os from "node:os"; | ||
| import * as readline from "node:readline/promises"; | ||
| import { stdin as input, stdout as output } from "node:process"; | ||
| import { execSync } from "node:child_process"; | ||
| import TOML from "@iarna/toml"; | ||
| import kleur from "kleur"; | ||
| import * as fs from 'node:fs'; | ||
| import * as path from 'node:path'; | ||
| import * as os from 'node:os'; | ||
| import * as readline from 'node:readline/promises'; | ||
| import { stdin as input, stdout as output } from 'node:process'; | ||
| import { execSync } from 'node:child_process'; | ||
| import TOML from '@iarna/toml'; | ||
| import kleur from 'kleur'; | ||
| const HOME = os.homedir(); | ||
| const STATE = path.join(HOME, ".sharekit"); | ||
| const STATE = path.join(HOME, '.sharekit'); | ||
| // profile/<tool>/** mirrors into these roots — one rule, not a filename allowlist | ||
| const ROOTS = { | ||
| claude: path.join(HOME, ".claude"), | ||
| cursor: path.join(HOME, ".cursor"), | ||
| claude: path.join(HOME, '.claude'), | ||
| cursor: path.join(HOME, '.cursor'), | ||
| shared: HOME, | ||
| }; | ||
| const tildify = (p) => (p.startsWith(HOME) ? "~" + p.slice(HOME.length) : p); | ||
| const tildify = (p) => (p.startsWith(HOME) ? '~' + p.slice(HOME.length) : p); | ||
| function walk(dir) { | ||
| return fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) => { | ||
| if (e.isSymbolicLink()) | ||
| return []; // skip symlinks: don't follow into arbitrary files; dir-links would EISDIR on copy | ||
| const p = path.join(dir, e.name); | ||
@@ -24,20 +26,42 @@ return e.isDirectory() ? walk(p) : [p]; | ||
| } | ||
| // copy a file, preserving its mode (e.g. a skill's executable toggle.sh) | ||
| function cp(src, dest) { | ||
| fs.copyFileSync(src, dest); | ||
| fs.chmodSync(dest, fs.statSync(src).mode); | ||
| } | ||
| // ponytail: profile lives at github.com/<user>/sharekit-profile — one convention, not a search | ||
| export function fetchProfile(user) { | ||
| const dir = path.join(STATE, "profiles", user); | ||
| export function fetchProfile(user, ref, baseUrl = 'https://github.com', cacheRoot = path.join(STATE, 'profiles')) { | ||
| // Cache key: <user> for HEAD, <user>@<ref> for a pinned ref | ||
| const cacheKey = ref ? `${user}@${ref}` : user; | ||
| const dir = path.join(cacheRoot, cacheKey); | ||
| if (fs.existsSync(dir)) { | ||
| try { | ||
| execSync(`git -C "${dir}" pull --ff-only`, { stdio: "pipe" }); | ||
| // If a ref is specified, don't pull (it's a detached pinned checkout). Just reuse. | ||
| if (!ref) { | ||
| try { | ||
| execSync(`git -C "${dir}" pull --ff-only`, { stdio: 'pipe' }); | ||
| } | ||
| catch { | ||
| // ponytail: refresh is best-effort — offline / no-remote falls back to the cached copy | ||
| } | ||
| } | ||
| catch { | ||
| // ponytail: refresh is best-effort — offline / no-remote falls back to the cached copy | ||
| } | ||
| return dir; | ||
| } | ||
| fs.mkdirSync(path.dirname(dir), { recursive: true }); | ||
| const url = `https://github.com/${user}/sharekit-profile`; | ||
| const url = `${baseUrl}/${user}/sharekit-profile`; | ||
| try { | ||
| execSync(`git clone --depth 1 "${url}" "${dir}"`, { stdio: "pipe" }); | ||
| if (ref) { | ||
| execSync(`git clone --depth 1 --branch "${ref}" "${url}" "${dir}"`, { stdio: 'pipe' }); | ||
| } | ||
| else { | ||
| execSync(`git clone --depth 1 "${url}" "${dir}"`, { stdio: 'pipe' }); | ||
| } | ||
| } | ||
| catch { | ||
| catch (e) { | ||
| if (e.status === 127) { | ||
| throw new Error('git not found — install git to use sharekit (https://git-scm.com)'); | ||
| } | ||
| const errMsg = e.message || ''; | ||
| if (ref && errMsg.includes('not found in')) { | ||
| throw new Error(`ref '${ref}' not found in ${user}'s profile`); | ||
| } | ||
| throw new Error(`No profile at ${url}\n` + | ||
@@ -49,7 +73,18 @@ ` Publish yours: a repo named "sharekit-profile" with a sharekit.toml`); | ||
| export function readManifest(profileDir) { | ||
| const p = path.join(profileDir, "sharekit.toml"); | ||
| const p = path.join(profileDir, 'sharekit.toml'); | ||
| if (!fs.existsSync(p)) | ||
| throw new Error(`Not a sharekit profile (no sharekit.toml in ${profileDir})`); | ||
| const profile = (TOML.parse(fs.readFileSync(p, "utf8")).profile ?? {}); | ||
| return { name: profile.name ?? "unknown", version: profile.version, description: profile.description }; | ||
| let parsed; | ||
| try { | ||
| parsed = TOML.parse(fs.readFileSync(p, 'utf8')); | ||
| } | ||
| catch (e) { | ||
| throw new Error(`Invalid sharekit.toml: ${e.message}`); | ||
| } | ||
| const profile = (parsed.profile ?? {}); | ||
| return { | ||
| name: profile.name ?? 'unknown', | ||
| version: profile.version, | ||
| description: profile.description, | ||
| }; | ||
| } | ||
@@ -72,12 +107,12 @@ export function plan(profileDir, roots = ROOTS) { | ||
| if (!fs.existsSync(dest)) | ||
| return "new"; | ||
| return fs.readFileSync(src).equals(fs.readFileSync(dest)) ? "same" : "changed"; | ||
| return 'new'; | ||
| return fs.readFileSync(src).equals(fs.readFileSync(dest)) ? 'same' : 'changed'; | ||
| } | ||
| // ponytail: settings.json carries hooks (arbitrary shell). v1 never auto-installs it. | ||
| // add `--include-hooks` when someone actually asks. | ||
| const isExecutable = (f) => f.tool === "claude" && path.basename(f.dest) === "settings.json"; | ||
| const isExecutable = (f, includeHooks = false) => !includeHooks && f.tool === 'claude' && path.basename(f.dest) === 'settings.json'; | ||
| export function printPlan(files, manifest) { | ||
| console.log(kleur.bold(`Profile: ${manifest.name}${manifest.version ? " v" + manifest.version : ""}`)); | ||
| console.log(kleur.bold(`Profile: ${manifest.name}${manifest.version ? ' v' + manifest.version : ''}`)); | ||
| if (manifest.description) | ||
| console.log(kleur.dim(" " + manifest.description)); | ||
| console.log(kleur.dim(' ' + manifest.description)); | ||
| const show = (s, label, c) => { | ||
@@ -91,8 +126,8 @@ const g = files.filter((f) => f.status === s); | ||
| }; | ||
| show("new", "+ new", kleur.green); | ||
| show("changed", "~ changed", kleur.yellow); | ||
| const same = files.filter((f) => f.status === "same").length; | ||
| show('new', '+ new', kleur.green); | ||
| show('changed', '~ changed', kleur.yellow); | ||
| const same = files.filter((f) => f.status === 'same').length; | ||
| if (same) | ||
| console.log(kleur.dim(`\n = ${same} unchanged`)); | ||
| if (files.some(isExecutable)) | ||
| if (files.some((f) => isExecutable(f))) | ||
| console.log(kleur.yellow(`\n ⚠ settings.json present — contains hooks; skipped. Merge manually.`)); | ||
@@ -104,24 +139,40 @@ } | ||
| rl.close(); | ||
| return a.trim().toLowerCase() === "y"; | ||
| return a.trim().toLowerCase() === 'y'; | ||
| } | ||
| function backup(files, user) { | ||
| const stamp = new Date().toISOString().replace(/[:.]/g, "-"); | ||
| const dir = path.join(STATE, "backups", `${user}-${stamp}`); | ||
| const applied = files.filter((f) => f.status !== "same" && !isExecutable(f)); | ||
| // Prune backups, keeping only the most recent 5 | ||
| function pruneBackups(user) { | ||
| const root = path.join(STATE, 'backups'); | ||
| if (!fs.existsSync(root)) | ||
| return; | ||
| const dirs = fs | ||
| .readdirSync(root) | ||
| .filter((e) => e.startsWith(user + '-')) | ||
| .sort(); | ||
| if (dirs.length > 5) { | ||
| const toRemove = dirs.slice(0, dirs.length - 5); | ||
| for (const dir of toRemove) { | ||
| fs.rmSync(path.join(root, dir), { recursive: true, force: true }); | ||
| } | ||
| } | ||
| } | ||
| function backup(files, user, includeHooks = false) { | ||
| const stamp = new Date().toISOString().replace(/[:.]/g, '-'); | ||
| const dir = path.join(STATE, 'backups', `${user}-${stamp}`); | ||
| const applied = files.filter((f) => f.status !== 'same' && !isExecutable(f, includeHooks)); | ||
| fs.mkdirSync(dir, { recursive: true }); | ||
| for (const f of applied.filter((f) => f.status === "changed")) { | ||
| for (const f of applied.filter((f) => f.status === 'changed')) { | ||
| const t = path.join(dir, path.relative(HOME, f.dest)); | ||
| fs.mkdirSync(path.dirname(t), { recursive: true }); | ||
| fs.copyFileSync(f.dest, t); | ||
| cp(f.dest, t); | ||
| } | ||
| fs.writeFileSync(path.join(dir, "applied.json"), JSON.stringify(applied.map((f) => ({ dest: f.dest, status: f.status })), null, 2)); | ||
| fs.writeFileSync(path.join(dir, 'applied.json'), JSON.stringify(applied.map((f) => ({ dest: f.dest, status: f.status })), null, 2)); | ||
| return dir; | ||
| } | ||
| function write(files) { | ||
| function write(files, includeHooks = false) { | ||
| let n = 0; | ||
| for (const f of files) { | ||
| if (f.status === "same" || isExecutable(f)) | ||
| if (f.status === 'same' || isExecutable(f, includeHooks)) | ||
| continue; | ||
| fs.mkdirSync(path.dirname(f.dest), { recursive: true }); | ||
| fs.copyFileSync(f.src, f.dest); | ||
| cp(f.src, f.dest); | ||
| n++; | ||
@@ -131,4 +182,39 @@ } | ||
| } | ||
| export async function install(user) { | ||
| const dir = fetchProfile(user); | ||
| // Exported pure functions for testability | ||
| export function applyProfile(files, user, includeHooks = false) { | ||
| const backupDir = backup(files, user, includeHooks); | ||
| const filesWritten = write(files, includeHooks); | ||
| pruneBackups(user); | ||
| return { backupDir, filesWritten }; | ||
| } | ||
| export function restoreBackup(user) { | ||
| const root = path.join(STATE, 'backups'); | ||
| const last = fs.existsSync(root) | ||
| ? fs | ||
| .readdirSync(root) | ||
| .filter((e) => e.startsWith(user + '-')) | ||
| .sort() | ||
| .pop() | ||
| : undefined; | ||
| if (!last) | ||
| throw new Error(`No backup for ${user}.`); | ||
| const dir = path.join(root, last); | ||
| const applied = JSON.parse(fs.readFileSync(path.join(dir, 'applied.json'), 'utf8')); | ||
| for (const a of applied) { | ||
| if (a.status === 'new') | ||
| fs.rmSync(a.dest, { force: true }); // ponytail: leaves empty parent dirs; harmless | ||
| else { | ||
| const src = path.join(dir, path.relative(HOME, a.dest)); | ||
| if (fs.existsSync(src)) { | ||
| fs.mkdirSync(path.dirname(a.dest), { recursive: true }); // dest dir may have been removed since install | ||
| cp(src, a.dest); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| export async function install(user, opts) { | ||
| const includeHooks = opts?.includeHooks ?? false; | ||
| const userRef = user.includes('@') ? user.split('@').reverse()[0] : undefined; | ||
| const userName = userRef ? user.slice(0, user.lastIndexOf('@')) : user; | ||
| const dir = fetchProfile(userName, userRef); | ||
| const manifest = readManifest(dir); | ||
@@ -138,14 +224,27 @@ const files = plan(dir); | ||
| printPlan(files, manifest); | ||
| const todo = files.filter((f) => f.status !== "same" && !isExecutable(f)); | ||
| const todo = files.filter((f) => f.status !== 'same' && !isExecutable(f, includeHooks)); | ||
| if (!todo.length) | ||
| return void console.log(kleur.dim("\n Already up to date.\n")); | ||
| return void console.log(kleur.dim('\n Already up to date.\n')); | ||
| // If hooks present and not explicitly included, warn | ||
| const hasHooks = files.some((f) => isExecutable(f, false)); | ||
| if (hasHooks && !includeHooks) { | ||
| console.log(kleur.yellow(`\n ⚠ This profile's settings.json contains hooks that run shell commands.`)); | ||
| } | ||
| // If hooks present and user wants to include them, ask for explicit confirm | ||
| if (hasHooks && includeHooks) { | ||
| if (!(await confirm(`This profile's settings.json contains hooks that run shell commands. Install it?`))) { | ||
| return void console.log(kleur.dim('\n Aborted.\n')); | ||
| } | ||
| } | ||
| if (!(await confirm(`Apply ${todo.length} change(s)?`))) | ||
| return void console.log(kleur.dim("\n Aborted.\n")); | ||
| const b = backup(files, user); | ||
| const n = write(files); | ||
| console.log(kleur.green(`\n ✓ Applied ${n} file(s).`) + kleur.dim(` Backup: ${tildify(b)}`)); | ||
| console.log(kleur.dim(` Undo: sharekit rollback ${user}\n`)); | ||
| return void console.log(kleur.dim('\n Aborted.\n')); | ||
| const { backupDir, filesWritten } = applyProfile(files, userName, includeHooks); | ||
| console.log(kleur.green(`\n ✓ Applied ${filesWritten} file(s).`) + | ||
| kleur.dim(` Backup: ${tildify(backupDir)}`)); | ||
| console.log(kleur.dim(` Undo: sharekit rollback ${userName}\n`)); | ||
| } | ||
| export async function preview(user) { | ||
| const dir = fetchProfile(user); | ||
| const userRef = user.includes('@') ? user.split('@').reverse()[0] : undefined; | ||
| const userName = userRef ? user.slice(0, user.lastIndexOf('@')) : user; | ||
| const dir = fetchProfile(userName, userRef); | ||
| console.log(); | ||
@@ -156,5 +255,9 @@ printPlan(plan(dir), readManifest(dir)); | ||
| export async function rollback(user) { | ||
| const root = path.join(STATE, "backups"); | ||
| const root = path.join(STATE, 'backups'); | ||
| const last = fs.existsSync(root) | ||
| ? fs.readdirSync(root).filter((e) => e.startsWith(user + "-")).sort().pop() | ||
| ? fs | ||
| .readdirSync(root) | ||
| .filter((e) => e.startsWith(user + '-')) | ||
| .sort() | ||
| .pop() | ||
| : undefined; | ||
@@ -164,16 +267,8 @@ if (!last) | ||
| const dir = path.join(root, last); | ||
| const applied = JSON.parse(fs.readFileSync(path.join(dir, "applied.json"), "utf8")); | ||
| const applied = JSON.parse(fs.readFileSync(path.join(dir, 'applied.json'), 'utf8')); | ||
| console.log(kleur.bold(`\n Rollback ${user} → ${tildify(dir)} (${applied.length} file(s))\n`)); | ||
| if (!(await confirm("Restore?"))) | ||
| return void console.log(kleur.dim("\n Aborted.\n")); | ||
| for (const a of applied) { | ||
| if (a.status === "new") | ||
| fs.rmSync(a.dest, { force: true }); // ponytail: leaves empty parent dirs; harmless | ||
| else { | ||
| const src = path.join(dir, path.relative(HOME, a.dest)); | ||
| if (fs.existsSync(src)) | ||
| fs.copyFileSync(src, a.dest); | ||
| } | ||
| } | ||
| console.log(kleur.green("\n ✓ Restored.\n")); | ||
| if (!(await confirm('Restore?'))) | ||
| return void console.log(kleur.dim('\n Aborted.\n')); | ||
| restoreBackup(user); | ||
| console.log(kleur.green('\n ✓ Restored.\n')); | ||
| } | ||
@@ -194,20 +289,37 @@ export function init(profileDir, skillNames = [], sourceRoot = HOME) { | ||
| `; | ||
| fs.writeFileSync(path.join(profileRoot, "sharekit.toml"), tomlContent); | ||
| console.log(kleur.green(` + ${tildify(path.join(profileRoot, "sharekit.toml"))}`)); | ||
| fs.writeFileSync(path.join(profileRoot, 'sharekit.toml'), tomlContent); | ||
| console.log(kleur.green(` + ${tildify(path.join(profileRoot, 'sharekit.toml'))}`)); | ||
| // 2. Copy CLAUDE.md from source root if it exists | ||
| const sourceClaude = path.join(sourceRoot, ".claude", "CLAUDE.md"); | ||
| const destClaude = path.join(profileRoot, "claude", "CLAUDE.md"); | ||
| const sourceClaude = path.join(sourceRoot, '.claude', 'CLAUDE.md'); | ||
| const destClaude = path.join(profileRoot, 'claude', 'CLAUDE.md'); | ||
| fs.mkdirSync(path.dirname(destClaude), { recursive: true }); | ||
| if (fs.existsSync(sourceClaude)) { | ||
| fs.copyFileSync(sourceClaude, destClaude); | ||
| cp(sourceClaude, destClaude); | ||
| console.log(kleur.green(` + ${tildify(destClaude)}`)); | ||
| } | ||
| else { | ||
| fs.writeFileSync(destClaude, "# My AI coding instructions\n"); | ||
| fs.writeFileSync(destClaude, '# My AI coding instructions\n'); | ||
| console.log(kleur.green(` + ${tildify(destClaude)} (placeholder)`)); | ||
| } | ||
| // 3. Copy skills if specified | ||
| // 3. Scaffold cursor/ directory | ||
| const destCursorRules = path.join(profileRoot, 'cursor', '.cursorrules'); | ||
| fs.mkdirSync(path.dirname(destCursorRules), { recursive: true }); | ||
| const sourceCursorRules = path.join(sourceRoot, '.cursor', '.cursorrules'); | ||
| if (fs.existsSync(sourceCursorRules)) { | ||
| cp(sourceCursorRules, destCursorRules); | ||
| console.log(kleur.green(` + ${tildify(destCursorRules)}`)); | ||
| } | ||
| else { | ||
| fs.writeFileSync(destCursorRules, '# Cursor IDE rules\n'); | ||
| console.log(kleur.green(` + ${tildify(destCursorRules)} (placeholder)`)); | ||
| } | ||
| // 4. Scaffold shared/ directory with .gitkeep | ||
| const destShared = path.join(profileRoot, 'shared'); | ||
| fs.mkdirSync(destShared, { recursive: true }); | ||
| fs.writeFileSync(path.join(destShared, '.gitkeep'), ''); | ||
| console.log(kleur.green(` + ${tildify(destShared)}/`)); | ||
| // 5. Copy skills if specified | ||
| let skillCount = 0; | ||
| for (const skillName of skillNames) { | ||
| const sourceSkill = path.join(sourceRoot, ".claude", "skills", skillName); | ||
| const sourceSkill = path.join(sourceRoot, '.claude', 'skills', skillName); | ||
| if (!fs.existsSync(sourceSkill)) { | ||
@@ -217,3 +329,3 @@ console.log(kleur.yellow(` ~ skill '${skillName}' not found at ${tildify(sourceSkill)}`)); | ||
| } | ||
| const destSkillBase = path.join(profileRoot, "claude", "skills", skillName); | ||
| const destSkillBase = path.join(profileRoot, 'claude', 'skills', skillName); | ||
| fs.mkdirSync(destSkillBase, { recursive: true }); | ||
@@ -224,3 +336,3 @@ for (const file of walk(sourceSkill)) { | ||
| fs.mkdirSync(path.dirname(dest), { recursive: true }); | ||
| fs.copyFileSync(file, dest); | ||
| cp(file, dest); | ||
| console.log(kleur.green(` + ${tildify(dest)}`)); | ||
@@ -231,3 +343,3 @@ skillCount++; | ||
| console.log(kleur.green(`\n ✓ Created profile at ${tildify(profileRoot)}` + | ||
| ` (sharekit.toml, CLAUDE.md${skillCount > 0 ? `, ${skillCount} skill file(s)` : ""})`)); | ||
| ` (sharekit.toml, CLAUDE.md, cursor/, shared/${skillCount > 0 ? `, ${skillCount} skill file(s)` : ''})`)); | ||
| } |
+4
-2
| { | ||
| "name": "@lucassantana/sharekit", | ||
| "version": "0.1.0", | ||
| "version": "0.2.0", | ||
| "description": "Share your AI coding setup with anyone — one command to install, one to rollback.", | ||
@@ -32,3 +32,4 @@ "keywords": [ | ||
| "typecheck": "tsc -p tsconfig.json --noEmit", | ||
| "prepublishOnly": "pnpm run build" | ||
| "format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"", | ||
| "prepublishOnly": "npm run build" | ||
| }, | ||
@@ -41,2 +42,3 @@ "dependencies": { | ||
| "@types/node": "^22.0.0", | ||
| "prettier": "^3.3.0", | ||
| "tsx": "^4.19.0", | ||
@@ -43,0 +45,0 @@ "typescript": "^5.7.0" |
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
21661
35.66%411
45.23%4
33.33%