@ghl-ai/aw 0.1.0 → 0.1.1

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 CHANGED
@@ -128,7 +128,7 @@ export async function run(argv) {
128
128
 
129
129
  if (command && COMMANDS[command]) {
130
130
  const handler = await COMMANDS[command]();
131
- handler(args);
131
+ await handler(args);
132
132
  return;
133
133
  }
134
134
 
package/commands/init.mjs CHANGED
@@ -1,8 +1,10 @@
1
1
  // commands/init.mjs — Initialize workspace config + pull ghl + template
2
2
 
3
- import { mkdirSync, existsSync } from 'node:fs';
3
+ import { mkdirSync, existsSync, writeFileSync, readFileSync } from 'node:fs';
4
4
  import { execSync } from 'node:child_process';
5
- import { join } from 'node:path';
5
+ import { join, resolve } from 'node:path';
6
+ import { homedir } from 'node:os';
7
+ import { select, isCancel } from '@clack/prompts';
6
8
  import * as config from '../config.mjs';
7
9
  import * as fmt from '../fmt.mjs';
8
10
  import { chalk } from '../fmt.mjs';
@@ -11,11 +13,72 @@ import { linkWorkspace } from '../link.mjs';
11
13
  import { generateCommands, copyInstructions, initAwDocs } from '../integrate.mjs';
12
14
  import { setupMcp } from '../mcp.mjs';
13
15
 
14
- export function initCommand(args) {
16
+ // Scope definitions
17
+ const SCOPES = {
18
+ user: {
19
+ label: 'Install for you (user scope)',
20
+ hint: 'Available in all your projects — stored in ~/.claude/',
21
+ rootDir: () => homedir(),
22
+ gitignore: false,
23
+ },
24
+ project: {
25
+ label: 'Install for all collaborators (project scope)',
26
+ hint: 'Checked into git — shared with your whole team via .claude/',
27
+ rootDir: () => process.cwd(),
28
+ gitignore: false,
29
+ },
30
+ local: {
31
+ label: 'Install for you, in this repo only (local scope)',
32
+ hint: 'Added to .gitignore — only you see it in this project',
33
+ rootDir: () => process.cwd(),
34
+ gitignore: true,
35
+ },
36
+ };
37
+
38
+ async function pickScope(preselected) {
39
+ if (preselected && SCOPES[preselected]) return preselected;
40
+
41
+ const result = await select({
42
+ message: 'Where would you like to install?',
43
+ options: Object.entries(SCOPES).map(([value, s]) => ({
44
+ value,
45
+ label: s.label,
46
+ hint: s.hint,
47
+ })),
48
+ initialValue: 'project',
49
+ });
50
+
51
+ if (isCancel(result)) {
52
+ fmt.cancel('Installation cancelled.');
53
+ process.exit(0);
54
+ }
55
+
56
+ return result;
57
+ }
58
+
59
+ function addToGitignore(cwd, patterns) {
60
+ const gitignorePath = join(cwd, '.gitignore');
61
+ const toAdd = patterns.filter(p => {
62
+ if (!existsSync(gitignorePath)) return true;
63
+ const content = readFileSync(gitignorePath, 'utf8');
64
+ return !content.split('\n').some(line => line.trim() === p);
65
+ });
66
+
67
+ if (toAdd.length === 0) return;
68
+
69
+ const block = `\n# Agentic Workspace (local scope)\n${toAdd.join('\n')}\n`;
70
+ if (existsSync(gitignorePath)) {
71
+ writeFileSync(gitignorePath, readFileSync(gitignorePath, 'utf8') + block);
72
+ } else {
73
+ writeFileSync(gitignorePath, block.trimStart());
74
+ }
75
+ fmt.logStep(`Added to .gitignore: ${toAdd.join(', ')}`);
76
+ }
77
+
78
+ export async function initCommand(args) {
15
79
  const namespace = args['--namespace'] || null;
16
80
  let user = args['--user'] || '';
17
81
  const cwd = process.cwd();
18
- const workspaceDir = join(cwd, '.aw_registry');
19
82
 
20
83
  fmt.intro('aw init');
21
84
 
@@ -28,6 +91,14 @@ export function initCommand(args) {
28
91
  fmt.cancel("'ghl' is a reserved namespace — it is the shared platform layer");
29
92
  }
30
93
 
94
+ // Pick install scope
95
+ const scopeKey = await pickScope(args['--scope']);
96
+ const scope = SCOPES[scopeKey];
97
+ const rootDir = scope.rootDir();
98
+ const workspaceDir = join(rootDir, '.aw_registry');
99
+
100
+ fmt.logStep(`Scope: ${chalk.bold(scope.label)}`);
101
+
31
102
  // Auto-detect user from GitHub if not provided
32
103
  if (!user) {
33
104
  try {
@@ -40,7 +111,7 @@ export function initCommand(args) {
40
111
  // Create workspace dir
41
112
  if (!existsSync(workspaceDir)) {
42
113
  mkdirSync(workspaceDir, { recursive: true });
43
- fmt.logStep('Created .aw_registry/');
114
+ fmt.logStep(`Created ${scopeKey === 'user' ? '~/' : ''}.aw_registry/`);
44
115
  }
45
116
 
46
117
  // Check existing config
@@ -48,28 +119,49 @@ export function initCommand(args) {
48
119
  fmt.cancel('.sync-config.json already exists. Delete it to re-init.');
49
120
  }
50
121
 
51
- // Create config
52
- const cfg = config.create(workspaceDir, { namespace, user });
122
+ // Create config (store scope so other commands know where to look)
123
+ const cfg = config.create(workspaceDir, { namespace, user, scope: scopeKey });
53
124
 
54
125
  const infoLines = [
55
126
  namespace ? `${chalk.dim('namespace:')} ${cfg.namespace}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`,
56
127
  user ? `${chalk.dim('user:')} ${cfg.user}` : null,
128
+ `${chalk.dim('scope:')} ${scopeKey} → ${resolve(workspaceDir)}`,
57
129
  ].filter(Boolean).join('\n');
58
130
 
59
131
  fmt.note(infoLines, 'Config created');
60
132
 
61
133
  // Always pull ghl/ (platform layer)
62
- pullCommand({ ...args, _positional: ['ghl'] });
134
+ pullCommand({ ...args, _positional: ['ghl'], _workspaceDir: workspaceDir });
63
135
 
64
136
  // Pull [template]/ as the team namespace
65
137
  if (namespace) {
66
- pullCommand({ ...args, _positional: ['[template]'], _renameNamespace: namespace });
138
+ pullCommand({ ...args, _positional: ['[template]'], _renameNamespace: namespace, _workspaceDir: workspaceDir });
67
139
  }
68
140
 
69
141
  // Post-pull IDE integration
70
- linkWorkspace(cwd);
71
- generateCommands(cwd);
72
- copyInstructions(cwd, null, namespace);
73
- initAwDocs(cwd);
74
- setupMcp(cwd, namespace);
142
+ linkWorkspace(rootDir);
143
+ generateCommands(rootDir);
144
+ const instructionFiles = copyInstructions(rootDir, null, namespace);
145
+ initAwDocs(rootDir);
146
+ const mcpFiles = setupMcp(rootDir, namespace);
147
+
148
+ // Write manifest of files created by aw init so nuke only deletes what we made
149
+ const manifestPath = join(workspaceDir, '.created-files.json');
150
+ const createdFiles = [...instructionFiles, ...mcpFiles].map(p =>
151
+ p.startsWith(rootDir) ? p.slice(rootDir.length + 1) : p
152
+ );
153
+ writeFileSync(manifestPath, JSON.stringify(createdFiles, null, 2) + '\n');
154
+
155
+ // For local scope: add to .gitignore
156
+ if (scope.gitignore) {
157
+ addToGitignore(cwd, ['.aw_registry/', '.aw_docs/', '.claude/', '.cursor/', '.codex/', '.agents/', 'mcp.json', 'CLAUDE.md', 'AGENTS.md']);
158
+ }
159
+
160
+ fmt.outro(
161
+ scopeKey === 'user'
162
+ ? `Installed to ~/ — available in all your projects`
163
+ : scopeKey === 'project'
164
+ ? `Installed to .claude/ — commit this to share with your team`
165
+ : `Installed locally — added to .gitignore`
166
+ );
75
167
  }
package/commands/nuke.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // commands/nuke.mjs — Remove entire .aw_registry/ and all generated IDE files
2
2
 
3
3
  import { join } from 'node:path';
4
- import { existsSync, rmSync, lstatSync, unlinkSync, readdirSync } from 'node:fs';
4
+ import { existsSync, rmSync, lstatSync, unlinkSync, readdirSync, readFileSync } from 'node:fs';
5
5
  import * as fmt from '../fmt.mjs';
6
6
  import { chalk } from '../fmt.mjs';
7
7
 
@@ -9,22 +9,10 @@ import { chalk } from '../fmt.mjs';
9
9
  const IDE_DIRS = ['.claude', '.cursor', '.codex'];
10
10
  const CONTENT_TYPES = ['agents', 'skills', 'commands', 'blueprints', 'evals'];
11
11
 
12
- // Generated files from mcp.mjs + integrate.mjs
13
- const FILES_TO_CLEAN = [
14
- '.claude/settings.local.json',
15
- '.cursor/mcp.json',
16
- '.codex/config.toml',
17
- 'mcp.json',
18
- 'CLAUDE.md',
19
- 'AGENTS.md',
20
- ];
21
-
22
- // Dirs to remove if empty after cleanup (deepest first)
12
+ // Dirs to remove if empty after cleanup (deepest first — IDE roots intentionally excluded)
23
13
  const DIRS_TO_PRUNE = [
24
14
  // IDE content dirs
25
15
  ...IDE_DIRS.flatMap(ide => CONTENT_TYPES.map(t => `${ide}/${t}`)),
26
- // IDE root dirs
27
- ...IDE_DIRS,
28
16
  // Codex agents dir
29
17
  '.agents/skills',
30
18
  '.agents',
@@ -40,7 +28,7 @@ export function nukeCommand(args) {
40
28
  fmt.cancel('No .aw_registry/ found — nothing to remove');
41
29
  }
42
30
 
43
- // 1. Remove symlinks + generated files from all IDE dirs
31
+ // 1. Remove symlinks from all IDE dirs
44
32
  for (const ide of IDE_DIRS) {
45
33
  for (const type of CONTENT_TYPES) {
46
34
  const dir = join(cwd, ide, type);
@@ -61,14 +49,22 @@ export function nukeCommand(args) {
61
49
  rmSync(awCmdDir, { recursive: true, force: true });
62
50
  }
63
51
  }
52
+
64
53
  // 2. Clean .agents/skills/ (symlinks + generated Codex skill dirs)
65
54
  const agentsSkillsDir = join(cwd, '.agents/skills');
66
55
  if (existsSync(agentsSkillsDir)) {
67
56
  rmSync(agentsSkillsDir, { recursive: true, force: true });
68
57
  }
69
58
 
70
- // 3. Remove generated files
71
- for (const rel of FILES_TO_CLEAN) {
59
+ // 3. Remove only files that aw init actually created (from manifest)
60
+ const manifestPath = join(workspaceDir, '.created-files.json');
61
+ let filesToClean = [];
62
+ if (existsSync(manifestPath)) {
63
+ try {
64
+ filesToClean = JSON.parse(readFileSync(manifestPath, 'utf8'));
65
+ } catch { /* ignore parse errors */ }
66
+ }
67
+ for (const rel of filesToClean) {
72
68
  const p = join(cwd, rel);
73
69
  try { if (existsSync(p)) rmSync(p); } catch { /* best effort */ }
74
70
  }
@@ -80,7 +76,7 @@ export function nukeCommand(args) {
80
76
  rmSync(awDocsDir, { recursive: true, force: true });
81
77
  }
82
78
 
83
- // 5. Prune empty dirs
79
+ // 5. Prune empty dirs (IDE roots are excluded — never fully delete .claude/.cursor/.codex)
84
80
  for (const rel of DIRS_TO_PRUNE) {
85
81
  const p = join(cwd, rel);
86
82
  try {
@@ -90,6 +86,6 @@ export function nukeCommand(args) {
90
86
  } catch { /* best effort */ }
91
87
  }
92
88
 
93
- fmt.logSuccess('Removed .aw_registry/ and all generated IDE files');
89
+ fmt.logSuccess('Removed .aw_registry/, .aw_docs/, and all generated IDE files');
94
90
  fmt.outro(`Run ${chalk.dim('aw init')} to start fresh`);
95
91
  }
package/integrate.mjs CHANGED
@@ -92,6 +92,7 @@ export function generateCommands(cwd) {
92
92
  * Copy CLAUDE.md and AGENTS.md to project root.
93
93
  */
94
94
  export function copyInstructions(cwd, tempDir, namespace) {
95
+ const createdFiles = [];
95
96
  for (const file of ['CLAUDE.md', 'AGENTS.md']) {
96
97
  const dest = join(cwd, file);
97
98
  if (existsSync(dest)) continue;
@@ -105,6 +106,7 @@ export function copyInstructions(cwd, tempDir, namespace) {
105
106
  }
106
107
  writeFileSync(dest, content);
107
108
  fmt.logSuccess(`Created ${file}`);
109
+ createdFiles.push(dest);
108
110
  continue;
109
111
  }
110
112
  }
@@ -115,8 +117,10 @@ export function copyInstructions(cwd, tempDir, namespace) {
115
117
  if (content) {
116
118
  writeFileSync(dest, content);
117
119
  fmt.logSuccess(`Created ${file}`);
120
+ createdFiles.push(dest);
118
121
  }
119
122
  }
123
+ return createdFiles;
120
124
  }
121
125
 
122
126
  function generateClaudeMd(cwd, namespace) {
package/mcp.mjs CHANGED
@@ -29,110 +29,98 @@ function detectPaths() {
29
29
 
30
30
  /**
31
31
  * Setup MCP configs for all IDEs. Only writes if file doesn't exist.
32
+ * Returns list of file paths that were actually created.
32
33
  */
33
34
  export function setupMcp(cwd, namespace) {
34
35
  const paths = detectPaths();
35
- let created = 0;
36
+ const createdFiles = [];
37
+
38
+ const attempt = (filePath, contentFn, append = false) => {
39
+ if (writeIfMissing(filePath, contentFn, append)) {
40
+ createdFiles.push(filePath);
41
+ }
42
+ };
36
43
 
37
44
  // .claude/settings.local.json
38
- created += writeIfMissing(
39
- join(cwd, '.claude', 'settings.local.json'),
40
- () => {
41
- const servers = {};
42
- if (paths.ghlAiBridge) {
43
- servers['ghl-ai'] = {
44
- command: 'node',
45
- args: [paths.ghlAiBridge],
46
- env: { GHL_MCP_URL: paths.ghlMcpUrl, TEAM_NAME: namespace || '' },
47
- };
48
- }
49
- if (paths.gitJenkinsPath) {
50
- servers['git-jenkins'] = {
51
- command: 'node',
52
- args: [paths.gitJenkinsPath],
53
- };
54
- }
55
- if (Object.keys(servers).length === 0) return null;
56
- return JSON.stringify({ mcpServers: servers }, null, 2) + '\n';
45
+ attempt(join(cwd, '.claude', 'settings.local.json'), () => {
46
+ const servers = {};
47
+ if (paths.ghlAiBridge) {
48
+ servers['ghl-ai'] = {
49
+ command: 'node',
50
+ args: [paths.ghlAiBridge],
51
+ env: { GHL_MCP_URL: paths.ghlMcpUrl, TEAM_NAME: namespace || '' },
52
+ };
53
+ }
54
+ if (paths.gitJenkinsPath) {
55
+ servers['git-jenkins'] = { command: 'node', args: [paths.gitJenkinsPath] };
57
56
  }
58
- );
57
+ if (Object.keys(servers).length === 0) return null;
58
+ return JSON.stringify({ mcpServers: servers }, null, 2) + '\n';
59
+ });
59
60
 
60
61
  // .cursor/mcp.json
61
- created += writeIfMissing(
62
- join(cwd, '.cursor', 'mcp.json'),
63
- () => {
64
- const servers = {};
65
- if (paths.ghlAiBridge) {
66
- servers['ghl-ai'] = {
67
- command: 'node',
68
- args: [paths.ghlAiBridge],
69
- env: { GHL_MCP_URL: paths.ghlMcpUrl, TEAM_NAME: namespace || '' },
70
- };
71
- }
72
- if (paths.gitJenkinsPath) {
73
- servers['git-jenkins'] = {
74
- command: 'node',
75
- args: [paths.gitJenkinsPath],
76
- };
77
- }
78
- if (Object.keys(servers).length === 0) return null;
79
- return JSON.stringify({ mcpServers: servers }, null, 2) + '\n';
62
+ attempt(join(cwd, '.cursor', 'mcp.json'), () => {
63
+ const servers = {};
64
+ if (paths.ghlAiBridge) {
65
+ servers['ghl-ai'] = {
66
+ command: 'node',
67
+ args: [paths.ghlAiBridge],
68
+ env: { GHL_MCP_URL: paths.ghlMcpUrl, TEAM_NAME: namespace || '' },
69
+ };
70
+ }
71
+ if (paths.gitJenkinsPath) {
72
+ servers['git-jenkins'] = { command: 'node', args: [paths.gitJenkinsPath] };
80
73
  }
81
- );
74
+ if (Object.keys(servers).length === 0) return null;
75
+ return JSON.stringify({ mcpServers: servers }, null, 2) + '\n';
76
+ });
82
77
 
83
78
  // .codex/config.toml — append MCP servers
84
79
  const codexConfig = join(cwd, '.codex', 'config.toml');
85
80
  if (!hasMcpSection(codexConfig)) {
86
- created += writeIfMissing(
87
- codexConfig,
88
- () => {
89
- const lines = [];
90
- if (paths.ghlAiBridge) {
91
- lines.push(
92
- '[mcp_servers.ghl-ai]',
93
- 'command = "node"',
94
- `args = ["${paths.ghlAiBridge}"]`,
95
- '',
96
- '[mcp_servers.ghl-ai.env]',
97
- `GHL_MCP_URL = "${paths.ghlMcpUrl}"`,
98
- `TEAM_NAME = "${namespace || ''}"`,
99
- '',
100
- );
101
- }
102
- if (paths.gitJenkinsPath) {
103
- lines.push(
104
- '[mcp_servers.git-jenkins]',
105
- 'command = "node"',
106
- `args = ["${paths.gitJenkinsPath}"]`,
107
- '',
108
- );
109
- }
110
- if (lines.length === 0) return null;
111
- return lines.join('\n');
112
- },
113
- true // append mode
114
- );
81
+ attempt(codexConfig, () => {
82
+ const lines = [];
83
+ if (paths.ghlAiBridge) {
84
+ lines.push(
85
+ '[mcp_servers.ghl-ai]',
86
+ 'command = "node"',
87
+ `args = ["${paths.ghlAiBridge}"]`,
88
+ '',
89
+ '[mcp_servers.ghl-ai.env]',
90
+ `GHL_MCP_URL = "${paths.ghlMcpUrl}"`,
91
+ `TEAM_NAME = "${namespace || ''}"`,
92
+ '',
93
+ );
94
+ }
95
+ if (paths.gitJenkinsPath) {
96
+ lines.push(
97
+ '[mcp_servers.git-jenkins]',
98
+ 'command = "node"',
99
+ `args = ["${paths.gitJenkinsPath}"]`,
100
+ '',
101
+ );
102
+ }
103
+ if (lines.length === 0) return null;
104
+ return lines.join('\n');
105
+ }, true /* append mode */);
115
106
  }
116
107
 
117
108
  // mcp.json at project root (Claude Code auto-discovery)
118
- created += writeIfMissing(
119
- join(cwd, 'mcp.json'),
120
- () => {
121
- const servers = {};
122
- if (paths.ghlAiBridge) {
123
- servers['ghl-ai'] = {
124
- command: 'node',
125
- args: [paths.ghlAiBridge],
126
- env: { GHL_MCP_URL: paths.ghlMcpUrl, TEAM_NAME: namespace || '' },
127
- };
128
- }
129
- if (Object.keys(servers).length === 0) return null;
130
- return JSON.stringify({ mcpServers: servers }, null, 2) + '\n';
109
+ attempt(join(cwd, 'mcp.json'), () => {
110
+ const servers = {};
111
+ if (paths.ghlAiBridge) {
112
+ servers['ghl-ai'] = {
113
+ command: 'node',
114
+ args: [paths.ghlAiBridge],
115
+ env: { GHL_MCP_URL: paths.ghlMcpUrl, TEAM_NAME: namespace || '' },
116
+ };
131
117
  }
132
- );
118
+ if (Object.keys(servers).length === 0) return null;
119
+ return JSON.stringify({ mcpServers: servers }, null, 2) + '\n';
120
+ });
133
121
 
134
- if (created > 0) {
135
- fmt.logSuccess(`Created ${created} MCP config${created > 1 ? 's' : ''}`);
122
+ if (createdFiles.length > 0) {
123
+ fmt.logSuccess(`Created ${createdFiles.length} MCP config${createdFiles.length > 1 ? 's' : ''}`);
136
124
  }
137
125
 
138
126
  const warnings = [];
@@ -142,13 +130,13 @@ export function setupMcp(cwd, namespace) {
142
130
  fmt.logWarn(w);
143
131
  }
144
132
 
145
- return created;
133
+ return createdFiles;
146
134
  }
147
135
 
148
136
  function writeIfMissing(filePath, contentFn, append = false) {
149
- if (!append && existsSync(filePath)) return 0;
137
+ if (!append && existsSync(filePath)) return false;
150
138
  const content = contentFn();
151
- if (!content) return 0;
139
+ if (!content) return false;
152
140
  mkdirSync(join(filePath, '..'), { recursive: true });
153
141
  if (append && existsSync(filePath)) {
154
142
  const existing = readFileSync(filePath, 'utf8');
@@ -156,7 +144,7 @@ function writeIfMissing(filePath, contentFn, append = false) {
156
144
  } else {
157
145
  writeFileSync(filePath, content);
158
146
  }
159
- return 1;
147
+ return true;
160
148
  }
161
149
 
162
150
  function hasMcpSection(filePath) {
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {
7
- "aw": "./bin.js"
7
+ "aw": "bin.js"
8
8
  },
9
9
  "files": [
10
10
  "bin.js",