@lotics/cli 0.40.0 → 0.42.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.js +33 -13
- package/dist/args.d.ts +1 -0
- package/dist/args.js +4 -0
- package/dist/args.test.js +6 -0
- package/dist/cli.js +14 -2
- package/dist/client.d.ts +15 -8
- package/dist/client.js +7 -10
- package/dist/dev/rpc_handler.d.ts +8 -2
- package/dist/dev/rpc_handler.js +13 -1
- package/dist/dev/rpc_handler.test.d.ts +1 -0
- package/dist/dev/rpc_handler.test.js +28 -0
- package/dist/dev/server.d.ts +3 -0
- package/dist/dev/server.js +1 -1
- package/dist/src/cli.js +67 -28
- package/package.json +1 -1
package/dist/app_commands.js
CHANGED
|
@@ -92,10 +92,6 @@ 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,
|
|
99
95
|
capabilities: pkg.lotics.capabilities,
|
|
100
96
|
};
|
|
101
97
|
}
|
|
@@ -346,15 +342,10 @@ export async function appDeploy(client, args) {
|
|
|
346
342
|
// Sync apps.queries from the manifest. Server validates each query
|
|
347
343
|
// template (parseQueryNode, table access, param coverage).
|
|
348
344
|
queries: meta.queries ?? {},
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
// Capabilities are manifest-authoritative (like workflows/queries, not
|
|
354
|
-
// like icon/theme): always send, defaulting to `{}` when the manifest
|
|
355
|
-
// declares none — so deleting the `capabilities` block turns every
|
|
356
|
-
// capability OFF on the next deploy (fail-safe; the declaration is the
|
|
357
|
-
// grant). The manifest is the only writer of capabilities.
|
|
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).
|
|
358
349
|
capabilities: meta.capabilities ?? {},
|
|
359
350
|
// Opt into destructive workflow removal. Default false — the server
|
|
360
351
|
// rejects deploys whose manifest is missing aliases the App row has.
|
|
@@ -367,6 +358,7 @@ export async function appDeploy(client, args) {
|
|
|
367
358
|
});
|
|
368
359
|
console.error(`Deployed v${result.version_number} (${result.version_id})`);
|
|
369
360
|
console.error(`Bundle size: ${(result.bundle_size_bytes / 1024).toFixed(1)} KB`);
|
|
361
|
+
await warnIfUnbranded(client, meta.app_id);
|
|
370
362
|
}
|
|
371
363
|
catch (err) {
|
|
372
364
|
const e = err;
|
|
@@ -385,6 +377,30 @@ export async function appDeploy(client, args) {
|
|
|
385
377
|
fs.unlinkSync(tmpDist);
|
|
386
378
|
}
|
|
387
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
|
+
}
|
|
388
404
|
/**
|
|
389
405
|
* `lotics app dev [path] [--port=5174] [--vite-port=5173]`
|
|
390
406
|
*
|
|
@@ -418,12 +434,16 @@ export async function appDev(client, args) {
|
|
|
418
434
|
port: args.port,
|
|
419
435
|
vitePort: args.vitePort,
|
|
420
436
|
client,
|
|
437
|
+
commentsEnabled: meta.capabilities?.comments,
|
|
421
438
|
});
|
|
422
439
|
await handle.ready;
|
|
423
440
|
const url = `http://localhost:${handle.port}`;
|
|
424
441
|
console.error(`\n lotics app dev`);
|
|
425
442
|
console.error(` app: ${app.name} (${meta.app_id})`);
|
|
426
443
|
console.error(` workspace: ${meta.workspace_id}`);
|
|
444
|
+
if (client.viewAsMemberId) {
|
|
445
|
+
console.error(` view as: ${client.viewAsMemberId} (is_current_member resolves to this member)`);
|
|
446
|
+
}
|
|
427
447
|
console.error(` vite: http://localhost:${handle.vitePort}/`);
|
|
428
448
|
console.error(` open: ${url}`);
|
|
429
449
|
console.error(` rpc: ${client.baseUrl} (via Bearer API key)\n`);
|
package/dist/args.d.ts
CHANGED
package/dist/args.js
CHANGED
|
@@ -18,6 +18,7 @@ export function parseArgs(argv) {
|
|
|
18
18
|
as: undefined,
|
|
19
19
|
apiKey: undefined,
|
|
20
20
|
workspace: undefined,
|
|
21
|
+
viewAs: undefined,
|
|
21
22
|
name: undefined,
|
|
22
23
|
timezone: undefined,
|
|
23
24
|
message: undefined,
|
|
@@ -55,6 +56,9 @@ export function parseArgs(argv) {
|
|
|
55
56
|
case "-w":
|
|
56
57
|
flags.workspace = argv[++i];
|
|
57
58
|
break;
|
|
59
|
+
case "--view-as":
|
|
60
|
+
flags.viewAs = argv[++i];
|
|
61
|
+
break;
|
|
58
62
|
case "--name":
|
|
59
63
|
flags.name = argv[++i];
|
|
60
64
|
break;
|
package/dist/args.test.js
CHANGED
|
@@ -45,6 +45,12 @@ describe("parseArgs", () => {
|
|
|
45
45
|
const r = parseArgs(["run", "query_tables", "-w", "wsp_123"]);
|
|
46
46
|
expect(r.flags.workspace).toBe("wsp_123");
|
|
47
47
|
});
|
|
48
|
+
it("parses --view-as as a value flag", () => {
|
|
49
|
+
const r = parseArgs(["app", "dev", "--view-as", "mbr_abc"]);
|
|
50
|
+
expect(r.flags.viewAs).toBe("mbr_abc");
|
|
51
|
+
expect(r.command).toBe("app");
|
|
52
|
+
expect(r.subcommand).toBe("dev");
|
|
53
|
+
});
|
|
48
54
|
it("parses --all as a boolean flag", () => {
|
|
49
55
|
const r = parseArgs(["auth", "logout", "--all"]);
|
|
50
56
|
expect(r.flags.all).toBe(true);
|
package/dist/cli.js
CHANGED
|
@@ -81,6 +81,9 @@ FLAGS
|
|
|
81
81
|
--as <name> Override upload filename
|
|
82
82
|
--api-key <key> One-off API key (overrides saved config + env)
|
|
83
83
|
--workspace <id> One-off workspace override (alias: -w)
|
|
84
|
+
--view-as <id> Admin "View as": run every request as this member, so
|
|
85
|
+
is_current_member / row-scoping resolve to them (also
|
|
86
|
+
LOTICS_VIEW_AS env; admin key only; writes stay yours)
|
|
84
87
|
--local Pin the current directory (lotics org use / auth api-key)
|
|
85
88
|
--all (lotics auth logout) Remove every saved credential
|
|
86
89
|
--version Show version
|
|
@@ -279,7 +282,13 @@ function requireClient(flags) {
|
|
|
279
282
|
console.error('Not authenticated. Run "lotics auth signup", "lotics auth api-key <key>", or set LOTICS_API_KEY.');
|
|
280
283
|
process.exit(1);
|
|
281
284
|
}
|
|
282
|
-
|
|
285
|
+
// Admin "View as": flag wins over env. The server enforces admin-only; a
|
|
286
|
+
// non-admin key gets a clear 403 on the first request.
|
|
287
|
+
const viewAsMemberId = flags.viewAs ?? process.env.LOTICS_VIEW_AS;
|
|
288
|
+
return {
|
|
289
|
+
client: new LoticsClient({ apiKey: ctx.apiKey, workspaceId: ctx.workspaceId, viewAsMemberId }),
|
|
290
|
+
ctx,
|
|
291
|
+
};
|
|
283
292
|
}
|
|
284
293
|
const SOURCE_LABELS = {
|
|
285
294
|
flag: "--api-key flag",
|
|
@@ -378,8 +387,9 @@ async function main() {
|
|
|
378
387
|
const info = await client.whoami();
|
|
379
388
|
const existing = loadGlobalConfig() ?? {};
|
|
380
389
|
saveGlobalConfig({ ...existing, email: info.email });
|
|
390
|
+
const viewAs = flags.viewAs ?? process.env.LOTICS_VIEW_AS;
|
|
381
391
|
if (flags.json) {
|
|
382
|
-
console.log(JSON.stringify({ ...info, workspace_id: ctx.workspaceId ?? null, source: ctx.source }, null, 2));
|
|
392
|
+
console.log(JSON.stringify({ ...info, workspace_id: ctx.workspaceId ?? null, source: ctx.source, view_as: viewAs ?? null }, null, 2));
|
|
383
393
|
}
|
|
384
394
|
else {
|
|
385
395
|
console.log(`Name: ${info.name}`);
|
|
@@ -387,6 +397,8 @@ async function main() {
|
|
|
387
397
|
console.log(`Org: ${info.organization_name} (${info.organization_id})`);
|
|
388
398
|
console.log(`Workspace: ${ctx.workspaceId ?? "(none selected)"}`);
|
|
389
399
|
console.log(`Source: ${SOURCE_LABELS[ctx.source]}`);
|
|
400
|
+
if (viewAs)
|
|
401
|
+
console.log(`View as: ${viewAs} (admin preview; requires an admin key)`);
|
|
390
402
|
}
|
|
391
403
|
return;
|
|
392
404
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -22,6 +22,11 @@ export type AppQueryFilter = AppQueryFilterCondition | AppQueryFilterGroup;
|
|
|
22
22
|
export interface LoticsClientOptions {
|
|
23
23
|
apiKey: string;
|
|
24
24
|
workspaceId?: string;
|
|
25
|
+
/** Admin "View as": when set, every request carries `x-view-as-member-id`, so
|
|
26
|
+
* the backend evaluates IAM scoping (and `is_current_member`) as this member.
|
|
27
|
+
* Admin-only — the server rejects a non-admin key. Writes stay attributed to
|
|
28
|
+
* the key's owner. */
|
|
29
|
+
viewAsMemberId?: string;
|
|
25
30
|
}
|
|
26
31
|
export interface WorkspaceInfo {
|
|
27
32
|
id: string;
|
|
@@ -56,6 +61,9 @@ export declare const API_BASE_URL: string;
|
|
|
56
61
|
export declare class LoticsClient {
|
|
57
62
|
private apiKey;
|
|
58
63
|
private workspaceId;
|
|
64
|
+
/** The active "View as" target member id, if any. Read-only after
|
|
65
|
+
* construction — surfaced so `lotics app dev` can show it in the banner. */
|
|
66
|
+
readonly viewAsMemberId: string | undefined;
|
|
59
67
|
/** API URL the client is configured against. Read-only after construction.
|
|
60
68
|
* Surfaced for callers that need to display or log it (e.g., `lotics app dev`
|
|
61
69
|
* shows it in the banner). */
|
|
@@ -65,6 +73,7 @@ export declare class LoticsClient {
|
|
|
65
73
|
private buildHeaders;
|
|
66
74
|
private request;
|
|
67
75
|
whoami(): Promise<{
|
|
76
|
+
member_id: string;
|
|
68
77
|
email: string;
|
|
69
78
|
name: string;
|
|
70
79
|
organization_id: string;
|
|
@@ -96,6 +105,12 @@ export declare class LoticsClient {
|
|
|
96
105
|
name: string;
|
|
97
106
|
workspace_id: string;
|
|
98
107
|
current_version_id: string | null;
|
|
108
|
+
/** Launcher icon — a Lucide name, or an image ref. Null when unset. */
|
|
109
|
+
icon?: string | null;
|
|
110
|
+
/** Launcher theme — `{ color }` is a palette token. Null when unset. */
|
|
111
|
+
theme?: {
|
|
112
|
+
color?: string | null;
|
|
113
|
+
} | null;
|
|
99
114
|
/**
|
|
100
115
|
* Live alias → workflow declaration map from `apps.workflows`. Source of
|
|
101
116
|
* truth for `lotics app pull` — supersedes the manifest embedded in the
|
|
@@ -232,14 +247,6 @@ export declare class LoticsClient {
|
|
|
232
247
|
ast: unknown;
|
|
233
248
|
params?: Record<string, unknown>;
|
|
234
249
|
}>;
|
|
235
|
-
/**
|
|
236
|
-
* App icon (Lucide name) + theme `{ color }` from `package.json#lotics`.
|
|
237
|
-
* `undefined` ⇒ not sent ⇒ the deploy leaves the App row's icon/theme
|
|
238
|
-
* as-is. A present value is applied authoritatively. `icon` rides as a
|
|
239
|
-
* raw form field; `theme` is an object, so it's JSON-encoded.
|
|
240
|
-
*/
|
|
241
|
-
icon?: string;
|
|
242
|
-
theme?: Record<string, unknown>;
|
|
243
250
|
/**
|
|
244
251
|
* Opt-in app capabilities from `package.json#lotics.capabilities`. The CLI
|
|
245
252
|
* sends this on every deploy (defaulting to `{}`): the manifest is
|
package/dist/client.js
CHANGED
|
@@ -49,6 +49,9 @@ export const API_BASE_URL = process.env.LOTICS_API_URL ?? "https://api.lotics.ai
|
|
|
49
49
|
export class LoticsClient {
|
|
50
50
|
apiKey;
|
|
51
51
|
workspaceId;
|
|
52
|
+
/** The active "View as" target member id, if any. Read-only after
|
|
53
|
+
* construction — surfaced so `lotics app dev` can show it in the banner. */
|
|
54
|
+
viewAsMemberId;
|
|
52
55
|
/** API URL the client is configured against. Read-only after construction.
|
|
53
56
|
* Surfaced for callers that need to display or log it (e.g., `lotics app dev`
|
|
54
57
|
* shows it in the banner). */
|
|
@@ -56,6 +59,7 @@ export class LoticsClient {
|
|
|
56
59
|
constructor(options) {
|
|
57
60
|
this.apiKey = options.apiKey;
|
|
58
61
|
this.workspaceId = options.workspaceId;
|
|
62
|
+
this.viewAsMemberId = options.viewAsMemberId;
|
|
59
63
|
this.baseUrl = API_BASE_URL;
|
|
60
64
|
}
|
|
61
65
|
async throwResponseError(response) {
|
|
@@ -77,6 +81,9 @@ export class LoticsClient {
|
|
|
77
81
|
if (this.workspaceId) {
|
|
78
82
|
headers["x-workspace-id"] = this.workspaceId;
|
|
79
83
|
}
|
|
84
|
+
if (this.viewAsMemberId) {
|
|
85
|
+
headers["x-view-as-member-id"] = this.viewAsMemberId;
|
|
86
|
+
}
|
|
80
87
|
return headers;
|
|
81
88
|
}
|
|
82
89
|
async request(method, path, body) {
|
|
@@ -220,16 +227,6 @@ export class LoticsClient {
|
|
|
220
227
|
// Always send queries — empty object clears any previously-declared
|
|
221
228
|
// named queries. Server validates each template.
|
|
222
229
|
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));
|
|
232
|
-
}
|
|
233
230
|
// capabilities is manifest-authoritative — the caller always passes it
|
|
234
231
|
// (`{}` when none declared), so a deploy turns off any capability the
|
|
235
232
|
// manifest no longer declares. JSON-encoded.
|
|
@@ -6,10 +6,16 @@
|
|
|
6
6
|
* { message }. Same shape as the production iframe-host's error path.
|
|
7
7
|
*/
|
|
8
8
|
import { LoticsClient } from "../client.js";
|
|
9
|
-
export type RpcOp = "query" | "workflow" | "members" | "upload_url" | "upload_complete";
|
|
9
|
+
export type RpcOp = "query" | "workflow" | "members" | "context" | "upload_url" | "upload_complete";
|
|
10
10
|
export interface RpcRequest {
|
|
11
11
|
app_id: string;
|
|
12
12
|
op: RpcOp;
|
|
13
13
|
payload: unknown;
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
/** Per-app facts the dev server knows from the local manifest that the
|
|
16
|
+
* production iframe-host supplies from the embedding context. */
|
|
17
|
+
export interface DispatchOptions {
|
|
18
|
+
/** From `package.json#lotics.capabilities.comments`. */
|
|
19
|
+
commentsEnabled?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare function dispatchRpc(client: LoticsClient, body: RpcRequest, opts?: DispatchOptions): Promise<unknown>;
|
package/dist/dev/rpc_handler.js
CHANGED
|
@@ -9,10 +9,11 @@ const SUPPORTED_OPS = new Set([
|
|
|
9
9
|
"query",
|
|
10
10
|
"workflow",
|
|
11
11
|
"members",
|
|
12
|
+
"context",
|
|
12
13
|
"upload_url",
|
|
13
14
|
"upload_complete",
|
|
14
15
|
]);
|
|
15
|
-
export async function dispatchRpc(client, body) {
|
|
16
|
+
export async function dispatchRpc(client, body, opts) {
|
|
16
17
|
if (!body || typeof body.app_id !== "string" || typeof body.op !== "string") {
|
|
17
18
|
throw new Error("RPC envelope must include app_id and op");
|
|
18
19
|
}
|
|
@@ -20,6 +21,17 @@ export async function dispatchRpc(client, body) {
|
|
|
20
21
|
throw new Error(`Unknown RPC op: ${body.op}`);
|
|
21
22
|
}
|
|
22
23
|
switch (body.op) {
|
|
24
|
+
case "context": {
|
|
25
|
+
// In production the iframe host supplies the context; here we resolve the
|
|
26
|
+
// effective viewer from the CLI key (the view-as target when the client
|
|
27
|
+
// carries `x-view-as-member-id`, else the key's owner), so `useViewer`
|
|
28
|
+
// and `is_current_member` agree in the dev loop.
|
|
29
|
+
const who = await client.whoami();
|
|
30
|
+
return {
|
|
31
|
+
member_id: who.member_id,
|
|
32
|
+
comments_enabled: opts?.commentsEnabled ?? false,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
23
35
|
case "query": {
|
|
24
36
|
const p = body.payload;
|
|
25
37
|
if (!p || typeof p.alias !== "string") {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { dispatchRpc } from "./rpc_handler.js";
|
|
3
|
+
function mockClient(over) {
|
|
4
|
+
return over;
|
|
5
|
+
}
|
|
6
|
+
const WHOAMI = {
|
|
7
|
+
member_id: "mbr_viewer",
|
|
8
|
+
email: "v@test.local",
|
|
9
|
+
name: "Viewer",
|
|
10
|
+
organization_id: "org_1",
|
|
11
|
+
organization_name: "Org",
|
|
12
|
+
};
|
|
13
|
+
describe("dispatchRpc — context op", () => {
|
|
14
|
+
it("resolves the viewer member_id from whoami + manifest comments_enabled", async () => {
|
|
15
|
+
const client = mockClient({ whoami: async () => WHOAMI });
|
|
16
|
+
const result = await dispatchRpc(client, { app_id: "app_x", op: "context", payload: {} }, { commentsEnabled: true });
|
|
17
|
+
expect(result).toEqual({ member_id: "mbr_viewer", comments_enabled: true });
|
|
18
|
+
});
|
|
19
|
+
it("defaults comments_enabled to false when the manifest declares none", async () => {
|
|
20
|
+
const client = mockClient({ whoami: async () => WHOAMI });
|
|
21
|
+
const result = await dispatchRpc(client, { app_id: "app_x", op: "context", payload: {} });
|
|
22
|
+
expect(result).toEqual({ member_id: "mbr_viewer", comments_enabled: false });
|
|
23
|
+
});
|
|
24
|
+
it("rejects an op that isn't supported", async () => {
|
|
25
|
+
const client = mockClient({});
|
|
26
|
+
await expect(dispatchRpc(client, { app_id: "app_x", op: "bogus", payload: {} })).rejects.toThrow(/Unknown RPC op/);
|
|
27
|
+
});
|
|
28
|
+
});
|
package/dist/dev/server.d.ts
CHANGED
|
@@ -25,6 +25,9 @@ export interface DevServerArgs {
|
|
|
25
25
|
/** Preferred Vite port. If taken, auto-picks a free one. */
|
|
26
26
|
vitePort?: number;
|
|
27
27
|
client: LoticsClient;
|
|
28
|
+
/** From `package.json#lotics.capabilities.comments` — fed to the `context` op
|
|
29
|
+
* so `useComments` reflects the manifest in the dev loop. */
|
|
30
|
+
commentsEnabled?: boolean;
|
|
28
31
|
}
|
|
29
32
|
export interface DevServerHandle {
|
|
30
33
|
port: number;
|
package/dist/dev/server.js
CHANGED
|
@@ -91,7 +91,7 @@ export async function startDevServer(args) {
|
|
|
91
91
|
app_id: body.app_id,
|
|
92
92
|
op: body.op,
|
|
93
93
|
payload: body.payload,
|
|
94
|
-
});
|
|
94
|
+
}, { commentsEnabled: args.commentsEnabled });
|
|
95
95
|
const ms = Date.now() - startedAt;
|
|
96
96
|
process.stderr.write(`[rpc] ${body.op} ${ms}ms\n`);
|
|
97
97
|
res.writeHead(200, { "Content-Type": "application/json" });
|
package/dist/src/cli.js
CHANGED
|
@@ -29625,6 +29625,9 @@ var API_BASE_URL = process.env.LOTICS_API_URL ?? "https://api.lotics.ai";
|
|
|
29625
29625
|
var LoticsClient = class {
|
|
29626
29626
|
apiKey;
|
|
29627
29627
|
workspaceId;
|
|
29628
|
+
/** The active "View as" target member id, if any. Read-only after
|
|
29629
|
+
* construction — surfaced so `lotics app dev` can show it in the banner. */
|
|
29630
|
+
viewAsMemberId;
|
|
29628
29631
|
/** API URL the client is configured against. Read-only after construction.
|
|
29629
29632
|
* Surfaced for callers that need to display or log it (e.g., `lotics app dev`
|
|
29630
29633
|
* shows it in the banner). */
|
|
@@ -29632,6 +29635,7 @@ var LoticsClient = class {
|
|
|
29632
29635
|
constructor(options) {
|
|
29633
29636
|
this.apiKey = options.apiKey;
|
|
29634
29637
|
this.workspaceId = options.workspaceId;
|
|
29638
|
+
this.viewAsMemberId = options.viewAsMemberId;
|
|
29635
29639
|
this.baseUrl = API_BASE_URL;
|
|
29636
29640
|
}
|
|
29637
29641
|
async throwResponseError(response) {
|
|
@@ -29652,6 +29656,9 @@ var LoticsClient = class {
|
|
|
29652
29656
|
if (this.workspaceId) {
|
|
29653
29657
|
headers["x-workspace-id"] = this.workspaceId;
|
|
29654
29658
|
}
|
|
29659
|
+
if (this.viewAsMemberId) {
|
|
29660
|
+
headers["x-view-as-member-id"] = this.viewAsMemberId;
|
|
29661
|
+
}
|
|
29655
29662
|
return headers;
|
|
29656
29663
|
}
|
|
29657
29664
|
async request(method, path7, body) {
|
|
@@ -29814,12 +29821,6 @@ var LoticsClient = class {
|
|
|
29814
29821
|
}
|
|
29815
29822
|
formData.append("workflows", JSON.stringify(args.workflows ?? {}));
|
|
29816
29823
|
formData.append("queries", JSON.stringify(args.queries ?? {}));
|
|
29817
|
-
if (args.icon !== void 0) {
|
|
29818
|
-
formData.append("icon", args.icon);
|
|
29819
|
-
}
|
|
29820
|
-
if (args.theme !== void 0) {
|
|
29821
|
-
formData.append("theme", JSON.stringify(args.theme));
|
|
29822
|
-
}
|
|
29823
29824
|
if (args.capabilities !== void 0) {
|
|
29824
29825
|
formData.append("capabilities", JSON.stringify(args.capabilities));
|
|
29825
29826
|
}
|
|
@@ -30648,10 +30649,11 @@ var SUPPORTED_OPS = /* @__PURE__ */ new Set([
|
|
|
30648
30649
|
"query",
|
|
30649
30650
|
"workflow",
|
|
30650
30651
|
"members",
|
|
30652
|
+
"context",
|
|
30651
30653
|
"upload_url",
|
|
30652
30654
|
"upload_complete"
|
|
30653
30655
|
]);
|
|
30654
|
-
async function dispatchRpc(client, body) {
|
|
30656
|
+
async function dispatchRpc(client, body, opts) {
|
|
30655
30657
|
if (!body || typeof body.app_id !== "string" || typeof body.op !== "string") {
|
|
30656
30658
|
throw new Error("RPC envelope must include app_id and op");
|
|
30657
30659
|
}
|
|
@@ -30659,6 +30661,13 @@ async function dispatchRpc(client, body) {
|
|
|
30659
30661
|
throw new Error(`Unknown RPC op: ${body.op}`);
|
|
30660
30662
|
}
|
|
30661
30663
|
switch (body.op) {
|
|
30664
|
+
case "context": {
|
|
30665
|
+
const who = await client.whoami();
|
|
30666
|
+
return {
|
|
30667
|
+
member_id: who.member_id,
|
|
30668
|
+
comments_enabled: opts?.commentsEnabled ?? false
|
|
30669
|
+
};
|
|
30670
|
+
}
|
|
30662
30671
|
case "query": {
|
|
30663
30672
|
const p = body.payload;
|
|
30664
30673
|
if (!p || typeof p.alias !== "string") {
|
|
@@ -30949,11 +30958,15 @@ async function startDevServer(args) {
|
|
|
30949
30958
|
try {
|
|
30950
30959
|
const body = await readJson(req);
|
|
30951
30960
|
const startedAt = Date.now();
|
|
30952
|
-
const result = await dispatchRpc(
|
|
30953
|
-
|
|
30954
|
-
|
|
30955
|
-
|
|
30956
|
-
|
|
30961
|
+
const result = await dispatchRpc(
|
|
30962
|
+
args.client,
|
|
30963
|
+
{
|
|
30964
|
+
app_id: body.app_id,
|
|
30965
|
+
op: body.op,
|
|
30966
|
+
payload: body.payload
|
|
30967
|
+
},
|
|
30968
|
+
{ commentsEnabled: args.commentsEnabled }
|
|
30969
|
+
);
|
|
30957
30970
|
const ms = Date.now() - startedAt;
|
|
30958
30971
|
process.stderr.write(`[rpc] ${body.op} ${ms}ms
|
|
30959
30972
|
`);
|
|
@@ -31239,10 +31252,6 @@ function readAppMeta(projectDir) {
|
|
|
31239
31252
|
version_number: pkg2.lotics.version_number ?? null,
|
|
31240
31253
|
workflows: pkg2.lotics.workflows ?? {},
|
|
31241
31254
|
queries: pkg2.lotics.queries ?? {},
|
|
31242
|
-
// Left as `undefined` when absent so the deploy omits them and the
|
|
31243
|
-
// App row's icon/theme is preserved.
|
|
31244
|
-
icon: pkg2.lotics.icon,
|
|
31245
|
-
theme: pkg2.lotics.theme,
|
|
31246
31255
|
capabilities: pkg2.lotics.capabilities
|
|
31247
31256
|
};
|
|
31248
31257
|
}
|
|
@@ -31417,15 +31426,10 @@ async function appDeploy(client, args) {
|
|
|
31417
31426
|
// Sync apps.queries from the manifest. Server validates each query
|
|
31418
31427
|
// template (parseQueryNode, table access, param coverage).
|
|
31419
31428
|
queries: meta.queries ?? {},
|
|
31420
|
-
//
|
|
31421
|
-
//
|
|
31422
|
-
|
|
31423
|
-
|
|
31424
|
-
// Capabilities are manifest-authoritative (like workflows/queries, not
|
|
31425
|
-
// like icon/theme): always send, defaulting to `{}` when the manifest
|
|
31426
|
-
// declares none — so deleting the `capabilities` block turns every
|
|
31427
|
-
// capability OFF on the next deploy (fail-safe; the declaration is the
|
|
31428
|
-
// grant). The manifest is the only writer of capabilities.
|
|
31429
|
+
// Capabilities are manifest-authoritative (like workflows/queries):
|
|
31430
|
+
// always send, defaulting to `{}` when the manifest declares none — so
|
|
31431
|
+
// deleting the `capabilities` block turns every capability OFF on the
|
|
31432
|
+
// next deploy (fail-safe; the declaration is the grant).
|
|
31429
31433
|
capabilities: meta.capabilities ?? {},
|
|
31430
31434
|
// Opt into destructive workflow removal. Default false — the server
|
|
31431
31435
|
// rejects deploys whose manifest is missing aliases the App row has.
|
|
@@ -31438,6 +31442,7 @@ async function appDeploy(client, args) {
|
|
|
31438
31442
|
});
|
|
31439
31443
|
console.error(`Deployed v${result.version_number} (${result.version_id})`);
|
|
31440
31444
|
console.error(`Bundle size: ${(result.bundle_size_bytes / 1024).toFixed(1)} KB`);
|
|
31445
|
+
await warnIfUnbranded(client, meta.app_id);
|
|
31441
31446
|
} catch (err2) {
|
|
31442
31447
|
const e = err2;
|
|
31443
31448
|
if (e.code === "VERSION_CONFLICT") {
|
|
@@ -31453,6 +31458,23 @@ async function appDeploy(client, args) {
|
|
|
31453
31458
|
if (fs3.existsSync(tmpDist)) fs3.unlinkSync(tmpDist);
|
|
31454
31459
|
}
|
|
31455
31460
|
}
|
|
31461
|
+
async function warnIfUnbranded(client, appId) {
|
|
31462
|
+
try {
|
|
31463
|
+
const app = await client.getApp(appId);
|
|
31464
|
+
const missing = [];
|
|
31465
|
+
if (!app.icon) missing.push("icon");
|
|
31466
|
+
if (!app.theme?.color) missing.push("color");
|
|
31467
|
+
if (missing.length === 0) return;
|
|
31468
|
+
console.error(
|
|
31469
|
+
`
|
|
31470
|
+
\u26A0 This app has no ${missing.join(" or ")} set \u2014 it shows a generic tile in the launcher.
|
|
31471
|
+
Set it: lotics run update_app '{"app_id":"${appId}","icon":"<lucide-name>","theme":{"color":"blue"}}'
|
|
31472
|
+
Find an icon: lotics run search_app_icons '{"query":"<word>"}'`
|
|
31473
|
+
);
|
|
31474
|
+
} catch (err2) {
|
|
31475
|
+
console.error(`(skipped branding check: ${err2.message})`);
|
|
31476
|
+
}
|
|
31477
|
+
}
|
|
31456
31478
|
async function appDev(client, args) {
|
|
31457
31479
|
const projectDir = path4.resolve(args.projectDir ?? process.cwd());
|
|
31458
31480
|
const meta = readAppMeta(projectDir);
|
|
@@ -31466,7 +31488,8 @@ async function appDev(client, args) {
|
|
|
31466
31488
|
api_url: client.baseUrl,
|
|
31467
31489
|
port: args.port,
|
|
31468
31490
|
vitePort: args.vitePort,
|
|
31469
|
-
client
|
|
31491
|
+
client,
|
|
31492
|
+
commentsEnabled: meta.capabilities?.comments
|
|
31470
31493
|
});
|
|
31471
31494
|
await handle.ready;
|
|
31472
31495
|
const url = `http://localhost:${handle.port}`;
|
|
@@ -31474,6 +31497,9 @@ async function appDev(client, args) {
|
|
|
31474
31497
|
lotics app dev`);
|
|
31475
31498
|
console.error(` app: ${app.name} (${meta.app_id})`);
|
|
31476
31499
|
console.error(` workspace: ${meta.workspace_id}`);
|
|
31500
|
+
if (client.viewAsMemberId) {
|
|
31501
|
+
console.error(` view as: ${client.viewAsMemberId} (is_current_member resolves to this member)`);
|
|
31502
|
+
}
|
|
31477
31503
|
console.error(` vite: http://localhost:${handle.vitePort}/`);
|
|
31478
31504
|
console.error(` open: ${url}`);
|
|
31479
31505
|
console.error(` rpc: ${client.baseUrl} (via Bearer API key)
|
|
@@ -31503,6 +31529,7 @@ function parseArgs(argv) {
|
|
|
31503
31529
|
as: void 0,
|
|
31504
31530
|
apiKey: void 0,
|
|
31505
31531
|
workspace: void 0,
|
|
31532
|
+
viewAs: void 0,
|
|
31506
31533
|
name: void 0,
|
|
31507
31534
|
timezone: void 0,
|
|
31508
31535
|
message: void 0,
|
|
@@ -31540,6 +31567,9 @@ function parseArgs(argv) {
|
|
|
31540
31567
|
case "-w":
|
|
31541
31568
|
flags.workspace = argv[++i];
|
|
31542
31569
|
break;
|
|
31570
|
+
case "--view-as":
|
|
31571
|
+
flags.viewAs = argv[++i];
|
|
31572
|
+
break;
|
|
31543
31573
|
case "--name":
|
|
31544
31574
|
flags.name = argv[++i];
|
|
31545
31575
|
break;
|
|
@@ -47360,6 +47390,9 @@ FLAGS
|
|
|
47360
47390
|
--as <name> Override upload filename
|
|
47361
47391
|
--api-key <key> One-off API key (overrides saved config + env)
|
|
47362
47392
|
--workspace <id> One-off workspace override (alias: -w)
|
|
47393
|
+
--view-as <id> Admin "View as": run every request as this member, so
|
|
47394
|
+
is_current_member / row-scoping resolve to them (also
|
|
47395
|
+
LOTICS_VIEW_AS env; admin key only; writes stay yours)
|
|
47363
47396
|
--local Pin the current directory (lotics org use / auth api-key)
|
|
47364
47397
|
--all (lotics auth logout) Remove every saved credential
|
|
47365
47398
|
--version Show version
|
|
@@ -47542,7 +47575,11 @@ function requireClient(flags) {
|
|
|
47542
47575
|
console.error('Not authenticated. Run "lotics auth signup", "lotics auth api-key <key>", or set LOTICS_API_KEY.');
|
|
47543
47576
|
process.exit(1);
|
|
47544
47577
|
}
|
|
47545
|
-
|
|
47578
|
+
const viewAsMemberId = flags.viewAs ?? process.env.LOTICS_VIEW_AS;
|
|
47579
|
+
return {
|
|
47580
|
+
client: new LoticsClient({ apiKey: ctx.apiKey, workspaceId: ctx.workspaceId, viewAsMemberId }),
|
|
47581
|
+
ctx
|
|
47582
|
+
};
|
|
47546
47583
|
}
|
|
47547
47584
|
var SOURCE_LABELS = {
|
|
47548
47585
|
flag: "--api-key flag",
|
|
@@ -47632,14 +47669,16 @@ async function main() {
|
|
|
47632
47669
|
const info = await client2.whoami();
|
|
47633
47670
|
const existing = loadGlobalConfig() ?? {};
|
|
47634
47671
|
saveGlobalConfig({ ...existing, email: info.email });
|
|
47672
|
+
const viewAs = flags.viewAs ?? process.env.LOTICS_VIEW_AS;
|
|
47635
47673
|
if (flags.json) {
|
|
47636
|
-
console.log(JSON.stringify({ ...info, workspace_id: ctx2.workspaceId ?? null, source: ctx2.source }, null, 2));
|
|
47674
|
+
console.log(JSON.stringify({ ...info, workspace_id: ctx2.workspaceId ?? null, source: ctx2.source, view_as: viewAs ?? null }, null, 2));
|
|
47637
47675
|
} else {
|
|
47638
47676
|
console.log(`Name: ${info.name}`);
|
|
47639
47677
|
console.log(`Email: ${info.email}`);
|
|
47640
47678
|
console.log(`Org: ${info.organization_name} (${info.organization_id})`);
|
|
47641
47679
|
console.log(`Workspace: ${ctx2.workspaceId ?? "(none selected)"}`);
|
|
47642
47680
|
console.log(`Source: ${SOURCE_LABELS[ctx2.source]}`);
|
|
47681
|
+
if (viewAs) console.log(`View as: ${viewAs} (admin preview; requires an admin key)`);
|
|
47643
47682
|
}
|
|
47644
47683
|
return;
|
|
47645
47684
|
}
|