create-urateam 0.1.45 → 0.1.47
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/dist/__tests__/scaffold.test.js +8 -8
- package/dist/__tests__/scaffold.test.js.map +1 -1
- package/dist/index.d.ts +3 -141
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -822
- package/dist/index.js.map +1 -1
- package/dist/scaffold.d.ts +197 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +492 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/wizard.d.ts +18 -0
- package/dist/wizard.d.ts.map +1 -0
- package/dist/wizard.js +346 -0
- package/dist/wizard.js.map +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,491 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Tier → implicit feature set, mirroring packages/core/src/license.ts.
|
|
10
|
-
* The runtime grants Pro tier ALL Pro features regardless of whether the
|
|
11
|
-
* JWT carries an explicit `features` array — so the scaffolder must do
|
|
12
|
-
* the same expansion or it'll skip tier-gated prompts for licenses
|
|
13
|
-
* issued without `--features`.
|
|
14
|
-
*/
|
|
15
|
-
const PRO_FEATURES = [
|
|
16
|
-
"slack-interface",
|
|
17
|
-
"conflict-detection",
|
|
18
|
-
"deep-review",
|
|
19
|
-
"approval-workflows",
|
|
20
|
-
"multi-repo",
|
|
21
|
-
"stage-models",
|
|
22
|
-
"advanced-automerge",
|
|
23
|
-
];
|
|
24
|
-
const ENTERPRISE_FEATURES = [
|
|
25
|
-
...PRO_FEATURES,
|
|
26
|
-
"sso",
|
|
27
|
-
"audit-log",
|
|
28
|
-
"spend-caps",
|
|
29
|
-
"rbac",
|
|
30
|
-
"cost-dashboard",
|
|
31
|
-
"cost-roi",
|
|
32
|
-
"org-policy",
|
|
33
|
-
"pm-agent-governance",
|
|
34
|
-
];
|
|
35
|
-
/**
|
|
36
|
-
* Decode a urateam license JWT payload WITHOUT verifying the signature.
|
|
37
|
-
*
|
|
38
|
-
* The scaffolder doesn't ship the public key (would bloat the package and
|
|
39
|
-
* couple it to a specific signing-key generation), and a malformed JWT here
|
|
40
|
-
* just produces a wrong prompt flow which is recoverable by editing .env
|
|
41
|
-
* after the fact. Production verification happens at runtime in the agent
|
|
42
|
-
* via packages/core/src/license.ts against the embedded public key.
|
|
43
|
-
*
|
|
44
|
-
* If the JWT has no explicit `features` array, this expands by tier to
|
|
45
|
-
* match runtime semantics. An explicit (possibly trimmed) array in the
|
|
46
|
-
* JWT takes precedence — operators can ship Pro licenses with a subset
|
|
47
|
-
* of features and the scaffolder honors that.
|
|
48
|
-
*
|
|
49
|
-
* Returns null on any parse failure.
|
|
50
|
-
*/
|
|
51
|
-
export function decodeLicense(jwt) {
|
|
52
|
-
if (!jwt)
|
|
53
|
-
return null;
|
|
54
|
-
const segments = jwt.split(".");
|
|
55
|
-
if (segments.length !== 3)
|
|
56
|
-
return null;
|
|
57
|
-
try {
|
|
58
|
-
const payloadJson = Buffer.from(segments[1].replace(/-/g, "+").replace(/_/g, "/") +
|
|
59
|
-
"=".repeat((4 - (segments[1].length % 4)) % 4), "base64").toString("utf-8");
|
|
60
|
-
const payload = JSON.parse(payloadJson);
|
|
61
|
-
const tier = payload.tier === "pro" || payload.tier === "enterprise" ? payload.tier : "oss";
|
|
62
|
-
// Honor an explicit features array (operators can issue restricted licenses).
|
|
63
|
-
// Otherwise expand by tier to match the runtime's tier-implicit feature set.
|
|
64
|
-
let features;
|
|
65
|
-
if (Array.isArray(payload.features) && payload.features.length > 0) {
|
|
66
|
-
features = payload.features;
|
|
67
|
-
}
|
|
68
|
-
else if (tier === "pro") {
|
|
69
|
-
features = [...PRO_FEATURES];
|
|
70
|
-
}
|
|
71
|
-
else if (tier === "enterprise") {
|
|
72
|
-
features = [...ENTERPRISE_FEATURES];
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
features = [];
|
|
76
|
-
}
|
|
77
|
-
return {
|
|
78
|
-
tier,
|
|
79
|
-
features,
|
|
80
|
-
customerId: payload.sub,
|
|
81
|
-
expiresAt: payload.exp ? new Date(payload.exp * 1000) : undefined,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Build the `.env` content for a scaffolded urateam sidecar.
|
|
90
|
-
*
|
|
91
|
-
* Pure function: takes already-resolved values, returns the file string.
|
|
92
|
-
* Auto-generation of missing secrets is the caller's responsibility (see
|
|
93
|
-
* resolveSecrets below) so this stays trivially testable.
|
|
94
|
-
*/
|
|
95
|
-
function buildEnv(options) {
|
|
96
|
-
const lines = [];
|
|
97
|
-
const push = (line) => lines.push(line);
|
|
98
|
-
const blank = () => lines.push("");
|
|
99
|
-
push("# === Linear (REQUIRED) ===");
|
|
100
|
-
push(`LINEAR_API_KEY=${options.linearApiKey}`);
|
|
101
|
-
// Comment out when blank so the runtime's "is required" check fails fast
|
|
102
|
-
// with the right message instead of silently treating "" as a valid secret.
|
|
103
|
-
if (options.linearWebhookSecret) {
|
|
104
|
-
push(`LINEAR_WEBHOOK_SECRET=${options.linearWebhookSecret}`);
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
push("# LINEAR_WEBHOOK_SECRET= # paste from Linear's webhook config UI");
|
|
108
|
-
}
|
|
109
|
-
push(`LINEAR_TEAM_ID=${options.linearTeamId}`);
|
|
110
|
-
blank();
|
|
111
|
-
push("# === Repository (REQUIRED) ===");
|
|
112
|
-
push(`REPO_URL=${options.repoUrl}`);
|
|
113
|
-
push(`REPO_DEFAULT_BRANCH=${options.defaultBranch}`);
|
|
114
|
-
// REPO_TEAM_ID is the map key for repoConfigs[teamId] — defaulted to
|
|
115
|
-
// LINEAR_TEAM_ID for single-team / single-repo setups (the env-var path).
|
|
116
|
-
// Multi-repo Pro deployments use repos.config.ts instead and can ignore
|
|
117
|
-
// this var. See packages/cli/src/commands/start.ts:50.
|
|
118
|
-
push(`REPO_TEAM_ID=${options.linearTeamId}`);
|
|
119
|
-
blank();
|
|
120
|
-
push("# === Anthropic auth ===");
|
|
121
|
-
if (options.anthropicApiKey) {
|
|
122
|
-
push(`ANTHROPIC_API_KEY=${options.anthropicApiKey}`);
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
push("# ANTHROPIC_API_KEY= # blank → run `docker compose exec agent claude login` after deploy");
|
|
126
|
-
}
|
|
127
|
-
blank();
|
|
128
|
-
push("# === Pro license (blank = OSS tier) ===");
|
|
129
|
-
push(`URATEAM_LICENSE_KEY=${options.licenseKey}`);
|
|
130
|
-
blank();
|
|
131
|
-
push("# === GitHub auth (REQUIRED for PR creation) ===");
|
|
132
|
-
push("# Either run `docker compose exec agent gh auth login` after deploy,");
|
|
133
|
-
push("# or set the GitHub App trio below:");
|
|
134
|
-
push("# GITHUB_APP_ID=");
|
|
135
|
-
push("# GITHUB_PRIVATE_KEY_PATH=/run/gh-app.pem");
|
|
136
|
-
push("# GITHUB_INSTALLATION_ID=");
|
|
137
|
-
// GITHUB_WEBHOOK_SECRET is shared with GitHub's webhook config. Auto-genned
|
|
138
|
-
// by default (operator pastes the value INTO GitHub when creating the
|
|
139
|
-
// webhook). Blank means PR-comment re-trigger feature stays disabled.
|
|
140
|
-
if (options.githubWebhookSecret) {
|
|
141
|
-
push(`GITHUB_WEBHOOK_SECRET=${options.githubWebhookSecret}`);
|
|
142
|
-
}
|
|
143
|
-
else {
|
|
144
|
-
push("# GITHUB_WEBHOOK_SECRET= # paste here AND into GitHub webhook config to enable PR-comment re-runs");
|
|
145
|
-
}
|
|
146
|
-
blank();
|
|
147
|
-
push("# === Database ===");
|
|
148
|
-
push(`POSTGRES_PASSWORD=${options.postgresPassword}`);
|
|
149
|
-
blank();
|
|
150
|
-
if (options.deployMode === "production") {
|
|
151
|
-
push("# === Domain (production deploy) ===");
|
|
152
|
-
push(`DOMAIN=${options.domain}`);
|
|
153
|
-
push(`CADDY_EMAIL=${options.caddyEmail}`);
|
|
154
|
-
blank();
|
|
155
|
-
}
|
|
156
|
-
push("# === Dashboard auth ===");
|
|
157
|
-
push(`DASHBOARD_USER=${options.dashboardUser}`);
|
|
158
|
-
push(`DASHBOARD_PASSWORD=${options.dashboardPassword}`);
|
|
159
|
-
if (options.dashboardBasePath) {
|
|
160
|
-
push(`DASHBOARD_BASE_PATH=${options.dashboardBasePath}`);
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
push("# DASHBOARD_BASE_PATH= # set with leading slash, no trailing, when behind a path prefix");
|
|
164
|
-
}
|
|
165
|
-
blank();
|
|
166
|
-
push("# === Concurrency ===");
|
|
167
|
-
push(`MAX_CONCURRENT_RUNS=${options.maxConcurrentRuns}`);
|
|
168
|
-
blank();
|
|
169
|
-
// AGENT_BYPASS_PERMISSIONS — Claude Code permission-mode override. Runtime
|
|
170
|
-
// logic at packages/core/src/executor/permissions.ts:
|
|
171
|
-
// - root user (UID 0): all permission flags ignored (Claude Code refuses)
|
|
172
|
-
// - else, env var unset: implement/reproduce=acceptEdits, test/review=default
|
|
173
|
-
// - else, env var =true: bypassPermissions for all stages
|
|
174
|
-
// The hardened compose template runs the container as root, so this var is
|
|
175
|
-
// a no-op for production VPS deploys. For local `pnpm dev` (non-root), the
|
|
176
|
-
// test/review stages would hang on interactive prompts without this — local
|
|
177
|
-
// mode therefore defaults to true.
|
|
178
|
-
push("# === Agent permissions (Claude Code) ===");
|
|
179
|
-
if (options.deployMode === "local") {
|
|
180
|
-
push("AGENT_BYPASS_PERMISSIONS=true");
|
|
181
|
-
}
|
|
182
|
-
else {
|
|
183
|
-
push("# AGENT_BYPASS_PERMISSIONS=true # no-op in root containers; uncomment if running container as a non-root user");
|
|
184
|
-
}
|
|
185
|
-
blank();
|
|
186
|
-
if (options.pmAgent) {
|
|
187
|
-
push("# === PM Agent (Pro: slack-interface) ===");
|
|
188
|
-
push("PM_AGENT_ENABLED=true");
|
|
189
|
-
push(`PM_AGENT_TEAM_IDS=${options.pmAgent.teamIds}`);
|
|
190
|
-
push(`PM_AGENT_SLACK_CHANNEL_ID=${options.pmAgent.slackChannelId}`);
|
|
191
|
-
push(`PM_AGENT_DAILY_TOKEN_BUDGET=${options.pmAgent.dailyTokenBudget ?? 5_000_000}`);
|
|
192
|
-
push(`PM_AGENT_MAX_IN_FLIGHT=3`);
|
|
193
|
-
push(`SLACK_BOT_TOKEN=${options.pmAgent.slackBotToken}`);
|
|
194
|
-
push(`SLACK_SIGNING_SECRET=${options.pmAgent.slackSigningSecret}`);
|
|
195
|
-
blank();
|
|
196
|
-
}
|
|
197
|
-
else {
|
|
198
|
-
push("# === PM Agent (Pro: slack-interface) — fill in to enable ===");
|
|
199
|
-
push("# PM_AGENT_ENABLED=true");
|
|
200
|
-
push("# PM_AGENT_TEAM_IDS=");
|
|
201
|
-
push("# PM_AGENT_SLACK_CHANNEL_ID=");
|
|
202
|
-
push("# PM_AGENT_DAILY_TOKEN_BUDGET=5000000");
|
|
203
|
-
push("# PM_AGENT_MAX_IN_FLIGHT=3");
|
|
204
|
-
push("# SLACK_BOT_TOKEN=");
|
|
205
|
-
push("# SLACK_SIGNING_SECRET=");
|
|
206
|
-
blank();
|
|
207
|
-
}
|
|
208
|
-
push("# === GitHub PR-comment re-trigger (optional, gated by GITHUB_WEBHOOK_SECRET above) ===");
|
|
209
|
-
if (options.githubFeedback) {
|
|
210
|
-
if (options.githubFeedback.autoTrigger === false) {
|
|
211
|
-
push("GITHUB_FEEDBACK_AUTO_TRIGGER=false");
|
|
212
|
-
}
|
|
213
|
-
else {
|
|
214
|
-
push("# GITHUB_FEEDBACK_AUTO_TRIGGER=true # default — fire on any qualifying review/comment");
|
|
215
|
-
}
|
|
216
|
-
if (options.githubFeedback.triggerKeyword) {
|
|
217
|
-
push(`GITHUB_FEEDBACK_TRIGGER_KEYWORD=${options.githubFeedback.triggerKeyword}`);
|
|
218
|
-
}
|
|
219
|
-
else {
|
|
220
|
-
push("# GITHUB_FEEDBACK_TRIGGER_KEYWORD= # require this keyword in the comment to fire");
|
|
221
|
-
}
|
|
222
|
-
if (options.githubFeedback.allowedReviewers) {
|
|
223
|
-
push(`GITHUB_FEEDBACK_ALLOWED_REVIEWERS=${options.githubFeedback.allowedReviewers}`);
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
push("# GITHUB_FEEDBACK_ALLOWED_REVIEWERS= # comma-separated GitHub usernames");
|
|
227
|
-
}
|
|
228
|
-
if (options.githubFeedback.botLogins) {
|
|
229
|
-
push(`GITHUB_FEEDBACK_BOT_LOGINS=${options.githubFeedback.botLogins}`);
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
push("# GITHUB_FEEDBACK_BOT_LOGINS= # comma-separated bot logins, e.g. github-actions[bot]");
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
else {
|
|
236
|
-
push("# GITHUB_FEEDBACK_AUTO_TRIGGER=true # default — fire on any qualifying review/comment");
|
|
237
|
-
push("# GITHUB_FEEDBACK_TRIGGER_KEYWORD= # require this keyword in the comment to fire");
|
|
238
|
-
push("# GITHUB_FEEDBACK_ALLOWED_REVIEWERS= # comma-separated GitHub usernames");
|
|
239
|
-
push("# GITHUB_FEEDBACK_BOT_LOGINS= # comma-separated bot logins, e.g. github-actions[bot]");
|
|
240
|
-
}
|
|
241
|
-
blank();
|
|
242
|
-
push("# === Pipeline notifications (no Pro license needed) ===");
|
|
243
|
-
if (options.slackWebhookUrl) {
|
|
244
|
-
push(`SLACK_WEBHOOK_URL=${options.slackWebhookUrl}`);
|
|
245
|
-
}
|
|
246
|
-
else {
|
|
247
|
-
push("# SLACK_WEBHOOK_URL= # Slack incoming-webhook for pipeline event posts");
|
|
248
|
-
}
|
|
249
|
-
if (options.discordWebhookUrl) {
|
|
250
|
-
push(`DISCORD_WEBHOOK_URL=${options.discordWebhookUrl}`);
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
push("# DISCORD_WEBHOOK_URL= # Discord webhook for pipeline event posts");
|
|
254
|
-
}
|
|
255
|
-
blank();
|
|
256
|
-
push("# === Per-stage agent budget overrides (urateam#38) ===");
|
|
257
|
-
if (options.agentProfiles && Object.keys(options.agentProfiles).length > 0) {
|
|
258
|
-
// Bare (unquoted) JSON. Surrounding single-quotes break Docker Compose's
|
|
259
|
-
// env_file parser — same gotcha as the env_file no-interpolation issue.
|
|
260
|
-
// Both Compose and Node 22 process.loadEnvFile read everything after `=`
|
|
261
|
-
// to EOL and JSON has no whitespace / `=` outside string literals.
|
|
262
|
-
push(`URATEAM_AGENT_PROFILES=${JSON.stringify(options.agentProfiles)}`);
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
push('# URATEAM_AGENT_PROFILES={"test":{"maxTurns":50,"maxInputTokens":80000}}');
|
|
266
|
-
}
|
|
267
|
-
blank();
|
|
268
|
-
if (options.openrouterApiKey && options.reviewModels && options.reviewModels.length > 0) {
|
|
269
|
-
push("");
|
|
270
|
-
push("# OpenRouter multi-model review fanout (BEC-134)");
|
|
271
|
-
push(`OPENROUTER_API_KEY=${options.openrouterApiKey}`);
|
|
272
|
-
push(`REVIEW_MODELS=${options.reviewModels.join(",")}`);
|
|
273
|
-
}
|
|
274
|
-
push("# === Optional ===");
|
|
275
|
-
push("# LOG_LEVEL=info");
|
|
276
|
-
push("");
|
|
277
|
-
push("# Additional tunables (worktree TTL, repo clone dir, agent run dir, etc.)");
|
|
278
|
-
push("# documented in .env.example next to this file. Keep that file as the");
|
|
279
|
-
push("# canonical reference; this .env is generated from prompts.");
|
|
280
|
-
return lines.join("\n") + "\n";
|
|
281
|
-
}
|
|
282
|
-
function resolveSecrets(options) {
|
|
283
|
-
const autoGen = options.autoGenSecrets ?? true;
|
|
284
|
-
const generated = {};
|
|
285
|
-
let dashboardPassword = options.dashboardPassword ?? "";
|
|
286
|
-
let postgresPassword = options.postgresPassword ?? "";
|
|
287
|
-
let githubWebhookSecret = options.githubWebhookSecret ?? "";
|
|
288
|
-
if (autoGen) {
|
|
289
|
-
// base64url avoids `+`, `/`, `=` which can trip strict env-file parsers
|
|
290
|
-
// and a few HMAC validators in the wild.
|
|
291
|
-
if (!dashboardPassword) {
|
|
292
|
-
dashboardPassword = randomBytes(18).toString("base64url");
|
|
293
|
-
generated.dashboardPassword = dashboardPassword;
|
|
294
|
-
}
|
|
295
|
-
if (!postgresPassword) {
|
|
296
|
-
postgresPassword = randomBytes(24).toString("base64url");
|
|
297
|
-
generated.postgresPassword = postgresPassword;
|
|
298
|
-
}
|
|
299
|
-
if (!githubWebhookSecret) {
|
|
300
|
-
// hex (not base64url) for compatibility with the broadest set of HMAC
|
|
301
|
-
// validators that operators paste this into. Operator pastes the same
|
|
302
|
-
// value into GitHub's webhook config so signatures match.
|
|
303
|
-
githubWebhookSecret = randomBytes(32).toString("hex");
|
|
304
|
-
generated.githubWebhookSecret = githubWebhookSecret;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
return { dashboardPassword, postgresPassword, githubWebhookSecret, generated };
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* Scaffold a urateam sidecar into a project directory.
|
|
311
|
-
*
|
|
312
|
-
* Creates:
|
|
313
|
-
* - <projectDir>/.urateam/ — isolated urateam config + deps
|
|
314
|
-
* - package.json — depends on @urateam/cli
|
|
315
|
-
* - .env — Linear keys, webhook secret, etc.
|
|
316
|
-
* - .env.example
|
|
317
|
-
* - Dockerfile
|
|
318
|
-
* - docker-compose.yml
|
|
319
|
-
* - Caddyfile — reverse proxy + auto-HTTPS
|
|
320
|
-
* - README.md — how to run the sidecar
|
|
321
|
-
* - <projectDir>/CLAUDE.md — project conventions (only if absent)
|
|
322
|
-
* - <projectDir>/README.md — project readme (only if absent)
|
|
323
|
-
* - <projectDir>/.gitignore — ensures .urateam/.env is ignored
|
|
324
|
-
*
|
|
325
|
-
* The project root `package.json` is NOT touched. Existing `.env` and
|
|
326
|
-
* `package.json` inside `.urateam/` are preserved on re-run.
|
|
327
|
-
*/
|
|
328
|
-
export function scaffold(options) {
|
|
329
|
-
const { projectDir, projectName, linearApiKey, linearTeamId, repoUrl, defaultBranch } = options;
|
|
330
|
-
mkdirSync(projectDir, { recursive: true });
|
|
331
|
-
let templateDir = join(__dirname, "..", "template");
|
|
332
|
-
if (!statSync(templateDir, { throwIfNoEntry: false })?.isDirectory()) {
|
|
333
|
-
templateDir = join(__dirname, "..", "..", "template");
|
|
334
|
-
}
|
|
335
|
-
const urateamDir = join(projectDir, ".urateam");
|
|
336
|
-
mkdirSync(urateamDir, { recursive: true });
|
|
337
|
-
const urateamTemplateDir = join(templateDir, ".urateam");
|
|
338
|
-
for (const entry of readdirSync(urateamTemplateDir)) {
|
|
339
|
-
if (entry === ".env")
|
|
340
|
-
continue;
|
|
341
|
-
const src = join(urateamTemplateDir, entry);
|
|
342
|
-
const dest = join(urateamDir, entry);
|
|
343
|
-
cpSync(src, dest, { recursive: true, force: true });
|
|
344
|
-
}
|
|
345
|
-
const pkgPath = join(urateamDir, "package.json");
|
|
346
|
-
if (!existsSync(pkgPath)) {
|
|
347
|
-
const pkg = {
|
|
348
|
-
name: `${projectName}-urateam`,
|
|
349
|
-
private: true,
|
|
350
|
-
type: "module",
|
|
351
|
-
scripts: {
|
|
352
|
-
dev: "ura dev",
|
|
353
|
-
start: "ura start",
|
|
354
|
-
},
|
|
355
|
-
dependencies: {
|
|
356
|
-
"@urateam/cli": "^0.1.4",
|
|
357
|
-
},
|
|
358
|
-
};
|
|
359
|
-
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
360
|
-
}
|
|
361
|
-
const license = decodeLicense(options.licenseKey);
|
|
362
|
-
const todos = [];
|
|
363
|
-
const { dashboardPassword, postgresPassword, githubWebhookSecret, generated } = resolveSecrets(options);
|
|
364
|
-
// --- Compose .env from inputs + resolved secrets ---
|
|
365
|
-
const envPath = join(urateamDir, ".env");
|
|
366
|
-
if (!existsSync(envPath)) {
|
|
367
|
-
const linearWebhookSecret = options.linearWebhookSecret ?? "";
|
|
368
|
-
if (!linearWebhookSecret) {
|
|
369
|
-
todos.push("LINEAR_WEBHOOK_SECRET — paste from Linear's webhook config UI " +
|
|
370
|
-
"(Workspace settings → API → Webhooks).");
|
|
371
|
-
}
|
|
372
|
-
if (!options.licenseKey) {
|
|
373
|
-
todos.push("URATEAM_LICENSE_KEY — Pro features (PM agent, Slack interface, multi-repo, " +
|
|
374
|
-
"deep-review, etc.) stay disabled until you set this.");
|
|
375
|
-
}
|
|
376
|
-
if (license?.tier &&
|
|
377
|
-
license.tier !== "oss" &&
|
|
378
|
-
license.features.includes("slack-interface") &&
|
|
379
|
-
!options.pmAgent) {
|
|
380
|
-
todos.push("PM_AGENT_* — your license includes `slack-interface`. Fill in the " +
|
|
381
|
-
"PM_AGENT_* + SLACK_* lines in .env to enable it.");
|
|
382
|
-
}
|
|
383
|
-
if (!options.anthropicApiKey) {
|
|
384
|
-
todos.push("Anthropic auth — run `docker compose exec agent claude login` after the stack is up " +
|
|
385
|
-
"(or set ANTHROPIC_API_KEY in .env for headless API auth).");
|
|
386
|
-
}
|
|
387
|
-
todos.push("GitHub auth — run `docker compose exec agent gh auth login` after the stack is up " +
|
|
388
|
-
"(or set the GITHUB_APP_* trio in .env for app-based auth).");
|
|
389
|
-
if (options.deployMode === "production" && !options.domain) {
|
|
390
|
-
todos.push("DOMAIN — set in .env before running `docker compose up`.");
|
|
391
|
-
}
|
|
392
|
-
if (options.deployMode === "production" && !options.caddyEmail) {
|
|
393
|
-
todos.push("CADDY_EMAIL — recommended for Let's Encrypt expiry warnings.");
|
|
394
|
-
}
|
|
395
|
-
if (!options.autoGenSecrets) {
|
|
396
|
-
if (!options.dashboardPassword)
|
|
397
|
-
todos.push("DASHBOARD_PASSWORD — fill in .env.");
|
|
398
|
-
if (!options.postgresPassword)
|
|
399
|
-
todos.push("POSTGRES_PASSWORD — fill in .env.");
|
|
400
|
-
}
|
|
401
|
-
// GITHUB_WEBHOOK_SECRET is optional (only needed for PR-comment re-runs).
|
|
402
|
-
// No TODO when blank — operator who didn't paste it in Stage 6 doesn't want
|
|
403
|
-
// the feature. But if they DID set GITHUB_FEEDBACK_*, the missing secret
|
|
404
|
-
// makes those values dead — flag that.
|
|
405
|
-
if (options.githubFeedback && !githubWebhookSecret) {
|
|
406
|
-
todos.push("GITHUB_WEBHOOK_SECRET — required for GITHUB_FEEDBACK_* to take effect; " +
|
|
407
|
-
"feedback values without the secret are silently ignored at runtime.");
|
|
408
|
-
}
|
|
409
|
-
const envContent = buildEnv({
|
|
410
|
-
linearApiKey,
|
|
411
|
-
linearTeamId,
|
|
412
|
-
repoUrl,
|
|
413
|
-
defaultBranch,
|
|
414
|
-
deployMode: options.deployMode ?? "local",
|
|
415
|
-
linearWebhookSecret,
|
|
416
|
-
domain: options.domain ?? "",
|
|
417
|
-
caddyEmail: options.caddyEmail ?? "",
|
|
418
|
-
anthropicApiKey: options.anthropicApiKey ?? "",
|
|
419
|
-
licenseKey: options.licenseKey ?? "",
|
|
420
|
-
dashboardUser: options.dashboardUser ?? "admin",
|
|
421
|
-
dashboardPassword,
|
|
422
|
-
dashboardBasePath: options.dashboardBasePath ?? "",
|
|
423
|
-
postgresPassword,
|
|
424
|
-
githubWebhookSecret,
|
|
425
|
-
maxConcurrentRuns: options.maxConcurrentRuns ?? 3,
|
|
426
|
-
pmAgent: options.pmAgent,
|
|
427
|
-
githubFeedback: options.githubFeedback,
|
|
428
|
-
slackWebhookUrl: options.slackWebhookUrl ?? "",
|
|
429
|
-
discordWebhookUrl: options.discordWebhookUrl ?? "",
|
|
430
|
-
agentProfiles: options.agentProfiles,
|
|
431
|
-
openrouterApiKey: options.openrouterApiKey ?? "",
|
|
432
|
-
reviewModels: options.reviewModels ?? [],
|
|
433
|
-
});
|
|
434
|
-
writeFileSync(envPath, envContent);
|
|
435
|
-
}
|
|
436
|
-
// --- Project root files: copy only if absent ---
|
|
437
|
-
const rootFilesWithPlaceholder = ["CLAUDE.md", "README.md"];
|
|
438
|
-
for (const file of rootFilesWithPlaceholder) {
|
|
439
|
-
const dest = join(projectDir, file);
|
|
440
|
-
if (existsSync(dest))
|
|
441
|
-
continue;
|
|
442
|
-
const src = join(templateDir, file);
|
|
443
|
-
if (!existsSync(src))
|
|
444
|
-
continue;
|
|
445
|
-
const content = readFileSync(src, "utf-8");
|
|
446
|
-
writeFileSync(dest, content.replace(/\{\{PROJECT_NAME\}\}/g, projectName));
|
|
447
|
-
}
|
|
448
|
-
const gitignorePath = join(projectDir, ".gitignore");
|
|
449
|
-
if (!existsSync(gitignorePath)) {
|
|
450
|
-
writeFileSync(gitignorePath, URATEAM_GITIGNORE);
|
|
451
|
-
}
|
|
452
|
-
else {
|
|
453
|
-
const existing = readFileSync(gitignorePath, "utf-8");
|
|
454
|
-
const hasBareEntry = existing
|
|
455
|
-
.split(/\r?\n/)
|
|
456
|
-
.some((line) => line.trim() === ".urateam/.env");
|
|
457
|
-
if (!hasBareEntry) {
|
|
458
|
-
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
459
|
-
appendFileSync(gitignorePath, separator + URATEAM_GITIGNORE);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
return { urateamDir, license, generatedSecrets: generated, todos };
|
|
463
|
-
}
|
|
464
|
-
/**
|
|
465
|
-
* Normalize a user-typed dashboard base path: strip trailing slashes, ensure
|
|
466
|
-
* leading slash, treat blank as undefined. Eliminates the "I typed /ateam/
|
|
467
|
-
* and now the dashboard 404s" footgun.
|
|
468
|
-
*/
|
|
469
|
-
export function normalizeBasePath(input) {
|
|
470
|
-
if (!input)
|
|
471
|
-
return undefined;
|
|
472
|
-
const trimmed = input.trim();
|
|
473
|
-
if (!trimmed)
|
|
474
|
-
return undefined;
|
|
475
|
-
const noTrailingSlash = trimmed.replace(/\/+$/, "");
|
|
476
|
-
if (!noTrailingSlash)
|
|
477
|
-
return undefined; // operator typed only `/` or `///`
|
|
478
|
-
return noTrailingSlash.startsWith("/") ? noTrailingSlash : `/${noTrailingSlash}`;
|
|
479
|
-
}
|
|
480
|
-
const URATEAM_GITIGNORE = `# urateam sidecar
|
|
481
|
-
.urateam/.env
|
|
482
|
-
.urateam/.env.*
|
|
483
|
-
!.urateam/.env.example
|
|
484
|
-
.urateam/node_modules/
|
|
485
|
-
.urateam/dist/
|
|
486
|
-
.urateam/pnpm-lock.yaml
|
|
487
|
-
`;
|
|
488
|
-
// CLI entrypoint — only runs when executed directly (not when imported for testing)
|
|
2
|
+
// Re-export all public types and functions from scaffold.ts so that
|
|
3
|
+
// existing code importing from @create-urateam (or ../index.js in tests)
|
|
4
|
+
// continues to work without changes.
|
|
5
|
+
import { scaffold, normalizeBasePath, decodeLicense, buildEnv, resolveSecrets, } from "./scaffold.js";
|
|
6
|
+
import { runWizard } from "./wizard.js";
|
|
7
|
+
export { scaffold, normalizeBasePath, decodeLicense, buildEnv, resolveSecrets };
|
|
489
8
|
async function main() {
|
|
490
9
|
const arg = process.argv[2] ?? ".";
|
|
491
10
|
if (arg === "--help" || arg === "-h") {
|
|
@@ -495,327 +14,13 @@ async function main() {
|
|
|
495
14
|
console.log(" create-urateam my-project # creates new directory and adds .urateam/ inside");
|
|
496
15
|
process.exit(0);
|
|
497
16
|
}
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
// scaffolder already preserves .env on re-run; without this, the prompts
|
|
502
|
-
// are pure UX waste.
|
|
503
|
-
const projectDirEarly = arg === "." ? process.cwd() : join(process.cwd(), arg);
|
|
504
|
-
const existingEnv = join(projectDirEarly, ".urateam", ".env");
|
|
505
|
-
if (existsSync(existingEnv)) {
|
|
506
|
-
console.log(`\n Existing .env detected at ${existingEnv}.\n` +
|
|
507
|
-
" Re-running create-urateam will refresh template files (Dockerfile, docker-compose.yml,\n" +
|
|
508
|
-
" Caddyfile, etc.) but will NOT touch your .env. To re-prompt for secrets, delete .env\n" +
|
|
509
|
-
" first and re-run. Continuing with template-refresh only…\n");
|
|
510
|
-
// Use minimal stub options — scaffold() needs them but won't write .env.
|
|
511
|
-
scaffold({
|
|
512
|
-
projectDir: projectDirEarly,
|
|
513
|
-
projectName: arg === "." ? basename(projectDirEarly) || "my-project" : arg,
|
|
514
|
-
linearApiKey: "",
|
|
515
|
-
linearTeamId: "",
|
|
516
|
-
repoUrl: "",
|
|
517
|
-
defaultBranch: "main",
|
|
518
|
-
});
|
|
519
|
-
console.log(` ✓ Template files refreshed in ${projectDirEarly}/.urateam\n`);
|
|
17
|
+
const wizardResult = await runWizard(arg);
|
|
18
|
+
if (wizardResult === null) {
|
|
19
|
+
// Early-exit: existing .env detected; template files were refreshed internally.
|
|
520
20
|
return;
|
|
521
21
|
}
|
|
522
|
-
|
|
523
|
-
const
|
|
524
|
-
{ type: "text", name: "linearApiKey", message: "Linear API key:" },
|
|
525
|
-
{ type: "text", name: "linearTeamId", message: "Linear team ID:" },
|
|
526
|
-
{ type: "text", name: "repoUrl", message: "Repo URL (GitHub/GitLab):" },
|
|
527
|
-
{ type: "text", name: "defaultBranch", message: "Default branch:", initial: "main" },
|
|
528
|
-
]);
|
|
529
|
-
if (!stage1.linearApiKey || !stage1.repoUrl) {
|
|
530
|
-
console.error("Cancelled.");
|
|
531
|
-
process.exit(1);
|
|
532
|
-
}
|
|
533
|
-
// --- Stage 2: deploy mode + Linear webhook secret (hidden input) ---
|
|
534
|
-
const stage2 = await prompts([
|
|
535
|
-
{
|
|
536
|
-
type: "select",
|
|
537
|
-
name: "deployMode",
|
|
538
|
-
message: "Deploy target:",
|
|
539
|
-
choices: [
|
|
540
|
-
{ title: "local (laptop / dev — pnpm dev)", value: "local" },
|
|
541
|
-
{ title: "production (VPS / docker compose)", value: "production" },
|
|
542
|
-
],
|
|
543
|
-
},
|
|
544
|
-
{
|
|
545
|
-
// password type masks input so the secret doesn't land in scrollback / shell history
|
|
546
|
-
type: "password",
|
|
547
|
-
name: "linearWebhookSecret",
|
|
548
|
-
message: "LINEAR_WEBHOOK_SECRET (paste from Linear webhook config; leave blank to fill in later):",
|
|
549
|
-
},
|
|
550
|
-
]);
|
|
551
|
-
// --- Stage 3: production-only details (domain / caddy email / dashboard base path) ---
|
|
552
|
-
const stage3 = stage2.deployMode === "production"
|
|
553
|
-
? await prompts([
|
|
554
|
-
{ type: "text", name: "domain", message: "Public domain (e.g. urateam.example.com):" },
|
|
555
|
-
{ type: "text", name: "caddyEmail", message: "Email for Let's Encrypt:" },
|
|
556
|
-
{
|
|
557
|
-
type: "text",
|
|
558
|
-
name: "dashboardBasePath",
|
|
559
|
-
message: "DASHBOARD_BASE_PATH (leading slash, no trailing — leave blank if dashboard is at root):",
|
|
560
|
-
},
|
|
561
|
-
])
|
|
562
|
-
: { domain: undefined, caddyEmail: undefined, dashboardBasePath: undefined };
|
|
563
|
-
// --- Stage 4: Anthropic auth choice ---
|
|
564
|
-
const stage4 = await prompts([
|
|
565
|
-
{
|
|
566
|
-
type: "select",
|
|
567
|
-
name: "anthropicAuth",
|
|
568
|
-
message: "Anthropic auth method:",
|
|
569
|
-
choices: [
|
|
570
|
-
{ title: "Claude Code CLI (`claude login` after deploy)", value: "cli" },
|
|
571
|
-
{ title: "API key (set ANTHROPIC_API_KEY now)", value: "apiKey" },
|
|
572
|
-
],
|
|
573
|
-
},
|
|
574
|
-
{
|
|
575
|
-
type: (prev) => (prev === "apiKey" ? "password" : null),
|
|
576
|
-
name: "anthropicApiKey",
|
|
577
|
-
message: "ANTHROPIC_API_KEY:",
|
|
578
|
-
},
|
|
579
|
-
]);
|
|
580
|
-
// --- Stage 5: license + tier-gated PM agent setup ---
|
|
581
|
-
const stage5 = await prompts([
|
|
582
|
-
{
|
|
583
|
-
// password type masks the JWT so it doesn't echo to scrollback / shell history.
|
|
584
|
-
// Decoded license summary printed below intentionally only shows tier + features,
|
|
585
|
-
// not the full JWT, so paste-into-terminal stays opaque.
|
|
586
|
-
type: "password",
|
|
587
|
-
name: "licenseKey",
|
|
588
|
-
message: "URATEAM_LICENSE_KEY (leave blank for OSS):",
|
|
589
|
-
},
|
|
590
|
-
]);
|
|
591
|
-
const license = decodeLicense(stage5.licenseKey);
|
|
592
|
-
if (license) {
|
|
593
|
-
console.log(`\n License decoded: tier=${license.tier}, features=[${license.features.join(", ")}]`);
|
|
594
|
-
if (license.expiresAt && license.expiresAt.getTime() < Date.now()) {
|
|
595
|
-
console.warn(` ⚠ License expired at ${license.expiresAt.toISOString()} — runtime will reject ` +
|
|
596
|
-
"it and fall back to OSS tier. Renew before deploying.");
|
|
597
|
-
}
|
|
598
|
-
console.log("");
|
|
599
|
-
}
|
|
600
|
-
let pmAgent;
|
|
601
|
-
if (license &&
|
|
602
|
-
license.tier !== "oss" &&
|
|
603
|
-
license.features.includes("slack-interface")) {
|
|
604
|
-
console.log("\n Your license includes the `slack-interface` feature (PM agent + Slack /pm slash commands).\n" +
|
|
605
|
-
" You can configure it now if you have your Slack app credentials handy, or skip and add\n" +
|
|
606
|
-
" the PM_AGENT_* + SLACK_* lines to .env later. Setup walkthrough:\n" +
|
|
607
|
-
" https://github.com/JonB32/urateam/blob/main/docs/slack-setup.md\n");
|
|
608
|
-
const setupNow = await prompts({
|
|
609
|
-
type: "confirm",
|
|
610
|
-
name: "setup",
|
|
611
|
-
message: "Set up PM agent + Slack interface now?",
|
|
612
|
-
initial: false,
|
|
613
|
-
});
|
|
614
|
-
if (setupNow.setup) {
|
|
615
|
-
const pmPrompts = await prompts([
|
|
616
|
-
{ type: "password", name: "slackBotToken", message: "SLACK_BOT_TOKEN (xoxb-...):" },
|
|
617
|
-
{ type: "password", name: "slackSigningSecret", message: "SLACK_SIGNING_SECRET:" },
|
|
618
|
-
{ type: "text", name: "slackChannelId", message: "PM_AGENT_SLACK_CHANNEL_ID (Cxxxxx):" },
|
|
619
|
-
{
|
|
620
|
-
type: "text",
|
|
621
|
-
name: "teamIds",
|
|
622
|
-
message: "PM_AGENT_TEAM_IDS (comma-separated):",
|
|
623
|
-
initial: stage1.linearTeamId,
|
|
624
|
-
},
|
|
625
|
-
{
|
|
626
|
-
type: "number",
|
|
627
|
-
name: "dailyTokenBudget",
|
|
628
|
-
message: "PM_AGENT_DAILY_TOKEN_BUDGET:",
|
|
629
|
-
initial: 5_000_000,
|
|
630
|
-
},
|
|
631
|
-
]);
|
|
632
|
-
if (pmPrompts.slackBotToken && pmPrompts.slackSigningSecret && pmPrompts.slackChannelId) {
|
|
633
|
-
pmAgent = {
|
|
634
|
-
slackBotToken: pmPrompts.slackBotToken,
|
|
635
|
-
slackSigningSecret: pmPrompts.slackSigningSecret,
|
|
636
|
-
slackChannelId: pmPrompts.slackChannelId,
|
|
637
|
-
teamIds: pmPrompts.teamIds || stage1.linearTeamId,
|
|
638
|
-
dailyTokenBudget: pmPrompts.dailyTokenBudget ?? 5_000_000,
|
|
639
|
-
};
|
|
640
|
-
}
|
|
641
|
-
else if (pmPrompts.slackBotToken || pmPrompts.slackSigningSecret || pmPrompts.slackChannelId) {
|
|
642
|
-
// Partial input — warn loudly so the operator doesn't think they configured PM.
|
|
643
|
-
console.warn("\n ⚠ PM agent setup incomplete — at least one of SLACK_BOT_TOKEN, " +
|
|
644
|
-
"SLACK_SIGNING_SECRET, PM_AGENT_SLACK_CHANNEL_ID was blank. The PM_AGENT_* " +
|
|
645
|
-
"block in .env has been left commented out; fill it in by hand to enable.\n");
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
// --- Stage 6: optional GitHub webhook secret + notification webhooks ---
|
|
650
|
-
const stage6 = await prompts([
|
|
651
|
-
{
|
|
652
|
-
// Hidden input — secret is shared with GitHub's webhook config. If the
|
|
653
|
-
// operator already has a secret in GitHub, paste it here. Otherwise leave
|
|
654
|
-
// blank and the auto-gen step at Stage 8 will mint one for both sides.
|
|
655
|
-
type: "password",
|
|
656
|
-
name: "githubWebhookSecret",
|
|
657
|
-
message: "GITHUB_WEBHOOK_SECRET (paste from GitHub if you already set one, or leave blank to auto-generate):",
|
|
658
|
-
},
|
|
659
|
-
{
|
|
660
|
-
type: "text",
|
|
661
|
-
name: "slackWebhookUrl",
|
|
662
|
-
message: "SLACK_WEBHOOK_URL (incoming-webhook for pipeline events; leave blank to skip):",
|
|
663
|
-
},
|
|
664
|
-
{
|
|
665
|
-
type: "text",
|
|
666
|
-
name: "discordWebhookUrl",
|
|
667
|
-
message: "DISCORD_WEBHOOK_URL (leave blank to skip):",
|
|
668
|
-
},
|
|
669
|
-
]);
|
|
670
|
-
// --- Stage 7: per-stage agent profile overrides (wizard) ---
|
|
671
|
-
const stage7Customize = await prompts({
|
|
672
|
-
type: "confirm",
|
|
673
|
-
name: "customize",
|
|
674
|
-
message: "Customize per-stage agent budgets (URATEAM_AGENT_PROFILES)? Most operators skip this.",
|
|
675
|
-
initial: false,
|
|
676
|
-
});
|
|
677
|
-
let agentProfiles;
|
|
678
|
-
if (stage7Customize.customize) {
|
|
679
|
-
agentProfiles = {};
|
|
680
|
-
const stages = ["implement", "test", "review"];
|
|
681
|
-
for (const stage of stages) {
|
|
682
|
-
const wantStage = await prompts({
|
|
683
|
-
type: "confirm",
|
|
684
|
-
name: "yes",
|
|
685
|
-
message: `Override budget for the \`${stage}\` stage?`,
|
|
686
|
-
initial: false,
|
|
687
|
-
});
|
|
688
|
-
if (!wantStage.yes)
|
|
689
|
-
continue;
|
|
690
|
-
// Min/max mirror the runtime ceilings in packages/core/src/executor/profiles.ts:96
|
|
691
|
-
// (MAX_TURNS_CEILING=500, MAX_INPUT_TOKENS_CEILING=500_000) so the wizard
|
|
692
|
-
// rejects out-of-range values at input time instead of letting the runtime
|
|
693
|
-
// silently drop them with a warn.
|
|
694
|
-
const profile = await prompts([
|
|
695
|
-
{
|
|
696
|
-
type: "number",
|
|
697
|
-
name: "maxTurns",
|
|
698
|
-
message: ` ${stage}.maxTurns (1–500, blank to keep default):`,
|
|
699
|
-
min: 1,
|
|
700
|
-
max: 500,
|
|
701
|
-
},
|
|
702
|
-
{
|
|
703
|
-
type: "number",
|
|
704
|
-
name: "maxInputTokens",
|
|
705
|
-
message: ` ${stage}.maxInputTokens (1–500000, blank to keep default):`,
|
|
706
|
-
min: 1,
|
|
707
|
-
max: 500_000,
|
|
708
|
-
},
|
|
709
|
-
{ type: "text", name: "model", message: ` ${stage}.model (blank to keep default):` },
|
|
710
|
-
]);
|
|
711
|
-
const entry = {};
|
|
712
|
-
if (typeof profile.maxTurns === "number")
|
|
713
|
-
entry.maxTurns = profile.maxTurns;
|
|
714
|
-
if (typeof profile.maxInputTokens === "number")
|
|
715
|
-
entry.maxInputTokens = profile.maxInputTokens;
|
|
716
|
-
if (profile.model)
|
|
717
|
-
entry.model = profile.model;
|
|
718
|
-
if (Object.keys(entry).length > 0)
|
|
719
|
-
agentProfiles[stage] = entry;
|
|
720
|
-
}
|
|
721
|
-
if (Object.keys(agentProfiles).length === 0) {
|
|
722
|
-
agentProfiles = undefined; // all stages skipped — don't write a `{}` JSON
|
|
723
|
-
}
|
|
724
|
-
else {
|
|
725
|
-
// Round-trip-validate the JSON we'd write so a typo doesn't get persisted as broken
|
|
726
|
-
// syntax that the agent would crash on at runtime.
|
|
727
|
-
try {
|
|
728
|
-
JSON.parse(JSON.stringify(agentProfiles));
|
|
729
|
-
}
|
|
730
|
-
catch (e) {
|
|
731
|
-
console.error("Internal: agent profiles JSON failed to round-trip — skipping.", e);
|
|
732
|
-
agentProfiles = undefined;
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
// --- Stage 8: secret generation strategy ---
|
|
737
|
-
// GITHUB_WEBHOOK_SECRET is in this set with one twist: if the operator
|
|
738
|
-
// explicitly pasted a value in Stage 6, it wins; otherwise it's auto-genned
|
|
739
|
-
// here. Either way the operator gets a value they can paste into GitHub's
|
|
740
|
-
// webhook config so HMAC signatures match.
|
|
741
|
-
const stage8 = await prompts({
|
|
742
|
-
type: "confirm",
|
|
743
|
-
name: "autoGen",
|
|
744
|
-
message: "Auto-generate POSTGRES_PASSWORD, DASHBOARD_PASSWORD, GITHUB_WEBHOOK_SECRET? (No → leave blank in .env for any not provided above)",
|
|
745
|
-
initial: true,
|
|
746
|
-
});
|
|
747
|
-
// --- Stage 9: GitHub PR-comment re-trigger config (gated) ---
|
|
748
|
-
// Only prompt when both signals are present:
|
|
749
|
-
// - URATEAM_LICENSE_KEY pasted (operator is engaged with the product enough
|
|
750
|
-
// to want advanced workflows)
|
|
751
|
-
// - A GITHUB_WEBHOOK_SECRET will exist in .env (either pasted in Stage 6
|
|
752
|
-
// or auto-genned in Stage 8) — without it the runtime won't even mount
|
|
753
|
-
// the feedback handler.
|
|
754
|
-
let githubFeedback;
|
|
755
|
-
const willHaveGhSecret = !!stage6.githubWebhookSecret || stage8.autoGen;
|
|
756
|
-
if (stage5.licenseKey && willHaveGhSecret) {
|
|
757
|
-
const setupFeedback = await prompts({
|
|
758
|
-
type: "confirm",
|
|
759
|
-
name: "setup",
|
|
760
|
-
message: "Configure GitHub PR-comment re-triggers (GITHUB_FEEDBACK_*) now? You can skip and add later.",
|
|
761
|
-
initial: false,
|
|
762
|
-
});
|
|
763
|
-
if (setupFeedback.setup) {
|
|
764
|
-
const fb = await prompts([
|
|
765
|
-
{
|
|
766
|
-
type: "text",
|
|
767
|
-
name: "triggerKeyword",
|
|
768
|
-
message: " GITHUB_FEEDBACK_TRIGGER_KEYWORD (require this string in PR comment to fire; blank = any review/comment):",
|
|
769
|
-
},
|
|
770
|
-
{
|
|
771
|
-
type: "text",
|
|
772
|
-
name: "allowedReviewers",
|
|
773
|
-
message: " GITHUB_FEEDBACK_ALLOWED_REVIEWERS (csv of GitHub usernames whose comments fire it; blank = all):",
|
|
774
|
-
},
|
|
775
|
-
{
|
|
776
|
-
type: "text",
|
|
777
|
-
name: "botLogins",
|
|
778
|
-
message: " GITHUB_FEEDBACK_BOT_LOGINS (csv of bot logins like github-actions[bot]; blank = none):",
|
|
779
|
-
},
|
|
780
|
-
{
|
|
781
|
-
type: "confirm",
|
|
782
|
-
name: "autoTrigger",
|
|
783
|
-
message: " GITHUB_FEEDBACK_AUTO_TRIGGER — fire automatically on qualifying comments?",
|
|
784
|
-
initial: true,
|
|
785
|
-
},
|
|
786
|
-
]);
|
|
787
|
-
githubFeedback = {
|
|
788
|
-
triggerKeyword: fb.triggerKeyword || undefined,
|
|
789
|
-
allowedReviewers: fb.allowedReviewers || undefined,
|
|
790
|
-
botLogins: fb.botLogins || undefined,
|
|
791
|
-
autoTrigger: fb.autoTrigger,
|
|
792
|
-
};
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
const projectDir = arg === "." ? process.cwd() : join(process.cwd(), arg);
|
|
796
|
-
const projectName = arg === "." ? basename(projectDir) || "my-project" : arg;
|
|
797
|
-
const result = scaffold({
|
|
798
|
-
projectDir,
|
|
799
|
-
projectName,
|
|
800
|
-
linearApiKey: stage1.linearApiKey,
|
|
801
|
-
linearTeamId: stage1.linearTeamId,
|
|
802
|
-
repoUrl: stage1.repoUrl,
|
|
803
|
-
defaultBranch: stage1.defaultBranch || "main",
|
|
804
|
-
deployMode: stage2.deployMode,
|
|
805
|
-
linearWebhookSecret: stage2.linearWebhookSecret,
|
|
806
|
-
domain: stage3.domain,
|
|
807
|
-
caddyEmail: stage3.caddyEmail,
|
|
808
|
-
dashboardBasePath: normalizeBasePath(stage3.dashboardBasePath),
|
|
809
|
-
anthropicApiKey: stage4.anthropicApiKey,
|
|
810
|
-
licenseKey: stage5.licenseKey,
|
|
811
|
-
pmAgent,
|
|
812
|
-
githubWebhookSecret: stage6.githubWebhookSecret || undefined,
|
|
813
|
-
githubFeedback,
|
|
814
|
-
slackWebhookUrl: stage6.slackWebhookUrl || undefined,
|
|
815
|
-
discordWebhookUrl: stage6.discordWebhookUrl || undefined,
|
|
816
|
-
agentProfiles,
|
|
817
|
-
autoGenSecrets: stage8.autoGen,
|
|
818
|
-
});
|
|
22
|
+
const { scaffoldOptions, deployMode, domain, dashboardBasePath, anthropicAuth } = wizardResult;
|
|
23
|
+
const result = scaffold(scaffoldOptions);
|
|
819
24
|
// --- Next steps printout ---
|
|
820
25
|
console.log(`\n ✓ urateam sidecar installed in ${result.urateamDir}\n`);
|
|
821
26
|
if (Object.keys(result.generatedSecrets).length > 0) {
|
|
@@ -841,14 +46,6 @@ async function main() {
|
|
|
841
46
|
}
|
|
842
47
|
console.log("");
|
|
843
48
|
}
|
|
844
|
-
if (result.todos.length > 0) {
|
|
845
|
-
console.log(" Still to do (edit .urateam/.env or run the noted commands):");
|
|
846
|
-
for (const todo of result.todos) {
|
|
847
|
-
console.log(` • ${todo}`);
|
|
848
|
-
}
|
|
849
|
-
console.log("");
|
|
850
|
-
}
|
|
851
|
-
// Detail any .env updates the operator must do before bringing the stack up.
|
|
852
49
|
if (result.todos.length > 0) {
|
|
853
50
|
console.log(" Before starting the stack, edit .urateam/.env to fill in:");
|
|
854
51
|
for (const todo of result.todos) {
|
|
@@ -863,25 +60,25 @@ async function main() {
|
|
|
863
60
|
if (result.todos.length > 0) {
|
|
864
61
|
console.log(" # ↑ open .env in your editor and fill in the TODOs above first");
|
|
865
62
|
}
|
|
866
|
-
if (
|
|
63
|
+
if (deployMode === "production") {
|
|
867
64
|
console.log(" docker compose up -d --build");
|
|
868
|
-
if (
|
|
65
|
+
if (anthropicAuth === "cli") {
|
|
869
66
|
console.log(" docker compose exec agent claude login # device-flow auth");
|
|
870
67
|
}
|
|
871
68
|
console.log(" docker compose exec agent gh auth login # device-flow auth");
|
|
872
69
|
console.log("");
|
|
873
70
|
console.log(" After the stack is up:");
|
|
874
71
|
console.log(` 1. Add a webhook in Linear → Settings → API → Webhooks`);
|
|
875
|
-
console.log(` URL: https://${
|
|
876
|
-
if (
|
|
877
|
-
console.log(` ⚠ NOT https://${
|
|
72
|
+
console.log(` URL: https://${domain || "<your-domain>"}/webhooks/linear`);
|
|
73
|
+
if (dashboardBasePath) {
|
|
74
|
+
console.log(` ⚠ NOT https://${domain || "<your-domain>"}${dashboardBasePath}/webhooks/linear`);
|
|
878
75
|
console.log(` (webhook routes are server-level, not under DASHBOARD_BASE_PATH)`);
|
|
879
76
|
}
|
|
880
77
|
console.log(` Subscribe to: Issue state changes`);
|
|
881
78
|
console.log(` Copy the secret → paste into .env as LINEAR_WEBHOOK_SECRET → restart the stack`);
|
|
882
|
-
const dashboardUrl =
|
|
883
|
-
? `https://${
|
|
884
|
-
: `https://${
|
|
79
|
+
const dashboardUrl = dashboardBasePath
|
|
80
|
+
? `https://${domain || "<your-domain>"}${dashboardBasePath}`
|
|
81
|
+
: `https://${domain || "<your-domain>"}`;
|
|
885
82
|
console.log(` 2. Open the dashboard at ${dashboardUrl} (admin / your-generated-password)`);
|
|
886
83
|
console.log(` 3. Move a Linear issue to Todo with a pipeline label to trigger your first run`);
|
|
887
84
|
}
|