git-intent
Advanced tools
Comparing version
#!/usr/bin/env node | ||
// src/index.ts | ||
import { getPackageInfo, storage as storage10 } from "@offlegacy/git-intent-core"; | ||
import { program } from "commander"; | ||
// src/utils/storage.ts | ||
import path3 from "node:path"; | ||
import fs3 from "fs-extra"; | ||
// src/utils/generateId.ts | ||
import { nanoid } from "nanoid"; | ||
function generateId(size) { | ||
return nanoid(size); | ||
} | ||
// src/utils/get-package-info.ts | ||
import path from "node:path"; | ||
import fs from "fs-extra"; | ||
function getPackageInfo() { | ||
const possiblePaths = [ | ||
path.resolve(__dirname, "../../package.json"), | ||
path.resolve(__dirname, "../package.json"), | ||
path.resolve(process.cwd(), "package.json") | ||
]; | ||
for (const packageJsonPath of possiblePaths) { | ||
try { | ||
if (fs.existsSync(packageJsonPath)) { | ||
const packageJson = fs.readJSONSync(packageJsonPath); | ||
if (packageJson.name === "git-intent") { | ||
return { | ||
version: packageJson.version || "0.0.0", | ||
description: packageJson.description || "Git Intent CLI" | ||
}; | ||
} | ||
} | ||
} catch (error) { | ||
} | ||
} | ||
return { | ||
version: "0.0.0", | ||
description: "Git Intent CLI" | ||
}; | ||
} | ||
// src/utils/git.ts | ||
import { spawnSync } from "node:child_process"; | ||
import path2 from "node:path"; | ||
import fs2 from "fs-extra"; | ||
import { simpleGit } from "simple-git"; | ||
function execGit(args, options = {}) { | ||
const { input, cwd } = options; | ||
const result = spawnSync("git", args, { | ||
input: input ? Buffer.from(input) : void 0, | ||
cwd, | ||
encoding: "utf-8", | ||
stdio: ["pipe", "pipe", "pipe"] | ||
}); | ||
if (result.status !== 0) { | ||
throw new Error(`Git command failed: git ${args.join(" ")} | ||
${result.stderr}`); | ||
} | ||
return result.stdout ? result.stdout.trim() : ""; | ||
} | ||
var createGit = (cwd) => simpleGit(cwd); | ||
async function findGitRoot(startDir = process.cwd()) { | ||
const dir = path2.resolve(startDir); | ||
const gitDir = path2.join(dir, ".git"); | ||
if (fs2.existsSync(gitDir) && fs2.statSync(gitDir).isDirectory()) { | ||
return dir; | ||
} | ||
const parentDir = path2.dirname(dir); | ||
if (parentDir === dir) { | ||
throw new Error("Not a git repository (or any of the parent directories)"); | ||
} | ||
return findGitRoot(parentDir); | ||
} | ||
async function checkIsRepo(cwd) { | ||
try { | ||
const git = createGit(cwd); | ||
await git.checkIsRepo(); | ||
return await findGitRoot(cwd); | ||
} catch { | ||
try { | ||
return await findGitRoot(cwd); | ||
} catch (error) { | ||
throw new Error("Not a git repository (or any of the parent directories)"); | ||
} | ||
} | ||
} | ||
async function getCurrentBranch(cwd) { | ||
const git = createGit(cwd); | ||
const branch = await git.revparse(["--abbrev-ref", "HEAD"]); | ||
return branch.trim(); | ||
} | ||
async function createCommit(message, cwd) { | ||
const git = createGit(cwd); | ||
const result = await git.commit(message); | ||
return result.commit; | ||
} | ||
async function hashObject(content, cwd) { | ||
return execGit(["hash-object", "-w", "--stdin"], { input: content, cwd }); | ||
} | ||
async function createTree(treeContent, cwd) { | ||
if (!treeContent || treeContent.trim() === "") { | ||
throw new Error("Invalid tree content: tree content cannot be empty"); | ||
} | ||
return execGit(["mktree"], { input: treeContent, cwd }); | ||
} | ||
async function createCommitTree(treeHash, message, cwd) { | ||
if (!treeHash || treeHash.trim() === "") { | ||
throw new Error("Invalid tree hash: tree hash cannot be empty"); | ||
} | ||
try { | ||
const result = execGit(["commit-tree", treeHash, "-m", message], { cwd }); | ||
if (!result || result.trim() === "") { | ||
throw new Error(`Failed to create commit tree from hash: ${treeHash}`); | ||
} | ||
return result; | ||
} catch (error) { | ||
console.error("Error creating commit tree:", error); | ||
throw error; | ||
} | ||
} | ||
async function updateRef(refName, commitHash, cwd) { | ||
if (!commitHash || commitHash.trim() === "") { | ||
throw new Error(`Invalid commit hash: commit hash cannot be empty for ref ${refName}`); | ||
} | ||
const git = createGit(cwd); | ||
await git.raw(["update-ref", refName, commitHash]); | ||
} | ||
async function deleteRef(refName, cwd) { | ||
const git = createGit(cwd); | ||
await git.raw(["update-ref", "-d", refName]); | ||
} | ||
async function checkRefExists(refName, cwd) { | ||
const git = createGit(cwd); | ||
try { | ||
await git.raw(["show-ref", "--verify", refName]); | ||
return true; | ||
} catch { | ||
return false; | ||
} | ||
} | ||
var git_default = createGit(); | ||
// src/utils/storage.ts | ||
var GitIntentionalCommitStorage = class _GitIntentionalCommitStorage { | ||
static instance; | ||
REFS_PREFIX = "refs/intentional-commits"; | ||
storageFilename; | ||
GIT_DIR = ".git"; | ||
gitRoot; | ||
constructor() { | ||
this.storageFilename = process.env.VITEST ? "test_intents.json" : "intents.json"; | ||
} | ||
static getInstance() { | ||
if (!_GitIntentionalCommitStorage.instance) { | ||
_GitIntentionalCommitStorage.instance = new _GitIntentionalCommitStorage(); | ||
} | ||
return _GitIntentionalCommitStorage.instance; | ||
} | ||
setGitRoot(root) { | ||
this.gitRoot = root; | ||
} | ||
async getGitRoot() { | ||
if (this.gitRoot) return this.gitRoot; | ||
return process.cwd(); | ||
} | ||
async getCommitsDir() { | ||
const root = await this.getGitRoot(); | ||
return path3.join(root, this.GIT_DIR, "intentional-commits"); | ||
} | ||
async getCommitsFile() { | ||
const commitsDir = await this.getCommitsDir(); | ||
return path3.join(commitsDir, "commits.json"); | ||
} | ||
getInitialData() { | ||
return { | ||
version: getPackageInfo().version, | ||
commits: [] | ||
}; | ||
} | ||
async ensureCommitsDir() { | ||
const commitsDir = await this.getCommitsDir(); | ||
const commitsFile = await this.getCommitsFile(); | ||
await fs3.ensureDir(commitsDir); | ||
try { | ||
await fs3.access(commitsFile); | ||
} catch { | ||
await fs3.writeJSON(commitsFile, this.getInitialData()); | ||
} | ||
} | ||
migrateData(data) { | ||
return data; | ||
} | ||
async loadCommits() { | ||
const root = await this.getGitRoot(); | ||
await checkIsRepo(root); | ||
await this.ensureCommitsDir(); | ||
try { | ||
const result = await git_default.cwd(root).show(`${this.REFS_PREFIX}/commits:${this.storageFilename}`); | ||
const data = this.migrateData(JSON.parse(result)); | ||
return data.commits; | ||
} catch { | ||
const commitsFile = await this.getCommitsFile(); | ||
const data = this.migrateData(await fs3.readJSON(commitsFile)); | ||
return data.commits; | ||
} | ||
} | ||
async saveCommitsData(data) { | ||
const root = await this.getGitRoot(); | ||
const commitsFile = await this.getCommitsFile(); | ||
const content = JSON.stringify(data, null, 2); | ||
const hash = await hashObject(content, root); | ||
const treeContent = `100644 blob ${hash} ${this.storageFilename} | ||
`; | ||
const treeHash = await createTree(treeContent, root); | ||
const commitHash = await createCommitTree(treeHash, "Update intent commits", root); | ||
await updateRef(`${this.REFS_PREFIX}/commits`, commitHash, root); | ||
await fs3.writeJSON(commitsFile, data, { spaces: 2 }); | ||
} | ||
async saveCommits(commits) { | ||
const data = { | ||
version: getPackageInfo().version, | ||
commits | ||
}; | ||
await this.saveCommitsData(data); | ||
} | ||
async addCommit(commit2) { | ||
const currentCommits = await this.loadCommits(); | ||
const newCommitId = generateId(8); | ||
const newCommit = { ...commit2, id: newCommitId }; | ||
const data = { | ||
version: getPackageInfo().version, | ||
commits: [...currentCommits, newCommit] | ||
}; | ||
await this.saveCommitsData(data); | ||
return newCommitId; | ||
} | ||
async updateCommitMessage(id, message) { | ||
const currentCommits = await this.loadCommits(); | ||
const existingCommit = currentCommits.find((c) => c.id === id); | ||
if (!existingCommit) { | ||
throw new Error("Commit not found"); | ||
} | ||
const data = { | ||
version: getPackageInfo().version, | ||
commits: currentCommits.map((c) => c.id === id ? { ...existingCommit, message } : c) | ||
}; | ||
await this.saveCommitsData(data); | ||
} | ||
async deleteCommit(id) { | ||
const currentCommits = await this.loadCommits(); | ||
const newCommits = currentCommits.filter((c) => c.id !== id); | ||
await this.saveCommits(newCommits); | ||
} | ||
async clearCommits() { | ||
const root = await this.getGitRoot(); | ||
const commitsFile = await this.getCommitsFile(); | ||
await fs3.remove(commitsFile); | ||
await deleteRef(`${this.REFS_PREFIX}/commits`, root); | ||
} | ||
async initializeRefs() { | ||
const root = await this.getGitRoot(); | ||
await checkIsRepo(root); | ||
const refExists = await checkRefExists(`${this.REFS_PREFIX}/commits`, root); | ||
if (!refExists) { | ||
const initialData = this.getInitialData(); | ||
const content = JSON.stringify(initialData, null, 2); | ||
const hash = await hashObject(content, root); | ||
const treeContent = `100644 blob ${hash} ${this.storageFilename} | ||
`; | ||
const treeHash = await createTree(treeContent, root); | ||
const commitHash = await createCommitTree(treeHash, "Initialize intent commits", root); | ||
if (!commitHash || commitHash.trim() === "") { | ||
throw new Error("Failed to create commit: commit hash is empty"); | ||
} | ||
await updateRef(`${this.REFS_PREFIX}/commits`, commitHash, root); | ||
} | ||
} | ||
}; | ||
var storage = GitIntentionalCommitStorage.getInstance(); | ||
// src/commands/list.ts | ||
import { storage } from "@offlegacy/git-intent-core"; | ||
import chalk from "chalk"; | ||
@@ -307,2 +31,3 @@ import { Command } from "commander"; | ||
// src/commands/start.ts | ||
import { git, storage as storage2 } from "@offlegacy/git-intent-core"; | ||
import chalk2 from "chalk"; | ||
@@ -312,3 +37,3 @@ import { Command as Command2 } from "commander"; | ||
var start = new Command2().command("start").argument("[id]", "Intent ID").description("Start working on a planned intent").action(async (id) => { | ||
const commits = await storage.loadCommits(); | ||
const commits = await storage2.loadCommits(); | ||
let selectedId = id; | ||
@@ -345,7 +70,7 @@ if (!selectedId) { | ||
} | ||
const currentBranch = await getCurrentBranch(); | ||
const currentBranch = await git.getCurrentBranch(); | ||
targetCommit.status = "in_progress"; | ||
targetCommit.metadata.startedAt = (/* @__PURE__ */ new Date()).toISOString(); | ||
targetCommit.metadata.branch = currentBranch; | ||
await storage.saveCommits(commits); | ||
await storage2.saveCommits(commits); | ||
console.log(chalk2.green("\u2713 Started working on:")); | ||
@@ -358,6 +83,7 @@ console.log(`ID: ${chalk2.blue(targetCommit.id)}`); | ||
// src/commands/show.ts | ||
import { storage as storage3 } from "@offlegacy/git-intent-core"; | ||
import chalk3 from "chalk"; | ||
import { Command as Command3 } from "commander"; | ||
var show = new Command3().command("show").description("Show current intention").action(async () => { | ||
const commits = await storage.loadCommits(); | ||
const commits = await storage3.loadCommits(); | ||
const currentCommit = commits.find((c) => c.status === "in_progress"); | ||
@@ -377,6 +103,7 @@ if (!currentCommit) { | ||
// src/commands/commit.ts | ||
import { git as git2, storage as storage4 } from "@offlegacy/git-intent-core"; | ||
import chalk4 from "chalk"; | ||
import { Command as Command4 } from "commander"; | ||
var commit = new Command4().command("commit").description("Complete current intention and commit").option("-m, --message <message>", "Additional commit message").action(async (options) => { | ||
const commits = await storage.loadCommits(); | ||
const commits = await storage4.loadCommits(); | ||
const currentCommit = commits.find((c) => c.status === "in_progress"); | ||
@@ -390,4 +117,4 @@ if (!currentCommit) { | ||
${options.message}` : currentCommit.message; | ||
await createCommit(message); | ||
await storage.deleteCommit(currentCommit.id); | ||
await git2.createCommit(message); | ||
await storage4.deleteCommit(currentCommit.id); | ||
console.log(chalk4.green("\u2713 Intention completed and committed")); | ||
@@ -398,2 +125,3 @@ }); | ||
// src/commands/drop.ts | ||
import { storage as storage5 } from "@offlegacy/git-intent-core"; | ||
import chalk5 from "chalk"; | ||
@@ -403,6 +131,6 @@ import { Command as Command5 } from "commander"; | ||
var drop = new Command5().command("drop").description("Drop a planned intent").argument("[id]", "Intent ID").option("-a, --all", "Drop all created intents").action(async (id, options) => { | ||
const commits = await storage.loadCommits(); | ||
const commits = await storage5.loadCommits(); | ||
const createdCommits = commits.filter((c) => c.status === "created"); | ||
if (options.all) { | ||
await storage.saveCommits([]); | ||
await storage5.saveCommits([]); | ||
console.log(chalk5.green("\u2713 All created intents removed")); | ||
@@ -442,3 +170,3 @@ return; | ||
const updatedCommits = commits.filter((c) => c.id !== selectedId); | ||
await storage.saveCommits(updatedCommits); | ||
await storage5.saveCommits(updatedCommits); | ||
console.log(chalk5.green("\u2713 Intent removed:")); | ||
@@ -451,2 +179,3 @@ console.log(`ID: ${chalk5.blue(targetCommit.id)}`); | ||
// src/commands/cancel.ts | ||
import { storage as storage6 } from "@offlegacy/git-intent-core"; | ||
import chalk6 from "chalk"; | ||
@@ -456,3 +185,3 @@ import { Command as Command6 } from "commander"; | ||
var cancel = new Command6().command("cancel").description("Cancel current intention").action(async () => { | ||
const commits = await storage.loadCommits(); | ||
const commits = await storage6.loadCommits(); | ||
const currentCommit = commits.find((c) => c.status === "in_progress"); | ||
@@ -485,3 +214,3 @@ if (!currentCommit) { | ||
} | ||
await storage.saveCommits(updatedCommits); | ||
await storage6.saveCommits(updatedCommits); | ||
console.log(`ID: ${chalk6.blue(currentCommit.id)}`); | ||
@@ -494,2 +223,3 @@ console.log(`Message: ${currentCommit.message}`); | ||
// src/commands/reset.ts | ||
import { storage as storage7 } from "@offlegacy/git-intent-core"; | ||
import { Command as Command7 } from "commander"; | ||
@@ -507,3 +237,3 @@ import prompts4 from "prompts"; | ||
} | ||
await storage.clearCommits(); | ||
await storage7.clearCommits(); | ||
console.log("All intents reset"); | ||
@@ -514,2 +244,3 @@ }); | ||
// src/commands/divide.ts | ||
import { storage as storage8 } from "@offlegacy/git-intent-core"; | ||
import chalk7 from "chalk"; | ||
@@ -520,3 +251,3 @@ import { Command as Command8 } from "commander"; | ||
var divide = new Command8().command("divide").description("Divide an intent into smaller parts").action(async () => { | ||
const commits = await storage.loadCommits(); | ||
const commits = await storage8.loadCommits(); | ||
if (commits.length === 0) { | ||
@@ -664,3 +395,3 @@ console.log("No intents found to divide"); | ||
for (const task of tasks) { | ||
const newCommitId = await storage.addCommit({ | ||
const newCommitId = await storage8.addCommit({ | ||
message: task, | ||
@@ -675,3 +406,3 @@ status: "created", | ||
if (shouldRemoveOriginal) { | ||
await storage.deleteCommit(targetCommit.id); | ||
await storage8.deleteCommit(targetCommit.id); | ||
} | ||
@@ -692,2 +423,3 @@ console.log(chalk7.green("\u2713 Successfully divided the commit:")); | ||
// src/commands/add.ts | ||
import { storage as storage9 } from "@offlegacy/git-intent-core"; | ||
import chalk8 from "chalk"; | ||
@@ -708,3 +440,3 @@ import { Command as Command9 } from "commander"; | ||
} | ||
const newCommitId = await storage.addCommit({ | ||
const newCommitId = await storage9.addCommit({ | ||
message: commitMessage, | ||
@@ -724,3 +456,3 @@ status: "created", | ||
(async () => { | ||
await storage.initializeRefs(); | ||
await storage10.initializeRefs(); | ||
const { version, description } = getPackageInfo(); | ||
@@ -727,0 +459,0 @@ program.name("git-intent").description(description).version(version).addCommand(add_default).addCommand(list_default).addCommand(start_default).addCommand(show_default).addCommand(commit_default).addCommand(cancel_default).addCommand(reset_default).addCommand(divide_default).addCommand(drop_default); |
{ | ||
"name": "git-intent", | ||
"version": "0.0.10", | ||
"version": "0.0.11", | ||
"description": "Git workflow tool for intentional commits — define your commit intentions first for clearer, more atomic changes.", | ||
@@ -37,18 +37,9 @@ "keywords": [ | ||
"external-editor": "^3.1.0", | ||
"fs-extra": "^11.3.0", | ||
"nanoid": "^3.3.7", | ||
"ora": "^5.4.1", | ||
"ora": "^8.2.0", | ||
"prompts": "^2.4.2", | ||
"simple-git": "^3.27.0" | ||
"@offlegacy/git-intent-core": "0.0.11" | ||
}, | ||
"devDependencies": { | ||
"@biomejs/biome": "^1.9.4", | ||
"@types/fs-extra": "^11.0.4", | ||
"@types/node": "^20.11.24", | ||
"@types/prompts": "^2.4.9", | ||
"pkg": "^5.8.1", | ||
"tsup": "^8.0.2", | ||
"type-fest": "^4.39.1", | ||
"typescript": "^5.3.3", | ||
"vitest": "^1.3.1" | ||
"pkg": "^5.8.1" | ||
}, | ||
@@ -58,9 +49,6 @@ "scripts": { | ||
"build": "tsup", | ||
"start": "node dist/index.js", | ||
"start": "node dist/index.cjs", | ||
"format": "biome format", | ||
"format:fix": "biome format --write", | ||
"pkg:build": "pnpm pkg:build:macos && pnpm pkg:build:linux && pnpm pkg:build:win", | ||
"pkg:build:macos": "pkg . --no-bytecode -t node18-macos-arm64 -o build/git-intent-macos", | ||
"pkg:build:linux": "pkg . --no-bytecode -t node18-linux-x64 -o build/git-intent-linux", | ||
"pkg:build:win": "pkg . --no-bytecode -t node18-win-x64 -o build/git-intent-win.exe", | ||
"pkg:build:macos": "pnpm build && pkg . --no-bytecode -t node18-macos-arm64 -o build/git-intent-macos", | ||
"test": "vitest", | ||
@@ -67,0 +55,0 @@ "test:ui": "vitest --ui" |
Sorry, the diff of this file is not supported yet
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
6
-25%2
-77.78%1
-88.89%0
-100%35839
-35.91%4
-20%879
-37.44%2
100%0
-100%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated