@lotics/cli 0.73.0 → 0.75.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.
@@ -1,4 +1,13 @@
1
1
  import { LoticsClient } from "./client.js";
2
+ /**
3
+ * Resolve the latest published version of a package from the npm registry.
4
+ * Returns null on any failure (network error, 404, malformed payload) so
5
+ * callers can fall back to a static pin rather than crashing `app create`.
6
+ *
7
+ * 1.5s timeout — npm registry is fast when reachable; the fallback is fine
8
+ * the rare times it isn't, and we don't want to block scaffold on a hang.
9
+ */
10
+ export declare function fetchLatestNpmVersion(packageName: string): Promise<string | null>;
2
11
  /**
3
12
  * Manifest declaration for one workflow alias:
4
13
  * `"alias": { workflow_id, inputs?: { key: { type, … } }, outputs?: { key: { type, … } } }`
@@ -32,7 +32,7 @@ import { loadProjectTypescript, checkWorkflowBodies, } from "./app_workflow_chec
32
32
  * 1.5s timeout — npm registry is fast when reachable; the fallback is fine
33
33
  * the rare times it isn't, and we don't want to block scaffold on a hang.
34
34
  */
35
- async function fetchLatestNpmVersion(packageName) {
35
+ export async function fetchLatestNpmVersion(packageName) {
36
36
  try {
37
37
  const controller = new AbortController();
38
38
  const timeout = setTimeout(() => controller.abort(), 1500);
package/dist/cli.js CHANGED
@@ -14,7 +14,7 @@ import { LoticsClient, API_BASE_URL } from "./client.js";
14
14
  import { resolveContext, deleteConfig, getConfigPath, loadGlobalConfig, saveGlobalConfig, loadLocalConfig, upsertProfile, removeProfile, setActiveOrg, setSelectedWorkspace, resolveProfileByNameOrId, checkForUpdate, } from "./config.js";
15
15
  import { VERSION } from "./version.js";
16
16
  import { appCreate, appPull, appDeploy, appDev, appSetSubdomain, appRename, appVersions, appCodegen, appExecuteWorkflow, appWorkflowSet, appWorkflowPull, appWorkflowCheck, appQuerySet, appUiLink, } from "./app_commands.js";
17
- import { packageInstall, packageEject, packageDoctor, packageUpgrade, parseResolveFlags, packageNew, packageBuild, packagePublish, packageDev, packageSync, packageReset, packageExtract, packageAdopt, packageFleetUpgrade, } from "./package_commands.js";
17
+ import { packageInstall, packageEject, packageDoctor, packageUpgrade, parseResolveFlags, packageNew, packageBuild, packagePublish, packageDev, packageSync, packageReset, packageExtract, packageAdopt, packageFleetUpgrade, packageYank, } from "./package_commands.js";
18
18
  import { parseArgs } from "./args.js";
19
19
  import { ingestJsonArgs } from "./inputs.js";
20
20
  import { runXlsxCommand } from "./xlsx.js";
@@ -124,6 +124,7 @@ COMMANDS
124
124
  origin app (reads .lotics/adopt_binding.json;
125
125
  --version N pins a specific version)
126
126
  lotics package fleet-upgrade <package_id> [--version N]
127
+ lotics package yank <package_id> <version> [--undo]
127
128
  Upgrade EVERY installation of the package across
128
129
  your org: applies where the preview is clean,
129
130
  skips + reports findings (exit 1 unless all current)
@@ -707,6 +708,7 @@ async function main() {
707
708
  console.error(" lotics package extract <app_id> [path] Promote a bespoke app to a draft package project");
708
709
  console.error(" lotics package adopt <app_id> [path] Bind the published project onto the origin app");
709
710
  console.error(" lotics package fleet-upgrade <package_id> [--version N] Upgrade every org installation (clean ones apply; findings skip)");
711
+ console.error(" lotics package yank <package_id> <version> [--undo] Refuse new installs/upgrades of a broken published version (pinned installations keep running)");
710
712
  process.exit(1);
711
713
  }
712
714
  if (command === "run" && !subcommand) {
@@ -736,6 +738,16 @@ async function main() {
736
738
  const { client, ctx } = requireClient(flags);
737
739
  // lotics workspace / lotics workspace list / lotics workspace select <id>
738
740
  if (command === "workspace") {
741
+ if (subcommand === "yank") {
742
+ const [packageId, versionRaw, maybeUndo] = toolArgs ? toolArgs.split(/\s+/) : [];
743
+ const version = Number(versionRaw);
744
+ if (!packageId || !Number.isInteger(version) || version <= 0) {
745
+ console.error("Usage: lotics package yank <package_id> <version> [--undo]");
746
+ process.exit(1);
747
+ }
748
+ await packageYank(client, { package_id: packageId, version, undo: maybeUndo === "--undo" });
749
+ return;
750
+ }
739
751
  if (subcommand === "doctor") {
740
752
  // The workspace command family runs before the global workspace
741
753
  // resolution — doctor needs the same first-workspace fallback every
package/dist/client.d.ts CHANGED
@@ -287,6 +287,17 @@ export declare class LoticsClient {
287
287
  * per-installation consent flow. Backs `lotics package fleet-upgrade`.
288
288
  * Admin-only; org-scoped (no workspace header needed).
289
289
  */
290
+ /**
291
+ * Yank / unyank a published package version — refuses NEW installs/upgrades/
292
+ * adopts targeting it; pinned installations keep running. Owner-org
293
+ * admin-only. Backs `lotics package yank`.
294
+ */
295
+ yankAppPackageVersion(package_id: string, version: number, yanked: boolean): Promise<{
296
+ package_id: string;
297
+ version: number;
298
+ yanked_at: string | null;
299
+ latest_version: number;
300
+ }>;
290
301
  fleetUpgradeAppPackage(package_id: string, body: {
291
302
  version?: number;
292
303
  }): Promise<{
@@ -559,6 +570,12 @@ export declare class LoticsClient {
559
570
  image: string | null;
560
571
  }>;
561
572
  }>;
573
+ /** A package installation's alias→id maps (fields/options/roles) — the app's runtime F/OPT resolution. */
574
+ appBinding(app_id: string): Promise<{
575
+ fields: Record<string, string>;
576
+ options: Record<string, string>;
577
+ roles: Record<string, string>;
578
+ }>;
562
579
  /**
563
580
  * Resolve the full option set (key, label, color) of a named query's select
564
581
  * columns — the picker companion to `appQuery`. Mirrors
package/dist/client.js CHANGED
@@ -255,6 +255,14 @@ export class LoticsClient {
255
255
  * per-installation consent flow. Backs `lotics package fleet-upgrade`.
256
256
  * Admin-only; org-scoped (no workspace header needed).
257
257
  */
258
+ /**
259
+ * Yank / unyank a published package version — refuses NEW installs/upgrades/
260
+ * adopts targeting it; pinned installations keep running. Owner-org
261
+ * admin-only. Backs `lotics package yank`.
262
+ */
263
+ async yankAppPackageVersion(package_id, version, yanked) {
264
+ return this.request("POST", `/v1/app-packages/${encodeURIComponent(package_id)}/versions/${version}/yank`, { yanked });
265
+ }
258
266
  async fleetUpgradeAppPackage(package_id, body) {
259
267
  return this.request("POST", `/v1/app-packages/${encodeURIComponent(package_id)}/fleet-upgrade`, body);
260
268
  }
@@ -433,6 +441,10 @@ export class LoticsClient {
433
441
  const qs = group_id ? `?group_id=${encodeURIComponent(group_id)}` : "";
434
442
  return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/members${qs}`);
435
443
  }
444
+ /** A package installation's alias→id maps (fields/options/roles) — the app's runtime F/OPT resolution. */
445
+ async appBinding(app_id) {
446
+ return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/binding`);
447
+ }
436
448
  /**
437
449
  * Resolve the full option set (key, label, color) of a named query's select
438
450
  * columns — the picker companion to `appQuery`. Mirrors
@@ -6,7 +6,7 @@
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" | "field_options" | "workflow" | "members" | "context" | "upload_url" | "upload_complete" | "comments.list" | "comments.create" | "comments.update" | "comments.delete" | "comments.counts";
9
+ export type RpcOp = "query" | "field_options" | "workflow" | "members" | "context" | "binding" | "upload_url" | "upload_complete" | "comments.list" | "comments.create" | "comments.update" | "comments.delete" | "comments.counts";
10
10
  export interface RpcRequest {
11
11
  app_id: string;
12
12
  op: RpcOp;
@@ -11,6 +11,7 @@ const SUPPORTED_OPS = new Set([
11
11
  "workflow",
12
12
  "members",
13
13
  "context",
14
+ "binding",
14
15
  "upload_url",
15
16
  "upload_complete",
16
17
  "comments.list",
@@ -76,6 +77,8 @@ export async function dispatchRpc(client, body, opts) {
76
77
  const p = body.payload;
77
78
  return client.appMembers(body.app_id, p?.group);
78
79
  }
80
+ case "binding":
81
+ return client.appBinding(body.app_id);
79
82
  case "upload_url": {
80
83
  const p = body.payload;
81
84
  if (!p ||
@@ -46,6 +46,8 @@ export interface TableSchema {
46
46
  * `upper` uppercases the result (TABLE aliases read as constants).
47
47
  */
48
48
  export declare function slugifyAlias(name: string, upper: boolean): string;
49
+ /** A property key for an object literal — bare when a valid identifier, else quoted. */
50
+ export declare function propKey(alias: string): string;
49
51
  /**
50
52
  * Generate the full `.lotics/app_fields.ts` source. `tables` is the resolved
51
53
  * workspace schema (the subset the app touches). An empty list yields valid,
@@ -68,7 +68,7 @@ function dedupeAliases(names, upper) {
68
68
  });
69
69
  }
70
70
  /** A property key for an object literal — bare when a valid identifier, else quoted. */
71
- function propKey(alias) {
71
+ export function propKey(alias) {
72
72
  return isValidIdentifier(alias) ? alias : JSON.stringify(alias);
73
73
  }
74
74
  /** Resolve table + field aliases once, so the `F`/`OPT` maps and the union types agree. */
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Codegen: emit a RUNTIME `.lotics/app_fields.ts` for a PACKAGE project from
3
+ * its `contract.json`.
4
+ *
5
+ * The bespoke twin (`generate_app_fields.ts`) bakes this workspace's concrete
6
+ * `fld_…`/`opt_…` ids at authoring time — correct when source and workspace
7
+ * co-live, fundamentally wrong for a package (one bundle, many workspaces,
8
+ * different ids in each). A package project instead resolves the SAME `F` /
9
+ * `OPT` surface at MODULE LOAD from the installation's binding via the SDK's
10
+ * `getAppBinding()` (the `binding` RPC op), using top-level await — the ESM
11
+ * graph waits for the binding before any importer evaluates, so
12
+ * `F.TASKS.title` is a plain string everywhere, including module-top-level
13
+ * constants. Extracted bespoke sources therefore compile unchanged: same
14
+ * import path, same shapes, same alias naming (contract aliases derive from
15
+ * the same display names the bespoke codegen slugifies).
16
+ *
17
+ * A missing alias fails LOUD at boot (the binding is adopt/install-verified
18
+ * complete, so a miss means the generated file is stale relative to the
19
+ * installed contract version) — never a silent undefined key in a write.
20
+ *
21
+ * Requires the app build to target es2022+ (the starter's `build.target`) —
22
+ * top-level await does not exist below it.
23
+ */
24
+ /** The subset of a contract this generator reads (structural, not zod-validated —
25
+ * publish is the validating boundary; codegen must work on drafts too). */
26
+ export interface PackageContractShape {
27
+ entities?: Array<{
28
+ alias: string;
29
+ fields?: Array<{
30
+ alias: string;
31
+ options?: Array<{
32
+ alias: string;
33
+ }>;
34
+ }>;
35
+ }>;
36
+ roles?: Array<{
37
+ alias: string;
38
+ }>;
39
+ }
40
+ /**
41
+ * Generate the full package `.lotics/app_fields.ts` source. Valid contract
42
+ * aliases are slug-grammar (`^[a-z][a-z0-9_]*$`) and emit as bare identifiers
43
+ * — but this also runs on unvalidated DRAFTS (dev/sync pre-publish), so every
44
+ * key goes through `propKey`, which quotes anything that isn't a valid
45
+ * identifier instead of emitting broken TypeScript into a "DO NOT EDIT" file.
46
+ * Pure; same contract → same bytes.
47
+ */
48
+ export declare function generatePackageAppFields(contract: PackageContractShape): string;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Codegen: emit a RUNTIME `.lotics/app_fields.ts` for a PACKAGE project from
3
+ * its `contract.json`.
4
+ *
5
+ * The bespoke twin (`generate_app_fields.ts`) bakes this workspace's concrete
6
+ * `fld_…`/`opt_…` ids at authoring time — correct when source and workspace
7
+ * co-live, fundamentally wrong for a package (one bundle, many workspaces,
8
+ * different ids in each). A package project instead resolves the SAME `F` /
9
+ * `OPT` surface at MODULE LOAD from the installation's binding via the SDK's
10
+ * `getAppBinding()` (the `binding` RPC op), using top-level await — the ESM
11
+ * graph waits for the binding before any importer evaluates, so
12
+ * `F.TASKS.title` is a plain string everywhere, including module-top-level
13
+ * constants. Extracted bespoke sources therefore compile unchanged: same
14
+ * import path, same shapes, same alias naming (contract aliases derive from
15
+ * the same display names the bespoke codegen slugifies).
16
+ *
17
+ * A missing alias fails LOUD at boot (the binding is adopt/install-verified
18
+ * complete, so a miss means the generated file is stale relative to the
19
+ * installed contract version) — never a silent undefined key in a write.
20
+ *
21
+ * Requires the app build to target es2022+ (the starter's `build.target`) —
22
+ * top-level await does not exist below it.
23
+ */
24
+ import { propKey } from "./generate_app_fields.js";
25
+ const HEADER = `// Auto-generated by 'lotics package new/extract/dev/sync' from contract.json.
26
+ // DO NOT EDIT — regenerated whenever the contract changes.
27
+ //
28
+ // Package apps resolve F/OPT/ROLE at MODULE LOAD from the installation's
29
+ // binding (contract alias → THIS workspace's concrete id) via the SDK's
30
+ // \`binding\` RPC. Top-level await: the module graph waits for the binding
31
+ // before any importer evaluates, so every entry is a plain string.
32
+ import { getAppBinding } from "@lotics/app-sdk";
33
+
34
+ const binding = await getAppBinding();
35
+
36
+ function bound(map: Record<string, string>, key: string, kind: string): string {
37
+ const id = map[key];
38
+ if (id === undefined) {
39
+ throw new Error(
40
+ \`app_fields: \${kind} "\${key}" is not in this installation's binding — \` +
41
+ \`the generated app_fields.ts is stale relative to the installed contract version.\`,
42
+ );
43
+ }
44
+ return id;
45
+ }
46
+ `;
47
+ /**
48
+ * Generate the full package `.lotics/app_fields.ts` source. Valid contract
49
+ * aliases are slug-grammar (`^[a-z][a-z0-9_]*$`) and emit as bare identifiers
50
+ * — but this also runs on unvalidated DRAFTS (dev/sync pre-publish), so every
51
+ * key goes through `propKey`, which quotes anything that isn't a valid
52
+ * identifier instead of emitting broken TypeScript into a "DO NOT EDIT" file.
53
+ * Pure; same contract → same bytes.
54
+ */
55
+ export function generatePackageAppFields(contract) {
56
+ const entities = contract.entities ?? [];
57
+ const roles = contract.roles ?? [];
58
+ const fieldBlocks = [];
59
+ for (const entity of entities) {
60
+ const lines = (entity.fields ?? []).map((field) => ` ${propKey(field.alias)}: bound(binding.fields, ${JSON.stringify(`${entity.alias}.${field.alias}`)}, "field"),`);
61
+ if (lines.length === 0)
62
+ continue;
63
+ fieldBlocks.push(` ${propKey(entity.alias.toUpperCase())}: {\n${lines.join("\n")}\n },`);
64
+ }
65
+ const optionBlocks = [];
66
+ for (const entity of entities) {
67
+ const perField = [];
68
+ for (const field of entity.fields ?? []) {
69
+ const options = field.options ?? [];
70
+ if (options.length === 0)
71
+ continue;
72
+ const lines = options.map((option) => ` ${propKey(option.alias)}: bound(binding.options, ${JSON.stringify(`${entity.alias}.${field.alias}:${option.alias}`)}, "option"),`);
73
+ perField.push(` ${propKey(field.alias)}: {\n${lines.join("\n")}\n },`);
74
+ }
75
+ if (perField.length > 0) {
76
+ optionBlocks.push(` ${propKey(entity.alias.toUpperCase())}: {\n${perField.join("\n")}\n },`);
77
+ }
78
+ }
79
+ const fMap = fieldBlocks.length > 0
80
+ ? `export const F = {\n${fieldBlocks.join("\n")}\n} as const;`
81
+ : `export const F = {} as const;`;
82
+ const optMap = optionBlocks.length > 0
83
+ ? `export const OPT = {\n${optionBlocks.join("\n")}\n} as const;`
84
+ : `export const OPT = {} as const;`;
85
+ const roleMap = roles.length > 0
86
+ ? `export const ROLE = {\n${roles
87
+ .map((role) => ` ${propKey(role.alias)}: bound(binding.roles, ${JSON.stringify(role.alias)}, "role"),`)
88
+ .join("\n")}\n} as const;`
89
+ : `export const ROLE = {} as const;`;
90
+ return `${HEADER}
91
+ ${fMap}
92
+
93
+ ${optMap}
94
+
95
+ ${roleMap}
96
+
97
+ /** Field-id alias map: \`F[<ENTITY>][<field>]\` is this installation's \`fld_…\` id. */
98
+ export type AppFields = typeof F;
99
+ /** Select-option alias map: \`OPT[<ENTITY>][<field>][<option>]\` is this installation's \`opt_…\` id. */
100
+ export type AppOptions = typeof OPT;
101
+ /** Role alias map: \`ROLE[<role>]\` is this installation's \`grp_…\` id. */
102
+ export type AppRoles = typeof ROLE;
103
+ `;
104
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { generatePackageAppFields } from "./generate_package_fields.js";
3
+ const contract = {
4
+ entities: [
5
+ {
6
+ alias: "tasks",
7
+ fields: [
8
+ { alias: "title" },
9
+ {
10
+ alias: "status",
11
+ options: [{ alias: "to_do" }, { alias: "doing" }, { alias: "done" }],
12
+ },
13
+ ],
14
+ },
15
+ { alias: "projects", fields: [{ alias: "name" }] },
16
+ ],
17
+ roles: [{ alias: "approver" }],
18
+ };
19
+ describe("generatePackageAppFields", () => {
20
+ it("emits runtime-resolved F/OPT/ROLE keyed by contract aliases", () => {
21
+ const src = generatePackageAppFields(contract);
22
+ // Resolves through the SDK binding at module load (top-level await).
23
+ expect(src).toContain('import { getAppBinding } from "@lotics/app-sdk";');
24
+ expect(src).toContain("const binding = await getAppBinding();");
25
+ // F: uppercased entity alias, fully-qualified binding keys.
26
+ expect(src).toContain('title: bound(binding.fields, "tasks.title", "field")');
27
+ expect(src).toContain('name: bound(binding.fields, "projects.name", "field")');
28
+ expect(src).toMatch(/TASKS: \{/);
29
+ expect(src).toMatch(/PROJECTS: \{/);
30
+ // OPT: qualified entity.field:opt keys; option-less fields absent.
31
+ expect(src).toContain('to_do: bound(binding.options, "tasks.status:to_do", "option")');
32
+ expect(src).toContain('done: bound(binding.options, "tasks.status:done", "option")');
33
+ expect(src).not.toContain('"tasks.title:');
34
+ // ROLE map from contract roles.
35
+ expect(src).toContain('approver: bound(binding.roles, "approver", "role")');
36
+ // No concrete id in any VALUE position — the module is workspace-agnostic.
37
+ // (Doc comments legitimately name the id prefixes, so match quoted ids.)
38
+ expect(src).not.toMatch(/"(fld|opt|tbl|grp)_/);
39
+ });
40
+ it("emits valid empty maps for an empty contract", () => {
41
+ const src = generatePackageAppFields({});
42
+ expect(src).toContain("export const F = {} as const;");
43
+ expect(src).toContain("export const OPT = {} as const;");
44
+ expect(src).toContain("export const ROLE = {} as const;");
45
+ // Still imports the SDK so the module shape is uniform.
46
+ expect(src).toContain("const binding = await getAppBinding();");
47
+ });
48
+ it("is deterministic — same contract, same bytes", () => {
49
+ expect(generatePackageAppFields(contract)).toBe(generatePackageAppFields(contract));
50
+ });
51
+ });
@@ -220,6 +220,17 @@ export declare function packageAdopt(client: LoticsClient, args: {
220
220
  * installations are reported per line and the process exits 1 so a release
221
221
  * script can gate on "fleet fully current".
222
222
  */
223
+ /**
224
+ * `lotics package yank <package_id> <version> [--undo]` — mark a published
225
+ * version uninstallable (or restore it). New installs/upgrades/adopts refuse a
226
+ * yanked version and "latest" skips it; installations already pinned keep
227
+ * running. Owner-org admin-only.
228
+ */
229
+ export declare function packageYank(client: LoticsClient, args: {
230
+ package_id: string;
231
+ version: number;
232
+ undo: boolean;
233
+ }): Promise<void>;
223
234
  export declare function packageFleetUpgrade(client: LoticsClient, args: {
224
235
  package_id: string;
225
236
  version?: number;
@@ -25,8 +25,9 @@ import fs from "node:fs";
25
25
  import path from "node:path";
26
26
  import { tmpdir } from "node:os";
27
27
  import "./client.js";
28
- import { buildStarterTemplate } from "./starter_template.js";
29
- import { appDirName, runNpm, runTar } from "./app_commands.js";
28
+ import { buildStarterTemplate, STARTER_FALLBACK_SDK_VERSION } from "./starter_template.js";
29
+ import { generatePackageAppFields, } from "./generate_package_fields.js";
30
+ import { appDirName, fetchLatestNpmVersion, runNpm, runTar } from "./app_commands.js";
30
31
  import { writeFileAtomic } from "./file_command_io.js";
31
32
  import { startDevServer, openBrowser } from "./dev/server.js";
32
33
  const CONTRACT_FILE = "contract.json";
@@ -160,6 +161,36 @@ export function formatExtractReport(report) {
160
161
  .map((f) => ` [${f.severity}] ${f.area}: ${f.message}`));
161
162
  return { lines, hasError: report.some((f) => f.severity === "error") };
162
163
  }
164
+ function dotLoticsDirEnsured(projectDir) {
165
+ const dir = path.join(projectDir, ".lotics");
166
+ fs.mkdirSync(dir, { recursive: true });
167
+ return dir;
168
+ }
169
+ /** Numeric "x.y.z" compare — no semver dep (the CLI has zero runtime deps). */
170
+ function cmpVersions(a, b) {
171
+ const pa = a.split(".").map(Number);
172
+ const pb = b.split(".").map(Number);
173
+ for (let i = 0; i < 3; i++) {
174
+ const d = (pa[i] || 0) - (pb[i] || 0);
175
+ if (d !== 0)
176
+ return d;
177
+ }
178
+ return 0;
179
+ }
180
+ /**
181
+ * The `@lotics/app-sdk` range a package project needs: the live npm latest,
182
+ * clamped to never fall below `STARTER_FALLBACK_SDK_VERSION` — the release
183
+ * that ships `getAppBinding`, which the generated `.lotics/app_fields.ts`
184
+ * imports. A `^0.x` caret range never crosses a minor, so pinning below the
185
+ * floor (offline, or npm not yet carrying the release) would permanently
186
+ * scaffold projects that cannot compile their own generated code.
187
+ */
188
+ function packageSdkRange(sdkLatest) {
189
+ const version = sdkLatest !== null && cmpVersions(sdkLatest, STARTER_FALLBACK_SDK_VERSION) > 0
190
+ ? sdkLatest
191
+ : STARTER_FALLBACK_SDK_VERSION;
192
+ return `^${version}`;
193
+ }
163
194
  /** Read + JSON-parse the project's contract.json (the alias-keyed declaration). */
164
195
  function readContract(projectDir) {
165
196
  const contractPath = path.join(projectDir, CONTRACT_FILE);
@@ -168,6 +199,21 @@ function readContract(projectDir) {
168
199
  }
169
200
  return JSON.parse(fs.readFileSync(contractPath, "utf-8"));
170
201
  }
202
+ /**
203
+ * (Re)generate the package project's `.lotics/app_fields.ts` from its
204
+ * contract.json — the runtime-resolving F/OPT/ROLE surface (see
205
+ * `generate_package_fields.ts`). Runs on new/extract and on every dev/sync so
206
+ * contract edits keep the aliases addressable. `.lotics` is in
207
+ * SOURCE_STAGE_EXCLUDES: the compiled module ships inside `dist/`, and a
208
+ * post-eject `lotics app pull` regenerates the bespoke (baked) variant.
209
+ */
210
+ function writePackageAppFields(projectDir) {
211
+ const contract = readContract(projectDir);
212
+ const dotLotics = path.join(projectDir, ".lotics");
213
+ fs.mkdirSync(dotLotics, { recursive: true });
214
+ fs.writeFileSync(path.join(dotLotics, "app_fields.ts"), generatePackageAppFields(contract));
215
+ console.error("Wrote .lotics/app_fields.ts (contract-derived, runtime-resolved)");
216
+ }
171
217
  /** Top-level project entries that never ship in the source archive. */
172
218
  const SOURCE_STAGE_EXCLUDES = new Set([
173
219
  "node_modules",
@@ -299,10 +345,21 @@ export async function packageNew(args) {
299
345
  // a package is workspace-agnostic, so those placeholders are replaced with the
300
346
  // package manifest below. The rest of the starter (config, src, tests) is the
301
347
  // package's code surface verbatim.
348
+ // Resolve the live @lotics/ui + @lotics/app-sdk versions like `app create`
349
+ // does — the generated .lotics/app_fields.ts imports `getAppBinding`, and a
350
+ // ^0.x caret range never crosses a minor, so a stale starter fallback would
351
+ // permanently pin the project below the API it needs (offline still works
352
+ // off STARTER_FALLBACK_*, kept ≥ that floor).
353
+ const [uiLatest, sdkLatest] = await Promise.all([
354
+ fetchLatestNpmVersion("@lotics/ui"),
355
+ fetchLatestNpmVersion("@lotics/app-sdk"),
356
+ ]);
302
357
  const files = buildStarterTemplate({
303
358
  app_name: args.name,
304
359
  app_id: "",
305
360
  workspace_id: "",
361
+ ui_version: uiLatest ? `^${uiLatest}` : undefined,
362
+ sdk_version: packageSdkRange(sdkLatest),
306
363
  });
307
364
  for (const file of files) {
308
365
  const fullPath = path.join(targetPath, file.path);
@@ -324,6 +381,7 @@ export async function packageNew(args) {
324
381
  fs.writeFileSync(fullPath, file.content);
325
382
  }
326
383
  fs.writeFileSync(path.join(targetPath, CONTRACT_FILE), JSON.stringify(starterContract(args.name), null, 2) + "\n");
384
+ writePackageAppFields(targetPath);
327
385
  console.error(`Scaffolded ${files.length + 1} files into ${targetPath}`);
328
386
  console.error("Installing npm dependencies...");
329
387
  await runNpm(["install"], targetPath);
@@ -426,6 +484,9 @@ async function syncToDevWorkspace(client, projectDir) {
426
484
  throw new Error(`Workspace ${devWorkspaceId} is not accessible with these credentials. Pass --workspace <dev_ws> (a workspace created with --dev).`);
427
485
  }
428
486
  assertDevWorkspace(workspace);
487
+ // Contract may have been edited since the last sync — regenerate the
488
+ // runtime F/OPT/ROLE surface before the build that publishVersion runs.
489
+ writePackageAppFields(projectDir);
429
490
  const { package_id, version } = await publishVersion(client, projectDir, {
430
491
  changelog: "dev sync",
431
492
  });
@@ -741,10 +802,57 @@ export async function packageExtract(client, args) {
741
802
  // Swap the app manifest (lotics.app_id/workspace_id) for a fresh, unpublished
742
803
  // package manifest — atomic write via the existing manifest writer.
743
804
  const appPkgJson = JSON.parse(fs.readFileSync(packageJsonPath(targetPath), "utf-8"));
744
- writePackageManifest(targetPath, draftPackageProjectFromApp(appPkgJson, { name: app.name, description: null }));
805
+ const draft = draftPackageProjectFromApp(appPkgJson, { name: app.name, description: null });
806
+ // The origin app's package.json rides in the source archive with ITS OWN
807
+ // @lotics/app-sdk range — raise it to the getAppBinding floor the generated
808
+ // .lotics/app_fields.ts needs (never lower an already-newer range).
809
+ const deps = isPlainObject(draft.pkgJson.dependencies) ? draft.pkgJson.dependencies : {};
810
+ const originRange = typeof deps["@lotics/app-sdk"] === "string" ? deps["@lotics/app-sdk"] : null;
811
+ const originFloor = originRange?.replace(/^[\^~]/, "") ?? null;
812
+ if (originFloor === null || cmpVersions(originFloor, STARTER_FALLBACK_SDK_VERSION) < 0) {
813
+ const raised = packageSdkRange(await fetchLatestNpmVersion("@lotics/app-sdk"));
814
+ draft.pkgJson.dependencies = { ...deps, "@lotics/app-sdk": raised };
815
+ console.error(`Raised @lotics/app-sdk to ${raised} (generated app_fields needs getAppBinding)`);
816
+ }
817
+ writePackageManifest(targetPath, draft);
745
818
  // The alias-keyed draft contract — the reviewable master. Pretty + trailing newline.
746
819
  fs.writeFileSync(path.join(targetPath, CONTRACT_FILE), JSON.stringify(extracted.contract, null, 2) + "\n");
747
820
  console.error(`Wrote ${CONTRACT_FILE}`);
821
+ // The origin's baked `.lotics/app_fields.ts` (concrete ids) never travels —
822
+ // regenerate it contract-derived so `F`/`OPT` resolve per-install at runtime
823
+ // and the extracted source compiles unchanged.
824
+ writePackageAppFields(targetPath);
825
+ // vite.config.ts is starter-owned machinery — refresh it wholesale so the
826
+ // package project carries the current starter config (notably
827
+ // `build.target: "es2022"`, which the generated app_fields' top-level await
828
+ // requires). Origin-side dev customizations (e.g. `lotics ui link` aliases)
829
+ // don't belong in a package.
830
+ const starterViteConfig = buildStarterTemplate({
831
+ app_name: app.name,
832
+ app_id: "",
833
+ workspace_id: "",
834
+ }).find((f) => f.path === "vite.config.ts");
835
+ if (starterViteConfig === undefined) {
836
+ throw new Error("starter template is missing vite.config.ts — cannot refresh the package project");
837
+ }
838
+ const viteConfigPath = path.join(targetPath, "vite.config.ts");
839
+ const originViteConfig = fs.existsSync(viteConfigPath)
840
+ ? fs.readFileSync(viteConfigPath, "utf-8")
841
+ : null;
842
+ fs.writeFileSync(viteConfigPath, starterViteConfig.content);
843
+ if (originViteConfig !== null && originViteConfig !== starterViteConfig.content) {
844
+ // The origin may carry legitimate customizations (optimizeDeps entries,
845
+ // plugins) beyond the dev-links that must not ship — never destroy them
846
+ // silently: stash the original for manual re-application.
847
+ const stash = path.join(dotLoticsDirEnsured(targetPath), "vite.config.origin.ts");
848
+ fs.writeFileSync(stash, originViteConfig);
849
+ console.error("Refreshed vite.config.ts from the starter (build.target es2022). The origin app's config " +
850
+ "differed — its original was saved to .lotics/vite.config.origin.ts; re-apply any " +
851
+ "custom optimizeDeps/plugins entries you still need (never dev-link aliases).");
852
+ }
853
+ else {
854
+ console.error("Refreshed vite.config.ts from the starter (build.target es2022)");
855
+ }
748
856
  // Stage each file-backed template at the `bytes_ref` the contract references.
749
857
  // The download lands in the target dir (fresh, so no name collision) under the
750
858
  // file's own name; rename it to the exact `bytes_ref` basename the contract
@@ -817,6 +925,23 @@ export async function packageAdopt(client, args) {
817
925
  * installations are reported per line and the process exits 1 so a release
818
926
  * script can gate on "fleet fully current".
819
927
  */
928
+ /**
929
+ * `lotics package yank <package_id> <version> [--undo]` — mark a published
930
+ * version uninstallable (or restore it). New installs/upgrades/adopts refuse a
931
+ * yanked version and "latest" skips it; installations already pinned keep
932
+ * running. Owner-org admin-only.
933
+ */
934
+ export async function packageYank(client, args) {
935
+ const result = await client.yankAppPackageVersion(args.package_id, args.version, !args.undo);
936
+ if (result.yanked_at !== null) {
937
+ console.error(`Yanked ${result.package_id} v${result.version} (${result.yanked_at}). ` +
938
+ `New installs/upgrades refuse it; pinned installations keep running.`);
939
+ }
940
+ else {
941
+ console.error(`Restored ${result.package_id} v${result.version} — installable again.`);
942
+ }
943
+ console.error(` Latest installable version: ${result.latest_version === 0 ? "none" : `v${result.latest_version}`}`);
944
+ }
820
945
  export async function packageFleetUpgrade(client, args) {
821
946
  const result = await client.fleetUpgradeAppPackage(args.package_id, {
822
947
  ...(args.version !== undefined ? { version: args.version } : {}),
package/dist/src/cli.js CHANGED
@@ -29818,6 +29818,18 @@ var LoticsClient = class {
29818
29818
  * per-installation consent flow. Backs `lotics package fleet-upgrade`.
29819
29819
  * Admin-only; org-scoped (no workspace header needed).
29820
29820
  */
29821
+ /**
29822
+ * Yank / unyank a published package version — refuses NEW installs/upgrades/
29823
+ * adopts targeting it; pinned installations keep running. Owner-org
29824
+ * admin-only. Backs `lotics package yank`.
29825
+ */
29826
+ async yankAppPackageVersion(package_id, version, yanked) {
29827
+ return this.request(
29828
+ "POST",
29829
+ `/v1/app-packages/${encodeURIComponent(package_id)}/versions/${version}/yank`,
29830
+ { yanked }
29831
+ );
29832
+ }
29821
29833
  async fleetUpgradeAppPackage(package_id, body) {
29822
29834
  return this.request(
29823
29835
  "POST",
@@ -30012,6 +30024,10 @@ var LoticsClient = class {
30012
30024
  const qs = group_id ? `?group_id=${encodeURIComponent(group_id)}` : "";
30013
30025
  return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/members${qs}`);
30014
30026
  }
30027
+ /** A package installation's alias→id maps (fields/options/roles) — the app's runtime F/OPT resolution. */
30028
+ async appBinding(app_id) {
30029
+ return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/binding`);
30030
+ }
30015
30031
  /**
30016
30032
  * Resolve the full option set (key, label, color) of a named query's select
30017
30033
  * columns — the picker companion to `appQuery`. Mirrors
@@ -30554,7 +30570,7 @@ import { tmpdir } from "node:os";
30554
30570
 
30555
30571
  // src/starter_template.ts
30556
30572
  var STARTER_FALLBACK_UI_VERSION = "6.1.0";
30557
- var STARTER_FALLBACK_SDK_VERSION = "0.40.0";
30573
+ var STARTER_FALLBACK_SDK_VERSION = "0.51.0";
30558
30574
  var STARTER_REACT_NATIVE_VERSION = "0.85.3";
30559
30575
  function buildStarterTemplate(args) {
30560
30576
  const uiVersion = args.ui_version ?? `^${STARTER_FALLBACK_UI_VERSION}`;
@@ -30774,6 +30790,11 @@ export default defineConfig({
30774
30790
  build: {
30775
30791
  outDir: "dist",
30776
30792
  sourcemap: true,
30793
+ // Top-level await (package projects' generated .lotics/app_fields.ts
30794
+ // resolves the installation binding at module load) needs es2022 \u2014 Vite's
30795
+ // default 'modules' baseline is es2020 and esbuild hard-fails TLA there.
30796
+ // Dev already transforms at esnext, so this only aligns the prod build.
30797
+ target: "es2022",
30777
30798
  },
30778
30799
  server: {
30779
30800
  // Allow the sandboxed null-origin iframe used by \`lotics app dev\` to
@@ -31277,6 +31298,7 @@ var SUPPORTED_OPS = /* @__PURE__ */ new Set([
31277
31298
  "workflow",
31278
31299
  "members",
31279
31300
  "context",
31301
+ "binding",
31280
31302
  "upload_url",
31281
31303
  "upload_complete",
31282
31304
  "comments.list",
@@ -31334,6 +31356,8 @@ async function dispatchRpc(client, body, opts) {
31334
31356
  const p = body.payload;
31335
31357
  return client.appMembers(body.app_id, p?.group);
31336
31358
  }
31359
+ case "binding":
31360
+ return client.appBinding(body.app_id);
31337
31361
  case "upload_url": {
31338
31362
  const p = body.payload;
31339
31363
  if (!p || typeof p.filename !== "string" || typeof p.mime_type !== "string" || typeof p.file_size !== "number") {
@@ -33351,6 +33375,86 @@ import fs6 from "node:fs";
33351
33375
  import path7 from "node:path";
33352
33376
  import { tmpdir as tmpdir2 } from "node:os";
33353
33377
 
33378
+ // src/generate_package_fields.ts
33379
+ var HEADER5 = `// Auto-generated by 'lotics package new/extract/dev/sync' from contract.json.
33380
+ // DO NOT EDIT \u2014 regenerated whenever the contract changes.
33381
+ //
33382
+ // Package apps resolve F/OPT/ROLE at MODULE LOAD from the installation's
33383
+ // binding (contract alias \u2192 THIS workspace's concrete id) via the SDK's
33384
+ // \`binding\` RPC. Top-level await: the module graph waits for the binding
33385
+ // before any importer evaluates, so every entry is a plain string.
33386
+ import { getAppBinding } from "@lotics/app-sdk";
33387
+
33388
+ const binding = await getAppBinding();
33389
+
33390
+ function bound(map: Record<string, string>, key: string, kind: string): string {
33391
+ const id = map[key];
33392
+ if (id === undefined) {
33393
+ throw new Error(
33394
+ \`app_fields: \${kind} "\${key}" is not in this installation's binding \u2014 \` +
33395
+ \`the generated app_fields.ts is stale relative to the installed contract version.\`,
33396
+ );
33397
+ }
33398
+ return id;
33399
+ }
33400
+ `;
33401
+ function generatePackageAppFields(contract) {
33402
+ const entities = contract.entities ?? [];
33403
+ const roles = contract.roles ?? [];
33404
+ const fieldBlocks = [];
33405
+ for (const entity of entities) {
33406
+ const lines = (entity.fields ?? []).map(
33407
+ (field) => ` ${propKey(field.alias)}: bound(binding.fields, ${JSON.stringify(`${entity.alias}.${field.alias}`)}, "field"),`
33408
+ );
33409
+ if (lines.length === 0) continue;
33410
+ fieldBlocks.push(` ${propKey(entity.alias.toUpperCase())}: {
33411
+ ${lines.join("\n")}
33412
+ },`);
33413
+ }
33414
+ const optionBlocks = [];
33415
+ for (const entity of entities) {
33416
+ const perField = [];
33417
+ for (const field of entity.fields ?? []) {
33418
+ const options = field.options ?? [];
33419
+ if (options.length === 0) continue;
33420
+ const lines = options.map(
33421
+ (option) => ` ${propKey(option.alias)}: bound(binding.options, ${JSON.stringify(`${entity.alias}.${field.alias}:${option.alias}`)}, "option"),`
33422
+ );
33423
+ perField.push(` ${propKey(field.alias)}: {
33424
+ ${lines.join("\n")}
33425
+ },`);
33426
+ }
33427
+ if (perField.length > 0) {
33428
+ optionBlocks.push(` ${propKey(entity.alias.toUpperCase())}: {
33429
+ ${perField.join("\n")}
33430
+ },`);
33431
+ }
33432
+ }
33433
+ const fMap = fieldBlocks.length > 0 ? `export const F = {
33434
+ ${fieldBlocks.join("\n")}
33435
+ } as const;` : `export const F = {} as const;`;
33436
+ const optMap = optionBlocks.length > 0 ? `export const OPT = {
33437
+ ${optionBlocks.join("\n")}
33438
+ } as const;` : `export const OPT = {} as const;`;
33439
+ const roleMap = roles.length > 0 ? `export const ROLE = {
33440
+ ${roles.map((role) => ` ${propKey(role.alias)}: bound(binding.roles, ${JSON.stringify(role.alias)}, "role"),`).join("\n")}
33441
+ } as const;` : `export const ROLE = {} as const;`;
33442
+ return `${HEADER5}
33443
+ ${fMap}
33444
+
33445
+ ${optMap}
33446
+
33447
+ ${roleMap}
33448
+
33449
+ /** Field-id alias map: \`F[<ENTITY>][<field>]\` is this installation's \`fld_\u2026\` id. */
33450
+ export type AppFields = typeof F;
33451
+ /** Select-option alias map: \`OPT[<ENTITY>][<field>][<option>]\` is this installation's \`opt_\u2026\` id. */
33452
+ export type AppOptions = typeof OPT;
33453
+ /** Role alias map: \`ROLE[<role>]\` is this installation's \`grp_\u2026\` id. */
33454
+ export type AppRoles = typeof ROLE;
33455
+ `;
33456
+ }
33457
+
33354
33458
  // src/file_command_io.ts
33355
33459
  import fs5 from "node:fs";
33356
33460
  import path6 from "node:path";
@@ -33474,6 +33578,24 @@ function formatExtractReport(report) {
33474
33578
  );
33475
33579
  return { lines, hasError: report.some((f) => f.severity === "error") };
33476
33580
  }
33581
+ function dotLoticsDirEnsured(projectDir) {
33582
+ const dir = path7.join(projectDir, ".lotics");
33583
+ fs6.mkdirSync(dir, { recursive: true });
33584
+ return dir;
33585
+ }
33586
+ function cmpVersions(a, b) {
33587
+ const pa = a.split(".").map(Number);
33588
+ const pb = b.split(".").map(Number);
33589
+ for (let i2 = 0; i2 < 3; i2++) {
33590
+ const d = (pa[i2] || 0) - (pb[i2] || 0);
33591
+ if (d !== 0) return d;
33592
+ }
33593
+ return 0;
33594
+ }
33595
+ function packageSdkRange(sdkLatest) {
33596
+ const version = sdkLatest !== null && cmpVersions(sdkLatest, STARTER_FALLBACK_SDK_VERSION) > 0 ? sdkLatest : STARTER_FALLBACK_SDK_VERSION;
33597
+ return `^${version}`;
33598
+ }
33477
33599
  function readContract(projectDir) {
33478
33600
  const contractPath = path7.join(projectDir, CONTRACT_FILE);
33479
33601
  if (!fs6.existsSync(contractPath)) {
@@ -33483,6 +33605,13 @@ function readContract(projectDir) {
33483
33605
  }
33484
33606
  return JSON.parse(fs6.readFileSync(contractPath, "utf-8"));
33485
33607
  }
33608
+ function writePackageAppFields(projectDir) {
33609
+ const contract = readContract(projectDir);
33610
+ const dotLotics = path7.join(projectDir, ".lotics");
33611
+ fs6.mkdirSync(dotLotics, { recursive: true });
33612
+ fs6.writeFileSync(path7.join(dotLotics, "app_fields.ts"), generatePackageAppFields(contract));
33613
+ console.error("Wrote .lotics/app_fields.ts (contract-derived, runtime-resolved)");
33614
+ }
33486
33615
  var SOURCE_STAGE_EXCLUDES = /* @__PURE__ */ new Set([
33487
33616
  "node_modules",
33488
33617
  "dist",
@@ -33576,10 +33705,16 @@ async function packageNew(args) {
33576
33705
  throw new Error(`Target directory ${targetPath} is not empty.`);
33577
33706
  }
33578
33707
  fs6.mkdirSync(targetPath, { recursive: true });
33708
+ const [uiLatest, sdkLatest] = await Promise.all([
33709
+ fetchLatestNpmVersion("@lotics/ui"),
33710
+ fetchLatestNpmVersion("@lotics/app-sdk")
33711
+ ]);
33579
33712
  const files = buildStarterTemplate({
33580
33713
  app_name: args.name,
33581
33714
  app_id: "",
33582
- workspace_id: ""
33715
+ workspace_id: "",
33716
+ ui_version: uiLatest ? `^${uiLatest}` : void 0,
33717
+ sdk_version: packageSdkRange(sdkLatest)
33583
33718
  });
33584
33719
  for (const file of files) {
33585
33720
  const fullPath = path7.join(targetPath, file.path);
@@ -33604,6 +33739,7 @@ async function packageNew(args) {
33604
33739
  path7.join(targetPath, CONTRACT_FILE),
33605
33740
  JSON.stringify(starterContract(args.name), null, 2) + "\n"
33606
33741
  );
33742
+ writePackageAppFields(targetPath);
33607
33743
  console.error(`Scaffolded ${files.length + 1} files into ${targetPath}`);
33608
33744
  console.error("Installing npm dependencies...");
33609
33745
  await runNpm(["install"], targetPath);
@@ -33675,6 +33811,7 @@ async function syncToDevWorkspace(client, projectDir) {
33675
33811
  );
33676
33812
  }
33677
33813
  assertDevWorkspace(workspace);
33814
+ writePackageAppFields(projectDir);
33678
33815
  const { package_id, version } = await publishVersion(client, projectDir, {
33679
33816
  changelog: "dev sync"
33680
33817
  });
@@ -33950,15 +34087,42 @@ async function packageExtract(client, args) {
33950
34087
  const appPkgJson = JSON.parse(
33951
34088
  fs6.readFileSync(packageJsonPath(targetPath), "utf-8")
33952
34089
  );
33953
- writePackageManifest(
33954
- targetPath,
33955
- draftPackageProjectFromApp(appPkgJson, { name: app.name, description: null })
33956
- );
34090
+ const draft = draftPackageProjectFromApp(appPkgJson, { name: app.name, description: null });
34091
+ const deps = isPlainObject(draft.pkgJson.dependencies) ? draft.pkgJson.dependencies : {};
34092
+ const originRange = typeof deps["@lotics/app-sdk"] === "string" ? deps["@lotics/app-sdk"] : null;
34093
+ const originFloor = originRange?.replace(/^[\^~]/, "") ?? null;
34094
+ if (originFloor === null || cmpVersions(originFloor, STARTER_FALLBACK_SDK_VERSION) < 0) {
34095
+ const raised = packageSdkRange(await fetchLatestNpmVersion("@lotics/app-sdk"));
34096
+ draft.pkgJson.dependencies = { ...deps, "@lotics/app-sdk": raised };
34097
+ console.error(`Raised @lotics/app-sdk to ${raised} (generated app_fields needs getAppBinding)`);
34098
+ }
34099
+ writePackageManifest(targetPath, draft);
33957
34100
  fs6.writeFileSync(
33958
34101
  path7.join(targetPath, CONTRACT_FILE),
33959
34102
  JSON.stringify(extracted.contract, null, 2) + "\n"
33960
34103
  );
33961
34104
  console.error(`Wrote ${CONTRACT_FILE}`);
34105
+ writePackageAppFields(targetPath);
34106
+ const starterViteConfig = buildStarterTemplate({
34107
+ app_name: app.name,
34108
+ app_id: "",
34109
+ workspace_id: ""
34110
+ }).find((f) => f.path === "vite.config.ts");
34111
+ if (starterViteConfig === void 0) {
34112
+ throw new Error("starter template is missing vite.config.ts \u2014 cannot refresh the package project");
34113
+ }
34114
+ const viteConfigPath = path7.join(targetPath, "vite.config.ts");
34115
+ const originViteConfig = fs6.existsSync(viteConfigPath) ? fs6.readFileSync(viteConfigPath, "utf-8") : null;
34116
+ fs6.writeFileSync(viteConfigPath, starterViteConfig.content);
34117
+ if (originViteConfig !== null && originViteConfig !== starterViteConfig.content) {
34118
+ const stash = path7.join(dotLoticsDirEnsured(targetPath), "vite.config.origin.ts");
34119
+ fs6.writeFileSync(stash, originViteConfig);
34120
+ console.error(
34121
+ "Refreshed vite.config.ts from the starter (build.target es2022). The origin app's config differed \u2014 its original was saved to .lotics/vite.config.origin.ts; re-apply any custom optimizeDeps/plugins entries you still need (never dev-link aliases)."
34122
+ );
34123
+ } else {
34124
+ console.error("Refreshed vite.config.ts from the starter (build.target es2022)");
34125
+ }
33962
34126
  for (const tf of extracted.template_files) {
33963
34127
  const dest = path7.join(targetPath, tf.bytes_ref);
33964
34128
  fs6.mkdirSync(path7.dirname(dest), { recursive: true });
@@ -34029,6 +34193,17 @@ async function packageAdopt(client, args) {
34029
34193
  );
34030
34194
  console.error(` Verify the installation: lotics package doctor ${app.id}`);
34031
34195
  }
34196
+ async function packageYank(client, args) {
34197
+ const result = await client.yankAppPackageVersion(args.package_id, args.version, !args.undo);
34198
+ if (result.yanked_at !== null) {
34199
+ console.error(
34200
+ `Yanked ${result.package_id} v${result.version} (${result.yanked_at}). New installs/upgrades refuse it; pinned installations keep running.`
34201
+ );
34202
+ } else {
34203
+ console.error(`Restored ${result.package_id} v${result.version} \u2014 installable again.`);
34204
+ }
34205
+ console.error(` Latest installable version: ${result.latest_version === 0 ? "none" : `v${result.latest_version}`}`);
34206
+ }
34032
34207
  async function packageFleetUpgrade(client, args) {
34033
34208
  const result = await client.fleetUpgradeAppPackage(args.package_id, {
34034
34209
  ...args.version !== void 0 ? { version: args.version } : {}
@@ -50856,6 +51031,7 @@ COMMANDS
50856
51031
  origin app (reads .lotics/adopt_binding.json;
50857
51032
  --version N pins a specific version)
50858
51033
  lotics package fleet-upgrade <package_id> [--version N]
51034
+ lotics package yank <package_id> <version> [--undo]
50859
51035
  Upgrade EVERY installation of the package across
50860
51036
  your org: applies where the preview is clean,
50861
51037
  skips + reports findings (exit 1 unless all current)
@@ -51386,6 +51562,7 @@ async function main() {
51386
51562
  console.error(" lotics package extract <app_id> [path] Promote a bespoke app to a draft package project");
51387
51563
  console.error(" lotics package adopt <app_id> [path] Bind the published project onto the origin app");
51388
51564
  console.error(" lotics package fleet-upgrade <package_id> [--version N] Upgrade every org installation (clean ones apply; findings skip)");
51565
+ console.error(" lotics package yank <package_id> <version> [--undo] Refuse new installs/upgrades of a broken published version (pinned installations keep running)");
51389
51566
  process.exit(1);
51390
51567
  }
51391
51568
  if (command === "run" && !subcommand) {
@@ -51414,6 +51591,16 @@ async function main() {
51414
51591
  }
51415
51592
  const { client, ctx } = requireClient(flags);
51416
51593
  if (command === "workspace") {
51594
+ if (subcommand === "yank") {
51595
+ const [packageId, versionRaw, maybeUndo] = toolArgs ? toolArgs.split(/\s+/) : [];
51596
+ const version = Number(versionRaw);
51597
+ if (!packageId || !Number.isInteger(version) || version <= 0) {
51598
+ console.error("Usage: lotics package yank <package_id> <version> [--undo]");
51599
+ process.exit(1);
51600
+ }
51601
+ await packageYank(client, { package_id: packageId, version, undo: maybeUndo === "--undo" });
51602
+ return;
51603
+ }
51417
51604
  if (subcommand === "doctor") {
51418
51605
  await resolveWorkspace(client, ctx);
51419
51606
  const dangling = await client.getWorkspaceDanglingReferences();
@@ -42,7 +42,7 @@ export interface StarterFile {
42
42
  * fall back here when the lookup fails.
43
43
  */
44
44
  export declare const STARTER_FALLBACK_UI_VERSION = "6.1.0";
45
- export declare const STARTER_FALLBACK_SDK_VERSION = "0.40.0";
45
+ export declare const STARTER_FALLBACK_SDK_VERSION = "0.51.0";
46
46
  /**
47
47
  * react-native pin for scaffolded apps. Matches the monorepo frontend's pin so
48
48
  * an app deep-typechecks `@lotics/ui`'s `.tsx` source against the SAME RN types
@@ -38,10 +38,11 @@
38
38
  * fall back here when the lookup fails.
39
39
  */
40
40
  export const STARTER_FALLBACK_UI_VERSION = "6.1.0";
41
- // Must be ≥ the release that added `@lotics/app-sdk/router` (`AppRouter`, used by
42
- // the scaffolded App.tsx) — an older offline pin won't resolve the subpath and
43
- // the scaffold won't typecheck.
44
- export const STARTER_FALLBACK_SDK_VERSION = "0.40.0";
41
+ // Must be ≥ the release that added `getAppBinding` (0.51.0, used by package
42
+ // projects' generated .lotics/app_fields.ts) — and note ^0.x caret ranges never
43
+ // cross a minor, so a stale floor here permanently pins scaffolds below the
44
+ // APIs the generated code imports.
45
+ export const STARTER_FALLBACK_SDK_VERSION = "0.51.0";
45
46
  /**
46
47
  * react-native pin for scaffolded apps. Matches the monorepo frontend's pin so
47
48
  * an app deep-typechecks `@lotics/ui`'s `.tsx` source against the SAME RN types
@@ -267,6 +268,11 @@ export default defineConfig({
267
268
  build: {
268
269
  outDir: "dist",
269
270
  sourcemap: true,
271
+ // Top-level await (package projects' generated .lotics/app_fields.ts
272
+ // resolves the installation binding at module load) needs es2022 — Vite's
273
+ // default 'modules' baseline is es2020 and esbuild hard-fails TLA there.
274
+ // Dev already transforms at esnext, so this only aligns the prod build.
275
+ target: "es2022",
270
276
  },
271
277
  server: {
272
278
  // Allow the sandboxed null-origin iframe used by \`lotics app dev\` to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.73.0",
3
+ "version": "0.75.0",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {