@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.
Files changed (139) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +146 -0
  3. package/VERSION +1 -0
  4. package/bin/awf.js +549 -0
  5. package/bin/awk.js +1759 -0
  6. package/core/AGENTS.md +39 -0
  7. package/core/GEMINI.md +202 -0
  8. package/core/GEMINI.md.bak +244 -0
  9. package/core/orchestrator.md +58 -0
  10. package/package.json +46 -0
  11. package/schemas/brain.schema.json +342 -0
  12. package/schemas/preferences.schema.json +95 -0
  13. package/schemas/session.schema.json +112 -0
  14. package/skill-packs/neural-memory/README.md +111 -0
  15. package/skill-packs/neural-memory/pack.json +35 -0
  16. package/skill-packs/neural-memory/schemas/brain-snapshot.json +167 -0
  17. package/skill-packs/neural-memory/skills/nm-memory-audit/SKILL.md +157 -0
  18. package/skill-packs/neural-memory/skills/nm-memory-evolution/SKILL.md +202 -0
  19. package/skill-packs/neural-memory/skills/nm-memory-intake/SKILL.md +135 -0
  20. package/skill-packs/neural-memory/skills/nm-memory-sync/SKILL.md +184 -0
  21. package/skill-packs/neural-memory/workflows/nm-import.md +73 -0
  22. package/skill-packs/neural-memory/workflows/nm-recall.md +67 -0
  23. package/skill-packs/neural-memory/workflows/nm-snapshot.md +69 -0
  24. package/skills/adaptive-language/SKILL.md +189 -0
  25. package/skills/ambient-brain/SKILL.md +314 -0
  26. package/skills/ambient-brain/brain-router.md +185 -0
  27. package/skills/ambient-brain/brain-templates.md +201 -0
  28. package/skills/auto-save/SKILL.md +223 -0
  29. package/skills/awf-adaptive-language/SKILL.md +189 -0
  30. package/skills/awf-context-help/SKILL.md +180 -0
  31. package/skills/awf-error-translator/SKILL.md +153 -0
  32. package/skills/awf-session-restore/SKILL.md +270 -0
  33. package/skills/awf-version-tracker/SKILL.md +32 -0
  34. package/skills/awf-version-tracker/scripts/snapshot.sh +22 -0
  35. package/skills/beads-manager/SKILL.md +323 -0
  36. package/skills/brainstorm-agent/SKILL.md +295 -0
  37. package/skills/context-help/SKILL.md +180 -0
  38. package/skills/error-translator/SKILL.md +153 -0
  39. package/skills/ios-engineer/SKILL.md +101 -0
  40. package/skills/memory-sync/SKILL.md +378 -0
  41. package/skills/memory-sync/memory-router.md +185 -0
  42. package/skills/memory-sync/memory-templates.md +201 -0
  43. package/skills/orchestrator/SKILL.md +193 -0
  44. package/skills/session-restore/SKILL.md +240 -0
  45. package/templates/CODEBASE.md +80 -0
  46. package/templates/brain.example.json +321 -0
  47. package/templates/preferences.example.json +21 -0
  48. package/templates/project-identity/android.json +28 -0
  49. package/templates/project-identity/backend-nestjs.json +24 -0
  50. package/templates/project-identity/expo.json +27 -0
  51. package/templates/project-identity/ios.json +27 -0
  52. package/templates/project-identity/web-nextjs.json +24 -0
  53. package/templates/session.example.json +53 -0
  54. package/templates/specs/design-template.md +166 -0
  55. package/templates/specs/requirements-template.md +65 -0
  56. package/templates/specs/tasks-template.md +132 -0
  57. package/templates/structures/android.txt +10 -0
  58. package/templates/structures/backend-nestjs.txt +6 -0
  59. package/templates/structures/expo.txt +9 -0
  60. package/templates/structures/ios.txt +9 -0
  61. package/templates/structures/web-nextjs.txt +6 -0
  62. package/templates/workflow_dual_mode_template.md +87 -0
  63. package/workflows/_uncategorized/README.md +339 -0
  64. package/workflows/_uncategorized/ads-creative.md +357 -0
  65. package/workflows/_uncategorized/ads-full-optimization.md +308 -0
  66. package/workflows/_uncategorized/ads-plan.md +247 -0
  67. package/workflows/_uncategorized/ads-user-analysis.md +337 -0
  68. package/workflows/_uncategorized/skill-health.md +35 -0
  69. package/workflows/_uncategorized/skill-rollback.md +35 -0
  70. package/workflows/ads/admob.md +62 -0
  71. package/workflows/ads/ads-analyst.md +201 -0
  72. package/workflows/ads/ads-audit.md +106 -0
  73. package/workflows/ads/ads-optimize.md +97 -0
  74. package/workflows/ads/ads-targeting.md +241 -0
  75. package/workflows/ads/adsExpert.md +160 -0
  76. package/workflows/ads/smali-ads-config.md +400 -0
  77. package/workflows/ads/smali-ads-flow.md +331 -0
  78. package/workflows/ads/smali-ads-interstitial.md +377 -0
  79. package/workflows/ads/smali-ads-native.md +382 -0
  80. package/workflows/context/auto-execution-workflow.md +291 -0
  81. package/workflows/context/auto-implement.md +211 -0
  82. package/workflows/context/codebase-sync.md +163 -0
  83. package/workflows/context/logic-reasoning-workflow.md +260 -0
  84. package/workflows/context/next.md +195 -0
  85. package/workflows/context/recap.md +212 -0
  86. package/workflows/context/save-brain.md +285 -0
  87. package/workflows/context/user-intent-analysis-workflow.md +206 -0
  88. package/workflows/expert/codeExpert.md +126 -0
  89. package/workflows/expert/debugExpert.md +136 -0
  90. package/workflows/expert/planExpert.md +112 -0
  91. package/workflows/git/cloudflare-tunnel.md +135 -0
  92. package/workflows/git/git-commit-workflow.md +75 -0
  93. package/workflows/git/hotfix.md +357 -0
  94. package/workflows/git/release-notes.md +160 -0
  95. package/workflows/git/rollback.md +52 -0
  96. package/workflows/git/smart-git-ops.md +103 -0
  97. package/workflows/lifecycle/brainstorm.md +377 -0
  98. package/workflows/lifecycle/code.md +663 -0
  99. package/workflows/lifecycle/debug.md +116 -0
  100. package/workflows/lifecycle/deploy.md +95 -0
  101. package/workflows/lifecycle/init.md +152 -0
  102. package/workflows/lifecycle/master-code-workflow.md +300 -0
  103. package/workflows/lifecycle/migration.md +196 -0
  104. package/workflows/lifecycle/plan.md +91 -0
  105. package/workflows/lifecycle/refactor.md +165 -0
  106. package/workflows/lifecycle/run.md +52 -0
  107. package/workflows/lifecycle/test.md +91 -0
  108. package/workflows/meta/customize.md +346 -0
  109. package/workflows/meta/file-protection-rules.md +129 -0
  110. package/workflows/meta/help.html +350 -0
  111. package/workflows/meta/project-identity-enforcement.md +180 -0
  112. package/workflows/mobile/app-analysis.md +64 -0
  113. package/workflows/mobile/maestro-qa-workflow.md +470 -0
  114. package/workflows/mobile/maestro-test-workflow.md +84 -0
  115. package/workflows/mobile/structure-clean-architect.md +271 -0
  116. package/workflows/mobile/turbo-mobile-build.md +190 -0
  117. package/workflows/quality/accessibility-audit.md +311 -0
  118. package/workflows/quality/audit.md +217 -0
  119. package/workflows/quality/bug-hunter.md +243 -0
  120. package/workflows/quality/code-janitor.md +209 -0
  121. package/workflows/quality/code-quality-rules.md +132 -0
  122. package/workflows/quality/performance-audit.md +343 -0
  123. package/workflows/quality/project-audit.md +61 -0
  124. package/workflows/quality/self-healing-test.md +192 -0
  125. package/workflows/quality/ui-review.md +130 -0
  126. package/workflows/quality/ux-audit.md +213 -0
  127. package/workflows/quality/visual-debug.md +34 -0
  128. package/workflows/roles/oracle.md +267 -0
  129. package/workflows/roles/product-manager-workflow.md +52 -0
  130. package/workflows/roles/qa-engineer-workflow.md +41 -0
  131. package/workflows/roles/tech-lead-workflow.md +45 -0
  132. package/workflows/roles/ui-ux-designer-workflow.md +42 -0
  133. package/workflows/roles/vibe-coding-master-workflow.md +52 -0
  134. package/workflows/ui/app-screen-analyzer.md +152 -0
  135. package/workflows/ui/create-feature.md +332 -0
  136. package/workflows/ui/create-spec-architect.md +184 -0
  137. package/workflows/ui/design-to-ui.md +308 -0
  138. package/workflows/ui/ui-first-methodology.md +279 -0
  139. 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
+ }