@lotics/cli 0.260.1 → 0.261.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/probe_page.js +1 -1
- package/dist/src/cli.js +77 -59
- package/dist/src/client.d.ts +6 -0
- package/dist/src/client.js +8 -0
- package/docs/cli_reference.md +1 -1
- package/package.json +1 -1
package/dist/probe_page.js
CHANGED
|
@@ -1835,7 +1835,7 @@ var __loticsProbe = (() => {
|
|
|
1835
1835
|
partial_count: {
|
|
1836
1836
|
law: "reviewing.md",
|
|
1837
1837
|
section: "8o",
|
|
1838
|
-
fix: "State the server's count of the set, never the rows a page loaded: count the read (`
|
|
1838
|
+
fix: "State the server's count of the set, never the rows a page loaded: count the read (`useQuery`'s `total`) and step or badge against that total."
|
|
1839
1839
|
},
|
|
1840
1840
|
record_primary: {
|
|
1841
1841
|
law: "hierarchy.md",
|
package/dist/src/cli.js
CHANGED
|
@@ -52,7 +52,7 @@ var __toESM = (mod2, isNodeMode, target) => (target = mod2 != null ? __create(__
|
|
|
52
52
|
var define_LOTICS_KIT_VERSIONS_default;
|
|
53
53
|
var init_define_LOTICS_KIT_VERSIONS = __esm({
|
|
54
54
|
"<define:__LOTICS_KIT_VERSIONS__>"() {
|
|
55
|
-
define_LOTICS_KIT_VERSIONS_default = { runtime: "0.
|
|
55
|
+
define_LOTICS_KIT_VERSIONS_default = { runtime: "0.43.0" };
|
|
56
56
|
}
|
|
57
57
|
});
|
|
58
58
|
|
|
@@ -30137,6 +30137,14 @@ var LoticsClient = class {
|
|
|
30137
30137
|
async appFieldOptions(app_id, alias2) {
|
|
30138
30138
|
return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/field-options`, { alias: alias2 });
|
|
30139
30139
|
}
|
|
30140
|
+
/**
|
|
30141
|
+
* What the app's writes will change — each bound workflow's record-write subset, each query's column
|
|
30142
|
+
* sources, the fields they name. Mirrors GET /v1/apps/{app_id}/write_model; the dev server hands it to
|
|
30143
|
+
* the app's SDK whole, so its parts stay opaque here.
|
|
30144
|
+
*/
|
|
30145
|
+
async appWriteModel(app_id) {
|
|
30146
|
+
return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/write_model`);
|
|
30147
|
+
}
|
|
30140
30148
|
/**
|
|
30141
30149
|
* Execute a workflow by alias declared in package.json#lotics.workflows.
|
|
30142
30150
|
* Mirrors POST /v1/apps/{app_id}/workflows/{alias}/execute.
|
|
@@ -64296,7 +64304,7 @@ function resultSideEffects(result) {
|
|
|
64296
64304
|
|
|
64297
64305
|
// src/version.ts
|
|
64298
64306
|
init_define_LOTICS_KIT_VERSIONS();
|
|
64299
|
-
var VERSION = "0.
|
|
64307
|
+
var VERSION = "0.261.0";
|
|
64300
64308
|
|
|
64301
64309
|
// src/timezone.ts
|
|
64302
64310
|
init_define_LOTICS_KIT_VERSIONS();
|
|
@@ -69642,6 +69650,47 @@ function matchesTimeOfDay(recordValue, startTime, endTime) {
|
|
|
69642
69650
|
|
|
69643
69651
|
// ../shared/src/app_query_ast.ts
|
|
69644
69652
|
init_define_LOTICS_KIT_VERSIONS();
|
|
69653
|
+
|
|
69654
|
+
// ../shared/src/interpolate.ts
|
|
69655
|
+
init_define_LOTICS_KIT_VERSIONS();
|
|
69656
|
+
function interpolateObject(obj, context) {
|
|
69657
|
+
if (obj === null || obj === void 0) {
|
|
69658
|
+
return obj;
|
|
69659
|
+
}
|
|
69660
|
+
if (typeof obj === "string") {
|
|
69661
|
+
const expressionMatches = obj.match(/\{\{.*?\}\}/g);
|
|
69662
|
+
if (expressionMatches && expressionMatches.length > 0) {
|
|
69663
|
+
if (expressionMatches.length === 1 && obj === expressionMatches[0]) {
|
|
69664
|
+
const expression = obj.slice(2, -2).trim();
|
|
69665
|
+
return evaluateJsExpression(expression, context);
|
|
69666
|
+
}
|
|
69667
|
+
const parts = obj.split(/(\{\{.*?\}\})/g);
|
|
69668
|
+
const evaluatedParts = parts.map((part) => {
|
|
69669
|
+
if (part.startsWith("{{") && part.endsWith("}}")) {
|
|
69670
|
+
const expression = part.slice(2, -2).trim();
|
|
69671
|
+
const result = evaluateJsExpression(expression, context);
|
|
69672
|
+
return result != null ? String(result) : "";
|
|
69673
|
+
}
|
|
69674
|
+
return part;
|
|
69675
|
+
});
|
|
69676
|
+
return evaluatedParts.join("");
|
|
69677
|
+
}
|
|
69678
|
+
return obj;
|
|
69679
|
+
}
|
|
69680
|
+
if (Array.isArray(obj)) {
|
|
69681
|
+
return obj.map((item) => interpolateObject(item, context));
|
|
69682
|
+
}
|
|
69683
|
+
if (typeof obj === "object") {
|
|
69684
|
+
const result = {};
|
|
69685
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
69686
|
+
result[key] = interpolateObject(value, context);
|
|
69687
|
+
}
|
|
69688
|
+
return result;
|
|
69689
|
+
}
|
|
69690
|
+
return obj;
|
|
69691
|
+
}
|
|
69692
|
+
|
|
69693
|
+
// ../shared/src/app_query_ast.ts
|
|
69645
69694
|
function collectQueryTableIds(node) {
|
|
69646
69695
|
const result = /* @__PURE__ */ new Set();
|
|
69647
69696
|
walk(node, (n) => {
|
|
@@ -71452,7 +71501,8 @@ function codeWithoutComments(sourceText) {
|
|
|
71452
71501
|
return out.join("");
|
|
71453
71502
|
}
|
|
71454
71503
|
var ALIAS_CALL_HOOKS = {
|
|
71455
|
-
|
|
71504
|
+
// `usePaginatedQuery` through `useCounts`: gone from the SDK, still called by bundles on runtime 0.42 or earlier.
|
|
71505
|
+
queries: ["useQuery", "useFieldOptions", "useQueries", "queryAll", "usePaginatedQuery", "useInfiniteQuery", "useCount", "useCounts"],
|
|
71456
71506
|
workflows: ["useWorkflow", "useWorkflows", "useRecording"],
|
|
71457
71507
|
agents: ["useAgentRun"]
|
|
71458
71508
|
};
|
|
@@ -76264,7 +76314,7 @@ init_define_LOTICS_KIT_VERSIONS();
|
|
|
76264
76314
|
var TOOL_INPUT_SEMANTICS = {
|
|
76265
76315
|
// ─── Bulk record mutations ─────────────────────────────────────────────
|
|
76266
76316
|
update_records: {
|
|
76267
|
-
table_id: { kind: "table_ref",
|
|
76317
|
+
table_id: { kind: "table_ref", rows: "change" },
|
|
76268
76318
|
record_ids: { kind: "record_refs" },
|
|
76269
76319
|
filters: { kind: "filter_tree", table_from: "table_id" },
|
|
76270
76320
|
set: { kind: "record_data", table_from: "table_id", writes: true },
|
|
@@ -76296,7 +76346,7 @@ var TOOL_INPUT_SEMANTICS = {
|
|
|
76296
76346
|
field_edits: { kind: "dynamic" }
|
|
76297
76347
|
},
|
|
76298
76348
|
create_records: {
|
|
76299
|
-
table_id: { kind: "table_ref",
|
|
76349
|
+
table_id: { kind: "table_ref", rows: "change" },
|
|
76300
76350
|
records: { kind: "record_data_array", table_from: "table_id", writes: true },
|
|
76301
76351
|
field_keys: { kind: "field_key_refs", table_from: "table_id", writes: true },
|
|
76302
76352
|
// Per-record cells — semantic depends on field_keys[i]; tool resolves.
|
|
@@ -76307,7 +76357,7 @@ var TOOL_INPUT_SEMANTICS = {
|
|
|
76307
76357
|
// `rec_*` identifiers) which is all this kind asserts: normalization passes it
|
|
76308
76358
|
// through raw and nothing resolves it against stored rows.
|
|
76309
76359
|
ids: { kind: "record_refs" },
|
|
76310
|
-
source_table_id: { kind: "table_ref",
|
|
76360
|
+
source_table_id: { kind: "table_ref", rows: "none" },
|
|
76311
76361
|
record_ids: { kind: "record_refs" },
|
|
76312
76362
|
filters: { kind: "filter_tree", table_from: "table_id" },
|
|
76313
76363
|
// Cross-table mapping — {source_field_key: target_field_key}. Not
|
|
@@ -76326,28 +76376,28 @@ var TOOL_INPUT_SEMANTICS = {
|
|
|
76326
76376
|
// rows of whatever tables a single-valued link points here from, and those
|
|
76327
76377
|
// are the link graph's answer rather than the source's, so a scan can no
|
|
76328
76378
|
// more resolve them than it can a field key computed at runtime.
|
|
76329
|
-
table_id: { kind: "table_ref",
|
|
76379
|
+
table_id: { kind: "table_ref", rows: "remove" },
|
|
76330
76380
|
record_ids: { kind: "record_refs" },
|
|
76331
76381
|
filters: { kind: "filter_tree", table_from: "table_id" },
|
|
76332
76382
|
cascade: { kind: "scalar" }
|
|
76333
76383
|
},
|
|
76334
76384
|
restore_records: {
|
|
76335
|
-
table_id: { kind: "table_ref",
|
|
76385
|
+
table_id: { kind: "table_ref", rows: "change" },
|
|
76336
76386
|
record_ids: { kind: "record_refs" },
|
|
76337
76387
|
filters: { kind: "filter_tree", table_from: "table_id" }
|
|
76338
76388
|
},
|
|
76339
76389
|
lock_records: {
|
|
76340
|
-
table_id: { kind: "table_ref",
|
|
76390
|
+
table_id: { kind: "table_ref", rows: "change" },
|
|
76341
76391
|
record_ids: { kind: "record_refs" },
|
|
76342
76392
|
filters: { kind: "filter_tree", table_from: "table_id" }
|
|
76343
76393
|
},
|
|
76344
76394
|
unlock_records: {
|
|
76345
|
-
table_id: { kind: "table_ref",
|
|
76395
|
+
table_id: { kind: "table_ref", rows: "change" },
|
|
76346
76396
|
record_ids: { kind: "record_refs" },
|
|
76347
76397
|
filters: { kind: "filter_tree", table_from: "table_id" }
|
|
76348
76398
|
},
|
|
76349
76399
|
request_locked_record_change: {
|
|
76350
|
-
table_id: { kind: "table_ref",
|
|
76400
|
+
table_id: { kind: "table_ref", rows: "none" },
|
|
76351
76401
|
record_id: { kind: "record_ref" },
|
|
76352
76402
|
// Map of field_key → new value; key validation and per-field value
|
|
76353
76403
|
// resolution depend on the table schema, resolved at execute time.
|
|
@@ -76356,7 +76406,7 @@ var TOOL_INPUT_SEMANTICS = {
|
|
|
76356
76406
|
},
|
|
76357
76407
|
// ─── Query surfaces ────────────────────────────────────────────────────
|
|
76358
76408
|
query_records: {
|
|
76359
|
-
table_id: { kind: "table_ref",
|
|
76409
|
+
table_id: { kind: "table_ref", rows: "none" },
|
|
76360
76410
|
view_id: { kind: "scalar" },
|
|
76361
76411
|
field_keys: { kind: "field_key_refs", table_from: "table_id", writes: false },
|
|
76362
76412
|
filters: { kind: "filter_tree", table_from: "table_id" },
|
|
@@ -76397,7 +76447,7 @@ var TOOL_INPUT_SEMANTICS = {
|
|
|
76397
76447
|
limit: { kind: "scalar" }
|
|
76398
76448
|
},
|
|
76399
76449
|
aggregate_records: {
|
|
76400
|
-
table_id: { kind: "table_ref",
|
|
76450
|
+
table_id: { kind: "table_ref", rows: "none" },
|
|
76401
76451
|
filters: { kind: "filter_tree", table_from: "table_id" },
|
|
76402
76452
|
// Nested {field_key, operation} shape — current vocabulary has no
|
|
76403
76453
|
// nested-field-key kind, so the tool resolves at execute time.
|
|
@@ -76408,14 +76458,14 @@ var TOOL_INPUT_SEMANTICS = {
|
|
|
76408
76458
|
get_record: {
|
|
76409
76459
|
// Schema only takes `record_id`; table_id is optional/derived.
|
|
76410
76460
|
record_id: { kind: "record_ref" },
|
|
76411
|
-
table_id: { kind: "table_ref",
|
|
76461
|
+
table_id: { kind: "table_ref", rows: "none" },
|
|
76412
76462
|
// Keys on the record's own table, which `table_id` names when supplied and
|
|
76413
76463
|
// the record resolves otherwise — the same position it holds on a query.
|
|
76414
76464
|
field_keys: { kind: "field_key_refs", table_from: "table_id", writes: false }
|
|
76415
76465
|
},
|
|
76416
76466
|
// ─── View management ──────────────────────────────────────────────────
|
|
76417
76467
|
create_view: {
|
|
76418
|
-
table_id: { kind: "table_ref",
|
|
76468
|
+
table_id: { kind: "table_ref", rows: "none" },
|
|
76419
76469
|
name: { kind: "text" },
|
|
76420
76470
|
frozen_columns: { kind: "scalar" },
|
|
76421
76471
|
// summary keys are field_keys on `table_id`, values are view-summary
|
|
@@ -76452,13 +76502,13 @@ var TOOL_INPUT_SEMANTICS = {
|
|
|
76452
76502
|
},
|
|
76453
76503
|
// ─── Buttons (table-scoped field with action) ─────────────────────────
|
|
76454
76504
|
press_button: {
|
|
76455
|
-
table_id: { kind: "table_ref",
|
|
76505
|
+
table_id: { kind: "table_ref", rows: "unnamed" },
|
|
76456
76506
|
record_id: { kind: "record_ref" },
|
|
76457
76507
|
field_key: { kind: "field_key_ref", table_from: "table_id", writes: false }
|
|
76458
76508
|
},
|
|
76459
76509
|
// ─── Table management ─────────────────────────────────────────────────
|
|
76460
76510
|
update_table: {
|
|
76461
|
-
table_id: { kind: "table_ref",
|
|
76511
|
+
table_id: { kind: "table_ref", rows: "change" },
|
|
76462
76512
|
name: { kind: "text" },
|
|
76463
76513
|
description: { kind: "text" },
|
|
76464
76514
|
// add_fields carries NEW field definitions (not refs to existing ones).
|
|
@@ -76898,7 +76948,7 @@ function bodyWrites(source) {
|
|
|
76898
76948
|
const semantic = semantics[kwarg];
|
|
76899
76949
|
if (semantic === void 0) continue;
|
|
76900
76950
|
if (semantic.kind === "table_ref") {
|
|
76901
|
-
if (semantic.
|
|
76951
|
+
if (semantic.rows === "remove") deletes.push(resolveTableFrom(semantics, step.input, kwarg));
|
|
76902
76952
|
continue;
|
|
76903
76953
|
}
|
|
76904
76954
|
if (!("writes" in semantic) || !semantic.writes) continue;
|
|
@@ -78268,45 +78318,6 @@ async function assertFilterTraversalsResolve(loadTable, filter2, fields, subject
|
|
|
78268
78318
|
}
|
|
78269
78319
|
}
|
|
78270
78320
|
|
|
78271
|
-
// ../shared/src/interpolate.ts
|
|
78272
|
-
init_define_LOTICS_KIT_VERSIONS();
|
|
78273
|
-
function interpolateObject(obj, context) {
|
|
78274
|
-
if (obj === null || obj === void 0) {
|
|
78275
|
-
return obj;
|
|
78276
|
-
}
|
|
78277
|
-
if (typeof obj === "string") {
|
|
78278
|
-
const expressionMatches = obj.match(/\{\{.*?\}\}/g);
|
|
78279
|
-
if (expressionMatches && expressionMatches.length > 0) {
|
|
78280
|
-
if (expressionMatches.length === 1 && obj === expressionMatches[0]) {
|
|
78281
|
-
const expression = obj.slice(2, -2).trim();
|
|
78282
|
-
return evaluateJsExpression(expression, context);
|
|
78283
|
-
}
|
|
78284
|
-
const parts = obj.split(/(\{\{.*?\}\})/g);
|
|
78285
|
-
const evaluatedParts = parts.map((part) => {
|
|
78286
|
-
if (part.startsWith("{{") && part.endsWith("}}")) {
|
|
78287
|
-
const expression = part.slice(2, -2).trim();
|
|
78288
|
-
const result = evaluateJsExpression(expression, context);
|
|
78289
|
-
return result != null ? String(result) : "";
|
|
78290
|
-
}
|
|
78291
|
-
return part;
|
|
78292
|
-
});
|
|
78293
|
-
return evaluatedParts.join("");
|
|
78294
|
-
}
|
|
78295
|
-
return obj;
|
|
78296
|
-
}
|
|
78297
|
-
if (Array.isArray(obj)) {
|
|
78298
|
-
return obj.map((item) => interpolateObject(item, context));
|
|
78299
|
-
}
|
|
78300
|
-
if (typeof obj === "object") {
|
|
78301
|
-
const result = {};
|
|
78302
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
78303
|
-
result[key] = interpolateObject(value, context);
|
|
78304
|
-
}
|
|
78305
|
-
return result;
|
|
78306
|
-
}
|
|
78307
|
-
return obj;
|
|
78308
|
-
}
|
|
78309
|
-
|
|
78310
78321
|
// ../shared/src/app_query_gate.ts
|
|
78311
78322
|
function refused(refusal) {
|
|
78312
78323
|
return { ok: false, refusal };
|
|
@@ -81346,6 +81357,7 @@ function createSource(plan) {
|
|
|
81346
81357
|
const opening = [
|
|
81347
81358
|
`const made = await create_records({`,
|
|
81348
81359
|
` table_id: ${str(plan.table.id)},`,
|
|
81360
|
+
` ids: isNull(i.${RECORD_INPUT}) ? null : [i.${RECORD_INPUT}],`,
|
|
81349
81361
|
` records: [`,
|
|
81350
81362
|
` {`,
|
|
81351
81363
|
...cells.map((cell) => ` ${cell}`),
|
|
@@ -82456,7 +82468,7 @@ function fieldInput(ctx, bf, required2) {
|
|
|
82456
82468
|
}
|
|
82457
82469
|
}
|
|
82458
82470
|
function createInputs(ctx, alias2, writes) {
|
|
82459
|
-
const inputs = {};
|
|
82471
|
+
const inputs = { [RECORD_INPUT]: { type: "text", required: false } };
|
|
82460
82472
|
if (writes.parent !== void 0) {
|
|
82461
82473
|
const via = fieldOf2(ctx, alias2, writes.parent);
|
|
82462
82474
|
if (via.field.type !== "select_record_link") fail(`"${alias2}.${via.alias}" files a row under its record, so it must be a link`);
|
|
@@ -86705,6 +86717,7 @@ var READ_OPS = /* @__PURE__ */ new Set([
|
|
|
86705
86717
|
"context",
|
|
86706
86718
|
"query",
|
|
86707
86719
|
"field_options",
|
|
86720
|
+
"write_model",
|
|
86708
86721
|
"members",
|
|
86709
86722
|
"agentRuns",
|
|
86710
86723
|
"agentRun.get",
|
|
@@ -86737,6 +86750,7 @@ function refusalMessage(refused2, by) {
|
|
|
86737
86750
|
var SUPPORTED_OPS = /* @__PURE__ */ new Set([
|
|
86738
86751
|
"query",
|
|
86739
86752
|
"field_options",
|
|
86753
|
+
"write_model",
|
|
86740
86754
|
"workflow",
|
|
86741
86755
|
"members",
|
|
86742
86756
|
"context",
|
|
@@ -86792,6 +86806,8 @@ async function dispatchRpc(client, body, opts) {
|
|
|
86792
86806
|
}
|
|
86793
86807
|
return client.appFieldOptions(body.app_id, p.alias);
|
|
86794
86808
|
}
|
|
86809
|
+
case "write_model":
|
|
86810
|
+
return client.appWriteModel(body.app_id);
|
|
86795
86811
|
case "workflow": {
|
|
86796
86812
|
const p = body.payload;
|
|
86797
86813
|
if (!p || typeof p.alias !== "string") {
|
|
@@ -88233,7 +88249,7 @@ var PROBES = {
|
|
|
88233
88249
|
partial_count: {
|
|
88234
88250
|
law: "reviewing.md",
|
|
88235
88251
|
section: "8o",
|
|
88236
|
-
fix: "State the server's count of the set, never the rows a page loaded: count the read (`
|
|
88252
|
+
fix: "State the server's count of the set, never the rows a page loaded: count the read (`useQuery`'s `total`) and step or badge against that total."
|
|
88237
88253
|
},
|
|
88238
88254
|
record_primary: {
|
|
88239
88255
|
law: "hierarchy.md",
|
|
@@ -89242,6 +89258,8 @@ function modelAnswer(data) {
|
|
|
89242
89258
|
return { members: [{ ...PREVIEW_MEMBER }] };
|
|
89243
89259
|
case "field_options":
|
|
89244
89260
|
return typeof alias2 === "string" ? { fields: data.field_options[alias2] ?? {}, units: data.field_units[alias2] ?? {} } : { fields: {}, units: {} };
|
|
89261
|
+
case "write_model":
|
|
89262
|
+
return { workflows: {}, queries: {}, tables: [] };
|
|
89245
89263
|
case "query": {
|
|
89246
89264
|
if (typeof alias2 !== "string") throw new Error("a query names its alias; this call named none");
|
|
89247
89265
|
const params = stated2(payload, "params");
|
package/dist/src/client.d.ts
CHANGED
|
@@ -1426,6 +1426,12 @@ export declare class LoticsClient {
|
|
|
1426
1426
|
}>;
|
|
1427
1427
|
}>;
|
|
1428
1428
|
}>;
|
|
1429
|
+
/**
|
|
1430
|
+
* What the app's writes will change — each bound workflow's record-write subset, each query's column
|
|
1431
|
+
* sources, the fields they name. Mirrors GET /v1/apps/{app_id}/write_model; the dev server hands it to
|
|
1432
|
+
* the app's SDK whole, so its parts stay opaque here.
|
|
1433
|
+
*/
|
|
1434
|
+
appWriteModel(app_id: string): Promise<unknown>;
|
|
1429
1435
|
/**
|
|
1430
1436
|
* Execute a workflow by alias declared in package.json#lotics.workflows.
|
|
1431
1437
|
* Mirrors POST /v1/apps/{app_id}/workflows/{alias}/execute.
|
package/dist/src/client.js
CHANGED
|
@@ -883,6 +883,14 @@ var LoticsClient = class {
|
|
|
883
883
|
async appFieldOptions(app_id, alias) {
|
|
884
884
|
return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/field-options`, { alias });
|
|
885
885
|
}
|
|
886
|
+
/**
|
|
887
|
+
* What the app's writes will change — each bound workflow's record-write subset, each query's column
|
|
888
|
+
* sources, the fields they name. Mirrors GET /v1/apps/{app_id}/write_model; the dev server hands it to
|
|
889
|
+
* the app's SDK whole, so its parts stay opaque here.
|
|
890
|
+
*/
|
|
891
|
+
async appWriteModel(app_id) {
|
|
892
|
+
return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/write_model`);
|
|
893
|
+
}
|
|
886
894
|
/**
|
|
887
895
|
* Execute a workflow by alias declared in package.json#lotics.workflows.
|
|
888
896
|
* Mirrors POST /v1/apps/{app_id}/workflows/{alias}/execute.
|
package/docs/cli_reference.md
CHANGED
|
@@ -81,7 +81,7 @@ Per-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. S
|
|
|
81
81
|
| `lotics app workflow check [alias...]` | Check the editable workflow bodies locally — **every alias you name**, or all of them when you name none; an alias that is not bound is refused BEFORE any body is checked, so a green ✓ never sits under an exit 1 — locally first, in the **server's own order** — parse, then type-check — then on the server. **Parse** runs `parseWorkflowJs` from `@lotics/shared` (the SAME module `verifyWorkflow` calls, never a second implementation) over the stripped body `set` would upload, with `toolNames: undefined` (the CLI ships no tool registry, so tool-name resolution stays a server check while every shape/scope rule runs here). A body the subset rejects reports **that error alone** and skips the compiler — it never reaches the server's compiler either, so tsc's opinion of it is noise. **Type-check** then builds an **isolated** `ts.Program` per alias from exactly that alias's `{body, globals}` pair — mirroring the server, which verifies one body at a time — so the per-alias ambient `trigger` never collides and `trigger.app_workflow.inputs` is checked against the right alias. All aliases run in ONE node process (N programs, not N `tsc` spawns), with the SAME compile options the server uses at set-time verify (lib `es2022` with no DOM, target ES2022, strict, NodeNext, `types:[]`, skipLibCheck) and the app's OWN `typescript` (resolved from its `node_modules`, never bundled into the CLI). What the compiler sees is the **checked source**, not the file: `rewriteAccumulatorAppends` from `@lotics/shared` — the SAME transform the server applies before its set-time compile — is applied in memory, so a pulled body's canonical `out = concat(out, [item])` accumulator checks green here exactly as it saves there, and the body on disk is never rewritten. Reports `<file>:<line>:<col> - <TS####\|subset>` at the **physical** line in `src/workflows/<alias>.ts`, so an editor jump lands on the offending code (these are deliberately NOT `set`'s body-relative numbers — `set` prints no file path, so there is no format to agree with); exits non-zero if any alias fails. **Then every body the local passes found clean goes to the server**: `set_app_workflow` with `verify_only: true`, sent exactly what `set` sends (the manifest's `inputs`/`outputs`/`description` and the synced `expected_body_sha`), which runs every check a save runs — names, lint, structural validation, table reach, the published-API guard — and writes nothing. Its issues print in the same `<file>:<line>:<col> - <source>/<code>` form at the physical line; one tied to a step rather than a position prints against the file, naming the step; a refusal (a stale baseline, a draft, a break) prints as `server/<code>`. All of them exit 1. A server it cannot reach — no credentials, the network, a server that predates `verify_only` — is a warning naming each alias and why, and the exit is then the local verdict's, so a green run says which of the two it is. A bound alias with no body file yet warns + skips; a body with no globals errors (naming `lotics app codegen`, which refreshes types WITHOUT touching the body — a pull would overwrite it). **It also keeps the types honest.** Each alias's `.lotics/workflows/<alias>.globals.d.ts` carries a `// lotics:declaration <hash>` stamp of the manifest declaration it was rendered from; `check` compares it to `package.json#lotics.workflows.<alias>` and, when they differ, re-renders that alias's dts from the LOCAL declaration before compiling. Without it the verdict was confidently wrong in the exact case an author needs it — declare an input, run `check`, and get `TS2339: Property 'x' does not exist` pointing at your body for a schema the types have never been told about. The server renders a dts from a SUPPLIED declaration, so this works before the manifest has ever been deployed, which is when it matters (the order is edit → check → set). That refresh is skipped when the stamps match, and with no credentials or a failed fetch it WARNS and checks against the older types rather than blocking. A file written before the stamp existed reads as unknown, never as matching, so a pre-existing checkout heals on its first run. |
|
|
82
82
|
| `lotics app subdomain <new-subdomain>` | Rename the app's public address under the instance's apps domain via `PUT /v1/apps/{id}/subdomain`. app_id comes from the local `package.json` manifest; the chosen slug must be a valid DNS label and free; the old address stops resolving. |
|
|
83
83
|
| `lotics app rename "<new name>" [--description <d>] [--icon <lucide-name>] [--theme <color>]` | **The app's display metadata, live and on disk, in one verb** — via the `update_app` tool (the single setter for name/description/icon/theme). app_id comes from the local `package.json` manifest; the public address (`subdomain`) and the code (`deploy`) are unchanged. **It also writes `package.json#name`**, folded from the new display name by the scaffold's own rule (`starter_template.ts` — diacritics are FOLDED, never dropped, so `Điều xe` is `dieu-xe` and not `i-u`). Nothing else folds it, so a rename that skipped it left every `npm` line, every CI log and every reader of the project calling the app by its old name. Written only after the server took the rename, and surgically: no other manifest key moves. The three flags set the branding `app check` warns about — a missing icon or colour draws a generic tile, a missing description gives the app's chat agent a roster of aliases and no brief — through the same call, so setting them is never a second `lotics run update_app` that forgets the manifest write. `--theme` takes the COLOUR (`theme.color` is the whole of what the launcher reads), not a JSON object. A flag you omit changes nothing: `update_app` merges, and absent means unchanged. CLEARING one is still `lotics run update_app` with an explicit `null` — a CLI flag has no spelling for that a shell cannot produce by accident. |
|
|
84
|
-
| `lotics app dev [path] [--port <n>] [--vite-port <n>] [--view-as=<member_id>]` | Spawn Vite dev server + an RPC-forwarding HTTP server. **Refused in one sentence on a project with no `vite.config.*`** — an app created with `--api` serves no bundle, and without the config Vite fails on a missing entry document in a bundler's words about a file the author never expected to have. `app check --screens` is refused on the same project for the same reason, rather than reporting "nothing blocking" for a pass it never ran; plain `app check` runs everything else. **`--port` is the wrapper you open and `--vite-port` is the module server, each as `--port <n>` or `--port=<n>`; pin both to run several apps at once.** A value that is not a port number is refused. With no `--vite-port`, `vite.config`'s own `server.port` is used when it states a literal one — the CLI passes `--port … --strictPort` to Vite, and a CLI flag beats the config in Vite's precedence, so the config's value could otherwise never win. `--vite-port` still outranks it and says so. **The wrapper serves every path that is not one of its own `/_…` routes**, so `http://localhost:PORT/lo/rec_…` opens that screen directly; `?_loc=<url-encoded path>` still works and wins. With `LOTICS_UI_SRC` set, Vite is started with `--force`: its optimizer cache survives a restart, so a NEW file added to the linked kit tree otherwise left the browser running the previous build of the module that imported it, silently. The wrapper page embeds the iframe with `sandbox="allow-scripts allow-same-origin"` matching production; postMessage ops (query / workflow / members / context / upload / openExternal / urlState / agentRun) are forwarded to api.lotics.ai using the CLI's API key — file bytes move in **both** directions through the dev server's own relays, never browser↔storage: dev runs against the PROD bucket, whose CORS admits `https://*.lotics.app` and not `http://localhost:<port>`, so a direct browser transfer is blocked — no upload could complete and no preview engine (PDF/Word/Excel all FETCH the bytes) could read a file. `upload` mints a presigned URL and PUTs it **to `PUT /_upload/<file_id>`** from the wrapper page — same-origin, so no preflight and no CORS — and Node forwards it on; every presigned `url`/`thumbnail_url`/`preview_url` on a **file object** in an RPC result is rewritten to **`GET /_file/<token>`** (absolute — the iframe would resolve a relative path against Vite), which streams the bytes back with `Range` passthrough (206s intact, so PDF seeking works) and an `Access-Control-Allow-Origin` for the Vite origin (the one cross-origin hop left is OUR response to allow). Neither relay ever takes a destination from the client — it gets a `file_id`/token and transfers only to/from a URL it minted or observed itself, so there is no client-controlled target and no SSRF surface. A URL in a record's own text cell is NOT rewritten. Production is unchanged (direct-to-storage, no bytes through the API server); `openExternal` and `urlState.get/set` are handled locally (the latter read/write the wrapper page's own address bar — `set` writes in place via `replaceState` and browser back/forward broadcast a `url-state` message back, so `useUrlState` survives refresh and is shareable in the dev loop; in-app *routing* is the app's own (the iframe owns its url via `@lotics/app-runtime/router`), and the wrapper bakes the saved screen (`_loc`) into the iframe src on load so a refresh restores it, mirroring production); `agentRun` (streaming) is proxied through `POST /_agent_run`, which opens the run's SSE with the CLI key and pipes chunks back to the iframe (`stream-chunk`* → `stream-end`), so `useAgentRun` works in the dev loop just like production; `context` resolves the viewer (`member_id` from `cli/whoami` + `comments_enabled` from the local manifest) and fetches the app's stored `config` live from the app row, so `useConfig()` renders the same values as production. `--view-as` (global flag; also `LOTICS_VIEW_AS`) threads `x-view-as-member-id` so `is_current_member` + `context` resolve to that member — **admin key only** (the server 403s a non-admin), writes stay attributed to the key owner. Hot reload via Vite; full DevTools / Playwright access via plain localhost. **Every forwarded op logs one line naming its ALIAS** — `[rpc] query applicants 231ms` — and `query applicants (count)` for a count request, which is a SECOND full execution of the same query rather than a cheap lookup. When requests overlap the line carries `· N in flight`. That number is the one to watch: the server bounds how many app queries run at once, so requests past the bound wait and the wait lands inside each request's own duration — a burst reads as "every query got slower", which looks like a slow database and is not one. A screen firing its list plus three facet counts on one keystroke shows up here as eight lines over one or two aliases; see `@lotics/app-runtime` `docs/data_fetching.md` (`
|
|
84
|
+
| `lotics app dev [path] [--port <n>] [--vite-port <n>] [--view-as=<member_id>]` | Spawn Vite dev server + an RPC-forwarding HTTP server. **Refused in one sentence on a project with no `vite.config.*`** — an app created with `--api` serves no bundle, and without the config Vite fails on a missing entry document in a bundler's words about a file the author never expected to have. `app check --screens` is refused on the same project for the same reason, rather than reporting "nothing blocking" for a pass it never ran; plain `app check` runs everything else. **`--port` is the wrapper you open and `--vite-port` is the module server, each as `--port <n>` or `--port=<n>`; pin both to run several apps at once.** A value that is not a port number is refused. With no `--vite-port`, `vite.config`'s own `server.port` is used when it states a literal one — the CLI passes `--port … --strictPort` to Vite, and a CLI flag beats the config in Vite's precedence, so the config's value could otherwise never win. `--vite-port` still outranks it and says so. **The wrapper serves every path that is not one of its own `/_…` routes**, so `http://localhost:PORT/lo/rec_…` opens that screen directly; `?_loc=<url-encoded path>` still works and wins. With `LOTICS_UI_SRC` set, Vite is started with `--force`: its optimizer cache survives a restart, so a NEW file added to the linked kit tree otherwise left the browser running the previous build of the module that imported it, silently. The wrapper page embeds the iframe with `sandbox="allow-scripts allow-same-origin"` matching production; postMessage ops (query / workflow / members / context / upload / openExternal / urlState / agentRun) are forwarded to api.lotics.ai using the CLI's API key — file bytes move in **both** directions through the dev server's own relays, never browser↔storage: dev runs against the PROD bucket, whose CORS admits `https://*.lotics.app` and not `http://localhost:<port>`, so a direct browser transfer is blocked — no upload could complete and no preview engine (PDF/Word/Excel all FETCH the bytes) could read a file. `upload` mints a presigned URL and PUTs it **to `PUT /_upload/<file_id>`** from the wrapper page — same-origin, so no preflight and no CORS — and Node forwards it on; every presigned `url`/`thumbnail_url`/`preview_url` on a **file object** in an RPC result is rewritten to **`GET /_file/<token>`** (absolute — the iframe would resolve a relative path against Vite), which streams the bytes back with `Range` passthrough (206s intact, so PDF seeking works) and an `Access-Control-Allow-Origin` for the Vite origin (the one cross-origin hop left is OUR response to allow). Neither relay ever takes a destination from the client — it gets a `file_id`/token and transfers only to/from a URL it minted or observed itself, so there is no client-controlled target and no SSRF surface. A URL in a record's own text cell is NOT rewritten. Production is unchanged (direct-to-storage, no bytes through the API server); `openExternal` and `urlState.get/set` are handled locally (the latter read/write the wrapper page's own address bar — `set` writes in place via `replaceState` and browser back/forward broadcast a `url-state` message back, so `useUrlState` survives refresh and is shareable in the dev loop; in-app *routing* is the app's own (the iframe owns its url via `@lotics/app-runtime/router`), and the wrapper bakes the saved screen (`_loc`) into the iframe src on load so a refresh restores it, mirroring production); `agentRun` (streaming) is proxied through `POST /_agent_run`, which opens the run's SSE with the CLI key and pipes chunks back to the iframe (`stream-chunk`* → `stream-end`), so `useAgentRun` works in the dev loop just like production; `context` resolves the viewer (`member_id` from `cli/whoami` + `comments_enabled` from the local manifest) and fetches the app's stored `config` live from the app row, so `useConfig()` renders the same values as production. `--view-as` (global flag; also `LOTICS_VIEW_AS`) threads `x-view-as-member-id` so `is_current_member` + `context` resolve to that member — **admin key only** (the server 403s a non-admin), writes stay attributed to the key owner. Hot reload via Vite; full DevTools / Playwright access via plain localhost. **Every forwarded op logs one line naming its ALIAS** — `[rpc] query applicants 231ms` — and `query applicants (count)` for a count request, which is a SECOND full execution of the same query rather than a cheap lookup. When requests overlap the line carries `· N in flight`. That number is the one to watch: the server bounds how many app queries run at once, so requests past the bound wait and the wait lands inside each request's own duration — a burst reads as "every query got slower", which looks like a slow database and is not one. A screen firing its list plus three facet counts on one keystroke shows up here as eight lines over one or two aliases; see `@lotics/app-runtime` `docs/data_fetching.md` (`total`, and `useQueries` for several counts at once) and `docs/queries.md` §10 for collapsing them. **Holds no realtime connection** — push belongs to the product frontend, so an app previewed here never updates on an external write (a CLI run, another tab, an agent): reload to see it. Deliberate rather than missing, since the alternative is a second implementation of the channel in the wrapper page, and a blanket poll here would hide an app whose queries do not declare their tables — the one mistake the real host punishes. The startup banner says `realtime: off` so this is visible without reading this table. The scaffold's `vite.config.ts` states `optimizeDeps: loticsOptimizeDeps()` — every published kit subpath, DERIVED from the kit's own `exports` rather than copied, and empty under `LOTICS_UI_SRC`. Vite's scanner reaches a subpath the moment something imports it, and meeting one mid-session re-optimizes, reloads, and inside this sandboxed iframe leaves two Reacts ("Invalid hook call") until a cold restart. `dev` and `codegen` heal that line, and the `server.fs.allow` one beside it, into a config scaffolded before them. Binds **loopback only** (`127.0.0.1`) — `/_rpc` dispatches with the developer's API key, so a socket on every interface would hand anyone on the network full read/write on the workspace. |
|
|
85
85
|
| `LOTICS_UI_SRC=<abs path to packages/ui/src>` (env, not a command) | Dev-link `@lotics/ui` to a monorepo checkout for the length of ONE command, **for every tool at once**. The app's `vite.config.ts` gets its whole `resolve` block from the kit (`resolve: loticsResolve()` — `@lotics/ui/vite`), which reads the variable at call time and adds the `@lotics/ui/*` → working-copy alias, so kit edits go live under `lotics app dev` (HMR) and bundle under `lotics app deploy`. In the same breath, every command that regenerates types (`create`/`pull`/`dev`/`deploy`/`codegen`, all via `writeAppDts`) writes **`.lotics/tsconfig.link.json`** — the matching `paths`, which the app's `tsconfig.json` `extends` — so `tsc`, vitest, eslint and your EDITOR resolve the same copy Vite does. Unset ⇒ every one of them goes back to `node_modules`, and the generated file is rewritten inert. **Why `paths` and not `npm link`:** under the dev-link a kit file sits OUTSIDE the app's `node_modules` and resolves its OWN `react` from the monorepo — two copies in one program and every shared type stops matching ("Two different types with this name exist, but they are unrelated"). The generated file therefore also pins every peer @lotics/ui declares to the APP's copy, types-package first (`react` → `@types/react`; pinning the runtime package instead strands tsc on a `.js` with no declarations). The pin set is derived from the installed kit's `peerDependencies`, so it tracks the kit rather than rotting. **The one hand-written file edited is `vite.config.ts`**, by `dev` and `codegen`, and only for the two blocks that are CALLS into `@lotics/ui/vite` (`optimizeDeps: loticsOptimizeDeps()`, `loticsFsAllow()` in `server.fs.allow`), spliced at the scaffold's own anchors in a config that already imports the subpath; a key the author answers themselves is left as typed and named back instead. Everything else generated lives in `.lotics/` (the CLI's own dir). Identical for a monorepo app and an EXTERNAL one (e.g. `~/lotics_apps`). `app deploy` still warns whenever the variable is set — that the bundle carries kit code from your working copy, or that the app's config predates `loticsResolve()` and never reads it, so the PUBLISHED kit is going out. An app whose `tsconfig.json` already `extends` something else is told rather than rewritten: add `./.lotics/tsconfig.link.json` to the array yourself. |
|
|
86
86
|
| `lotics file preview <file\|fil_id> [-o out.png]` | (also `lotics preview`) A PDF is refused and the refusal names the route: `lotics file download <fil_id>`, then `pdftoppm -png -r 150` (poppler-utils) for one PNG per page — "open it" is an instruction for a person at a screen, and the caller here is usually an agent. A `.html` renders as the page it is — served from its own directory so what it refers to beside it resolves, read once its images have loaded, captured at its content size — which is how a demo's paper props (an official letter, a stamped minute, a supplier's bill) are looked at before they go into an `html` template. Otherwise render a .docx/.xlsx to a PNG using the SAME engines the frontend FilePreview uses (`@lotics/docx` `loadDocxIntoElement` / `@lotics/xlsx` `drawSpreadsheet`) — so what you see matches an operator. Accepts a **local path** OR a stored **`fil_…` id** (a bare id, no extension): an id is first downloaded to a temp dir (the same presign path as `lotics file download`), rendered, then the transient source is removed; with no `-o` the PNG lands in cwd under the stored file's base name. Drives a headless Chrome over **CDP with only Node built-ins** — zero npm deps, the CLI stays a single bundled binary. The browser render logic is a separate browser bundle shipped at `dist/render_page.js`, served over a throwaway localhost http server and screenshotted full-page. **Requires a Chrome/Chromium on the machine** — detected from `CHROME_PATH`/`LOTICS_CHROME`, then Playwright's installed chromium, then system paths — inherent to rendering these browser formats; a clear "install a browser" error otherwise. PDFs need no render (open them directly). |
|
|
87
87
|
|