🎩 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.5.0
to
0.6.0
+2
-1
dist/commands.d.ts
import { Dirs } from './paths.js';
export interface InstallOpts {
includeHooks?: boolean;
includeDotfiles?: boolean;
yes?: boolean;

@@ -10,3 +11,3 @@ dryRun?: boolean;

export declare function search(query?: string): Promise<void>;
export declare function updateApply(user: string, includeHooks?: boolean, dirs?: Dirs, additive?: boolean): {
export declare function updateApply(user: string, includeHooks?: boolean, dirs?: Dirs, additive?: boolean, includeDotfiles?: boolean): {
filesWritten: number;

@@ -13,0 +14,0 @@ backupDir: string;

@@ -10,5 +10,5 @@ import * as fs from 'node:fs';

import { restoreBackup, parseApplied } from './backup.js';
import { recordInstall, readInstalled, isImmutableRef } from './state.js';
import { recordInstall, readInstalled, isImmutableRef, acquireLock, releaseLock, } from './state.js';
import { scanForSecrets, printAndGateFindings } from './scanner.js';
import { walk, tildify, DEFAULT_DIRS, cp } from './paths.js';
import { walk, tildify, DEFAULT_DIRS, cp, rootsFor } from './paths.js';
export async function confirm(q, autoYes = false) {

@@ -28,3 +28,3 @@ if (autoYes)

export async function search(query) {
const q = encodeURIComponent(`sharekit-profile in:name${query ? ` ${query}` : ''} is:archived:false`);
const q = encodeURIComponent(`sharekit-profile in:name${query ? ` ${query}` : ''} archived:false`);
const url = `https://api.github.com/search/repositories?q=${q}&sort=stars&per_page=30`;

@@ -51,3 +51,3 @@ let data;

const resetTime = new Date(parseInt(resetStr) * 1000).toLocaleString();
throw new Error(`GitHub rate limit exceeded — resets at ${resetTime}. Set GITHUB_TOKEN env var for 5000 req/hr.`);
throw new Error(`GitHub rate limit exceeded — resets at ${resetTime}. Set GITHUB_TOKEN for higher rate limits (5000/hr vs 60/hr unauthenticated).`);
}

@@ -79,3 +79,3 @@ }

// Core update logic without interactive prompts (for testing and direct apply)
export function updateApply(user, includeHooks = false, dirs = DEFAULT_DIRS, additive = false) {
export function updateApply(user, includeHooks = false, dirs = DEFAULT_DIRS, additive = false, includeDotfiles = false) {
// Get the install record for this user

@@ -99,10 +99,7 @@ const installed = readInstalled(dirs);

// Compute roots relative to injected home for testability
const roots = {
claude: path.join(dirs.home, '.claude'),
cursor: path.join(dirs.home, '.cursor'),
shared: dirs.home,
};
const roots = rootsFor(dirs.home);
const files = plan(dir, roots);
printPlan(files, manifest);
const todo = files.filter((f) => (additive ? f.status === 'new' : f.status !== 'same') && !isExecutable(f, includeHooks));
printPlan(files, manifest, includeDotfiles);
const todo = files.filter((f) => (additive ? f.status === 'new' : f.status !== 'same') &&
!isExecutable(f, includeHooks, includeDotfiles));
if (!todo.length) {

@@ -113,3 +110,3 @@ console.log(kleur.dim(additive ? '\n No new files to add.\n' : '\n Already up to date.\n'));

if (additive) {
const skipped = files.filter((f) => f.status === 'changed' && !isExecutable(f, includeHooks));
const skipped = files.filter((f) => f.status === 'changed' && !isExecutable(f, includeHooks, includeDotfiles));
if (skipped.length) {

@@ -119,3 +116,3 @@ console.log(kleur.dim(`\n = ${skipped.length} locally-modified file(s) preserved (additive mode)`));

}
const { backupDir, filesWritten } = applyProfile(todo, user, includeHooks, dirs);
const { backupDir, filesWritten } = applyProfile(todo, user, includeHooks, dirs, false, includeDotfiles);
// Update the install record with the new commit and timestamp

@@ -127,4 +124,7 @@ recordInstall(user, dir, ref, manifest.version, dirs);

export async function update(user, opts, dirs = DEFAULT_DIRS) {
const lockPath = path.join(dirs.state, '.lock');
acquireLock(lockPath);
try {
const includeHooks = opts?.includeHooks ?? false;
const includeDotfiles = opts?.includeDotfiles ?? false;
const yes = opts?.yes ?? false;

@@ -151,14 +151,11 @@ const dryRun = opts?.dryRun ?? false;

// Compute roots relative to injected home for testability
const roots = {
claude: path.join(dirs.home, '.claude'),
cursor: path.join(dirs.home, '.cursor'),
shared: dirs.home,
};
const roots = rootsFor(dirs.home);
const files = plan(fetchDir, roots);
printPlan(files, manifest);
const todo = files.filter((f) => (additive ? f.status === 'new' : f.status !== 'same') && !isExecutable(f, includeHooks));
printPlan(files, manifest, includeDotfiles);
const todo = files.filter((f) => (additive ? f.status === 'new' : f.status !== 'same') &&
!isExecutable(f, includeHooks, includeDotfiles));
if (!todo.length)
return void console.log(kleur.dim(additive ? '\n No new files to add.\n' : '\n Already up to date.\n'));
if (additive) {
const skipped = files.filter((f) => f.status === 'changed' && !isExecutable(f, includeHooks));
const skipped = files.filter((f) => f.status === 'changed' && !isExecutable(f, includeHooks, includeDotfiles));
if (skipped.length) {

@@ -169,3 +166,3 @@ console.log(kleur.dim(`\n = ${skipped.length} locally-modified file(s) preserved (additive mode)`));

// If hooks present and not explicitly included, warn
const hasHooks = files.some((f) => isExecutable(f, false));
const hasHooks = files.some((f) => isExecutable(f, false, true));
if (hasHooks && !includeHooks) {

@@ -180,2 +177,13 @@ console.log(kleur.yellow(`\n ⚠ This profile's settings.json contains hooks that run shell commands.`));

}
// If dangerous dotfiles present and not explicitly included, warn
const hasDotfiles = files.some((f) => isExecutable(f, true, false));
if (hasDotfiles && !includeDotfiles) {
console.log(kleur.yellow(`\n ⚠ This profile contains executable dotfiles (.zshrc, .bashrc, etc.) that run on shell startup.`));
}
// If dotfiles present and user wants to include them, ask for explicit confirm
if (hasDotfiles && includeDotfiles) {
if (!(await confirm(`This profile contains executable dotfiles that run on shell startup. Update with them?`, yes))) {
return void console.log(kleur.dim('\n Aborted.\n'));
}
}
if (!(await confirm(`Apply ${todo.length} change(s)?`, yes)))

@@ -191,3 +199,3 @@ return void console.log(kleur.dim('\n Aborted.\n'));

}
const { backupDir, filesWritten } = updateApply(user, includeHooks, dirs, additive);
const { backupDir, filesWritten } = updateApply(user, includeHooks, dirs, additive, includeDotfiles);
console.log(kleur.green(`\n ✓ Updated ${filesWritten} file(s).`) +

@@ -198,7 +206,11 @@ kleur.dim(` Backup: ${tildify(backupDir)}`));

finally {
releaseLock(lockPath);
}
}
export async function install(user, opts) {
const lockPath = path.join(DEFAULT_DIRS.state, '.lock');
acquireLock(lockPath);
try {
const includeHooks = opts?.includeHooks ?? false;
const includeDotfiles = opts?.includeDotfiles ?? false;
const { user: userName, ref: userRef } = parseUserRef(user);

@@ -211,8 +223,13 @@ const yes = opts?.yes ?? false;

console.log();
printPlan(files, manifest);
const todo = files.filter((f) => f.status !== 'same' && !isExecutable(f, includeHooks));
printPlan(files, manifest, includeDotfiles);
// Issue #151: Warn if profile has no tool directories (nothing to install)
if (files.length === 0) {
console.log(kleur.yellow(`\n ⚠ profile has no tool directories — nothing to install\n`));
process.exit(1);
}
const todo = files.filter((f) => f.status !== 'same' && !isExecutable(f, includeHooks, includeDotfiles));
if (!todo.length)
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));
const hasHooks = files.some((f) => isExecutable(f, false, true));
if (hasHooks && !includeHooks) {

@@ -227,5 +244,16 @@ console.log(kleur.yellow(`\n ⚠ This profile's settings.json contains hooks that run shell commands.`));

}
// If dangerous dotfiles present and not explicitly included, warn
const hasDotfiles = files.some((f) => isExecutable(f, true, false));
if (hasDotfiles && !includeDotfiles) {
console.log(kleur.yellow(`\n ⚠ This profile contains executable dotfiles (.zshrc, .bashrc, etc.) that run on shell startup.`));
}
// If dotfiles present and user wants to include them, ask for explicit confirm
if (hasDotfiles && includeDotfiles) {
if (!(await confirm(`This profile contains executable dotfiles that run on shell startup. Install them?`, yes))) {
return void console.log(kleur.dim('\n Aborted.\n'));
}
}
if (!(await confirm(`Apply ${todo.length} change(s)?`, yes)))
return void console.log(kleur.dim('\n Aborted.\n'));
const { backupDir, filesWritten } = applyProfile(files, userName, includeHooks, DEFAULT_DIRS, dryRun);
const { backupDir, filesWritten } = applyProfile(files, userName, includeHooks, DEFAULT_DIRS, dryRun, includeDotfiles);
// Only record install if not a dry-run

@@ -246,2 +274,3 @@ if (!dryRun) {

finally {
releaseLock(lockPath);
}

@@ -288,143 +317,157 @@ }

const STATE = path.join(HOME, '.sharekit');
const yes = opts?.yes ?? false;
const dryRun = opts?.dryRun ?? false;
const root = path.join(STATE, 'backups');
const last = fs.existsSync(root)
? fs
.readdirSync(root)
.filter((e) => e.startsWith(user + '-'))
.sort()
.pop()
: undefined;
if (!last)
return void console.log(kleur.yellow(`No backup for ${user}.`));
const dir = path.join(root, last);
// Read and parse applied.json with safe error handling
let applied;
const appliedPath = path.join(dir, 'applied.json');
const lockPath = path.join(STATE, '.lock');
acquireLock(lockPath);
try {
const rawData = fs.readFileSync(appliedPath, 'utf8');
applied = parseApplied(rawData);
}
catch (error) {
const msg = error instanceof Error ? error.message : String(error);
throw new Error(`Backup data is corrupt or unreadable: ${msg}`);
}
let versionStr = '';
const metadataPath = path.join(dir, 'metadata.json');
if (fs.existsSync(metadataPath)) {
const yes = opts?.yes ?? false;
const dryRun = opts?.dryRun ?? false;
const root = path.join(STATE, 'backups');
const last = fs.existsSync(root)
? fs
.readdirSync(root)
.filter((e) => e.startsWith(user + '-'))
.sort()
.pop()
: undefined;
if (!last)
return void console.log(kleur.yellow(`No backup for ${user}.`));
const dir = path.join(root, last);
// Read and parse applied.json with safe error handling
let applied;
const appliedPath = path.join(dir, 'applied.json');
try {
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
if (metadata.sourceVersion)
versionStr = ` (v${metadata.sourceVersion})`;
const rawData = fs.readFileSync(appliedPath, 'utf8');
applied = parseApplied(rawData);
}
catch {
// If metadata can't be read, just continue without version info
catch (error) {
const msg = error instanceof Error ? error.message : String(error);
throw new Error(`Backup data is corrupt or unreadable: ${msg}`);
}
}
console.log(kleur.bold(`\n Rollback ${user}${versionStr} (${applied.length} file(s))\n`));
if (!(await confirm('Restore?', yes)))
return void console.log(kleur.dim('\n Aborted.\n'));
if (dryRun) {
console.log(kleur.cyan(`\n (dry-run — no files restored)`));
console.log(kleur.green(`\n ✓ Would restore ${applied.length} file(s).`));
let versionStr = '';
const metadataPath = path.join(dir, 'metadata.json');
if (fs.existsSync(metadataPath)) {
try {
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
if (metadata.sourceVersion)
versionStr = ` (v${metadata.sourceVersion})`;
}
catch {
// If metadata can't be read, just continue without version info
}
}
console.log(kleur.bold(`\n Rollback ${user}${versionStr} (${applied.length} file(s))\n`));
if (!(await confirm('Restore?', yes)))
return void console.log(kleur.dim('\n Aborted.\n'));
if (dryRun) {
console.log(kleur.cyan(`\n (dry-run — no files restored)`));
console.log(kleur.green(`\n ✓ Would restore ${applied.length} file(s).`));
console.log();
return;
}
const metadata = restoreBackup(user);
const summary = `${metadata.filesRestored} file(s) restored${metadata.filesRemoved > 0 ? `, ${metadata.filesRemoved} removed` : ''}`;
// Handle null sourceCommit (offline cache case)
let versionSuffix = '';
if (metadata.sourceCommit === null) {
versionSuffix = ' — from offline cache, exact version unknown';
}
else if (metadata.sourceVersion) {
versionSuffix = ` (reverted to v${metadata.sourceVersion})`;
}
console.log(kleur.green(`\n ✓ ${summary}${versionSuffix}`));
console.log();
return;
}
const metadata = restoreBackup(user);
const summary = `${metadata.filesRestored} file(s) restored${metadata.filesRemoved > 0 ? `, ${metadata.filesRemoved} removed` : ''}`;
// Handle null sourceCommit (offline cache case)
let versionSuffix = '';
if (metadata.sourceCommit === null) {
versionSuffix = ' — from offline cache, exact version unknown';
finally {
releaseLock(lockPath);
}
else if (metadata.sourceVersion) {
versionSuffix = ` (reverted to v${metadata.sourceVersion})`;
}
console.log(kleur.green(`\n ✓ ${summary}${versionSuffix}`));
console.log();
}
export async function uninstall(user, dirs = DEFAULT_DIRS, force = false) {
const installed = readInstalled(dirs);
const record = installed[user];
if (!record) {
throw new Error(`${user} is not installed.`);
}
// Find the latest backup for this user
const root = path.join(dirs.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 found for ${user}. Cannot uninstall without restore information.`);
}
const backupDir = path.join(root, last);
// Read and parse applied.json with safe error handling
let applied;
const appliedPath = path.join(backupDir, 'applied.json');
const lockPath = path.join(dirs.state, '.lock');
acquireLock(lockPath);
try {
const rawData = fs.readFileSync(appliedPath, 'utf8');
applied = parseApplied(rawData);
}
catch (error) {
const msg = error instanceof Error ? error.message : String(error);
throw new Error(`Backup data is corrupt or unreadable: ${msg}`);
}
// Print what will be removed/restored
const toRemove = applied.filter((a) => a.status === 'new');
const toRestore = applied.filter((a) => a.status === 'changed');
console.log();
console.log(kleur.bold(` Uninstall ${user}${record.version ? ` (v${record.version})` : ''}\n`));
if (toRemove.length > 0) {
console.log(kleur.red(` - remove (${toRemove.length})`));
for (const a of toRemove) {
console.log(kleur.red(` ${tildify(a.dest)}`));
const installed = readInstalled(dirs);
const record = installed[user];
if (!record) {
throw new Error(`${user} is not installed.`);
}
}
if (toRestore.length > 0) {
console.log(kleur.yellow(`\n ~ restore (${toRestore.length})`));
for (const a of toRestore) {
console.log(kleur.yellow(` ${tildify(a.dest)}`));
// Find the latest backup for this user
const root = path.join(dirs.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 found for ${user}. Cannot uninstall without restore information.`);
}
}
console.log();
if (!force && !(await confirm(`Remove ${user}?`))) {
return void console.log(kleur.dim('\n Aborted.\n'));
}
// Resolve home directory once for bounds checking
const resolvedHome = path.resolve(dirs.home);
// Execute the uninstall: reverse all changes
for (const a of applied) {
// Bounds-check: ensure dest is within dirs.home
const resolvedDest = path.resolve(a.dest);
if (!resolvedDest.startsWith(resolvedHome + path.sep)) {
console.warn(`Skipping out-of-bounds entry: ${a.dest}`);
continue;
const backupDir = path.join(root, last);
// Read and parse applied.json with safe error handling
let applied;
const appliedPath = path.join(backupDir, 'applied.json');
try {
const rawData = fs.readFileSync(appliedPath, 'utf8');
applied = parseApplied(rawData);
}
if (a.status === 'new') {
// File was added by the profile — remove it
fs.rmSync(resolvedDest, { force: true });
catch (error) {
const msg = error instanceof Error ? error.message : String(error);
throw new Error(`Backup data is corrupt or unreadable: ${msg}`);
}
else if (a.status === 'changed') {
// File was changed — restore from backup
const src = path.join(backupDir, path.relative(resolvedHome, resolvedDest));
if (fs.existsSync(src)) {
fs.mkdirSync(path.dirname(resolvedDest), { recursive: true });
cp(src, resolvedDest);
// Print what will be removed/restored
const toRemove = applied.filter((a) => a.status === 'new');
const toRestore = applied.filter((a) => a.status === 'changed');
console.log();
console.log(kleur.bold(` Uninstall ${user}${record.version ? ` (v${record.version})` : ''}\n`));
if (toRemove.length > 0) {
console.log(kleur.red(` - remove (${toRemove.length})`));
for (const a of toRemove) {
console.log(kleur.red(` ${tildify(a.dest)}`));
}
}
if (toRestore.length > 0) {
console.log(kleur.yellow(`\n ~ restore (${toRestore.length})`));
for (const a of toRestore) {
console.log(kleur.yellow(` ${tildify(a.dest)}`));
}
}
console.log();
if (!force && !(await confirm(`Remove ${user}?`))) {
return void console.log(kleur.dim('\n Aborted.\n'));
}
// Resolve home directory once for bounds checking
const resolvedHome = path.resolve(dirs.home);
// Execute the uninstall: reverse all changes
for (const a of applied) {
// Bounds-check: ensure dest is within dirs.home
const resolvedDest = path.resolve(a.dest);
if (!resolvedDest.startsWith(resolvedHome + path.sep)) {
console.warn(`Skipping out-of-bounds entry: ${a.dest}`);
continue;
}
if (a.status === 'new') {
// File was added by the profile — remove it
fs.rmSync(resolvedDest, { force: true });
}
else if (a.status === 'changed') {
// File was changed — restore from backup
const src = path.join(backupDir, path.relative(resolvedHome, resolvedDest));
if (fs.existsSync(src)) {
fs.mkdirSync(path.dirname(resolvedDest), { recursive: true });
cp(src, resolvedDest);
}
}
}
// Remove user from installed.json atomically (#122)
delete installed[user];
const stateFile = path.join(dirs.state, 'installed.json');
const tmp = stateFile + '.tmp';
fs.writeFileSync(tmp, JSON.stringify(installed, null, 2));
fs.renameSync(tmp, stateFile);
const summary = `${toRemove.length} file(s) removed${toRestore.length > 0 ? `, ${toRestore.length} restored` : ''}`;
console.log(kleur.green(`\n ✓ Uninstalled ${user}. ${summary}`));
console.log();
}
// Remove user from installed.json atomically (#122)
delete installed[user];
const stateFile = path.join(dirs.state, 'installed.json');
const tmp = stateFile + '.tmp';
fs.writeFileSync(tmp, JSON.stringify(installed, null, 2));
fs.renameSync(tmp, stateFile);
const summary = `${toRemove.length} file(s) removed${toRestore.length > 0 ? `, ${toRestore.length} restored` : ''}`;
console.log(kleur.green(`\n ✓ Uninstalled ${user}. ${summary}`));
console.log();
finally {
releaseLock(lockPath);
}
}

@@ -509,3 +552,53 @@ export async function scan(dir, force = false) {

}
// 4. Scaffold shared/ directory with .gitkeep
// 4. Scaffold opencode/ directory
const destOpencode = path.join(profileRoot, 'opencode');
fs.mkdirSync(destOpencode, { recursive: true });
const sourceOpencodeDir = path.join(sourceRoot, '.config', 'opencode');
let opencodeFound = false;
if (fs.existsSync(sourceOpencodeDir)) {
for (const file of fs.readdirSync(sourceOpencodeDir).sort()) {
const filePath = path.join(sourceOpencodeDir, file);
const stat = fs.statSync(filePath);
if (stat.isFile()) {
const destFile = path.join(destOpencode, file);
cp(filePath, destFile);
console.log(kleur.green(` + ${tildify(destFile)}`));
const content = fs.readFileSync(destFile, 'utf8');
const findings = scanForSecrets(content, tildify(destFile));
allFindings.push(...findings);
opencodeFound = true;
break; // Copy first small text config found
}
}
}
if (!opencodeFound) {
fs.writeFileSync(path.join(destOpencode, 'config.json'), '{}\n');
console.log(kleur.green(` + ${tildify(path.join(destOpencode, 'config.json'))} (placeholder)`));
}
// 5. Scaffold gjc/ directory
const destGjc = path.join(profileRoot, 'gjc');
fs.mkdirSync(destGjc, { recursive: true });
const sourceGjcDir = path.join(sourceRoot, '.gjc');
let gjcFound = false;
if (fs.existsSync(sourceGjcDir)) {
for (const file of fs.readdirSync(sourceGjcDir).sort()) {
const filePath = path.join(sourceGjcDir, file);
const stat = fs.statSync(filePath);
if (stat.isFile()) {
const destFile = path.join(destGjc, file);
cp(filePath, destFile);
console.log(kleur.green(` + ${tildify(destFile)}`));
const content = fs.readFileSync(destFile, 'utf8');
const findings = scanForSecrets(content, tildify(destFile));
allFindings.push(...findings);
gjcFound = true;
break; // Copy first small text config found
}
}
}
if (!gjcFound) {
fs.writeFileSync(path.join(destGjc, 'config.toml'), '# gjc configuration\n');
console.log(kleur.green(` + ${tildify(path.join(destGjc, 'config.toml'))} (placeholder)`));
}
// 6. Scaffold shared/ directory with .gitkeep
const destShared = path.join(profileRoot, 'shared');

@@ -515,3 +608,3 @@ fs.mkdirSync(destShared, { recursive: true });

console.log(kleur.green(` + ${tildify(destShared)}/`));
// 5. Copy skills if specified
// 7. Copy skills if specified
let skillCount = 0;

@@ -518,0 +611,0 @@ for (const skillName of skillNames) {

@@ -7,5 +7,6 @@ #!/usr/bin/env node

import { install, preview, inspect, rollback, init, search, scan, listBackups, restoreBackupToStamp, confirm, list, update, uninstall, } from './sharekit.js';
import { parseApplied } from './backup.js';
const HOME = os.homedir();
const STATE = path.join(HOME, '.sharekit');
const VERSION = '0.5.0';
const VERSION = '0.6.0';
const USAGE = `${kleur.bold('sharekit')} v${VERSION} — share your AI coding setup

@@ -19,2 +20,3 @@

--include-hooks also install settings.json with shell hooks
--include-dotfiles also install executable shared/ dotfiles
--yes auto-approve prompts

@@ -29,2 +31,3 @@ --dry-run show changes without writing files

--include-hooks also apply settings.json with shell hooks
--include-dotfiles also apply executable shared/ dotfiles
--additive only add new files; preserve local edits

@@ -44,2 +47,6 @@ ${kleur.cyan('rollback')} <user> [opts] restore the last backup

const cmds = { install, preview, inspect, rollback };
function printUserHint() {
console.error(kleur.dim(' <user> is a GitHub username hosting a repo named "sharekit-profile"'));
console.error(kleur.dim(' example: sharekit install lucassantana'));
}
export async function main(argv = process.argv.slice(2)) {

@@ -83,2 +90,3 @@ const [cmd, ...rest] = argv;

console.error(kleur.red('usage: sharekit update <user>'));
printUserHint();
process.exit(1);

@@ -93,2 +101,4 @@ }

opts.includeHooks = true;
if (flags.includes('--include-dotfiles'))
opts.includeDotfiles = true;
if (flags.includes('--additive'))

@@ -141,2 +151,3 @@ opts.additive = true;

console.error(kleur.red('usage: sharekit rollback <user> [--list] [--to <stamp>]'));
printUserHint();
process.exit(1);

@@ -175,6 +186,8 @@ }

try {
applied = JSON.parse(fs.readFileSync(appliedPath, 'utf8'));
const rawData = fs.readFileSync(appliedPath, 'utf8');
applied = parseApplied(rawData);
}
catch {
console.error(kleur.red(`Could not read backup metadata at ${appliedPath}`));
catch (error) {
const msg = error instanceof Error ? error.message : String(error);
console.error(kleur.red(`Could not read backup metadata: ${msg}`));
process.exit(1);

@@ -238,2 +251,3 @@ }

console.error(kleur.red('usage: sharekit uninstall <user>'));
printUserHint();
process.exit(1);

@@ -255,2 +269,3 @@ }

console.error(kleur.red(`usage: sharekit ${cmd} <user>${showRef ? '[@<ref>]' : ''}`));
printUserHint();
process.exit(1);

@@ -269,2 +284,5 @@ }

}
if (cmd === 'install' && flags.includes('--include-dotfiles')) {
opts.includeDotfiles = true;
}
await fn(arg, opts);

@@ -271,0 +289,0 @@ }

export declare const HOME: string;
export declare const STATE: string;
export declare const MAX_MANIFEST_BYTES: number;
export declare function rootsFor(home: string): Record<string, string>;
export declare const ROOTS: Record<string, string>;

@@ -5,0 +6,0 @@ export type Dirs = {

@@ -9,8 +9,15 @@ import * as fs from 'node:fs';

export const MAX_MANIFEST_BYTES = 512 * 1024; // 512 KB
// profile/<tool>/** mirrors into these roots — one rule, not a filename allowlist
export const ROOTS = {
claude: path.join(HOME, '.claude'),
cursor: path.join(HOME, '.cursor'),
shared: HOME,
};
// profile/<tool>/** mirrors into these roots — one rule, not a filename allowlist.
// rootsFor is the single source of truth: ROOTS is just rootsFor(HOME), and the
// CLI builds rootsFor(dirs.home) so injected test homes are honoured.
export function rootsFor(home) {
return {
claude: path.join(home, '.claude'),
cursor: path.join(home, '.cursor'),
opencode: path.join(home, '.config', 'opencode'),
gjc: path.join(home, '.gjc'),
shared: home,
};
}
export const ROOTS = rootsFor(HOME);
export const DEFAULT_DIRS = { home: HOME, state: STATE };

@@ -17,0 +24,0 @@ export const tildify = (p, maxLen) => {

@@ -11,10 +11,11 @@ import { Dirs } from './paths.js';

}
export declare const DANGEROUS_SHARED_DOTFILES: Set<string>;
export declare function plan(profileDir: string, roots?: Record<string, string>): PlanFile[];
export declare const isExecutable: (f: PlanFile, includeHooks?: boolean) => boolean;
export declare function printPlan(files: PlanFile[], manifest: ReturnType<typeof readManifest>): void;
export declare function writeAtomic(files: PlanFile[], backupDir: string, user: string, includeHooks?: boolean, dirs?: Dirs): number;
export declare function backup(files: PlanFile[], user: string, includeHooks?: boolean, dirs?: Dirs): string;
export declare function applyProfile(files: PlanFile[], user: string, includeHooks?: boolean, dirs?: Dirs, dryRun?: boolean): {
export declare const isExecutable: (f: PlanFile, includeHooks?: boolean, includeDotfiles?: boolean) => boolean;
export declare function printPlan(files: PlanFile[], manifest: ReturnType<typeof readManifest>, includeDotfiles?: boolean): void;
export declare function writeAtomic(files: PlanFile[], backupDir: string, user: string, includeHooks?: boolean, dirs?: Dirs, includeDotfiles?: boolean): number;
export declare function backup(files: PlanFile[], user: string, includeHooks?: boolean, dirs?: Dirs, includeDotfiles?: boolean): string;
export declare function applyProfile(files: PlanFile[], user: string, includeHooks?: boolean, dirs?: Dirs, dryRun?: boolean, includeDotfiles?: boolean): {
backupDir: string;
filesWritten: number;
};

@@ -7,2 +7,17 @@ import * as fs from 'node:fs';

import { readInstalled } from './state.js';
// Denylist of executable-on-load dotfiles in shared/ that bypass shell gate
// These files are sourced/executed on every shell startup and pose an RCE risk
export const DANGEROUS_SHARED_DOTFILES = new Set([
'.zshrc',
'.zshenv',
'.zprofile',
'.zlogin',
'.bashrc',
'.bash_profile',
'.bash_login',
'.profile',
'.bash_logout',
'.xinitrc',
'.xprofile',
]);
// Track skipped symlinks for the current plan

@@ -47,4 +62,16 @@ let currentPlanSkippedSymlinks = [];

// add `--include-hooks` when someone actually asks.
export const isExecutable = (f, includeHooks = false) => !includeHooks && f.tool === 'claude' && path.basename(f.dest) === 'settings.json';
export function printPlan(files, manifest) {
// shared/ dotfiles (.zshrc, .bashrc, etc.) are sourced on shell startup → RCE on install.
// add `--include-dotfiles` when someone explicitly requests.
export const isExecutable = (f, includeHooks = false, includeDotfiles = false) => {
if (!includeHooks && f.tool === 'claude' && path.basename(f.dest) === 'settings.json') {
return true;
}
if (!includeDotfiles &&
f.tool === 'shared' &&
DANGEROUS_SHARED_DOTFILES.has(path.basename(f.dest))) {
return true;
}
return false;
};
export function printPlan(files, manifest, includeDotfiles = false) {
console.log(kleur.bold(`Profile: ${manifest.name}${manifest.version ? ' v' + manifest.version : ''}`));

@@ -66,9 +93,11 @@ if (manifest.description)

console.log(kleur.dim(`\n = ${same} unchanged`));
if (files.some((f) => isExecutable(f)))
if (files.some((f) => isExecutable(f, false, includeDotfiles)))
console.log(kleur.yellow(`\n ⚠ settings.json present — contains hooks; skipped. Merge manually.`));
if (files.some((f) => isExecutable(f, true, false) && f.tool === 'shared'))
console.log(kleur.yellow(`\n ⚠ shared/ contains executable dotfiles (.zshrc, .bashrc, etc.); skipped for security. Use --include-dotfiles to merge.`));
}
function write(files, includeHooks = false) {
function write(files, includeHooks = false, includeDotfiles = false) {
let n = 0;
for (const f of files) {
if (f.status === 'same' || isExecutable(f, includeHooks))
if (f.status === 'same' || isExecutable(f, includeHooks, includeDotfiles))
continue;

@@ -81,5 +110,5 @@ fs.mkdirSync(path.dirname(f.dest), { recursive: true });

}
export function writeAtomic(files, backupDir, user, includeHooks = false, dirs = DEFAULT_DIRS) {
export function writeAtomic(files, backupDir, user, includeHooks = false, dirs = DEFAULT_DIRS, includeDotfiles = false) {
let n = 0;
const applied = files.filter((f) => f.status !== 'same' && !isExecutable(f, includeHooks));
const applied = files.filter((f) => f.status !== 'same' && !isExecutable(f, includeHooks, includeDotfiles));
try {

@@ -105,6 +134,6 @@ for (const f of applied) {

}
export function backup(files, user, includeHooks = false, dirs = DEFAULT_DIRS) {
export function backup(files, user, includeHooks = false, dirs = DEFAULT_DIRS, includeDotfiles = false) {
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
const dir = path.join(dirs.state, 'backups', `${user}-${stamp}`);
const applied = files.filter((f) => f.status !== 'same' && !isExecutable(f, includeHooks));
const applied = files.filter((f) => f.status !== 'same' && !isExecutable(f, includeHooks, includeDotfiles));
fs.mkdirSync(dir, { recursive: true });

@@ -131,12 +160,12 @@ for (const f of applied.filter((f) => f.status === 'changed')) {

// Exported pure functions for testability
export function applyProfile(files, user, includeHooks = false, dirs = DEFAULT_DIRS, dryRun = false) {
export function applyProfile(files, user, includeHooks = false, dirs = DEFAULT_DIRS, dryRun = false, includeDotfiles = false) {
if (dryRun) {
// In dry-run, just count files without writing anything
const filesWritten = files.filter((f) => f.status !== 'same' && !isExecutable(f, includeHooks)).length;
const filesWritten = files.filter((f) => f.status !== 'same' && !isExecutable(f, includeHooks, includeDotfiles)).length;
return { backupDir: '', filesWritten };
}
const backupDir = backup(files, user, includeHooks, dirs);
const filesWritten = writeAtomic(files, backupDir, user, includeHooks, dirs);
const backupDir = backup(files, user, includeHooks, dirs, includeDotfiles);
const filesWritten = writeAtomic(files, backupDir, user, includeHooks, dirs, includeDotfiles);
pruneBackups(user, dirs.state);
return { backupDir, filesWritten };
}

@@ -101,3 +101,3 @@ import kleur from 'kleur';

// Rule 6: Google API keys AIza format (HIGH)
const googleMatch = /AIza[0-9A-Za-z\-_]{35}/.exec(line);
const googleMatch = /AIza[0-9A-Za-z\-_]{30,40}/.exec(line);
if (googleMatch) {

@@ -104,0 +104,0 @@ const preview = truncatePreview(line, googleMatch.index, 5, 40);

@@ -6,9 +6,9 @@ export { HOME, STATE, MAX_MANIFEST_BYTES, ROOTS, DEFAULT_DIRS, tildify, cp, walk, walkWithSymlinks, } from './paths.js';

export { parseUserRef, fetchProfile, readManifest } from './fetch.js';
export { plan, printPlan, isExecutable, applyProfile } from './plan.js';
export { plan, printPlan, isExecutable, applyProfile, DANGEROUS_SHARED_DOTFILES } from './plan.js';
export type { Status, PlanFile } from './plan.js';
export { listBackups, pruneBackups, restoreBackup, restoreBackupInternal, restoreBackupToStamp, readMetadata, writeMetadata, parseApplied, } from './backup.js';
export type { BackupInfo, RestoreMetadata } from './backup.js';
export { recordInstall, readInstalled, list, isImmutableRef } from './state.js';
export { recordInstall, readInstalled, list, isImmutableRef, acquireLock, releaseLock, } from './state.js';
export type { InstallRecord } from './state.js';
export { confirm, search, updateApply, update, install, preview, inspect, rollback, uninstall, scan, init, } from './commands.js';
export type { InstallOpts } from './commands.js';

@@ -9,8 +9,8 @@ // Barrel file: re-exports the entire public API for backward compatibility

// From plan.ts
export { plan, printPlan, isExecutable, applyProfile } from './plan.js';
export { plan, printPlan, isExecutable, applyProfile, DANGEROUS_SHARED_DOTFILES } from './plan.js';
// From backup.ts
export { listBackups, pruneBackups, restoreBackup, restoreBackupInternal, restoreBackupToStamp, readMetadata, writeMetadata, parseApplied, } from './backup.js';
// From state.ts
export { recordInstall, readInstalled, list, isImmutableRef } from './state.js';
export { recordInstall, readInstalled, list, isImmutableRef, acquireLock, releaseLock, } from './state.js';
// From commands.ts
export { confirm, search, updateApply, update, install, preview, inspect, rollback, uninstall, scan, init, } from './commands.js';

@@ -82,2 +82,22 @@ import * as fs from 'node:fs';

const LOCK_FILE = path.join(DEFAULT_DIRS.state, '.lock');
/**
* Portable cross-platform PID liveness check using process.kill(pid, 0).
* Signal 0 doesn't send a signal; it only checks if the process exists and is accessible.
* - EPERM: process exists but we don't own it (return true — alive)
* - ESRCH: process doesn't exist (return false — dead)
* - Other errors: treat as dead
*/
function isPidAlive(pidStr) {
const pid = Number(pidStr);
if (!Number.isInteger(pid) || pid <= 0)
return false;
try {
process.kill(pid, 0);
return true;
}
catch (e) {
const code = e.code;
return code === 'EPERM'; // EPERM = alive but not owned by us; ESRCH = dead
}
}
export function acquireLock(lockPath = LOCK_FILE) {

@@ -88,15 +108,17 @@ const pid = process.pid.toString();

const existingPid = fs.readFileSync(lockPath, 'utf8').trim();
if (isPidAlive(existingPid)) {
throw new Error(`another sharekit process is running (PID ${existingPid}) — retry after it finishes, or delete ${lockPath} if stale`);
}
// Process is dead — clean up stale lock
try {
fs.statSync(`/proc/${existingPid}`);
console.error(`Another sharekit operation is running (PID ${existingPid}). Try again in a moment.`);
process.exit(1);
fs.unlinkSync(lockPath);
}
catch {
try {
fs.unlinkSync(lockPath);
}
catch { }
catch { }
}
catch (e) {
// If checking liveness or reading the lock file fails, check if it's our error
if (e instanceof Error && e.message.includes('another sharekit process')) {
throw e; // Re-throw the "process still running" error
}
}
catch {
// Otherwise try to clean up the lock file
try {

@@ -103,0 +125,0 @@ fs.unlinkSync(lockPath);

{
"name": "@lucassantana/sharekit",
"version": "0.5.0",
"version": "0.6.0",
"description": "Share your AI coding setup with anyone — one command to install, one to rollback.",

@@ -29,6 +29,8 @@ "keywords": [

"build": "rm -rf dist && tsc -p tsconfig.json && chmod +x dist/index.js",
"build:binaries": "mkdir -p dist && bun build --compile --target=bun-linux-x64 src/index.ts --outfile=dist/sharekit-linux-x64 && bun build --compile --target=bun-linux-arm64 src/index.ts --outfile=dist/sharekit-linux-arm64 && bun build --compile --target=bun-darwin-x64 src/index.ts --outfile=dist/sharekit-macos-x64 && bun build --compile --target=bun-darwin-arm64 src/index.ts --outfile=dist/sharekit-macos-arm64 && bun build --compile --target=bun-windows-x64 src/index.ts --outfile=dist/sharekit-windows-x64.exe",
"dev": "tsx src/index.ts",
"test": "node --import tsx --test test/*.test.ts",
"typecheck": "tsc -p tsconfig.json --noEmit",
"format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"",
"format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\" \"scripts/**/*.mjs\"",
"bump": "node scripts/bump-version.mjs",
"prepublishOnly": "npm run build"

@@ -35,0 +37,0 @@ },

+159
-51

@@ -5,6 +5,16 @@ # sharekit

Share your AI coding setup — CLAUDE.md, skills, cursorrules, and dotfiles — with anyone. One command to install, one to rollback.
**Share your entire AI coding setup in one command.**
## Install
Distribute your CLAUDE.md, skills, cursorrules, and dotfiles to team members, colleagues, or the public. One command to install. One command to rollback. Built for Claude Code, Cursor, Windsurf, Codex, and more.
[![npm version](https://img.shields.io/npm/v/@lucassantana/sharekit.svg)](https://www.npmjs.com/package/@lucassantana/sharekit)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen)
---
## Quick Start
Install a profile from GitHub in seconds:
```bash

@@ -14,77 +24,129 @@ npx @lucassantana/sharekit install <github-user>

Fetches a profile from `github.com/<github-user>/sharekit-profile`, previews the changes (colors + counts), asks for confirmation, backs up any files it will overwrite, and applies them. Undo with `sharekit rollback <github-user>`.
That's it. sharekit will:
- Fetch the profile from `github.com/<github-user>/sharekit-profile`
- Show you a preview of changes (files, counts, paths)
- Ask for confirmation
- Back up existing files before overwriting
- Apply the configuration
**Undo anytime:**
```bash
npx @lucassantana/sharekit preview <github-user> # see what would change, apply nothing
npx @lucassantana/sharekit rollback <github-user> # restore the last backup
npx @lucassantana/sharekit rollback <github-user>
```
## Discover profiles
---
GitHub is the registry — find published profiles (any repo named `sharekit-profile`):
## Core Commands
| Command | Purpose |
|---------|---------|
| `install <user>` | Install a profile and apply its configuration |
| `preview <user>` | See what would change without applying anything |
| `rollback <user>` | Restore the last backup for a profile |
| `list` | View all installed profiles with versions and dates |
| `update <user>` | Sync an installed profile to its latest version |
| `search [keyword]` | Discover published profiles on GitHub |
| `init [skills...]` | Create a profile from your current `~/.claude` |
| `scan` | Detect secrets before publishing a profile |
---
## How It Works
### What Gets Synced
sharekit copies files from a profile into your local environment:
- **`claude/`** → `~/.claude/` — CLAUDE.md, skills, settings, hooks, standards
- **`cursor/`** → `~/.cursor/` — Cursor IDE configuration
- **`opencode/`** → `~/.config/opencode/` — OpenCode configuration
- **`gjc/`** → `~/.gjc/` — gajae-code configuration
- **`shared/`** → `~/` — Root dotfiles and shared config
Files are copied as-is (TOML, text, binary). Use `preview` to spot conflicts before applying.
### Pinning Versions
Track a specific version or branch instead of `HEAD`:
```bash
npx @lucassantana/sharekit search # list all published profiles
npx @lucassantana/sharekit search react # filter by keyword
npx @lucassantana/sharekit install <github-user>@v1.0 # tag
npx @lucassantana/sharekit install <github-user>@stable # branch
```
Each result shows the one-liner to install it.
Update only works on HEAD-tracked profiles. Pinned refs are skipped.
### Pin to a version
### Hooks & Safety
Install a specific tag or branch of a profile instead of the latest:
Sharekit gates two types of executable files by default: hooks in `.claude/settings.json` and shell startup files in `shared/`. Both are **flagged but not installed by default** because they run commands under your account and require explicit trust.
#### Hooks in `settings.json`
Settings files (`.claude/settings.json`) may define hooks that run shell commands. After reviewing a profile in `preview`, install hooks with:
```bash
npx @lucassantana/sharekit install <github-user>@v1.0 # a tag
npx @lucassantana/sharekit install <github-user>@stable # a branch
npx @lucassantana/sharekit install <github-user> --include-hooks
```
Plain `install <github-user>` always tracks the profile's default branch (HEAD).
You'll get a second confirmation before the settings file is written.
## Manage installed profiles
#### Executable dotfiles in `shared/`
List all installed profiles with version, commit, and date applied:
Shell startup files in `shared/` like `.zshrc`, `.bashrc`, `.profile`, `.xinitrc` and similar files are **automatically skipped for security** — these are sourced on every shell startup and could execute arbitrary code when you open a terminal. Review the profile in `preview`, then optionally include them:
```bash
npx @lucassantana/sharekit list
npx @lucassantana/sharekit install <github-user> --include-dotfiles
```
Output:
You'll get a second confirmation before dotfiles are written.
```
Installed profiles:
Both flags also work with `update` and are independent—you can install hooks without dotfiles, dotfiles without hooks, or both.
user1@HEAD v1.0.2 abc1234 Jun 15, 2025
user2@v1.0 (no version) ? Jun 1, 2025
```
---
Update an installed profile (tracked profiles only; pinned refs are no-ops):
## Discover Profiles
GitHub is the registry. Find published profiles by searching for repos named `sharekit-profile`:
```bash
npx @lucassantana/sharekit update <github-user>
npx @lucassantana/sharekit search # list all profiles
npx @lucassantana/sharekit search react # filter by keyword
```
Shows what changed, asks for confirmation, and applies. Pinned refs (tags, commit SHAs) do not update.
Each result includes the one-liner to install it.
### Hooks
Manage installed profiles:
A profile's `.claude/settings.json` can define hooks that run shell commands, so it is **never installed by default** — it's flagged in the preview and skipped. To opt in after reviewing it, pass `--include-hooks` (you'll get a second explicit confirmation before it's written):
```bash
npx @lucassantana/sharekit install <github-user> --include-hooks
npx @lucassantana/sharekit list # see installed profiles + versions
npx @lucassantana/sharekit update user # sync to latest (HEAD-tracked only)
```
## Publish your own profile
---
Create a GitHub repo named **`sharekit-profile`** with this structure:
## Publish Your Own Profile
Create a GitHub repository named **`sharekit-profile`** with this structure:
```
sharekit-profile/
├── sharekit.toml
├── .gitignore
├── claude/ (→ ~/.claude/)
│ ├── CLAUDE.md
│ ├── skills/
│ │ ├── skill-1/SKILL.md
│ │ └── skill-2/SKILL.md
│ ├── settings.json
│ └── standards/
├── cursor/ (→ ~/.cursor/)
│ └── settings.json
├── opencode/ (→ ~/.config/opencode/)
├── gjc/ (→ ~/.gjc/)
└── shared/ (→ ~/)
└── .cursorrules
```
**sharekit.toml** example:
### Profile Metadata: `sharekit.toml`

@@ -94,14 +156,23 @@ ```toml

name = "My Setup"
version = "1.0"
description = "Claude + Cursor config with custom skills"
version = "1.0.0"
description = "Claude Code + Cursor config with custom skills and standards"
```
Subdirectories mirror into their corresponding roots: files in `claude/` go to `~/.claude/`, files in `cursor/` to `~/.cursor/`, and files in `shared/` to `~/`.
Subdirectories automatically mirror to their targets: `claude/` → `~/.claude/`, `cursor/` → `~/.cursor/`, `opencode/` → `~/.config/opencode/`, `gjc/` → `~/.gjc/`, `shared/` → `~/`.
Run `sharekit init [skill...]` to scaffold a profile from your `~/.claude` — copies your `CLAUDE.md` and any named skills into a ready-to-push `sharekit-profile/`.
### Scaffold from Your Config
### Safety
Bootstrap a profile repo from your current setup:
Before pushing your profile to a public GitHub repo, use `sharekit scan` to check for secrets:
```bash
npx @lucassantana/sharekit init # copy CLAUDE.md + all skills
npx @lucassantana/sharekit init skill-1 skill-2 # copy specific skills only
```
This creates a ready-to-push `sharekit-profile/` directory.
### Security Check Before Publishing
Always scan for secrets before pushing:
```bash

@@ -111,5 +182,7 @@ npx @lucassantana/sharekit scan ./sharekit-profile

The scanner detects private keys, AWS/GitHub/Slack/Google API tokens, bearer tokens, sensitive environment variables (`SECRET|TOKEN|PASSWORD|API_KEY|APIKEY|ACCESS_KEY`), and home-path leaks. High-severity findings (keys/tokens) **block the `init` and `scan` commands with a non-zero exit**. Medium/low findings (e.g., env-var names, paths) warn only.
The scanner detects:
- **High-severity** (blocks): Private keys, AWS/GitHub/Slack/Google API tokens, bearer tokens
- **Medium/Low** (warns): Sensitive env variable names, home-path leaks
Override a block with `--force` if you've manually reviewed and redacted the findings:
Override a block (after manual review) with:

@@ -120,15 +193,50 @@ ```bash

Always review the profile and `.gitignore` sensitive files before `git push`. The scanner is best-effort; you are responsible for ensuring no real secrets escape.
**You are responsible** for ensuring no real secrets escape. The scanner is best-effort. Always review the profile and `.gitignore` sensitive files before `git push`.
## Security
---
- **Hooks are never auto-installed.** Settings containing hooks (`.claude/settings.json`) are flagged in the preview and skipped. Merge them manually after reviewing.
- **Preview before applying.** `sharekit preview` shows the exact diff (new/changed/unchanged counts and paths) — trust gate before any write.
- **Everything is backed up.** Before applying, sharekit saves changed files to `~/.sharekit/backups/<user>-<timestamp>/`. Rollback restores them.
## Supported Tools
See [SECURITY.md](SECURITY.md) for the full trust model, path assumptions, and responsible disclosure policy.
sharekit works with any tool that stores config in `~/`, `~/.claude/`, `~/.cursor/`, or similar:
## Status
- ✅ **Claude Code** — CLAUDE.md, skills, settings, hooks, standards
- ✅ **Cursor** — settings.json, keyboard shortcuts
- ✅ **Windsurf** — configuration
- ✅ **Codex** — setup files
- ✅ **VS Code** — via `shared/` dotfiles
- ✅ **Neovim** — via `shared/` dotfiles
- ✅ Any tool with home-relative paths
- No registry/discovery yet — GitHub is the registry. Search `sharekit-profile` repos by topic or follow the convention.
- File-copy only; no multi-tool merge logic. TOML, text, and binary files are copied as-is. Use `sharekit preview` to spot conflicts.
---
## Security Model
- **Hooks are never auto-installed.** Settings with hooks are flagged in `preview` and skipped. Merge them manually after reviewing.
- **Preview before applying.** `sharekit preview` shows exact diffs (new/changed/unchanged counts, file paths) — inspect before any write.
- **Everything is backed up.** Changed files are saved to `~/.sharekit/backups/<user>-<timestamp>/` before apply. `rollback` restores them instantly.
For the full trust model, path assumptions, and responsible disclosure policy, see [SECURITY.md](SECURITY.md).
---
## Status & Roadmap
- ✅ Install, preview, rollback, update
- ✅ Profile discovery via GitHub
- ✅ Version pinning (tags, branches)
- ✅ Backup & restore
- ✅ Hook safety gating
- ✅ Secret scanning
- ⏳ Multi-tool merge logic (planned)
File-copy only for now—no smart merging. Use `preview` to spot conflicts.
---
## License
[MIT](LICENSE) — Share freely.
---
Made with ❤️ by Lucas Santana. [Contribute on GitHub](https://github.com/LucasSantana-Dev/sharekit).