@leejungkiin/awkit 1.0.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/CHANGELOG.md +27 -0
- package/README.md +146 -0
- package/VERSION +1 -0
- package/bin/awf.js +549 -0
- package/bin/awk.js +1759 -0
- package/core/AGENTS.md +39 -0
- package/core/GEMINI.md +202 -0
- package/core/GEMINI.md.bak +244 -0
- package/core/orchestrator.md +58 -0
- package/package.json +46 -0
- package/schemas/brain.schema.json +342 -0
- package/schemas/preferences.schema.json +95 -0
- package/schemas/session.schema.json +112 -0
- package/skill-packs/neural-memory/README.md +111 -0
- package/skill-packs/neural-memory/pack.json +35 -0
- package/skill-packs/neural-memory/schemas/brain-snapshot.json +167 -0
- package/skill-packs/neural-memory/skills/nm-memory-audit/SKILL.md +157 -0
- package/skill-packs/neural-memory/skills/nm-memory-evolution/SKILL.md +202 -0
- package/skill-packs/neural-memory/skills/nm-memory-intake/SKILL.md +135 -0
- package/skill-packs/neural-memory/skills/nm-memory-sync/SKILL.md +184 -0
- package/skill-packs/neural-memory/workflows/nm-import.md +73 -0
- package/skill-packs/neural-memory/workflows/nm-recall.md +67 -0
- package/skill-packs/neural-memory/workflows/nm-snapshot.md +69 -0
- package/skills/adaptive-language/SKILL.md +189 -0
- package/skills/ambient-brain/SKILL.md +314 -0
- package/skills/ambient-brain/brain-router.md +185 -0
- package/skills/ambient-brain/brain-templates.md +201 -0
- package/skills/auto-save/SKILL.md +223 -0
- package/skills/awf-adaptive-language/SKILL.md +189 -0
- package/skills/awf-context-help/SKILL.md +180 -0
- package/skills/awf-error-translator/SKILL.md +153 -0
- package/skills/awf-session-restore/SKILL.md +270 -0
- package/skills/awf-version-tracker/SKILL.md +32 -0
- package/skills/awf-version-tracker/scripts/snapshot.sh +22 -0
- package/skills/beads-manager/SKILL.md +323 -0
- package/skills/brainstorm-agent/SKILL.md +295 -0
- package/skills/context-help/SKILL.md +180 -0
- package/skills/error-translator/SKILL.md +153 -0
- package/skills/ios-engineer/SKILL.md +101 -0
- package/skills/memory-sync/SKILL.md +378 -0
- package/skills/memory-sync/memory-router.md +185 -0
- package/skills/memory-sync/memory-templates.md +201 -0
- package/skills/orchestrator/SKILL.md +193 -0
- package/skills/session-restore/SKILL.md +240 -0
- package/templates/CODEBASE.md +80 -0
- package/templates/brain.example.json +321 -0
- package/templates/preferences.example.json +21 -0
- package/templates/project-identity/android.json +28 -0
- package/templates/project-identity/backend-nestjs.json +24 -0
- package/templates/project-identity/expo.json +27 -0
- package/templates/project-identity/ios.json +27 -0
- package/templates/project-identity/web-nextjs.json +24 -0
- package/templates/session.example.json +53 -0
- package/templates/specs/design-template.md +166 -0
- package/templates/specs/requirements-template.md +65 -0
- package/templates/specs/tasks-template.md +132 -0
- package/templates/structures/android.txt +10 -0
- package/templates/structures/backend-nestjs.txt +6 -0
- package/templates/structures/expo.txt +9 -0
- package/templates/structures/ios.txt +9 -0
- package/templates/structures/web-nextjs.txt +6 -0
- package/templates/workflow_dual_mode_template.md +87 -0
- package/workflows/_uncategorized/README.md +339 -0
- package/workflows/_uncategorized/ads-creative.md +357 -0
- package/workflows/_uncategorized/ads-full-optimization.md +308 -0
- package/workflows/_uncategorized/ads-plan.md +247 -0
- package/workflows/_uncategorized/ads-user-analysis.md +337 -0
- package/workflows/_uncategorized/skill-health.md +35 -0
- package/workflows/_uncategorized/skill-rollback.md +35 -0
- package/workflows/ads/admob.md +62 -0
- package/workflows/ads/ads-analyst.md +201 -0
- package/workflows/ads/ads-audit.md +106 -0
- package/workflows/ads/ads-optimize.md +97 -0
- package/workflows/ads/ads-targeting.md +241 -0
- package/workflows/ads/adsExpert.md +160 -0
- package/workflows/ads/smali-ads-config.md +400 -0
- package/workflows/ads/smali-ads-flow.md +331 -0
- package/workflows/ads/smali-ads-interstitial.md +377 -0
- package/workflows/ads/smali-ads-native.md +382 -0
- package/workflows/context/auto-execution-workflow.md +291 -0
- package/workflows/context/auto-implement.md +211 -0
- package/workflows/context/codebase-sync.md +163 -0
- package/workflows/context/logic-reasoning-workflow.md +260 -0
- package/workflows/context/next.md +195 -0
- package/workflows/context/recap.md +212 -0
- package/workflows/context/save-brain.md +285 -0
- package/workflows/context/user-intent-analysis-workflow.md +206 -0
- package/workflows/expert/codeExpert.md +126 -0
- package/workflows/expert/debugExpert.md +136 -0
- package/workflows/expert/planExpert.md +112 -0
- package/workflows/git/cloudflare-tunnel.md +135 -0
- package/workflows/git/git-commit-workflow.md +75 -0
- package/workflows/git/hotfix.md +357 -0
- package/workflows/git/release-notes.md +160 -0
- package/workflows/git/rollback.md +52 -0
- package/workflows/git/smart-git-ops.md +103 -0
- package/workflows/lifecycle/brainstorm.md +377 -0
- package/workflows/lifecycle/code.md +663 -0
- package/workflows/lifecycle/debug.md +116 -0
- package/workflows/lifecycle/deploy.md +95 -0
- package/workflows/lifecycle/init.md +152 -0
- package/workflows/lifecycle/master-code-workflow.md +300 -0
- package/workflows/lifecycle/migration.md +196 -0
- package/workflows/lifecycle/plan.md +91 -0
- package/workflows/lifecycle/refactor.md +165 -0
- package/workflows/lifecycle/run.md +52 -0
- package/workflows/lifecycle/test.md +91 -0
- package/workflows/meta/customize.md +346 -0
- package/workflows/meta/file-protection-rules.md +129 -0
- package/workflows/meta/help.html +350 -0
- package/workflows/meta/project-identity-enforcement.md +180 -0
- package/workflows/mobile/app-analysis.md +64 -0
- package/workflows/mobile/maestro-qa-workflow.md +470 -0
- package/workflows/mobile/maestro-test-workflow.md +84 -0
- package/workflows/mobile/structure-clean-architect.md +271 -0
- package/workflows/mobile/turbo-mobile-build.md +190 -0
- package/workflows/quality/accessibility-audit.md +311 -0
- package/workflows/quality/audit.md +217 -0
- package/workflows/quality/bug-hunter.md +243 -0
- package/workflows/quality/code-janitor.md +209 -0
- package/workflows/quality/code-quality-rules.md +132 -0
- package/workflows/quality/performance-audit.md +343 -0
- package/workflows/quality/project-audit.md +61 -0
- package/workflows/quality/self-healing-test.md +192 -0
- package/workflows/quality/ui-review.md +130 -0
- package/workflows/quality/ux-audit.md +213 -0
- package/workflows/quality/visual-debug.md +34 -0
- package/workflows/roles/oracle.md +267 -0
- package/workflows/roles/product-manager-workflow.md +52 -0
- package/workflows/roles/qa-engineer-workflow.md +41 -0
- package/workflows/roles/tech-lead-workflow.md +45 -0
- package/workflows/roles/ui-ux-designer-workflow.md +42 -0
- package/workflows/roles/vibe-coding-master-workflow.md +52 -0
- package/workflows/ui/app-screen-analyzer.md +152 -0
- package/workflows/ui/create-feature.md +332 -0
- package/workflows/ui/create-spec-architect.md +184 -0
- package/workflows/ui/design-to-ui.md +308 -0
- package/workflows/ui/ui-first-methodology.md +279 -0
- package/workflows/ui/visualize.md +298 -0
package/bin/awk.js
ADDED
|
@@ -0,0 +1,1759 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AWK v1.0 CLI — Antigravity Workflow Kit
|
|
5
|
+
* Unified installer, updater, and manager for AI agent workflows.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* awkit install Install AWK into ~/.gemini/antigravity/
|
|
9
|
+
* awkit uninstall Remove AWK from system
|
|
10
|
+
* awkit update Update to latest version
|
|
11
|
+
* awkit init Init a new mobile project with Firebase setup
|
|
12
|
+
* awkit sync Harvest from ~/.gemini/ then install (full sync)
|
|
13
|
+
* awkit status Compare repo vs installed files (diff view)
|
|
14
|
+
* awkit harvest Pull from ~/.gemini/antigravity/ into repo
|
|
15
|
+
* awkit doctor Check installation health
|
|
16
|
+
* awkit enable-pack Enable a skill pack
|
|
17
|
+
* awkit disable-pack Disable a skill pack
|
|
18
|
+
* awkit list-packs List available skill packs
|
|
19
|
+
* awkit version Show current version
|
|
20
|
+
*
|
|
21
|
+
* Created by Kien AI
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const { execSync, spawnSync } = require('child_process');
|
|
27
|
+
|
|
28
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const AWK_VERSION = fs.readFileSync(path.join(__dirname, '..', 'VERSION'), 'utf8').trim();
|
|
31
|
+
const AWK_ROOT = path.join(__dirname, '..');
|
|
32
|
+
const HOME = process.env.HOME || process.env.USERPROFILE;
|
|
33
|
+
|
|
34
|
+
const TARGETS = {
|
|
35
|
+
antigravity: path.join(HOME, '.gemini', 'antigravity'),
|
|
36
|
+
geminiMd: path.join(HOME, '.gemini', 'GEMINI.md'),
|
|
37
|
+
versionFile: path.join(HOME, '.gemini', 'awk_version'),
|
|
38
|
+
agentsDir: null, // Set per-project
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Mapping: source dir in package → target dir in antigravity
|
|
42
|
+
const SYNC_MAP = {
|
|
43
|
+
'core/GEMINI.md': 'GEMINI.md',
|
|
44
|
+
'core/AGENTS.md': 'global_workflows/AGENTS.md',
|
|
45
|
+
'core/orchestrator.md': 'skills/orchestrator/SKILL.md',
|
|
46
|
+
'workflows': 'global_workflows',
|
|
47
|
+
'skills': 'skills',
|
|
48
|
+
'schemas': 'schemas',
|
|
49
|
+
'templates': 'templates',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// ─── Colors ──────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
const C = {
|
|
55
|
+
reset: '\x1b[0m',
|
|
56
|
+
red: '\x1b[31m',
|
|
57
|
+
green: '\x1b[32m',
|
|
58
|
+
yellow: '\x1b[33m',
|
|
59
|
+
cyan: '\x1b[36m',
|
|
60
|
+
gray: '\x1b[90m',
|
|
61
|
+
bold: '\x1b[1m',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function log(msg) { console.log(msg); }
|
|
65
|
+
function ok(msg) { log(`${C.green}✅ ${msg}${C.reset}`); }
|
|
66
|
+
function warn(msg) { log(`${C.yellow}⚠️ ${msg}${C.reset}`); }
|
|
67
|
+
function err(msg) { log(`${C.red}❌ ${msg}${C.reset}`); }
|
|
68
|
+
function info(msg) { log(`${C.cyan}ℹ️ ${msg}${C.reset}`); }
|
|
69
|
+
function dim(msg) { log(`${C.gray} ${msg}${C.reset}`); }
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Prompt user for Y/N input synchronously (macOS/Linux only).
|
|
73
|
+
* Returns true if user answers 'y' or 'yes'.
|
|
74
|
+
*/
|
|
75
|
+
function promptYN(question) {
|
|
76
|
+
try {
|
|
77
|
+
const answer = execSync(
|
|
78
|
+
`bash -c 'read -p "${question} [y/N]: " ans; echo $ans'`,
|
|
79
|
+
{ stdio: ['inherit', 'pipe', 'inherit'] }
|
|
80
|
+
).toString().trim().toLowerCase();
|
|
81
|
+
return answer === 'y' || answer === 'yes';
|
|
82
|
+
} catch (e) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── Utility Functions ──────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Recursively copy directory, preserving structure.
|
|
91
|
+
* Does NOT overwrite files prefixed with `.` (user configs).
|
|
92
|
+
*/
|
|
93
|
+
function copyDirRecursive(src, dest, options = {}) {
|
|
94
|
+
const { flatten = false, dryRun = false, overwrite = true } = options;
|
|
95
|
+
let count = 0;
|
|
96
|
+
|
|
97
|
+
if (!fs.existsSync(src)) return count;
|
|
98
|
+
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
99
|
+
|
|
100
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
101
|
+
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
if (entry.name === '.DS_Store') continue;
|
|
104
|
+
|
|
105
|
+
const srcPath = path.join(src, entry.name);
|
|
106
|
+
|
|
107
|
+
if (entry.isDirectory()) {
|
|
108
|
+
if (flatten) {
|
|
109
|
+
// Flatten: copy workflow files from subcategories into single dir
|
|
110
|
+
count += copyDirRecursive(srcPath, dest, options);
|
|
111
|
+
} else {
|
|
112
|
+
const destPath = path.join(dest, entry.name);
|
|
113
|
+
count += copyDirRecursive(srcPath, destPath, options);
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
const destPath = path.join(dest, entry.name);
|
|
117
|
+
|
|
118
|
+
// Skip user config files if they already exist and overwrite is off
|
|
119
|
+
if (!overwrite && fs.existsSync(destPath)) {
|
|
120
|
+
dim(`Skip (exists): ${entry.name}`);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!dryRun) {
|
|
125
|
+
fs.copyFileSync(srcPath, destPath);
|
|
126
|
+
}
|
|
127
|
+
count++;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return count;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Flatten workflow category dirs into single global_workflows dir.
|
|
136
|
+
* workflows/lifecycle/code.md → global_workflows/code.md
|
|
137
|
+
* workflows/context/recap.md → global_workflows/recap.md
|
|
138
|
+
*/
|
|
139
|
+
function flattenWorkflows(srcBase, destDir) {
|
|
140
|
+
let count = 0;
|
|
141
|
+
|
|
142
|
+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
143
|
+
|
|
144
|
+
const categories = fs.readdirSync(srcBase, { withFileTypes: true });
|
|
145
|
+
|
|
146
|
+
for (const cat of categories) {
|
|
147
|
+
if (!cat.isDirectory()) continue;
|
|
148
|
+
|
|
149
|
+
const catPath = path.join(srcBase, cat.name);
|
|
150
|
+
const files = fs.readdirSync(catPath, { withFileTypes: true });
|
|
151
|
+
|
|
152
|
+
for (const file of files) {
|
|
153
|
+
if (file.name === '.DS_Store') continue;
|
|
154
|
+
|
|
155
|
+
if (file.isFile() && file.name.endsWith('.md')) {
|
|
156
|
+
const srcFile = path.join(catPath, file.name);
|
|
157
|
+
const destFile = path.join(destDir, file.name);
|
|
158
|
+
fs.copyFileSync(srcFile, destFile);
|
|
159
|
+
count++;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return count;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Install or update GEMINI.md into ~/.gemini/
|
|
169
|
+
*/
|
|
170
|
+
function syncGeminiMd() {
|
|
171
|
+
const srcGemini = path.join(AWK_ROOT, 'core', 'GEMINI.md');
|
|
172
|
+
const destGemini = TARGETS.geminiMd;
|
|
173
|
+
|
|
174
|
+
const destDir = path.dirname(destGemini);
|
|
175
|
+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
176
|
+
|
|
177
|
+
// Read existing GEMINI.md if present
|
|
178
|
+
let existingContent = '';
|
|
179
|
+
if (fs.existsSync(destGemini)) {
|
|
180
|
+
existingContent = fs.readFileSync(destGemini, 'utf8');
|
|
181
|
+
|
|
182
|
+
// Backup existing
|
|
183
|
+
const backupDir = path.join(destDir, 'antigravity', 'backup');
|
|
184
|
+
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
|
|
185
|
+
|
|
186
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
187
|
+
const backupPath = path.join(backupDir, `GEMINI_${timestamp}.md.bak`);
|
|
188
|
+
fs.copyFileSync(destGemini, backupPath);
|
|
189
|
+
dim(`Backup: ${backupPath}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Copy new GEMINI.md
|
|
193
|
+
fs.copyFileSync(srcGemini, destGemini);
|
|
194
|
+
ok('GEMINI.md updated');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ─── Commands ────────────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Ensure 'bd' (Beads) is installed. Install via Homebrew if missing.
|
|
201
|
+
* Returns true if bd is available after the call.
|
|
202
|
+
*/
|
|
203
|
+
function installBeads({ silent = false } = {}) {
|
|
204
|
+
// Check if bd already installed
|
|
205
|
+
try {
|
|
206
|
+
execSync('which bd', { stdio: 'ignore' });
|
|
207
|
+
if (!silent) ok('bd (Beads) already installed');
|
|
208
|
+
return true;
|
|
209
|
+
} catch (_) { /* not installed */ }
|
|
210
|
+
|
|
211
|
+
if (!silent) info('bd (Beads) not found — installing via Homebrew...');
|
|
212
|
+
|
|
213
|
+
// Check if brew is available
|
|
214
|
+
let brewAvailable = false;
|
|
215
|
+
try { execSync('which brew', { stdio: 'ignore' }); brewAvailable = true; } catch (_) { }
|
|
216
|
+
|
|
217
|
+
if (!brewAvailable) {
|
|
218
|
+
warn('Homebrew not found. Please install bd manually:');
|
|
219
|
+
dim(' brew install beads');
|
|
220
|
+
dim(' or visit: https://github.com/steveyegge/beads');
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
if (!silent) info('Running: brew install beads');
|
|
226
|
+
execSync('brew install beads', { stdio: silent ? 'pipe' : 'inherit' });
|
|
227
|
+
// Verify install
|
|
228
|
+
execSync('which bd', { stdio: 'ignore' });
|
|
229
|
+
if (!silent) ok('bd (Beads) installed successfully ✨');
|
|
230
|
+
return true;
|
|
231
|
+
} catch (e) {
|
|
232
|
+
warn(`Failed to install bd via brew: ${e.message}`);
|
|
233
|
+
dim('Try manually: brew install beads');
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function cmdInstall() {
|
|
239
|
+
log('');
|
|
240
|
+
log(`${C.cyan}${C.bold}╔══════════════════════════════════════════════════════════╗${C.reset}`);
|
|
241
|
+
log(`${C.cyan}${C.bold}║ 🚀 AWK v${AWK_VERSION} — Antigravity Workflow Kit ║${C.reset}`);
|
|
242
|
+
log(`${C.cyan}${C.bold}╚══════════════════════════════════════════════════════════╝${C.reset}`);
|
|
243
|
+
log('');
|
|
244
|
+
|
|
245
|
+
const target = TARGETS.antigravity;
|
|
246
|
+
|
|
247
|
+
// 0. Install bd (Beads) if missing
|
|
248
|
+
log('');
|
|
249
|
+
info('Checking dependencies...');
|
|
250
|
+
installBeads();
|
|
251
|
+
|
|
252
|
+
// 1. Ensure target dirs exist
|
|
253
|
+
info('Creating directories...');
|
|
254
|
+
const dirs = ['global_workflows', 'skills', 'schemas', 'templates'];
|
|
255
|
+
for (const dir of dirs) {
|
|
256
|
+
const fullPath = path.join(target, dir);
|
|
257
|
+
if (!fs.existsSync(fullPath)) {
|
|
258
|
+
fs.mkdirSync(fullPath, { recursive: true });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
ok('Directories ready');
|
|
262
|
+
|
|
263
|
+
// 2. Sync GEMINI.md
|
|
264
|
+
info('Syncing GEMINI.md...');
|
|
265
|
+
syncGeminiMd();
|
|
266
|
+
|
|
267
|
+
// 3. Backup and flatten workflows
|
|
268
|
+
info('Installing workflows...');
|
|
269
|
+
const wfSrc = path.join(AWK_ROOT, 'workflows');
|
|
270
|
+
const wfDest = path.join(target, 'global_workflows');
|
|
271
|
+
|
|
272
|
+
// Backup existing global_workflows to a zip file
|
|
273
|
+
if (fs.existsSync(wfDest)) {
|
|
274
|
+
const backupDir = path.join(target, 'backup');
|
|
275
|
+
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
|
|
276
|
+
|
|
277
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
278
|
+
const zipFile = path.join(backupDir, `global_workflows_${timestamp}.bak.zip`);
|
|
279
|
+
try {
|
|
280
|
+
info('Creating zip backup of global_workflows...');
|
|
281
|
+
execSync(`zip -r "${zipFile}" .`, { cwd: wfDest, stdio: 'ignore' });
|
|
282
|
+
ok(`Backup created: ${zipFile}`);
|
|
283
|
+
} catch (e) {
|
|
284
|
+
warn(`Failed to create backup zip: ${e.message}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const wfCount = flattenWorkflows(wfSrc, wfDest);
|
|
289
|
+
ok(`${wfCount} workflows installed`);
|
|
290
|
+
|
|
291
|
+
// 4. Copy AGENTS.md
|
|
292
|
+
const agentsSrc = path.join(AWK_ROOT, 'core', 'AGENTS.md');
|
|
293
|
+
const agentsDest = path.join(target, 'global_workflows', 'AGENTS.md');
|
|
294
|
+
if (fs.existsSync(agentsSrc)) {
|
|
295
|
+
fs.copyFileSync(agentsSrc, agentsDest);
|
|
296
|
+
ok('AGENTS.md installed');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// 5. Copy skills
|
|
300
|
+
info('Installing skills...');
|
|
301
|
+
const skillsSrc = path.join(AWK_ROOT, 'skills');
|
|
302
|
+
const skillsDest = path.join(target, 'skills');
|
|
303
|
+
const skillCount = copyDirRecursive(skillsSrc, skillsDest);
|
|
304
|
+
ok(`${skillCount} skill files installed`);
|
|
305
|
+
|
|
306
|
+
// 6. Copy orchestrator
|
|
307
|
+
const orchSrc = path.join(AWK_ROOT, 'core', 'orchestrator.md');
|
|
308
|
+
const orchDestDir = path.join(target, 'skills', 'orchestrator');
|
|
309
|
+
if (!fs.existsSync(orchDestDir)) fs.mkdirSync(orchDestDir, { recursive: true });
|
|
310
|
+
fs.copyFileSync(orchSrc, path.join(orchDestDir, 'SKILL.md'));
|
|
311
|
+
ok('Orchestrator skill installed');
|
|
312
|
+
|
|
313
|
+
// 7. Copy schemas (always overwrite)
|
|
314
|
+
info('Installing schemas...');
|
|
315
|
+
const schemaSrc = path.join(AWK_ROOT, 'schemas');
|
|
316
|
+
const schemaDest = path.join(target, 'schemas');
|
|
317
|
+
const schemaCount = copyDirRecursive(schemaSrc, schemaDest);
|
|
318
|
+
ok(`${schemaCount} schemas installed`);
|
|
319
|
+
|
|
320
|
+
// 8. Copy templates (don't overwrite existing)
|
|
321
|
+
info('Installing templates...');
|
|
322
|
+
const tmplSrc = path.join(AWK_ROOT, 'templates');
|
|
323
|
+
const tmplDest = path.join(target, 'templates');
|
|
324
|
+
const tmplCount = copyDirRecursive(tmplSrc, tmplDest, { overwrite: false });
|
|
325
|
+
ok(`${tmplCount} templates installed`);
|
|
326
|
+
|
|
327
|
+
// 9. Save version
|
|
328
|
+
fs.writeFileSync(TARGETS.versionFile, AWK_VERSION);
|
|
329
|
+
ok(`Version ${AWK_VERSION} saved`);
|
|
330
|
+
|
|
331
|
+
// 10. Install default skill packs
|
|
332
|
+
const defaultPacks = installDefaultPacks();
|
|
333
|
+
|
|
334
|
+
// 11. Summary
|
|
335
|
+
log('');
|
|
336
|
+
log(`${C.gray}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}`);
|
|
337
|
+
log(`${C.yellow}${C.bold}🎉 AWK v${AWK_VERSION} installed successfully!${C.reset}`);
|
|
338
|
+
log('');
|
|
339
|
+
dim(`Workflows: ${path.join(target, 'global_workflows')}`);
|
|
340
|
+
dim(`Skills: ${path.join(target, 'skills')}`);
|
|
341
|
+
dim(`Schemas: ${path.join(target, 'schemas')}`);
|
|
342
|
+
dim(`Templates: ${path.join(target, 'templates')}`);
|
|
343
|
+
dim(`GEMINI.md: ${TARGETS.geminiMd}`);
|
|
344
|
+
if (defaultPacks.length > 0) {
|
|
345
|
+
dim(`Packs: ${defaultPacks.join(', ')} (auto-enabled)`);
|
|
346
|
+
}
|
|
347
|
+
const bdVer = (() => { try { return execSync('bd --version', { encoding: 'utf8' }).trim().split('\n')[0]; } catch (_) { return 'installed'; } })();
|
|
348
|
+
dim(`Beads: ${bdVer} — task tracking ready`);
|
|
349
|
+
log('');
|
|
350
|
+
log(`${C.cyan}👉 Type '/plan' in your AI chat to get started.${C.reset}`);
|
|
351
|
+
log(`${C.cyan}👉 Run 'awkit init' in any project to initialize it.${C.reset}`);
|
|
352
|
+
log(`${C.cyan}👉 Run 'awkit doctor' to verify installation.${C.reset}`);
|
|
353
|
+
log('');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Scan skill-packs/ for packs with "auto_install": true in pack.json
|
|
358
|
+
* and enable each of them (copy files + handle requirements).
|
|
359
|
+
* Returns array of enabled pack names.
|
|
360
|
+
*/
|
|
361
|
+
function installDefaultPacks() {
|
|
362
|
+
const packsDir = path.join(AWK_ROOT, 'skill-packs');
|
|
363
|
+
if (!fs.existsSync(packsDir)) return [];
|
|
364
|
+
|
|
365
|
+
const enabled = [];
|
|
366
|
+
|
|
367
|
+
const packs = fs.readdirSync(packsDir, { withFileTypes: true })
|
|
368
|
+
.filter(d => d.isDirectory());
|
|
369
|
+
|
|
370
|
+
const defaultPacks = packs.filter(d => {
|
|
371
|
+
const cfgPath = path.join(packsDir, d.name, 'pack.json');
|
|
372
|
+
if (!fs.existsSync(cfgPath)) return false;
|
|
373
|
+
try {
|
|
374
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
375
|
+
return cfg.auto_install === true;
|
|
376
|
+
} catch (_) {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
if (defaultPacks.length === 0) return [];
|
|
382
|
+
|
|
383
|
+
log('');
|
|
384
|
+
log(`${C.cyan}${C.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}`);
|
|
385
|
+
log(`${C.cyan}${C.bold}📦 Installing default skill packs...${C.reset}`);
|
|
386
|
+
log('');
|
|
387
|
+
|
|
388
|
+
for (const pack of defaultPacks) {
|
|
389
|
+
log(`${C.yellow}▶ ${pack.name}${C.reset}`);
|
|
390
|
+
cmdEnablePack(pack.name, { autoMode: true });
|
|
391
|
+
enabled.push(pack.name);
|
|
392
|
+
log('');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return enabled;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function cmdUninstall() {
|
|
399
|
+
warn('Uninstalling AWK...');
|
|
400
|
+
|
|
401
|
+
// Remove version file
|
|
402
|
+
if (fs.existsSync(TARGETS.versionFile)) {
|
|
403
|
+
fs.unlinkSync(TARGETS.versionFile);
|
|
404
|
+
ok('Version file removed');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Don't remove workflows/skills — user may have custom ones
|
|
408
|
+
warn('Workflow and skill files were NOT removed (may contain custom content).');
|
|
409
|
+
warn('To fully remove, manually delete:');
|
|
410
|
+
dim(TARGETS.antigravity);
|
|
411
|
+
|
|
412
|
+
ok('AWK uninstalled. Custom files preserved.');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function cmdUpdate() {
|
|
416
|
+
info(`Updating to AWK v${AWK_VERSION}...`);
|
|
417
|
+
|
|
418
|
+
// Check current version
|
|
419
|
+
let currentVersion = '0.0.0';
|
|
420
|
+
if (fs.existsSync(TARGETS.versionFile)) {
|
|
421
|
+
currentVersion = fs.readFileSync(TARGETS.versionFile, 'utf8').trim();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (currentVersion === AWK_VERSION) {
|
|
425
|
+
ok(`Already on latest version (${AWK_VERSION})`);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
info(`Upgrading from ${currentVersion} → ${AWK_VERSION}`);
|
|
430
|
+
cmdInstall();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function cmdDoctor() {
|
|
434
|
+
log('');
|
|
435
|
+
log(`${C.cyan}${C.bold}🏥 AWK Health Check${C.reset}`);
|
|
436
|
+
log('');
|
|
437
|
+
|
|
438
|
+
let issues = 0;
|
|
439
|
+
|
|
440
|
+
// 1. Check GEMINI.md
|
|
441
|
+
if (fs.existsSync(TARGETS.geminiMd)) {
|
|
442
|
+
ok('GEMINI.md exists');
|
|
443
|
+
} else {
|
|
444
|
+
err('GEMINI.md missing'); issues++;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// 2. Check global_workflows
|
|
448
|
+
const wfDir = path.join(TARGETS.antigravity, 'global_workflows');
|
|
449
|
+
if (fs.existsSync(wfDir)) {
|
|
450
|
+
const wfFiles = fs.readdirSync(wfDir).filter(f => f.endsWith('.md'));
|
|
451
|
+
ok(`${wfFiles.length} workflows found`);
|
|
452
|
+
|
|
453
|
+
// Check essential workflows
|
|
454
|
+
const essential = ['code.md', 'plan.md', 'debug.md', 'save-brain.md', 'recap.md', 'init.md'];
|
|
455
|
+
for (const wf of essential) {
|
|
456
|
+
if (!wfFiles.includes(wf)) {
|
|
457
|
+
warn(`Essential workflow missing: ${wf}`); issues++;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
} else {
|
|
461
|
+
err('global_workflows/ directory missing'); issues++;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// 3. Check skills
|
|
465
|
+
const skillsDir = path.join(TARGETS.antigravity, 'skills');
|
|
466
|
+
if (fs.existsSync(skillsDir)) {
|
|
467
|
+
const skills = fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
468
|
+
.filter(d => d.isDirectory())
|
|
469
|
+
.map(d => d.name);
|
|
470
|
+
ok(`${skills.length} skills found`);
|
|
471
|
+
|
|
472
|
+
// Check essential skills
|
|
473
|
+
const essentialSkills = ['orchestrator', 'beads-manager', 'awf-session-restore'];
|
|
474
|
+
for (const s of essentialSkills) {
|
|
475
|
+
if (!skills.includes(s)) {
|
|
476
|
+
warn(`Essential skill missing: ${s}`); issues++;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
} else {
|
|
480
|
+
err('skills/ directory missing'); issues++;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// 4. Check schemas
|
|
484
|
+
const schemasDir = path.join(TARGETS.antigravity, 'schemas');
|
|
485
|
+
if (fs.existsSync(schemasDir)) {
|
|
486
|
+
const schemas = fs.readdirSync(schemasDir).filter(f => f.endsWith('.json'));
|
|
487
|
+
ok(`${schemas.length} schemas found`);
|
|
488
|
+
} else {
|
|
489
|
+
warn('schemas/ directory missing'); issues++;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// 5. Check version
|
|
493
|
+
if (fs.existsSync(TARGETS.versionFile)) {
|
|
494
|
+
const v = fs.readFileSync(TARGETS.versionFile, 'utf8').trim();
|
|
495
|
+
ok(`AWK version: ${v}`);
|
|
496
|
+
if (v !== AWK_VERSION) {
|
|
497
|
+
warn(`Package version (${AWK_VERSION}) differs from installed (${v}). Run 'awkit update'.`);
|
|
498
|
+
}
|
|
499
|
+
} else {
|
|
500
|
+
warn('Version file missing. Run "awkit install" first.'); issues++;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Summary
|
|
504
|
+
log('');
|
|
505
|
+
if (issues === 0) {
|
|
506
|
+
log(`${C.green}${C.bold}✅ All checks passed! AWK is healthy.${C.reset}`);
|
|
507
|
+
} else {
|
|
508
|
+
log(`${C.yellow}${C.bold}⚠️ ${issues} issue(s) found. Run 'awkit install' to fix.${C.reset}`);
|
|
509
|
+
}
|
|
510
|
+
log('');
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Find a compatible Python interpreter meeting the minimum version requirement.
|
|
515
|
+
* Tries python3.13, python3.12, python3.11, python3, python in order.
|
|
516
|
+
* Returns { cmd, version } or null if none found.
|
|
517
|
+
*/
|
|
518
|
+
function findCompatiblePython(minVersion) {
|
|
519
|
+
if (!minVersion) {
|
|
520
|
+
// No version constraint — just return first python found
|
|
521
|
+
for (const cmd of ['python3', 'python']) {
|
|
522
|
+
try {
|
|
523
|
+
execSync(`${cmd} --version`, { stdio: 'ignore' });
|
|
524
|
+
return { cmd, version: 'any' };
|
|
525
|
+
} catch (_) { }
|
|
526
|
+
}
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const [minMajor, minMinor] = minVersion.split('.').map(Number);
|
|
531
|
+
// Prefer newest first
|
|
532
|
+
const candidates = ['python3.13', 'python3.12', 'python3.11', 'python3', 'python'];
|
|
533
|
+
|
|
534
|
+
for (const cmd of candidates) {
|
|
535
|
+
try {
|
|
536
|
+
const out = execSync(`${cmd} --version 2>&1`, { stdio: ['ignore', 'pipe', 'pipe'] })
|
|
537
|
+
.toString().trim();
|
|
538
|
+
const match = out.match(/(\d+)\.(\d+)/);
|
|
539
|
+
if (!match) continue;
|
|
540
|
+
const major = parseInt(match[1]);
|
|
541
|
+
const minor = parseInt(match[2]);
|
|
542
|
+
if (major > minMajor || (major === minMajor && minor >= minMinor)) {
|
|
543
|
+
return { cmd, version: `${major}.${minor}` };
|
|
544
|
+
}
|
|
545
|
+
} catch (_) { /* not installed */ }
|
|
546
|
+
}
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Auto-update mcp_config.json with absolute path to the MCP server command.
|
|
552
|
+
* Resolves `<serverName>-mcp` via `which`, patches the matching mcpServer entry.
|
|
553
|
+
*/
|
|
554
|
+
function autoUpdateMcpConfig(serverName) {
|
|
555
|
+
const mcpConfigPath = path.join(TARGETS.antigravity, 'mcp_config.json');
|
|
556
|
+
if (!fs.existsSync(mcpConfigPath)) return;
|
|
557
|
+
|
|
558
|
+
// Resolve command path (e.g. neural-memory-mcp → nmem-mcp)
|
|
559
|
+
const candidates = [`${serverName}-mcp`, serverName, 'nmem-mcp'];
|
|
560
|
+
let absPath = '';
|
|
561
|
+
for (const c of candidates) {
|
|
562
|
+
try { absPath = execSync(`which ${c}`, { encoding: 'utf8' }).trim(); break; } catch (_) { }
|
|
563
|
+
}
|
|
564
|
+
if (!absPath) {
|
|
565
|
+
dim(`Could not resolve MCP command for '${serverName}' — skipping config update.`);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
try {
|
|
570
|
+
const cfg = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf8'));
|
|
571
|
+
if (!cfg.mcpServers) cfg.mcpServers = {};
|
|
572
|
+
if (!cfg.mcpServers[serverName]) cfg.mcpServers[serverName] = {};
|
|
573
|
+
cfg.mcpServers[serverName].command = absPath;
|
|
574
|
+
delete cfg.mcpServers[serverName].disabled; // enable it
|
|
575
|
+
fs.writeFileSync(mcpConfigPath, JSON.stringify(cfg, null, 2) + '\n');
|
|
576
|
+
ok(`MCP config updated: "${serverName}" → ${absPath}`);
|
|
577
|
+
} catch (e) {
|
|
578
|
+
warn(`Could not update mcp_config.json: ${e.message}`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Handle pack requirements defined in pack.json:
|
|
584
|
+
* - Check pip packages (with Python version detection)
|
|
585
|
+
* - Prompt to install if missing (or auto-install in autoMode)
|
|
586
|
+
* - Run post_install steps
|
|
587
|
+
* - Show MCP setup instructions
|
|
588
|
+
*/
|
|
589
|
+
function handlePackRequirements(packSrc, packName, { autoMode = false } = {}) {
|
|
590
|
+
const packConfigPath = path.join(packSrc, 'pack.json');
|
|
591
|
+
if (!fs.existsSync(packConfigPath)) return; // No requirements defined
|
|
592
|
+
|
|
593
|
+
let config;
|
|
594
|
+
try {
|
|
595
|
+
config = JSON.parse(fs.readFileSync(packConfigPath, 'utf8'));
|
|
596
|
+
} catch (e) {
|
|
597
|
+
warn(`Could not parse pack.json: ${e.message}`);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// ── 1. Check pip/npm dependencies ─────────────────────────────────────
|
|
602
|
+
const requires = config.requires || [];
|
|
603
|
+
if (requires.length > 0) {
|
|
604
|
+
log('');
|
|
605
|
+
log(`${C.cyan}${C.bold}📦 Dependencies${C.reset}`);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
for (const req of requires) {
|
|
609
|
+
const label = `${req.type === 'pip' ? '🐍 pip' : '📦 npm'}: ${req.package}`;
|
|
610
|
+
|
|
611
|
+
// Check if already installed
|
|
612
|
+
let installed = false;
|
|
613
|
+
try {
|
|
614
|
+
execSync(req.check_cmd, { stdio: 'ignore' });
|
|
615
|
+
installed = true;
|
|
616
|
+
} catch (_) {
|
|
617
|
+
installed = false;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (installed) {
|
|
621
|
+
ok(`${req.package} already installed`);
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
warn(`${req.package} not found — required for this pack to work`);
|
|
626
|
+
if (req.description) dim(req.description);
|
|
627
|
+
log('');
|
|
628
|
+
|
|
629
|
+
// Build actual install command — detect correct Python if needed
|
|
630
|
+
let installCmd = req.install_cmd;
|
|
631
|
+
if (req.type === 'pip') {
|
|
632
|
+
const pyInfo = findCompatiblePython(req.python_min || null);
|
|
633
|
+
if (pyInfo) {
|
|
634
|
+
installCmd = `${pyInfo.cmd} -m pip install ${req.package}`;
|
|
635
|
+
dim(`Using: ${pyInfo.cmd} (Python ${pyInfo.version})`);
|
|
636
|
+
} else if (req.python_min) {
|
|
637
|
+
err(`Requires Python >= ${req.python_min}, but none found on this system.`);
|
|
638
|
+
log('');
|
|
639
|
+
log(`${C.yellow} Fix options:${C.reset}`);
|
|
640
|
+
log(`${C.gray} 1. brew install python@${req.python_min}${C.reset}`);
|
|
641
|
+
log(`${C.gray} 2. pyenv install ${req.python_min} && pyenv global ${req.python_min}${C.reset}`);
|
|
642
|
+
log(`${C.gray} 3. Download from https://www.python.org/downloads/${C.reset}`);
|
|
643
|
+
log('');
|
|
644
|
+
log(`${C.gray} Then re-run: awkit enable-pack ${packName}${C.reset}`);
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const doInstall = autoMode ? true : promptYN(` Install now? (${installCmd})`);
|
|
650
|
+
if (autoMode) info(`Auto-installing: ${installCmd}`);
|
|
651
|
+
if (doInstall) {
|
|
652
|
+
log('');
|
|
653
|
+
|
|
654
|
+
// runPipCmd: streams output to terminal + captures stderr for PEP 668 detection
|
|
655
|
+
const runPipCmd = (cmd) => {
|
|
656
|
+
const parts = cmd.split(' ');
|
|
657
|
+
const result = spawnSync(parts[0], parts.slice(1), {
|
|
658
|
+
encoding: 'utf8',
|
|
659
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
660
|
+
});
|
|
661
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
662
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
663
|
+
return { success: result.status === 0, stderr: result.stderr || '' };
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
info(`Running: ${installCmd}`);
|
|
667
|
+
const r1 = runPipCmd(installCmd);
|
|
668
|
+
|
|
669
|
+
if (r1.success) {
|
|
670
|
+
ok(`${req.package} installed successfully`);
|
|
671
|
+
} else {
|
|
672
|
+
const isPep668 = r1.stderr.includes('externally-managed-environment')
|
|
673
|
+
|| r1.stderr.includes('break-system-packages');
|
|
674
|
+
|
|
675
|
+
if (isPep668) {
|
|
676
|
+
// Step 2: retry with --user
|
|
677
|
+
warn('System Python is externally managed (PEP 668). Retrying with --user...');
|
|
678
|
+
log('');
|
|
679
|
+
const userCmd = `${installCmd} --user`;
|
|
680
|
+
info(`Running: ${userCmd}`);
|
|
681
|
+
const r2 = runPipCmd(userCmd);
|
|
682
|
+
|
|
683
|
+
if (r2.success) {
|
|
684
|
+
ok(`${req.package} installed to user directory`);
|
|
685
|
+
const pyVer = installCmd.match(/python(\d+\.\d+)/)?.[1] || '';
|
|
686
|
+
if (pyVer) {
|
|
687
|
+
log('');
|
|
688
|
+
log(`${C.yellow} ⚠️ PATH hint:${C.reset}`);
|
|
689
|
+
log(`${C.gray} The 'nmem' command lives in your user bin dir.${C.reset}`);
|
|
690
|
+
log(`${C.gray} Add to ~/.zshrc:${C.reset}`);
|
|
691
|
+
log(`${C.gray} export PATH="$PATH:$HOME/Library/Python/${pyVer}/bin"${C.reset}`);
|
|
692
|
+
log(`${C.gray} Then: source ~/.zshrc && nmem init${C.reset}`);
|
|
693
|
+
}
|
|
694
|
+
} else {
|
|
695
|
+
// Step 3: try pipx (best for Homebrew Python environments)
|
|
696
|
+
let pipxAvailable = false;
|
|
697
|
+
try { execSync('which pipx', { stdio: 'ignore' }); pipxAvailable = true; } catch (_) { }
|
|
698
|
+
|
|
699
|
+
// autoMode: if pipx not found, try to install it via brew
|
|
700
|
+
if (!pipxAvailable && autoMode) {
|
|
701
|
+
let brewAvailable = false;
|
|
702
|
+
try { execSync('which brew', { stdio: 'ignore' }); brewAvailable = true; } catch (_) { }
|
|
703
|
+
if (brewAvailable) {
|
|
704
|
+
info('pipx not found. Auto-installing via brew...');
|
|
705
|
+
const rb = runPipCmd('brew install pipx');
|
|
706
|
+
if (rb.success) {
|
|
707
|
+
try { execSync('which pipx', { stdio: 'ignore' }); pipxAvailable = true; } catch (_) { }
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (pipxAvailable) {
|
|
713
|
+
warn('Trying pipx (recommended for Homebrew Python)...');
|
|
714
|
+
const pipxCmd = `pipx install ${req.package}`;
|
|
715
|
+
log('');
|
|
716
|
+
info(`Running: ${pipxCmd}`);
|
|
717
|
+
const r3 = runPipCmd(pipxCmd);
|
|
718
|
+
if (r3.success) {
|
|
719
|
+
ok(`${req.package} installed via pipx ✨`);
|
|
720
|
+
dim(`Commands like 'nmem' are now globally available via pipx.`);
|
|
721
|
+
// Auto-update mcp_config.json with absolute path
|
|
722
|
+
if (autoMode && config.mcp_setup?.server_name) {
|
|
723
|
+
autoUpdateMcpConfig(config.mcp_setup.server_name);
|
|
724
|
+
}
|
|
725
|
+
} else {
|
|
726
|
+
err(`pipx install also failed.`);
|
|
727
|
+
log('');
|
|
728
|
+
log(`${C.yellow} Manual options:${C.reset}`);
|
|
729
|
+
log(`${C.gray} 1. ${installCmd} --break-system-packages${C.reset}`);
|
|
730
|
+
log(`${C.gray} 2. python3 -m venv ~/.venv && source ~/.venv/bin/activate${C.reset}`);
|
|
731
|
+
log(`${C.gray} pip install ${req.package}${C.reset}`);
|
|
732
|
+
}
|
|
733
|
+
} else {
|
|
734
|
+
// pipx not available — show all options
|
|
735
|
+
err(`Installation failed even with --user.`);
|
|
736
|
+
log('');
|
|
737
|
+
log(`${C.yellow} Options (pick one):${C.reset}`);
|
|
738
|
+
log(`${C.gray} 1. brew install pipx && pipx install ${req.package} ← recommended${C.reset}`);
|
|
739
|
+
log(`${C.gray} 2. ${installCmd} --break-system-packages${C.reset}`);
|
|
740
|
+
log(`${C.gray} 3. python3 -m venv ~/.venv && source ~/.venv/bin/activate${C.reset}`);
|
|
741
|
+
log(`${C.gray} pip install ${req.package}${C.reset}`);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
} else {
|
|
745
|
+
err(`Installation failed. Try manually: ${installCmd}`);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
} else {
|
|
749
|
+
warn(`Skipped. Run manually: ${installCmd}`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// ── 2. Post-install steps ──────────────────────────────────────────────
|
|
754
|
+
const postInstall = config.post_install || [];
|
|
755
|
+
for (const step of postInstall) {
|
|
756
|
+
// Skip if artifact already exists
|
|
757
|
+
if (step.skip_if_exists) {
|
|
758
|
+
const expandedPath = step.skip_if_exists.replace('~', process.env.HOME || '');
|
|
759
|
+
if (fs.existsSync(expandedPath)) {
|
|
760
|
+
dim(`Already exists, skipping: ${step.cmd}`);
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
log('');
|
|
766
|
+
info(`Post-install: ${step.description || step.cmd}`);
|
|
767
|
+
|
|
768
|
+
// Check if the command is actually available before running
|
|
769
|
+
const cmdName = step.cmd.split(' ')[0];
|
|
770
|
+
let cmdAvailable = false;
|
|
771
|
+
try {
|
|
772
|
+
execSync(`which ${cmdName}`, { stdio: 'ignore' });
|
|
773
|
+
cmdAvailable = true;
|
|
774
|
+
} catch (_) { cmdAvailable = false; }
|
|
775
|
+
|
|
776
|
+
if (!cmdAvailable) {
|
|
777
|
+
warn(`Skipped: '${cmdName}' not found. Complete the install above first.`);
|
|
778
|
+
dim(`Then run: ${step.cmd}`);
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
if (step.optional && !autoMode) {
|
|
783
|
+
const doRun = promptYN(` Run now? (${step.cmd})`);
|
|
784
|
+
if (!doRun) {
|
|
785
|
+
dim(`Skipped. Run manually: ${step.cmd}`);
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
} else if (step.optional && autoMode) {
|
|
789
|
+
info(`Auto-running post-install: ${step.cmd}`);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
try {
|
|
793
|
+
execSync(step.cmd, { stdio: 'inherit' });
|
|
794
|
+
ok(`Done: ${step.cmd}`);
|
|
795
|
+
} catch (e) {
|
|
796
|
+
warn(`Step failed: ${e.message}`);
|
|
797
|
+
dim(`Try manually: ${step.cmd}`);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// ── 3. MCP Setup Instructions ──────────────────────────────────────────
|
|
802
|
+
const mcp = config.mcp_setup;
|
|
803
|
+
if (mcp) {
|
|
804
|
+
log('');
|
|
805
|
+
log(`${C.cyan}${C.bold}🔌 MCP Server Setup${C.reset}`);
|
|
806
|
+
log(`${C.gray} ${mcp.description}${C.reset}`);
|
|
807
|
+
log('');
|
|
808
|
+
|
|
809
|
+
if (mcp.config_json) {
|
|
810
|
+
log(`${C.gray} Add to your editor's MCP config:${C.reset}`);
|
|
811
|
+
log(`${C.gray} {${C.reset}`);
|
|
812
|
+
log(`${C.gray} "mcpServers": {${C.reset}`);
|
|
813
|
+
log(`${C.gray} "${mcp.server_name}": ${JSON.stringify(mcp.config_json)}${C.reset}`);
|
|
814
|
+
log(`${C.gray} }${C.reset}`);
|
|
815
|
+
log(`${C.gray} }${C.reset}`);
|
|
816
|
+
log('');
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
if (mcp.editors) {
|
|
820
|
+
log(`${C.cyan} Editor-specific setup:${C.reset}`);
|
|
821
|
+
for (const [editor, instruction] of Object.entries(mcp.editors)) {
|
|
822
|
+
log(`${C.gray} • ${editor}:${C.reset}`);
|
|
823
|
+
log(`${C.gray} ${instruction}${C.reset}`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function cmdEnablePack(packName, { autoMode = false } = {}) {
|
|
830
|
+
if (!packName) {
|
|
831
|
+
err('Usage: awkit enable-pack <pack-name>');
|
|
832
|
+
log('');
|
|
833
|
+
cmdListPacks();
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const packSrc = path.join(AWK_ROOT, 'skill-packs', packName);
|
|
838
|
+
|
|
839
|
+
if (!fs.existsSync(packSrc)) {
|
|
840
|
+
err(`Skill pack "${packName}" not found.`);
|
|
841
|
+
cmdListPacks();
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
info(`Enabling skill pack: ${packName}`);
|
|
846
|
+
let totalCount = 0;
|
|
847
|
+
|
|
848
|
+
// 1. Copy skills/ subdirs → ~/.gemini/antigravity/skills/
|
|
849
|
+
const packSkillsDir = path.join(packSrc, 'skills');
|
|
850
|
+
if (fs.existsSync(packSkillsDir)) {
|
|
851
|
+
const skillDirs = fs.readdirSync(packSkillsDir, { withFileTypes: true }).filter(d => d.isDirectory());
|
|
852
|
+
for (const skillDir of skillDirs) {
|
|
853
|
+
const src = path.join(packSkillsDir, skillDir.name);
|
|
854
|
+
const dest = path.join(TARGETS.antigravity, 'skills', skillDir.name);
|
|
855
|
+
const n = copyDirRecursive(src, dest);
|
|
856
|
+
totalCount += n;
|
|
857
|
+
dim(`Skill: ${skillDir.name} (${n} files)`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// 2. Copy workflows/ → ~/.gemini/antigravity/global_workflows/
|
|
862
|
+
const packWfDir = path.join(packSrc, 'workflows');
|
|
863
|
+
if (fs.existsSync(packWfDir)) {
|
|
864
|
+
const wfDest = path.join(TARGETS.antigravity, 'global_workflows');
|
|
865
|
+
const n = copyDirRecursive(packWfDir, wfDest, { flatten: false });
|
|
866
|
+
totalCount += n;
|
|
867
|
+
dim(`Workflows: ${n} files`);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// 3. Copy schemas/ → ~/.gemini/antigravity/schemas/
|
|
871
|
+
const packSchemaDir = path.join(packSrc, 'schemas');
|
|
872
|
+
if (fs.existsSync(packSchemaDir)) {
|
|
873
|
+
const schemaDest = path.join(TARGETS.antigravity, 'schemas');
|
|
874
|
+
const n = copyDirRecursive(packSchemaDir, schemaDest);
|
|
875
|
+
totalCount += n;
|
|
876
|
+
dim(`Schemas: ${n} files`);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
ok(`${totalCount} files from "${packName}" pack installed`);
|
|
880
|
+
|
|
881
|
+
// Handle pack.json requirements (pip deps, post-install, MCP setup)
|
|
882
|
+
handlePackRequirements(packSrc, packName, { autoMode });
|
|
883
|
+
|
|
884
|
+
log('');
|
|
885
|
+
log(`${C.cyan}👉 Skills available: type skill name in your AI chat.${C.reset}`);
|
|
886
|
+
log(`${C.cyan}👉 Workflows available: use /nm-recall, /memory-audit, etc.${C.reset}`);
|
|
887
|
+
log(`${C.cyan}👉 Run 'awkit doctor' to verify installation.${C.reset}`);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function cmdDisablePack(packName) {
|
|
891
|
+
if (!packName) {
|
|
892
|
+
err('Usage: awkit disable-pack <pack-name>');
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const packSrc = path.join(AWK_ROOT, 'skill-packs', packName);
|
|
897
|
+
|
|
898
|
+
if (!fs.existsSync(packSrc)) {
|
|
899
|
+
err(`Skill pack "${packName}" not found.`);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// Get list of skill dirs from pack/skills/
|
|
904
|
+
const packSkillsDir = path.join(packSrc, 'skills');
|
|
905
|
+
const skillDirs = fs.existsSync(packSkillsDir)
|
|
906
|
+
? fs.readdirSync(packSkillsDir, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => d.name)
|
|
907
|
+
: [];
|
|
908
|
+
|
|
909
|
+
const target = path.join(TARGETS.antigravity, 'skills');
|
|
910
|
+
const backupDir = path.join(TARGETS.antigravity, 'backup', 'skills');
|
|
911
|
+
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
|
|
912
|
+
|
|
913
|
+
for (const skillDir of skillDirs) {
|
|
914
|
+
const destPath = path.join(target, skillDir);
|
|
915
|
+
if (fs.existsSync(destPath)) {
|
|
916
|
+
fs.renameSync(destPath, path.join(backupDir, skillDir));
|
|
917
|
+
dim(`Moved to backup: ${skillDir}`);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
ok(`Skill pack "${packName}" disabled (skills backed up to ${backupDir})`);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function cmdListPacks() {
|
|
925
|
+
const packsDir = path.join(AWK_ROOT, 'skill-packs');
|
|
926
|
+
|
|
927
|
+
log('');
|
|
928
|
+
log(`${C.cyan}${C.bold}📦 Available Skill Packs${C.reset}`);
|
|
929
|
+
log('');
|
|
930
|
+
|
|
931
|
+
if (!fs.existsSync(packsDir)) {
|
|
932
|
+
warn('No skill packs directory found.');
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const packs = fs.readdirSync(packsDir, { withFileTypes: true })
|
|
937
|
+
.filter(d => d.isDirectory());
|
|
938
|
+
|
|
939
|
+
if (packs.length === 0) {
|
|
940
|
+
info('No skill packs available yet.');
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
for (const pack of packs) {
|
|
945
|
+
const readmePath = path.join(packsDir, pack.name, 'README.md');
|
|
946
|
+
let desc = '';
|
|
947
|
+
if (fs.existsSync(readmePath)) {
|
|
948
|
+
const content = fs.readFileSync(readmePath, 'utf8');
|
|
949
|
+
desc = content.split('\n').find(l => l.trim() && !l.startsWith('#')) || '';
|
|
950
|
+
}
|
|
951
|
+
log(` ${C.green}${pack.name}${C.reset} ${C.gray}${desc}${C.reset}`);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
log('');
|
|
955
|
+
log(`${C.cyan}Usage: awkit enable-pack <name>${C.reset}`);
|
|
956
|
+
log('');
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function cmdVersion() {
|
|
960
|
+
log(`AWK v${AWK_VERSION}`);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function cmdLint() {
|
|
964
|
+
log('');
|
|
965
|
+
log(`${C.cyan}${C.bold}🔍 AWK Lint — Skill & Workflow Guards${C.reset}`);
|
|
966
|
+
log('');
|
|
967
|
+
|
|
968
|
+
const targetDirs = [
|
|
969
|
+
path.join(TARGETS.antigravity, 'global_workflows'),
|
|
970
|
+
path.join(TARGETS.antigravity, 'skills')
|
|
971
|
+
];
|
|
972
|
+
|
|
973
|
+
let fileCount = 0;
|
|
974
|
+
let issues = 0;
|
|
975
|
+
const MAX_LINES = 500;
|
|
976
|
+
|
|
977
|
+
function checkFile(filePath) {
|
|
978
|
+
if (!filePath.endsWith('.md')) return;
|
|
979
|
+
fileCount++;
|
|
980
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
981
|
+
const lines = content.split('\n');
|
|
982
|
+
|
|
983
|
+
const localIssues = [];
|
|
984
|
+
|
|
985
|
+
// 1. Check max lines
|
|
986
|
+
if (lines.length > MAX_LINES) {
|
|
987
|
+
localIssues.push(`File too large: ${lines.length} lines (max ${MAX_LINES})`);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// 2. Check frontmatter / description length
|
|
991
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
992
|
+
if (frontmatterMatch) {
|
|
993
|
+
const frontmatter = frontmatterMatch[1];
|
|
994
|
+
const descMatch = frontmatter.match(/description:\s*(.*)/);
|
|
995
|
+
if (descMatch) {
|
|
996
|
+
const desc = descMatch[1].trim();
|
|
997
|
+
// We're lax on description length: warn only if overly verbose
|
|
998
|
+
if (desc.length > 200) {
|
|
999
|
+
localIssues.push(`Description too long: ${desc.length} chars (max 200)`);
|
|
1000
|
+
}
|
|
1001
|
+
} else {
|
|
1002
|
+
// If it has frontmatter but no description, warn them
|
|
1003
|
+
localIssues.push('Missing description field in frontmatter');
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// 3. Report if there are local issues
|
|
1008
|
+
if (localIssues.length > 0) {
|
|
1009
|
+
issues += localIssues.length;
|
|
1010
|
+
const relPath = filePath.replace(TARGETS.antigravity + '/', '');
|
|
1011
|
+
log(`${C.red}✖ ${relPath}${C.reset}`);
|
|
1012
|
+
for (const issue of localIssues) {
|
|
1013
|
+
log(` ${C.yellow}↳ ${issue}${C.reset}`);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function scanDir(dir) {
|
|
1019
|
+
if (!fs.existsSync(dir)) return;
|
|
1020
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1021
|
+
for (const entry of entries) {
|
|
1022
|
+
const fullPath = path.join(dir, entry.name);
|
|
1023
|
+
if (entry.isDirectory()) {
|
|
1024
|
+
scanDir(fullPath);
|
|
1025
|
+
} else {
|
|
1026
|
+
checkFile(fullPath);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
for (const dir of targetDirs) {
|
|
1032
|
+
scanDir(dir);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
log('');
|
|
1036
|
+
if (issues === 0) {
|
|
1037
|
+
log(`${C.green}✅ All ${fileCount} files passed linting.${C.reset}`);
|
|
1038
|
+
} else {
|
|
1039
|
+
log(`${C.red}✖ Found ${issues} issue(s) across ${fileCount} files.${C.reset}`);
|
|
1040
|
+
process.exitCode = 1;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// ─── Status: Diff repo vs installed ──────────────────────────────────────────
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Collect all .md files under a directory (recursively, flat list of basenames)
|
|
1048
|
+
*/
|
|
1049
|
+
function collectFiles(dir, ext = '.md') {
|
|
1050
|
+
const result = new Set();
|
|
1051
|
+
if (!fs.existsSync(dir)) return result;
|
|
1052
|
+
function walk(current) {
|
|
1053
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
1054
|
+
if (entry.name === '.DS_Store') continue;
|
|
1055
|
+
if (entry.isDirectory()) { walk(path.join(current, entry.name)); }
|
|
1056
|
+
else if (entry.name.endsWith(ext) || ext === '*') { result.add(entry.name); }
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
walk(dir);
|
|
1060
|
+
return result;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function cmdStatus() {
|
|
1064
|
+
log('');
|
|
1065
|
+
log(`${C.cyan}${C.bold}📊 AWK Status — Repo vs Installed${C.reset}`);
|
|
1066
|
+
log('');
|
|
1067
|
+
|
|
1068
|
+
const repoWfDir = path.join(AWK_ROOT, 'workflows');
|
|
1069
|
+
const liveWfDir = path.join(TARGETS.antigravity, 'global_workflows');
|
|
1070
|
+
const repoSkillDir = path.join(AWK_ROOT, 'skills');
|
|
1071
|
+
const liveSkillDir = path.join(TARGETS.antigravity, 'skills');
|
|
1072
|
+
|
|
1073
|
+
// ── Workflows ──────────────────────────────────────────────────────────
|
|
1074
|
+
log(`${C.bold}Workflows:${C.reset}`);
|
|
1075
|
+
const repoWf = collectFiles(repoWfDir);
|
|
1076
|
+
const liveWf = collectFiles(liveWfDir);
|
|
1077
|
+
|
|
1078
|
+
const onlyInRepo = [...repoWf].filter(f => !liveWf.has(f));
|
|
1079
|
+
const onlyInLive = [...liveWf].filter(f => !repoWf.has(f));
|
|
1080
|
+
const inBoth = [...repoWf].filter(f => liveWf.has(f));
|
|
1081
|
+
|
|
1082
|
+
log(` ${C.green}✅ In sync:${C.reset} ${inBoth.length} workflows`);
|
|
1083
|
+
if (onlyInRepo.length > 0) {
|
|
1084
|
+
log(` ${C.yellow}⬆ Repo only:${C.reset} ${onlyInRepo.length} → run 'awkit install' to deploy`);
|
|
1085
|
+
onlyInRepo.forEach(f => log(`${C.gray} + ${f}${C.reset}`));
|
|
1086
|
+
}
|
|
1087
|
+
if (onlyInLive.length > 0) {
|
|
1088
|
+
log(` ${C.cyan}⬇ Live only:${C.reset} ${onlyInLive.length} → run 'awkit harvest' to pull`);
|
|
1089
|
+
onlyInLive.forEach(f => log(`${C.gray} - ${f}${C.reset}`));
|
|
1090
|
+
}
|
|
1091
|
+
if (onlyInRepo.length === 0 && onlyInLive.length === 0) {
|
|
1092
|
+
log(` ${C.green}Perfect sync! ✨${C.reset}`);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
log('');
|
|
1096
|
+
|
|
1097
|
+
// ── Skills ─────────────────────────────────────────────────────────────
|
|
1098
|
+
log(`${C.bold}Skills:${C.reset}`);
|
|
1099
|
+
const repoSkills = fs.existsSync(repoSkillDir)
|
|
1100
|
+
? new Set(fs.readdirSync(repoSkillDir, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => d.name))
|
|
1101
|
+
: new Set();
|
|
1102
|
+
const liveSkills = fs.existsSync(liveSkillDir)
|
|
1103
|
+
? new Set(fs.readdirSync(liveSkillDir, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => d.name))
|
|
1104
|
+
: new Set();
|
|
1105
|
+
|
|
1106
|
+
const skillsOnlyRepo = [...repoSkills].filter(s => !liveSkills.has(s));
|
|
1107
|
+
const skillsOnlyLive = [...liveSkills].filter(s => !repoSkills.has(s));
|
|
1108
|
+
const skillsInBoth = [...repoSkills].filter(s => liveSkills.has(s));
|
|
1109
|
+
|
|
1110
|
+
log(` ${C.green}✅ In sync:${C.reset} ${skillsInBoth.length} skills`);
|
|
1111
|
+
if (skillsOnlyRepo.length > 0) {
|
|
1112
|
+
log(` ${C.yellow}⬆ Repo only:${C.reset} ${skillsOnlyRepo.length} → run 'awkit install'`);
|
|
1113
|
+
skillsOnlyRepo.forEach(s => log(`${C.gray} + ${s}${C.reset}`));
|
|
1114
|
+
}
|
|
1115
|
+
if (skillsOnlyLive.length > 0) {
|
|
1116
|
+
log(` ${C.cyan}⬇ Live only:${C.reset} ${skillsOnlyLive.length} → run 'awkit harvest'`);
|
|
1117
|
+
skillsOnlyLive.forEach(s => log(`${C.gray} - ${s}${C.reset}`));
|
|
1118
|
+
}
|
|
1119
|
+
if (skillsOnlyRepo.length === 0 && skillsOnlyLive.length === 0) {
|
|
1120
|
+
log(` ${C.green}Perfect sync! ✨${C.reset}`);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
log('');
|
|
1124
|
+
|
|
1125
|
+
// ── Versions ───────────────────────────────────────────────────────────
|
|
1126
|
+
log(`${C.bold}Versions:${C.reset}`);
|
|
1127
|
+
const installedVer = fs.existsSync(TARGETS.versionFile)
|
|
1128
|
+
? fs.readFileSync(TARGETS.versionFile, 'utf8').trim()
|
|
1129
|
+
: '(not installed)';
|
|
1130
|
+
log(` Repo: ${C.cyan}${AWK_VERSION}${C.reset}`);
|
|
1131
|
+
log(` Installed: ${installedVer === AWK_VERSION ? C.green : C.yellow}${installedVer}${C.reset}`);
|
|
1132
|
+
if (installedVer !== AWK_VERSION) {
|
|
1133
|
+
log(` ${C.yellow}⚠️ Run 'awkit install' to sync versions.${C.reset}`);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
log('');
|
|
1137
|
+
log(`${C.gray}Tip: 'awkit sync' = harvest (pull live→repo) + install (push repo→live)${C.reset}`);
|
|
1138
|
+
log('');
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// ─── Harvest: Pull from live ~/.gemini/ into repo ─────────────────────────────
|
|
1142
|
+
|
|
1143
|
+
function cmdHarvest(dryRun = false) {
|
|
1144
|
+
const { execSync: exec } = require('child_process');
|
|
1145
|
+
const harvestScript = path.join(AWK_ROOT, 'scripts', 'harvest.js');
|
|
1146
|
+
if (!fs.existsSync(harvestScript)) {
|
|
1147
|
+
err(`harvest.js not found at: ${harvestScript}`);
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
const args = dryRun ? '--dry-run' : '';
|
|
1151
|
+
try {
|
|
1152
|
+
exec(`node "${harvestScript}" ${args}`, { stdio: 'inherit' });
|
|
1153
|
+
} catch (e) {
|
|
1154
|
+
err(`Harvest failed: ${e.message}`);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// ─── Sync: Harvest + Install ──────────────────────────────────────────────────
|
|
1159
|
+
|
|
1160
|
+
function cmdSync() {
|
|
1161
|
+
log('');
|
|
1162
|
+
log(`${C.cyan}${C.bold}🔄 AWK Sync — Harvest → Install${C.reset}`);
|
|
1163
|
+
log('');
|
|
1164
|
+
log(`${C.gray}Step 1/2: Harvesting from ~/.gemini/antigravity/ → repo...${C.reset}`);
|
|
1165
|
+
log('');
|
|
1166
|
+
cmdHarvest(false);
|
|
1167
|
+
log('');
|
|
1168
|
+
log(`${C.gray}Step 2/2: Installing from repo → ~/.gemini/antigravity/...${C.reset}`);
|
|
1169
|
+
log('');
|
|
1170
|
+
cmdInstall();
|
|
1171
|
+
log('');
|
|
1172
|
+
log(`${C.yellow}${C.bold}🔄 Full sync complete!${C.reset}`);
|
|
1173
|
+
log(`${C.gray}Tip: Commit the repo changes to save this snapshot in git.${C.reset}`);
|
|
1174
|
+
log('');
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function cmdHelp() {
|
|
1178
|
+
const line = `${C.gray}${'─'.repeat(56)}${C.reset}`;
|
|
1179
|
+
log('');
|
|
1180
|
+
log(`${C.cyan}${C.bold}╔═══════════════════════════════════════════════════════╗${C.reset}`);
|
|
1181
|
+
log(`${C.cyan}${C.bold}║ 🚀 AWK v${AWK_VERSION} — Antigravity Workflow Kit ║${C.reset}`);
|
|
1182
|
+
log(`${C.cyan}${C.bold}╚═══════════════════════════════════════════════════════╝${C.reset}`);
|
|
1183
|
+
log('');
|
|
1184
|
+
|
|
1185
|
+
// Install
|
|
1186
|
+
log(`${C.bold}⚙️ Setup${C.reset}`);
|
|
1187
|
+
log(line);
|
|
1188
|
+
log(` ${C.green}install${C.reset} Deploy AWK into ~/.gemini/antigravity/`);
|
|
1189
|
+
log(` ${C.green}uninstall${C.reset} Remove AWK (preserves custom files)`);
|
|
1190
|
+
log(` ${C.green}update${C.reset} Pull latest + reinstall`);
|
|
1191
|
+
log(` ${C.green}lint${C.reset} Run skill & workflow guards (check length, frontmatter)`);
|
|
1192
|
+
log(` ${C.green}doctor${C.reset} Check installation health`);
|
|
1193
|
+
log('');
|
|
1194
|
+
|
|
1195
|
+
// Project Init
|
|
1196
|
+
log(`${C.bold}✨ Project${C.reset}`);
|
|
1197
|
+
log(line);
|
|
1198
|
+
log(` ${C.green}init${C.reset} Init mobile project (Firebase) in CWD`);
|
|
1199
|
+
log(` ${C.gray} --force${C.reset} Overwrite existing files`);
|
|
1200
|
+
log(` ${C.gray} Generates: .project-identity, <Name>.code-workspace,${C.reset}`);
|
|
1201
|
+
log(` ${C.gray} CODEBASE.md, .beads/ (Beads task DB)${C.reset}`);
|
|
1202
|
+
log('');
|
|
1203
|
+
|
|
1204
|
+
// Sync
|
|
1205
|
+
log(`${C.bold}🔄 Sync${C.reset}`);
|
|
1206
|
+
log(line);
|
|
1207
|
+
log(` ${C.green}status${C.reset} Compare repo vs installed (diff view)`);
|
|
1208
|
+
log(` ${C.green}harvest${C.reset} Pull live edits from ~/.gemini/ → repo`);
|
|
1209
|
+
log(` ${C.green}sync${C.reset} Full sync: harvest + install (one shot)`);
|
|
1210
|
+
log('');
|
|
1211
|
+
|
|
1212
|
+
// Packs
|
|
1213
|
+
log(`${C.bold}📦 Skill Packs${C.reset}`);
|
|
1214
|
+
log(line);
|
|
1215
|
+
log(` ${C.green}list-packs${C.reset} List available skill packs`);
|
|
1216
|
+
log(` ${C.green}enable-pack${C.reset} <name> Install a skill pack`);
|
|
1217
|
+
log(` ${C.green}disable-pack${C.reset} <name> Uninstall a skill pack (backed up)`);
|
|
1218
|
+
log('');
|
|
1219
|
+
|
|
1220
|
+
// Available packs
|
|
1221
|
+
const packsDir = path.join(AWK_ROOT, 'skill-packs');
|
|
1222
|
+
if (fs.existsSync(packsDir)) {
|
|
1223
|
+
const packs = fs.readdirSync(packsDir, { withFileTypes: true }).filter(d => d.isDirectory());
|
|
1224
|
+
if (packs.length) {
|
|
1225
|
+
log(` Available packs:`);
|
|
1226
|
+
for (const p of packs) {
|
|
1227
|
+
const readmePath = path.join(packsDir, p.name, 'README.md');
|
|
1228
|
+
let tagline = '';
|
|
1229
|
+
if (fs.existsSync(readmePath)) {
|
|
1230
|
+
const content = fs.readFileSync(readmePath, 'utf8');
|
|
1231
|
+
const match = content.match(/^>\s*(.+)/m);
|
|
1232
|
+
if (match) tagline = `— ${match[1].trim().substring(0, 42)}`;
|
|
1233
|
+
}
|
|
1234
|
+
log(` ${C.gray} • ${p.name} ${tagline}${C.reset}`);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
log('');
|
|
1239
|
+
|
|
1240
|
+
// Info
|
|
1241
|
+
log(`${C.bold}ℹ️ Info${C.reset}`);
|
|
1242
|
+
log(line);
|
|
1243
|
+
log(` ${C.green}version${C.reset} Show current version`);
|
|
1244
|
+
log(` ${C.green}help${C.reset} Show this help`);
|
|
1245
|
+
log('');
|
|
1246
|
+
|
|
1247
|
+
// Typical workflow
|
|
1248
|
+
log(`${C.bold}💡 Typical Workflow${C.reset}`);
|
|
1249
|
+
log(line);
|
|
1250
|
+
log(` ${C.cyan}# First time setup${C.reset}`);
|
|
1251
|
+
log(` ${C.gray}npm install -g github:babyskill/awk${C.reset}`);
|
|
1252
|
+
log(` ${C.gray}awkit install${C.reset}`);
|
|
1253
|
+
log(` ${C.gray}awkit doctor${C.reset}`);
|
|
1254
|
+
log('');
|
|
1255
|
+
log(` ${C.cyan}# Daily usage${C.reset}`);
|
|
1256
|
+
log(` ${C.gray}awkit status # What's out of sync?${C.reset}`);
|
|
1257
|
+
log(` ${C.gray}awkit harvest # Pull live edits → repo${C.reset}`);
|
|
1258
|
+
log(` ${C.gray}awkit sync # harvest + install in one shot${C.reset}`);
|
|
1259
|
+
log('');
|
|
1260
|
+
log(` ${C.cyan}# Enable NeuralMemory${C.reset}`);
|
|
1261
|
+
log(` ${C.gray}awkit enable-pack neural-memory${C.reset}`);
|
|
1262
|
+
log('');
|
|
1263
|
+
log(` ${C.cyan}# Repo${C.reset}`);
|
|
1264
|
+
log(` ${C.gray}https://github.com/babyskill/awk${C.reset}`);
|
|
1265
|
+
log('');
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// ─── Init: Mobile Project Initializer ───────────────────────────────────────
|
|
1269
|
+
|
|
1270
|
+
/**
|
|
1271
|
+
* Detect project type from CWD by inspecting known file signatures.
|
|
1272
|
+
* Returns: 'ios' | 'android' | 'expo' | 'flutter' | 'mobile-firebase'
|
|
1273
|
+
*/
|
|
1274
|
+
function detectProjectType(cwd) {
|
|
1275
|
+
const entries = fs.readdirSync(cwd);
|
|
1276
|
+
|
|
1277
|
+
// iOS: .xcworkspace or .xcodeproj folder
|
|
1278
|
+
if (entries.some(e => e.endsWith('.xcworkspace') || e.endsWith('.xcodeproj'))) {
|
|
1279
|
+
return 'ios';
|
|
1280
|
+
}
|
|
1281
|
+
// Android: build.gradle or settings.gradle
|
|
1282
|
+
if (entries.includes('build.gradle') || entries.includes('settings.gradle') || entries.includes('build.gradle.kts')) {
|
|
1283
|
+
return 'android';
|
|
1284
|
+
}
|
|
1285
|
+
// Expo: app.json with expo key, or expo.json, or app.config.ts
|
|
1286
|
+
if (entries.includes('app.json') || entries.includes('expo.json') || entries.includes('app.config.ts') || entries.includes('app.config.js')) {
|
|
1287
|
+
try {
|
|
1288
|
+
if (entries.includes('app.json')) {
|
|
1289
|
+
const appJson = JSON.parse(fs.readFileSync(path.join(cwd, 'app.json'), 'utf8'));
|
|
1290
|
+
if (appJson.expo) return 'expo';
|
|
1291
|
+
}
|
|
1292
|
+
} catch (_) { /* continue */ }
|
|
1293
|
+
return 'expo';
|
|
1294
|
+
}
|
|
1295
|
+
// Flutter: pubspec.yaml
|
|
1296
|
+
if (entries.includes('pubspec.yaml')) {
|
|
1297
|
+
return 'flutter';
|
|
1298
|
+
}
|
|
1299
|
+
// Default: generic mobile-firebase
|
|
1300
|
+
return 'mobile-firebase';
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* Build .project-identity object from detected type + project name.
|
|
1305
|
+
*/
|
|
1306
|
+
function buildProjectIdentity(projectName, projectType, cwd, date) {
|
|
1307
|
+
const bundleBase = projectName.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
1308
|
+
|
|
1309
|
+
const identityMap = {
|
|
1310
|
+
ios: {
|
|
1311
|
+
projectType: 'ios',
|
|
1312
|
+
bundleIdentifier: `com.company.${bundleBase}`,
|
|
1313
|
+
techStack: {
|
|
1314
|
+
platform: 'iOS',
|
|
1315
|
+
language: 'Swift',
|
|
1316
|
+
minVersion: 'iOS 17.0',
|
|
1317
|
+
framework: 'SwiftUI',
|
|
1318
|
+
architecture: 'MVVM + Clean Architecture',
|
|
1319
|
+
dependencyInjection: 'Manual DI',
|
|
1320
|
+
networking: 'URLSession + async/await',
|
|
1321
|
+
storage: 'SwiftData',
|
|
1322
|
+
testing: 'XCTest',
|
|
1323
|
+
packageManager: 'SPM',
|
|
1324
|
+
backend: 'Firebase',
|
|
1325
|
+
},
|
|
1326
|
+
codingStandards: { language: 'en', namingConvention: 'camelCase', indentation: 'spaces-4', lineLength: 120 },
|
|
1327
|
+
architecture: 'MVVM + Clean Architecture',
|
|
1328
|
+
stateManagement: 'ObservableObject / @State',
|
|
1329
|
+
networking: 'URLSession + async/await',
|
|
1330
|
+
storage: 'SwiftData',
|
|
1331
|
+
featuresDir: 'Sources/Features',
|
|
1332
|
+
sharedUIDir: 'Sources/Shared/UI',
|
|
1333
|
+
servicesDir: 'Sources/Shared/Services',
|
|
1334
|
+
modelsDir: 'Sources/Shared/Models',
|
|
1335
|
+
},
|
|
1336
|
+
android: {
|
|
1337
|
+
projectType: 'android',
|
|
1338
|
+
packageName: `com.company.${bundleBase}`,
|
|
1339
|
+
techStack: {
|
|
1340
|
+
platform: 'Android',
|
|
1341
|
+
language: 'Kotlin',
|
|
1342
|
+
minSdk: '24',
|
|
1343
|
+
targetSdk: '35',
|
|
1344
|
+
framework: 'Jetpack Compose',
|
|
1345
|
+
architecture: 'MVVM + Clean Architecture',
|
|
1346
|
+
dependencyInjection: 'Hilt',
|
|
1347
|
+
networking: 'Retrofit + Coroutines',
|
|
1348
|
+
storage: 'Room',
|
|
1349
|
+
testing: 'JUnit + Espresso',
|
|
1350
|
+
buildSystem: 'Gradle (KTS)',
|
|
1351
|
+
backend: 'Firebase',
|
|
1352
|
+
},
|
|
1353
|
+
codingStandards: { language: 'en', namingConvention: 'camelCase', indentation: 'spaces-4', lineLength: 120 },
|
|
1354
|
+
architecture: 'MVVM + Clean Architecture',
|
|
1355
|
+
stateManagement: 'ViewModel + StateFlow',
|
|
1356
|
+
networking: 'Retrofit + Coroutines',
|
|
1357
|
+
storage: 'Room',
|
|
1358
|
+
featuresDir: 'app/src/main/java/.../features',
|
|
1359
|
+
sharedUIDir: 'app/src/main/java/.../ui/components',
|
|
1360
|
+
servicesDir: 'app/src/main/java/.../data',
|
|
1361
|
+
modelsDir: 'app/src/main/java/.../model',
|
|
1362
|
+
},
|
|
1363
|
+
expo: {
|
|
1364
|
+
projectType: 'expo',
|
|
1365
|
+
bundleIdentifier: `com.company.${bundleBase}`,
|
|
1366
|
+
techStack: {
|
|
1367
|
+
platform: 'Expo / React Native',
|
|
1368
|
+
language: 'TypeScript',
|
|
1369
|
+
framework: 'Expo SDK 52+',
|
|
1370
|
+
router: 'Expo Router',
|
|
1371
|
+
styling: 'NativeWind (Tailwind)',
|
|
1372
|
+
stateManagement: 'Zustand',
|
|
1373
|
+
networking: 'TanStack Query',
|
|
1374
|
+
storage: 'Expo SQLite',
|
|
1375
|
+
testing: 'Jest + Detox',
|
|
1376
|
+
build: 'EAS Build',
|
|
1377
|
+
backend: 'Firebase',
|
|
1378
|
+
},
|
|
1379
|
+
codingStandards: { language: 'en', namingConvention: 'camelCase', indentation: 'spaces-2', lineLength: 100 },
|
|
1380
|
+
architecture: 'Feature-based',
|
|
1381
|
+
stateManagement: 'Zustand',
|
|
1382
|
+
networking: 'TanStack Query + Axios',
|
|
1383
|
+
storage: 'Expo SQLite / AsyncStorage',
|
|
1384
|
+
featuresDir: 'src/features',
|
|
1385
|
+
sharedUIDir: 'src/components',
|
|
1386
|
+
servicesDir: 'src/services',
|
|
1387
|
+
modelsDir: 'src/models',
|
|
1388
|
+
},
|
|
1389
|
+
flutter: {
|
|
1390
|
+
projectType: 'flutter',
|
|
1391
|
+
bundleIdentifier: `com.company.${bundleBase}`,
|
|
1392
|
+
techStack: {
|
|
1393
|
+
platform: 'Flutter',
|
|
1394
|
+
language: 'Dart',
|
|
1395
|
+
framework: 'Flutter 3+',
|
|
1396
|
+
architecture: 'BLoC + Clean Architecture',
|
|
1397
|
+
stateManagement: 'BLoC / Riverpod',
|
|
1398
|
+
networking: 'Dio + FutureBuilder',
|
|
1399
|
+
storage: 'Hive / SQLite',
|
|
1400
|
+
testing: 'flutter_test',
|
|
1401
|
+
backend: 'Firebase',
|
|
1402
|
+
},
|
|
1403
|
+
codingStandards: { language: 'en', namingConvention: 'camelCase', indentation: 'spaces-2', lineLength: 100 },
|
|
1404
|
+
architecture: 'BLoC + Clean Architecture',
|
|
1405
|
+
stateManagement: 'BLoC / Riverpod',
|
|
1406
|
+
networking: 'Dio + FutureBuilder',
|
|
1407
|
+
storage: 'Hive / SQLite',
|
|
1408
|
+
featuresDir: 'lib/features',
|
|
1409
|
+
sharedUIDir: 'lib/shared/widgets',
|
|
1410
|
+
servicesDir: 'lib/shared/services',
|
|
1411
|
+
modelsDir: 'lib/shared/models',
|
|
1412
|
+
},
|
|
1413
|
+
};
|
|
1414
|
+
|
|
1415
|
+
// Fallback: generic mobile-firebase
|
|
1416
|
+
const cfg = identityMap[projectType] || {
|
|
1417
|
+
projectType: 'mobile-firebase',
|
|
1418
|
+
bundleIdentifier: `com.company.${bundleBase}`,
|
|
1419
|
+
techStack: { platform: 'Mobile', backend: 'Firebase' },
|
|
1420
|
+
codingStandards: { language: 'en', namingConvention: 'camelCase', indentation: 'spaces-4', lineLength: 120 },
|
|
1421
|
+
architecture: 'MVVM',
|
|
1422
|
+
stateManagement: 'Custom',
|
|
1423
|
+
networking: 'Custom',
|
|
1424
|
+
storage: 'Custom',
|
|
1425
|
+
featuresDir: 'src/features',
|
|
1426
|
+
sharedUIDir: 'src/components',
|
|
1427
|
+
servicesDir: 'src/services',
|
|
1428
|
+
modelsDir: 'src/models',
|
|
1429
|
+
};
|
|
1430
|
+
|
|
1431
|
+
return {
|
|
1432
|
+
projectName,
|
|
1433
|
+
projectType: cfg.projectType,
|
|
1434
|
+
...(cfg.bundleIdentifier && { bundleIdentifier: cfg.bundleIdentifier }),
|
|
1435
|
+
...(cfg.packageName && { packageName: cfg.packageName }),
|
|
1436
|
+
primaryLanguage: 'en',
|
|
1437
|
+
techStack: cfg.techStack,
|
|
1438
|
+
services: {
|
|
1439
|
+
firebase: {
|
|
1440
|
+
enabled: true,
|
|
1441
|
+
features: ['analytics', 'crashlytics', 'remote-config', 'auth'],
|
|
1442
|
+
},
|
|
1443
|
+
},
|
|
1444
|
+
projectStage: 'development',
|
|
1445
|
+
codingStandards: cfg.codingStandards,
|
|
1446
|
+
projectGoals: [],
|
|
1447
|
+
createdDate: date,
|
|
1448
|
+
lastUpdated: date,
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/**
|
|
1453
|
+
* Build VS Code .code-workspace JSON for the given project type.
|
|
1454
|
+
*/
|
|
1455
|
+
function buildWorkspace(projectName, projectType) {
|
|
1456
|
+
const extensionsByType = {
|
|
1457
|
+
ios: [
|
|
1458
|
+
'sweetpad.sweetpad',
|
|
1459
|
+
'sswg.swift-lang',
|
|
1460
|
+
'aaron-bond.better-comments',
|
|
1461
|
+
'github.copilot',
|
|
1462
|
+
],
|
|
1463
|
+
android: [
|
|
1464
|
+
'fwcd.kotlin',
|
|
1465
|
+
'mathiasfrohlich.Kotlin',
|
|
1466
|
+
'redhat.java',
|
|
1467
|
+
'github.copilot',
|
|
1468
|
+
],
|
|
1469
|
+
expo: [
|
|
1470
|
+
'expo.vscode-expo-tools',
|
|
1471
|
+
'dsznajder.es7-react-js-snippets',
|
|
1472
|
+
'dbaeumer.vscode-eslint',
|
|
1473
|
+
'esbenp.prettier-vscode',
|
|
1474
|
+
'github.copilot',
|
|
1475
|
+
],
|
|
1476
|
+
flutter: [
|
|
1477
|
+
'dart-code.dart-code',
|
|
1478
|
+
'dart-code.flutter',
|
|
1479
|
+
'github.copilot',
|
|
1480
|
+
],
|
|
1481
|
+
};
|
|
1482
|
+
|
|
1483
|
+
return {
|
|
1484
|
+
folders: [{ path: '.' }],
|
|
1485
|
+
settings: {
|
|
1486
|
+
'editor.formatOnSave': true,
|
|
1487
|
+
'editor.tabSize': (projectType === 'expo' || projectType === 'flutter') ? 2 : 4,
|
|
1488
|
+
'files.exclude': {
|
|
1489
|
+
'**/.DS_Store': true,
|
|
1490
|
+
'**/node_modules': true,
|
|
1491
|
+
'**/.git': true,
|
|
1492
|
+
'**/build': projectType === 'android',
|
|
1493
|
+
'**/.gradle': projectType === 'android',
|
|
1494
|
+
'**/DerivedData': projectType === 'ios',
|
|
1495
|
+
},
|
|
1496
|
+
'files.watcherExclude': {
|
|
1497
|
+
'**/node_modules/**': true,
|
|
1498
|
+
'**/build/**': true,
|
|
1499
|
+
'**/DerivedData/**': true,
|
|
1500
|
+
},
|
|
1501
|
+
},
|
|
1502
|
+
extensions: {
|
|
1503
|
+
recommendations: extensionsByType[projectType] || ['github.copilot'],
|
|
1504
|
+
},
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
/**
|
|
1509
|
+
* Build CODEBASE.md content from template file.
|
|
1510
|
+
*/
|
|
1511
|
+
function buildCodebaseMd(projectName, projectType, identity) {
|
|
1512
|
+
const tmplPath = path.join(AWK_ROOT, 'templates', 'CODEBASE.md');
|
|
1513
|
+
let content = fs.existsSync(tmplPath)
|
|
1514
|
+
? fs.readFileSync(tmplPath, 'utf8')
|
|
1515
|
+
: '# {{PROJECT_NAME}}\n\n> Auto-generated by awkit init\n';
|
|
1516
|
+
|
|
1517
|
+
const techSummary = Object.entries(identity.techStack || {})
|
|
1518
|
+
.map(([k, v]) => v)
|
|
1519
|
+
.slice(0, 4)
|
|
1520
|
+
.join(' + ');
|
|
1521
|
+
|
|
1522
|
+
const dirStructure = {
|
|
1523
|
+
ios: `Sources/\n├── Features/ ← Feature modules (one dir per feature)\n├── Shared/\n│ ├── UI/ ← Reusable SwiftUI components\n│ ├── Services/ ← Firebase, API, business logic\n│ ├── Models/ ← Data models & DTOs\n│ └── Extensions/ ← Swift extensions\n├── Resources/ ← Assets, fonts, localization\n└── Tests/ ← XCTest unit & UI tests`,
|
|
1524
|
+
android: `app/src/main/java/…/\n├── features/ ← Feature modules (one dir per feature)\n├── ui/\n│ └── components/ ← Reusable Compose components\n├── data/ ← Repositories, data sources\n├── model/ ← Data classes & DTOs\n└── di/ ← Hilt modules`,
|
|
1525
|
+
expo: `app/ ← Expo Router screens\n├── (tabs)/ ← Tab navigation\n├── (auth)/ ← Auth screens\nsrc/\n├── features/ ← Feature modules\n├── components/ ← Shared UI components\n├── services/ ← Firebase, API clients\n├── hooks/ ← Custom React hooks\n├── models/ ← TypeScript interfaces\n└── constants/ ← App constants & theme`,
|
|
1526
|
+
flutter: `lib/\n├── features/ ← Feature modules (BLoC pattern)\n├── shared/\n│ ├── widgets/ ← Reusable Flutter widgets\n│ ├── services/ ← Firebase, API services\n│ └── models/ ← Dart model classes\n└── core/ ← App config, routing, DI`,
|
|
1527
|
+
};
|
|
1528
|
+
|
|
1529
|
+
content = content
|
|
1530
|
+
.replace(/{{PROJECT_NAME}}/g, projectName)
|
|
1531
|
+
.replace(/{{PROJECT_TYPE}}/g, identity.projectType)
|
|
1532
|
+
.replace(/{{PROJECT_STAGE}}/g, identity.projectStage || 'development')
|
|
1533
|
+
.replace(/{{TECH_STACK_SUMMARY}}/g, techSummary)
|
|
1534
|
+
.replace(/{{DATE}}/g, new Date().toISOString().split('T')[0])
|
|
1535
|
+
.replace(/{{DIR_STRUCTURE}}/g, dirStructure[projectType] || `src/\n├── features/\n├── services/\n└── models/`)
|
|
1536
|
+
.replace(/{{ARCHITECTURE}}/g, identity.techStack?.architecture || 'MVVM')
|
|
1537
|
+
.replace(/{{STATE_MANAGEMENT}}/g, identity.techStack?.stateManagement || identity.stateManagement || 'Custom')
|
|
1538
|
+
.replace(/{{NETWORKING}}/g, identity.techStack?.networking || 'Custom')
|
|
1539
|
+
.replace(/{{STORAGE}}/g, identity.techStack?.storage || 'Custom')
|
|
1540
|
+
.replace(/{{FEATURES_DIR}}/g, identity.featuresDir || 'src/features')
|
|
1541
|
+
.replace(/{{SHARED_UI_DIR}}/g, identity.sharedUIDir || 'src/components')
|
|
1542
|
+
.replace(/{{SERVICES_DIR}}/g, identity.servicesDir || 'src/services')
|
|
1543
|
+
.replace(/{{MODELS_DIR}}/g, identity.modelsDir || 'src/models');
|
|
1544
|
+
|
|
1545
|
+
return content;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
/**
|
|
1549
|
+
* awkit init — Initialize a new mobile project with Firebase.
|
|
1550
|
+
* Runs from CWD. Zero prompts. Auto-detects project type.
|
|
1551
|
+
*
|
|
1552
|
+
* Creates:
|
|
1553
|
+
* .project-identity
|
|
1554
|
+
* <ProjectName>.code-workspace
|
|
1555
|
+
* CODEBASE.md
|
|
1556
|
+
* .beads/ (via bd init)
|
|
1557
|
+
*/
|
|
1558
|
+
function cmdInit(forceFlag = false) {
|
|
1559
|
+
const cwd = process.cwd();
|
|
1560
|
+
const dirName = path.basename(cwd);
|
|
1561
|
+
// Convert dir name to PascalCase project name: my-app → MyApp, fitbite → Fitbite
|
|
1562
|
+
const projectName = dirName
|
|
1563
|
+
.split(/[-_\s]+/)
|
|
1564
|
+
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
|
1565
|
+
.join('');
|
|
1566
|
+
const date = new Date().toISOString().split('T')[0];
|
|
1567
|
+
|
|
1568
|
+
log('');
|
|
1569
|
+
log(`${C.cyan}${C.bold}╔══════════════════════════════════════════════════════════╗${C.reset}`);
|
|
1570
|
+
log(`${C.cyan}${C.bold}║ ✨ awkit init — Mobile Project Setup ║${C.reset}`);
|
|
1571
|
+
log(`${C.cyan}${C.bold}╚══════════════════════════════════════════════════════════╝${C.reset}`);
|
|
1572
|
+
log('');
|
|
1573
|
+
log(`${C.gray} Directory: ${cwd}${C.reset}`);
|
|
1574
|
+
log(`${C.gray} Project: ${projectName}${C.reset}`);
|
|
1575
|
+
|
|
1576
|
+
// ── 1. Detect project type ────────────────────────────────────────────────
|
|
1577
|
+
info('Detecting project type...');
|
|
1578
|
+
const projectType = detectProjectType(cwd);
|
|
1579
|
+
ok(`Detected: ${projectType}`);
|
|
1580
|
+
|
|
1581
|
+
// ── 2. .project-identity ──────────────────────────────────────────────────
|
|
1582
|
+
const identityPath = path.join(cwd, '.project-identity');
|
|
1583
|
+
if (fs.existsSync(identityPath) && !forceFlag) {
|
|
1584
|
+
warn('.project-identity already exists — skipping (use --force to overwrite)');
|
|
1585
|
+
} else {
|
|
1586
|
+
info('Creating .project-identity...');
|
|
1587
|
+
const identity = buildProjectIdentity(projectName, projectType, cwd, date);
|
|
1588
|
+
fs.writeFileSync(identityPath, JSON.stringify(identity, null, 2) + '\n');
|
|
1589
|
+
ok('.project-identity created');
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// Read identity back for use in other files
|
|
1593
|
+
let identity;
|
|
1594
|
+
try {
|
|
1595
|
+
identity = JSON.parse(fs.readFileSync(identityPath, 'utf8'));
|
|
1596
|
+
} catch (_) {
|
|
1597
|
+
identity = { projectName, projectType, techStack: {}, projectStage: 'development' };
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// ── 3. .code-workspace ───────────────────────────────────────────────────
|
|
1601
|
+
const workspaceName = `${projectName}.code-workspace`;
|
|
1602
|
+
const workspacePath = path.join(cwd, workspaceName);
|
|
1603
|
+
if (fs.existsSync(workspacePath) && !forceFlag) {
|
|
1604
|
+
warn(`${workspaceName} already exists — skipping (use --force to overwrite)`);
|
|
1605
|
+
} else {
|
|
1606
|
+
info(`Creating ${workspaceName}...`);
|
|
1607
|
+
const workspace = buildWorkspace(projectName, projectType);
|
|
1608
|
+
fs.writeFileSync(workspacePath, JSON.stringify(workspace, null, 2) + '\n');
|
|
1609
|
+
ok(`${workspaceName} created`);
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// ── 4. CODEBASE.md ────────────────────────────────────────────────────────
|
|
1613
|
+
const codebasePath = path.join(cwd, 'CODEBASE.md');
|
|
1614
|
+
if (fs.existsSync(codebasePath) && !forceFlag) {
|
|
1615
|
+
warn('CODEBASE.md already exists — skipping (use --force to overwrite)');
|
|
1616
|
+
} else {
|
|
1617
|
+
info('Creating CODEBASE.md...');
|
|
1618
|
+
const mdContent = buildCodebaseMd(projectName, projectType, identity);
|
|
1619
|
+
fs.writeFileSync(codebasePath, mdContent);
|
|
1620
|
+
ok('CODEBASE.md created');
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
// ── 5. Beads init ─────────────────────────────────────────────────────────
|
|
1624
|
+
const beadsDir = path.join(cwd, '.beads');
|
|
1625
|
+
if (fs.existsSync(beadsDir) && !forceFlag) {
|
|
1626
|
+
warn('.beads/ already exists — skipping bd init');
|
|
1627
|
+
} else {
|
|
1628
|
+
info('Initializing Beads task database...');
|
|
1629
|
+
// Ensure bd is installed (auto-install silently if missing)
|
|
1630
|
+
const bdReady = installBeads({ silent: true });
|
|
1631
|
+
if (!bdReady) {
|
|
1632
|
+
warn('bd not available — skipping bd init');
|
|
1633
|
+
dim('Install manually: brew install beads');
|
|
1634
|
+
} else {
|
|
1635
|
+
try {
|
|
1636
|
+
execSync('bd init', { cwd, stdio: 'pipe' });
|
|
1637
|
+
ok('Beads database initialized (.beads/)');
|
|
1638
|
+
} catch (e) {
|
|
1639
|
+
const msg = (e.stderr || e.stdout || e.message || '').toString().trim();
|
|
1640
|
+
if (msg.includes('already')) {
|
|
1641
|
+
warn('Beads already initialized — skipping');
|
|
1642
|
+
} else {
|
|
1643
|
+
warn(`bd init failed: ${msg || e.message}`);
|
|
1644
|
+
dim('Try manually: cd <project> && bd init');
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
// ── 6. Summary ─────────────────────────────────────────────────────────────
|
|
1651
|
+
log('');
|
|
1652
|
+
log(`${C.gray}${'─'.repeat(56)}${C.reset}`);
|
|
1653
|
+
log(`${C.yellow}${C.bold}🎉 ${projectName} initialized!${C.reset}`);
|
|
1654
|
+
log('');
|
|
1655
|
+
dim(`Type: ${projectType}`);
|
|
1656
|
+
dim(`Firebase: analytics, crashlytics, remote-config, auth`);
|
|
1657
|
+
dim(`Files: .project-identity, ${workspaceName}, CODEBASE.md`);
|
|
1658
|
+
dim(`Beads: .beads/ (task tracking ready)`);
|
|
1659
|
+
log('');
|
|
1660
|
+
log(`${C.cyan}👉 Open ${workspaceName} in VS Code to get started.${C.reset}`);
|
|
1661
|
+
log(`${C.cyan}👉 Run '/codebase-sync' in AI chat to keep CODEBASE.md updated.${C.reset}`);
|
|
1662
|
+
log(`${C.cyan}👉 Run 'bd list' to manage tasks.${C.reset}`);
|
|
1663
|
+
log('');
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
// ─── Auto-Update Checker ──────────────────────────────────────────────────────
|
|
1667
|
+
|
|
1668
|
+
function checkAutoUpdate() {
|
|
1669
|
+
const checkFile = path.join(TARGETS.antigravity, '.awk_update_check');
|
|
1670
|
+
const now = Date.now();
|
|
1671
|
+
const ONEDAY = 24 * 60 * 60 * 1000;
|
|
1672
|
+
|
|
1673
|
+
if (fs.existsSync(checkFile)) {
|
|
1674
|
+
try {
|
|
1675
|
+
const lastCheck = parseInt(fs.readFileSync(checkFile, 'utf8'), 10);
|
|
1676
|
+
if (!isNaN(lastCheck) && (now - lastCheck < ONEDAY)) {
|
|
1677
|
+
return; // already checked recently
|
|
1678
|
+
}
|
|
1679
|
+
} catch (_) { }
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// Touch the file so we don't retry immediately on failure
|
|
1683
|
+
try { fs.writeFileSync(checkFile, now.toString()); } catch (_) { }
|
|
1684
|
+
|
|
1685
|
+
// Check for update using npm registry
|
|
1686
|
+
try {
|
|
1687
|
+
const output = execSync('npm view @leejungkiin/awkit version', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000 });
|
|
1688
|
+
const npmVersion = output.trim();
|
|
1689
|
+
|
|
1690
|
+
// Simple string comparison for versions like "1.0.0" (Assumes SemVer)
|
|
1691
|
+
if (npmVersion && npmVersion !== AWK_VERSION) {
|
|
1692
|
+
log('');
|
|
1693
|
+
log(`${C.yellow}${C.bold}🌟 [Thông báo] Có phiên bản mới cho AWKit! (v${npmVersion})${C.reset}`);
|
|
1694
|
+
log(`${C.gray} Phiên bản hiện tại: v${AWK_VERSION}${C.reset}`);
|
|
1695
|
+
log(`${C.gray} Chạy lệnh sau để nâng cấp:${C.reset}`);
|
|
1696
|
+
log(`${C.cyan} npm i -g @leejungkiin/awkit && awkit install${C.reset}`);
|
|
1697
|
+
log('');
|
|
1698
|
+
}
|
|
1699
|
+
} catch (_) {
|
|
1700
|
+
// Fail silently (offline, npm not installed, package not published yet)
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
1705
|
+
|
|
1706
|
+
// Check for updates (max once per day) before continuing
|
|
1707
|
+
checkAutoUpdate();
|
|
1708
|
+
|
|
1709
|
+
const [, , command, ...args] = process.argv;
|
|
1710
|
+
|
|
1711
|
+
switch (command) {
|
|
1712
|
+
case 'init':
|
|
1713
|
+
cmdInit(args.includes('--force'));
|
|
1714
|
+
break;
|
|
1715
|
+
case 'install':
|
|
1716
|
+
cmdInstall();
|
|
1717
|
+
break;
|
|
1718
|
+
case 'uninstall':
|
|
1719
|
+
cmdUninstall();
|
|
1720
|
+
break;
|
|
1721
|
+
case 'update':
|
|
1722
|
+
cmdUpdate();
|
|
1723
|
+
break;
|
|
1724
|
+
case 'sync':
|
|
1725
|
+
cmdSync();
|
|
1726
|
+
break;
|
|
1727
|
+
case 'status':
|
|
1728
|
+
cmdStatus();
|
|
1729
|
+
break;
|
|
1730
|
+
case 'harvest':
|
|
1731
|
+
cmdHarvest(args.includes('--dry-run'));
|
|
1732
|
+
break;
|
|
1733
|
+
case 'doctor':
|
|
1734
|
+
cmdDoctor();
|
|
1735
|
+
break;
|
|
1736
|
+
case 'enable-pack':
|
|
1737
|
+
cmdEnablePack(args[0]);
|
|
1738
|
+
break;
|
|
1739
|
+
case 'disable-pack':
|
|
1740
|
+
cmdDisablePack(args[0]);
|
|
1741
|
+
break;
|
|
1742
|
+
case 'list-packs':
|
|
1743
|
+
cmdListPacks();
|
|
1744
|
+
break;
|
|
1745
|
+
case 'version':
|
|
1746
|
+
case '--version':
|
|
1747
|
+
case '-v':
|
|
1748
|
+
cmdVersion();
|
|
1749
|
+
break;
|
|
1750
|
+
case 'lint':
|
|
1751
|
+
cmdLint();
|
|
1752
|
+
break;
|
|
1753
|
+
case 'help':
|
|
1754
|
+
case '--help':
|
|
1755
|
+
case '-h':
|
|
1756
|
+
default:
|
|
1757
|
+
cmdHelp();
|
|
1758
|
+
break;
|
|
1759
|
+
}
|