@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.
- package/dist/app_commands.d.ts +9 -0
- package/dist/app_commands.js +1 -1
- package/dist/cli.js +13 -1
- package/dist/client.d.ts +17 -0
- package/dist/client.js +12 -0
- package/dist/dev/rpc_handler.d.ts +1 -1
- package/dist/dev/rpc_handler.js +3 -0
- package/dist/generate_app_fields.d.ts +2 -0
- package/dist/generate_app_fields.js +1 -1
- package/dist/generate_package_fields.d.ts +48 -0
- package/dist/generate_package_fields.js +104 -0
- package/dist/generate_package_fields.test.d.ts +1 -0
- package/dist/generate_package_fields.test.js +51 -0
- package/dist/package_commands.d.ts +11 -0
- package/dist/package_commands.js +128 -3
- package/dist/src/cli.js +193 -6
- package/dist/starter_template.d.ts +1 -1
- package/dist/starter_template.js +10 -4
- package/package.json +1 -1
package/dist/app_commands.d.ts
CHANGED
|
@@ -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, … } } }`
|
package/dist/app_commands.js
CHANGED
|
@@ -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;
|
package/dist/dev/rpc_handler.js
CHANGED
|
@@ -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;
|
package/dist/package_commands.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
33954
|
-
|
|
33955
|
-
|
|
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.
|
|
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
|
package/dist/starter_template.js
CHANGED
|
@@ -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
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
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
|