@enterpriseai/cli 3.8.3 → 3.10.0

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