@lotics/cli 0.123.0 → 0.128.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 +388 -152
- package/dist/src/client.d.ts +8 -0
- package/docs/cli_reference.md +3 -3
- package/package.json +1 -1
package/dist/src/cli.js
CHANGED
|
@@ -45587,8 +45587,8 @@ import { randomUUID } from "node:crypto";
|
|
|
45587
45587
|
import { tmpdir } from "node:os";
|
|
45588
45588
|
|
|
45589
45589
|
// src/starter_template.ts
|
|
45590
|
-
var STARTER_FALLBACK_UI_VERSION = "
|
|
45591
|
-
var STARTER_FALLBACK_SDK_VERSION = "0.
|
|
45590
|
+
var STARTER_FALLBACK_UI_VERSION = "28.1.0";
|
|
45591
|
+
var STARTER_FALLBACK_SDK_VERSION = "0.73.0";
|
|
45592
45592
|
var STARTER_REACT_NATIVE_VERSION = "0.85.3";
|
|
45593
45593
|
var VITEST_SETUP_FILENAME = "vitest.setup.ts";
|
|
45594
45594
|
var VITEST_SETUP_CONTENT = `import { vi } from "vitest";
|
|
@@ -45793,7 +45793,6 @@ export default defineConfig({
|
|
|
45793
45793
|
// const base = loticsResolve();
|
|
45794
45794
|
// resolve: { ...base, alias: [...base.alias, { find: "x", replacement: "y" }] }
|
|
45795
45795
|
resolve: loticsResolve(),
|
|
45796
|
-
},
|
|
45797
45796
|
optimizeDeps: {
|
|
45798
45797
|
// The dev dep-optimizer must pre-bundle @lotics/ui's RN-ecosystem + markdown
|
|
45799
45798
|
// CJS-interop deps or \`lotics app dev\` blanks the iframe ("does not provide an
|
|
@@ -47501,6 +47500,9 @@ function calledAppAliases(sourceText) {
|
|
|
47501
47500
|
}
|
|
47502
47501
|
return out;
|
|
47503
47502
|
}
|
|
47503
|
+
function orphanedAliases(bound, called) {
|
|
47504
|
+
return unboundAliases(bound, called);
|
|
47505
|
+
}
|
|
47504
47506
|
function unboundAliases(called, bound) {
|
|
47505
47507
|
const missing = (from, against) => {
|
|
47506
47508
|
const have = new Set(against ?? []);
|
|
@@ -64786,6 +64788,7 @@ var querySourceSchema = zod_default.lazy(
|
|
|
64786
64788
|
zod_default.object({ expression: zod_default.string() })
|
|
64787
64789
|
])
|
|
64788
64790
|
);
|
|
64791
|
+
var FILES_PROJECTION_MAX_LIMIT = 10;
|
|
64789
64792
|
var queryProjectionColumnObjectSchema = zod_default.object({
|
|
64790
64793
|
output: zod_default.string().optional().describe(
|
|
64791
64794
|
"Output column name. Omit for a passthrough \u2014 it defaults to the source field key. Required only to rename, or for a computed source."
|
|
@@ -64801,6 +64804,9 @@ var queryProjectionColumnObjectSchema = zod_default.object({
|
|
|
64801
64804
|
kind: zod_default.literal("source_field").describe("Currently the only writable target kind")
|
|
64802
64805
|
}).optional().describe(
|
|
64803
64806
|
"Marks this column as writable through to the originating source field. Valid only when `source` is a bare-string field reference and upstream addressing (source_table_id, source_field_key) is unambiguous. The compiler validates this and rejects otherwise."
|
|
64807
|
+
),
|
|
64808
|
+
limit: zod_default.number().int().min(1).max(FILES_PROJECTION_MAX_LIMIT).optional().describe(
|
|
64809
|
+
"FILES columns only: return at most this many entries per cell, in stored order. For a list that renders ONE thumbnail per row \u2014 without it the column yields the whole array and the response pays to resolve every entry it will never show."
|
|
64804
64810
|
)
|
|
64805
64811
|
});
|
|
64806
64812
|
var queryProjectionColumnSchema = zod_default.union([
|
|
@@ -70433,56 +70439,53 @@ function sameSchema(a, b) {
|
|
|
70433
70439
|
};
|
|
70434
70440
|
return JSON.stringify(canonical(a)) === JSON.stringify(canonical(b));
|
|
70435
70441
|
}
|
|
70436
|
-
function agentTypeDivergences(manifestAgents, liveAgents) {
|
|
70437
|
-
const out = [];
|
|
70438
|
-
for (const [alias, declared] of Object.entries(manifestAgents ?? {})) {
|
|
70439
|
-
const live = (liveAgents ?? {})[alias];
|
|
70440
|
-
if (!live) {
|
|
70441
|
-
out.push(
|
|
70442
|
-
`${alias} \u2014 typed locally but NOT bound on the app, so useAgentRun("${alias}") 400s at runtime`
|
|
70443
|
-
);
|
|
70444
|
-
continue;
|
|
70445
|
-
}
|
|
70446
|
-
for (const key of AGENT_TYPED_KEYS) {
|
|
70447
|
-
if (!sameSchema(declared[key], live[key])) {
|
|
70448
|
-
out.push(`${alias}.${key} \u2014 local types disagree with the live agent`);
|
|
70449
|
-
}
|
|
70450
|
-
}
|
|
70451
|
-
}
|
|
70452
|
-
return out;
|
|
70453
|
-
}
|
|
70454
70442
|
function workflowTypeDivergences(manifestWorkflows, liveWorkflows) {
|
|
70455
70443
|
const out = [];
|
|
70456
70444
|
for (const [alias, declared] of Object.entries(manifestWorkflows ?? {})) {
|
|
70457
70445
|
const live = (liveWorkflows ?? {})[alias];
|
|
70458
70446
|
if (!live) {
|
|
70459
|
-
out.push(
|
|
70447
|
+
out.push({ alias });
|
|
70460
70448
|
continue;
|
|
70461
70449
|
}
|
|
70462
70450
|
for (const key of ["inputs", "outputs"]) {
|
|
70463
|
-
if (!sameSchema(declared[key], live[key])) {
|
|
70464
|
-
out.push(`${alias}.${key} \u2014 the manifest declares a schema the live binding does not have`);
|
|
70465
|
-
}
|
|
70451
|
+
if (!sameSchema(declared[key], live[key])) out.push({ alias, key });
|
|
70466
70452
|
}
|
|
70467
70453
|
}
|
|
70468
70454
|
return out;
|
|
70469
70455
|
}
|
|
70456
|
+
function syncedEntry(entry) {
|
|
70457
|
+
if (entry === void 0) return void 0;
|
|
70458
|
+
return typeof entry === "string" ? { content: entry } : entry;
|
|
70459
|
+
}
|
|
70470
70460
|
function readSynced(projectDir) {
|
|
70471
70461
|
try {
|
|
70472
70462
|
const pkg2 = JSON.parse(
|
|
70473
70463
|
fs6.readFileSync(path7.join(projectDir, "package.json"), "utf-8")
|
|
70474
70464
|
);
|
|
70475
|
-
|
|
70465
|
+
const raw = pkg2.lotics?.synced ?? {};
|
|
70466
|
+
return {
|
|
70467
|
+
workflows: normalizeSyncedMap(raw.workflows),
|
|
70468
|
+
agents: normalizeSyncedMap(raw.agents),
|
|
70469
|
+
queries: normalizeSyncedMap(raw.queries)
|
|
70470
|
+
};
|
|
70476
70471
|
} catch {
|
|
70477
|
-
return {};
|
|
70472
|
+
return { workflows: {}, agents: {}, queries: {} };
|
|
70478
70473
|
}
|
|
70479
70474
|
}
|
|
70480
|
-
function
|
|
70475
|
+
function normalizeSyncedMap(map3) {
|
|
70476
|
+
return Object.fromEntries(
|
|
70477
|
+
Object.entries(map3 ?? {}).map(([alias, entry]) => [
|
|
70478
|
+
alias,
|
|
70479
|
+
syncedEntry(entry)
|
|
70480
|
+
])
|
|
70481
|
+
);
|
|
70482
|
+
}
|
|
70483
|
+
function writeSynced(projectDir, kind, alias, entry) {
|
|
70481
70484
|
const pkgPath2 = path7.join(projectDir, "package.json");
|
|
70482
70485
|
const pkg2 = JSON.parse(fs6.readFileSync(pkgPath2, "utf-8"));
|
|
70483
70486
|
if (!pkg2.lotics) return;
|
|
70484
70487
|
pkg2.lotics.synced ??= {};
|
|
70485
|
-
pkg2.lotics.synced[kind] = { ...pkg2.lotics.synced[kind], [alias]:
|
|
70488
|
+
pkg2.lotics.synced[kind] = { ...pkg2.lotics.synced[kind], [alias]: entry };
|
|
70486
70489
|
fs6.writeFileSync(pkgPath2, JSON.stringify(pkg2, null, 2) + "\n");
|
|
70487
70490
|
}
|
|
70488
70491
|
var WORKFLOWS_DIR = path7.join("src", "workflows");
|
|
@@ -70544,11 +70547,14 @@ function writeWorkflowGlobals(projectDir, alias, dts) {
|
|
|
70544
70547
|
`);
|
|
70545
70548
|
return file2;
|
|
70546
70549
|
}
|
|
70550
|
+
function normalizeWorkflowBody(source) {
|
|
70551
|
+
return source.replace(/\s+$/, "");
|
|
70552
|
+
}
|
|
70547
70553
|
function writeWorkflowFile(projectDir, alias, source, envelope = { prefix: WORKFLOW_ENVELOPE_PREFIX, suffix: WORKFLOW_ENVELOPE_SUFFIX }) {
|
|
70548
70554
|
const dir = path7.join(projectDir, WORKFLOWS_DIR);
|
|
70549
70555
|
fs6.mkdirSync(dir, { recursive: true });
|
|
70550
70556
|
const file2 = workflowFilePath(projectDir, alias);
|
|
70551
|
-
const body = source
|
|
70557
|
+
const body = normalizeWorkflowBody(source);
|
|
70552
70558
|
fs6.writeFileSync(file2, `${workflowFileHeader(alias)}
|
|
70553
70559
|
${envelope.prefix}${body}${envelope.suffix}
|
|
70554
70560
|
`);
|
|
@@ -70570,7 +70576,7 @@ function writeAgentFile(projectDir, alias, instructions) {
|
|
|
70570
70576
|
);
|
|
70571
70577
|
return file2;
|
|
70572
70578
|
}
|
|
70573
|
-
function writeAgentFiles(projectDir, agents) {
|
|
70579
|
+
function writeAgentFiles(projectDir, agents, opts) {
|
|
70574
70580
|
const written = [];
|
|
70575
70581
|
for (const [alias, declaration] of Object.entries(agents)) {
|
|
70576
70582
|
const instructions = declaration.instructions;
|
|
@@ -70578,15 +70584,33 @@ function writeAgentFiles(projectDir, agents) {
|
|
|
70578
70584
|
console.error(`\u26A0 Skipped src/agents/${alias}.md \u2014 the live declaration carries no instructions.`);
|
|
70579
70585
|
continue;
|
|
70580
70586
|
}
|
|
70587
|
+
if (!opts.overwrite && localPromptUnpushed(projectDir, alias)) {
|
|
70588
|
+
opts.kept.push(`src/agents/${alias}.md`);
|
|
70589
|
+
continue;
|
|
70590
|
+
}
|
|
70581
70591
|
writeAgentFile(projectDir, alias, instructions);
|
|
70582
70592
|
written.push(alias);
|
|
70583
70593
|
}
|
|
70584
70594
|
return written;
|
|
70585
70595
|
}
|
|
70596
|
+
function localPromptUnpushed(projectDir, alias) {
|
|
70597
|
+
const file2 = agentFilePath2(projectDir, alias);
|
|
70598
|
+
if (!fs6.existsSync(file2)) return false;
|
|
70599
|
+
const baseline = readSynced(projectDir).agents[alias]?.content;
|
|
70600
|
+
if (baseline === void 0) return true;
|
|
70601
|
+
return contentSha(stripAgentHeader(fs6.readFileSync(file2, "utf-8"))) !== baseline;
|
|
70602
|
+
}
|
|
70586
70603
|
function generatedWorkflowDescription(appName, alias) {
|
|
70587
70604
|
return `Workflow invoked by app "${appName}" via useWorkflow("${alias}").`;
|
|
70588
70605
|
}
|
|
70589
|
-
|
|
70606
|
+
function localBodyUnpushed(projectDir, alias) {
|
|
70607
|
+
const file2 = workflowFilePath(projectDir, alias);
|
|
70608
|
+
if (!fs6.existsSync(file2)) return false;
|
|
70609
|
+
const baseline = readSynced(projectDir).workflows[alias]?.content;
|
|
70610
|
+
if (baseline === void 0) return true;
|
|
70611
|
+
return contentSha(stripWorkflowHeader(fs6.readFileSync(file2, "utf-8"))) !== baseline;
|
|
70612
|
+
}
|
|
70613
|
+
async function writeWorkflowFiles(client, projectDir, app_id, appName, workflows, opts) {
|
|
70590
70614
|
const written = [];
|
|
70591
70615
|
for (const [alias, declaration] of Object.entries(workflows)) {
|
|
70592
70616
|
const res = await client.getAppWorkflow(app_id, alias);
|
|
@@ -70595,7 +70619,9 @@ async function writeWorkflowFiles(client, projectDir, app_id, appName, workflows
|
|
|
70595
70619
|
`Could not read workflow "${alias}" of ${app_id}: ${res.error}. Reading app workflow source requires an admin member \u2014 check the role behind this API key.`
|
|
70596
70620
|
);
|
|
70597
70621
|
}
|
|
70598
|
-
const
|
|
70622
|
+
const result = res.result === null || typeof res.result !== "object" ? null : res.result;
|
|
70623
|
+
const source = result?.source;
|
|
70624
|
+
const body_sha = typeof result?.body_sha === "string" ? result.body_sha : void 0;
|
|
70599
70625
|
if (typeof source !== "string" || source.trim() === "") {
|
|
70600
70626
|
console.error(
|
|
70601
70627
|
`\u26A0 Skipped src/workflows/${alias}.ts \u2014 legacy workflow with no rendered body.`
|
|
@@ -70609,8 +70635,15 @@ async function writeWorkflowFiles(client, projectDir, app_id, appName, workflows
|
|
|
70609
70635
|
alias,
|
|
70610
70636
|
toWorkflowDtsDeclaration(declaration)
|
|
70611
70637
|
);
|
|
70638
|
+
if (!opts.overwrite && localBodyUnpushed(projectDir, alias)) {
|
|
70639
|
+
opts.kept.push(`src/workflows/${alias}.ts`);
|
|
70640
|
+
continue;
|
|
70641
|
+
}
|
|
70612
70642
|
writeWorkflowFile(projectDir, alias, source, envelope);
|
|
70613
|
-
writeSynced(projectDir, "workflows", alias,
|
|
70643
|
+
writeSynced(projectDir, "workflows", alias, {
|
|
70644
|
+
content: contentSha(normalizeWorkflowBody(source)),
|
|
70645
|
+
...body_sha ? { live: body_sha } : {}
|
|
70646
|
+
});
|
|
70614
70647
|
const rowDescription = res.result.description;
|
|
70615
70648
|
if (typeof rowDescription === "string" && rowDescription !== "" && // Skip the server's GENERATED default. It is what an undescribed alias
|
|
70616
70649
|
// carries, so writing it back would commit a line of noise per workflow
|
|
@@ -70725,9 +70758,12 @@ function writeAppMeta(projectDir, meta3) {
|
|
|
70725
70758
|
const pkgPath2 = path7.join(projectDir, "package.json");
|
|
70726
70759
|
const pkg2 = JSON.parse(fs6.readFileSync(pkgPath2, "utf-8"));
|
|
70727
70760
|
const existing = pkg2.lotics && typeof pkg2.lotics === "object" ? pkg2.lotics : {};
|
|
70761
|
+
const supplied = new Set(
|
|
70762
|
+
Object.entries(meta3).filter(([, value2]) => value2 !== void 0).map(([key]) => key)
|
|
70763
|
+
);
|
|
70728
70764
|
const preserved = {};
|
|
70729
70765
|
for (const [key, value2] of Object.entries(existing)) {
|
|
70730
|
-
if (!
|
|
70766
|
+
if (!supplied.has(key)) preserved[key] = value2;
|
|
70731
70767
|
}
|
|
70732
70768
|
pkg2.lotics = { ...meta3, ...preserved };
|
|
70733
70769
|
fs6.writeFileSync(pkgPath2, JSON.stringify(pkg2, null, 2) + "\n");
|
|
@@ -70747,7 +70783,7 @@ function writeWorkflowOutputs(projectDir, alias, outputs) {
|
|
|
70747
70783
|
function workflowBodyDrift(projectDir, workflows, synced = {}) {
|
|
70748
70784
|
const drifted = [];
|
|
70749
70785
|
for (const alias of Object.keys(workflows ?? {})) {
|
|
70750
|
-
const baseline = synced[alias];
|
|
70786
|
+
const baseline = syncedEntry(synced[alias])?.content;
|
|
70751
70787
|
if (baseline === void 0) continue;
|
|
70752
70788
|
const file2 = workflowFilePath(projectDir, alias);
|
|
70753
70789
|
if (!fs6.existsSync(file2)) continue;
|
|
@@ -70757,17 +70793,6 @@ function workflowBodyDrift(projectDir, workflows, synced = {}) {
|
|
|
70757
70793
|
}
|
|
70758
70794
|
return drifted;
|
|
70759
70795
|
}
|
|
70760
|
-
function reportWorkflowBodyDrift(projectDir, workflows, synced = {}) {
|
|
70761
|
-
const drifted = workflowBodyDrift(projectDir, workflows, synced);
|
|
70762
|
-
if (drifted.length === 0) return false;
|
|
70763
|
-
console.error(
|
|
70764
|
-
`
|
|
70765
|
-
\u2717 ${drifted.length === 1 ? "This workflow body differs" : "These workflow bodies differ"} from what is live: ${drifted.join(", ")}
|
|
70766
|
-
A deploy does not push bodies, so this version would not reproduce what runs.
|
|
70767
|
-
Send each with 'lotics app workflow set <alias>' \u2014 or revert the file to drop the edit.`
|
|
70768
|
-
);
|
|
70769
|
-
return true;
|
|
70770
|
-
}
|
|
70771
70796
|
function ensureAppTsconfig(projectDir) {
|
|
70772
70797
|
const tsconfigPath = path7.join(projectDir, "tsconfig.json");
|
|
70773
70798
|
if (!fs6.existsSync(tsconfigPath)) {
|
|
@@ -70882,6 +70907,7 @@ function writeAppDts(projectDir, manifest) {
|
|
|
70882
70907
|
writeDevLinkTsconfig(projectDir);
|
|
70883
70908
|
writeKitTypeAugmentation(projectDir);
|
|
70884
70909
|
ensureAppTsconfig(projectDir);
|
|
70910
|
+
ensureAppVitestSetup(projectDir);
|
|
70885
70911
|
return written.map(([file2]) => file2);
|
|
70886
70912
|
}
|
|
70887
70913
|
function readCodegenTablesAllowlist(projectDir) {
|
|
@@ -70909,7 +70935,6 @@ function writeBindingAppFields(projectDir, binding) {
|
|
|
70909
70935
|
fs6.mkdirSync(dotLotics, { recursive: true });
|
|
70910
70936
|
const file2 = path7.join(dotLotics, "app_fields.ts");
|
|
70911
70937
|
fs6.writeFileSync(file2, generatePackageAppFields(binding));
|
|
70912
|
-
ensureAppVitestSetup(projectDir);
|
|
70913
70938
|
return file2;
|
|
70914
70939
|
}
|
|
70915
70940
|
async function writeGeneratedAppFields(client, projectDir, app, queries) {
|
|
@@ -70930,6 +70955,15 @@ async function writeGeneratedAppFields(client, projectDir, app, queries) {
|
|
|
70930
70955
|
} else {
|
|
70931
70956
|
const tableIds = resolveCodegenTableIds(projectDir, queries);
|
|
70932
70957
|
const tables = await client.getWorkspaceSchema(tableIds);
|
|
70958
|
+
const missing = tableIds.filter((id) => !tables.some((t) => t.id === id));
|
|
70959
|
+
if (missing.length > 0) {
|
|
70960
|
+
console.error(
|
|
70961
|
+
`\u26A0 Skipped .lotics/app_fields.ts \u2014 ${missing.length} table(s) the app queries could not be read: ${missing.join(", ")}.
|
|
70962
|
+
Writing anyway would bake a narrowed F/OPT map that compiles and then throws at runtime. Check this key's access to those tables.
|
|
70963
|
+
An existing app_fields.ts is kept and still works; a project that has none yet will fail to BUILD until this resolves \u2014 which is the loud failure, not a new problem.`
|
|
70964
|
+
);
|
|
70965
|
+
return;
|
|
70966
|
+
}
|
|
70933
70967
|
const fieldsPath = writeAppFields(projectDir, tables);
|
|
70934
70968
|
console.error(
|
|
70935
70969
|
`Wrote ${fieldsPath} (baked form \u2014 bespoke; ${tables.length} table${tables.length === 1 ? "" : "s"})`
|
|
@@ -71216,11 +71250,15 @@ async function appPull(client, args) {
|
|
|
71216
71250
|
const tmpFile = path7.join(tmpdir(), `lotics-app-${app.id}-${Date.now()}.tar.gz`);
|
|
71217
71251
|
console.error(`Downloading source archive...`);
|
|
71218
71252
|
await downloadToFile(sourceUrl, tmpFile);
|
|
71253
|
+
const keptLocal = [];
|
|
71254
|
+
const stagingDir = fs6.mkdtempSync(path7.join(tmpdir(), `lotics-pull-${app.id}-`));
|
|
71219
71255
|
try {
|
|
71256
|
+
await runTar(["-xzf", tmpFile, "-C", stagingDir], stagingDir);
|
|
71220
71257
|
console.error(`Extracting to ${targetPath}...`);
|
|
71221
|
-
|
|
71258
|
+
keptLocal.push(...copyTree(stagingDir, targetPath, { overwrite: args.force === true }));
|
|
71222
71259
|
} finally {
|
|
71223
71260
|
if (fs6.existsSync(tmpFile)) fs6.unlinkSync(tmpFile);
|
|
71261
|
+
fs6.rmSync(stagingDir, { recursive: true, force: true });
|
|
71224
71262
|
}
|
|
71225
71263
|
stampPulledManifest(targetPath, {
|
|
71226
71264
|
app_id: app.id,
|
|
@@ -71232,7 +71270,8 @@ async function appPull(client, args) {
|
|
|
71232
71270
|
agents: app.agents ?? {}
|
|
71233
71271
|
});
|
|
71234
71272
|
for (const [alias, declaration] of Object.entries(app.queries ?? {})) {
|
|
71235
|
-
|
|
71273
|
+
const sha = declarationSha(declaration);
|
|
71274
|
+
writeSynced(targetPath, "queries", alias, { content: sha, live: sha });
|
|
71236
71275
|
}
|
|
71237
71276
|
await writeGeneratedAppFields(
|
|
71238
71277
|
client,
|
|
@@ -71242,7 +71281,10 @@ async function appPull(client, args) {
|
|
|
71242
71281
|
);
|
|
71243
71282
|
const workflows = app.workflows ?? {};
|
|
71244
71283
|
if (Object.keys(workflows).length > 0) {
|
|
71245
|
-
const written = await writeWorkflowFiles(client, targetPath, app.id, app.name, workflows
|
|
71284
|
+
const written = await writeWorkflowFiles(client, targetPath, app.id, app.name, workflows, {
|
|
71285
|
+
overwrite: args.force === true,
|
|
71286
|
+
kept: keptLocal
|
|
71287
|
+
});
|
|
71246
71288
|
if (written.length > 0) {
|
|
71247
71289
|
console.error(
|
|
71248
71290
|
`Wrote ${written.length} workflow ${written.length === 1 ? "body" : "bodies"} to ${WORKFLOWS_DIR}/ (${written.join(", ")})`
|
|
@@ -71251,10 +71293,15 @@ async function appPull(client, args) {
|
|
|
71251
71293
|
}
|
|
71252
71294
|
const agents = app.agents ?? {};
|
|
71253
71295
|
if (Object.keys(agents).length > 0) {
|
|
71254
|
-
const written = writeAgentFiles(targetPath, agents
|
|
71296
|
+
const written = writeAgentFiles(targetPath, agents, {
|
|
71297
|
+
overwrite: args.force === true,
|
|
71298
|
+
kept: keptLocal
|
|
71299
|
+
});
|
|
71255
71300
|
for (const alias of written) {
|
|
71256
71301
|
const instructions = agents[alias]?.instructions;
|
|
71257
|
-
if (instructions
|
|
71302
|
+
if (instructions === void 0) continue;
|
|
71303
|
+
const sha = contentSha(instructions);
|
|
71304
|
+
writeSynced(targetPath, "agents", alias, { content: sha, live: sha });
|
|
71258
71305
|
}
|
|
71259
71306
|
if (written.length > 0) {
|
|
71260
71307
|
console.error(
|
|
@@ -71264,6 +71311,7 @@ async function appPull(client, args) {
|
|
|
71264
71311
|
}
|
|
71265
71312
|
console.error(`Installing npm dependencies...`);
|
|
71266
71313
|
await runNpm(["install"], targetPath);
|
|
71314
|
+
reportKeptLocalFiles(keptLocal, app.id);
|
|
71267
71315
|
console.error(`
|
|
71268
71316
|
Ready. Next steps:`);
|
|
71269
71317
|
console.error(` cd ${path7.relative(process.cwd(), targetPath) || "."}`);
|
|
@@ -71289,35 +71337,33 @@ async function appDeploy(client, args) {
|
|
|
71289
71337
|
const meta3 = readAppMeta(projectDir);
|
|
71290
71338
|
warnAboutDevLink(projectDir, "deploy");
|
|
71291
71339
|
const liveApp = await client.getApp(meta3.app_id);
|
|
71292
|
-
const
|
|
71293
|
-
|
|
71294
|
-
|
|
71295
|
-
|
|
71296
|
-
|
|
71297
|
-
|
|
71298
|
-
|
|
71299
|
-
|
|
71300
|
-
types useAgentRun \u2014 so this build was checked against an agent that does not exist.
|
|
71301
|
-
To take the app's shape: lotics app codegen (refreshes the block + the types)
|
|
71302
|
-
To CHANGE the agent instead: lotics run set_app_agent '{"app_id":"${meta3.app_id}","alias":"\u2026","outputs":{\u2026}}'
|
|
71303
|
-
(send only what changes \u2014 the server merges), then codegen.`
|
|
71304
|
-
);
|
|
71305
|
-
process.exit(1);
|
|
71340
|
+
const pending = pendingBindings({
|
|
71341
|
+
projectDir,
|
|
71342
|
+
meta: meta3,
|
|
71343
|
+
synced: readSynced(projectDir).workflows,
|
|
71344
|
+
live: liveApp
|
|
71345
|
+
});
|
|
71346
|
+
if (!nothingPending(pending)) {
|
|
71347
|
+
await pushPendingBindings(client, projectDir, pending);
|
|
71306
71348
|
}
|
|
71307
|
-
const declarationDrift = reportWorkflowDeclarationDrift(meta3.workflows, liveApp.workflows);
|
|
71308
71349
|
const sourceText = readAppSourceText(projectDir);
|
|
71309
71350
|
const called = calledAppAliases(sourceText);
|
|
71310
|
-
const proseDrift = reportAgentProseDrift(projectDir, liveApp.agents);
|
|
71311
|
-
const bodyDrift = reportWorkflowBodyDrift(projectDir, meta3.workflows, readSynced(projectDir).workflows);
|
|
71312
|
-
const queryDrift = reportQueryDrift(liveApp, meta3.queries ?? {});
|
|
71313
|
-
if (bodyDrift || declarationDrift || proseDrift || queryDrift) {
|
|
71314
|
-
console.error(`
|
|
71315
|
-
Nothing was deployed.`);
|
|
71316
|
-
process.exit(1);
|
|
71317
|
-
}
|
|
71318
71351
|
warnIfDynamicAliases(called);
|
|
71319
71352
|
warnIfUndeclaredCapabilities(sourceText, meta3.capabilities);
|
|
71320
71353
|
writeAppDts(projectDir, { workflows: meta3.workflows, queries: meta3.queries, agents: meta3.agents });
|
|
71354
|
+
try {
|
|
71355
|
+
await writeGeneratedAppFields(
|
|
71356
|
+
client,
|
|
71357
|
+
projectDir,
|
|
71358
|
+
{ app_id: meta3.app_id, package_id: liveApp.package_id },
|
|
71359
|
+
meta3.queries ?? {}
|
|
71360
|
+
);
|
|
71361
|
+
} catch (err2) {
|
|
71362
|
+
console.error(
|
|
71363
|
+
`\u26A0 Could not regenerate .lotics/app_fields.ts (${err2.message}).
|
|
71364
|
+
The build will use whatever is on disk \u2014 run 'lotics app codegen' if this app is published.`
|
|
71365
|
+
);
|
|
71366
|
+
}
|
|
71321
71367
|
console.error("Building...");
|
|
71322
71368
|
await runNpm(["run", "build"], projectDir);
|
|
71323
71369
|
const distDir = path7.join(projectDir, "dist");
|
|
@@ -71354,7 +71400,7 @@ Refusing to deploy \u2014 package.json#lotics.agents disagrees with the live app
|
|
|
71354
71400
|
source_archive: fs6.readFileSync(tmpSource),
|
|
71355
71401
|
dist_archive: fs6.readFileSync(tmpDist),
|
|
71356
71402
|
prev_version_id: meta3.current_version_id,
|
|
71357
|
-
message: args.message,
|
|
71403
|
+
message: args.message ?? derivedDeployMessage(pending),
|
|
71358
71404
|
// Capabilities ARE manifest-authoritative: always send,
|
|
71359
71405
|
// defaulting to `{}` when the manifest declares none — so deleting the
|
|
71360
71406
|
// `capabilities` block turns every capability OFF on the next deploy
|
|
@@ -71379,13 +71425,24 @@ Refusing to deploy \u2014 package.json#lotics.agents disagrees with the live app
|
|
|
71379
71425
|
});
|
|
71380
71426
|
console.error(`Deployed v${result.version_number} (${result.version_id})`);
|
|
71381
71427
|
console.error(`Bundle size: ${(result.bundle_size_bytes / 1024).toFixed(1)} KB`);
|
|
71428
|
+
let liveAfter;
|
|
71382
71429
|
try {
|
|
71383
|
-
|
|
71384
|
-
warnIfUnbranded(app);
|
|
71385
|
-
warnIfUnboundAliases(app, called);
|
|
71430
|
+
liveAfter = await client.getApp(meta3.app_id);
|
|
71386
71431
|
} catch (err2) {
|
|
71387
71432
|
console.error(`(skipped post-deploy checks: ${err2.message})`);
|
|
71388
71433
|
}
|
|
71434
|
+
if (liveAfter) {
|
|
71435
|
+
warnIfUnbranded(liveAfter);
|
|
71436
|
+
warnIfUnboundAliases(liveAfter, called);
|
|
71437
|
+
}
|
|
71438
|
+
if (liveAfter) {
|
|
71439
|
+
await pruneOrphanedBindings(
|
|
71440
|
+
client,
|
|
71441
|
+
meta3.app_id,
|
|
71442
|
+
orphanedBindings(called, liveAfter),
|
|
71443
|
+
called.dynamic
|
|
71444
|
+
);
|
|
71445
|
+
}
|
|
71389
71446
|
} catch (err2) {
|
|
71390
71447
|
const e = err2;
|
|
71391
71448
|
if (e.code === "VERSION_CONFLICT") {
|
|
@@ -71425,78 +71482,233 @@ async function readLiveQueries(client, appId) {
|
|
|
71425
71482
|
process.exit(1);
|
|
71426
71483
|
}
|
|
71427
71484
|
}
|
|
71428
|
-
function reportQueryDrift(app, manifest) {
|
|
71429
|
-
const drifted = driftedQueryAliases(manifest, app.queries);
|
|
71430
|
-
if (drifted.length === 0) return false;
|
|
71431
|
-
console.error(
|
|
71432
|
-
`
|
|
71433
|
-
\u2717 ${drifted.length} quer${drifted.length === 1 ? "y" : "ies"} in package.json differ from the app: ${drifted.join(", ")}
|
|
71434
|
-
A deploy does not write them, so this version would not reproduce what runs.
|
|
71435
|
-
Push yours with 'lotics app query set --all' (or one alias at a time),
|
|
71436
|
-
or adopt the app's with 'lotics app pull'.`
|
|
71437
|
-
);
|
|
71438
|
-
return drifted.length > 0;
|
|
71439
|
-
}
|
|
71440
71485
|
async function appCheck(client, args = {}) {
|
|
71441
71486
|
const projectDir = path7.resolve(args.projectDir ?? process.cwd());
|
|
71442
71487
|
const meta3 = readAppMeta(projectDir);
|
|
71443
71488
|
const app = await client.getApp(meta3.app_id);
|
|
71444
71489
|
const sourceText = readAppSourceText(projectDir);
|
|
71445
71490
|
const called = calledAppAliases(sourceText);
|
|
71446
|
-
const divergences = agentTypeDivergences(meta3.agents, app.agents);
|
|
71447
|
-
for (const d of divergences) {
|
|
71448
|
-
console.error(`\u2717 package.json#lotics.agents.${d}`);
|
|
71449
|
-
}
|
|
71450
|
-
if (divergences.length > 0) {
|
|
71451
|
-
console.error(
|
|
71452
|
-
` A deploy REFUSES this: the bundle would be typed against an agent that does not exist.
|
|
71453
|
-
Take the app's shape with 'lotics app pull ${meta3.app_id}', or change the agent with set_app_agent.`
|
|
71454
|
-
);
|
|
71455
|
-
}
|
|
71456
71491
|
warnAboutDevLink(projectDir, "deploy");
|
|
71457
|
-
|
|
71458
|
-
const
|
|
71459
|
-
|
|
71460
|
-
|
|
71492
|
+
warnAboutOrphanedBindings(called, app);
|
|
71493
|
+
const pending = pendingBindings({
|
|
71494
|
+
projectDir,
|
|
71495
|
+
meta: meta3,
|
|
71496
|
+
synced: readSynced(projectDir).workflows,
|
|
71497
|
+
live: app
|
|
71498
|
+
});
|
|
71499
|
+
reportPending(pending);
|
|
71461
71500
|
warnIfDynamicAliases(called);
|
|
71462
71501
|
warnIfUndeclaredCapabilities(sourceText, meta3.capabilities);
|
|
71463
71502
|
warnIfUnboundAliases(app, called);
|
|
71464
71503
|
warnIfUnbranded(app);
|
|
71465
|
-
if (
|
|
71504
|
+
if (!nothingPending(pending)) {
|
|
71466
71505
|
process.exit(1);
|
|
71467
71506
|
}
|
|
71468
71507
|
console.error("Checked the app's bindings, capabilities and agent schemas \u2014 nothing blocking.");
|
|
71469
71508
|
}
|
|
71470
|
-
function
|
|
71471
|
-
|
|
71509
|
+
async function pushPendingBindings(client, projectDir, pending) {
|
|
71510
|
+
const plan = [
|
|
71511
|
+
...pending.queries.map((alias) => `query ${alias}`),
|
|
71512
|
+
...pending.workflows.map((alias) => `workflow ${alias}`),
|
|
71513
|
+
...pending.agents.map((alias) => `agent ${alias}`)
|
|
71514
|
+
];
|
|
71515
|
+
console.error(
|
|
71516
|
+
`
|
|
71517
|
+
Pushing ${plan.length} binding${plan.length === 1 ? "" : "s"} ahead of the app: ${plan.join(", ")}`
|
|
71518
|
+
);
|
|
71519
|
+
for (const alias of pending.queries) {
|
|
71520
|
+
await appQuerySet(client, { alias, projectDir });
|
|
71521
|
+
}
|
|
71522
|
+
for (const alias of pending.workflows) {
|
|
71523
|
+
await appWorkflowSet(client, { alias, projectDir });
|
|
71524
|
+
}
|
|
71525
|
+
for (const alias of pending.agents) {
|
|
71526
|
+
await appAgentSet(client, { alias, projectDir });
|
|
71527
|
+
}
|
|
71528
|
+
}
|
|
71529
|
+
function pendingBindings(args) {
|
|
71530
|
+
const { projectDir, meta: meta3, synced, live } = args;
|
|
71531
|
+
const workflows = /* @__PURE__ */ new Set([
|
|
71532
|
+
...workflowBodyDrift(projectDir, meta3.workflows, synced),
|
|
71533
|
+
...divergedWorkflowAliases(meta3.workflows, live.workflows)
|
|
71534
|
+
]);
|
|
71535
|
+
return {
|
|
71536
|
+
workflows: [...workflows].sort(),
|
|
71537
|
+
agents: [
|
|
71538
|
+
.../* @__PURE__ */ new Set([
|
|
71539
|
+
...driftedAgentAliases(projectDir, live.agents),
|
|
71540
|
+
// A manifest declaration that disagrees with the live row is a push now,
|
|
71541
|
+
// the same as a workflow's. `agentTypeDivergences` renders one line per
|
|
71542
|
+
// (alias, key) for a human, and the alias is its first token — but an
|
|
71543
|
+
// alias that diverged on both `inputs` and `outputs` still needs exactly
|
|
71544
|
+
// one push.
|
|
71545
|
+
...divergedAgentAliases(meta3.agents, live.agents)
|
|
71546
|
+
])
|
|
71547
|
+
].sort(),
|
|
71548
|
+
// Only aliases the manifest DECLARES. `driftedQueryAliases` is symmetric —
|
|
71549
|
+
// it unions both key sets — so an alias deleted locally but still bound live
|
|
71550
|
+
// reads as drift, and queueing it sends the author to a push that refuses
|
|
71551
|
+
// ("declare it first") for a declaration they deliberately removed. A
|
|
71552
|
+
// deletion is not a push, and this detector only decides what to push;
|
|
71553
|
+
// unbinding is `remove_app_query`, which no deploy runs for any artifact.
|
|
71554
|
+
queries: [...driftedQueryAliases(meta3.queries ?? {}, live.queries)].filter((alias) => (meta3.queries ?? {})[alias] !== void 0).sort()
|
|
71555
|
+
};
|
|
71556
|
+
}
|
|
71557
|
+
function reportPending(pending) {
|
|
71558
|
+
const lines = [
|
|
71559
|
+
...pending.workflows.map((a) => ` \u2022 workflow "${a}" \u2014 lotics app workflow set ${a}`),
|
|
71560
|
+
...pending.agents.map((a) => ` \u2022 agent "${a}" \u2014 lotics app agent set ${a}`),
|
|
71561
|
+
...pending.queries.map((a) => ` \u2022 query "${a}" \u2014 lotics app query set ${a}`)
|
|
71562
|
+
];
|
|
71563
|
+
if (lines.length === 0) return;
|
|
71564
|
+
console.error(
|
|
71565
|
+
`
|
|
71566
|
+
\u2717 ${lines.length} binding${lines.length === 1 ? " is" : "s are"} ahead of the app:
|
|
71567
|
+
` + lines.join("\n") + `
|
|
71568
|
+
|
|
71569
|
+
A version shipped now would not reproduce what runs. 'lotics app deploy' pushes
|
|
71570
|
+
these for you; revert the file instead to drop the edit.`
|
|
71571
|
+
);
|
|
71572
|
+
}
|
|
71573
|
+
function nothingPending(pending) {
|
|
71574
|
+
return pending.workflows.length === 0 && pending.agents.length === 0 && pending.queries.length === 0;
|
|
71575
|
+
}
|
|
71576
|
+
function divergedAgentAliases(manifestAgents, liveAgents) {
|
|
71577
|
+
const out = [];
|
|
71578
|
+
for (const [alias, declared] of Object.entries(manifestAgents ?? {})) {
|
|
71579
|
+
const live = (liveAgents ?? {})[alias];
|
|
71580
|
+
if (!live) continue;
|
|
71581
|
+
if (AGENT_TYPED_KEYS.some((key) => !sameSchema(declared[key], live[key]))) out.push(alias);
|
|
71582
|
+
}
|
|
71583
|
+
return out;
|
|
71584
|
+
}
|
|
71585
|
+
function driftedAgentAliases(projectDir, liveAgents) {
|
|
71586
|
+
const drifted = [];
|
|
71472
71587
|
for (const [alias, live] of Object.entries(liveAgents ?? {})) {
|
|
71473
71588
|
const file2 = agentFilePath2(projectDir, alias);
|
|
71474
71589
|
if (!fs6.existsSync(file2)) continue;
|
|
71475
71590
|
const local = stripAgentHeader(fs6.readFileSync(file2, "utf-8"));
|
|
71476
71591
|
if (local === "" || local === (live.instructions ?? "")) continue;
|
|
71477
|
-
drifted
|
|
71592
|
+
drifted.push(alias);
|
|
71593
|
+
}
|
|
71594
|
+
return drifted.sort();
|
|
71595
|
+
}
|
|
71596
|
+
function divergedWorkflowAliases(manifestWorkflows, liveWorkflows) {
|
|
71597
|
+
return [
|
|
71598
|
+
...new Set(workflowTypeDivergences(manifestWorkflows, liveWorkflows).map((d) => d.alias))
|
|
71599
|
+
].sort();
|
|
71600
|
+
}
|
|
71601
|
+
function orphanedBindings(called, live) {
|
|
71602
|
+
const agentDeclarations = Object.values(live.agents ?? {});
|
|
71603
|
+
const referenced = {
|
|
71604
|
+
queries: [...called.queries, ...agentDeclarations.flatMap((a) => a.query_aliases ?? [])],
|
|
71605
|
+
workflows: [...called.workflows, ...agentDeclarations.flatMap((a) => a.workflow_aliases ?? [])],
|
|
71606
|
+
// Nothing declares an agent but the client bundle — an agent cannot run another.
|
|
71607
|
+
agents: called.agents
|
|
71608
|
+
};
|
|
71609
|
+
return orphanedAliases(
|
|
71610
|
+
{
|
|
71611
|
+
queries: Object.keys(live.queries ?? {}),
|
|
71612
|
+
workflows: Object.keys(live.workflows ?? {}),
|
|
71613
|
+
agents: Object.keys(live.agents ?? {})
|
|
71614
|
+
},
|
|
71615
|
+
referenced
|
|
71616
|
+
);
|
|
71617
|
+
}
|
|
71618
|
+
function warnAboutOrphanedBindings(called, live) {
|
|
71619
|
+
const orphans = orphanedBindings(called, live);
|
|
71620
|
+
const lines = [
|
|
71621
|
+
...orphans.queries.map((a) => ` \u2022 query ${a}`),
|
|
71622
|
+
...orphans.workflows.map((a) => ` \u2022 workflow ${a}`),
|
|
71623
|
+
...orphans.agents.map((a) => ` \u2022 agent ${a}`)
|
|
71624
|
+
];
|
|
71625
|
+
if (lines.length === 0) return 0;
|
|
71626
|
+
console.error(
|
|
71627
|
+
`
|
|
71628
|
+
\u26A0 ${lines.length} binding(s) are live but named nowhere in this bundle:
|
|
71629
|
+
` + lines.join("\n") + (called.dynamic.length > 0 ? `
|
|
71630
|
+
|
|
71631
|
+
This app computes an alias at runtime, so one of these may be called after
|
|
71632
|
+
all \u2014 a deploy therefore leaves them ALL in place. Make those call sites
|
|
71633
|
+
literal and a deploy will clean them up.` : `
|
|
71634
|
+
|
|
71635
|
+
A deploy unbinds these after it ships.`)
|
|
71636
|
+
);
|
|
71637
|
+
return lines.length;
|
|
71638
|
+
}
|
|
71639
|
+
async function pruneOrphanedBindings(client, appId, orphans, dynamic) {
|
|
71640
|
+
const targets = [
|
|
71641
|
+
...orphans.queries.map((alias) => ({ tool: "remove_app_query", alias, label: `query ${alias}` })),
|
|
71642
|
+
...orphans.workflows.map((alias) => ({
|
|
71643
|
+
tool: "remove_app_workflow",
|
|
71644
|
+
alias,
|
|
71645
|
+
label: `workflow ${alias}`
|
|
71646
|
+
})),
|
|
71647
|
+
...orphans.agents.map((alias) => ({ tool: "remove_app_agent", alias, label: `agent ${alias}` }))
|
|
71648
|
+
];
|
|
71649
|
+
if (targets.length === 0) return 0;
|
|
71650
|
+
if (dynamic.length > 0) {
|
|
71478
71651
|
console.error(
|
|
71479
71652
|
`
|
|
71480
|
-
\
|
|
71481
|
-
|
|
71482
|
-
|
|
71653
|
+
\u26A0 Left ${targets.length} unused binding(s) in place: this app calls ${dynamic.join(" / ")} with an alias it computes at runtime, and a static scan cannot tell which binding that reaches \u2014 so any of these may be live:
|
|
71654
|
+
` + targets.map((t) => ` \u2022 ${t.label}`).join("\n") + `
|
|
71655
|
+
Make those call sites literal and the next deploy will clean them up, or remove the ones you know are dead by hand.`
|
|
71483
71656
|
);
|
|
71657
|
+
return targets.length;
|
|
71484
71658
|
}
|
|
71485
|
-
|
|
71659
|
+
let failed = 0;
|
|
71660
|
+
for (const target of targets) {
|
|
71661
|
+
const res = await client.execute(target.tool, { app_id: appId, alias: target.alias });
|
|
71662
|
+
if (res.error) {
|
|
71663
|
+
failed += 1;
|
|
71664
|
+
console.error(` \u2717 could not unbind ${target.label} \u2014 ${res.error}`);
|
|
71665
|
+
continue;
|
|
71666
|
+
}
|
|
71667
|
+
console.error(` \u2713 unbound ${target.label}`);
|
|
71668
|
+
}
|
|
71669
|
+
return failed;
|
|
71670
|
+
}
|
|
71671
|
+
function derivedDeployMessage(pending) {
|
|
71672
|
+
const parts = [
|
|
71673
|
+
...pending.queries.map((a) => `query ${a}`),
|
|
71674
|
+
...pending.workflows.map((a) => `workflow ${a}`),
|
|
71675
|
+
...pending.agents.map((a) => `agent ${a}`)
|
|
71676
|
+
];
|
|
71677
|
+
return parts.length === 0 ? "Deployed app code" : `Deployed app code; pushed ${parts.join(", ")}`;
|
|
71678
|
+
}
|
|
71679
|
+
function copyTree(from, to, opts, relative = "") {
|
|
71680
|
+
const kept = [];
|
|
71681
|
+
for (const entry of fs6.readdirSync(from, { withFileTypes: true })) {
|
|
71682
|
+
const src = path7.join(from, entry.name);
|
|
71683
|
+
const dest = path7.join(to, entry.name);
|
|
71684
|
+
const rel = path7.join(relative, entry.name);
|
|
71685
|
+
if (entry.isDirectory()) {
|
|
71686
|
+
fs6.mkdirSync(dest, { recursive: true });
|
|
71687
|
+
kept.push(...copyTree(src, dest, opts, rel));
|
|
71688
|
+
continue;
|
|
71689
|
+
}
|
|
71690
|
+
if (!opts.overwrite && fs6.existsSync(dest)) {
|
|
71691
|
+
if (fs6.readFileSync(src).equals(fs6.readFileSync(dest))) continue;
|
|
71692
|
+
kept.push(rel);
|
|
71693
|
+
continue;
|
|
71694
|
+
}
|
|
71695
|
+
fs6.copyFileSync(src, dest);
|
|
71696
|
+
}
|
|
71697
|
+
return kept;
|
|
71486
71698
|
}
|
|
71487
|
-
function
|
|
71488
|
-
|
|
71489
|
-
if (divergences.length === 0) return false;
|
|
71699
|
+
function reportKeptLocalFiles(kept, appId) {
|
|
71700
|
+
if (kept.length === 0) return;
|
|
71490
71701
|
console.error(
|
|
71491
71702
|
`
|
|
71492
|
-
|
|
71493
|
-
` +
|
|
71494
|
-
|
|
71495
|
-
|
|
71496
|
-
|
|
71497
|
-
|
|
71703
|
+
${kept.length} local file(s) differ from the app and were KEPT, not overwritten:
|
|
71704
|
+
` + kept.slice(0, 20).map((f) => ` \u2022 ${f}`).join("\n") + (kept.length > 20 ? `
|
|
71705
|
+
\u2026 and ${kept.length - 20} more` : ``) + `
|
|
71706
|
+
|
|
71707
|
+
Everything else was refreshed from the app. Your edits are intact.
|
|
71708
|
+
Ship them: lotics app deploy (pushes bodies, prompts and declarations)
|
|
71709
|
+
See the difference: lotics app check
|
|
71710
|
+
Take the app's copy: lotics app pull ${appId} --force (DISCARDS the files above)`
|
|
71498
71711
|
);
|
|
71499
|
-
return true;
|
|
71500
71712
|
}
|
|
71501
71713
|
function warnIfDynamicAliases(called) {
|
|
71502
71714
|
if (called.dynamic.length === 0) return;
|
|
@@ -71760,7 +71972,7 @@ Agent "${args.alias}" run ${run.id} \u2192 ${run.status}${run.error_message ? `:
|
|
|
71760
71972
|
if (run.status !== "completed") process.exit(1);
|
|
71761
71973
|
}
|
|
71762
71974
|
async function appAgentSet(client, args) {
|
|
71763
|
-
const projectDir = process.cwd();
|
|
71975
|
+
const projectDir = args.projectDir ?? process.cwd();
|
|
71764
71976
|
const meta3 = readAppMeta(projectDir);
|
|
71765
71977
|
noteScope({ alias: args.alias });
|
|
71766
71978
|
const app = await client.getApp(meta3.app_id);
|
|
@@ -71771,37 +71983,51 @@ async function appAgentSet(client, args) {
|
|
|
71771
71983
|
);
|
|
71772
71984
|
process.exit(1);
|
|
71773
71985
|
}
|
|
71986
|
+
const declared = meta3.agents?.[args.alias];
|
|
71987
|
+
const hasDeclaration = declared?.inputs !== void 0 || declared?.outputs !== void 0;
|
|
71774
71988
|
const file2 = agentFilePath2(projectDir, args.alias);
|
|
71775
|
-
if (!fs6.existsSync(file2)) {
|
|
71989
|
+
if (!fs6.existsSync(file2) && !hasDeclaration) {
|
|
71776
71990
|
console.error(
|
|
71777
|
-
`No instructions at ${path7.relative(projectDir, file2)}
|
|
71991
|
+
`No instructions at ${path7.relative(projectDir, file2)}, and nothing declared in package.json#lotics.agents.${args.alias} to push instead.
|
|
71992
|
+
Run 'lotics app pull ${meta3.app_id}' to write ${AGENTS_DIR}/${args.alias}.md, then edit it.`
|
|
71778
71993
|
);
|
|
71779
71994
|
process.exit(1);
|
|
71780
71995
|
}
|
|
71781
|
-
const instructions = stripAgentHeader(fs6.readFileSync(file2, "utf-8"));
|
|
71996
|
+
const instructions = fs6.existsSync(file2) ? stripAgentHeader(fs6.readFileSync(file2, "utf-8")) : void 0;
|
|
71782
71997
|
if (instructions === "") {
|
|
71783
71998
|
console.error(
|
|
71784
71999
|
`${path7.relative(projectDir, file2)} is empty after stripping the header \u2014 refusing to push an empty prompt.`
|
|
71785
72000
|
);
|
|
71786
72001
|
process.exit(1);
|
|
71787
72002
|
}
|
|
72003
|
+
const proseBaseline = readSynced(projectDir).agents[args.alias]?.live;
|
|
71788
72004
|
const res = await client.setAppAgent(meta3.app_id, args.alias, {
|
|
71789
|
-
|
|
71790
|
-
//
|
|
71791
|
-
//
|
|
71792
|
-
...
|
|
72005
|
+
// Prose and its precondition ride together. Gating a declaration-only push on
|
|
72006
|
+
// the prose baseline would refuse it because someone edited a PROMPT — a
|
|
72007
|
+
// precondition must guard what this call actually writes.
|
|
72008
|
+
...instructions !== void 0 ? { instructions, ...proseBaseline ? { expected_instructions_sha: proseBaseline } : {} } : {},
|
|
72009
|
+
...declared?.inputs !== void 0 ? { inputs: declared.inputs } : {},
|
|
72010
|
+
...declared?.outputs !== void 0 ? { outputs: declared.outputs } : {}
|
|
71793
72011
|
});
|
|
71794
72012
|
if (res.error) {
|
|
71795
72013
|
console.error(`Failed to set agent "${args.alias}": ${res.error}`);
|
|
71796
72014
|
process.exit(1);
|
|
71797
72015
|
}
|
|
71798
|
-
|
|
72016
|
+
if (instructions !== void 0) {
|
|
72017
|
+
const proseSha = contentSha(instructions);
|
|
72018
|
+
writeSynced(projectDir, "agents", args.alias, { content: proseSha, live: proseSha });
|
|
72019
|
+
}
|
|
72020
|
+
const pushed = [
|
|
72021
|
+
...instructions !== void 0 ? [`${instructions.length} chars of instructions`] : [],
|
|
72022
|
+
...declared?.inputs !== void 0 ? ["inputs"] : [],
|
|
72023
|
+
...declared?.outputs !== void 0 ? ["outputs"] : []
|
|
72024
|
+
];
|
|
71799
72025
|
console.error(
|
|
71800
|
-
`Set agent "${args.alias}" (${
|
|
72026
|
+
`Set agent "${args.alias}" (${pushed.join(", ")}` + (live.model_tier ? `, ${live.model_tier}` : "") + `). Everything else left as it is.`
|
|
71801
72027
|
);
|
|
71802
72028
|
}
|
|
71803
72029
|
async function appWorkflowSet(client, args) {
|
|
71804
|
-
const projectDir = process.cwd();
|
|
72030
|
+
const projectDir = args.projectDir ?? process.cwd();
|
|
71805
72031
|
const meta3 = readAppMeta(projectDir);
|
|
71806
72032
|
const declaration = meta3.workflows?.[args.alias];
|
|
71807
72033
|
if (!declaration) {
|
|
@@ -71813,7 +72039,9 @@ async function appWorkflowSet(client, args) {
|
|
|
71813
72039
|
const file2 = workflowFilePath(projectDir, args.alias);
|
|
71814
72040
|
if (!fs6.existsSync(file2)) {
|
|
71815
72041
|
console.error(
|
|
71816
|
-
`No workflow body at ${path7.relative(projectDir, file2)}.
|
|
72042
|
+
`No workflow body at ${path7.relative(projectDir, file2)}.
|
|
72043
|
+
If "${args.alias}" is already bound: lotics app pull ${meta3.app_id} (writes the body)
|
|
72044
|
+
If it is a NEW alias you declared: create that file yourself, then re-run this.`
|
|
71817
72045
|
);
|
|
71818
72046
|
process.exit(1);
|
|
71819
72047
|
}
|
|
@@ -71838,13 +72066,17 @@ async function appWorkflowSet(client, args) {
|
|
|
71838
72066
|
// The body this edit was built on. Turns the push into a conditional write:
|
|
71839
72067
|
// if the live body moved since this project last saw it, the server refuses
|
|
71840
72068
|
// rather than letting this copy overwrite a change nobody here has seen.
|
|
71841
|
-
expected_body_sha: readSynced(projectDir).workflows
|
|
72069
|
+
expected_body_sha: readSynced(projectDir).workflows[args.alias]?.live
|
|
71842
72070
|
});
|
|
71843
72071
|
if (res.error) {
|
|
71844
72072
|
console.error(`Failed to set workflow "${args.alias}": ${res.error}`);
|
|
71845
72073
|
process.exit(1);
|
|
71846
72074
|
}
|
|
71847
|
-
|
|
72075
|
+
const pushedBodySha = res.result !== null && typeof res.result === "object" ? res.result.body_sha : void 0;
|
|
72076
|
+
writeSynced(projectDir, "workflows", args.alias, {
|
|
72077
|
+
content: contentSha(source),
|
|
72078
|
+
...typeof pushedBodySha === "string" ? { live: pushedBodySha } : {}
|
|
72079
|
+
});
|
|
71848
72080
|
const result = res.result ?? {};
|
|
71849
72081
|
const workflowId = typeof result.workflow_id === "string" ? result.workflow_id : "(unknown)";
|
|
71850
72082
|
console.error(`Set workflow "${args.alias}" \u2192 ${workflowId}`);
|
|
@@ -71864,7 +72096,7 @@ async function appWorkflowSet(client, args) {
|
|
|
71864
72096
|
}
|
|
71865
72097
|
}
|
|
71866
72098
|
async function appQuerySet(client, args) {
|
|
71867
|
-
const projectDir = process.cwd();
|
|
72099
|
+
const projectDir = args.projectDir ?? process.cwd();
|
|
71868
72100
|
const meta3 = readAppMeta(projectDir);
|
|
71869
72101
|
const declared = meta3.queries ?? {};
|
|
71870
72102
|
const aliases = "all" in args ? Object.keys(declared).sort() : [args.alias];
|
|
@@ -71886,21 +72118,22 @@ async function appQuerySet(client, args) {
|
|
|
71886
72118
|
meta3.app_id,
|
|
71887
72119
|
alias,
|
|
71888
72120
|
declaration,
|
|
71889
|
-
readSynced(projectDir).queries
|
|
72121
|
+
readSynced(projectDir).queries[alias]?.live
|
|
71890
72122
|
);
|
|
71891
72123
|
if (res.error) {
|
|
71892
72124
|
if (pushed.length > 0) console.error(`Pushed before the failure: ${pushed.join(", ")}.`);
|
|
71893
72125
|
console.error(`Failed to set query "${alias}": ${res.error}`);
|
|
71894
72126
|
process.exit(1);
|
|
71895
72127
|
}
|
|
71896
|
-
|
|
72128
|
+
const querySha = declarationSha(declaration);
|
|
72129
|
+
writeSynced(projectDir, "queries", alias, { content: querySha, live: querySha });
|
|
71897
72130
|
pushed.push(alias);
|
|
71898
72131
|
}
|
|
71899
72132
|
console.error(
|
|
71900
72133
|
`Set ${pushed.length} quer${pushed.length === 1 ? "y" : "ies"} on ${meta3.app_id}: ${pushed.join(", ")}.`
|
|
71901
72134
|
);
|
|
71902
72135
|
}
|
|
71903
|
-
async function appWorkflowPull(client) {
|
|
72136
|
+
async function appWorkflowPull(client, args = {}) {
|
|
71904
72137
|
const projectDir = process.cwd();
|
|
71905
72138
|
const meta3 = readAppMeta(projectDir);
|
|
71906
72139
|
const app = await client.getApp(meta3.app_id);
|
|
@@ -71909,10 +72142,15 @@ async function appWorkflowPull(client) {
|
|
|
71909
72142
|
console.error(`App ${meta3.app_id} has no bound workflows.`);
|
|
71910
72143
|
return;
|
|
71911
72144
|
}
|
|
71912
|
-
const
|
|
72145
|
+
const kept = [];
|
|
72146
|
+
const written = await writeWorkflowFiles(client, projectDir, meta3.app_id, app.name, workflows, {
|
|
72147
|
+
overwrite: args.force === true,
|
|
72148
|
+
kept
|
|
72149
|
+
});
|
|
71913
72150
|
console.error(
|
|
71914
72151
|
`Wrote ${written.length} workflow ${written.length === 1 ? "body" : "bodies"} to ${WORKFLOWS_DIR}/` + (written.length > 0 ? ` (${written.join(", ")})` : "")
|
|
71915
72152
|
);
|
|
72153
|
+
reportKeptLocalFiles(kept, meta3.app_id);
|
|
71916
72154
|
ensureAppTsconfig(projectDir);
|
|
71917
72155
|
}
|
|
71918
72156
|
async function appWorkflowCheck(args) {
|
|
@@ -71995,6 +72233,7 @@ ${totalErrors} error${totalErrors === 1 ? "" : "s"} across ${results.length - pa
|
|
|
71995
72233
|
function parseArgs(argv) {
|
|
71996
72234
|
const flags = {
|
|
71997
72235
|
json: false,
|
|
72236
|
+
force: false,
|
|
71998
72237
|
timeout: void 0,
|
|
71999
72238
|
output: void 0,
|
|
72000
72239
|
as: void 0,
|
|
@@ -72027,6 +72266,9 @@ function parseArgs(argv) {
|
|
|
72027
72266
|
case "--json":
|
|
72028
72267
|
flags.json = true;
|
|
72029
72268
|
break;
|
|
72269
|
+
case "--force":
|
|
72270
|
+
flags.force = true;
|
|
72271
|
+
break;
|
|
72030
72272
|
case "--timeout":
|
|
72031
72273
|
flags.timeout = parseInt(argv[++i2], 10);
|
|
72032
72274
|
break;
|
|
@@ -100725,17 +100967,11 @@ Available workspaces:`);
|
|
|
100725
100967
|
process.exit(1);
|
|
100726
100968
|
}
|
|
100727
100969
|
const targetPath = restArgs[0];
|
|
100728
|
-
await appPull(client, { app_id: appId, targetPath });
|
|
100970
|
+
await appPull(client, { app_id: appId, targetPath, force: flags.force === true });
|
|
100729
100971
|
return;
|
|
100730
100972
|
}
|
|
100731
100973
|
if (subcommand === "deploy") {
|
|
100732
|
-
|
|
100733
|
-
if (!message2) {
|
|
100734
|
-
console.error('Usage: lotics app deploy -m "<what changed + why>"');
|
|
100735
|
-
console.error("A deploy message is required \u2014 it's the version's audit trail (see `lotics app versions`).");
|
|
100736
|
-
process.exit(1);
|
|
100737
|
-
}
|
|
100738
|
-
await appDeploy(client, { message: message2 });
|
|
100974
|
+
await appDeploy(client, { message: (flags.message ?? toolArgs)?.trim() || void 0 });
|
|
100739
100975
|
return;
|
|
100740
100976
|
}
|
|
100741
100977
|
if (subcommand === "subdomain") {
|
|
@@ -100816,7 +101052,7 @@ Available workspaces:`);
|
|
|
100816
101052
|
return;
|
|
100817
101053
|
}
|
|
100818
101054
|
if (action === "pull") {
|
|
100819
|
-
await appWorkflowPull(client);
|
|
101055
|
+
await appWorkflowPull(client, { force: flags.force === true });
|
|
100820
101056
|
return;
|
|
100821
101057
|
}
|
|
100822
101058
|
workflowUsage();
|
package/dist/src/client.d.ts
CHANGED
|
@@ -244,6 +244,14 @@ export declare class LoticsClient {
|
|
|
244
244
|
model_tier?: string;
|
|
245
245
|
inputs?: Record<string, unknown>;
|
|
246
246
|
outputs?: Record<string, unknown>;
|
|
247
|
+
/**
|
|
248
|
+
* The app's own queries/workflows this agent may call through
|
|
249
|
+
* `run_app_query` / `run_app_workflow`. These are REFERENCES that never
|
|
250
|
+
* appear in the client bundle, so anything asking "is this alias still
|
|
251
|
+
* used" must read them or it will answer no for an agent-driven app.
|
|
252
|
+
*/
|
|
253
|
+
query_aliases?: string[];
|
|
254
|
+
workflow_aliases?: string[];
|
|
247
255
|
}> | null;
|
|
248
256
|
/**
|
|
249
257
|
* Installation-level customization config values (the `useConfig()`
|
package/docs/cli_reference.md
CHANGED
|
@@ -29,11 +29,11 @@ Per-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. S
|
|
|
29
29
|
| `lotics knowledge update <id> [--from <file.md> \| --content <str>] [--name <n>] [--description <d>]` | Call `update_knowledge` with **only** the provided fields (a body from --from/--content becomes `content`; the tool diffs + CASes the content change internally, so the CLI passes no `expected_content_file_id`). At least one field required; --from and --content are mutually exclusive. |
|
|
30
30
|
| `lotics knowledge rm <id>` | Archive the doc via `delete_knowledge` (`{ knowledge_doc_id }`). The REST execute path does not gate `needsApproval`, so this runs unattended. |
|
|
31
31
|
| `lotics app create <name> [path]` | Scaffold a Vite+React+TS custom-code app project; POST /v1/apps; npm install; vite build; upload as v1 |
|
|
32
|
-
| `lotics app pull <app_id> [path]` | Download source archive from R2 (presigned), extract, npm install, stamp package.json's `lotics` field. With no `[path]`: refresh the cwd IN PLACE when it's already this app's own project (its manifest `app_id` matches — the documented `cd <app> && lotics app pull` flow), else clone into an `<name>/` subdir; this avoids the stray nested `./<name>/` subdir a pull-from-inside-the-app used to drop. — `workflows` and `agents` are sourced from the live App row (NOT the archived manifest), so `set_app_workflow` / `set_app_agent` authoring survives the pull. Regenerates `.lotics/app_{workflows,queries,agents}.d.ts` so `useWorkflow` / `useQuery` / `useAgentRun` stay typed, AND the runtime `.lotics/app_fields.ts` (the same linked-vs-bespoke branch `app codegen` runs, off the app row already fetched — see that row for the two forms). That one is not optional: `app deploy` tars source with `--exclude=.lotics`, so no archive can carry it, and a pulled project whose `src/` imports `F`/`OPT` would fail to build with `Could not resolve "../../.lotics/app_fields"` until `app codegen` was run by hand. The write NAMES the form and the reason, because an in-place pull can FLIP a project between them (`opctl app publish` links an origin, `package eject` unlinks it) and that changes what the module does at load. Skipped under `--view-as` (the schema is read as that member and silently drops tables they cannot see — a narrowed `F` map compiles and then throws at runtime, worse than the missing module). A binding/schema fetch failure is non-fatal and names the right recovery for what is on disk: an existing file is kept, an ABSENT one warns about the build error and points at `app codegen`. Pull GENERATES but never RECONCILES `.lotics/` — deleting a companion whose alias the manifest no longer declares is `app codegen`'s alone, since pull's authority is the server's alias set and a declared-but-not-yet-`set` alias is supported. Also writes one `src/workflows/<alias>.ts` per bound workflow (faithful body from `get_app_workflow`) and one `src/agents/<alias>.md` per bound agent (its instructions, straight off the live row) — so the prose an author actually edits lives in a file, and pull always overwrites it from live, leaving no second copy to drift. A legacy workflow alias with no rendered source, or an agent with no instructions, warns and is skipped. The stamped `lotics.agents` map carries the TYPED half only (`inputs`/`outputs`/`tool_names`/`model_tier`/…) — an agent's prose lives solely in its `.md`, so there is never a second local copy to desync; a stale `instructions` left by an older CLI is inert and disappears on the next pull |
|
|
33
|
-
| `lotics app deploy -m <message>` |
|
|
32
|
+
| `lotics app pull <app_id> [path]` | Download source archive from R2 (presigned), extract, npm install, stamp package.json's `lotics` field. With no `[path]`: refresh the cwd IN PLACE when it's already this app's own project (its manifest `app_id` matches — the documented `cd <app> && lotics app pull` flow), else clone into an `<name>/` subdir; this avoids the stray nested `./<name>/` subdir a pull-from-inside-the-app used to drop. **A pull never overwrites a file that differs from what it is about to write** — it writes only what is ABSENT or already identical, keeps the rest, and reports which files it kept plus the commands that close the gap. An in-place refresh used to replace uncommitted work silently, since `tar -xzf` overwrites unconditionally and the command still exits 0. The same rule covers the two kinds the archive does NOT carry — `src/workflows/<alias>.ts` and `src/agents/<alias>.md`, both rewritten from the live row on every pull — so an unpushed body or prompt survives too. The comparison is against the app's own content, not git, so it holds for a project that was never a repo. `--force` takes the app's copy and DISCARDS local edits; there is no other way to lose them. — `workflows` and `agents` are sourced from the live App row (NOT the archived manifest), so `set_app_workflow` / `set_app_agent` authoring survives the pull. Regenerates `.lotics/app_{workflows,queries,agents}.d.ts` so `useWorkflow` / `useQuery` / `useAgentRun` stay typed, AND the runtime `.lotics/app_fields.ts` (the same linked-vs-bespoke branch `app codegen` runs, off the app row already fetched — see that row for the two forms). That one is not optional: `app deploy` tars source with `--exclude=.lotics`, so no archive can carry it, and a pulled project whose `src/` imports `F`/`OPT` would fail to build with `Could not resolve "../../.lotics/app_fields"` until `app codegen` was run by hand. The write NAMES the form and the reason, because an in-place pull can FLIP a project between them (`opctl app publish` links an origin, `package eject` unlinks it) and that changes what the module does at load. Skipped under `--view-as` (the schema is read as that member and silently drops tables they cannot see — a narrowed `F` map compiles and then throws at runtime, worse than the missing module). A binding/schema fetch failure is non-fatal and names the right recovery for what is on disk: an existing file is kept, an ABSENT one warns about the build error and points at `app codegen`. Pull GENERATES but never RECONCILES `.lotics/` — deleting a companion whose alias the manifest no longer declares is `app codegen`'s alone, since pull's authority is the server's alias set and a declared-but-not-yet-`set` alias is supported. Also writes one `src/workflows/<alias>.ts` per bound workflow (faithful body from `get_app_workflow`) and one `src/agents/<alias>.md` per bound agent (its instructions, straight off the live row) — so the prose an author actually edits lives in a file, and pull always overwrites it from live, leaving no second copy to drift. A legacy workflow alias with no rendered source, or an agent with no instructions, warns and is skipped. The stamped `lotics.agents` map carries the TYPED half only (`inputs`/`outputs`/`tool_names`/`model_tier`/…) — an agent's prose lives solely in its `.md`, so there is never a second local copy to desync; a stale `instructions` left by an older CLI is inert and disappears on the next pull |
|
|
33
|
+
| `lotics app deploy -m <message>` | `-m` is OPTIONAL — omitted, the deploy derives the version message from what it actually pushed. Requiring it was the most common real failure in CLI telemetry, and a hard stop yields a retry plus filler rather than an audit trail; pass `-m` when you have a reason worth recording. npm run build; tar source + dist; POST /v1/apps/{id}/versions multipart. **One command ships everything**: before the bundle moves, a deploy pushes every binding the project has ahead of the app — an edited workflow body or declaration, edited agent prose, a changed query — through `set_app_query`, then `set_app_workflow`, then `set_app_agent`, and fails the release if any push is refused. That order is required: an agent declares the query and workflow aliases it may call, so pushing it before its own new query is refused. It never AUTHORS a binding itself — those verbs stay the single writers — and each push carries the fingerprint the project last saw live (`lotics.synced`), so a stale checkout is refused rather than overwriting another author's edit. `package.json` means the same thing for both artifacts: editing `lotics.agents.<alias>.inputs`/`outputs` is pushed exactly like the workflow equivalent (only those two fields — `set_app_agent` merges, so everything the manifest does not model is left untouched). It also regenerates `.lotics/app_fields.ts` before building, since the build INLINES it and which form is correct follows from whether the app is a package installation — a deploy that skipped it could ship an origin's baked ids into every other install. `lotics app check` reports the same set without pushing; neither has a `--strict`. Deploy also sends the manifest's `lotics.workflows` alias KEYS as `workflow_aliases`, recorded on the version row so `remove_app_workflow` can refuse to unbind an alias the served version still declares. After a successful deploy it warns about any alias the source CALLS that is NOT bound, and **unbinds the inverse** — bindings the app still serves that this bundle names nowhere. No flag: an orphan is a live, callable read path under the deployer's authority, and a deploy that adds bindings automatically but requires a decision to remove one just accumulates them. Unbinding runs AFTER the version is live, because the removal tools refuse an alias the SERVED version still declares — so doing it first is refused by the guard that makes it safe. It is skipped ENTIRELY (with a warning, never a failure) when the source computes an alias at run time, since the scan cannot tell which binding that reaches; that is the only case where the reference set is incomplete, because a binding is reachable from the bundle and from an agent's `query_aliases`/`workflow_aliases` and from nothing else — an app workflow carries no `on({...})` trigger, so no table event or schedule reaches one. A binding that will not unbind is reported and does NOT fail the release: the version is live and correct, and the leftover is the state every deploy left behind before this existed. |
|
|
34
34
|
| `lotics app versions [app_id]` | `GET /v1/apps/{id}/versions` — print deploy history newest-first (version number, timestamp, deployer name, build status, the `-m` message; `*` marks the currently-served version). app_id from the local manifest, or pass one to inspect any app without pulling it. Admin-only server-side (mirrors deploy + source download). Answers "what shipped, when, by whom" — e.g. whether a fix was live at an incident's time. The deploy pipeline already persisted all of this in `app_versions`; this is the read surface. Title → stderr, table → stdout (pipeable). |
|
|
35
35
|
| `lotics app codegen [path]` | Regenerate `.lotics/*` from the manifest + workspace schema **without a deploy**. The three `.d.ts` companions (`app_{workflows,queries,agents}.d.ts`) are always rewritten (synchronous, no network). When credentials resolve, also rewrites the **runtime** `.lotics/app_fields.ts` — **branched on whether the app is a package installation** (`getApp().package_id` set, from `generate_package_fields.ts`): a **linked/published** app emits the BINDING form (`F`/`OPT`/`ROLE` resolved from the installation's LIVE binding — via `appBinding` / the `binding` RPC — at module load through `getAppBinding()` + top-level await, so the source stays portable across every install); a **bespoke** app emits the BAKED form (`generate_app_fields.ts`) — a real `.ts` exporting `F` (table→field→`"fld_…"`) + `OPT` (table→select-field→option→`"opt_…"`) keyed by display-name aliases, for the tables the app's queries reference (+ optional `package.json#lotics.codegen.tables` allowlist). Both forms share the `F`/`OPT` shape (contract aliases derive from the same slugified display names), so a published origin's deployed source compiles unchanged. Writing the BINDING form also heals the project's vitest setup (`ensureAppVitestSetup`, folded into the same write boundary): the binding form awaits `getAppBinding()` (a network call) at module load, so without a stub `npm test` fails to collect any test that imports the app graph — the heal writes `vitest.setup.ts` (mocks only `getAppBinding`, returning an echo binding: any alias → a self-identifying `fld:test:…`/`opt:test:…`/`grp:test:…` id) if absent, and warns the one-liner to add to `vite.config.ts`'s `test.setupFiles` if the wiring is missing (TS source isn't safely munged, mirroring `ensureAppTsconfig`'s JSONC-tsconfig warn). New scaffolds ship both. Also refreshes each bound workflow's `.lotics/workflows/<alias>.globals.d.ts` + re-wraps its EXISTING `src/workflows/<alias>.ts` body in the current envelope (strips + re-wraps; never re-fetches the body, so local edits survive). **`.lotics/` is reconciled to the manifest, not merely added to** — a `<alias>.globals.d.ts` whose alias the manifest no longer declares is DELETED (that directory is read as the app's alias inventory, so a companion for a binding nobody can reach misreports what the app has). Only that exact filename shape is removed; anything else in the directory is left alone. The reconcile runs before the credential branch, so it happens offline too. The authored counterpart is never deleted — a `src/workflows/<alias>.ts` the manifest does not declare is NAMED instead (`check` and `set` both take their alias set from the manifest, so editing an undeclared body is a silent no-op). A getApp / binding / schema / dts-fetch failure is non-fatal (warns, keeps the last-generated files). **Re-silvers `package.json#lotics.agents`** from the live app row whenever its `inputs`/`outputs` disagree, then rewrites the agent `.d.ts` from the refreshed block: that block is a mirror AND the offline seed for `useAgentRun` typings, so a stale copy types the app against an agent that does not exist. Refreshing here makes the divergence self-healing on a command already in the loop and keeps the remedy off `app pull` (which rewrites `src/workflows/*.ts` and would eat uncommitted body edits). The write is surgical and order-preserving (`orderedLike`), so it changes only the fields that actually differ. A hand edit to that block is therefore reverted — it never changed the agent anyway; to change one, `set_app_agent`. |
|
|
36
|
-
| `lotics app check` | Every pre-flight `deploy` runs, WITHOUT building or shipping: the manifest's agent schemas against the live app row, aliases the source calls that nothing bound (queries, workflows AND agents), capability-gated SDK calls the manifest doesn't declare,
|
|
36
|
+
| `lotics app check` | Every pre-flight `deploy` runs, WITHOUT building or shipping: 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, and a notice for any alias the source computes at runtime (invisible to every check here and to the deploy's unbind guard). 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. The point is the question being ASKABLE: these checks used to cost a build, a tar, an upload and a version row in the audit trail, which is expensive enough that the honest move was to skip them and find out in production. |
|
|
37
37
|
| `lotics app workflow run <alias> '<json>'` | Execute a bound app workflow end-to-end via `appWorkflow`. `app_id` comes from the local manifest; the alias must be bound (`set_app_workflow`). Inputs ingest exactly like `lotics run` (inline JSON / `@file` / stdin — bulk inputs bypass `ARG_MAX`). Prints the full `{status,message,data,files,side_effects}` JSON to stdout + a one-line summary to stderr; exits non-zero on `status:"error"` (assertable). `--print-created` (alias `--report-effects`) renders the honest post-run harvest: created records grouped by table, a paste-ready `lotics run delete_records …` per table, then the **mandatory caveat** naming what cannot be auto-undone (external integrations + notifications) and that sub-workflows may have run. `--cleanup` (DEFAULT OFF, implies the report) additionally runs the deletes for harvested records ONLY — never files / external / notifications. Neither is a rollback — a rollback is structurally impossible here. |
|
|
38
38
|
| `lotics app workflow set <alias>` | Push the edited `src/workflows/<alias>.ts` body through `set_app_workflow` (the single author of `apps.workflows`). Reads the body from disk (header + `/// <reference>` + `export {};` marker + the `__workflow` wrapper all stripped) + the typed `inputs`/`outputs` **and the `description`** from `package.json#lotics.workflows.<alias>`; the **server** re-verifies the body and echoes the bound `outputs` (declared, else DERIVED from `return({ data })`). The `description` is the one line an agent reads when choosing between the app's aliases (the workflow counterpart to a query's) — authored in the manifest so it lives beside the body in version control and rides every push; omit it and the workflow keeps whatever description it already has, so a push can never blank one set elsewhere. When the manifest declared NO `outputs`, the DERIVED echo is written back into `package.json#lotics.workflows.<alias>.outputs` (a SURGICAL write — preserves `knowledge`/`config` and every other manifest field) and that alias's types are refreshed in place, so `useWorkflow("<alias>")`'s `result.data` is typed immediately with no hand-copy and no second `lotics app codegen`; an explicitly-declared `outputs` is authoritative and never overwritten. Deploy still never authors workflows — this is a CLI convenience over the existing tool. Clear error + non-zero exit on a missing file, an alias absent from the manifest, or a verify failure. |
|
|
39
39
|
| `lotics app agent set <alias>` | Push the edited `src/agents/<alias>.md` instructions back through `set_app_agent` — the agent mirror of `app workflow set`, and the deploy-free authoring path for an agent's PROSE. It sends the instructions and nothing else: the server merges against the stored declaration, so every typed field keeps exactly what is bound. This is deliberate and it is the opposite of what the symmetry with `app workflow set` suggests — **the manifest is a snapshot from the last `app pull`, so replaying its typed half would silently revert whatever was bound since** (the chat authoring agent adding `knowledge_doc_ids`, another operator granting `query_aliases`), and the CLI would print success while the agent quietly lost its knowledge and its read surface. To change a typed field, call `set_app_agent` with just that field (`lotics run set_app_agent '{"app_id":…,"alias":…,"outputs":{…}}'` — it merges), then `app pull` to bring the manifest back in step. Editing `package.json#lotics.agents` by hand pushes nothing, and since that block is what types `useAgentRun`, `codegen` warns and `deploy` REFUSES while it disagrees with the live app. Clear error + non-zero exit on a missing file, an alias absent from the manifest, or a file that is empty once the header is stripped (refusing to push an empty prompt). `app pull` writes the file; edit, then `set`. |
|