@ghl-ai/aw 0.1.38-beta.1 → 0.1.38-beta.3
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 +25 -4
- package/commands/init.mjs +7 -4
- package/commands/link-project.mjs +4 -0
- package/commands/nuke.mjs +19 -14
- package/commands/pull.mjs +1 -1
- package/commands/push-rules.mjs +2 -2
- package/commands/push.mjs +11 -4
- package/commands/slack-sim.mjs +128 -0
- package/commands/telemetry.mjs +31 -0
- package/config.mjs +1 -1
- package/constants.mjs +7 -0
- package/ecc.mjs +83 -4
- package/fmt.mjs +14 -0
- package/hooks.mjs +106 -5
- package/package.json +8 -5
- 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 +233 -0
package/cli.mjs
CHANGED
|
@@ -4,8 +4,9 @@ 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
|
+
import { startSpan } from './telemetry.mjs';
|
|
9
10
|
|
|
10
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
const VERSION = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8')).version;
|
|
@@ -24,6 +25,7 @@ const COMMANDS = {
|
|
|
24
25
|
link: () => import('./commands/link-project.mjs').then(m => m.linkProjectCommand),
|
|
25
26
|
nuke: () => import('./commands/nuke.mjs').then(m => m.nukeCommand),
|
|
26
27
|
daemon: () => import('./commands/daemon.mjs').then(m => m.daemonCommand),
|
|
28
|
+
telemetry: () => import('./commands/telemetry.mjs').then(m => m.telemetryCommand),
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
function parseArgs(argv) {
|
|
@@ -109,6 +111,13 @@ function printHelp() {
|
|
|
109
111
|
cmd('aw daemon install --interval 30m', 'Set custom interval (e.g. 30m, 2h, 3600)'),
|
|
110
112
|
cmd('aw daemon uninstall', 'Stop the background daemon'),
|
|
111
113
|
cmd('aw daemon status', 'Check if daemon is running'),
|
|
114
|
+
cmd('aw slack-sim run <scenario>', 'Replay Slack-like scenarios against real runtime'),
|
|
115
|
+
cmd('aw slack-sim list-scenarios', 'List built-in Slack simulator scenarios'),
|
|
116
|
+
|
|
117
|
+
sec('Settings'),
|
|
118
|
+
cmd('aw telemetry status', 'Show telemetry status'),
|
|
119
|
+
cmd('aw telemetry disable', 'Opt out of anonymous analytics'),
|
|
120
|
+
cmd('aw telemetry enable', 'Re-enable analytics'),
|
|
112
121
|
|
|
113
122
|
sec('Examples'),
|
|
114
123
|
'',
|
|
@@ -158,9 +167,21 @@ export async function run(argv) {
|
|
|
158
167
|
}
|
|
159
168
|
|
|
160
169
|
if (command && COMMANDS[command]) {
|
|
170
|
+
const span = await startSpan(command, args);
|
|
171
|
+
span.notice();
|
|
161
172
|
args._updateCheck = updateCheck;
|
|
162
|
-
|
|
163
|
-
|
|
173
|
+
try {
|
|
174
|
+
const handler = await COMMANDS[command]();
|
|
175
|
+
await handler(args);
|
|
176
|
+
await span.end({ status: 'completed' });
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (err instanceof CancelError) {
|
|
179
|
+
await span.end({ status: 'cancelled', error_type: 'CancelError' });
|
|
180
|
+
process.exit(err.exitCode ?? 1);
|
|
181
|
+
}
|
|
182
|
+
await span.end({ status: 'failed', error_type: err.constructor.name });
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
164
185
|
notifyUpdate(await updateCheck);
|
|
165
186
|
return;
|
|
166
187
|
}
|
|
@@ -170,5 +191,5 @@ export async function run(argv) {
|
|
|
170
191
|
process.exit(0);
|
|
171
192
|
}
|
|
172
193
|
|
|
173
|
-
fmt.
|
|
194
|
+
fmt.cancelAndExit(`Unknown command: ${command}`);
|
|
174
195
|
}
|
package/commands/init.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import { linkWorkspace } from '../link.mjs';
|
|
|
26
26
|
import { generateCommands, copyInstructions, initAwDocs, syncHomeHarnessInstructions } from '../integrate.mjs';
|
|
27
27
|
import { setupMcp } from '../mcp.mjs';
|
|
28
28
|
import { applyStoredStartupPreferences, ensureAwRuntimeHook } from '../startup.mjs';
|
|
29
|
+
import { installLocalCommitHook } from '../hooks.mjs';
|
|
29
30
|
import { autoUpdate, promptUpdate } from '../update.mjs';
|
|
30
31
|
import { installGlobalHooks } from '../hooks.mjs';
|
|
31
32
|
import { installAwEcc } from '../ecc.mjs';
|
|
@@ -100,7 +101,7 @@ function installIdeTasks() {
|
|
|
100
101
|
{
|
|
101
102
|
label: 'aw: sync registry',
|
|
102
103
|
type: 'shell',
|
|
103
|
-
command: 'aw init --silent',
|
|
104
|
+
command: 'AW_TRIGGER=ide:task aw init --silent',
|
|
104
105
|
presentation: { reveal: 'silent', panel: 'shared', close: true },
|
|
105
106
|
runOptions: { runOn: 'folderOpen' },
|
|
106
107
|
problemMatcher: [],
|
|
@@ -221,7 +222,7 @@ export async function initCommand(args) {
|
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
if (choice === 'platform-only') {
|
|
224
|
-
namespace =
|
|
225
|
+
namespace = 'platform'; team = 'platform'; subTeam = null; folderName = null;
|
|
225
226
|
}
|
|
226
227
|
}
|
|
227
228
|
|
|
@@ -310,6 +311,7 @@ export async function initCommand(args) {
|
|
|
310
311
|
const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
|
|
311
312
|
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
312
313
|
const commands = generateCommands(HOME, { silent: true });
|
|
314
|
+
if (cwd !== HOME) installLocalCommitHook(cwd);
|
|
313
315
|
|
|
314
316
|
if (silent) {
|
|
315
317
|
autoUpdate(await args._updateCheck);
|
|
@@ -397,8 +399,8 @@ export async function initCommand(args) {
|
|
|
397
399
|
}
|
|
398
400
|
}
|
|
399
401
|
|
|
400
|
-
// Create sync config
|
|
401
|
-
const cfg = config.create(GLOBAL_AW_DIR, { namespace: team, user });
|
|
402
|
+
// Create sync config — default to 'platform' when no namespace specified
|
|
403
|
+
const cfg = config.create(GLOBAL_AW_DIR, { namespace: team || 'platform', user });
|
|
402
404
|
if (folderName) {
|
|
403
405
|
config.addPattern(GLOBAL_AW_DIR, folderName);
|
|
404
406
|
}
|
|
@@ -445,6 +447,7 @@ export async function initCommand(args) {
|
|
|
445
447
|
const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
|
|
446
448
|
const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
|
|
447
449
|
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
450
|
+
if (cwd !== HOME) installLocalCommitHook(cwd);
|
|
448
451
|
ideSpinner.message('Generating commands...');
|
|
449
452
|
const commands = generateCommands(HOME, { silent: true });
|
|
450
453
|
ideSpinner.stop(`IDE wired — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`);
|
|
@@ -11,6 +11,7 @@ import { linkWorkspace } from '../link.mjs';
|
|
|
11
11
|
import { generateCommands } from '../integrate.mjs';
|
|
12
12
|
import { removeWorkspaceHookDefaults } from '../codex.mjs';
|
|
13
13
|
import { applyStoredStartupPreferences } from '../startup.mjs';
|
|
14
|
+
import { installLocalCommitHook } from '../hooks.mjs';
|
|
14
15
|
|
|
15
16
|
const HOME = homedir();
|
|
16
17
|
const AW_HOME = join(HOME, '.aw');
|
|
@@ -43,8 +44,10 @@ export function linkProjectCommand(args) {
|
|
|
43
44
|
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
44
45
|
const commands = generateCommands(HOME, { silent: true });
|
|
45
46
|
applyStoredStartupPreferences(HOME);
|
|
47
|
+
installLocalCommitHook(cwd);
|
|
46
48
|
const removedLegacyStartupFiles = removeWorkspaceHookDefaults(cwd);
|
|
47
49
|
fmt.logSuccess(`Already linked — refreshed ${chalk.bold(symlinks)} IDE symlinks · ${chalk.bold(commands)} commands${removedLegacyStartupFiles.length > 0 ? ` · removed ${removedLegacyStartupFiles.length} legacy repo startup file${removedLegacyStartupFiles.length > 1 ? 's' : ''}` : ''}`);
|
|
50
|
+
|
|
48
51
|
return;
|
|
49
52
|
}
|
|
50
53
|
|
|
@@ -55,6 +58,7 @@ export function linkProjectCommand(args) {
|
|
|
55
58
|
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
56
59
|
const commands = generateCommands(HOME, { silent: true });
|
|
57
60
|
applyStoredStartupPreferences(HOME);
|
|
61
|
+
installLocalCommitHook(cwd);
|
|
58
62
|
const removedLegacyStartupFiles = removeWorkspaceHookDefaults(cwd);
|
|
59
63
|
fmt.logSuccess([
|
|
60
64
|
`Project linked — ${chalk.bold(symlinks)} IDE symlinks · ${chalk.bold(commands)} commands`,
|
package/commands/nuke.mjs
CHANGED
|
@@ -148,19 +148,24 @@ async function removeProjectSymlinks() {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
// Also remove legacy local .git/hooks/post-checkout installed by old aw versions
|
|
151
|
+
// and prepare-commit-msg hooks installed by installLocalCommitHook
|
|
151
152
|
let hooksRemoved = 0;
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
{
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
153
|
+
const hookNames = ['post-checkout', 'prepare-commit-msg'];
|
|
154
|
+
for (const hookName of hookNames) {
|
|
155
|
+
const { stdout: hookFiles } = await exec(
|
|
156
|
+
`find "${HOME}" -maxdepth 5 -path "*/.git/hooks/${hookName}" -type f 2>/dev/null || true`,
|
|
157
|
+
{ encoding: 'utf8', timeout: 30000 }
|
|
158
|
+
);
|
|
159
|
+
for (const hookPath of hookFiles.trim().split('\n').filter(Boolean)) {
|
|
160
|
+
try {
|
|
161
|
+
const content = readFileSync(hookPath, 'utf8');
|
|
162
|
+
// Only remove hooks that AW installed — identified by our marker comment
|
|
163
|
+
if (content.includes('aw:') || content.includes('aw: auto-link registry') || content.includes('ln -s "$AW_REGISTRY"')) {
|
|
164
|
+
unlinkSync(hookPath);
|
|
165
|
+
hooksRemoved++;
|
|
166
|
+
}
|
|
167
|
+
} catch { /* best effort */ }
|
|
168
|
+
}
|
|
164
169
|
}
|
|
165
170
|
|
|
166
171
|
return { removed, hooksRemoved };
|
|
@@ -209,8 +214,8 @@ function removeIdeTasks() {
|
|
|
209
214
|
|
|
210
215
|
export async function nukeCommand(args) {
|
|
211
216
|
// Catch unhandled errors and surface them instead of letting clack show generic "Something went wrong"
|
|
212
|
-
process.on('uncaughtException', (e) => { fmt.
|
|
213
|
-
process.on('unhandledRejection', (e) => { fmt.
|
|
217
|
+
process.on('uncaughtException', (e) => { fmt.cancelAndExit(`Unexpected error: ${e.message}`); });
|
|
218
|
+
process.on('unhandledRejection', (e) => { fmt.cancelAndExit(`Unexpected error: ${e?.message ?? e}`); });
|
|
214
219
|
|
|
215
220
|
fmt.intro('aw nuke');
|
|
216
221
|
|
package/commands/pull.mjs
CHANGED
|
@@ -44,7 +44,7 @@ export async function pullCommand(args) {
|
|
|
44
44
|
const silent = args['--silent'] === true || args._silent === true;
|
|
45
45
|
|
|
46
46
|
const log = {
|
|
47
|
-
cancel: silent ? () => {
|
|
47
|
+
cancel: silent ? (msg) => { throw new fmt.CancelError(msg || 'silent cancel', { exitCode: 0 }); } : fmt.cancel,
|
|
48
48
|
logInfo: silent ? () => {} : fmt.logInfo,
|
|
49
49
|
logSuccess: silent ? () => {} : fmt.logSuccess,
|
|
50
50
|
logStep: silent ? () => {} : fmt.logStep,
|
package/commands/push-rules.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { tmpdir } from 'node:os';
|
|
|
5
5
|
|
|
6
6
|
import * as fmt from '../fmt.mjs';
|
|
7
7
|
import { chalk } from '../fmt.mjs';
|
|
8
|
-
import { REGISTRY_BASE_BRANCH, REGISTRY_REPO, RULES_SOURCE_DIR } from '../constants.mjs';
|
|
8
|
+
import { REGISTRY_BASE_BRANCH, REGISTRY_REPO, RULES_SOURCE_DIR, AW_CO_AUTHOR } from '../constants.mjs';
|
|
9
9
|
import { syncFileTree } from '../file-tree.mjs';
|
|
10
10
|
|
|
11
11
|
function normalizeSlashes(path) {
|
|
@@ -147,7 +147,7 @@ function pushRulesTree(sourceRoot, { repo, dryRun, cwd }) {
|
|
|
147
147
|
const prTitle = buildRulesPrTitle(sourceRoot, cwd);
|
|
148
148
|
const prBody = buildRulesPrBody(sourceRoot, sourceType, cwd);
|
|
149
149
|
|
|
150
|
-
execSync(
|
|
150
|
+
execSync(`git commit -m "registry: sync platform rules\n\n${AW_CO_AUTHOR}"`, { cwd: tempDir, stdio: 'pipe' });
|
|
151
151
|
s2.stop('Rules sync prepared');
|
|
152
152
|
|
|
153
153
|
const s3 = fmt.spinner();
|
package/commands/push.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// commands/push.mjs — Push local agents/skills to registry via PR using persistent git clone
|
|
2
2
|
|
|
3
3
|
import { existsSync, statSync, readFileSync, appendFileSync } from 'node:fs';
|
|
4
|
-
import { join } from 'node:path';
|
|
4
|
+
import { join, dirname } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { exec as execCb, execFile as execFileCb } from 'node:child_process';
|
|
6
7
|
import { promisify } from 'node:util';
|
|
7
8
|
import { homedir } from 'node:os';
|
|
@@ -10,7 +11,7 @@ const exec = promisify(execCb);
|
|
|
10
11
|
const execFile = promisify(execFileCb);
|
|
11
12
|
import * as fmt from '../fmt.mjs';
|
|
12
13
|
import { chalk } from '../fmt.mjs';
|
|
13
|
-
import { REGISTRY_REPO, REGISTRY_URL, REGISTRY_BASE_BRANCH, REGISTRY_DIR } from '../constants.mjs';
|
|
14
|
+
import { REGISTRY_REPO, REGISTRY_URL, REGISTRY_BASE_BRANCH, REGISTRY_DIR, AW_CO_AUTHOR } from '../constants.mjs';
|
|
14
15
|
import { resolveInput } from '../paths.mjs';
|
|
15
16
|
import { walkRegistryTree } from '../registry.mjs';
|
|
16
17
|
import {
|
|
@@ -25,6 +26,9 @@ import {
|
|
|
25
26
|
} from '../git.mjs';
|
|
26
27
|
import { hasRulesChanges, isRulesPushInput, pushRulesCommand } from './push-rules.mjs';
|
|
27
28
|
|
|
29
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
const VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version;
|
|
31
|
+
|
|
28
32
|
const PUSHABLE_TYPES = ['agents', 'skills', 'commands', 'evals', 'references'];
|
|
29
33
|
|
|
30
34
|
// ── PR content generation ────────────────────────────────────────────
|
|
@@ -187,11 +191,14 @@ function generateCommitMsg(files) {
|
|
|
187
191
|
const deletedParts = Object.entries(groupBy(deleted, 'type')).map(([t, items]) => `${items.length} ${singular(t, items.length)} removed`);
|
|
188
192
|
const countParts = [...addedParts, ...deletedParts];
|
|
189
193
|
|
|
194
|
+
const version = VERSION;
|
|
195
|
+
const trailer = `\n\nGenerated-By: aw/${version}\n${AW_CO_AUTHOR}`;
|
|
196
|
+
|
|
190
197
|
if (files.length === 1) {
|
|
191
198
|
const f = files[0];
|
|
192
|
-
return `registry: ${f.deleted ? 'remove' : 'add'} ${f.type}/${f.slug} ${f.deleted ? 'from' : 'to'} ${f.namespace}`;
|
|
199
|
+
return `registry: ${f.deleted ? 'remove' : 'add'} ${f.type}/${f.slug} ${f.deleted ? 'from' : 'to'} ${f.namespace}${trailer}`;
|
|
193
200
|
}
|
|
194
|
-
return `registry: sync ${files.length} files (${countParts.join(', ')})`;
|
|
201
|
+
return `registry: sync ${files.length} files (${countParts.join(', ')})${trailer}`;
|
|
195
202
|
}
|
|
196
203
|
|
|
197
204
|
// ── Batch file collection from folder ────────────────────────────────
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import * as fmt from '../fmt.mjs';
|
|
2
|
+
import { loadScenario, listBuiltInScenarios } from '../slack-sim/scenario.mjs';
|
|
3
|
+
import { printDryRun, printScenarioIntro, printState } from '../slack-sim/render.mjs';
|
|
4
|
+
|
|
5
|
+
function sleep(ms) {
|
|
6
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function slackSimCommand(args) {
|
|
10
|
+
const [subcommand = 'list-scenarios', scenarioRef] = args._positional;
|
|
11
|
+
|
|
12
|
+
if (args['--help']) {
|
|
13
|
+
printHelp();
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (subcommand === 'list-scenarios') {
|
|
18
|
+
fmt.intro('aw slack-sim list-scenarios');
|
|
19
|
+
const scenarios = listBuiltInScenarios();
|
|
20
|
+
fmt.note(
|
|
21
|
+
scenarios.map((s) => `${s.name} ${fmt.chalk.dim(s.path)}`).join('\n'),
|
|
22
|
+
'Built-in scenarios',
|
|
23
|
+
);
|
|
24
|
+
fmt.outro('Done');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!['run', 'replay'].includes(subcommand)) {
|
|
29
|
+
fmt.cancel(`Unknown slack-sim subcommand: ${subcommand}`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const scenario = loadScenario(scenarioRef);
|
|
34
|
+
const useHttp = Boolean(args['--api-base']) && !args['--in-process'];
|
|
35
|
+
|
|
36
|
+
printScenarioIntro(scenario, useHttp ? 'http' : 'in-process');
|
|
37
|
+
fmt.note(
|
|
38
|
+
[
|
|
39
|
+
`source: ${scenario.__file}`,
|
|
40
|
+
`actions: ${scenario.actions.length}`,
|
|
41
|
+
`org: ${scenario.session.orgId}`,
|
|
42
|
+
`team: ${scenario.session.teamId}`,
|
|
43
|
+
].join('\n'),
|
|
44
|
+
'Scenario',
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
if (args['--dry-run']) {
|
|
48
|
+
printDryRun(scenario);
|
|
49
|
+
fmt.outro('Dry run complete');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const transport = useHttp
|
|
54
|
+
? await (await import('../slack-sim/http.mjs')).createHttpTransport({
|
|
55
|
+
apiBase: args['--api-base'],
|
|
56
|
+
orgId: scenario.session.orgId,
|
|
57
|
+
teamId: scenario.session.teamId,
|
|
58
|
+
})
|
|
59
|
+
: await (await import('../slack-sim/in-process.mjs')).createInProcessTransport();
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
await transport.start?.();
|
|
63
|
+
let currentChannel = scenario.defaults.channel;
|
|
64
|
+
let currentThreadTs = scenario.defaults.threadTs;
|
|
65
|
+
for (let i = 0; i < scenario.actions.length; i++) {
|
|
66
|
+
const action = scenario.actions[i];
|
|
67
|
+
const actionResult = await transport.runAction(action, scenario);
|
|
68
|
+
currentChannel = actionResult?.channel || action.channel || currentChannel;
|
|
69
|
+
currentThreadTs = actionResult?.threadTs || action.threadTs || action.thread_ts || (action.type === 'app_mention_top_level' ? actionResult?.ts : currentThreadTs);
|
|
70
|
+
const state = await transport.getState({
|
|
71
|
+
channel: currentChannel,
|
|
72
|
+
threadTs: currentThreadTs,
|
|
73
|
+
});
|
|
74
|
+
if (action.type === 'assert') {
|
|
75
|
+
assertState(state, action);
|
|
76
|
+
fmt.logSuccess(`Assertion ${i + 1} passed`);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
printState(i, action, state);
|
|
80
|
+
if (args['--wait-for-finish'] && state.status === 'running' && action.type !== 'wait') {
|
|
81
|
+
await sleep(250);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
fmt.outro('Scenario complete');
|
|
85
|
+
} finally {
|
|
86
|
+
await transport.close?.();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function assertState(state, action) {
|
|
91
|
+
const failures = [];
|
|
92
|
+
if (action.status && state.status !== action.status) failures.push(`expected status=${action.status}, got ${state.status}`);
|
|
93
|
+
if (action.prRole && state.prRole !== action.prRole) failures.push(`expected prRole=${action.prRole}, got ${state.prRole}`);
|
|
94
|
+
if (action.pollerActive !== undefined && state.pollerActive !== action.pollerActive) {
|
|
95
|
+
failures.push(`expected pollerActive=${action.pollerActive}, got ${state.pollerActive}`);
|
|
96
|
+
}
|
|
97
|
+
if (action.checkpointActive !== undefined && state.checkpointActive !== action.checkpointActive) {
|
|
98
|
+
failures.push(`expected checkpointActive=${action.checkpointActive}, got ${state.checkpointActive}`);
|
|
99
|
+
}
|
|
100
|
+
if (action.prUrlIncludes && !String(state.prUrl || '').includes(action.prUrlIncludes)) {
|
|
101
|
+
failures.push(`expected prUrl to include "${action.prUrlIncludes}", got ${state.prUrl || '<none>'}`);
|
|
102
|
+
}
|
|
103
|
+
if (action.threadContains) {
|
|
104
|
+
const haystack = (state.messages || []).map((m) => m.text).join('\n');
|
|
105
|
+
if (!haystack.includes(action.threadContains)) failures.push(`expected thread to contain "${action.threadContains}"`);
|
|
106
|
+
}
|
|
107
|
+
if (action.threadNotContains) {
|
|
108
|
+
const haystack = (state.messages || []).map((m) => m.text).join('\n');
|
|
109
|
+
if (haystack.includes(action.threadNotContains)) failures.push(`expected thread to not contain "${action.threadNotContains}"`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (failures.length) {
|
|
113
|
+
throw new Error(failures.join('; '));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function printHelp() {
|
|
118
|
+
fmt.intro('aw slack-sim');
|
|
119
|
+
fmt.note(
|
|
120
|
+
[
|
|
121
|
+
'aw slack-sim list-scenarios',
|
|
122
|
+
'aw slack-sim run <scenario-file-or-name> [--dry-run] [--in-process] [--api-base <url>]',
|
|
123
|
+
'aw slack-sim replay <scenario-file-or-name> [--dry-run] [--in-process] [--api-base <url>]',
|
|
124
|
+
].join('\n'),
|
|
125
|
+
'Usage',
|
|
126
|
+
);
|
|
127
|
+
fmt.outro('Done');
|
|
128
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// commands/telemetry.mjs — `aw telemetry [enable|disable|status]`
|
|
2
|
+
|
|
3
|
+
import { enableTelemetry, disableTelemetry, getStatus } from '../telemetry.mjs';
|
|
4
|
+
import * as fmt from '../fmt.mjs';
|
|
5
|
+
import { chalk } from '../fmt.mjs';
|
|
6
|
+
|
|
7
|
+
export async function telemetryCommand(args) {
|
|
8
|
+
const sub = args._positional?.[0];
|
|
9
|
+
|
|
10
|
+
if (sub === 'disable') {
|
|
11
|
+
disableTelemetry();
|
|
12
|
+
fmt.logSuccess('Telemetry disabled. No anonymous usage data will be sent.');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (sub === 'enable') {
|
|
17
|
+
enableTelemetry();
|
|
18
|
+
fmt.logSuccess('Telemetry enabled. Anonymous usage stats help improve aw.');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// status (default)
|
|
23
|
+
const status = getStatus();
|
|
24
|
+
fmt.intro('aw telemetry');
|
|
25
|
+
fmt.logStep(`Status: ${status.enabled ? chalk.green('enabled') : chalk.red('disabled')}`);
|
|
26
|
+
fmt.logStep(`Machine ID: ${chalk.dim(status.machine_id)}`);
|
|
27
|
+
fmt.logStep(`Config: ${chalk.dim(status.config_path)}`);
|
|
28
|
+
fmt.logMessage('');
|
|
29
|
+
fmt.logMessage(` ${chalk.dim('aw telemetry disable')} — opt out of anonymous analytics`);
|
|
30
|
+
fmt.logMessage(` ${chalk.dim('aw telemetry enable')} — re-enable analytics`);
|
|
31
|
+
}
|
package/config.mjs
CHANGED
package/constants.mjs
CHANGED
|
@@ -27,3 +27,10 @@ export const AW_HOME = join(homedir(), '.aw');
|
|
|
27
27
|
|
|
28
28
|
/** Directory in platform-docs repo containing platform rules (pulled into .aw_registry/.aw_rules/) */
|
|
29
29
|
export const RULES_SOURCE_DIR = '.aw_rules';
|
|
30
|
+
/** Telemetry endpoint — override with AW_TELEMETRY_URL env var */
|
|
31
|
+
export const TELEMETRY_URL = process.env.AW_TELEMETRY_URL || 'https://services.leadconnectorhq.com/agentic-workspace/api/telemetry/events';
|
|
32
|
+
|
|
33
|
+
/** AW bot identity for Co-Authored-By trailers */
|
|
34
|
+
export const AW_BOT_NAME = 'AW';
|
|
35
|
+
export const AW_BOT_EMAIL = process.env.AW_BOT_EMAIL || '273421570+ghl-aw@users.noreply.github.com';
|
|
36
|
+
export const AW_CO_AUTHOR = `Co-Authored-By: ${AW_BOT_NAME} <${AW_BOT_EMAIL}>`;
|
package/ecc.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { applyStoredStartupPreferences } from "./startup.mjs";
|
|
|
10
10
|
|
|
11
11
|
const AW_ECC_REPO_SSH = "git@github.com:shreyansh-ghl/aw-ecc.git";
|
|
12
12
|
const AW_ECC_REPO_HTTPS = "https://github.com/shreyansh-ghl/aw-ecc.git";
|
|
13
|
-
export const AW_ECC_TAG = "v1.4.
|
|
13
|
+
export const AW_ECC_TAG = "v1.4.22";
|
|
14
14
|
|
|
15
15
|
const MARKETPLACE_NAME = "aw-marketplace";
|
|
16
16
|
const PLUGIN_KEY = `aw@${MARKETPLACE_NAME}`;
|
|
@@ -37,6 +37,14 @@ const TARGET_STATE = {
|
|
|
37
37
|
codex: { state: ".codex/ecc-install-state.json" },
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
+
// User-owned config files that must never be clobbered by aw-ecc install.
|
|
41
|
+
// If these files already exist before `aw init`, we preserve them exactly.
|
|
42
|
+
const PROTECTED_CONFIG_BY_TARGET = {
|
|
43
|
+
claude: [".claude/settings.json", ".claude.json"],
|
|
44
|
+
cursor: [".cursor/settings.json", ".cursor/mcp.json"],
|
|
45
|
+
codex: [".codex/config.toml"],
|
|
46
|
+
};
|
|
47
|
+
|
|
40
48
|
function run(cmd, opts = {}) {
|
|
41
49
|
return execSync(cmd, { stdio: "pipe", ...opts });
|
|
42
50
|
}
|
|
@@ -55,6 +63,59 @@ function cloneWithRef(url, ref, dest) {
|
|
|
55
63
|
}
|
|
56
64
|
}
|
|
57
65
|
|
|
66
|
+
function readIfExists(path) {
|
|
67
|
+
try { return existsSync(path) ? readFileSync(path, "utf8") : null; } catch { return null; }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function snapshotProtectedConfigs(home, target) {
|
|
71
|
+
const relPaths = PROTECTED_CONFIG_BY_TARGET[target] || [];
|
|
72
|
+
return relPaths.map((relPath) => {
|
|
73
|
+
const absPath = join(home, relPath);
|
|
74
|
+
const content = readIfExists(absPath);
|
|
75
|
+
return {
|
|
76
|
+
relPath,
|
|
77
|
+
absPath,
|
|
78
|
+
existedBeforeInstall: content !== null,
|
|
79
|
+
contentBeforeInstall: content,
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function restoreProtectedConfigs(snapshot) {
|
|
85
|
+
let restored = 0;
|
|
86
|
+
for (const entry of snapshot) {
|
|
87
|
+
if (entry.existedBeforeInstall) {
|
|
88
|
+
const current = readIfExists(entry.absPath);
|
|
89
|
+
if (current === entry.contentBeforeInstall) continue;
|
|
90
|
+
try {
|
|
91
|
+
mkdirSync(dirname(entry.absPath), { recursive: true });
|
|
92
|
+
writeFileSync(entry.absPath, entry.contentBeforeInstall ?? "");
|
|
93
|
+
restored++;
|
|
94
|
+
} catch { /* best effort */ }
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// If aw-ecc created a protected settings file from scratch, remove it.
|
|
99
|
+
// AW should not own these user config files.
|
|
100
|
+
try {
|
|
101
|
+
if (existsSync(entry.absPath)) {
|
|
102
|
+
rmSync(entry.absPath, { force: true });
|
|
103
|
+
restored++;
|
|
104
|
+
}
|
|
105
|
+
} catch { /* best effort */ }
|
|
106
|
+
}
|
|
107
|
+
return restored;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function relProtectedPath(absPath, home) {
|
|
111
|
+
const normalized = String(absPath || "").replace(/\\/g, "/");
|
|
112
|
+
for (const rel of Object.values(PROTECTED_CONFIG_BY_TARGET).flat()) {
|
|
113
|
+
const full = join(home, rel).replace(/\\/g, "/");
|
|
114
|
+
if (normalized === full) return rel;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
58
119
|
function cloneOrUpdate(tag, dest) {
|
|
59
120
|
// AW_ECC_CLONE_URL overrides the remote (used in tests to point at a local fake repo)
|
|
60
121
|
const overrideUrl = process.env.AW_ECC_CLONE_URL;
|
|
@@ -175,10 +236,14 @@ export async function installAwEcc(
|
|
|
175
236
|
if (!silent) fmt.logStep("Installing aw-ecc engine...");
|
|
176
237
|
|
|
177
238
|
const repoDir = eccDir();
|
|
239
|
+
const home = homedir();
|
|
178
240
|
|
|
179
241
|
try {
|
|
180
242
|
cloneOrUpdate(AW_ECC_TAG, repoDir);
|
|
181
243
|
|
|
244
|
+
// Ensure telemetry state directory exists (vendor-agnostic, shared across IDEs)
|
|
245
|
+
mkdirSync(join(home, ".aw", "telemetry"), { recursive: true });
|
|
246
|
+
|
|
182
247
|
// Claude Code: plugin install via marketplace CLI (proper agent dispatch)
|
|
183
248
|
if (targets.includes("claude")) {
|
|
184
249
|
try {
|
|
@@ -196,6 +261,8 @@ export async function installAwEcc(
|
|
|
196
261
|
});
|
|
197
262
|
for (const target of fileCopyTargets) {
|
|
198
263
|
try {
|
|
264
|
+
const snapshot = snapshotProtectedConfigs(home, target);
|
|
265
|
+
|
|
199
266
|
// Always use HOME as cwd so files land in ~/.<target>/ globally.
|
|
200
267
|
const runCwd = homedir();
|
|
201
268
|
// For claude: install the safe no-commands module set. The plugin
|
|
@@ -219,6 +286,10 @@ export async function installAwEcc(
|
|
|
219
286
|
if (target === "codex") {
|
|
220
287
|
syncEccToCodex(repoDir);
|
|
221
288
|
}
|
|
289
|
+
|
|
290
|
+
// Critical: preserve user-owned config files if they existed before
|
|
291
|
+
// running aw-ecc (aw should only add, never replace user settings).
|
|
292
|
+
restoreProtectedConfigs(snapshot);
|
|
222
293
|
} catch { /* target not supported — skip */ }
|
|
223
294
|
}
|
|
224
295
|
}
|
|
@@ -249,10 +320,18 @@ export function uninstallAwEcc({ silent = false } = {}) {
|
|
|
249
320
|
try {
|
|
250
321
|
const data = JSON.parse(readFileSync(statePath, "utf8"));
|
|
251
322
|
for (const op of data.operations || []) {
|
|
252
|
-
if (op.destinationPath
|
|
253
|
-
|
|
323
|
+
if (!op.destinationPath) continue;
|
|
324
|
+
const absPath = op.destinationPath.startsWith("/") ? op.destinationPath : join(HOME, op.destinationPath);
|
|
325
|
+
const relProtected = relProtectedPath(absPath, HOME);
|
|
326
|
+
if (relProtected) {
|
|
327
|
+
// Never delete user settings files as part of aw-ecc uninstall.
|
|
328
|
+
// AW-specific MCP removal is handled separately in removeMcpConfig().
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (existsSync(absPath)) {
|
|
332
|
+
rmSync(absPath, { recursive: true, force: true });
|
|
254
333
|
removed++;
|
|
255
|
-
pruneEmptyParents(
|
|
334
|
+
pruneEmptyParents(absPath, join(HOME, cfg.state.split("/")[0]));
|
|
256
335
|
}
|
|
257
336
|
}
|
|
258
337
|
rmSync(statePath, { force: true });
|
package/fmt.mjs
CHANGED
|
@@ -72,7 +72,21 @@ export const isCancel = p.isCancel;
|
|
|
72
72
|
|
|
73
73
|
export const spinner = () => p.spinner();
|
|
74
74
|
|
|
75
|
+
export class CancelError extends Error {
|
|
76
|
+
constructor(message, { exitCode = 1 } = {}) {
|
|
77
|
+
super(message);
|
|
78
|
+
this.name = 'CancelError';
|
|
79
|
+
this.exitCode = exitCode;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
75
83
|
export function cancel(msg) {
|
|
84
|
+
p.cancel(msg);
|
|
85
|
+
throw new CancelError(msg);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Hard exit — for use in process exception handlers where throwing is unsafe */
|
|
89
|
+
export function cancelAndExit(msg) {
|
|
76
90
|
p.cancel(msg);
|
|
77
91
|
process.exit(1);
|
|
78
92
|
}
|