🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@lucassantana/sharekit

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@lucassantana/sharekit - npm Package Compare versions

Comparing version
0.1.0
to
0.2.0
+24
-15
dist/index.js
#!/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) => {

@@ -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>;

@@ -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)` : ''})`));
}
{
"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"