@lotics/cli 0.39.0 → 0.41.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.
@@ -92,10 +92,7 @@ function readAppMeta(projectDir) {
92
92
  version_number: pkg.lotics.version_number ?? null,
93
93
  workflows: pkg.lotics.workflows ?? {},
94
94
  queries: pkg.lotics.queries ?? {},
95
- // Left as `undefined` when absent so the deploy omits them and the
96
- // App row's icon/theme is preserved.
97
- icon: pkg.lotics.icon,
98
- theme: pkg.lotics.theme,
95
+ capabilities: pkg.lotics.capabilities,
99
96
  };
100
97
  }
101
98
  function writeAppMeta(projectDir, meta) {
@@ -345,10 +342,11 @@ export async function appDeploy(client, args) {
345
342
  // Sync apps.queries from the manifest. Server validates each query
346
343
  // template (parseQueryNode, table access, param coverage).
347
344
  queries: meta.queries ?? {},
348
- // Optional icon/theme overrides only sent when the manifest
349
- // declares them (undefined omitted App row left untouched).
350
- icon: meta.icon,
351
- theme: meta.theme,
345
+ // Capabilities are manifest-authoritative (like workflows/queries):
346
+ // always send, defaulting to `{}` when the manifest declares none — so
347
+ // deleting the `capabilities` block turns every capability OFF on the
348
+ // next deploy (fail-safe; the declaration is the grant).
349
+ capabilities: meta.capabilities ?? {},
352
350
  // Opt into destructive workflow removal. Default false — the server
353
351
  // rejects deploys whose manifest is missing aliases the App row has.
354
352
  force_workflow_sync: args.forceWorkflowSync,
@@ -360,6 +358,7 @@ export async function appDeploy(client, args) {
360
358
  });
361
359
  console.error(`Deployed v${result.version_number} (${result.version_id})`);
362
360
  console.error(`Bundle size: ${(result.bundle_size_bytes / 1024).toFixed(1)} KB`);
361
+ await warnIfUnbranded(client, meta.app_id);
363
362
  }
364
363
  catch (err) {
365
364
  const e = err;
@@ -378,6 +377,30 @@ export async function appDeploy(client, args) {
378
377
  fs.unlinkSync(tmpDist);
379
378
  }
380
379
  }
380
+ /**
381
+ * Non-blocking nudge after a successful deploy: an app with no icon/color shows a
382
+ * generic tile in the launcher. Branding is set via `update_app` (the single
383
+ * setter) — this only reminds; it never fails the deploy.
384
+ */
385
+ async function warnIfUnbranded(client, appId) {
386
+ try {
387
+ const app = await client.getApp(appId);
388
+ const missing = [];
389
+ if (!app.icon)
390
+ missing.push("icon");
391
+ if (!app.theme?.color)
392
+ missing.push("color");
393
+ if (missing.length === 0)
394
+ return;
395
+ console.error(`\n⚠ This app has no ${missing.join(" or ")} set — it shows a generic tile in the launcher.\n` +
396
+ ` Set it: lotics run update_app '{"app_id":"${appId}","icon":"<lucide-name>","theme":{"color":"blue"}}'\n` +
397
+ ` Find an icon: lotics run search_app_icons '{"query":"<word>"}'`);
398
+ }
399
+ catch (err) {
400
+ // Deploy already succeeded; a failed branding check must not mask that.
401
+ console.error(`(skipped branding check: ${err.message})`);
402
+ }
403
+ }
381
404
  /**
382
405
  * `lotics app dev [path] [--port=5174] [--vite-port=5173]`
383
406
  *
package/dist/client.d.ts CHANGED
@@ -96,6 +96,12 @@ export declare class LoticsClient {
96
96
  name: string;
97
97
  workspace_id: string;
98
98
  current_version_id: string | null;
99
+ /** Launcher icon — a Lucide name, or an image ref. Null when unset. */
100
+ icon?: string | null;
101
+ /** Launcher theme — `{ color }` is a palette token. Null when unset. */
102
+ theme?: {
103
+ color?: string | null;
104
+ } | null;
99
105
  /**
100
106
  * Live alias → workflow declaration map from `apps.workflows`. Source of
101
107
  * truth for `lotics app pull` — supersedes the manifest embedded in the
@@ -155,8 +161,10 @@ export declare class LoticsClient {
155
161
  offset?: number;
156
162
  sort?: AppQuerySortKey[];
157
163
  filter?: AppQueryFilter;
164
+ count?: boolean;
158
165
  }): Promise<{
159
166
  rows: unknown[];
167
+ total?: number;
160
168
  }>;
161
169
  appMembers(app_id: string, group_id?: string): Promise<{
162
170
  members: Array<{
@@ -231,13 +239,14 @@ export declare class LoticsClient {
231
239
  params?: Record<string, unknown>;
232
240
  }>;
233
241
  /**
234
- * App icon (Lucide name) + theme `{ color }` from `package.json#lotics`.
235
- * `undefined` not sent the deploy leaves the App row's icon/theme
236
- * as-is. A present value is applied authoritatively. `icon` rides as a
237
- * raw form field; `theme` is an object, so it's JSON-encoded.
242
+ * Opt-in app capabilities from `package.json#lotics.capabilities`. The CLI
243
+ * sends this on every deploy (defaulting to `{}`): the manifest is
244
+ * authoritative, so an empty/absent block turns every capability off.
245
+ * `comments: true` enables the members-only `useComments` primitive.
238
246
  */
239
- icon?: string;
240
- theme?: Record<string, unknown>;
247
+ capabilities?: {
248
+ comments?: boolean;
249
+ };
241
250
  /**
242
251
  * Opt into destructive workflow removal. When false/absent, the server
243
252
  * rejects a deploy whose `workflows` map is missing aliases that exist
package/dist/client.js CHANGED
@@ -220,15 +220,11 @@ export class LoticsClient {
220
220
  // Always send queries — empty object clears any previously-declared
221
221
  // named queries. Server validates each template.
222
222
  formData.append("queries", JSON.stringify(args.queries ?? {}));
223
- // icon/theme are optional overrides only sent when the manifest
224
- // declares them, so a deploy never clobbers a value set in the UI.
225
- // `icon` is a plain string → raw field, like `message`. `theme` is an
226
- // object JSON-encoded, like `workflows`/`queries`.
227
- if (args.icon !== undefined) {
228
- formData.append("icon", args.icon);
229
- }
230
- if (args.theme !== undefined) {
231
- formData.append("theme", JSON.stringify(args.theme));
223
+ // capabilities is manifest-authoritativethe caller always passes it
224
+ // (`{}` when none declared), so a deploy turns off any capability the
225
+ // manifest no longer declares. JSON-encoded.
226
+ if (args.capabilities !== undefined) {
227
+ formData.append("capabilities", JSON.stringify(args.capabilities));
232
228
  }
233
229
  // Only post the override flag when the user explicitly opts in. The
234
230
  // server defaults to "honor the guard" — opt-in is loud, opt-out
@@ -32,6 +32,7 @@ export async function dispatchRpc(client, body) {
32
32
  offset: p.offset,
33
33
  sort: p.sort,
34
34
  filter: p.filter,
35
+ count: p.count,
35
36
  });
36
37
  }
37
38
  case "workflow": {