@lucassantana/sharekit
Advanced tools
+8
-8
| #!/usr/bin/env node | ||
| import kleur from 'kleur'; | ||
| import { install, preview, rollback, init } from './sharekit.js'; | ||
| const VERSION = '0.2.0'; | ||
| const VERSION = '0.2.1'; | ||
| const USAGE = `${kleur.bold('sharekit')} v${VERSION} — share your AI coding setup | ||
@@ -34,13 +34,13 @@ | ||
| } | ||
| const arg = rest[0]; | ||
| // Separate flags from positionals so flags work in any position | ||
| const flags = rest.filter((x) => x.startsWith('--')); | ||
| const arg = rest.find((x) => !x.startsWith('--')); | ||
| if (!arg) { | ||
| console.error(kleur.red(`usage: sharekit ${cmd} <user>${cmd === 'install' ? '[@<ref>]' : ''}`)); | ||
| const showRef = cmd === 'install' || cmd === 'preview'; | ||
| console.error(kleur.red(`usage: sharekit ${cmd} <user>${showRef ? '[@<ref>]' : ''}`)); | ||
| process.exit(1); | ||
| } | ||
| // Parse options for install command | ||
| const opts = {}; | ||
| if (cmd === 'install') { | ||
| if (rest.includes('--include-hooks')) { | ||
| opts.includeHooks = true; | ||
| } | ||
| if (cmd === 'install' && flags.includes('--include-hooks')) { | ||
| opts.includeHooks = true; | ||
| } | ||
@@ -47,0 +47,0 @@ await fn(arg, opts); |
+11
-5
@@ -9,2 +9,9 @@ type Status = 'new' | 'changed' | 'same'; | ||
| } | ||
| type Dirs = { | ||
| home: string; | ||
| state: string; | ||
| }; | ||
| export interface InstallOpts { | ||
| includeHooks?: boolean; | ||
| } | ||
| export declare function fetchProfile(user: string, ref?: string, baseUrl?: string, cacheRoot?: string): string; | ||
@@ -18,10 +25,9 @@ export declare function readManifest(profileDir: string): { | ||
| export declare function printPlan(files: PlanFile[], manifest: ReturnType<typeof readManifest>): void; | ||
| export declare function applyProfile(files: PlanFile[], user: string, includeHooks?: boolean): { | ||
| export declare function pruneBackups(user: string, state?: string): void; | ||
| export declare function applyProfile(files: PlanFile[], user: string, includeHooks?: boolean, dirs?: Dirs): { | ||
| 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 restoreBackup(user: string, dirs?: Dirs): void; | ||
| export declare function install(user: string, opts?: InstallOpts): Promise<void>; | ||
| export declare function preview(user: string): Promise<void>; | ||
@@ -28,0 +34,0 @@ export declare function rollback(user: string): Promise<void>; |
+21
-18
@@ -6,3 +6,3 @@ import * as fs from 'node:fs'; | ||
| import { stdin as input, stdout as output } from 'node:process'; | ||
| import { execSync } from 'node:child_process'; | ||
| import { execFileSync } from 'node:child_process'; | ||
| import TOML from '@iarna/toml'; | ||
@@ -18,2 +18,3 @@ import kleur from 'kleur'; | ||
| }; | ||
| const DEFAULT_DIRS = { home: HOME, state: STATE }; | ||
| const tildify = (p) => (p.startsWith(HOME) ? '~' + p.slice(HOME.length) : p); | ||
@@ -42,3 +43,3 @@ function walk(dir) { | ||
| try { | ||
| execSync(`git -C "${dir}" pull --ff-only`, { stdio: 'pipe' }); | ||
| execFileSync('git', ['-C', dir, 'pull', '--ff-only'], { stdio: 'pipe' }); | ||
| } | ||
@@ -55,14 +56,16 @@ catch { | ||
| if (ref) { | ||
| execSync(`git clone --depth 1 --branch "${ref}" "${url}" "${dir}"`, { stdio: 'pipe' }); | ||
| execFileSync('git', ['clone', '--depth', '1', '--branch', ref, '--', url, dir], { | ||
| stdio: 'pipe', | ||
| }); | ||
| } | ||
| else { | ||
| execSync(`git clone --depth 1 "${url}" "${dir}"`, { stdio: 'pipe' }); | ||
| execFileSync('git', ['clone', '--depth', '1', '--', url, dir], { stdio: 'pipe' }); | ||
| } | ||
| } | ||
| catch (e) { | ||
| if (e.status === 127) { | ||
| if (e.code === 'ENOENT') { | ||
| 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')) { | ||
| const errOut = (e.stderr?.toString() ?? '') + (e.message ?? ''); | ||
| if (ref && errOut.includes('not found')) { | ||
| throw new Error(`ref '${ref}' not found in ${user}'s profile`); | ||
@@ -142,4 +145,4 @@ } | ||
| // Prune backups, keeping only the most recent 5 | ||
| function pruneBackups(user) { | ||
| const root = path.join(STATE, 'backups'); | ||
| export function pruneBackups(user, state = STATE) { | ||
| const root = path.join(state, 'backups'); | ||
| if (!fs.existsSync(root)) | ||
@@ -158,9 +161,9 @@ return; | ||
| } | ||
| function backup(files, user, includeHooks = false) { | ||
| function backup(files, user, includeHooks = false, dirs = DEFAULT_DIRS) { | ||
| const stamp = new Date().toISOString().replace(/[:.]/g, '-'); | ||
| const dir = path.join(STATE, 'backups', `${user}-${stamp}`); | ||
| const dir = path.join(dirs.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')) { | ||
| const t = path.join(dir, path.relative(HOME, f.dest)); | ||
| const t = path.join(dir, path.relative(dirs.home, f.dest)); | ||
| fs.mkdirSync(path.dirname(t), { recursive: true }); | ||
@@ -184,10 +187,10 @@ cp(f.dest, t); | ||
| // Exported pure functions for testability | ||
| export function applyProfile(files, user, includeHooks = false) { | ||
| const backupDir = backup(files, user, includeHooks); | ||
| export function applyProfile(files, user, includeHooks = false, dirs = DEFAULT_DIRS) { | ||
| const backupDir = backup(files, user, includeHooks, dirs); | ||
| const filesWritten = write(files, includeHooks); | ||
| pruneBackups(user); | ||
| pruneBackups(user, dirs.state); | ||
| return { backupDir, filesWritten }; | ||
| } | ||
| export function restoreBackup(user) { | ||
| const root = path.join(STATE, 'backups'); | ||
| export function restoreBackup(user, dirs = DEFAULT_DIRS) { | ||
| const root = path.join(dirs.state, 'backups'); | ||
| const last = fs.existsSync(root) | ||
@@ -208,3 +211,3 @@ ? fs | ||
| else { | ||
| const src = path.join(dir, path.relative(HOME, a.dest)); | ||
| const src = path.join(dir, path.relative(dirs.home, a.dest)); | ||
| if (fs.existsSync(src)) { | ||
@@ -211,0 +214,0 @@ fs.mkdirSync(path.dirname(a.dest), { recursive: true }); // dest dir may have been removed since install |
+1
-1
| { | ||
| "name": "@lucassantana/sharekit", | ||
| "version": "0.2.0", | ||
| "version": "0.2.1", | ||
| "description": "Share your AI coding setup with anyone — one command to install, one to rollback.", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
+19
-0
@@ -18,2 +18,21 @@ # sharekit | ||
| ### Pin to a version | ||
| Install a specific tag or branch of a profile instead of the latest: | ||
| ```bash | ||
| npx @lucassantana/sharekit install <github-user>@v1.0 # a tag | ||
| npx @lucassantana/sharekit install <github-user>@stable # a branch | ||
| ``` | ||
| Plain `install <github-user>` always tracks the profile's default branch (HEAD). | ||
| ### Hooks | ||
| 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 | ||
| ``` | ||
| ## Publish your own profile | ||
@@ -20,0 +39,0 @@ |
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
22970
6.04%420
2.19%72
35.85%3
50%