🎩 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.4.0
to
0.5.0
+4
-0
dist/backup.d.ts

@@ -13,2 +13,6 @@ import { Dirs } from './paths.js';

export type Status = 'new' | 'changed' | 'same';
export declare function parseApplied(raw: string): Array<{
dest: string;
status: Status;
}>;
export declare function readMetadata(backupDir: string): {

@@ -15,0 +19,0 @@ sourceVersion?: string;

+29
-19
import * as fs from 'node:fs';
import * as path from 'node:path';
import { STATE, cp, DEFAULT_DIRS } from './paths.js';
// Helper: Parse and validate applied.json
export function parseApplied(raw) {
let parsed;
try {
parsed = JSON.parse(raw);
}
catch {
throw new Error('applied.json is not valid JSON');
}
if (!Array.isArray(parsed)) {
throw new Error('applied.json must be an array');
}
for (let i = 0; i < parsed.length; i++) {
const item = parsed[i];
if (typeof item !== 'object' || item === null) {
throw new Error(`applied.json[${i}] is not an object`);
}
const { dest, status } = item;
if (typeof dest !== 'string') {
throw new Error(`applied.json[${i}].dest must be a string, got ${typeof dest}`);
}
if (typeof status !== 'string') {
throw new Error(`applied.json[${i}].status must be a string, got ${typeof status}`);
}
}
return parsed;
}
// Helper: read metadata.json from backup directory

@@ -76,21 +103,4 @@ // Returns {sourceVersion?, sourceCommit?} or {} if missing/unparseable

try {
const rawData = JSON.parse(fs.readFileSync(appliedPath, 'utf8'));
// Validate that it's an array
if (!Array.isArray(rawData)) {
throw new Error('applied.json must be an array');
}
// Validate array elements have required shape
applied = rawData.map((item, index) => {
if (typeof item !== 'object' || item === null) {
throw new Error(`applied.json[${index}] is not an object`);
}
const { dest, status } = item;
if (typeof dest !== 'string') {
throw new Error(`applied.json[${index}].dest must be a string, got ${typeof dest}`);
}
if (typeof status !== 'string') {
throw new Error(`applied.json[${index}].status must be a string, got ${typeof status}`);
}
return { dest, status: status };
});
const rawData = fs.readFileSync(appliedPath, 'utf8');
applied = parseApplied(rawData);
}

@@ -97,0 +107,0 @@ catch (error) {

@@ -6,6 +6,7 @@ import { Dirs } from './paths.js';

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

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

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

import { plan, printPlan, isExecutable, applyProfile } from './plan.js';
import { restoreBackup } from './backup.js';
import { restoreBackup, parseApplied } from './backup.js';
import { recordInstall, readInstalled, isImmutableRef } from './state.js';

@@ -28,10 +28,27 @@ import { scanForSecrets, printAndGateFindings } from './scanner.js';

export async function search(query) {
const q = encodeURIComponent(`sharekit-profile in:name${query ? ` ${query}` : ''}`);
const q = encodeURIComponent(`sharekit-profile in:name${query ? ` ${query}` : ''} is:archived:false`);
const url = `https://api.github.com/search/repositories?q=${q}&sort=stars&per_page=30`;
let data;
try {
const headers = {
'User-Agent': 'sharekit-cli',
Accept: 'application/vnd.github+json',
};
// Add GITHUB_TOKEN if available for higher rate limits (5000 req/hr instead of 60)
if (process.env.GITHUB_TOKEN) {
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
}
const res = await fetch(url, {
headers: { 'User-Agent': 'sharekit-cli', Accept: 'application/vnd.github+json' },
headers,
signal: AbortSignal.timeout(15_000), // don't hang forever on a stalled GitHub response
});
// Handle rate limit 403
if (res.status === 403) {
const remaining = res.headers.get('X-RateLimit-Remaining');
const resetStr = res.headers.get('X-RateLimit-Reset');
if (remaining === '0' && resetStr) {
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.`);
}
}
if (!res.ok)

@@ -61,3 +78,3 @@ throw new Error(`GitHub API ${res.status}`);

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

@@ -88,8 +105,14 @@ const installed = readInstalled(dirs);

printPlan(files, manifest);
const todo = files.filter((f) => f.status !== 'same' && !isExecutable(f, includeHooks));
const todo = files.filter((f) => (additive ? f.status === 'new' : f.status !== 'same') && !isExecutable(f, includeHooks));
if (!todo.length) {
console.log(kleur.dim('\n Already up to date.\n'));
console.log(kleur.dim(additive ? '\n No new files to add.\n' : '\n Already up to date.\n'));
return { filesWritten: 0, backupDir: '' };
}
const { backupDir, filesWritten } = applyProfile(files, user, includeHooks, dirs);
if (additive) {
const skipped = files.filter((f) => f.status === 'changed' && !isExecutable(f, includeHooks));
if (skipped.length) {
console.log(kleur.dim(`\n = ${skipped.length} locally-modified file(s) preserved (additive mode)`));
}
}
const { backupDir, filesWritten } = applyProfile(todo, user, includeHooks, dirs);
// Update the install record with the new commit and timestamp

@@ -101,99 +124,114 @@ recordInstall(user, dir, ref, manifest.version, dirs);

export async function update(user, opts, dirs = DEFAULT_DIRS) {
const includeHooks = opts?.includeHooks ?? false;
const yes = opts?.yes ?? false;
const dryRun = opts?.dryRun ?? false;
// Get the install record for this user
const installed = readInstalled(dirs);
const record = installed[user];
if (!record) {
throw new Error(`not installed — run 'sharekit install ${user}' first`);
}
const ref = record.ref;
// If ref is immutable (pinned to a tag or commit), don't update
if (isImmutableRef(ref)) {
console.log(kleur.yellow(`\n pinned to ${ref} — nothing to update\n`));
return;
}
// Ref is mutable (branch or HEAD), so fetch the latest
// Use injected cache root for offline testability
const cacheRoot = path.join(dirs.state, 'profiles');
const fetchDir = fetchProfile(user, ref, 'https://github.com', cacheRoot);
const manifest = readManifest(fetchDir);
// 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 files = plan(fetchDir, roots);
printPlan(files, manifest);
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'));
// 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. Update with it?`, yes))) {
try {
const includeHooks = opts?.includeHooks ?? false;
const yes = opts?.yes ?? false;
const dryRun = opts?.dryRun ?? false;
const additive = opts?.additive ?? false;
// Get the install record for this user
const installed = readInstalled(dirs);
const record = installed[user];
if (!record) {
throw new Error(`not installed — run 'sharekit install ${user}' first`);
}
const ref = record.ref;
// If ref is immutable (pinned to a tag or commit), don't update
if (isImmutableRef(ref)) {
console.log(kleur.yellow(`\n pinned to ${ref} — nothing to update\n`));
return;
}
// Ref is mutable (branch or HEAD), so fetch the latest
// Use injected cache root for offline testability
const cacheRoot = path.join(dirs.state, 'profiles');
const fetchDir = fetchProfile(user, ref, 'https://github.com', cacheRoot);
const manifest = readManifest(fetchDir);
// 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 files = plan(fetchDir, roots);
printPlan(files, manifest);
const todo = files.filter((f) => (additive ? f.status === 'new' : f.status !== 'same') && !isExecutable(f, includeHooks));
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));
if (skipped.length) {
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));
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. Update with it?`, 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'));
if (dryRun) {
// For dry-run, just count files
const filesWritten = todo.length;
console.log(kleur.cyan(`\n (dry-run — no files written)`));
console.log(kleur.green(`\n ✓ Would update ${filesWritten} file(s).`));
console.log();
return;
}
const { backupDir, filesWritten } = updateApply(user, includeHooks, dirs, additive);
console.log(kleur.green(`\n ✓ Updated ${filesWritten} file(s).`) +
kleur.dim(` Backup: ${tildify(backupDir)}`));
console.log(kleur.dim(` Undo: sharekit rollback ${user}\n`));
}
if (!(await confirm(`Apply ${todo.length} change(s)?`, yes)))
return void console.log(kleur.dim('\n Aborted.\n'));
if (dryRun) {
// For dry-run, just count files
const filesWritten = todo.length;
console.log(kleur.cyan(`\n (dry-run — no files written)`));
console.log(kleur.green(`\n ✓ Would update ${filesWritten} file(s).`));
console.log();
return;
finally {
}
const { backupDir, filesWritten } = updateApply(user, includeHooks, dirs);
console.log(kleur.green(`\n ✓ Updated ${filesWritten} file(s).`) +
kleur.dim(` Backup: ${tildify(backupDir)}`));
console.log(kleur.dim(` Undo: sharekit rollback ${user}\n`));
}
export async function install(user, opts) {
const includeHooks = opts?.includeHooks ?? false;
const { user: userName, ref: userRef } = parseUserRef(user);
const yes = opts?.yes ?? false;
const dryRun = opts?.dryRun ?? false;
const dir = fetchProfile(userName, userRef);
const manifest = readManifest(dir);
const files = plan(dir);
console.log();
printPlan(files, manifest);
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'));
// 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?`, yes))) {
try {
const includeHooks = opts?.includeHooks ?? false;
const { user: userName, ref: userRef } = parseUserRef(user);
const yes = opts?.yes ?? false;
const dryRun = opts?.dryRun ?? false;
const dir = fetchProfile(userName, userRef);
const manifest = readManifest(dir);
const files = plan(dir);
console.log();
printPlan(files, manifest);
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'));
// 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?`, 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);
// Only record install if not a dry-run
if (!dryRun) {
recordInstall(userName, dir, userRef ?? 'HEAD', manifest.version);
}
if (dryRun) {
console.log(kleur.cyan(`\n (dry-run — no files written)`));
}
console.log(kleur.green(`\n ✓ Applied ${filesWritten} file(s).`) +
(dryRun ? '' : kleur.dim(` Backup: ${tildify(backupDir)}`)));
if (!dryRun) {
console.log(kleur.dim(` Undo: sharekit rollback ${userName}`));
}
console.log();
}
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);
// Only record install if not a dry-run
if (!dryRun) {
recordInstall(userName, dir, userRef ?? 'HEAD', manifest.version);
finally {
}
if (dryRun) {
console.log(kleur.cyan(`\n (dry-run — no files written)`));
}
console.log(kleur.green(`\n ✓ Applied ${filesWritten} file(s).`) +
(dryRun ? '' : kleur.dim(` Backup: ${tildify(backupDir)}`)));
if (!dryRun) {
console.log(kleur.dim(` Undo: sharekit rollback ${userName}`));
}
console.log();
}

@@ -256,21 +294,4 @@ export async function preview(user) {

try {
const rawData = JSON.parse(fs.readFileSync(appliedPath, 'utf8'));
// Validate that it's an array
if (!Array.isArray(rawData)) {
throw new Error('applied.json must be an array');
}
// Validate array elements have required shape
applied = rawData.map((item, index) => {
if (typeof item !== 'object' || item === null) {
throw new Error(`applied.json[${index}] is not an object`);
}
const { dest, status } = item;
if (typeof dest !== 'string') {
throw new Error(`applied.json[${index}].dest must be a string, got ${typeof dest}`);
}
if (typeof status !== 'string') {
throw new Error(`applied.json[${index}].status must be a string, got ${typeof status}`);
}
return { dest, status };
});
const rawData = fs.readFileSync(appliedPath, 'utf8');
applied = parseApplied(rawData);
}

@@ -338,21 +359,4 @@ catch (error) {

try {
const rawData = JSON.parse(fs.readFileSync(appliedPath, 'utf8'));
// Validate that it's an array
if (!Array.isArray(rawData)) {
throw new Error('applied.json must be an array');
}
// Validate array elements have required shape
applied = rawData.map((item, index) => {
if (typeof item !== 'object' || item === null) {
throw new Error(`applied.json[${index}] is not an object`);
}
const { dest, status } = item;
if (typeof dest !== 'string') {
throw new Error(`applied.json[${index}].dest must be a string, got ${typeof dest}`);
}
if (typeof status !== 'string') {
throw new Error(`applied.json[${index}].status must be a string, got ${typeof status}`);
}
return { dest, status };
});
const rawData = fs.readFileSync(appliedPath, 'utf8');
applied = parseApplied(rawData);
}

@@ -407,6 +411,8 @@ catch (error) {

}
// Remove user from installed.json
// Remove user from installed.json atomically (#122)
delete installed[user];
const stateFile = path.join(dirs.state, 'installed.json');
fs.writeFileSync(stateFile, JSON.stringify(installed, null, 2));
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` : ''}`;

@@ -413,0 +419,0 @@ console.log(kleur.green(`\n ✓ Uninstalled ${user}. ${summary}`));

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

import { parse as parseToml } from 'smol-toml';
import kleur from 'kleur';
import { STATE, MAX_MANIFEST_BYTES } from './paths.js';

@@ -26,5 +27,8 @@ export function parseUserRef(user) {

// Validate userName
if (!userName || userName.trim() === '') {
if (!userName || userName.trim() === '' || /^@+$/.test(userName)) {
throw new Error(`Invalid user reference '${user}': missing GitHub username before '@'`);
}
if (userName.includes('@')) {
throw new Error(`Invalid user reference '${user}': username cannot contain '@'`);
}
if (userName.includes('..')) {

@@ -53,3 +57,4 @@ throw new Error(`Invalid user '${userName}': username cannot contain '..'`);

const resolvedDir = path.resolve(dir);
if (!resolvedDir.startsWith(resolvedCache + path.sep)) {
const rel = path.relative(resolvedCache, resolvedDir);
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) {
throw new Error(`Invalid profile path: cache escape detected for '${cacheKey}'`);

@@ -64,3 +69,12 @@ }

}
catch {
catch (e) {
// Check if the cache dir is corrupted
try {
execFileSync('git', ['rev-parse', '--git-dir'], { cwd: dir });
}
catch {
// Cache dir is corrupt — clean it up
fs.rmSync(dir, { recursive: true, force: true });
console.log(`Profile cache corrupted — cleaned up, retry to re-clone`);
}
// ponytail: refresh is best-effort — offline / no-remote / detached HEAD falls back to the cached copy

@@ -87,2 +101,13 @@ }

catch (e) {
// Check if the cache dir is corrupted and clean it up
if (fs.existsSync(dir)) {
try {
execFileSync('git', ['rev-parse', '--git-dir'], { cwd: dir });
}
catch {
// Cache dir is corrupt — clean it up
fs.rmSync(dir, { recursive: true, force: true });
console.log(`Profile cache corrupted — cleaned up, retry to re-clone`);
}
}
if (e.code === 'ENOENT') {

@@ -120,7 +145,12 @@ throw new Error('git not found — install git to use sharekit (https://git-scm.com)');

const profile = (parsed.profile ?? {});
const version = profile.version;
// Warn if version is not semver format
if (version && !/^\d+\.\d+\.\d+/.test(version)) {
console.warn(kleur.yellow(`Warning: profile version "${version}" is not semver — treating as-is`));
}
return {
name: profile.name ?? 'unknown',
version: profile.version,
version,
description: profile.description,
};
}

@@ -9,3 +9,3 @@ #!/usr/bin/env node

const STATE = path.join(HOME, '.sharekit');
const VERSION = '0.4.0';
const VERSION = '0.5.0';
const USAGE = `${kleur.bold('sharekit')} v${VERSION} — share your AI coding setup

@@ -28,2 +28,3 @@

--include-hooks also apply settings.json with shell hooks
--additive only add new files; preserve local edits
${kleur.cyan('rollback')} <user> [opts] restore the last backup

@@ -89,2 +90,4 @@ --yes auto-approve prompts

opts.includeHooks = true;
if (flags.includes('--additive'))
opts.additive = true;
await update(user, opts);

@@ -91,0 +94,0 @@ return;

@@ -10,3 +10,3 @@ export declare const HOME: string;

export declare const DEFAULT_DIRS: Dirs;
export declare const tildify: (p: string) => string;
export declare const tildify: (p: string, maxLen?: number) => string;
export declare function cp(src: string, dest: string): void;

@@ -13,0 +13,0 @@ export interface WalkResult {

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

export const DEFAULT_DIRS = { home: HOME, state: STATE };
export const tildify = (p) => {
if (!p.startsWith(HOME))
return p;
// Normalize separators to forward slashes for cross-platform display
return '~' + p.slice(HOME.length).split(path.sep).join('/');
export const tildify = (p, maxLen) => {
const result = p.startsWith(HOME) ? '~' + p.slice(HOME.length).split(path.sep).join('/') : p;
if (maxLen && result.length > maxLen && p.startsWith(HOME)) {
const half = Math.floor((maxLen - 3) / 2);
const end = result.length - (maxLen - 3 - half);
return result.slice(0, half) + '...' + result.slice(end);
}
return result;
};

@@ -31,3 +34,10 @@ // copy a file, preserving its mode (e.g. a skill's executable toggle.sh)

const skippedSymlinks = [];
const visited = new Set();
const traverse = (currentDir) => {
const realDir = fs.realpathSync(currentDir);
if (visited.has(realDir)) {
console.warn(` Warning: symlink loop detected at ${currentDir} — skipping`);
return;
}
visited.add(realDir);
for (const e of fs.readdirSync(currentDir, { withFileTypes: true })) {

@@ -34,0 +44,0 @@ const p = path.join(currentDir, e.name);

@@ -28,3 +28,3 @@ import * as fs from 'node:fs';

}
return files;
return files.filter((f) => f.status !== 'same');
}

@@ -31,0 +31,0 @@ function classify(src, dest) {

@@ -113,4 +113,4 @@ import kleur from 'kleur';

}
// Rule 7: Bearer JWT (HIGH) — eyJ prefix identifies base64-encoded JSON header (JWT)
const bearerMatch = /Bearer eyJ[A-Za-z0-9._\-]{17,}/.exec(line);
// Rule 7: Bearer JWT (HIGH) — require 2 dots to match JWT structure (header.payload.signature)
const bearerMatch = /Bearer eyJ[A-Za-z0-9._\-]*\.[A-Za-z0-9._\-]+\.[A-Za-z0-9._\-]+/.exec(line);
if (bearerMatch) {

@@ -117,0 +117,0 @@ const preview = truncatePreview(line, bearerMatch.index, 0, 30, false);

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

export type { Status, PlanFile } from './plan.js';
export { listBackups, pruneBackups, restoreBackup, restoreBackupInternal, restoreBackupToStamp, readMetadata, writeMetadata, } from './backup.js';
export { listBackups, pruneBackups, restoreBackup, restoreBackupInternal, restoreBackupToStamp, readMetadata, writeMetadata, parseApplied, } from './backup.js';
export type { BackupInfo, RestoreMetadata } from './backup.js';

@@ -11,0 +11,0 @@ export { recordInstall, readInstalled, list, isImmutableRef } from './state.js';

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

// From backup.ts
export { listBackups, pruneBackups, restoreBackup, restoreBackupInternal, restoreBackupToStamp, readMetadata, writeMetadata, } from './backup.js';
export { listBackups, pruneBackups, restoreBackup, restoreBackupInternal, restoreBackupToStamp, readMetadata, writeMetadata, parseApplied, } from './backup.js';
// From state.ts

@@ -14,0 +14,0 @@ export { recordInstall, readInstalled, list, isImmutableRef } from './state.js';

@@ -13,1 +13,3 @@ import { Dirs } from './paths.js';

export declare function isImmutableRef(ref: string): boolean;
export declare function acquireLock(lockPath?: string): void;
export declare function releaseLock(lockPath?: string): void;

@@ -6,3 +6,2 @@ import * as fs from 'node:fs';

import { DEFAULT_DIRS } from './paths.js';
// Record an installation: resolve commit SHA, write to state file (keyed by user)
export function recordInstall(user, profileDir, ref, version, dirs = DEFAULT_DIRS) {

@@ -17,13 +16,4 @@ let commit = null;

}
catch {
// best-effort: if rev-parse fails (offline cache, odd state), record null
}
const record = {
user,
ref,
commit,
version,
appliedAt: new Date().toISOString(),
};
// Read existing state or create empty map
catch { }
const record = { user, ref, commit, version, appliedAt: new Date().toISOString() };
const stateFile = path.join(dirs.state, 'installed.json');

@@ -35,15 +25,10 @@ let installed = {};

}
catch {
// corrupt state file; overwrite with fresh state
installed = {};
}
catch { }
}
// Update or insert this user's record
installed[user] = record;
// Write back
fs.mkdirSync(dirs.state, { recursive: true });
fs.writeFileSync(stateFile, JSON.stringify(installed, null, 2));
const tmp = stateFile + '.tmp';
fs.writeFileSync(tmp, JSON.stringify(installed, null, 2));
fs.renameSync(tmp, stateFile);
}
// Read the install state file (returns a map keyed by user, or empty object if not found)
// If the file exists but is corrupt, warn to stderr to help the user diagnose the issue.
export function readInstalled(dirs = DEFAULT_DIRS) {

@@ -54,11 +39,14 @@ const stateFile = path.join(dirs.state, 'installed.json');

try {
return JSON.parse(fs.readFileSync(stateFile, 'utf8'));
const parsed = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
console.warn(kleur.yellow(' Warning: installed.json is corrupt — resetting to empty'));
return {};
}
return parsed;
}
catch (e) {
console.error(`${kleur.yellow('⚠ install state is corrupt')} — ${stateFile}\n` +
` ${kleur.dim(`Reset: rm ${stateFile}`)} to rebuild from scratch.`);
console.error(`${kleur.yellow('⚠ install state is corrupt')} — ${stateFile}\n ${kleur.dim(`Reset: rm ${stateFile}`)} to rebuild from scratch.`);
return {};
}
}
// List installed profiles with version, short commit SHA, and applied date
export function list(dirs = DEFAULT_DIRS) {

@@ -74,3 +62,2 @@ const installed = readInstalled(dirs);

const shortSha = record.commit ? record.commit.slice(0, 7) : '?';
// Guard against invalid appliedAt timestamps
let dateStr = '(unknown)';

@@ -92,16 +79,42 @@ if (record.appliedAt) {

}
// Check if a ref is an immutable ref (tag or commit hash).
// Only return true for HIGH-CONFIDENCE immutable cases. Bias toward mutable for ambiguous names
// because the best-effort git pull (from #52a) harmlessly no-ops on detached checkouts.
// Wrongly skipping an update to a real branch (the current risk) is worse than attempting
// a redundant pull on a tag (which fails harmlessly).
export function isImmutableRef(ref) {
// Hex commit SHA (7-40 hex chars, any case) is definitely immutable
if (/^[a-fA-F0-9]{7,40}$/.test(ref))
return true;
// Anchored dotted semver: v1.0.0, 1.2, v2.0, etc. Excludes v2-wip, v3-feature, release-2
if (/^v?\d+\.\d+(\.\d+)*$/.test(ref))
return true;
// Everything else (branches, loose tags, ambiguous names) → mutable. Pull is safe; no-ops on detached.
return false;
}
const LOCK_FILE = path.join(DEFAULT_DIRS.state, '.lock');
export function acquireLock(lockPath = LOCK_FILE) {
const pid = process.pid.toString();
if (fs.existsSync(lockPath)) {
try {
const existingPid = fs.readFileSync(lockPath, 'utf8').trim();
try {
fs.statSync(`/proc/${existingPid}`);
console.error(`Another sharekit operation is running (PID ${existingPid}). Try again in a moment.`);
process.exit(1);
}
catch {
try {
fs.unlinkSync(lockPath);
}
catch { }
}
}
catch {
try {
fs.unlinkSync(lockPath);
}
catch { }
}
}
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
fs.writeFileSync(lockPath, pid);
}
export function releaseLock(lockPath = LOCK_FILE) {
try {
fs.unlinkSync(lockPath);
}
catch { }
}
{
"name": "@lucassantana/sharekit",
"version": "0.4.0",
"version": "0.5.0",
"description": "Share your AI coding setup with anyone — one command to install, one to rollback.",

@@ -5,0 +5,0 @@ "keywords": [