@lotics/cli 0.192.0 → 0.192.2
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/README.md +1 -1
- package/dist/src/cli.js +144 -78
- package/dist/src/client.d.ts +17 -11
- package/dist/src/client.js +17 -18
- package/docs/cli_reference.md +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -195,7 +195,7 @@ Every request identifies the CLI (`user-agent: lotics-cli/<version> node/<v> <pl
|
|
|
195
195
|
**`LOTICS_TELEMETRY=1` additionally records the session.** Off by default. Set it in your shell profile rather than per command — each invocation is its own process. When set:
|
|
196
196
|
|
|
197
197
|
- Requests carry a session id shared by every command in the sitting — under an agent harness it adopts the harness's own session id, otherwise it rolls over after 30 minutes idle (`~/.lotics/session.json`).
|
|
198
|
-
- Each invocation appends one record to `~/.lotics/telemetry
|
|
198
|
+
- Each invocation appends one record to `~/.lotics/telemetry-<host>.ndjson` — the command, its exit code, how long it took, and, on a failure, the message that was printed. One spool per Lotics the CLI talks to, so a record made against one instance is only ever sent to that instance. Batches are sent on a later run against the same host; a failed send is retried, never dropped silently.
|
|
199
199
|
|
|
200
200
|
Arguments contribute a **hash** and a **shape** (`records[].data.name:string`) and nothing else — no values, no file contents, no record data. The hash is the point: two failures in a row with the same hash mean the error message did not tell you enough to fix the call.
|
|
201
201
|
|
package/dist/src/cli.js
CHANGED
|
@@ -44372,10 +44372,9 @@ var LoticsRequestError = class extends Error {
|
|
|
44372
44372
|
status;
|
|
44373
44373
|
body;
|
|
44374
44374
|
};
|
|
44375
|
-
var API_BASE_URL = process.env.LOTICS_API_URL ?? "https://api.lotics.ai";
|
|
44376
44375
|
var WEB_APP_URL = process.env.LOTICS_WEB_URL ?? "https://lotics.ai";
|
|
44377
|
-
async function fetchOfficialStarters() {
|
|
44378
|
-
const read = await getPublicJson(`${
|
|
44376
|
+
async function fetchOfficialStarters(apiUrl) {
|
|
44377
|
+
const read = await getPublicJson(`${apiUrl}/v1/starters/official`, "list the packages");
|
|
44379
44378
|
if (!read.ok) {
|
|
44380
44379
|
throw new Error(
|
|
44381
44380
|
`Lotics answered ${read.status} listing the packages. If this keeps happening, browse ${WEB_APP_URL}/docs/cli.`
|
|
@@ -44395,16 +44394,16 @@ async function getPublicJson(url2, what) {
|
|
|
44395
44394
|
if (!response.ok) return { ok: false, status: response.status };
|
|
44396
44395
|
return { ok: true, body: await response.json() };
|
|
44397
44396
|
}
|
|
44398
|
-
async function readOfficialStarter(starter_id) {
|
|
44397
|
+
async function readOfficialStarter(apiUrl, starter_id) {
|
|
44399
44398
|
const read = await getPublicJson(
|
|
44400
|
-
`${
|
|
44399
|
+
`${apiUrl}/v1/starters/official/${encodeURIComponent(starter_id)}`,
|
|
44401
44400
|
`read ${starter_id}`
|
|
44402
44401
|
);
|
|
44403
44402
|
if (!read.ok) return read;
|
|
44404
44403
|
return { ok: true, package: read.body };
|
|
44405
44404
|
}
|
|
44406
|
-
async function getOfficialStarter(starter_id) {
|
|
44407
|
-
const read = await readOfficialStarter(starter_id);
|
|
44405
|
+
async function getOfficialStarter(apiUrl, starter_id) {
|
|
44406
|
+
const read = await readOfficialStarter(apiUrl, starter_id);
|
|
44408
44407
|
if (!read.ok) {
|
|
44409
44408
|
throw new Error(
|
|
44410
44409
|
`Lotics answered ${read.status} reading ${starter_id}. Only packages Lotics publishes can be read without an account.`
|
|
@@ -44413,10 +44412,10 @@ async function getOfficialStarter(starter_id) {
|
|
|
44413
44412
|
return read.package;
|
|
44414
44413
|
}
|
|
44415
44414
|
var PUBLIC_FETCH_TIMEOUT_MS = 1e4;
|
|
44416
|
-
async function startCliLogin(email3) {
|
|
44415
|
+
async function startCliLogin(apiUrl, email3) {
|
|
44417
44416
|
let response;
|
|
44418
44417
|
try {
|
|
44419
|
-
response = await fetch(`${
|
|
44418
|
+
response = await fetch(`${apiUrl}/v1/cli/login_requests`, {
|
|
44420
44419
|
method: "POST",
|
|
44421
44420
|
headers: { "Content-Type": "application/json" },
|
|
44422
44421
|
body: JSON.stringify({ email: email3 }),
|
|
@@ -44432,8 +44431,8 @@ async function startCliLogin(email3) {
|
|
|
44432
44431
|
}
|
|
44433
44432
|
return await response.json();
|
|
44434
44433
|
}
|
|
44435
|
-
async function pollCliLogin(request_id, secret) {
|
|
44436
|
-
const url2 = `${
|
|
44434
|
+
async function pollCliLogin(apiUrl, request_id, secret) {
|
|
44435
|
+
const url2 = `${apiUrl}/v1/cli/login_requests/${encodeURIComponent(request_id)}`;
|
|
44437
44436
|
let response;
|
|
44438
44437
|
try {
|
|
44439
44438
|
response = await fetch(url2, {
|
|
@@ -44460,14 +44459,14 @@ var LoticsClient = class {
|
|
|
44460
44459
|
* construction — surfaced so `lotics app dev` can show it in the banner. */
|
|
44461
44460
|
viewAsMemberId;
|
|
44462
44461
|
/** API URL the client is configured against. Read-only after construction.
|
|
44463
|
-
* Surfaced for callers that need to display
|
|
44464
|
-
*
|
|
44462
|
+
* Surfaced for callers that need to display it (`lotics app dev`'s banner) or
|
|
44463
|
+
* to hand it on (the wrapper page's RPC target, the scaffold's font proxy). */
|
|
44465
44464
|
baseUrl;
|
|
44466
44465
|
constructor(options) {
|
|
44467
44466
|
this.apiKey = options.apiKey;
|
|
44468
44467
|
this.workspaceId = options.workspaceId;
|
|
44469
44468
|
this.viewAsMemberId = options.viewAsMemberId;
|
|
44470
|
-
this.baseUrl =
|
|
44469
|
+
this.baseUrl = options.apiUrl;
|
|
44471
44470
|
}
|
|
44472
44471
|
/**
|
|
44473
44472
|
* The id is appended to the MESSAGE rather than carried on a field, because
|
|
@@ -44870,9 +44869,10 @@ var LoticsClient = class {
|
|
|
44870
44869
|
return tables.filter((t) => t !== null);
|
|
44871
44870
|
}
|
|
44872
44871
|
/**
|
|
44873
|
-
* Rename an app's public subdomain — its
|
|
44874
|
-
* Mirrors PUT /v1/apps/{app_id}/subdomain
|
|
44875
|
-
*
|
|
44872
|
+
* Rename an app's public subdomain — the label its origin is built on.
|
|
44873
|
+
* Mirrors PUT /v1/apps/{app_id}/subdomain, and returns the finished `origin`
|
|
44874
|
+
* because only the instance knows the domain and scheme it serves apps on.
|
|
44875
|
+
* The old subdomain stops resolving once the change lands.
|
|
44876
44876
|
*/
|
|
44877
44877
|
async setAppSubdomain(app_id, public_subdomain) {
|
|
44878
44878
|
return this.request(
|
|
@@ -45500,8 +45500,16 @@ function canonicalize(value2) {
|
|
|
45500
45500
|
var MAX_ERROR_CHARS = 2e3;
|
|
45501
45501
|
var MAX_SPOOL_BYTES = 1024 * 1024;
|
|
45502
45502
|
var MAX_FLUSH_EVENTS = 500;
|
|
45503
|
-
function spoolPath() {
|
|
45504
|
-
|
|
45503
|
+
function spoolPath(apiUrl) {
|
|
45504
|
+
const shard = crypto2.createHash("sha256").update(apiUrl).digest("hex").slice(0, 12);
|
|
45505
|
+
return path3.join(os2.homedir(), ".lotics", `telemetry-${shard}.ndjson`);
|
|
45506
|
+
}
|
|
45507
|
+
var targetApiUrl = "";
|
|
45508
|
+
function noteTarget(apiUrl) {
|
|
45509
|
+
targetApiUrl = apiUrl;
|
|
45510
|
+
}
|
|
45511
|
+
function invocationTarget() {
|
|
45512
|
+
return targetApiUrl;
|
|
45505
45513
|
}
|
|
45506
45514
|
var stderrTail = "";
|
|
45507
45515
|
function recordStderr(text) {
|
|
@@ -45561,8 +45569,8 @@ function buildEvent(exitCode, durationMs, now) {
|
|
|
45561
45569
|
...pendingScope
|
|
45562
45570
|
};
|
|
45563
45571
|
}
|
|
45564
|
-
function appendEvent(event) {
|
|
45565
|
-
const file2 = spoolPath();
|
|
45572
|
+
function appendEvent(event, apiUrl) {
|
|
45573
|
+
const file2 = spoolPath(apiUrl);
|
|
45566
45574
|
try {
|
|
45567
45575
|
fs3.mkdirSync(path3.dirname(file2), { recursive: true });
|
|
45568
45576
|
if ((fs3.statSync(file2, { throwIfNoEntry: false })?.size ?? 0) > MAX_SPOOL_BYTES) {
|
|
@@ -45573,11 +45581,11 @@ function appendEvent(event) {
|
|
|
45573
45581
|
} catch {
|
|
45574
45582
|
}
|
|
45575
45583
|
}
|
|
45576
|
-
function readSpool() {
|
|
45584
|
+
function readSpool(apiUrl) {
|
|
45577
45585
|
try {
|
|
45578
45586
|
const events = [];
|
|
45579
45587
|
const lines = [];
|
|
45580
|
-
for (const line of fs3.readFileSync(spoolPath(), "utf-8").split("\n")) {
|
|
45588
|
+
for (const line of fs3.readFileSync(spoolPath(apiUrl), "utf-8").split("\n")) {
|
|
45581
45589
|
if (line.trim() === "") continue;
|
|
45582
45590
|
try {
|
|
45583
45591
|
events.push(JSON.parse(line));
|
|
@@ -45590,32 +45598,34 @@ function readSpool() {
|
|
|
45590
45598
|
return { events: [], lines: [] };
|
|
45591
45599
|
}
|
|
45592
45600
|
}
|
|
45593
|
-
function clearSpool(sent) {
|
|
45601
|
+
function clearSpool(apiUrl, sent) {
|
|
45594
45602
|
try {
|
|
45595
|
-
const
|
|
45596
|
-
|
|
45603
|
+
const file2 = spoolPath(apiUrl);
|
|
45604
|
+
const current = fs3.readFileSync(file2, "utf-8");
|
|
45605
|
+
fs3.writeFileSync(file2, current.startsWith(sent) ? current.slice(sent.length) : "", {
|
|
45597
45606
|
mode: 384
|
|
45598
45607
|
});
|
|
45599
45608
|
} catch {
|
|
45600
45609
|
}
|
|
45601
45610
|
}
|
|
45602
|
-
function installTelemetry(startedAt2 = Date.now()) {
|
|
45611
|
+
function installTelemetry(target, startedAt2 = Date.now()) {
|
|
45603
45612
|
if (!telemetryEnabled()) return;
|
|
45613
|
+
noteTarget(target);
|
|
45604
45614
|
installStderrCapture();
|
|
45605
45615
|
process.on("exit", (code) => {
|
|
45606
45616
|
const event = buildEvent(code, Date.now() - startedAt2, /* @__PURE__ */ new Date());
|
|
45607
|
-
if (event !== null) appendEvent(event);
|
|
45617
|
+
if (event !== null) appendEvent(event, invocationTarget());
|
|
45608
45618
|
});
|
|
45609
45619
|
}
|
|
45610
|
-
function flushSpool(post, minEvents = 5) {
|
|
45620
|
+
function flushSpool(apiUrl, post, minEvents = 5) {
|
|
45611
45621
|
if (!telemetryEnabled()) return;
|
|
45612
|
-
const { events, lines } = readSpool();
|
|
45622
|
+
const { events, lines } = readSpool(apiUrl);
|
|
45613
45623
|
if (events.length < minEvents) return;
|
|
45614
45624
|
const batch = events.slice(0, MAX_FLUSH_EVENTS);
|
|
45615
45625
|
const sent = `${lines.slice(0, batch.length).join("\n")}
|
|
45616
45626
|
`;
|
|
45617
45627
|
void post(batch).then((outcome) => {
|
|
45618
|
-
if (outcome !== "retry") clearSpool(sent);
|
|
45628
|
+
if (outcome !== "retry") clearSpool(apiUrl, sent);
|
|
45619
45629
|
}).catch(() => {
|
|
45620
45630
|
});
|
|
45621
45631
|
}
|
|
@@ -45641,6 +45651,10 @@ async function postEvents(baseUrl, apiKey, events, timeoutMs = 5e3) {
|
|
|
45641
45651
|
import fs4 from "node:fs";
|
|
45642
45652
|
import path4 from "node:path";
|
|
45643
45653
|
import os3 from "node:os";
|
|
45654
|
+
var HOSTED_API_URL = "https://api.lotics.ai";
|
|
45655
|
+
function envApiUrl() {
|
|
45656
|
+
return process.env.LOTICS_API_URL ?? HOSTED_API_URL;
|
|
45657
|
+
}
|
|
45644
45658
|
function globalConfigFile() {
|
|
45645
45659
|
return path4.join(os3.homedir(), ".lotics", "config.json");
|
|
45646
45660
|
}
|
|
@@ -45663,12 +45677,34 @@ function configFileForScope(scope) {
|
|
|
45663
45677
|
return globalConfigFile();
|
|
45664
45678
|
}
|
|
45665
45679
|
function readConfigFile(file2) {
|
|
45680
|
+
let parsed;
|
|
45666
45681
|
try {
|
|
45667
45682
|
const raw = fs4.readFileSync(file2, "utf-8");
|
|
45668
|
-
|
|
45683
|
+
parsed = JSON.parse(raw);
|
|
45669
45684
|
} catch {
|
|
45670
45685
|
return null;
|
|
45671
45686
|
}
|
|
45687
|
+
return upgradeProfileApiUrls(file2, parsed);
|
|
45688
|
+
}
|
|
45689
|
+
function upgradeProfileApiUrls(file2, config2) {
|
|
45690
|
+
const profiles = config2.profiles;
|
|
45691
|
+
if (!profiles) return config2;
|
|
45692
|
+
const entries2 = Object.entries(profiles);
|
|
45693
|
+
if (!entries2.some(([, profile]) => !profile.api_url)) return config2;
|
|
45694
|
+
const upgraded = {
|
|
45695
|
+
...config2,
|
|
45696
|
+
profiles: Object.fromEntries(
|
|
45697
|
+
entries2.map(([orgId, profile]) => [
|
|
45698
|
+
orgId,
|
|
45699
|
+
{ ...profile, api_url: profile.api_url || HOSTED_API_URL }
|
|
45700
|
+
])
|
|
45701
|
+
)
|
|
45702
|
+
};
|
|
45703
|
+
try {
|
|
45704
|
+
writeConfigFile(file2, upgraded);
|
|
45705
|
+
} catch {
|
|
45706
|
+
}
|
|
45707
|
+
return upgraded;
|
|
45672
45708
|
}
|
|
45673
45709
|
function writeConfigFile(file2, config2) {
|
|
45674
45710
|
fs4.mkdirSync(path4.dirname(file2), { recursive: true, mode: 448 });
|
|
@@ -45715,6 +45751,11 @@ function resolveProfileByNameOrId(profiles, nameOrId) {
|
|
|
45715
45751
|
return null;
|
|
45716
45752
|
}
|
|
45717
45753
|
function resolveContext(flags, appWorkspaceId) {
|
|
45754
|
+
const resolved = resolveCredential(flags, appWorkspaceId);
|
|
45755
|
+
if (resolved) noteTarget(resolved.apiUrl);
|
|
45756
|
+
return resolved;
|
|
45757
|
+
}
|
|
45758
|
+
function resolveCredential(flags, appWorkspaceId) {
|
|
45718
45759
|
const envKey = process.env.LOTICS_API_KEY;
|
|
45719
45760
|
const envOrg = process.env.LOTICS_ORG;
|
|
45720
45761
|
const envWorkspace = process.env.LOTICS_WORKSPACE;
|
|
@@ -45732,6 +45773,7 @@ function resolveContext(flags, appWorkspaceId) {
|
|
|
45732
45773
|
const [orgId, profile] = resolved;
|
|
45733
45774
|
return {
|
|
45734
45775
|
apiKey: profile.api_key,
|
|
45776
|
+
apiUrl: profile.api_url,
|
|
45735
45777
|
orgId,
|
|
45736
45778
|
orgName: profile.org_name,
|
|
45737
45779
|
workspaceId: wsOverride ?? profile.workspace_id,
|
|
@@ -45739,10 +45781,10 @@ function resolveContext(flags, appWorkspaceId) {
|
|
|
45739
45781
|
};
|
|
45740
45782
|
}
|
|
45741
45783
|
if (flags.apiKey) {
|
|
45742
|
-
return { apiKey: flags.apiKey, workspaceId: wsOverride, source: "flag" };
|
|
45784
|
+
return { apiKey: flags.apiKey, apiUrl: envApiUrl(), workspaceId: wsOverride, source: "flag" };
|
|
45743
45785
|
}
|
|
45744
45786
|
if (envKey) {
|
|
45745
|
-
return { apiKey: envKey, workspaceId: wsOverride, source: "env_key" };
|
|
45787
|
+
return { apiKey: envKey, apiUrl: envApiUrl(), workspaceId: wsOverride, source: "env_key" };
|
|
45746
45788
|
}
|
|
45747
45789
|
const global2 = loadGlobalConfig();
|
|
45748
45790
|
const profiles = global2?.profiles ?? {};
|
|
@@ -45756,6 +45798,7 @@ function resolveContext(flags, appWorkspaceId) {
|
|
|
45756
45798
|
const [orgId, profile] = resolved;
|
|
45757
45799
|
return {
|
|
45758
45800
|
apiKey: profile.api_key,
|
|
45801
|
+
apiUrl: profile.api_url,
|
|
45759
45802
|
orgId,
|
|
45760
45803
|
orgName: profile.org_name,
|
|
45761
45804
|
workspaceId: wsOverride ?? profile.workspace_id,
|
|
@@ -45772,6 +45815,7 @@ function resolveContext(flags, appWorkspaceId) {
|
|
|
45772
45815
|
}
|
|
45773
45816
|
return {
|
|
45774
45817
|
apiKey: profile.api_key,
|
|
45818
|
+
apiUrl: profile.api_url,
|
|
45775
45819
|
orgId: local.active_org,
|
|
45776
45820
|
orgName: profile.org_name,
|
|
45777
45821
|
workspaceId: wsOverride ?? local.workspace_id ?? profile.workspace_id,
|
|
@@ -45789,6 +45833,7 @@ function resolveContext(flags, appWorkspaceId) {
|
|
|
45789
45833
|
const [orgId, profile] = matches[0];
|
|
45790
45834
|
return {
|
|
45791
45835
|
apiKey: profile.api_key,
|
|
45836
|
+
apiUrl: profile.api_url,
|
|
45792
45837
|
orgId,
|
|
45793
45838
|
orgName: profile.org_name,
|
|
45794
45839
|
workspaceId: appWorkspaceId,
|
|
@@ -45805,6 +45850,7 @@ function resolveContext(flags, appWorkspaceId) {
|
|
|
45805
45850
|
}
|
|
45806
45851
|
return {
|
|
45807
45852
|
apiKey: profile.api_key,
|
|
45853
|
+
apiUrl: profile.api_url,
|
|
45808
45854
|
orgId: global2.active_org,
|
|
45809
45855
|
orgName: profile.org_name,
|
|
45810
45856
|
workspaceId: wsOverride ?? profile.workspace_id,
|
|
@@ -45819,6 +45865,7 @@ function upsertProfile(orgId, fields) {
|
|
|
45819
45865
|
const profile = {
|
|
45820
45866
|
api_key: fields.api_key,
|
|
45821
45867
|
org_name: fields.org_name,
|
|
45868
|
+
api_url: fields.api_url,
|
|
45822
45869
|
workspace_id: fields.workspace_id ?? existing?.workspace_id
|
|
45823
45870
|
};
|
|
45824
45871
|
saveGlobalConfig({
|
|
@@ -46001,7 +46048,7 @@ function resultSideEffects(result) {
|
|
|
46001
46048
|
}
|
|
46002
46049
|
|
|
46003
46050
|
// src/version.ts
|
|
46004
|
-
var VERSION = "0.192.
|
|
46051
|
+
var VERSION = "0.192.2";
|
|
46005
46052
|
|
|
46006
46053
|
// src/timezone.ts
|
|
46007
46054
|
function machineTimezone() {
|
|
@@ -46562,7 +46609,7 @@ var COMMANDS = [
|
|
|
46562
46609
|
" fields keep whatever is bound (a manifest is a",
|
|
46563
46610
|
" snapshot; replaying it would revert them). Change",
|
|
46564
46611
|
" those with set_app_agent, then pull.",
|
|
46565
|
-
" lotics app subdomain <new-subdomain> Rename the app's public
|
|
46612
|
+
" lotics app subdomain <new-subdomain> Rename the app's public address (its subdomain)",
|
|
46566
46613
|
` lotics app rename "<new name>" Rename the app's display name (launcher title)`,
|
|
46567
46614
|
" lotics app dev [path] Run the app locally with HMR (RPC forwarded to prod)"
|
|
46568
46615
|
]
|
|
@@ -46997,8 +47044,8 @@ export default defineConfig({
|
|
|
46997
47044
|
server: {
|
|
46998
47045
|
// Allow the sandboxed null-origin iframe used by \`lotics app dev\` to
|
|
46999
47046
|
// fetch HMR client + source modules from the dev server. Production
|
|
47000
|
-
// iframe loads modules from
|
|
47001
|
-
//
|
|
47047
|
+
// iframe loads modules from the API, which already permits null origin
|
|
47048
|
+
// via CORS.
|
|
47002
47049
|
cors: { origin: "*" },
|
|
47003
47050
|
// Chat-authoring previews reach this dev server through the sandbox proxy
|
|
47004
47051
|
// at \`5173-<id>-<token>.lotics-sandbox.app\`. Vite \u22655.4.12 rejects HMR
|
|
@@ -65067,7 +65114,7 @@ var appSchema = zod_default.object({
|
|
|
65067
65114
|
"Pointer to the currently-published app_versions row in R2. Set once the app has been deployed at least once via `lotics app deploy`; null before the first deploy."
|
|
65068
65115
|
),
|
|
65069
65116
|
public_subdomain: zod_default.string().describe(
|
|
65070
|
-
"DNS label for the app's public origin
|
|
65117
|
+
"DNS label for the app's public origin, under the instance's apps domain. Server-generated, high-entropy, and distinct from `id` (app_ids are not valid DNS labels). Assigned at creation for every app."
|
|
65071
65118
|
),
|
|
65072
65119
|
public_password_set: zod_default.boolean().optional().describe(
|
|
65073
65120
|
"Whether a shared password gates the public binding. True \u2192 anonymous visitors must authenticate at `/v1/apps/{app_id}/public/authenticate` before any publicAppAccess endpoint resolves. The hash itself is never sent over the wire; only this flag is exposed (and only on authenticated owner-side reads \u2014 the public by-subdomain response surfaces the same fact as `requires_password`)."
|
|
@@ -68942,8 +68989,8 @@ function noteCopiedContent(templates, knowledgeDocs) {
|
|
|
68942
68989
|
);
|
|
68943
68990
|
}
|
|
68944
68991
|
}
|
|
68945
|
-
async function libraryListPublic() {
|
|
68946
|
-
const [presets, packages] = await Promise.all([fetchPresetIndex(), fetchOfficialStarters()]);
|
|
68992
|
+
async function libraryListPublic(apiUrl) {
|
|
68993
|
+
const [presets, packages] = await Promise.all([fetchPresetIndex(), fetchOfficialStarters(apiUrl)]);
|
|
68947
68994
|
printPresetSection(presets);
|
|
68948
68995
|
printPackageSection(packages, "published by Lotics");
|
|
68949
68996
|
console.error(
|
|
@@ -69071,8 +69118,8 @@ function printPackageHead(pkg) {
|
|
|
69071
69118
|
console.log(`
|
|
69072
69119
|
version ${pkg.latest_version}`);
|
|
69073
69120
|
}
|
|
69074
|
-
async function libraryShowPublic(starter_id) {
|
|
69075
|
-
const pkg = await getOfficialStarter(starter_id);
|
|
69121
|
+
async function libraryShowPublic(apiUrl, starter_id) {
|
|
69122
|
+
const pkg = await getOfficialStarter(apiUrl, starter_id);
|
|
69076
69123
|
printPackageHead(pkg);
|
|
69077
69124
|
console.log(`trust official \u2014 reviewed by Lotics`);
|
|
69078
69125
|
printPackageEntities(pkg.contract.entities);
|
|
@@ -77541,7 +77588,7 @@ function withPlanFiles(starter, plan) {
|
|
|
77541
77588
|
async function appSetSubdomain(client, args) {
|
|
77542
77589
|
const meta3 = readAppMeta(process.cwd());
|
|
77543
77590
|
const result = await client.setAppSubdomain(meta3.app_id, args.subdomain);
|
|
77544
|
-
console.error(`Public address set:
|
|
77591
|
+
console.error(`Public address set: ${result.origin}`);
|
|
77545
77592
|
}
|
|
77546
77593
|
async function appRename(client, args) {
|
|
77547
77594
|
const meta3 = readAppMeta(process.cwd());
|
|
@@ -79363,33 +79410,37 @@ function signInInstructions(pending, options) {
|
|
|
79363
79410
|
${then}`;
|
|
79364
79411
|
}
|
|
79365
79412
|
async function startPendingLogin(email3) {
|
|
79366
|
-
const
|
|
79413
|
+
const apiUrl = envApiUrl();
|
|
79414
|
+
const request = await startCliLogin(apiUrl, email3);
|
|
79367
79415
|
const pending = {
|
|
79368
79416
|
request_id: request.request_id,
|
|
79369
79417
|
secret: request.secret,
|
|
79370
79418
|
code: request.code,
|
|
79371
79419
|
expires_at: request.expires_at,
|
|
79372
79420
|
email: email3,
|
|
79373
|
-
api_url:
|
|
79421
|
+
api_url: apiUrl
|
|
79374
79422
|
};
|
|
79375
79423
|
savePendingLogin(pending);
|
|
79376
79424
|
return pending;
|
|
79377
79425
|
}
|
|
79378
|
-
function saveApproval(state,
|
|
79426
|
+
function saveApproval(state, pending, options) {
|
|
79379
79427
|
requireNonEmpty(state.api_key, "api_key");
|
|
79380
79428
|
requireNonEmpty(state.organization_id, "organization_id");
|
|
79381
79429
|
requireNonEmpty(state.workspace_id, "workspace_id");
|
|
79382
79430
|
upsertProfile(state.organization_id, {
|
|
79383
79431
|
api_key: state.api_key,
|
|
79384
79432
|
org_name: state.organization_name,
|
|
79433
|
+
// The host that minted the key, taken from the request rather than re-read:
|
|
79434
|
+
// this is the whole of "which Lotics does this registration belong to".
|
|
79435
|
+
api_url: pending.api_url,
|
|
79385
79436
|
workspace_id: state.workspace_id
|
|
79386
79437
|
});
|
|
79387
79438
|
const existing = loadGlobalConfig() ?? {};
|
|
79388
|
-
saveGlobalConfig({ ...existing, email:
|
|
79439
|
+
saveGlobalConfig({ ...existing, email: pending.email });
|
|
79389
79440
|
setActiveOrg(state.organization_id, options?.local === true ? "local" : "global");
|
|
79390
79441
|
clearPendingLogin();
|
|
79391
79442
|
return {
|
|
79392
|
-
email:
|
|
79443
|
+
email: pending.email,
|
|
79393
79444
|
orgId: state.organization_id,
|
|
79394
79445
|
orgName: state.organization_name,
|
|
79395
79446
|
workspaceId: state.workspace_id
|
|
@@ -79399,14 +79450,15 @@ async function claimPendingLogin() {
|
|
|
79399
79450
|
const pending = loadPendingLogin();
|
|
79400
79451
|
if (!pending) return { status: "none" };
|
|
79401
79452
|
const expired = `The sign-in request expired. Run again: lotics auth login ${pending.email}`;
|
|
79453
|
+
const target = envApiUrl();
|
|
79402
79454
|
if (Date.parse(pending.expires_at) <= Date.now()) {
|
|
79403
79455
|
clearPendingLogin();
|
|
79404
|
-
return pending.api_url ===
|
|
79456
|
+
return pending.api_url === target ? { status: "gone", message: expired } : { status: "none" };
|
|
79405
79457
|
}
|
|
79406
|
-
if (pending.api_url !==
|
|
79407
|
-
const state = await pollCliLogin(pending.request_id, pending.secret);
|
|
79458
|
+
if (pending.api_url !== target) return { status: "none" };
|
|
79459
|
+
const state = await pollCliLogin(pending.api_url, pending.request_id, pending.secret);
|
|
79408
79460
|
if (state.status === "approved") {
|
|
79409
|
-
return { status: "signed_in", account: saveApproval(state, pending
|
|
79461
|
+
return { status: "signed_in", account: saveApproval(state, pending) };
|
|
79410
79462
|
}
|
|
79411
79463
|
if (state.status === "pending") {
|
|
79412
79464
|
return {
|
|
@@ -79427,14 +79479,14 @@ async function awaitCliLogin(email3, options) {
|
|
|
79427
79479
|
for (; ; ) {
|
|
79428
79480
|
let state;
|
|
79429
79481
|
try {
|
|
79430
|
-
state = await pollCliLogin(pending.request_id, pending.secret);
|
|
79482
|
+
state = await pollCliLogin(pending.api_url, pending.request_id, pending.secret);
|
|
79431
79483
|
} catch (error52) {
|
|
79432
79484
|
if (!(Date.now() < expiresAt)) throw error52;
|
|
79433
79485
|
await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL_MS));
|
|
79434
79486
|
continue;
|
|
79435
79487
|
}
|
|
79436
79488
|
if (state.status === "approved") {
|
|
79437
|
-
return saveApproval(state,
|
|
79489
|
+
return saveApproval(state, pending, options);
|
|
79438
79490
|
}
|
|
79439
79491
|
if (state.status === "expired") {
|
|
79440
79492
|
throw new Error(`That sign-in link expired. Run "lotics auth login ${email3}" again.`);
|
|
@@ -108451,8 +108503,8 @@ function prompt(question) {
|
|
|
108451
108503
|
});
|
|
108452
108504
|
});
|
|
108453
108505
|
}
|
|
108454
|
-
async function publicPost(path14, body) {
|
|
108455
|
-
const response = await fetch(`${
|
|
108506
|
+
async function publicPost(apiUrl, path14, body) {
|
|
108507
|
+
const response = await fetch(`${apiUrl}${path14}`, {
|
|
108456
108508
|
method: "POST",
|
|
108457
108509
|
headers: { "Content-Type": "application/json" },
|
|
108458
108510
|
body: JSON.stringify(body)
|
|
@@ -108465,7 +108517,8 @@ async function signupAccount(args) {
|
|
|
108465
108517
|
if (args.name) body.name = args.name;
|
|
108466
108518
|
const timezone = args.timezone ?? machineTimezone();
|
|
108467
108519
|
if (timezone) body.timezone = timezone;
|
|
108468
|
-
const
|
|
108520
|
+
const apiUrl = envApiUrl();
|
|
108521
|
+
const { ok, status, data: data2 } = await publicPost(apiUrl, "/v1/cli/signup", body);
|
|
108469
108522
|
if (!ok) {
|
|
108470
108523
|
if (status === 409) {
|
|
108471
108524
|
return { kind: "email_taken" };
|
|
@@ -108479,7 +108532,7 @@ async function signupAccount(args) {
|
|
|
108479
108532
|
const orgId = data2.organization_id;
|
|
108480
108533
|
const workspaceId = data2.workspace_id;
|
|
108481
108534
|
const orgName = args.name || args.email.split("@")[0];
|
|
108482
|
-
upsertProfile(orgId, { api_key: apiKey, org_name: orgName, workspace_id: workspaceId });
|
|
108535
|
+
upsertProfile(orgId, { api_key: apiKey, org_name: orgName, api_url: apiUrl, workspace_id: workspaceId });
|
|
108483
108536
|
const existing = loadGlobalConfig() ?? {};
|
|
108484
108537
|
saveGlobalConfig({ ...existing, email: args.email });
|
|
108485
108538
|
setActiveOrg(orgId, args.local ? "local" : "global");
|
|
@@ -108621,7 +108674,8 @@ async function handleSetup(providedKey, local) {
|
|
|
108621
108674
|
console.error("No API key provided.");
|
|
108622
108675
|
process.exit(1);
|
|
108623
108676
|
}
|
|
108624
|
-
const
|
|
108677
|
+
const apiUrl = envApiUrl();
|
|
108678
|
+
const client = new LoticsClient({ apiKey, apiUrl });
|
|
108625
108679
|
let info;
|
|
108626
108680
|
try {
|
|
108627
108681
|
info = await client.whoami();
|
|
@@ -108647,6 +108701,7 @@ Multiple workspaces in ${info.organization_name}. Run "lotics workspace select <
|
|
|
108647
108701
|
upsertProfile(info.organization_id, {
|
|
108648
108702
|
api_key: apiKey,
|
|
108649
108703
|
org_name: info.organization_name,
|
|
108704
|
+
api_url: apiUrl,
|
|
108650
108705
|
workspace_id: workspaceId
|
|
108651
108706
|
});
|
|
108652
108707
|
const existing = loadGlobalConfig() ?? {};
|
|
@@ -108684,9 +108739,9 @@ async function requireClient(flags, appWorkspaceId) {
|
|
|
108684
108739
|
}
|
|
108685
108740
|
const viewAsMemberId = flags.viewAs ?? process.env.LOTICS_VIEW_AS;
|
|
108686
108741
|
noteScope({ org_id: ctx.orgId ?? void 0, workspace_id: ctx.workspaceId });
|
|
108687
|
-
flushSpool((events) => postEvents(
|
|
108742
|
+
flushSpool(ctx.apiUrl, (events) => postEvents(ctx.apiUrl, ctx.apiKey, events));
|
|
108688
108743
|
return {
|
|
108689
|
-
client: new LoticsClient({ apiKey: ctx.apiKey, workspaceId: ctx.workspaceId, viewAsMemberId }),
|
|
108744
|
+
client: new LoticsClient({ apiKey: ctx.apiKey, apiUrl: ctx.apiUrl, workspaceId: ctx.workspaceId, viewAsMemberId }),
|
|
108690
108745
|
ctx
|
|
108691
108746
|
};
|
|
108692
108747
|
}
|
|
@@ -108775,7 +108830,7 @@ function looksLikeModelFile(arg) {
|
|
|
108775
108830
|
}
|
|
108776
108831
|
async function main() {
|
|
108777
108832
|
setInvocation(commandPath(process.argv.slice(2)), sessionId(), VERSION);
|
|
108778
|
-
installTelemetry(startedAt);
|
|
108833
|
+
installTelemetry(envApiUrl(), startedAt);
|
|
108779
108834
|
const parsed = parseArgs(process.argv.slice(2));
|
|
108780
108835
|
const { restArgs, flags } = parsed;
|
|
108781
108836
|
let { command, subcommand, toolArgs } = parsed;
|
|
@@ -108995,7 +109050,7 @@ async function main() {
|
|
|
108995
109050
|
if (command === "library") {
|
|
108996
109051
|
if (subcommand === "list") {
|
|
108997
109052
|
if (resolveContext(flags) === null) {
|
|
108998
|
-
await libraryListPublic();
|
|
109053
|
+
await libraryListPublic(envApiUrl());
|
|
108999
109054
|
return;
|
|
109000
109055
|
}
|
|
109001
109056
|
const { client: client2, ctx: ctx2 } = await requireClient(flags);
|
|
@@ -109026,11 +109081,12 @@ async function main() {
|
|
|
109026
109081
|
}
|
|
109027
109082
|
if (flags.json) {
|
|
109028
109083
|
setMachineOutput(true);
|
|
109029
|
-
|
|
109030
|
-
|
|
109084
|
+
const shelfCtx = resolveContext(flags);
|
|
109085
|
+
if (shelfCtx === null) {
|
|
109086
|
+
emitJson(await getOfficialStarter(envApiUrl(), toolArgs));
|
|
109031
109087
|
return;
|
|
109032
109088
|
}
|
|
109033
|
-
const published = await readOfficialStarter(toolArgs);
|
|
109089
|
+
const published = await readOfficialStarter(shelfCtx.apiUrl, toolArgs);
|
|
109034
109090
|
if (published.ok) {
|
|
109035
109091
|
emitJson(published.package);
|
|
109036
109092
|
return;
|
|
@@ -109052,7 +109108,7 @@ async function main() {
|
|
|
109052
109108
|
return;
|
|
109053
109109
|
}
|
|
109054
109110
|
if (resolveContext(flags) === null) {
|
|
109055
|
-
await libraryShowPublic(toolArgs);
|
|
109111
|
+
await libraryShowPublic(envApiUrl(), toolArgs);
|
|
109056
109112
|
return;
|
|
109057
109113
|
}
|
|
109058
109114
|
const { client: client2, ctx: ctx2 } = await requireClient(flags);
|
|
@@ -109173,7 +109229,7 @@ async function main() {
|
|
|
109173
109229
|
}
|
|
109174
109230
|
noteScope({ org_id: reportCtx.orgId ?? void 0, workspace_id: reportCtx.workspaceId });
|
|
109175
109231
|
await reportCommand(
|
|
109176
|
-
new LoticsClient({ apiKey: reportCtx.apiKey, workspaceId: reportCtx.workspaceId }),
|
|
109232
|
+
new LoticsClient({ apiKey: reportCtx.apiKey, apiUrl: reportCtx.apiUrl, workspaceId: reportCtx.workspaceId }),
|
|
109177
109233
|
{
|
|
109178
109234
|
report: parsed2.report,
|
|
109179
109235
|
version: VERSION,
|
|
@@ -109205,23 +109261,28 @@ async function main() {
|
|
|
109205
109261
|
console.error('Not authenticated. Run "lotics auth signup", "lotics auth api-key <key>", or set LOTICS_API_KEY.');
|
|
109206
109262
|
process.exit(1);
|
|
109207
109263
|
}
|
|
109208
|
-
const client2 = new LoticsClient({ apiKey: ctx2.apiKey });
|
|
109264
|
+
const client2 = new LoticsClient({ apiKey: ctx2.apiKey, apiUrl: ctx2.apiUrl });
|
|
109209
109265
|
const info = await client2.whoami();
|
|
109210
109266
|
const existing = loadGlobalConfig() ?? {};
|
|
109211
109267
|
saveGlobalConfig({ ...existing, email: info.email });
|
|
109212
109268
|
const cached2 = existing.profiles?.[info.organization_id];
|
|
109213
109269
|
if (cached2 && cached2.org_name !== info.organization_name) {
|
|
109214
|
-
upsertProfile(info.organization_id, {
|
|
109270
|
+
upsertProfile(info.organization_id, {
|
|
109271
|
+
api_key: cached2.api_key,
|
|
109272
|
+
org_name: info.organization_name,
|
|
109273
|
+
api_url: cached2.api_url
|
|
109274
|
+
});
|
|
109215
109275
|
console.error(`Renamed since this key was saved: "${cached2.org_name}" \u2192 "${info.organization_name}" (updated locally)`);
|
|
109216
109276
|
}
|
|
109217
109277
|
const viewAs = flags.viewAs ?? process.env.LOTICS_VIEW_AS;
|
|
109218
109278
|
if (flags.json) {
|
|
109219
|
-
console.log(JSON.stringify({ ...info, workspace_id: ctx2.workspaceId ?? null, source: ctx2.source, view_as: viewAs ?? null }, null, 2));
|
|
109279
|
+
console.log(JSON.stringify({ ...info, workspace_id: ctx2.workspaceId ?? null, api_url: ctx2.apiUrl, source: ctx2.source, view_as: viewAs ?? null }, null, 2));
|
|
109220
109280
|
} else {
|
|
109221
109281
|
console.log(`Name: ${info.name}`);
|
|
109222
109282
|
console.log(`Email: ${info.email}`);
|
|
109223
109283
|
console.log(`Org: ${info.organization_name} (${info.organization_id})`);
|
|
109224
109284
|
console.log(`Workspace: ${ctx2.workspaceId ?? "(none selected)"}`);
|
|
109285
|
+
console.log(`API: ${ctx2.apiUrl}`);
|
|
109225
109286
|
console.log(`Source: ${SOURCE_LABELS[ctx2.source]}`);
|
|
109226
109287
|
if (viewAs) console.log(`View as: ${viewAs} (admin preview; requires an admin key)`);
|
|
109227
109288
|
}
|
|
@@ -109302,7 +109363,7 @@ async function main() {
|
|
|
109302
109363
|
try {
|
|
109303
109364
|
const ctx2 = resolveContext(flags, appManifestWorkspaceId(command, subcommand, void 0, flags));
|
|
109304
109365
|
if (ctx2) {
|
|
109305
|
-
client2 = new LoticsClient({ apiKey: ctx2.apiKey, workspaceId: ctx2.workspaceId });
|
|
109366
|
+
client2 = new LoticsClient({ apiKey: ctx2.apiKey, apiUrl: ctx2.apiUrl, workspaceId: ctx2.workspaceId });
|
|
109306
109367
|
await resolveWorkspace(client2, ctx2);
|
|
109307
109368
|
}
|
|
109308
109369
|
} catch (err2) {
|
|
@@ -109323,7 +109384,7 @@ async function main() {
|
|
|
109323
109384
|
}
|
|
109324
109385
|
applyAppManifestWorkspace(ctx2, command, subcommand, projectDir, flags);
|
|
109325
109386
|
const viewAsMemberId = flags.viewAs ?? process.env.LOTICS_VIEW_AS;
|
|
109326
|
-
const client2 = new LoticsClient({ apiKey: ctx2.apiKey, workspaceId: ctx2.workspaceId, viewAsMemberId });
|
|
109387
|
+
const client2 = new LoticsClient({ apiKey: ctx2.apiKey, apiUrl: ctx2.apiUrl, workspaceId: ctx2.workspaceId, viewAsMemberId });
|
|
109327
109388
|
await resolveWorkspace(client2, ctx2);
|
|
109328
109389
|
await appCodegen({ projectDir, client: client2 });
|
|
109329
109390
|
return;
|
|
@@ -109353,7 +109414,11 @@ async function main() {
|
|
|
109353
109414
|
console.error("Note: a local pin (.lotics/config.json) overrides the global default in this directory. Use --local to change the pin here.");
|
|
109354
109415
|
}
|
|
109355
109416
|
}
|
|
109356
|
-
await validateOrgWorkspacePin(
|
|
109417
|
+
await validateOrgWorkspacePin(
|
|
109418
|
+
new LoticsClient({ apiKey: profile.api_key, apiUrl: profile.api_url }),
|
|
109419
|
+
orgId,
|
|
109420
|
+
profile
|
|
109421
|
+
);
|
|
109357
109422
|
return;
|
|
109358
109423
|
}
|
|
109359
109424
|
if (subcommand && subcommand !== "list") {
|
|
@@ -109372,6 +109437,7 @@ async function main() {
|
|
|
109372
109437
|
orgIds.map((id) => ({
|
|
109373
109438
|
org_id: id,
|
|
109374
109439
|
org_name: profiles[id].org_name,
|
|
109440
|
+
api_url: profiles[id].api_url,
|
|
109375
109441
|
workspace_id: profiles[id].workspace_id ?? null,
|
|
109376
109442
|
active: id === effectiveActive
|
|
109377
109443
|
})),
|
|
@@ -109381,7 +109447,7 @@ async function main() {
|
|
|
109381
109447
|
} else {
|
|
109382
109448
|
for (const id of orgIds) {
|
|
109383
109449
|
const marker = id === effectiveActive ? " (active)" : "";
|
|
109384
|
-
console.log(`${id} ${profiles[id].org_name}${marker}`);
|
|
109450
|
+
console.log(`${id} ${profiles[id].org_name} ${profiles[id].api_url}${marker}`);
|
|
109385
109451
|
}
|
|
109386
109452
|
}
|
|
109387
109453
|
return;
|
|
@@ -109414,7 +109480,7 @@ async function main() {
|
|
|
109414
109480
|
console.error(" lotics app workflow pull Rewrite src/workflows/*.ts from the server");
|
|
109415
109481
|
console.error(" lotics app workflow check [alias] Typecheck src/workflows bodies locally");
|
|
109416
109482
|
console.error(" lotics app query set <alias> Push lotics.queries.<alias> to apps.queries (no deploy)");
|
|
109417
|
-
console.error(" lotics app subdomain <new-subdomain> Rename the app's public
|
|
109483
|
+
console.error(" lotics app subdomain <new-subdomain> Rename the app's public address (its subdomain)");
|
|
109418
109484
|
console.error(` lotics app rename "<new name>" Rename the app's display name (launcher title)`);
|
|
109419
109485
|
console.error(" lotics app dev [path] Run the app locally with HMR + RPC forwarding");
|
|
109420
109486
|
process.exit(1);
|
|
@@ -109634,7 +109700,7 @@ Available workspaces:`);
|
|
|
109634
109700
|
const newSubdomain = toolArgs;
|
|
109635
109701
|
if (!newSubdomain) {
|
|
109636
109702
|
console.error("Usage: lotics app subdomain <new-subdomain>");
|
|
109637
|
-
console.error("Renames the app's public address \u2014
|
|
109703
|
+
console.error("Renames the app's public address \u2014 its subdomain under the apps domain");
|
|
109638
109704
|
process.exit(1);
|
|
109639
109705
|
}
|
|
109640
109706
|
await appSetSubdomain(client, { subdomain: newSubdomain });
|
package/dist/src/client.d.ts
CHANGED
|
@@ -21,6 +21,11 @@ export interface AppQueryFilterGroup {
|
|
|
21
21
|
export type AppQueryFilter = AppQueryFilterCondition | AppQueryFilterGroup;
|
|
22
22
|
export interface LoticsClientOptions {
|
|
23
23
|
apiKey: string;
|
|
24
|
+
/** The Lotics this key belongs to — `ResolvedContext.apiUrl`, which is the
|
|
25
|
+
* registration's own `api_url`. Required and never defaulted: a client that
|
|
26
|
+
* can fall back to an ambient host is a client that can send a private
|
|
27
|
+
* instance's key to production (`docs/on_premise.md` section 8). */
|
|
28
|
+
apiUrl: string;
|
|
24
29
|
workspaceId?: string;
|
|
25
30
|
/** Admin "View as": when set, every request carries `x-view-as-member-id`, so
|
|
26
31
|
* the backend evaluates IAM scoping (and `is_current_member`) as this member.
|
|
@@ -342,7 +347,6 @@ export declare class LoticsRequestError extends Error {
|
|
|
342
347
|
readonly body: Record<string, unknown>;
|
|
343
348
|
constructor(message: string, status: number, body: Record<string, unknown>);
|
|
344
349
|
}
|
|
345
|
-
export declare const API_BASE_URL: string;
|
|
346
350
|
/**
|
|
347
351
|
* The website: where a person goes when the CLI cannot finish the job, and where
|
|
348
352
|
* the presets are SERVED FROM. Held beside the API base so the pair is read
|
|
@@ -379,7 +383,7 @@ export interface OfficialStarter {
|
|
|
379
383
|
* starter for what I do, or should I build?" is decided before an account
|
|
380
384
|
* exists, so needing one to ask means signing up to find out the answer was no.
|
|
381
385
|
*/
|
|
382
|
-
export declare function fetchOfficialStarters(): Promise<OfficialStarter[]>;
|
|
386
|
+
export declare function fetchOfficialStarters(apiUrl: string): Promise<OfficialStarter[]>;
|
|
383
387
|
/**
|
|
384
388
|
* One keyless GET of JSON, bounded — the shape every read that predates an
|
|
385
389
|
* account takes.
|
|
@@ -419,7 +423,7 @@ export declare function getPublicJson(url: string, what: string): Promise<{
|
|
|
419
423
|
* throws: "unreachable" is not "not on the shelf", and answering both the same
|
|
420
424
|
* way would tell an owner their package is missing every time the link drops.
|
|
421
425
|
*/
|
|
422
|
-
export declare function readOfficialStarter(starter_id: string): Promise<{
|
|
426
|
+
export declare function readOfficialStarter(apiUrl: string, starter_id: string): Promise<{
|
|
423
427
|
ok: true;
|
|
424
428
|
package: OfficialStarterRead;
|
|
425
429
|
} | {
|
|
@@ -427,7 +431,7 @@ export declare function readOfficialStarter(starter_id: string): Promise<{
|
|
|
427
431
|
status: number;
|
|
428
432
|
}>;
|
|
429
433
|
/** The same read for a caller with nothing to fall back to — a refusal is the end. */
|
|
430
|
-
export declare function getOfficialStarter(starter_id: string): Promise<OfficialStarterRead>;
|
|
434
|
+
export declare function getOfficialStarter(apiUrl: string, starter_id: string): Promise<OfficialStarterRead>;
|
|
431
435
|
/** A column as the public read shows it: enough to write a model field from. */
|
|
432
436
|
export interface OfficialStarterField {
|
|
433
437
|
alias: string;
|
|
@@ -524,8 +528,8 @@ export type CliLoginState = {
|
|
|
524
528
|
* The same answer whether or not the address has an account: it would
|
|
525
529
|
* otherwise tell any stranger which emails are registered here.
|
|
526
530
|
*/
|
|
527
|
-
export declare function startCliLogin(email: string): Promise<CliLoginRequest>;
|
|
528
|
-
export declare function pollCliLogin(request_id: string, secret: string): Promise<CliLoginState>;
|
|
531
|
+
export declare function startCliLogin(apiUrl: string, email: string): Promise<CliLoginRequest>;
|
|
532
|
+
export declare function pollCliLogin(apiUrl: string, request_id: string, secret: string): Promise<CliLoginState>;
|
|
529
533
|
export declare class LoticsClient {
|
|
530
534
|
private apiKey;
|
|
531
535
|
private workspaceId;
|
|
@@ -533,8 +537,8 @@ export declare class LoticsClient {
|
|
|
533
537
|
* construction — surfaced so `lotics app dev` can show it in the banner. */
|
|
534
538
|
readonly viewAsMemberId: string | undefined;
|
|
535
539
|
/** API URL the client is configured against. Read-only after construction.
|
|
536
|
-
* Surfaced for callers that need to display
|
|
537
|
-
*
|
|
540
|
+
* Surfaced for callers that need to display it (`lotics app dev`'s banner) or
|
|
541
|
+
* to hand it on (the wrapper page's RPC target, the scaffold's font proxy). */
|
|
538
542
|
readonly baseUrl: string;
|
|
539
543
|
constructor(options: LoticsClientOptions);
|
|
540
544
|
private throwResponseError;
|
|
@@ -987,13 +991,15 @@ export declare class LoticsClient {
|
|
|
987
991
|
}>;
|
|
988
992
|
}>>;
|
|
989
993
|
/**
|
|
990
|
-
* Rename an app's public subdomain — its
|
|
991
|
-
* Mirrors PUT /v1/apps/{app_id}/subdomain
|
|
992
|
-
*
|
|
994
|
+
* Rename an app's public subdomain — the label its origin is built on.
|
|
995
|
+
* Mirrors PUT /v1/apps/{app_id}/subdomain, and returns the finished `origin`
|
|
996
|
+
* because only the instance knows the domain and scheme it serves apps on.
|
|
997
|
+
* The old subdomain stops resolving once the change lands.
|
|
993
998
|
*/
|
|
994
999
|
setAppSubdomain(app_id: string, public_subdomain: string): Promise<{
|
|
995
1000
|
app_id: string;
|
|
996
1001
|
public_subdomain: string;
|
|
1002
|
+
origin: string;
|
|
997
1003
|
}>;
|
|
998
1004
|
getAppVersion(app_id: string, version_id: string): Promise<{
|
|
999
1005
|
id: string;
|
package/dist/src/client.js
CHANGED
|
@@ -149,10 +149,9 @@ var LoticsRequestError = class extends Error {
|
|
|
149
149
|
status;
|
|
150
150
|
body;
|
|
151
151
|
};
|
|
152
|
-
var API_BASE_URL = process.env.LOTICS_API_URL ?? "https://api.lotics.ai";
|
|
153
152
|
var WEB_APP_URL = process.env.LOTICS_WEB_URL ?? "https://lotics.ai";
|
|
154
|
-
async function fetchOfficialStarters() {
|
|
155
|
-
const read = await getPublicJson(`${
|
|
153
|
+
async function fetchOfficialStarters(apiUrl) {
|
|
154
|
+
const read = await getPublicJson(`${apiUrl}/v1/starters/official`, "list the packages");
|
|
156
155
|
if (!read.ok) {
|
|
157
156
|
throw new Error(
|
|
158
157
|
`Lotics answered ${read.status} listing the packages. If this keeps happening, browse ${WEB_APP_URL}/docs/cli.`
|
|
@@ -172,16 +171,16 @@ async function getPublicJson(url, what) {
|
|
|
172
171
|
if (!response.ok) return { ok: false, status: response.status };
|
|
173
172
|
return { ok: true, body: await response.json() };
|
|
174
173
|
}
|
|
175
|
-
async function readOfficialStarter(starter_id) {
|
|
174
|
+
async function readOfficialStarter(apiUrl, starter_id) {
|
|
176
175
|
const read = await getPublicJson(
|
|
177
|
-
`${
|
|
176
|
+
`${apiUrl}/v1/starters/official/${encodeURIComponent(starter_id)}`,
|
|
178
177
|
`read ${starter_id}`
|
|
179
178
|
);
|
|
180
179
|
if (!read.ok) return read;
|
|
181
180
|
return { ok: true, package: read.body };
|
|
182
181
|
}
|
|
183
|
-
async function getOfficialStarter(starter_id) {
|
|
184
|
-
const read = await readOfficialStarter(starter_id);
|
|
182
|
+
async function getOfficialStarter(apiUrl, starter_id) {
|
|
183
|
+
const read = await readOfficialStarter(apiUrl, starter_id);
|
|
185
184
|
if (!read.ok) {
|
|
186
185
|
throw new Error(
|
|
187
186
|
`Lotics answered ${read.status} reading ${starter_id}. Only packages Lotics publishes can be read without an account.`
|
|
@@ -190,10 +189,10 @@ async function getOfficialStarter(starter_id) {
|
|
|
190
189
|
return read.package;
|
|
191
190
|
}
|
|
192
191
|
var PUBLIC_FETCH_TIMEOUT_MS = 1e4;
|
|
193
|
-
async function startCliLogin(email) {
|
|
192
|
+
async function startCliLogin(apiUrl, email) {
|
|
194
193
|
let response;
|
|
195
194
|
try {
|
|
196
|
-
response = await fetch(`${
|
|
195
|
+
response = await fetch(`${apiUrl}/v1/cli/login_requests`, {
|
|
197
196
|
method: "POST",
|
|
198
197
|
headers: { "Content-Type": "application/json" },
|
|
199
198
|
body: JSON.stringify({ email }),
|
|
@@ -209,8 +208,8 @@ async function startCliLogin(email) {
|
|
|
209
208
|
}
|
|
210
209
|
return await response.json();
|
|
211
210
|
}
|
|
212
|
-
async function pollCliLogin(request_id, secret) {
|
|
213
|
-
const url = `${
|
|
211
|
+
async function pollCliLogin(apiUrl, request_id, secret) {
|
|
212
|
+
const url = `${apiUrl}/v1/cli/login_requests/${encodeURIComponent(request_id)}`;
|
|
214
213
|
let response;
|
|
215
214
|
try {
|
|
216
215
|
response = await fetch(url, {
|
|
@@ -237,14 +236,14 @@ var LoticsClient = class {
|
|
|
237
236
|
* construction — surfaced so `lotics app dev` can show it in the banner. */
|
|
238
237
|
viewAsMemberId;
|
|
239
238
|
/** API URL the client is configured against. Read-only after construction.
|
|
240
|
-
* Surfaced for callers that need to display
|
|
241
|
-
*
|
|
239
|
+
* Surfaced for callers that need to display it (`lotics app dev`'s banner) or
|
|
240
|
+
* to hand it on (the wrapper page's RPC target, the scaffold's font proxy). */
|
|
242
241
|
baseUrl;
|
|
243
242
|
constructor(options) {
|
|
244
243
|
this.apiKey = options.apiKey;
|
|
245
244
|
this.workspaceId = options.workspaceId;
|
|
246
245
|
this.viewAsMemberId = options.viewAsMemberId;
|
|
247
|
-
this.baseUrl =
|
|
246
|
+
this.baseUrl = options.apiUrl;
|
|
248
247
|
}
|
|
249
248
|
/**
|
|
250
249
|
* The id is appended to the MESSAGE rather than carried on a field, because
|
|
@@ -647,9 +646,10 @@ var LoticsClient = class {
|
|
|
647
646
|
return tables.filter((t) => t !== null);
|
|
648
647
|
}
|
|
649
648
|
/**
|
|
650
|
-
* Rename an app's public subdomain — its
|
|
651
|
-
* Mirrors PUT /v1/apps/{app_id}/subdomain
|
|
652
|
-
*
|
|
649
|
+
* Rename an app's public subdomain — the label its origin is built on.
|
|
650
|
+
* Mirrors PUT /v1/apps/{app_id}/subdomain, and returns the finished `origin`
|
|
651
|
+
* because only the instance knows the domain and scheme it serves apps on.
|
|
652
|
+
* The old subdomain stops resolving once the change lands.
|
|
653
653
|
*/
|
|
654
654
|
async setAppSubdomain(app_id, public_subdomain) {
|
|
655
655
|
return this.request(
|
|
@@ -1161,7 +1161,6 @@ var LoticsClient = class {
|
|
|
1161
1161
|
}
|
|
1162
1162
|
};
|
|
1163
1163
|
export {
|
|
1164
|
-
API_BASE_URL,
|
|
1165
1164
|
LoticsClient,
|
|
1166
1165
|
LoticsRequestError,
|
|
1167
1166
|
WEB_APP_URL,
|
package/docs/cli_reference.md
CHANGED
|
@@ -7,11 +7,11 @@ Per-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. S
|
|
|
7
7
|
| `lotics` / `lotics --help` | Show full help with capabilities, tool categories, workflow |
|
|
8
8
|
| `lotics auth signup <email>` | Create account + org + API key, sends magic link email. Registers the new org as a profile; `--local` pins this directory to it (pointer) instead of setting the global default. |
|
|
9
9
|
| `lotics auth login <email>` | Sign in an account that already exists, on a machine holding no key. **Two steps, and it does not wait for the person.** The first prints the page to open — `https://lotics.ai/cli_login/<request_id>`, also mailed — and the code that page must show, records the request, and exits 0. They sign in there if asked, check the code and press Confirm. **Then the next command that needs a credential collects the key** before it does its own work, so the second step is just re-running whatever was wanted; a command run before Confirm exits 1 naming the page and the code again, and once the 15 minutes are up it says to ask again. The handful that run WITHOUT a credential — `library list`/`show`, `scaffold docs`/`check`, `app codegen`, `app workflow check` — claim nothing, so one of those run after Confirm still answers as though signed out. `--wait` keeps one command instead, holding the terminal until Confirm; `--local` pins this directory to that org rather than setting the global default, and implies `--wait` (a pin names THIS directory, so only the terminal that stays in it can write one). `--json` prints `organization_id`, `workspace_id` and `organization_name` when it finishes signed in, and `request_id`, `confirm_url`, `code`, `email`, `expires_at` when it is the first step. The request's secret is never printed and the org's key never leaves the store. |
|
|
10
|
-
| `lotics auth api-key [key]` | `whoami` → **upsert** the key's org as a profile in the global store (never overwrites). `--local` additionally pins this directory to it (pointer) instead of setting the global default. |
|
|
10
|
+
| `lotics auth api-key [key]` | `whoami` → **upsert** the key's org as a profile in the global store (never overwrites). The profile records the instance the key was verified against (`LOTICS_API_URL`, default `https://api.lotics.ai`), and every later command for that org goes there. `--local` additionally pins this directory to it (pointer) instead of setting the global default. |
|
|
11
11
|
| `lotics auth web` | Send a magic link email to access the web app (requires auth) |
|
|
12
|
-
| `lotics auth whoami` | Print active account name, email, org, resolved workspace, and the resolution **source** (flag/env/local/app-manifest/global). `--json` adds `workspace_id` + `source`. |
|
|
12
|
+
| `lotics auth whoami` | Print active account name, email, org, resolved workspace, the instance the credential belongs to, and the resolution **source** (flag/env/local/app-manifest/global). `--json` adds `workspace_id`, `api_url` + `source`. |
|
|
13
13
|
| `lotics auth logout [<name\|id>]` | In a pinned dir: delete the local pin. Else: remove one profile (default the active org). `--all`: wipe the global store. |
|
|
14
|
-
| `lotics org` | List saved orgs (profiles) from the global store, marks active for this directory (a local pin wins over the global default). |
|
|
14
|
+
| `lotics org` | List saved orgs (profiles) from the global store with the instance each belongs to, marks active for this directory (a local pin wins over the global default). |
|
|
15
15
|
| `lotics org use <name\|id> [--local]` | Switch the active org by org name (case-insensitive, ambiguous → error) or id. No flag → global `active_org`; `--local` → a `.lotics/config.json` pointer in the current dir. |
|
|
16
16
|
| `lotics workspace` | List workspaces in the active org, marks current with `(current)` |
|
|
17
17
|
| `lotics workspace select <id>` | Set the workspace in the **active scope** — a local pin if the dir has one, else the active org's global profile |
|
|
@@ -61,7 +61,7 @@ Per-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. S
|
|
|
61
61
|
| `lotics app query set <alias>` \| `--all` | Push `package.json#lotics.queries` (`{ ast, params? }` per alias) to `apps.queries` through `set_app_query` — **the only author of a query binding**, the mirror of `app workflow set`. A deploy pushes a DRIFTED declaration through this same verb before it ships (see `app deploy`), so this is the explicit single-alias path, not the only way a query reaches the app. The **server** validates each one exactly as it always did (alias identifier, workspace-only tables, resolvable fields, declared params). `--all` pushes every declared alias, alias-sorted, stopping at the first failure and naming what already landed. Clear error + non-zero exit on an alias absent from the manifest or a validation failure. **The declaration's fields MERGE**, so the manifest is not a snapshot: deleting `params` from an alias and pushing leaves the live params exactly where they were, because an absent key means "unchanged". Clear one with `params: null`, or replace the map with the set you want. After the push it regenerates `.lotics/app_queries.d.ts` from the manifest, so the types the next `npm run typecheck` reads match what was just pushed. |
|
|
62
62
|
| `lotics app workflow pull` | Rewrite every `src/workflows/<alias>.ts` from the server (faithful body per bound alias via `get_app_workflow`) **+ its `.lotics/workflows/<alias>.globals.d.ts`** (via `getAppWorkflowDts`, so the body is locally typecheckable via `lotics app workflow check`) without a full `app pull` (no source archive, no npm install). A legacy alias with no rendered source warns and is skipped; a dts-fetch failure is non-fatal (body still written with the fallback wrapper, typecheck degraded). Each alias's `description` is folded back into `package.json#lotics.workflows.<alias>` from the same read — the alias binding the manifest is otherwise stamped from carries `inputs`/`outputs` but not the description, which lives on the workflow ROW, so without this a pull would erase an authored one. The server's GENERATED default is skipped, so an app that never described its workflows gains no manifest noise. Also idempotently patches the main `tsconfig.json` `exclude` to cover `src/workflows` + `.lotics/workflows` so a pre-existing app's `npm run typecheck` never loads the bodies or the colliding per-alias globals. |
|
|
63
63
|
| `lotics app workflow check [alias]` | Check the editable workflow bodies locally, no auth / no network, in the **server's own order** — parse, then type-check. **Parse** runs `parseWorkflowJs` from `@lotics/shared` (the SAME module `verifyWorkflow` calls, never a second implementation) over the stripped body `set` would upload, with `toolNames: undefined` (the CLI ships no tool registry, so tool-name resolution stays a server check while every shape/scope rule runs here). A body the subset rejects reports **that error alone** and skips the compiler — it never reaches the server's compiler either, so tsc's opinion of it is noise. **Type-check** then builds an **isolated** `ts.Program` per alias from exactly that alias's `{body, globals}` pair — mirroring the server, which verifies one body at a time — so the per-alias ambient `trigger` never collides and `trigger.app_workflow.inputs` is checked against the right alias. All aliases run in ONE node process (N programs, not N `tsc` spawns), with the SAME compile options the server uses at set-time verify (lib `es2022` with no DOM, target ES2022, strict, NodeNext, `types:[]`, skipLibCheck) and the app's OWN `typescript` (resolved from its `node_modules`, never bundled into the CLI). What the compiler sees is the **checked source**, not the file: `rewriteAccumulatorAppends` from `@lotics/shared` — the SAME transform the server applies before its set-time compile — is applied in memory, so a pulled body's canonical `out = concat(out, [item])` accumulator checks green here exactly as it saves there, and the body on disk is never rewritten. Reports `<file>:<line>:<col> - <TS####\|subset>` at the **physical** line in `src/workflows/<alias>.ts`, so an editor jump lands on the offending code (these are deliberately NOT `set`'s body-relative numbers — `set` prints no file path, so there is no format to agree with); exits non-zero if any alias fails. Green is honest but not total: `set` additionally resolves names, lints and structurally validates against the live workspace — passes that need its tables and tool schemas, so they cannot run offline, and the success line says so. A bound alias with no body file yet warns + skips; a body with no globals errors (naming `lotics app codegen`, which refreshes types WITHOUT touching the body — a pull would overwrite it). **It also keeps the types honest.** Each alias's `.lotics/workflows/<alias>.globals.d.ts` carries a `// lotics:declaration <hash>` stamp of the manifest declaration it was rendered from; `check` compares it to `package.json#lotics.workflows.<alias>` and, when they differ, re-renders that alias's dts from the LOCAL declaration before compiling. Without it the verdict was confidently wrong in the exact case an author needs it — declare an input, run `check`, and get `TS2339: Property 'x' does not exist` pointing at your body for a schema the types have never been told about. The server renders a dts from a SUPPLIED declaration, so this works before the manifest has ever been deployed, which is when it matters (the order is edit → check → set). This is the ONE thing `check` uses the API for: it is skipped entirely when the stamps match (the common case, so `check` stays instant and offline), and with no credentials or a failed fetch it WARNS and checks against the older types rather than blocking. A file written before the stamp existed reads as unknown, never as matching, so a pre-existing checkout heals on its first run. |
|
|
64
|
-
| `lotics app subdomain <new-subdomain>` | Rename the app's public
|
|
64
|
+
| `lotics app subdomain <new-subdomain>` | Rename the app's public address under the instance's apps domain via `PUT /v1/apps/{id}/subdomain`. app_id comes from the local `package.json` manifest; the chosen slug must be a valid DNS label and free; the old address stops resolving. |
|
|
65
65
|
| `lotics app rename "<new name>"` | Change the app's display name (launcher/title) via the `update_app` tool. app_id comes from the local `package.json` manifest; the public address (`subdomain`) and code (`deploy`) are unchanged. |
|
|
66
66
|
| `lotics app dev [path] [--port=N] [--vite-port=N] [--view-as=<member_id>]` | Spawn Vite dev server + an RPC-forwarding HTTP server. The wrapper page embeds the iframe with `sandbox="allow-scripts allow-same-origin"` matching production; postMessage ops (query / workflow / members / context / upload / openExternal / urlState / agentRun) are forwarded to api.lotics.ai using the CLI's API key — file bytes move in **both** directions through the dev server's own relays, never browser↔storage: dev runs against the PROD bucket, whose CORS admits `https://*.lotics.app` and not `http://localhost:<port>`, so a direct browser transfer is blocked — no upload could complete and no preview engine (PDF/Word/Excel all FETCH the bytes) could read a file. `upload` mints a presigned URL and PUTs it **to `PUT /_upload/<file_id>`** (`dev/upload_relay.ts`) from the wrapper page — same-origin, so no preflight and no CORS — and Node forwards it on; every presigned `url`/`thumbnail_url`/`preview_url` on a **file object** in an RPC result is rewritten to **`GET /_file/<token>`** (`dev/file_relay.ts`, absolute — the iframe would resolve a relative path against Vite), which streams the bytes back with `Range` passthrough (206s intact, so PDF seeking works) and an `Access-Control-Allow-Origin` for the Vite origin (the one cross-origin hop left is OUR response to allow). Neither relay ever takes a destination from the client — it gets a `file_id`/token and transfers only to/from a URL it minted or observed itself, so there is no client-controlled target and no SSRF surface. A URL in a record's own text cell is NOT rewritten. Production is unchanged (direct-to-storage, no bytes through the API server); `openExternal` and `urlState.get/set` are handled locally (the latter read/write the wrapper page's own address bar — `set` writes in place via `replaceState` and browser back/forward broadcast a `url-state` message back, so `useUrlState` survives refresh and is shareable in the dev loop; in-app *routing* is the app's own (the iframe owns its url via `@lotics/app-sdk/router`), and the wrapper bakes the saved screen (`_loc`) into the iframe src on load so a refresh restores it, mirroring production); `agentRun` (streaming) is proxied through `POST /_agent_run`, which opens the run's SSE with the CLI key and pipes chunks back to the iframe (`stream-chunk`* → `stream-end`), so `useAgentRun` works in the dev loop just like production; `context` resolves the viewer (`member_id` from `cli/whoami` + `comments_enabled` from the local manifest) and fetches the app's stored `config` live from the app row, so `useConfig()` renders the same values as production. `--view-as` (global flag; also `LOTICS_VIEW_AS`) threads `x-view-as-member-id` so `is_current_member` + `context` resolve to that member — **admin key only** (the server 403s a non-admin), writes stay attributed to the key owner. Hot reload via Vite; full DevTools / Playwright access via plain localhost. **Every forwarded op logs one line naming its ALIAS** — `[rpc] query applicants 231ms` — and `query applicants (count)` for a count request, which is a SECOND full execution of the same query rather than a cheap lookup. When requests overlap the line carries `· N in flight`. That number is the one to watch: the server bounds how many app queries run at once, so requests past the bound wait and the wait lands inside each request's own duration — a burst reads as "every query got slower", which looks like a slow database and is not one. A screen firing its list plus three facet counts on one keystroke shows up here as eight lines over one or two aliases; see `@lotics/app-sdk` `docs/data_fetching.md` (`useCount`, and handing `usePaginatedQuery` a `total`) and `docs/queries.md` §10 for collapsing them. **Holds no realtime connection** — push belongs to the product frontend, so an app previewed here never updates on an external write (a CLI run, another tab, an agent): reload to see it. Deliberate rather than missing, since the alternative is a second implementation of the channel in the wrapper page, and a blanket poll here would hide an app whose queries do not declare their tables — the one mistake the real host punishes. The startup banner says `realtime: off` so this is visible without reading this table. The scaffold's `vite.config.ts` carries no dev-optimizer list: @lotics/ui ships built ESM, so Vite's own dep scanner reaches its CJS-interop imports and pre-bundles them without being told to. Binds **loopback only** (`127.0.0.1`) — `/_rpc` dispatches with the developer's API key, so a socket on every interface would hand anyone on the network full read/write on the workspace. |
|
|
67
67
|
| `LOTICS_UI_SRC=<abs path to packages/ui/src>` (env, not a command) | Dev-link `@lotics/ui` to a monorepo checkout for the length of ONE command, **for every tool at once**. The app's `vite.config.ts` gets its whole `resolve` block from the kit (`resolve: loticsResolve()` — `@lotics/ui/vite`), which reads the variable at call time and adds the `@lotics/ui/*` → working-copy alias, so kit edits go live under `lotics app dev` (HMR) and bundle under `lotics app deploy`. In the same breath, every command that regenerates types (`create`/`pull`/`dev`/`deploy`/`codegen`, all via `writeAppDts`) writes **`.lotics/tsconfig.link.json`** — the matching `paths`, which the app's `tsconfig.json` `extends` — so `tsc`, vitest, eslint and your EDITOR resolve the same copy Vite does. Unset ⇒ every one of them goes back to `node_modules`, and the generated file is rewritten inert. **Why `paths` and not `npm link`:** under the dev-link a kit file sits OUTSIDE the app's `node_modules` and resolves its OWN `react` from the monorepo — two copies in one program and every shared type stops matching ("Two different types with this name exist, but they are unrelated"). The generated file therefore also pins every peer @lotics/ui declares to the APP's copy, types-package first (`react` → `@types/react`; pinning the runtime package instead strands tsc on a `.js` with no declarations). The pin set is derived from the installed kit's `peerDependencies`, so it tracks the kit rather than rotting. **Nothing hand-written is touched** — the generated file lives in `.lotics/` (the CLI's own dir) and no config is edited by regex. Identical for a monorepo app and an EXTERNAL one (e.g. `~/lotics_apps`). `app deploy` still warns whenever the variable is set — that the bundle carries kit code from your working copy, or that the app's config predates `loticsResolve()` and never reads it, so the PUBLISHED kit is going out. An app whose `tsconfig.json` already `extends` something else is told rather than rewritten: add `./.lotics/tsconfig.link.json` to the array yourself. |
|