@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.
- package/README.md +84 -0
- package/dist/bin/traqr.d.ts +20 -0
- package/dist/bin/traqr.d.ts.map +1 -0
- package/dist/bin/traqr.js +104 -0
- package/dist/bin/traqr.js.map +1 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +772 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/projects.d.ts +9 -0
- package/dist/commands/projects.d.ts.map +1 -0
- package/dist/commands/projects.js +78 -0
- package/dist/commands/projects.js.map +1 -0
- package/dist/commands/render.d.ts +12 -0
- package/dist/commands/render.d.ts.map +1 -0
- package/dist/commands/render.js +49 -0
- package/dist/commands/render.js.map +1 -0
- package/dist/commands/setup.d.ts +8 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +343 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/status.d.ts +8 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +46 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/checks.d.ts +8 -0
- package/dist/lib/checks.d.ts.map +1 -0
- package/dist/lib/checks.js +45 -0
- package/dist/lib/checks.js.map +1 -0
- package/dist/lib/prompts.d.ts +24 -0
- package/dist/lib/prompts.d.ts.map +1 -0
- package/dist/lib/prompts.js +76 -0
- package/dist/lib/prompts.js.map +1 -0
- package/dist/lib/writer.d.ts +22 -0
- package/dist/lib/writer.d.ts.map +1 -0
- package/dist/lib/writer.js +43 -0
- package/dist/lib/writer.js.map +1 -0
- 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
|