@lotics/cli 0.45.2 → 0.47.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/app_commands.d.ts +6 -1
- package/dist/app_commands.js +59 -10
- package/dist/app_commands.test.js +29 -1
- package/dist/args.d.ts +2 -3
- package/dist/args.js +2 -6
- package/dist/args.test.js +3 -14
- package/dist/cli.js +6 -10
- package/dist/client.d.ts +0 -21
- package/dist/client.js +2 -10
- package/dist/src/cli.js +47 -26
- package/package.json +1 -1
package/dist/app_commands.d.ts
CHANGED
|
@@ -102,6 +102,12 @@ export declare function appPull(client: LoticsClient, args: {
|
|
|
102
102
|
app_id: string;
|
|
103
103
|
targetPath?: string;
|
|
104
104
|
}): Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* Capabilities the source CALLS but the manifest does not DECLARE — each one
|
|
107
|
+
* silently 403s at runtime (GAP-29). Pure over the concatenated source text so
|
|
108
|
+
* it's unit-testable; the deploy warns (non-blocking) on a non-empty result.
|
|
109
|
+
*/
|
|
110
|
+
export declare function undeclaredCapabilities(sourceText: string, declared: Record<string, boolean | undefined> | undefined): string[];
|
|
105
111
|
/**
|
|
106
112
|
* `lotics app deploy [-m <message>]`
|
|
107
113
|
*
|
|
@@ -113,7 +119,6 @@ export declare function appPull(client: LoticsClient, args: {
|
|
|
113
119
|
export declare function appDeploy(client: LoticsClient, args: {
|
|
114
120
|
projectDir?: string;
|
|
115
121
|
message?: string;
|
|
116
|
-
forceWorkflowSync?: boolean;
|
|
117
122
|
}): Promise<void>;
|
|
118
123
|
/**
|
|
119
124
|
* `lotics app dev [path] [--port=5174] [--vite-port=5173]`
|
package/dist/app_commands.js
CHANGED
|
@@ -289,6 +289,48 @@ export async function appPull(client, args) {
|
|
|
289
289
|
console.error(` # edit src/App.tsx`);
|
|
290
290
|
console.error(` lotics app deploy`);
|
|
291
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* SDK calls that only work when the matching capability is declared in
|
|
294
|
+
* `package.json#lotics.capabilities`. Used by the deploy pre-flight (GAP-29):
|
|
295
|
+
* code that calls one of these but omits the capability silently 403s at
|
|
296
|
+
* runtime, with no build or deploy error. Extend this map when a new capability
|
|
297
|
+
* gate ships in `@lotics/app-sdk`.
|
|
298
|
+
*/
|
|
299
|
+
const CAPABILITY_GATED_CALLS = {
|
|
300
|
+
comments: ["useComments", "createComment", "updateComment", "deleteComment"],
|
|
301
|
+
};
|
|
302
|
+
/**
|
|
303
|
+
* Capabilities the source CALLS but the manifest does not DECLARE — each one
|
|
304
|
+
* silently 403s at runtime (GAP-29). Pure over the concatenated source text so
|
|
305
|
+
* it's unit-testable; the deploy warns (non-blocking) on a non-empty result.
|
|
306
|
+
*/
|
|
307
|
+
export function undeclaredCapabilities(sourceText, declared) {
|
|
308
|
+
const used = [];
|
|
309
|
+
for (const [capability, calls] of Object.entries(CAPABILITY_GATED_CALLS)) {
|
|
310
|
+
const isCalled = calls.some((call) => new RegExp(`\\b${call}\\b`).test(sourceText));
|
|
311
|
+
if (isCalled && declared?.[capability] !== true)
|
|
312
|
+
used.push(capability);
|
|
313
|
+
}
|
|
314
|
+
return used;
|
|
315
|
+
}
|
|
316
|
+
/** Concatenated text of the app's `src/` files — the input to the capability pre-flight. */
|
|
317
|
+
function readAppSourceText(projectDir) {
|
|
318
|
+
const srcDir = path.join(projectDir, "src");
|
|
319
|
+
if (!fs.existsSync(srcDir))
|
|
320
|
+
return "";
|
|
321
|
+
const parts = [];
|
|
322
|
+
const walk = (dir) => {
|
|
323
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
324
|
+
const full = path.join(dir, entry.name);
|
|
325
|
+
if (entry.isDirectory())
|
|
326
|
+
walk(full);
|
|
327
|
+
else if (/\.(ts|tsx|js|jsx)$/.test(entry.name))
|
|
328
|
+
parts.push(fs.readFileSync(full, "utf8"));
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
walk(srcDir);
|
|
332
|
+
return parts.join("\n");
|
|
333
|
+
}
|
|
292
334
|
/**
|
|
293
335
|
* `lotics app deploy [-m <message>]`
|
|
294
336
|
*
|
|
@@ -300,6 +342,16 @@ export async function appPull(client, args) {
|
|
|
300
342
|
export async function appDeploy(client, args) {
|
|
301
343
|
const projectDir = path.resolve(args.projectDir ?? process.cwd());
|
|
302
344
|
const meta = readAppMeta(projectDir);
|
|
345
|
+
// Pre-flight (GAP-29): a capability the code calls but the manifest doesn't
|
|
346
|
+
// declare silently 403s at runtime with no other signal — warn before shipping
|
|
347
|
+
// it. Non-blocking, like the unbranded nudge below.
|
|
348
|
+
const undeclared = undeclaredCapabilities(readAppSourceText(projectDir), meta.capabilities);
|
|
349
|
+
if (undeclared.length > 0) {
|
|
350
|
+
const block = JSON.stringify(Object.fromEntries(undeclared.map((c) => [c, true])));
|
|
351
|
+
console.error(`\n⚠ This app calls capability-gated SDK functions for ${undeclared.join(", ")} but the ` +
|
|
352
|
+
`manifest doesn't declare ${undeclared.length > 1 ? "them" : "it"} — those calls will 403 at runtime.\n` +
|
|
353
|
+
` Add to package.json#lotics.capabilities: ${block}`);
|
|
354
|
+
}
|
|
303
355
|
// Regenerate AppWorkflows typing before the build picks up source. Keeps
|
|
304
356
|
// .lotics/app_workflows.d.ts in sync with the manifest's workflows map
|
|
305
357
|
// every time the developer ships.
|
|
@@ -336,20 +388,17 @@ export async function appDeploy(client, args) {
|
|
|
336
388
|
dist_archive: fs.readFileSync(tmpDist),
|
|
337
389
|
prev_version_id: meta.current_version_id,
|
|
338
390
|
message: args.message,
|
|
339
|
-
// Sync apps.workflows from the manifest. Server validates each
|
|
340
|
-
// workflow_id exists in the workspace before committing.
|
|
341
|
-
workflows: meta.workflows ?? {},
|
|
342
391
|
// Sync apps.queries from the manifest. Server validates each query
|
|
343
392
|
// template (parseQueryNode, table access, param coverage).
|
|
344
393
|
queries: meta.queries ?? {},
|
|
345
|
-
// Capabilities are manifest-authoritative (like
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
394
|
+
// Capabilities are manifest-authoritative (like queries): always send,
|
|
395
|
+
// defaulting to `{}` when the manifest declares none — so deleting the
|
|
396
|
+
// `capabilities` block turns every capability OFF on the next deploy
|
|
397
|
+
// (fail-safe; the declaration is the grant).
|
|
349
398
|
capabilities: meta.capabilities ?? {},
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
|
|
399
|
+
// Workflow bindings are NOT a deploy concern — set_app_workflow /
|
|
400
|
+
// remove_app_workflow own apps.workflows. The manifest's `workflows`
|
|
401
|
+
// map is a pulled reflection used only for the .d.ts codegen above.
|
|
353
402
|
});
|
|
354
403
|
writeAppMeta(projectDir, {
|
|
355
404
|
...meta,
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
|
-
import { stampPulledManifest } from "./app_commands.js";
|
|
5
|
+
import { stampPulledManifest, undeclaredCapabilities } from "./app_commands.js";
|
|
6
6
|
/**
|
|
7
7
|
* `appPull` reads workflows from the live App row (server response), NOT from
|
|
8
8
|
* the manifest embedded in the extracted source archive. The frozen archive
|
|
@@ -104,3 +104,31 @@ describe("stampPulledManifest", () => {
|
|
|
104
104
|
expect(dts).toContain("only_live");
|
|
105
105
|
});
|
|
106
106
|
});
|
|
107
|
+
/**
|
|
108
|
+
* The deploy pre-flight that catches GAP-29: source that CALLS a capability-gated
|
|
109
|
+
* SDK function while the manifest omits that capability would silently 403 at
|
|
110
|
+
* runtime. `undeclaredCapabilities` is the pure decision the warning is built on.
|
|
111
|
+
*/
|
|
112
|
+
describe("undeclaredCapabilities", () => {
|
|
113
|
+
const commentsSource = `import { useComments } from "@lotics/app-sdk";\nconst { available } = useComments({ record_id });`;
|
|
114
|
+
it("flags a capability that's called but not declared", () => {
|
|
115
|
+
expect(undeclaredCapabilities(commentsSource, undefined)).toEqual(["comments"]);
|
|
116
|
+
expect(undeclaredCapabilities(commentsSource, {})).toEqual(["comments"]);
|
|
117
|
+
expect(undeclaredCapabilities(commentsSource, { comments: false })).toEqual(["comments"]);
|
|
118
|
+
});
|
|
119
|
+
it("stays quiet when the called capability is declared", () => {
|
|
120
|
+
expect(undeclaredCapabilities(commentsSource, { comments: true })).toEqual([]);
|
|
121
|
+
});
|
|
122
|
+
it("stays quiet when the capability isn't used at all", () => {
|
|
123
|
+
expect(undeclaredCapabilities(`const x = createInvoice();`, undefined)).toEqual([]);
|
|
124
|
+
expect(undeclaredCapabilities("", { comments: true })).toEqual([]);
|
|
125
|
+
});
|
|
126
|
+
it("matches on whole identifiers, not substrings", () => {
|
|
127
|
+
// `myUseCommentsHelper` is not a call to the gated `useComments`.
|
|
128
|
+
expect(undeclaredCapabilities(`const x = myUseCommentsHelper();`, undefined)).toEqual([]);
|
|
129
|
+
});
|
|
130
|
+
it("detects any of a capability's gated calls", () => {
|
|
131
|
+
expect(undeclaredCapabilities(`await createComment({ body });`, {})).toEqual(["comments"]);
|
|
132
|
+
expect(undeclaredCapabilities(`await deleteComment(id);`, {})).toEqual(["comments"]);
|
|
133
|
+
});
|
|
134
|
+
});
|
package/dist/args.d.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Splits `process.argv.slice(2)` into a command / subcommand / positional
|
|
5
5
|
* (`toolArgs`) / remaining-positionals (`restArgs`) shape plus typed `flags`.
|
|
6
6
|
* Value-taking flags (`-m`, `--api-key`, …) consume the next token; boolean
|
|
7
|
-
* flags (`--
|
|
8
|
-
*
|
|
7
|
+
* flags (`--json`, `--local`, …) toggle. Anything not matching a known flag is
|
|
8
|
+
* positional.
|
|
9
9
|
*
|
|
10
10
|
* Lives apart from `cli.ts` so it can be unit-tested — `cli.ts` runs `main()`
|
|
11
11
|
* on import, so importing the parser from there would execute the CLI.
|
|
@@ -26,7 +26,6 @@ export declare function parseArgs(argv: string[]): {
|
|
|
26
26
|
name?: string;
|
|
27
27
|
timezone?: string;
|
|
28
28
|
message?: string;
|
|
29
|
-
forceWorkflowSync: boolean;
|
|
30
29
|
local: boolean;
|
|
31
30
|
all: boolean;
|
|
32
31
|
version: boolean;
|
package/dist/args.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Splits `process.argv.slice(2)` into a command / subcommand / positional
|
|
5
5
|
* (`toolArgs`) / remaining-positionals (`restArgs`) shape plus typed `flags`.
|
|
6
6
|
* Value-taking flags (`-m`, `--api-key`, …) consume the next token; boolean
|
|
7
|
-
* flags (`--
|
|
8
|
-
*
|
|
7
|
+
* flags (`--json`, `--local`, …) toggle. Anything not matching a known flag is
|
|
8
|
+
* positional.
|
|
9
9
|
*
|
|
10
10
|
* Lives apart from `cli.ts` so it can be unit-tested — `cli.ts` runs `main()`
|
|
11
11
|
* on import, so importing the parser from there would execute the CLI.
|
|
@@ -22,7 +22,6 @@ export function parseArgs(argv) {
|
|
|
22
22
|
name: undefined,
|
|
23
23
|
timezone: undefined,
|
|
24
24
|
message: undefined,
|
|
25
|
-
forceWorkflowSync: false,
|
|
26
25
|
local: false,
|
|
27
26
|
all: false,
|
|
28
27
|
version: false,
|
|
@@ -69,9 +68,6 @@ export function parseArgs(argv) {
|
|
|
69
68
|
case "--message":
|
|
70
69
|
flags.message = argv[++i];
|
|
71
70
|
break;
|
|
72
|
-
case "--force-workflow-sync":
|
|
73
|
-
flags.forceWorkflowSync = true;
|
|
74
|
-
break;
|
|
75
71
|
case "--local":
|
|
76
72
|
flags.local = true;
|
|
77
73
|
break;
|
package/dist/args.test.js
CHANGED
|
@@ -8,8 +8,8 @@ describe("parseArgs", () => {
|
|
|
8
8
|
expect(r.toolArgs).toBe("my message");
|
|
9
9
|
expect(r.restArgs).toEqual([]);
|
|
10
10
|
});
|
|
11
|
-
// `app deploy` regression: `-m`
|
|
12
|
-
//
|
|
11
|
+
// `app deploy` regression: `-m` must be parsed as a flag, not silently
|
|
12
|
+
// consumed as the positional message.
|
|
13
13
|
it("parses -m as the message flag", () => {
|
|
14
14
|
const r = parseArgs(["app", "deploy", "-m", "a message"]);
|
|
15
15
|
expect(r.flags.message).toBe("a message");
|
|
@@ -19,19 +19,8 @@ describe("parseArgs", () => {
|
|
|
19
19
|
const r = parseArgs(["app", "deploy", "--message", "a message"]);
|
|
20
20
|
expect(r.flags.message).toBe("a message");
|
|
21
21
|
});
|
|
22
|
-
it("
|
|
23
|
-
const r = parseArgs(["app", "deploy", "msg", "--force-workflow-sync"]);
|
|
24
|
-
expect(r.flags.forceWorkflowSync).toBe(true);
|
|
25
|
-
expect(r.toolArgs).toBe("msg");
|
|
26
|
-
});
|
|
27
|
-
it("parses --force-workflow-sync before -m without eating the message", () => {
|
|
28
|
-
const r = parseArgs(["app", "deploy", "--force-workflow-sync", "-m", "msg"]);
|
|
29
|
-
expect(r.flags.forceWorkflowSync).toBe(true);
|
|
30
|
-
expect(r.flags.message).toBe("msg");
|
|
31
|
-
});
|
|
32
|
-
it("defaults forceWorkflowSync to false and message to undefined", () => {
|
|
22
|
+
it("defaults message to undefined for a bare deploy", () => {
|
|
33
23
|
const r = parseArgs(["app", "deploy"]);
|
|
34
|
-
expect(r.flags.forceWorkflowSync).toBe(false);
|
|
35
24
|
expect(r.flags.message).toBeUndefined();
|
|
36
25
|
});
|
|
37
26
|
it("parses --workspace as a value flag", () => {
|
package/dist/cli.js
CHANGED
|
@@ -60,10 +60,9 @@ COMMANDS
|
|
|
60
60
|
Download all files on a record file field
|
|
61
61
|
lotics app create <name> [path] Create a new custom-code app + scaffold locally
|
|
62
62
|
lotics app pull <app_id> [path] Bootstrap full local env (source + npm install + types)
|
|
63
|
-
lotics app deploy [-m <message>]
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
workflow aliases; default refuses)
|
|
63
|
+
lotics app deploy [-m <message>] Build + upload current dir as a new version
|
|
64
|
+
(code + queries only — workflow bindings are
|
|
65
|
+
managed by set_app_workflow / remove_app_workflow)
|
|
67
66
|
lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address
|
|
68
67
|
lotics app rename "<new name>" Rename the app's display name (launcher title)
|
|
69
68
|
lotics app dev [path] Run the app locally with HMR (RPC forwarded to prod)
|
|
@@ -527,8 +526,7 @@ async function main() {
|
|
|
527
526
|
console.error("Usage:");
|
|
528
527
|
console.error(" lotics app create <name> [path] Scaffold a new app locally");
|
|
529
528
|
console.error(" lotics app pull <app_id> [path] Pull an existing app for local editing");
|
|
530
|
-
console.error(" lotics app deploy [-m <message>]
|
|
531
|
-
console.error(" Build + upload the current directory");
|
|
529
|
+
console.error(" lotics app deploy [-m <message>] Build + upload the current directory");
|
|
532
530
|
console.error(" lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address");
|
|
533
531
|
console.error(" lotics app rename \"<new name>\" Rename the app's display name (launcher title)");
|
|
534
532
|
console.error(" lotics app dev [path] Run the app locally with HMR + RPC forwarding");
|
|
@@ -650,11 +648,9 @@ async function main() {
|
|
|
650
648
|
return;
|
|
651
649
|
}
|
|
652
650
|
if (subcommand === "deploy") {
|
|
653
|
-
// The message is either `-m <message>` or a bare positional arg after
|
|
654
|
-
// `deploy`. `--force-workflow-sync` is a parsed boolean flag, valid in
|
|
655
|
-
// any position.
|
|
651
|
+
// The message is either `-m <message>` or a bare positional arg after `deploy`.
|
|
656
652
|
const message = flags.message ?? toolArgs;
|
|
657
|
-
await appDeploy(client, { message
|
|
653
|
+
await appDeploy(client, { message });
|
|
658
654
|
return;
|
|
659
655
|
}
|
|
660
656
|
if (subcommand === "subdomain") {
|
package/dist/client.d.ts
CHANGED
|
@@ -235,19 +235,6 @@ export declare class LoticsClient {
|
|
|
235
235
|
dist_archive: Buffer;
|
|
236
236
|
prev_version_id?: string | null;
|
|
237
237
|
message?: string | null;
|
|
238
|
-
/**
|
|
239
|
-
* Alias → workflow declaration map from the app's `package.json#lotics.workflows`.
|
|
240
|
-
* Each value is a `{ workflow_id, inputs? }` object — inputs declares a typed
|
|
241
|
-
* schema or is omitted when the workflow accepts no typed inputs. Always sent
|
|
242
|
-
* (empty object when none declared) so the server can overwrite apps.workflows
|
|
243
|
-
* authoritatively. Deleting an alias from the manifest removes it from the DB
|
|
244
|
-
* on next deploy — gated by the destructive-removal guard unless
|
|
245
|
-
* `force_workflow_sync` is true.
|
|
246
|
-
*/
|
|
247
|
-
workflows?: Record<string, {
|
|
248
|
-
workflow_id: string;
|
|
249
|
-
inputs?: Record<string, unknown>;
|
|
250
|
-
}>;
|
|
251
238
|
/**
|
|
252
239
|
* Alias → query declaration map from `package.json#lotics.queries`. Each
|
|
253
240
|
* value is `{ ast, params? }` — a fixed query AST template and an optional
|
|
@@ -267,14 +254,6 @@ export declare class LoticsClient {
|
|
|
267
254
|
capabilities?: {
|
|
268
255
|
comments?: boolean;
|
|
269
256
|
};
|
|
270
|
-
/**
|
|
271
|
-
* Opt into destructive workflow removal. When false/absent, the server
|
|
272
|
-
* rejects a deploy whose `workflows` map is missing aliases that exist
|
|
273
|
-
* on the App row. Set true to deploy anyway and wipe the missing aliases —
|
|
274
|
-
* the user is asserting they know what they're doing. Wired to CLI flag
|
|
275
|
-
* `--force-workflow-sync`.
|
|
276
|
-
*/
|
|
277
|
-
force_workflow_sync?: boolean;
|
|
278
257
|
}): Promise<{
|
|
279
258
|
version_id: string;
|
|
280
259
|
version_number: number;
|
package/dist/client.js
CHANGED
|
@@ -239,11 +239,9 @@ export class LoticsClient {
|
|
|
239
239
|
if (args.message) {
|
|
240
240
|
formData.append("message", args.message);
|
|
241
241
|
}
|
|
242
|
-
// Always send workflows — empty object is meaningful (clears any
|
|
243
|
-
// previously-declared aliases). Server validates each entry.
|
|
244
|
-
formData.append("workflows", JSON.stringify(args.workflows ?? {}));
|
|
245
242
|
// Always send queries — empty object clears any previously-declared
|
|
246
|
-
// named queries. Server validates each template.
|
|
243
|
+
// named queries. Server validates each template. (Workflow bindings are
|
|
244
|
+
// not sent: set_app_workflow / remove_app_workflow own apps.workflows.)
|
|
247
245
|
formData.append("queries", JSON.stringify(args.queries ?? {}));
|
|
248
246
|
// capabilities is manifest-authoritative — the caller always passes it
|
|
249
247
|
// (`{}` when none declared), so a deploy turns off any capability the
|
|
@@ -251,12 +249,6 @@ export class LoticsClient {
|
|
|
251
249
|
if (args.capabilities !== undefined) {
|
|
252
250
|
formData.append("capabilities", JSON.stringify(args.capabilities));
|
|
253
251
|
}
|
|
254
|
-
// Only post the override flag when the user explicitly opts in. The
|
|
255
|
-
// server defaults to "honor the guard" — opt-in is loud, opt-out
|
|
256
|
-
// requires intent.
|
|
257
|
-
if (args.force_workflow_sync) {
|
|
258
|
-
formData.append("force_workflow_sync", "true");
|
|
259
|
-
}
|
|
260
252
|
const url = `${this.baseUrl}/v1/apps/${encodeURIComponent(args.app_id)}/versions`;
|
|
261
253
|
const response = await fetch(url, {
|
|
262
254
|
method: "POST",
|
package/dist/src/cli.js
CHANGED
|
@@ -29858,14 +29858,10 @@ var LoticsClient = class {
|
|
|
29858
29858
|
if (args.message) {
|
|
29859
29859
|
formData.append("message", args.message);
|
|
29860
29860
|
}
|
|
29861
|
-
formData.append("workflows", JSON.stringify(args.workflows ?? {}));
|
|
29862
29861
|
formData.append("queries", JSON.stringify(args.queries ?? {}));
|
|
29863
29862
|
if (args.capabilities !== void 0) {
|
|
29864
29863
|
formData.append("capabilities", JSON.stringify(args.capabilities));
|
|
29865
29864
|
}
|
|
29866
|
-
if (args.force_workflow_sync) {
|
|
29867
|
-
formData.append("force_workflow_sync", "true");
|
|
29868
|
-
}
|
|
29869
29865
|
const url = `${this.baseUrl}/v1/apps/${encodeURIComponent(args.app_id)}/versions`;
|
|
29870
29866
|
const response = await fetch(url, {
|
|
29871
29867
|
method: "POST",
|
|
@@ -31570,9 +31566,43 @@ Ready. Next steps:`);
|
|
|
31570
31566
|
console.error(` # edit src/App.tsx`);
|
|
31571
31567
|
console.error(` lotics app deploy`);
|
|
31572
31568
|
}
|
|
31569
|
+
var CAPABILITY_GATED_CALLS = {
|
|
31570
|
+
comments: ["useComments", "createComment", "updateComment", "deleteComment"]
|
|
31571
|
+
};
|
|
31572
|
+
function undeclaredCapabilities(sourceText, declared) {
|
|
31573
|
+
const used = [];
|
|
31574
|
+
for (const [capability, calls] of Object.entries(CAPABILITY_GATED_CALLS)) {
|
|
31575
|
+
const isCalled = calls.some((call) => new RegExp(`\\b${call}\\b`).test(sourceText));
|
|
31576
|
+
if (isCalled && declared?.[capability] !== true) used.push(capability);
|
|
31577
|
+
}
|
|
31578
|
+
return used;
|
|
31579
|
+
}
|
|
31580
|
+
function readAppSourceText(projectDir) {
|
|
31581
|
+
const srcDir = path4.join(projectDir, "src");
|
|
31582
|
+
if (!fs3.existsSync(srcDir)) return "";
|
|
31583
|
+
const parts = [];
|
|
31584
|
+
const walk = (dir) => {
|
|
31585
|
+
for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
|
|
31586
|
+
const full = path4.join(dir, entry.name);
|
|
31587
|
+
if (entry.isDirectory()) walk(full);
|
|
31588
|
+
else if (/\.(ts|tsx|js|jsx)$/.test(entry.name)) parts.push(fs3.readFileSync(full, "utf8"));
|
|
31589
|
+
}
|
|
31590
|
+
};
|
|
31591
|
+
walk(srcDir);
|
|
31592
|
+
return parts.join("\n");
|
|
31593
|
+
}
|
|
31573
31594
|
async function appDeploy(client, args) {
|
|
31574
31595
|
const projectDir = path4.resolve(args.projectDir ?? process.cwd());
|
|
31575
31596
|
const meta = readAppMeta(projectDir);
|
|
31597
|
+
const undeclared = undeclaredCapabilities(readAppSourceText(projectDir), meta.capabilities);
|
|
31598
|
+
if (undeclared.length > 0) {
|
|
31599
|
+
const block = JSON.stringify(Object.fromEntries(undeclared.map((c) => [c, true])));
|
|
31600
|
+
console.error(
|
|
31601
|
+
`
|
|
31602
|
+
\u26A0 This app calls capability-gated SDK functions for ${undeclared.join(", ")} but the manifest doesn't declare ${undeclared.length > 1 ? "them" : "it"} \u2014 those calls will 403 at runtime.
|
|
31603
|
+
Add to package.json#lotics.capabilities: ${block}`
|
|
31604
|
+
);
|
|
31605
|
+
}
|
|
31576
31606
|
writeAppDts(projectDir, { workflows: meta.workflows, queries: meta.queries });
|
|
31577
31607
|
console.error("Building...");
|
|
31578
31608
|
await runNpm(["run", "build"], projectDir);
|
|
@@ -31609,20 +31639,17 @@ async function appDeploy(client, args) {
|
|
|
31609
31639
|
dist_archive: fs3.readFileSync(tmpDist),
|
|
31610
31640
|
prev_version_id: meta.current_version_id,
|
|
31611
31641
|
message: args.message,
|
|
31612
|
-
// Sync apps.workflows from the manifest. Server validates each
|
|
31613
|
-
// workflow_id exists in the workspace before committing.
|
|
31614
|
-
workflows: meta.workflows ?? {},
|
|
31615
31642
|
// Sync apps.queries from the manifest. Server validates each query
|
|
31616
31643
|
// template (parseQueryNode, table access, param coverage).
|
|
31617
31644
|
queries: meta.queries ?? {},
|
|
31618
|
-
// Capabilities are manifest-authoritative (like
|
|
31619
|
-
//
|
|
31620
|
-
//
|
|
31621
|
-
//
|
|
31622
|
-
capabilities: meta.capabilities ?? {}
|
|
31623
|
-
//
|
|
31624
|
-
//
|
|
31625
|
-
|
|
31645
|
+
// Capabilities are manifest-authoritative (like queries): always send,
|
|
31646
|
+
// defaulting to `{}` when the manifest declares none — so deleting the
|
|
31647
|
+
// `capabilities` block turns every capability OFF on the next deploy
|
|
31648
|
+
// (fail-safe; the declaration is the grant).
|
|
31649
|
+
capabilities: meta.capabilities ?? {}
|
|
31650
|
+
// Workflow bindings are NOT a deploy concern — set_app_workflow /
|
|
31651
|
+
// remove_app_workflow own apps.workflows. The manifest's `workflows`
|
|
31652
|
+
// map is a pulled reflection used only for the .d.ts codegen above.
|
|
31626
31653
|
});
|
|
31627
31654
|
writeAppMeta(projectDir, {
|
|
31628
31655
|
...meta,
|
|
@@ -31722,7 +31749,6 @@ function parseArgs(argv) {
|
|
|
31722
31749
|
name: void 0,
|
|
31723
31750
|
timezone: void 0,
|
|
31724
31751
|
message: void 0,
|
|
31725
|
-
forceWorkflowSync: false,
|
|
31726
31752
|
local: false,
|
|
31727
31753
|
all: false,
|
|
31728
31754
|
version: false,
|
|
@@ -31769,9 +31795,6 @@ function parseArgs(argv) {
|
|
|
31769
31795
|
case "--message":
|
|
31770
31796
|
flags.message = argv[++i2];
|
|
31771
31797
|
break;
|
|
31772
|
-
case "--force-workflow-sync":
|
|
31773
|
-
flags.forceWorkflowSync = true;
|
|
31774
|
-
break;
|
|
31775
31798
|
case "--local":
|
|
31776
31799
|
flags.local = true;
|
|
31777
31800
|
break;
|
|
@@ -47561,10 +47584,9 @@ COMMANDS
|
|
|
47561
47584
|
Download all files on a record file field
|
|
47562
47585
|
lotics app create <name> [path] Create a new custom-code app + scaffold locally
|
|
47563
47586
|
lotics app pull <app_id> [path] Bootstrap full local env (source + npm install + types)
|
|
47564
|
-
lotics app deploy [-m <message>]
|
|
47565
|
-
|
|
47566
|
-
|
|
47567
|
-
workflow aliases; default refuses)
|
|
47587
|
+
lotics app deploy [-m <message>] Build + upload current dir as a new version
|
|
47588
|
+
(code + queries only \u2014 workflow bindings are
|
|
47589
|
+
managed by set_app_workflow / remove_app_workflow)
|
|
47568
47590
|
lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address
|
|
47569
47591
|
lotics app rename "<new name>" Rename the app's display name (launcher title)
|
|
47570
47592
|
lotics app dev [path] Run the app locally with HMR (RPC forwarded to prod)
|
|
@@ -47993,8 +48015,7 @@ async function main() {
|
|
|
47993
48015
|
console.error("Usage:");
|
|
47994
48016
|
console.error(" lotics app create <name> [path] Scaffold a new app locally");
|
|
47995
48017
|
console.error(" lotics app pull <app_id> [path] Pull an existing app for local editing");
|
|
47996
|
-
console.error(" lotics app deploy [-m <message>]
|
|
47997
|
-
console.error(" Build + upload the current directory");
|
|
48018
|
+
console.error(" lotics app deploy [-m <message>] Build + upload the current directory");
|
|
47998
48019
|
console.error(" lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address");
|
|
47999
48020
|
console.error(` lotics app rename "<new name>" Rename the app's display name (launcher title)`);
|
|
48000
48021
|
console.error(" lotics app dev [path] Run the app locally with HMR + RPC forwarding");
|
|
@@ -48111,7 +48132,7 @@ Available workspaces:`);
|
|
|
48111
48132
|
}
|
|
48112
48133
|
if (subcommand === "deploy") {
|
|
48113
48134
|
const message = flags.message ?? toolArgs;
|
|
48114
|
-
await appDeploy(client, { message
|
|
48135
|
+
await appDeploy(client, { message });
|
|
48115
48136
|
return;
|
|
48116
48137
|
}
|
|
48117
48138
|
if (subcommand === "subdomain") {
|