@leejungkiin/awkit 1.6.3 → 1.6.5

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.
@@ -0,0 +1,402 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Automation Gate — Enforces .project-identity automation config.
5
+ *
6
+ * Reads automation.git / automation.trello / automation.telegram from
7
+ * .project-identity BEFORE executing any operation. Blocks if disabled.
8
+ *
9
+ * Usage (via awkit CLI):
10
+ * awkit gate git commit "feat: add feature"
11
+ * awkit gate git push
12
+ * awkit gate git auto "feat: add feature"
13
+ * awkit gate trello complete "Task name"
14
+ * awkit gate trello comment "Note"
15
+ * awkit gate telegram send "Message"
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const { execSync } = require('child_process');
21
+
22
+ // Lazy-load obsidian-sync to avoid circular dependencies
23
+ let _autoSyncObsidian = null;
24
+ function getAutoSyncObsidian() {
25
+ if (!_autoSyncObsidian) {
26
+ try {
27
+ _autoSyncObsidian = require('./obsidian-sync').autoSyncObsidian;
28
+ } catch (_) {
29
+ _autoSyncObsidian = () => {}; // Graceful fallback
30
+ }
31
+ }
32
+ return _autoSyncObsidian;
33
+ }
34
+
35
+ // ─── Color helpers (borrowed from main CLI) ──────────────────────────────────
36
+
37
+ const C = {
38
+ reset: '\x1b[0m',
39
+ red: '\x1b[31m',
40
+ green: '\x1b[32m',
41
+ yellow: '\x1b[33m',
42
+ cyan: '\x1b[36m',
43
+ gray: '\x1b[90m',
44
+ bold: '\x1b[1m',
45
+ };
46
+
47
+ const log = (msg) => console.log(msg);
48
+ const ok = (msg) => log(`${C.green}✔${C.reset} ${msg}`);
49
+ const warn = (msg) => log(`${C.yellow}⚠${C.reset} ${msg}`);
50
+ const err = (msg) => log(`${C.red}✖${C.reset} ${msg}`);
51
+ const info = (msg) => log(`${C.cyan}ℹ${C.reset} ${msg}`);
52
+ const dim = (msg) => log(`${C.gray}${msg}${C.reset}`);
53
+
54
+ // ─── .project-identity reader ─────────────────────────────────────────────────
55
+
56
+ /**
57
+ * Walk up directories from `startDir` to find .project-identity.
58
+ * Returns parsed JSON or null.
59
+ */
60
+ function readProjectIdentity(startDir = process.cwd()) {
61
+ let dir = path.resolve(startDir);
62
+ const root = path.parse(dir).root;
63
+
64
+ while (dir !== root) {
65
+ const candidate = path.join(dir, '.project-identity');
66
+ if (fs.existsSync(candidate)) {
67
+ try {
68
+ return JSON.parse(fs.readFileSync(candidate, 'utf8'));
69
+ } catch (e) {
70
+ warn(`Failed to parse ${candidate}: ${e.message}`);
71
+ return null;
72
+ }
73
+ }
74
+ dir = path.dirname(dir);
75
+ }
76
+ return null;
77
+ }
78
+
79
+ // ─── Gate checker ─────────────────────────────────────────────────────────────
80
+
81
+ /**
82
+ * Check if an automation action is allowed.
83
+ *
84
+ * @param {string} domain - 'git' | 'trello' | 'telegram'
85
+ * @param {string} action - Domain-specific action name
86
+ * @returns {{ allowed: boolean, config: object|null, reason: string }}
87
+ */
88
+ function checkGate(domain, action) {
89
+ const identity = readProjectIdentity();
90
+
91
+ if (!identity) {
92
+ return { allowed: true, config: null, reason: 'No .project-identity found — default allow' };
93
+ }
94
+
95
+ const automation = identity.automation;
96
+ if (!automation) {
97
+ return { allowed: true, config: null, reason: 'No automation config — default allow' };
98
+ }
99
+
100
+ switch (domain) {
101
+ case 'git': {
102
+ const gitConfig = automation.git;
103
+ if (!gitConfig) {
104
+ return { allowed: true, config: null, reason: 'No automation.git config — default allow' };
105
+ }
106
+ if (action === 'commit' && gitConfig.autoCommit === false) {
107
+ return { allowed: false, config: gitConfig, reason: 'automation.git.autoCommit is false' };
108
+ }
109
+ if (action === 'push' && gitConfig.autoPush === false) {
110
+ return { allowed: false, config: gitConfig, reason: 'automation.git.autoPush is false' };
111
+ }
112
+ return { allowed: true, config: gitConfig, reason: 'automation.git allows this action' };
113
+ }
114
+
115
+ case 'trello': {
116
+ const trelloConfig = automation.trello;
117
+ if (!trelloConfig) {
118
+ return { allowed: true, config: null, reason: 'No automation.trello config — default allow' };
119
+ }
120
+ if (trelloConfig.enabled === false || trelloConfig.autoSync === false) {
121
+ return { allowed: false, config: trelloConfig, reason: 'automation.trello is disabled (enabled=false or autoSync=false)' };
122
+ }
123
+ // Check trigger-level gates
124
+ if (trelloConfig.triggers) {
125
+ const triggerMap = {
126
+ 'complete': 'task_complete',
127
+ 'comment': 'milestone',
128
+ 'block': 'blocked',
129
+ };
130
+ const trigger = triggerMap[action];
131
+ if (trigger && trelloConfig.triggers[trigger] === false) {
132
+ return { allowed: false, config: trelloConfig, reason: `automation.trello.triggers.${trigger} is false` };
133
+ }
134
+ }
135
+ return { allowed: true, config: trelloConfig, reason: 'automation.trello allows this action' };
136
+ }
137
+
138
+ case 'telegram': {
139
+ const tgConfig = automation.telegram;
140
+ if (!tgConfig) {
141
+ return { allowed: true, config: null, reason: 'No automation.telegram config — default allow' };
142
+ }
143
+ if (tgConfig.enabled === false) {
144
+ return { allowed: false, config: tgConfig, reason: 'automation.telegram.enabled is false' };
145
+ }
146
+ // Check trigger-level gates
147
+ if (tgConfig.triggers && action !== 'send') {
148
+ const triggerMap = {
149
+ 'git_push': 'git_push',
150
+ 'task_complete': 'task_complete',
151
+ 'deploy': 'deploy',
152
+ };
153
+ const trigger = triggerMap[action];
154
+ if (trigger && tgConfig.triggers[trigger] === false) {
155
+ return { allowed: false, config: tgConfig, reason: `automation.telegram.triggers.${trigger} is false` };
156
+ }
157
+ }
158
+ return { allowed: true, config: tgConfig, reason: 'automation.telegram allows this action' };
159
+ }
160
+
161
+ default:
162
+ return { allowed: true, config: null, reason: `Unknown domain '${domain}' — default allow` };
163
+ }
164
+ }
165
+
166
+ // ─── Git executors ────────────────────────────────────────────────────────────
167
+
168
+ function execGitCommit(message) {
169
+ const gate = checkGate('git', 'commit');
170
+ if (!gate.allowed) {
171
+ err(`🚫 GIT COMMIT BLOCKED: ${gate.reason}`);
172
+ dim(' Modify .project-identity automation.git.autoCommit to change this.');
173
+ return false;
174
+ }
175
+
176
+ try {
177
+ execSync('git add -A', { stdio: 'inherit' });
178
+ execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, { stdio: 'inherit' });
179
+ ok(`Committed: ${message}`);
180
+ return true;
181
+ } catch (e) {
182
+ // git commit returns non-zero if nothing to commit
183
+ if (e.status === 1) {
184
+ warn('Nothing to commit (working tree clean).');
185
+ return true;
186
+ }
187
+ err(`Git commit failed: ${e.message}`);
188
+ return false;
189
+ }
190
+ }
191
+
192
+ function execGitPush() {
193
+ const gate = checkGate('git', 'push');
194
+ if (!gate.allowed) {
195
+ err(`🚫 GIT PUSH BLOCKED: ${gate.reason}`);
196
+ dim(' Modify .project-identity automation.git.autoPush to change this.');
197
+ return false;
198
+ }
199
+
200
+ try {
201
+ execSync('git push', { stdio: 'inherit' });
202
+ ok('Pushed successfully.');
203
+ return true;
204
+ } catch (e) {
205
+ warn('Push failed. Retrying with git pull --rebase...');
206
+ try {
207
+ execSync('git pull --rebase && git push', { stdio: 'inherit' });
208
+ ok('Pushed successfully after rebase.');
209
+ return true;
210
+ } catch (e2) {
211
+ err(`Push failed after retry: ${e2.message}`);
212
+ err('Please resolve conflicts manually. DO NOT force push.');
213
+ return false;
214
+ }
215
+ }
216
+ }
217
+
218
+ function execGitAuto(message) {
219
+ log('');
220
+ log(`${C.cyan}${C.bold}🔒 Automation Gate — Git Auto${C.reset}`);
221
+ dim(` Checking .project-identity...`);
222
+
223
+ const committed = execGitCommit(message);
224
+ if (!committed) return false;
225
+
226
+ const pushed = execGitPush();
227
+ if (!pushed) return false;
228
+
229
+ // Trigger Telegram notification if git_push trigger is enabled
230
+ const tgGate = checkGate('telegram', 'git_push');
231
+ if (tgGate.allowed) {
232
+ info('Triggering Telegram notification...');
233
+ try {
234
+ execSync(`awkit tg send "✅ Pushed: ${message.replace(/"/g, '\\"')}"`, { stdio: 'inherit' });
235
+ } catch (_) {
236
+ dim('Telegram notification skipped (not configured or failed).');
237
+ }
238
+ }
239
+
240
+ // Trigger Obsidian sync after successful git auto
241
+ getAutoSyncObsidian()();
242
+
243
+ return true;
244
+ }
245
+
246
+ // ─── Trello executors ─────────────────────────────────────────────────────────
247
+
248
+ function execTrelloAction(action, args) {
249
+ const gate = checkGate('trello', action);
250
+ if (!gate.allowed) {
251
+ err(`🚫 TRELLO ${action.toUpperCase()} BLOCKED: ${gate.reason}`);
252
+ dim(' Modify .project-identity automation.trello to change this.');
253
+ return false;
254
+ }
255
+
256
+ try {
257
+ const cmd = `awkit trello ${action} ${args.map(a => `"${a}"`).join(' ')}`;
258
+ execSync(cmd, { stdio: 'inherit' });
259
+ // Trigger Obsidian sync after successful trello action
260
+ getAutoSyncObsidian()();
261
+ return true;
262
+ } catch (e) {
263
+ err(`Trello ${action} failed: ${e.message}`);
264
+ return false;
265
+ }
266
+ }
267
+
268
+ // ─── Telegram executor ────────────────────────────────────────────────────────
269
+
270
+ function execTelegramSend(args) {
271
+ const gate = checkGate('telegram', 'send');
272
+ if (!gate.allowed) {
273
+ err(`🚫 TELEGRAM BLOCKED: ${gate.reason}`);
274
+ dim(' Modify .project-identity automation.telegram to change this.');
275
+ return false;
276
+ }
277
+
278
+ try {
279
+ const cmd = `awkit tg send ${args.map(a => `"${a}"`).join(' ')}`;
280
+ execSync(cmd, { stdio: 'inherit' });
281
+ return true;
282
+ } catch (e) {
283
+ err(`Telegram send failed: ${e.message}`);
284
+ return false;
285
+ }
286
+ }
287
+
288
+ // ─── CLI handler ──────────────────────────────────────────────────────────────
289
+
290
+ function gateHelp() {
291
+ log('');
292
+ log(`${C.cyan}${C.bold}🔒 Automation Gate${C.reset}`);
293
+ log(`${C.gray} Enforces .project-identity automation config before executing.${C.reset}`);
294
+ log('');
295
+ log(` ${C.green}awkit gate git commit${C.reset} <message> Gated git add + commit`);
296
+ log(` ${C.green}awkit gate git push${C.reset} Gated git push`);
297
+ log(` ${C.green}awkit gate git auto${C.reset} <message> Commit + push + telegram`);
298
+ log('');
299
+ log(` ${C.green}awkit gate trello complete${C.reset} <name> Gated trello complete`);
300
+ log(` ${C.green}awkit gate trello comment${C.reset} <text> Gated trello comment`);
301
+ log(` ${C.green}awkit gate trello block${C.reset} <reason> Gated trello block`);
302
+ log('');
303
+ log(` ${C.green}awkit gate telegram send${C.reset} <message> Gated telegram send`);
304
+ log('');
305
+ log(` ${C.green}awkit gate check${C.reset} <domain> Check gate status (dry-run)`);
306
+ log('');
307
+ }
308
+
309
+ function gateCheck(domain) {
310
+ log('');
311
+ log(`${C.cyan}${C.bold}🔒 Gate Status — ${domain || 'all'}${C.reset}`);
312
+ log('');
313
+
314
+ const domains = domain ? [domain] : ['git', 'trello', 'telegram'];
315
+ const actions = {
316
+ git: ['commit', 'push'],
317
+ trello: ['complete', 'comment', 'block'],
318
+ telegram: ['send', 'git_push', 'task_complete', 'deploy'],
319
+ };
320
+
321
+ for (const d of domains) {
322
+ log(` ${C.bold}${d}${C.reset}`);
323
+ for (const a of (actions[d] || [])) {
324
+ const gate = checkGate(d, a);
325
+ const icon = gate.allowed ? `${C.green}✔${C.reset}` : `${C.red}✖${C.reset}`;
326
+ log(` ${icon} ${a}: ${C.gray}${gate.reason}${C.reset}`);
327
+ }
328
+ log('');
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Main handler for `awkit gate` subcommand.
334
+ * @param {string[]} args - CLI args after 'gate'
335
+ */
336
+ function cmdGate(args) {
337
+ const domain = args[0];
338
+ const action = args[1];
339
+ const rest = args.slice(2);
340
+
341
+ if (!domain || domain === 'help' || domain === '--help' || domain === '-h') {
342
+ gateHelp();
343
+ return;
344
+ }
345
+
346
+ if (domain === 'check') {
347
+ gateCheck(action); // action is optional domain filter
348
+ return;
349
+ }
350
+
351
+ switch (domain) {
352
+ case 'git':
353
+ switch (action) {
354
+ case 'commit':
355
+ execGitCommit(rest.join(' ') || 'chore: update');
356
+ break;
357
+ case 'push':
358
+ execGitPush();
359
+ break;
360
+ case 'auto':
361
+ execGitAuto(rest.join(' ') || 'chore: update');
362
+ break;
363
+ default:
364
+ err(`Unknown git action: ${action}`);
365
+ gateHelp();
366
+ break;
367
+ }
368
+ break;
369
+
370
+ case 'trello':
371
+ if (!action || !rest.length) {
372
+ err(`Usage: awkit gate trello <action> <text>`);
373
+ gateHelp();
374
+ return;
375
+ }
376
+ execTrelloAction(action, rest);
377
+ break;
378
+
379
+ case 'telegram':
380
+ case 'tg':
381
+ if (action === 'send') {
382
+ execTelegramSend(rest);
383
+ } else {
384
+ err(`Unknown telegram action: ${action}`);
385
+ gateHelp();
386
+ }
387
+ break;
388
+
389
+ default:
390
+ err(`Unknown gate domain: ${domain}`);
391
+ gateHelp();
392
+ break;
393
+ }
394
+ }
395
+
396
+ // ─── Exports ──────────────────────────────────────────────────────────────────
397
+
398
+ module.exports = {
399
+ cmdGate,
400
+ checkGate,
401
+ readProjectIdentity,
402
+ };
@@ -0,0 +1,122 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ const HOME = os.homedir();
6
+ const MAIN_AWF_DIR = path.join(__dirname, '..');
7
+
8
+ // Nguồn v4 (Chi tiết, Persona, Auto-test loop)
9
+ const AWF_V4_DIR = path.join(HOME, 'Dev', 'NodeJS', 'awf');
10
+ // Nguồn v5 (Symphony, Brain, Fast execution)
11
+ const ANTIGRAVITY_V5_DIR = path.join(HOME, '.gemini', 'antigravity');
12
+
13
+ function ensureDir(dir) {
14
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
15
+ }
16
+
17
+ function copyFile(src, dest) {
18
+ if (fs.existsSync(src)) {
19
+ ensureDir(path.dirname(dest));
20
+ fs.copyFileSync(src, dest);
21
+ console.log(`✅ Copied: ${path.basename(dest)}`);
22
+ return true;
23
+ } else {
24
+ console.log(`❌ Source not found: ${src}`);
25
+ return false;
26
+ }
27
+ }
28
+
29
+ function copySkill(skillName, destDir) {
30
+ const destPath = path.join(MAIN_AWF_DIR, 'skills', destDir || skillName, 'SKILL.md');
31
+
32
+ // Try sources in order of priority
33
+ const sources = [
34
+ path.join(ANTIGRAVITY_V5_DIR, 'skills', `awf-${skillName}`, 'SKILL.md'),
35
+ path.join(ANTIGRAVITY_V5_DIR, 'skills', skillName, 'SKILL.md'),
36
+ path.join(AWF_V4_DIR, 'awf_skills', `awf-${skillName}`, 'SKILL.md')
37
+ ];
38
+
39
+ for (const src of sources) {
40
+ if (fs.existsSync(src)) {
41
+ ensureDir(path.dirname(destPath));
42
+ fs.copyFileSync(src, destPath);
43
+ console.log(`✅ Skill installed: ${skillName}`);
44
+ return;
45
+ }
46
+ }
47
+ console.log(`⚠️ Skill NOT found: ${skillName}`);
48
+ }
49
+
50
+ async function run() {
51
+ console.log('🚀 Bootstrapping AWF v6.0 Workflows & Skills...\n');
52
+
53
+ // 1. CURATE LIFECYCLE WORKFLOWS
54
+ console.log('--- Lifecycle Workflows ---');
55
+ // V4 is better for deep planning & coding
56
+ copyFile(path.join(AWF_V4_DIR, 'workflows', 'code.md'), path.join(MAIN_AWF_DIR, 'workflows', 'lifecycle', 'code.md'));
57
+ copyFile(path.join(AWF_V4_DIR, 'workflows', 'plan.md'), path.join(MAIN_AWF_DIR, 'workflows', 'lifecycle', 'plan.md'));
58
+ copyFile(path.join(AWF_V4_DIR, 'workflows', 'debug.md'), path.join(MAIN_AWF_DIR, 'workflows', 'lifecycle', 'debug.md'));
59
+ copyFile(path.join(AWF_V4_DIR, 'workflows', 'brainstorm.md'), path.join(MAIN_AWF_DIR, 'workflows', 'lifecycle', 'brainstorm.md'));
60
+ copyFile(path.join(AWF_V4_DIR, 'workflows', 'test.md'), path.join(MAIN_AWF_DIR, 'workflows', 'lifecycle', 'test.md'));
61
+ copyFile(path.join(AWF_V4_DIR, 'workflows', 'deploy.md'), path.join(MAIN_AWF_DIR, 'workflows', 'lifecycle', 'deploy.md'));
62
+ copyFile(path.join(AWF_V4_DIR, 'workflows', 'refactor.md'), path.join(MAIN_AWF_DIR, 'workflows', 'lifecycle', 'refactor.md'));
63
+ // V5 is better for init (has project-identity integration)
64
+ copyFile(path.join(ANTIGRAVITY_V5_DIR, 'global_workflows', 'init.md'), path.join(MAIN_AWF_DIR, 'workflows', 'lifecycle', 'init.md'));
65
+
66
+ // 2. CURATE CONTEXT WORKFLOWS
67
+ console.log('\n--- Context Workflows ---');
68
+ // Both from V5
69
+ copyFile(path.join(ANTIGRAVITY_V5_DIR, 'global_workflows', 'save-brain.md'), path.join(MAIN_AWF_DIR, 'workflows', 'context', 'save-brain.md'));
70
+ copyFile(path.join(ANTIGRAVITY_V5_DIR, 'global_workflows', 'recap.md'), path.join(MAIN_AWF_DIR, 'workflows', 'context', 'recap.md'));
71
+ copyFile(path.join(ANTIGRAVITY_V5_DIR, 'global_workflows', 'next.md'), path.join(MAIN_AWF_DIR, 'workflows', 'context', 'next.md'));
72
+
73
+ // 3. CURATE QUALITY & UI WORKFLOWS (From v5)
74
+ console.log('\n--- Quality & UI Workflows ---');
75
+ const v5Workflows = [
76
+ ['audit.md', 'quality'],
77
+ ['performance-audit.md', 'quality'],
78
+ ['ux-audit.md', 'quality'],
79
+ ['visualize.md', 'ui'],
80
+ ['design-to-ui.md', 'ui'],
81
+ ['ui-review.md', 'ui'],
82
+ ['ads-audit.md', 'ads'],
83
+ ['ads-optimize.md', 'ads'],
84
+ ['adsExpert.md', 'ads'],
85
+ ['git-commit-workflow.md', 'git', 'commit.md'],
86
+ ['hotfix.md', 'git'],
87
+ ['planExpert.md', 'expert'],
88
+ ['codeExpert.md', 'expert'],
89
+ ['debugExpert.md', 'expert']
90
+ ];
91
+
92
+ for (const [file, category, rename] of v5Workflows) {
93
+ const src = path.join(ANTIGRAVITY_V5_DIR, 'global_workflows', file);
94
+ const dest = path.join(MAIN_AWF_DIR, 'workflows', category, rename || file);
95
+ copyFile(src, dest);
96
+ }
97
+
98
+ // 4. CURATE META WORKFLOWS (From v4)
99
+ console.log('\n--- Meta Workflows ---');
100
+ copyFile(path.join(AWF_V4_DIR, 'workflows', 'customize.md'), path.join(MAIN_AWF_DIR, 'workflows', 'meta', 'customize.md'));
101
+ copyFile(path.join(AWF_V4_DIR, 'workflows', 'help.md'), path.join(MAIN_AWF_DIR, 'workflows', 'meta', 'help.md'));
102
+ copyFile(path.join(AWF_V4_DIR, 'workflows', 'awf-update.md'), path.join(MAIN_AWF_DIR, 'workflows', 'meta', 'awf-update.md'));
103
+ copyFile(path.join(AWF_V4_DIR, 'workflows', 'rollback.md'), path.join(MAIN_AWF_DIR, 'workflows', 'meta', 'rollback.md'));
104
+
105
+ // 5. CURATE CORE SKILLS
106
+ console.log('\n--- Core Skills ---');
107
+ const coreSkills = [
108
+ 'session-restore',
109
+ 'auto-save',
110
+ 'adaptive-language',
111
+ 'error-translator',
112
+ 'context-help',
113
+ 'symphony-orchestrator'
114
+ ];
115
+ for (const skill of coreSkills) {
116
+ copySkill(skill, skill);
117
+ }
118
+
119
+ console.log('\n✅ Bootstrap Complete! Essential framework files synchronized.');
120
+ }
121
+
122
+ run().catch(console.error);