@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 +49 -0
- package/bin.js +3 -0
- package/cli.mjs +141 -0
- package/commands/drop.mjs +88 -0
- package/commands/init.mjs +75 -0
- package/commands/nuke.mjs +95 -0
- package/commands/pull.mjs +252 -0
- package/commands/push.mjs +214 -0
- package/commands/search.mjs +183 -0
- package/commands/status.mjs +108 -0
- package/config.mjs +70 -0
- package/constants.mjs +7 -0
- package/fmt.mjs +99 -0
- package/git.mjs +61 -0
- package/glob.mjs +22 -0
- package/integrate.mjs +466 -0
- package/link.mjs +209 -0
- package/manifest.mjs +62 -0
- package/mcp.mjs +166 -0
- package/package.json +47 -0
- package/paths.mjs +139 -0
- package/plan.mjs +133 -0
- package/registry.mjs +138 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// commands/status.mjs — Show workspace status
|
|
2
|
+
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { existsSync, readFileSync } 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 { load as loadManifest } from '../manifest.mjs';
|
|
9
|
+
import { hashFile } from '../registry.mjs';
|
|
10
|
+
|
|
11
|
+
export function statusCommand(args) {
|
|
12
|
+
const cwd = process.cwd();
|
|
13
|
+
const workspaceDir = join(cwd, '.aw_registry');
|
|
14
|
+
|
|
15
|
+
const cfg = config.load(workspaceDir);
|
|
16
|
+
if (!cfg) {
|
|
17
|
+
fmt.cancel('No .sync-config.json found. Run: aw --init');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const manifest = loadManifest(workspaceDir);
|
|
21
|
+
|
|
22
|
+
fmt.intro(`aw status`);
|
|
23
|
+
|
|
24
|
+
// Info note
|
|
25
|
+
const infoLines = [
|
|
26
|
+
`${chalk.dim('namespace:')} ${cfg.namespace || 'none'}`,
|
|
27
|
+
cfg.user ? `${chalk.dim('user:')} ${cfg.user}` : null,
|
|
28
|
+
manifest.synced_at ? `${chalk.dim('last sync:')} ${manifest.synced_at}` : null,
|
|
29
|
+
].filter(Boolean).join('\n');
|
|
30
|
+
fmt.note(infoLines, 'Workspace');
|
|
31
|
+
|
|
32
|
+
// Show synced paths
|
|
33
|
+
if (cfg.include.length > 0) {
|
|
34
|
+
const pathLines = cfg.include.map(p => ` ${chalk.cyan(p)}`).join('\n');
|
|
35
|
+
fmt.note(pathLines, 'Synced Paths');
|
|
36
|
+
} else {
|
|
37
|
+
fmt.logInfo('No paths synced. Run: aw pull <path>');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const files = Object.entries(manifest.files || {});
|
|
41
|
+
const modified = [];
|
|
42
|
+
const missing = [];
|
|
43
|
+
const conflicts = [];
|
|
44
|
+
|
|
45
|
+
for (const [key, entry] of files) {
|
|
46
|
+
const filePath = join(workspaceDir, key);
|
|
47
|
+
if (!existsSync(filePath)) {
|
|
48
|
+
missing.push(key);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const currentHash = hashFile(filePath);
|
|
53
|
+
|
|
54
|
+
if (currentHash !== entry.sha256) {
|
|
55
|
+
const content = readFileSync(filePath, 'utf8');
|
|
56
|
+
if (content.includes('<<<<<<<')) {
|
|
57
|
+
conflicts.push(key);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
modified.push(key);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Summary line
|
|
65
|
+
const summaryParts = [
|
|
66
|
+
`${files.length} synced`,
|
|
67
|
+
modified.length > 0 ? chalk.yellow(`${modified.length} modified`) : null,
|
|
68
|
+
conflicts.length > 0 ? chalk.red(`${conflicts.length} conflicts`) : null,
|
|
69
|
+
missing.length > 0 ? chalk.dim(`${missing.length} missing`) : null,
|
|
70
|
+
].filter(Boolean).join(chalk.dim(' · '));
|
|
71
|
+
|
|
72
|
+
fmt.logInfo(summaryParts);
|
|
73
|
+
|
|
74
|
+
if (conflicts.length > 0) {
|
|
75
|
+
fmt.note(
|
|
76
|
+
conflicts.map(c => chalk.red(c)).join('\n'),
|
|
77
|
+
chalk.red('Unresolved Conflicts')
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (modified.length > 0) {
|
|
82
|
+
fmt.note(
|
|
83
|
+
modified.map(m => chalk.yellow(m)).join('\n'),
|
|
84
|
+
chalk.yellow('Modified')
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (missing.length > 0) {
|
|
89
|
+
fmt.note(
|
|
90
|
+
missing.map(m => chalk.dim(m)).join('\n'),
|
|
91
|
+
chalk.dim('Missing (deleted locally)')
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (modified.length === 0 && conflicts.length === 0 && missing.length === 0) {
|
|
96
|
+
fmt.logSuccess('Workspace is clean');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Hints
|
|
100
|
+
if (conflicts.length > 0) {
|
|
101
|
+
fmt.logWarn(`Fix conflicts: ${chalk.dim('grep -r "<<<<<<< " .aw_registry/')}`);
|
|
102
|
+
}
|
|
103
|
+
if (modified.length > 0) {
|
|
104
|
+
fmt.logInfo(`Push changes: ${chalk.dim('aw push <path>')}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fmt.outro(`${chalk.dim('aw pull <pattern>')} to pull latest`);
|
|
108
|
+
}
|
package/config.mjs
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// config.mjs — .sync-config.json management. Zero dependencies.
|
|
2
|
+
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { REGISTRY_REPO } from './constants.mjs';
|
|
6
|
+
|
|
7
|
+
const CONFIG_FILE = '.sync-config.json';
|
|
8
|
+
|
|
9
|
+
const DEFAULTS = {
|
|
10
|
+
namespace: null,
|
|
11
|
+
user: '',
|
|
12
|
+
repo: REGISTRY_REPO,
|
|
13
|
+
include: [],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function configPath(workspaceDir) {
|
|
17
|
+
return join(workspaceDir, CONFIG_FILE);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function exists(workspaceDir) {
|
|
21
|
+
return existsSync(configPath(workspaceDir));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function load(workspaceDir) {
|
|
25
|
+
const p = configPath(workspaceDir);
|
|
26
|
+
if (!existsSync(p)) return null;
|
|
27
|
+
try {
|
|
28
|
+
return { ...DEFAULTS, ...JSON.parse(readFileSync(p, 'utf8')) };
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function save(workspaceDir, config) {
|
|
35
|
+
writeFileSync(configPath(workspaceDir), JSON.stringify(config, null, 2) + '\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function create(workspaceDir, { namespace, user }) {
|
|
39
|
+
const config = {
|
|
40
|
+
...DEFAULTS,
|
|
41
|
+
namespace: namespace || null,
|
|
42
|
+
user: user || '',
|
|
43
|
+
include: [],
|
|
44
|
+
};
|
|
45
|
+
save(workspaceDir, config);
|
|
46
|
+
return config;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function addPattern(workspaceDir, pattern) {
|
|
50
|
+
const config = load(workspaceDir);
|
|
51
|
+
if (!config) throw new Error('No .sync-config.json found. Run: aw --init --namespace <name>');
|
|
52
|
+
// If a parent path already covers this, skip
|
|
53
|
+
if (config.include.some(p => pattern === p || pattern.startsWith(p + '/'))) {
|
|
54
|
+
return config;
|
|
55
|
+
}
|
|
56
|
+
// Remove any children that the new parent covers
|
|
57
|
+
config.include = config.include.filter(p => !p.startsWith(pattern + '/'));
|
|
58
|
+
config.include.push(pattern);
|
|
59
|
+
save(workspaceDir, config);
|
|
60
|
+
return config;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function removePattern(workspaceDir, pattern) {
|
|
64
|
+
const config = load(workspaceDir);
|
|
65
|
+
if (!config) throw new Error('No .sync-config.json found. Run: aw --init --namespace <name>');
|
|
66
|
+
// Remove exact match + all children
|
|
67
|
+
config.include = config.include.filter(p => p !== pattern && !p.startsWith(pattern + '/'));
|
|
68
|
+
save(workspaceDir, config);
|
|
69
|
+
return config;
|
|
70
|
+
}
|
package/constants.mjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// constants.mjs — Single source of truth for registry settings.
|
|
2
|
+
|
|
3
|
+
/** Base branch for PRs and sync checkout */
|
|
4
|
+
export const REGISTRY_BASE_BRANCH = 'master';
|
|
5
|
+
|
|
6
|
+
/** Default registry repository */
|
|
7
|
+
export const REGISTRY_REPO = 'GoHighLevel/ghl-agentic-workspace';
|
package/fmt.mjs
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// fmt.mjs — Terminal formatting with @clack/prompts + chalk + figlet
|
|
2
|
+
|
|
3
|
+
import * as p from '@clack/prompts';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import figlet from 'figlet';
|
|
6
|
+
|
|
7
|
+
export { chalk };
|
|
8
|
+
|
|
9
|
+
// ─── Banner ───
|
|
10
|
+
|
|
11
|
+
export function banner(text, opts = {}) {
|
|
12
|
+
const {
|
|
13
|
+
font = 'ANSI Shadow',
|
|
14
|
+
color = chalk.hex('#FF6B35'),
|
|
15
|
+
subtitle = '',
|
|
16
|
+
dividerChar = '─',
|
|
17
|
+
} = opts;
|
|
18
|
+
|
|
19
|
+
const cols = process.stdout.columns || 80;
|
|
20
|
+
const art = figlet.textSync(text, { font, horizontalLayout: 'default' });
|
|
21
|
+
const divider = chalk.dim(dividerChar.repeat(Math.min(cols, 80)));
|
|
22
|
+
|
|
23
|
+
console.log('');
|
|
24
|
+
console.log(divider);
|
|
25
|
+
console.log(color(art));
|
|
26
|
+
console.log(divider);
|
|
27
|
+
if (subtitle) {
|
|
28
|
+
console.log('');
|
|
29
|
+
console.log(chalk.dim(subtitle));
|
|
30
|
+
}
|
|
31
|
+
console.log('');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── Clack wrappers ───
|
|
35
|
+
|
|
36
|
+
export const intro = (msg) => p.intro(chalk.bgCyan.black(` ${msg} `));
|
|
37
|
+
export const outro = (msg) => p.outro(chalk.green(msg));
|
|
38
|
+
export const spinner = () => p.spinner();
|
|
39
|
+
|
|
40
|
+
export function cancel(msg) {
|
|
41
|
+
p.cancel(msg);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── Log helpers (clack-styled) ───
|
|
46
|
+
|
|
47
|
+
export const note = (msg, title) => p.note(msg, title);
|
|
48
|
+
export const logInfo = (msg) => p.log.info(msg);
|
|
49
|
+
export const logSuccess = (msg) => p.log.success(msg);
|
|
50
|
+
export const logWarn = (msg) => p.log.warn(msg);
|
|
51
|
+
export const logError = (msg) => p.log.error(msg);
|
|
52
|
+
export const logStep = (msg) => p.log.step(msg);
|
|
53
|
+
export const logMessage = (msg) => p.log.message(msg);
|
|
54
|
+
|
|
55
|
+
// ─── Styled text helpers ───
|
|
56
|
+
|
|
57
|
+
export const dim = (s) => chalk.dim(s);
|
|
58
|
+
export const bold = (s) => chalk.bold(s);
|
|
59
|
+
export const red = (s) => chalk.red(s);
|
|
60
|
+
export const green = (s) => chalk.green(s);
|
|
61
|
+
export const yellow = (s) => chalk.yellow(s);
|
|
62
|
+
export const blue = (s) => chalk.blue(s);
|
|
63
|
+
export const cyan = (s) => chalk.cyan(s);
|
|
64
|
+
export const magenta = (s) => chalk.magenta(s);
|
|
65
|
+
|
|
66
|
+
// ─── Backward compat aliases (used by existing commands) ───
|
|
67
|
+
|
|
68
|
+
export function log(msg = '') { process.stderr.write(msg + '\n'); }
|
|
69
|
+
export function info(msg) { p.log.info(msg); }
|
|
70
|
+
export function success(msg) { p.log.success(msg); }
|
|
71
|
+
export function warn(msg) { p.log.warn(msg); }
|
|
72
|
+
export function error(msg) { p.log.error(msg); }
|
|
73
|
+
export function heading(msg) { p.log.step(chalk.bold(msg)); }
|
|
74
|
+
export function item(label, value) { p.log.message(`${chalk.dim(label)} ${value}`); }
|
|
75
|
+
|
|
76
|
+
// ─── Action labels ───
|
|
77
|
+
|
|
78
|
+
export function actionLabel(action) {
|
|
79
|
+
switch (action) {
|
|
80
|
+
case 'ADD': return chalk.bgGreen.black(' ADD ');
|
|
81
|
+
case 'UPDATE': return chalk.bgCyan.black(' UPD ');
|
|
82
|
+
case 'SKIP': return chalk.bgYellow.black(' SKP ');
|
|
83
|
+
case 'CONFLICT': return chalk.bgRed.white(' CON ');
|
|
84
|
+
case 'ORPHAN': return chalk.bgYellow.black(' ORP ');
|
|
85
|
+
case 'UNCHANGED': return chalk.dim(' --- ');
|
|
86
|
+
default: return chalk.dim(` ${action} `);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function countSummary(counts) {
|
|
91
|
+
const parts = [];
|
|
92
|
+
if (counts.ADD > 0) parts.push(chalk.green(`${counts.ADD} added`));
|
|
93
|
+
if (counts.UPDATE > 0) parts.push(chalk.cyan(`${counts.UPDATE} updated`));
|
|
94
|
+
if (counts.UNCHANGED > 0) parts.push(chalk.dim(`${counts.UNCHANGED} unchanged`));
|
|
95
|
+
if (counts.SKIP > 0) parts.push(chalk.yellow(`${counts.SKIP} skipped`));
|
|
96
|
+
if (counts.CONFLICT > 0) parts.push(chalk.red(`${counts.CONFLICT} conflict`));
|
|
97
|
+
if (counts.ORPHAN > 0) parts.push(chalk.yellow(`${counts.ORPHAN} orphan`));
|
|
98
|
+
return parts.join(chalk.dim(' · '));
|
|
99
|
+
}
|
package/git.mjs
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// git.mjs — Git sparse checkout helpers. Zero dependencies.
|
|
2
|
+
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
import { mkdtempSync, existsSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { REGISTRY_BASE_BRANCH } from './constants.mjs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Sparse-checkout registry paths from GitHub.
|
|
11
|
+
* Returns the temp directory path containing the checkout.
|
|
12
|
+
*/
|
|
13
|
+
export function sparseCheckout(repo, paths) {
|
|
14
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'aw-'));
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
execSync(`gh repo clone ${repo} ${tempDir} -- --filter=blob:none --no-checkout`, {
|
|
18
|
+
stdio: 'pipe',
|
|
19
|
+
});
|
|
20
|
+
} catch (e) {
|
|
21
|
+
throw new Error(`Failed to clone ${repo}. Check: gh auth status`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
execSync('git sparse-checkout init --cone', { cwd: tempDir, stdio: 'pipe' });
|
|
26
|
+
execSync(`git sparse-checkout set --skip-checks ${paths.map(p => `"${p}"`).join(' ')}`, {
|
|
27
|
+
cwd: tempDir, stdio: 'pipe',
|
|
28
|
+
});
|
|
29
|
+
execSync(`git checkout ${REGISTRY_BASE_BRANCH}`, { cwd: tempDir, stdio: 'pipe' });
|
|
30
|
+
} catch (e) {
|
|
31
|
+
throw new Error(`Failed sparse checkout: ${e.message}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return tempDir;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Clean up temp directory.
|
|
39
|
+
*/
|
|
40
|
+
export function cleanup(tempDir) {
|
|
41
|
+
try {
|
|
42
|
+
execSync(`rm -rf "${tempDir}"`, { stdio: 'pipe' });
|
|
43
|
+
} catch {
|
|
44
|
+
// Best effort
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Compute sparse checkout paths from include paths.
|
|
50
|
+
* e.g., ["ghl", "dev/agents/debugger"] -> ["registry/ghl", "registry/dev/agents/debugger"]
|
|
51
|
+
*/
|
|
52
|
+
export function includeToSparsePaths(paths) {
|
|
53
|
+
const result = new Set();
|
|
54
|
+
for (const p of paths) {
|
|
55
|
+
result.add(`registry/${p}`);
|
|
56
|
+
}
|
|
57
|
+
// Also fetch root instruction files
|
|
58
|
+
result.add('registry/CLAUDE.md');
|
|
59
|
+
result.add('registry/AGENTS.md');
|
|
60
|
+
return [...result];
|
|
61
|
+
}
|
package/glob.mjs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// glob.mjs — Simple path matching. Zero dependencies.
|
|
2
|
+
// No wildcards. Path prefix matching only.
|
|
3
|
+
// "ghl" matches "ghl/agents/foo", "ghl/skills/bar", etc.
|
|
4
|
+
// "ghl/agents/foo" matches exactly "ghl/agents/foo"
|
|
5
|
+
// "dev/skills/my-skill/references" matches "dev/skills/my-skill" (parent match)
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Test if a registry path matches any of the given paths.
|
|
9
|
+
* Matches if:
|
|
10
|
+
* 1. path equals pattern (exact match)
|
|
11
|
+
* 2. path starts with pattern/ (pattern is parent of path)
|
|
12
|
+
* 3. pattern starts with path/ (path is parent of pattern — e.g., pulling a subfolder pulls the skill)
|
|
13
|
+
*/
|
|
14
|
+
export function matchesAny(path, patterns) {
|
|
15
|
+
for (const p of patterns) {
|
|
16
|
+
const normalized = p.replace(/\.md$/, '');
|
|
17
|
+
if (path === normalized) return true;
|
|
18
|
+
if (path.startsWith(normalized + '/')) return true;
|
|
19
|
+
if (normalized.startsWith(path + '/')) return true;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|