@traqr/cli 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.
Files changed (42) hide show
  1. package/README.md +84 -0
  2. package/dist/bin/traqr.d.ts +20 -0
  3. package/dist/bin/traqr.d.ts.map +1 -0
  4. package/dist/bin/traqr.js +104 -0
  5. package/dist/bin/traqr.js.map +1 -0
  6. package/dist/commands/init.d.ts +8 -0
  7. package/dist/commands/init.d.ts.map +1 -0
  8. package/dist/commands/init.js +772 -0
  9. package/dist/commands/init.js.map +1 -0
  10. package/dist/commands/projects.d.ts +9 -0
  11. package/dist/commands/projects.d.ts.map +1 -0
  12. package/dist/commands/projects.js +78 -0
  13. package/dist/commands/projects.js.map +1 -0
  14. package/dist/commands/render.d.ts +12 -0
  15. package/dist/commands/render.d.ts.map +1 -0
  16. package/dist/commands/render.js +49 -0
  17. package/dist/commands/render.js.map +1 -0
  18. package/dist/commands/setup.d.ts +8 -0
  19. package/dist/commands/setup.d.ts.map +1 -0
  20. package/dist/commands/setup.js +343 -0
  21. package/dist/commands/setup.js.map +1 -0
  22. package/dist/commands/status.d.ts +8 -0
  23. package/dist/commands/status.d.ts.map +1 -0
  24. package/dist/commands/status.js +46 -0
  25. package/dist/commands/status.js.map +1 -0
  26. package/dist/index.d.ts +8 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +8 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/lib/checks.d.ts +8 -0
  31. package/dist/lib/checks.d.ts.map +1 -0
  32. package/dist/lib/checks.js +45 -0
  33. package/dist/lib/checks.js.map +1 -0
  34. package/dist/lib/prompts.d.ts +24 -0
  35. package/dist/lib/prompts.d.ts.map +1 -0
  36. package/dist/lib/prompts.js +76 -0
  37. package/dist/lib/prompts.js.map +1 -0
  38. package/dist/lib/writer.d.ts +22 -0
  39. package/dist/lib/writer.d.ts.map +1 -0
  40. package/dist/lib/writer.js +43 -0
  41. package/dist/lib/writer.js.map +1 -0
  42. package/package.json +52 -0
@@ -0,0 +1,772 @@
1
+ /**
2
+ * traqr init — Interactive project setup wizard
3
+ *
4
+ * Walks through project config, starter pack selection,
5
+ * and renders all templates to disk.
6
+ */
7
+ import path from 'path';
8
+ import fs from 'fs/promises';
9
+ import { existsSync } from 'fs';
10
+ import { execSync } from 'child_process';
11
+ import { STARTER_PACK_DEFAULTS, calculateAutomationScore, mergePreferredStack, renderAllTemplates, renderSubAppTemplates, loadOrgConfig, writeOrgConfig, writeAliasFile, registerProject, generateMotd, writeShellInit, detectMonorepo, buildSubAppChecklist, deriveAppChannels, deriveLinearTeamConfig, formatChecklist, generatePortTable, } from '@traqr/core';
12
+ import { ask, confirm, select, info, askValidated, closePrompts } from '../lib/prompts.js';
13
+ import { writeFiles } from '../lib/writer.js';
14
+ import { checkPrerequisites } from '../lib/checks.js';
15
+ const RAQR_WELCOME = `
16
+ /\\___/\\
17
+ ( o o ) Hey! Let's set up Traqr.
18
+ ( =^= ) I'll walk you through it.
19
+ (______)
20
+ `;
21
+ // ============================================================
22
+ // Step 0 — Ensure we're in a project directory
23
+ // ============================================================
24
+ async function ensureProjectDir() {
25
+ const cwd = process.cwd();
26
+ const home = process.env.HOME || '';
27
+ const signals = ['.git', 'package.json', 'src', 'app', 'lib', 'pages', 'Cargo.toml', 'pyproject.toml', 'go.mod'];
28
+ let hasProject = false;
29
+ for (const s of signals) {
30
+ try {
31
+ await fs.access(path.join(cwd, s));
32
+ hasProject = true;
33
+ break;
34
+ }
35
+ catch { /* noop */ }
36
+ }
37
+ if (hasProject)
38
+ return cwd;
39
+ console.log(" Doesn't look like you're in a project directory.\n");
40
+ const choice = await select('What would you like to do?', [
41
+ { label: 'Create a new project', value: 'new', description: 'Pick a framework and scaffold a fresh project' },
42
+ { label: 'Point to an existing project', value: 'navigate', description: 'Enter the path to a project folder' },
43
+ { label: 'Use this directory anyway', value: 'here', description: 'Set up Traqr right here' },
44
+ ]);
45
+ if (choice === 'here')
46
+ return cwd;
47
+ if (choice === 'navigate') {
48
+ info('Enter the full path, or drag the folder into the terminal.');
49
+ const projectPath = await ask('Project path');
50
+ const resolved = path.resolve(projectPath.replace(/^~/, home));
51
+ try {
52
+ await fs.access(resolved);
53
+ }
54
+ catch {
55
+ console.error(` Directory not found: ${resolved}`);
56
+ process.exit(1);
57
+ }
58
+ process.chdir(resolved);
59
+ return resolved;
60
+ }
61
+ // 'new' — scaffold a project
62
+ return await scaffoldNewProject();
63
+ }
64
+ async function scaffoldNewProject() {
65
+ const home = process.env.HOME || '';
66
+ const { config: existingOrg } = loadOrgConfig();
67
+ const suggestedRoot = existingOrg?.projectsRoot || path.join(home, 'Projects');
68
+ const root = await ask('Where do you keep projects?', suggestedRoot);
69
+ const resolvedRoot = path.resolve(root.replace(/^~/, home));
70
+ await fs.mkdir(resolvedRoot, { recursive: true });
71
+ // Save projectsRoot for next time
72
+ if (!existingOrg?.projectsRoot) {
73
+ writeOrgConfig({ ...existingOrg, projectsRoot: resolvedRoot });
74
+ }
75
+ const framework = await select('Which framework?', [
76
+ { label: 'Next.js', value: 'nextjs', description: 'React framework with App Router' },
77
+ { label: 'Vite (React)', value: 'vite-react', description: 'Fast build tool + React' },
78
+ { label: 'Vite (Vue)', value: 'vite-vue', description: 'Fast build tool + Vue' },
79
+ { label: 'None (empty folder)', value: 'none', description: 'Just git init, no framework' },
80
+ ]);
81
+ const name = await ask('Project name');
82
+ const projectDir = path.join(resolvedRoot, name);
83
+ if (framework === 'none') {
84
+ await fs.mkdir(projectDir, { recursive: true });
85
+ execSync('git init', { cwd: projectDir, stdio: 'pipe' });
86
+ execSync('git commit --allow-empty -m "chore: initial commit"', { cwd: projectDir, stdio: 'pipe' });
87
+ }
88
+ else {
89
+ console.log(`\n Scaffolding ${name}...`);
90
+ const cmds = {
91
+ 'nextjs': `npx create-next-app@latest "${name}" --ts --tailwind --eslint --app --src-dir --use-npm`,
92
+ 'vite-react': `npm create vite@latest "${name}" -- --template react-ts`,
93
+ 'vite-vue': `npm create vite@latest "${name}" -- --template vue-ts`,
94
+ };
95
+ try {
96
+ execSync(cmds[framework], { cwd: resolvedRoot, stdio: 'inherit' });
97
+ }
98
+ catch {
99
+ console.error(' Scaffolding failed. Creating empty project instead.');
100
+ await fs.mkdir(projectDir, { recursive: true });
101
+ }
102
+ // Ensure git is initialized
103
+ if (!existsSync(path.join(projectDir, '.git'))) {
104
+ execSync('git init', { cwd: projectDir, stdio: 'pipe' });
105
+ execSync('git add -A && git commit -m "chore: initial scaffold"', { cwd: projectDir, stdio: 'pipe', shell: '/bin/sh' });
106
+ }
107
+ }
108
+ console.log(` Created: ${projectDir}\n`);
109
+ process.chdir(projectDir);
110
+ return projectDir;
111
+ }
112
+ // ============================================================
113
+ // Detection helpers
114
+ // ============================================================
115
+ async function detectDefaults() {
116
+ const repoPath = process.cwd();
117
+ let ghOrgRepo = '';
118
+ let packageManager = 'npm';
119
+ let framework = 'unknown';
120
+ // Detect git remote
121
+ try {
122
+ const remote = execSync('git remote get-url origin', { encoding: 'utf-8' }).trim();
123
+ const match = remote.match(/github\.com[/:]([\w.-]+\/[\w.-]+?)(?:\.git)?$/);
124
+ if (match)
125
+ ghOrgRepo = match[1];
126
+ }
127
+ catch { /* not a git repo or no remote */ }
128
+ // Detect package manager
129
+ try {
130
+ await fs.access(path.join(repoPath, 'bun.lockb'));
131
+ packageManager = 'bun';
132
+ }
133
+ catch {
134
+ try {
135
+ await fs.access(path.join(repoPath, 'pnpm-lock.yaml'));
136
+ packageManager = 'pnpm';
137
+ }
138
+ catch {
139
+ try {
140
+ await fs.access(path.join(repoPath, 'yarn.lock'));
141
+ packageManager = 'yarn';
142
+ }
143
+ catch { /* default: npm */ }
144
+ }
145
+ }
146
+ // Detect framework
147
+ try {
148
+ const pkg = JSON.parse(await fs.readFile(path.join(repoPath, 'package.json'), 'utf-8'));
149
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
150
+ if (deps.next)
151
+ framework = 'nextjs';
152
+ else if (deps.nuxt)
153
+ framework = 'nuxt';
154
+ else if (deps.svelte || deps['@sveltejs/kit'])
155
+ framework = 'svelte';
156
+ else if (deps.react)
157
+ framework = 'react';
158
+ else if (deps.vue)
159
+ framework = 'vue';
160
+ else if (deps.express)
161
+ framework = 'express';
162
+ else if (deps.hono)
163
+ framework = 'hono';
164
+ }
165
+ catch { /* no package.json */ }
166
+ return { repoPath, ghOrgRepo, packageManager, framework };
167
+ }
168
+ function detectOrgServices() {
169
+ try {
170
+ const { config } = loadOrgConfig();
171
+ if (!config?.services)
172
+ return { orgConfig: config, connectedServices: [] };
173
+ const connectedServices = Object.entries(config.services)
174
+ .filter(([, svc]) => svc.connected)
175
+ .map(([name]) => name);
176
+ return { orgConfig: config, connectedServices };
177
+ }
178
+ catch {
179
+ return { orgConfig: null, connectedServices: [] };
180
+ }
181
+ }
182
+ function mergeOrgDefaults(config, orgConfig) {
183
+ var _a, _b;
184
+ let merged = { ...config };
185
+ if (orgConfig.coAuthor)
186
+ merged.coAuthor = orgConfig.coAuthor;
187
+ if (orgConfig.memory) {
188
+ merged.memory = { ...merged.memory, ...orgConfig.memory };
189
+ }
190
+ if (orgConfig.issues) {
191
+ merged.issues = { ...merged.issues, ...orgConfig.issues };
192
+ }
193
+ if (merged.issues?.provider === 'linear' && orgConfig.services?.linear) {
194
+ const svc = orgConfig.services.linear;
195
+ if (svc.defaultTeamId)
196
+ (_a = merged.issues).linearTeamId ?? (_a.linearTeamId = svc.defaultTeamId);
197
+ if (svc.workspaceSlug)
198
+ (_b = merged.issues).linearWorkspaceSlug ?? (_b.linearWorkspaceSlug = svc.workspaceSlug);
199
+ }
200
+ if (orgConfig.notifications) {
201
+ merged.notifications = { ...merged.notifications, ...orgConfig.notifications };
202
+ }
203
+ if (orgConfig.daemon) {
204
+ merged.daemon = { ...merged.daemon, ...orgConfig.daemon };
205
+ }
206
+ if (orgConfig.guardian) {
207
+ merged.guardian = { ...merged.guardian, ...orgConfig.guardian };
208
+ }
209
+ // Apply preferredStack defaults for services not yet configured
210
+ if (orgConfig.preferredStack) {
211
+ merged = mergePreferredStack(merged, orgConfig.preferredStack);
212
+ }
213
+ return merged;
214
+ }
215
+ // ============================================================
216
+ // Monorepo Sub-App Init
217
+ // ============================================================
218
+ async function runSubAppInit(mono) {
219
+ const repoPath = process.cwd();
220
+ // Load existing project config
221
+ const existingConfigPath = path.join(repoPath, '.traqr', 'config.json');
222
+ let config;
223
+ try {
224
+ const raw = await fs.readFile(existingConfigPath, 'utf-8');
225
+ config = JSON.parse(raw);
226
+ }
227
+ catch {
228
+ console.error(' No .traqr/config.json found. Run traqr init first for the root project.');
229
+ process.exit(1);
230
+ }
231
+ // Load org config for service connection info
232
+ const { orgConfig } = detectOrgServices();
233
+ // Display parent config summary
234
+ const parentTier = config.tier;
235
+ const parentScore = config.automationScore ?? calculateAutomationScore(config);
236
+ const sharedInfra = [];
237
+ if (config.memory?.provider === 'supabase')
238
+ sharedInfra.push('Supabase');
239
+ if (config.issues?.provider === 'linear')
240
+ sharedInfra.push('Linear');
241
+ if (config.notifications?.slackLevel && config.notifications.slackLevel !== 'none')
242
+ sharedInfra.push('Slack');
243
+ if (config.monitoring?.analytics === 'posthog')
244
+ sharedInfra.push('PostHog');
245
+ if (config.edge?.provider === 'cloudflare')
246
+ sharedInfra.push('Cloudflare');
247
+ if (config.memory?.crossProject)
248
+ sharedInfra.push('Memory');
249
+ console.log(`\n Parent: ${config.project.displayName} (Tier ${parentTier}, Score ${parentScore}/100)`);
250
+ if (sharedInfra.length > 0) {
251
+ console.log(` Shared infra: ${sharedInfra.join(', ')}`);
252
+ }
253
+ // App name
254
+ const appName = await ask('App name (slug, e.g. "pokotraqr")');
255
+ const appDisplayName = await ask('Display name', appName);
256
+ const appDir = `apps/${appName}`;
257
+ // Check if app directory already exists
258
+ if (existsSync(path.join(repoPath, appDir))) {
259
+ console.error(` Directory ${appDir} already exists.`);
260
+ process.exit(1);
261
+ }
262
+ // Calculate port offset from existing app count
263
+ const portOffset = mono.existingApps.length * 1000;
264
+ console.log(`\n Port offset: ${portOffset} (feature1 port: ${3001 + portOffset})`);
265
+ // Build the provisioning checklist
266
+ const plan = buildSubAppChecklist(config, orgConfig, appName, appDisplayName);
267
+ // Derive expected per-app resources
268
+ const aliasGuess = appName.replace(/[^a-z0-9]/gi, '').toLowerCase().slice(0, 2);
269
+ const slackChannelPrefix = await ask('Slack channel prefix (2-3 chars)', aliasGuess);
270
+ const derivedChannels = deriveAppChannels(config, slackChannelPrefix);
271
+ const derivedLinear = config.issues?.provider === 'linear'
272
+ ? deriveLinearTeamConfig(config, appName)
273
+ : null;
274
+ // Display checklist preview
275
+ console.log(`\n Provisioning checklist for ${appDisplayName}:`);
276
+ console.log(formatChecklist(plan));
277
+ // Auth provider
278
+ const authProvider = await select('Auth provider for this app:', [
279
+ { label: 'None', value: 'none', description: 'No auth (add later)' },
280
+ { label: 'Clerk', value: 'clerk', description: 'Drop-in auth with Clerk' },
281
+ { label: 'Firebase', value: 'firebase', description: 'Firebase Authentication' },
282
+ { label: 'Supabase Auth', value: 'supabase', description: 'Supabase built-in auth' },
283
+ { label: 'Custom', value: 'custom', description: 'Roll your own' },
284
+ ]);
285
+ // Companion data package
286
+ const wantCompanion = await confirm('Create a companion data package?', false);
287
+ let companionPackage;
288
+ let companionDir;
289
+ if (wantCompanion) {
290
+ const companionName = await ask('Package name', `@${appName}/data`);
291
+ companionPackage = companionName;
292
+ companionDir = `packages/${companionName.replace(/^@/, '').replace('/', '-')}`;
293
+ }
294
+ // Build workspace deps
295
+ const baseDeps = ['@traqr/core'];
296
+ if (companionPackage)
297
+ baseDeps.push(companionPackage);
298
+ const workspaceDeps = baseDeps;
299
+ // Add monorepo section if not present
300
+ if (!config.monorepo) {
301
+ config.monorepo = {
302
+ enabled: true,
303
+ appDirs: mono.existingApps.map(a => `apps/${a}`),
304
+ apps: {},
305
+ };
306
+ for (let i = 0; i < mono.existingApps.length; i++) {
307
+ const existing = mono.existingApps[i];
308
+ config.monorepo.apps[existing] = {
309
+ displayName: existing,
310
+ appDir: `apps/${existing}`,
311
+ portOffset: i * 1000,
312
+ workspaceDeps: ['@traqr/core'],
313
+ };
314
+ }
315
+ }
316
+ // Add the new app with per-app service fields
317
+ config.monorepo.apps[appName] = {
318
+ displayName: appDisplayName,
319
+ appDir: appDir,
320
+ portOffset,
321
+ auth: { provider: authProvider },
322
+ framework: 'nextjs',
323
+ workspaceDeps,
324
+ companionPackage,
325
+ slackChannelPrefix,
326
+ slackChannels: derivedChannels,
327
+ linearTeamId: derivedLinear ? undefined : undefined, // populated by Claude via MCP
328
+ ticketPrefix: derivedLinear?.ticketPrefix,
329
+ };
330
+ config.monorepo.appDirs.push(appDir);
331
+ // Update linearTeamMap if Linear is used
332
+ if (derivedLinear && config.issues) {
333
+ if (!config.issues.linearTeamMap) {
334
+ config.issues.linearTeamMap = {};
335
+ // Register parent team
336
+ if (config.issues.ticketPrefix && config.issues.linearTeamId) {
337
+ config.issues.linearTeamMap[config.issues.ticketPrefix] = config.issues.linearTeamId;
338
+ }
339
+ }
340
+ // New app entry will be populated by Claude via MCP during provisioning
341
+ }
342
+ // Update channelPrefixMap if Slack is used
343
+ if (slackChannelPrefix && config.issues) {
344
+ if (!config.issues.channelPrefixMap) {
345
+ config.issues.channelPrefixMap = {};
346
+ if (config.issues.ticketPrefix && config.notifications?.slackChannelPrefix) {
347
+ config.issues.channelPrefixMap[config.issues.ticketPrefix] = config.notifications.slackChannelPrefix;
348
+ }
349
+ }
350
+ if (derivedLinear) {
351
+ config.issues.channelPrefixMap[derivedLinear.ticketPrefix] = slackChannelPrefix;
352
+ }
353
+ }
354
+ // Preview
355
+ console.log(`\n Will create:`);
356
+ console.log(` ${appDir}/ (Next.js app)`);
357
+ if (companionDir)
358
+ console.log(` ${companionDir}/ (data package)`);
359
+ console.log(` Port offset: ${portOffset}`);
360
+ console.log(` Auth: ${authProvider}`);
361
+ console.log(` Slack prefix: ${slackChannelPrefix}`);
362
+ if (derivedLinear)
363
+ console.log(` Ticket prefix: ${derivedLinear.ticketPrefix}`);
364
+ console.log(` Deps: ${workspaceDeps.join(', ')}`);
365
+ if (Object.keys(derivedChannels).length > 0) {
366
+ console.log(`\n Expected Slack channels:`);
367
+ for (const [purpose, channel] of Object.entries(derivedChannels)) {
368
+ console.log(` ${purpose}: ${channel}`);
369
+ }
370
+ }
371
+ const proceed = await confirm('\nProceed?');
372
+ if (!proceed) {
373
+ console.log('Aborted.');
374
+ process.exit(0);
375
+ }
376
+ // Render sub-app templates
377
+ console.log('\nRendering sub-app templates...');
378
+ const result = await renderSubAppTemplates(config, appName);
379
+ if (result.warnings.length > 0) {
380
+ for (const w of result.warnings)
381
+ console.log(` Warning: ${w}`);
382
+ }
383
+ // Create app directory structure
384
+ await fs.mkdir(path.join(repoPath, appDir, 'src', 'app'), { recursive: true });
385
+ // Write rendered files
386
+ for (const [filePath, content] of Object.entries(result.files)) {
387
+ if (filePath === 'companion-package.json' && companionDir) {
388
+ await fs.mkdir(path.join(repoPath, companionDir, 'src'), { recursive: true });
389
+ await fs.writeFile(path.join(repoPath, companionDir, 'package.json'), content, 'utf-8');
390
+ console.log(` ${companionDir}/package.json`);
391
+ const companionTsconfig = JSON.stringify({
392
+ extends: '../../tsconfig.base.json',
393
+ compilerOptions: {
394
+ outDir: './dist',
395
+ rootDir: './src',
396
+ declaration: true,
397
+ },
398
+ include: ['src'],
399
+ exclude: ['node_modules', 'dist'],
400
+ }, null, 2);
401
+ await fs.writeFile(path.join(repoPath, companionDir, 'tsconfig.json'), companionTsconfig, 'utf-8');
402
+ console.log(` ${companionDir}/tsconfig.json`);
403
+ await fs.writeFile(path.join(repoPath, companionDir, 'src', 'index.ts'), `/**\n * ${appDisplayName} shared data layer\n */\n\nexport {}\n`, 'utf-8');
404
+ console.log(` ${companionDir}/src/index.ts`);
405
+ continue;
406
+ }
407
+ const absPath = path.join(repoPath, filePath);
408
+ await fs.mkdir(path.dirname(absPath), { recursive: true });
409
+ await fs.writeFile(absPath, content, 'utf-8');
410
+ console.log(` ${filePath}`);
411
+ }
412
+ // Create minimal src/app/page.tsx
413
+ const pagePath = path.join(repoPath, appDir, 'src', 'app', 'page.tsx');
414
+ if (!existsSync(pagePath)) {
415
+ await fs.writeFile(pagePath, `export default function Home() {\n return (\n <main>\n <h1>${appDisplayName}</h1>\n <p>Powered by Traqr</p>\n </main>\n );\n}\n`, 'utf-8');
416
+ console.log(` ${appDir}/src/app/page.tsx`);
417
+ }
418
+ // Create minimal src/app/layout.tsx
419
+ const layoutPath = path.join(repoPath, appDir, 'src', 'app', 'layout.tsx');
420
+ if (!existsSync(layoutPath)) {
421
+ await fs.writeFile(layoutPath, `export const metadata = {\n title: '${appDisplayName}',\n description: '${appDisplayName} — powered by Traqr',\n};\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n return (\n <html lang="en">\n <body>{children}</body>\n </html>\n );\n}\n`, 'utf-8');
422
+ console.log(` ${appDir}/src/app/layout.tsx`);
423
+ }
424
+ // Update root tsconfig.json references
425
+ const rootTsconfigPath = path.join(repoPath, 'tsconfig.json');
426
+ try {
427
+ const rootTsconfig = JSON.parse(await fs.readFile(rootTsconfigPath, 'utf-8'));
428
+ const refs = rootTsconfig.references || [];
429
+ if (!refs.some(r => r.path === appDir)) {
430
+ refs.push({ path: appDir });
431
+ rootTsconfig.references = refs;
432
+ await fs.writeFile(rootTsconfigPath, JSON.stringify(rootTsconfig, null, 2) + '\n', 'utf-8');
433
+ console.log(` Updated tsconfig.json references`);
434
+ }
435
+ if (companionDir && !refs.some(r => r.path === companionDir)) {
436
+ refs.push({ path: companionDir });
437
+ rootTsconfig.references = refs;
438
+ await fs.writeFile(rootTsconfigPath, JSON.stringify(rootTsconfig, null, 2) + '\n', 'utf-8');
439
+ console.log(` Added ${companionDir} to tsconfig.json references`);
440
+ }
441
+ }
442
+ catch {
443
+ console.warn(' Warning: could not update root tsconfig.json');
444
+ }
445
+ // Save updated config
446
+ await fs.writeFile(existingConfigPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
447
+ console.log(` Updated .traqr/config.json`);
448
+ // Port allocation table
449
+ console.log('\nPort allocation:');
450
+ console.log(generatePortTable(config));
451
+ // Summary: show what Claude should do next via the skill template
452
+ console.log(`\nScaffolding complete! The /traqr-init skill will now guide you through`);
453
+ console.log(`MCP-based discovery and wiring for each service.`);
454
+ console.log(`\nRun "npm install" to wire workspace dependencies.`);
455
+ console.log(`Worktrees are shared — no new slots needed.`);
456
+ }
457
+ // ============================================================
458
+ // Main
459
+ // ============================================================
460
+ async function run() {
461
+ checkPrerequisites();
462
+ console.log(RAQR_WELCOME);
463
+ // Step 0: Ensure we're in a project directory
464
+ await ensureProjectDir();
465
+ const defaults = await detectDefaults();
466
+ // Detect monorepo context
467
+ const mono = detectMonorepo();
468
+ if (mono.isMonorepo) {
469
+ console.log(` Monorepo detected! Found ${mono.existingApps.length} app(s): ${mono.existingApps.join(', ')}`);
470
+ if (mono.existingPackages.length > 0) {
471
+ console.log(` Packages: ${mono.existingPackages.join(', ')}`);
472
+ }
473
+ console.log('');
474
+ const monoChoice = await select('What would you like to do?', [
475
+ { label: 'Add a new app to this monorepo', value: 'sub-app', description: 'Scaffold a new app in apps/' },
476
+ { label: 'Configure this monorepo as standalone', value: 'standalone', description: 'Standard Traqr init for the root project' },
477
+ ]);
478
+ if (monoChoice === 'sub-app') {
479
+ await runSubAppInit(mono);
480
+ closePrompts();
481
+ return;
482
+ }
483
+ // else: fall through to standard init
484
+ }
485
+ // Detect org profile and services
486
+ const { orgConfig, connectedServices } = detectOrgServices();
487
+ if (orgConfig && connectedServices.length > 0) {
488
+ console.log(`\n Global profile detected. These services carry over:`);
489
+ if (orgConfig.coAuthor)
490
+ console.log(` Co-author: ${orgConfig.coAuthor}`);
491
+ const svcDetails = [];
492
+ if (orgConfig.services?.slack?.connected)
493
+ svcDetails.push('Slack');
494
+ if (orgConfig.services?.linear?.connected)
495
+ svcDetails.push(`Linear (${orgConfig.services.linear.workspaceSlug || 'connected'})`);
496
+ if (orgConfig.services?.supabase?.connected)
497
+ svcDetails.push(`Supabase (${orgConfig.services.supabase.projectRef || 'connected'})`);
498
+ if (svcDetails.length > 0)
499
+ console.log(` Services: ${svcDetails.join(', ')}`);
500
+ console.log('');
501
+ const customize = await confirm('Want to change anything for this project?', false);
502
+ if (customize) {
503
+ console.log(' (Per-project service customization coming soon. Using global defaults.)');
504
+ }
505
+ }
506
+ // Inline minimal setup if no global profile
507
+ if (!orgConfig) {
508
+ console.log(' No global profile found.\n');
509
+ const coAuthor = await ask('Co-author for git commits', 'Claude Opus 4.6');
510
+ writeOrgConfig({ coAuthor, maxConcurrentSlots: 3, projects: {} });
511
+ console.log(' Saved to ~/.traqr/config.json\n');
512
+ }
513
+ // Basic project info
514
+ const projectName = await ask('Project name', path.basename(defaults.repoPath));
515
+ const displayName = await ask('Display name', projectName);
516
+ const description = await ask('Description', `${displayName} — powered by Traqr`);
517
+ const ghOrgRepo = await ask('GitHub org/repo', defaults.ghOrgRepo);
518
+ // Prefix validation + explanation
519
+ info('Short code for your project (used in shell commands like z1, c1).\n Example: "nk" for NookTraqr.');
520
+ const prefix = await askValidated('Project prefix (2-6 chars)', projectName.replace(/[^a-z0-9]/gi, '').toLowerCase().slice(0, 4), (input) => {
521
+ const v = input.toLowerCase();
522
+ if (v.length < 2)
523
+ return { valid: false, message: 'Must be at least 2 characters.', suggestion: projectName.replace(/[^a-z0-9]/gi, '').toLowerCase().slice(0, 4) };
524
+ if (v.length > 6)
525
+ return { valid: false, message: '6 characters max.', suggestion: v.slice(0, 6) };
526
+ if (!/^[a-z][a-z0-9]*$/.test(v))
527
+ return { valid: false, message: 'Lowercase letters and numbers only, starting with a letter.', suggestion: v.replace(/[^a-z0-9]/g, '').slice(0, 6) || 'tp' };
528
+ return { valid: true };
529
+ });
530
+ // Starter pack selection with plain-English descriptions
531
+ const starterPack = await select('Choose a starter pack:', [
532
+ { label: 'Solo', value: 'solo',
533
+ description: 'Just you and Claude. Parallel workspaces, clean git workflow.' },
534
+ { label: 'Smart', value: 'smart',
535
+ description: 'Adds project memory + issue tracking. Claude remembers past decisions.' },
536
+ { label: 'Production', value: 'production',
537
+ description: 'Team notifications, error tracking, analytics. For apps with users.' },
538
+ { label: 'Full', value: 'full',
539
+ description: 'Everything on. Autonomous agents, all integrations, full ops.' },
540
+ ]);
541
+ const packDefaults = STARTER_PACK_DEFAULTS[starterPack];
542
+ const repoPath = defaults.repoPath;
543
+ const worktreesPath = `${repoPath}/.worktrees`;
544
+ // Build the config
545
+ let config = {
546
+ version: '1.0.0',
547
+ project: {
548
+ name: projectName,
549
+ displayName,
550
+ description,
551
+ repoPath,
552
+ worktreesPath,
553
+ ghOrgRepo,
554
+ framework: defaults.framework,
555
+ packageManager: defaults.packageManager,
556
+ buildCommand: `${defaults.packageManager} run build`,
557
+ typecheckCommand: `${defaults.packageManager} run typecheck`,
558
+ deployPlatform: 'none',
559
+ },
560
+ tier: packDefaults.tier ?? 0,
561
+ starterPack,
562
+ slots: packDefaults.slots ?? { feature: 3, bugfix: 1, devops: 0, analysis: false },
563
+ ports: {
564
+ main: 3000,
565
+ featureStart: 3001,
566
+ bugfixStart: 3011,
567
+ devopsStart: 3021,
568
+ analysis: 3099,
569
+ },
570
+ prefix,
571
+ shipEnvVar: `${prefix.toUpperCase()}_SHIP_AUTHORIZED`,
572
+ sessionPrefix: prefix.toUpperCase(),
573
+ coAuthor: 'Claude Opus 4.6',
574
+ memory: packDefaults.memory,
575
+ issues: packDefaults.issues,
576
+ notifications: packDefaults.notifications,
577
+ monitoring: packDefaults.monitoring,
578
+ email: packDefaults.email,
579
+ crons: packDefaults.crons,
580
+ daemon: packDefaults.daemon,
581
+ guardian: packDefaults.guardian,
582
+ };
583
+ // Always merge org defaults if they exist
584
+ if (orgConfig) {
585
+ config = mergeOrgDefaults(config, orgConfig);
586
+ }
587
+ config.automationScore = calculateAutomationScore(config);
588
+ // Render templates
589
+ console.log('\nRendering templates...');
590
+ const result = await renderAllTemplates(config);
591
+ const fileCount = Object.keys(result.files).length;
592
+ const globalCount = Object.keys(result.globalFiles).length;
593
+ // Grouped file preview
594
+ const categories = { Skills: [], Scripts: [], Config: [], Design: [], Other: [] };
595
+ for (const fp of Object.keys(result.files).sort()) {
596
+ if (fp.startsWith('.claude/commands/'))
597
+ categories.Skills.push(fp);
598
+ else if (fp.startsWith('scripts/'))
599
+ categories.Scripts.push(fp);
600
+ else if (fp.startsWith('src/components/') || fp.includes('globals.css') || fp.includes('tailwind'))
601
+ categories.Design.push(fp);
602
+ else if (fp.startsWith('src/') || fp.startsWith('.'))
603
+ categories.Other.push(fp);
604
+ else
605
+ categories.Config.push(fp);
606
+ }
607
+ const verbose = process.argv.includes('--verbose');
608
+ console.log(`\n${fileCount} project files will be generated:`);
609
+ for (const [cat, files] of Object.entries(categories)) {
610
+ if (files.length === 0)
611
+ continue;
612
+ if (verbose) {
613
+ console.log(` ${cat}:`);
614
+ for (const f of files)
615
+ console.log(` ${f}`);
616
+ }
617
+ else {
618
+ console.log(` ${cat}: ${files.length} file${files.length > 1 ? 's' : ''}`);
619
+ }
620
+ }
621
+ if (globalCount > 0)
622
+ console.log(` Global skills: ${globalCount} files -> ~/.claude/commands/`);
623
+ if (!verbose && fileCount > 8)
624
+ console.log(' (run with --verbose to see full list)');
625
+ if (result.warnings.length > 0) {
626
+ console.log(`\nWarnings:`);
627
+ for (const w of result.warnings) {
628
+ console.log(` ${w}`);
629
+ }
630
+ }
631
+ console.log(`\nAutomation score: ${config.automationScore}/100`);
632
+ console.log(`Starter pack: ${starterPack} (Tier ${config.tier})`);
633
+ const proceed = await confirm('\nWrite files to disk?');
634
+ if (!proceed) {
635
+ console.log('Aborted.');
636
+ closePrompts();
637
+ process.exit(0);
638
+ }
639
+ // Write config
640
+ const configDir = path.join(repoPath, '.traqr');
641
+ await fs.mkdir(configDir, { recursive: true });
642
+ await fs.writeFile(path.join(configDir, 'config.json'), JSON.stringify(config, null, 2), 'utf-8');
643
+ console.log(' .traqr/config.json');
644
+ // Write rendered files
645
+ const writeResult = await writeFiles(result.files, repoPath, { force: false });
646
+ // Write global skills to ~/.claude/commands/
647
+ const globalEntries = Object.entries(result.globalFiles);
648
+ if (globalEntries.length > 0) {
649
+ const home = process.env.HOME || '';
650
+ for (const [globalPath, content] of globalEntries) {
651
+ const absPath = globalPath.replace(/^~/, home);
652
+ await fs.mkdir(path.dirname(absPath), { recursive: true });
653
+ await fs.writeFile(absPath, content, 'utf-8');
654
+ }
655
+ console.log(` Global skills: ${globalEntries.length} files -> ~/.claude/commands/`);
656
+ }
657
+ console.log(`\nDone!`);
658
+ console.log(` Written: ${writeResult.written.length} files`);
659
+ if (writeResult.skipped.length > 0) {
660
+ console.log(` Skipped ${writeResult.skipped.length} existing files (use --force to overwrite):`);
661
+ for (const f of writeResult.skipped)
662
+ console.log(` ${f}`);
663
+ }
664
+ try {
665
+ registerProject(projectName, {
666
+ repoPath,
667
+ worktreesPath,
668
+ displayName,
669
+ aliasPrefix: prefix,
670
+ registeredAt: new Date().toISOString(),
671
+ });
672
+ console.log(` Registered in ~/.traqr/config.json`);
673
+ }
674
+ catch {
675
+ console.warn(' Warning: could not register project in ~/.traqr/config.json');
676
+ }
677
+ // Generate alias file
678
+ const home = process.env.HOME || '';
679
+ const { config: latestOrg } = loadOrgConfig();
680
+ const isPrimary = !latestOrg?.primaryProject || latestOrg.primaryProject === projectName;
681
+ writeAliasFile(config, { isPrimary });
682
+ console.log(` Alias file written to ~/.traqr/aliases/${projectName}.sh`);
683
+ // Generate MOTD
684
+ generateMotd();
685
+ console.log(' MOTD updated at ~/.traqr/motd.sh');
686
+ // Generate shell-init.sh (single entry point)
687
+ writeShellInit();
688
+ console.log(' Shell init written to ~/.traqr/shell-init.sh');
689
+ // Shell integration setup
690
+ const shell = process.env.SHELL || '/bin/zsh';
691
+ const rcFile = shell.includes('zsh') ? path.join(home, '.zshrc') : path.join(home, '.bashrc');
692
+ const rcContent = await fs.readFile(rcFile, 'utf-8').catch(() => '');
693
+ const hasShellInit = rcContent.includes('.traqr/shell-init.sh');
694
+ const hasLegacy = rcContent.includes('worktree-aliases.sh');
695
+ const hasOldAliases = rcContent.includes('.traqr/aliases') && !hasShellInit;
696
+ if (hasLegacy) {
697
+ // Legacy migration: offer to replace worktree-aliases.sh with shell-init.sh
698
+ const migrate = await confirm('Legacy shell config detected (worktree-aliases.sh). Replace with generated Traqr shell-init?');
699
+ if (migrate) {
700
+ // Comment out the legacy line and add the new one
701
+ const updatedRc = rcContent
702
+ .split('\n')
703
+ .map(line => (line.includes('worktree-aliases.sh') && !line.startsWith('#')) ? `# ${line} # replaced by Traqr shell-init` : line)
704
+ .join('\n');
705
+ // Also remove old .traqr/aliases sourcing if present (shell-init.sh handles it)
706
+ const cleanedRc = updatedRc
707
+ .split('\n')
708
+ .map(line => (line.includes('.traqr/aliases') && !line.startsWith('#')) ? `# ${line} # handled by shell-init.sh` : line)
709
+ .join('\n');
710
+ const finalRc = cleanedRc
711
+ .split('\n')
712
+ .map(line => (line.includes('.traqr/motd.sh') && !line.startsWith('#')) ? `# ${line} # handled by shell-init.sh` : line)
713
+ .join('\n');
714
+ const separator = finalRc.endsWith('\n') ? '' : '\n';
715
+ await fs.writeFile(rcFile, finalRc + separator + `\n# Traqr shell init (generated)\nsource ~/.traqr/shell-init.sh\n`, 'utf-8');
716
+ console.log(` Migrated ${path.basename(rcFile)}: legacy lines commented, shell-init.sh added`);
717
+ }
718
+ else {
719
+ info('Both may conflict. Run "traqr render" after removing the legacy line.');
720
+ }
721
+ }
722
+ else if (hasOldAliases && !hasShellInit) {
723
+ // Old-style .traqr/aliases sourcing — upgrade to shell-init.sh
724
+ const upgrade = await confirm('Upgrade shell config to use single shell-init.sh entry point?');
725
+ if (upgrade) {
726
+ const updatedRc = rcContent
727
+ .split('\n')
728
+ .map(line => {
729
+ if (line.includes('.traqr/aliases') && !line.startsWith('#'))
730
+ return `# ${line} # handled by shell-init.sh`;
731
+ if (line.includes('.traqr/motd.sh') && !line.startsWith('#'))
732
+ return `# ${line} # handled by shell-init.sh`;
733
+ return line;
734
+ })
735
+ .join('\n');
736
+ const sep = updatedRc.endsWith('\n') ? '' : '\n';
737
+ await fs.writeFile(rcFile, updatedRc + sep + `\n# Traqr shell init (generated)\nsource ~/.traqr/shell-init.sh\n`, 'utf-8');
738
+ console.log(` Upgraded ${path.basename(rcFile)} to use shell-init.sh`);
739
+ }
740
+ }
741
+ else if (!hasShellInit) {
742
+ const addInit = await confirm('Add Traqr shell-init to your shell?');
743
+ if (addInit) {
744
+ await fs.appendFile(rcFile, `\n# Traqr shell init (generated)\nsource ~/.traqr/shell-init.sh\n`);
745
+ console.log(` Added to ${path.basename(rcFile)}`);
746
+ }
747
+ else {
748
+ info('Add manually later:\n echo \'source ~/.traqr/shell-init.sh\' >> ' + path.basename(rcFile));
749
+ }
750
+ }
751
+ else {
752
+ console.log(' Shell init already configured.');
753
+ }
754
+ // Offer to create worktrees
755
+ const createWorktrees = await confirm('Create worktrees now?');
756
+ if (createWorktrees) {
757
+ try {
758
+ execSync(`bash "${path.join(repoPath, 'scripts', 'setup-worktrees.sh')}"`, { stdio: 'inherit' });
759
+ }
760
+ catch {
761
+ console.error(' Worktree setup had an error. Run manually:');
762
+ console.error(` bash "${path.join(repoPath, 'scripts', 'setup-worktrees.sh')}"`);
763
+ }
764
+ }
765
+ else {
766
+ info(`Create worktrees later:\n bash "${repoPath}/scripts/setup-worktrees.sh"`);
767
+ }
768
+ console.log('\nAll set! Reload your shell and try: z1 && c1');
769
+ closePrompts();
770
+ }
771
+ void run();
772
+ //# sourceMappingURL=init.js.map