@base44-preview/cli 0.1.15-pr.630.3f8fa50 → 0.1.15-pr.630.434be14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -269450,1431 +269450,1482 @@ function getAgentsCommand() {
269450
269450
  return new Command2("agents").description("Manage project agents").addCommand(getAgentsPushCommand()).addCommand(getAgentsPullCommand());
269451
269451
  }
269452
269452
 
269453
- // src/core/model.ts
269454
- var MODELS = [
269455
- { name: "default", id: null, note: "let Base44 choose" },
269456
- { name: "Opus 5", id: "claude_opus_5" },
269457
- { name: "Sonnet 5", id: "claude-sonnet-5" },
269458
- { name: "Fable 5", id: "claude_fable_5", note: "uses more credits" },
269459
- { name: "GPT-5.6 Sol", id: "gpt_5_6_sol" },
269460
- { name: "Gemini 3.8 Flash", id: "gemini_3_8_flash", note: "fast" },
269461
- { name: "Base 1", id: "base1" }
269462
- ];
269463
- var fold = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
269464
- function resolvePick(input) {
269465
- const key = fold(input);
269466
- const byExact = MODELS.find((m) => fold(m.name) === key || m.id && fold(m.id) === key);
269467
- if (byExact)
269468
- return byExact;
269469
- const byContains = MODELS.filter((m) => fold(m.name).includes(key) || m.id && fold(m.id).includes(key));
269470
- if (byContains.length === 1)
269471
- return byContains[0];
269472
- const names = MODELS.map((m) => m.name).join(", ");
269473
- throw new InvalidInputError(byContains.length > 1 ? `"${input}" is ambiguous — matches ${byContains.map((m) => m.name).join(", ")}.` : `Unknown model "${input}". Choose one of: ${names}.`);
269474
- }
269475
- async function getMe() {
269476
- try {
269477
- return await base44Client.get("api/auth/me").json();
269478
- } catch (error) {
269479
- throw await ApiError.fromHttpError(error, "reading your account");
269453
+ // src/cli/commands/auth/password-login.ts
269454
+ import { dirname as dirname15, join as join21 } from "node:path";
269455
+ async function passwordLoginAction({ log, runTask }, action) {
269456
+ const shouldEnable = action === "enable";
269457
+ const { project } = await readProjectConfig();
269458
+ const configDir = dirname15(project.configPath);
269459
+ const authDir = join21(configDir, project.authDir);
269460
+ const updated = await runTask("Updating local auth config", async () => {
269461
+ const current = await readAuthConfig(authDir) ?? DEFAULT_AUTH_CONFIG;
269462
+ const merged = { ...current, enableUsernamePassword: shouldEnable };
269463
+ await writeAuthConfig(authDir, merged);
269464
+ return merged;
269465
+ });
269466
+ if (!shouldEnable && !hasAnyLoginMethod(updated)) {
269467
+ log.warn("Disabling password auth will leave no login methods enabled. Users will be locked out.");
269480
269468
  }
269469
+ const newStatus = shouldEnable ? "enabled" : "disabled";
269470
+ return {
269471
+ outroMessage: `Username & password authentication ${newStatus} in local config. Run \`base44 auth push\` or \`base44 deploy\` to apply.`
269472
+ };
269481
269473
  }
269482
- async function saveBuilderModel(userId, modelId) {
269483
- try {
269484
- await base44Client.post(`api/auth/${userId}/update-user`, {
269485
- json: { builder_model: modelId }
269486
- });
269487
- } catch (error) {
269488
- if (error instanceof HTTPError && error.response.status === 400) {
269489
- throw new InvalidInputError("This account can't pick a builder model yet — enable the PER_USER_BUILDER_MODEL_SELECTION flag for it in PostHog, or the model isn't available in this workspace.");
269490
- }
269491
- throw await ApiError.fromHttpError(error, "saving your model choice");
269474
+ function getPasswordLoginCommand() {
269475
+ return new Base44Command("password-login").description("Enable or disable username & password authentication").addArgument(new Argument2("<action>", "enable or disable password authentication").choices(["enable", "disable"])).action(passwordLoginAction);
269476
+ }
269477
+
269478
+ // src/cli/commands/auth/pull.ts
269479
+ import { dirname as dirname16, join as join22 } from "node:path";
269480
+ async function pullAuthAction({
269481
+ log,
269482
+ runTask
269483
+ }) {
269484
+ const { project } = await readProjectConfig();
269485
+ const configDir = dirname16(project.configPath);
269486
+ const authDir = join22(configDir, project.authDir);
269487
+ const remoteConfig = await runTask("Fetching auth config from Base44", async () => {
269488
+ return await pullAuthConfig();
269489
+ }, {
269490
+ successMessage: "Auth config fetched successfully",
269491
+ errorMessage: "Failed to fetch auth config"
269492
+ });
269493
+ const { written } = await runTask("Syncing auth config file", async () => {
269494
+ return await writeAuthConfig(authDir, remoteConfig);
269495
+ }, {
269496
+ successMessage: "Auth config file synced successfully",
269497
+ errorMessage: "Failed to sync auth config file"
269498
+ });
269499
+ if (written) {
269500
+ log.success("Auth config written to local file");
269501
+ } else {
269502
+ log.info("Auth config is already up to date");
269492
269503
  }
269504
+ return {
269505
+ outroMessage: `Pulled auth config to ${authDir} (overwrites local file)`
269506
+ };
269507
+ }
269508
+ function getAuthPullCommand() {
269509
+ return new Base44Command("pull").description("Pull auth config from Base44 to local file").action(pullAuthAction);
269493
269510
  }
269494
- var displayName = (id) => MODELS.find((m) => m.id === id)?.name ?? id ?? "default";
269495
269511
 
269496
- // src/cli/commands/app/model.ts
269497
- async function modelAction({ log, jsonMode }, input) {
269498
- const me = await getMe();
269499
- const current = me.builder_model ?? null;
269500
- if (!input) {
269501
- if (jsonMode) {
269502
- return {
269503
- stdout: `${JSON.stringify({
269504
- current,
269505
- models: MODELS.map((m) => ({ name: m.name, id: m.id }))
269506
- })}
269507
- `
269508
- };
269509
- }
269510
- for (const m of MODELS) {
269511
- const active = (m.id ?? null) === current;
269512
- const marker = active ? theme.styles.bold("●") : theme.styles.dim("○");
269513
- const note = m.note ? theme.styles.dim(` (${m.note})`) : "";
269514
- log.message(`${marker} ${active ? theme.styles.bold(m.name) : m.name}${note}`);
269515
- }
269512
+ // src/cli/commands/auth/push.ts
269513
+ async function pushAuthAction({ isNonInteractive, log, runTask }, options) {
269514
+ const { authConfig } = await readProjectConfig();
269515
+ if (authConfig.length === 0) {
269516
+ log.info("No local auth config found");
269516
269517
  return {
269517
- outroMessage: `Current: ${theme.styles.bold(displayName(current))}. Set with \`base44 app model <name>\`.`
269518
+ outroMessage: "No auth config to push. Run `base44 auth pull` to fetch the remote config first."
269518
269519
  };
269519
269520
  }
269520
- const pick = resolvePick(input);
269521
- if ((pick.id ?? null) === current) {
269522
- return { outroMessage: `Already on ${theme.styles.bold(pick.name)}.` };
269521
+ if (!hasAnyLoginMethod(authConfig[0])) {
269522
+ log.warn("This config has no login methods enabled. Pushing it will lock out all users.");
269523
269523
  }
269524
- await saveBuilderModel(me.id, pick.id);
269525
- if (jsonMode)
269526
- return { stdout: `${JSON.stringify({ current: pick.id })}
269527
- ` };
269524
+ if (!options.yes) {
269525
+ if (isNonInteractive) {
269526
+ throw new InvalidInputError("--yes is required in non-interactive mode");
269527
+ }
269528
+ const shouldPush = await Re({
269529
+ message: "Push auth config to Base44?"
269530
+ });
269531
+ if (Ct(shouldPush) || !shouldPush) {
269532
+ return { outroMessage: "Push cancelled" };
269533
+ }
269534
+ }
269535
+ await runTask("Pushing auth config to Base44", async () => {
269536
+ return await pushAuthConfig(authConfig[0] ?? null);
269537
+ }, {
269538
+ successMessage: "Auth config pushed successfully",
269539
+ errorMessage: "Failed to push auth config"
269540
+ });
269528
269541
  return {
269529
- outroMessage: pick.id === null ? "Model reset — Base44 chooses per app again." : `Builder model set to ${theme.styles.bold(pick.name)} for every new turn.`
269542
+ outroMessage: "Auth config pushed to Base44"
269530
269543
  };
269531
269544
  }
269532
- function getModelCommand() {
269533
- const command = new Base44Command("model", { requireAppContext: false });
269534
- command.description("Pick the builder model for your turns (account-wide). No argument lists models and the current pick; `default` clears it").argument("[model]", 'Model name or id, e.g. "Opus 5" or default').action(modelAction);
269535
- return command;
269545
+ function getAuthPushCommand() {
269546
+ return new Base44Command("push").description("Push local auth config to Base44").option("-y, --yes", "Skip confirmation prompt").action(pushAuthAction);
269536
269547
  }
269537
269548
 
269538
- // src/cli/commands/app/shared.ts
269539
- import { mkdir as mkdir3, writeFile as writeFile2 } from "node:fs/promises";
269540
- import { join as join21 } from "node:path";
269541
-
269542
- // src/cli/commands/code/render.ts
269543
- var TOOL_ALIASES = {
269544
- run_shell_command: "bash",
269545
- read_repo_file: "read",
269546
- write_repo_file: "write",
269547
- edit_repo_file: "edit",
269548
- set_secrets: "secrets",
269549
- generate_development_secrets: "secrets",
269550
- create_pull_request: "pr",
269551
- merge_pull_request: "merge",
269552
- list_pr_threads: "pr threads",
269553
- reply_to_pr_thread: "pr reply",
269554
- comment_on_pr: "pr comment",
269555
- resolve_pr_thread: "pr resolve",
269556
- reload_preview: "reload",
269557
- preview_execute_code: "preview js",
269558
- preview_screenshot: "screenshot",
269559
- connect_github_account: "github"
269549
+ // src/cli/commands/auth/social-login.ts
269550
+ import { dirname as dirname17, join as join23, resolve as resolve6 } from "node:path";
269551
+ var PROVIDER_LABELS = {
269552
+ google: "Google",
269553
+ microsoft: "Microsoft",
269554
+ facebook: "Facebook",
269555
+ apple: "Apple"
269560
269556
  };
269561
- var QUIET_OK_RESULTS = new Set(["read", "write", "edit", "reload"]);
269562
- function toolAlias(name) {
269563
- return TOOL_ALIASES[name] ?? name;
269564
- }
269565
- var ESC_CHAR = String.fromCharCode(27);
269566
- var BEL = String.fromCharCode(7);
269567
- var OSC8_CLOSE = `${ESC_CHAR}]8;;${BEL}`;
269568
- function terminalLink(label, url) {
269569
- return `${ESC_CHAR}]8;;${url}${BEL}${source_default.dim.underline(label)}${OSC8_CLOSE}`;
269570
- }
269571
- function linkifyUrls(text) {
269572
- let n = 0;
269573
- return text.replace(/https?:\/\/[^\s]+/g, (raw) => {
269574
- const trailing = raw.match(/[.,;:!?)\]}'"]+$/)?.[0] ?? "";
269575
- const url = trailing ? raw.slice(0, -trailing.length) : raw;
269576
- const id = `b44-${n++}`;
269577
- return `${ESC_CHAR}]8;id=${id};${url}${BEL}${url}${OSC8_CLOSE}${trailing}`;
269578
- });
269579
- }
269580
- function hardWrapAnsi(text, width) {
269581
- const ESC = new RegExp(`^(?:${ESC_CHAR}\\[[0-9;]*m|${ESC_CHAR}\\]8;[^${BEL}]*${BEL})`);
269582
- const RESET = `${ESC_CHAR}[0m`;
269583
- const out = [];
269584
- for (const logical of text.split(`
269585
- `)) {
269586
- let line = "";
269587
- let visible = 0;
269588
- let active = [];
269589
- let link = "";
269590
- let i = 0;
269591
- while (i < logical.length) {
269592
- const esc = ESC.exec(logical.slice(i));
269593
- if (esc) {
269594
- const seq = esc[0];
269595
- line += seq;
269596
- if (seq === RESET)
269597
- active = [];
269598
- else if (seq.endsWith("m"))
269599
- active.push(seq);
269600
- else if (seq === OSC8_CLOSE)
269601
- link = "";
269602
- else
269603
- link = seq;
269604
- i += seq.length;
269605
- continue;
269606
- }
269607
- if (visible >= width) {
269608
- out.push(`${line}${link ? OSC8_CLOSE : ""}${RESET}`);
269609
- line = active.join("") + link;
269610
- visible = 0;
269611
- }
269612
- line += logical[i];
269613
- visible++;
269614
- i++;
269615
- }
269616
- out.push(line);
269557
+ var VALID_PROVIDER_NAMES = Object.keys(SOCIAL_PROVIDERS);
269558
+ var PROVIDER_OAUTH_CLI = {
269559
+ google: {
269560
+ envVar: "google_oauth_client_secret",
269561
+ promptMessage: "Enter Google OAuth client secret"
269617
269562
  }
269618
- return out;
269563
+ };
269564
+ function hasSecretOptions(options) {
269565
+ return Boolean(options.clientSecret || options.clientSecretStdin || options.envFile);
269619
269566
  }
269620
- function formatDuration2(ms) {
269621
- const seconds = Math.round(ms / 1000);
269622
- if (seconds < 90)
269623
- return `${seconds}s`;
269624
- return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
269567
+ function hasCustomOAuthOptions(options) {
269568
+ return Boolean(options.clientId || hasSecretOptions(options));
269625
269569
  }
269626
- function eventLine(event, elapsedMs) {
269627
- switch (event.kind) {
269628
- case "thinking":
269629
- return source_default.dim(`✻ ${event.text}`);
269630
- case "text":
269631
- return linkifyUrls(event.text);
269632
- case "tool_start":
269633
- return null;
269634
- case "waiting": {
269635
- const what = event.label || toolAlias(event.name);
269636
- return source_default.yellow(`⏸ ${what} — needs your input (answer in the editor)`);
269637
- }
269638
- case "tool_end": {
269639
- const alias = toolAlias(event.name);
269640
- const mark = event.ok ? source_default.green("✓") : source_default.red("✗");
269641
- const title = source_default.bold(event.label || alias);
269642
- const took = elapsedMs != null && elapsedMs >= 3000 ? ` ${source_default.dim(`· ${formatDuration2(elapsedMs)}`)}` : "";
269643
- const inlineDetail = !event.label && event.summary ? ` ${source_default.dim(event.summary)}` : "";
269644
- const paramsLine = event.label && event.summary ? `
269645
- ${source_default.dim(`${alias}: ${event.summary}`)}` : "";
269646
- const head = `${mark} ${title}${inlineDetail}${took}${paramsLine}`;
269647
- if (event.ok && (QUIET_OK_RESULTS.has(alias) || !event.result)) {
269648
- return head;
269570
+ async function socialLoginAction({ log, isNonInteractive, runTask }, provider, action, options) {
269571
+ const shouldEnable = action === "enable";
269572
+ const providerInfo = SOCIAL_PROVIDERS[provider];
269573
+ const label = PROVIDER_LABELS[provider];
269574
+ const hasOAuthOptions = hasCustomOAuthOptions(options);
269575
+ if (hasOAuthOptions && !providerInfo.customOAuth) {
269576
+ throw new InvalidInputError(`Custom OAuth options are only supported for providers with custom OAuth (e.g., google). Use: base44 auth social-login ${provider} ${action}`);
269577
+ }
269578
+ if (hasOAuthOptions && !shouldEnable) {
269579
+ throw new InvalidInputError(`Custom OAuth options cannot be used with disable. To disable ${label} login: base44 auth social-login ${provider} disable`);
269580
+ }
269581
+ if (hasSecretOptions(options) && !options.clientId) {
269582
+ throw new InvalidInputError(`--client-id is required when providing a client secret. Use: base44 auth social-login ${provider} enable --client-id <id> --client-secret <secret>`);
269583
+ }
269584
+ const oauth = providerInfo.customOAuth;
269585
+ const oauthCli = PROVIDER_OAUTH_CLI[provider];
269586
+ const useCustomOAuth = shouldEnable && hasOAuthOptions && oauth != null;
269587
+ let clientSecret;
269588
+ if (useCustomOAuth && oauth && oauthCli && hasSecretOptions(options)) {
269589
+ if (options.envFile) {
269590
+ const secrets = await parseEnvFile(resolve6(options.envFile));
269591
+ const value = secrets[oauthCli.envVar];
269592
+ if (!value) {
269593
+ throw new InvalidInputError(`Key "${oauthCli.envVar}" not found in ${options.envFile}.`);
269649
269594
  }
269650
- const result = event.ok ? source_default.dim(linkifyUrls(event.result)) : source_default.red(linkifyUrls(event.result));
269651
- return `${head}${event.result ? `
269652
- ${result}` : ""}`;
269595
+ clientSecret = value;
269596
+ } else {
269597
+ clientSecret = await resolveSecret({
269598
+ flagValue: options.clientSecret,
269599
+ fromStdin: options.clientSecretStdin,
269600
+ envVar: oauthCli.envVar,
269601
+ promptMessage: oauthCli.promptMessage,
269602
+ isNonInteractive,
269603
+ name: "client secret",
269604
+ hints: [
269605
+ {
269606
+ message: `Provide via flag: base44 auth social-login ${provider} enable --client-id <id> --client-secret <secret>`,
269607
+ command: `base44 auth social-login ${provider} enable --client-id <id> --client-secret <secret>`
269608
+ },
269609
+ {
269610
+ message: `Provide via stdin: echo <secret> | base44 auth social-login ${provider} enable --client-id <id> --client-secret-stdin`
269611
+ },
269612
+ {
269613
+ message: `Provide via env: ${oauthCli.envVar}=<secret> base44 auth social-login ${provider} enable --client-id <id>`
269614
+ }
269615
+ ]
269616
+ });
269653
269617
  }
269654
269618
  }
269619
+ const { project } = await readProjectConfig();
269620
+ const configDir = dirname17(project.configPath);
269621
+ const authDir = join23(configDir, project.authDir);
269622
+ const { config: updated } = await runTask("Updating local auth config", async () => updateSocialLoginConfig(authDir, provider, shouldEnable, useCustomOAuth && options.clientId ? { clientId: options.clientId } : undefined));
269623
+ if (clientSecret) {
269624
+ await runTask("Saving client secret", async () => pushCustomOAuthSecret(provider, clientSecret));
269625
+ }
269626
+ if (!shouldEnable && !hasAnyLoginMethod(updated)) {
269627
+ log.warn(`Disabling ${label} login will leave no login methods enabled. Users will be locked out.`);
269628
+ }
269629
+ const newStatus = shouldEnable ? "enabled" : "disabled";
269630
+ const oauthNote = useCustomOAuth ? " with custom OAuth" : "";
269631
+ let outroMessage = `${label} login ${newStatus}${oauthNote} in local config. Run \`base44 auth push\` or \`base44 deploy\` to apply.`;
269632
+ if (useCustomOAuth && !clientSecret) {
269633
+ outroMessage += `
269634
+ Remember to push the client secret separately: base44 secrets set --env-file <path>`;
269635
+ }
269636
+ return { outroMessage };
269655
269637
  }
269656
- var FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
269657
- var MUSINGS = [
269658
- "Shmoozing",
269659
- "Shmoogling",
269660
- "Percolating",
269661
- "Noodling",
269662
- "Marinating",
269663
- "Brewing",
269664
- "Simmering",
269665
- "Conjuring",
269666
- "Tinkering",
269667
- "Scheming",
269668
- "Pondering",
269669
- "Mulling",
269670
- "Whirring",
269671
- "Crunching",
269672
- "Weaving",
269673
- "Sketching",
269674
- "Hatching",
269675
- "Riffing",
269676
- "Cooking",
269677
- "Composting ideas",
269678
- "Rummaging",
269679
- "Vibing responsibly",
269680
- "Untangling",
269681
- "Squinting at the repo"
269682
- ];
269683
- var MUSING_ROTATE_MS = 6000;
269684
- function idleMusing(seed) {
269685
- return `${MUSINGS[(seed + Math.floor(Date.now() / MUSING_ROTATE_MS)) % MUSINGS.length]}…`;
269686
- }
269687
- function createTurnStream(interactive, write = (text) => process.stdout.write(text), options = {}) {
269688
- const running = new Map;
269689
- const footer = options.footer ?? [];
269690
- let frame = 0;
269691
- let stopped = false;
269692
- let drawnLines = 0;
269693
- const musingSeed = Math.floor(Math.random() * MUSINGS.length);
269694
- const statusLabel = () => {
269695
- if (running.size === 0) {
269696
- if (options.idleLabel)
269697
- return `${options.idleLabel}…`;
269698
- const index = (musingSeed + Math.floor(Date.now() / MUSING_ROTATE_MS)) % MUSINGS.length;
269699
- return `${MUSINGS[index]}…`;
269700
- }
269701
- const newest = [...running.values()].at(-1);
269702
- const elapsed = Math.round((Date.now() - newest.startedAt) / 1000);
269703
- const others = running.size > 1 ? ` (+${running.size - 1} more)` : "";
269704
- const what = newest.label || `${newest.alias}${newest.summary ? ` ${newest.summary}` : ""}`;
269705
- return `${what}${others} · ${elapsed}s`;
269706
- };
269707
- const clearBlock = () => {
269708
- if (!drawnLines)
269709
- return;
269710
- write("\r\x1B[2K");
269711
- for (let i = 1;i < drawnLines; i++)
269712
- write("\x1B[1A\r\x1B[2K");
269713
- drawnLines = 0;
269714
- };
269715
- const drawBlock = () => {
269716
- if (!interactive || stopped)
269717
- return;
269718
- const lines = [
269719
- ...footer.length ? ["", ...footer] : [],
269720
- source_default.dim(`${FRAMES[frame]} ${statusLabel()}`)
269721
- ];
269722
- write(lines.join(`
269723
- `));
269724
- drawnLines = lines.length;
269725
- };
269726
- const tick = () => {
269727
- if (!interactive || stopped)
269728
- return;
269729
- frame = (frame + 1) % FRAMES.length;
269730
- clearBlock();
269731
- drawBlock();
269732
- };
269733
- const timer = interactive ? setInterval(tick, 120) : null;
269734
- if (timer)
269735
- timer.unref?.();
269736
- return {
269737
- onEvent(event) {
269738
- if (event.kind === "tool_start") {
269739
- running.set(event.id, {
269740
- alias: toolAlias(event.name),
269741
- label: event.label,
269742
- summary: event.summary,
269743
- startedAt: Date.now()
269744
- });
269745
- if (interactive) {
269746
- clearBlock();
269747
- drawBlock();
269748
- }
269749
- return;
269750
- }
269751
- let elapsedMs;
269752
- if (event.kind === "tool_end") {
269753
- const started = running.get(event.id)?.startedAt;
269754
- if (started != null)
269755
- elapsedMs = Date.now() - started;
269756
- running.delete(event.id);
269757
- }
269758
- const line = eventLine(event, elapsedMs);
269759
- if (line == null)
269760
- return;
269761
- if (interactive) {
269762
- clearBlock();
269763
- write(`${line}
269764
- `);
269765
- drawBlock();
269766
- } else {
269767
- write(`${line}
269768
- `);
269769
- }
269770
- },
269771
- stop() {
269772
- if (interactive) {
269773
- clearBlock();
269774
- if (footer.length)
269775
- write(`
269776
- ${footer.join(`
269777
- `)}
269778
- `);
269779
- }
269780
- stopped = true;
269781
- if (timer)
269782
- clearInterval(timer);
269783
- }
269784
- };
269638
+ function getSocialLoginCommand() {
269639
+ return new Base44Command("social-login").description("Enable or disable social login providers (google, microsoft, facebook, apple)").addArgument(new Argument2("<provider>", "social login provider").choices(VALID_PROVIDER_NAMES)).addArgument(new Argument2("<action>", "enable or disable the provider").choices([
269640
+ "enable",
269641
+ "disable"
269642
+ ])).option("--client-id <id>", "custom OAuth client ID (Google only)").option("--client-secret <secret>", "custom OAuth client secret (Google only)").option("--client-secret-stdin", "read client secret from stdin (Google only)").option("--env-file <path>", "read client secret from a .env file (Google only)").action(socialLoginAction);
269785
269643
  }
269786
269644
 
269787
- // src/core/resources/apps/api.ts
269788
- var CreatedAppSchema = object({
269789
- id: string2().min(1),
269790
- name: string2().nullish(),
269791
- imported_repo_url: string2().nullish()
269792
- });
269793
- var AppStateSchema = object({
269794
- id: string2(),
269795
- status: object({
269796
- state: string2().nullish(),
269797
- message: string2().nullish()
269798
- }).nullish()
269799
- });
269800
- var ChatTurnSchema = object({
269801
- queued: boolean2().optional(),
269802
- status: object({
269803
- state: string2().nullish(),
269804
- message: string2().nullish(),
269805
- error_source: string2().nullish()
269806
- }).nullish(),
269807
- conversation: object({
269808
- messages: array(object({
269809
- role: string2().nullish(),
269810
- content: unknown().nullish()
269811
- })).nullish()
269812
- }).nullish()
269813
- });
269814
- var PreviewUrlSchema = object({
269815
- preview_url: string2().min(1)
269816
- });
269817
- var ConversationMessageSchema = object({
269818
- id: string2(),
269819
- role: string2(),
269820
- hidden: boolean2().nullish(),
269821
- outcome: unknown().nullish(),
269822
- content: unknown().nullish(),
269823
- reasoning: object({ content: string2().nullish() }).nullish(),
269824
- tool_calls: array(object({
269825
- id: string2(),
269826
- name: string2(),
269827
- arguments_string: string2().nullish(),
269828
- status: string2().nullish(),
269829
- results: unknown().nullish()
269830
- })).nullish()
269831
- });
269832
- var FullConversationSchema = object({
269833
- messages: array(ConversationMessageSchema).default([])
269645
+ // src/cli/commands/auth/sso.ts
269646
+ import { dirname as dirname18, join as join24, resolve as resolve7 } from "node:path";
269647
+ var SSOConfigFileSchema = object({
269648
+ provider: _enum(Object.values(KNOWN_SSO_PROVIDERS)),
269649
+ clientId: string2(),
269650
+ clientSecret: string2(),
269651
+ scope: string2().optional(),
269652
+ discoveryUrl: string2().optional(),
269653
+ tenantId: string2().optional(),
269654
+ oktaDomain: string2().optional(),
269655
+ authEndpoint: string2().optional(),
269656
+ tokenEndpoint: string2().optional(),
269657
+ userinfoEndpoint: string2().optional(),
269658
+ jwksUri: string2().optional(),
269659
+ ssoName: string2().optional()
269834
269660
  });
269835
- var OAuthInitiateSchema = object({ authorization_url: string2().min(1) });
269836
- function parseOrThrow(schema, payload, what) {
269837
- const result = schema.safeParse(payload);
269661
+ async function loadSSOConfigFile(filePath) {
269662
+ const resolved = resolve7(filePath);
269663
+ const raw = await readJsonFile(resolved);
269664
+ const result = SSOConfigFileSchema.safeParse(raw);
269838
269665
  if (!result.success) {
269839
- throw new SchemaValidationError(`Invalid ${what} response from server`, result.error);
269666
+ throw new SchemaValidationError("Invalid SSO config file", result.error, filePath);
269840
269667
  }
269841
269668
  return result.data;
269842
269669
  }
269843
- function branchScope(branchId) {
269844
- return branchId ? { branch_id: branchId } : {};
269670
+ function mergeFileWithFlags(fileConfig, options) {
269671
+ return {
269672
+ provider: options.provider ?? fileConfig.provider,
269673
+ clientId: options.clientId ?? fileConfig.clientId,
269674
+ clientSecret: options.clientSecret ?? fileConfig.clientSecret,
269675
+ clientSecretStdin: options.clientSecretStdin,
269676
+ envFile: options.envFile,
269677
+ scope: options.scope ?? fileConfig.scope,
269678
+ discoveryUrl: options.discoveryUrl ?? fileConfig.discoveryUrl,
269679
+ tenantId: options.tenantId ?? fileConfig.tenantId,
269680
+ oktaDomain: options.oktaDomain ?? fileConfig.oktaDomain,
269681
+ authEndpoint: options.authEndpoint ?? fileConfig.authEndpoint,
269682
+ tokenEndpoint: options.tokenEndpoint ?? fileConfig.tokenEndpoint,
269683
+ userinfoEndpoint: options.userinfoEndpoint ?? fileConfig.userinfoEndpoint,
269684
+ jwksUri: options.jwksUri ?? fileConfig.jwksUri,
269685
+ ssoName: options.ssoName ?? fileConfig.ssoName
269686
+ };
269845
269687
  }
269846
- async function createApp(options) {
269847
- let response;
269848
- try {
269849
- response = await base44Client.post("api/apps", {
269850
- timeout: false,
269851
- json: {
269852
- ...options.appName ? { name: options.appName } : {},
269853
- ...options.organizationId ? { organization_id: options.organizationId } : {},
269854
- ...options.prompt ? { initial_message: { content: options.prompt } } : {}
269855
- }
269688
+ var providerNames = Object.keys(KNOWN_SSO_PROVIDERS);
269689
+ var SECRET_KEY_TO_FLAG = {
269690
+ ["sso_name" /* Name */]: "--sso-name",
269691
+ ["sso_client_id" /* ClientId */]: "--client-id",
269692
+ ["sso_client_secret" /* ClientSecret */]: "--client-secret",
269693
+ ["sso_scope" /* Scope */]: "--scope",
269694
+ ["sso_discovery_url" /* DiscoveryUrl */]: "--discovery-url",
269695
+ ["sso_tenant_id" /* TenantId */]: "--tenant-id",
269696
+ ["sso_auth_endpoint" /* AuthEndpoint */]: "--auth-endpoint",
269697
+ ["sso_token_endpoint" /* TokenEndpoint */]: "--token-endpoint",
269698
+ ["sso_userinfo_endpoint" /* UserinfoEndpoint */]: "--userinfo-endpoint",
269699
+ ["sso_okta_domain" /* OktaDomain */]: "--okta-domain",
269700
+ ["sso_jwks_uri" /* JwksUri */]: "--jwks-uri"
269701
+ };
269702
+ function secretKeyToFlag(key) {
269703
+ return SECRET_KEY_TO_FLAG[key];
269704
+ }
269705
+ function exampleCommand(provider) {
269706
+ let cmd = `base44 auth sso enable --provider ${provider} --client-id <id> --client-secret <secret>`;
269707
+ if (provider === KNOWN_SSO_PROVIDERS.microsoft)
269708
+ cmd += " --tenant-id <id>";
269709
+ if (provider === KNOWN_SSO_PROVIDERS.okta)
269710
+ cmd += " --okta-domain <domain>";
269711
+ if (provider === KNOWN_SSO_PROVIDERS.custom)
269712
+ cmd += " --sso-name <name> --auth-endpoint <url> --token-endpoint <url> --userinfo-endpoint <url> --jwks-uri <url>";
269713
+ return cmd;
269714
+ }
269715
+ function validateProvider(provider) {
269716
+ if (!provider) {
269717
+ throw new InvalidInputError("Missing --provider.", {
269718
+ hints: [
269719
+ {
269720
+ message: `Valid providers: ${providerNames.join(", ")}`,
269721
+ command: "base44 auth sso enable --provider <provider> --client-id <id> --client-secret <secret>"
269722
+ }
269723
+ ]
269856
269724
  });
269857
- } catch (error) {
269858
- throw await ApiError.fromHttpError(error, "creating app");
269859
269725
  }
269860
- return parseOrThrow(CreatedAppSchema, await response.json(), "app");
269726
+ return provider;
269861
269727
  }
269862
- async function createImportedApp(options) {
269863
- let response;
269864
- try {
269865
- response = await base44Client.post("api/apps", {
269866
- timeout: false,
269867
- json: {
269868
- app_type: "imported_app",
269869
- name: options.appName,
269870
- imported_source_mode: options.sourceMode,
269871
- imported_repo_url: options.repoUrl,
269872
- ...options.newRepoName ? { imported_new_repo_name: options.newRepoName } : {},
269873
- ...options.branch ? { imported_branch: options.branch } : {},
269874
- ...options.prompt ? { initial_message: { content: options.prompt } } : {}
269875
- }
269728
+ async function ssoEnableAction({ isNonInteractive, runTask }, options) {
269729
+ if (options.file && options.envFile) {
269730
+ throw new InvalidInputError("--file and --env-file cannot be used together. Provide the client secret either inside --file or via --env-file.");
269731
+ }
269732
+ let merged = options;
269733
+ if (options.file) {
269734
+ const fileConfig = await loadSSOConfigFile(options.file);
269735
+ merged = mergeFileWithFlags(fileConfig, options);
269736
+ }
269737
+ const provider = validateProvider(merged.provider);
269738
+ if (!merged.clientId) {
269739
+ throw new InvalidInputError("Missing --client-id.", {
269740
+ hints: [
269741
+ {
269742
+ message: `Example: base44 auth sso enable --provider ${provider} --client-id <id> --client-secret <secret>`,
269743
+ command: `base44 auth sso enable --provider ${provider} --client-id <id> --client-secret <secret>`
269744
+ }
269745
+ ]
269876
269746
  });
269747
+ }
269748
+ let clientSecret;
269749
+ if (merged.envFile && !merged.clientSecret) {
269750
+ const secrets = await parseEnvFile(resolve7(merged.envFile));
269751
+ const value = secrets.sso_client_secret;
269752
+ if (!value) {
269753
+ throw new InvalidInputError(`Key "sso_client_secret" not found in ${merged.envFile}.`);
269754
+ }
269755
+ clientSecret = value;
269756
+ } else {
269757
+ clientSecret = await resolveSecret({
269758
+ flagValue: merged.clientSecret,
269759
+ fromStdin: merged.clientSecretStdin,
269760
+ envVar: "sso_client_secret",
269761
+ promptMessage: "Enter SSO client secret",
269762
+ isNonInteractive,
269763
+ name: "client secret",
269764
+ hints: [
269765
+ {
269766
+ message: `Provide via flag: base44 auth sso enable --provider ${provider} --client-id <id> --client-secret <secret>`,
269767
+ command: `base44 auth sso enable --provider ${provider} --client-id <id> --client-secret <secret>`
269768
+ },
269769
+ {
269770
+ message: `Provide via stdin: echo <secret> | base44 auth sso enable --provider ${provider} --client-id <id> --client-secret-stdin`
269771
+ },
269772
+ {
269773
+ message: `Provide via env: sso_client_secret=<secret> base44 auth sso enable --provider ${provider} --client-id <id>`
269774
+ }
269775
+ ]
269776
+ });
269777
+ }
269778
+ const secretOptions = {
269779
+ clientId: merged.clientId,
269780
+ clientSecret,
269781
+ scope: merged.scope,
269782
+ discoveryUrl: merged.discoveryUrl,
269783
+ tenantId: merged.tenantId,
269784
+ oktaDomain: merged.oktaDomain,
269785
+ authEndpoint: merged.authEndpoint,
269786
+ tokenEndpoint: merged.tokenEndpoint,
269787
+ userinfoEndpoint: merged.userinfoEndpoint,
269788
+ jwksUri: merged.jwksUri,
269789
+ ssoName: merged.ssoName
269790
+ };
269791
+ let secrets;
269792
+ try {
269793
+ secrets = buildSSOSecrets(provider, secretOptions);
269877
269794
  } catch (error) {
269878
- throw await ApiError.fromHttpError(error, "importing repository");
269795
+ if (error instanceof MissingSSOFieldsError) {
269796
+ const flagNames = error.missingKeys.map(secretKeyToFlag);
269797
+ throw new InvalidInputError(`Missing required fields for ${error.provider}: ${flagNames.join(", ")}`, {
269798
+ hints: [
269799
+ {
269800
+ message: `Example: ${exampleCommand(provider)}`,
269801
+ command: exampleCommand(provider)
269802
+ }
269803
+ ]
269804
+ });
269805
+ }
269806
+ throw error;
269879
269807
  }
269880
- return parseOrThrow(CreatedAppSchema, await response.json(), "app");
269808
+ const { project } = await readProjectConfig();
269809
+ const configDir = dirname18(project.configPath);
269810
+ const authDir = join24(configDir, project.authDir);
269811
+ await runTask("Updating local auth config", async () => updateSSOConfig(authDir, provider, true));
269812
+ await runTask("Saving SSO credentials", async () => pushSSOSecrets(secrets));
269813
+ return {
269814
+ outroMessage: `SSO configured with ${provider} in local config. Run \`base44 auth push\` or \`base44 deploy\` to apply.`
269815
+ };
269816
+ }
269817
+ function hasEnableOnlyOptions(options) {
269818
+ return Boolean(options.provider || options.clientId || options.clientSecret || options.clientSecretStdin || options.envFile || options.file || options.scope || options.discoveryUrl || options.tenantId || options.oktaDomain || options.authEndpoint || options.tokenEndpoint || options.userinfoEndpoint || options.jwksUri || options.ssoName);
269819
+ }
269820
+ async function ssoDisableAction({ log, runTask }, options) {
269821
+ if (hasEnableOnlyOptions(options)) {
269822
+ throw new InvalidInputError("Configuration options cannot be used with disable. To disable SSO: base44 auth sso disable");
269823
+ }
269824
+ const { project } = await readProjectConfig();
269825
+ const configDir = dirname18(project.configPath);
269826
+ const authDir = join24(configDir, project.authDir);
269827
+ const updated = await runTask("Updating local auth config", async () => updateSSOConfig(authDir, null, false));
269828
+ await runTask("Removing SSO credentials", async () => deleteSSOSecrets());
269829
+ if (!hasAnyLoginMethod(updated)) {
269830
+ log.warn("Disabling SSO will leave no login methods enabled. Users will be locked out.");
269831
+ }
269832
+ return {
269833
+ outroMessage: "SSO disabled in local config and credentials removed. Run `base44 auth push` or `base44 deploy` to apply."
269834
+ };
269835
+ }
269836
+ async function ssoAction(context, action, options) {
269837
+ if (action === "disable") {
269838
+ return ssoDisableAction(context, options);
269839
+ }
269840
+ return ssoEnableAction(context, options);
269841
+ }
269842
+ function getSSOCommand() {
269843
+ return new Base44Command("sso").description("Configure SSO identity provider (google, microsoft, github, okta, custom). SSO and social login are mutually exclusive — enabling one disables the other in the local auth config.").addArgument(new Argument2("<action>", "enable or disable SSO").choices([
269844
+ "enable",
269845
+ "disable"
269846
+ ])).addOption(new Option2("--provider <provider>", "SSO provider").choices(Object.values(KNOWN_SSO_PROVIDERS))).option("--client-id <id>", "OAuth client ID").option("--client-secret <secret>", "OAuth client secret").option("--client-secret-stdin", "Read client secret from stdin").option("--env-file <path>", "Read client secret from a .env file (key: sso_client_secret)").option("--file <path>", "JSON config file with all SSO settings").option("--scope <scope>", "OAuth scope (defaults per provider)").option("--discovery-url <url>", "OIDC discovery URL").option("--tenant-id <id>", "Microsoft tenant ID (required for microsoft)").option("--okta-domain <domain>", "Okta domain (required for okta)").option("--auth-endpoint <url>", "Authorization endpoint (required for custom)").option("--token-endpoint <url>", "Token endpoint (required for custom)").option("--userinfo-endpoint <url>", "Userinfo endpoint (required for custom)").option("--jwks-uri <url>", "JWKS URI (required for custom)").option("--sso-name <name>", "Provider display name (required for custom)").action(ssoAction);
269847
+ }
269848
+
269849
+ // src/cli/commands/auth/index.ts
269850
+ function getAuthCommand() {
269851
+ return new Command2("auth").description("Manage app authentication settings").addCommand(getPasswordLoginCommand()).addCommand(getSocialLoginCommand()).addCommand(getSSOCommand()).addCommand(getAuthPullCommand()).addCommand(getAuthPushCommand());
269881
269852
  }
269882
- async function startGithubReauth() {
269883
- const response = await base44Client.post("api/github/oauth/initiate?skip_installation=true");
269884
- return parseOrThrow(OAuthInitiateSchema, await response.json(), "github oauth initiate").authorization_url;
269853
+
269854
+ // src/cli/commands/auth/login.ts
269855
+ function getLoginCommand() {
269856
+ return new Base44Command("login", {
269857
+ requireAuth: false,
269858
+ requireAppContext: false
269859
+ }).description("Authenticate with Base44").action(login);
269885
269860
  }
269886
- function isGithubUserTokenError(error) {
269887
- const message = error instanceof Error ? error.message : String(error);
269888
- return /api\.github\.com/i.test(message) && /\b401\b|unauthorized/i.test(message);
269861
+
269862
+ // src/cli/commands/auth/logout.ts
269863
+ async function logout(_ctx) {
269864
+ await deleteAuth();
269865
+ return { outroMessage: "Logged out successfully" };
269889
269866
  }
269890
- async function getAppState(appId) {
269891
- let response;
269892
- try {
269893
- response = await base44Client.get(`api/apps/${appId}`, {
269894
- searchParams: { fields: "id,status" }
269895
- });
269896
- } catch (error) {
269897
- throw await ApiError.fromHttpError(error, "reading app status");
269898
- }
269899
- return parseOrThrow(AppStateSchema, await response.json(), "app status");
269867
+ function getLogoutCommand() {
269868
+ return new Base44Command("logout", {
269869
+ requireAuth: false,
269870
+ requireAppContext: false
269871
+ }).description("Logout from current device").action(logout);
269900
269872
  }
269901
- async function sendTurn(content, branchId) {
269902
- let response;
269903
- try {
269904
- response = await getAppClient().post("chat/message", {
269905
- timeout: false,
269906
- searchParams: {
269907
- conversation_messages: "current_turn",
269908
- ...branchScope(branchId)
269909
- },
269910
- json: { content }
269911
- });
269912
- } catch (error) {
269913
- throw await ApiError.fromHttpError(error, "sending message");
269873
+
269874
+ // src/cli/commands/auth/whoami.ts
269875
+ async function whoami(_ctx) {
269876
+ const workspaceApiKey = getWorkspaceApiKeyFromEnv();
269877
+ if (workspaceApiKey && isWorkspaceApiKey(workspaceApiKey)) {
269878
+ return {
269879
+ outroMessage: `Using workspace API key: ${theme.styles.bold(workspaceApiKey.slice(0, 10))}`
269880
+ };
269914
269881
  }
269915
- return parseOrThrow(ChatTurnSchema, await response.json(), "chat turn");
269882
+ const auth = await readAuth();
269883
+ return { outroMessage: `Logged in as: ${theme.styles.bold(auth.email)}` };
269916
269884
  }
269917
- async function stopTurn(branchId) {
269918
- try {
269919
- await getAppClient().post("chat/stop", {
269920
- searchParams: branchScope(branchId)
269921
- });
269922
- } catch (error) {
269923
- throw await ApiError.fromHttpError(error, "stopping the turn");
269924
- }
269885
+ function getWhoamiCommand() {
269886
+ return new Base44Command("whoami", { requireAppContext: false }).description("Display current authenticated user").action(whoami);
269925
269887
  }
269926
- async function getFullConversation(limit, branchId) {
269927
- let response;
269888
+
269889
+ // src/cli/commands/branches/index.ts
269890
+ async function listBranchesAction({
269891
+ log,
269892
+ runTask,
269893
+ jsonMode
269894
+ }) {
269895
+ const remote = await runTask("Fetching branches", () => listBranches());
269896
+ const branches = [
269897
+ { name: "main", status: "active" },
269898
+ ...remote.map((branch) => ({
269899
+ name: branch.branch_name,
269900
+ status: branch.status
269901
+ }))
269902
+ ];
269903
+ if (jsonMode)
269904
+ return { stdout: `${JSON.stringify({ branches })}
269905
+ ` };
269906
+ for (const branch of branches)
269907
+ log.message(`${branch.name} (${branch.status})`);
269908
+ return { outroMessage: `${branches.length} branches` };
269909
+ }
269910
+ function getBranchesCommand() {
269911
+ return new Command2("branches").description("Discover an app's branches").addCommand(new Base44Command("list").description("List main and active branch names for use with --branch").action(listBranchesAction));
269912
+ }
269913
+
269914
+ // src/core/model.ts
269915
+ var MODELS = [
269916
+ {
269917
+ name: "Automatic",
269918
+ id: null,
269919
+ note: "matched with the best model",
269920
+ aliases: ["default", "auto"]
269921
+ },
269922
+ { name: "Opus 5", id: "claude_opus_5" },
269923
+ { name: "Sonnet 5", id: "claude-sonnet-5" },
269924
+ { name: "Fable 5", id: "claude_fable_5", note: "uses more credits" },
269925
+ { name: "GPT-5.6 Sol", id: "gpt_5_6_sol" },
269926
+ {
269927
+ name: "Gemini 3.8 Flash",
269928
+ id: "gemini_3_8_flash",
269929
+ note: "fast responses for everyday tasks"
269930
+ },
269931
+ { name: "Base 1", id: "base1" }
269932
+ ];
269933
+ var fold = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
269934
+ function resolvePick(input) {
269935
+ const key = fold(input);
269936
+ const byExact = MODELS.find((m) => fold(m.name) === key || m.id && fold(m.id) === key || m.aliases?.some((a) => fold(a) === key));
269937
+ if (byExact)
269938
+ return byExact;
269939
+ const byContains = MODELS.filter((m) => fold(m.name).includes(key) || m.id && fold(m.id).includes(key));
269940
+ if (byContains.length === 1)
269941
+ return byContains[0];
269942
+ const names = MODELS.map((m) => m.name).join(", ");
269943
+ throw new InvalidInputError(byContains.length > 1 ? `"${input}" is ambiguous — matches ${byContains.map((m) => m.name).join(", ")}.` : `Unknown model "${input}". Choose one of: ${names}.`);
269944
+ }
269945
+ async function getMe() {
269928
269946
  try {
269929
- response = await getAppClient().get("chat/full-conversation", {
269930
- timeout: 30000,
269931
- searchParams: { limit: String(limit), ...branchScope(branchId) }
269932
- });
269947
+ return await base44Client.get("api/auth/me").json();
269933
269948
  } catch (error) {
269934
- throw await ApiError.fromHttpError(error, "reading the conversation");
269949
+ throw await ApiError.fromHttpError(error, "reading your account");
269935
269950
  }
269936
- return parseOrThrow(FullConversationSchema, await response.json(), "conversation").messages;
269937
269951
  }
269938
- async function resolveActiveBranchId() {
269939
- const branches = await listBranches();
269940
- return branches.length === 1 ? branches[0].id : undefined;
269941
- }
269942
- async function getPreviewUrl() {
269943
- let response;
269952
+ async function saveBuilderModel(userId, modelId) {
269944
269953
  try {
269945
- response = await getAppClient().get("sandbox/preview-url", {
269946
- timeout: false
269954
+ await base44Client.post(`api/auth/${userId}/update-user`, {
269955
+ json: { builder_model: modelId }
269947
269956
  });
269948
269957
  } catch (error) {
269949
- throw await ApiError.fromHttpError(error, "fetching preview URL");
269958
+ if (error instanceof HTTPError && error.response.status === 400) {
269959
+ throw new InvalidInputError("This account can't pick a builder model yet — enable the PER_USER_BUILDER_MODEL_SELECTION flag for it in PostHog, or the model isn't available in this workspace.");
269960
+ }
269961
+ throw await ApiError.fromHttpError(error, "saving your model choice");
269950
269962
  }
269951
- const url = parseOrThrow(PreviewUrlSchema, await response.json(), "preview URL").preview_url;
269952
- return /^https?:\/\//.test(url) ? url : `https://${url}`;
269953
269963
  }
269964
+ var displayName = (id) => MODELS.find((m) => m.id === id)?.name ?? id ?? "Automatic";
269954
269965
 
269955
- // src/cli/commands/app/shared.ts
269956
- var APP_NAME_RE = /^[A-Za-z0-9._-]+$/;
269957
- var NAME_STOPWORDS = new Set("a an the and or of for with to in on that this its it my our your me".split(" "));
269958
- var FALLBACK_WORDS = [
269959
- "swift-otter",
269960
- "sunny-comet",
269961
- "tidy-maple",
269962
- "brisk-panda"
269963
- ];
269964
- function inventAppName(prompt) {
269965
- const suffix = Math.random().toString(36).slice(2, 5);
269966
- const words = (prompt ?? "").toLowerCase().match(/[a-z0-9]+/g)?.filter((w) => w.length > 2 && !NAME_STOPWORDS.has(w)).slice(0, 3) ?? [];
269967
- const core = words.length ? words.join("-") : FALLBACK_WORDS[Math.floor(Math.random() * FALLBACK_WORDS.length)];
269968
- return `base44-${core}-${suffix}`.slice(0, 60);
269969
- }
269970
- function repoBasename(repoUrl) {
269971
- return repoUrl.replace(/\/+$/, "").replace(/\.git$/, "").split("/").pop() ?? "app";
269972
- }
269973
- async function createAndLinkApp(options) {
269974
- if (options.name && !APP_NAME_RE.test(options.name)) {
269975
- throw new InvalidInputError("The name becomes a directory — letters, digits, dots, dashes and underscores only.");
269966
+ // src/cli/commands/builder/model.ts
269967
+ async function modelAction({ log, jsonMode }, input) {
269968
+ const me = await getMe();
269969
+ const current = me.builder_model ?? null;
269970
+ if (!input) {
269971
+ if (jsonMode) {
269972
+ return {
269973
+ stdout: `${JSON.stringify({
269974
+ current,
269975
+ models: MODELS.map((m) => ({ name: m.name, id: m.id }))
269976
+ })}
269977
+ `
269978
+ };
269979
+ }
269980
+ for (const m of MODELS) {
269981
+ const active = (m.id ?? null) === current;
269982
+ const marker = active ? theme.styles.bold("") : theme.styles.dim("");
269983
+ const note = m.note ? theme.styles.dim(` (${m.note})`) : "";
269984
+ log.message(`${marker} ${active ? theme.styles.bold(m.name) : m.name}${note}`);
269985
+ }
269986
+ return {
269987
+ outroMessage: `Current: ${theme.styles.bold(displayName(current))}. Set with \`base44 builder model <name>\`.`
269988
+ };
269976
269989
  }
269977
- const name = options.name ?? (options.importRepo ? repoBasename(options.importRepo) : inventAppName(options.prompt));
269978
- const targetDir = join21(process.cwd(), name);
269979
- await mkdir3(targetDir, { recursive: true });
269980
- if (await appConfigExists(targetDir)) {
269981
- throw new InvalidInputError(`./${name} is already linked to a Base44 app. Pick another name.`);
269990
+ const pick = resolvePick(input);
269991
+ if ((pick.id ?? null) === current) {
269992
+ return { outroMessage: `Already on ${theme.styles.bold(pick.name)}.` };
269982
269993
  }
269983
- const created = options.importRepo ? await createImportedApp({
269984
- appName: name,
269985
- repoUrl: options.importRepo,
269986
- sourceMode: options.mode ?? "direct",
269987
- newRepoName: options.repoName,
269988
- branch: options.fromBranch,
269989
- prompt: options.prompt
269990
- }) : await createApp({ appName: name, prompt: options.prompt });
269991
- await writeAppConfig(targetDir, created.id);
269992
- await mkdir3(join21(targetDir, "base44"), { recursive: true });
269993
- try {
269994
- await writeFile2(join21(targetDir, "base44", "config.jsonc"), `// Base44 project configuration.
269995
- {
269996
- "name": ${JSON.stringify(name)}
269997
- }
269998
- `, { flag: "wx" });
269999
- } catch {}
270000
- setAppContext({ id: created.id, projectRoot: targetDir });
269994
+ await saveBuilderModel(me.id, pick.id);
269995
+ if (jsonMode)
269996
+ return { stdout: `${JSON.stringify({ current: pick.id })}
269997
+ ` };
270001
269998
  return {
270002
- id: created.id,
270003
- editorUrl: `${getBase44ApiUrl()}/apps/${created.id}/editor/preview`,
270004
- repoUrl: created.imported_repo_url ?? undefined,
270005
- dirName: name,
270006
- targetDir
269999
+ outroMessage: pick.id === null ? "Model reset — Base44 chooses per app again." : `Builder model set to ${theme.styles.bold(pick.name)} for every new turn.`
270007
270000
  };
270008
270001
  }
270009
- async function githubReauthLines(error) {
270010
- if (!isGithubUserTokenError(error))
270011
- return null;
270012
- const link = await startGithubReauth().catch(() => null);
270013
- return [
270014
- "Your GitHub authorization expired. Reconnect, then run this again:",
270015
- link ? terminalLink("Reconnect GitHub", link) : "Open Base44 → GitHub settings to reconnect your account."
270016
- ];
270017
- }
270018
- async function resolveBranchId(ctx) {
270019
- return ctx.branchId ?? await resolveActiveBranchId().catch(() => {
270020
- return;
270021
- });
270002
+ function getModelCommand() {
270003
+ const command = new Base44Command("model", { requireAppContext: false });
270004
+ command.description("Pick the builder model for your turns (account-wide). No argument lists models and the current pick; `default` clears it").argument("[model]", 'Model name or id, e.g. "Opus 5" or default').action(modelAction);
270005
+ return command;
270022
270006
  }
270023
270007
 
270024
- // src/core/resources/apps/stream.ts
270025
- function newStreamState() {
270026
- return { perMessage: new Map };
270008
+ // src/cli/commands/builder/shared.ts
270009
+ import { mkdir as mkdir3, writeFile as writeFile2 } from "node:fs/promises";
270010
+ import { basename as basename6, join as join25, relative as relative6, resolve as resolve8 } from "node:path";
270011
+
270012
+ // src/cli/commands/code/render.ts
270013
+ var TOOL_ALIASES = {
270014
+ run_shell_command: "bash",
270015
+ read_repo_file: "read",
270016
+ write_repo_file: "write",
270017
+ edit_repo_file: "edit",
270018
+ set_secrets: "secrets",
270019
+ generate_development_secrets: "secrets",
270020
+ create_pull_request: "pr",
270021
+ merge_pull_request: "merge",
270022
+ list_pr_threads: "pr threads",
270023
+ reply_to_pr_thread: "pr reply",
270024
+ comment_on_pr: "pr comment",
270025
+ resolve_pr_thread: "pr resolve",
270026
+ reload_preview: "reload",
270027
+ preview_execute_code: "preview js",
270028
+ preview_screenshot: "screenshot",
270029
+ connect_github_account: "github"
270030
+ };
270031
+ var QUIET_OK_RESULTS = new Set(["read", "write", "edit", "reload"]);
270032
+ function toolAlias(name) {
270033
+ return TOOL_ALIASES[name] ?? name;
270027
270034
  }
270028
- var TOOL_SETTLED = new Set(["success", "error", "stopped"]);
270029
- function oneLine(value, max) {
270030
- const text = typeof value === "string" ? value : value == null ? "" : JSON.stringify(value);
270031
- const flat = text.replace(/\s+/g, " ").trim();
270032
- return flat.length > max ? `${flat.slice(0, max)}…` : flat;
270035
+ var ESC_CHAR = String.fromCharCode(27);
270036
+ var BEL = String.fromCharCode(7);
270037
+ var OSC8_CLOSE = `${ESC_CHAR}]8;;${BEL}`;
270038
+ function terminalLink(label, url) {
270039
+ return `${ESC_CHAR}]8;;${url}${BEL}${source_default.dim.underline(label)}${OSC8_CLOSE}`;
270033
270040
  }
270034
- var SALIENT_KEYS = ["command", "path", "file_path", "title"];
270035
- function salvageKey(raw, keys) {
270036
- for (const key of keys) {
270037
- const match = raw.match(new RegExp(`"${key}"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)"`));
270038
- if (match?.[1])
270039
- return match[1].replace(/\\(.)/g, "$1");
270040
- }
270041
- return;
270041
+ function linkifyUrls(text) {
270042
+ let n = 0;
270043
+ return text.replace(/https?:\/\/[^\s]+/g, (raw) => {
270044
+ const trailing = raw.match(/[.,;:!?)\]}'"]+$/)?.[0] ?? "";
270045
+ const url = trailing ? raw.slice(0, -trailing.length) : raw;
270046
+ const id = `b44-${n++}`;
270047
+ return `${ESC_CHAR}]8;id=${id};${url}${BEL}${url}${OSC8_CLOSE}${trailing}`;
270048
+ });
270042
270049
  }
270043
- function toolMeta(name, argumentsString) {
270044
- const raw = argumentsString ?? "";
270045
- let args = {};
270046
- try {
270047
- const parsed = JSON.parse(raw);
270048
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
270049
- args = parsed;
270050
+ function hardWrapAnsi(text, width) {
270051
+ const ESC = new RegExp(`^(?:${ESC_CHAR}\\[[0-9;]*m|${ESC_CHAR}\\]8;[^${BEL}]*${BEL})`);
270052
+ const RESET = `${ESC_CHAR}[0m`;
270053
+ const out = [];
270054
+ for (const logical of text.split(`
270055
+ `)) {
270056
+ let line = "";
270057
+ let visible = 0;
270058
+ let active = [];
270059
+ let link = "";
270060
+ let i = 0;
270061
+ while (i < logical.length) {
270062
+ const esc = ESC.exec(logical.slice(i));
270063
+ if (esc) {
270064
+ const seq = esc[0];
270065
+ line += seq;
270066
+ if (seq === RESET)
270067
+ active = [];
270068
+ else if (seq.endsWith("m"))
270069
+ active.push(seq);
270070
+ else if (seq === OSC8_CLOSE)
270071
+ link = "";
270072
+ else
270073
+ link = seq;
270074
+ i += seq.length;
270075
+ continue;
270076
+ }
270077
+ if (visible >= width) {
270078
+ out.push(`${line}${link ? OSC8_CLOSE : ""}${RESET}`);
270079
+ line = active.join("") + link;
270080
+ visible = 0;
270081
+ }
270082
+ line += logical[i];
270083
+ visible++;
270084
+ i++;
270050
270085
  }
270051
- } catch {
270052
- return {
270053
- label: oneLine(salvageKey(raw, ["summary"]) ?? "", 90),
270054
- summary: oneLine(salvageKey(raw, SALIENT_KEYS) ?? raw, 90)
270055
- };
270086
+ out.push(line);
270056
270087
  }
270057
- const pick = (key) => typeof args[key] === "string" && args[key].trim() ? args[key] : undefined;
270058
- const salient = {
270059
- run_shell_command: pick("command"),
270060
- read_repo_file: pick("path") ?? pick("file_path"),
270061
- write_repo_file: pick("path") ?? pick("file_path"),
270062
- edit_repo_file: pick("path") ?? pick("file_path"),
270063
- create_pull_request: pick("title"),
270064
- reload_preview: ""
270065
- };
270066
- const summary = salient[name] ?? Object.entries(args).find(([key, v]) => key !== "summary" && typeof v === "string" && v.trim().length > 0)?.[1] ?? "";
270067
- return {
270068
- label: oneLine(pick("summary") ?? "", 90),
270069
- summary: oneLine(summary, 90)
270070
- };
270088
+ return out;
270071
270089
  }
270072
- function labelTense(label, tense) {
270073
- const parts = label.split(/\s*\|\s*/);
270074
- if (parts.length < 2)
270075
- return label;
270076
- return tense === "running" ? parts[0] : parts[1];
270090
+ function formatDuration2(ms) {
270091
+ const seconds = Math.round(ms / 1000);
270092
+ if (seconds < 90)
270093
+ return `${seconds}s`;
270094
+ return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
270077
270095
  }
270078
- function progressFor(state, id) {
270079
- let progress = state.perMessage.get(id);
270080
- if (!progress) {
270081
- progress = {
270082
- contentLength: 0,
270083
- reasoningLength: 0,
270084
- announcedTools: new Map,
270085
- settledTools: new Set,
270086
- waitingNotified: new Set
270087
- };
270088
- state.perMessage.set(id, progress);
270096
+ function eventLine(event, elapsedMs) {
270097
+ switch (event.kind) {
270098
+ case "thinking":
270099
+ return source_default.dim(`✻ ${event.text}`);
270100
+ case "text":
270101
+ return linkifyUrls(event.text);
270102
+ case "tool_start":
270103
+ return null;
270104
+ case "waiting": {
270105
+ const what = event.label || toolAlias(event.name);
270106
+ return source_default.yellow(`⏸ ${what} — needs your input (answer in the editor)`);
270107
+ }
270108
+ case "tool_end": {
270109
+ const alias = toolAlias(event.name);
270110
+ const mark = event.ok ? source_default.green("✓") : source_default.red("✗");
270111
+ const title = source_default.bold(event.label || alias);
270112
+ const took = elapsedMs != null && elapsedMs >= 3000 ? ` ${source_default.dim(`· ${formatDuration2(elapsedMs)}`)}` : "";
270113
+ const inlineDetail = !event.label && event.summary ? ` ${source_default.dim(event.summary)}` : "";
270114
+ const paramsLine = event.label && event.summary ? `
270115
+ ${source_default.dim(`${alias}: ${event.summary}`)}` : "";
270116
+ const head = `${mark} ${title}${inlineDetail}${took}${paramsLine}`;
270117
+ if (event.ok && (QUIET_OK_RESULTS.has(alias) || !event.result)) {
270118
+ return head;
270119
+ }
270120
+ const result = event.ok ? source_default.dim(linkifyUrls(event.result)) : source_default.red(linkifyUrls(event.result));
270121
+ return `${head}${event.result ? `
270122
+ ${result}` : ""}`;
270123
+ }
270089
270124
  }
270090
- return progress;
270091
270125
  }
270092
- function diffConversation(state, messages) {
270093
- const events = [];
270094
- for (const message of messages) {
270095
- if (message.role !== "assistant" || message.hidden)
270096
- continue;
270097
- const progress = progressFor(state, message.id);
270098
- const reasoning = message.reasoning?.content ?? "";
270099
- if (reasoning.length > progress.reasoningLength) {
270100
- const delta = reasoning.slice(progress.reasoningLength).trim();
270101
- if (delta)
270102
- events.push({ kind: "thinking", text: oneLine(delta, 300) });
270103
- progress.reasoningLength = reasoning.length;
270104
- }
270105
- if (typeof message.content === "string" && message.content.length > progress.contentLength) {
270106
- const delta = message.content.slice(progress.contentLength).trim();
270107
- if (delta)
270108
- events.push({ kind: "text", text: delta });
270109
- progress.contentLength = message.content.length;
270126
+ var FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
270127
+ var MUSINGS = [
270128
+ "Shmoozing",
270129
+ "Shmoogling",
270130
+ "Percolating",
270131
+ "Noodling",
270132
+ "Marinating",
270133
+ "Brewing",
270134
+ "Simmering",
270135
+ "Conjuring",
270136
+ "Tinkering",
270137
+ "Scheming",
270138
+ "Pondering",
270139
+ "Mulling",
270140
+ "Whirring",
270141
+ "Crunching",
270142
+ "Weaving",
270143
+ "Sketching",
270144
+ "Hatching",
270145
+ "Riffing",
270146
+ "Cooking",
270147
+ "Composting ideas",
270148
+ "Rummaging",
270149
+ "Vibing responsibly",
270150
+ "Untangling",
270151
+ "Squinting at the repo"
270152
+ ];
270153
+ var MUSING_ROTATE_MS = 6000;
270154
+ function idleMusing(seed) {
270155
+ return `${MUSINGS[(seed + Math.floor(Date.now() / MUSING_ROTATE_MS)) % MUSINGS.length]}…`;
270156
+ }
270157
+ function createTurnStream(interactive, write = (text) => process.stdout.write(text), options = {}) {
270158
+ const running = new Map;
270159
+ const footer = options.footer ?? [];
270160
+ let frame = 0;
270161
+ let stopped = false;
270162
+ let drawnLines = 0;
270163
+ const musingSeed = Math.floor(Math.random() * MUSINGS.length);
270164
+ const statusLabel = () => {
270165
+ if (running.size === 0) {
270166
+ if (options.idleLabel)
270167
+ return `${options.idleLabel}…`;
270168
+ const index = (musingSeed + Math.floor(Date.now() / MUSING_ROTATE_MS)) % MUSINGS.length;
270169
+ return `${MUSINGS[index]}…`;
270110
270170
  }
270111
- for (const tool of message.tool_calls ?? []) {
270112
- if (!progress.announcedTools.has(tool.id)) {
270113
- progress.announcedTools.set(tool.id, toolMeta(tool.name, tool.arguments_string));
270114
- const meta = progress.announcedTools.get(tool.id);
270115
- events.push({
270116
- kind: "tool_start",
270117
- id: tool.id,
270118
- name: tool.name,
270119
- label: labelTense(meta.label, "running"),
270120
- summary: meta.summary
270171
+ const newest = [...running.values()].at(-1);
270172
+ const elapsed = Math.round((Date.now() - newest.startedAt) / 1000);
270173
+ const others = running.size > 1 ? ` (+${running.size - 1} more)` : "";
270174
+ const what = newest.label || `${newest.alias}${newest.summary ? ` ${newest.summary}` : ""}`;
270175
+ return `${what}${others} · ${elapsed}s`;
270176
+ };
270177
+ const clearBlock = () => {
270178
+ if (!drawnLines)
270179
+ return;
270180
+ write("\r\x1B[2K");
270181
+ for (let i = 1;i < drawnLines; i++)
270182
+ write("\x1B[1A\r\x1B[2K");
270183
+ drawnLines = 0;
270184
+ };
270185
+ const drawBlock = () => {
270186
+ if (!interactive || stopped)
270187
+ return;
270188
+ const lines = [
270189
+ ...footer.length ? ["", ...footer] : [],
270190
+ source_default.dim(`${FRAMES[frame]} ${statusLabel()}`)
270191
+ ];
270192
+ write(lines.join(`
270193
+ `));
270194
+ drawnLines = lines.length;
270195
+ };
270196
+ const tick = () => {
270197
+ if (!interactive || stopped)
270198
+ return;
270199
+ frame = (frame + 1) % FRAMES.length;
270200
+ clearBlock();
270201
+ drawBlock();
270202
+ };
270203
+ const timer = interactive ? setInterval(tick, 120) : null;
270204
+ if (timer)
270205
+ timer.unref?.();
270206
+ return {
270207
+ onEvent(event) {
270208
+ if (event.kind === "tool_start") {
270209
+ running.set(event.id, {
270210
+ alias: toolAlias(event.name),
270211
+ label: event.label,
270212
+ summary: event.summary,
270213
+ startedAt: Date.now()
270121
270214
  });
270215
+ if (interactive) {
270216
+ clearBlock();
270217
+ drawBlock();
270218
+ }
270219
+ return;
270122
270220
  }
270123
- const status = tool.status ?? "running";
270124
- if (status === "waiting_for_user_input" && !progress.waitingNotified.has(tool.id)) {
270125
- progress.waitingNotified.add(tool.id);
270126
- const meta = progress.announcedTools.get(tool.id);
270127
- events.push({
270128
- kind: "waiting",
270129
- id: tool.id,
270130
- name: tool.name,
270131
- label: labelTense(meta.label, "running")
270132
- });
270221
+ let elapsedMs;
270222
+ if (event.kind === "tool_end") {
270223
+ const started = running.get(event.id)?.startedAt;
270224
+ if (started != null)
270225
+ elapsedMs = Date.now() - started;
270226
+ running.delete(event.id);
270133
270227
  }
270134
- if (TOOL_SETTLED.has(status) && !progress.settledTools.has(tool.id)) {
270135
- progress.settledTools.add(tool.id);
270136
- const meta = progress.announcedTools.get(tool.id);
270137
- events.push({
270138
- kind: "tool_end",
270139
- id: tool.id,
270140
- name: tool.name,
270141
- label: labelTense(meta.label, "done"),
270142
- summary: meta.summary,
270143
- ok: status === "success",
270144
- result: oneLine(tool.results, 110)
270145
- });
270228
+ const line = eventLine(event, elapsedMs);
270229
+ if (line == null)
270230
+ return;
270231
+ if (interactive) {
270232
+ clearBlock();
270233
+ write(`${line}
270234
+ `);
270235
+ drawBlock();
270236
+ } else {
270237
+ write(`${line}
270238
+ `);
270146
270239
  }
270147
- }
270148
- }
270149
- return events;
270150
- }
270151
- function turnSettled(messages) {
270152
- return newestUserTurn(messages)?.settled ?? false;
270153
- }
270154
- function newestUserTurn(messages) {
270155
- for (let i = messages.length - 1;i >= 0; i--) {
270156
- const message = messages[i];
270157
- if (message.role === "user" && !message.hidden) {
270158
- const outcome = message.outcome;
270159
- const backendStatus = outcome && typeof outcome === "object" ? outcome.backend_status : undefined;
270160
- return {
270161
- id: message.id,
270162
- settled: outcome != null && backendStatus !== "pending",
270163
- backendStatus
270164
- };
270165
- }
270166
- }
270167
- return null;
270168
- }
270169
- var sleep3 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
270170
- function makePoller(onEvent, options) {
270171
- const state = newStreamState();
270172
- return async (prime = false) => {
270173
- try {
270174
- const messages = await getFullConversation(30, options.branchId);
270175
- const events = diffConversation(state, messages);
270176
- if (!prime)
270177
- for (const event of events)
270178
- onEvent(event);
270179
- return messages;
270180
- } catch {
270181
- return [];
270240
+ },
270241
+ stop() {
270242
+ if (interactive) {
270243
+ clearBlock();
270244
+ if (footer.length)
270245
+ write(`
270246
+ ${footer.join(`
270247
+ `)}
270248
+ `);
270249
+ }
270250
+ stopped = true;
270251
+ if (timer)
270252
+ clearInterval(timer);
270182
270253
  }
270183
270254
  };
270184
270255
  }
270185
- async function streamConversationDuring(start, onEvent, options = {}) {
270186
- const intervalMs = options.intervalMs ?? 1000;
270187
- const poll = makePoller(onEvent, options);
270188
- await poll(true);
270189
- const work = start();
270190
- let pending = true;
270191
- const settled = work.then(() => {
270192
- pending = false;
270193
- }, () => {
270194
- pending = false;
270195
- });
270196
- while (pending) {
270197
- await Promise.race([sleep3(intervalMs), settled]);
270198
- if (!pending)
270199
- break;
270200
- await poll();
270201
- }
270202
- await poll();
270203
- return work;
270204
- }
270205
- async function streamConversationUntilSettled(onEvent, options = {}) {
270206
- const intervalMs = options.intervalMs ?? 1000;
270207
- const deadline = Date.now() + (options.timeoutMs ?? 20 * 60000);
270208
- const poll = makePoller(onEvent, options);
270209
- while (Date.now() < deadline) {
270210
- const messages = await poll();
270211
- if (messages.length > 0 && turnSettled(messages))
270212
- return "settled";
270213
- await sleep3(intervalMs);
270214
- }
270215
- return "timeout";
270216
- }
270217
270256
 
270218
- // src/cli/commands/app/new.ts
270219
- var POLL_TIMEOUT_MS = 20 * 60000;
270220
- var MODES = ["direct", "fork", "copy"];
270221
- async function newAction({ log, runTask, jsonMode }, prompt, options) {
270222
- if (options.mode && !MODES.includes(options.mode)) {
270223
- throw new InvalidInputError("--mode must be direct, fork, or copy.");
270224
- }
270225
- if ((options.mode || options.repoName || options.fromBranch) && !options.import) {
270226
- throw new InvalidInputError("--mode, --repo-name and --from-branch apply only with --import <repo>.");
270227
- }
270228
- if (!prompt && !options.import) {
270229
- throw new InvalidInputError('Describe the app ("<prompt>") or pass --import <repo>.');
270230
- }
270231
- let app;
270232
- try {
270233
- app = await runTask(options.import ? "Importing the repository" : "Creating your app", () => createAndLinkApp({
270234
- prompt,
270235
- name: options.name,
270236
- importRepo: options.import,
270237
- mode: options.mode,
270238
- repoName: options.repoName,
270239
- fromBranch: options.fromBranch
270240
- }));
270241
- } catch (error) {
270242
- for (const line of await githubReauthLines(error) ?? [])
270243
- log.message(line);
270244
- throw error;
270245
- }
270246
- if (!jsonMode) {
270247
- if (app.repoUrl)
270248
- log.message(source_default.dim(`repo ${app.repoUrl}`));
270249
- log.message(source_default.dim(`editor ${app.editorUrl}`));
270250
- log.message(source_default.dim(`linked ./${app.dirName}`));
270251
- }
270252
- let finalState;
270253
- let previewUrl;
270254
- const startedAt = Date.now();
270255
- if (prompt) {
270256
- const branchId = await resolveActiveBranchId().catch(() => {
270257
- return;
270258
- });
270259
- const stream = createTurnStream(process.stdout.isTTY === true && !jsonMode, undefined, { idleLabel: "provisioning the sandbox and starting the build" });
270260
- try {
270261
- const settled = await streamConversationUntilSettled((event) => {
270262
- if (!jsonMode)
270263
- stream.onEvent(event);
270264
- }, { branchId, timeoutMs: POLL_TIMEOUT_MS });
270265
- finalState = settled === "timeout" ? "processing" : (await getAppState(app.id)).status?.state ?? "ready";
270266
- if (finalState === "ready") {
270267
- previewUrl = await getPreviewUrl().catch(() => {
270268
- return;
270269
- });
270270
- }
270271
- } finally {
270272
- stream.stop();
270273
- }
270274
- }
270275
- if (jsonMode) {
270276
- return {
270277
- stdout: `${JSON.stringify({
270278
- id: app.id,
270279
- repo_url: app.repoUrl ?? null,
270280
- editor_url: app.editorUrl,
270281
- preview_url: previewUrl ?? null,
270282
- status: finalState ?? "created"
270283
- })}
270284
- `
270285
- };
270286
- }
270287
- if (previewUrl)
270288
- log.message(`preview ${previewUrl}`);
270289
- const cd = ` Next: cd ${app.dirName}`;
270290
- if (finalState === "error") {
270291
- return {
270292
- outroMessage: `The first build reported an error — open the editor for details.${cd}`
270293
- };
270257
+ // src/core/resources/apps/api.ts
270258
+ var CreatedAppSchema = object({
270259
+ id: string2().min(1),
270260
+ name: string2().nullish(),
270261
+ imported_repo_url: string2().nullish()
270262
+ });
270263
+ var AppStateSchema = object({
270264
+ id: string2(),
270265
+ app_type: string2().nullish(),
270266
+ is_managed_source_code: boolean2().nullish(),
270267
+ imported_repo_url: string2().nullish(),
270268
+ status: object({
270269
+ state: string2().nullish(),
270270
+ message: string2().nullish()
270271
+ }).nullish()
270272
+ });
270273
+ var ChatTurnSchema = object({
270274
+ queued: boolean2().optional(),
270275
+ status: object({
270276
+ state: string2().nullish(),
270277
+ message: string2().nullish(),
270278
+ error_source: string2().nullish()
270279
+ }).nullish(),
270280
+ conversation: object({
270281
+ messages: array(object({
270282
+ role: string2().nullish(),
270283
+ content: unknown().nullish()
270284
+ })).nullish()
270285
+ }).nullish()
270286
+ });
270287
+ var PreviewUrlSchema = object({
270288
+ preview_url: string2().min(1)
270289
+ });
270290
+ var ConversationMessageSchema = object({
270291
+ id: string2(),
270292
+ role: string2(),
270293
+ hidden: boolean2().nullish(),
270294
+ outcome: unknown().nullish(),
270295
+ content: unknown().nullish(),
270296
+ reasoning: object({ content: string2().nullish() }).nullish(),
270297
+ tool_calls: array(object({
270298
+ id: string2(),
270299
+ name: string2(),
270300
+ arguments_string: string2().nullish(),
270301
+ status: string2().nullish(),
270302
+ results: unknown().nullish()
270303
+ })).nullish()
270304
+ });
270305
+ var FullConversationSchema = object({
270306
+ messages: array(ConversationMessageSchema).default([])
270307
+ });
270308
+ var OAuthInitiateSchema = object({ authorization_url: string2().min(1) });
270309
+ function parseOrThrow(schema, payload, what) {
270310
+ const result = schema.safeParse(payload);
270311
+ if (!result.success) {
270312
+ throw new SchemaValidationError(`Invalid ${what} response from server`, result.error);
270294
270313
  }
270295
- if (finalState === "processing") {
270296
- return {
270297
- outroMessage: `Still building — follow it with \`base44 app status\`.${cd}`
270298
- };
270314
+ return result.data;
270315
+ }
270316
+ function branchScope(branchId) {
270317
+ return branchId ? { branch_id: branchId } : {};
270318
+ }
270319
+ async function createApp(options) {
270320
+ let response;
270321
+ try {
270322
+ response = await base44Client.post("api/apps", {
270323
+ timeout: false,
270324
+ json: {
270325
+ ...options.appName ? { name: options.appName } : {},
270326
+ ...options.organizationId ? { organization_id: options.organizationId } : {},
270327
+ ...options.prompt ? { initial_message: { content: options.prompt } } : {}
270328
+ }
270329
+ });
270330
+ } catch (error) {
270331
+ throw await ApiError.fromHttpError(error, "creating app");
270299
270332
  }
270300
- return {
270301
- outroMessage: prompt ? `First build finished · ${formatDuration2(Date.now() - startedAt)}.${cd}` : `App created.${cd}`
270302
- };
270333
+ return parseOrThrow(CreatedAppSchema, await response.json(), "app");
270303
270334
  }
270304
- function getNewCommand() {
270305
- const command = new Base44Command("new", { requireAppContext: false });
270306
- command.description("Create an app and start building: from a prompt (the Base44 template), or over an existing GitHub repo with --import").argument("[prompt]", "What to build; the first agent turn starts immediately").option("--import <repo>", "Build over an existing GitHub repository instead of the template").option("--mode <mode>", "How to import: direct, fork, or copy (default: direct)").option("--name <name>", "Directory and app name (invented when omitted)").option("--repo-name <name>", "Name for the new GitHub repo when forking/copying").option("--from-branch <name>", "Import a specific branch of the repo").action(newAction);
270307
- return command;
270335
+ async function createImportedApp(options) {
270336
+ let response;
270337
+ try {
270338
+ response = await base44Client.post("api/apps", {
270339
+ timeout: false,
270340
+ json: {
270341
+ app_type: "imported_app",
270342
+ name: options.appName,
270343
+ imported_source_mode: options.sourceMode,
270344
+ imported_repo_url: options.repoUrl,
270345
+ ...options.newRepoName ? { imported_new_repo_name: options.newRepoName } : {},
270346
+ ...options.branch ? { imported_branch: options.branch } : {},
270347
+ ...options.prompt ? { initial_message: { content: options.prompt } } : {}
270348
+ }
270349
+ });
270350
+ } catch (error) {
270351
+ throw await ApiError.fromHttpError(error, "importing repository");
270352
+ }
270353
+ return parseOrThrow(CreatedAppSchema, await response.json(), "app");
270308
270354
  }
270309
-
270310
- // src/cli/commands/app/preview.ts
270311
- async function previewAction(ctx) {
270312
- const url = await ctx.runTask("Resolving preview URL (boots the sandbox if needed)", () => getPreviewUrl());
270313
- if (ctx.jsonMode)
270314
- return { stdout: `${JSON.stringify({ preview_url: url })}
270315
- ` };
270316
- ctx.log.message(url);
270317
- return { outroMessage: "Preview is live." };
270355
+ async function startGithubReauth() {
270356
+ const response = await base44Client.post("api/github/oauth/initiate?skip_installation=true");
270357
+ return parseOrThrow(OAuthInitiateSchema, await response.json(), "github oauth initiate").authorization_url;
270318
270358
  }
270319
- function getPreviewCommand() {
270320
- const command = new Base44Command("preview");
270321
- command.description("Print the app's live preview URL").action(previewAction);
270322
- return command;
270359
+ function isGithubUserTokenError(error) {
270360
+ const message = error instanceof Error ? error.message : String(error);
270361
+ return /api\.github\.com/i.test(message) && /\b401\b|unauthorized/i.test(message);
270323
270362
  }
270324
-
270325
- // src/cli/commands/app/send.ts
270326
- function lastAssistantReply(turn) {
270327
- const messages = turn.conversation?.messages ?? [];
270328
- for (let i = messages.length - 1;i >= 0; i--) {
270329
- const { role, content } = messages[i];
270330
- if (role === "assistant" && typeof content === "string" && content.trim()) {
270331
- return content.trim();
270332
- }
270363
+ async function getAppState(appId) {
270364
+ let response;
270365
+ try {
270366
+ response = await base44Client.get(`api/apps/${appId}`, {
270367
+ searchParams: {
270368
+ fields: "id,status,app_type,is_managed_source_code,imported_repo_url"
270369
+ }
270370
+ });
270371
+ } catch (error) {
270372
+ throw await ApiError.fromHttpError(error, "reading app status");
270333
270373
  }
270334
- return;
270374
+ return parseOrThrow(AppStateSchema, await response.json(), "app status");
270335
270375
  }
270336
- async function sendAction(ctx, message) {
270337
- const branchId = await resolveBranchId(ctx);
270338
- if (ctx.jsonMode) {
270339
- const turn = await ctx.runTask("Agent working (a turn can take minutes)", () => sendTurn(message, branchId));
270340
- if (turn.queued)
270341
- return { stdout: `${JSON.stringify({ queued: true })}
270342
- ` };
270343
- return {
270344
- stdout: `${JSON.stringify({
270345
- status: turn.status?.state ?? "ready",
270346
- error_source: turn.status?.error_source ?? null,
270347
- reply: lastAssistantReply(turn) ?? null
270348
- })}
270349
- `
270350
- };
270351
- }
270352
- const stream = createTurnStream(process.stdout.isTTY === true);
270353
- let turn;
270376
+ async function sendTurn(content, branchId) {
270377
+ let response;
270354
270378
  try {
270355
- turn = await streamConversationDuring(() => sendTurn(message, branchId), stream.onEvent, { branchId });
270356
- } finally {
270357
- stream.stop();
270379
+ response = await getAppClient().post("chat/message", {
270380
+ timeout: false,
270381
+ searchParams: {
270382
+ conversation_messages: "current_turn",
270383
+ ...branchScope(branchId)
270384
+ },
270385
+ json: { content }
270386
+ });
270387
+ } catch (error) {
270388
+ throw await ApiError.fromHttpError(error, "sending message");
270358
270389
  }
270359
- if (turn.queued) {
270360
- return {
270361
- outroMessage: "The agent is busy with an earlier message — yours was queued and runs next."
270362
- };
270390
+ return parseOrThrow(ChatTurnSchema, await response.json(), "chat turn");
270391
+ }
270392
+ async function stopTurn(branchId) {
270393
+ try {
270394
+ await getAppClient().post("chat/stop", {
270395
+ searchParams: branchScope(branchId)
270396
+ });
270397
+ } catch (error) {
270398
+ throw await ApiError.fromHttpError(error, "stopping the turn");
270363
270399
  }
270364
- if (turn.status?.state === "error") {
270365
- return {
270366
- outroMessage: `Turn failed (${turn.status.error_source ?? "unknown"}) — see the editor for details.`
270367
- };
270400
+ }
270401
+ async function getFullConversation(limit, branchId) {
270402
+ let response;
270403
+ try {
270404
+ response = await getAppClient().get("chat/full-conversation", {
270405
+ timeout: 30000,
270406
+ searchParams: { limit: String(limit), ...branchScope(branchId) }
270407
+ });
270408
+ } catch (error) {
270409
+ throw await ApiError.fromHttpError(error, "reading the conversation");
270368
270410
  }
270369
- return { outroMessage: "Turn finished." };
270411
+ return parseOrThrow(FullConversationSchema, await response.json(), "conversation").messages;
270370
270412
  }
270371
- function getSendCommand() {
270372
- const command = new Base44Command("send", { supportsBranch: true });
270373
- command.description("Send the agent one message and stream the turn until it finishes").argument("<message>", "What you want the agent to do").action(sendAction);
270374
- return command;
270413
+ async function resolveActiveBranchId() {
270414
+ const branches = await listBranches();
270415
+ return branches.length === 1 ? branches[0].id : undefined;
270375
270416
  }
270376
-
270377
- // src/cli/commands/app/status.ts
270378
- async function statusAction(ctx) {
270379
- const id = ctx.app?.id;
270380
- const app = await ctx.runTask("Reading app status", () => getAppState(id));
270381
- const state = app.status?.state ?? "ready";
270382
- if (ctx.jsonMode) {
270383
- return {
270384
- stdout: `${JSON.stringify({ id: app.id, state, message: app.status?.message ?? null })}
270385
- `
270386
- };
270417
+ async function getPreviewUrl() {
270418
+ let response;
270419
+ try {
270420
+ response = await getAppClient().get("sandbox/preview-url", {
270421
+ timeout: false
270422
+ });
270423
+ } catch (error) {
270424
+ throw await ApiError.fromHttpError(error, "fetching preview URL");
270387
270425
  }
270388
- ctx.log.message(`State: ${state}`);
270389
- if (app.status?.message)
270390
- ctx.log.message(`Note: ${app.status.message}`);
270391
- return { outroMessage: "Status read." };
270392
- }
270393
- function getStatusCommand() {
270394
- const command = new Base44Command("status");
270395
- command.description("Show whether the app is building, ready, or errored").action(statusAction);
270396
- return command;
270426
+ const url = parseOrThrow(PreviewUrlSchema, await response.json(), "preview URL").preview_url;
270427
+ return /^https?:\/\//.test(url) ? url : `https://${url}`;
270397
270428
  }
270398
270429
 
270399
- // src/cli/commands/app/stop.ts
270400
- async function stopAction(ctx) {
270401
- const branchId = await resolveBranchId(ctx);
270402
- await ctx.runTask("Stopping the running turn", () => stopTurn(branchId));
270403
- if (ctx.jsonMode)
270404
- return { stdout: `${JSON.stringify({ stopped: true })}
270405
- ` };
270406
- return { outroMessage: "Stopped." };
270407
- }
270408
- function getStopCommand() {
270409
- const command = new Base44Command("stop", { supportsBranch: true });
270410
- command.description("Stop the agent's running turn").action(stopAction);
270411
- return command;
270430
+ // src/cli/commands/builder/shared.ts
270431
+ var APP_NAME_RE = /^[A-Za-z0-9._-]+$/;
270432
+ var NAME_STOPWORDS = new Set("a an the and or of for with to in on that this its it my our your me".split(" "));
270433
+ var FALLBACK_WORDS = [
270434
+ "swift-otter",
270435
+ "sunny-comet",
270436
+ "tidy-maple",
270437
+ "brisk-panda"
270438
+ ];
270439
+ function inventAppName(prompt) {
270440
+ const suffix = Math.random().toString(36).slice(2, 5);
270441
+ const words = (prompt ?? "").toLowerCase().match(/[a-z0-9]+/g)?.filter((w) => w.length > 2 && !NAME_STOPWORDS.has(w)).slice(0, 3) ?? [];
270442
+ const core = words.length ? words.join("-") : FALLBACK_WORDS[Math.floor(Math.random() * FALLBACK_WORDS.length)];
270443
+ return `base44-${core}-${suffix}`.slice(0, 60);
270412
270444
  }
270413
-
270414
- // src/cli/commands/app/index.ts
270415
- function getAppCommand() {
270416
- return new Command2("app").description("Build a Base44 app with the agent, non-interactively: create it, send turns, read status, get the preview").addCommand(getNewCommand()).addCommand(getSendCommand()).addCommand(getStatusCommand()).addCommand(getPreviewCommand()).addCommand(getStopCommand()).addCommand(getModelCommand());
270445
+ function repoBasename(repoUrl) {
270446
+ return repoUrl.replace(/\/+$/, "").replace(/\.git$/, "").split("/").pop() ?? "app";
270417
270447
  }
270418
-
270419
- // src/cli/commands/auth/password-login.ts
270420
- import { dirname as dirname15, join as join22 } from "node:path";
270421
- async function passwordLoginAction({ log, runTask }, action) {
270422
- const shouldEnable = action === "enable";
270423
- const { project } = await readProjectConfig();
270424
- const configDir = dirname15(project.configPath);
270425
- const authDir = join22(configDir, project.authDir);
270426
- const updated = await runTask("Updating local auth config", async () => {
270427
- const current = await readAuthConfig(authDir) ?? DEFAULT_AUTH_CONFIG;
270428
- const merged = { ...current, enableUsernamePassword: shouldEnable };
270429
- await writeAuthConfig(authDir, merged);
270430
- return merged;
270431
- });
270432
- if (!shouldEnable && !hasAnyLoginMethod(updated)) {
270433
- log.warn("Disabling password auth will leave no login methods enabled. Users will be locked out.");
270448
+ async function createAndLinkApp(options) {
270449
+ if (options.name && !APP_NAME_RE.test(options.name)) {
270450
+ throw new InvalidInputError("The name becomes a directory — letters, digits, dots, dashes and underscores only.");
270434
270451
  }
270435
- const newStatus = shouldEnable ? "enabled" : "disabled";
270436
- return {
270437
- outroMessage: `Username & password authentication ${newStatus} in local config. Run \`base44 auth push\` or \`base44 deploy\` to apply.`
270438
- };
270439
- }
270440
- function getPasswordLoginCommand() {
270441
- return new Base44Command("password-login").description("Enable or disable username & password authentication").addArgument(new Argument2("<action>", "enable or disable password authentication").choices(["enable", "disable"])).action(passwordLoginAction);
270442
- }
270443
-
270444
- // src/cli/commands/auth/pull.ts
270445
- import { dirname as dirname16, join as join23 } from "node:path";
270446
- async function pullAuthAction({
270447
- log,
270448
- runTask
270449
- }) {
270450
- const { project } = await readProjectConfig();
270451
- const configDir = dirname16(project.configPath);
270452
- const authDir = join23(configDir, project.authDir);
270453
- const remoteConfig = await runTask("Fetching auth config from Base44", async () => {
270454
- return await pullAuthConfig();
270455
- }, {
270456
- successMessage: "Auth config fetched successfully",
270457
- errorMessage: "Failed to fetch auth config"
270458
- });
270459
- const { written } = await runTask("Syncing auth config file", async () => {
270460
- return await writeAuthConfig(authDir, remoteConfig);
270461
- }, {
270462
- successMessage: "Auth config file synced successfully",
270463
- errorMessage: "Failed to sync auth config file"
270464
- });
270465
- if (written) {
270466
- log.success("Auth config written to local file");
270467
- } else {
270468
- log.info("Auth config is already up to date");
270452
+ const cwd = process.cwd();
270453
+ const fallbackName = () => options.importRepo ? repoBasename(options.importRepo) : inventAppName(options.prompt);
270454
+ const chosenDir = options.path ? resolve8(cwd, options.path) : await isDirEmpty(cwd) ? cwd : undefined;
270455
+ const dirBase = chosenDir ? basename6(chosenDir) : undefined;
270456
+ const name = options.name ?? (dirBase && APP_NAME_RE.test(dirBase) ? dirBase : fallbackName());
270457
+ const targetDir = chosenDir ?? join25(cwd, name);
270458
+ const here = targetDir === cwd;
270459
+ const dirName = here ? "." : relative6(cwd, targetDir) || name;
270460
+ await mkdir3(targetDir, { recursive: true });
270461
+ if (await appConfigExists(targetDir)) {
270462
+ throw new InvalidInputError(here ? "This directory is already linked to a Base44 app. Run `base44 code` here to keep building it, or pass --path for a new one." : `./${dirName} is already linked to a Base44 app. Pick another name or --path.`);
270469
270463
  }
270464
+ const created = options.importRepo ? await createImportedApp({
270465
+ appName: name,
270466
+ repoUrl: options.importRepo,
270467
+ sourceMode: options.mode ?? "direct",
270468
+ newRepoName: options.repoName,
270469
+ branch: options.fromBranch,
270470
+ prompt: options.prompt
270471
+ }) : await createApp({ appName: name, prompt: options.prompt });
270472
+ await writeAppConfig(targetDir, created.id);
270473
+ await mkdir3(join25(targetDir, "base44"), { recursive: true });
270474
+ try {
270475
+ await writeFile2(join25(targetDir, "base44", "config.jsonc"), `// Base44 project configuration.
270476
+ {
270477
+ "name": ${JSON.stringify(name)}
270478
+ }
270479
+ `, { flag: "wx" });
270480
+ } catch {}
270481
+ setAppContext({ id: created.id, projectRoot: targetDir });
270470
270482
  return {
270471
- outroMessage: `Pulled auth config to ${authDir} (overwrites local file)`
270472
- };
270483
+ id: created.id,
270484
+ editorUrl: `${getBase44ApiUrl()}/apps/${created.id}/editor/preview`,
270485
+ repoUrl: created.imported_repo_url ?? undefined,
270486
+ dirName,
270487
+ targetDir,
270488
+ here
270489
+ };
270490
+ }
270491
+ function repoLabel(url) {
270492
+ return url.replace(/^https?:\/\//, "").replace(/\.git$/, "").replace(/\/$/, "");
270493
+ }
270494
+ function appTypeChip(state) {
270495
+ switch (state.app_type ?? "user_app") {
270496
+ case "imported_app":
270497
+ return state.imported_repo_url ? repoLabel(state.imported_repo_url) : "repository app";
270498
+ case "user_game":
270499
+ return "game";
270500
+ case "mobile_app":
270501
+ return "mobile app";
270502
+ case "slide":
270503
+ return "slides";
270504
+ case "user_agent":
270505
+ return "superagent";
270506
+ default:
270507
+ return "web app";
270508
+ }
270473
270509
  }
270474
- function getAuthPullCommand() {
270475
- return new Base44Command("pull").description("Pull auth config from Base44 to local file").action(pullAuthAction);
270510
+ async function assertBuilderApp(appId) {
270511
+ const state = await getAppState(appId);
270512
+ if (state.is_managed_source_code === false) {
270513
+ throw new InvalidInputError("This project is code-first (base44 create): you own the source and the builder cannot work on it. Run `base44 builder new` for an agent-built app.");
270514
+ }
270515
+ if (state.app_type === "user_agent") {
270516
+ throw new InvalidInputError("This is a Superagent, not an app the builder works on. Superagent has no CLI surface yet.");
270517
+ }
270518
+ return state;
270519
+ }
270520
+ function nextStepsLines(app) {
270521
+ const cd = app.here ? "" : `cd ${app.dirName} && `;
270522
+ return [
270523
+ "",
270524
+ `${source_default.bold("Your app lives in")} ${app.here ? "./ (this directory)" : `./${app.dirName}`}`,
270525
+ source_default.dim(` ${cd}base44 code # keep building with the agent`),
270526
+ source_default.dim(` ${cd}base44 builder send "…" # one non-interactive turn`),
270527
+ source_default.dim(` files live remotely — ${cd}base44 sandbox ls to look, base44 eject for a copy`)
270528
+ ];
270529
+ }
270530
+ async function githubReauthLines(error) {
270531
+ if (!isGithubUserTokenError(error))
270532
+ return null;
270533
+ const link = await startGithubReauth().catch(() => null);
270534
+ return [
270535
+ "Your GitHub authorization expired. Reconnect, then run this again:",
270536
+ link ? terminalLink("Reconnect GitHub", link) : "Open Base44 → GitHub settings to reconnect your account."
270537
+ ];
270538
+ }
270539
+ async function resolveBranchId(ctx) {
270540
+ return ctx.branchId ?? await resolveActiveBranchId().catch(() => {
270541
+ return;
270542
+ });
270476
270543
  }
270477
270544
 
270478
- // src/cli/commands/auth/push.ts
270479
- async function pushAuthAction({ isNonInteractive, log, runTask }, options) {
270480
- const { authConfig } = await readProjectConfig();
270481
- if (authConfig.length === 0) {
270482
- log.info("No local auth config found");
270483
- return {
270484
- outroMessage: "No auth config to push. Run `base44 auth pull` to fetch the remote config first."
270485
- };
270486
- }
270487
- if (!hasAnyLoginMethod(authConfig[0])) {
270488
- log.warn("This config has no login methods enabled. Pushing it will lock out all users.");
270545
+ // src/core/resources/apps/stream.ts
270546
+ function newStreamState() {
270547
+ return { perMessage: new Map };
270548
+ }
270549
+ var TOOL_SETTLED = new Set(["success", "error", "stopped"]);
270550
+ function oneLine(value, max) {
270551
+ const text = typeof value === "string" ? value : value == null ? "" : JSON.stringify(value);
270552
+ const flat = text.replace(/\s+/g, " ").trim();
270553
+ return flat.length > max ? `${flat.slice(0, max)}…` : flat;
270554
+ }
270555
+ var SALIENT_KEYS = ["command", "path", "file_path", "title"];
270556
+ function salvageKey(raw, keys) {
270557
+ for (const key of keys) {
270558
+ const match = raw.match(new RegExp(`"${key}"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)"`));
270559
+ if (match?.[1])
270560
+ return match[1].replace(/\\(.)/g, "$1");
270489
270561
  }
270490
- if (!options.yes) {
270491
- if (isNonInteractive) {
270492
- throw new InvalidInputError("--yes is required in non-interactive mode");
270493
- }
270494
- const shouldPush = await Re({
270495
- message: "Push auth config to Base44?"
270496
- });
270497
- if (Ct(shouldPush) || !shouldPush) {
270498
- return { outroMessage: "Push cancelled" };
270562
+ return;
270563
+ }
270564
+ function toolMeta(name, argumentsString) {
270565
+ const raw = argumentsString ?? "";
270566
+ let args = {};
270567
+ try {
270568
+ const parsed = JSON.parse(raw);
270569
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
270570
+ args = parsed;
270499
270571
  }
270572
+ } catch {
270573
+ return {
270574
+ label: oneLine(salvageKey(raw, ["summary"]) ?? "", 90),
270575
+ summary: oneLine(salvageKey(raw, SALIENT_KEYS) ?? raw, 90)
270576
+ };
270500
270577
  }
270501
- await runTask("Pushing auth config to Base44", async () => {
270502
- return await pushAuthConfig(authConfig[0] ?? null);
270503
- }, {
270504
- successMessage: "Auth config pushed successfully",
270505
- errorMessage: "Failed to push auth config"
270506
- });
270578
+ const pick = (key) => typeof args[key] === "string" && args[key].trim() ? args[key] : undefined;
270579
+ const salient = {
270580
+ run_shell_command: pick("command"),
270581
+ read_repo_file: pick("path") ?? pick("file_path"),
270582
+ write_repo_file: pick("path") ?? pick("file_path"),
270583
+ edit_repo_file: pick("path") ?? pick("file_path"),
270584
+ create_pull_request: pick("title"),
270585
+ reload_preview: ""
270586
+ };
270587
+ const summary = salient[name] ?? Object.entries(args).find(([key, v]) => key !== "summary" && typeof v === "string" && v.trim().length > 0)?.[1] ?? "";
270507
270588
  return {
270508
- outroMessage: "Auth config pushed to Base44"
270589
+ label: oneLine(pick("summary") ?? "", 90),
270590
+ summary: oneLine(summary, 90)
270509
270591
  };
270510
270592
  }
270511
- function getAuthPushCommand() {
270512
- return new Base44Command("push").description("Push local auth config to Base44").option("-y, --yes", "Skip confirmation prompt").action(pushAuthAction);
270593
+ function labelTense(label, tense) {
270594
+ const parts = label.split(/\s*\|\s*/);
270595
+ if (parts.length < 2)
270596
+ return label;
270597
+ return tense === "running" ? parts[0] : parts[1];
270513
270598
  }
270514
-
270515
- // src/cli/commands/auth/social-login.ts
270516
- import { dirname as dirname17, join as join24, resolve as resolve6 } from "node:path";
270517
- var PROVIDER_LABELS = {
270518
- google: "Google",
270519
- microsoft: "Microsoft",
270520
- facebook: "Facebook",
270521
- apple: "Apple"
270522
- };
270523
- var VALID_PROVIDER_NAMES = Object.keys(SOCIAL_PROVIDERS);
270524
- var PROVIDER_OAUTH_CLI = {
270525
- google: {
270526
- envVar: "google_oauth_client_secret",
270527
- promptMessage: "Enter Google OAuth client secret"
270599
+ function progressFor(state, id) {
270600
+ let progress = state.perMessage.get(id);
270601
+ if (!progress) {
270602
+ progress = {
270603
+ contentLength: 0,
270604
+ reasoningLength: 0,
270605
+ announcedTools: new Map,
270606
+ settledTools: new Set,
270607
+ waitingNotified: new Set
270608
+ };
270609
+ state.perMessage.set(id, progress);
270528
270610
  }
270529
- };
270530
- function hasSecretOptions(options) {
270531
- return Boolean(options.clientSecret || options.clientSecretStdin || options.envFile);
270532
- }
270533
- function hasCustomOAuthOptions(options) {
270534
- return Boolean(options.clientId || hasSecretOptions(options));
270611
+ return progress;
270535
270612
  }
270536
- async function socialLoginAction({ log, isNonInteractive, runTask }, provider, action, options) {
270537
- const shouldEnable = action === "enable";
270538
- const providerInfo = SOCIAL_PROVIDERS[provider];
270539
- const label = PROVIDER_LABELS[provider];
270540
- const hasOAuthOptions = hasCustomOAuthOptions(options);
270541
- if (hasOAuthOptions && !providerInfo.customOAuth) {
270542
- throw new InvalidInputError(`Custom OAuth options are only supported for providers with custom OAuth (e.g., google). Use: base44 auth social-login ${provider} ${action}`);
270543
- }
270544
- if (hasOAuthOptions && !shouldEnable) {
270545
- throw new InvalidInputError(`Custom OAuth options cannot be used with disable. To disable ${label} login: base44 auth social-login ${provider} disable`);
270546
- }
270547
- if (hasSecretOptions(options) && !options.clientId) {
270548
- throw new InvalidInputError(`--client-id is required when providing a client secret. Use: base44 auth social-login ${provider} enable --client-id <id> --client-secret <secret>`);
270549
- }
270550
- const oauth = providerInfo.customOAuth;
270551
- const oauthCli = PROVIDER_OAUTH_CLI[provider];
270552
- const useCustomOAuth = shouldEnable && hasOAuthOptions && oauth != null;
270553
- let clientSecret;
270554
- if (useCustomOAuth && oauth && oauthCli && hasSecretOptions(options)) {
270555
- if (options.envFile) {
270556
- const secrets = await parseEnvFile(resolve6(options.envFile));
270557
- const value = secrets[oauthCli.envVar];
270558
- if (!value) {
270559
- throw new InvalidInputError(`Key "${oauthCli.envVar}" not found in ${options.envFile}.`);
270560
- }
270561
- clientSecret = value;
270562
- } else {
270563
- clientSecret = await resolveSecret({
270564
- flagValue: options.clientSecret,
270565
- fromStdin: options.clientSecretStdin,
270566
- envVar: oauthCli.envVar,
270567
- promptMessage: oauthCli.promptMessage,
270568
- isNonInteractive,
270569
- name: "client secret",
270570
- hints: [
270571
- {
270572
- message: `Provide via flag: base44 auth social-login ${provider} enable --client-id <id> --client-secret <secret>`,
270573
- command: `base44 auth social-login ${provider} enable --client-id <id> --client-secret <secret>`
270574
- },
270575
- {
270576
- message: `Provide via stdin: echo <secret> | base44 auth social-login ${provider} enable --client-id <id> --client-secret-stdin`
270577
- },
270578
- {
270579
- message: `Provide via env: ${oauthCli.envVar}=<secret> base44 auth social-login ${provider} enable --client-id <id>`
270580
- }
270581
- ]
270582
- });
270613
+ function diffConversation(state, messages) {
270614
+ const events = [];
270615
+ for (const message of messages) {
270616
+ if (message.role !== "assistant" || message.hidden)
270617
+ continue;
270618
+ const progress = progressFor(state, message.id);
270619
+ const reasoning = message.reasoning?.content ?? "";
270620
+ if (reasoning.length > progress.reasoningLength) {
270621
+ const delta = reasoning.slice(progress.reasoningLength).trim();
270622
+ if (delta)
270623
+ events.push({ kind: "thinking", text: oneLine(delta, 300) });
270624
+ progress.reasoningLength = reasoning.length;
270625
+ }
270626
+ if (typeof message.content === "string" && message.content.length > progress.contentLength) {
270627
+ const delta = message.content.slice(progress.contentLength).trim();
270628
+ if (delta)
270629
+ events.push({ kind: "text", text: delta });
270630
+ progress.contentLength = message.content.length;
270631
+ }
270632
+ for (const tool of message.tool_calls ?? []) {
270633
+ if (!progress.announcedTools.has(tool.id)) {
270634
+ progress.announcedTools.set(tool.id, toolMeta(tool.name, tool.arguments_string));
270635
+ const meta = progress.announcedTools.get(tool.id);
270636
+ events.push({
270637
+ kind: "tool_start",
270638
+ id: tool.id,
270639
+ name: tool.name,
270640
+ label: labelTense(meta.label, "running"),
270641
+ summary: meta.summary
270642
+ });
270643
+ }
270644
+ const status = tool.status ?? "running";
270645
+ if (status === "waiting_for_user_input" && !progress.waitingNotified.has(tool.id)) {
270646
+ progress.waitingNotified.add(tool.id);
270647
+ const meta = progress.announcedTools.get(tool.id);
270648
+ events.push({
270649
+ kind: "waiting",
270650
+ id: tool.id,
270651
+ name: tool.name,
270652
+ label: labelTense(meta.label, "running")
270653
+ });
270654
+ }
270655
+ if (TOOL_SETTLED.has(status) && !progress.settledTools.has(tool.id)) {
270656
+ progress.settledTools.add(tool.id);
270657
+ const meta = progress.announcedTools.get(tool.id);
270658
+ events.push({
270659
+ kind: "tool_end",
270660
+ id: tool.id,
270661
+ name: tool.name,
270662
+ label: labelTense(meta.label, "done"),
270663
+ summary: meta.summary,
270664
+ ok: status === "success",
270665
+ result: oneLine(tool.results, 110)
270666
+ });
270667
+ }
270583
270668
  }
270584
270669
  }
270585
- const { project } = await readProjectConfig();
270586
- const configDir = dirname17(project.configPath);
270587
- const authDir = join24(configDir, project.authDir);
270588
- const { config: updated } = await runTask("Updating local auth config", async () => updateSocialLoginConfig(authDir, provider, shouldEnable, useCustomOAuth && options.clientId ? { clientId: options.clientId } : undefined));
270589
- if (clientSecret) {
270590
- await runTask("Saving client secret", async () => pushCustomOAuthSecret(provider, clientSecret));
270591
- }
270592
- if (!shouldEnable && !hasAnyLoginMethod(updated)) {
270593
- log.warn(`Disabling ${label} login will leave no login methods enabled. Users will be locked out.`);
270594
- }
270595
- const newStatus = shouldEnable ? "enabled" : "disabled";
270596
- const oauthNote = useCustomOAuth ? " with custom OAuth" : "";
270597
- let outroMessage = `${label} login ${newStatus}${oauthNote} in local config. Run \`base44 auth push\` or \`base44 deploy\` to apply.`;
270598
- if (useCustomOAuth && !clientSecret) {
270599
- outroMessage += `
270600
- Remember to push the client secret separately: base44 secrets set --env-file <path>`;
270601
- }
270602
- return { outroMessage };
270670
+ return events;
270603
270671
  }
270604
- function getSocialLoginCommand() {
270605
- return new Base44Command("social-login").description("Enable or disable social login providers (google, microsoft, facebook, apple)").addArgument(new Argument2("<provider>", "social login provider").choices(VALID_PROVIDER_NAMES)).addArgument(new Argument2("<action>", "enable or disable the provider").choices([
270606
- "enable",
270607
- "disable"
270608
- ])).option("--client-id <id>", "custom OAuth client ID (Google only)").option("--client-secret <secret>", "custom OAuth client secret (Google only)").option("--client-secret-stdin", "read client secret from stdin (Google only)").option("--env-file <path>", "read client secret from a .env file (Google only)").action(socialLoginAction);
270672
+ function turnSettled(messages) {
270673
+ return newestUserTurn(messages)?.settled ?? false;
270609
270674
  }
270610
-
270611
- // src/cli/commands/auth/sso.ts
270612
- import { dirname as dirname18, join as join25, resolve as resolve7 } from "node:path";
270613
- var SSOConfigFileSchema = object({
270614
- provider: _enum(Object.values(KNOWN_SSO_PROVIDERS)),
270615
- clientId: string2(),
270616
- clientSecret: string2(),
270617
- scope: string2().optional(),
270618
- discoveryUrl: string2().optional(),
270619
- tenantId: string2().optional(),
270620
- oktaDomain: string2().optional(),
270621
- authEndpoint: string2().optional(),
270622
- tokenEndpoint: string2().optional(),
270623
- userinfoEndpoint: string2().optional(),
270624
- jwksUri: string2().optional(),
270625
- ssoName: string2().optional()
270626
- });
270627
- async function loadSSOConfigFile(filePath) {
270628
- const resolved = resolve7(filePath);
270629
- const raw = await readJsonFile(resolved);
270630
- const result = SSOConfigFileSchema.safeParse(raw);
270631
- if (!result.success) {
270632
- throw new SchemaValidationError("Invalid SSO config file", result.error, filePath);
270675
+ function newestUserTurn(messages) {
270676
+ for (let i = messages.length - 1;i >= 0; i--) {
270677
+ const message = messages[i];
270678
+ if (message.role === "user" && !message.hidden) {
270679
+ const outcome = message.outcome;
270680
+ const backendStatus = outcome && typeof outcome === "object" ? outcome.backend_status : undefined;
270681
+ return {
270682
+ id: message.id,
270683
+ settled: outcome != null && backendStatus !== "pending",
270684
+ backendStatus
270685
+ };
270686
+ }
270633
270687
  }
270634
- return result.data;
270688
+ return null;
270635
270689
  }
270636
- function mergeFileWithFlags(fileConfig, options) {
270637
- return {
270638
- provider: options.provider ?? fileConfig.provider,
270639
- clientId: options.clientId ?? fileConfig.clientId,
270640
- clientSecret: options.clientSecret ?? fileConfig.clientSecret,
270641
- clientSecretStdin: options.clientSecretStdin,
270642
- envFile: options.envFile,
270643
- scope: options.scope ?? fileConfig.scope,
270644
- discoveryUrl: options.discoveryUrl ?? fileConfig.discoveryUrl,
270645
- tenantId: options.tenantId ?? fileConfig.tenantId,
270646
- oktaDomain: options.oktaDomain ?? fileConfig.oktaDomain,
270647
- authEndpoint: options.authEndpoint ?? fileConfig.authEndpoint,
270648
- tokenEndpoint: options.tokenEndpoint ?? fileConfig.tokenEndpoint,
270649
- userinfoEndpoint: options.userinfoEndpoint ?? fileConfig.userinfoEndpoint,
270650
- jwksUri: options.jwksUri ?? fileConfig.jwksUri,
270651
- ssoName: options.ssoName ?? fileConfig.ssoName
270690
+ var sleep3 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
270691
+ function makePoller(onEvent, options) {
270692
+ const state = newStreamState();
270693
+ return async (prime = false) => {
270694
+ try {
270695
+ const messages = await getFullConversation(30, options.branchId);
270696
+ const events = diffConversation(state, messages);
270697
+ if (!prime)
270698
+ for (const event of events)
270699
+ onEvent(event);
270700
+ return messages;
270701
+ } catch {
270702
+ return [];
270703
+ }
270652
270704
  };
270653
270705
  }
270654
- var providerNames = Object.keys(KNOWN_SSO_PROVIDERS);
270655
- var SECRET_KEY_TO_FLAG = {
270656
- ["sso_name" /* Name */]: "--sso-name",
270657
- ["sso_client_id" /* ClientId */]: "--client-id",
270658
- ["sso_client_secret" /* ClientSecret */]: "--client-secret",
270659
- ["sso_scope" /* Scope */]: "--scope",
270660
- ["sso_discovery_url" /* DiscoveryUrl */]: "--discovery-url",
270661
- ["sso_tenant_id" /* TenantId */]: "--tenant-id",
270662
- ["sso_auth_endpoint" /* AuthEndpoint */]: "--auth-endpoint",
270663
- ["sso_token_endpoint" /* TokenEndpoint */]: "--token-endpoint",
270664
- ["sso_userinfo_endpoint" /* UserinfoEndpoint */]: "--userinfo-endpoint",
270665
- ["sso_okta_domain" /* OktaDomain */]: "--okta-domain",
270666
- ["sso_jwks_uri" /* JwksUri */]: "--jwks-uri"
270667
- };
270668
- function secretKeyToFlag(key) {
270669
- return SECRET_KEY_TO_FLAG[key];
270670
- }
270671
- function exampleCommand(provider) {
270672
- let cmd = `base44 auth sso enable --provider ${provider} --client-id <id> --client-secret <secret>`;
270673
- if (provider === KNOWN_SSO_PROVIDERS.microsoft)
270674
- cmd += " --tenant-id <id>";
270675
- if (provider === KNOWN_SSO_PROVIDERS.okta)
270676
- cmd += " --okta-domain <domain>";
270677
- if (provider === KNOWN_SSO_PROVIDERS.custom)
270678
- cmd += " --sso-name <name> --auth-endpoint <url> --token-endpoint <url> --userinfo-endpoint <url> --jwks-uri <url>";
270679
- return cmd;
270680
- }
270681
- function validateProvider(provider) {
270682
- if (!provider) {
270683
- throw new InvalidInputError("Missing --provider.", {
270684
- hints: [
270685
- {
270686
- message: `Valid providers: ${providerNames.join(", ")}`,
270687
- command: "base44 auth sso enable --provider <provider> --client-id <id> --client-secret <secret>"
270688
- }
270689
- ]
270690
- });
270706
+ async function streamConversationDuring(start, onEvent, options = {}) {
270707
+ const intervalMs = options.intervalMs ?? 1000;
270708
+ const poll = makePoller(onEvent, options);
270709
+ await poll(true);
270710
+ const work = start();
270711
+ let pending = true;
270712
+ const settled = work.then(() => {
270713
+ pending = false;
270714
+ }, () => {
270715
+ pending = false;
270716
+ });
270717
+ while (pending) {
270718
+ await Promise.race([sleep3(intervalMs), settled]);
270719
+ if (!pending)
270720
+ break;
270721
+ await poll();
270691
270722
  }
270692
- return provider;
270723
+ await poll();
270724
+ return work;
270693
270725
  }
270694
- async function ssoEnableAction({ isNonInteractive, runTask }, options) {
270695
- if (options.file && options.envFile) {
270696
- throw new InvalidInputError("--file and --env-file cannot be used together. Provide the client secret either inside --file or via --env-file.");
270726
+ async function streamConversationUntilSettled(onEvent, options = {}) {
270727
+ const intervalMs = options.intervalMs ?? 1000;
270728
+ const deadline = Date.now() + (options.timeoutMs ?? 20 * 60000);
270729
+ const poll = makePoller(onEvent, options);
270730
+ while (Date.now() < deadline) {
270731
+ const messages = await poll();
270732
+ if (messages.length > 0 && turnSettled(messages))
270733
+ return "settled";
270734
+ await sleep3(intervalMs);
270697
270735
  }
270698
- let merged = options;
270699
- if (options.file) {
270700
- const fileConfig = await loadSSOConfigFile(options.file);
270701
- merged = mergeFileWithFlags(fileConfig, options);
270736
+ return "timeout";
270737
+ }
270738
+
270739
+ // src/cli/commands/builder/new.ts
270740
+ var POLL_TIMEOUT_MS = 20 * 60000;
270741
+ var MODES = ["direct", "fork", "copy"];
270742
+ async function newAction({ log, runTask, jsonMode }, prompt, options) {
270743
+ if (options.mode && !MODES.includes(options.mode)) {
270744
+ throw new InvalidInputError("--mode must be direct, fork, or copy.");
270702
270745
  }
270703
- const provider = validateProvider(merged.provider);
270704
- if (!merged.clientId) {
270705
- throw new InvalidInputError("Missing --client-id.", {
270706
- hints: [
270707
- {
270708
- message: `Example: base44 auth sso enable --provider ${provider} --client-id <id> --client-secret <secret>`,
270709
- command: `base44 auth sso enable --provider ${provider} --client-id <id> --client-secret <secret>`
270710
- }
270711
- ]
270712
- });
270746
+ if ((options.mode || options.repoName || options.fromBranch) && !options.import) {
270747
+ throw new InvalidInputError("--mode, --repo-name and --from-branch apply only with --import <repo>.");
270713
270748
  }
270714
- let clientSecret;
270715
- if (merged.envFile && !merged.clientSecret) {
270716
- const secrets = await parseEnvFile(resolve7(merged.envFile));
270717
- const value = secrets.sso_client_secret;
270718
- if (!value) {
270719
- throw new InvalidInputError(`Key "sso_client_secret" not found in ${merged.envFile}.`);
270720
- }
270721
- clientSecret = value;
270722
- } else {
270723
- clientSecret = await resolveSecret({
270724
- flagValue: merged.clientSecret,
270725
- fromStdin: merged.clientSecretStdin,
270726
- envVar: "sso_client_secret",
270727
- promptMessage: "Enter SSO client secret",
270728
- isNonInteractive,
270729
- name: "client secret",
270730
- hints: [
270731
- {
270732
- message: `Provide via flag: base44 auth sso enable --provider ${provider} --client-id <id> --client-secret <secret>`,
270733
- command: `base44 auth sso enable --provider ${provider} --client-id <id> --client-secret <secret>`
270734
- },
270735
- {
270736
- message: `Provide via stdin: echo <secret> | base44 auth sso enable --provider ${provider} --client-id <id> --client-secret-stdin`
270737
- },
270738
- {
270739
- message: `Provide via env: sso_client_secret=<secret> base44 auth sso enable --provider ${provider} --client-id <id>`
270740
- }
270741
- ]
270742
- });
270749
+ if (!prompt && !options.import) {
270750
+ throw new InvalidInputError('Describe the app ("<prompt>") or pass --import <repo>.');
270743
270751
  }
270744
- const secretOptions = {
270745
- clientId: merged.clientId,
270746
- clientSecret,
270747
- scope: merged.scope,
270748
- discoveryUrl: merged.discoveryUrl,
270749
- tenantId: merged.tenantId,
270750
- oktaDomain: merged.oktaDomain,
270751
- authEndpoint: merged.authEndpoint,
270752
- tokenEndpoint: merged.tokenEndpoint,
270753
- userinfoEndpoint: merged.userinfoEndpoint,
270754
- jwksUri: merged.jwksUri,
270755
- ssoName: merged.ssoName
270756
- };
270757
- let secrets;
270752
+ let app;
270758
270753
  try {
270759
- secrets = buildSSOSecrets(provider, secretOptions);
270754
+ app = await runTask(options.import ? "Importing the repository" : "Creating your app", () => createAndLinkApp({
270755
+ prompt,
270756
+ name: options.name,
270757
+ importRepo: options.import,
270758
+ mode: options.mode,
270759
+ repoName: options.repoName,
270760
+ fromBranch: options.fromBranch,
270761
+ path: options.path
270762
+ }));
270760
270763
  } catch (error) {
270761
- if (error instanceof MissingSSOFieldsError) {
270762
- const flagNames = error.missingKeys.map(secretKeyToFlag);
270763
- throw new InvalidInputError(`Missing required fields for ${error.provider}: ${flagNames.join(", ")}`, {
270764
- hints: [
270765
- {
270766
- message: `Example: ${exampleCommand(provider)}`,
270767
- command: exampleCommand(provider)
270768
- }
270769
- ]
270770
- });
270764
+ for (const line of await githubReauthLines(error) ?? [])
270765
+ log.message(line);
270766
+ throw error;
270767
+ }
270768
+ if (!jsonMode) {
270769
+ if (app.repoUrl)
270770
+ log.message(source_default.dim(`repo ${app.repoUrl}`));
270771
+ log.message(source_default.dim(`editor ${app.editorUrl}`));
270772
+ log.message(source_default.dim(`linked ${app.here ? "./ (this directory)" : `./${app.dirName}`}`));
270773
+ }
270774
+ let finalState;
270775
+ let previewUrl;
270776
+ const startedAt = Date.now();
270777
+ if (prompt) {
270778
+ const branchId = await resolveActiveBranchId().catch(() => {
270779
+ return;
270780
+ });
270781
+ const stream = createTurnStream(process.stdout.isTTY === true && !jsonMode, undefined, { idleLabel: "provisioning the sandbox and starting the build" });
270782
+ try {
270783
+ const settled = await streamConversationUntilSettled((event) => {
270784
+ if (!jsonMode)
270785
+ stream.onEvent(event);
270786
+ }, { branchId, timeoutMs: POLL_TIMEOUT_MS });
270787
+ finalState = settled === "timeout" ? "processing" : (await getAppState(app.id)).status?.state ?? "ready";
270788
+ if (finalState === "ready") {
270789
+ previewUrl = await getPreviewUrl().catch(() => {
270790
+ return;
270791
+ });
270792
+ }
270793
+ } finally {
270794
+ stream.stop();
270771
270795
  }
270772
- throw error;
270773
270796
  }
270774
- const { project } = await readProjectConfig();
270775
- const configDir = dirname18(project.configPath);
270776
- const authDir = join25(configDir, project.authDir);
270777
- await runTask("Updating local auth config", async () => updateSSOConfig(authDir, provider, true));
270778
- await runTask("Saving SSO credentials", async () => pushSSOSecrets(secrets));
270779
- return {
270780
- outroMessage: `SSO configured with ${provider} in local config. Run \`base44 auth push\` or \`base44 deploy\` to apply.`
270781
- };
270782
- }
270783
- function hasEnableOnlyOptions(options) {
270784
- return Boolean(options.provider || options.clientId || options.clientSecret || options.clientSecretStdin || options.envFile || options.file || options.scope || options.discoveryUrl || options.tenantId || options.oktaDomain || options.authEndpoint || options.tokenEndpoint || options.userinfoEndpoint || options.jwksUri || options.ssoName);
270785
- }
270786
- async function ssoDisableAction({ log, runTask }, options) {
270787
- if (hasEnableOnlyOptions(options)) {
270788
- throw new InvalidInputError("Configuration options cannot be used with disable. To disable SSO: base44 auth sso disable");
270797
+ if (jsonMode) {
270798
+ return {
270799
+ stdout: `${JSON.stringify({
270800
+ id: app.id,
270801
+ repo_url: app.repoUrl ?? null,
270802
+ editor_url: app.editorUrl,
270803
+ preview_url: previewUrl ?? null,
270804
+ dir: app.dirName,
270805
+ path: app.targetDir,
270806
+ status: finalState ?? "created"
270807
+ })}
270808
+ `
270809
+ };
270789
270810
  }
270790
- const { project } = await readProjectConfig();
270791
- const configDir = dirname18(project.configPath);
270792
- const authDir = join25(configDir, project.authDir);
270793
- const updated = await runTask("Updating local auth config", async () => updateSSOConfig(authDir, null, false));
270794
- await runTask("Removing SSO credentials", async () => deleteSSOSecrets());
270795
- if (!hasAnyLoginMethod(updated)) {
270796
- log.warn("Disabling SSO will leave no login methods enabled. Users will be locked out.");
270811
+ if (previewUrl)
270812
+ log.message(`preview ${previewUrl}`);
270813
+ for (const line of nextStepsLines(app))
270814
+ log.message(line);
270815
+ if (finalState === "error") {
270816
+ return {
270817
+ outroMessage: `The first build reported an error open the editor for details.`
270818
+ };
270819
+ }
270820
+ if (finalState === "processing") {
270821
+ return {
270822
+ outroMessage: `Still building — follow it with \`base44 builder status\`.`
270823
+ };
270797
270824
  }
270798
270825
  return {
270799
- outroMessage: "SSO disabled in local config and credentials removed. Run `base44 auth push` or `base44 deploy` to apply."
270826
+ outroMessage: prompt ? `First build finished · ${formatDuration2(Date.now() - startedAt)}.` : "App created."
270800
270827
  };
270801
270828
  }
270802
- async function ssoAction(context, action, options) {
270803
- if (action === "disable") {
270804
- return ssoDisableAction(context, options);
270805
- }
270806
- return ssoEnableAction(context, options);
270807
- }
270808
- function getSSOCommand() {
270809
- return new Base44Command("sso").description("Configure SSO identity provider (google, microsoft, github, okta, custom). SSO and social login are mutually exclusive — enabling one disables the other in the local auth config.").addArgument(new Argument2("<action>", "enable or disable SSO").choices([
270810
- "enable",
270811
- "disable"
270812
- ])).addOption(new Option2("--provider <provider>", "SSO provider").choices(Object.values(KNOWN_SSO_PROVIDERS))).option("--client-id <id>", "OAuth client ID").option("--client-secret <secret>", "OAuth client secret").option("--client-secret-stdin", "Read client secret from stdin").option("--env-file <path>", "Read client secret from a .env file (key: sso_client_secret)").option("--file <path>", "JSON config file with all SSO settings").option("--scope <scope>", "OAuth scope (defaults per provider)").option("--discovery-url <url>", "OIDC discovery URL").option("--tenant-id <id>", "Microsoft tenant ID (required for microsoft)").option("--okta-domain <domain>", "Okta domain (required for okta)").option("--auth-endpoint <url>", "Authorization endpoint (required for custom)").option("--token-endpoint <url>", "Token endpoint (required for custom)").option("--userinfo-endpoint <url>", "Userinfo endpoint (required for custom)").option("--jwks-uri <url>", "JWKS URI (required for custom)").option("--sso-name <name>", "Provider display name (required for custom)").action(ssoAction);
270813
- }
270814
-
270815
- // src/cli/commands/auth/index.ts
270816
- function getAuthCommand() {
270817
- return new Command2("auth").description("Manage app authentication settings").addCommand(getPasswordLoginCommand()).addCommand(getSocialLoginCommand()).addCommand(getSSOCommand()).addCommand(getAuthPullCommand()).addCommand(getAuthPushCommand());
270829
+ function getNewCommand() {
270830
+ const command = new Base44Command("new", { requireAppContext: false });
270831
+ command.description("Create an app and start building: from a prompt (the Base44 template), or over an existing GitHub repo with --import").argument("[prompt]", "What to build; the first agent turn starts immediately").option("--import <repo>", "Build over an existing GitHub repository instead of the template").option("--mode <mode>", "How to import: direct, fork, or copy (default: direct)").option("--name <name>", "Directory and app name (invented when omitted)").option("--path <dir>", "Directory to link (default: the current directory when empty, else ./<name>)").option("--repo-name <name>", "Name for the new GitHub repo when forking/copying").option("--from-branch <name>", "Import a specific branch of the repo").action(newAction);
270832
+ return command;
270818
270833
  }
270819
270834
 
270820
- // src/cli/commands/auth/login.ts
270821
- function getLoginCommand() {
270822
- return new Base44Command("login", {
270823
- requireAuth: false,
270824
- requireAppContext: false
270825
- }).description("Authenticate with Base44").action(login);
270835
+ // src/cli/commands/builder/send.ts
270836
+ function lastAssistantReply(turn) {
270837
+ const messages = turn.conversation?.messages ?? [];
270838
+ for (let i = messages.length - 1;i >= 0; i--) {
270839
+ const { role, content } = messages[i];
270840
+ if (role === "assistant" && typeof content === "string" && content.trim()) {
270841
+ return content.trim();
270842
+ }
270843
+ }
270844
+ return;
270826
270845
  }
270827
-
270828
- // src/cli/commands/auth/logout.ts
270829
- async function logout(_ctx) {
270830
- await deleteAuth();
270831
- return { outroMessage: "Logged out successfully" };
270846
+ async function sendAction(ctx, message) {
270847
+ if (ctx.app)
270848
+ await assertBuilderApp(ctx.app.id);
270849
+ const branchId = await resolveBranchId(ctx);
270850
+ if (ctx.jsonMode) {
270851
+ const turn = await ctx.runTask("Agent working (a turn can take minutes)", () => sendTurn(message, branchId));
270852
+ if (turn.queued)
270853
+ return { stdout: `${JSON.stringify({ queued: true })}
270854
+ ` };
270855
+ return {
270856
+ stdout: `${JSON.stringify({
270857
+ status: turn.status?.state ?? "ready",
270858
+ error_source: turn.status?.error_source ?? null,
270859
+ reply: lastAssistantReply(turn) ?? null
270860
+ })}
270861
+ `
270862
+ };
270863
+ }
270864
+ const stream = createTurnStream(process.stdout.isTTY === true);
270865
+ let turn;
270866
+ try {
270867
+ turn = await streamConversationDuring(() => sendTurn(message, branchId), stream.onEvent, { branchId });
270868
+ } finally {
270869
+ stream.stop();
270870
+ }
270871
+ if (turn.queued) {
270872
+ return {
270873
+ outroMessage: "The agent is busy with an earlier message — yours was queued and runs next."
270874
+ };
270875
+ }
270876
+ if (turn.status?.state === "error") {
270877
+ return {
270878
+ outroMessage: `Turn failed (${turn.status.error_source ?? "unknown"}) — see the editor for details.`
270879
+ };
270880
+ }
270881
+ return { outroMessage: "Turn finished." };
270832
270882
  }
270833
- function getLogoutCommand() {
270834
- return new Base44Command("logout", {
270835
- requireAuth: false,
270836
- requireAppContext: false
270837
- }).description("Logout from current device").action(logout);
270883
+ function getSendCommand() {
270884
+ const command = new Base44Command("send", { supportsBranch: true });
270885
+ command.description("Send the agent one message and stream the turn until it finishes").argument("<message>", "What you want the agent to do").action(sendAction);
270886
+ return command;
270838
270887
  }
270839
270888
 
270840
- // src/cli/commands/auth/whoami.ts
270841
- async function whoami(_ctx) {
270842
- const workspaceApiKey = getWorkspaceApiKeyFromEnv();
270843
- if (workspaceApiKey && isWorkspaceApiKey(workspaceApiKey)) {
270889
+ // src/cli/commands/builder/status.ts
270890
+ async function statusAction(ctx) {
270891
+ const id = ctx.app?.id;
270892
+ const app = await ctx.runTask("Reading app status", () => getAppState(id));
270893
+ const state = app.status?.state ?? "ready";
270894
+ if (ctx.jsonMode) {
270844
270895
  return {
270845
- outroMessage: `Using workspace API key: ${theme.styles.bold(workspaceApiKey.slice(0, 10))}`
270896
+ stdout: `${JSON.stringify({ id: app.id, state, message: app.status?.message ?? null })}
270897
+ `
270846
270898
  };
270847
270899
  }
270848
- const auth = await readAuth();
270849
- return { outroMessage: `Logged in as: ${theme.styles.bold(auth.email)}` };
270900
+ ctx.log.message(`State: ${state}`);
270901
+ if (app.status?.message)
270902
+ ctx.log.message(`Note: ${app.status.message}`);
270903
+ return { outroMessage: "Status read." };
270850
270904
  }
270851
- function getWhoamiCommand() {
270852
- return new Base44Command("whoami", { requireAppContext: false }).description("Display current authenticated user").action(whoami);
270905
+ function getStatusCommand() {
270906
+ const command = new Base44Command("status");
270907
+ command.description("Show whether the app is building, ready, or errored").action(statusAction);
270908
+ return command;
270853
270909
  }
270854
270910
 
270855
- // src/cli/commands/branches/index.ts
270856
- async function listBranchesAction({
270857
- log,
270858
- runTask,
270859
- jsonMode
270860
- }) {
270861
- const remote = await runTask("Fetching branches", () => listBranches());
270862
- const branches = [
270863
- { name: "main", status: "active" },
270864
- ...remote.map((branch) => ({
270865
- name: branch.branch_name,
270866
- status: branch.status
270867
- }))
270868
- ];
270869
- if (jsonMode)
270870
- return { stdout: `${JSON.stringify({ branches })}
270911
+ // src/cli/commands/builder/stop.ts
270912
+ async function stopAction(ctx) {
270913
+ const branchId = await resolveBranchId(ctx);
270914
+ await ctx.runTask("Stopping the running turn", () => stopTurn(branchId));
270915
+ if (ctx.jsonMode)
270916
+ return { stdout: `${JSON.stringify({ stopped: true })}
270871
270917
  ` };
270872
- for (const branch of branches)
270873
- log.message(`${branch.name} (${branch.status})`);
270874
- return { outroMessage: `${branches.length} branches` };
270918
+ return { outroMessage: "Stopped." };
270875
270919
  }
270876
- function getBranchesCommand() {
270877
- return new Command2("branches").description("Discover an app's branches").addCommand(new Base44Command("list").description("List main and active branch names for use with --branch").action(listBranchesAction));
270920
+ function getStopCommand() {
270921
+ const command = new Base44Command("stop", { supportsBranch: true });
270922
+ command.description("Stop the agent's running turn").action(stopAction);
270923
+ return command;
270924
+ }
270925
+
270926
+ // src/cli/commands/builder/index.ts
270927
+ function getBuilderCommand() {
270928
+ return new Command2("builder").description("Build an app with the Base44 builder agent, non-interactively: create it, send turns, read status, stop, pick the model").addCommand(getNewCommand()).addCommand(getSendCommand()).addCommand(getStatusCommand()).addCommand(getStopCommand()).addCommand(getModelCommand());
270878
270929
  }
270879
270930
 
270880
270931
  // ../../node_modules/ink/build/render.js
@@ -275840,6 +275891,119 @@ var build_default = TextInput;
275840
275891
  // src/cli/commands/code/session.tsx
275841
275892
  var import_react23 = __toESM(require_react(), 1);
275842
275893
 
275894
+ // src/cli/commands/code/logo.ts
275895
+ var ROWS = 6;
275896
+ var ASPECT = 0.44;
275897
+ var SAMPLES = 4;
275898
+ var FILL = 0.6;
275899
+ var GAP_ROW = 3.5 + 0.5;
275900
+ var GAP_HEIGHT_ROWS = 0.8;
275901
+ var LOGO_COLS = Math.max(2, Math.floor(ROWS / ASPECT + 0.5));
275902
+ var OCTANT = {
275903
+ rx: 2,
275904
+ ry: 4,
275905
+ table: Array.from(" \uD83E\uDF82\uD833\uDD00▘\uD833\uDD01\uD833\uDD02\uD833\uDD03\uD833\uDD04▝\uD833\uDD05\uD833\uDD06\uD833\uDD07\uD833\uDD08▀\uD833\uDD09\uD833\uDD0A\uD833\uDD0B\uD833\uDD0C\uD833\uDD00\uD833\uDD0D\uD833\uDD0E\uD833\uDD0F\uD833\uDD10\uD833\uDD11\uD833\uDD12\uD833\uDD13\uD833\uDD14\uD833\uDD15\uD833\uDD16\uD833\uDD17\uD833\uDD18\uD833\uDD19\uD833\uDD1A\uD833\uDD1B\uD833\uDD1C\uD833\uDD1D\uD833\uDD1E\uD833\uDD1F\uD833\uDD03\uD833\uDD20\uD833\uDD21\uD833\uDD22\uD833\uDD23\uD833\uDD24\uD833\uDD25\uD833\uDD26\uD833\uDD27\uD833\uDD28\uD833\uDD29\uD833\uDD2A\uD833\uDD2B\uD833\uDD2C\uD833\uDD2D\uD833\uDD2E\uD833\uDD2F\uD833\uDD30\uD833\uDD31\uD833\uDD32\uD833\uDD33\uD833\uDD34\uD833\uDD35\uD83E\uDF85 \uD833\uDD36\uD833\uDD37\uD833\uDD38\uD833\uDD39\uD833\uDD3A\uD833\uDD3B\uD833\uDD3C\uD833\uDD3D\uD833\uDD3E\uD833\uDD3F\uD833\uDD40\uD833\uDD41\uD833\uDD42\uD833\uDD43\uD833\uDD44▖\uD833\uDD45\uD833\uDD46\uD833\uDD47\uD833\uDD48▌\uD833\uDD49\uD833\uDD4A\uD833\uDD4B\uD833\uDD4C▞\uD833\uDD4D\uD833\uDD4E\uD833\uDD4F\uD833\uDD50▛\uD833\uDD51\uD833\uDD52\uD833\uDD53\uD833\uDD54\uD833\uDD55\uD833\uDD56\uD833\uDD57\uD833\uDD58\uD833\uDD59\uD833\uDD5A\uD833\uDD5B\uD833\uDD5C\uD833\uDD5D\uD833\uDD5E\uD833\uDD5F\uD833\uDD60\uD833\uDD61\uD833\uDD62\uD833\uDD63\uD833\uDD64\uD833\uDD65\uD833\uDD66\uD833\uDD67\uD833\uDD68\uD833\uDD69\uD833\uDD6A\uD833\uDD6B\uD833\uDD6C\uD833\uDD6D\uD833\uDD6E\uD833\uDD6F\uD833\uDD70 \uD833\uDD71\uD833\uDD72\uD833\uDD73\uD833\uDD74\uD833\uDD75\uD833\uDD76\uD833\uDD77\uD833\uDD78\uD833\uDD79\uD833\uDD7A\uD833\uDD7B\uD833\uDD7C\uD833\uDD7D\uD833\uDD7E\uD833\uDD7F\uD833\uDD80\uD833\uDD81\uD833\uDD82\uD833\uDD83\uD833\uDD84\uD833\uDD85\uD833\uDD86\uD833\uDD87\uD833\uDD88\uD833\uDD89\uD833\uDD8A\uD833\uDD8B\uD833\uDD8C\uD833\uDD8D\uD833\uDD8E\uD833\uDD8F▗\uD833\uDD90\uD833\uDD91\uD833\uDD92\uD833\uDD93▚\uD833\uDD94\uD833\uDD95\uD833\uDD96\uD833\uDD97▐\uD833\uDD98\uD833\uDD99\uD833\uDD9A\uD833\uDD9B▜\uD833\uDD9C\uD833\uDD9D\uD833\uDD9E\uD833\uDD9F\uD833\uDDA0\uD833\uDDA1\uD833\uDDA2\uD833\uDDA3\uD833\uDDA4\uD833\uDDA5\uD833\uDDA6\uD833\uDDA7\uD833\uDDA8\uD833\uDDA9\uD833\uDDAA\uD833\uDDAB▂\uD833\uDDAC\uD833\uDDAD\uD833\uDDAE\uD833\uDDAF\uD833\uDDB0\uD833\uDDB1\uD833\uDDB2\uD833\uDDB3\uD833\uDDB4\uD833\uDDB5\uD833\uDDB6\uD833\uDDB7\uD833\uDDB8\uD833\uDDB9\uD833\uDDBA\uD833\uDDBB\uD833\uDDBC\uD833\uDDBD\uD833\uDDBE\uD833\uDDBF\uD833\uDDC0\uD833\uDDC1\uD833\uDDC2\uD833\uDDC3\uD833\uDDC4\uD833\uDDC5\uD833\uDDC6\uD833\uDDC7\uD833\uDDC8\uD833\uDDC9\uD833\uDDCA\uD833\uDDCB\uD833\uDDCC\uD833\uDDCD\uD833\uDDCE\uD833\uDDCF\uD833\uDDD0\uD833\uDDD1\uD833\uDDD2\uD833\uDDD3\uD833\uDDD4\uD833\uDDD5\uD833\uDDD6\uD833\uDDD7\uD833\uDDD8\uD833\uDDD9\uD833\uDDDA▄\uD833\uDDDB\uD833\uDDDC\uD833\uDDDD\uD833\uDDDE▙\uD833\uDDDF\uD833\uDDE0\uD833\uDDE1\uD833\uDDE2▟\uD833\uDDE3▆\uD833\uDDE4\uD833\uDDE5█")
275906
+ };
275907
+ var QUAD = {
275908
+ rx: 2,
275909
+ ry: 2,
275910
+ table: Array.from(" ▘▝▀▖▌▞▛▗▚▐▜▄▙▟█")
275911
+ };
275912
+ function detectTier(env = process.env) {
275913
+ const term = env.TERM ?? "";
275914
+ const prog = env.TERM_PROGRAM ?? "";
275915
+ if (prog === "ghostty" || term.includes("ghostty"))
275916
+ return "octant";
275917
+ if (env.KITTY_WINDOW_ID || term.includes("kitty"))
275918
+ return "octant";
275919
+ if (env.WEZTERM_PANE || env.WEZTERM_EXECUTABLE)
275920
+ return "octant";
275921
+ if (term.startsWith("foot") || prog === "contour")
275922
+ return "octant";
275923
+ return "quad";
275924
+ }
275925
+ function coverageGrid(rows, cols, aspect, pixelH) {
275926
+ const worldH = rows * pixelH;
275927
+ const worldW = cols * aspect;
275928
+ const radius = Math.min(worldH, worldW) / 2;
275929
+ const cx = worldW / 2;
275930
+ const cy = worldH / 2;
275931
+ const step = 1 / SAMPLES;
275932
+ const grid = [];
275933
+ for (let py = 0;py < rows; py++) {
275934
+ const y0 = py * pixelH;
275935
+ const y1 = y0 + pixelH;
275936
+ const dyLo = y0 <= cy && cy <= y1 ? 0 : Math.min(Math.abs(y0 - cy), Math.abs(y1 - cy));
275937
+ const dyHi = Math.max(Math.abs(y0 - cy), Math.abs(y1 - cy));
275938
+ const line = [];
275939
+ for (let px = 0;px < cols; px++) {
275940
+ const x0 = px * aspect;
275941
+ const x1 = x0 + aspect;
275942
+ const dxLo = x0 <= cx && cx <= x1 ? 0 : Math.min(Math.abs(x0 - cx), Math.abs(x1 - cx));
275943
+ const dxHi = Math.max(Math.abs(x0 - cx), Math.abs(x1 - cx));
275944
+ if (Math.hypot(dxLo, dyLo) >= radius) {
275945
+ line.push(0);
275946
+ continue;
275947
+ }
275948
+ if (Math.hypot(dxHi, dyHi) <= radius) {
275949
+ line.push(1);
275950
+ continue;
275951
+ }
275952
+ let hits = 0;
275953
+ for (let j = 0;j < SAMPLES; j++) {
275954
+ const dy = y0 + (j + 0.5) * step * pixelH - cy;
275955
+ for (let i = 0;i < SAMPLES; i++) {
275956
+ const dx = x0 + (i + 0.5) * step * aspect - cx;
275957
+ if (Math.hypot(dx, dy) <= radius)
275958
+ hits++;
275959
+ }
275960
+ }
275961
+ line.push(hits / (SAMPLES * SAMPLES));
275962
+ }
275963
+ grid.push(line);
275964
+ }
275965
+ return grid;
275966
+ }
275967
+ function carveGap(grid, ry) {
275968
+ const n = grid.length;
275969
+ const thick = Math.max(1, Math.round(GAP_HEIGHT_ROWS * ry));
275970
+ const centre = GAP_ROW >= 0 ? GAP_ROW * ry : n + GAP_ROW * ry;
275971
+ const start = Math.max(0, Math.min(n - thick, centre - thick / 2));
275972
+ for (let i = Math.trunc(start);i < Math.trunc(start + thick); i++) {
275973
+ grid[i].fill(0);
275974
+ }
275975
+ }
275976
+ function logoRows(color, tier = detectTier()) {
275977
+ const { rx, ry, table } = tier === "octant" ? OCTANT : QUAD;
275978
+ const grid = coverageGrid(ROWS * ry, LOGO_COLS * rx, ASPECT / rx, 1 / ry);
275979
+ carveGap(grid, ry);
275980
+ const fg = color ? source_default.hex(color) : (s) => s;
275981
+ const bg = color ? source_default.bgHex(color) : (s) => s;
275982
+ const full = (1 << rx * ry) - 1;
275983
+ const out = [];
275984
+ for (let r = 0;r < ROWS; r++) {
275985
+ let row = "";
275986
+ for (let c = 0;c < LOGO_COLS; c++) {
275987
+ let mask = 0;
275988
+ for (let sr = 0;sr < ry; sr++) {
275989
+ for (let sc = 0;sc < rx; sc++) {
275990
+ if (grid[r * ry + sr][c * rx + sc] >= FILL)
275991
+ mask |= 1 << sr * rx + sc;
275992
+ }
275993
+ }
275994
+ const glyph = table[mask];
275995
+ if (glyph === " ")
275996
+ row += " ";
275997
+ else if (mask === full && color)
275998
+ row += bg(" ");
275999
+ else
276000
+ row += fg(glyph);
276001
+ }
276002
+ out.push(row);
276003
+ }
276004
+ return out;
276005
+ }
276006
+
275843
276007
  // src/cli/commands/code/paste.ts
275844
276008
  import { PassThrough as PassThrough3 } from "node:stream";
275845
276009
  var START = "\x1B[200~";
@@ -276339,43 +276503,11 @@ function SessionView({ engine, footer, subscribe }) {
276339
276503
  ]
276340
276504
  }, undefined, true, undefined, this);
276341
276505
  }
276342
- var LOGO_ROWS = 6;
276343
- var LOGO_GAP_SUBROW = 8;
276344
- var QUAD = " ▘▝▀▖▌▞▛▗▚▐▜▄▙▟█";
276345
- function buildLogoRows() {
276346
- const sy = 2 * LOGO_ROWS;
276347
- const sx = 2 * sy;
276348
- const cx = (sx - 1) / 2;
276349
- const cy = (sy - 1) / 2;
276350
- const rad = sy / 2 - 0.5;
276351
- const on = (px, py) => {
276352
- if (py === LOGO_GAP_SUBROW)
276353
- return false;
276354
- const dx = (px - cx) * 0.5;
276355
- const dy = py - cy;
276356
- return dx * dx + dy * dy <= rad * rad;
276357
- };
276358
- const rows = [];
276359
- for (let ty = 0;ty < sy; ty += 2) {
276360
- let row = "";
276361
- for (let tx = 0;tx < sx; tx += 2) {
276362
- const bits = (on(tx, ty) ? 1 : 0) | (on(tx + 1, ty) ? 2 : 0) | (on(tx, ty + 1) ? 4 : 0) | (on(tx + 1, ty + 1) ? 8 : 0);
276363
- row += QUAD[bits];
276364
- }
276365
- rows.push(row.trim());
276366
- }
276367
- return rows.map((row) => row.trim());
276368
- }
276369
276506
  function renderHeader(who, mode) {
276370
276507
  const orange = source_default.hex(BRAND_ORANGE);
276371
276508
  const cwd = process.cwd().replace(process.env.HOME ?? "", "~");
276372
- const logo = buildLogoRows();
276373
- const logoW = Math.max(...logo.map((r) => r.length));
276374
- const center = (s) => {
276375
- const total = Math.max(0, logoW - s.length);
276376
- const left = Math.floor(total / 2);
276377
- return " ".repeat(left) + s + " ".repeat(total - left);
276378
- };
276509
+ const logo = logoRows(BRAND_ORANGE);
276510
+ const logoW = LOGO_COLS;
276379
276511
  const text = [
276380
276512
  `${orange.bold("Base44 Code")} ${source_default.dim(`v${package_default.version}`)}`,
276381
276513
  source_default.bold(who ? `Welcome back, ${who}!` : "Welcome!"),
@@ -276387,7 +276519,7 @@ function renderHeader(who, mode) {
276387
276519
  const textTop = Math.max(0, Math.floor((logo.length - text.length) / 2));
276388
276520
  const out = [];
276389
276521
  for (let i = 0;i < height; i++) {
276390
- const left = i < logo.length ? orange(center(logo[i])) : " ".repeat(logoW);
276522
+ const left = i < logo.length ? logo[i] : " ".repeat(logoW);
276391
276523
  const right = text[i - textTop] ?? "";
276392
276524
  out.push(` ${left} ${right}`.trimEnd());
276393
276525
  }
@@ -276578,19 +276710,21 @@ async function runGenesisSession(options) {
276578
276710
 
276579
276711
  // src/cli/commands/code/index.ts
276580
276712
  var BRAND_ORANGE2 = "#E86B3C";
276581
- async function bootstrapApp(prompt, footer, emit, importRepo) {
276713
+ async function bootstrapApp(prompt, footer, emit, onCreated, importRepo, path) {
276582
276714
  let app;
276583
276715
  try {
276584
- app = await createAndLinkApp({ prompt, importRepo });
276716
+ app = await createAndLinkApp({ prompt, importRepo, path });
276585
276717
  } catch (error) {
276586
276718
  for (const line of await githubReauthLines(error) ?? [])
276587
276719
  emit(line);
276588
276720
  throw error;
276589
276721
  }
276722
+ onCreated(app);
276723
+ footer.push(source_default.dim(`dir ${app.here ? "./" : `./${app.dirName}`}`));
276590
276724
  if (app.repoUrl)
276591
276725
  footer.push(terminalLink("repo", app.repoUrl));
276592
276726
  footer.push(terminalLink("editor", app.editorUrl));
276593
- emit(source_default.dim(`linked ./${app.dirName} (cd ${app.dirName} after the session)`));
276727
+ emit(source_default.dim(app.here ? "linked ./ (this directory)" : `linked ./${app.dirName} (cd ${app.dirName} after the session)`));
276594
276728
  const branchId = await resolveActiveBranchId().catch(() => {
276595
276729
  return;
276596
276730
  });
@@ -276614,46 +276748,64 @@ async function bootstrapApp(prompt, footer, emit, importRepo) {
276614
276748
  }
276615
276749
  };
276616
276750
  }
276617
- async function codeAction(_ctx, options) {
276751
+ async function codeAction({ log }, options, appId) {
276752
+ const orange = source_default.hex(BRAND_ORANGE2);
276753
+ const chip = (label) => source_default.dim(`${orange("●")} ${label}`);
276618
276754
  if (process.stdout.isTTY !== true) {
276619
276755
  throw new InvalidInputError("base44 code is an interactive session and needs a terminal.");
276620
276756
  }
276621
276757
  let linked = false;
276622
276758
  try {
276623
- await initAppContext();
276759
+ await initAppContext(appId ? { appId } : {});
276624
276760
  linked = true;
276625
276761
  } catch {}
276626
276762
  if (linked) {
276627
- if (options.import) {
276628
- throw new InvalidInputError("--import creates a new app; run it outside a linked project.");
276763
+ if (options.import || options.path) {
276764
+ throw new InvalidInputError("--import and --path create a new app; run them outside a linked project, without --app-id.");
276629
276765
  }
276766
+ const { id, projectRoot } = getAppContext();
276767
+ const state = await assertBuilderApp(id);
276630
276768
  const branchId = await resolveActiveBranchId().catch(() => {
276631
276769
  return;
276632
276770
  });
276633
276771
  await runInteractiveSession({
276634
276772
  branchId,
276635
- footer: [],
276773
+ footer: [chip(appTypeChip(state))],
276636
276774
  primeFirstPoll: true,
276637
276775
  idleHint: "what should the agent do next?"
276638
276776
  });
276639
- return { outroMessage: "Done." };
276777
+ if (projectRoot) {
276778
+ log.message(source_default.dim(`app dir ${projectRoot}`));
276779
+ return {
276780
+ outroMessage: "Session closed. Run `base44 code` here to resume."
276781
+ };
276782
+ }
276783
+ return {
276784
+ outroMessage: `Session closed. Resume with \`base44 code --app-id ${id}\`.`
276785
+ };
276640
276786
  }
276641
- const orange = source_default.hex(BRAND_ORANGE2);
276642
- const footer = [
276643
- source_default.dim(`${orange("●")} ${options.import ? "import" : "builder"}`)
276644
- ];
276787
+ let created;
276788
+ const footer = [chip(options.import ? repoLabel(options.import) : "web app")];
276645
276789
  await runGenesisSession({
276646
- idleHint: options.import ? "describe what to build over the repo" : "describe the app you want to build",
276790
+ idleHint: options.import ? "describe what to build over the repository" : "describe the app you want to build · have one already? base44 code --app-id <id>, or base44 link",
276647
276791
  creatingLabel: options.import ? "importing the repository" : "creating your app",
276648
- modeLabel: options.import ? `Import — ${options.import}` : "Builder — Base44 template + agent",
276792
+ modeLabel: options.import ? `Repository — ${repoLabel(options.import)}` : "Web app — Base44 template + builder agent",
276649
276793
  footer,
276650
- createApp: (prompt, emit) => bootstrapApp(prompt, footer, emit, options.import)
276794
+ createApp: (prompt, emit) => bootstrapApp(prompt, footer, emit, (app) => {
276795
+ created = app;
276796
+ }, options.import, options.path)
276651
276797
  });
276652
- return { outroMessage: "Done." };
276798
+ if (!created) {
276799
+ return { outroMessage: "Session closed. No app was created." };
276800
+ }
276801
+ for (const line of nextStepsLines(created))
276802
+ log.message(line);
276803
+ log.message(source_default.dim(` editor ${created.editorUrl}`));
276804
+ return { outroMessage: "Session closed." };
276653
276805
  }
276654
276806
  function getCodeCommand() {
276655
276807
  const command = new Base44Command("code", { requireAppContext: false });
276656
- command.description("Open Base44 Code: an interactive agent session in an empty directory your first prompt creates the app (add --import <repo> to build over an existing repository)").option("--import <repo>", "Build over an existing GitHub repository instead of the Base44 template").action(codeAction);
276808
+ command.description("Open Base44 Code, an interactive builder session. In a linked directory (or with --app-id <id>) it opens that app; anywhere else your first prompt creates one (--import <repo> to build over your own repository). Attach a directory to an existing app with base44 link.").option("--import <repo>", "Build over an existing GitHub repository instead of the Base44 template").option("--path <dir>", "Directory to link the new app to (default: the current directory when empty, else ./<name>)").action((ctx, options) => codeAction(ctx, options, command.optsWithGlobals().appId));
276657
276809
  return command;
276658
276810
  }
276659
276811
 
@@ -277451,10 +277603,10 @@ function getConnectorsListAvailableCommand() {
277451
277603
  }
277452
277604
 
277453
277605
  // src/cli/commands/connectors/pull.ts
277454
- import { dirname as dirname19, join as join27, resolve as resolve8 } from "node:path";
277606
+ import { dirname as dirname19, join as join27, resolve as resolve9 } from "node:path";
277455
277607
  async function resolveConnectorsDir(options) {
277456
277608
  if (!getAppContext().projectRoot) {
277457
- return resolve8(options.dir ?? "connectors");
277609
+ return resolve9(options.dir ?? "connectors");
277458
277610
  }
277459
277611
  const { project } = await readProjectConfig();
277460
277612
  return join27(dirname19(project.configPath), project.connectorsDir);
@@ -277498,10 +277650,10 @@ function getConnectorsPullCommand() {
277498
277650
  }
277499
277651
 
277500
277652
  // src/cli/commands/connectors/push.ts
277501
- import { resolve as resolve9 } from "node:path";
277653
+ import { resolve as resolve10 } from "node:path";
277502
277654
  async function readConnectorsToPush(options) {
277503
277655
  if (!getAppContext().projectRoot) {
277504
- return readAllConnectors(resolve9(options.dir ?? "connectors"));
277656
+ return readAllConnectors(resolve10(options.dir ?? "connectors"));
277505
277657
  }
277506
277658
  const { connectors } = await readProjectConfig();
277507
277659
  return connectors;
@@ -277970,7 +278122,7 @@ function getBuildCommand() {
277970
278122
  }
277971
278123
 
277972
278124
  // src/cli/commands/project/create.ts
277973
- import { basename as basename6, resolve as resolve10 } from "node:path";
278125
+ import { basename as basename7, resolve as resolve11 } from "node:path";
277974
278126
  var import_kebabCase2 = __toESM(require_kebabCase(), 1);
277975
278127
 
277976
278128
  // src/cli/commands/project/scaffold-shared.ts
@@ -278135,8 +278287,8 @@ async function createInteractive(options, ctx) {
278135
278287
  name: () => {
278136
278288
  return options.name ? Promise.resolve(options.name) : Ze({
278137
278289
  message: "What is the name of your project?",
278138
- placeholder: basename6(process.cwd()),
278139
- initialValue: basename6(process.cwd()),
278290
+ placeholder: basename7(process.cwd()),
278291
+ initialValue: basename7(process.cwd()),
278140
278292
  validate: (value) => {
278141
278293
  if (!value || value.trim().length === 0) {
278142
278294
  return "Every project deserves a name";
@@ -278166,7 +278318,7 @@ async function createInteractive(options, ctx) {
278166
278318
  }, ctx);
278167
278319
  }
278168
278320
  async function createNonInteractive(options, ctx) {
278169
- ctx.log.info(`Creating a new project at ${resolve10(options.path)}`);
278321
+ ctx.log.info(`Creating a new project at ${resolve11(options.path)}`);
278170
278322
  const template = await getTemplateById(options.template ?? DEFAULT_TEMPLATE_ID);
278171
278323
  return await executeCreate({
278172
278324
  template,
@@ -278190,7 +278342,7 @@ async function executeCreate({
278190
278342
  }, ctx) {
278191
278343
  const { log, runTask } = ctx;
278192
278344
  const name = rawName.trim();
278193
- const resolvedPath = resolve10(projectPath);
278345
+ const resolvedPath = resolve11(projectPath);
278194
278346
  const organizationId = await resolveWorkspaceId(ctx, flagWorkspaceId, isInteractive);
278195
278347
  const { projectId } = await runTask("Setting up your project...", async () => {
278196
278348
  return await createProjectFiles({
@@ -278831,7 +278983,7 @@ function getLogsCommand() {
278831
278983
  }
278832
278984
 
278833
278985
  // src/cli/commands/project/scaffold.ts
278834
- import { basename as basename7, resolve as resolve11 } from "node:path";
278986
+ import { basename as basename8, resolve as resolve12 } from "node:path";
278835
278987
  function resolveAppId(options) {
278836
278988
  const appId = options.appId;
278837
278989
  if (!appId) {
@@ -278847,8 +278999,8 @@ function resolveAppId(options) {
278847
278999
  async function scaffoldAction(ctx, name, options, command) {
278848
279000
  const { log, runTask } = ctx;
278849
279001
  const appId = resolveAppId(command.optsWithGlobals());
278850
- const resolvedPath = resolve11("./");
278851
- const projectName = (name ?? basename7(resolvedPath)).trim();
279002
+ const resolvedPath = resolve12("./");
279003
+ const projectName = (name ?? basename8(resolvedPath)).trim();
278852
279004
  const template = await getTemplateById("backend-only");
278853
279005
  log.info(`Scaffolding project at ${resolvedPath}`);
278854
279006
  const { projectId } = await runTask("Setting up your project...", async () => {
@@ -279165,6 +279317,21 @@ function getSandboxListDirectoryCommand() {
279165
279317
  return new Base44Command("ls", { supportsBranch: true }).description("List directory entries in an app's remote sandbox").argument("[path]", "Directory relative to the app root (default: app root)").option("--recursive", "List nested entries").option("--max-depth <n>", "Max depth when recursive (1-10, default 3)").option("--include-hidden", "Include dotfiles").action(listDirectoryAction);
279166
279318
  }
279167
279319
 
279320
+ // src/cli/commands/sandbox/preview.ts
279321
+ async function previewAction(ctx) {
279322
+ const url = await ctx.runTask("Resolving preview URL (boots the sandbox if needed)", () => getPreviewUrl());
279323
+ if (ctx.jsonMode)
279324
+ return { stdout: `${JSON.stringify({ preview_url: url })}
279325
+ ` };
279326
+ ctx.log.message(url);
279327
+ return { outroMessage: "Preview is live." };
279328
+ }
279329
+ function getSandboxPreviewCommand() {
279330
+ const command = new Base44Command("preview");
279331
+ command.description("Print the app's live preview URL").action(previewAction);
279332
+ return command;
279333
+ }
279334
+
279168
279335
  // src/cli/commands/sandbox/read-file.ts
279169
279336
  async function readFileAction({ runTask, branchId }, paths, options) {
279170
279337
  const { id: appId } = getAppContext();
@@ -279218,7 +279385,7 @@ Examples:
279218
279385
 
279219
279386
  // src/cli/commands/sandbox/index.ts
279220
279387
  function getSandboxCommand() {
279221
- return new Command2("sandbox").description("Develop an app remotely via its server-side sandbox").addCommand(getSandboxListDirectoryCommand()).addCommand(getSandboxReadFileCommand()).addCommand(getSandboxWriteFileCommand()).addCommand(getSandboxEditFileCommand()).addCommand(getSandboxGrepCommand()).addCommand(getSandboxRunCommandCommand()).addCommand(getSandboxCheckpointCommand());
279388
+ return new Command2("sandbox").description("Develop an app remotely via its server-side sandbox").addCommand(getSandboxListDirectoryCommand()).addCommand(getSandboxReadFileCommand()).addCommand(getSandboxWriteFileCommand()).addCommand(getSandboxEditFileCommand()).addCommand(getSandboxGrepCommand()).addCommand(getSandboxRunCommandCommand()).addCommand(getSandboxCheckpointCommand()).addCommand(getSandboxPreviewCommand());
279222
279389
  }
279223
279390
 
279224
279391
  // src/cli/commands/secrets/delete.ts
@@ -279264,7 +279431,7 @@ function getSecretsListCommand() {
279264
279431
  }
279265
279432
 
279266
279433
  // src/cli/commands/secrets/set.ts
279267
- import { resolve as resolve12 } from "node:path";
279434
+ import { resolve as resolve13 } from "node:path";
279268
279435
  function parseEntries(entries) {
279269
279436
  const secrets = {};
279270
279437
  for (const entry of entries) {
@@ -279295,7 +279462,7 @@ async function setSecretsAction({ log, runTask }, entries, options) {
279295
279462
  validateInput(entries, options);
279296
279463
  let secrets;
279297
279464
  if (options.envFile) {
279298
- secrets = await parseEnvFile(resolve12(options.envFile));
279465
+ secrets = await parseEnvFile(resolve13(options.envFile));
279299
279466
  if (Object.keys(secrets).length === 0) {
279300
279467
  throw new InvalidInputError("The env file contains no valid KEY=VALUE entries.");
279301
279468
  }
@@ -279324,7 +279491,7 @@ function getSecretsCommand() {
279324
279491
  }
279325
279492
 
279326
279493
  // src/cli/commands/site/deploy.ts
279327
- import { resolve as resolve13 } from "node:path";
279494
+ import { resolve as resolve14 } from "node:path";
279328
279495
  async function deployAction2(ctx, options) {
279329
279496
  const { isNonInteractive } = ctx;
279330
279497
  if (isNonInteractive && !options.yes) {
@@ -279402,7 +279569,7 @@ async function deployTarball({ runTask }, project) {
279402
279569
  }
279403
279570
  function siteOutputDir(project) {
279404
279571
  const outputDirectory = project.site?.outputDirectory;
279405
- return outputDirectory ? resolve13(project.root, outputDirectory) : null;
279572
+ return outputDirectory ? resolve14(project.root, outputDirectory) : null;
279406
279573
  }
279407
279574
  function getSiteDeployCommand() {
279408
279575
  const command = new Base44Command("deploy").description("Deploy built site files to Base44 hosting").option("-y, --yes", "Skip confirmation prompt").option("--build", "Build the site before deploying (skips the prompt)").option("--no-build", "Deploy without building (skips the prompt)");
@@ -281904,7 +282071,7 @@ function createCustomIntegrationRoutes(remoteProxy, logger) {
281904
282071
 
281905
282072
  // src/cli/dev/dev-server/watcher.ts
281906
282073
  import { EventEmitter as EventEmitter6 } from "node:events";
281907
- import { relative as relative9 } from "node:path";
282074
+ import { relative as relative10 } from "node:path";
281908
282075
 
281909
282076
  // ../../node_modules/chokidar/index.js
281910
282077
  import { EventEmitter as EventEmitter5 } from "node:events";
@@ -283589,7 +283756,7 @@ class WatchBase44 extends EventEmitter6 {
283589
283756
  ignoreInitial: true
283590
283757
  });
283591
283758
  watcher.on("all", import_debounce4.default(async (_event, path) => {
283592
- this.emit("change", name, relative9(targetPath, path));
283759
+ this.emit("change", name, relative10(targetPath, path));
283593
283760
  }, WATCH_DEBOUNCE_MS));
283594
283761
  watcher.on("error", (err) => {
283595
283762
  this.logger.error(`Watch handler failed for ${targetPath}`, err);
@@ -284099,7 +284266,7 @@ Examples:
284099
284266
  }
284100
284267
 
284101
284268
  // src/cli/commands/project/eject.ts
284102
- import { resolve as resolve17 } from "node:path";
284269
+ import { resolve as resolve18 } from "node:path";
284103
284270
  var import_kebabCase3 = __toESM(require_kebabCase(), 1);
284104
284271
  async function eject(ctx, options, command) {
284105
284272
  const { log, runTask, isNonInteractive } = ctx;
@@ -284163,7 +284330,7 @@ async function eject(ctx, options, command) {
284163
284330
  Ne("Operation cancelled.");
284164
284331
  throw new CLIExitError(0);
284165
284332
  }
284166
- const resolvedPath = resolve17(selectedPath);
284333
+ const resolvedPath = resolve18(selectedPath);
284167
284334
  await runTask("Downloading your project's code...", async (updateMessage) => {
284168
284335
  await createProjectFilesForExistingProject({
284169
284336
  projectId,
@@ -284245,7 +284412,7 @@ function createProgram(context) {
284245
284412
  program.addCommand(getSecretsCommand());
284246
284413
  program.addCommand(getSandboxCommand());
284247
284414
  program.addCommand(getBranchesCommand());
284248
- program.addCommand(getAppCommand());
284415
+ program.addCommand(getBuilderCommand());
284249
284416
  program.addCommand(getCodeCommand());
284250
284417
  program.addCommand(getAuthCommand());
284251
284418
  program.addCommand(getSiteCommand());
@@ -288300,4 +288467,4 @@ export {
288300
288467
  runCLI
288301
288468
  };
288302
288469
 
288303
- //# debugId=0D8DC4BF0CB9F93A64756E2164756E21
288470
+ //# debugId=C14BEE0A94355AC364756E2164756E21