@dezkareid/osddt 1.11.2 → 1.11.4

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,3 +1,9 @@
1
1
  import { Command } from 'commander';
2
+ import { type AgentType } from '../utils/prompt.js';
2
3
  export declare function resolveNpxCommand(cwd: string): Promise<string>;
4
+ export declare function cloneBareRepository(cwd: string, repositoryUrl: string): string;
5
+ export declare function detectDefaultBranch(barePath: string): string;
6
+ export declare function addDefaultBranchWorktree(barePath: string, branch: string): void;
7
+ export declare function detectPackageManager(worktreePath: string): Promise<string>;
8
+ export declare function writeAgentPointerFiles(cwd: string, agents: AgentType[], branch: string): Promise<void>;
3
9
  export declare function setupCommand(): Command;
package/dist/index.js CHANGED
@@ -716,39 +716,8 @@ function checkGitVersion() {
716
716
  return { label: 'Git version >= 2.5', passed: false, detail: 'git not found or not executable' };
717
717
  }
718
718
  }
719
- function checkNotAWorktree(cwd) {
720
- try {
721
- const gitCommonDir = execSync('git rev-parse --git-common-dir', { cwd, encoding: 'utf-8' }).trim();
722
- const gitDir = execSync('git rev-parse --git-dir', { cwd, encoding: 'utf-8' }).trim();
723
- const isWorktree = gitDir !== gitCommonDir && gitDir !== '.git';
724
- return {
725
- label: 'Current directory is not a worktree',
726
- passed: !isWorktree,
727
- detail: isWorktree
728
- ? `This directory is itself a worktree (git-dir: ${gitDir}). Run setup from the main repository.`
729
- : 'OK',
730
- };
731
- }
732
- catch {
733
- return { label: 'Current directory is not a worktree', passed: false, detail: 'Not inside a git repository' };
734
- }
735
- }
736
- async function checkTargetWritable(cwd) {
737
- let targetBase;
738
- try {
739
- const repoRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' }).trim();
740
- const rcPath = path.join(repoRoot, '.osddtrc');
741
- if (await fs.pathExists(rcPath)) {
742
- const rc = await fs.readJson(rcPath);
743
- targetBase = rc.worktreeBase ?? path.dirname(repoRoot);
744
- }
745
- else {
746
- targetBase = path.dirname(repoRoot);
747
- }
748
- }
749
- catch {
750
- return { label: 'Worktree target directory is writable', passed: false, detail: 'Could not resolve repo root' };
751
- }
719
+ async function checkTargetWritable(barePath) {
720
+ const targetBase = path.dirname(barePath);
752
721
  try {
753
722
  await fs.access(targetBase, fs.constants.W_OK);
754
723
  return { label: 'Worktree target directory is writable', passed: true, detail: `${targetBase} is writable` };
@@ -757,21 +726,31 @@ async function checkTargetWritable(cwd) {
757
726
  return { label: 'Worktree target directory is writable', passed: false, detail: `${targetBase} is not writable` };
758
727
  }
759
728
  }
760
- async function initStateFile(cwd) {
761
- try {
762
- const repoRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' }).trim();
763
- const stateFile = path.join(path.dirname(repoRoot), '.osddt-worktrees');
764
- if (!(await fs.pathExists(stateFile))) {
765
- await fs.writeJson(stateFile, [], { spaces: 2 });
766
- console.log(` ✓ Initialized worktree state file: ${stateFile}`);
767
- }
768
- else {
769
- console.log(` ✓ Worktree state file already exists: ${stateFile}`);
770
- }
729
+ async function resolveBarePath(cwd) {
730
+ const rcPath = path.join(cwd, '.osddtrc');
731
+ if (await fs.pathExists(rcPath)) {
732
+ const rc = await fs.readJson(rcPath);
733
+ if (rc['bare-path'])
734
+ return rc['bare-path'];
771
735
  }
772
- catch {
773
- console.log(' ✗ Could not initialize worktree state file');
736
+ return execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' }).trim();
737
+ }
738
+ function findWorktreeByFeature(barePath, featureName) {
739
+ const output = execSync('git worktree list --porcelain', { cwd: barePath, encoding: 'utf-8' });
740
+ const blocks = output.trim().split(/\n\n+/);
741
+ for (const block of blocks) {
742
+ const match = block.match(/^worktree (.+)$/m);
743
+ if (!match)
744
+ continue;
745
+ const worktreePath = match[1].trim();
746
+ const basename = path.basename(worktreePath);
747
+ const segments = basename.split('-');
748
+ const suffixMatch = segments.length > 1 && basename.slice(basename.indexOf('-') + 1) === featureName;
749
+ if (basename === featureName || suffixMatch) {
750
+ return worktreePath;
751
+ }
774
752
  }
753
+ return undefined;
775
754
  }
776
755
  function printCheckResult(result) {
777
756
  const icon = result.passed ? '✓' : '✗';
@@ -780,11 +759,10 @@ function printCheckResult(result) {
780
759
  console.log(` → ${result.detail}`);
781
760
  }
782
761
  }
783
- async function runWorktreeChecks(cwd) {
762
+ async function runWorktreeChecks(barePath) {
784
763
  const results = [
785
764
  checkGitVersion(),
786
- checkNotAWorktree(cwd),
787
- await checkTargetWritable(cwd),
765
+ await checkTargetWritable(barePath),
788
766
  ];
789
767
  for (const result of results) {
790
768
  printCheckResult(result);
@@ -857,34 +835,68 @@ async function writeAgentFiles(cwd, agents, npxCommand) {
857
835
  console.log('');
858
836
  }
859
837
  }
860
- function isGitRepository(cwd) {
838
+ function cloneBareRepository(cwd, repositoryUrl) {
839
+ const barePath = path.join(cwd, '.bare');
840
+ console.log(`Cloning bare repository into ${barePath} ...\n`);
841
+ execSync(`git clone --bare "${repositoryUrl}" "${barePath}"`, { stdio: 'inherit' });
842
+ console.log('');
843
+ return barePath;
844
+ }
845
+ function detectDefaultBranch(barePath) {
846
+ const output = execSync('git remote show origin', { cwd: barePath, encoding: 'utf-8' });
847
+ const match = output.match(/HEAD branch:\s*(\S+)/);
848
+ if (match)
849
+ return match[1];
850
+ // fallback: try main then master
861
851
  try {
862
- execSync('git rev-parse --git-dir', { cwd, stdio: 'ignore' });
863
- return true;
852
+ execSync('git rev-parse --verify main', { cwd: barePath, stdio: 'ignore' });
853
+ return 'main';
864
854
  }
865
855
  catch {
866
- return false;
856
+ return 'master';
867
857
  }
868
858
  }
869
- function cloneBareRepository(cwd, repositoryUrl) {
870
- console.log(`Cloning repository as bare into ${cwd}/.git ...\n`);
871
- execSync(`git clone --bare "${repositoryUrl}" .git`, { cwd, stdio: 'inherit' });
872
- execSync('git config --local core.bare false', { cwd, stdio: 'inherit' });
873
- console.log('');
859
+ function addDefaultBranchWorktree(barePath, branch) {
860
+ const worktreePath = path.join(barePath, branch);
861
+ execSync(`git worktree add "${worktreePath}" ${branch}`, { cwd: barePath, stdio: 'inherit' });
874
862
  }
875
- async function setupWorktreeEnvironment(cwd, worktreeRepository) {
876
- if (!isGitRepository(cwd)) {
877
- cloneBareRepository(cwd, worktreeRepository);
863
+ async function detectPackageManager(worktreePath) {
864
+ if (await fs.pathExists(path.join(worktreePath, 'pnpm-lock.yaml')))
865
+ return 'pnpm';
866
+ if (await fs.pathExists(path.join(worktreePath, 'yarn.lock')))
867
+ return 'yarn';
868
+ if (await fs.pathExists(path.join(worktreePath, 'package-lock.json')))
869
+ return 'npm';
870
+ return 'npm';
871
+ }
872
+ async function writeAgentPointerFiles(cwd, agents, branch) {
873
+ if (agents.includes('claude')) {
874
+ const filePath = path.join(cwd, 'CLAUDE.md');
875
+ await fs.writeFile(filePath, `@.bare/${branch}/CLAUDE.md\n`, 'utf-8');
876
+ console.log(` Created: ${filePath}`);
877
+ }
878
+ if (agents.includes('gemini')) {
879
+ const filePath = path.join(cwd, 'GEMINI.md');
880
+ await fs.writeFile(filePath, `@.bare/${branch}/GEMINI.md\n`, 'utf-8');
881
+ console.log(` Created: ${filePath}`);
878
882
  }
883
+ }
884
+ async function setupWorktreeEnvironment(cwd, worktreeRepository, agents) {
885
+ const barePath = cloneBareRepository(cwd, worktreeRepository);
886
+ const branch = detectDefaultBranch(barePath);
887
+ addDefaultBranchWorktree(barePath, branch);
888
+ const worktreePath = path.join(barePath, branch);
889
+ const packageManager = await detectPackageManager(worktreePath);
879
890
  console.log('Checking environment for git worktree support...\n');
880
- const allPassed = await runWorktreeChecks(cwd);
891
+ const allPassed = await runWorktreeChecks(barePath);
881
892
  console.log('');
882
893
  if (!allPassed) {
883
894
  console.log('Some checks failed. Resolve the issues above before using the worktree workflow.');
884
895
  process.exit(1);
885
896
  }
886
- await initStateFile(cwd);
897
+ await writeAgentPointerFiles(cwd, agents, branch);
887
898
  console.log('');
899
+ return { barePath, packageManager };
888
900
  }
889
901
  async function runSetup(cwd, rawAgents, rawRepoType, rawWorktreeRepository) {
890
902
  const agents = rawAgents !== undefined ? parseAgents(rawAgents) : await askAgents();
@@ -896,8 +908,10 @@ async function runSetup(cwd, rawAgents, rawRepoType, rawWorktreeRepository) {
896
908
  const worktreeRepository = rawWorktreeRepository !== undefined ? rawWorktreeRepository : (await askWorktreeUrl()) || undefined;
897
909
  if (rawWorktreeRepository === undefined)
898
910
  console.log('');
911
+ let barePath;
912
+ let packageManager;
899
913
  if (worktreeRepository) {
900
- await setupWorktreeEnvironment(cwd, worktreeRepository);
914
+ ({ barePath, packageManager } = await setupWorktreeEnvironment(cwd, worktreeRepository, agents));
901
915
  }
902
916
  const npxCommand = await resolveNpxCommand(cwd);
903
917
  console.log('Setting up OSDDT command files...\n');
@@ -905,6 +919,10 @@ async function runSetup(cwd, rawAgents, rawRepoType, rawWorktreeRepository) {
905
919
  const config = { repoType, agents };
906
920
  if (worktreeRepository)
907
921
  config['worktree-repository'] = worktreeRepository;
922
+ if (barePath)
923
+ config['bare-path'] = barePath;
924
+ if (packageManager)
925
+ config['packageManager'] = packageManager;
908
926
  await writeConfig(cwd, config);
909
927
  console.log('\nSetup complete!');
910
928
  console.log('Commands created: osddt.spec, osddt.plan, osddt.tasks, osddt.implement');
@@ -956,12 +974,6 @@ function todayPrefix() {
956
974
  const dd = String(now.getDate()).padStart(2, '0');
957
975
  return `${yyyy}-${mm}-${dd}`;
958
976
  }
959
- function resolveRepoRoot$2(cwd) {
960
- return execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' }).trim();
961
- }
962
- function stateFilePath$2(repoRoot) {
963
- return path.join(path.dirname(repoRoot), '.osddt-worktrees');
964
- }
965
977
  async function runDone(featureName, cwd, worktree) {
966
978
  const src = path.join(cwd, 'working-on', featureName);
967
979
  const destName = `${todayPrefix()}-${featureName}`;
@@ -975,28 +987,19 @@ async function runDone(featureName, cwd, worktree) {
975
987
  console.log(`Moved: working-on/${featureName} → done/${destName}`);
976
988
  if (!worktree)
977
989
  return;
978
- const repoRoot = resolveRepoRoot$2(process.cwd());
979
- const stateFile = stateFilePath$2(repoRoot);
980
- if (!(await fs.pathExists(stateFile))) {
981
- console.error(`Warning: .osddt-worktrees not found at ${stateFile}. Skipping worktree cleanup.`);
982
- return;
983
- }
984
- const entries = await fs.readJson(stateFile);
985
- const entry = entries.find(e => e.featureName === featureName);
986
- if (!entry) {
987
- console.error(`Warning: No worktree entry found for "${featureName}". Skipping worktree cleanup.`);
990
+ const barePath = await resolveBarePath(process.cwd());
991
+ const worktreePath = findWorktreeByFeature(barePath, featureName);
992
+ if (!worktreePath) {
993
+ console.error(`Warning: No worktree found for "${featureName}". Skipping worktree cleanup.`);
988
994
  return;
989
995
  }
990
- if (await fs.pathExists(entry.worktreePath)) {
991
- execSync(`git worktree remove "${entry.worktreePath}" --force`, { cwd: repoRoot, stdio: 'inherit' });
992
- console.log(`Removed worktree: ${entry.worktreePath}`);
996
+ if (await fs.pathExists(worktreePath)) {
997
+ execSync(`git worktree remove "${worktreePath}" --force`, { cwd: barePath, stdio: 'inherit' });
998
+ console.log(`Removed worktree: ${worktreePath}`);
993
999
  }
994
1000
  else {
995
1001
  console.log(`Worktree path not found on filesystem, skipping git worktree remove.`);
996
1002
  }
997
- const updated = entries.filter(e => e.featureName !== featureName);
998
- await fs.writeJson(stateFile, updated, { spaces: 2 });
999
- console.log(`Removed state entry for "${featureName}" from .osddt-worktrees`);
1000
1003
  }
1001
1004
  function doneCommand() {
1002
1005
  const cmd = new Command('done');
@@ -1004,7 +1007,7 @@ function doneCommand() {
1004
1007
  .description('Move a feature from working-on/<feature-name> to done/<feature-name>')
1005
1008
  .argument('<feature-name>', 'name of the feature to mark as done')
1006
1009
  .option('-d, --dir <directory>', 'project directory', process.cwd())
1007
- .option('--worktree', 'also remove the git worktree and clean up the state file')
1010
+ .option('--worktree', 'also remove the git worktree')
1008
1011
  .action(async (featureName, options) => {
1009
1012
  const targetDir = path.resolve(options.dir);
1010
1013
  await runDone(featureName, targetDir, options.worktree ?? false);
@@ -1143,23 +1146,9 @@ function contextCommand() {
1143
1146
  return cmd;
1144
1147
  }
1145
1148
 
1146
- function resolveRepoRoot$1(cwd) {
1147
- return execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' }).trim();
1148
- }
1149
1149
  function repoName(repoRoot) {
1150
1150
  return path.basename(repoRoot);
1151
1151
  }
1152
- function stateFilePath$1(repoRoot) {
1153
- return path.join(path.dirname(repoRoot), '.osddt-worktrees');
1154
- }
1155
- async function readStateFile(stateFile) {
1156
- if (!(await fs.pathExists(stateFile)))
1157
- return [];
1158
- return fs.readJson(stateFile);
1159
- }
1160
- async function writeStateFile(stateFile, entries) {
1161
- await fs.writeJson(stateFile, entries, { spaces: 2 });
1162
- }
1163
1152
  function branchExists(branch, cwd) {
1164
1153
  try {
1165
1154
  execSync(`git rev-parse --verify ${branch}`, { cwd, stdio: 'ignore' });
@@ -1212,40 +1201,47 @@ async function createWorktree(branch, worktreePath, repoRoot) {
1212
1201
  execSync(`git worktree add "${worktreePath}" -b ${branch}`, { cwd: repoRoot, stdio: 'inherit' });
1213
1202
  }
1214
1203
  }
1204
+ function runInstall(worktreePath, packageManager) {
1205
+ if (!packageManager) {
1206
+ console.log(`\n ℹ No packageManager configured in .osddtrc — skipping install. Run it manually inside ${worktreePath}`);
1207
+ return;
1208
+ }
1209
+ console.log(`\n Running ${packageManager} install...`);
1210
+ execSync(`${packageManager} install`, { cwd: worktreePath, stdio: 'inherit' });
1211
+ }
1215
1212
  async function runStartWorktree(featureName, options) {
1216
1213
  const cwd = process.cwd();
1217
- const repoRoot = resolveRepoRoot$1(cwd);
1214
+ const repoRoot = await resolveBarePath(cwd);
1218
1215
  const branch = `feat/${featureName}`;
1219
1216
  // Read .osddtrc
1220
- const rcPath = path.join(repoRoot, '.osddtrc');
1217
+ const rcPath = path.join(cwd, '.osddtrc');
1221
1218
  let rc = { repoType: 'single' };
1222
1219
  if (await fs.pathExists(rcPath)) {
1223
1220
  rc = await fs.readJson(rcPath);
1224
1221
  }
1225
- // Resolve worktree path
1226
- const base = rc.worktreeBase ?? path.dirname(repoRoot);
1227
- const worktreePath = path.join(base, `${repoName(repoRoot)}-${featureName}`);
1228
- // Check state file for existing entry
1229
- const stateFile = stateFilePath$1(repoRoot);
1230
- const entries = await readStateFile(stateFile);
1231
- const existing = entries.find(e => e.featureName === featureName);
1232
- if (existing) {
1233
- console.log(`\nWorktree for "${featureName}" already exists at: ${existing.worktreePath}`);
1222
+ // Check for existing worktree via git worktree list
1223
+ const existingPath = findWorktreeByFeature(repoRoot, featureName);
1224
+ if (existingPath) {
1225
+ const workingDir = path.join(existingPath, 'working-on', featureName);
1226
+ console.log(`\nWorktree for "${featureName}" already exists at: ${existingPath}`);
1234
1227
  const answer = await prompt('Resume or Abort? [R/a] ');
1235
1228
  if (answer.toLowerCase() === 'a') {
1236
1229
  console.log('Aborted.');
1237
1230
  process.exit(0);
1238
1231
  }
1239
1232
  console.log(`\nResuming existing worktree.`);
1240
- console.log(` Branch: ${existing.branch}`);
1241
- console.log(` Worktree path: ${existing.worktreePath}`);
1242
- console.log(` Working dir: ${existing.workingDir}`);
1233
+ console.log(` Branch: ${branch}`);
1234
+ console.log(` Worktree path: ${existingPath}`);
1235
+ console.log(` Working dir: ${workingDir}`);
1243
1236
  return;
1244
1237
  }
1238
+ // Resolve worktree path — sibling of repoRoot (i.e. sibling of .bare)
1239
+ const base = rc['worktreeBase'] ?? path.dirname(repoRoot);
1240
+ const worktreePath = path.join(base, `${repoName(repoRoot)}-${featureName}`);
1245
1241
  await createWorktree(branch, worktreePath, repoRoot);
1246
1242
  // Resolve working dir
1247
1243
  let projectPath;
1248
- if (rc.repoType === 'monorepo') {
1244
+ if (rc['repoType'] === 'monorepo') {
1249
1245
  const pkg = options.dir ?? await prompt('Package path (e.g. packages/my-package): ');
1250
1246
  projectPath = path.join(worktreePath, pkg);
1251
1247
  }
@@ -1254,9 +1250,7 @@ async function runStartWorktree(featureName, options) {
1254
1250
  }
1255
1251
  const workingDir = path.join(projectPath, 'working-on', featureName);
1256
1252
  await fs.ensureDir(workingDir);
1257
- // Write state file entry
1258
- entries.push({ featureName, branch, worktreePath, workingDir, repoRoot });
1259
- await writeStateFile(stateFile, entries);
1253
+ runInstall(worktreePath, rc['packageManager']);
1260
1254
  console.log(`\nWorktree feature started:`);
1261
1255
  console.log(` Branch: ${branch}`);
1262
1256
  console.log(` Worktree path: ${worktreePath}`);
@@ -1274,14 +1268,20 @@ function startWorktreeCommand() {
1274
1268
  return cmd;
1275
1269
  }
1276
1270
 
1277
- function resolveRepoRoot(cwd) {
1271
+ async function resolveRepoRoot(cwd) {
1272
+ const rcPath = path.join(cwd, '.osddtrc');
1273
+ if (await fs.pathExists(rcPath)) {
1274
+ const rc = await fs.readJson(rcPath);
1275
+ if (rc['bare-path'])
1276
+ return rc['bare-path'];
1277
+ }
1278
1278
  return execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' }).trim();
1279
1279
  }
1280
1280
  function stateFilePath(repoRoot) {
1281
1281
  return path.join(path.dirname(repoRoot), '.osddt-worktrees');
1282
1282
  }
1283
1283
  async function runWorktreeInfo(featureName) {
1284
- const repoRoot = resolveRepoRoot(process.cwd());
1284
+ const repoRoot = await resolveRepoRoot(process.cwd());
1285
1285
  const stateFile = stateFilePath(repoRoot);
1286
1286
  if (!(await fs.pathExists(stateFile))) {
1287
1287
  console.error(`No .osddt-worktrees file found at: ${stateFile}`);
@@ -4,8 +4,8 @@ export interface CheckResult {
4
4
  detail: string;
5
5
  }
6
6
  export declare function checkGitVersion(): CheckResult;
7
- export declare function checkNotAWorktree(cwd: string): CheckResult;
8
- export declare function checkTargetWritable(cwd: string): Promise<CheckResult>;
9
- export declare function initStateFile(cwd: string): Promise<void>;
7
+ export declare function checkTargetWritable(barePath: string): Promise<CheckResult>;
8
+ export declare function resolveBarePath(cwd: string): Promise<string>;
9
+ export declare function findWorktreeByFeature(barePath: string, featureName: string): string | undefined;
10
10
  export declare function printCheckResult(result: CheckResult): void;
11
- export declare function runWorktreeChecks(cwd: string): Promise<boolean>;
11
+ export declare function runWorktreeChecks(barePath: string): Promise<boolean>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dezkareid/osddt",
3
- "version": "1.11.2",
3
+ "version": "1.11.4",
4
4
  "description": "Package for Spec-Driven Development workflow",
5
5
  "keywords": [
6
6
  "spec-driven",