@lotics/cli 0.41.0 → 0.43.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 +4 -0
- 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 +9 -0
- package/dist/client.js +7 -0
- 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/dev/wrapper_page.d.ts +3 -1
- package/dist/dev/wrapper_page.js +4 -2
- package/dist/src/cli.js +46 -10
- package/package.json +1 -1
package/dist/app_commands.js
CHANGED
|
@@ -434,12 +434,16 @@ export async function appDev(client, args) {
|
|
|
434
434
|
port: args.port,
|
|
435
435
|
vitePort: args.vitePort,
|
|
436
436
|
client,
|
|
437
|
+
commentsEnabled: meta.capabilities?.comments,
|
|
437
438
|
});
|
|
438
439
|
await handle.ready;
|
|
439
440
|
const url = `http://localhost:${handle.port}`;
|
|
440
441
|
console.error(`\n lotics app dev`);
|
|
441
442
|
console.error(` app: ${app.name} (${meta.app_id})`);
|
|
442
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
|
+
}
|
|
443
447
|
console.error(` vite: http://localhost:${handle.vitePort}/`);
|
|
444
448
|
console.error(` open: ${url}`);
|
|
445
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;
|
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) {
|
|
@@ -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" });
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* wrapper (different port), so the app gets its own real origin (and Web
|
|
9
9
|
* Storage) without being able to reach the wrapper. `allow-downloads` lets
|
|
10
10
|
* apps trigger file downloads (generated xlsx / pdf etc.) — Chrome blocks
|
|
11
|
-
* every download path from sandboxed iframes without it.
|
|
11
|
+
* every download path from sandboxed iframes without it. `allow="...; geolocation"`
|
|
12
|
+
* delegates the host's location permission to the app so geofenced actions can
|
|
13
|
+
* read the device position — matching production. The iframe sends postMessage RPCs to
|
|
12
14
|
* this wrapper, which forwards them to the local /_rpc endpoint, which
|
|
13
15
|
* dispatches to api.lotics.ai with the CLI's API key.
|
|
14
16
|
*
|
package/dist/dev/wrapper_page.js
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* wrapper (different port), so the app gets its own real origin (and Web
|
|
9
9
|
* Storage) without being able to reach the wrapper. `allow-downloads` lets
|
|
10
10
|
* apps trigger file downloads (generated xlsx / pdf etc.) — Chrome blocks
|
|
11
|
-
* every download path from sandboxed iframes without it.
|
|
11
|
+
* every download path from sandboxed iframes without it. `allow="...; geolocation"`
|
|
12
|
+
* delegates the host's location permission to the app so geofenced actions can
|
|
13
|
+
* read the device position — matching production. The iframe sends postMessage RPCs to
|
|
12
14
|
* this wrapper, which forwards them to the local /_rpc endpoint, which
|
|
13
15
|
* dispatches to api.lotics.ai with the CLI's API key.
|
|
14
16
|
*
|
|
@@ -60,7 +62,7 @@ export function buildWrapperPage(args) {
|
|
|
60
62
|
<span>workspace <code>${escapeHtml(workspace_id)}</code></span>
|
|
61
63
|
<span>RPC → <code>${escapeHtml(api_url)}</code></span>
|
|
62
64
|
</header>
|
|
63
|
-
<iframe id="app" sandbox="allow-scripts allow-same-origin allow-downloads" allow="clipboard-write"></iframe>
|
|
65
|
+
<iframe id="app" sandbox="allow-scripts allow-same-origin allow-downloads" allow="clipboard-write; geolocation"></iframe>
|
|
64
66
|
<script>
|
|
65
67
|
(function () {
|
|
66
68
|
const APP_ID = ${JSON.stringify(app_id)};
|
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) {
|
|
@@ -30642,10 +30649,11 @@ var SUPPORTED_OPS = /* @__PURE__ */ new Set([
|
|
|
30642
30649
|
"query",
|
|
30643
30650
|
"workflow",
|
|
30644
30651
|
"members",
|
|
30652
|
+
"context",
|
|
30645
30653
|
"upload_url",
|
|
30646
30654
|
"upload_complete"
|
|
30647
30655
|
]);
|
|
30648
|
-
async function dispatchRpc(client, body) {
|
|
30656
|
+
async function dispatchRpc(client, body, opts) {
|
|
30649
30657
|
if (!body || typeof body.app_id !== "string" || typeof body.op !== "string") {
|
|
30650
30658
|
throw new Error("RPC envelope must include app_id and op");
|
|
30651
30659
|
}
|
|
@@ -30653,6 +30661,13 @@ async function dispatchRpc(client, body) {
|
|
|
30653
30661
|
throw new Error(`Unknown RPC op: ${body.op}`);
|
|
30654
30662
|
}
|
|
30655
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
|
+
}
|
|
30656
30671
|
case "query": {
|
|
30657
30672
|
const p = body.payload;
|
|
30658
30673
|
if (!p || typeof p.alias !== "string") {
|
|
@@ -30751,7 +30766,7 @@ function buildWrapperPage(args) {
|
|
|
30751
30766
|
<span>workspace <code>${escapeHtml2(workspace_id)}</code></span>
|
|
30752
30767
|
<span>RPC \u2192 <code>${escapeHtml2(api_url)}</code></span>
|
|
30753
30768
|
</header>
|
|
30754
|
-
<iframe id="app" sandbox="allow-scripts allow-same-origin allow-downloads" allow="clipboard-write"></iframe>
|
|
30769
|
+
<iframe id="app" sandbox="allow-scripts allow-same-origin allow-downloads" allow="clipboard-write; geolocation"></iframe>
|
|
30755
30770
|
<script>
|
|
30756
30771
|
(function () {
|
|
30757
30772
|
const APP_ID = ${JSON.stringify(app_id)};
|
|
@@ -30943,11 +30958,15 @@ async function startDevServer(args) {
|
|
|
30943
30958
|
try {
|
|
30944
30959
|
const body = await readJson(req);
|
|
30945
30960
|
const startedAt = Date.now();
|
|
30946
|
-
const result = await dispatchRpc(
|
|
30947
|
-
|
|
30948
|
-
|
|
30949
|
-
|
|
30950
|
-
|
|
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
|
+
);
|
|
30951
30970
|
const ms = Date.now() - startedAt;
|
|
30952
30971
|
process.stderr.write(`[rpc] ${body.op} ${ms}ms
|
|
30953
30972
|
`);
|
|
@@ -31469,7 +31488,8 @@ async function appDev(client, args) {
|
|
|
31469
31488
|
api_url: client.baseUrl,
|
|
31470
31489
|
port: args.port,
|
|
31471
31490
|
vitePort: args.vitePort,
|
|
31472
|
-
client
|
|
31491
|
+
client,
|
|
31492
|
+
commentsEnabled: meta.capabilities?.comments
|
|
31473
31493
|
});
|
|
31474
31494
|
await handle.ready;
|
|
31475
31495
|
const url = `http://localhost:${handle.port}`;
|
|
@@ -31477,6 +31497,9 @@ async function appDev(client, args) {
|
|
|
31477
31497
|
lotics app dev`);
|
|
31478
31498
|
console.error(` app: ${app.name} (${meta.app_id})`);
|
|
31479
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
|
+
}
|
|
31480
31503
|
console.error(` vite: http://localhost:${handle.vitePort}/`);
|
|
31481
31504
|
console.error(` open: ${url}`);
|
|
31482
31505
|
console.error(` rpc: ${client.baseUrl} (via Bearer API key)
|
|
@@ -31506,6 +31529,7 @@ function parseArgs(argv) {
|
|
|
31506
31529
|
as: void 0,
|
|
31507
31530
|
apiKey: void 0,
|
|
31508
31531
|
workspace: void 0,
|
|
31532
|
+
viewAs: void 0,
|
|
31509
31533
|
name: void 0,
|
|
31510
31534
|
timezone: void 0,
|
|
31511
31535
|
message: void 0,
|
|
@@ -31543,6 +31567,9 @@ function parseArgs(argv) {
|
|
|
31543
31567
|
case "-w":
|
|
31544
31568
|
flags.workspace = argv[++i];
|
|
31545
31569
|
break;
|
|
31570
|
+
case "--view-as":
|
|
31571
|
+
flags.viewAs = argv[++i];
|
|
31572
|
+
break;
|
|
31546
31573
|
case "--name":
|
|
31547
31574
|
flags.name = argv[++i];
|
|
31548
31575
|
break;
|
|
@@ -47363,6 +47390,9 @@ FLAGS
|
|
|
47363
47390
|
--as <name> Override upload filename
|
|
47364
47391
|
--api-key <key> One-off API key (overrides saved config + env)
|
|
47365
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)
|
|
47366
47396
|
--local Pin the current directory (lotics org use / auth api-key)
|
|
47367
47397
|
--all (lotics auth logout) Remove every saved credential
|
|
47368
47398
|
--version Show version
|
|
@@ -47545,7 +47575,11 @@ function requireClient(flags) {
|
|
|
47545
47575
|
console.error('Not authenticated. Run "lotics auth signup", "lotics auth api-key <key>", or set LOTICS_API_KEY.');
|
|
47546
47576
|
process.exit(1);
|
|
47547
47577
|
}
|
|
47548
|
-
|
|
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
|
+
};
|
|
47549
47583
|
}
|
|
47550
47584
|
var SOURCE_LABELS = {
|
|
47551
47585
|
flag: "--api-key flag",
|
|
@@ -47635,14 +47669,16 @@ async function main() {
|
|
|
47635
47669
|
const info = await client2.whoami();
|
|
47636
47670
|
const existing = loadGlobalConfig() ?? {};
|
|
47637
47671
|
saveGlobalConfig({ ...existing, email: info.email });
|
|
47672
|
+
const viewAs = flags.viewAs ?? process.env.LOTICS_VIEW_AS;
|
|
47638
47673
|
if (flags.json) {
|
|
47639
|
-
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));
|
|
47640
47675
|
} else {
|
|
47641
47676
|
console.log(`Name: ${info.name}`);
|
|
47642
47677
|
console.log(`Email: ${info.email}`);
|
|
47643
47678
|
console.log(`Org: ${info.organization_name} (${info.organization_id})`);
|
|
47644
47679
|
console.log(`Workspace: ${ctx2.workspaceId ?? "(none selected)"}`);
|
|
47645
47680
|
console.log(`Source: ${SOURCE_LABELS[ctx2.source]}`);
|
|
47681
|
+
if (viewAs) console.log(`View as: ${viewAs} (admin preview; requires an admin key)`);
|
|
47646
47682
|
}
|
|
47647
47683
|
return;
|
|
47648
47684
|
}
|