@lakakala/kgit 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,26 +1,27 @@
1
1
  import path from 'node:path';
2
- import fs from 'node:fs';
3
2
  import { loadConfig, findProject } from '../config.js';
3
+ import { createRunner } from '../runner.js';
4
4
  import { addWorktree, isGitRepo } from '../git.js';
5
5
  export async function appendCommand(workspaceName, projectEntries, newBranch) {
6
6
  const config = loadConfig();
7
+ const r = createRunner(config.machine);
7
8
  const targetDir = path.join(config.workspace, workspaceName);
8
9
  const branchName = newBranch ?? workspaceName;
9
- if (!fs.existsSync(targetDir)) {
10
+ if (!(await r.exists(targetDir))) {
10
11
  throw new Error(`Workspace directory does not exist: ${targetDir}. Use "kgit new" to create it first.`);
11
12
  }
12
13
  for (const entry of projectEntries) {
13
14
  const project = findProject(config, entry.name);
14
- if (!(await isGitRepo(project.path))) {
15
+ if (!(await isGitRepo(r, project.path))) {
15
16
  throw new Error(`Not a git repository: ${project.path}`);
16
17
  }
17
18
  const worktreePath = path.join(targetDir, project.name);
18
- if (fs.existsSync(worktreePath)) {
19
+ if (await r.exists(worktreePath)) {
19
20
  console.warn(`Worktree already exists, skipping: ${worktreePath}`);
20
21
  continue;
21
22
  }
22
23
  console.log(`Adding worktree for "${project.name}" (new branch: ${branchName}, base: ${entry.branch}) -> ${worktreePath}`);
23
- await addWorktree(project.path, worktreePath, branchName, entry.branch);
24
+ await addWorktree(r, project.path, worktreePath, branchName, entry.branch);
24
25
  }
25
26
  console.log(`\nProjects appended to workspace "${workspaceName}" at ${targetDir}`);
26
27
  }
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
- import fs from 'node:fs';
3
2
  import { execa } from 'execa';
4
3
  import { loadConfig } from '../config.js';
4
+ import { createRunner } from '../runner.js';
5
5
  const IDE_COMMANDS = {
6
6
  code: 'code',
7
7
  trae: 'trae',
@@ -14,6 +14,8 @@ const IDE_COMMANDS = {
14
14
  webstorm: 'webstorm',
15
15
  };
16
16
  const TERMINAL_IDES = new Set(['nvim', 'vim', 'vi', 'nano']);
17
+ // IDEs that support --remote ssh-remote+host
18
+ const SSH_REMOTE_IDES = new Set(['code', 'cursor']);
17
19
  async function detectDefaultIde() {
18
20
  for (const cmd of ['code', 'trae', 'cursor', 'nvim', 'vim']) {
19
21
  try {
@@ -26,10 +28,14 @@ async function detectDefaultIde() {
26
28
  }
27
29
  return null;
28
30
  }
31
+ function parseSshHost(machine) {
32
+ return machine.replace(/^ssh:\/\//, '');
33
+ }
29
34
  export async function editCommand(workspaceName, projectName, options) {
30
35
  const config = loadConfig();
36
+ const r = createRunner(config.machine);
31
37
  const worktreePath = path.join(config.workspace, workspaceName, projectName);
32
- if (!fs.existsSync(worktreePath)) {
38
+ if (!(await r.exists(worktreePath))) {
33
39
  throw new Error(`Project path does not exist: ${worktreePath}`);
34
40
  }
35
41
  let ideName = options.ide?.toLowerCase()
@@ -43,11 +49,30 @@ export async function editCommand(workspaceName, projectName, options) {
43
49
  console.log(`Using detected IDE: ${ideName}`);
44
50
  }
45
51
  const command = IDE_COMMANDS[ideName] ?? ideName;
46
- console.log(`Opening ${worktreePath} with ${command}...`);
47
- if (TERMINAL_IDES.has(ideName)) {
48
- await execa(command, [worktreePath], { stdio: 'inherit' });
52
+ if (config.machine) {
53
+ const host = parseSshHost(config.machine);
54
+ if (SSH_REMOTE_IDES.has(ideName)) {
55
+ // VS Code / Cursor: use --remote ssh-remote+host
56
+ console.log(`Opening ${worktreePath} on ${host} with ${command} --remote...`);
57
+ execa(command, ['--remote', `ssh-remote+${host}`, worktreePath], { detached: true, stdio: 'ignore' }).unref();
58
+ }
59
+ else if (TERMINAL_IDES.has(ideName)) {
60
+ // Terminal editors: ssh -t host 'vim /path'
61
+ console.log(`Opening ${worktreePath} on ${host} with ${command}...`);
62
+ await execa('ssh', ['-t', host, command, worktreePath], { stdio: 'inherit' });
63
+ }
64
+ else {
65
+ console.error(`Error: "${ideName}" does not support remote editing. Use code, cursor, or a terminal editor (vim, nvim).`);
66
+ process.exit(1);
67
+ }
49
68
  }
50
69
  else {
51
- execa(command, [worktreePath], { detached: true, stdio: 'ignore' }).unref();
70
+ console.log(`Opening ${worktreePath} with ${command}...`);
71
+ if (TERMINAL_IDES.has(ideName)) {
72
+ await execa(command, [worktreePath], { stdio: 'inherit' });
73
+ }
74
+ else {
75
+ execa(command, [worktreePath], { detached: true, stdio: 'ignore' }).unref();
76
+ }
52
77
  }
53
78
  }
@@ -1,36 +1,39 @@
1
1
  import path from 'node:path';
2
- import fs from 'node:fs';
3
2
  import { loadConfig } from '../config.js';
4
- import { execa } from 'execa';
5
- async function getCurrentBranch(worktreePath) {
6
- try {
7
- const { stdout } = await execa('git', ['-C', worktreePath, 'branch', '--show-current'], { stdio: 'pipe' });
8
- return stdout.trim() || 'detached HEAD';
9
- }
10
- catch {
11
- return 'unknown';
12
- }
13
- }
3
+ import { createRunner } from '../runner.js';
14
4
  export async function listCommand() {
15
5
  const config = loadConfig();
16
- if (!fs.existsSync(config.workspace)) {
6
+ const r = createRunner(config.machine);
7
+ if (!(await r.exists(config.workspace))) {
17
8
  console.log('No workspaces found (workspace directory does not exist).');
18
9
  return;
19
10
  }
20
- const entries = fs.readdirSync(config.workspace, { withFileTypes: true });
21
- const workspaces = entries.filter(e => e.isDirectory());
11
+ const entries = await r.readdir(config.workspace);
12
+ const workspaces = [];
13
+ for (const entry of entries) {
14
+ if (await r.isDirectory(path.join(config.workspace, entry))) {
15
+ workspaces.push(entry);
16
+ }
17
+ }
22
18
  if (workspaces.length === 0) {
23
19
  console.log('No workspaces found.');
24
20
  return;
25
21
  }
26
22
  for (const ws of workspaces) {
27
- console.log(`${ws.name}`);
28
- const wsPath = path.join(config.workspace, ws.name);
29
- const projects = fs.readdirSync(wsPath, { withFileTypes: true }).filter(e => e.isDirectory());
30
- for (const proj of projects) {
31
- const projPath = path.join(wsPath, proj.name);
32
- const branch = await getCurrentBranch(projPath);
33
- console.log(` └─ ${proj.name} (${branch})`);
23
+ console.log(`${ws}`);
24
+ const wsPath = path.join(config.workspace, ws);
25
+ const children = await r.readdir(wsPath);
26
+ for (const proj of children) {
27
+ const projPath = path.join(wsPath, proj);
28
+ if (!(await r.isDirectory(projPath)))
29
+ continue;
30
+ let branch = 'unknown';
31
+ try {
32
+ const { stdout } = await r.exec(['git', '-C', projPath, 'branch', '--show-current']);
33
+ branch = stdout.trim() || 'detached HEAD';
34
+ }
35
+ catch { /* ignore */ }
36
+ console.log(` └─ ${proj} (${branch})`);
34
37
  }
35
38
  }
36
39
  }
@@ -1,24 +1,25 @@
1
1
  import path from 'node:path';
2
- import fs from 'node:fs';
3
2
  import { loadConfig, findProject } from '../config.js';
3
+ import { createRunner } from '../runner.js';
4
4
  import { addWorktree, isGitRepo } from '../git.js';
5
5
  export async function newCommand(workspaceName, projectEntries, newBranch) {
6
6
  const config = loadConfig();
7
+ const r = createRunner(config.machine);
7
8
  const targetDir = path.join(config.workspace, workspaceName);
8
9
  const branchName = newBranch ?? workspaceName;
9
- if (fs.existsSync(targetDir)) {
10
+ if (await r.exists(targetDir)) {
10
11
  throw new Error(`Workspace directory already exists: ${targetDir}`);
11
12
  }
12
- await fs.promises.mkdir(targetDir, { recursive: true });
13
+ await r.mkdir(targetDir);
13
14
  console.log(`Created workspace directory: ${targetDir}`);
14
15
  for (const entry of projectEntries) {
15
16
  const project = findProject(config, entry.name);
16
- if (!(await isGitRepo(project.path))) {
17
+ if (!(await isGitRepo(r, project.path))) {
17
18
  throw new Error(`Not a git repository: ${project.path}`);
18
19
  }
19
20
  const worktreePath = path.join(targetDir, project.name);
20
21
  console.log(`Adding worktree for "${project.name}" (new branch: ${branchName}, base: ${entry.branch}) -> ${worktreePath}`);
21
- await addWorktree(project.path, worktreePath, branchName, entry.branch);
22
+ await addWorktree(r, project.path, worktreePath, branchName, entry.branch);
22
23
  }
23
24
  console.log(`\nWorkspace "${workspaceName}" created successfully at ${targetDir}`);
24
25
  }
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
- import fs from 'node:fs';
3
2
  import readline from 'node:readline';
4
3
  import { loadConfig, findProject } from '../config.js';
4
+ import { createRunner } from '../runner.js';
5
5
  import { removeWorktree, isBranchSyncedToRemote } from '../git.js';
6
6
  function confirm(question) {
7
7
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -12,8 +12,8 @@ function confirm(question) {
12
12
  });
13
13
  });
14
14
  }
15
- async function checkAndRemove(repoPath, worktreePath, projectName) {
16
- const synced = await isBranchSyncedToRemote(worktreePath);
15
+ async function checkAndRemove(r, repoPath, worktreePath, projectName) {
16
+ const synced = await isBranchSyncedToRemote(r, worktreePath);
17
17
  if (!synced) {
18
18
  console.warn(` Warning: "${projectName}" has unpushed commits or no remote tracking branch.`);
19
19
  const ok = await confirm(` Remove worktree "${projectName}" anyway?`);
@@ -23,25 +23,26 @@ async function checkAndRemove(repoPath, worktreePath, projectName) {
23
23
  }
24
24
  }
25
25
  console.log(`Removing worktree "${projectName}" from ${repoPath}`);
26
- await removeWorktree(repoPath, worktreePath);
26
+ await removeWorktree(r, repoPath, worktreePath);
27
27
  }
28
28
  export async function removeCommand(workspaceName, projectEntries) {
29
29
  const config = loadConfig();
30
+ const r = createRunner(config.machine);
30
31
  const targetDir = path.join(config.workspace, workspaceName);
31
- if (!fs.existsSync(targetDir)) {
32
+ if (!(await r.exists(targetDir))) {
32
33
  throw new Error(`Workspace directory does not exist: ${targetDir}`);
33
34
  }
34
35
  if (projectEntries.length === 0) {
35
- const entries = fs.readdirSync(targetDir);
36
+ const entries = await r.readdir(targetDir);
36
37
  for (const entry of entries) {
37
38
  const worktreePath = path.join(targetDir, entry);
38
39
  const project = config.projects.find(p => p.name === entry);
39
- if (project && fs.statSync(worktreePath).isDirectory()) {
40
- await checkAndRemove(project.path, worktreePath, entry);
40
+ if (project && await r.isDirectory(worktreePath)) {
41
+ await checkAndRemove(r, project.path, worktreePath, entry);
41
42
  }
42
43
  }
43
- if (fs.readdirSync(targetDir).length === 0) {
44
- fs.rmSync(targetDir, { recursive: true, force: true });
44
+ if ((await r.readdir(targetDir)).length === 0) {
45
+ await r.rm(targetDir);
45
46
  console.log(`Workspace "${workspaceName}" removed.`);
46
47
  }
47
48
  else {
@@ -52,14 +53,14 @@ export async function removeCommand(workspaceName, projectEntries) {
52
53
  for (const entry of projectEntries) {
53
54
  const project = findProject(config, entry.name);
54
55
  const worktreePath = path.join(targetDir, project.name);
55
- if (!fs.existsSync(worktreePath)) {
56
+ if (!(await r.exists(worktreePath))) {
56
57
  console.warn(`Worktree not found, skipping: ${worktreePath}`);
57
58
  continue;
58
59
  }
59
- await checkAndRemove(project.path, worktreePath, project.name);
60
+ await checkAndRemove(r, project.path, worktreePath, project.name);
60
61
  }
61
- if (fs.readdirSync(targetDir).length === 0) {
62
- fs.rmdirSync(targetDir);
62
+ if ((await r.readdir(targetDir)).length === 0) {
63
+ await r.rmdir(targetDir);
63
64
  console.log(`Workspace directory is now empty and has been removed.`);
64
65
  }
65
66
  }
package/dist/config.d.ts CHANGED
@@ -22,6 +22,7 @@ declare const ConfigSchema: z.ZodObject<{
22
22
  path: string;
23
23
  }>, "many">;
24
24
  ide: z.ZodOptional<z.ZodString>;
25
+ machine: z.ZodOptional<z.ZodString>;
25
26
  }, "strip", z.ZodTypeAny, {
26
27
  workspace: string;
27
28
  projects: {
@@ -29,6 +30,7 @@ declare const ConfigSchema: z.ZodObject<{
29
30
  path: string;
30
31
  }[];
31
32
  ide?: string | undefined;
33
+ machine?: string | undefined;
32
34
  }, {
33
35
  workspace: string;
34
36
  projects: {
@@ -36,6 +38,7 @@ declare const ConfigSchema: z.ZodObject<{
36
38
  path: string;
37
39
  }[];
38
40
  ide?: string | undefined;
41
+ machine?: string | undefined;
39
42
  }>;
40
43
  export type Project = z.infer<typeof ProjectSchema>;
41
44
  export type Config = z.infer<typeof ConfigSchema>;
package/dist/config.js CHANGED
@@ -11,6 +11,7 @@ const ConfigSchema = z.object({
11
11
  workspace: z.string(),
12
12
  projects: z.array(ProjectSchema),
13
13
  ide: z.string().optional(),
14
+ machine: z.string().optional(),
14
15
  });
15
16
  function expandEnvVars(value) {
16
17
  return value.replace(/\$([A-Z_][A-Z0-9_]*)/g, (_, name) => {
@@ -26,6 +27,8 @@ function expandConfig(config) {
26
27
  name: p.name,
27
28
  path: expandEnvVars(p.path),
28
29
  })),
30
+ ide: config.ide,
31
+ machine: config.machine,
29
32
  };
30
33
  }
31
34
  export function loadConfig() {
package/dist/git.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- export declare function addWorktree(repoPath: string, worktreePath: string, newBranch: string, baseBranch: string): Promise<void>;
2
- export declare function isBranchSyncedToRemote(worktreePath: string): Promise<boolean>;
3
- export declare function removeWorktree(repoPath: string, worktreePath: string): Promise<void>;
4
- export declare function isGitRepo(dirPath: string): Promise<boolean>;
1
+ import type { Runner } from './runner.js';
2
+ export declare function addWorktree(r: Runner, repoPath: string, worktreePath: string, newBranch: string, baseBranch: string): Promise<void>;
3
+ export declare function isBranchSyncedToRemote(r: Runner, worktreePath: string): Promise<boolean>;
4
+ export declare function removeWorktree(r: Runner, repoPath: string, worktreePath: string): Promise<void>;
5
+ export declare function isGitRepo(r: Runner, dirPath: string): Promise<boolean>;
package/dist/git.js CHANGED
@@ -1,23 +1,11 @@
1
- import { execa } from 'execa';
2
- import fs from 'node:fs';
3
1
  import path from 'node:path';
4
- async function run(args) {
5
- console.log(` $ ${args.join(' ')}`);
6
- const result = await execa(args[0], args.slice(1), { all: true }).catch(err => {
7
- if (err.all)
8
- process.stderr.write(err.all + '\n');
9
- throw err;
10
- });
11
- if (result.all)
12
- process.stdout.write(result.all + '\n');
13
- }
14
- async function localBranchExists(repoPath, branch) {
15
- const { stdout } = await execa('git', ['-C', repoPath, 'branch', '--list', branch], { stdio: 'pipe' });
2
+ async function localBranchExists(r, repoPath, branch) {
3
+ const { stdout } = await r.exec(['git', '-C', repoPath, 'branch', '--list', branch]);
16
4
  return stdout.trim().length > 0;
17
5
  }
18
- async function remoteBranchExists(repoPath, branch) {
6
+ async function remoteBranchExists(r, repoPath, branch) {
19
7
  try {
20
- const { stdout } = await execa('git', ['-C', repoPath, 'ls-remote', '--heads', 'origin', branch], { stdio: 'pipe' });
8
+ const { stdout } = await r.exec(['git', '-C', repoPath, 'ls-remote', '--heads', 'origin', branch]);
21
9
  if (stdout.trim().length === 0)
22
10
  return null;
23
11
  return `origin/${branch}`;
@@ -26,37 +14,57 @@ async function remoteBranchExists(repoPath, branch) {
26
14
  return null;
27
15
  }
28
16
  }
29
- export async function addWorktree(repoPath, worktreePath, newBranch, baseBranch) {
30
- await fs.promises.mkdir(path.dirname(worktreePath), { recursive: true });
31
- if (await localBranchExists(repoPath, newBranch)) {
32
- console.log(` Branch "${newBranch}" exists locally, using it directly.`);
33
- await run(['git', '-C', repoPath, 'worktree', 'add', worktreePath, newBranch]);
17
+ async function syncLocalBranch(r, repoPath, branch) {
18
+ await r.run(['git', '-C', repoPath, 'fetch', 'origin', branch]);
19
+ const { stdout: remoteRef } = await r.exec(['git', '-C', repoPath, 'rev-parse', '--verify', `origin/${branch}`]).catch(() => ({ stdout: '' }));
20
+ if (!remoteRef.trim()) {
21
+ console.log(` No remote tracking branch for "${branch}", skipping sync.`);
22
+ return;
23
+ }
24
+ const { stdout: headRef } = await r.exec(['git', '-C', repoPath, 'symbolic-ref', 'HEAD']).catch(() => ({ stdout: '' }));
25
+ if (headRef.trim() === `refs/heads/${branch}`) {
26
+ console.log(` Branch "${branch}" is currently checked out, skipping sync.`);
27
+ return;
28
+ }
29
+ const { stdout: aheadCount } = await r.exec(['git', '-C', repoPath, 'rev-list', '--count', `origin/${branch}..${branch}`]);
30
+ if (parseInt(aheadCount.trim(), 10) > 0) {
31
+ console.warn(` Warning: "${branch}" has local commits not in remote, skipping sync.`);
32
+ return;
33
+ }
34
+ await r.run(['git', '-C', repoPath, 'branch', '-f', branch, `origin/${branch}`]);
35
+ }
36
+ export async function addWorktree(r, repoPath, worktreePath, newBranch, baseBranch) {
37
+ await r.mkdir(path.dirname(worktreePath));
38
+ if (await localBranchExists(r, repoPath, newBranch)) {
39
+ console.log(` Branch "${newBranch}" exists locally, syncing with remote...`);
40
+ await syncLocalBranch(r, repoPath, newBranch);
41
+ await r.run(['git', '-C', repoPath, 'worktree', 'add', worktreePath, newBranch]);
34
42
  return;
35
43
  }
36
- const remoteBranch = await remoteBranchExists(repoPath, newBranch);
44
+ const remoteBranch = await remoteBranchExists(r, repoPath, newBranch);
37
45
  if (remoteBranch) {
38
46
  console.log(` Branch "${newBranch}" found on remote (${remoteBranch}), creating tracking branch.`);
39
- await run(['git', '-C', repoPath, 'worktree', 'add', '--track', '-b', newBranch, worktreePath, remoteBranch]);
47
+ await r.run(['git', '-C', repoPath, 'worktree', 'add', '--track', '-b', newBranch, worktreePath, remoteBranch]);
40
48
  return;
41
49
  }
42
- await run(['git', '-C', repoPath, 'worktree', 'add', '-b', newBranch, worktreePath, baseBranch]);
50
+ await r.run(['git', '-C', repoPath, 'worktree', 'add', '-b', newBranch, worktreePath, baseBranch]);
43
51
  }
44
- export async function isBranchSyncedToRemote(worktreePath) {
52
+ export async function isBranchSyncedToRemote(r, worktreePath) {
45
53
  try {
46
- await execa('git', ['-C', worktreePath, 'rev-parse', '--abbrev-ref', '@{u}'], { stdio: 'pipe' });
54
+ await r.exec(['git', '-C', worktreePath, 'rev-parse', '--abbrev-ref', '@{u}']);
47
55
  }
48
56
  catch {
49
57
  return false;
50
58
  }
51
- const { stdout } = await execa('git', ['-C', worktreePath, 'rev-list', '--count', '@{u}..HEAD'], { stdio: 'pipe' });
59
+ const { stdout } = await r.exec(['git', '-C', worktreePath, 'rev-list', '--count', '@{u}..HEAD']);
52
60
  return parseInt(stdout.trim(), 10) === 0;
53
61
  }
54
- export async function removeWorktree(repoPath, worktreePath) {
55
- await run(['git', '-C', repoPath, 'worktree', 'remove', worktreePath, '--force']);
62
+ export async function removeWorktree(r, repoPath, worktreePath) {
63
+ await r.run(['git', '-C', repoPath, 'worktree', 'remove', worktreePath, '--force']);
56
64
  }
57
- export async function isGitRepo(dirPath) {
65
+ export async function isGitRepo(r, dirPath) {
58
66
  try {
59
- await execa('git', ['-C', dirPath, 'rev-parse', '--git-dir'], { stdio: 'pipe' });
67
+ await r.exec(['git', '-C', dirPath, 'rev-parse', '--git-dir']);
60
68
  return true;
61
69
  }
62
70
  catch {
@@ -0,0 +1,14 @@
1
+ export interface Runner {
2
+ exec(args: string[]): Promise<{
3
+ stdout: string;
4
+ }>;
5
+ run(args: string[]): Promise<void>;
6
+ exists(remotePath: string): Promise<boolean>;
7
+ isDirectory(remotePath: string): Promise<boolean>;
8
+ readdir(remotePath: string): Promise<string[]>;
9
+ mkdir(remotePath: string): Promise<void>;
10
+ rm(remotePath: string): Promise<void>;
11
+ rmdir(remotePath: string): Promise<void>;
12
+ isRemote: boolean;
13
+ }
14
+ export declare function createRunner(machine?: string): Runner;
package/dist/runner.js ADDED
@@ -0,0 +1,111 @@
1
+ import { execa } from 'execa';
2
+ import fs from 'node:fs';
3
+ function parseSshHost(machine) {
4
+ // "ssh://bytedance" → "bytedance"
5
+ return machine.replace(/^ssh:\/\//, '');
6
+ }
7
+ function shellEscape(arg) {
8
+ return `'${arg.replace(/'/g, "'\\''")}'`;
9
+ }
10
+ export function createRunner(machine) {
11
+ if (!machine) {
12
+ return createLocalRunner();
13
+ }
14
+ return createSshRunner(parseSshHost(machine));
15
+ }
16
+ function createLocalRunner() {
17
+ return {
18
+ isRemote: false,
19
+ async exec(args) {
20
+ const { stdout } = await execa(args[0], args.slice(1), { stdio: 'pipe' });
21
+ return { stdout };
22
+ },
23
+ async run(args) {
24
+ console.log(` $ ${args.join(' ')}`);
25
+ const result = await execa(args[0], args.slice(1), { all: true }).catch(err => {
26
+ if (err.all)
27
+ process.stderr.write(err.all + '\n');
28
+ throw err;
29
+ });
30
+ if (result.all)
31
+ process.stdout.write(result.all + '\n');
32
+ },
33
+ async exists(p) {
34
+ return fs.existsSync(p);
35
+ },
36
+ async isDirectory(p) {
37
+ try {
38
+ return fs.statSync(p).isDirectory();
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ },
44
+ async readdir(p) {
45
+ return fs.readdirSync(p);
46
+ },
47
+ async mkdir(p) {
48
+ await fs.promises.mkdir(p, { recursive: true });
49
+ },
50
+ async rm(p) {
51
+ fs.rmSync(p, { recursive: true, force: true });
52
+ },
53
+ async rmdir(p) {
54
+ fs.rmdirSync(p);
55
+ },
56
+ };
57
+ }
58
+ function createSshRunner(host) {
59
+ function sshArgs(args) {
60
+ const remoteCmd = args.map(shellEscape).join(' ');
61
+ return [host, remoteCmd];
62
+ }
63
+ return {
64
+ isRemote: true,
65
+ async exec(args) {
66
+ const { stdout } = await execa('ssh', sshArgs(args), { stdio: 'pipe' });
67
+ return { stdout };
68
+ },
69
+ async run(args) {
70
+ console.log(` $ [${host}] ${args.join(' ')}`);
71
+ const result = await execa('ssh', sshArgs(args), { all: true }).catch(err => {
72
+ if (err.all)
73
+ process.stderr.write(err.all + '\n');
74
+ throw err;
75
+ });
76
+ if (result.all)
77
+ process.stdout.write(result.all + '\n');
78
+ },
79
+ async exists(p) {
80
+ try {
81
+ await execa('ssh', [host, `test -e ${shellEscape(p)}`], { stdio: 'pipe' });
82
+ return true;
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ },
88
+ async isDirectory(p) {
89
+ try {
90
+ await execa('ssh', [host, `test -d ${shellEscape(p)}`], { stdio: 'pipe' });
91
+ return true;
92
+ }
93
+ catch {
94
+ return false;
95
+ }
96
+ },
97
+ async readdir(p) {
98
+ const { stdout } = await execa('ssh', [host, `ls ${shellEscape(p)}`], { stdio: 'pipe' });
99
+ return stdout.trim().split('\n').filter(l => l.length > 0);
100
+ },
101
+ async mkdir(p) {
102
+ await execa('ssh', [host, `mkdir -p ${shellEscape(p)}`], { stdio: 'pipe' });
103
+ },
104
+ async rm(p) {
105
+ await execa('ssh', [host, `rm -rf ${shellEscape(p)}`], { stdio: 'pipe' });
106
+ },
107
+ async rmdir(p) {
108
+ await execa('ssh', [host, `rmdir ${shellEscape(p)}`], { stdio: 'pipe' });
109
+ },
110
+ };
111
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lakakala/kgit",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Git worktree workspace manager",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1 +0,0 @@
1
- export declare function cdCommand(workspace: string, project?: string): void;
@@ -1,14 +0,0 @@
1
- import path from 'node:path';
2
- import fs from 'node:fs';
3
- import { loadConfig } from '../config.js';
4
- export function cdCommand(workspace, project) {
5
- const config = loadConfig();
6
- const targetPath = project
7
- ? path.join(config.workspace, workspace, project)
8
- : path.join(config.workspace, workspace);
9
- if (!fs.existsSync(targetPath)) {
10
- process.stderr.write(`Error: path does not exist: ${targetPath}\n`);
11
- process.exit(1);
12
- }
13
- process.stdout.write(targetPath);
14
- }