wendkeep 0.76.9 → 0.78.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.
package/src/worktree.mjs CHANGED
@@ -10,6 +10,13 @@ import {
10
10
  import { dirname, join, relative, resolve } from 'node:path';
11
11
 
12
12
  import { resolveProjectVault } from './project-vault.mjs';
13
+ import {
14
+ cleanupMergedWorktrees,
15
+ diagnoseManagedWorktreeCleanups,
16
+ finishManagedWorktree,
17
+ pruneManagedWorktrees,
18
+ removeManagedWorktree,
19
+ } from './worktree-cleanup.mjs';
13
20
  import { getLocale } from '../packages/vault/src/locale.mjs';
14
21
  import {
15
22
  discoverWorktreeRepository,
@@ -150,12 +157,29 @@ function vscodeWorktreeTasks() {
150
157
  args: ['--no-install', 'wendkeep', 'worktree', 'open', '${input:wendkeepWorktreeSlug}'],
151
158
  problemMatcher: [],
152
159
  },
160
+ {
161
+ label: 'WendKeep: Finish merged worktree',
162
+ type: 'process',
163
+ command: 'npx',
164
+ args: [
165
+ '--no-install', 'wendkeep', 'worktree', 'finish',
166
+ '${input:wendkeepWorktreeSlug}', '--pr', '${input:wendkeepPullRequest}', '--open-main',
167
+ ],
168
+ problemMatcher: [],
169
+ },
170
+ ],
171
+ inputs: [
172
+ {
173
+ id: 'wendkeepWorktreeSlug',
174
+ type: 'promptString',
175
+ description: 'Managed worktree slug',
176
+ },
177
+ {
178
+ id: 'wendkeepPullRequest',
179
+ type: 'promptString',
180
+ description: 'Merged pull request number or URL',
181
+ },
153
182
  ],
154
- inputs: [{
155
- id: 'wendkeepWorktreeSlug',
156
- type: 'promptString',
157
- description: 'Managed worktree slug',
158
- }],
159
183
  };
160
184
  }
161
185
 
@@ -312,18 +336,26 @@ export function diagnoseManagedWorktrees({ startDir = process.cwd(), spawn = spa
312
336
  throw error;
313
337
  }
314
338
  const issues = listed.worktrees
315
- .filter((entry) => entry.state !== 'ready' || !entry.binding.healthy)
339
+ .filter((entry) => (
340
+ !['ready', 'cleaning', 'cleanup-failed', 'cleaned'].includes(entry.state)
341
+ || (entry.state === 'ready' && !entry.binding.healthy)
342
+ ))
316
343
  .map((entry) => ({
317
344
  slug: entry.slug,
318
345
  state: entry.state,
319
346
  errorCode: entry.errorCode || entry.binding.errorCode || 'WENDKEEP_WORKTREE_UNHEALTHY',
320
347
  repair: entry.recovery || `wendkeep worktree status ${entry.slug}`,
321
348
  }));
322
- return { initialized: true, issues };
349
+ const cleanup = diagnoseManagedWorktreeCleanups({ startDir, spawn });
350
+ return { initialized: true, issues: [...issues, ...cleanup.issues] };
323
351
  }
324
352
 
325
- const WORKTREE_VALUE_OPTIONS = new Set(['--base', '--branch', '--open', '--editor', '--project']);
326
- const WORKTREE_FLAG_OPTIONS = new Set(['--json']);
353
+ const WORKTREE_VALUE_OPTIONS = new Set([
354
+ '--base', '--branch', '--open', '--editor', '--project', '--pr', '--reason',
355
+ ]);
356
+ const WORKTREE_FLAG_OPTIONS = new Set([
357
+ '--json', '--delete-remote', '--open-main', '--merged', '--dry-run', '--apply',
358
+ ]);
327
359
 
328
360
  function parseWorktreeArgv(argv) {
329
361
  const values = new Map();
@@ -360,10 +392,14 @@ function parseWorktreeArgv(argv) {
360
392
 
361
393
  function assertCommandOptions(command, parsed) {
362
394
  const allowed = {
363
- create: new Set(['--base', '--branch', '--open', '--project']),
364
- list: new Set(['--project']),
365
- status: new Set(['--project']),
366
- open: new Set(['--editor', '--project']),
395
+ create: new Set(['--base', '--branch', '--open', '--project', '--json']),
396
+ list: new Set(['--project', '--json']),
397
+ status: new Set(['--project', '--json']),
398
+ open: new Set(['--editor', '--project', '--json']),
399
+ finish: new Set(['--pr', '--project', '--delete-remote', '--open-main', '--json']),
400
+ cleanup: new Set(['--project', '--merged', '--dry-run', '--apply', '--json']),
401
+ remove: new Set(['--reason', '--project', '--json']),
402
+ prune: new Set(['--project', '--dry-run', '--apply', '--json']),
367
403
  }[command];
368
404
  if (!allowed) return;
369
405
  for (const name of parsed.values.keys()) {
@@ -371,6 +407,11 @@ function assertCommandOptions(command, parsed) {
371
407
  throw worktreeError('WENDKEEP_WORKTREE_USAGE', `${name} não é válido para worktree ${command}.`);
372
408
  }
373
409
  }
410
+ for (const name of parsed.flags) {
411
+ if (!allowed.has(name)) {
412
+ throw worktreeError('WENDKEEP_WORKTREE_USAGE', `${name} não é válido para worktree ${command}.`);
413
+ }
414
+ }
374
415
  }
375
416
 
376
417
  const ERROR_TEXT = {
@@ -401,6 +442,15 @@ const ERROR_TEXT = {
401
442
  WENDKEEP_WORKTREE_EDITOR_UNSUPPORTED: 'Unsupported worktree editor.',
402
443
  WENDKEEP_WORKTREE_NOT_FOUND: 'Managed worktree was not found.',
403
444
  WENDKEEP_WORKTREE_NOT_READY: 'Managed worktree is not ready.',
445
+ WENDKEEP_WORKTREE_PR_INVALID: 'Pull Request reference is invalid or missing.',
446
+ WENDKEEP_WORKTREE_PR_UNAVAILABLE: 'Pull Request proof is unavailable.',
447
+ WENDKEEP_WORKTREE_PR_NOT_MERGED: 'Pull Request is not merged.',
448
+ WENDKEEP_WORKTREE_PR_MISMATCH: 'Pull Request does not match the managed branch.',
449
+ WENDKEEP_WORKTREE_PR_MERGE_UNREACHABLE: 'Pull Request merge commit is not reachable from the local base.',
450
+ WENDKEEP_WORKTREE_CLEANUP_BUSY: 'Worktree cleanup is already running.',
451
+ WENDKEEP_WORKTREE_REASON_REQUIRED: 'Worktree removal requires a reason.',
452
+ WENDKEEP_WORKTREE_REMOTE_UNAVAILABLE: 'Remote branch state is unavailable.',
453
+ WENDKEEP_WORKTREE_REMOTE_DIVERGED: 'Remote branch diverged from the proven head.',
404
454
  WENDKEEP_WORKTREE_USAGE: 'Invalid worktree command usage.',
405
455
  },
406
456
  'pt-BR': {
@@ -430,6 +480,15 @@ const ERROR_TEXT = {
430
480
  WENDKEEP_WORKTREE_EDITOR_UNSUPPORTED: 'Editor de worktree não suportado.',
431
481
  WENDKEEP_WORKTREE_NOT_FOUND: 'Worktree gerenciada não encontrada.',
432
482
  WENDKEEP_WORKTREE_NOT_READY: 'Worktree gerenciada ainda não está pronta.',
483
+ WENDKEEP_WORKTREE_PR_INVALID: 'Referência de Pull Request inválida ou ausente.',
484
+ WENDKEEP_WORKTREE_PR_UNAVAILABLE: 'A prova do Pull Request está indisponível.',
485
+ WENDKEEP_WORKTREE_PR_NOT_MERGED: 'O Pull Request não está merged.',
486
+ WENDKEEP_WORKTREE_PR_MISMATCH: 'O Pull Request não corresponde à branch gerenciada.',
487
+ WENDKEEP_WORKTREE_PR_MERGE_UNREACHABLE: 'O merge commit do Pull Request não está alcançável pela base local.',
488
+ WENDKEEP_WORKTREE_CLEANUP_BUSY: 'A limpeza da worktree já está em andamento.',
489
+ WENDKEEP_WORKTREE_REASON_REQUIRED: 'A remoção da worktree exige um motivo.',
490
+ WENDKEEP_WORKTREE_REMOTE_UNAVAILABLE: 'O estado da branch remota está indisponível.',
491
+ WENDKEEP_WORKTREE_REMOTE_DIVERGED: 'A branch remota divergiu do head comprovado.',
433
492
  WENDKEEP_WORKTREE_USAGE: 'Uso inválido do comando worktree.',
434
493
  },
435
494
  };
@@ -464,17 +523,23 @@ function writeWorktreeResult(payload, { json, locale, action }) {
464
523
  process.stdout.write(`${JSON.stringify({ ok: true, ...payload })}\n`);
465
524
  return;
466
525
  }
467
- const subject = payload.worktree?.slug || `${payload.worktrees?.length || 0}`;
526
+ const subject = payload.worktree?.slug
527
+ || payload.receipt?.slug
528
+ || `${payload.worktrees?.length ?? payload.actions?.length ?? 0}`;
468
529
  const messages = locale === 'en'
469
- ? { create: `worktree created: ${subject}`, list: `managed worktrees: ${subject}`, status: `worktree status: ${subject}`, open: `worktree opened: ${subject}` }
470
- : { create: `worktree criada: ${subject}`, list: `worktrees gerenciadas: ${subject}`, status: `status da worktree: ${subject}`, open: `worktree aberta: ${subject}` };
530
+ ? { create: `worktree created: ${subject}`, list: `managed worktrees: ${subject}`, status: `worktree status: ${subject}`, open: `worktree opened: ${subject}`, finish: `worktree finished: ${subject}`, cleanup: `merged worktree cleanup: ${subject}`, remove: `worktree removed: ${subject}`, prune: `worktree prune: ${subject}` }
531
+ : { create: `worktree criada: ${subject}`, list: `worktrees gerenciadas: ${subject}`, status: `status da worktree: ${subject}`, open: `worktree aberta: ${subject}`, finish: `worktree finalizada: ${subject}`, cleanup: `limpeza de worktrees merged: ${subject}`, remove: `worktree removida: ${subject}`, prune: `prune de worktrees: ${subject}` };
471
532
  const details = action === 'list'
472
533
  ? payload.worktrees.map(renderWorktreeLine)
473
- : (action === 'status' ? [renderWorktreeLine(payload.worktree)] : []);
534
+ : (action === 'status'
535
+ ? [renderWorktreeLine(payload.worktree)]
536
+ : (payload.actions || []).map((item) => [
537
+ `slug=${item.slug || ''}`, `path=${item.path || ''}`, `outcome=${item.outcome || item.action || ''}`,
538
+ ].join(' ')));
474
539
  process.stdout.write(`${messages[action]}${details.length ? `\n${details.join('\n')}` : ''}\n`);
475
540
  }
476
541
 
477
- export function runWorktree(argv = []) {
542
+ export async function runWorktree(argv = [], dependencies = {}) {
478
543
  let parsed;
479
544
  let locale = 'pt-BR';
480
545
  try {
@@ -484,6 +549,9 @@ export function runWorktree(argv = []) {
484
549
  const startDir = parsed.values.get('--project') || process.cwd();
485
550
  locale = commandLocale(startDir);
486
551
  const json = parsed.flags.has('--json');
552
+ if (parsed.flags.has('--dry-run') && parsed.flags.has('--apply')) {
553
+ throw worktreeError('WENDKEEP_WORKTREE_USAGE', 'Use apenas --dry-run ou --apply.');
554
+ }
487
555
  if (command === 'create') {
488
556
  if (positionals.length !== 1) throw worktreeError('WENDKEEP_WORKTREE_USAGE', 'create requer <slug>.');
489
557
  const worktree = createManagedWorktree({
@@ -518,7 +586,50 @@ export function runWorktree(argv = []) {
518
586
  writeWorktreeResult({ worktree }, { json, locale, action: 'open' });
519
587
  return 0;
520
588
  }
521
- throw worktreeError('WENDKEEP_WORKTREE_USAGE', 'Use create, list, status ou open.');
589
+ if (command === 'finish') {
590
+ if (positionals.length !== 1) throw worktreeError('WENDKEEP_WORKTREE_USAGE', 'finish requer <slug>.');
591
+ const operation = dependencies.finishManagedWorktree || finishManagedWorktree;
592
+ const result = await operation({
593
+ startDir,
594
+ slug: positionals[0],
595
+ pullRequest: parsed.values.get('--pr') || '',
596
+ deleteRemote: parsed.flags.has('--delete-remote'),
597
+ });
598
+ if (parsed.flags.has('--open-main')) {
599
+ const main = discoverWorktreeRepository({ startDir }).mainWorktree;
600
+ const openMain = dependencies.openMain
601
+ || ((path) => openWorktreePath(path, 'vscode', spawnSync));
602
+ openMain(main);
603
+ }
604
+ writeWorktreeResult(result, { json, locale, action: 'finish' });
605
+ return 0;
606
+ }
607
+ if (command === 'cleanup') {
608
+ if (positionals.length || !parsed.flags.has('--merged')) {
609
+ throw worktreeError('WENDKEEP_WORKTREE_USAGE', 'cleanup exige --merged e não aceita slug.');
610
+ }
611
+ const operation = dependencies.cleanupMergedWorktrees || cleanupMergedWorktrees;
612
+ const result = await operation({ startDir, apply: parsed.flags.has('--apply') });
613
+ writeWorktreeResult(result, { json, locale, action: 'cleanup' });
614
+ return result.actions.some((item) => item.outcome === 'blocked') ? 2 : 0;
615
+ }
616
+ if (command === 'remove') {
617
+ if (positionals.length !== 1) throw worktreeError('WENDKEEP_WORKTREE_USAGE', 'remove requer <slug>.');
618
+ const operation = dependencies.removeManagedWorktree || removeManagedWorktree;
619
+ const result = await operation({
620
+ startDir, slug: positionals[0], reason: parsed.values.get('--reason') || '',
621
+ });
622
+ writeWorktreeResult(result, { json, locale, action: 'remove' });
623
+ return 0;
624
+ }
625
+ if (command === 'prune') {
626
+ if (positionals.length) throw worktreeError('WENDKEEP_WORKTREE_USAGE', 'prune não aceita slug.');
627
+ const operation = dependencies.pruneManagedWorktrees || pruneManagedWorktrees;
628
+ const result = await operation({ startDir, apply: parsed.flags.has('--apply') });
629
+ writeWorktreeResult(result, { json, locale, action: 'prune' });
630
+ return 0;
631
+ }
632
+ throw worktreeError('WENDKEEP_WORKTREE_USAGE', 'Use create, list, status, open, finish, cleanup, remove ou prune.');
522
633
  } catch (error) {
523
634
  process.stderr.write(`${renderWorktreeError(error, locale)}\n`);
524
635
  return 2;