@ghl-ai/aw 0.1.37-beta.8 → 0.1.37
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/cli.mjs +12 -2
- package/commands/init.mjs +27 -7
- package/commands/link-project.mjs +3 -0
- package/commands/nuke.mjs +19 -14
- package/commands/pull.mjs +74 -32
- package/commands/push-rules.mjs +212 -0
- package/commands/push.mjs +27 -3
- package/commands/slack-sim.mjs +128 -0
- package/config.mjs +1 -1
- package/constants.mjs +8 -1
- package/ecc.mjs +80 -4
- package/file-tree.mjs +76 -0
- package/fmt.mjs +14 -0
- package/git.mjs +2 -1
- package/hooks.mjs +101 -0
- package/integrate.mjs +49 -11
- package/package.json +10 -4
- package/render-rules.mjs +483 -0
- package/slack-sim/fake-slack.mjs +200 -0
- package/slack-sim/http.mjs +170 -0
- package/slack-sim/in-process.mjs +263 -0
- package/slack-sim/render.mjs +42 -0
- package/slack-sim/scenario.mjs +64 -0
- package/slack-sim/scenarios/checkpoint-approve.json +21 -0
- package/slack-sim/scenarios/image-thread.json +27 -0
- package/slack-sim/scenarios/implementation-basic.json +18 -0
- package/slack-sim/scenarios/poll-webhook-race.json +18 -0
- package/slack-sim/scenarios/review-pr.json +14 -0
- package/telemetry.mjs +5 -3
package/cli.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { readFileSync } from 'node:fs';
|
|
|
4
4
|
import { join, dirname } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import * as fmt from './fmt.mjs';
|
|
7
|
-
import { chalk } from './fmt.mjs';
|
|
7
|
+
import { chalk, CancelError } from './fmt.mjs';
|
|
8
8
|
import { checkForUpdate, notifyUpdate } from './update.mjs';
|
|
9
9
|
import { startSpan } from './telemetry.mjs';
|
|
10
10
|
|
|
@@ -15,6 +15,7 @@ const COMMANDS = {
|
|
|
15
15
|
init: () => import('./commands/init.mjs').then(m => m.initCommand),
|
|
16
16
|
pull: () => import('./commands/pull.mjs').then(m => m.pullCommand),
|
|
17
17
|
push: () => import('./commands/push.mjs').then(m => m.pushCommand),
|
|
18
|
+
'push-rules': () => import('./commands/push-rules.mjs').then(m => m.pushRulesCommand),
|
|
18
19
|
drop: () => import('./commands/drop.mjs').then(m => m.dropCommand),
|
|
19
20
|
status: () => import('./commands/status.mjs').then(m => m.statusCommand),
|
|
20
21
|
search: () => import('./commands/search.mjs').then(m => m.searchCommand),
|
|
@@ -88,6 +89,7 @@ function printHelp() {
|
|
|
88
89
|
sec('Upload'),
|
|
89
90
|
cmd('aw push', 'Push all modified files (creates one PR)'),
|
|
90
91
|
cmd('aw push <path>', 'Push file, folder, or namespace to registry'),
|
|
92
|
+
cmd('aw push-rules [path]', 'Push platform rules to platform-docs'),
|
|
91
93
|
cmd('aw push --dry-run [path]', 'Preview what would be pushed'),
|
|
92
94
|
|
|
93
95
|
sec('Discover'),
|
|
@@ -102,6 +104,8 @@ function printHelp() {
|
|
|
102
104
|
cmd('aw daemon install --interval 30m', 'Set custom interval (e.g. 30m, 2h, 3600)'),
|
|
103
105
|
cmd('aw daemon uninstall', 'Stop the background daemon'),
|
|
104
106
|
cmd('aw daemon status', 'Check if daemon is running'),
|
|
107
|
+
cmd('aw slack-sim run <scenario>', 'Replay Slack-like scenarios against real runtime'),
|
|
108
|
+
cmd('aw slack-sim list-scenarios', 'List built-in Slack simulator scenarios'),
|
|
105
109
|
|
|
106
110
|
sec('Settings'),
|
|
107
111
|
cmd('aw telemetry status', 'Show telemetry status'),
|
|
@@ -122,6 +126,8 @@ function printHelp() {
|
|
|
122
126
|
cmd('aw push .aw_registry/<team>/', 'Push entire namespace (one PR)'),
|
|
123
127
|
cmd('aw push .aw_registry/agents/<name>.md', 'Push a single agent'),
|
|
124
128
|
cmd('aw push .aw_registry/skills/<name>/', 'Push a single skill folder'),
|
|
129
|
+
cmd('aw push .aw_rules', 'Auto-redirects to aw push-rules'),
|
|
130
|
+
cmd('aw push-rules', 'Pushes .aw_rules or .aw_registry/.aw_rules'),
|
|
125
131
|
'',
|
|
126
132
|
` ${chalk.dim('# Remove content from workspace')}`,
|
|
127
133
|
cmd('aw drop <team>', 'Stop syncing a namespace (removes all files)'),
|
|
@@ -162,6 +168,10 @@ export async function run(argv) {
|
|
|
162
168
|
await handler(args);
|
|
163
169
|
await span.end({ status: 'completed' });
|
|
164
170
|
} catch (err) {
|
|
171
|
+
if (err instanceof CancelError) {
|
|
172
|
+
await span.end({ status: 'cancelled', error_type: 'CancelError' });
|
|
173
|
+
process.exit(err.exitCode ?? 1);
|
|
174
|
+
}
|
|
165
175
|
await span.end({ status: 'failed', error_type: err.constructor.name });
|
|
166
176
|
throw err;
|
|
167
177
|
}
|
|
@@ -174,5 +184,5 @@ export async function run(argv) {
|
|
|
174
184
|
process.exit(0);
|
|
175
185
|
}
|
|
176
186
|
|
|
177
|
-
fmt.
|
|
187
|
+
fmt.cancelAndExit(`Unknown command: ${command}`);
|
|
178
188
|
}
|
package/commands/init.mjs
CHANGED
|
@@ -4,7 +4,17 @@
|
|
|
4
4
|
// Uses core.hooksPath (git-lfs pattern) for system-wide hook interception.
|
|
5
5
|
// Uses IDE tasks for auto-pull on workspace open.
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
existsSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
symlinkSync,
|
|
11
|
+
lstatSync,
|
|
12
|
+
readdirSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
realpathSync,
|
|
16
|
+
appendFileSync,
|
|
17
|
+
} from 'node:fs';
|
|
8
18
|
import { execSync } from 'node:child_process';
|
|
9
19
|
import { join, dirname, sep } from 'node:path';
|
|
10
20
|
import { homedir } from 'node:os';
|
|
@@ -15,6 +25,7 @@ import { chalk } from '../fmt.mjs';
|
|
|
15
25
|
import { linkWorkspace } from '../link.mjs';
|
|
16
26
|
import { generateCommands, copyInstructions, initAwDocs } from '../integrate.mjs';
|
|
17
27
|
import { setupMcp } from '../mcp.mjs';
|
|
28
|
+
import { installLocalCommitHook } from '../hooks.mjs';
|
|
18
29
|
import { autoUpdate, promptUpdate } from '../update.mjs';
|
|
19
30
|
import { installGlobalHooks } from '../hooks.mjs';
|
|
20
31
|
import { installAwEcc } from '../ecc.mjs';
|
|
@@ -31,7 +42,8 @@ import {
|
|
|
31
42
|
syncWorktreeSparseCheckout,
|
|
32
43
|
findNearestWorktree,
|
|
33
44
|
} from '../git.mjs';
|
|
34
|
-
import { REGISTRY_DIR, REGISTRY_REPO, REGISTRY_URL } from '../constants.mjs';
|
|
45
|
+
import { REGISTRY_DIR, REGISTRY_REPO, REGISTRY_URL, RULES_SOURCE_DIR } from '../constants.mjs';
|
|
46
|
+
import { syncFileTree } from '../file-tree.mjs';
|
|
35
47
|
|
|
36
48
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
37
49
|
const VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version;
|
|
@@ -195,7 +207,7 @@ export async function initCommand(args) {
|
|
|
195
207
|
}
|
|
196
208
|
|
|
197
209
|
if (choice === 'platform-only') {
|
|
198
|
-
namespace =
|
|
210
|
+
namespace = 'platform'; team = 'platform'; subTeam = null; folderName = null;
|
|
199
211
|
}
|
|
200
212
|
}
|
|
201
213
|
|
|
@@ -210,7 +222,7 @@ export async function initCommand(args) {
|
|
|
210
222
|
const isNewSubTeam = folderName && cfg && !cfg.include.includes(folderName);
|
|
211
223
|
if (isNewSubTeam) {
|
|
212
224
|
if (!silent) fmt.logStep(`Adding sub-team ${chalk.cyan(folderName)}...`);
|
|
213
|
-
const newSparsePaths = [`.aw_registry/${folderName}`,
|
|
225
|
+
const newSparsePaths = [`.aw_registry/${folderName}`, 'content', RULES_SOURCE_DIR];
|
|
214
226
|
addToSparseCheckout(AW_HOME, newSparsePaths);
|
|
215
227
|
config.addPattern(GLOBAL_AW_DIR, folderName);
|
|
216
228
|
} else {
|
|
@@ -235,6 +247,9 @@ export async function initCommand(args) {
|
|
|
235
247
|
|
|
236
248
|
ensureAwGitignore(AW_HOME);
|
|
237
249
|
const freshCfg = config.load(GLOBAL_AW_DIR);
|
|
250
|
+
if (existsSync(GLOBAL_AW_DIR)) {
|
|
251
|
+
syncFileTree(join(AW_HOME, RULES_SOURCE_DIR), join(GLOBAL_AW_DIR, RULES_SOURCE_DIR));
|
|
252
|
+
}
|
|
238
253
|
|
|
239
254
|
// Ensure project worktree sparse checkout matches the global clone.
|
|
240
255
|
// Covers the case where a namespace was added from HOME (or another project)
|
|
@@ -279,6 +294,7 @@ export async function initCommand(args) {
|
|
|
279
294
|
const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
|
|
280
295
|
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
281
296
|
const commands = generateCommands(HOME, { silent: true });
|
|
297
|
+
if (cwd !== HOME) installLocalCommitHook(cwd);
|
|
282
298
|
|
|
283
299
|
if (silent) {
|
|
284
300
|
autoUpdate(await args._updateCheck);
|
|
@@ -317,7 +333,7 @@ export async function initCommand(args) {
|
|
|
317
333
|
}
|
|
318
334
|
|
|
319
335
|
// Determine sparse paths
|
|
320
|
-
const sparsePaths = [`.aw_registry/platform`, `content`, `.aw_registry/AW-PROTOCOL.md`, `CODEOWNERS`];
|
|
336
|
+
const sparsePaths = [`.aw_registry/platform`, `content`, RULES_SOURCE_DIR, `.aw_registry/AW-PROTOCOL.md`, `CODEOWNERS`];
|
|
321
337
|
if (folderName) {
|
|
322
338
|
sparsePaths.push(`.aw_registry/${folderName}`);
|
|
323
339
|
}
|
|
@@ -363,11 +379,14 @@ export async function initCommand(args) {
|
|
|
363
379
|
}
|
|
364
380
|
}
|
|
365
381
|
|
|
366
|
-
// Create sync config
|
|
367
|
-
const cfg = config.create(GLOBAL_AW_DIR, { namespace: team, user });
|
|
382
|
+
// Create sync config — default to 'platform' when no namespace specified
|
|
383
|
+
const cfg = config.create(GLOBAL_AW_DIR, { namespace: team || 'platform', user });
|
|
368
384
|
if (folderName) {
|
|
369
385
|
config.addPattern(GLOBAL_AW_DIR, folderName);
|
|
370
386
|
}
|
|
387
|
+
if (existsSync(GLOBAL_AW_DIR)) {
|
|
388
|
+
syncFileTree(join(AW_HOME, RULES_SOURCE_DIR), join(GLOBAL_AW_DIR, RULES_SOURCE_DIR));
|
|
389
|
+
}
|
|
371
390
|
|
|
372
391
|
// Step 3: Setup tasks, MCP, hooks
|
|
373
392
|
await installAwEcc(cwd, { silent });
|
|
@@ -406,6 +425,7 @@ export async function initCommand(args) {
|
|
|
406
425
|
const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
|
|
407
426
|
const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
|
|
408
427
|
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
428
|
+
if (cwd !== HOME) installLocalCommitHook(cwd);
|
|
409
429
|
ideSpinner.message('Generating commands...');
|
|
410
430
|
const commands = generateCommands(HOME, { silent: true });
|
|
411
431
|
ideSpinner.stop(`IDE wired — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`);
|
|
@@ -9,6 +9,7 @@ import { addProjectWorktree, isWorktree, isValidClone } from '../git.mjs';
|
|
|
9
9
|
import { REGISTRY_DIR, REGISTRY_URL } from '../constants.mjs';
|
|
10
10
|
import { linkWorkspace } from '../link.mjs';
|
|
11
11
|
import { generateCommands } from '../integrate.mjs';
|
|
12
|
+
import { installLocalCommitHook } from '../hooks.mjs';
|
|
12
13
|
|
|
13
14
|
const HOME = homedir();
|
|
14
15
|
const AW_HOME = join(HOME, '.aw');
|
|
@@ -40,6 +41,7 @@ export function linkProjectCommand(args) {
|
|
|
40
41
|
const awDirForLinks = existsSync(projectRegistryDir) ? projectRegistryDir : null;
|
|
41
42
|
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
42
43
|
const commands = generateCommands(HOME, { silent: true });
|
|
44
|
+
installLocalCommitHook(cwd);
|
|
43
45
|
fmt.logSuccess(`Already linked — refreshed ${chalk.bold(symlinks)} IDE symlinks · ${chalk.bold(commands)} commands`);
|
|
44
46
|
return;
|
|
45
47
|
}
|
|
@@ -50,6 +52,7 @@ export function linkProjectCommand(args) {
|
|
|
50
52
|
const awDirForLinks = existsSync(projectRegistryDir) ? projectRegistryDir : null;
|
|
51
53
|
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
52
54
|
const commands = generateCommands(HOME, { silent: true });
|
|
55
|
+
installLocalCommitHook(cwd);
|
|
53
56
|
fmt.logSuccess([
|
|
54
57
|
`Project linked — ${chalk.bold(symlinks)} IDE symlinks · ${chalk.bold(commands)} commands`,
|
|
55
58
|
'',
|
package/commands/nuke.mjs
CHANGED
|
@@ -143,19 +143,24 @@ async function removeProjectSymlinks() {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
// Also remove legacy local .git/hooks/post-checkout installed by old aw versions
|
|
146
|
+
// and prepare-commit-msg hooks installed by installLocalCommitHook
|
|
146
147
|
let hooksRemoved = 0;
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
{
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
148
|
+
const hookNames = ['post-checkout', 'prepare-commit-msg'];
|
|
149
|
+
for (const hookName of hookNames) {
|
|
150
|
+
const { stdout: hookFiles } = await exec(
|
|
151
|
+
`find "${HOME}" -maxdepth 5 -path "*/.git/hooks/${hookName}" -type f 2>/dev/null || true`,
|
|
152
|
+
{ encoding: 'utf8', timeout: 30000 }
|
|
153
|
+
);
|
|
154
|
+
for (const hookPath of hookFiles.trim().split('\n').filter(Boolean)) {
|
|
155
|
+
try {
|
|
156
|
+
const content = readFileSync(hookPath, 'utf8');
|
|
157
|
+
// Only remove hooks that AW installed — identified by our marker comment
|
|
158
|
+
if (content.includes('aw:') || content.includes('aw: auto-link registry') || content.includes('ln -s "$AW_REGISTRY"')) {
|
|
159
|
+
unlinkSync(hookPath);
|
|
160
|
+
hooksRemoved++;
|
|
161
|
+
}
|
|
162
|
+
} catch { /* best effort */ }
|
|
163
|
+
}
|
|
159
164
|
}
|
|
160
165
|
|
|
161
166
|
return { removed, hooksRemoved };
|
|
@@ -204,8 +209,8 @@ function removeIdeTasks() {
|
|
|
204
209
|
|
|
205
210
|
export async function nukeCommand(args) {
|
|
206
211
|
// Catch unhandled errors and surface them instead of letting clack show generic "Something went wrong"
|
|
207
|
-
process.on('uncaughtException', (e) => { fmt.
|
|
208
|
-
process.on('unhandledRejection', (e) => { fmt.
|
|
212
|
+
process.on('uncaughtException', (e) => { fmt.cancelAndExit(`Unexpected error: ${e.message}`); });
|
|
213
|
+
process.on('unhandledRejection', (e) => { fmt.cancelAndExit(`Unexpected error: ${e?.message ?? e}`); });
|
|
209
214
|
|
|
210
215
|
fmt.intro('aw nuke');
|
|
211
216
|
|
package/commands/pull.mjs
CHANGED
|
@@ -1,17 +1,34 @@
|
|
|
1
1
|
// commands/pull.mjs — Pull content from registry using persistent git clone
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
lstatSync,
|
|
6
|
+
} from 'node:fs';
|
|
4
7
|
import { join, extname } from 'node:path';
|
|
5
8
|
import { homedir } from 'node:os';
|
|
6
|
-
import { exec as execCb } from 'node:child_process';
|
|
9
|
+
import { exec as execCb, execSync } from 'node:child_process';
|
|
7
10
|
import { promisify } from 'node:util';
|
|
8
11
|
|
|
9
12
|
const exec = promisify(execCb);
|
|
10
13
|
import * as config from '../config.mjs';
|
|
11
14
|
import * as fmt from '../fmt.mjs';
|
|
12
15
|
import { chalk } from '../fmt.mjs';
|
|
13
|
-
import {
|
|
14
|
-
|
|
16
|
+
import {
|
|
17
|
+
fetchAndMerge,
|
|
18
|
+
addToSparseCheckout,
|
|
19
|
+
removeFromSparseCheckout,
|
|
20
|
+
syncWorktreeSparseCheckout,
|
|
21
|
+
isValidClone,
|
|
22
|
+
findNearestWorktree,
|
|
23
|
+
rebaseOntoOriginMain,
|
|
24
|
+
} from '../git.mjs';
|
|
25
|
+
import {
|
|
26
|
+
REGISTRY_DIR,
|
|
27
|
+
REGISTRY_URL,
|
|
28
|
+
DOCS_SOURCE_DIR,
|
|
29
|
+
RULES_SOURCE_DIR,
|
|
30
|
+
} from '../constants.mjs';
|
|
31
|
+
import { collectAllPaths, syncFileTree } from '../file-tree.mjs';
|
|
15
32
|
import { linkWorkspace } from '../link.mjs';
|
|
16
33
|
import { generateCommands, copyInstructions } from '../integrate.mjs';
|
|
17
34
|
|
|
@@ -25,7 +42,7 @@ export async function pullCommand(args) {
|
|
|
25
42
|
const silent = args['--silent'] === true || args._silent === true;
|
|
26
43
|
|
|
27
44
|
const log = {
|
|
28
|
-
cancel: silent ? () => {
|
|
45
|
+
cancel: silent ? (msg) => { throw new fmt.CancelError(msg || 'silent cancel', { exitCode: 0 }); } : fmt.cancel,
|
|
29
46
|
logInfo: silent ? () => {} : fmt.logInfo,
|
|
30
47
|
logSuccess: silent ? () => {} : fmt.logSuccess,
|
|
31
48
|
logStep: silent ? () => {} : fmt.logStep,
|
|
@@ -52,6 +69,15 @@ export async function pullCommand(args) {
|
|
|
52
69
|
return;
|
|
53
70
|
}
|
|
54
71
|
|
|
72
|
+
// Ensure platform pulls also fetch docs and rules on older installs that
|
|
73
|
+
// pre-date the new sparse-checkout paths.
|
|
74
|
+
if (input === 'platform') {
|
|
75
|
+
addToSparseCheckout(AW_HOME, [`.aw_registry/platform`, DOCS_SOURCE_DIR, RULES_SOURCE_DIR]);
|
|
76
|
+
if (!cfg.include.includes('platform')) {
|
|
77
|
+
config.addPattern(GLOBAL_AW_DIR, 'platform');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
55
81
|
// If input is a new namespace to add
|
|
56
82
|
let addedInput = null;
|
|
57
83
|
let addedSparsePath = null;
|
|
@@ -64,7 +90,7 @@ export async function pullCommand(args) {
|
|
|
64
90
|
const label = input.split('/').pop();
|
|
65
91
|
if (!cfg.include.includes(input)) {
|
|
66
92
|
log.logStep(`Adding ${chalk.cyan(label)} to sparse checkout...`);
|
|
67
|
-
addToSparseCheckout(AW_HOME, [sparsePath,
|
|
93
|
+
addToSparseCheckout(AW_HOME, [sparsePath, DOCS_SOURCE_DIR]);
|
|
68
94
|
config.addPattern(GLOBAL_AW_DIR, input);
|
|
69
95
|
addedInput = input;
|
|
70
96
|
addedSparsePath = sparsePath;
|
|
@@ -79,33 +105,33 @@ export async function pullCommand(args) {
|
|
|
79
105
|
const rebaseInProgress = existsSync(join(awGitDir, 'rebase-merge')) || existsSync(join(awGitDir, 'rebase-apply'));
|
|
80
106
|
const mergeInProgress = existsSync(join(awGitDir, 'MERGE_HEAD'));
|
|
81
107
|
if (rebaseInProgress || mergeInProgress) {
|
|
82
|
-
// Check for still-unresolved files (conflict markers present in index)
|
|
83
108
|
let unresolved = [];
|
|
84
109
|
try {
|
|
85
110
|
const { stdout } = await exec(`git -C "${AW_HOME}" diff --name-only --diff-filter=U`);
|
|
86
111
|
unresolved = stdout.trim().split('\n').filter(Boolean);
|
|
87
|
-
} catch {
|
|
112
|
+
} catch {
|
|
113
|
+
// best effort
|
|
114
|
+
}
|
|
88
115
|
|
|
89
116
|
if (unresolved.length > 0) {
|
|
90
|
-
|
|
91
|
-
log.logWarn(`Rebase paused — resolve conflicts in your IDE, then run \`aw pull\` again.`);
|
|
117
|
+
log.logWarn('Rebase paused — resolve conflicts in your IDE, then run `aw pull` again.');
|
|
92
118
|
if (!silent) fmt.outro(chalk.yellow('Pull skipped'));
|
|
93
119
|
return;
|
|
94
120
|
}
|
|
95
121
|
|
|
96
|
-
// All conflicts resolved (files are staged) — continue the rebase automatically
|
|
97
122
|
try {
|
|
98
123
|
await exec(`git -C "${AW_HOME}" rebase --continue`, { env: { ...process.env, GIT_EDITOR: 'true' } });
|
|
99
124
|
log.logStep('Rebase continued after conflict resolution.');
|
|
100
|
-
// Force-push if on a push branch so origin stays in sync
|
|
101
125
|
const { stdout: branchOut } = await exec(`git -C "${AW_HOME}" rev-parse --abbrev-ref HEAD`);
|
|
102
126
|
const resumedBranch = branchOut.trim();
|
|
103
|
-
if (['upload/', 'remove/', 'sync/'].some(
|
|
104
|
-
try {
|
|
127
|
+
if (['upload/', 'remove/', 'sync/'].some(prefix => resumedBranch.startsWith(prefix))) {
|
|
128
|
+
try {
|
|
129
|
+
await exec(`git -C "${AW_HOME}" push --force-with-lease origin "${resumedBranch}"`);
|
|
130
|
+
} catch {
|
|
131
|
+
// non-blocking
|
|
132
|
+
}
|
|
105
133
|
}
|
|
106
134
|
} catch {
|
|
107
|
-
// Could happen if there are more conflicting commits in the rebase sequence,
|
|
108
|
-
// or if the resolved changes result in an empty commit (skip it).
|
|
109
135
|
try {
|
|
110
136
|
await exec(`git -C "${AW_HOME}" rebase --skip`);
|
|
111
137
|
log.logStep('Empty commit skipped during rebase continuation.');
|
|
@@ -115,10 +141,8 @@ export async function pullCommand(args) {
|
|
|
115
141
|
return;
|
|
116
142
|
}
|
|
117
143
|
}
|
|
118
|
-
// Fall through to re-link IDE dirs after successful rebase continuation
|
|
119
144
|
}
|
|
120
145
|
|
|
121
|
-
// Fetch + merge latest
|
|
122
146
|
const s = log.spinner();
|
|
123
147
|
s.start('Fetching latest from registry...');
|
|
124
148
|
let fetchResult = { updated: false, conflicts: [] };
|
|
@@ -130,16 +154,15 @@ export async function pullCommand(args) {
|
|
|
130
154
|
if (!silent) log.logWarn(`Fetch error: ${e.message}`);
|
|
131
155
|
}
|
|
132
156
|
|
|
133
|
-
// Validate that the requested path actually exists in the registry after fetch.
|
|
134
|
-
// If not, undo the sparse checkout addition and inform the user.
|
|
135
157
|
if (addedInput) {
|
|
136
158
|
const localPath = join(AW_HOME, REGISTRY_DIR, addedInput);
|
|
137
159
|
if (!existsSync(localPath)) {
|
|
138
|
-
// Undo: remove from sparse checkout and config
|
|
139
160
|
try {
|
|
140
161
|
removeFromSparseCheckout(AW_HOME, [addedSparsePath]);
|
|
141
162
|
config.removePattern(GLOBAL_AW_DIR, addedInput);
|
|
142
|
-
} catch {
|
|
163
|
+
} catch {
|
|
164
|
+
// best effort
|
|
165
|
+
}
|
|
143
166
|
log.cancel(`Path ${chalk.red(addedInput)} not found in the registry.\nCheck the exact path with ${chalk.bold('aw search <query>')} or browse ${chalk.dim('~/.aw_registry/')}.`);
|
|
144
167
|
return;
|
|
145
168
|
}
|
|
@@ -147,23 +170,31 @@ export async function pullCommand(args) {
|
|
|
147
170
|
|
|
148
171
|
if (fetchResult.conflicts.length > 0) {
|
|
149
172
|
if (!silent) {
|
|
150
|
-
// Interactive mode: rebase is paused with conflict markers in the working tree.
|
|
151
|
-
// Leave it for the user to resolve in their IDE, then re-run `aw pull`.
|
|
152
173
|
log.logWarn(`Merge conflict in: ${fetchResult.conflicts.join(', ')}`);
|
|
153
174
|
log.logWarn('Merge aborted — your branch is unchanged. Resolve conflicts and run `aw pull` again.');
|
|
154
175
|
return;
|
|
155
176
|
}
|
|
156
|
-
// Silent mode: rebase was already aborted in fetchAndMerge; just report.
|
|
157
177
|
log.logWarn(`Conflicts in: ${fetchResult.conflicts.join(', ')}`);
|
|
158
178
|
}
|
|
159
179
|
|
|
180
|
+
const rulesSrc = join(AW_HOME, RULES_SOURCE_DIR);
|
|
181
|
+
if (existsSync(rulesSrc)) {
|
|
182
|
+
const rulesDest = join(GLOBAL_AW_DIR, RULES_SOURCE_DIR);
|
|
183
|
+
syncFileTree(rulesSrc, rulesDest);
|
|
184
|
+
if (!silent) log.logSuccess('Synced .aw_rules');
|
|
185
|
+
}
|
|
186
|
+
|
|
160
187
|
// Rebase project worktree branch onto origin/main — only for legacy git worktrees.
|
|
161
188
|
// In the symlink model, <project>/.aw IS ~/.aw (same repo), so fetchAndMerge already
|
|
162
189
|
// brought it up to date. Nothing to rebase.
|
|
163
190
|
const localAw = cwd !== HOME ? findNearestWorktree(cwd, HOME) : null;
|
|
164
191
|
let isSymlinkWorktree = false;
|
|
165
192
|
if (localAw) {
|
|
166
|
-
try {
|
|
193
|
+
try {
|
|
194
|
+
isSymlinkWorktree = lstatSync(localAw).isSymbolicLink();
|
|
195
|
+
} catch {
|
|
196
|
+
// ignore
|
|
197
|
+
}
|
|
167
198
|
}
|
|
168
199
|
if (localAw && !isSymlinkWorktree) {
|
|
169
200
|
const rebaseSpinner = log.spinner();
|
|
@@ -173,7 +204,9 @@ export async function pullCommand(args) {
|
|
|
173
204
|
rebaseSpinner.stop('Local branch up to date');
|
|
174
205
|
} catch (e) {
|
|
175
206
|
const isConflict = e.message?.includes('could not apply') || e.message?.includes('CONFLICT');
|
|
176
|
-
const isAlreadyInProgress = e.message?.includes('rebase-merge')
|
|
207
|
+
const isAlreadyInProgress = e.message?.includes('rebase-merge')
|
|
208
|
+
|| e.message?.includes('rebase-apply')
|
|
209
|
+
|| e.message?.includes('already in progress');
|
|
177
210
|
|
|
178
211
|
if (isAlreadyInProgress) {
|
|
179
212
|
rebaseSpinner.stop(chalk.yellow('Rebase paused — conflicts pending'));
|
|
@@ -184,26 +217,31 @@ export async function pullCommand(args) {
|
|
|
184
217
|
try {
|
|
185
218
|
const { stdout } = await exec(`git -C "${localAw}" diff --name-only --diff-filter=U`);
|
|
186
219
|
conflictedFiles = stdout.trim().split('\n').filter(Boolean);
|
|
187
|
-
} catch {
|
|
220
|
+
} catch {
|
|
221
|
+
// best effort
|
|
222
|
+
}
|
|
188
223
|
|
|
189
224
|
if (!silent) {
|
|
190
|
-
try {
|
|
225
|
+
try {
|
|
226
|
+
await exec(`git -C "${localAw}" rebase --abort`);
|
|
227
|
+
} catch {
|
|
228
|
+
// best effort
|
|
229
|
+
}
|
|
191
230
|
log.logWarn('Rebase aborted — your branch is unchanged.');
|
|
192
231
|
if (conflictedFiles.length > 0) {
|
|
193
232
|
log.logWarn(`Conflicting file${conflictedFiles.length > 1 ? 's' : ''}:`);
|
|
194
|
-
for (const
|
|
233
|
+
for (const file of conflictedFiles) log.logMessage(` ${chalk.red('✗')} ${file}`);
|
|
195
234
|
}
|
|
196
235
|
log.logWarn('Resolve the conflict and run `aw pull` again, or check with `aw status`.');
|
|
197
236
|
}
|
|
198
237
|
} else {
|
|
199
|
-
const msg = e.message?.split('\n').find(
|
|
238
|
+
const msg = e.message?.split('\n').find(line => line.trim()) ?? e.message;
|
|
200
239
|
rebaseSpinner.stop(chalk.dim('Rebase skipped'));
|
|
201
240
|
if (!silent) log.logWarn(`Rebase skipped: ${msg}`);
|
|
202
241
|
}
|
|
203
242
|
}
|
|
204
243
|
}
|
|
205
244
|
|
|
206
|
-
// Re-link IDE dirs
|
|
207
245
|
if (!args._skipIntegrate) {
|
|
208
246
|
const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
|
|
209
247
|
const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
|
|
@@ -237,6 +275,10 @@ export async function pullAsync(args) {
|
|
|
237
275
|
return { pattern: args._positional?.[0] || '', actions: [], conflictCount: 0 };
|
|
238
276
|
}
|
|
239
277
|
|
|
278
|
+
export const __test__ = {
|
|
279
|
+
collectAllPaths,
|
|
280
|
+
syncFileTree,
|
|
281
|
+
};
|
|
240
282
|
|
|
241
283
|
function registerMcp(namespace) {
|
|
242
284
|
const mcpUrl = process.env.GHL_MCP_URL;
|