@ghl-ai/aw 0.1.0

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/apply.mjs ADDED
@@ -0,0 +1,49 @@
1
+ // apply.mjs — Apply sync plan (file operations). Zero dependencies.
2
+
3
+ import { readFileSync, writeFileSync, mkdirSync, cpSync, existsSync } from 'node:fs';
4
+ import { dirname } from 'node:path';
5
+
6
+ function writeConflictMarkers(filePath, localContent, registryContent) {
7
+ writeFileSync(filePath, [
8
+ '<<<<<<< LOCAL',
9
+ localContent.trimEnd(),
10
+ '=======',
11
+ registryContent.trimEnd(),
12
+ '>>>>>>> REGISTRY',
13
+ '',
14
+ ].join('\n'));
15
+ }
16
+
17
+ /**
18
+ * Apply actions to the workspace.
19
+ * All actions are file-level (no directory-level operations).
20
+ * Returns count of files with conflicts.
21
+ */
22
+ export function applyActions(actions) {
23
+ let conflicts = 0;
24
+
25
+ for (const act of actions) {
26
+ switch (act.action) {
27
+ case 'ADD':
28
+ case 'UPDATE':
29
+ mkdirSync(dirname(act.targetPath), { recursive: true });
30
+ cpSync(act.sourcePath, act.targetPath);
31
+ break;
32
+
33
+ case 'CONFLICT': {
34
+ const local = existsSync(act.targetPath) ? readFileSync(act.targetPath, 'utf8') : '';
35
+ const registry = readFileSync(act.sourcePath, 'utf8');
36
+ mkdirSync(dirname(act.targetPath), { recursive: true });
37
+ writeConflictMarkers(act.targetPath, local, registry);
38
+ conflicts++;
39
+ break;
40
+ }
41
+
42
+ case 'ORPHAN':
43
+ case 'UNCHANGED':
44
+ break;
45
+ }
46
+ }
47
+
48
+ return conflicts;
49
+ }
package/bin.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ // Standalone bin entrypoint for published npm package
3
+ import('./cli.mjs').then(m => m.run(process.argv.slice(2)));
package/cli.mjs ADDED
@@ -0,0 +1,141 @@
1
+ // cli.mjs — Argument parser and command router
2
+
3
+ import * as fmt from './fmt.mjs';
4
+ import { chalk } from './fmt.mjs';
5
+
6
+ const VERSION = '0.1.0';
7
+
8
+ const COMMANDS = {
9
+ init: () => import('./commands/init.mjs').then(m => m.initCommand),
10
+ pull: () => import('./commands/pull.mjs').then(m => m.pullCommand),
11
+ push: () => import('./commands/push.mjs').then(m => m.pushCommand),
12
+ drop: () => import('./commands/drop.mjs').then(m => m.dropCommand),
13
+ status: () => import('./commands/status.mjs').then(m => m.statusCommand),
14
+ search: () => import('./commands/search.mjs').then(m => m.searchCommand),
15
+ nuke: () => import('./commands/nuke.mjs').then(m => m.nukeCommand),
16
+ };
17
+
18
+ function parseArgs(argv) {
19
+ const args = { _positional: [] };
20
+ let command = null;
21
+ let i = 0;
22
+
23
+ while (i < argv.length) {
24
+ const arg = argv[i];
25
+
26
+ if (!command && !arg.startsWith('-') && COMMANDS[arg]) {
27
+ command = arg;
28
+ i++;
29
+ continue;
30
+ }
31
+
32
+ if (arg === '--dry-run') {
33
+ args['--dry-run'] = true;
34
+ i++;
35
+ } else if (arg === '-v' || arg === '--verbose') {
36
+ args['-v'] = true;
37
+ i++;
38
+ } else if (arg === '--version') {
39
+ args['--version'] = true;
40
+ i++;
41
+ } else if (arg === '-h' || arg === '--help') {
42
+ args['--help'] = true;
43
+ i++;
44
+ } else if (arg.startsWith('--') && i + 1 < argv.length && !argv[i + 1].startsWith('-')) {
45
+ args[arg] = argv[i + 1];
46
+ i += 2;
47
+ } else if (arg.startsWith('--')) {
48
+ args[arg] = true;
49
+ i++;
50
+ } else {
51
+ args._positional.push(arg);
52
+ i++;
53
+ }
54
+ }
55
+
56
+ return { command, args };
57
+ }
58
+
59
+ function printHelp() {
60
+ fmt.banner('aw', {
61
+ subtitle: ` ${chalk.dim('v' + VERSION)} ${chalk.hex('#FF6B35')('Agentic Workspace CLI')} ${chalk.dim('— pull, push & manage agents, skills and more from the registry')}`,
62
+ });
63
+
64
+ const cmd = (c, d) => ` ${chalk.hex('#FF6B35')(c.padEnd(38))} ${chalk.dim(d)}`;
65
+ const sec = (title) => `\n ${chalk.bold.underline(title)}`;
66
+ const help = [
67
+ sec('Setup'),
68
+ cmd('aw init', 'Initialize workspace & pull ghl platform layer'),
69
+ cmd('aw init --namespace <team>', 'Init with ghl + team template'),
70
+
71
+ sec('Download'),
72
+ cmd('aw pull', 'Re-pull all synced paths (like git pull)'),
73
+ cmd('aw pull <path>', 'Pull agents, skills & more from registry'),
74
+ cmd('aw pull --dry-run <path>', 'Preview what would be downloaded'),
75
+
76
+ sec('Upload'),
77
+ cmd('aw push', 'Show modified files ready to push'),
78
+ cmd('aw push <path>', 'Create a PR to add/update registry content'),
79
+ cmd('aw push --dry-run <path>', 'Preview the upload without creating PR'),
80
+
81
+ sec('Discover'),
82
+ cmd('aw search <query>', 'Find agents & skills (local + registry)'),
83
+
84
+ sec('Manage'),
85
+ cmd('aw status', 'Show synced paths, modified files & conflicts'),
86
+ cmd('aw drop <path>', 'Stop syncing or delete local content'),
87
+ cmd('aw nuke', 'Remove entire .aw_registry/ & start fresh'),
88
+
89
+ sec('Examples'),
90
+ '',
91
+ ` ${chalk.dim('# Pull content from registry using path')}`,
92
+ cmd('aw pull ghl', 'All shared platform agents & skills'),
93
+ cmd('aw pull <team>', 'Everything from a team namespace'),
94
+ cmd('aw pull <team>/agents', 'All agents from a team'),
95
+ cmd('aw pull <team>/agents/<name>', 'One specific agent'),
96
+ cmd('aw pull <team>/skills/<name>', 'One specific skill folder'),
97
+ '',
98
+ ` ${chalk.dim('# Push your local changes to registry')}`,
99
+ cmd('aw push .aw_registry/agents/<name>.md', 'Upload an agent'),
100
+ cmd('aw push .aw_registry/skills/<name>/', 'Upload a skill folder'),
101
+ '',
102
+ ` ${chalk.dim('# Remove content from workspace')}`,
103
+ cmd('aw drop <team>', 'Stop syncing a namespace (removes all files)'),
104
+ cmd('aw drop <team>/agents/<name>', 'Stop syncing one agent'),
105
+ cmd('aw drop .aw_registry/agents/<name>.md', 'Delete a local file (next pull restores it)'),
106
+ '',
107
+ ` ${chalk.dim('# All commands accept both registry paths and local paths')}`,
108
+ ` ${chalk.dim('# registry: <team>/agents/<name>')}`,
109
+ ` ${chalk.dim('# local: .aw_registry/<team>/agents/<name>.md')}`,
110
+ '',
111
+ ].join('\n');
112
+
113
+ console.log(help);
114
+ }
115
+
116
+ export async function run(argv) {
117
+ const { command, args } = parseArgs(argv);
118
+
119
+ if (args['--version']) {
120
+ console.log(VERSION);
121
+ process.exit(0);
122
+ }
123
+
124
+ if (args['--help'] && !command) {
125
+ printHelp();
126
+ process.exit(0);
127
+ }
128
+
129
+ if (command && COMMANDS[command]) {
130
+ const handler = await COMMANDS[command]();
131
+ handler(args);
132
+ return;
133
+ }
134
+
135
+ if (!command) {
136
+ printHelp();
137
+ process.exit(0);
138
+ }
139
+
140
+ fmt.cancel(`Unknown command: ${command}`);
141
+ }
@@ -0,0 +1,88 @@
1
+ // commands/drop.mjs — Remove file or synced path from workspace
2
+
3
+ import { join, resolve } from 'node:path';
4
+ import { rmSync, existsSync } from 'node:fs';
5
+ import * as config from '../config.mjs';
6
+ import * as fmt from '../fmt.mjs';
7
+ import { chalk } from '../fmt.mjs';
8
+ import { matchesAny } from '../glob.mjs';
9
+ import { resolveInput } from '../paths.mjs';
10
+ import { load as loadManifest, save as saveManifest } from '../manifest.mjs';
11
+
12
+ export function dropCommand(args) {
13
+ const input = args._positional?.[0];
14
+ const cwd = process.cwd();
15
+ const workspaceDir = join(cwd, '.aw_registry');
16
+
17
+ fmt.intro('aw drop');
18
+
19
+ if (!input) {
20
+ fmt.cancel('Missing target. Usage:\n aw drop example-team (stop syncing namespace)\n aw drop example-team/skills/example-skill (stop syncing skill)\n aw drop example-team/skills/example-skill/SKILL (delete single file)');
21
+ }
22
+
23
+ const cfg = config.load(workspaceDir);
24
+ if (!cfg) fmt.cancel('No .sync-config.json found. Run: aw init');
25
+
26
+ // Resolve to registry path (accepts both local and registry paths)
27
+ const resolved = resolveInput(input, workspaceDir);
28
+ const regPath = resolved.registryPath;
29
+
30
+ if (!regPath) {
31
+ fmt.cancel(`Could not resolve "${input}" to a registry path`);
32
+ }
33
+
34
+ // Check if this path (or a parent) is in config → remove from config + delete files
35
+ const isConfigPath = cfg.include.some(p => p === regPath || p.startsWith(regPath + '/'));
36
+
37
+ if (isConfigPath) {
38
+ config.removePattern(workspaceDir, regPath);
39
+ fmt.logSuccess(`Removed ${chalk.cyan(regPath)} from sync config`);
40
+ }
41
+
42
+ // Delete matching local files
43
+ const removed = deleteMatchingFiles(workspaceDir, regPath);
44
+
45
+ if (removed > 0) {
46
+ fmt.logInfo(`${chalk.bold(removed)} file${removed > 1 ? 's' : ''} removed from workspace`);
47
+ } else if (!isConfigPath) {
48
+ fmt.cancel(`Nothing found for ${chalk.cyan(regPath)}.\n\n Use ${chalk.dim('aw status')} to see synced paths.`);
49
+ }
50
+
51
+ if (!isConfigPath && removed > 0) {
52
+ fmt.logWarn(`Path still in sync config — next ${chalk.dim('aw pull')} will restore it`);
53
+ }
54
+
55
+ fmt.outro('Done');
56
+ }
57
+
58
+ /**
59
+ * Find and delete local files whose registry path matches the given path.
60
+ */
61
+ function deleteMatchingFiles(workspaceDir, path) {
62
+ const manifest = loadManifest(workspaceDir);
63
+ let removed = 0;
64
+
65
+ for (const [manifestKey] of Object.entries(manifest.files)) {
66
+ const registryPath = manifestKeyToRegistryPath(manifestKey);
67
+
68
+ if (matchesAny(registryPath, [path])) {
69
+ const filePath = join(workspaceDir, manifestKey);
70
+ if (existsSync(filePath)) {
71
+ rmSync(filePath, { recursive: true, force: true });
72
+ removed++;
73
+ }
74
+ delete manifest.files[manifestKey];
75
+ }
76
+ }
77
+
78
+ saveManifest(workspaceDir, manifest);
79
+ return removed;
80
+ }
81
+
82
+ /**
83
+ * Convert manifest key to registry path.
84
+ * Manifest key now mirrors registry: "ghl/agents/architecture-reviewer.md" → "ghl/agents/architecture-reviewer"
85
+ */
86
+ function manifestKeyToRegistryPath(manifestKey) {
87
+ return manifestKey.replace(/\.md$/, '');
88
+ }
@@ -0,0 +1,75 @@
1
+ // commands/init.mjs — Initialize workspace config + pull ghl + template
2
+
3
+ import { mkdirSync, existsSync } from 'node:fs';
4
+ import { execSync } from 'node:child_process';
5
+ import { join } from 'node:path';
6
+ import * as config from '../config.mjs';
7
+ import * as fmt from '../fmt.mjs';
8
+ import { chalk } from '../fmt.mjs';
9
+ import { pullCommand } from './pull.mjs';
10
+ import { linkWorkspace } from '../link.mjs';
11
+ import { generateCommands, copyInstructions, initAwDocs } from '../integrate.mjs';
12
+ import { setupMcp } from '../mcp.mjs';
13
+
14
+ export function initCommand(args) {
15
+ const namespace = args['--namespace'] || null;
16
+ let user = args['--user'] || '';
17
+ const cwd = process.cwd();
18
+ const workspaceDir = join(cwd, '.aw_registry');
19
+
20
+ fmt.intro('aw init');
21
+
22
+ // Validate namespace slug if provided
23
+ if (namespace && !/^[a-z][a-z0-9-]{1,38}[a-z0-9]$/.test(namespace)) {
24
+ fmt.cancel(`Invalid namespace '${namespace}' — must match: ^[a-z][a-z0-9-]{1,38}[a-z0-9]$`);
25
+ }
26
+
27
+ if (namespace === 'ghl') {
28
+ fmt.cancel("'ghl' is a reserved namespace — it is the shared platform layer");
29
+ }
30
+
31
+ // Auto-detect user from GitHub if not provided
32
+ if (!user) {
33
+ try {
34
+ user = execSync('gh api user --jq .login', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
35
+ } catch {
36
+ // gh not available or not logged in — skip
37
+ }
38
+ }
39
+
40
+ // Create workspace dir
41
+ if (!existsSync(workspaceDir)) {
42
+ mkdirSync(workspaceDir, { recursive: true });
43
+ fmt.logStep('Created .aw_registry/');
44
+ }
45
+
46
+ // Check existing config
47
+ if (config.exists(workspaceDir)) {
48
+ fmt.cancel('.sync-config.json already exists. Delete it to re-init.');
49
+ }
50
+
51
+ // Create config
52
+ const cfg = config.create(workspaceDir, { namespace, user });
53
+
54
+ const infoLines = [
55
+ namespace ? `${chalk.dim('namespace:')} ${cfg.namespace}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`,
56
+ user ? `${chalk.dim('user:')} ${cfg.user}` : null,
57
+ ].filter(Boolean).join('\n');
58
+
59
+ fmt.note(infoLines, 'Config created');
60
+
61
+ // Always pull ghl/ (platform layer)
62
+ pullCommand({ ...args, _positional: ['ghl'] });
63
+
64
+ // Pull [template]/ as the team namespace
65
+ if (namespace) {
66
+ pullCommand({ ...args, _positional: ['[template]'], _renameNamespace: namespace });
67
+ }
68
+
69
+ // Post-pull IDE integration
70
+ linkWorkspace(cwd);
71
+ generateCommands(cwd);
72
+ copyInstructions(cwd, null, namespace);
73
+ initAwDocs(cwd);
74
+ setupMcp(cwd, namespace);
75
+ }
@@ -0,0 +1,95 @@
1
+ // commands/nuke.mjs — Remove entire .aw_registry/ and all generated IDE files
2
+
3
+ import { join } from 'node:path';
4
+ import { existsSync, rmSync, lstatSync, unlinkSync, readdirSync } from 'node:fs';
5
+ import * as fmt from '../fmt.mjs';
6
+ import { chalk } from '../fmt.mjs';
7
+
8
+ // IDE dirs to scan for per-file symlinks
9
+ const IDE_DIRS = ['.claude', '.cursor', '.codex'];
10
+ const CONTENT_TYPES = ['agents', 'skills', 'commands', 'blueprints', 'evals'];
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)
23
+ const DIRS_TO_PRUNE = [
24
+ // IDE content dirs
25
+ ...IDE_DIRS.flatMap(ide => CONTENT_TYPES.map(t => `${ide}/${t}`)),
26
+ // IDE root dirs
27
+ ...IDE_DIRS,
28
+ // Codex agents dir
29
+ '.agents/skills',
30
+ '.agents',
31
+ ];
32
+
33
+ export function nukeCommand(args) {
34
+ const cwd = process.cwd();
35
+ const workspaceDir = join(cwd, '.aw_registry');
36
+
37
+ fmt.intro('aw nuke');
38
+
39
+ if (!existsSync(workspaceDir)) {
40
+ fmt.cancel('No .aw_registry/ found — nothing to remove');
41
+ }
42
+
43
+ // 1. Remove symlinks + generated files from all IDE dirs
44
+ for (const ide of IDE_DIRS) {
45
+ for (const type of CONTENT_TYPES) {
46
+ const dir = join(cwd, ide, type);
47
+ if (!existsSync(dir)) continue;
48
+ for (const entry of readdirSync(dir)) {
49
+ const p = join(dir, entry);
50
+ try {
51
+ const stat = lstatSync(p);
52
+ if (stat.isSymbolicLink()) {
53
+ unlinkSync(p);
54
+ }
55
+ } catch { /* best effort */ }
56
+ }
57
+ }
58
+ // Clean commands/aw/ (generated commands + symlinked registry commands)
59
+ const awCmdDir = join(cwd, ide, 'commands/aw');
60
+ if (existsSync(awCmdDir)) {
61
+ rmSync(awCmdDir, { recursive: true, force: true });
62
+ }
63
+ }
64
+ // 2. Clean .agents/skills/ (symlinks + generated Codex skill dirs)
65
+ const agentsSkillsDir = join(cwd, '.agents/skills');
66
+ if (existsSync(agentsSkillsDir)) {
67
+ rmSync(agentsSkillsDir, { recursive: true, force: true });
68
+ }
69
+
70
+ // 3. Remove generated files
71
+ for (const rel of FILES_TO_CLEAN) {
72
+ const p = join(cwd, rel);
73
+ try { if (existsSync(p)) rmSync(p); } catch { /* best effort */ }
74
+ }
75
+
76
+ // 4. Remove .aw_registry/ and .aw_docs/
77
+ rmSync(workspaceDir, { recursive: true, force: true });
78
+ const awDocsDir = join(cwd, '.aw_docs');
79
+ if (existsSync(awDocsDir)) {
80
+ rmSync(awDocsDir, { recursive: true, force: true });
81
+ }
82
+
83
+ // 5. Prune empty dirs
84
+ for (const rel of DIRS_TO_PRUNE) {
85
+ const p = join(cwd, rel);
86
+ try {
87
+ if (existsSync(p) && readdirSync(p).length === 0) {
88
+ rmSync(p, { recursive: true });
89
+ }
90
+ } catch { /* best effort */ }
91
+ }
92
+
93
+ fmt.logSuccess('Removed .aw_registry/ and all generated IDE files');
94
+ fmt.outro(`Run ${chalk.dim('aw init')} to start fresh`);
95
+ }