tweakidea 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/install.js ADDED
@@ -0,0 +1,449 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+ const readline = require('readline');
9
+
10
+ const pkg = require('../package.json');
11
+
12
+ // ── ANSI helpers ────────────────────────────────────────────────────────────────
13
+
14
+ const cyan = '\x1b[36m';
15
+ const green = '\x1b[32m';
16
+ const yellow = '\x1b[33m';
17
+ const red = '\x1b[31m';
18
+ const dim = '\x1b[2m';
19
+ const bold = '\x1b[1m';
20
+ const reset = '\x1b[0m';
21
+
22
+ // ── File manifest ───────────────────────────────────────────────────────────────
23
+
24
+ const AGENT_FILES = [
25
+ 'ti-evaluator.md',
26
+ 'ti-extractor.md',
27
+ 'ti-merger.md',
28
+ 'ti-researcher.md',
29
+ ];
30
+
31
+ const SKILL_DIRS = [
32
+ 'ti-scoring',
33
+ 'ti-founder',
34
+ 'ti-html-report',
35
+ ];
36
+
37
+ const COMMAND_SRC = 'commands/tweak/evaluate.md';
38
+
39
+ // ── CLI argument parsing ────────────────────────────────────────────────────────
40
+
41
+ const args = process.argv.slice(2);
42
+
43
+ function hasFlag(...flags) {
44
+ return flags.some((f) => args.includes(f));
45
+ }
46
+
47
+ const wantGlobal = hasFlag('--global', '-g');
48
+ const wantLocal = hasFlag('--local', '-l');
49
+ const wantUninstall = hasFlag('--uninstall', '-u');
50
+ const wantHelp = hasFlag('--help', '-h');
51
+ const wantVersion = hasFlag('--version', '-v');
52
+
53
+ // ── Path resolution ─────────────────────────────────────────────────────────────
54
+
55
+ function getGlobalDir() {
56
+ return path.join(os.homedir(), '.claude');
57
+ }
58
+
59
+ function getLocalDir() {
60
+ return path.join(process.cwd(), '.claude');
61
+ }
62
+
63
+ function resolveTargetDir(isGlobal) {
64
+ return isGlobal ? getGlobalDir() : getLocalDir();
65
+ }
66
+
67
+ function getSourceDir() {
68
+ return path.join(__dirname, '..');
69
+ }
70
+
71
+ // ── File utilities ──────────────────────────────────────────────────────────────
72
+
73
+ function mkdirp(dir) {
74
+ fs.mkdirSync(dir, { recursive: true });
75
+ }
76
+
77
+ function copyFileSync(src, dest) {
78
+ mkdirp(path.dirname(dest));
79
+ fs.copyFileSync(src, dest);
80
+ }
81
+
82
+ function copyDirSync(src, dest) {
83
+ mkdirp(dest);
84
+ const entries = fs.readdirSync(src, { withFileTypes: true });
85
+ for (const entry of entries) {
86
+ const srcPath = path.join(src, entry.name);
87
+ const destPath = path.join(dest, entry.name);
88
+ if (entry.isDirectory()) {
89
+ copyDirSync(srcPath, destPath);
90
+ } else {
91
+ fs.copyFileSync(srcPath, destPath);
92
+ }
93
+ }
94
+ }
95
+
96
+ function rmrf(target) {
97
+ if (!fs.existsSync(target)) return;
98
+ fs.rmSync(target, { recursive: true, force: true });
99
+ }
100
+
101
+ // ── Cleanup ─────────────────────────────────────────────────────────────────────
102
+
103
+ function cleanupPreviousInstall(targetDir) {
104
+ let removed = 0;
105
+
106
+ // Remove agents
107
+ for (const file of AGENT_FILES) {
108
+ const p = path.join(targetDir, 'agents', file);
109
+ if (fs.existsSync(p)) {
110
+ fs.unlinkSync(p);
111
+ removed++;
112
+ }
113
+ }
114
+
115
+ // Remove skill directories
116
+ for (const dir of SKILL_DIRS) {
117
+ const p = path.join(targetDir, 'skills', dir);
118
+ if (fs.existsSync(p)) {
119
+ rmrf(p);
120
+ removed++;
121
+ }
122
+ }
123
+
124
+ // Remove command (local install format)
125
+ const cmdPath = path.join(targetDir, 'commands', 'tweak', 'evaluate.md');
126
+ if (fs.existsSync(cmdPath)) {
127
+ fs.unlinkSync(cmdPath);
128
+ // Clean empty parent dirs
129
+ const tweakDir = path.join(targetDir, 'commands', 'tweak');
130
+ if (fs.existsSync(tweakDir) && fs.readdirSync(tweakDir).length === 0) {
131
+ fs.rmdirSync(tweakDir);
132
+ }
133
+ removed++;
134
+ }
135
+
136
+ // Remove skill (global install format)
137
+ const skillPath = path.join(targetDir, 'skills', 'tweak-evaluate');
138
+ if (fs.existsSync(skillPath)) {
139
+ rmrf(skillPath);
140
+ removed++;
141
+ }
142
+
143
+ // Remove version tracking
144
+ const versionDir = path.join(targetDir, 'tweakidea');
145
+ if (fs.existsSync(versionDir)) {
146
+ rmrf(versionDir);
147
+ removed++;
148
+ }
149
+
150
+ return removed;
151
+ }
152
+
153
+ // ── Settings merger ─────────────────────────────────────────────────────────────
154
+
155
+ function mergeSettings(targetDir) {
156
+ const settingsPath = path.join(targetDir, 'settings.json');
157
+ let settings = {};
158
+
159
+ if (fs.existsSync(settingsPath)) {
160
+ try {
161
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
162
+ } catch {
163
+ // If settings.json is malformed, start fresh
164
+ settings = {};
165
+ }
166
+ }
167
+
168
+ // Ensure permissions structure
169
+ if (!settings.permissions) settings.permissions = {};
170
+ if (!Array.isArray(settings.permissions.allow)) settings.permissions.allow = [];
171
+ if (!Array.isArray(settings.permissions.additionalDirectories)) {
172
+ settings.permissions.additionalDirectories = [];
173
+ }
174
+
175
+ // Add required permissions (deduplicated)
176
+ const requiredAllow = ['WebSearch', 'WebFetch'];
177
+ for (const perm of requiredAllow) {
178
+ if (!settings.permissions.allow.includes(perm)) {
179
+ settings.permissions.allow.push(perm);
180
+ }
181
+ }
182
+
183
+ const requiredDirs = ['~/.tweakidea/*'];
184
+ for (const dir of requiredDirs) {
185
+ if (!settings.permissions.additionalDirectories.includes(dir)) {
186
+ settings.permissions.additionalDirectories.push(dir);
187
+ }
188
+ }
189
+
190
+ mkdirp(targetDir);
191
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
192
+ }
193
+
194
+ function removeSettingsEntries(targetDir) {
195
+ const settingsPath = path.join(targetDir, 'settings.json');
196
+ if (!fs.existsSync(settingsPath)) return;
197
+
198
+ let settings;
199
+ try {
200
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
201
+ } catch {
202
+ return;
203
+ }
204
+
205
+ if (settings.permissions) {
206
+ if (Array.isArray(settings.permissions.allow)) {
207
+ settings.permissions.allow = settings.permissions.allow.filter(
208
+ (p) => p !== 'WebSearch' && p !== 'WebFetch'
209
+ );
210
+ }
211
+ if (Array.isArray(settings.permissions.additionalDirectories)) {
212
+ settings.permissions.additionalDirectories =
213
+ settings.permissions.additionalDirectories.filter(
214
+ (d) => d !== '~/.tweakidea/*'
215
+ );
216
+ }
217
+ }
218
+
219
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
220
+ }
221
+
222
+ // ── Version tracking ────────────────────────────────────────────────────────────
223
+
224
+ function writeVersion(targetDir, version) {
225
+ const versionDir = path.join(targetDir, 'tweakidea');
226
+ mkdirp(versionDir);
227
+ fs.writeFileSync(path.join(versionDir, 'VERSION'), version + '\n');
228
+ }
229
+
230
+ function readInstalledVersion(targetDir) {
231
+ const versionFile = path.join(targetDir, 'tweakidea', 'VERSION');
232
+ if (!fs.existsSync(versionFile)) return null;
233
+ return fs.readFileSync(versionFile, 'utf8').trim();
234
+ }
235
+
236
+ // ── Install ─────────────────────────────────────────────────────────────────────
237
+
238
+ function install(isGlobal) {
239
+ const targetDir = resolveTargetDir(isGlobal);
240
+ const sourceDir = getSourceDir();
241
+ const location = isGlobal ? 'global' : 'local';
242
+ const displayPath = isGlobal
243
+ ? '~/.claude'
244
+ : path.relative(process.cwd(), targetDir) || '.claude';
245
+
246
+ // Check existing version
247
+ const existingVersion = readInstalledVersion(targetDir);
248
+ if (existingVersion) {
249
+ if (existingVersion === pkg.version) {
250
+ console.log(
251
+ `\n${dim}TweakIdea v${existingVersion} already installed. Reinstalling...${reset}`
252
+ );
253
+ } else {
254
+ console.log(
255
+ `\n${cyan}Upgrading TweakIdea from v${existingVersion} to v${pkg.version}...${reset}`
256
+ );
257
+ }
258
+ }
259
+
260
+ // Clean previous install
261
+ const removed = cleanupPreviousInstall(targetDir);
262
+ if (removed > 0) {
263
+ console.log(`${dim}Cleaned ${removed} previous TweakIdea file(s).${reset}`);
264
+ }
265
+
266
+ // Copy agents
267
+ mkdirp(path.join(targetDir, 'agents'));
268
+ for (const file of AGENT_FILES) {
269
+ copyFileSync(
270
+ path.join(sourceDir, 'agents', file),
271
+ path.join(targetDir, 'agents', file)
272
+ );
273
+ }
274
+
275
+ // Copy skills
276
+ for (const dir of SKILL_DIRS) {
277
+ copyDirSync(
278
+ path.join(sourceDir, 'skills', dir),
279
+ path.join(targetDir, 'skills', dir)
280
+ );
281
+ }
282
+
283
+ // Copy command/skill
284
+ if (isGlobal) {
285
+ // Global: install as skill (Claude Code discovers skills/*/SKILL.md)
286
+ const skillDir = path.join(targetDir, 'skills', 'tweak-evaluate');
287
+ mkdirp(skillDir);
288
+ copyFileSync(
289
+ path.join(sourceDir, COMMAND_SRC),
290
+ path.join(skillDir, 'SKILL.md')
291
+ );
292
+ } else {
293
+ // Local: install as command (Claude Code discovers commands/**/*.md)
294
+ copyFileSync(
295
+ path.join(sourceDir, COMMAND_SRC),
296
+ path.join(targetDir, 'commands', 'tweak', 'evaluate.md')
297
+ );
298
+ }
299
+
300
+ // Merge settings
301
+ mergeSettings(targetDir);
302
+
303
+ // Write version
304
+ writeVersion(targetDir, pkg.version);
305
+
306
+ // Verify
307
+ const agentCount = AGENT_FILES.filter((f) =>
308
+ fs.existsSync(path.join(targetDir, 'agents', f))
309
+ ).length;
310
+ const skillCount = SKILL_DIRS.filter((d) =>
311
+ fs.existsSync(path.join(targetDir, 'skills', d))
312
+ ).length;
313
+ const hasCommand = isGlobal
314
+ ? fs.existsSync(path.join(targetDir, 'skills', 'tweak-evaluate', 'SKILL.md'))
315
+ : fs.existsSync(path.join(targetDir, 'commands', 'tweak', 'evaluate.md'));
316
+
317
+ console.log('');
318
+ console.log(`${green}${bold}TweakIdea v${pkg.version} installed successfully!${reset}`);
319
+ console.log('');
320
+ console.log(` ${dim}Location:${reset} ${displayPath} (${location})`);
321
+ console.log(` ${dim}Agents:${reset} ${agentCount} installed`);
322
+ console.log(` ${dim}Skills:${reset} ${skillCount} installed`);
323
+ console.log(` ${dim}Command:${reset} ${hasCommand ? 'yes' : 'MISSING'}`);
324
+ console.log('');
325
+ console.log(`${cyan}Get started:${reset}`);
326
+ console.log(` ${bold}/tweak:evaluate${reset} ${dim}"Your startup idea description"${reset}`);
327
+ console.log('');
328
+ }
329
+
330
+ // ── Uninstall ───────────────────────────────────────────────────────────────────
331
+
332
+ function uninstall(isGlobal) {
333
+ const targetDir = resolveTargetDir(isGlobal);
334
+ const displayPath = isGlobal ? '~/.claude' : '.claude';
335
+
336
+ const existingVersion = readInstalledVersion(targetDir);
337
+ if (!existingVersion) {
338
+ console.log(`\n${yellow}TweakIdea is not installed in ${displayPath}.${reset}\n`);
339
+ return;
340
+ }
341
+
342
+ cleanupPreviousInstall(targetDir);
343
+ removeSettingsEntries(targetDir);
344
+
345
+ console.log('');
346
+ console.log(
347
+ `${green}TweakIdea v${existingVersion} uninstalled from ${displayPath}.${reset}`
348
+ );
349
+ console.log(`${dim}Your evaluation data in ~/.tweakidea/ was not removed.${reset}`);
350
+ console.log('');
351
+ }
352
+
353
+ // ── Interactive prompts ─────────────────────────────────────────────────────────
354
+
355
+ function prompt(question) {
356
+ return new Promise((resolve) => {
357
+ const rl = readline.createInterface({
358
+ input: process.stdin,
359
+ output: process.stdout,
360
+ });
361
+ rl.question(question, (answer) => {
362
+ rl.close();
363
+ resolve(answer.trim());
364
+ });
365
+ });
366
+ }
367
+
368
+ async function promptLocation() {
369
+ console.log('');
370
+ console.log(` ${bold}Where should TweakIdea be installed?${reset}`);
371
+ console.log('');
372
+ console.log(` ${cyan}1)${reset} Global ${dim}(~/.claude)${reset} - available in all projects`);
373
+ console.log(` ${cyan}2)${reset} Local ${dim}(./.claude)${reset} - this project only`);
374
+ console.log('');
375
+
376
+ const answer = await prompt(` ${bold}Select [1]:${reset} `);
377
+ return answer === '2' ? false : true;
378
+ }
379
+
380
+ // ── Help ────────────────────────────────────────────────────────────────────────
381
+
382
+ function printHelp() {
383
+ console.log(`
384
+ ${bold}TweakIdea v${pkg.version}${reset}
385
+ 14-dimension startup idea evaluator for Claude Code
386
+
387
+ ${bold}Usage:${reset}
388
+ npx tweakidea [options]
389
+
390
+ ${bold}Options:${reset}
391
+ -g, --global Install globally (~/.claude)
392
+ -l, --local Install locally (./.claude)
393
+ -u, --uninstall Remove TweakIdea files
394
+ -v, --version Show version
395
+ -h, --help Show this help
396
+
397
+ ${bold}Examples:${reset}
398
+ npx tweakidea Interactive install
399
+ npx tweakidea -g Global install (no prompt)
400
+ npx tweakidea -l Local install (no prompt)
401
+ npx tweakidea -u -g Uninstall from global
402
+ `);
403
+ }
404
+
405
+ // ── Entry point ─────────────────────────────────────────────────────────────────
406
+
407
+ async function main() {
408
+ // Banner
409
+ console.log('');
410
+ console.log(
411
+ `${cyan}${bold}TweakIdea${reset} ${dim}v${pkg.version}${reset}`
412
+ );
413
+
414
+ if (wantHelp) {
415
+ printHelp();
416
+ return;
417
+ }
418
+
419
+ if (wantVersion) {
420
+ console.log(pkg.version);
421
+ return;
422
+ }
423
+
424
+ if (wantUninstall) {
425
+ const isGlobal = wantLocal ? false : true; // default to global for uninstall
426
+ uninstall(isGlobal);
427
+ return;
428
+ }
429
+
430
+ // Determine install location
431
+ let isGlobal;
432
+ if (wantGlobal) {
433
+ isGlobal = true;
434
+ } else if (wantLocal) {
435
+ isGlobal = false;
436
+ } else if (!process.stdin.isTTY) {
437
+ // Non-interactive: default to global
438
+ isGlobal = true;
439
+ } else {
440
+ isGlobal = await promptLocation();
441
+ }
442
+
443
+ install(isGlobal);
444
+ }
445
+
446
+ main().catch((err) => {
447
+ console.error(`\n${red}Error: ${err.message}${reset}\n`);
448
+ process.exit(1);
449
+ });