@ghl-ai/aw 0.1.36-beta.73 → 0.1.36-beta.74

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/init.mjs CHANGED
@@ -235,10 +235,13 @@ export async function initCommand(args) {
235
235
  }
236
236
 
237
237
  const isInsideAw = cwd.endsWith(`${sep}.aw`) || cwd.includes(`${sep}.aw${sep}`);
238
- if (cwd !== HOME && !isInsideAw && !isWorktree(join(cwd, '.aw'))) {
238
+ // Only skip if already a valid symlink (new model). Old git worktrees must be migrated.
239
+ const awLink = join(cwd, '.aw');
240
+ const isAlreadySymlink = (() => { try { return lstatSync(awLink).isSymbolicLink() && existsSync(awLink); } catch { return false; } })();
241
+ if (cwd !== HOME && !isInsideAw && !isAlreadySymlink) {
239
242
  try {
240
243
  addProjectWorktree(AW_HOME, cwd);
241
- if (!silent) fmt.logStep('Linked current project as git worktree');
244
+ if (!silent) fmt.logStep('Linked current project');
242
245
  } catch { /* best effort */ }
243
246
  }
244
247
 
@@ -356,12 +359,14 @@ export async function initCommand(args) {
356
359
  } catch { /* not there, fine */ }
357
360
  }
358
361
 
359
- // Step 4: Link current project as a git worktree (gives IDE git panel)
362
+ // Step 4: Link current project as a symlink to ~/.aw (gives IDE git panel, shared across all workspaces)
360
363
  const isInsideAw = cwd.endsWith(`${sep}.aw`) || cwd.includes(`${sep}.aw${sep}`);
361
- if (cwd !== HOME && !isInsideAw && !isWorktree(join(cwd, '.aw'))) {
364
+ const awLinkFresh = join(cwd, '.aw');
365
+ const isAlreadySymlinkFresh = (() => { try { return lstatSync(awLinkFresh).isSymbolicLink() && existsSync(awLinkFresh); } catch { return false; } })();
366
+ if (cwd !== HOME && !isInsideAw && !isAlreadySymlinkFresh) {
362
367
  try {
363
368
  addProjectWorktree(AW_HOME, cwd);
364
- fmt.logStep('Linked current project as git worktree');
369
+ fmt.logStep('Linked current project');
365
370
  } catch { /* best effort */ }
366
371
  }
367
372
 
package/commands/nuke.mjs CHANGED
@@ -260,30 +260,45 @@ export async function nukeCommand(args) {
260
260
  try { if (existsSync(p)) rmSync(p, { recursive: true, force: true }); } catch { /* best effort */ }
261
261
  }
262
262
 
263
- // Phase 4: git worktrees
263
+ // Phase 4: project .aw/ links — covers both old git worktrees and new symlinks
264
264
  const AW_HOME = join(HOME, '.aw');
265
+ const wtSpinner = fmt.spinner();
266
+ wtSpinner.start('Removing project .aw/ links...');
267
+ let wtRemoved = 0;
268
+
269
+ // 4a. Remove old git worktrees (worktree/* branches in ~/.aw)
265
270
  if (existsSync(AW_HOME)) {
266
271
  try {
267
272
  const worktrees = listProjectWorktrees(AW_HOME);
268
- if (worktrees.length > 0) {
269
- const wtSpinner = fmt.spinner();
270
- wtSpinner.start('Removing git worktrees...');
271
- let wtRemoved = 0;
272
- for (const wt of worktrees) {
273
- try {
274
- await exec(`git -C "${AW_HOME}" worktree remove "${wt.path}" --force`);
275
- wtRemoved++;
276
- } catch {
277
- try { rmSync(wt.path, { recursive: true, force: true }); } catch { /* best effort */ }
278
- wtRemoved++;
279
- }
280
- }
281
- try { await exec(`git -C "${AW_HOME}" worktree prune`); } catch { /* best effort */ }
282
- wtSpinner.stop(`Removed ${wtRemoved} project worktree${wtRemoved > 1 ? 's' : ''}`);
273
+ for (const wt of worktrees) {
274
+ try { await exec(`git -C "${AW_HOME}" worktree remove "${wt.path}" --force`); } catch {}
275
+ try { rmSync(wt.path, { recursive: true, force: true }); } catch {}
276
+ wtRemoved++;
283
277
  }
278
+ try { await exec(`git -C "${AW_HOME}" worktree prune`); } catch {}
284
279
  } catch { /* best effort */ }
285
280
  }
286
281
 
282
+ // 4b. Remove new-style .aw symlinks pointing to ~/.aw
283
+ // These are created by the symlink model and are NOT tracked by git worktree list.
284
+ try {
285
+ const { stdout: awLinks } = await exec(
286
+ `find "${HOME}" -maxdepth 5 -name ".aw" -type l 2>/dev/null || true`,
287
+ { encoding: 'utf8', timeout: 30000 }
288
+ );
289
+ for (const linkPath of awLinks.trim().split('\n').filter(Boolean)) {
290
+ try {
291
+ const target = readlinkSync(linkPath);
292
+ if (target === AW_HOME || target.endsWith('/.aw')) {
293
+ unlinkSync(linkPath);
294
+ wtRemoved++;
295
+ }
296
+ } catch { /* best effort */ }
297
+ }
298
+ } catch { /* best effort */ }
299
+
300
+ wtSpinner.stop(`Removed ${wtRemoved} project .aw/ link${wtRemoved !== 1 ? 's' : ''}`);
301
+
287
302
  // Phase 6: remove registry clone + docs
288
303
  const cloneSpinner = fmt.spinner();
289
304
  cloneSpinner.start('Removing registry clone...');
@@ -89,22 +89,54 @@ export function statusCommand(args) {
89
89
  }
90
90
 
91
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];
92
+ const rawLocal = detectChanges(AW_HOME, REGISTRY_DIR);
93
+ // Filter out internal config files that are managed by `aw init`, not the user
94
+ const skipFiles = new Set(['.sync-config.json']);
95
+ const filterConfig = arr => arr.filter(e => !skipFiles.has(e.registryPath));
96
+ const local = {
97
+ modified: filterConfig(rawLocal.modified),
98
+ added: filterConfig(rawLocal.added),
99
+ deleted: filterConfig(rawLocal.deleted),
100
+ untracked: filterConfig(rawLocal.untracked),
101
+ };
102
+ const localNew = [...local.added, ...local.untracked];
103
+ const localDirty = [...local.modified, ...localNew, ...local.deleted];
94
104
 
95
105
  if (localDirty.length > 0) {
96
106
  const localParts = [
97
107
  (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,
108
+ (localNew.length > 0) ? chalk.green(`${localNew.length} new`) : null,
99
109
  (local.deleted.length > 0) ? chalk.dim(`${local.deleted.length} deleted`) : null,
100
110
  ].filter(Boolean);
101
111
  fmt.logWarn(`Uncommitted local changes: ${localParts.join(chalk.dim(' · '))} — run ${chalk.dim('aw push')} to commit & open PR`);
112
+
113
+ if (localNew.length > 0) {
114
+ fmt.note(
115
+ localNew.map(f => chalk.green(` + ${f.registryPath}`)).join('\n'),
116
+ chalk.green('New (uncommitted)')
117
+ );
118
+ }
119
+ if (local.modified.length > 0) {
120
+ fmt.note(
121
+ local.modified.map(f => chalk.yellow(` ~ ${f.registryPath}`)).join('\n'),
122
+ chalk.yellow('Modified (uncommitted)')
123
+ );
124
+ }
125
+ if (local.deleted.length > 0) {
126
+ fmt.note(
127
+ local.deleted.map(f => chalk.dim(` - ${f.registryPath}`)).join('\n'),
128
+ chalk.dim('Deleted (uncommitted)')
129
+ );
130
+ }
102
131
  }
103
132
 
104
- if (!isOnMain) {
105
- const ahead = commitsAheadOfMain(AW_HOME);
106
- const aheadStr = ahead > 0 ? ` (${chalk.yellow(`${ahead} commit${ahead !== 1 ? 's' : ''} ahead of main`)})` : '';
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`);
133
+ const ahead = commitsAheadOfMain(AW_HOME);
134
+ if (ahead > 0) {
135
+ const aheadStr = chalk.yellow(`${ahead} commit${ahead !== 1 ? 's' : ''} ahead of main`);
136
+ const branchStr = isOnMain ? '' : ` on ${chalk.yellow(branch)}`;
137
+ fmt.logWarn(`${aheadStr}${branchStr} — run ${chalk.dim('aw push')} to open a PR or ${chalk.dim('aw pull')} to sync`);
138
+ } else if (!isOnMain) {
139
+ fmt.logWarn(`On branch ${chalk.yellow(branch)} — run ${chalk.dim('aw push')} to open a PR or ${chalk.dim('aw pull')} to sync`);
108
140
  }
109
141
 
110
142
  // Hints
package/git.mjs CHANGED
@@ -404,18 +404,22 @@ export async function createPushBranch(awHome, branchName, files, commitMsg, pre
404
404
  }
405
405
 
406
406
  /**
407
- * Count commits on the current branch that are not yet in main.
407
+ * Count commits on the current branch that are not yet in origin/main.
408
+ * Falls back to local main if origin/main is not available (e.g. offline).
408
409
  */
409
410
  export function commitsAheadOfMain(awHome) {
410
- try {
411
- const out = execSync(
412
- `git -C "${awHome}" rev-list --count ${REGISTRY_BASE_BRANCH}..HEAD`,
413
- { stdio: 'pipe', encoding: 'utf8' },
414
- );
415
- return parseInt(out.trim(), 10) || 0;
416
- } catch {
417
- return 0;
411
+ for (const base of [`origin/${REGISTRY_BASE_BRANCH}`, REGISTRY_BASE_BRANCH]) {
412
+ try {
413
+ const out = execSync(
414
+ `git -C "${awHome}" rev-list --count ${base}..HEAD`,
415
+ { stdio: 'pipe', encoding: 'utf8' },
416
+ );
417
+ return parseInt(out.trim(), 10) || 0;
418
+ } catch {
419
+ continue;
420
+ }
418
421
  }
422
+ return 0;
419
423
  }
420
424
 
421
425
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.36-beta.73",
3
+ "version": "0.1.36-beta.74",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": "bin.js",