@lynxship/cli 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,6 +25,26 @@ npx @lynxship/cli@latest doctor --project-dir ./my-lynx-app
25
25
 
26
26
  The package exposes the `lynxship` executable.
27
27
 
28
+ ## Plugin ecosystem
29
+
30
+ Install a community plugin as a normal project dependency, list it in
31
+ `lynxship.json`, then validate it before building:
32
+
33
+ ```bash
34
+ npm install --save-dev @example/lynx-plugin
35
+ npx lynxship plugin doctor
36
+ npx lynxship plugin list
37
+ npx lynxship plugin apply --platform android --profile production
38
+ ```
39
+
40
+ The public contract is exported by `@lynxship/plugin-api`. Plugins can make
41
+ JSON configuration contributions and bounded, idempotent Android/iOS/native
42
+ file changes. Autolink remains the official Lynx `lynx.lib.json` and codegen
43
+ workflow. Template and cloud contributions are discoverable metadata until a
44
+ reviewed provider or template service consumes them.
45
+
46
+ See the full contract in the repository `docs/plugin-ecosystem.md`.
47
+
28
48
  ## Vue Lynx projects
29
49
 
30
50
  LynxShip supports projects created with the official Vue Lynx scaffold. Vue
@@ -514,7 +534,8 @@ actual recording and analysis.
514
534
 
515
535
  ## Package layout
516
536
 
517
- The CLI is backed by public `@lynxship/*` runtime packages in this workspace.
537
+ The CLI is backed by public `@lynxship/*` runtime packages in this workspace,
538
+ including the public `@lynxship/plugin-api` contract.
518
539
  Their published versions may differ from the CLI version; workspace ranges are
519
540
  rewritten to released versions when the package is packed. Publish a changed
520
541
  runtime package before publishing the CLI when its source or version changed.
package/dist/config.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type Platform } from "@lynxship/contracts";
2
+ import type { PluginReference } from "@lynxship/plugin-api";
2
3
  export interface BuildProfile {
3
4
  distribution?: string;
4
5
  channel?: string;
@@ -43,6 +44,7 @@ export interface BuildProfile {
43
44
  }
44
45
  export interface LynxShipConfig {
45
46
  projectId?: string;
47
+ plugins?: PluginReference[];
46
48
  cli?: {
47
49
  apiUrl?: string;
48
50
  organizationId?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
package/dist/config.js CHANGED
@@ -33,6 +33,7 @@ const allowedRootKeys = new Set([
33
33
  "submit",
34
34
  "update",
35
35
  "artifacts",
36
+ "plugins",
36
37
  ]);
37
38
  export function validateConfig(config, options = {}) {
38
39
  assert(config && typeof config === "object" && !Array.isArray(config), "CONFIG_INVALID", "lynxship.json must contain an object");
@@ -41,6 +42,20 @@ export function validateConfig(config, options = {}) {
41
42
  assert(!options.ci || unknown.length === 0, "CONFIG_UNKNOWN_KEY", `Unknown configuration key(s): ${unknown.join(", ")}`, { unknown });
42
43
  if (value.projectId !== undefined)
43
44
  assert(typeof value.projectId === "string" && value.projectId.length > 0, "CONFIG_PROJECT_ID", "projectId must be a non-empty string");
45
+ if (value.plugins !== undefined) {
46
+ assert(Array.isArray(value.plugins), "CONFIG_PLUGINS", "plugins must be an array of package names or [package, options] tuples");
47
+ for (const plugin of value.plugins) {
48
+ const validString = typeof plugin === "string" && plugin.length > 0;
49
+ const validTuple = Array.isArray(plugin) &&
50
+ plugin.length === 2 &&
51
+ typeof plugin[0] === "string" &&
52
+ plugin[0].length > 0 &&
53
+ plugin[1] !== null &&
54
+ typeof plugin[1] === "object" &&
55
+ !Array.isArray(plugin[1]);
56
+ assert(validString || validTuple, "CONFIG_PLUGINS", "Each plugin must be a package name or [package name, JSON options]");
57
+ }
58
+ }
44
59
  assert(value.runtimeVersion?.policy === undefined ||
45
60
  ["fingerprint", "manual"].includes(value.runtimeVersion.policy), "CONFIG_RUNTIME_POLICY", "runtimeVersion.policy must be fingerprint or manual");
46
61
  if (value.runtimeVersion?.policy === "manual")
package/dist/index.js CHANGED
@@ -35,6 +35,7 @@ import { hasHarmonyHost, runRealHarmonyBuild } from "./harmony-build.js";
35
35
  import { hasWebConfiguration, runRealWebBuild } from "./web-build.js";
36
36
  import { hasDesktopHost, runRealDesktopBuild } from "./desktop-build.js";
37
37
  import { extractDevServerUrl, shouldPrintDevServerQr } from "./dev-qr.js";
38
+ import { applyProjectPlugins, inspectProjectPlugins } from "./plugins.js";
38
39
  import { inspectDesktopTarget, inspectHarmonyTarget, inspectWebTarget, } from "./target-toolchain.js";
39
40
  const rawArgs = process.argv.slice(2);
40
41
  const args = [...rawArgs];
@@ -216,7 +217,20 @@ async function executeBuild(options, showResult = true) {
216
217
  // Host initialization can write lynxship.json (for example, the generated
217
218
  // iOS scheme). Always read it again before resolving the build profile so a
218
219
  // first build uses the host that was just created in the same invocation.
219
- const effectiveConfig = wait && !local ? await loadConfig(root) : config;
220
+ const loadedConfig = wait && !local ? await loadConfig(root) : config;
221
+ const pluginApplication = wait && !local
222
+ ? await applyProjectPlugins(root, loadedConfig, { platform, profile })
223
+ : {
224
+ config: loadedConfig,
225
+ report: { configured: 0, plugins: [] },
226
+ applied: [],
227
+ templates: [],
228
+ cloud: [],
229
+ build: [],
230
+ changes: [],
231
+ autolink: [],
232
+ };
233
+ const effectiveConfig = pluginApplication.config;
220
234
  if (platform === "android" || platform === "ios")
221
235
  await requireAutolinkReady(root, platform);
222
236
  const runtime = await inspectRuntimeFingerprint(root, platform, effectiveConfig);
@@ -418,6 +432,7 @@ function commandTitle(command) {
418
432
  devtool: "Lynx DevTool diagnostics",
419
433
  trace: "Lynx Trace diagnostics",
420
434
  recorder: "Lynx Recorder diagnostics",
435
+ plugin: "LynxShip plugins",
421
436
  }[command] ?? command);
422
437
  }
423
438
  function helpText() {
@@ -433,6 +448,10 @@ Commands:
433
448
  devtool doctor Check Lynx DevTool integration and dev runtime
434
449
  trace doctor Check Lynx Trace prerequisites
435
450
  recorder doctor Check Lynx Recorder prerequisites
451
+ plugin list List project plugins and their capabilities
452
+ plugin doctor Validate plugin packages without modifying native files
453
+ plugin apply Apply project plugin native changes for a platform
454
+ plugin apply --dry-run Preview plugin native changes without writing files
436
455
  autolink check Check Lynx native-library Autolink wiring
437
456
  autolink codegen Run the project's Native Module codegen script
438
457
  ota doctor Check native OTA host integration
@@ -905,6 +924,80 @@ async function main() {
905
924
  });
906
925
  return;
907
926
  }
927
+ if (command === "plugin") {
928
+ const subcommand = args.shift() ?? "list";
929
+ assert(["list", "doctor", "apply"].includes(subcommand), "CLI_PLUGIN_COMMAND", "Use \`lynxship plugin list\`, \`lynxship plugin doctor\` or \`lynxship plugin apply\`.");
930
+ await requireProjectRoot();
931
+ const config = await loadConfig(root);
932
+ const report = await inspectProjectPlugins(root, config);
933
+ if (subcommand === "apply") {
934
+ const platform = platformValue(flag("--platform", "android"));
935
+ const profile = flag("--profile", "production");
936
+ const dryRun = args.includes("--dry-run");
937
+ const result = await applyProjectPlugins(root, config, {
938
+ platform,
939
+ profile,
940
+ mode: dryRun ? "plan" : "apply",
941
+ });
942
+ printValue({
943
+ status: dryRun ? "planned" : "applied",
944
+ platform,
945
+ profile,
946
+ plugins: result.applied,
947
+ nativeChanges: result.changes.filter((change) => change.changed)
948
+ .length,
949
+ changes: result.changes,
950
+ templates: result.templates,
951
+ cloud: result.cloud,
952
+ build: result.build,
953
+ }, {
954
+ title: `LynxShip plugins · ${platform}`,
955
+ rows: [
956
+ {
957
+ label: "Applied",
958
+ value: result.applied.length ? result.applied.join(", ") : "none",
959
+ valueColor: "green",
960
+ },
961
+ {
962
+ label: "Native changes",
963
+ value: String(result.changes.filter((change) => change.changed).length),
964
+ valueColor: "blue",
965
+ },
966
+ ],
967
+ done: dryRun
968
+ ? "No native files were modified; review the planned changes."
969
+ : "Project plugin changes are applied atomically and idempotently.",
970
+ });
971
+ return;
972
+ }
973
+ const invalid = report.plugins.filter((plugin) => plugin.status !== "ready");
974
+ printValue(report, {
975
+ title: `LynxShip plugins · ${subcommand}`,
976
+ rows: report.plugins.length > 0
977
+ ? report.plugins.map((plugin) => ({
978
+ label: plugin.name,
979
+ value: `${plugin.status} · ${plugin.capabilities.join(", ") || "no capabilities"} · ${plugin.reason}`,
980
+ valueColor: plugin.status === "ready"
981
+ ? "green"
982
+ : plugin.status === "missing"
983
+ ? "red"
984
+ : "yellow",
985
+ }))
986
+ : [
987
+ {
988
+ label: "Plugins",
989
+ value: "none configured",
990
+ valueColor: "muted",
991
+ },
992
+ ],
993
+ done: invalid.length === 0
994
+ ? "All project plugin manifests are valid."
995
+ : "Fix the invalid plugin package before building.",
996
+ });
997
+ if (subcommand === "doctor" && invalid.length > 0)
998
+ process.exitCode = 1;
999
+ return;
1000
+ }
908
1001
  if (command === "doctor") {
909
1002
  const config = await loadConfig(root);
910
1003
  const configuration = await readConfigurationStatus();
@@ -946,6 +1039,8 @@ async function main() {
946
1039
  const nodeSupported = nodeMajor >= 22;
947
1040
  const nodeRecommended = nodeMajor % 2 === 0;
948
1041
  const framework = await detectLynxFramework(root);
1042
+ const pluginReport = await inspectProjectPlugins(root, config);
1043
+ const invalidPlugins = pluginReport.plugins.filter((plugin) => plugin.status !== "ready");
949
1044
  const checks = [
950
1045
  {
951
1046
  name: "lynx-framework",
@@ -1002,6 +1097,16 @@ async function main() {
1002
1097
  status: config.projectId ? "pass" : "fail",
1003
1098
  value: config.projectId ? "found" : "missing · fix: lynxship init",
1004
1099
  },
1100
+ {
1101
+ name: "lynxship-plugins",
1102
+ ok: invalidPlugins.length === 0,
1103
+ status: invalidPlugins.length === 0 ? "pass" : "fail",
1104
+ value: invalidPlugins.length === 0
1105
+ ? pluginReport.configured === 0
1106
+ ? "none configured"
1107
+ : `${pluginReport.configured} plugin(s) ready`
1108
+ : `${invalidPlugins.map((plugin) => plugin.name).join(", ")} · fix: lynxship plugin doctor`,
1109
+ },
1005
1110
  {
1006
1111
  name: "credential-store",
1007
1112
  ok: true,
@@ -0,0 +1,2 @@
1
+ export * from "@lynxship/plugin-api";
2
+ //# sourceMappingURL=plugin-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-api.d.ts","sourceRoot":"","sources":["../src/plugin-api.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC"}
@@ -0,0 +1 @@
1
+ export * from "@lynxship/plugin-api";
@@ -0,0 +1,35 @@
1
+ import type { Platform } from "@lynxship/contracts";
2
+ import { type BuildContribution, type CloudIntegrationContribution, type PluginCapability, type AutolinkContribution, type PluginPermission, type PluginPlanChange, type TemplateContribution } from "@lynxship/plugin-api";
3
+ import type { LynxShipConfig } from "./config.js";
4
+ export interface ProjectPluginInfo {
5
+ name: string;
6
+ version?: string;
7
+ packagePath?: string;
8
+ entry?: string;
9
+ apiVersion?: number;
10
+ capabilities: PluginCapability[];
11
+ permissions: PluginPermission[];
12
+ status: "ready" | "missing" | "invalid";
13
+ reason: string;
14
+ }
15
+ export interface PluginReport {
16
+ configured: number;
17
+ plugins: ProjectPluginInfo[];
18
+ }
19
+ export interface PluginApplication {
20
+ config: LynxShipConfig;
21
+ report: PluginReport;
22
+ applied: string[];
23
+ templates: TemplateContribution[];
24
+ cloud: CloudIntegrationContribution[];
25
+ build: BuildContribution[];
26
+ changes: PluginPlanChange[];
27
+ autolink: AutolinkContribution[];
28
+ }
29
+ export declare function inspectProjectPlugins(root: string, config: LynxShipConfig): Promise<PluginReport>;
30
+ export declare function applyProjectPlugins(root: string, config: LynxShipConfig, target: {
31
+ platform: Platform;
32
+ profile: string;
33
+ mode?: "plan" | "apply";
34
+ }): Promise<PluginApplication>;
35
+ //# sourceMappingURL=plugins.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugins.d.ts","sourceRoot":"","sources":["../src/plugins.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,4BAA4B,EAKjC,KAAK,gBAAgB,EAErB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EAGrB,KAAK,oBAAoB,EAC1B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAclD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,gBAAgB,EAAE,CAAC;IACjC,WAAW,EAAE,gBAAgB,EAAE,CAAC;IAChC,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;IACxC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,iBAAiB,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,YAAY,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,oBAAoB,EAAE,CAAC;IAClC,KAAK,EAAE,4BAA4B,EAAE,CAAC;IACtC,KAAK,EAAE,iBAAiB,EAAE,CAAC;IAC3B,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,QAAQ,EAAE,oBAAoB,EAAE,CAAC;CAClC;AAqUD,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,YAAY,CAAC,CAQvB;AA+XD,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,GACvE,OAAO,CAAC,iBAAiB,CAAC,CAqF5B"}
@@ -0,0 +1,595 @@
1
+ import { createRequire } from "node:module";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { mkdir, readFile, rm, writeFile, } from "node:fs/promises";
4
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ import { LYNXSHIP_PLUGIN_API_VERSION, } from "@lynxship/plugin-api";
7
+ function pluginReference(reference) {
8
+ if (typeof reference === "string")
9
+ return { name: reference, options: {} };
10
+ return { name: reference[0], options: reference[1] };
11
+ }
12
+ function packageDirectory(entry) {
13
+ let current = dirname(entry);
14
+ while (true) {
15
+ if (existsSync(join(current, "package.json")))
16
+ return current;
17
+ const parent = dirname(current);
18
+ if (parent === current)
19
+ return dirname(entry);
20
+ current = parent;
21
+ }
22
+ }
23
+ function parsePackageJson(file) {
24
+ try {
25
+ return JSON.parse(readFileSync(file, "utf8"));
26
+ }
27
+ catch {
28
+ return {};
29
+ }
30
+ }
31
+ function packageJsonFromEntry(entry) {
32
+ return join(packageDirectory(entry), "package.json");
33
+ }
34
+ function findInstalledPackageJson(root, name) {
35
+ let current = root;
36
+ while (true) {
37
+ const candidate = join(current, "node_modules", ...name.split("/"), "package.json");
38
+ if (existsSync(candidate))
39
+ return candidate;
40
+ const parent = dirname(current);
41
+ if (parent === current)
42
+ return undefined;
43
+ current = parent;
44
+ }
45
+ }
46
+ function capabilities(value) {
47
+ if (!Array.isArray(value))
48
+ return [];
49
+ return value.filter((candidate) => ["config", "build", "native", "autolink", "template", "cloud"].includes(candidate));
50
+ }
51
+ const KNOWN_CAPABILITIES = [
52
+ "config",
53
+ "build",
54
+ "native",
55
+ "autolink",
56
+ "template",
57
+ "cloud",
58
+ ];
59
+ const KNOWN_PERMISSIONS = [
60
+ "config:write",
61
+ "build:metadata",
62
+ "native:write",
63
+ "autolink:metadata",
64
+ "template:register",
65
+ "cloud:provider",
66
+ ];
67
+ function isKnownList(value, allowed) {
68
+ return (Array.isArray(value) &&
69
+ value.every((candidate) => typeof candidate === "string" && allowed.includes(candidate)));
70
+ }
71
+ function permissions(value) {
72
+ if (!Array.isArray(value))
73
+ return [];
74
+ return value.filter((candidate) => [
75
+ "config:write",
76
+ "build:metadata",
77
+ "native:write",
78
+ "autolink:metadata",
79
+ "template:register",
80
+ "cloud:provider",
81
+ ].includes(candidate));
82
+ }
83
+ function validPluginDefinition(value) {
84
+ if (!value || typeof value !== "object")
85
+ return false;
86
+ const definition = value;
87
+ return (definition.apiVersion === LYNXSHIP_PLUGIN_API_VERSION &&
88
+ typeof definition.name === "string" &&
89
+ definition.name.length > 0 &&
90
+ isKnownList(definition.capabilities, KNOWN_CAPABILITIES) &&
91
+ isKnownList(definition.permissions, KNOWN_PERMISSIONS) &&
92
+ (definition.platforms === undefined ||
93
+ isKnownList(definition.platforms, [
94
+ "all",
95
+ "android",
96
+ "ios",
97
+ "harmonyos",
98
+ "web",
99
+ "desktop",
100
+ ])) &&
101
+ typeof definition.apply === "function");
102
+ }
103
+ function resolvePackageEntry(root, name) {
104
+ try {
105
+ const requireFromProject = createRequire(join(root, "package.json"));
106
+ let entry;
107
+ let packagePath;
108
+ try {
109
+ entry = requireFromProject.resolve(name);
110
+ packagePath = packageJsonFromEntry(entry);
111
+ }
112
+ catch {
113
+ packagePath = findInstalledPackageJson(root, name) ?? "";
114
+ if (!packagePath)
115
+ throw new Error("Cannot find module '" + name + "'");
116
+ entry = packagePath;
117
+ }
118
+ const packageJson = parsePackageJson(packagePath);
119
+ const plugin = packageJson.lynxship?.plugin;
120
+ if (typeof plugin !== "string" || !plugin)
121
+ return {
122
+ packagePath,
123
+ reason: `${name} does not declare package.json#lynxship.plugin`,
124
+ };
125
+ const packageRoot = dirname(packagePath);
126
+ const resolved = resolve(packageRoot, plugin);
127
+ const outside = relative(packageRoot, resolved).startsWith("..");
128
+ if (outside)
129
+ return {
130
+ packagePath,
131
+ reason: `${name} points its plugin entry outside the package`,
132
+ };
133
+ if (!existsSync(resolved))
134
+ return {
135
+ packagePath,
136
+ reason: `${name} plugin entry does not exist: ${plugin}`,
137
+ };
138
+ return { packagePath, entry: resolved };
139
+ }
140
+ catch (error) {
141
+ return {
142
+ reason: error instanceof Error
143
+ ? `Cannot resolve ${name}: ${error.message}`
144
+ : `Cannot resolve ${name}`,
145
+ };
146
+ }
147
+ }
148
+ async function resolvePlugin(root, reference, load = true) {
149
+ const requested = pluginReference(reference);
150
+ const resolved = resolvePackageEntry(root, requested.name);
151
+ if (!resolved.packagePath || !resolved.entry) {
152
+ return {
153
+ info: {
154
+ name: requested.name,
155
+ capabilities: [],
156
+ permissions: [],
157
+ status: "missing",
158
+ reason: resolved.reason ?? "Plugin package was not found",
159
+ },
160
+ };
161
+ }
162
+ const packageJson = parsePackageJson(resolved.packagePath);
163
+ const metadata = packageJson.lynxship;
164
+ const apiVersion = typeof metadata?.apiVersion === "number" ? metadata.apiVersion : undefined;
165
+ const packageCapabilities = capabilities(metadata?.capabilities);
166
+ const packagePermissions = permissions(metadata?.permissions);
167
+ if (!isKnownList(metadata?.capabilities, KNOWN_CAPABILITIES) ||
168
+ !isKnownList(metadata?.permissions, KNOWN_PERMISSIONS)) {
169
+ return {
170
+ info: {
171
+ name: requested.name,
172
+ version: typeof packageJson.version === "string"
173
+ ? packageJson.version
174
+ : undefined,
175
+ packagePath: resolved.packagePath,
176
+ entry: resolved.entry,
177
+ apiVersion,
178
+ capabilities: packageCapabilities,
179
+ permissions: packagePermissions,
180
+ status: "invalid",
181
+ reason: "Plugin package capabilities and permissions must be arrays of known values",
182
+ },
183
+ };
184
+ }
185
+ if (apiVersion !== LYNXSHIP_PLUGIN_API_VERSION) {
186
+ return {
187
+ info: {
188
+ name: requested.name,
189
+ version: typeof packageJson.version === "string"
190
+ ? packageJson.version
191
+ : undefined,
192
+ packagePath: resolved.packagePath,
193
+ entry: resolved.entry,
194
+ apiVersion,
195
+ capabilities: packageCapabilities,
196
+ permissions: packagePermissions,
197
+ status: "invalid",
198
+ reason: `Plugin API ${String(apiVersion ?? "missing")} is not supported; expected ${LYNXSHIP_PLUGIN_API_VERSION}`,
199
+ },
200
+ };
201
+ }
202
+ const info = {
203
+ name: requested.name,
204
+ version: typeof packageJson.version === "string" ? packageJson.version : undefined,
205
+ packagePath: resolved.packagePath,
206
+ entry: resolved.entry,
207
+ apiVersion,
208
+ capabilities: packageCapabilities,
209
+ permissions: packagePermissions,
210
+ status: "ready",
211
+ reason: "Plugin manifest is valid",
212
+ };
213
+ if (!load)
214
+ return { info };
215
+ try {
216
+ const module = (await import(pathToFileURL(resolved.entry).href));
217
+ const definition = module.default ?? module.plugin;
218
+ if (!validPluginDefinition(definition)) {
219
+ return {
220
+ info: {
221
+ ...info,
222
+ status: "invalid",
223
+ reason: "Plugin entry must export a LynxShipPluginDefinition",
224
+ },
225
+ };
226
+ }
227
+ if (capabilities(definition.capabilities).join(",") !==
228
+ packageCapabilities.join(",") ||
229
+ permissions(definition.permissions).join(",") !==
230
+ packagePermissions.join(",")) {
231
+ return {
232
+ info: {
233
+ ...info,
234
+ status: "invalid",
235
+ reason: "Plugin code capabilities/permissions do not match package.json",
236
+ },
237
+ };
238
+ }
239
+ if (definition.name !== requested.name &&
240
+ definition.name !== packageJson.name) {
241
+ return {
242
+ info: {
243
+ ...info,
244
+ status: "invalid",
245
+ reason: `Plugin entry name '${definition.name}' does not match its package`,
246
+ },
247
+ };
248
+ }
249
+ return { info, definition };
250
+ }
251
+ catch (error) {
252
+ return {
253
+ info: {
254
+ ...info,
255
+ status: "invalid",
256
+ reason: error instanceof Error
257
+ ? `Plugin entry failed to load: ${error.message}`
258
+ : "Plugin entry failed to load",
259
+ },
260
+ };
261
+ }
262
+ }
263
+ function pluginReferences(config) {
264
+ const value = config.plugins;
265
+ if (!Array.isArray(value))
266
+ return [];
267
+ return value;
268
+ }
269
+ export async function inspectProjectPlugins(root, config) {
270
+ const references = pluginReferences(config);
271
+ const plugins = await Promise.all(references.map(async (reference) => (await resolvePlugin(root, reference, false)).info));
272
+ return { configured: references.length, plugins };
273
+ }
274
+ function appliesToPlatform(platforms, platform) {
275
+ return (!platforms ||
276
+ platforms.length === 0 ||
277
+ platforms.includes("all") ||
278
+ platforms.includes(platform));
279
+ }
280
+ function mergeJson(left, right) {
281
+ const result = { ...left };
282
+ for (const [key, value] of Object.entries(right)) {
283
+ const current = result[key];
284
+ if (current &&
285
+ typeof current === "object" &&
286
+ !Array.isArray(current) &&
287
+ value &&
288
+ typeof value === "object" &&
289
+ !Array.isArray(value)) {
290
+ result[key] = mergeJson(current, value);
291
+ }
292
+ else {
293
+ result[key] = value;
294
+ }
295
+ }
296
+ return result;
297
+ }
298
+ function safeProjectFile(root, file) {
299
+ if (isAbsolute(file))
300
+ throw new Error(`Plugin native file must be relative: ${file}`);
301
+ const target = resolve(root, file);
302
+ const projectRelative = relative(root, target);
303
+ if (!projectRelative ||
304
+ projectRelative.startsWith("..") ||
305
+ isAbsolute(projectRelative))
306
+ throw new Error(`Plugin native file escapes the project: ${file}`);
307
+ return target;
308
+ }
309
+ function permissionForContribution(contribution) {
310
+ const required = [];
311
+ if (contribution.config)
312
+ required.push("config:write");
313
+ if (contribution.build)
314
+ required.push("build:metadata");
315
+ if (contribution.native?.length)
316
+ required.push("native:write");
317
+ if (contribution.autolink)
318
+ required.push("autolink:metadata");
319
+ if (contribution.templates?.length)
320
+ required.push("template:register");
321
+ if (contribution.cloud?.length)
322
+ required.push("cloud:provider");
323
+ return required;
324
+ }
325
+ function capabilityForPermission(permission) {
326
+ return permission.split(":")[0];
327
+ }
328
+ function isJsonObject(value) {
329
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
330
+ }
331
+ function assertPluginContribution(plugin, contribution) {
332
+ if (!isJsonObject(contribution))
333
+ throw new Error(`Plugin '${plugin.name}' returned an invalid contribution object`);
334
+ if (contribution.config !== undefined && !isJsonObject(contribution.config))
335
+ throw new Error(`Plugin '${plugin.name}' returned an invalid config contribution`);
336
+ if (contribution.native !== undefined && !Array.isArray(contribution.native))
337
+ throw new Error(`Plugin '${plugin.name}' returned an invalid native contribution`);
338
+ for (const operation of contribution.native ?? []) {
339
+ if (!isJsonObject(operation))
340
+ throw new Error(`Plugin '${plugin.name}' returned an invalid native operation`);
341
+ if (!["all", "android", "ios", "harmonyos", "web", "desktop"].includes(String(operation.platform)) ||
342
+ typeof operation.file !== "string" ||
343
+ operation.file.length === 0 ||
344
+ !["ensure-text", "append-text", "replace-text", "json-merge"].includes(String(operation.operation)))
345
+ throw new Error(`Plugin '${plugin.name}' returned an invalid native operation`);
346
+ if ((operation.operation === "ensure-text" ||
347
+ operation.operation === "append-text") &&
348
+ typeof operation.text !== "string")
349
+ throw new Error(`Plugin '${plugin.name}' returned an invalid text operation`);
350
+ if (operation.operation === "replace-text" &&
351
+ (typeof operation.from !== "string" || typeof operation.to !== "string"))
352
+ throw new Error(`Plugin '${plugin.name}' returned an invalid replacement operation`);
353
+ if (operation.operation === "json-merge" && !isJsonObject(operation.value))
354
+ throw new Error(`Plugin '${plugin.name}' returned an invalid JSON merge operation`);
355
+ }
356
+ if (contribution.autolink !== undefined) {
357
+ if (!isJsonObject(contribution.autolink))
358
+ throw new Error(`Plugin '${plugin.name}' returned an invalid Autolink contribution`);
359
+ for (const key of ["packageNames", "capabilities"]) {
360
+ const value = contribution.autolink[key];
361
+ if (value !== undefined &&
362
+ (!Array.isArray(value) ||
363
+ !value.every((item) => typeof item === "string")))
364
+ throw new Error(`Plugin '${plugin.name}' returned invalid Autolink metadata`);
365
+ }
366
+ }
367
+ if (contribution.build !== undefined) {
368
+ if (!isJsonObject(contribution.build))
369
+ throw new Error(`Plugin '${plugin.name}' returned an invalid build contribution`);
370
+ for (const key of [
371
+ "requiredTools",
372
+ "artifactExtensions",
373
+ "notes",
374
+ ]) {
375
+ const value = contribution.build[key];
376
+ if (value !== undefined &&
377
+ (!Array.isArray(value) ||
378
+ !value.every((item) => typeof item === "string")))
379
+ throw new Error(`Plugin '${plugin.name}' returned invalid build metadata`);
380
+ }
381
+ }
382
+ for (const template of Array.isArray(contribution.templates)
383
+ ? contribution.templates
384
+ : []) {
385
+ if (!isJsonObject(template) ||
386
+ typeof template.id !== "string" ||
387
+ typeof template.displayName !== "string" ||
388
+ typeof template.source !== "string")
389
+ throw new Error(`Plugin '${plugin.name}' returned an invalid template contribution`);
390
+ }
391
+ for (const integration of Array.isArray(contribution.cloud)
392
+ ? contribution.cloud
393
+ : []) {
394
+ if (!isJsonObject(integration) ||
395
+ typeof integration.id !== "string" ||
396
+ typeof integration.displayName !== "string" ||
397
+ !Array.isArray(integration.capabilities) ||
398
+ !integration.capabilities.every((item) => ["build", "artifacts", "updates", "submit"].includes(String(item))))
399
+ throw new Error(`Plugin '${plugin.name}' returned an invalid cloud contribution`);
400
+ }
401
+ }
402
+ function assertPluginPermissions(plugin, contribution) {
403
+ const missing = permissionForContribution(contribution).filter((permission) => !plugin.permissions.includes(permission));
404
+ const missingCapabilities = permissionForContribution(contribution)
405
+ .map(capabilityForPermission)
406
+ .filter((capability) => !plugin.capabilities.includes(capability));
407
+ if (missing.length)
408
+ throw new Error(`Plugin '${plugin.name}' returned contributions without declaring permission(s): ${missing.join(", ")}`);
409
+ if (missingCapabilities.length)
410
+ throw new Error(`Plugin '${plugin.name}' returned contributions without declaring capability(ies): ${[...new Set(missingCapabilities)].join(", ")}`);
411
+ }
412
+ async function mergeJsonFile(file, value) {
413
+ const current = JSON.parse(await readFile(file, "utf8"));
414
+ await writeFile(file, `${JSON.stringify(mergeJson(current, value), null, 2)}\n`);
415
+ }
416
+ async function inspectNativeOperation(root, owned, platform) {
417
+ const operation = owned.operation;
418
+ if (!appliesToPlatform([operation.platform], platform))
419
+ return undefined;
420
+ const file = safeProjectFile(root, operation.file);
421
+ const current = existsSync(file) ? await readFile(file, "utf8") : "";
422
+ let changed = false;
423
+ if (operation.operation === "json-merge") {
424
+ if (!existsSync(file))
425
+ throw new Error(`Plugin JSON target does not exist: ${operation.file}`);
426
+ const merged = `${JSON.stringify(mergeJson(JSON.parse(current), operation.value), null, 2)}\n`;
427
+ changed = merged !== current;
428
+ }
429
+ else if (operation.operation === "ensure-text") {
430
+ changed = !current.includes(operation.text);
431
+ }
432
+ else if (operation.operation === "append-text") {
433
+ changed = !current.includes(operation.text);
434
+ }
435
+ else if (operation.operation === "replace-text") {
436
+ if (!current.includes(operation.from))
437
+ throw new Error(`Plugin replacement target was not found in ${operation.file}`);
438
+ changed = current.replace(operation.from, operation.to) !== current;
439
+ }
440
+ return {
441
+ plugin: owned.plugin,
442
+ platform,
443
+ file: operation.file,
444
+ operation: operation.operation,
445
+ changed,
446
+ };
447
+ }
448
+ async function applyNativeOperation(root, operation, platform) {
449
+ if (!appliesToPlatform([operation.platform], platform))
450
+ return;
451
+ const file = safeProjectFile(root, operation.file);
452
+ await mkdir(dirname(file), { recursive: true });
453
+ if (operation.operation === "json-merge") {
454
+ if (!existsSync(file))
455
+ throw new Error(`Plugin JSON target does not exist: ${operation.file}`);
456
+ await mergeJsonFile(file, operation.value);
457
+ return;
458
+ }
459
+ const current = existsSync(file) ? await readFile(file, "utf8") : "";
460
+ if (operation.operation === "ensure-text") {
461
+ if (!current.includes(operation.text))
462
+ await writeFile(file, `${current}${current && !current.endsWith("\n") ? "\n" : ""}${operation.text}\n`);
463
+ return;
464
+ }
465
+ if (operation.operation === "append-text") {
466
+ if (current.includes(operation.text))
467
+ return;
468
+ await writeFile(file, `${current}${current && !current.endsWith("\n") ? "\n" : ""}${operation.text}\n`);
469
+ return;
470
+ }
471
+ if (operation.operation !== "replace-text")
472
+ return;
473
+ if (!current.includes(operation.from))
474
+ throw new Error(`Plugin replacement target was not found in ${operation.file}`);
475
+ await writeFile(file, current.replace(operation.from, operation.to));
476
+ }
477
+ function assertNativeConflicts(root, operations) {
478
+ const seen = new Map();
479
+ for (const owned of operations) {
480
+ const file = safeProjectFile(root, owned.operation.file);
481
+ const previous = seen.get(file);
482
+ if (!previous) {
483
+ seen.set(file, owned);
484
+ continue;
485
+ }
486
+ const currentOperation = owned.operation;
487
+ const previousOperation = previous.operation;
488
+ if (currentOperation.operation === "replace-text" &&
489
+ previousOperation.operation === "replace-text" &&
490
+ currentOperation.from === previousOperation.from)
491
+ throw new Error(`Conflicting or duplicate LynxShip plugins '${previous.plugin}' and '${owned.plugin}' both replace '${currentOperation.from}' in ${currentOperation.file}`);
492
+ }
493
+ }
494
+ async function applyNativeOperationsAtomically(root, operations, platform) {
495
+ assertNativeConflicts(root, operations);
496
+ const backups = new Map();
497
+ for (const owned of operations) {
498
+ if (!appliesToPlatform([owned.operation.platform], platform))
499
+ continue;
500
+ const file = safeProjectFile(root, owned.operation.file);
501
+ if (!backups.has(file))
502
+ backups.set(file, existsSync(file) ? await readFile(file, "utf8") : undefined);
503
+ }
504
+ try {
505
+ const changes = (await Promise.all(operations.map((operation) => inspectNativeOperation(root, operation, platform)))).filter((change) => Boolean(change));
506
+ for (const owned of operations)
507
+ await applyNativeOperation(root, owned.operation, platform);
508
+ return changes;
509
+ }
510
+ catch (error) {
511
+ for (const [file, content] of backups) {
512
+ if (content === undefined)
513
+ await rm(file, { force: true });
514
+ else {
515
+ await mkdir(dirname(file), { recursive: true });
516
+ await writeFile(file, content);
517
+ }
518
+ }
519
+ throw new Error(`${error instanceof Error ? error.message : String(error)}; native plugin changes were rolled back`);
520
+ }
521
+ }
522
+ export async function applyProjectPlugins(root, config, target) {
523
+ const references = pluginReferences(config);
524
+ const resolved = [];
525
+ for (const reference of references)
526
+ resolved.push(await resolvePlugin(root, reference));
527
+ const report = {
528
+ configured: references.length,
529
+ plugins: resolved.map((plugin) => plugin.info),
530
+ };
531
+ const invalid = report.plugins.find((plugin) => plugin.status !== "ready");
532
+ if (invalid)
533
+ throw new Error(`LynxShip plugin '${invalid.name}' is invalid: ${invalid.reason}`);
534
+ let effectiveConfig = JSON.parse(JSON.stringify(config));
535
+ const applied = [];
536
+ const templates = [];
537
+ const cloud = [];
538
+ const build = [];
539
+ const autolink = [];
540
+ const operations = [];
541
+ for (let index = 0; index < resolved.length; index += 1) {
542
+ const plugin = resolved[index];
543
+ const reference = references[index];
544
+ if (!plugin?.definition || !reference)
545
+ continue;
546
+ if (!appliesToPlatform(plugin.definition.platforms, target.platform))
547
+ continue;
548
+ const options = pluginReference(reference).options;
549
+ const context = {
550
+ rootDir: root,
551
+ platform: target.platform,
552
+ profile: target.profile,
553
+ mode: target.mode ?? "apply",
554
+ projectId: config.projectId,
555
+ config: effectiveConfig,
556
+ options,
557
+ };
558
+ const contribution = await plugin.definition.apply(context);
559
+ if (!contribution) {
560
+ applied.push(plugin.info.name);
561
+ continue;
562
+ }
563
+ assertPluginContribution(plugin.definition, contribution);
564
+ assertPluginPermissions(plugin.definition, contribution);
565
+ if (contribution.config)
566
+ effectiveConfig = mergeJson(effectiveConfig, contribution.config);
567
+ for (const operation of contribution.native ?? [])
568
+ operations.push({ plugin: plugin.info.name, operation });
569
+ if (contribution.autolink)
570
+ autolink.push(contribution.autolink);
571
+ templates.push(...(contribution.templates ?? []));
572
+ cloud.push(...(contribution.cloud ?? []));
573
+ if (contribution.build)
574
+ build.push(contribution.build);
575
+ applied.push(plugin.info.name);
576
+ }
577
+ let changes;
578
+ if (target.mode === "plan") {
579
+ assertNativeConflicts(root, operations);
580
+ changes = (await Promise.all(operations.map((operation) => inspectNativeOperation(root, operation, target.platform)))).filter((change) => Boolean(change));
581
+ }
582
+ else {
583
+ changes = await applyNativeOperationsAtomically(root, operations, target.platform);
584
+ }
585
+ return {
586
+ config: effectiveConfig,
587
+ report,
588
+ applied,
589
+ templates,
590
+ cloud,
591
+ build,
592
+ autolink,
593
+ changes,
594
+ };
595
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lynxship/cli",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Build, sign, store, submit and update LynxJS applications from the terminal.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -58,6 +58,10 @@
58
58
  "types": "./dist/frameworks.d.ts",
59
59
  "import": "./dist/frameworks.js"
60
60
  },
61
+ "./plugin-api": {
62
+ "types": "./dist/plugin-api.d.ts",
63
+ "import": "./dist/plugin-api.js"
64
+ },
61
65
  "./ui/*": {
62
66
  "types": "./dist/ui/*.d.ts",
63
67
  "import": "./dist/ui/*.js"
@@ -76,9 +80,10 @@
76
80
  "@resvg/resvg-js": "2.6.2",
77
81
  "jsdom": "29.1.1",
78
82
  "pngjs": "7.0.0",
79
- "@lynxship/contracts": "0.1.0",
80
83
  "@lynxship/build-orchestrator": "0.1.0",
84
+ "@lynxship/contracts": "0.1.0",
81
85
  "@lynxship/submit": "0.1.0",
86
+ "@lynxship/plugin-api": "0.1.0",
82
87
  "@lynxship/signing": "0.1.0",
83
88
  "@lynxship/db": "0.1.0"
84
89
  },