@lotics/cli 0.123.0 → 0.127.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 +382 -150
- 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,7 +45587,7 @@ 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 = "
|
|
45590
|
+
var STARTER_FALLBACK_UI_VERSION = "28.1.0";
|
|
45591
45591
|
var STARTER_FALLBACK_SDK_VERSION = "0.52.0";
|
|
45592
45592
|
var STARTER_REACT_NATIVE_VERSION = "0.85.3";
|
|
45593
45593
|
var VITEST_SETUP_FILENAME = "vitest.setup.ts";
|
|
@@ -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 ?? []);
|
|
@@ -70433,56 +70435,53 @@ function sameSchema(a, b) {
|
|
|
70433
70435
|
};
|
|
70434
70436
|
return JSON.stringify(canonical(a)) === JSON.stringify(canonical(b));
|
|
70435
70437
|
}
|
|
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
70438
|
function workflowTypeDivergences(manifestWorkflows, liveWorkflows) {
|
|
70455
70439
|
const out = [];
|
|
70456
70440
|
for (const [alias, declared] of Object.entries(manifestWorkflows ?? {})) {
|
|
70457
70441
|
const live = (liveWorkflows ?? {})[alias];
|
|
70458
70442
|
if (!live) {
|
|
70459
|
-
out.push(
|
|
70443
|
+
out.push({ alias });
|
|
70460
70444
|
continue;
|
|
70461
70445
|
}
|
|
70462
70446
|
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
|
-
}
|
|
70447
|
+
if (!sameSchema(declared[key], live[key])) out.push({ alias, key });
|
|
70466
70448
|
}
|
|
70467
70449
|
}
|
|
70468
70450
|
return out;
|
|
70469
70451
|
}
|
|
70452
|
+
function syncedEntry(entry) {
|
|
70453
|
+
if (entry === void 0) return void 0;
|
|
70454
|
+
return typeof entry === "string" ? { content: entry } : entry;
|
|
70455
|
+
}
|
|
70470
70456
|
function readSynced(projectDir) {
|
|
70471
70457
|
try {
|
|
70472
70458
|
const pkg2 = JSON.parse(
|
|
70473
70459
|
fs6.readFileSync(path7.join(projectDir, "package.json"), "utf-8")
|
|
70474
70460
|
);
|
|
70475
|
-
|
|
70461
|
+
const raw = pkg2.lotics?.synced ?? {};
|
|
70462
|
+
return {
|
|
70463
|
+
workflows: normalizeSyncedMap(raw.workflows),
|
|
70464
|
+
agents: normalizeSyncedMap(raw.agents),
|
|
70465
|
+
queries: normalizeSyncedMap(raw.queries)
|
|
70466
|
+
};
|
|
70476
70467
|
} catch {
|
|
70477
|
-
return {};
|
|
70468
|
+
return { workflows: {}, agents: {}, queries: {} };
|
|
70478
70469
|
}
|
|
70479
70470
|
}
|
|
70480
|
-
function
|
|
70471
|
+
function normalizeSyncedMap(map3) {
|
|
70472
|
+
return Object.fromEntries(
|
|
70473
|
+
Object.entries(map3 ?? {}).map(([alias, entry]) => [
|
|
70474
|
+
alias,
|
|
70475
|
+
syncedEntry(entry)
|
|
70476
|
+
])
|
|
70477
|
+
);
|
|
70478
|
+
}
|
|
70479
|
+
function writeSynced(projectDir, kind, alias, entry) {
|
|
70481
70480
|
const pkgPath2 = path7.join(projectDir, "package.json");
|
|
70482
70481
|
const pkg2 = JSON.parse(fs6.readFileSync(pkgPath2, "utf-8"));
|
|
70483
70482
|
if (!pkg2.lotics) return;
|
|
70484
70483
|
pkg2.lotics.synced ??= {};
|
|
70485
|
-
pkg2.lotics.synced[kind] = { ...pkg2.lotics.synced[kind], [alias]:
|
|
70484
|
+
pkg2.lotics.synced[kind] = { ...pkg2.lotics.synced[kind], [alias]: entry };
|
|
70486
70485
|
fs6.writeFileSync(pkgPath2, JSON.stringify(pkg2, null, 2) + "\n");
|
|
70487
70486
|
}
|
|
70488
70487
|
var WORKFLOWS_DIR = path7.join("src", "workflows");
|
|
@@ -70544,11 +70543,14 @@ function writeWorkflowGlobals(projectDir, alias, dts) {
|
|
|
70544
70543
|
`);
|
|
70545
70544
|
return file2;
|
|
70546
70545
|
}
|
|
70546
|
+
function normalizeWorkflowBody(source) {
|
|
70547
|
+
return source.replace(/\s+$/, "");
|
|
70548
|
+
}
|
|
70547
70549
|
function writeWorkflowFile(projectDir, alias, source, envelope = { prefix: WORKFLOW_ENVELOPE_PREFIX, suffix: WORKFLOW_ENVELOPE_SUFFIX }) {
|
|
70548
70550
|
const dir = path7.join(projectDir, WORKFLOWS_DIR);
|
|
70549
70551
|
fs6.mkdirSync(dir, { recursive: true });
|
|
70550
70552
|
const file2 = workflowFilePath(projectDir, alias);
|
|
70551
|
-
const body = source
|
|
70553
|
+
const body = normalizeWorkflowBody(source);
|
|
70552
70554
|
fs6.writeFileSync(file2, `${workflowFileHeader(alias)}
|
|
70553
70555
|
${envelope.prefix}${body}${envelope.suffix}
|
|
70554
70556
|
`);
|
|
@@ -70570,7 +70572,7 @@ function writeAgentFile(projectDir, alias, instructions) {
|
|
|
70570
70572
|
);
|
|
70571
70573
|
return file2;
|
|
70572
70574
|
}
|
|
70573
|
-
function writeAgentFiles(projectDir, agents) {
|
|
70575
|
+
function writeAgentFiles(projectDir, agents, opts) {
|
|
70574
70576
|
const written = [];
|
|
70575
70577
|
for (const [alias, declaration] of Object.entries(agents)) {
|
|
70576
70578
|
const instructions = declaration.instructions;
|
|
@@ -70578,15 +70580,33 @@ function writeAgentFiles(projectDir, agents) {
|
|
|
70578
70580
|
console.error(`\u26A0 Skipped src/agents/${alias}.md \u2014 the live declaration carries no instructions.`);
|
|
70579
70581
|
continue;
|
|
70580
70582
|
}
|
|
70583
|
+
if (!opts.overwrite && localPromptUnpushed(projectDir, alias)) {
|
|
70584
|
+
opts.kept.push(`src/agents/${alias}.md`);
|
|
70585
|
+
continue;
|
|
70586
|
+
}
|
|
70581
70587
|
writeAgentFile(projectDir, alias, instructions);
|
|
70582
70588
|
written.push(alias);
|
|
70583
70589
|
}
|
|
70584
70590
|
return written;
|
|
70585
70591
|
}
|
|
70592
|
+
function localPromptUnpushed(projectDir, alias) {
|
|
70593
|
+
const file2 = agentFilePath2(projectDir, alias);
|
|
70594
|
+
if (!fs6.existsSync(file2)) return false;
|
|
70595
|
+
const baseline = readSynced(projectDir).agents[alias]?.content;
|
|
70596
|
+
if (baseline === void 0) return true;
|
|
70597
|
+
return contentSha(stripAgentHeader(fs6.readFileSync(file2, "utf-8"))) !== baseline;
|
|
70598
|
+
}
|
|
70586
70599
|
function generatedWorkflowDescription(appName, alias) {
|
|
70587
70600
|
return `Workflow invoked by app "${appName}" via useWorkflow("${alias}").`;
|
|
70588
70601
|
}
|
|
70589
|
-
|
|
70602
|
+
function localBodyUnpushed(projectDir, alias) {
|
|
70603
|
+
const file2 = workflowFilePath(projectDir, alias);
|
|
70604
|
+
if (!fs6.existsSync(file2)) return false;
|
|
70605
|
+
const baseline = readSynced(projectDir).workflows[alias]?.content;
|
|
70606
|
+
if (baseline === void 0) return true;
|
|
70607
|
+
return contentSha(stripWorkflowHeader(fs6.readFileSync(file2, "utf-8"))) !== baseline;
|
|
70608
|
+
}
|
|
70609
|
+
async function writeWorkflowFiles(client, projectDir, app_id, appName, workflows, opts) {
|
|
70590
70610
|
const written = [];
|
|
70591
70611
|
for (const [alias, declaration] of Object.entries(workflows)) {
|
|
70592
70612
|
const res = await client.getAppWorkflow(app_id, alias);
|
|
@@ -70595,7 +70615,9 @@ async function writeWorkflowFiles(client, projectDir, app_id, appName, workflows
|
|
|
70595
70615
|
`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
70616
|
);
|
|
70597
70617
|
}
|
|
70598
|
-
const
|
|
70618
|
+
const result = res.result === null || typeof res.result !== "object" ? null : res.result;
|
|
70619
|
+
const source = result?.source;
|
|
70620
|
+
const body_sha = typeof result?.body_sha === "string" ? result.body_sha : void 0;
|
|
70599
70621
|
if (typeof source !== "string" || source.trim() === "") {
|
|
70600
70622
|
console.error(
|
|
70601
70623
|
`\u26A0 Skipped src/workflows/${alias}.ts \u2014 legacy workflow with no rendered body.`
|
|
@@ -70609,8 +70631,15 @@ async function writeWorkflowFiles(client, projectDir, app_id, appName, workflows
|
|
|
70609
70631
|
alias,
|
|
70610
70632
|
toWorkflowDtsDeclaration(declaration)
|
|
70611
70633
|
);
|
|
70634
|
+
if (!opts.overwrite && localBodyUnpushed(projectDir, alias)) {
|
|
70635
|
+
opts.kept.push(`src/workflows/${alias}.ts`);
|
|
70636
|
+
continue;
|
|
70637
|
+
}
|
|
70612
70638
|
writeWorkflowFile(projectDir, alias, source, envelope);
|
|
70613
|
-
writeSynced(projectDir, "workflows", alias,
|
|
70639
|
+
writeSynced(projectDir, "workflows", alias, {
|
|
70640
|
+
content: contentSha(normalizeWorkflowBody(source)),
|
|
70641
|
+
...body_sha ? { live: body_sha } : {}
|
|
70642
|
+
});
|
|
70614
70643
|
const rowDescription = res.result.description;
|
|
70615
70644
|
if (typeof rowDescription === "string" && rowDescription !== "" && // Skip the server's GENERATED default. It is what an undescribed alias
|
|
70616
70645
|
// carries, so writing it back would commit a line of noise per workflow
|
|
@@ -70725,9 +70754,12 @@ function writeAppMeta(projectDir, meta3) {
|
|
|
70725
70754
|
const pkgPath2 = path7.join(projectDir, "package.json");
|
|
70726
70755
|
const pkg2 = JSON.parse(fs6.readFileSync(pkgPath2, "utf-8"));
|
|
70727
70756
|
const existing = pkg2.lotics && typeof pkg2.lotics === "object" ? pkg2.lotics : {};
|
|
70757
|
+
const supplied = new Set(
|
|
70758
|
+
Object.entries(meta3).filter(([, value2]) => value2 !== void 0).map(([key]) => key)
|
|
70759
|
+
);
|
|
70728
70760
|
const preserved = {};
|
|
70729
70761
|
for (const [key, value2] of Object.entries(existing)) {
|
|
70730
|
-
if (!
|
|
70762
|
+
if (!supplied.has(key)) preserved[key] = value2;
|
|
70731
70763
|
}
|
|
70732
70764
|
pkg2.lotics = { ...meta3, ...preserved };
|
|
70733
70765
|
fs6.writeFileSync(pkgPath2, JSON.stringify(pkg2, null, 2) + "\n");
|
|
@@ -70747,7 +70779,7 @@ function writeWorkflowOutputs(projectDir, alias, outputs) {
|
|
|
70747
70779
|
function workflowBodyDrift(projectDir, workflows, synced = {}) {
|
|
70748
70780
|
const drifted = [];
|
|
70749
70781
|
for (const alias of Object.keys(workflows ?? {})) {
|
|
70750
|
-
const baseline = synced[alias];
|
|
70782
|
+
const baseline = syncedEntry(synced[alias])?.content;
|
|
70751
70783
|
if (baseline === void 0) continue;
|
|
70752
70784
|
const file2 = workflowFilePath(projectDir, alias);
|
|
70753
70785
|
if (!fs6.existsSync(file2)) continue;
|
|
@@ -70757,17 +70789,6 @@ function workflowBodyDrift(projectDir, workflows, synced = {}) {
|
|
|
70757
70789
|
}
|
|
70758
70790
|
return drifted;
|
|
70759
70791
|
}
|
|
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
70792
|
function ensureAppTsconfig(projectDir) {
|
|
70772
70793
|
const tsconfigPath = path7.join(projectDir, "tsconfig.json");
|
|
70773
70794
|
if (!fs6.existsSync(tsconfigPath)) {
|
|
@@ -70930,6 +70951,15 @@ async function writeGeneratedAppFields(client, projectDir, app, queries) {
|
|
|
70930
70951
|
} else {
|
|
70931
70952
|
const tableIds = resolveCodegenTableIds(projectDir, queries);
|
|
70932
70953
|
const tables = await client.getWorkspaceSchema(tableIds);
|
|
70954
|
+
const missing = tableIds.filter((id) => !tables.some((t) => t.id === id));
|
|
70955
|
+
if (missing.length > 0) {
|
|
70956
|
+
console.error(
|
|
70957
|
+
`\u26A0 Skipped .lotics/app_fields.ts \u2014 ${missing.length} table(s) the app queries could not be read: ${missing.join(", ")}.
|
|
70958
|
+
Writing anyway would bake a narrowed F/OPT map that compiles and then throws at runtime. Check this key's access to those tables.
|
|
70959
|
+
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.`
|
|
70960
|
+
);
|
|
70961
|
+
return;
|
|
70962
|
+
}
|
|
70933
70963
|
const fieldsPath = writeAppFields(projectDir, tables);
|
|
70934
70964
|
console.error(
|
|
70935
70965
|
`Wrote ${fieldsPath} (baked form \u2014 bespoke; ${tables.length} table${tables.length === 1 ? "" : "s"})`
|
|
@@ -71216,11 +71246,15 @@ async function appPull(client, args) {
|
|
|
71216
71246
|
const tmpFile = path7.join(tmpdir(), `lotics-app-${app.id}-${Date.now()}.tar.gz`);
|
|
71217
71247
|
console.error(`Downloading source archive...`);
|
|
71218
71248
|
await downloadToFile(sourceUrl, tmpFile);
|
|
71249
|
+
const keptLocal = [];
|
|
71250
|
+
const stagingDir = fs6.mkdtempSync(path7.join(tmpdir(), `lotics-pull-${app.id}-`));
|
|
71219
71251
|
try {
|
|
71252
|
+
await runTar(["-xzf", tmpFile, "-C", stagingDir], stagingDir);
|
|
71220
71253
|
console.error(`Extracting to ${targetPath}...`);
|
|
71221
|
-
|
|
71254
|
+
keptLocal.push(...copyTree(stagingDir, targetPath, { overwrite: args.force === true }));
|
|
71222
71255
|
} finally {
|
|
71223
71256
|
if (fs6.existsSync(tmpFile)) fs6.unlinkSync(tmpFile);
|
|
71257
|
+
fs6.rmSync(stagingDir, { recursive: true, force: true });
|
|
71224
71258
|
}
|
|
71225
71259
|
stampPulledManifest(targetPath, {
|
|
71226
71260
|
app_id: app.id,
|
|
@@ -71232,7 +71266,8 @@ async function appPull(client, args) {
|
|
|
71232
71266
|
agents: app.agents ?? {}
|
|
71233
71267
|
});
|
|
71234
71268
|
for (const [alias, declaration] of Object.entries(app.queries ?? {})) {
|
|
71235
|
-
|
|
71269
|
+
const sha = declarationSha(declaration);
|
|
71270
|
+
writeSynced(targetPath, "queries", alias, { content: sha, live: sha });
|
|
71236
71271
|
}
|
|
71237
71272
|
await writeGeneratedAppFields(
|
|
71238
71273
|
client,
|
|
@@ -71242,7 +71277,10 @@ async function appPull(client, args) {
|
|
|
71242
71277
|
);
|
|
71243
71278
|
const workflows = app.workflows ?? {};
|
|
71244
71279
|
if (Object.keys(workflows).length > 0) {
|
|
71245
|
-
const written = await writeWorkflowFiles(client, targetPath, app.id, app.name, workflows
|
|
71280
|
+
const written = await writeWorkflowFiles(client, targetPath, app.id, app.name, workflows, {
|
|
71281
|
+
overwrite: args.force === true,
|
|
71282
|
+
kept: keptLocal
|
|
71283
|
+
});
|
|
71246
71284
|
if (written.length > 0) {
|
|
71247
71285
|
console.error(
|
|
71248
71286
|
`Wrote ${written.length} workflow ${written.length === 1 ? "body" : "bodies"} to ${WORKFLOWS_DIR}/ (${written.join(", ")})`
|
|
@@ -71251,10 +71289,15 @@ async function appPull(client, args) {
|
|
|
71251
71289
|
}
|
|
71252
71290
|
const agents = app.agents ?? {};
|
|
71253
71291
|
if (Object.keys(agents).length > 0) {
|
|
71254
|
-
const written = writeAgentFiles(targetPath, agents
|
|
71292
|
+
const written = writeAgentFiles(targetPath, agents, {
|
|
71293
|
+
overwrite: args.force === true,
|
|
71294
|
+
kept: keptLocal
|
|
71295
|
+
});
|
|
71255
71296
|
for (const alias of written) {
|
|
71256
71297
|
const instructions = agents[alias]?.instructions;
|
|
71257
|
-
if (instructions
|
|
71298
|
+
if (instructions === void 0) continue;
|
|
71299
|
+
const sha = contentSha(instructions);
|
|
71300
|
+
writeSynced(targetPath, "agents", alias, { content: sha, live: sha });
|
|
71258
71301
|
}
|
|
71259
71302
|
if (written.length > 0) {
|
|
71260
71303
|
console.error(
|
|
@@ -71264,6 +71307,7 @@ async function appPull(client, args) {
|
|
|
71264
71307
|
}
|
|
71265
71308
|
console.error(`Installing npm dependencies...`);
|
|
71266
71309
|
await runNpm(["install"], targetPath);
|
|
71310
|
+
reportKeptLocalFiles(keptLocal, app.id);
|
|
71267
71311
|
console.error(`
|
|
71268
71312
|
Ready. Next steps:`);
|
|
71269
71313
|
console.error(` cd ${path7.relative(process.cwd(), targetPath) || "."}`);
|
|
@@ -71289,35 +71333,33 @@ async function appDeploy(client, args) {
|
|
|
71289
71333
|
const meta3 = readAppMeta(projectDir);
|
|
71290
71334
|
warnAboutDevLink(projectDir, "deploy");
|
|
71291
71335
|
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);
|
|
71336
|
+
const pending = pendingBindings({
|
|
71337
|
+
projectDir,
|
|
71338
|
+
meta: meta3,
|
|
71339
|
+
synced: readSynced(projectDir).workflows,
|
|
71340
|
+
live: liveApp
|
|
71341
|
+
});
|
|
71342
|
+
if (!nothingPending(pending)) {
|
|
71343
|
+
await pushPendingBindings(client, projectDir, pending);
|
|
71306
71344
|
}
|
|
71307
|
-
const declarationDrift = reportWorkflowDeclarationDrift(meta3.workflows, liveApp.workflows);
|
|
71308
71345
|
const sourceText = readAppSourceText(projectDir);
|
|
71309
71346
|
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
71347
|
warnIfDynamicAliases(called);
|
|
71319
71348
|
warnIfUndeclaredCapabilities(sourceText, meta3.capabilities);
|
|
71320
71349
|
writeAppDts(projectDir, { workflows: meta3.workflows, queries: meta3.queries, agents: meta3.agents });
|
|
71350
|
+
try {
|
|
71351
|
+
await writeGeneratedAppFields(
|
|
71352
|
+
client,
|
|
71353
|
+
projectDir,
|
|
71354
|
+
{ app_id: meta3.app_id, package_id: liveApp.package_id },
|
|
71355
|
+
meta3.queries ?? {}
|
|
71356
|
+
);
|
|
71357
|
+
} catch (err2) {
|
|
71358
|
+
console.error(
|
|
71359
|
+
`\u26A0 Could not regenerate .lotics/app_fields.ts (${err2.message}).
|
|
71360
|
+
The build will use whatever is on disk \u2014 run 'lotics app codegen' if this app is published.`
|
|
71361
|
+
);
|
|
71362
|
+
}
|
|
71321
71363
|
console.error("Building...");
|
|
71322
71364
|
await runNpm(["run", "build"], projectDir);
|
|
71323
71365
|
const distDir = path7.join(projectDir, "dist");
|
|
@@ -71354,7 +71396,7 @@ Refusing to deploy \u2014 package.json#lotics.agents disagrees with the live app
|
|
|
71354
71396
|
source_archive: fs6.readFileSync(tmpSource),
|
|
71355
71397
|
dist_archive: fs6.readFileSync(tmpDist),
|
|
71356
71398
|
prev_version_id: meta3.current_version_id,
|
|
71357
|
-
message: args.message,
|
|
71399
|
+
message: args.message ?? derivedDeployMessage(pending),
|
|
71358
71400
|
// Capabilities ARE manifest-authoritative: always send,
|
|
71359
71401
|
// defaulting to `{}` when the manifest declares none — so deleting the
|
|
71360
71402
|
// `capabilities` block turns every capability OFF on the next deploy
|
|
@@ -71379,13 +71421,24 @@ Refusing to deploy \u2014 package.json#lotics.agents disagrees with the live app
|
|
|
71379
71421
|
});
|
|
71380
71422
|
console.error(`Deployed v${result.version_number} (${result.version_id})`);
|
|
71381
71423
|
console.error(`Bundle size: ${(result.bundle_size_bytes / 1024).toFixed(1)} KB`);
|
|
71424
|
+
let liveAfter;
|
|
71382
71425
|
try {
|
|
71383
|
-
|
|
71384
|
-
warnIfUnbranded(app);
|
|
71385
|
-
warnIfUnboundAliases(app, called);
|
|
71426
|
+
liveAfter = await client.getApp(meta3.app_id);
|
|
71386
71427
|
} catch (err2) {
|
|
71387
71428
|
console.error(`(skipped post-deploy checks: ${err2.message})`);
|
|
71388
71429
|
}
|
|
71430
|
+
if (liveAfter) {
|
|
71431
|
+
warnIfUnbranded(liveAfter);
|
|
71432
|
+
warnIfUnboundAliases(liveAfter, called);
|
|
71433
|
+
}
|
|
71434
|
+
if (liveAfter) {
|
|
71435
|
+
await pruneOrphanedBindings(
|
|
71436
|
+
client,
|
|
71437
|
+
meta3.app_id,
|
|
71438
|
+
orphanedBindings(called, liveAfter),
|
|
71439
|
+
called.dynamic
|
|
71440
|
+
);
|
|
71441
|
+
}
|
|
71389
71442
|
} catch (err2) {
|
|
71390
71443
|
const e = err2;
|
|
71391
71444
|
if (e.code === "VERSION_CONFLICT") {
|
|
@@ -71425,78 +71478,233 @@ async function readLiveQueries(client, appId) {
|
|
|
71425
71478
|
process.exit(1);
|
|
71426
71479
|
}
|
|
71427
71480
|
}
|
|
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
71481
|
async function appCheck(client, args = {}) {
|
|
71441
71482
|
const projectDir = path7.resolve(args.projectDir ?? process.cwd());
|
|
71442
71483
|
const meta3 = readAppMeta(projectDir);
|
|
71443
71484
|
const app = await client.getApp(meta3.app_id);
|
|
71444
71485
|
const sourceText = readAppSourceText(projectDir);
|
|
71445
71486
|
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
71487
|
warnAboutDevLink(projectDir, "deploy");
|
|
71457
|
-
|
|
71458
|
-
const
|
|
71459
|
-
|
|
71460
|
-
|
|
71488
|
+
warnAboutOrphanedBindings(called, app);
|
|
71489
|
+
const pending = pendingBindings({
|
|
71490
|
+
projectDir,
|
|
71491
|
+
meta: meta3,
|
|
71492
|
+
synced: readSynced(projectDir).workflows,
|
|
71493
|
+
live: app
|
|
71494
|
+
});
|
|
71495
|
+
reportPending(pending);
|
|
71461
71496
|
warnIfDynamicAliases(called);
|
|
71462
71497
|
warnIfUndeclaredCapabilities(sourceText, meta3.capabilities);
|
|
71463
71498
|
warnIfUnboundAliases(app, called);
|
|
71464
71499
|
warnIfUnbranded(app);
|
|
71465
|
-
if (
|
|
71500
|
+
if (!nothingPending(pending)) {
|
|
71466
71501
|
process.exit(1);
|
|
71467
71502
|
}
|
|
71468
71503
|
console.error("Checked the app's bindings, capabilities and agent schemas \u2014 nothing blocking.");
|
|
71469
71504
|
}
|
|
71470
|
-
function
|
|
71471
|
-
|
|
71505
|
+
async function pushPendingBindings(client, projectDir, pending) {
|
|
71506
|
+
const plan = [
|
|
71507
|
+
...pending.queries.map((alias) => `query ${alias}`),
|
|
71508
|
+
...pending.workflows.map((alias) => `workflow ${alias}`),
|
|
71509
|
+
...pending.agents.map((alias) => `agent ${alias}`)
|
|
71510
|
+
];
|
|
71511
|
+
console.error(
|
|
71512
|
+
`
|
|
71513
|
+
Pushing ${plan.length} binding${plan.length === 1 ? "" : "s"} ahead of the app: ${plan.join(", ")}`
|
|
71514
|
+
);
|
|
71515
|
+
for (const alias of pending.queries) {
|
|
71516
|
+
await appQuerySet(client, { alias, projectDir });
|
|
71517
|
+
}
|
|
71518
|
+
for (const alias of pending.workflows) {
|
|
71519
|
+
await appWorkflowSet(client, { alias, projectDir });
|
|
71520
|
+
}
|
|
71521
|
+
for (const alias of pending.agents) {
|
|
71522
|
+
await appAgentSet(client, { alias, projectDir });
|
|
71523
|
+
}
|
|
71524
|
+
}
|
|
71525
|
+
function pendingBindings(args) {
|
|
71526
|
+
const { projectDir, meta: meta3, synced, live } = args;
|
|
71527
|
+
const workflows = /* @__PURE__ */ new Set([
|
|
71528
|
+
...workflowBodyDrift(projectDir, meta3.workflows, synced),
|
|
71529
|
+
...divergedWorkflowAliases(meta3.workflows, live.workflows)
|
|
71530
|
+
]);
|
|
71531
|
+
return {
|
|
71532
|
+
workflows: [...workflows].sort(),
|
|
71533
|
+
agents: [
|
|
71534
|
+
.../* @__PURE__ */ new Set([
|
|
71535
|
+
...driftedAgentAliases(projectDir, live.agents),
|
|
71536
|
+
// A manifest declaration that disagrees with the live row is a push now,
|
|
71537
|
+
// the same as a workflow's. `agentTypeDivergences` renders one line per
|
|
71538
|
+
// (alias, key) for a human, and the alias is its first token — but an
|
|
71539
|
+
// alias that diverged on both `inputs` and `outputs` still needs exactly
|
|
71540
|
+
// one push.
|
|
71541
|
+
...divergedAgentAliases(meta3.agents, live.agents)
|
|
71542
|
+
])
|
|
71543
|
+
].sort(),
|
|
71544
|
+
// Only aliases the manifest DECLARES. `driftedQueryAliases` is symmetric —
|
|
71545
|
+
// it unions both key sets — so an alias deleted locally but still bound live
|
|
71546
|
+
// reads as drift, and queueing it sends the author to a push that refuses
|
|
71547
|
+
// ("declare it first") for a declaration they deliberately removed. A
|
|
71548
|
+
// deletion is not a push, and this detector only decides what to push;
|
|
71549
|
+
// unbinding is `remove_app_query`, which no deploy runs for any artifact.
|
|
71550
|
+
queries: [...driftedQueryAliases(meta3.queries ?? {}, live.queries)].filter((alias) => (meta3.queries ?? {})[alias] !== void 0).sort()
|
|
71551
|
+
};
|
|
71552
|
+
}
|
|
71553
|
+
function reportPending(pending) {
|
|
71554
|
+
const lines = [
|
|
71555
|
+
...pending.workflows.map((a) => ` \u2022 workflow "${a}" \u2014 lotics app workflow set ${a}`),
|
|
71556
|
+
...pending.agents.map((a) => ` \u2022 agent "${a}" \u2014 lotics app agent set ${a}`),
|
|
71557
|
+
...pending.queries.map((a) => ` \u2022 query "${a}" \u2014 lotics app query set ${a}`)
|
|
71558
|
+
];
|
|
71559
|
+
if (lines.length === 0) return;
|
|
71560
|
+
console.error(
|
|
71561
|
+
`
|
|
71562
|
+
\u2717 ${lines.length} binding${lines.length === 1 ? " is" : "s are"} ahead of the app:
|
|
71563
|
+
` + lines.join("\n") + `
|
|
71564
|
+
|
|
71565
|
+
A version shipped now would not reproduce what runs. 'lotics app deploy' pushes
|
|
71566
|
+
these for you; revert the file instead to drop the edit.`
|
|
71567
|
+
);
|
|
71568
|
+
}
|
|
71569
|
+
function nothingPending(pending) {
|
|
71570
|
+
return pending.workflows.length === 0 && pending.agents.length === 0 && pending.queries.length === 0;
|
|
71571
|
+
}
|
|
71572
|
+
function divergedAgentAliases(manifestAgents, liveAgents) {
|
|
71573
|
+
const out = [];
|
|
71574
|
+
for (const [alias, declared] of Object.entries(manifestAgents ?? {})) {
|
|
71575
|
+
const live = (liveAgents ?? {})[alias];
|
|
71576
|
+
if (!live) continue;
|
|
71577
|
+
if (AGENT_TYPED_KEYS.some((key) => !sameSchema(declared[key], live[key]))) out.push(alias);
|
|
71578
|
+
}
|
|
71579
|
+
return out;
|
|
71580
|
+
}
|
|
71581
|
+
function driftedAgentAliases(projectDir, liveAgents) {
|
|
71582
|
+
const drifted = [];
|
|
71472
71583
|
for (const [alias, live] of Object.entries(liveAgents ?? {})) {
|
|
71473
71584
|
const file2 = agentFilePath2(projectDir, alias);
|
|
71474
71585
|
if (!fs6.existsSync(file2)) continue;
|
|
71475
71586
|
const local = stripAgentHeader(fs6.readFileSync(file2, "utf-8"));
|
|
71476
71587
|
if (local === "" || local === (live.instructions ?? "")) continue;
|
|
71477
|
-
drifted
|
|
71588
|
+
drifted.push(alias);
|
|
71589
|
+
}
|
|
71590
|
+
return drifted.sort();
|
|
71591
|
+
}
|
|
71592
|
+
function divergedWorkflowAliases(manifestWorkflows, liveWorkflows) {
|
|
71593
|
+
return [
|
|
71594
|
+
...new Set(workflowTypeDivergences(manifestWorkflows, liveWorkflows).map((d) => d.alias))
|
|
71595
|
+
].sort();
|
|
71596
|
+
}
|
|
71597
|
+
function orphanedBindings(called, live) {
|
|
71598
|
+
const agentDeclarations = Object.values(live.agents ?? {});
|
|
71599
|
+
const referenced = {
|
|
71600
|
+
queries: [...called.queries, ...agentDeclarations.flatMap((a) => a.query_aliases ?? [])],
|
|
71601
|
+
workflows: [...called.workflows, ...agentDeclarations.flatMap((a) => a.workflow_aliases ?? [])],
|
|
71602
|
+
// Nothing declares an agent but the client bundle — an agent cannot run another.
|
|
71603
|
+
agents: called.agents
|
|
71604
|
+
};
|
|
71605
|
+
return orphanedAliases(
|
|
71606
|
+
{
|
|
71607
|
+
queries: Object.keys(live.queries ?? {}),
|
|
71608
|
+
workflows: Object.keys(live.workflows ?? {}),
|
|
71609
|
+
agents: Object.keys(live.agents ?? {})
|
|
71610
|
+
},
|
|
71611
|
+
referenced
|
|
71612
|
+
);
|
|
71613
|
+
}
|
|
71614
|
+
function warnAboutOrphanedBindings(called, live) {
|
|
71615
|
+
const orphans = orphanedBindings(called, live);
|
|
71616
|
+
const lines = [
|
|
71617
|
+
...orphans.queries.map((a) => ` \u2022 query ${a}`),
|
|
71618
|
+
...orphans.workflows.map((a) => ` \u2022 workflow ${a}`),
|
|
71619
|
+
...orphans.agents.map((a) => ` \u2022 agent ${a}`)
|
|
71620
|
+
];
|
|
71621
|
+
if (lines.length === 0) return 0;
|
|
71622
|
+
console.error(
|
|
71623
|
+
`
|
|
71624
|
+
\u26A0 ${lines.length} binding(s) are live but named nowhere in this bundle:
|
|
71625
|
+
` + lines.join("\n") + (called.dynamic.length > 0 ? `
|
|
71626
|
+
|
|
71627
|
+
This app computes an alias at runtime, so one of these may be called after
|
|
71628
|
+
all \u2014 a deploy therefore leaves them ALL in place. Make those call sites
|
|
71629
|
+
literal and a deploy will clean them up.` : `
|
|
71630
|
+
|
|
71631
|
+
A deploy unbinds these after it ships.`)
|
|
71632
|
+
);
|
|
71633
|
+
return lines.length;
|
|
71634
|
+
}
|
|
71635
|
+
async function pruneOrphanedBindings(client, appId, orphans, dynamic) {
|
|
71636
|
+
const targets = [
|
|
71637
|
+
...orphans.queries.map((alias) => ({ tool: "remove_app_query", alias, label: `query ${alias}` })),
|
|
71638
|
+
...orphans.workflows.map((alias) => ({
|
|
71639
|
+
tool: "remove_app_workflow",
|
|
71640
|
+
alias,
|
|
71641
|
+
label: `workflow ${alias}`
|
|
71642
|
+
})),
|
|
71643
|
+
...orphans.agents.map((alias) => ({ tool: "remove_app_agent", alias, label: `agent ${alias}` }))
|
|
71644
|
+
];
|
|
71645
|
+
if (targets.length === 0) return 0;
|
|
71646
|
+
if (dynamic.length > 0) {
|
|
71478
71647
|
console.error(
|
|
71479
71648
|
`
|
|
71480
|
-
\
|
|
71481
|
-
|
|
71482
|
-
|
|
71649
|
+
\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:
|
|
71650
|
+
` + targets.map((t) => ` \u2022 ${t.label}`).join("\n") + `
|
|
71651
|
+
Make those call sites literal and the next deploy will clean them up, or remove the ones you know are dead by hand.`
|
|
71483
71652
|
);
|
|
71653
|
+
return targets.length;
|
|
71484
71654
|
}
|
|
71485
|
-
|
|
71655
|
+
let failed = 0;
|
|
71656
|
+
for (const target of targets) {
|
|
71657
|
+
const res = await client.execute(target.tool, { app_id: appId, alias: target.alias });
|
|
71658
|
+
if (res.error) {
|
|
71659
|
+
failed += 1;
|
|
71660
|
+
console.error(` \u2717 could not unbind ${target.label} \u2014 ${res.error}`);
|
|
71661
|
+
continue;
|
|
71662
|
+
}
|
|
71663
|
+
console.error(` \u2713 unbound ${target.label}`);
|
|
71664
|
+
}
|
|
71665
|
+
return failed;
|
|
71666
|
+
}
|
|
71667
|
+
function derivedDeployMessage(pending) {
|
|
71668
|
+
const parts = [
|
|
71669
|
+
...pending.queries.map((a) => `query ${a}`),
|
|
71670
|
+
...pending.workflows.map((a) => `workflow ${a}`),
|
|
71671
|
+
...pending.agents.map((a) => `agent ${a}`)
|
|
71672
|
+
];
|
|
71673
|
+
return parts.length === 0 ? "Deployed app code" : `Deployed app code; pushed ${parts.join(", ")}`;
|
|
71674
|
+
}
|
|
71675
|
+
function copyTree(from, to, opts, relative = "") {
|
|
71676
|
+
const kept = [];
|
|
71677
|
+
for (const entry of fs6.readdirSync(from, { withFileTypes: true })) {
|
|
71678
|
+
const src = path7.join(from, entry.name);
|
|
71679
|
+
const dest = path7.join(to, entry.name);
|
|
71680
|
+
const rel = path7.join(relative, entry.name);
|
|
71681
|
+
if (entry.isDirectory()) {
|
|
71682
|
+
fs6.mkdirSync(dest, { recursive: true });
|
|
71683
|
+
kept.push(...copyTree(src, dest, opts, rel));
|
|
71684
|
+
continue;
|
|
71685
|
+
}
|
|
71686
|
+
if (!opts.overwrite && fs6.existsSync(dest)) {
|
|
71687
|
+
if (fs6.readFileSync(src).equals(fs6.readFileSync(dest))) continue;
|
|
71688
|
+
kept.push(rel);
|
|
71689
|
+
continue;
|
|
71690
|
+
}
|
|
71691
|
+
fs6.copyFileSync(src, dest);
|
|
71692
|
+
}
|
|
71693
|
+
return kept;
|
|
71486
71694
|
}
|
|
71487
|
-
function
|
|
71488
|
-
|
|
71489
|
-
if (divergences.length === 0) return false;
|
|
71695
|
+
function reportKeptLocalFiles(kept, appId) {
|
|
71696
|
+
if (kept.length === 0) return;
|
|
71490
71697
|
console.error(
|
|
71491
71698
|
`
|
|
71492
|
-
|
|
71493
|
-
` +
|
|
71494
|
-
|
|
71495
|
-
|
|
71496
|
-
|
|
71497
|
-
|
|
71699
|
+
${kept.length} local file(s) differ from the app and were KEPT, not overwritten:
|
|
71700
|
+
` + kept.slice(0, 20).map((f) => ` \u2022 ${f}`).join("\n") + (kept.length > 20 ? `
|
|
71701
|
+
\u2026 and ${kept.length - 20} more` : ``) + `
|
|
71702
|
+
|
|
71703
|
+
Everything else was refreshed from the app. Your edits are intact.
|
|
71704
|
+
Ship them: lotics app deploy (pushes bodies, prompts and declarations)
|
|
71705
|
+
See the difference: lotics app check
|
|
71706
|
+
Take the app's copy: lotics app pull ${appId} --force (DISCARDS the files above)`
|
|
71498
71707
|
);
|
|
71499
|
-
return true;
|
|
71500
71708
|
}
|
|
71501
71709
|
function warnIfDynamicAliases(called) {
|
|
71502
71710
|
if (called.dynamic.length === 0) return;
|
|
@@ -71760,7 +71968,7 @@ Agent "${args.alias}" run ${run.id} \u2192 ${run.status}${run.error_message ? `:
|
|
|
71760
71968
|
if (run.status !== "completed") process.exit(1);
|
|
71761
71969
|
}
|
|
71762
71970
|
async function appAgentSet(client, args) {
|
|
71763
|
-
const projectDir = process.cwd();
|
|
71971
|
+
const projectDir = args.projectDir ?? process.cwd();
|
|
71764
71972
|
const meta3 = readAppMeta(projectDir);
|
|
71765
71973
|
noteScope({ alias: args.alias });
|
|
71766
71974
|
const app = await client.getApp(meta3.app_id);
|
|
@@ -71771,37 +71979,51 @@ async function appAgentSet(client, args) {
|
|
|
71771
71979
|
);
|
|
71772
71980
|
process.exit(1);
|
|
71773
71981
|
}
|
|
71982
|
+
const declared = meta3.agents?.[args.alias];
|
|
71983
|
+
const hasDeclaration = declared?.inputs !== void 0 || declared?.outputs !== void 0;
|
|
71774
71984
|
const file2 = agentFilePath2(projectDir, args.alias);
|
|
71775
|
-
if (!fs6.existsSync(file2)) {
|
|
71985
|
+
if (!fs6.existsSync(file2) && !hasDeclaration) {
|
|
71776
71986
|
console.error(
|
|
71777
|
-
`No instructions at ${path7.relative(projectDir, file2)}
|
|
71987
|
+
`No instructions at ${path7.relative(projectDir, file2)}, and nothing declared in package.json#lotics.agents.${args.alias} to push instead.
|
|
71988
|
+
Run 'lotics app pull ${meta3.app_id}' to write ${AGENTS_DIR}/${args.alias}.md, then edit it.`
|
|
71778
71989
|
);
|
|
71779
71990
|
process.exit(1);
|
|
71780
71991
|
}
|
|
71781
|
-
const instructions = stripAgentHeader(fs6.readFileSync(file2, "utf-8"));
|
|
71992
|
+
const instructions = fs6.existsSync(file2) ? stripAgentHeader(fs6.readFileSync(file2, "utf-8")) : void 0;
|
|
71782
71993
|
if (instructions === "") {
|
|
71783
71994
|
console.error(
|
|
71784
71995
|
`${path7.relative(projectDir, file2)} is empty after stripping the header \u2014 refusing to push an empty prompt.`
|
|
71785
71996
|
);
|
|
71786
71997
|
process.exit(1);
|
|
71787
71998
|
}
|
|
71999
|
+
const proseBaseline = readSynced(projectDir).agents[args.alias]?.live;
|
|
71788
72000
|
const res = await client.setAppAgent(meta3.app_id, args.alias, {
|
|
71789
|
-
|
|
71790
|
-
//
|
|
71791
|
-
//
|
|
71792
|
-
...
|
|
72001
|
+
// Prose and its precondition ride together. Gating a declaration-only push on
|
|
72002
|
+
// the prose baseline would refuse it because someone edited a PROMPT — a
|
|
72003
|
+
// precondition must guard what this call actually writes.
|
|
72004
|
+
...instructions !== void 0 ? { instructions, ...proseBaseline ? { expected_instructions_sha: proseBaseline } : {} } : {},
|
|
72005
|
+
...declared?.inputs !== void 0 ? { inputs: declared.inputs } : {},
|
|
72006
|
+
...declared?.outputs !== void 0 ? { outputs: declared.outputs } : {}
|
|
71793
72007
|
});
|
|
71794
72008
|
if (res.error) {
|
|
71795
72009
|
console.error(`Failed to set agent "${args.alias}": ${res.error}`);
|
|
71796
72010
|
process.exit(1);
|
|
71797
72011
|
}
|
|
71798
|
-
|
|
72012
|
+
if (instructions !== void 0) {
|
|
72013
|
+
const proseSha = contentSha(instructions);
|
|
72014
|
+
writeSynced(projectDir, "agents", args.alias, { content: proseSha, live: proseSha });
|
|
72015
|
+
}
|
|
72016
|
+
const pushed = [
|
|
72017
|
+
...instructions !== void 0 ? [`${instructions.length} chars of instructions`] : [],
|
|
72018
|
+
...declared?.inputs !== void 0 ? ["inputs"] : [],
|
|
72019
|
+
...declared?.outputs !== void 0 ? ["outputs"] : []
|
|
72020
|
+
];
|
|
71799
72021
|
console.error(
|
|
71800
|
-
`Set agent "${args.alias}" (${
|
|
72022
|
+
`Set agent "${args.alias}" (${pushed.join(", ")}` + (live.model_tier ? `, ${live.model_tier}` : "") + `). Everything else left as it is.`
|
|
71801
72023
|
);
|
|
71802
72024
|
}
|
|
71803
72025
|
async function appWorkflowSet(client, args) {
|
|
71804
|
-
const projectDir = process.cwd();
|
|
72026
|
+
const projectDir = args.projectDir ?? process.cwd();
|
|
71805
72027
|
const meta3 = readAppMeta(projectDir);
|
|
71806
72028
|
const declaration = meta3.workflows?.[args.alias];
|
|
71807
72029
|
if (!declaration) {
|
|
@@ -71813,7 +72035,9 @@ async function appWorkflowSet(client, args) {
|
|
|
71813
72035
|
const file2 = workflowFilePath(projectDir, args.alias);
|
|
71814
72036
|
if (!fs6.existsSync(file2)) {
|
|
71815
72037
|
console.error(
|
|
71816
|
-
`No workflow body at ${path7.relative(projectDir, file2)}.
|
|
72038
|
+
`No workflow body at ${path7.relative(projectDir, file2)}.
|
|
72039
|
+
If "${args.alias}" is already bound: lotics app pull ${meta3.app_id} (writes the body)
|
|
72040
|
+
If it is a NEW alias you declared: create that file yourself, then re-run this.`
|
|
71817
72041
|
);
|
|
71818
72042
|
process.exit(1);
|
|
71819
72043
|
}
|
|
@@ -71838,13 +72062,17 @@ async function appWorkflowSet(client, args) {
|
|
|
71838
72062
|
// The body this edit was built on. Turns the push into a conditional write:
|
|
71839
72063
|
// if the live body moved since this project last saw it, the server refuses
|
|
71840
72064
|
// rather than letting this copy overwrite a change nobody here has seen.
|
|
71841
|
-
expected_body_sha: readSynced(projectDir).workflows
|
|
72065
|
+
expected_body_sha: readSynced(projectDir).workflows[args.alias]?.live
|
|
71842
72066
|
});
|
|
71843
72067
|
if (res.error) {
|
|
71844
72068
|
console.error(`Failed to set workflow "${args.alias}": ${res.error}`);
|
|
71845
72069
|
process.exit(1);
|
|
71846
72070
|
}
|
|
71847
|
-
|
|
72071
|
+
const pushedBodySha = res.result !== null && typeof res.result === "object" ? res.result.body_sha : void 0;
|
|
72072
|
+
writeSynced(projectDir, "workflows", args.alias, {
|
|
72073
|
+
content: contentSha(source),
|
|
72074
|
+
...typeof pushedBodySha === "string" ? { live: pushedBodySha } : {}
|
|
72075
|
+
});
|
|
71848
72076
|
const result = res.result ?? {};
|
|
71849
72077
|
const workflowId = typeof result.workflow_id === "string" ? result.workflow_id : "(unknown)";
|
|
71850
72078
|
console.error(`Set workflow "${args.alias}" \u2192 ${workflowId}`);
|
|
@@ -71864,7 +72092,7 @@ async function appWorkflowSet(client, args) {
|
|
|
71864
72092
|
}
|
|
71865
72093
|
}
|
|
71866
72094
|
async function appQuerySet(client, args) {
|
|
71867
|
-
const projectDir = process.cwd();
|
|
72095
|
+
const projectDir = args.projectDir ?? process.cwd();
|
|
71868
72096
|
const meta3 = readAppMeta(projectDir);
|
|
71869
72097
|
const declared = meta3.queries ?? {};
|
|
71870
72098
|
const aliases = "all" in args ? Object.keys(declared).sort() : [args.alias];
|
|
@@ -71886,21 +72114,22 @@ async function appQuerySet(client, args) {
|
|
|
71886
72114
|
meta3.app_id,
|
|
71887
72115
|
alias,
|
|
71888
72116
|
declaration,
|
|
71889
|
-
readSynced(projectDir).queries
|
|
72117
|
+
readSynced(projectDir).queries[alias]?.live
|
|
71890
72118
|
);
|
|
71891
72119
|
if (res.error) {
|
|
71892
72120
|
if (pushed.length > 0) console.error(`Pushed before the failure: ${pushed.join(", ")}.`);
|
|
71893
72121
|
console.error(`Failed to set query "${alias}": ${res.error}`);
|
|
71894
72122
|
process.exit(1);
|
|
71895
72123
|
}
|
|
71896
|
-
|
|
72124
|
+
const querySha = declarationSha(declaration);
|
|
72125
|
+
writeSynced(projectDir, "queries", alias, { content: querySha, live: querySha });
|
|
71897
72126
|
pushed.push(alias);
|
|
71898
72127
|
}
|
|
71899
72128
|
console.error(
|
|
71900
72129
|
`Set ${pushed.length} quer${pushed.length === 1 ? "y" : "ies"} on ${meta3.app_id}: ${pushed.join(", ")}.`
|
|
71901
72130
|
);
|
|
71902
72131
|
}
|
|
71903
|
-
async function appWorkflowPull(client) {
|
|
72132
|
+
async function appWorkflowPull(client, args = {}) {
|
|
71904
72133
|
const projectDir = process.cwd();
|
|
71905
72134
|
const meta3 = readAppMeta(projectDir);
|
|
71906
72135
|
const app = await client.getApp(meta3.app_id);
|
|
@@ -71909,10 +72138,15 @@ async function appWorkflowPull(client) {
|
|
|
71909
72138
|
console.error(`App ${meta3.app_id} has no bound workflows.`);
|
|
71910
72139
|
return;
|
|
71911
72140
|
}
|
|
71912
|
-
const
|
|
72141
|
+
const kept = [];
|
|
72142
|
+
const written = await writeWorkflowFiles(client, projectDir, meta3.app_id, app.name, workflows, {
|
|
72143
|
+
overwrite: args.force === true,
|
|
72144
|
+
kept
|
|
72145
|
+
});
|
|
71913
72146
|
console.error(
|
|
71914
72147
|
`Wrote ${written.length} workflow ${written.length === 1 ? "body" : "bodies"} to ${WORKFLOWS_DIR}/` + (written.length > 0 ? ` (${written.join(", ")})` : "")
|
|
71915
72148
|
);
|
|
72149
|
+
reportKeptLocalFiles(kept, meta3.app_id);
|
|
71916
72150
|
ensureAppTsconfig(projectDir);
|
|
71917
72151
|
}
|
|
71918
72152
|
async function appWorkflowCheck(args) {
|
|
@@ -71995,6 +72229,7 @@ ${totalErrors} error${totalErrors === 1 ? "" : "s"} across ${results.length - pa
|
|
|
71995
72229
|
function parseArgs(argv) {
|
|
71996
72230
|
const flags = {
|
|
71997
72231
|
json: false,
|
|
72232
|
+
force: false,
|
|
71998
72233
|
timeout: void 0,
|
|
71999
72234
|
output: void 0,
|
|
72000
72235
|
as: void 0,
|
|
@@ -72027,6 +72262,9 @@ function parseArgs(argv) {
|
|
|
72027
72262
|
case "--json":
|
|
72028
72263
|
flags.json = true;
|
|
72029
72264
|
break;
|
|
72265
|
+
case "--force":
|
|
72266
|
+
flags.force = true;
|
|
72267
|
+
break;
|
|
72030
72268
|
case "--timeout":
|
|
72031
72269
|
flags.timeout = parseInt(argv[++i2], 10);
|
|
72032
72270
|
break;
|
|
@@ -100725,17 +100963,11 @@ Available workspaces:`);
|
|
|
100725
100963
|
process.exit(1);
|
|
100726
100964
|
}
|
|
100727
100965
|
const targetPath = restArgs[0];
|
|
100728
|
-
await appPull(client, { app_id: appId, targetPath });
|
|
100966
|
+
await appPull(client, { app_id: appId, targetPath, force: flags.force === true });
|
|
100729
100967
|
return;
|
|
100730
100968
|
}
|
|
100731
100969
|
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 });
|
|
100970
|
+
await appDeploy(client, { message: (flags.message ?? toolArgs)?.trim() || void 0 });
|
|
100739
100971
|
return;
|
|
100740
100972
|
}
|
|
100741
100973
|
if (subcommand === "subdomain") {
|
|
@@ -100816,7 +101048,7 @@ Available workspaces:`);
|
|
|
100816
101048
|
return;
|
|
100817
101049
|
}
|
|
100818
101050
|
if (action === "pull") {
|
|
100819
|
-
await appWorkflowPull(client);
|
|
101051
|
+
await appWorkflowPull(client, { force: flags.force === true });
|
|
100820
101052
|
return;
|
|
100821
101053
|
}
|
|
100822
101054
|
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`. |
|