@ghl-ai/aw 0.1.36-beta.71 → 0.1.36-beta.73
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/commands/pull.mjs +26 -4
- package/commands/push.mjs +5 -6
- package/commands/status.mjs +35 -25
- package/git.mjs +84 -78
- package/package.json +1 -1
package/commands/pull.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// commands/pull.mjs — Pull content from registry using persistent git clone
|
|
2
2
|
|
|
3
|
-
import { mkdirSync, existsSync, readdirSync, copyFileSync, unlinkSync, rmdirSync } from 'node:fs';
|
|
3
|
+
import { mkdirSync, existsSync, readdirSync, copyFileSync, unlinkSync, rmdirSync, lstatSync } from 'node:fs';
|
|
4
4
|
import { join, relative, extname } from 'node:path';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { exec as execCb } from 'node:child_process';
|
|
@@ -74,6 +74,16 @@ export async function pullCommand(args) {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
// Guard: if a rebase or merge is already in progress on awHome, don't proceed
|
|
78
|
+
const awGitDir = join(AW_HOME, '.git');
|
|
79
|
+
if (existsSync(join(awGitDir, 'rebase-merge')) ||
|
|
80
|
+
existsSync(join(awGitDir, 'rebase-apply')) ||
|
|
81
|
+
existsSync(join(awGitDir, 'MERGE_HEAD'))) {
|
|
82
|
+
log.logWarn('Rebase paused — resolve conflicts in your IDE, then run `aw pull` again.');
|
|
83
|
+
if (!silent) fmt.outro(chalk.yellow('Pull skipped'));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
77
87
|
// Fetch + merge latest
|
|
78
88
|
const s = log.spinner();
|
|
79
89
|
s.start('Fetching latest from registry...');
|
|
@@ -102,12 +112,26 @@ export async function pullCommand(args) {
|
|
|
102
112
|
}
|
|
103
113
|
|
|
104
114
|
if (fetchResult.conflicts.length > 0) {
|
|
115
|
+
if (!silent) {
|
|
116
|
+
// Interactive mode: abort the merge so the working tree is clean again
|
|
117
|
+
try { await exec(`git -C "${AW_HOME}" merge --abort`); } catch { /* best effort */ }
|
|
118
|
+
log.logWarn(`Merge conflict in: ${fetchResult.conflicts.join(', ')}`);
|
|
119
|
+
log.logWarn('Merge aborted — your branch is unchanged. Resolve conflicts and run `aw pull` again.');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// Silent mode: leave conflict markers in place for IDE to show
|
|
105
123
|
log.logWarn(`Conflicts in: ${fetchResult.conflicts.join(', ')}`);
|
|
106
124
|
}
|
|
107
125
|
|
|
108
|
-
//
|
|
126
|
+
// Rebase project worktree branch onto origin/main — only for legacy git worktrees.
|
|
127
|
+
// In the symlink model, <project>/.aw IS ~/.aw (same repo), so fetchAndMerge already
|
|
128
|
+
// brought it up to date. Nothing to rebase.
|
|
109
129
|
const localAw = cwd !== HOME ? findNearestWorktree(cwd, HOME) : null;
|
|
130
|
+
let isSymlinkWorktree = false;
|
|
110
131
|
if (localAw) {
|
|
132
|
+
try { isSymlinkWorktree = lstatSync(localAw).isSymbolicLink(); } catch {}
|
|
133
|
+
}
|
|
134
|
+
if (localAw && !isSymlinkWorktree) {
|
|
111
135
|
const rebaseSpinner = log.spinner();
|
|
112
136
|
rebaseSpinner.start('Rebasing local branch onto latest...');
|
|
113
137
|
try {
|
|
@@ -129,9 +153,7 @@ export async function pullCommand(args) {
|
|
|
129
153
|
} catch { /* best effort */ }
|
|
130
154
|
|
|
131
155
|
if (!silent) {
|
|
132
|
-
// Interactive mode: abort and show clear terminal warning
|
|
133
156
|
try { await exec(`git -C "${localAw}" rebase --abort`); } catch { /* best effort */ }
|
|
134
|
-
|
|
135
157
|
log.logWarn('Rebase aborted — your branch is unchanged.');
|
|
136
158
|
if (conflictedFiles.length > 0) {
|
|
137
159
|
log.logWarn(`Conflicting file${conflictedFiles.length > 1 ? 's' : ''}:`);
|
package/commands/push.mjs
CHANGED
|
@@ -427,15 +427,14 @@ export async function pushCommand(args) {
|
|
|
427
427
|
const HOME = homedir();
|
|
428
428
|
const globalAw = join(HOME, '.aw');
|
|
429
429
|
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
const awHome = hasWorktree ? localAw : globalAw;
|
|
430
|
+
// Always push from the global clone (~/.aw), same as aw pull.
|
|
431
|
+
// All agent/skill edits via IDE symlinks land in ~/.aw/.aw_registry/ directly —
|
|
432
|
+
// project worktrees are just local views for IDE visibility, not the source of truth.
|
|
433
|
+
const awHome = globalAw;
|
|
435
434
|
const registrySubDir = join(awHome, REGISTRY_DIR);
|
|
436
435
|
const workspaceDir = getLocalRegistryDir(cwd, join(HOME, '.aw_registry'));
|
|
437
436
|
|
|
438
|
-
const worktreeFlow =
|
|
437
|
+
const worktreeFlow = false;
|
|
439
438
|
|
|
440
439
|
fmt.intro('aw push');
|
|
441
440
|
|
package/commands/status.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { homedir } from 'node:os';
|
|
|
5
5
|
import * as config from '../config.mjs';
|
|
6
6
|
import * as fmt from '../fmt.mjs';
|
|
7
7
|
import { chalk } from '../fmt.mjs';
|
|
8
|
-
import { detectChanges, getCurrentBranch, commitsAheadOfMain, isValidClone, findNearestWorktree } from '../git.mjs';
|
|
8
|
+
import { detectChanges, diffFromMain, getCurrentBranch, commitsAheadOfMain, isValidClone, findNearestWorktree } from '../git.mjs';
|
|
9
9
|
import { REGISTRY_DIR, REGISTRY_REPO, REGISTRY_URL } from '../constants.mjs';
|
|
10
10
|
|
|
11
11
|
export function statusCommand(args) {
|
|
@@ -51,54 +51,64 @@ export function statusCommand(args) {
|
|
|
51
51
|
fmt.logInfo('No extra paths synced (platform/ always included).');
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
//
|
|
55
|
-
const
|
|
54
|
+
// ── Drift from main (3-dot diff: captures committed+pushed changes too) ────
|
|
55
|
+
const drift = diffFromMain(AW_HOME, REGISTRY_DIR);
|
|
56
|
+
const driftTotal = drift.added.length + drift.modified.length + drift.deleted.length;
|
|
56
57
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const summaryParts = [
|
|
62
|
-
modified.length > 0 ? chalk.yellow(`${modified.length} modified`) : null,
|
|
63
|
-
allNew.length > 0 ? chalk.green(`${allNew.length} new (unpushed)`) : null,
|
|
64
|
-
deleted.length > 0 ? chalk.dim(`${deleted.length} deleted`) : null,
|
|
58
|
+
const driftParts = [
|
|
59
|
+
drift.added.length > 0 ? chalk.green(`${drift.added.length} added`) : null,
|
|
60
|
+
drift.modified.length > 0 ? chalk.yellow(`${drift.modified.length} modified`) : null,
|
|
61
|
+
drift.deleted.length > 0 ? chalk.dim(`${drift.deleted.length} deleted`) : null,
|
|
65
62
|
].filter(Boolean);
|
|
66
63
|
|
|
67
|
-
if (
|
|
68
|
-
fmt.logSuccess('
|
|
64
|
+
if (driftTotal === 0) {
|
|
65
|
+
fmt.logSuccess('No drift from main — workspace is in sync');
|
|
69
66
|
} else {
|
|
70
|
-
fmt.logInfo(
|
|
67
|
+
fmt.logInfo(`Drift from main: ${driftParts.join(chalk.dim(' · '))}`);
|
|
71
68
|
}
|
|
72
69
|
|
|
73
|
-
if (
|
|
70
|
+
if (drift.added.length > 0) {
|
|
74
71
|
fmt.note(
|
|
75
|
-
|
|
76
|
-
chalk.green('
|
|
72
|
+
drift.added.map(f => chalk.green(` + ${f.registryPath}`)).join('\n'),
|
|
73
|
+
chalk.green('Added (vs main)')
|
|
77
74
|
);
|
|
78
75
|
}
|
|
79
76
|
|
|
80
|
-
if (modified.length > 0) {
|
|
77
|
+
if (drift.modified.length > 0) {
|
|
81
78
|
fmt.note(
|
|
82
|
-
modified.map(f => chalk.yellow(` ${f.registryPath}`)).join('\n'),
|
|
83
|
-
chalk.yellow('Modified')
|
|
79
|
+
drift.modified.map(f => chalk.yellow(` ~ ${f.registryPath}`)).join('\n'),
|
|
80
|
+
chalk.yellow('Modified (vs main)')
|
|
84
81
|
);
|
|
85
82
|
}
|
|
86
83
|
|
|
87
|
-
if (deleted.length > 0) {
|
|
84
|
+
if (drift.deleted.length > 0) {
|
|
88
85
|
fmt.note(
|
|
89
|
-
deleted.map(f => chalk.dim(` ${f.registryPath}`)).join('\n'),
|
|
90
|
-
chalk.dim('Deleted')
|
|
86
|
+
drift.deleted.map(f => chalk.dim(` - ${f.registryPath}`)).join('\n'),
|
|
87
|
+
chalk.dim('Deleted (vs main)')
|
|
91
88
|
);
|
|
92
89
|
}
|
|
93
90
|
|
|
91
|
+
// ── Uncommitted local changes (on top of drift) ───────────────────────────
|
|
92
|
+
const local = detectChanges(AW_HOME, REGISTRY_DIR);
|
|
93
|
+
const localDirty = [...local.modified, ...local.added, ...local.deleted, ...local.untracked];
|
|
94
|
+
|
|
95
|
+
if (localDirty.length > 0) {
|
|
96
|
+
const localParts = [
|
|
97
|
+
(local.modified.length > 0) ? chalk.yellow(`${local.modified.length} modified`) : null,
|
|
98
|
+
(local.added.length + local.untracked.length > 0) ? chalk.green(`${local.added.length + local.untracked.length} new`) : null,
|
|
99
|
+
(local.deleted.length > 0) ? chalk.dim(`${local.deleted.length} deleted`) : null,
|
|
100
|
+
].filter(Boolean);
|
|
101
|
+
fmt.logWarn(`Uncommitted local changes: ${localParts.join(chalk.dim(' · '))} — run ${chalk.dim('aw push')} to commit & open PR`);
|
|
102
|
+
}
|
|
103
|
+
|
|
94
104
|
if (!isOnMain) {
|
|
95
105
|
const ahead = commitsAheadOfMain(AW_HOME);
|
|
96
|
-
const aheadStr = ahead > 0 ? ` (${chalk.yellow(`${ahead} commit${ahead !== 1 ? 's' : ''} ahead`)})` : '';
|
|
106
|
+
const aheadStr = ahead > 0 ? ` (${chalk.yellow(`${ahead} commit${ahead !== 1 ? 's' : ''} ahead of main`)})` : '';
|
|
97
107
|
fmt.logWarn(`On branch ${chalk.yellow(branch)}${aheadStr} — run ${chalk.dim('aw push')} to open a PR or ${chalk.dim('aw pull')} to sync`);
|
|
98
108
|
}
|
|
99
109
|
|
|
100
110
|
// Hints
|
|
101
|
-
if (
|
|
111
|
+
if (driftTotal > 0 || localDirty.length > 0) {
|
|
102
112
|
fmt.logInfo(`Push changes: ${chalk.dim('aw push')} or ${chalk.dim('aw push <path>')}`);
|
|
103
113
|
}
|
|
104
114
|
|
package/git.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// git.mjs — Git helpers: sparse checkout (temp) + persistent clone operations.
|
|
2
2
|
|
|
3
3
|
import { execSync, exec as execCb } from 'node:child_process';
|
|
4
|
-
import { mkdtempSync, existsSync, lstatSync, rmSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { mkdtempSync, existsSync, lstatSync, rmSync, readFileSync, symlinkSync, unlinkSync } from 'node:fs';
|
|
5
5
|
import { join, basename, dirname } from 'node:path';
|
|
6
6
|
import { homedir, tmpdir } from 'node:os';
|
|
7
7
|
import { promisify } from 'node:util';
|
|
@@ -172,6 +172,8 @@ export function addToSparseCheckout(awHome, newPaths) {
|
|
|
172
172
|
* Called after `addToSparseCheckout` so newly-pulled content appears in project/.aw/.
|
|
173
173
|
*/
|
|
174
174
|
export function syncWorktreeSparseCheckout(awHome, worktreeDir) {
|
|
175
|
+
// Symlink model: worktreeDir IS awHome — sparse checkout is already shared, nothing to sync
|
|
176
|
+
try { if (lstatSync(worktreeDir).isSymbolicLink()) return; } catch { return; }
|
|
175
177
|
try {
|
|
176
178
|
const out = execSync('git sparse-checkout list', { cwd: awHome, stdio: 'pipe', encoding: 'utf8' });
|
|
177
179
|
const paths = out.trim().split('\n').filter(Boolean);
|
|
@@ -323,7 +325,7 @@ export function commitToCurrentBranch(awHome, files, commitMsg, preStaged = fals
|
|
|
323
325
|
if (!preStaged) {
|
|
324
326
|
try {
|
|
325
327
|
const quotedFiles = files.map(f => `"${f}"`).join(' ');
|
|
326
|
-
execSync(`git -C "${awHome}" add
|
|
328
|
+
execSync(`git -C "${awHome}" add ${quotedFiles}`, { stdio: 'pipe' });
|
|
327
329
|
} catch (e) {
|
|
328
330
|
throw new Error(`Failed to stage files: ${e.message}`);
|
|
329
331
|
}
|
|
@@ -379,7 +381,7 @@ export async function createPushBranch(awHome, branchName, files, commitMsg, pre
|
|
|
379
381
|
if (!preStaged) {
|
|
380
382
|
try {
|
|
381
383
|
const quotedFiles = files.map(f => `"${f}"`).join(' ');
|
|
382
|
-
await exec(`git -C "${awHome}" add
|
|
384
|
+
await exec(`git -C "${awHome}" add ${quotedFiles}`);
|
|
383
385
|
} catch (e) {
|
|
384
386
|
throw new Error(`Failed to stage files: ${e.message}`);
|
|
385
387
|
}
|
|
@@ -416,6 +418,42 @@ export function commitsAheadOfMain(awHome) {
|
|
|
416
418
|
}
|
|
417
419
|
}
|
|
418
420
|
|
|
421
|
+
/**
|
|
422
|
+
* Get all files that differ from origin/main (or local main as fallback).
|
|
423
|
+
* Uses 3-dot diff (origin/main...HEAD) so it captures the full branch delta —
|
|
424
|
+
* including commits already pushed — not just uncommitted working-tree changes.
|
|
425
|
+
*
|
|
426
|
+
* Returns { added: [], modified: [], deleted: [] }
|
|
427
|
+
* Each entry: { path: 'relative/to/awHome', registryPath: 'relative/to/registryDir' }
|
|
428
|
+
*/
|
|
429
|
+
export function diffFromMain(awHome, registryDir) {
|
|
430
|
+
for (const base of [`origin/${REGISTRY_BASE_BRANCH}`, REGISTRY_BASE_BRANCH]) {
|
|
431
|
+
try {
|
|
432
|
+
const out = execSync(
|
|
433
|
+
`git -C "${awHome}" diff ${base}...HEAD --name-status -- "${registryDir}/"`,
|
|
434
|
+
{ stdio: 'pipe', encoding: 'utf8' },
|
|
435
|
+
);
|
|
436
|
+
const added = [], modified = [], deleted = [];
|
|
437
|
+
for (const line of out.trim().split('\n')) {
|
|
438
|
+
if (!line) continue;
|
|
439
|
+
const [status, filePath] = line.split('\t');
|
|
440
|
+
if (!filePath) continue;
|
|
441
|
+
const registryPath = filePath.startsWith(registryDir + '/')
|
|
442
|
+
? filePath.slice(registryDir.length + 1)
|
|
443
|
+
: filePath;
|
|
444
|
+
const entry = { path: filePath, registryPath };
|
|
445
|
+
if (status === 'A') added.push(entry);
|
|
446
|
+
else if (status === 'D') deleted.push(entry);
|
|
447
|
+
else modified.push(entry); // M, R*, C*, T
|
|
448
|
+
}
|
|
449
|
+
return { added, modified, deleted };
|
|
450
|
+
} catch {
|
|
451
|
+
continue; // try next base ref
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return { added: [], modified: [], deleted: [] };
|
|
455
|
+
}
|
|
456
|
+
|
|
419
457
|
/**
|
|
420
458
|
* Get one-line log of commits ahead of main.
|
|
421
459
|
* Returns array of { hash, message } objects.
|
|
@@ -510,9 +548,17 @@ export function findNearestWorktree(startDir, stopDir) {
|
|
|
510
548
|
}
|
|
511
549
|
|
|
512
550
|
/**
|
|
513
|
-
* Check if dir is a
|
|
551
|
+
* Check if dir is a valid project .aw/ link — either:
|
|
552
|
+
* (a) a symlink to awHome (new model — one shared repo for all workspaces), or
|
|
553
|
+
* (b) a legacy git linked worktree (.git is a FILE pointing to a valid gitdir).
|
|
514
554
|
*/
|
|
515
555
|
export function isWorktree(dir) {
|
|
556
|
+
// (a) symlink model: <project>/.aw → ~/.aw
|
|
557
|
+
try {
|
|
558
|
+
if (lstatSync(dir).isSymbolicLink() && existsSync(dir)) return true;
|
|
559
|
+
} catch { /* doesn't exist */ }
|
|
560
|
+
|
|
561
|
+
// (b) legacy git worktree model
|
|
516
562
|
const gitPath = join(dir, '.git');
|
|
517
563
|
try {
|
|
518
564
|
if (!lstatSync(gitPath).isFile()) return false;
|
|
@@ -527,11 +573,13 @@ export function isWorktree(dir) {
|
|
|
527
573
|
}
|
|
528
574
|
|
|
529
575
|
/**
|
|
530
|
-
* Create project/.aw/ as a
|
|
531
|
-
* Sets up sparse checkout mirroring the main clone, then creates
|
|
532
|
-
* project/.aw_registry → project/.aw/.aw_registry/ (relative symlink).
|
|
576
|
+
* Create project/.aw/ as a symlink to awHome (~/.aw).
|
|
533
577
|
*
|
|
534
|
-
*
|
|
578
|
+
* All IDE workspaces share the exact same git repo via this symlink — same
|
|
579
|
+
* branch, same content, always in sync. aw pull/push both operate on awHome
|
|
580
|
+
* directly, so there are no per-project diverging branches.
|
|
581
|
+
*
|
|
582
|
+
* Safe to call multiple times — skips if already set up correctly.
|
|
535
583
|
*/
|
|
536
584
|
export function addProjectWorktree(awHome, projectDir) {
|
|
537
585
|
const worktreeDir = join(projectDir, '.aw');
|
|
@@ -542,89 +590,47 @@ export function addProjectWorktree(awHome, projectDir) {
|
|
|
542
590
|
if (lstatSync(staleSymlink).isSymbolicLink()) rmSync(staleSymlink);
|
|
543
591
|
} catch { /* doesn't exist, fine */ }
|
|
544
592
|
|
|
545
|
-
// Already
|
|
546
|
-
if (isWorktree(worktreeDir)) return worktreeDir;
|
|
547
|
-
|
|
548
|
-
// Stale .aw/ exists (broken git link — e.g. after aw nuke + re-init) — remove so git can recreate it
|
|
549
|
-
if (existsSync(worktreeDir)) {
|
|
550
|
-
try { execSync(`rm -rf "${worktreeDir}"`, { stdio: 'pipe' }); } catch { /* best effort */ }
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// Prune stale worktree metadata from ~/.aw/.git/worktrees/ before re-adding.
|
|
554
|
-
// Without this, `git worktree add` fails because the old metadata entry may still
|
|
555
|
-
// reference the deleted path, causing a "already exists" or similar error.
|
|
556
|
-
try { execSync(`git -C "${awHome}" worktree prune`, { stdio: 'pipe' }); } catch { /* best effort */ }
|
|
557
|
-
|
|
558
|
-
const slug = basename(projectDir)
|
|
559
|
-
.toLowerCase()
|
|
560
|
-
.replace(/[^a-z0-9]/g, '-')
|
|
561
|
-
.replace(/-+/g, '-')
|
|
562
|
-
.replace(/^-|-$/g, '')
|
|
563
|
-
.slice(0, 40) || 'project';
|
|
564
|
-
const branchName = `worktree/${slug}`;
|
|
565
|
-
|
|
566
|
-
// Try to create the worktree with a new branch
|
|
567
|
-
let created = false;
|
|
593
|
+
// Already a valid symlink pointing to awHome → done
|
|
568
594
|
try {
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
} catch {
|
|
572
|
-
// Branch already exists from a previous setup — reuse it
|
|
573
|
-
try {
|
|
574
|
-
execSync(`git -C "${awHome}" worktree add "${worktreeDir}" --no-checkout "${branchName}"`, { stdio: 'pipe' });
|
|
575
|
-
created = true;
|
|
576
|
-
} catch {
|
|
577
|
-
// Last resort: unique branch name
|
|
578
|
-
const unique = `${branchName}-${Date.now().toString(36).slice(-4)}`;
|
|
579
|
-
execSync(`git -C "${awHome}" worktree add "${worktreeDir}" --no-checkout -b "${unique}"`, { stdio: 'pipe' });
|
|
580
|
-
created = true;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
595
|
+
if (lstatSync(worktreeDir).isSymbolicLink() && existsSync(worktreeDir)) return worktreeDir;
|
|
596
|
+
} catch { /* doesn't exist yet */ }
|
|
583
597
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
// Set up sparse checkout in the worktree, mirroring the main clone's paths
|
|
598
|
+
// Remove old git worktree or stale directory/symlink so we can create a clean symlink
|
|
587
599
|
try {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
if (sparsePaths.length === 0) {
|
|
597
|
-
sparsePaths = [`${REGISTRY_DIR}/platform`, 'content', 'CODEOWNERS'];
|
|
600
|
+
const stat = lstatSync(worktreeDir);
|
|
601
|
+
if (stat.isSymbolicLink()) {
|
|
602
|
+
unlinkSync(worktreeDir); // dangling symlink — replace
|
|
603
|
+
} else if (stat.isDirectory()) {
|
|
604
|
+
// Legacy git worktree — detach from git before removing
|
|
605
|
+
try { execSync(`git -C "${awHome}" worktree remove "${worktreeDir}" --force`, { stdio: 'pipe' }); } catch {}
|
|
606
|
+
try { execSync(`git -C "${awHome}" worktree prune`, { stdio: 'pipe' }); } catch {}
|
|
607
|
+
rmSync(worktreeDir, { recursive: true, force: true });
|
|
598
608
|
}
|
|
609
|
+
} catch { /* nothing there — fine */ }
|
|
599
610
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
} catch (e) {
|
|
603
|
-
throw new Error(`Failed to configure worktree sparse checkout: ${e.message}`);
|
|
604
|
-
}
|
|
605
|
-
|
|
611
|
+
// Create symlink: <project>/.aw → ~/.aw
|
|
612
|
+
symlinkSync(awHome, worktreeDir);
|
|
606
613
|
return worktreeDir;
|
|
607
614
|
}
|
|
608
615
|
|
|
609
616
|
/**
|
|
610
|
-
* Remove a project
|
|
611
|
-
*
|
|
617
|
+
* Remove a project .aw/ link created by addProjectWorktree.
|
|
618
|
+
* For the new symlink model: just removes the symlink (never the target ~/.aw).
|
|
619
|
+
* For legacy git worktrees: removes via git worktree remove.
|
|
612
620
|
*/
|
|
613
621
|
export function removeProjectWorktree(awHome, projectDir) {
|
|
614
622
|
const worktreeDir = join(projectDir, '.aw');
|
|
615
|
-
|
|
616
|
-
// Remove via git first
|
|
617
623
|
try {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
}
|
|
624
|
+
const stat = lstatSync(worktreeDir);
|
|
625
|
+
if (stat.isSymbolicLink()) {
|
|
626
|
+
unlinkSync(worktreeDir); // remove symlink only — never delete ~/.aw
|
|
627
|
+
} else if (stat.isDirectory()) {
|
|
628
|
+
// Legacy git worktree
|
|
629
|
+
try { execSync(`git -C "${awHome}" worktree remove "${worktreeDir}" --force`, { stdio: 'pipe' }); } catch {}
|
|
630
|
+
try { rmSync(worktreeDir, { recursive: true, force: true }); } catch {}
|
|
631
|
+
try { execSync(`git -C "${awHome}" worktree prune`, { stdio: 'pipe' }); } catch {}
|
|
632
|
+
}
|
|
633
|
+
} catch { /* doesn't exist — nothing to do */ }
|
|
628
634
|
}
|
|
629
635
|
|
|
630
636
|
/**
|