@lotics/cli 0.172.1 → 0.175.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/src/cli.js +93 -22
- package/dist/src/client.d.ts +14 -0
- package/docs/cli_reference.md +1 -0
- package/package.json +1 -1
package/dist/src/cli.js
CHANGED
|
@@ -45696,7 +45696,7 @@ function resultSideEffects(result) {
|
|
|
45696
45696
|
}
|
|
45697
45697
|
|
|
45698
45698
|
// src/version.ts
|
|
45699
|
-
var VERSION = "0.
|
|
45699
|
+
var VERSION = "0.175.0";
|
|
45700
45700
|
|
|
45701
45701
|
// src/timezone.ts
|
|
45702
45702
|
function machineTimezone() {
|
|
@@ -45708,6 +45708,9 @@ function machineTimezone() {
|
|
|
45708
45708
|
}
|
|
45709
45709
|
}
|
|
45710
45710
|
|
|
45711
|
+
// src/cli.ts
|
|
45712
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
45713
|
+
|
|
45711
45714
|
// src/docs_command.ts
|
|
45712
45715
|
import fs5 from "node:fs";
|
|
45713
45716
|
import path5 from "node:path";
|
|
@@ -45919,6 +45922,12 @@ var COMMANDS = [
|
|
|
45919
45922
|
" these rows ship to everyone who copies the starter"
|
|
45920
45923
|
]
|
|
45921
45924
|
},
|
|
45925
|
+
{
|
|
45926
|
+
verbs: ["upgrade"],
|
|
45927
|
+
help: [
|
|
45928
|
+
" lotics upgrade Update this CLI to the latest version"
|
|
45929
|
+
]
|
|
45930
|
+
},
|
|
45922
45931
|
{
|
|
45923
45932
|
verbs: ["docs"],
|
|
45924
45933
|
help: [
|
|
@@ -48393,7 +48402,7 @@ function walk(node, visit) {
|
|
|
48393
48402
|
}
|
|
48394
48403
|
}
|
|
48395
48404
|
|
|
48396
|
-
// src/generate_app_fields.ts
|
|
48405
|
+
// ../shared/src/generate_app_fields.ts
|
|
48397
48406
|
var HEADER = `// Auto-generated by 'lotics app codegen' and 'lotics app pull'.
|
|
48398
48407
|
// DO NOT EDIT \u2014 regenerated from the workspace schema.
|
|
48399
48408
|
//
|
|
@@ -48511,6 +48520,17 @@ export type AppFields = typeof F;
|
|
|
48511
48520
|
export type AppOptions = typeof OPT;
|
|
48512
48521
|
`;
|
|
48513
48522
|
}
|
|
48523
|
+
function codegenTableIds(queries, allowlist) {
|
|
48524
|
+
const ids = new Set(allowlist);
|
|
48525
|
+
for (const declaration of Object.values(queries)) {
|
|
48526
|
+
for (const id of collectQueryTableIds(declaration.ast)) ids.add(id);
|
|
48527
|
+
}
|
|
48528
|
+
return [...ids];
|
|
48529
|
+
}
|
|
48530
|
+
function readCodegenTablesAllowlist(pkg) {
|
|
48531
|
+
const tables = pkg?.lotics?.codegen?.tables;
|
|
48532
|
+
return Array.isArray(tables) ? tables.filter((t) => typeof t === "string") : [];
|
|
48533
|
+
}
|
|
48514
48534
|
|
|
48515
48535
|
// src/app_workflow_check.ts
|
|
48516
48536
|
import fs7 from "node:fs";
|
|
@@ -71970,18 +71990,13 @@ function writeAppDts(projectDir, manifest) {
|
|
|
71970
71990
|
ensureAppVitestSetup(projectDir);
|
|
71971
71991
|
return written.map(([file2]) => file2);
|
|
71972
71992
|
}
|
|
71973
|
-
function readCodegenTablesAllowlist(projectDir) {
|
|
71974
|
-
const pkg = JSON.parse(fs8.readFileSync(path9.join(projectDir, "package.json"), "utf-8"));
|
|
71975
|
-
const tables = pkg.lotics?.codegen?.tables;
|
|
71976
|
-
return Array.isArray(tables) ? tables.filter((t) => typeof t === "string") : [];
|
|
71977
|
-
}
|
|
71978
71993
|
function resolveCodegenTableIds(projectDir, queries) {
|
|
71979
|
-
|
|
71980
|
-
|
|
71981
|
-
|
|
71994
|
+
let manifest = null;
|
|
71995
|
+
try {
|
|
71996
|
+
manifest = JSON.parse(fs8.readFileSync(path9.join(projectDir, "package.json"), "utf-8"));
|
|
71997
|
+
} catch {
|
|
71982
71998
|
}
|
|
71983
|
-
|
|
71984
|
-
return [...ids];
|
|
71999
|
+
return codegenTableIds(queries, readCodegenTablesAllowlist(manifest));
|
|
71985
72000
|
}
|
|
71986
72001
|
function writeAppFields(projectDir, tables) {
|
|
71987
72002
|
const dotLotics = path9.join(projectDir, ".lotics");
|
|
@@ -74023,15 +74038,19 @@ function reportKnowledgeWarnings(warnings) {
|
|
|
74023
74038
|
answering from nowhere. Create them with those names, or edit the agent.`
|
|
74024
74039
|
);
|
|
74025
74040
|
}
|
|
74041
|
+
function instantiateBody(args) {
|
|
74042
|
+
return {
|
|
74043
|
+
...args.noSampleData === true ? { no_sample_data: true } : {},
|
|
74044
|
+
...args.adopt === true ? { adopt: true } : {},
|
|
74045
|
+
build_on_server: true
|
|
74046
|
+
};
|
|
74047
|
+
}
|
|
74026
74048
|
async function starterInit(client, args) {
|
|
74027
74049
|
const starter = await client.getPackage(args.starter_id);
|
|
74028
74050
|
note(
|
|
74029
74051
|
`Copying ${starter.name}${starter.is_official ? " (official)" : ""} into this workspace\u2026`
|
|
74030
74052
|
);
|
|
74031
|
-
const result = await client.instantiateStarter(args.starter_id,
|
|
74032
|
-
...args.noSampleData === true ? { no_sample_data: true } : {},
|
|
74033
|
-
...args.adopt === true ? { adopt: true } : {}
|
|
74034
|
-
});
|
|
74053
|
+
const result = await client.instantiateStarter(args.starter_id, instantiateBody(args));
|
|
74035
74054
|
const tables = Object.keys(result.binding.entities ?? {}).sort();
|
|
74036
74055
|
const knowledgeDocs = Object.keys(result.binding.knowledge ?? {}).sort();
|
|
74037
74056
|
const templates = Object.keys(result.binding.templates ?? {}).sort();
|
|
@@ -74094,19 +74113,42 @@ Done \u2014 these are yours now, with no link back to the starter.`);
|
|
|
74094
74113
|
stamp: { id: null, number: null },
|
|
74095
74114
|
kept
|
|
74096
74115
|
});
|
|
74116
|
+
const serverDeployed = result.deployed ?? null;
|
|
74097
74117
|
if (starter.owned_by_caller !== true) {
|
|
74098
74118
|
warn(
|
|
74099
|
-
`
|
|
74119
|
+
serverDeployed !== null ? `
|
|
74120
|
+
Built ${starter.name} on Lotics \u2014 its package scripts and vite config are the
|
|
74121
|
+
publisher's code, run in an isolated container rather than on your machine.${starter.is_official ? " Lotics reviewed this starter." : ""}` : `
|
|
74100
74122
|
Building ${starter.name} \u2014 this runs its build on your machine (its package scripts
|
|
74101
74123
|
and vite config are the publisher's code, executed as you).${starter.is_official ? " Lotics reviewed this starter." : ""}`
|
|
74102
74124
|
);
|
|
74103
|
-
} else {
|
|
74125
|
+
} else if (serverDeployed === null) {
|
|
74104
74126
|
note(`Building and deploying\u2026`);
|
|
74105
74127
|
}
|
|
74106
|
-
|
|
74107
|
-
|
|
74108
|
-
|
|
74109
|
-
|
|
74128
|
+
const resumeDir = path10.relative(process.cwd(), targetPath) || ".";
|
|
74129
|
+
try {
|
|
74130
|
+
if (serverDeployed !== null) {
|
|
74131
|
+
note(`Deployed v${serverDeployed.version_number} \u2014 built on Lotics, no Node needed here.`);
|
|
74132
|
+
} else {
|
|
74133
|
+
await appDeploy(client, {
|
|
74134
|
+
projectDir: targetPath,
|
|
74135
|
+
message: `Copied from starter ${starter.name}`
|
|
74136
|
+
});
|
|
74137
|
+
}
|
|
74138
|
+
} catch (error52) {
|
|
74139
|
+
const reason = error52 instanceof Error ? error52.message : String(error52);
|
|
74140
|
+
throw new Error(
|
|
74141
|
+
`${reason}
|
|
74142
|
+
|
|
74143
|
+
The copy itself is DONE and nothing needs repeating: the tables, the sample
|
|
74144
|
+
records and the project all landed. Only the deploy is left.
|
|
74145
|
+
|
|
74146
|
+
Finish it: cd ${resumeDir} && lotics app deploy -m "Copied from starter ${starter.name}"
|
|
74147
|
+
|
|
74148
|
+
Do NOT re-run the copy \u2014 it would adopt the tables this one just created and
|
|
74149
|
+
insert the sample records again.`
|
|
74150
|
+
);
|
|
74151
|
+
}
|
|
74110
74152
|
let signInUrl = null;
|
|
74111
74153
|
try {
|
|
74112
74154
|
const link = await client.login({
|
|
@@ -103302,6 +103344,31 @@ function alreadySetUp(email3) {
|
|
|
103302
103344
|
if (config2?.email !== email3) return false;
|
|
103303
103345
|
return Object.keys(config2.profiles ?? {}).length > 0;
|
|
103304
103346
|
}
|
|
103347
|
+
async function runUpgrade() {
|
|
103348
|
+
const cmd = updateCommand();
|
|
103349
|
+
console.error(`Upgrading ${VERSION} \u2192 latest
|
|
103350
|
+
${cmd}
|
|
103351
|
+
`);
|
|
103352
|
+
const proc = spawn4(cmd, {
|
|
103353
|
+
// The command is one of three literals this binary chooses between, never
|
|
103354
|
+
// anything a caller supplied — and each is a pipeline that needs a shell.
|
|
103355
|
+
shell: true,
|
|
103356
|
+
stdio: "inherit",
|
|
103357
|
+
env: process.env
|
|
103358
|
+
});
|
|
103359
|
+
const code = await new Promise((resolve2, reject2) => {
|
|
103360
|
+
proc.on("error", reject2);
|
|
103361
|
+
proc.on("exit", (exitCode) => resolve2(exitCode ?? 1));
|
|
103362
|
+
});
|
|
103363
|
+
if (code !== 0) {
|
|
103364
|
+
console.error(
|
|
103365
|
+
`
|
|
103366
|
+
Upgrade failed (exit ${code}). Run it yourself to see why:
|
|
103367
|
+
${cmd}`
|
|
103368
|
+
);
|
|
103369
|
+
process.exit(1);
|
|
103370
|
+
}
|
|
103371
|
+
}
|
|
103305
103372
|
async function handleSignup(positionalEmail, flags) {
|
|
103306
103373
|
const email3 = positionalEmail ?? (process.stdin.isTTY ? await prompt("Email: ") : "");
|
|
103307
103374
|
if (!email3) {
|
|
@@ -103650,6 +103717,10 @@ async function main() {
|
|
|
103650
103717
|
docsCommand({ area: subcommand });
|
|
103651
103718
|
return;
|
|
103652
103719
|
}
|
|
103720
|
+
if (command === "upgrade") {
|
|
103721
|
+
await runUpgrade();
|
|
103722
|
+
return;
|
|
103723
|
+
}
|
|
103653
103724
|
if (command === "report") {
|
|
103654
103725
|
let input = subcommand ?? "";
|
|
103655
103726
|
if (input.startsWith("@")) {
|
package/dist/src/client.d.ts
CHANGED
|
@@ -403,11 +403,25 @@ export declare class LoticsClient {
|
|
|
403
403
|
version?: number;
|
|
404
404
|
no_sample_data?: boolean;
|
|
405
405
|
adopt?: boolean;
|
|
406
|
+
/** Ask the server to build and deploy the copy, so this machine needs no Node. */
|
|
407
|
+
build_on_server?: boolean;
|
|
406
408
|
}): Promise<{
|
|
407
409
|
app_id: string | null;
|
|
408
410
|
starter_id: string;
|
|
409
411
|
version: number;
|
|
410
412
|
bundle_url: string | null;
|
|
413
|
+
/**
|
|
414
|
+
* The version the SERVER deployed, when it did.
|
|
415
|
+
*
|
|
416
|
+
* Optional in this type on purpose: a server that predates the field omits
|
|
417
|
+
* it entirely, and it is absent rather than null. Callers must treat "not
|
|
418
|
+
* there" and "null" alike and build locally — assuming the request was
|
|
419
|
+
* honoured would report success over an app nobody built.
|
|
420
|
+
*/
|
|
421
|
+
deployed?: {
|
|
422
|
+
version_id: string;
|
|
423
|
+
version_number: number;
|
|
424
|
+
} | null;
|
|
411
425
|
binding: Record<string, Record<string, string>>;
|
|
412
426
|
sample_record_ids: Record<string, string[]>;
|
|
413
427
|
knowledge_warnings: {
|
package/docs/cli_reference.md
CHANGED
|
@@ -46,6 +46,7 @@ Per-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. S
|
|
|
46
46
|
| `lotics starter show <starter_id>` | One starter's name, description, current version and trust standing (`official` — reviewed by Lotics; `your organization's own`). Read it before copying a starter you did not publish. |
|
|
47
47
|
| `lotics starter init <starter_id> [path]` | **Copy a starter into this workspace.** Server-side it scaffolds the tables and fields, creates the document templates and knowledge docs, inserts the sample records, creates a BESPOKE app and materializes its queries, workflows and agents onto it. Then it stops: a starter ships **source only**, with no prebuilt bundle, so the app source is downloaded here, hydrated against the live app (the same steps `app pull` runs), built, and deployed — which is why this needs node and a few minutes, and why the app is not servable until the deploy lands. **What you get is yours outright**: an ordinary app plus ordinary tables, with no link back to the starter, nothing pinned, and nothing to upgrade. Edit any of it. **It builds the publisher's code on your machine** — the copy has to build where your workspace's field ids are, so `npm run build` runs their build script and vite loads their `vite.config.ts` in Node, as you. Dependencies install with `npm ci --ignore-scripts` — the lockfile's exact tree (so a starter published months ago resolves the same packages today), and no lifecycle scripts (the path that fires before you run anything). Neither changes the build itself. That is why provenance is the gate: **copyable only if the starter is Lotics-reviewed or your own organization published it** — copying runs the author's code (its bundle, its workflows, its agents) under YOUR authority, so provenance is the gate, enforced server-side. **Refuses a workspace that already has tables** unless `--adopt`: scaffold matches an entity by DISPLAY NAME, so a starter declaring `Contacts` would bind to yours. `--json` prints one object on stdout instead of progress (the shape is under `lotics start`) and captures npm/vite output unless the build fails. `--no-sample-data` skips the sample records; with them, the created record ids are reported — they are ordinary records, delete them whenever. Resolves and ANNOUNCES its workspace first (`lotics → <org> / <workspace>` on stderr). Admin-only. Authoring the registry (`starter publish/release/unpublish`) stays operator-only. |
|
|
48
48
|
| `lotics starter fixtures capture [--entity <alias> ...] [--limit <n>]` | **(authoring)** Write this app's live records into the project as `fixtures/<entity-alias>.json` — the sample data a starter carries, so a copy lands with something in it. Run from an app project; the app id comes from its manifest. The alias-keyed shape is produced server-side, because the aliases are minted when the starter is extracted and exist nowhere a project can read them. `--entity` is repeatable and comma-separated; omitted, every table the app declares is captured. **Capture a linked set in ONE call** — a link between two rows only resolves within a single capture, so taking companies and contacts separately drops the edge between them (it says so when it happens). `--limit` bounds rows per table (default 10, max 200). **READ WHAT IT WROTE before committing**: these rows are created verbatim in every workspace that copies the starter, so a real customer name, price or address captured here is published. Files, formulas, rollups, lookups and autonumbers are never captured — the platform writes those. Admin-only; writes nothing to the workspace. |
|
|
49
|
+
| `lotics upgrade` | Update this CLI in place. Runs the same installer a person would, chosen by how THIS copy arrived: an npm install upgrades through npm, a script install re-runs the script — the runtime knows which (the executable is compiled, the npm bin runs under node), so nobody has to. It downloads nothing itself; resolving a version, verifying the checksum and replacing a running executable already exist in the installers, and a second copy of that inside the binary would be a second thing to get right. Replacing the binary while it runs is safe — a rename leaves the running image mapped on unix, and on Windows the installer moves the old aside precisely because the file is in use. Already current is a no-op that says so. Needs no auth. |
|
|
49
50
|
| `lotics docs` \| `lotics docs <area>` | The index of the reference docs, **resolved out of the packages installed beside this project** — never carried by this CLI. **Both levels are discovered by looking**: every `@lotics/*` package carrying an `AGENTS.md` or a `docs/` in any `node_modules/@lotics` from the current directory UPWARD (nearest wins, so a hoisted root copy never shadows the one a project's own imports resolve to), and within each, every area it actually ships. Titles come from each file's own `# heading` and the version from the installed `package.json`, so a doc OR a whole package added upstream appears with no change to this CLI, and a skewed install is visible rather than reassuring. A package's index is named after the package (`lotics docs ui`), never `index`. `@lotics/app-sdk`, `@lotics/ui` and `@lotics/cli` sort first as a reading ORDER, not a filter. Both the index and `<area>` print to **stdout** — the index is the payload of a bare `lotics docs`, so `lotics docs | grep -i excel` works — with only the provenance line on stderr, so `lotics docs ai > ai.md` is the doc alone; a name two packages share is refused with both qualified forms (`lotics docs ui/templates`) rather than resolved silently. Needs no auth. Outside a project only `@lotics/cli`'s own resolve, and it says so. |
|
|
50
51
|
| `lotics report '<json>'` \| `lotics report @report.json` | File a report with the Lotics team about what got in your way. **Covers the classes telemetry structurally cannot see**: a capability that does not exist (no command ran, so nothing was recorded), a command that exited 0 having done the wrong thing, an error whose message did not name the remedy, and anything that made authoring slower than it should be. **A frame, not a paragraph** — `{goal, actual, expected?, tried?, wanted?}`, `goal` and `actual` required, unknown keys dropped rather than refused. **No severity or category.** Ingest is inline JSON, `@file`, or `-` for stdin. A bare sentence is refused with the frame printed beside it, so the fix is one step; a bare invocation prints the frame BEFORE asking for a credential, since someone whose key will not resolve is exactly who has something to report. **Not spooled**: unlike telemetry it posts inline, prints whether it landed, and exits non-zero if it did not, echoing the report back so a failed send never loses it. Runs regardless of `LOTICS_TELEMETRY` — invoking it IS the consent that passive collection needs an opt-in for — but with telemetry off there are no recorded commands to attach, and it says so rather than implying context it does not have. Requires auth. Never paste records, file contents, or credentials. |
|
|
51
52
|
| `lotics app check` | Every pre-flight `deploy` runs, WITHOUT building or shipping. **First, whether this project is even based on the served version** — the one thing a deploy REFUSES outright rather than pushing (the server 409s a stale `prev_version_id`), and the one finding that invalidates every other: a stale tree and the live app are two different apps, so comparing them reports nothing trustworthy. Stale exits 1 naming both versions and stops before the rest; a project with no stamp at all — or an app with no version yet — is a first deploy, not a conflict. `deploy` runs the SAME assertion off the app row it already fetched, so a stale tree fails before it pushes a binding or builds, instead of after the upload arrives and the server 409s. Then: the manifest's agent schemas against the live app row, every binding a deploy would push, aliases the source calls that nothing bound (queries, workflows AND agents), bindings the app serves that the source names nowhere, capability-gated SDK calls the manifest doesn't declare, a missing icon/theme, a missing app `description` (it heads the capability catalog the chat agent reads every turn, and its absence has no other symptom), a `vite.config.ts` that never defines `global`/`__DEV__`, a `window.open` in the app's own source, and an INSTALLED `@lotics/app-sdk` below the version that understands the host's realtime push — read from `node_modules`, not the dependency range, because a caret is minor-locked below 1.0 so `^0.79.x` can never resolve `0.80` and `npm update` does nothing (all three fail ONLY in the deployed app — dev bundles with esbuild and production with rollup, so typecheck, lint, build and `app dev` are all green while react-native-web reads `global.cancelAnimationFrame` as a free variable and the sandboxed iframe drops a popup silently), an agent holding `run_app_query`/`run_app_workflow` with an EMPTY `query_aliases`/`workflow_aliases` (the tool is the capability, the alias list is the reach — empty means every call it makes is refused while the run still COMPLETES, so it surfaces as a model ignoring its prompt; read off the live row, never the manifest, which mirrors those fields but is pushed by no verb), and a notice for any alias the source computes at runtime (invisible to every check here and to `--prune`'s unbind guard). **And whether the kit this app builds against has fallen behind what is published** — `@lotics/ui` and `@lotics/app-sdk`, read from `node_modules` for the same reason as the floor check above: a range keeps accepting, so an app pinned `^44.x` reads healthy for a year, and even an in-range one sits on the lockfile's older patch until `npm update` (never `npm install`, which honours the lock). A MAJOR behind is loud and names the packages actually behind — plus `@lotics/ui`'s `MIGRATION.md`, when ui is one of them, since it is the only half that keeps one; anything smaller is one quiet line, because a warning that fires on every deploy is one the reader stops seeing. The registry lookup is bounded and every failure — offline, slow, private — is silence: a version check must never become a new way for a deploy to fail. Adds no rule of its own — each finding is the same helper `deploy` calls, so a green check means a deploy will not complain. **Exits 1 on what a `deploy` would REFUSE or PUSH** — an agent schema that disagrees with the live app, and any binding the project has ahead of the app (an edited workflow body or declaration, edited agent prose, a changed query). Both are things a deploy would act on, so CI gating on a green check means a deploy has nothing left to do; genuine advisories (capabilities, branding, a runtime-computed alias, orphaned bindings) stay advisory and never fail it. |
|