@enterpriseai/cli 3.8.3 → 3.9.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.
@@ -15,21 +15,101 @@ import inquirer from "inquirer";
15
15
  import * as out from "../lib/output.js";
16
16
  import { installGoferResources } from "../lib/gofer-installer.js";
17
17
  import { applyGoferRefresh, planGoferRefresh } from "../lib/gofer-refresh.js";
18
- import { isAuthenticated, loadTokens } from "../lib/auth.js";
18
+ import { browserLogin, isAuthenticated, loadTokens, resolveAuthConfig, storeTokens, validateResolvedAuthConfig, } from "../lib/auth.js";
19
19
  import { publicApiUrlForHomeRegion, resolveActiveTenantContext, resolveMainCompanyTenantId, resolvePublicApiUrl, } from "../lib/tenant-context.js";
20
20
  import { buildTenantHierarchy, promptForTenantFromHierarchy, } from "../lib/tenant-hierarchy.js";
21
21
  import { parseApiError, PlatformAPIClient, } from "../lib/api.js";
22
- import { patchEnvFile } from "../lib/config.js";
22
+ import { findProjectRoot, patchEnvFile } from "../lib/config.js";
23
23
  import { pullCloudEnvValues } from "../lib/cloud-env.js";
24
+ import { findGuidance } from "../lib/error-guidance/match.js";
25
+ import { formatGuidanceText } from "../lib/error-guidance/render.js";
24
26
  import { getActiveProfile, loadProfileConfig } from "../lib/profile.js";
25
27
  import { errMsg, normalizeChildTenantDisplayNameOption } from "../lib/utils.js";
26
28
  import { saveProjectManifest } from "../lib/project-manifest.js";
29
+ import { printEaiSplash } from "../lib/splash.js";
27
30
  const exec = promisify(execFile);
28
31
  const require = createRequire(import.meta.url);
29
32
  const pkg = require("../../package.json");
30
33
  const TEMPLATE_REPO = "https://github.com/eai-tools/eai-app-template.git";
31
34
  const GITHUB_ORG = "eai-tools";
32
35
  const TEMPLATE_REPO_LABEL = `${GITHUB_ORG}/eai-app-template`;
36
+ const ONBOARDING_DOCS_URL = "https://www.enterpriseaigroup.com/docs/getting-started";
37
+ export function describeAppCreationFailure(error) {
38
+ const guidance = findGuidance({
39
+ operation: "tenant app create",
40
+ status: error.status,
41
+ serverCode: error.code,
42
+ message: error.message,
43
+ });
44
+ const lines = [`App creation failed: ${error.message}`];
45
+ if (guidance) {
46
+ lines.push("", formatGuidanceText(guidance));
47
+ }
48
+ else {
49
+ lines.push("", "Try next:", "1. eai whoami [read-only]", " Confirm the signed-in account and selected workspace.", "2. eai tenant list --all --format json [read-only]", " Check that the account can access the workspace.", "3. eai errors list [read-only]", " Inspect known recovery guidance before retrying.");
50
+ }
51
+ lines.push("", `Getting started: ${ONBOARDING_DOCS_URL}`);
52
+ return lines.join("\n");
53
+ }
54
+ export function describeCreateFlowFailure(error) {
55
+ return [
56
+ errMsg(error),
57
+ "",
58
+ "Try next:",
59
+ "1. eai whoami [read-only]",
60
+ " Confirm the signed-in account and selected workspace.",
61
+ "2. eai errors list [read-only]",
62
+ " Inspect known recovery guidance before retrying.",
63
+ `3. Read the setup guide: ${ONBOARDING_DOCS_URL}`,
64
+ ].join("\n");
65
+ }
66
+ function startEaiStep(message) {
67
+ return ora({
68
+ text: message,
69
+ spinner: { interval: 120, frames: ["◇"] },
70
+ color: "cyan",
71
+ indent: 2,
72
+ }).start();
73
+ }
74
+ function showCreateSection(title) {
75
+ out.blank();
76
+ out.heading(`${chalk.cyan("◇")} ${title}`);
77
+ }
78
+ const CREATE_AI_TOOL_CHOICES = [
79
+ { name: "Codex", value: "codex" },
80
+ { name: "Claude", value: "claude" },
81
+ { name: "VS Code", value: "vscode" },
82
+ { name: "Gemini", value: "gemini" },
83
+ ];
84
+ const CREATE_AI_TOOL_LABELS = {
85
+ codex: "Codex",
86
+ claude: "Claude",
87
+ vscode: "VS Code",
88
+ gemini: "Gemini",
89
+ };
90
+ const CREATE_PROMPT_THEME = {
91
+ prefix: {
92
+ idle: chalk.cyan("◇"),
93
+ done: chalk.green("✔"),
94
+ },
95
+ style: {
96
+ message: (text) => chalk.bold(text),
97
+ },
98
+ };
99
+ const CREATE_SELECT_THEME = {
100
+ ...CREATE_PROMPT_THEME,
101
+ icon: { cursor: chalk.cyan("●") },
102
+ style: {
103
+ message: (text, status) => status === "done" ? "" : chalk.bold(text),
104
+ },
105
+ };
106
+ const CREATE_NESTED_PROMPT_THEME = {
107
+ ...CREATE_PROMPT_THEME,
108
+ prefix: {
109
+ idle: ` ${chalk.cyan("◇")}`,
110
+ done: ` ${chalk.green("✔")}`,
111
+ },
112
+ };
33
113
  function buildInitialProjectManifest(templatePlan, packageProfile) {
34
114
  return {
35
115
  schemaVersion: 1,
@@ -159,6 +239,27 @@ async function copyTemplateIntoTargetDir(templateSource, targetDir) {
159
239
  await rm(templateDir, { recursive: true, force: true });
160
240
  }
161
241
  }
242
+ /**
243
+ * `--from` accepts any GitHub repo or local path, and install runs after
244
+ * .env.local has been generated and hydrated. Only the canonical template is
245
+ * trusted to execute npm lifecycle scripts on the developer machine.
246
+ */
247
+ export function buildTemplateInstallArgs(from) {
248
+ const args = ["install", "--no-audit", "--no-fund"];
249
+ if (from !== TEMPLATE_REPO)
250
+ args.push("--ignore-scripts");
251
+ return args;
252
+ }
253
+ /**
254
+ * Tenant binding produced by the most recent `init` run, so the guided `create`
255
+ * flow can report against the runtime tenant instead of the parent workspace.
256
+ */
257
+ let lastInitBinding;
258
+ export function consumeLastInitBinding() {
259
+ const binding = lastInitBinding;
260
+ lastInitBinding = undefined;
261
+ return binding;
262
+ }
162
263
  export const initCommand = new Command("init")
163
264
  .description("Scaffold a new application")
164
265
  .argument("[name]", "Name for the app (kebab-case)")
@@ -171,7 +272,11 @@ export const initCommand = new Command("init")
171
272
  .option("--child-tenant <name>", "Create or reuse a child company tenant display name for the app runtime boundary")
172
273
  .option("--create-child-tenant", "Prompt for a child company tenant instead of using the selected company tenant")
173
274
  .option("--no-gofer", "Skip installing Gofer AI CLI assets")
275
+ .option("--no-install", "Skip installing the generated app dependencies")
174
276
  .option("--package-profile <profile>", "Package profile to record for block catalog discovery: external, internal, or hybrid", "external")
277
+ .option("--display-name <name>", "Display name for the app")
278
+ .option("--description <description>", "One-sentence description for the app")
279
+ .option("--no-splash", "Skip the interactive EAI wordmark")
175
280
  .addHelpText("after", `
176
281
  Gofer AI CLI assets are installed by default:
177
282
  .specify/ commands, scripts, templates, hooks, and memory folders
@@ -187,6 +292,7 @@ another repo or local path.
187
292
  Use --no-gofer only when you need a bare app scaffold.
188
293
  `)
189
294
  .action(async (nameArg, options) => {
295
+ await printEaiSplash(options.splash);
190
296
  const publicApiUrl = await resolvePublicApiUrl();
191
297
  const tenantContext = await loadActiveTenantForInit(publicApiUrl);
192
298
  const activeTenant = tenantContext.activeTenant;
@@ -201,17 +307,19 @@ Use --no-gofer only when you need a bare app scaffold.
201
307
  targetDir = await resolveInitTargetDir(nameArg, targetUsesCurrentDir);
202
308
  const binding = await createTenantAppForInit(publicApiUrl, tenantContext, options.companyTenant || options.tenant, options.parentTenant, {
203
309
  slug: nameArg,
204
- displayName: toDisplayName(nameArg),
310
+ displayName: options.displayName || toDisplayName(nameArg),
205
311
  }, options.childTenant, Boolean(options.createChildTenant), false);
206
312
  parentTenantId = binding.parentTenantId;
207
313
  tenantId = binding.runtimeTenantId;
314
+ lastInitBinding = binding;
208
315
  const capabilities = tenantId
209
316
  ? await evaluateInitCapabilities(publicApiUrl, tenantId)
210
317
  : defaultInitCapabilities();
211
318
  initOptions = {
212
319
  name: nameArg,
213
- displayName: toDisplayName(nameArg),
214
- description: `${toDisplayName(nameArg)} application`,
320
+ displayName: options.displayName || toDisplayName(nameArg),
321
+ description: options.description ||
322
+ `${options.displayName || toDisplayName(nameArg)} application`,
215
323
  parentTenantId,
216
324
  tenantId,
217
325
  tenantHomeRegion: binding.runtimeTenantHomeRegion ?? activeTenant?.homeRegion,
@@ -227,7 +335,7 @@ Use --no-gofer only when you need a bare app scaffold.
227
335
  type: "input",
228
336
  name: "name",
229
337
  message: "App name (kebab-case):",
230
- default: nameArg,
338
+ default: nameArg || undefined,
231
339
  validate: (input) => {
232
340
  if (!/^[a-z][a-z0-9-]*$/.test(input)) {
233
341
  return "Must be lowercase, start with a letter, and contain only letters, numbers, and hyphens";
@@ -239,13 +347,13 @@ Use --no-gofer only when you need a bare app scaffold.
239
347
  type: "input",
240
348
  name: "displayName",
241
349
  message: "Display name:",
242
- default: (answers) => toDisplayName(answers.name),
350
+ default: (answers) => options.displayName || toDisplayName(answers.name),
243
351
  },
244
352
  {
245
353
  type: "input",
246
354
  name: "description",
247
355
  message: "Description:",
248
- default: (answers) => `${answers.displayName} application`,
356
+ default: (answers) => options.description || `${answers.displayName} application`,
249
357
  },
250
358
  ]);
251
359
  const appName = String(baseAnswers.name);
@@ -270,6 +378,7 @@ Use --no-gofer only when you need a bare app scaffold.
270
378
  }, options.childTenant, Boolean(options.createChildTenant), true);
271
379
  parentTenantId = binding.parentTenantId;
272
380
  tenantId = binding.runtimeTenantId;
381
+ lastInitBinding = binding;
273
382
  const featureAnswers = await promptFeatureOptions(publicApiUrl, tenantId);
274
383
  initOptions = {
275
384
  ...baseAnswers,
@@ -280,10 +389,10 @@ Use --no-gofer only when you need a bare app scaffold.
280
389
  packageProfile,
281
390
  };
282
391
  }
283
- out.heading(`Creating ${chalk.cyan(initOptions.displayName)}`);
392
+ out.heading(` ${chalk.cyan("◇")} Creating ${chalk.cyan(initOptions.displayName)}`);
284
393
  out.blank();
285
394
  // Step 1: Clone template
286
- const cloneSpinner = ora("Cloning template...").start();
395
+ const cloneSpinner = startEaiStep("Cloning template...");
287
396
  const templatePlan = resolveTemplateClonePlan(options.from);
288
397
  try {
289
398
  if (targetUsesCurrentDir) {
@@ -306,7 +415,7 @@ Use --no-gofer only when you need a bare app scaffold.
306
415
  process.exit(1);
307
416
  }
308
417
  // Step 2: Update package.json
309
- const pkgSpinner = ora("Customizing package.json...").start();
418
+ const pkgSpinner = startEaiStep("Customizing package.json...");
310
419
  try {
311
420
  const pkgPath = join(targetDir, "package.json");
312
421
  const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
@@ -320,7 +429,7 @@ Use --no-gofer only when you need a bare app scaffold.
320
429
  pkgSpinner.fail("Failed to update package.json");
321
430
  }
322
431
  // Step 3: Generate .env.local with placeholders
323
- const envSpinner = ora("Generating .env.local...").start();
432
+ const envSpinner = startEaiStep("Generating .env.local...");
324
433
  try {
325
434
  const envContent = generateEnvFile(initOptions);
326
435
  await writeFile(join(targetDir, ".env.local"), envContent, "utf-8");
@@ -331,7 +440,7 @@ Use --no-gofer only when you need a bare app scaffold.
331
440
  envSpinner.fail("Failed to generate .env.local");
332
441
  }
333
442
  // Step 4: Generate Object Types scaffold
334
- const typesSpinner = ora("Creating Object Types scaffold...").start();
443
+ const typesSpinner = startEaiStep("Creating Object Types scaffold...");
335
444
  try {
336
445
  const typesContent = generateObjectTypesScaffold(initOptions);
337
446
  await writeFile(join(targetDir, "src", "eai.config", "object-types.ts"), typesContent, "utf-8");
@@ -341,7 +450,7 @@ Use --no-gofer only when you need a bare app scaffold.
341
450
  typesSpinner.fail("Failed to create Object Types scaffold");
342
451
  }
343
452
  // Step 5: Generate deploy workflow
344
- const deploySpinner = ora("Creating deployment workflow...").start();
453
+ const deploySpinner = startEaiStep("Creating deployment workflow...");
345
454
  try {
346
455
  const workflowDir = join(targetDir, ".github", "workflows");
347
456
  await mkdir(workflowDir, { recursive: true });
@@ -353,7 +462,7 @@ Use --no-gofer only when you need a bare app scaffold.
353
462
  deploySpinner.fail("Failed to create deployment workflow");
354
463
  }
355
464
  // Step 6: Generate project CLAUDE.md
356
- const claudeSpinner = ora("Generating CLAUDE.md...").start();
465
+ const claudeSpinner = startEaiStep("Generating CLAUDE.md...");
357
466
  try {
358
467
  const claudeContent = generateClaudeMd(initOptions);
359
468
  await writeFile(join(targetDir, "CLAUDE.md"), claudeContent, "utf-8");
@@ -364,7 +473,7 @@ Use --no-gofer only when you need a bare app scaffold.
364
473
  }
365
474
  // Step 7: Install Gofer AI CLI assets
366
475
  if (options.gofer) {
367
- const goferSpinner = ora("Installing Gofer AI CLI assets...").start();
476
+ const goferSpinner = startEaiStep("Installing Gofer AI CLI assets...");
368
477
  try {
369
478
  const summary = await installGoferResources(targetDir, {
370
479
  workflowProfile: "enterpriseai",
@@ -378,7 +487,7 @@ Use --no-gofer only when you need a bare app scaffold.
378
487
  }
379
488
  }
380
489
  // Step 8: Record project manifest for future safe refreshes
381
- const manifestSpinner = ora("Recording project manifest...").start();
490
+ const manifestSpinner = startEaiStep("Recording project manifest...");
382
491
  try {
383
492
  const initialManifest = buildInitialProjectManifest(templatePlan, initOptions.packageProfile);
384
493
  await saveProjectManifest(targetDir, initialManifest);
@@ -395,8 +504,30 @@ Use --no-gofer only when you need a bare app scaffold.
395
504
  out.error(err instanceof Error ? err.message : String(err));
396
505
  process.exit(1);
397
506
  }
398
- // Step 9: Initialize git
399
- const gitSpinner = ora("Initializing git...").start();
507
+ // Step 9: Install project dependencies
508
+ if (options.install !== false) {
509
+ const installArgs = buildTemplateInstallArgs(options.from);
510
+ const trusted = !installArgs.includes("--ignore-scripts");
511
+ if (!trusted) {
512
+ out.warn(`Installing a custom template with ${chalk.cyan("--ignore-scripts")} because ${chalk.cyan(options.from)} is not the canonical EAI app template.`);
513
+ out.nestedInfo(`If you trust that source, run ${chalk.cyan("npm rebuild")} inside ${chalk.cyan(targetDir)} to execute its lifecycle scripts.`);
514
+ }
515
+ const installSpinner = startEaiStep("Installing app dependencies...");
516
+ try {
517
+ await exec("npm", installArgs, {
518
+ cwd: targetDir,
519
+ });
520
+ installSpinner.succeed("Installed app dependencies");
521
+ }
522
+ catch (err) {
523
+ installSpinner.fail("Failed to install app dependencies");
524
+ out.error(errMsg(err));
525
+ out.nestedInfo(`Run \`npm install\` inside ${chalk.cyan(targetDir)} and retry.`);
526
+ process.exit(1);
527
+ }
528
+ }
529
+ // Step 10: Initialize git
530
+ const gitSpinner = startEaiStep("Initializing git...");
400
531
  try {
401
532
  await exec("git", ["init"], { cwd: targetDir });
402
533
  await exec("git", ["add", "."], { cwd: targetDir });
@@ -434,25 +565,374 @@ Use --no-gofer only when you need a bare app scaffold.
434
565
  }
435
566
  }
436
567
  out.blank();
437
- out.success(`Created ${chalk.bold(initOptions.displayName)} at ${chalk.cyan(targetDir)}`);
568
+ out.nestedSuccess(`Created ${chalk.bold(initOptions.displayName)} at ${chalk.cyan(targetDir)}`);
438
569
  out.blank();
439
- out.heading("Next steps:");
570
+ out.heading(" Next steps:");
440
571
  out.blank();
441
572
  if (initOptions.tenantId) {
442
- out.dim(`Main company tenant: ${chalk.cyan(initOptions.parentTenantId)}`);
443
- out.dim(`Bound to tenant: ${chalk.cyan(initOptions.tenantId)}`);
573
+ out.nestedDim(`Main company tenant: ${chalk.cyan(initOptions.parentTenantId)}`);
574
+ out.nestedDim(`Bound to tenant: ${chalk.cyan(initOptions.tenantId)}`);
444
575
  }
445
576
  if (!entraProvisioned) {
446
- out.dim(`Run ${chalk.cyan("eai provision entra")} inside the project to set up Entra authentication.`);
577
+ out.nestedDim(`Run ${chalk.cyan("eai provision entra")} inside the project to set up Entra authentication.`);
447
578
  }
448
- out.dim(`Template: ${templatePlan.displaySource}`);
579
+ out.nestedDim(`Template: ${templatePlan.displaySource}`);
449
580
  if (options.gofer) {
450
- out.dim("Gofer: Claude /0_gofer_start; Codex uses the repo-local Gofer skills; Gemini /gofer:1_gofer_research; Copilot .github prompts/skills.");
581
+ out.nestedDim("Gofer: Claude /0_gofer_start; Codex uses the repo-local Gofer skills; Gemini /gofer:1_gofer_research; Copilot .github prompts/skills.");
451
582
  }
452
- out.dim(`Package profile: ${initOptions.packageProfile}`);
453
- out.dim(`CLI docs: https://github.com/${GITHUB_ORG}/eai`);
583
+ out.nestedDim(`Package profile: ${initOptions.packageProfile}`);
584
+ out.nestedDim(`CLI docs: https://github.com/${GITHUB_ORG}/eai`);
454
585
  out.blank();
455
586
  });
587
+ /**
588
+ * Guided first-run setup matching the public Getting Started flow.
589
+ *
590
+ * The legacy `init` command remains the low-level scaffold entry point. This
591
+ * command owns the first-run experience: local checks, browser auth, tenant
592
+ * confirmation, non-interactive scaffolding, and the hand-off to Gofer.
593
+ */
594
+ export const createCommand = new Command("create")
595
+ .description("Guide a new builder through EAI setup and create an application")
596
+ .argument("[name]", "Name for the app (kebab-case)")
597
+ .option("--from <repo>", "GitHub repo URL or local path for template", TEMPLATE_REPO)
598
+ .option("--skip-prompts", "Use defaults without interactive prompts", false)
599
+ .option("--skip-onboarding", "Skip first-run checks and use the legacy init scaffold flow", false)
600
+ .option("--current-dir", "Scaffold into the current directory instead of creating ./<name>", false)
601
+ .option("--tenant <id>", "Main company tenant ID (deprecated alias for --company-tenant)")
602
+ .option("--company-tenant <id>", "Main company tenant ID that owns this app")
603
+ .option("--parent-tenant <id>", "Immediate parent company tenant ID for the new child company")
604
+ .option("--child-tenant <name>", "Create or reuse a child company tenant display name for the app runtime boundary")
605
+ .option("--create-child-tenant", "Prompt for a child company tenant instead of using the selected company tenant")
606
+ .option("--no-gofer", "Skip installing Gofer AI CLI assets")
607
+ .option("--no-install", "Skip installing the generated app dependencies")
608
+ .option("--package-profile <profile>", "Package profile to record for block catalog discovery: external, internal, or hybrid", "external")
609
+ .option("--tool <tool>", "AI tool to prepare for: codex, claude, vscode, or gemini")
610
+ .option("--no-splash", "Skip the interactive EAI wordmark")
611
+ .addHelpText("after", `
612
+ Guided setup:
613
+ 1. Check Git, Node.js, and npm
614
+ 2. Sign in with the browser and choose the signup workspace
615
+ 3. Confirm the project name and folder
616
+ 4. Create the app with Gofer AI CLI assets
617
+ 5. Check builder readiness and hand off to /0_business_scenario
618
+
619
+ The command does not create a root tenant. Complete Website signup first so
620
+ the CLI can use the onboarding-created company workspace.
621
+
622
+ Use --skip-onboarding for the legacy scaffold prompts, or use eai init for
623
+ the low-level scaffold command directly.
624
+ `)
625
+ .action(async (nameArg, options) => {
626
+ await runCreateFlow(nameArg, options);
627
+ });
628
+ async function runCreateFlow(nameArg, options) {
629
+ await printEaiSplash(options.splash);
630
+ if (options.skipOnboarding || options.skipPrompts) {
631
+ const initArgs = buildForwardedInitArgs(nameArg, options);
632
+ await initCommand.parseAsync(initArgs, { from: "user" });
633
+ return;
634
+ }
635
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
636
+ out.error("Guided `eai create` requires an interactive terminal. Use `eai create <name> --skip-prompts` for automation, or run it from a real terminal.");
637
+ process.exit(1);
638
+ }
639
+ try {
640
+ await runCreatePreflight();
641
+ const answers = await promptCreateOnboarding(nameArg, options);
642
+ showCreateSection("Sign in to EAI");
643
+ await ensureCreateAuthentication();
644
+ showCreateSection("Choose your EAI workspace");
645
+ const bootstrapPublicApiUrl = await resolvePublicApiUrl();
646
+ let tenantContext;
647
+ try {
648
+ tenantContext = await resolveCreateTenantContext(bootstrapPublicApiUrl, options);
649
+ }
650
+ catch (error) {
651
+ out.error(error instanceof Error ? error.message : String(error));
652
+ out.nestedInfo(`Complete Website signup, then retry: ${ONBOARDING_DOCS_URL}`);
653
+ process.exit(1);
654
+ }
655
+ out.nestedSuccess(`Using company workspace ${chalk.cyan(tenantContext.activeTenant.displayName)} ${chalk.dim(`(${tenantContext.activeTenant.id})`)}`);
656
+ showCreateSection("Build your project");
657
+ const initArgs = buildForwardedInitArgs(answers.name, options, answers, tenantContext.activeTenant.id);
658
+ await initCommand.parseAsync(initArgs, { from: "user" });
659
+ // `init` may have bound the app to a freshly created child company. Readiness
660
+ // must be checked against that runtime tenant, not the parent workspace.
661
+ const binding = consumeLastInitBinding();
662
+ const targetDir = answers.useCurrentDirectory
663
+ ? resolve(process.cwd())
664
+ : resolve(process.cwd(), answers.name);
665
+ await reportCreateCompletion(targetDir, publicApiUrlForHomeRegion(binding?.runtimeTenantHomeRegion) ||
666
+ tenantContext.publicApiUrl, binding?.runtimeTenantId || tenantContext.activeTenant.id, answers.aiTool, options.gofer !== false);
667
+ }
668
+ catch (error) {
669
+ out.error(describeCreateFlowFailure(error));
670
+ process.exit(1);
671
+ }
672
+ }
673
+ /**
674
+ * Resolve the workspace for guided create.
675
+ *
676
+ * An explicit --company-tenant/--tenant is passed through as `tenantId` so the
677
+ * membership is validated and the cached active tenant cannot silently replace
678
+ * the operator's choice on a state-changing app create.
679
+ */
680
+ export function resolveCreateTenantContext(publicApiUrl, options) {
681
+ return resolveActiveTenantContext({
682
+ publicApiUrl,
683
+ interactive: true,
684
+ tenantId: options.companyTenant || options.tenant,
685
+ });
686
+ }
687
+ export function buildForwardedInitArgs(nameArg, options, answers, tenantId) {
688
+ const appName = answers?.name || nameArg;
689
+ const args = [];
690
+ if (appName)
691
+ args.push(appName);
692
+ if (options.skipPrompts || answers)
693
+ args.push("--skip-prompts");
694
+ args.push("--no-splash");
695
+ // `tenantId` is the validated resolution of the explicit options, so the two
696
+ // can no longer disagree; prefer it because it is always a canonical ID.
697
+ const companyTenant = tenantId || options.companyTenant || options.tenant;
698
+ if (companyTenant)
699
+ args.push("--company-tenant", companyTenant);
700
+ if (options.parentTenant)
701
+ args.push("--parent-tenant", options.parentTenant);
702
+ if (options.childTenant)
703
+ args.push("--child-tenant", options.childTenant);
704
+ if (options.createChildTenant)
705
+ args.push("--create-child-tenant");
706
+ if (answers?.useCurrentDirectory || options.currentDir) {
707
+ args.push("--current-dir");
708
+ }
709
+ if (answers?.displayName)
710
+ args.push("--display-name", answers.displayName);
711
+ if (answers?.description)
712
+ args.push("--description", answers.description);
713
+ if (options.from && options.from !== TEMPLATE_REPO) {
714
+ args.push("--from", options.from);
715
+ }
716
+ if (options.gofer === false)
717
+ args.push("--no-gofer");
718
+ if (options.install === false)
719
+ args.push("--no-install");
720
+ if (options.packageProfile) {
721
+ args.push("--package-profile", options.packageProfile);
722
+ }
723
+ return args;
724
+ }
725
+ async function runCreatePreflight() {
726
+ showCreateSection("Making sure your computer has the correct prerequisites.");
727
+ const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] || "0", 10);
728
+ if (nodeMajor < 20) {
729
+ throw new Error(`Node.js ${process.versions.node} is too old. EAI CLI requires Node.js 20 or newer.`);
730
+ }
731
+ const checks = [
732
+ { label: "Git", command: "git", args: ["--version"] },
733
+ { label: "npm", command: "npm", args: ["--version"] },
734
+ ];
735
+ out.nestedSuccess(`Node.js ${process.versions.node}`);
736
+ for (const check of checks) {
737
+ try {
738
+ const result = await exec(check.command, check.args);
739
+ const version = result.stdout.trim() || result.stderr.trim();
740
+ out.nestedSuccess(`${check.label} ${version}`);
741
+ }
742
+ catch {
743
+ throw new Error(`${check.label} is required before creating an EAI app. Install it, reopen your terminal, and run \`npx eai-cli create\` again.`);
744
+ }
745
+ }
746
+ out.nestedSuccess("Local tooling is ready");
747
+ }
748
+ async function promptCreateOnboarding(nameArg, options) {
749
+ const requestedTool = options.tool?.trim().toLowerCase();
750
+ if (requestedTool &&
751
+ !CREATE_AI_TOOL_CHOICES.some((choice) => choice.value === requestedTool)) {
752
+ throw new Error(`Unknown --tool "${options.tool}". Use codex, claude, vscode, or gemini.`);
753
+ }
754
+ out.blank();
755
+ const toolAnswer = requestedTool
756
+ ? { aiTool: requestedTool }
757
+ : await inquirer.prompt([
758
+ {
759
+ type: "select",
760
+ name: "aiTool",
761
+ message: "Which AI tool will you use for this project?",
762
+ choices: CREATE_AI_TOOL_CHOICES,
763
+ default: "codex",
764
+ theme: CREATE_SELECT_THEME,
765
+ },
766
+ ]);
767
+ out.blank();
768
+ const nameAnswer = await inquirer.prompt([
769
+ {
770
+ type: "input",
771
+ name: "name",
772
+ message: "What is the name of your project?\n ",
773
+ default: nameArg ? toKebabCase(nameArg) : undefined,
774
+ theme: CREATE_PROMPT_THEME,
775
+ validate: (input) => {
776
+ if (!/^[a-z][a-z0-9-]*$/.test(input.trim())) {
777
+ return "Use lowercase letters, numbers, and hyphens; start with a letter";
778
+ }
779
+ return true;
780
+ },
781
+ },
782
+ ]);
783
+ out.blank();
784
+ const displayNameAnswer = await inquirer.prompt([
785
+ {
786
+ type: "input",
787
+ name: "displayName",
788
+ message: "What should we call your project?\n ",
789
+ default: toDisplayName(String(nameAnswer.name)),
790
+ theme: CREATE_PROMPT_THEME,
791
+ },
792
+ ]);
793
+ out.blank();
794
+ const descriptionAnswer = await inquirer.prompt([
795
+ {
796
+ type: "input",
797
+ name: "description",
798
+ message: "What does your project do?\n ",
799
+ default: `${displayNameAnswer.displayName} application`,
800
+ theme: CREATE_PROMPT_THEME,
801
+ },
802
+ ]);
803
+ const appName = String(nameAnswer.name).trim();
804
+ let useCurrentDirectory = true;
805
+ if (!options.currentDir) {
806
+ out.blank();
807
+ const locationAnswer = await inquirer.prompt([
808
+ {
809
+ type: "select",
810
+ name: "location",
811
+ message: "Where should we create your project?",
812
+ choices: [
813
+ { name: `New folder ./${appName}`, value: "new" },
814
+ {
815
+ name: `Current folder ./${basename(process.cwd())}`,
816
+ value: "current",
817
+ },
818
+ ],
819
+ default: "new",
820
+ theme: CREATE_SELECT_THEME,
821
+ },
822
+ ]);
823
+ useCurrentDirectory = String(locationAnswer.location) === "current";
824
+ }
825
+ return {
826
+ name: appName,
827
+ displayName: String(displayNameAnswer.displayName).trim(),
828
+ description: String(descriptionAnswer.description).trim(),
829
+ useCurrentDirectory,
830
+ aiTool: String(toolAnswer.aiTool),
831
+ };
832
+ }
833
+ async function ensureCreateAuthentication() {
834
+ if (await isAuthenticated()) {
835
+ out.nestedSuccess("EAI login is already active");
836
+ return;
837
+ }
838
+ const { proceed } = await inquirer.prompt([
839
+ {
840
+ type: "confirm",
841
+ name: "proceed",
842
+ message: "No EAI login was found. Open the browser to sign in now?\n ",
843
+ default: true,
844
+ theme: CREATE_NESTED_PROMPT_THEME,
845
+ },
846
+ ]);
847
+ if (!proceed) {
848
+ throw new Error("Sign-in is required before creating an EAI app.");
849
+ }
850
+ const profile = getActiveProfile();
851
+ const projectRoot = await findProjectRoot();
852
+ const resolvedConfig = await resolveAuthConfig(projectRoot || undefined, profile);
853
+ const configIssue = validateResolvedAuthConfig(resolvedConfig);
854
+ if (configIssue)
855
+ throw new Error(configIssue);
856
+ out.nestedInfo("Opening your browser to complete EAI sign-in...");
857
+ const tokens = await browserLogin(resolvedConfig.tenantName, resolvedConfig.tenantId, resolvedConfig.clientId, resolvedConfig.authScope);
858
+ await storeTokens(tokens);
859
+ out.nestedSuccess(`Authenticated as ${chalk.bold(tokens.upn || "user")}`);
860
+ }
861
+ async function reportCreateCompletion(targetDir, publicApiUrl, tenantId, aiTool, goferExpected) {
862
+ const hasGofer = await Promise.all([
863
+ access(join(targetDir, ".specify")),
864
+ access(join(targetDir, ".agents")),
865
+ ])
866
+ .then(() => true)
867
+ .catch(() => false);
868
+ out.blank();
869
+ if (goferExpected && hasGofer) {
870
+ out.nestedSuccess("Gofer AI CLI assets confirmed");
871
+ }
872
+ else if (goferExpected) {
873
+ out.warn("Gofer assets were not found; run `eai gofer refresh` inside the project.");
874
+ }
875
+ let builderReady = false;
876
+ try {
877
+ const client = new PlatformAPIClient(publicApiUrl, tenantId);
878
+ const readiness = await client.getBuilderReadiness({ tenantId, workflowKeys: [] });
879
+ builderReady = readiness.status === "available";
880
+ if (builderReady) {
881
+ out.nestedSuccess("Builder readiness is available");
882
+ }
883
+ else {
884
+ out.warn(`Builder readiness: ${readiness.status}`);
885
+ for (const check of readiness.checks) {
886
+ out.nestedInfo(`${check.key}: ${check.status} — ${check.reasonMessage}`);
887
+ }
888
+ }
889
+ }
890
+ catch (error) {
891
+ out.warn(`Builder readiness could not be checked yet: ${error instanceof Error ? error.message : String(error)}`);
892
+ }
893
+ out.blank();
894
+ const summary = buildCreateCompletionSummary(builderReady, aiTool);
895
+ out.heading(summary.heading);
896
+ out.nestedInfo(`Project folder: ${chalk.cyan(targetDir)}`);
897
+ out.nestedInfo(`AI tool: ${chalk.cyan(CREATE_AI_TOOL_LABELS[aiTool])}`);
898
+ for (const step of summary.steps)
899
+ out.nestedInfo(step);
900
+ out.nestedDim(`Setup guide: ${ONBOARDING_DOCS_URL}`);
901
+ out.blank();
902
+ }
903
+ /**
904
+ * Next-step copy for guided create. A failed or unchecked builder readiness must
905
+ * not be presented as a ready workspace, and must not send the builder straight
906
+ * into a hand-off that is known to be unproven.
907
+ */
908
+ export function buildCreateCompletionSummary(builderReady, aiTool) {
909
+ const toolLabel = CREATE_AI_TOOL_LABELS[aiTool];
910
+ if (builderReady) {
911
+ return {
912
+ heading: `${chalk.green("✔")} Your EAI workspace is ready`,
913
+ steps: [
914
+ `Open that folder in ${toolLabel}, then start:`,
915
+ chalk.cyan("/0_business_scenario <describe what you want to build>"),
916
+ ],
917
+ };
918
+ }
919
+ return {
920
+ heading: `${chalk.yellow("!")} Project created; builder setup is not confirmed yet`,
921
+ steps: [
922
+ `Re-check with ${chalk.cyan("eai doctor")} inside the project folder.`,
923
+ `If it stays unavailable, ask your workspace tenant-admin to finish setup: ${ONBOARDING_DOCS_URL}`,
924
+ `Once readiness reports available, open the folder in ${toolLabel} and start ${chalk.cyan("/0_business_scenario")}.`,
925
+ ],
926
+ };
927
+ }
928
+ export function toKebabCase(value) {
929
+ return value
930
+ .trim()
931
+ .replace(/([a-z])([A-Z])/g, "$1-$2")
932
+ .replace(/[^a-zA-Z0-9]+/g, "-")
933
+ .replace(/^-+|-+$/g, "")
934
+ .toLowerCase();
935
+ }
456
936
  function resolvePackageProfile(value) {
457
937
  if (value === "external" || value === "internal" || value === "hybrid") {
458
938
  return value;
@@ -466,7 +946,7 @@ function resolvePackageProfile(value) {
466
946
  * on success. Non-fatal: logs a warning and returns false on any failure.
467
947
  */
468
948
  async function provisionEntraInline(targetDir, appName, tenantId, publicApiUrl) {
469
- const spinner = ora("Provisioning Entra app registration...").start();
949
+ const spinner = startEaiStep("Provisioning Entra app registration...");
470
950
  try {
471
951
  const client = new PlatformAPIClient(publicApiUrl, tenantId);
472
952
  const authSiteUrl = `http://localhost:3000/${appName}`;
@@ -717,7 +1197,7 @@ async function createTenantAppForInit(publicApiUrl, tenantContext, companyFlag,
717
1197
  });
718
1198
  if (!res.ok) {
719
1199
  const error = await parseApiError(res);
720
- out.error(`App creation failed: ${error.message}`);
1200
+ out.error(describeAppCreationFailure(error));
721
1201
  process.exit(1);
722
1202
  }
723
1203
  const payload = (await res.json());
@@ -726,14 +1206,14 @@ async function createTenantAppForInit(publicApiUrl, tenantContext, companyFlag,
726
1206
  ? String(childTenant.id || "")
727
1207
  : "";
728
1208
  if (!childTenantId) {
729
- out.info(`Created app ${chalk.cyan(appSeed.slug)} under company tenant ${chalk.cyan(immediateParentTenantId)}.`);
1209
+ out.nestedInfo(`Created app ${chalk.cyan(appSeed.slug)} under company tenant ${chalk.cyan(immediateParentTenantId)}.`);
730
1210
  return {
731
1211
  parentTenantId: companyTenantId,
732
1212
  runtimeTenantId: immediateParentTenantId,
733
1213
  runtimeTenantHomeRegion: activeTenant?.homeRegion,
734
1214
  };
735
1215
  }
736
- out.info(`Created app ${chalk.cyan(appSeed.slug)} under main company ${chalk.cyan(companyTenantId)} with child company ${chalk.cyan(childTenantId)}.`);
1216
+ out.nestedInfo(`Created app ${chalk.cyan(appSeed.slug)} under main company ${chalk.cyan(companyTenantId)} with child company ${chalk.cyan(childTenantId)}.`);
737
1217
  const childTenantHomeRegion = childTenant && typeof childTenant === "object"
738
1218
  ? childTenant.homeRegion
739
1219
  : undefined;