@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.
- package/README.md +36 -29
- package/dist/commands/deploy.d.ts +5 -2
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +75 -15
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/init.d.ts +69 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +516 -35
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/template.js +1 -1
- package/dist/commands/template.js.map +1 -1
- package/dist/commands/user.js +93 -7
- package/dist/commands/user.js.map +1 -1
- package/dist/commands/verify.d.ts +2 -0
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +13 -4
- package/dist/commands/verify.js.map +1 -1
- package/dist/commands/vertical.d.ts.map +1 -1
- package/dist/commands/vertical.js +155 -3
- package/dist/commands/vertical.js.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/api.d.ts +2 -0
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +4 -0
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/error-guidance/catalog.d.ts +47 -31
- package/dist/lib/error-guidance/catalog.d.ts.map +1 -1
- package/dist/lib/error-guidance/catalog.js +77 -4
- package/dist/lib/error-guidance/catalog.js.map +1 -1
- package/dist/lib/error-guidance/match.d.ts.map +1 -1
- package/dist/lib/error-guidance/match.js +59 -1
- package/dist/lib/error-guidance/match.js.map +1 -1
- package/dist/lib/gofer-refresh.d.ts +2 -0
- package/dist/lib/gofer-refresh.d.ts.map +1 -1
- package/dist/lib/gofer-refresh.js +4 -4
- package/dist/lib/gofer-refresh.js.map +1 -1
- package/dist/lib/output.d.ts +3 -0
- package/dist/lib/output.d.ts.map +1 -1
- package/dist/lib/output.js +21 -0
- package/dist/lib/output.js.map +1 -1
- package/dist/lib/project-manifest.js +4 -4
- package/dist/lib/project-manifest.js.map +1 -1
- package/dist/lib/runtime-contract.d.ts +3 -0
- package/dist/lib/runtime-contract.d.ts.map +1 -1
- package/dist/lib/runtime-contract.js +7 -0
- package/dist/lib/runtime-contract.js.map +1 -1
- package/dist/lib/splash.d.ts +13 -0
- package/dist/lib/splash.d.ts.map +1 -0
- package/dist/lib/splash.js +151 -0
- package/dist/lib/splash.js.map +1 -0
- package/dist/lib/update-check.d.ts +2 -2
- package/dist/lib/update-check.d.ts.map +1 -1
- package/dist/lib/update-check.js +1 -1
- package/dist/lib/update-check.js.map +1 -1
- package/dist/lib/update-maintenance.js +1 -1
- package/dist/lib/update-maintenance.js.map +1 -1
- package/package.json +5 -5
- package/resources/gofer/.gofer-version +1 -1
- package/resources/gofer/agents-skills/0_gofer_start/SKILL.md +6 -6
- package/resources/gofer/agents-skills/1_gofer_research/SKILL.md +5 -5
- package/resources/gofer/agents-skills/gofer_bootstrap_workspace/SKILL.md +1 -1
- package/resources/gofer/agents-skills/gofer_eai_first_run/SKILL.md +2 -2
- package/resources/gofer/claude-commands/0_gofer_start.md +6 -6
- package/resources/gofer/claude-commands/1_gofer_research.md +5 -5
- package/resources/gofer/claude-commands/gofer_bootstrap_workspace.md +1 -1
- package/resources/gofer/claude-commands/gofer_eai_first_run.md +2 -2
- package/resources/gofer/commands/0_gofer_start.md +6 -6
- package/resources/gofer/commands/1_gofer_research.md +5 -5
- package/resources/gofer/commands/gofer_bootstrap_workspace.md +1 -1
- package/resources/gofer/commands/gofer_eai_first_run.md +2 -2
- package/resources/gofer/copilot-prompts/0_gofer_start.prompt.md +6 -6
- package/resources/gofer/copilot-prompts/1_gofer_research.prompt.md +5 -5
- package/resources/gofer/copilot-prompts/gofer_bootstrap_workspace.prompt.md +1 -1
- package/resources/gofer/copilot-prompts/gofer_eai_first_run.prompt.md +2 -2
- package/resources/gofer/gemini/commands/gofer/0_gofer_start.md +6 -6
- package/resources/gofer/gemini/commands/gofer/1_gofer_research.md +5 -5
- package/resources/gofer/gemini/commands/gofer/gofer_bootstrap_workspace.md +1 -1
- package/resources/gofer/gemini/commands/gofer/gofer_eai_first_run.md +2 -2
- package/resources/gofer/gemini/extension.json +7 -7
- package/resources/gofer/node-scripts/generate-commands.mjs +1 -1
- package/resources/gofer/node-scripts/package-agent-plugin.mjs +2 -2
- package/resources/gofer/references/platform/eai-app-template.md +1 -1
- package/resources/gofer/references/platform/eai-config-driven-ui.md +1 -1
- package/resources/gofer/references/platform/eai-error-catalog.yaml +1 -1
- package/resources/gofer/references/platform/vertical-template.md +1 -1
- package/resources/gofer/specify-commands/0_gofer_start.md +6 -6
- package/resources/gofer/specify-commands/1_gofer_research.md +5 -5
- package/resources/gofer/specify-commands/gofer_bootstrap_workspace.md +1 -1
- package/resources/gofer/specify-commands/gofer_eai_first_run.md +2 -2
- package/resources/gofer/system-skills/0_gofer_start/SKILL.md +6 -6
- package/resources/gofer/system-skills/1_gofer_research/SKILL.md +5 -5
- package/resources/gofer/system-skills/gofer_bootstrap_workspace/SKILL.md +1 -1
- package/resources/gofer/system-skills/gofer_eai_first_run/SKILL.md +2 -2
- package/resources/gofer/templates/eai-preflight-template.md +5 -5
- package/resources/linked-sources.json +2 -2
package/dist/commands/init.js
CHANGED
|
@@ -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-
|
|
31
|
-
const GITHUB_ORG = "eai-
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 = `@${
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
399
|
-
|
|
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.
|
|
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.
|
|
443
|
-
out.
|
|
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.
|
|
578
|
+
out.nestedDim(`Run ${chalk.cyan("eai provision entra")} inside the project to set up Entra authentication.`);
|
|
447
579
|
}
|
|
448
|
-
out.
|
|
580
|
+
out.nestedDim(`Template: ${templatePlan.displaySource}`);
|
|
449
581
|
if (options.gofer) {
|
|
450
|
-
out.
|
|
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.
|
|
453
|
-
out.
|
|
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 =
|
|
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(
|
|
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.
|
|
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.
|
|
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;
|