create-cmp-cli 0.2.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.
Files changed (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +232 -0
  3. package/bin/create-cmp.mjs +91 -0
  4. package/options.schema.json +110 -0
  5. package/package.json +52 -0
  6. package/src/bootstrap/checks.mjs +283 -0
  7. package/src/bootstrap/exec.mjs +90 -0
  8. package/src/commands/clean.mjs +195 -0
  9. package/src/commands/create.mjs +223 -0
  10. package/src/commands/doctor.mjs +256 -0
  11. package/src/commands/upgrade.mjs +196 -0
  12. package/src/commands/verify.mjs +94 -0
  13. package/src/doctor.mjs +109 -0
  14. package/src/lib/args.mjs +38 -0
  15. package/src/lib/clean.mjs +72 -0
  16. package/src/lib/fsutil.mjs +90 -0
  17. package/src/lib/log.mjs +40 -0
  18. package/src/lib/project-doctor.mjs +329 -0
  19. package/src/lib/registry.mjs +102 -0
  20. package/src/lib/rename.mjs +116 -0
  21. package/src/lib/schema.mjs +123 -0
  22. package/src/lib/toggle.mjs +130 -0
  23. package/src/lib/tokens.mjs +96 -0
  24. package/src/lib/toml.mjs +137 -0
  25. package/src/lib/upgrade.mjs +159 -0
  26. package/src/lib/verify.mjs +97 -0
  27. package/src/scaffold.mjs +347 -0
  28. package/src/verify.mjs +4 -0
  29. package/src/versions/registry.json +42 -0
  30. package/template/.github/workflows/verify.yml +70 -0
  31. package/template/build.gradle.kts +17 -0
  32. package/template/composeApp/build.gradle.kts +257 -0
  33. package/template/composeApp/google-services.json +29 -0
  34. package/template/composeApp/proguard-rules.pro +34 -0
  35. package/template/composeApp/src/androidDebug/AndroidManifest.xml +13 -0
  36. package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/ComposeRootRegistry.kt +40 -0
  37. package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorCatalog.kt +80 -0
  38. package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorHttpServer.kt +322 -0
  39. package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorInit.kt +16 -0
  40. package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/LiveSemanticsJson.kt +102 -0
  41. package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/RemoteControlPage.kt +106 -0
  42. package/template/composeApp/src/androidDebug/res/xml/debug_network_security_config.xml +9 -0
  43. package/template/composeApp/src/androidMain/AndroidManifest.xml +28 -0
  44. package/template/composeApp/src/androidMain/kotlin/com/example/app/AppApplication.kt +72 -0
  45. package/template/composeApp/src/androidMain/kotlin/com/example/app/MainActivity.kt +24 -0
  46. package/template/composeApp/src/androidMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.kt +42 -0
  47. package/template/composeApp/src/androidMain/kotlin/com/example/app/data/local/DatabaseBuilder.android.kt +13 -0
  48. package/template/composeApp/src/androidMain/kotlin/com/example/app/di/AndroidModule.kt +9 -0
  49. package/template/composeApp/src/androidMain/kotlin/com/example/app/presentation/components/TestTagAutomation.android.kt +11 -0
  50. package/template/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml +9 -0
  51. package/template/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml +6 -0
  52. package/template/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
  53. package/template/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png +0 -0
  54. package/template/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.png +0 -0
  55. package/template/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png +0 -0
  56. package/template/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png +0 -0
  57. package/template/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.png +0 -0
  58. package/template/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png +0 -0
  59. package/template/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png +0 -0
  60. package/template/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
  61. package/template/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
  62. package/template/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  63. package/template/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
  64. package/template/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  65. package/template/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  66. package/template/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
  67. package/template/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  68. package/template/composeApp/src/androidMain/res/values/colors.xml +6 -0
  69. package/template/composeApp/src/androidMain/res/values/themes.xml +9 -0
  70. package/template/composeApp/src/androidMain/res/values-v31/themes.xml +8 -0
  71. package/template/composeApp/src/androidRelease/kotlin/com/example/app/inspector/InspectorInit.kt +13 -0
  72. package/template/composeApp/src/commonMain/composeResources/font/DMSans_Bold.ttf +0 -0
  73. package/template/composeApp/src/commonMain/composeResources/font/DMSans_Medium.ttf +0 -0
  74. package/template/composeApp/src/commonMain/composeResources/font/DMSans_Regular.ttf +0 -0
  75. package/template/composeApp/src/commonMain/composeResources/font/DMSans_SemiBold.ttf +0 -0
  76. package/template/composeApp/src/commonMain/kotlin/com/example/app/App.kt +7 -0
  77. package/template/composeApp/src/commonMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.kt +7 -0
  78. package/template/composeApp/src/commonMain/kotlin/com/example/app/data/local/AppDatabase.kt +28 -0
  79. package/template/composeApp/src/commonMain/kotlin/com/example/app/data/local/DatabaseBuilder.kt +14 -0
  80. package/template/composeApp/src/commonMain/kotlin/com/example/app/data/local/ItemDao.kt +18 -0
  81. package/template/composeApp/src/commonMain/kotlin/com/example/app/data/remote/FirebaseConfig.kt +5 -0
  82. package/template/composeApp/src/commonMain/kotlin/com/example/app/data/remote/ItemRepositoryImpl.kt +22 -0
  83. package/template/composeApp/src/commonMain/kotlin/com/example/app/di/AppModule.kt +23 -0
  84. package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/model/Item.kt +8 -0
  85. package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/repository/ItemRepository.kt +8 -0
  86. package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/usecase/GetItemsUseCase.kt +12 -0
  87. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/App.kt +13 -0
  88. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/BaseScreen.kt +69 -0
  89. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/TestTagAutomation.kt +18 -0
  90. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/DetailScreen.kt +39 -0
  91. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeScreen.kt +87 -0
  92. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeViewModel.kt +35 -0
  93. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppNavHost.kt +43 -0
  94. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppShell.kt +144 -0
  95. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppTab.kt +25 -0
  96. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/Screen.kt +15 -0
  97. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/profile/ProfileScreen.kt +43 -0
  98. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/DesignToken.kt +25 -0
  99. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Shape.kt +12 -0
  100. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Theme.kt +64 -0
  101. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Tokens.kt +19 -0
  102. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Typography.kt +36 -0
  103. package/template/composeApp/src/desktopMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.desktop.kt +9 -0
  104. package/template/composeApp/src/desktopMain/kotlin/com/example/app/data/local/DatabaseBuilder.desktop.kt +13 -0
  105. package/template/composeApp/src/desktopMain/kotlin/com/example/app/di/DesktopModule.kt +32 -0
  106. package/template/composeApp/src/desktopMain/kotlin/com/example/app/main.kt +29 -0
  107. package/template/composeApp/src/desktopMain/kotlin/com/example/app/presentation/components/TestTagAutomation.desktop.kt +6 -0
  108. package/template/composeApp/src/iosMain/kotlin/com/example/app/KoinHelper.kt +50 -0
  109. package/template/composeApp/src/iosMain/kotlin/com/example/app/MainViewController.kt +10 -0
  110. package/template/composeApp/src/iosMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.kt +32 -0
  111. package/template/composeApp/src/iosMain/kotlin/com/example/app/data/local/DatabaseBuilder.ios.kt +10 -0
  112. package/template/composeApp/src/iosMain/kotlin/com/example/app/presentation/components/TestTagAutomation.ios.kt +11 -0
  113. package/template/docs/dev-client.md +52 -0
  114. package/template/gitignore +21 -0
  115. package/template/gradle/libs.versions.toml +100 -0
  116. package/template/gradle/wrapper/gradle-wrapper.jar +0 -0
  117. package/template/gradle/wrapper/gradle-wrapper.properties +7 -0
  118. package/template/gradle.properties +9 -0
  119. package/template/gradlew +152 -0
  120. package/template/gradlew.bat +94 -0
  121. package/template/iosApp/Podfile +27 -0
  122. package/template/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png +0 -0
  123. package/template/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
  124. package/template/iosApp/iosApp/Assets.xcassets/Contents.json +6 -0
  125. package/template/iosApp/iosApp/ContentView.swift +17 -0
  126. package/template/iosApp/iosApp/GoogleService-Info.plist +34 -0
  127. package/template/iosApp/iosApp/Info.plist +43 -0
  128. package/template/iosApp/iosApp/PrivacyInfo.xcprivacy +42 -0
  129. package/template/iosApp/iosApp/iOSApp.swift +33 -0
  130. package/template/iosApp/project.yml +56 -0
  131. package/template/local.properties.example +2 -0
  132. package/template/manifest.json +125 -0
  133. package/template/qa/appium/README.md +23 -0
  134. package/template/qa/appium/lib/appium-client.mjs +225 -0
  135. package/template/qa/appium/package.json +8 -0
  136. package/template/qa/appium/run-android-smoke.mjs +39 -0
  137. package/template/settings.gradle.kts +34 -0
  138. package/template/tests/appium/cmp/conftest.py +96 -0
  139. package/template/tests/appium/cmp/test_smoke.py +17 -0
@@ -0,0 +1,94 @@
1
+ // `create-cmp verify` — run the green-build gate against an EXISTING project
2
+ // directory (the same north-star gate the scaffold ends on, exposed
3
+ // standalone). Usage: create-cmp verify [--target-dir .] [--no-ios] [--dry-run]
4
+ //
5
+ // Command resolution order:
6
+ // 1. <project>/manifest.json `verify` block (rare — the scaffold deletes it)
7
+ // 2. the bundled template's manifest.json `verify` block
8
+ // 3. hard fallbacks (./gradlew :composeApp:assembleDebug)
9
+
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ import { flagBool } from "../lib/args.mjs";
15
+ import { runVerify, printVerifyVerdict } from "../lib/verify.mjs";
16
+ import { colors } from "../lib/log.mjs";
17
+
18
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
+ const REPO_ROOT = path.resolve(__dirname, "..", "..");
20
+
21
+ const FALLBACK_VERIFY = {
22
+ android: "./gradlew :composeApp:assembleDebug",
23
+ };
24
+
25
+ /** Resolve the verify command block for a project dir. */
26
+ export function resolveVerifyCommands(projectDir) {
27
+ for (const candidate of [
28
+ path.join(projectDir, "manifest.json"),
29
+ path.join(REPO_ROOT, "template", "manifest.json"),
30
+ ]) {
31
+ try {
32
+ const manifest = JSON.parse(fs.readFileSync(candidate, "utf8"));
33
+ if (manifest && manifest.verify && manifest.verify.android) {
34
+ return { verify: manifest.verify, source: candidate };
35
+ }
36
+ } catch {
37
+ // keep falling through
38
+ }
39
+ }
40
+ return { verify: FALLBACK_VERIFY, source: "built-in fallback" };
41
+ }
42
+
43
+ /**
44
+ * @param {Record<string,string|boolean>} flags
45
+ * @param {string|undefined} positional optional target dir positional
46
+ */
47
+ export async function runVerifyCommand(flags, positional) {
48
+ const targetDir =
49
+ (typeof flags["target-dir"] === "string" && flags["target-dir"]) || positional || ".";
50
+ const projectDir = path.resolve(targetDir);
51
+
52
+ if (!fs.existsSync(projectDir)) {
53
+ process.stderr.write(`Error: ${projectDir} does not exist.\n`);
54
+ process.exit(1);
55
+ }
56
+ const looksGradle =
57
+ fs.existsSync(path.join(projectDir, "settings.gradle.kts")) ||
58
+ fs.existsSync(path.join(projectDir, "settings.gradle")) ||
59
+ fs.existsSync(path.join(projectDir, "gradlew"));
60
+ if (!looksGradle) {
61
+ process.stderr.write(
62
+ `Error: ${projectDir} does not look like a Gradle project (no settings.gradle[.kts] or gradlew).\n`
63
+ );
64
+ process.exit(1);
65
+ }
66
+
67
+ const { verify, source } = resolveVerifyCommands(projectDir);
68
+
69
+ // iOS leg runs only when the project actually has an iOS shell (and the user
70
+ // didn't opt out); runVerify additionally gates on macOS.
71
+ const hasIosShell = fs.existsSync(path.join(projectDir, "iosApp"));
72
+ const ios = flagBool(flags, "ios", true) && hasIosShell;
73
+
74
+ process.stdout.write(
75
+ `${colors.bold("create-cmp verify")} — green-build gate\n` +
76
+ ` project: ${colors.cyan(projectDir)}\n` +
77
+ ` commands: ${colors.dim(source)}\n\n`
78
+ );
79
+
80
+ const dryRun = flags["dry-run"] === true;
81
+ const verdict = await runVerify({
82
+ projectDir,
83
+ manifest: { verify },
84
+ config: { platforms: { ios } },
85
+ dryRun,
86
+ });
87
+ printVerifyVerdict(verdict);
88
+ if (dryRun) {
89
+ process.stdout.write(
90
+ `${colors.yellow("Dry run")} — commands printed, nothing executed; the build is NOT proven.\n`
91
+ );
92
+ }
93
+ process.exit(verdict.green ? 0 : 1);
94
+ }
package/src/doctor.mjs ADDED
@@ -0,0 +1,109 @@
1
+ // doctor → bootstrap → verify toolchain installer.
2
+ //
3
+ // Detects the CMP toolchain (JDK 17, Android SDK/cmdline-tools/system-image/AVD,
4
+ // Xcode+CLT, CocoaPods, XcodeGen, Node, Appium 3.x + uiautomator2/xcuitest, adb).
5
+ // For each missing dep: prints the EXACT install command and asks consent
6
+ // (--yes skips prompts for CI). Idempotent (safe to re-run). OS-aware
7
+ // (Linux → Android-only, said explicitly). Xcode can't be CLI-installed — the
8
+ // App Store step is surfaced as the one manual action. Ends with a
9
+ // per-dependency GREEN/FAIL verdict table.
10
+
11
+ import { checksForHost, hostInfo } from "./bootstrap/checks.mjs";
12
+ import { runInstall } from "./bootstrap/exec.mjs";
13
+ import { colors } from "./lib/log.mjs";
14
+
15
+ /**
16
+ * @param {object} opts
17
+ * @param {boolean} [opts.assumeYes] --yes: auto-approve installs (CI)
18
+ * @param {boolean} [opts.dryRun] print install commands, don't run them
19
+ * @param {boolean} [opts.ios] whether iOS support is wanted (gates iOS checks)
20
+ * @param {boolean} [opts.installMissing=true] attempt installs for missing deps
21
+ * @returns {Promise<{green:boolean, rows:Array}>}
22
+ */
23
+ export async function doctor(opts = {}) {
24
+ const {
25
+ assumeYes = false,
26
+ dryRun = false,
27
+ ios = true,
28
+ installMissing = true,
29
+ } = opts;
30
+
31
+ process.stdout.write(`\n${colors.bold("create-cmp doctor")} — toolchain preflight\n`);
32
+ if (hostInfo.isLinux) {
33
+ process.stdout.write(
34
+ `${colors.yellow("Linux detected")}: scoping to Android-only (iOS toolchain is macOS-only).\n`
35
+ );
36
+ } else if (!hostInfo.isMac) {
37
+ process.stdout.write(
38
+ `${colors.yellow("Non-macOS host")}: iOS checks are skipped.\n`
39
+ );
40
+ }
41
+ process.stdout.write("\n");
42
+
43
+ const hostChecks = checksForHost({ iosWanted: ios });
44
+ const rows = [];
45
+
46
+ for (const check of hostChecks) {
47
+ let { present, detail } = check.detect();
48
+ const cmd = present ? null : check.installCommand({});
49
+
50
+ if (present) {
51
+ process.stdout.write(`${colors.green("✓")} ${check.label} — ${colors.dim(detail)}\n`);
52
+ } else {
53
+ process.stdout.write(`${colors.red("✗")} ${check.label} — ${colors.dim(detail)}\n`);
54
+ if (check.manual && cmd === null) {
55
+ process.stdout.write(` ${colors.yellow("manual:")} ${check.manual}\n`);
56
+ } else if (cmd) {
57
+ process.stdout.write(` ${colors.cyan("fix:")} ${cmd}\n`);
58
+ if (installMissing) {
59
+ const res = await runInstall({ command: cmd, assumeYes, dryRun });
60
+ if (res.ran && res.code === 0) {
61
+ // Re-detect after install (idempotent verify).
62
+ const after = check.detect();
63
+ present = after.present;
64
+ detail = after.detail;
65
+ if (present) {
66
+ process.stdout.write(` ${colors.green("→ now present")} (${detail})\n`);
67
+ } else {
68
+ process.stdout.write(
69
+ ` ${colors.yellow("→ still not detected")} (may need a new shell / PATH export)\n`
70
+ );
71
+ }
72
+ } else if (res.ran && res.code !== 0) {
73
+ process.stdout.write(` ${colors.red(`install exited ${res.code}`)}\n`);
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ rows.push({
80
+ id: check.id,
81
+ label: check.label,
82
+ present,
83
+ detail,
84
+ manual: !present && check.installCommand({}) === null,
85
+ });
86
+ }
87
+
88
+ // Verdict table.
89
+ process.stdout.write(`\n${colors.bold("Verdict")}\n`);
90
+ let green = true;
91
+ for (const r of rows) {
92
+ if (r.present) {
93
+ process.stdout.write(` ${colors.green("GREEN")} ${r.label}\n`);
94
+ } else if (r.manual) {
95
+ process.stdout.write(` ${colors.yellow("MANUAL")} ${r.label}\n`);
96
+ green = false;
97
+ } else {
98
+ process.stdout.write(` ${colors.red("FAIL ")} ${r.label}\n`);
99
+ green = false;
100
+ }
101
+ }
102
+ process.stdout.write(
103
+ green
104
+ ? `\n${colors.green("GREEN — toolchain ready.")}\n`
105
+ : `\n${colors.red("FAIL — toolchain incomplete.")} Re-run after addressing the items above (idempotent).\n`
106
+ );
107
+
108
+ return { green, rows };
109
+ }
@@ -0,0 +1,38 @@
1
+ // Tiny shared arg parser for the CLI (moved verbatim from bin/create-cmp.mjs
2
+ // when the bin became a thin subcommand dispatcher). No new behavior.
3
+
4
+ /**
5
+ * Parse argv into positionals + flags. `--flag value` captures the value;
6
+ * `--flag` followed by another `--flag` (or nothing) is boolean true.
7
+ * @param {string[]} argv
8
+ * @returns {{_: string[], flags: Record<string, string|boolean>}}
9
+ */
10
+ export function parseArgs(argv) {
11
+ const args = { _: [], flags: {} };
12
+ for (let i = 0; i < argv.length; i++) {
13
+ const a = argv[i];
14
+ if (a.startsWith("--")) {
15
+ const key = a.slice(2);
16
+ const next = argv[i + 1];
17
+ if (next === undefined || next.startsWith("--")) {
18
+ args.flags[key] = true; // boolean flag
19
+ } else {
20
+ args.flags[key] = next;
21
+ i++;
22
+ }
23
+ } else {
24
+ args._.push(a);
25
+ }
26
+ }
27
+ return args;
28
+ }
29
+
30
+ /**
31
+ * Tri-state boolean flag: `--name`/`--name true` → true, `--no-name`/`--name false`
32
+ * → false, otherwise the default.
33
+ */
34
+ export function flagBool(flags, name, dflt) {
35
+ if (flags[name] === true || flags[name] === "true") return true;
36
+ if (flags[`no-${name}`] === true || flags[name] === "false") return false;
37
+ return dflt;
38
+ }
@@ -0,0 +1,72 @@
1
+ // Pure selection logic for `create-cmp clean`. The command layer does the
2
+ // listing/sizing/deleting; this module decides WHAT is safe to remove, so the
3
+ // policy is unit-testable against fake directory listings.
4
+ //
5
+ // Policy (deliberately conservative):
6
+ // - ~/.konan: only "clearly stale" entries — kotlin-native toolchain dirs
7
+ // whose trailing version does NOT match the project's kotlin version.
8
+ // Shared dirs (dependencies/, cache/, kotlin-native-prebuilt-<current>)
9
+ // are never selected. No known project kotlin version → nothing is stale.
10
+ // - project: only `build/` dirs that sit next to a build.gradle(.kts) (i.e.
11
+ // real Gradle module outputs) plus the root `.gradle/` dir.
12
+ // - ~/.gradle/caches: REPORT ONLY, never auto-deleted.
13
+
14
+ /** kotlin-native toolchain dir with a trailing version, e.g.
15
+ * kotlin-native-prebuilt-macos-aarch64-2.1.20 */
16
+ const KONAN_TOOLCHAIN_RE = /^kotlin-native(?:-prebuilt)?(?:-[a-z0-9_]+)*?-(\d+\.\d+(?:\.\d+)?(?:-[A-Za-z0-9.]+)?)$/;
17
+
18
+ /**
19
+ * Which ~/.konan entries are clearly stale?
20
+ * @param {string[]} entries directory names inside ~/.konan
21
+ * @param {string|null|undefined} projectKotlinVersion the project's kotlin version
22
+ * @returns {{stale:string[], kept:Array<{name:string, reason:string}>}}
23
+ */
24
+ export function selectStaleKonan(entries, projectKotlinVersion) {
25
+ const stale = [];
26
+ const kept = [];
27
+ for (const name of entries) {
28
+ const m = name.match(KONAN_TOOLCHAIN_RE);
29
+ if (!m) {
30
+ kept.push({ name, reason: "not a versioned kotlin-native toolchain dir (shared cache — never touched)" });
31
+ continue;
32
+ }
33
+ if (!projectKotlinVersion) {
34
+ kept.push({ name, reason: "project kotlin version unknown — cannot prove staleness" });
35
+ continue;
36
+ }
37
+ if (m[1] === projectKotlinVersion) {
38
+ kept.push({ name, reason: `matches the project's kotlin ${projectKotlinVersion}` });
39
+ } else {
40
+ stale.push(name);
41
+ }
42
+ }
43
+ return { stale, kept };
44
+ }
45
+
46
+ /**
47
+ * Which project-relative paths are safe Gradle outputs to delete?
48
+ * @param {object} input
49
+ * @param {string[]} input.dirs ALL directory paths in the project, RELATIVE with "/" separators
50
+ * @param {string[]} input.files ALL file paths in the project, RELATIVE with "/" separators
51
+ * @returns {string[]} relative dir paths safe to delete (root `.gradle` + module `build` dirs)
52
+ */
53
+ export function selectProjectCleanDirs({ dirs, files }) {
54
+ const fileSet = new Set(files);
55
+ const out = [];
56
+ for (const dir of dirs) {
57
+ if (dir === ".gradle") {
58
+ out.push(dir);
59
+ continue;
60
+ }
61
+ const segments = dir.split("/");
62
+ if (segments[segments.length - 1] !== "build") continue;
63
+ // Never reach inside another build dir or hidden/vendored trees.
64
+ if (segments.slice(0, -1).some((s) => s === "build" || s === "node_modules" || s.startsWith("."))) continue;
65
+ const parent = segments.slice(0, -1).join("/");
66
+ const sibling = (name) => (parent ? `${parent}/${name}` : name);
67
+ if (fileSet.has(sibling("build.gradle.kts")) || fileSet.has(sibling("build.gradle")) || fileSet.has(sibling("settings.gradle.kts")) || fileSet.has(sibling("settings.gradle"))) {
68
+ out.push(dir);
69
+ }
70
+ }
71
+ return out;
72
+ }
@@ -0,0 +1,90 @@
1
+ // Filesystem helpers: recursive copy and a text-file walker.
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+
6
+ /**
7
+ * Recursively copy srcDir → destDir, creating dest dirs as needed.
8
+ * Symlinks are copied as symlinks. Existing dest files are overwritten.
9
+ * @param {string} srcDir
10
+ * @param {string} destDir
11
+ */
12
+ export function copyDir(srcDir, destDir) {
13
+ fs.mkdirSync(destDir, { recursive: true });
14
+ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
15
+ const s = path.join(srcDir, entry.name);
16
+ const d = path.join(destDir, entry.name);
17
+ if (entry.isSymbolicLink()) {
18
+ const link = fs.readlinkSync(s);
19
+ try { fs.unlinkSync(d); } catch {}
20
+ fs.symlinkSync(link, d);
21
+ } else if (entry.isDirectory()) {
22
+ copyDir(s, d);
23
+ } else {
24
+ fs.copyFileSync(s, d);
25
+ }
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Yield absolute paths of every regular file under dir (recursive).
31
+ * @param {string} dir
32
+ * @returns {string[]}
33
+ */
34
+ export function listFiles(dir) {
35
+ const out = [];
36
+ const stack = [dir];
37
+ while (stack.length) {
38
+ const cur = stack.pop();
39
+ let entries;
40
+ try {
41
+ entries = fs.readdirSync(cur, { withFileTypes: true });
42
+ } catch {
43
+ continue;
44
+ }
45
+ for (const e of entries) {
46
+ const full = path.join(cur, e.name);
47
+ if (e.isDirectory()) stack.push(full);
48
+ else if (e.isFile()) out.push(full);
49
+ }
50
+ }
51
+ return out;
52
+ }
53
+
54
+ /**
55
+ * Yield absolute paths of every directory under dir (recursive), DEEPEST FIRST
56
+ * (so path-token renames of nested dirs are safe to apply bottom-up).
57
+ * @param {string} dir
58
+ * @returns {string[]}
59
+ */
60
+ export function listDirsDeepestFirst(dir) {
61
+ const out = [];
62
+ const stack = [dir];
63
+ while (stack.length) {
64
+ const cur = stack.pop();
65
+ let entries;
66
+ try {
67
+ entries = fs.readdirSync(cur, { withFileTypes: true });
68
+ } catch {
69
+ continue;
70
+ }
71
+ for (const e of entries) {
72
+ if (e.isDirectory()) {
73
+ const full = path.join(cur, e.name);
74
+ out.push(full);
75
+ stack.push(full);
76
+ }
77
+ }
78
+ }
79
+ // Deepest first = longest path first.
80
+ out.sort((a, b) => b.length - a.length);
81
+ return out;
82
+ }
83
+
84
+ export function isMacOS() {
85
+ return process.platform === "darwin";
86
+ }
87
+
88
+ export function isLinux() {
89
+ return process.platform === "linux";
90
+ }
@@ -0,0 +1,40 @@
1
+ // Thin logging helper with optional color. Falls back gracefully if picocolors
2
+ // isn't installed (so the engine still works zero-dep in a pinch).
3
+
4
+ let pc;
5
+ try {
6
+ pc = (await import("picocolors")).default;
7
+ } catch {
8
+ const identity = (s) => s;
9
+ pc = {
10
+ green: identity, red: identity, yellow: identity, cyan: identity,
11
+ dim: identity, bold: identity, blue: identity, magenta: identity,
12
+ gray: identity,
13
+ };
14
+ }
15
+
16
+ export const colors = pc;
17
+
18
+ export function info(msg) {
19
+ process.stdout.write(`${msg}\n`);
20
+ }
21
+
22
+ export function ok(msg) {
23
+ process.stdout.write(`${pc.green("✓")} ${msg}\n`);
24
+ }
25
+
26
+ export function warn(msg) {
27
+ process.stdout.write(`${pc.yellow("!")} ${msg}\n`);
28
+ }
29
+
30
+ export function fail(msg) {
31
+ process.stdout.write(`${pc.red("✗")} ${msg}\n`);
32
+ }
33
+
34
+ export function step(msg) {
35
+ process.stdout.write(`${pc.cyan("›")} ${msg}\n`);
36
+ }
37
+
38
+ export function dim(msg) {
39
+ process.stdout.write(`${pc.dim(msg)}\n`);
40
+ }