@ghl-ai/aw 0.1.35 → 0.1.36-beta.2
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 +2 -1
- package/commands/drop.mjs +45 -47
- package/commands/init.mjs +122 -125
- package/commands/nuke.mjs +30 -10
- package/commands/pull.mjs +57 -370
- package/commands/push.mjs +297 -287
- package/commands/status.mjs +50 -80
- package/config.mjs +2 -2
- package/constants.mjs +6 -0
- package/ecc.mjs +180 -0
- package/fmt.mjs +2 -0
- package/git.mjs +233 -1
- package/integrate.mjs +8 -6
- package/package.json +3 -2
- package/apply.mjs +0 -79
- package/manifest.mjs +0 -64
- package/plan.mjs +0 -147
package/commands/status.mjs
CHANGED
|
@@ -1,130 +1,100 @@
|
|
|
1
|
-
// commands/status.mjs — Show workspace status
|
|
1
|
+
// commands/status.mjs — Show workspace status using native git
|
|
2
2
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
5
|
import * as config from '../config.mjs';
|
|
6
6
|
import * as fmt from '../fmt.mjs';
|
|
7
7
|
import { chalk } from '../fmt.mjs';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { detectChanges, getCurrentBranch, isValidClone } from '../git.mjs';
|
|
9
|
+
import { REGISTRY_DIR, REGISTRY_REPO } from '../constants.mjs';
|
|
10
10
|
|
|
11
11
|
export function statusCommand(args) {
|
|
12
|
-
const
|
|
13
|
-
const
|
|
12
|
+
const HOME = homedir();
|
|
13
|
+
const AW_HOME = join(HOME, '.aw');
|
|
14
|
+
const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
fmt.intro('aw status');
|
|
17
|
+
|
|
18
|
+
const repoUrl = `https://github.com/${REGISTRY_REPO}.git`;
|
|
19
|
+
if (!isValidClone(AW_HOME, repoUrl)) {
|
|
20
|
+
fmt.cancel('Registry not initialized. Run: aw init');
|
|
21
|
+
return;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
const
|
|
24
|
+
const cfg = config.load(GLOBAL_AW_DIR);
|
|
25
|
+
if (!cfg) {
|
|
26
|
+
fmt.cancel('No .sync-config.json found. Run: aw init');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
21
29
|
|
|
22
|
-
|
|
30
|
+
const branch = getCurrentBranch(AW_HOME);
|
|
31
|
+
const isOnMain = branch === 'main';
|
|
23
32
|
|
|
24
33
|
// Info note
|
|
25
34
|
const infoLines = [
|
|
26
|
-
`${chalk.dim('
|
|
35
|
+
`${chalk.dim('source:')} ~/.aw/ (git clone)`,
|
|
36
|
+
`${chalk.dim('branch:')} ${isOnMain ? chalk.dim(branch) : chalk.yellow(branch)}`,
|
|
37
|
+
cfg.namespace ? `${chalk.dim('namespace:')} ${cfg.namespace}` : null,
|
|
27
38
|
cfg.user ? `${chalk.dim('user:')} ${cfg.user}` : null,
|
|
28
|
-
manifest.synced_at ? `${chalk.dim('last sync:')} ${manifest.synced_at}` : null,
|
|
29
39
|
].filter(Boolean).join('\n');
|
|
30
40
|
fmt.note(infoLines, 'Workspace');
|
|
31
41
|
|
|
32
42
|
// Show synced paths
|
|
33
|
-
if (cfg.include.length > 0) {
|
|
43
|
+
if (cfg.include && cfg.include.length > 0) {
|
|
34
44
|
const pathLines = cfg.include.map(p => ` ${chalk.cyan(p)}`).join('\n');
|
|
35
45
|
fmt.note(pathLines, 'Synced Paths');
|
|
36
46
|
} else {
|
|
37
|
-
fmt.logInfo('No paths synced
|
|
47
|
+
fmt.logInfo('No extra paths synced (platform/ always included).');
|
|
38
48
|
}
|
|
39
49
|
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
for (const [key, entry] of files) {
|
|
47
|
-
const filePath = join(workspaceDir, key);
|
|
48
|
-
if (!existsSync(filePath)) {
|
|
49
|
-
missing.push(key);
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const currentHash = hashFile(filePath);
|
|
54
|
-
|
|
55
|
-
// Template-derived files that were never pushed to the remote registry
|
|
56
|
-
if (!entry.registry_sha256) {
|
|
57
|
-
unpushed.push(key);
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (currentHash !== entry.sha256) {
|
|
62
|
-
const content = readFileSync(filePath, 'utf8');
|
|
63
|
-
if (content.includes('<<<<<<<')) {
|
|
64
|
-
conflicts.push(key);
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
modified.push(key);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
50
|
+
// Git-native change detection
|
|
51
|
+
const changes = detectChanges(AW_HOME, REGISTRY_DIR);
|
|
52
|
+
|
|
53
|
+
const { modified, added, deleted, untracked } = changes;
|
|
54
|
+
const allNew = [...added, ...untracked];
|
|
70
55
|
|
|
71
56
|
// Summary line
|
|
72
57
|
const summaryParts = [
|
|
73
|
-
`${files.length} synced`,
|
|
74
|
-
unpushed.length > 0 ? chalk.green(`${unpushed.length} new (unpushed)`) : null,
|
|
75
58
|
modified.length > 0 ? chalk.yellow(`${modified.length} modified`) : null,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
].filter(Boolean)
|
|
59
|
+
allNew.length > 0 ? chalk.green(`${allNew.length} new (unpushed)`) : null,
|
|
60
|
+
deleted.length > 0 ? chalk.dim(`${deleted.length} deleted`) : null,
|
|
61
|
+
].filter(Boolean);
|
|
79
62
|
|
|
80
|
-
|
|
63
|
+
if (summaryParts.length === 0) {
|
|
64
|
+
fmt.logSuccess('Workspace is clean');
|
|
65
|
+
} else {
|
|
66
|
+
fmt.logInfo(summaryParts.join(chalk.dim(' · ')));
|
|
67
|
+
}
|
|
81
68
|
|
|
82
|
-
if (
|
|
69
|
+
if (allNew.length > 0) {
|
|
83
70
|
fmt.note(
|
|
84
|
-
|
|
85
|
-
chalk.
|
|
71
|
+
allNew.map(f => chalk.green(` ${f.registryPath}`)).join('\n'),
|
|
72
|
+
chalk.green('New (unpushed)')
|
|
86
73
|
);
|
|
87
74
|
}
|
|
88
75
|
|
|
89
|
-
if (unpushed.length > 0) {
|
|
90
|
-
// Group by namespace for cleaner display
|
|
91
|
-
const nsCounts = {};
|
|
92
|
-
for (const key of unpushed) {
|
|
93
|
-
const ns = key.split('/').slice(0, 2).join('/');
|
|
94
|
-
nsCounts[ns] = (nsCounts[ns] || 0) + 1;
|
|
95
|
-
}
|
|
96
|
-
const nsLines = Object.entries(nsCounts).map(([ns, count]) => chalk.green(` ${ns}: ${count} files`)).join('\n');
|
|
97
|
-
fmt.note(nsLines, chalk.green('New (unpushed)'));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
76
|
if (modified.length > 0) {
|
|
101
77
|
fmt.note(
|
|
102
|
-
modified.map(
|
|
78
|
+
modified.map(f => chalk.yellow(` ${f.registryPath}`)).join('\n'),
|
|
103
79
|
chalk.yellow('Modified')
|
|
104
80
|
);
|
|
105
81
|
}
|
|
106
82
|
|
|
107
|
-
if (
|
|
83
|
+
if (deleted.length > 0) {
|
|
108
84
|
fmt.note(
|
|
109
|
-
|
|
110
|
-
chalk.dim('
|
|
85
|
+
deleted.map(f => chalk.dim(` ${f.registryPath}`)).join('\n'),
|
|
86
|
+
chalk.dim('Deleted')
|
|
111
87
|
);
|
|
112
88
|
}
|
|
113
89
|
|
|
114
|
-
if (
|
|
115
|
-
fmt.
|
|
90
|
+
if (!isOnMain) {
|
|
91
|
+
fmt.logWarn(`On branch ${chalk.yellow(branch)} — run ${chalk.dim('aw pull')} to sync or checkout main first`);
|
|
116
92
|
}
|
|
117
93
|
|
|
118
94
|
// Hints
|
|
119
|
-
if (
|
|
120
|
-
fmt.
|
|
121
|
-
}
|
|
122
|
-
if (unpushed.length > 0) {
|
|
123
|
-
fmt.logInfo(`Push new files: ${chalk.dim('aw push')} or ${chalk.dim('aw push <namespace>')}`);
|
|
124
|
-
}
|
|
125
|
-
if (modified.length > 0) {
|
|
126
|
-
fmt.logInfo(`Push changes: ${chalk.dim('aw push <path>')}`);
|
|
95
|
+
if (allNew.length > 0 || modified.length > 0 || deleted.length > 0) {
|
|
96
|
+
fmt.logInfo(`Push changes: ${chalk.dim('aw push')} or ${chalk.dim('aw push <path>')}`);
|
|
127
97
|
}
|
|
128
98
|
|
|
129
|
-
fmt.outro(`${chalk.dim('aw pull
|
|
99
|
+
fmt.outro(`${chalk.dim('aw pull')} to fetch latest`);
|
|
130
100
|
}
|
package/config.mjs
CHANGED
|
@@ -48,7 +48,7 @@ export function create(workspaceDir, { namespace, user }) {
|
|
|
48
48
|
|
|
49
49
|
export function addPattern(workspaceDir, pattern) {
|
|
50
50
|
const config = load(workspaceDir);
|
|
51
|
-
if (!config) throw new Error('No .sync-config.json found. Run: aw
|
|
51
|
+
if (!config) throw new Error('No .sync-config.json found. Run: aw init');
|
|
52
52
|
// If a parent path already covers this, skip
|
|
53
53
|
if (config.include.some(p => pattern === p || pattern.startsWith(p + '/'))) {
|
|
54
54
|
return config;
|
|
@@ -62,7 +62,7 @@ export function addPattern(workspaceDir, pattern) {
|
|
|
62
62
|
|
|
63
63
|
export function removePattern(workspaceDir, pattern) {
|
|
64
64
|
const config = load(workspaceDir);
|
|
65
|
-
if (!config) throw new Error('No .sync-config.json found. Run: aw
|
|
65
|
+
if (!config) throw new Error('No .sync-config.json found. Run: aw init');
|
|
66
66
|
// Remove exact match + all children
|
|
67
67
|
config.include = config.include.filter(p => p !== pattern && !p.startsWith(pattern + '/'));
|
|
68
68
|
save(workspaceDir, config);
|
package/constants.mjs
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// constants.mjs — Single source of truth for registry settings.
|
|
2
2
|
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
3
6
|
/** Base branch for PRs and sync checkout */
|
|
4
7
|
export const REGISTRY_BASE_BRANCH = 'main';
|
|
5
8
|
|
|
@@ -11,3 +14,6 @@ export const REGISTRY_DIR = '.aw_registry';
|
|
|
11
14
|
|
|
12
15
|
/** Directory in platform-docs repo containing documentation (pulled into platform/docs/) */
|
|
13
16
|
export const DOCS_SOURCE_DIR = 'content';
|
|
17
|
+
|
|
18
|
+
/** Persistent git clone root — ~/.aw/ */
|
|
19
|
+
export const AW_HOME = join(homedir(), '.aw');
|
package/ecc.mjs
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
existsSync, readFileSync, readdirSync,
|
|
4
|
+
mkdirSync, rmSync, writeFileSync,
|
|
5
|
+
} from "node:fs";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import * as fmt from "./fmt.mjs";
|
|
9
|
+
|
|
10
|
+
const AW_ECC_REPO_SSH = "git@github.com:shreyansh-ghl/aw-ecc.git";
|
|
11
|
+
const AW_ECC_REPO_HTTPS = "https://github.com/shreyansh-ghl/aw-ecc.git";
|
|
12
|
+
const AW_ECC_TAG = "v1.2.2";
|
|
13
|
+
|
|
14
|
+
const MARKETPLACE_NAME = "aw-marketplace";
|
|
15
|
+
const PLUGIN_KEY = `aw@${MARKETPLACE_NAME}`;
|
|
16
|
+
|
|
17
|
+
function eccDir() { return join(homedir(), ".aw-ecc"); }
|
|
18
|
+
|
|
19
|
+
const FILE_COPY_TARGETS = ["cursor", "codex"];
|
|
20
|
+
|
|
21
|
+
const TARGET_STATE = {
|
|
22
|
+
cursor: { state: ".cursor/ecc-install-state.json" },
|
|
23
|
+
codex: { state: ".codex/ecc-install-state.json" },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function run(cmd, opts = {}) {
|
|
27
|
+
return execSync(cmd, { stdio: "pipe", ...opts });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function cloneOrUpdate(tag, dest) {
|
|
31
|
+
if (existsSync(join(dest, ".git"))) {
|
|
32
|
+
try {
|
|
33
|
+
run(`git -C ${dest} fetch --quiet --depth 1 origin tag ${tag}`);
|
|
34
|
+
run(`git -C ${dest} checkout --quiet ${tag}`);
|
|
35
|
+
return;
|
|
36
|
+
} catch { /* fall through to fresh clone */ }
|
|
37
|
+
}
|
|
38
|
+
if (existsSync(dest)) rmSync(dest, { recursive: true, force: true });
|
|
39
|
+
try {
|
|
40
|
+
run(`git clone --quiet --depth 1 --branch ${tag} ${AW_ECC_REPO_SSH} ${dest}`);
|
|
41
|
+
} catch {
|
|
42
|
+
run(`git clone --quiet --depth 1 --branch ${tag} ${AW_ECC_REPO_HTTPS} ${dest}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function installClaudePlugin(repoDir) {
|
|
47
|
+
try {
|
|
48
|
+
run(`claude plugin marketplace add ${repoDir} --scope user`);
|
|
49
|
+
} catch {
|
|
50
|
+
try { run(`claude plugin marketplace update ${MARKETPLACE_NAME}`); } catch { /* ok */ }
|
|
51
|
+
}
|
|
52
|
+
run(`claude plugin install ${PLUGIN_KEY} --scope user`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function uninstallClaudePlugin() {
|
|
56
|
+
try { run(`claude plugin uninstall ${PLUGIN_KEY} --scope user`); } catch { /* not installed */ }
|
|
57
|
+
try { run(`claude plugin marketplace remove ${MARKETPLACE_NAME}`); } catch { /* not registered */ }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function installAwEcc(
|
|
61
|
+
cwd,
|
|
62
|
+
{ targets = ["cursor", "claude", "codex"], silent = false } = {},
|
|
63
|
+
) {
|
|
64
|
+
if (!silent) fmt.logStep("Installing aw-ecc engine...");
|
|
65
|
+
|
|
66
|
+
const repoDir = eccDir();
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
cloneOrUpdate(AW_ECC_TAG, repoDir);
|
|
70
|
+
|
|
71
|
+
// Claude Code: plugin install via marketplace CLI (proper agent dispatch)
|
|
72
|
+
if (targets.includes("claude")) {
|
|
73
|
+
try {
|
|
74
|
+
installClaudePlugin(repoDir);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (!silent) fmt.logWarn(`Claude plugin install skipped: ${err.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Cursor + Codex: file-copy via install-apply.js
|
|
81
|
+
const fileCopyTargets = targets.filter((t) => FILE_COPY_TARGETS.includes(t));
|
|
82
|
+
if (fileCopyTargets.length > 0) {
|
|
83
|
+
run("npm install --no-audit --no-fund --ignore-scripts --loglevel=error", {
|
|
84
|
+
cwd: repoDir,
|
|
85
|
+
});
|
|
86
|
+
for (const target of fileCopyTargets) {
|
|
87
|
+
try {
|
|
88
|
+
run(
|
|
89
|
+
`node ${join(repoDir, "scripts/install-apply.js")} --target ${target} --profile full`,
|
|
90
|
+
{ cwd },
|
|
91
|
+
);
|
|
92
|
+
} catch { /* target not supported — skip */ }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!silent) fmt.logSuccess("aw-ecc engine installed");
|
|
97
|
+
} catch (err) {
|
|
98
|
+
if (!silent) fmt.logWarn(`aw-ecc install failed: ${err.message}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function uninstallAwEcc({ silent = false } = {}) {
|
|
103
|
+
const HOME = homedir();
|
|
104
|
+
let removed = 0;
|
|
105
|
+
|
|
106
|
+
// Claude Code: uninstall plugin + remove marketplace via CLI
|
|
107
|
+
try {
|
|
108
|
+
uninstallClaudePlugin();
|
|
109
|
+
removed++;
|
|
110
|
+
} catch { /* best effort */ }
|
|
111
|
+
|
|
112
|
+
// Cursor + Codex: remove file-copied content via install-state
|
|
113
|
+
for (const cfg of Object.values(TARGET_STATE)) {
|
|
114
|
+
const statePath = join(HOME, cfg.state);
|
|
115
|
+
if (!existsSync(statePath)) continue;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const data = JSON.parse(readFileSync(statePath, "utf8"));
|
|
119
|
+
for (const op of data.operations || []) {
|
|
120
|
+
if (op.destinationPath && existsSync(op.destinationPath)) {
|
|
121
|
+
rmSync(op.destinationPath, { recursive: true, force: true });
|
|
122
|
+
removed++;
|
|
123
|
+
pruneEmptyParents(op.destinationPath, join(HOME, cfg.state.split("/")[0]));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
rmSync(statePath, { force: true });
|
|
127
|
+
pruneEmptyParents(statePath, join(HOME, cfg.state.split("/")[0]));
|
|
128
|
+
} catch { /* corrupted state — skip */ }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Clean leftover claude install-state from older file-copy versions
|
|
132
|
+
const claudeState = join(HOME, ".claude", "ecc", "install-state.json");
|
|
133
|
+
if (existsSync(claudeState)) {
|
|
134
|
+
try {
|
|
135
|
+
const data = JSON.parse(readFileSync(claudeState, "utf8"));
|
|
136
|
+
for (const op of data.operations || []) {
|
|
137
|
+
if (op.destinationPath && existsSync(op.destinationPath)) {
|
|
138
|
+
rmSync(op.destinationPath, { recursive: true, force: true });
|
|
139
|
+
removed++;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
rmSync(claudeState, { force: true });
|
|
143
|
+
pruneEmptyParents(claudeState, join(HOME, ".claude"));
|
|
144
|
+
} catch { /* best effort */ }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Clean leftover manual plugin cache from older versions
|
|
148
|
+
const oldCache = join(HOME, ".claude", "plugins", "cache", "aw");
|
|
149
|
+
if (existsSync(oldCache)) {
|
|
150
|
+
rmSync(oldCache, { recursive: true, force: true });
|
|
151
|
+
removed++;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Remove permanent aw-ecc repo clone
|
|
155
|
+
const repoDir = eccDir();
|
|
156
|
+
if (existsSync(repoDir)) {
|
|
157
|
+
rmSync(repoDir, { recursive: true, force: true });
|
|
158
|
+
removed++;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!silent && removed > 0)
|
|
162
|
+
fmt.logStep(`Removed ${removed} aw-ecc file${removed > 1 ? "s" : ""}`);
|
|
163
|
+
return removed;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function pruneEmptyParents(filePath, stopAt) {
|
|
167
|
+
let dir = dirname(filePath);
|
|
168
|
+
while (dir !== stopAt && dir.startsWith(stopAt)) {
|
|
169
|
+
try {
|
|
170
|
+
if (readdirSync(dir).length === 0) {
|
|
171
|
+
rmSync(dir);
|
|
172
|
+
dir = dirname(dir);
|
|
173
|
+
} else {
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
package/fmt.mjs
CHANGED
|
@@ -36,6 +36,8 @@ export function banner(text, opts = {}) {
|
|
|
36
36
|
export const intro = (msg) => p.intro(chalk.bgCyan.black(` ${msg} `));
|
|
37
37
|
export const outro = (msg) => p.outro(chalk.green(msg));
|
|
38
38
|
export const spinner = () => p.spinner();
|
|
39
|
+
export const select = p.select;
|
|
40
|
+
export const isCancel = p.isCancel;
|
|
39
41
|
|
|
40
42
|
export function cancel(msg) {
|
|
41
43
|
p.cancel(msg);
|
package/git.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// git.mjs — Git sparse checkout
|
|
1
|
+
// git.mjs — Git helpers: sparse checkout (temp) + persistent clone operations.
|
|
2
2
|
|
|
3
3
|
import { execSync, exec as execCb } from 'node:child_process';
|
|
4
4
|
import { mkdtempSync, existsSync } from 'node:fs';
|
|
@@ -9,6 +9,8 @@ import { REGISTRY_BASE_BRANCH, REGISTRY_DIR, DOCS_SOURCE_DIR } from './constants
|
|
|
9
9
|
|
|
10
10
|
const exec = promisify(execCb);
|
|
11
11
|
|
|
12
|
+
// ── Backward-compat: temp-dir sparse checkout (used by search.mjs) ────────────
|
|
13
|
+
|
|
12
14
|
/**
|
|
13
15
|
* Sparse-checkout registry paths from GitHub (sync).
|
|
14
16
|
* Returns the temp directory path containing the checkout.
|
|
@@ -92,3 +94,233 @@ export function includeToSparsePaths(paths) {
|
|
|
92
94
|
}
|
|
93
95
|
return [...result];
|
|
94
96
|
}
|
|
97
|
+
|
|
98
|
+
// ── Persistent clone operations ────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if awHome is already a valid git clone of the given registry repo.
|
|
102
|
+
* Returns boolean.
|
|
103
|
+
*/
|
|
104
|
+
export function isValidClone(awHome, repoUrl) {
|
|
105
|
+
if (!existsSync(join(awHome, '.git'))) return false;
|
|
106
|
+
try {
|
|
107
|
+
const remote = execSync('git remote get-url origin', { cwd: awHome, stdio: 'pipe', encoding: 'utf8' }).trim();
|
|
108
|
+
return remote === repoUrl || remote === repoUrl.replace(/\.git$/, '') + '.git' || remote.replace(/\.git$/, '') === repoUrl.replace(/\.git$/, '');
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Initialize a persistent clone with sparse checkout at awHome.
|
|
116
|
+
* sparsePaths: array of repo-relative paths to include (e.g. ['.aw_registry/platform', 'content'])
|
|
117
|
+
*/
|
|
118
|
+
export function initPersistentClone(repoUrl, awHome, sparsePaths) {
|
|
119
|
+
try {
|
|
120
|
+
execSync(`git clone --filter=blob:none --no-checkout "${repoUrl}" "${awHome}"`, { stdio: 'pipe' });
|
|
121
|
+
} catch (e) {
|
|
122
|
+
throw new Error(`Failed to clone ${repoUrl}: ${e.message}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
execSync('git sparse-checkout init --cone', { cwd: awHome, stdio: 'pipe' });
|
|
127
|
+
execSync(`git sparse-checkout set ${sparsePaths.map(p => `"${p}"`).join(' ')}`, { cwd: awHome, stdio: 'pipe' });
|
|
128
|
+
execSync(`git checkout ${REGISTRY_BASE_BRANCH}`, { cwd: awHome, stdio: 'pipe' });
|
|
129
|
+
} catch (e) {
|
|
130
|
+
throw new Error(`Failed to configure sparse checkout: ${e.message}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Add paths to an existing sparse checkout.
|
|
136
|
+
* Merges with currently checked-out paths.
|
|
137
|
+
*/
|
|
138
|
+
export function addToSparseCheckout(awHome, newPaths) {
|
|
139
|
+
let existing = [];
|
|
140
|
+
try {
|
|
141
|
+
const out = execSync('git sparse-checkout list', { cwd: awHome, stdio: 'pipe', encoding: 'utf8' });
|
|
142
|
+
existing = out.trim().split('\n').filter(Boolean);
|
|
143
|
+
} catch {
|
|
144
|
+
// no sparse-checkout set yet — start fresh
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const combined = [...new Set([...existing, ...newPaths])];
|
|
148
|
+
try {
|
|
149
|
+
execSync(`git sparse-checkout set ${combined.map(p => `"${p}"`).join(' ')}`, { cwd: awHome, stdio: 'pipe' });
|
|
150
|
+
execSync(`git checkout ${REGISTRY_BASE_BRANCH}`, { cwd: awHome, stdio: 'pipe' });
|
|
151
|
+
} catch (e) {
|
|
152
|
+
throw new Error(`Failed to add to sparse checkout: ${e.message}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Remove paths from an existing sparse checkout.
|
|
158
|
+
* Remaining paths stay checked out.
|
|
159
|
+
*/
|
|
160
|
+
export function removeFromSparseCheckout(awHome, removePaths) {
|
|
161
|
+
let existing = [];
|
|
162
|
+
try {
|
|
163
|
+
const out = execSync('git sparse-checkout list', { cwd: awHome, stdio: 'pipe', encoding: 'utf8' });
|
|
164
|
+
existing = out.trim().split('\n').filter(Boolean);
|
|
165
|
+
} catch {
|
|
166
|
+
return; // nothing to remove
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const removeSet = new Set(removePaths);
|
|
170
|
+
const remaining = existing.filter(p => !removeSet.has(p));
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
if (remaining.length > 0) {
|
|
174
|
+
execSync(`git sparse-checkout set ${remaining.map(p => `"${p}"`).join(' ')}`, { cwd: awHome, stdio: 'pipe' });
|
|
175
|
+
} else {
|
|
176
|
+
// Disable sparse checkout entirely
|
|
177
|
+
execSync('git sparse-checkout disable', { cwd: awHome, stdio: 'pipe' });
|
|
178
|
+
}
|
|
179
|
+
} catch (e) {
|
|
180
|
+
throw new Error(`Failed to remove from sparse checkout: ${e.message}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Fetch and merge latest from origin/main into the persistent clone.
|
|
186
|
+
* Returns { updated: boolean, conflicts: string[] }
|
|
187
|
+
*/
|
|
188
|
+
export function fetchAndMerge(awHome) {
|
|
189
|
+
try {
|
|
190
|
+
execSync(`git -C "${awHome}" fetch origin ${REGISTRY_BASE_BRANCH}`, { stdio: 'pipe' });
|
|
191
|
+
} catch (e) {
|
|
192
|
+
throw new Error(`Failed to fetch from origin: ${e.message}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let updated = false;
|
|
196
|
+
const conflicts = [];
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const result = execSync(`git -C "${awHome}" merge origin/${REGISTRY_BASE_BRANCH} --ff-only`, {
|
|
200
|
+
stdio: 'pipe',
|
|
201
|
+
encoding: 'utf8',
|
|
202
|
+
});
|
|
203
|
+
updated = !result.includes('Already up to date');
|
|
204
|
+
} catch {
|
|
205
|
+
// ff-only failed — try no-edit merge
|
|
206
|
+
try {
|
|
207
|
+
execSync(`git -C "${awHome}" merge origin/${REGISTRY_BASE_BRANCH} --no-edit`, {
|
|
208
|
+
stdio: 'pipe',
|
|
209
|
+
encoding: 'utf8',
|
|
210
|
+
});
|
|
211
|
+
updated = true;
|
|
212
|
+
} catch (mergeErr) {
|
|
213
|
+
// Parse conflicts
|
|
214
|
+
try {
|
|
215
|
+
const statusOut = execSync(`git -C "${awHome}" diff --name-only --diff-filter=U`, {
|
|
216
|
+
stdio: 'pipe',
|
|
217
|
+
encoding: 'utf8',
|
|
218
|
+
});
|
|
219
|
+
conflicts.push(...statusOut.trim().split('\n').filter(Boolean));
|
|
220
|
+
} catch { /* best effort */ }
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return { updated, conflicts };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Detect local changes in registryDir/ within awHome.
|
|
229
|
+
* Returns { modified: [], added: [], deleted: [], untracked: [] }
|
|
230
|
+
* Each entry: { path: 'relative/to/awHome', registryPath: 'relative/to/registryDir' }
|
|
231
|
+
*/
|
|
232
|
+
export function detectChanges(awHome, registryDir) {
|
|
233
|
+
let statusOut = '';
|
|
234
|
+
try {
|
|
235
|
+
statusOut = execSync(`git -C "${awHome}" status --porcelain "${registryDir}/"`, {
|
|
236
|
+
stdio: 'pipe',
|
|
237
|
+
encoding: 'utf8',
|
|
238
|
+
});
|
|
239
|
+
} catch {
|
|
240
|
+
return { modified: [], added: [], deleted: [], untracked: [] };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const modified = [];
|
|
244
|
+
const added = [];
|
|
245
|
+
const deleted = [];
|
|
246
|
+
const untracked = [];
|
|
247
|
+
|
|
248
|
+
for (const line of statusOut.split('\n')) {
|
|
249
|
+
if (!line || line.length < 3) continue;
|
|
250
|
+
const xy = line.slice(0, 2);
|
|
251
|
+
const filePath = line.slice(3).trim();
|
|
252
|
+
const registryPath = filePath.startsWith(registryDir + '/') ? filePath.slice(registryDir.length + 1) : filePath;
|
|
253
|
+
const entry = { path: filePath, registryPath };
|
|
254
|
+
|
|
255
|
+
if (xy === '??') {
|
|
256
|
+
untracked.push(entry);
|
|
257
|
+
} else if (xy[0] === 'D' || xy[1] === 'D') {
|
|
258
|
+
deleted.push(entry);
|
|
259
|
+
} else if (xy[0] === 'A' || xy[1] === 'A') {
|
|
260
|
+
added.push(entry);
|
|
261
|
+
} else if (xy[0] === 'M' || xy[1] === 'M') {
|
|
262
|
+
modified.push(entry);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return { modified, added, deleted, untracked };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Create a branch, stage files, commit, push to origin.
|
|
271
|
+
* Branch stays on disk for iteration.
|
|
272
|
+
* Returns the branch name.
|
|
273
|
+
*/
|
|
274
|
+
export function createPushBranch(awHome, branchName, files, commitMsg) {
|
|
275
|
+
try {
|
|
276
|
+
execSync(`git -C "${awHome}" checkout -b "${branchName}"`, { stdio: 'pipe' });
|
|
277
|
+
} catch (e) {
|
|
278
|
+
throw new Error(`Failed to create branch ${branchName}: ${e.message}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const quotedFiles = files.map(f => `"${f}"`).join(' ');
|
|
283
|
+
execSync(`git -C "${awHome}" add ${quotedFiles}`, { stdio: 'pipe' });
|
|
284
|
+
} catch (e) {
|
|
285
|
+
throw new Error(`Failed to stage files: ${e.message}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
execSync(`git -C "${awHome}" commit -m "${commitMsg.replace(/"/g, '\\"')}"`, { stdio: 'pipe' });
|
|
290
|
+
} catch (e) {
|
|
291
|
+
throw new Error(`Failed to commit: ${e.message}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
execSync(`git -C "${awHome}" push -u origin "${branchName}"`, { stdio: 'pipe' });
|
|
296
|
+
} catch (e) {
|
|
297
|
+
throw new Error(`Failed to push branch: ${e.message}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return branchName;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Get the current branch name in awHome.
|
|
305
|
+
*/
|
|
306
|
+
export function getCurrentBranch(awHome) {
|
|
307
|
+
try {
|
|
308
|
+
return execSync(`git -C "${awHome}" rev-parse --abbrev-ref HEAD`, {
|
|
309
|
+
stdio: 'pipe',
|
|
310
|
+
encoding: 'utf8',
|
|
311
|
+
}).trim();
|
|
312
|
+
} catch {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Checkout the main branch in awHome.
|
|
319
|
+
*/
|
|
320
|
+
export function checkoutMain(awHome) {
|
|
321
|
+
try {
|
|
322
|
+
execSync(`git -C "${awHome}" checkout ${REGISTRY_BASE_BRANCH}`, { stdio: 'pipe' });
|
|
323
|
+
} catch (e) {
|
|
324
|
+
throw new Error(`Failed to checkout ${REGISTRY_BASE_BRANCH}: ${e.message}`);
|
|
325
|
+
}
|
|
326
|
+
}
|