@lynxship/cli 0.1.4 → 0.1.7

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.
Files changed (57) hide show
  1. package/README.md +90 -2
  2. package/dist/android-build.d.ts +1 -0
  3. package/dist/android-build.d.ts.map +1 -1
  4. package/dist/android-build.js +12 -19
  5. package/dist/android-host.d.ts +12 -0
  6. package/dist/android-host.d.ts.map +1 -0
  7. package/dist/android-host.js +70 -0
  8. package/dist/bundle-build.d.ts +7 -0
  9. package/dist/bundle-build.d.ts.map +1 -0
  10. package/dist/bundle-build.js +24 -0
  11. package/dist/guidance.d.ts +6 -0
  12. package/dist/guidance.d.ts.map +1 -0
  13. package/dist/guidance.js +354 -0
  14. package/dist/index.js +349 -93
  15. package/dist/ios-build.d.ts +1 -0
  16. package/dist/ios-build.d.ts.map +1 -1
  17. package/dist/ios-build.js +38 -12
  18. package/dist/ios-host.d.ts +17 -0
  19. package/dist/ios-host.d.ts.map +1 -0
  20. package/dist/ios-host.js +93 -0
  21. package/dist/ui/components.d.ts +2 -0
  22. package/dist/ui/components.d.ts.map +1 -1
  23. package/dist/ui/components.js +30 -16
  24. package/dist/ui/index.d.ts +3 -1
  25. package/dist/ui/index.d.ts.map +1 -1
  26. package/dist/ui/index.js +6 -1
  27. package/package.json +5 -4
  28. package/templates/android-host/app/build.gradle +38 -0
  29. package/templates/android-host/app/proguard-rules.pro +2 -0
  30. package/templates/android-host/app/src/main/AndroidManifest.xml +20 -0
  31. package/templates/android-host/app/src/main/java/template/LynxShipApplication.java +13 -0
  32. package/templates/android-host/app/src/main/java/template/MainActivity.java +19 -0
  33. package/templates/android-host/app/src/main/java/template/ProjectTemplateProvider.java +32 -0
  34. package/templates/android-host/app/src/main/res/values/strings.xml +3 -0
  35. package/templates/android-host/app/src/main/res/values/themes.xml +6 -0
  36. package/templates/android-host/build.gradle +3 -0
  37. package/templates/android-host/gradle/wrapper/gradle-wrapper.jar +0 -0
  38. package/templates/android-host/gradle/wrapper/gradle-wrapper.properties +7 -0
  39. package/templates/android-host/gradle.properties +3 -0
  40. package/templates/android-host/gradlew +185 -0
  41. package/templates/android-host/gradlew.bat +89 -0
  42. package/templates/android-host/settings.gradle +20 -0
  43. package/templates/ios-host/ExportOptions.plist +16 -0
  44. package/templates/ios-host/Podfile +42 -0
  45. package/templates/ios-host/__IOS_TARGET_NAME__/AppDelegate.swift +11 -0
  46. package/templates/ios-host/__IOS_TARGET_NAME__/Assets.xcassets/AccentColor.colorset/Contents.json +11 -0
  47. package/templates/ios-host/__IOS_TARGET_NAME__/Assets.xcassets/AppIcon.appiconset/Contents.json +13 -0
  48. package/templates/ios-host/__IOS_TARGET_NAME__/Assets.xcassets/Contents.json +6 -0
  49. package/templates/ios-host/__IOS_TARGET_NAME__/Base.lproj/LaunchScreen.storyboard +25 -0
  50. package/templates/ios-host/__IOS_TARGET_NAME__/Base.lproj/Main.storyboard +24 -0
  51. package/templates/ios-host/__IOS_TARGET_NAME__/DemoLynxProvider.swift +18 -0
  52. package/templates/ios-host/__IOS_TARGET_NAME__/Hello-Lynx-Bridging-Header.h +4 -0
  53. package/templates/ios-host/__IOS_TARGET_NAME__/Info.plist +25 -0
  54. package/templates/ios-host/__IOS_TARGET_NAME__/SceneDelegate.swift +49 -0
  55. package/templates/ios-host/__IOS_TARGET_NAME__/ViewController.swift +22 -0
  56. package/templates/ios-host/__IOS_TARGET_NAME__.xcodeproj/project.pbxproj +452 -0
  57. package/templates/ios-host/sync-bundle.mjs +11 -0
package/dist/ios-build.js CHANGED
@@ -5,7 +5,8 @@ import { assert, sha256 } from "@lynxship/contracts";
5
5
  import { transitionBuild } from "@lynxship/build-orchestrator";
6
6
  import { loadR2, uploadR2Artifact } from "./r2.js";
7
7
  import { nativeArtifactName } from "./artifact-name.js";
8
- import { commandExists, packageManagerScriptCommand, runProcess, } from "./process-runner.js";
8
+ import { commandExists, runProcess } from "./process-runner.js";
9
+ import { buildLynxBundle } from "./bundle-build.js";
9
10
  export function hasIosHost(root, profile) {
10
11
  if (process.platform !== "darwin")
11
12
  return false;
@@ -23,8 +24,15 @@ export function hasIosHost(root, profile) {
23
24
  }
24
25
  function findProject(root, profile) {
25
26
  const configured = profile.ios?.workspace ?? profile.ios?.project;
26
- if (configured)
27
- return resolve(root, configured);
27
+ if (configured) {
28
+ const configuredPath = resolve(root, configured);
29
+ if (configuredPath.endsWith(".xcodeproj")) {
30
+ const workspace = configuredPath.replace(/\.xcodeproj$/, ".xcworkspace");
31
+ if (existsSync(workspace))
32
+ return workspace;
33
+ }
34
+ return configuredPath;
35
+ }
28
36
  for (const directory of ["ios", "macos"]) {
29
37
  try {
30
38
  const candidate = readdirSync(join(root, directory)).find((name) => name.endsWith(".xcworkspace") || name.endsWith(".xcodeproj"));
@@ -37,6 +45,19 @@ function findProject(root, profile) {
37
45
  }
38
46
  throw new Error("No Xcode workspace or project found under ios/ or macos/");
39
47
  }
48
+ async function installCocoaPods(root, quiet, onEvent) {
49
+ const iosDirectory = join(root, "ios");
50
+ const podfile = join(iosDirectory, "Podfile");
51
+ if (!existsSync(podfile))
52
+ return;
53
+ assert(commandExists("pod"), "IOS_COCOAPODS_REQUIRED", "CocoaPods was not found. Install CocoaPods on macOS, then rerun the build.");
54
+ onEvent?.("Installing iOS CocoaPods dependencies…");
55
+ await runProcess("pod", ["install"], {
56
+ cwd: iosDirectory,
57
+ quiet,
58
+ onOutput: onEvent,
59
+ });
60
+ }
40
61
  async function findIpa(directory) {
41
62
  const files = await readdir(directory, { withFileTypes: true });
42
63
  const ipa = files.find((file) => file.isFile() && file.name.endsWith(".ipa"));
@@ -50,6 +71,8 @@ export async function runRealIosBuild(job, options) {
50
71
  assert(commandExists("xcrun"), "IOS_XCRUN_REQUIRED", "xcrun was not found. Install Xcode command-line tools.");
51
72
  const project = findProject(options.root, options.profile);
52
73
  assert(existsSync(project), "IOS_PROJECT_REQUIRED", `Configured Xcode project was not found: ${project}`);
74
+ await installCocoaPods(options.root, options.quiet, options.onEvent);
75
+ const resolvedProject = findProject(options.root, options.profile);
53
76
  const ios = options.profile.ios ?? {};
54
77
  const scheme = ios.scheme;
55
78
  assert(scheme, "IOS_SCHEME_REQUIRED", "Configure build.<profile>.ios.scheme in lynxship.json");
@@ -57,7 +80,7 @@ export async function runRealIosBuild(job, options) {
57
80
  assert(exportOptions, "IOS_EXPORT_OPTIONS_REQUIRED", "Configure build.<profile>.ios.exportOptionsPlist for a signed IPA export");
58
81
  const exportOptionsPath = resolve(options.root, exportOptions);
59
82
  assert(existsSync(exportOptionsPath), "IOS_EXPORT_OPTIONS_REQUIRED", `Export options file was not found: ${exportOptionsPath}`);
60
- const projectFlag = project.endsWith(".xcworkspace")
83
+ const projectFlag = resolvedProject.endsWith(".xcworkspace")
61
84
  ? "-workspace"
62
85
  : "-project";
63
86
  const configuration = ios.configuration ?? "Release";
@@ -74,13 +97,16 @@ export async function runRealIosBuild(job, options) {
74
97
  await loadR2(options.root);
75
98
  await mkdir(archiveDirectory, { recursive: true });
76
99
  transitionBuild(job, "uploading_source", "iOS source prepared");
77
- step("Building Lynx bundle with Rspeedy…", 5);
78
- const packageManager = packageManagerScriptCommand(options.root, "build");
79
- await runProcess(packageManager.command, packageManager.args, {
80
- cwd: options.root,
81
- quiet: options.quiet,
82
- onOutput: options.onEvent,
83
- });
100
+ if (options.skipBundleBuild) {
101
+ step("Using shared Lynx bundle", 5);
102
+ }
103
+ else {
104
+ step("Building Lynx bundle with Rspeedy…", 5);
105
+ await buildLynxBundle(options.root, {
106
+ quiet: options.quiet,
107
+ onOutput: options.onEvent,
108
+ });
109
+ }
84
110
  if (ios.bundleScript) {
85
111
  step("Syncing bundle into the iOS host…", 8);
86
112
  await runProcess(process.execPath, [resolve(options.root, ios.bundleScript)], {
@@ -92,7 +118,7 @@ export async function runRealIosBuild(job, options) {
92
118
  step("Preparing Xcode archive…", 10);
93
119
  await runProcess("xcodebuild", [
94
120
  projectFlag,
95
- project,
121
+ resolvedProject,
96
122
  "-scheme",
97
123
  scheme,
98
124
  "-configuration",
@@ -0,0 +1,17 @@
1
+ export interface IosHostOptions {
2
+ bundleIdentifier: string;
3
+ appName: string;
4
+ }
5
+ export interface IosHostResult {
6
+ directory: string;
7
+ targetName: string;
8
+ bundleIdentifier: string;
9
+ project: string;
10
+ scheme: string;
11
+ exportOptionsPlist: string;
12
+ bundleScript: string;
13
+ configUpdated: boolean;
14
+ }
15
+ export declare function initializeIosHost(root: string, options: IosHostOptions): Promise<IosHostResult>;
16
+ export declare function suggestedIosBundleIdentifier(root: string): string;
17
+ //# sourceMappingURL=ios-host.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ios-host.d.ts","sourceRoot":"","sources":["../src/ios-host.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,cAAc;IAC7B,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,kBAAkB,EAAE,MAAM,CAAC;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,OAAO,CAAC;CACxB;AAoED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC,CA0CxB;AAED,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKjE"}
@@ -0,0 +1,93 @@
1
+ import { access, cp, readdir, readFile, rename, writeFile, } from "node:fs/promises";
2
+ import { fileURLToPath } from "node:url";
3
+ import { basename, join } from "node:path";
4
+ import { assert } from "@lynxship/contracts";
5
+ const templateRoot = fileURLToPath(new URL("../templates/ios-host/", import.meta.url));
6
+ function validateBundleIdentifier(bundleIdentifier) {
7
+ assert(/^[a-zA-Z][a-zA-Z0-9-]*(\.[a-zA-Z][a-zA-Z0-9-]*)+$/.test(bundleIdentifier), "IOS_BUNDLE_IDENTIFIER_INVALID", "iOS bundle identifier must contain at least two dot-separated segments, for example com.example.myapp.");
8
+ }
9
+ async function exists(path) {
10
+ try {
11
+ await access(path);
12
+ return true;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ function targetName(value) {
19
+ const words = value.match(/[a-zA-Z0-9]+/g) ?? [];
20
+ const result = words
21
+ .map((word) => `${word[0]?.toUpperCase() ?? ""}${word.slice(1)}`)
22
+ .join("");
23
+ if (result && /^[A-Za-z]/.test(result))
24
+ return result;
25
+ return `LynxShip${result || "App"}`;
26
+ }
27
+ async function textFiles(directory) {
28
+ const entries = await readdir(directory, { withFileTypes: true });
29
+ const files = [];
30
+ for (const entry of entries) {
31
+ const path = join(directory, entry.name);
32
+ if (entry.isDirectory())
33
+ files.push(...(await textFiles(path)));
34
+ else if ([".h", ".m", ".plist", ".pbxproj", ".swift", ".rb", ".mjs"].some((extension) => entry.name.endsWith(extension)) ||
35
+ entry.name === "Podfile")
36
+ files.push(path);
37
+ }
38
+ return files;
39
+ }
40
+ async function updateProjectConfig(root, result) {
41
+ const configPath = join(root, "lynxship.json");
42
+ if (!(await exists(configPath)))
43
+ return false;
44
+ const config = JSON.parse(await readFile(configPath, "utf8"));
45
+ config.build ??= {};
46
+ config.build.production ??= {};
47
+ config.build.production.ios = {
48
+ ...config.build.production.ios,
49
+ project: `ios/${result.targetName}.xcodeproj`,
50
+ scheme: result.scheme,
51
+ configuration: "Release",
52
+ exportOptionsPlist: "ios/ExportOptions.plist",
53
+ bundleScript: "ios/sync-bundle.mjs",
54
+ };
55
+ await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
56
+ return true;
57
+ }
58
+ export async function initializeIosHost(root, options) {
59
+ validateBundleIdentifier(options.bundleIdentifier);
60
+ const ios = join(root, "ios");
61
+ assert(!(await exists(ios)), "IOS_HOST_EXISTS", `The iOS host already exists at ${ios}. LynxShip will not overwrite it.`);
62
+ const scheme = targetName(options.appName);
63
+ await cp(templateRoot, ios, { recursive: true, force: false });
64
+ await rename(join(ios, "__IOS_TARGET_NAME__"), join(ios, scheme));
65
+ await rename(join(ios, "__IOS_TARGET_NAME__.xcodeproj"), join(ios, `${scheme}.xcodeproj`));
66
+ await rename(join(ios, scheme, "Hello-Lynx-Bridging-Header.h"), join(ios, scheme, `${scheme}-Bridging-Header.h`));
67
+ const files = await textFiles(ios);
68
+ for (const file of files) {
69
+ let content = await readFile(file, "utf8");
70
+ content = content
71
+ .replaceAll("test.Hello-Lynx", options.bundleIdentifier)
72
+ .replaceAll("Hello-Lynx", scheme);
73
+ await writeFile(file, content, "utf8");
74
+ }
75
+ const project = `ios/${scheme}.xcodeproj`;
76
+ const result = {
77
+ directory: ios,
78
+ targetName: scheme,
79
+ bundleIdentifier: options.bundleIdentifier,
80
+ project,
81
+ scheme,
82
+ exportOptionsPlist: "ios/ExportOptions.plist",
83
+ bundleScript: "ios/sync-bundle.mjs",
84
+ };
85
+ const configUpdated = await updateProjectConfig(root, result);
86
+ return { ...result, configUpdated };
87
+ }
88
+ export function suggestedIosBundleIdentifier(root) {
89
+ const project = basename(root)
90
+ .toLowerCase()
91
+ .replace(/[^a-z0-9]+/g, "");
92
+ return `com.example.${project || "lynxapp"}`;
93
+ }
@@ -18,6 +18,7 @@ export interface ProgressHandle {
18
18
  update(value?: number, label?: string): void;
19
19
  stop(): void;
20
20
  }
21
+ export declare function formatEvent(message: string): string;
21
22
  export declare function createProgress(label: string, enabled: boolean): ProgressHandle;
22
23
  export interface SpinnerHandle {
23
24
  succeed(message: string): void;
@@ -32,5 +33,6 @@ export declare const pill: {
32
33
  error: (text: string) => string;
33
34
  };
34
35
  export declare function finalLine(message: string, success?: boolean): void;
36
+ export declare function nextSteps(commands: string[], note?: string): void;
35
37
  export declare function downloadArtifact(url: string, expiresAt?: string, enabled?: boolean): void;
36
38
  //# sourceMappingURL=components.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../../src/ui/components.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAI7C,MAAM,WAAW,MAAM;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,SAAS,CAAC;CACxB;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAEjD;AAED,eAAO,MAAM,GAAG;qBACG,MAAM,GAAG,IAAI;kBAGhB,MAAM,GAAG,IAAI;mBAGZ,MAAM,GAAG,IAAI;kBAGd,MAAM,GAAG,IAAI;mBAGZ,MAAM,GAAG,IAAI;CAG7B,CAAC;AAgBF,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CA6C9D;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,IAAI,IAAI,IAAI,CAAC;CACd;AA2FD,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,OAAO,GACf,cAAc,CA8DhB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,IAAI,IAAI,IAAI,CAAC;CACd;AAED,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,aAAa,CA2BlE;AAYD,eAAO,MAAM,IAAI;oBACC,MAAM;iBACT,MAAM;iBACN,MAAM;kBACL,MAAM;CACrB,CAAC;AAEF,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,UAAO,GAAG,IAAI,CAE/D;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,UAAO,GACb,IAAI,CAQN"}
1
+ {"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../../src/ui/components.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAI7C,MAAM,WAAW,MAAM;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,SAAS,CAAC;CACxB;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAEjD;AAED,eAAO,MAAM,GAAG;qBACG,MAAM,GAAG,IAAI;kBAGhB,MAAM,GAAG,IAAI;mBAGZ,MAAM,GAAG,IAAI;kBAGd,MAAM,GAAG,IAAI;mBAGZ,MAAM,GAAG,IAAI;CAG7B,CAAC;AAgBF,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CA6C9D;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,IAAI,IAAI,IAAI,CAAC;CACd;AA6CD,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAoBnD;AAyBD,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,OAAO,GACf,cAAc,CAkEhB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,IAAI,IAAI,IAAI,CAAC;CACd;AAED,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,aAAa,CA2BlE;AAYD,eAAO,MAAM,IAAI;oBACC,MAAM;iBACT,MAAM;iBACN,MAAM;kBACL,MAAM;CACrB,CAAC;AAEF,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,UAAO,GAAG,IAAI,CAE/D;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAOjE;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,UAAO,GACb,IAAI,CAQN"}
@@ -10,13 +10,13 @@ export function sectionHeader(title) {
10
10
  }
11
11
  export const log = {
12
12
  success(message) {
13
- console.log(` ${c.teal("[OK] ")} ${c.text(message)}`);
13
+ console.log(` ${c.green("")} ${c.text(message)}`);
14
14
  },
15
15
  warn(message) {
16
- console.log(` ${c.yellow("[WARN]")} ${c.yellow(message)}`);
16
+ console.log(` ${c.yellow("!")} ${c.yellow(message)}`);
17
17
  },
18
18
  error(message) {
19
- console.error(` ${c.red("[ERROR]")} ${c.red(message)}`);
19
+ console.error(` ${c.red("x")} ${c.red(message)}`);
20
20
  },
21
21
  info(message) {
22
22
  console.log(` ${c.muted(message)}`);
@@ -105,7 +105,7 @@ function eventTone(message) {
105
105
  return "step";
106
106
  return "output";
107
107
  }
108
- function formatEvent(message) {
108
+ export function formatEvent(message) {
109
109
  const colors = {
110
110
  command: c.blue,
111
111
  step: c.teal,
@@ -115,17 +115,16 @@ function formatEvent(message) {
115
115
  output: c.text,
116
116
  };
117
117
  const tone = eventTone(message);
118
- const tag = {
119
- command: "[CMD]",
120
- step: "[STEP]",
121
- success: "[OK]",
122
- warning: "[WARN]",
123
- error: "[ERROR]",
124
- output: "[LOG]",
125
- }[tone];
126
118
  const normalized = message.trim().replace(/^\$\s*/, "");
127
- const rail = supportsUnicode() ? "│" : "|";
128
- return ` ${colors[tone](rail)} ${colors[tone](tag.padEnd(7))} ${colors[tone](normalized)}`;
119
+ const marker = {
120
+ command: supportsUnicode() ? "➜" : ">",
121
+ step: "-",
122
+ success: supportsUnicode() ? "◆" : "*",
123
+ warning: "!",
124
+ error: "x",
125
+ output: supportsUnicode() ? "│" : "|",
126
+ }[tone];
127
+ return ` ${colors[tone](marker)} ${colors[tone](normalized)}`;
129
128
  }
130
129
  const activeAnimations = new Set();
131
130
  let cleanupHooksInstalled = false;
@@ -162,6 +161,7 @@ export function createProgress(label, enabled) {
162
161
  let spinnerIndex = 0;
163
162
  let currentValue = 0;
164
163
  let hasMeasuredValue = false;
164
+ let previousTone;
165
165
  const formatPercent = (value) => `${value.toFixed(2).replace(/\.?0+$/, "")}%`;
166
166
  const bar = new cliProgress.SingleBar({
167
167
  format: ` {spinner} {stage} ${c.teal("{bar}")} ${c.muted("{percent}")}`,
@@ -191,9 +191,13 @@ export function createProgress(label, enabled) {
191
191
  const logEvent = (message) => {
192
192
  if (!bar.isActive)
193
193
  return;
194
+ const tone = eventTone(message);
194
195
  bar.stop();
196
+ if (tone === "output" && previousTone === "output")
197
+ console.log();
195
198
  console.log(formatEvent(message));
196
199
  bar.start(100, currentValue, payload(currentLabel));
200
+ previousTone = tone;
197
201
  };
198
202
  return {
199
203
  event: logEvent,
@@ -228,11 +232,11 @@ export function spin(text, enabled) {
228
232
  const untrack = trackAnimation(() => spinner.stop());
229
233
  return {
230
234
  succeed: (message) => {
231
- spinner.succeed(`${c.teal("[OK]")} ${c.text(message)}`);
235
+ spinner.succeed(`${c.green("")} ${c.text(message)}`);
232
236
  untrack();
233
237
  },
234
238
  fail: (message) => {
235
- spinner.fail(`${c.red("[ERROR]")} ${c.red(message)}`);
239
+ spinner.fail(`${c.red("x")} ${c.red(message)}`);
236
240
  untrack();
237
241
  },
238
242
  stop: () => {
@@ -255,6 +259,16 @@ export const pill = {
255
259
  export function finalLine(message, success = true) {
256
260
  console.log(`\n${success ? c.brand("◆") : c.red("◆")} ${c.text(message)}`);
257
261
  }
262
+ export function nextSteps(commands, note) {
263
+ if (commands.length === 0)
264
+ return;
265
+ console.error(`\n${c.yellow("Next steps")}`);
266
+ commands.forEach((command, index) => {
267
+ console.error(` ${c.muted(`${index + 1}.`)} ${c.teal(command)}`);
268
+ });
269
+ if (note)
270
+ console.error(` ${c.muted(note)}`);
271
+ }
258
272
  export function downloadArtifact(url, expiresAt, enabled = true) {
259
273
  if (!enabled)
260
274
  return;
@@ -1,5 +1,6 @@
1
1
  import { type BoxRow, type ProgressHandle, type SpinnerHandle } from "./components.js";
2
2
  import { terminalOptions, type TerminalOptions } from "./terminal.js";
3
+ import type { CliGuidance } from "../guidance.js";
3
4
  export declare class CliUi {
4
5
  readonly options: TerminalOptions;
5
6
  readonly interactive: boolean;
@@ -11,6 +12,7 @@ export declare class CliUi {
11
12
  info(message: string): void;
12
13
  debug(message: string): void;
13
14
  error(message: string): void;
15
+ nextSteps(guidance: CliGuidance): void;
14
16
  summary(title: string, rows: BoxRow[]): void;
15
17
  configurationStatus(rows: BoxRow[]): void;
16
18
  progress(label: string): ProgressHandle;
@@ -20,5 +22,5 @@ export declare class CliUi {
20
22
  }
21
23
  export declare function createCliUi(args: string[]): CliUi;
22
24
  export { terminalOptions };
23
- export type { BoxRow, TerminalOptions };
25
+ export type { BoxRow, ProgressHandle, TerminalOptions };
24
26
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,MAAM,EACX,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAEL,eAAe,EACf,KAAK,eAAe,EACrB,MAAM,eAAe,CAAC;AAEvB,qBAAa,KAAK;IAChB,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAElC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;gBAElB,IAAI,EAAE,MAAM,EAAE;IAU1B,MAAM,IAAI,IAAI;IAId,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI3B,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI9B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI3B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI3B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAK5B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI5B,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI;IAI5C,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI;IAKzC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc;IAIvC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa;IAIpC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,UAAO,GAAG,IAAI;IAI3C,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;CAGxD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,CAEjD;AAED,OAAO,EAAE,eAAe,EAAE,CAAC;AAE3B,YAAY,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,KAAK,MAAM,EACX,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAEL,eAAe,EACf,KAAK,eAAe,EACrB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,qBAAa,KAAK;IAChB,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAElC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;gBAElB,IAAI,EAAE,MAAM,EAAE;IAU1B,MAAM,IAAI,IAAI;IAId,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI3B,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI9B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI3B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI3B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAK5B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI5B,SAAS,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI;IAKtC,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI;IAI5C,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI;IAKzC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc;IAIvC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa;IAIpC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,UAAO,GAAG,IAAI;IAI3C,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;CAGxD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,CAEjD;AAED,OAAO,EAAE,eAAe,EAAE,CAAC;AAE3B,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,eAAe,EAAE,CAAC"}
package/dist/ui/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { finalLine, log, sectionHeader, summaryBox, createProgress, spin, downloadArtifact, } from "./components.js";
1
+ import { finalLine, log, sectionHeader, summaryBox, nextSteps, createProgress, spin, downloadArtifact, } from "./components.js";
2
2
  import { printBanner } from "./logo.js";
3
3
  import { setColors } from "./state.js";
4
4
  import { isInteractive, terminalOptions, } from "./terminal.js";
@@ -40,6 +40,11 @@ export class CliUi {
40
40
  if (!this.options.json)
41
41
  log.error(message);
42
42
  }
43
+ nextSteps(guidance) {
44
+ if (this.options.json || this.options.quiet)
45
+ return;
46
+ nextSteps(guidance.commands, guidance.note);
47
+ }
43
48
  summary(title, rows) {
44
49
  if (this.interactive)
45
50
  summaryBox(title, rows);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lynxship/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.7",
4
4
  "description": "Build, sign, store, submit and update LynxJS applications from the terminal.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,7 +25,8 @@
25
25
  ],
26
26
  "files": [
27
27
  "dist",
28
- "README.md"
28
+ "README.md",
29
+ "templates"
29
30
  ],
30
31
  "engines": {
31
32
  "node": ">=22.0.0"
@@ -68,10 +69,10 @@
68
69
  "ora": "8.2.0",
69
70
  "qrcode-terminal": "0.12.0",
70
71
  "@lynxship/build-orchestrator": "0.1.0",
71
- "@lynxship/db": "0.1.0",
72
72
  "@lynxship/contracts": "0.1.0",
73
73
  "@lynxship/signing": "0.1.0",
74
- "@lynxship/submit": "0.1.0"
74
+ "@lynxship/submit": "0.1.0",
75
+ "@lynxship/db": "0.1.0"
75
76
  },
76
77
  "devDependencies": {
77
78
  "@types/cli-progress": "3.11.6",
@@ -0,0 +1,38 @@
1
+ plugins {
2
+ id "com.android.application"
3
+ }
4
+
5
+ android {
6
+ namespace "__APPLICATION_ID__"
7
+ compileSdk 35
8
+
9
+ defaultConfig {
10
+ applicationId "__APPLICATION_ID__"
11
+ minSdk 24
12
+ targetSdk 35
13
+ versionCode 1
14
+ versionName "0.1.0"
15
+ }
16
+
17
+ buildTypes {
18
+ release {
19
+ minifyEnabled false
20
+ proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"),
21
+ "proguard-rules.pro"
22
+ }
23
+ }
24
+
25
+ compileOptions {
26
+ sourceCompatibility JavaVersion.VERSION_17
27
+ targetCompatibility JavaVersion.VERSION_17
28
+ }
29
+ }
30
+
31
+ dependencies {
32
+ implementation "org.lynxsdk.lynx:lynx:4.0.0"
33
+ implementation "org.lynxsdk.lynx:lynx-jssdk:4.0.0"
34
+ implementation "org.lynxsdk.lynx:lynx-trace:4.0.0"
35
+ implementation "org.lynxsdk.lynx:primjs:4.0.0"
36
+ implementation "org.lynxsdk.lynx:lynx-service-log:4.0.0"
37
+ implementation "org.lynxsdk.lynx:lynx-service-http:4.0.0"
38
+ }
@@ -0,0 +1,2 @@
1
+ # Lynx's official release rules are supplied by the Lynx Android artifacts.
2
+ # Add application-specific keep rules here when native modules require them.
@@ -0,0 +1,20 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
3
+ <uses-permission android:name="android.permission.INTERNET" />
4
+
5
+ <application
6
+ android:allowBackup="false"
7
+ android:name=".LynxShipApplication"
8
+ android:label="@string/app_name"
9
+ android:supportsRtl="true"
10
+ android:theme="@style/AppTheme">
11
+ <activity
12
+ android:name=".MainActivity"
13
+ android:exported="true">
14
+ <intent-filter>
15
+ <action android:name="android.intent.action.MAIN" />
16
+ <category android:name="android.intent.category.LAUNCHER" />
17
+ </intent-filter>
18
+ </activity>
19
+ </application>
20
+ </manifest>
@@ -0,0 +1,13 @@
1
+ package __PACKAGE_NAME__;
2
+
3
+ import android.app.Application;
4
+
5
+ import com.lynx.tasm.LynxEnv;
6
+
7
+ public final class LynxShipApplication extends Application {
8
+ @Override
9
+ public void onCreate() {
10
+ super.onCreate();
11
+ LynxEnv.inst().init(this, null, null, null);
12
+ }
13
+ }
@@ -0,0 +1,19 @@
1
+ package __PACKAGE_NAME__;
2
+
3
+ import android.app.Activity;
4
+ import android.os.Bundle;
5
+
6
+ import com.lynx.tasm.LynxView;
7
+ import com.lynx.tasm.LynxViewBuilder;
8
+
9
+ public final class MainActivity extends Activity {
10
+ @Override
11
+ protected void onCreate(Bundle savedInstanceState) {
12
+ super.onCreate(savedInstanceState);
13
+ LynxView lynxView = new LynxViewBuilder()
14
+ .setTemplateProvider(new ProjectTemplateProvider(this))
15
+ .build(this);
16
+ setContentView(lynxView);
17
+ lynxView.renderTemplateUrl("main.lynx.bundle", "");
18
+ }
19
+ }
@@ -0,0 +1,32 @@
1
+ package __PACKAGE_NAME__;
2
+
3
+ import android.content.Context;
4
+
5
+ import com.lynx.tasm.provider.AbsTemplateProvider;
6
+
7
+ import java.io.ByteArrayOutputStream;
8
+ import java.io.IOException;
9
+ import java.io.InputStream;
10
+
11
+ public final class ProjectTemplateProvider extends AbsTemplateProvider {
12
+ private final Context context;
13
+
14
+ public ProjectTemplateProvider(Context context) {
15
+ this.context = context.getApplicationContext();
16
+ }
17
+
18
+ @Override
19
+ public void loadTemplate(String uri, Callback callback) {
20
+ new Thread(() -> {
21
+ try (InputStream input = context.getAssets().open(uri);
22
+ ByteArrayOutputStream output = new ByteArrayOutputStream()) {
23
+ byte[] buffer = new byte[8192];
24
+ int read;
25
+ while ((read = input.read(buffer)) != -1) output.write(buffer, 0, read);
26
+ callback.onSuccess(output.toByteArray());
27
+ } catch (IOException error) {
28
+ callback.onFailed(error.getMessage());
29
+ }
30
+ }, "lynx-template-loader").start();
31
+ }
32
+ }
@@ -0,0 +1,3 @@
1
+ <resources>
2
+ <string name="app_name">__APP_NAME__</string>
3
+ </resources>
@@ -0,0 +1,6 @@
1
+ <resources>
2
+ <style name="AppTheme" parent="android:style/Theme.Material.Light.NoActionBar">
3
+ <item name="android:fontFamily">sans</item>
4
+ <item name="android:colorAccent">#1976D2</item>
5
+ </style>
6
+ </resources>
@@ -0,0 +1,3 @@
1
+ plugins {
2
+ id "com.android.application" version "8.7.0" apply false
3
+ }
@@ -0,0 +1,7 @@
1
+ distributionBase=GRADLE_USER_HOME
2
+ distributionPath=wrapper/dists
3
+ distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
4
+ networkTimeout=60000
5
+ validateDistributionUrl=true
6
+ zipStoreBase=GRADLE_USER_HOME
7
+ zipStorePath=wrapper/dists
@@ -0,0 +1,3 @@
1
+ android.useAndroidX=true
2
+ android.nonTransitiveRClass=true
3
+ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8