@wix/himalaya-cli 0.1181.0 → 0.1183.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1391,6 +1391,21 @@ function defaultWorkerTsconfig() {
1391
1391
  function offRepoImports(body) {
1392
1392
  return body.replace(/(["'])@himalaya\/([^"']+)\1/g, "$1@wix/himalaya/$2$1");
1393
1393
  }
1394
+ function templateTakesBase44AppId(template) {
1395
+ if (!availableTemplates().includes(template)) return void 0;
1396
+ const config = join3(templateSourceDir(template), "config.ts");
1397
+ return existsSync2(config) && readFileSync3(config, "utf8").includes(`"${BASE44_APP_ID_PLACEHOLDER}"`);
1398
+ }
1399
+ function applyBase44AppId(dir, appId2) {
1400
+ if (!BASE44_APP_ID_RE.test(appId2)) throw new Error(`"${appId2}" is not a Base44 app id (8\u201364 letters and digits)`);
1401
+ const config = join3(dir, "config.ts");
1402
+ if (!existsSync2(config)) return false;
1403
+ const body = readFileSync3(config, "utf8");
1404
+ const literal = `"${BASE44_APP_ID_PLACEHOLDER}"`;
1405
+ if (!body.includes(literal)) return false;
1406
+ writeFileSync3(config, body.replaceAll(literal, `"${appId2}"`));
1407
+ return true;
1408
+ }
1394
1409
  function templateIconPath(name) {
1395
1410
  if (!/^[a-z0-9][a-z0-9-]*$/i.test(name)) return null;
1396
1411
  const p = join3(TEMPLATE_ICONS_DIR, `${name}.png`);
@@ -1785,7 +1800,7 @@ A per-app \`npm i @wix/himalaya\` is optional and only buys your editor local ty
1785
1800
  );
1786
1801
  return finishScaffold({ dir, files, ...kept.length ? { kept: kept.sort() } : {} });
1787
1802
  }
1788
- var PLAN_FILES, PACKAGE_GITIGNORE, MODULE_SOURCE_FILE, TEMPLATES_DIR, SHOWCASE_TEMPLATES_DIR, SHOWCASE_TEMPLATE_NAMES, TEMPLATE_META_FILE, TEMPLATE_ICONS_DIR, HEX_COLOR, PALETTE_KEYS, IDENTITY_SENTINELS, SENTINEL_RE, escapeRe, NAME_FIELDS, IDENTITY_FILES, DOTID_RE, BINARY_EXTENSIONS, extOf;
1803
+ var PLAN_FILES, PACKAGE_GITIGNORE, MODULE_SOURCE_FILE, TEMPLATES_DIR, SHOWCASE_TEMPLATES_DIR, SHOWCASE_TEMPLATE_NAMES, BASE44_APP_ID_PLACEHOLDER, BASE44_APP_ID_RE, TEMPLATE_META_FILE, TEMPLATE_ICONS_DIR, HEX_COLOR, PALETTE_KEYS, IDENTITY_SENTINELS, SENTINEL_RE, escapeRe, NAME_FIELDS, IDENTITY_FILES, DOTID_RE, BINARY_EXTENSIONS, extOf;
1789
1804
  var init_scaffold = __esm({
1790
1805
  "src/scaffold.ts"() {
1791
1806
  init_source_deny();
@@ -1796,7 +1811,9 @@ var init_scaffold = __esm({
1796
1811
  MODULE_SOURCE_FILE = /\.(?:[cm]?[jt]sx?)$/i;
1797
1812
  TEMPLATES_DIR = resolve2(dirname2(fileURLToPath(import.meta.url)), "../templates");
1798
1813
  SHOWCASE_TEMPLATES_DIR = resolve2(dirname2(fileURLToPath(import.meta.url)), "../showcase-apps");
1799
- SHOWCASE_TEMPLATE_NAMES = ["video-courses"];
1814
+ SHOWCASE_TEMPLATE_NAMES = ["video-courses", "base44-starter"];
1815
+ BASE44_APP_ID_PLACEHOLDER = "ReplaceWithYourBase44AppId";
1816
+ BASE44_APP_ID_RE = /^[A-Za-z0-9]{8,64}$/;
1800
1817
  TEMPLATE_META_FILE = "himalaya.template.json";
1801
1818
  TEMPLATE_ICONS_DIR = resolve2(dirname2(fileURLToPath(import.meta.url)), "../template-icons");
1802
1819
  HEX_COLOR = /^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
@@ -6790,7 +6807,6 @@ function walk(node, registry, path2, out2) {
6790
6807
  var SYSTEM_FONT_FAMILY, TONAL_ELEVATION_ALPHA, COLOR_TOKEN_PROPS, TYPOGRAPHY_TOKEN_PROPS;
6791
6808
  var init_tokens = __esm({
6792
6809
  "../../core/ts/src/tokens.ts"() {
6793
- "use strict";
6794
6810
  SYSTEM_FONT_FAMILY = "$system";
6795
6811
  TONAL_ELEVATION_ALPHA = [0, 0.05, 0.08, 0.11, 0.12, 0.14];
6796
6812
  COLOR_TOKEN_PROPS = {
@@ -24526,7 +24542,6 @@ function shouldRevealError(args) {
24526
24542
  var EMAIL_RE, URL_HOST_RE, INTEGER_RE, NUMBER_RE;
24527
24543
  var init_validation = __esm({
24528
24544
  "../../core/ts/src/validation.ts"() {
24529
- "use strict";
24530
24545
  EMAIL_RE = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$/;
24531
24546
  URL_HOST_RE = /^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*(:[0-9]+)?$/;
24532
24547
  INTEGER_RE = /^[+-]?[0-9]+$/;
@@ -25186,7 +25201,6 @@ function validateWorkerSubtree(raw, slotId, policy, actionCatalog) {
25186
25201
  var WORKER_SUBTREE_CEILINGS, ID_PART, ACTION_ID;
25187
25202
  var init_worker_subtree = __esm({
25188
25203
  "../../core/ts/src/worker-subtree.ts"() {
25189
- "use strict";
25190
25204
  init_worker_subtree_safe_types_generated();
25191
25205
  WORKER_SUBTREE_CEILINGS = Object.freeze({
25192
25206
  maxEncodedBytes: 65536,
@@ -78140,9 +78154,11 @@ var USAGE = `himi \u2014 the Himalaya authoring CLI (remote dev loop)
78140
78154
  create/store or inspect CAPTCHA keys (not login tokens).
78141
78155
  Setup needs final --ios-bundle-id / --android-package-name.
78142
78156
 
78143
- himi init <name> [--dir <parent>] [--template <t>] [--remote] [--push-setup | --no-push-setup] [--no-agent] [--no-first-look]
78157
+ himi init <name> [--dir <parent>] [--template <t>] [--remote] [--base44-app-id <id>] [--push-setup | --no-push-setup] [--no-agent] [--no-first-look]
78144
78158
  scaffold a starter content package (--template <t> starts
78145
- from a reference app; --remote fetches it off the serve plane),
78159
+ from a reference app; --remote fetches it off the serve plane;
78160
+ --base44-app-id writes a Base44 app id into a Base44 template
78161
+ such as base44-starter \u2014 himi docs base44-app-to-himi-app),
78146
78162
  with the agent files inside it (AGENTS.md, CLAUDE.md, the himalaya
78147
78163
  skill, a session-start hook); --no-agent skips them. Also makes
78148
78164
  the build board (no browser is opened) and the first picture of
@@ -78873,7 +78889,7 @@ WHAT A PATCH DOES NOT CARRY \u2014 all reported in the output, never silent
78873
78889
  himi docs releasing-with-himi-serve`;
78874
78890
  var COMMANDS2 = [
78875
78891
  { name: "captcha", summary: "create and store native CAPTCHA keys in Serve, or inspect setup status; customer setup verifies Wix site ownership; does not enable member CAPTCHA SDKs", required: "setup|status --app <app> [--content <dir>] [--site <msid>] [--ios-bundle-id <id>] [--android-package-name <id>] [--target <t>]" },
78876
- { name: "init", summary: "scaffold a starter content package (OTA-only; no native shells). --template <t> starts from a full reference app (branded-lite = consumer storefront over a Wix site; owner-lite = owner CRUD/management) instead of the minimal counter; --templates lists bundled ones; --remote fetches the template off the serve plane (falls back to the bundled copy offline). The new app also gets its agent files (the same set `himi agent-init` writes: AGENTS.md, CLAUDE.md, the himalaya skill, HIMALAYA.md, a SessionStart hook) so the next session started inside it picks the work up; --no-agent skips them. It also makes the build board (.himi/studio/, exactly as `himi studio --no-open` does \u2014 it never opens a browser), so every later render, test, validate, push and progress entry refreshes it; and, for a template bundled with this CLI, it makes the FIRST PICTURE itself (never for a --remote one: that is its publisher's code, and a render runs it on this machine, so next[] leaves the render to the agent after it has read the code): exactly `himi render <homeScreenId> --png --mocks --device iphone` (offline \u2014 no request; never --live; --mocks loads the app's recorded data when it has any), run as a child it kills after a time budget, writing .himi/shots/<home>.png and journaling it (by: himi) as the STARTING POINT (\"Starting point: the <template> template, before any of your changes\") so the board and the journal hold a picture when init returns. init also records the source it wrote (.himi/init-source.json: a content fingerprint, Markdown left out), and the first home PNG render after the source diverges from it is journaled once as \"First look at your app\" \u2014 reported as `firstLook: {image}` or `{skipped: <reason>}`, never a failed init; `--no-first-look` skips it (for scripts and CI that render themselves). For a bundled template (never a --remote one \u2014 the preview would build and serve its publisher's code) init also STARTS the live preview itself: `himi run --watch --content <dir> --port <p> --no-open --idle-exit 30`, detached in its own process group with its output in .himi/watch.log, never waited for (the url is chosen before it builds), recorded in .himi/watch.json, and reported as `watch: {started, pid, url, board, log}` \u2014 or `{skipped: <reason>}` for a `CI` environment, a test runner, a watch already running, or a start that failed (never a failed init). Nothing else skips it: outside CI it is started, and `watch.note` says an opt-out was ignored. The envelope LEADS with `doNow`: three lines to run now (the consent preflight, the live preview \u2014 its url and board when init started it, else `himi run --watch --content <dir> &` with the board's path \u2014 and the postcard line), and ENDS with the same lines as `doNowAgain`, so an agent that reads only the head or only the tail (`| tail`) gets them; at a terminal they are also printed as plain text on stderr, before the JSON and again after it. Its `next[]` LEADS with the steps for THIS session, in this order for every app \u2014 the offline first render and that picture's `himi progress` postcard (only when init could not make the picture), the skill's consent preflight (authorize or mock any Wix or external startup request; with none, go straight on), and only then the live preview (the running one's url, or `himi run --watch &` when init did not start it) \u2014 ahead of the next-session guidance", required: "<name> [--dir] [--template <t>] [--remote] [--no-agent] [--no-first-look] | --templates" },
78892
+ { name: "init", summary: "scaffold a starter content package (OTA-only; no native shells). --template <t> starts from a full reference app (branded-lite = consumer storefront over a Wix site; owner-lite = owner CRUD/management) instead of the minimal counter; --templates lists bundled ones; --remote fetches the template off the serve plane (falls back to the bundled copy offline). The new app also gets its agent files (the same set `himi agent-init` writes: AGENTS.md, CLAUDE.md, the himalaya skill, HIMALAYA.md, a SessionStart hook) so the next session started inside it picks the work up; --no-agent skips them. It also makes the build board (.himi/studio/, exactly as `himi studio --no-open` does \u2014 it never opens a browser), so every later render, test, validate, push and progress entry refreshes it; and, for a template bundled with this CLI, it makes the FIRST PICTURE itself (never for a --remote one: that is its publisher's code, and a render runs it on this machine, so next[] leaves the render to the agent after it has read the code): exactly `himi render <homeScreenId> --png --mocks --device iphone` (offline \u2014 no request; never --live; --mocks loads the app's recorded data when it has any), run as a child it kills after a time budget, writing .himi/shots/<home>.png and journaling it (by: himi) as the STARTING POINT (\"Starting point: the <template> template, before any of your changes\") so the board and the journal hold a picture when init returns. init also records the source it wrote (.himi/init-source.json: a content fingerprint, Markdown left out), and the first home PNG render after the source diverges from it is journaled once as \"First look at your app\" \u2014 reported as `firstLook: {image}` or `{skipped: <reason>}`, never a failed init; `--no-first-look` skips it (for scripts and CI that render themselves). For a bundled template (never a --remote one \u2014 the preview would build and serve its publisher's code) init also STARTS the live preview itself: `himi run --watch --content <dir> --port <p> --no-open --idle-exit 30`, detached in its own process group with its output in .himi/watch.log, never waited for (the url is chosen before it builds), recorded in .himi/watch.json, and reported as `watch: {started, pid, url, board, log}` \u2014 or `{skipped: <reason>}` for a `CI` environment, a test runner, a watch already running, or a start that failed (never a failed init). Nothing else skips it: outside CI it is started, and `watch.note` says an opt-out was ignored. The envelope LEADS with `doNow`: three lines to run now (the consent preflight, the live preview \u2014 its url and board when init started it, else `himi run --watch --content <dir> &` with the board's path \u2014 and the postcard line), and ENDS with the same lines as `doNowAgain`, so an agent that reads only the head or only the tail (`| tail`) gets them; at a terminal they are also printed as plain text on stderr, before the JSON and again after it. Its `next[]` LEADS with the steps for THIS session, in this order for every app \u2014 the offline first render and that picture's `himi progress` postcard (only when init could not make the picture), the skill's consent preflight (authorize or mock any Wix or external startup request; with none, go straight on), and only then the live preview (the running one's url, or `himi run --watch &` when init did not start it) \u2014 ahead of the next-session guidance", required: "<name> [--dir] [--template <t>] [--remote] [--base44-app-id <id>] [--no-agent] [--no-first-look] | --templates" },
78877
78893
  { name: "template", summary: "starter-template catalog + publishing. Every template is a SHOWCASE APP \u2014 a real published release you can open running before you start from it. `list` prints available templates with metadata (title/description/tags) plus each one's start prompt \u2014 pass --remote to include the serve-plane catalog so an agent can pick one; `prompt <name>` prints just that sentence for one template (the skill URL is the public door, or the plane named by --target/--serve-url); `publish` publishes an app AS a `himi init --template` (needs a himalaya.template.json; builds a previewable release + attaches the scaffold source; --source-dir overrides which tree becomes the source, --name sets the template name; bakes a launcher icon \u2014 --icon <png>, or the committed template-icons/<name>.png \u2014 and extracts accent/palette gallery meta from the scaffold's tokens.json; --visibility unlisted keeps it out of the /v1/templates catalog \u2014 templates default to public); `trim --keep <ids>` cuts an app scaffolded from a template down to those screens (the home, and any screen the navigation pins such as a drawer body, always stays) \u2014 it deletes the other SCREENS/TEMPLATED entries whole, prunes navigation (linkableScreens, tabBar, drawer, deep links, surfaces) and drops tier5-src workers no kept screen uses, then runs validate --strict and rolls everything back if it fails (--force keeps it, --dry-run writes nothing). A kept screen that links to a removed one is refused unless --links follow (keep those too) or --links drop (remove the taps). Adopting one: `inspect` writes the adoption inventory (.himi/adopt.json: screens, workers, which screens can call Wix/network on appear \u2014 bootsOffline \u2014 placeholder and sample copy, proposed Data access rows, typed-journey stubs); `screens` lists, shows, rewrites or adds ONE dev/index.ts entry; `sweep` replaces sample identity/copy inside string literals; `recommend` ranks the catalog for a business description or a site's verticals; `trim --from-spec` takes the kept screens from SPEC.md's Screen catalogue", required: "list [--remote] | prompt <name> | publish [--dir <app>] [--source-dir <dir>] [--name <n>] [--icon <png>] [--visibility public|unlisted] [--target <t>] | trim --keep <id,id,\u2026>|--from-spec [SPEC.md] [--content <dir>] [--links follow|drop] [--dry-run] [--force] | inspect [--content <dir>] [--dry-run] | screens [--show <id>] [--write <id> --from <file>] [--add <id> --from <file>] | sweep --set 'Old=>New' | --map <file> [--dry-run] | recommend [--text <t>] [--verticals a,b] [--book <dir>] [--remote]" },
78878
78894
  { name: "build", summary: "compile tier5-src/<name>/index.ts -> dist/bundles/<name>.bundle.js with esbuild (--binary = resolve published @himalaya/*); semantic TypeScript checking belongs to himi validate", required: "[--content <dir>] [--binary]" },
78879
78895
  { name: "functions", summary: "inspect/invoke server functions and operate durable jobs or schedules", required: "list|describe|openapi|invoke | jobs list|get|cancel|retry|dlq | schedule describe|invoke [--app <a>] [--environment preview|production] [--release <id>]" },
@@ -80030,7 +80046,9 @@ function keptPlan(kept) {
80030
80046
  const note = keptPlanNote(kept);
80031
80047
  return note ? { kept, note } : {};
80032
80048
  }
80033
- async function initFromRemoteTemplate(name, parentDir, template, c) {
80049
+ var NoBase44SlotError = class extends Error {
80050
+ };
80051
+ async function initFromRemoteTemplate(name, parentDir, template, c, opts = {}) {
80034
80052
  const dir = resolve56(parentDir, name);
80035
80053
  if (existsSync73(join74(dir, "himalaya.content.json"))) {
80036
80054
  throw new Error(`content package already exists at ${dir}`);
@@ -80041,6 +80059,9 @@ async function initFromRemoteTemplate(name, parentDir, template, c) {
80041
80059
  if (!archive?.files || typeof archive.files !== "object") {
80042
80060
  throw new Error(`template "${template}" returned no source files`);
80043
80061
  }
80062
+ if (opts.requireBase44Slot && !String(archive.files["config.ts"] ?? "").includes(`"${BASE44_APP_ID_PLACEHOLDER}"`)) {
80063
+ throw new NoBase44SlotError(`template "${template}" has no Base44 app id to fill`);
80064
+ }
80044
80065
  return finishScaffold(materializeSource(archive.files, name, dir), template);
80045
80066
  }
80046
80067
  function contentDir(flags) {
@@ -80955,6 +80976,26 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
80955
80976
  }
80956
80977
  const template = str9(flags.template);
80957
80978
  const parentDir = resolve56(str9(flags.dir) ?? process.cwd());
80979
+ const base44AppId = flags["base44-app-id"] === void 0 ? void 0 : String(flags["base44-app-id"]);
80980
+ if (base44AppId !== void 0) {
80981
+ if (!BASE44_APP_ID_RE.test(base44AppId)) {
80982
+ io.error(JSON.stringify({ error: `--base44-app-id "${base44AppId}" is not a Base44 app id (8\u201364 letters and digits)`, hint: "it is the id in your app's URL: https://app.base44.com/apps/<id>" }));
80983
+ return 2;
80984
+ }
80985
+ if (!template) {
80986
+ io.error(JSON.stringify({ error: "--base44-app-id needs --template (e.g. --template base44-starter)" }));
80987
+ return 2;
80988
+ }
80989
+ if (flags.remote !== true && templateTakesBase44AppId(template) === false) {
80990
+ io.error(JSON.stringify({ error: `template "${template}" has no Base44 app id to fill`, hint: "use --template base44-starter" }));
80991
+ return 2;
80992
+ }
80993
+ }
80994
+ const base44Applied = (dir) => {
80995
+ if (base44AppId === void 0) return {};
80996
+ if (!applyBase44AppId(dir, base44AppId)) throw new Error(`--base44-app-id: ${dir}/config.ts lost its Base44 app id placeholder`);
80997
+ return { base44: { appId: base44AppId } };
80998
+ };
80958
80999
  const withAgentFiles = async (dir) => {
80959
81000
  const seeded = await seedAgentFilesFor(dir, flags);
80960
81001
  if (!seeded.agent || "error" in seeded.agent) return seeded;
@@ -80999,8 +81040,12 @@ ${doNow.map((l) => ` ${l}`).join("\n")}`);
80999
81040
  if (template && flags.remote === true) {
81000
81041
  let fetched;
81001
81042
  try {
81002
- fetched = await initFromRemoteTemplate(name, parentDir, template, client2());
81043
+ fetched = await initFromRemoteTemplate(name, parentDir, template, client2(), { requireBase44Slot: base44AppId !== void 0 });
81003
81044
  } catch (e) {
81045
+ if (e instanceof NoBase44SlotError) {
81046
+ io.error(JSON.stringify({ error: e.message, hint: "use --template base44-starter" }));
81047
+ return 2;
81048
+ }
81004
81049
  if (!availableTemplates().includes(template)) {
81005
81050
  io.error(JSON.stringify({ error: `remote template "${template}" unavailable: ${e.message}`, hint: "check the template name with `himi template list --remote`, or drop --remote to use a bundled template" }));
81006
81051
  return 1;
@@ -81008,17 +81053,23 @@ ${doNow.map((l) => ` ${l}`).join("\n")}`);
81008
81053
  io.error(JSON.stringify({ warning: `remote template fetch failed (${e.message}); falling back to the bundled "${template}"` }));
81009
81054
  }
81010
81055
  if (fetched) {
81056
+ const b442 = base44Applied(fetched.dir);
81011
81057
  const agent2 = await startSession(fetched.dir, await withAgentFiles(fetched.dir), "remote");
81012
81058
  const pushSetup2 = await offerPushSetup2({ contentDir: fetched.dir, flags, io, deps: { validate: createTrimValidator(deps) } });
81013
- print({ ok: true, doNow: agent2.doNow, app: name, dir: fetched.dir, template, source: "remote", files: fetched.files, ...keptPlan(fetched.kept), ...agent2, ...await adoptAtInit(fetched.dir), pushSetup: pushSetup2, doNowAgain: agent2.doNow });
81059
+ print({ ok: true, doNow: agent2.doNow, app: name, dir: fetched.dir, template, source: "remote", files: fetched.files, ...b442, ...keptPlan(fetched.kept), ...agent2, ...await adoptAtInit(fetched.dir), pushSetup: pushSetup2, doNowAgain: agent2.doNow });
81014
81060
  sayDoNow(agent2.doNow, true);
81015
81061
  return 0;
81016
81062
  }
81017
81063
  }
81064
+ if (base44AppId !== void 0 && template && templateTakesBase44AppId(template) === false) {
81065
+ io.error(JSON.stringify({ error: `template "${template}" has no Base44 app id to fill`, hint: "use --template base44-starter" }));
81066
+ return 2;
81067
+ }
81018
81068
  const res = scaffoldContentPackage(name, parentDir, template);
81069
+ const b44 = base44Applied(res.dir);
81019
81070
  const agent = await startSession(res.dir, await withAgentFiles(res.dir), "bundled");
81020
81071
  const pushSetup = await offerPushSetup2({ contentDir: res.dir, flags, io, deps: { validate: createTrimValidator(deps) } });
81021
- print({ ok: true, doNow: agent.doNow, app: name, dir: res.dir, ...template ? { template, source: "embedded" } : {}, files: res.files, ...keptPlan(res.kept), ...agent, ...template ? await adoptAtInit(res.dir) : {}, pushSetup, doNowAgain: agent.doNow });
81072
+ print({ ok: true, doNow: agent.doNow, app: name, dir: res.dir, ...template ? { template, source: "embedded" } : {}, files: res.files, ...b44, ...keptPlan(res.kept), ...agent, ...template ? await adoptAtInit(res.dir) : {}, pushSetup, doNowAgain: agent.doNow });
81022
81073
  sayDoNow(agent.doNow, true);
81023
81074
  return 0;
81024
81075
  }
@@ -0,0 +1,241 @@
1
+ # From a Base44 app → a native Himi app
2
+
3
+ This page takes an agent from a [Base44](https://base44.com) app to a running Himi app that
4
+ uses that app's backend, with the himi CLI alone:
5
+
6
+ ```
7
+ himi base44 analyze → himi init --template base44-starter → one list + detail per entity → himi test → himi run / Himi Player
8
+ ```
9
+
10
+ The Base44 app keeps its data, its users and its backend functions. The Himi app is a native UI
11
+ over them: every screen is a Tier-5 worker that calls the app through the `base44` capability
12
+ (`@wix/himalaya/base44` off-repo, `@himalaya/base44` in this repo). Nothing is copied out of
13
+ Base44 and nothing is written back to it except through the same API calls the web app makes.
14
+
15
+ > Read [`base44-app-context`](base44-app-context.md) (`himi docs base44-app-context`) for what the
16
+ > book contains and what it never collects. This page is what to *do* with the book.
17
+
18
+ ## 0. What you need
19
+
20
+ - The Base44 **app id**: the id in the app's editor URL, `https://app.base44.com/apps/<id>`. It
21
+ is a public id, not a secret.
22
+ - For the full book (entities, functions, auth), a Base44 session as the app's owner or an
23
+ editor: `himi base44 login` (or `BASE44_ACCESS_TOKEN`). Without one, `--url` reads only what
24
+ any visitor sees, which has no entity schemas.
25
+
26
+ ## 1. Read the app into a book
27
+
28
+ ```bash
29
+ himi base44 analyze --app-id <id> # owner pass → .himi/base44-book/
30
+ ```
31
+
32
+ Read `.himi/base44-book/BOOK.md` first, then `book.json` for detail. Check `gaps[]` before you
33
+ plan: a missing `entities` chapter means you have no schemas to build from, so sign in and run it
34
+ again instead of guessing fields.
35
+
36
+ The parts of the book this procedure uses:
37
+
38
+ | Book field | Drives |
39
+ |---|---|
40
+ | `identity.name`, `identity.colors`, `look` | the app name, `tokens.json` accent and surfaces |
41
+ | `entities[]` (`name`, `fields[]`, `required`, `rls`, `sampleRows`) | one list + detail (+ form) per entity |
42
+ | `routes[]` (`path`, `component`, `entities`) | which entities the web app treats as primary, so which get a tab |
43
+ | `auth.visibility`, `auth.providers` | whether the app needs the Account tab, and which sign-in it offers |
44
+ | `functions[]` (`name`, `kind`, `invoke`) | actions on the screens that need them |
45
+ | `agents[]`, `connectors[]`, `secrets.names` | nothing on the device. See [What the SDK cannot do](#what-the-sdk-cannot-do) |
46
+
47
+ ## 2. Scaffold from the starter
48
+
49
+ ```bash
50
+ himi init my-app --template base44-starter --base44-app-id <id>
51
+ cd my-app
52
+ ```
53
+
54
+ `base44-starter` is a complete, working app over any Base44 app with a `Task` entity. To try it
55
+ before pointing it at yours, pass the public sample app (`--base44-app-id 69725232345f461f64ad1623`),
56
+ whose `Task` entity anyone may read and write:
57
+
58
+ | Tab | Screen | Worker | Pattern it shows |
59
+ |---|---|---|---|
60
+ | Tasks | `b44_tasks` | `tasks` | list, create, bulk-create, update, delete; loading / empty / error / ready; pull to refresh |
61
+ | (pushed) | `b44_task` | `task` | read one row by its id (a nav param), edit, delete then go back |
62
+ | Functions | `b44_functions` | `functions` | `functions.invoke(name, payload)` with the reply and the error painted |
63
+ | Account | `b44_account` | `account` | Base44 email + password and emailed-code sign-in, register, reset, profile, sign out |
64
+
65
+ `config.ts` declares `capabilities: ["base44"]` and `base44: { appId }`. `--base44-app-id` writes
66
+ the id there, and that line is the only place it is read from. Without the flag the id is the
67
+ placeholder `ReplaceWithYourBase44AppId`, which talks to no app, so every screen shows its error
68
+ state until you edit it in. A malformed id (anything but 8–64 letters and digits) is refused before
69
+ anything is written.
70
+
71
+ Run it once before you change anything, so you know the starter works against your app:
72
+
73
+ ```bash
74
+ himi run --content . # web preview — the Tasks tab lists your Task rows
75
+ ```
76
+
77
+ If your app has no `Task` entity, the Tasks tab shows its error state with Base44's own message.
78
+ That is expected: you are about to replace it.
79
+
80
+ ## 3. Plan the screens from the book
81
+
82
+ Write `.himi/screen-plan.md` before editing code. For each entity in `book.entities`:
83
+
84
+ 1. **Decide whether it gets a tab.** An entity that a top-level route reads (`routes[].entities`)
85
+ and that a user browses gets a tab. A child entity (one whose rows only make sense under a
86
+ parent, such as `OrderItem` under `Order`) gets a section on the parent's detail screen
87
+ instead. Keep to 3–5 tabs. Account always takes one when the app is not public (step 5).
88
+ 2. **Pick the list row.** The title is the first `required` string field, preferring one named
89
+ `title`, `name` or `label`. The subtitle is one or two short fields (a status `enum`, a date, a
90
+ number). Never put a long text field in a row.
91
+ 3. **Pick the detail and form fields** from each field's JSON Schema type:
92
+
93
+ | Field (`type` / `format` / `enum`) | Detail shows | Form control |
94
+ |---|---|---|
95
+ | `string` | `Text` | `TextField` |
96
+ | `string` + long text (`description`, `notes`, `body`, …) | `Text` with `maxLines` | `TextField` with `multilineMaxLines` |
97
+ | `string` + `enum` | `Badge` / `Text` | `SegmentedControl` (≤ 4 values) or `Picker` |
98
+ | `string` + `format: date` / `date-time` | formatted `Text` | `DatePicker` |
99
+ | `string` + `format: email` / `uri` | `Text`, tap opens mail / URL | `TextField` with `keyboard: "email"` / `"url"` |
100
+ | `string` holding an image URL (`image`, `photo`, `avatar`, `*_url`) | `Image` | read-only, see [file upload](#what-the-sdk-cannot-do) |
101
+ | `number` / `integer` | `Text` (with unit) | `Stepper` (small ranges) or numeric `TextField` |
102
+ | `boolean` | `Badge` or `Text` | `Toggle` |
103
+ | `array` of strings | `FilterChips` / `ChipsInput` (read-only) | `ChipsInput` |
104
+ | `object` / nested `array` of objects | a sub-section of rows | none at first. Edit it on the web app |
105
+ | built-ins `id`, `created_date`, `updated_date`, `created_by` | "Created …" metadata row | never editable |
106
+
107
+ 4. **Respect the entity's rules.** `rls` (and field-level `rls`) decide what the signed-in user
108
+ may do. Offer only the writes `BOOK.md` lists for the entity. An operation whose rule is
109
+ `false` gets no button. A rule that is a condition (`{ "created_by": "{{user.email}}" }`)
110
+ means the write only works for a signed-in user, on their own rows, so it belongs behind
111
+ sign-in (step 5). A denied call still fails with Base44's message, and every screen must paint
112
+ that error rather than swallow it.
113
+ 5. **Use the sample rows** (`sampleRows`, already redacted) to size rows and choose the subtitle,
114
+ never as data the app ships.
115
+
116
+ Compose each screen with [`designing-himi-screens`](designing-himi-screens.md)
117
+ (`himi docs designing-himi-screens`): hierarchy, spacing, and the four states.
118
+
119
+ ## 4. Build one list + detail per entity
120
+
121
+ Copy the Task pair and rename it. For an entity `Project`:
122
+
123
+ - `tier5-src/projects/index.ts` from `tier5-src/tasks/index.ts`. Set `ENTITY = "Project"`, the
124
+ service name (`b44Projects`), and `toRow()` for the fields you chose. Keep the structure:
125
+ module-eval `load()`, the `latestList` sequence that drops stale replies, the `pendingWrites`
126
+ guard, and `derive()` producing the `hide*` flags.
127
+ - `tier5-src/project/index.ts` from `tier5-src/task/index.ts`. `inputId()` reads the row id from
128
+ the nav param, `get(id)` loads it, and `remove` returns `navigate(…, { presentation: "back" })`
129
+ from the action the host dispatched.
130
+ - In `dev/index.ts`, a `projects()` and a `project()` builder (copy `tasks()` and `task()`), each
131
+ bound with `bundle: "<worker dir>"` and `declaredCapabilities: ["base44"]`. Register both in
132
+ `screens.exact` and the tab root in `seed.screens`.
133
+ - In `config.ts`, a `linkableScreens` entry for each (the detail with `paramKeys: ["id"]` and its
134
+ list as `parentScreenId`) and a `tabBar` item for the list.
135
+
136
+ The calls a worker makes, all on `entities("<Name>")`:
137
+
138
+ | Need | Call |
139
+ |---|---|
140
+ | newest first, one page | `.list({ sort: "-created_date", limit: 20, fields: [...] })` |
141
+ | next page | `.list({ sort, limit, skip })` |
142
+ | filtered list (a tab per status, the signed-in user's rows) | `.filter({ status: "open" }, { sort, limit })` |
143
+ | one row | `.get(id)` |
144
+ | create / update / delete | `.create(data)` / `.update(id, patch)` / `.delete(id)` |
145
+ | several rows at once | `.bulkCreate(rows)` |
146
+
147
+ Rules that keep it correct:
148
+
149
+ - **The detail is an exact screen fed its id as a nav param**, opened with
150
+ `himi.open { screenId: "<detail>", id: "$item.id" }`. It is not a templated route, because
151
+ Base44 row ids exist only at runtime and a release can bake only the ids it knows.
152
+ - **Bindings substitute whole values only.** `"Open $item.title"` renders literally on device.
153
+ Compute the whole string in the worker (`openLabel: \`Open ${title}\``) and bind
154
+ `$item.openLabel`. `himi validate` flags this as an error.
155
+ - **Every write re-lists**, and a failed write keeps the list on screen with the error above it.
156
+ Only a failed *first* list is the error state.
157
+ - Remove `tasks` / `task` once your own entities replace them, and change `homeScreenId`.
158
+
159
+ ## 5. Auth: the Account tab
160
+
161
+ | `book.auth.visibility` | Do |
162
+ |---|---|
163
+ | `public` | Keep Account only if writes need a user (an `rls` condition on `user`). Otherwise remove it. |
164
+ | `private` / `workspace` | Keep Account, and make it the first thing a signed-out user sees: gate each list's write controls (and, for `private`, the lists themselves) on the account state. |
165
+
166
+ Offer only the providers in `book.auth.providers` that the SDK supports: `email-password` (with
167
+ the emailed-code form the starter already has). **Do not offer Google, Microsoft, Facebook, Apple
168
+ or SSO sign-in**: the Base44 capability has no social sign-in yet, so those buttons would be dead.
169
+
170
+ The token never reaches the worker. Native code keeps it (Keychain / encrypted prefs), so the
171
+ session survives a relaunch, and `auth.status()` reports it on the next boot.
172
+
173
+ An app that lets users sign up must let them delete their account in the app (App Store
174
+ 5.1.1(v)). A regular Base44 user cannot delete their own record, so write a backend function that
175
+ does it with the service role. Name it in `config.ts`
176
+ (`base44DeleteAccountFunction = "deleteMyAccount"`) and the Account tab shows the Delete row.
177
+
178
+ ## 6. Functions as actions
179
+
180
+ Each `book.functions[]` entry with `kind: "invoke"` is callable from a worker as
181
+ `functions.invoke("<name>", payload)`. Put it where the user needs it, not on a debug screen:
182
+
183
+ - a function that acts on one row (send an invoice, recalculate a total) → a button on that
184
+ entity's detail screen, passing the row id in the payload, then re-`get` the row;
185
+ - a function that acts on the user (export, invite) → a row on the Account tab;
186
+ - a scheduled or entity-triggered function (`automations`) → nothing on the device. It runs on
187
+ Base44 by itself.
188
+
189
+ A `kind: "http"` function serves its own HTTP requests (`Deno.serve`). Call it with
190
+ `functions.invoke` only if it also handles an invoke payload. Otherwise leave it on the web.
191
+
192
+ Paint the reply and paint the failure: a function that is missing, not deployed or that throws
193
+ must read as an error, never as an empty reply. The Functions tab of the starter is a developer
194
+ tool for trying functions by name. Remove it from the shipped app once each function you need
195
+ has its own button.
196
+
197
+ ## 7. Look
198
+
199
+ Take `identity.colors` and the `look` chapter into `tokens.json`: `accentPrimary` from the
200
+ brand colour (check that it reaches 4.5:1 on `surfacePrimary` and `surfaceTertiary`, since
201
+ `himi validate` warns when it does not), the app name from `identity.name` into
202
+ `himalaya.content.json`, and the icon with `himi icon set` or `himi icon generate`. Keep the web
203
+ app's words: screen titles, tab labels and empty states come from its pages, not invented copy.
204
+
205
+ ## 8. Test, run, ship
206
+
207
+ ```bash
208
+ himi validate --content . --strict # zero errors
209
+ himi test --content . --json # derived crawl: failures and warnings must be 0
210
+ himi run --content . # web preview against the live Base44 app
211
+ ```
212
+
213
+ `himi test` crawls every action against the real workers with hermetic transport, so it proves
214
+ the wiring, not your data. Its gap ledger names what the hermetic crawl could not reach (rows that
215
+ only exist on the live app, for instance). Close a gap with the remedy it names, or accept it.
216
+ Then drive the real flow in `himi run`: list, create, edit, delete a row of each entity, and
217
+ sign in when the app is not public.
218
+
219
+ **Where it runs.** The web preview and the web Himi Player serve `base44` themselves. A native
220
+ shell has to link the Base44 SDK. The native Himi Player does not link it yet, so it lists a Base44
221
+ release as not supported on that device. A branded native binary of the `himi-base44` shell is
222
+ #3674.
223
+
224
+ Publish with `himi push` like any other content package.
225
+
226
+ ## What the SDK cannot do
227
+
228
+ The `base44` capability is `auth.*`, `entities(name).*` and `functions.invoke`. It has no:
229
+
230
+ - **realtime** subscriptions. Re-list on `onAppear` and on pull-to-refresh instead;
231
+ - **`integrations.*`** (LLM, email, image generation, file storage). Call them from a backend
232
+ function and invoke that;
233
+ - **file upload**. Upload from a backend function (pass the file's URL or base64 in the payload),
234
+ or keep the upload on the web app;
235
+ - **agents**, **connectors** or **secrets**. They stay on Base44, and a function that needs one
236
+ uses it server-side;
237
+ - **service-role** access. A shipped client never holds an admin credential, so anything that
238
+ needs one is a backend function.
239
+
240
+ Route each of these through `functions.invoke`. Never put a token, key or secret in a worker or
241
+ in `config.ts`.
package/guides/index.json CHANGED
@@ -220,6 +220,12 @@
220
220
  "category": "CLI & Loop",
221
221
  "file": "base44-app-context.md"
222
222
  },
223
+ {
224
+ "id": "base44-app-to-himi-app",
225
+ "title": "From a Base44 app → a native Himi app",
226
+ "category": "CLI & Loop",
227
+ "file": "base44-app-to-himi-app.md"
228
+ },
223
229
  {
224
230
  "id": "headless-business-to-himi-app",
225
231
  "title": "From Wix Headless / Back Office → a native Himi app",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1181.0",
3
+ "version": "0.1183.0",
4
4
  "type": "module",
5
5
  "description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
6
6
  "keywords": [
@@ -67,5 +67,5 @@
67
67
  "artifactId": "himalaya-cli"
68
68
  }
69
69
  },
70
- "falconPackageHash": "b9567648a0cc9010de85880423e53d9ef97a10fb139c7074a2d1cd78"
70
+ "falconPackageHash": "7ca344d0a8415adec66c315d8b104421a94353b9f7fc240429171467"
71
71
  }