lecodes-cli 0.9.2 → 0.10.1
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 +4 -0
- package/dist/index.js +677 -230
- package/package.json +2 -2
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk/compile/aspectMacro.ts +42 -0
- package/runtime/sdk/compile/assetMacro.ts +45 -0
- package/runtime/sdk/compile/bundler.ts +239 -0
- package/runtime/sdk/compile/compileProject.ts +104 -0
- package/runtime/sdk/compile/detectEntry.ts +125 -0
- package/runtime/sdk/compile/fontMacro.ts +459 -0
- package/runtime/sdk/compile/fontRegistry.ts +78 -0
- package/runtime/sdk/compile/header.ts +51 -0
- package/runtime/sdk/compile/index.ts +38 -0
- package/runtime/sdk/compile/libraryImports.ts +52 -0
- package/runtime/sdk/compile/sceneEditor.ts +78 -0
- package/runtime/sdk/compile/sfnt.ts +98 -0
- package/runtime/sdk/compile/sourcemap.ts +25 -0
- package/runtime/sdk/gl/Material.ts +52 -0
- package/runtime/sdk/gl/Particles.ts +477 -116
- package/runtime/sdk/inject.ts +1 -1
- package/runtime/sdk/ui/NativeView.ts +5 -5
- package/runtime/sdk/ui/UIButton.ts +5 -5
- package/runtime/sdk/ui/UIContainer.ts +13 -13
- package/runtime/sdk/ui/UIImage.ts +5 -5
- package/runtime/sdk/ui/UIInput.ts +9 -9
- package/runtime/sdk/ui/UINode.ts +114 -34
- package/runtime/sdk/ui/UIPager.ts +5 -9
- package/runtime/sdk/ui/UIScreen.ts +5 -5
- package/runtime/sdk/ui/UIScrollable.ts +5 -5
- package/runtime/sdk/ui/UISpacer.ts +5 -5
- package/runtime/sdk/ui/UIText.ts +5 -5
- package/runtime/sdk/ui/UIVideo.ts +5 -5
- package/runtime/sdk/ui/UIVirtualizedList.ts +5 -5
- package/runtime/sdk/ui/UIWidget.ts +5 -5
- package/runtime/sdk-types.json +1 -1
package/dist/index.js
CHANGED
|
@@ -247,7 +247,7 @@ var resolveInject = () => {
|
|
|
247
247
|
inject: ["/__sdk/inject.ts"],
|
|
248
248
|
format,
|
|
249
249
|
minify,
|
|
250
|
-
fuse: process.env.LECODES_CHISEL_FUSE
|
|
250
|
+
fuse: process.env.LECODES_CHISEL_FUSE !== "0",
|
|
251
251
|
define: { ...CHISEL_DEFINE, EDITOR: editor ? "true" : "false" },
|
|
252
252
|
keep: CHISEL_KEEP,
|
|
253
253
|
sourcemap,
|
|
@@ -268,7 +268,10 @@ var resolveInject = () => {
|
|
|
268
268
|
throw new Error(`chisel: ${parsed.error}`);
|
|
269
269
|
if (parsed.code == null)
|
|
270
270
|
throw new Error("chisel produced no code");
|
|
271
|
-
|
|
271
|
+
const diagnostics = parsed.diagnostics ?? [];
|
|
272
|
+
for (const d of diagnostics)
|
|
273
|
+
console.warn(`warning: ${d}`);
|
|
274
|
+
return { code: parsed.code, map: parsed.map ?? null, diagnostics };
|
|
272
275
|
}, bundleProject = async (entrypoint, files, format = "esm") => (await bundleProjectChisel(entrypoint, files, format, false)).code, bundleProjectWithMap = async (entrypoint, files, format = "esm", minify, editor, libraries, sourcemap = true) => bundleProjectChisel(entrypoint, files, format, sourcemap, minify, editor, libraries);
|
|
273
276
|
var init_bundler = __esm(() => {
|
|
274
277
|
init_assetMacro();
|
|
@@ -2088,9 +2091,9 @@ var require_main = __commonJS((exports, module) => {
|
|
|
2088
2091
|
// package.json
|
|
2089
2092
|
var package_default = {
|
|
2090
2093
|
name: "lecodes-cli",
|
|
2091
|
-
version: "0.
|
|
2094
|
+
version: "0.10.1",
|
|
2092
2095
|
dependencies: {
|
|
2093
|
-
"@letary/chisel": "^0.
|
|
2096
|
+
"@letary/chisel": "^0.6.0"
|
|
2094
2097
|
},
|
|
2095
2098
|
devDependencies: {
|
|
2096
2099
|
"lecodes-design": "workspace:*",
|
|
@@ -3472,6 +3475,11 @@ ${keys.map((k) => ` <key>${k}</key>
|
|
|
3472
3475
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3473
3476
|
<plist version="1.0">
|
|
3474
3477
|
<dict>
|
|
3478
|
+
<!-- Standard-encryption-only (HTTPS/TLS): declares export-compliance exemption
|
|
3479
|
+
so App Store Connect skips the "Missing Compliance" prompt on every build.
|
|
3480
|
+
Flip to <true/> only if the app adds its own non-exempt cryptography. -->
|
|
3481
|
+
<key>ITSAppUsesNonExemptEncryption</key>
|
|
3482
|
+
<false/>
|
|
3475
3483
|
<key>UIApplicationSceneManifest</key>
|
|
3476
3484
|
<dict>
|
|
3477
3485
|
<key>UIApplicationSupportsMultipleScenes</key>
|
|
@@ -3588,8 +3596,9 @@ var runtimePackageTemplate = (sdkVersion, variant, plugins) => {
|
|
|
3588
3596
|
//
|
|
3589
3597
|
// GENERATED by lecodes — regenerated by \`lecodes app sync\`; do not edit.
|
|
3590
3598
|
// Owns the app's native dependency graph: the LeCodesSDK variant (auto-picked from what
|
|
3591
|
-
// the compiled bundle uses, or forced via app.json ios.variant)
|
|
3592
|
-
//
|
|
3599
|
+
// the compiled bundle uses, or forced via app.json ios.variant), the exact SDK release
|
|
3600
|
+
// (app.json ios.sdk — move it with \`lecodes app update\`) and one vendored target per
|
|
3601
|
+
// installed plugin (see plugins.lock.json).
|
|
3593
3602
|
|
|
3594
3603
|
import PackageDescription
|
|
3595
3604
|
|
|
@@ -3600,7 +3609,7 @@ let package = Package(
|
|
|
3600
3609
|
.library(name: "LeCodesRuntime", targets: ["LeCodesRuntime"]),
|
|
3601
3610
|
],
|
|
3602
3611
|
dependencies: [
|
|
3603
|
-
.package(url: "https://github.com/letary/lecodes-ios-sdk.git",
|
|
3612
|
+
.package(url: "https://github.com/letary/lecodes-ios-sdk.git", exact: "${sdkVersion}"),
|
|
3604
3613
|
],
|
|
3605
3614
|
targets: [
|
|
3606
3615
|
.target(
|
|
@@ -3653,6 +3662,22 @@ lecodes app open # open App.xcodeproj in Xcode — build & run from there
|
|
|
3653
3662
|
\`lecodes app doctor\` preflights builds; \`lecodes app build ios\` archives and exports an
|
|
3654
3663
|
.ipa; \`lecodes app upload ios\` sends the archive to App Store Connect.
|
|
3655
3664
|
|
|
3665
|
+
## SDK version
|
|
3666
|
+
|
|
3667
|
+
\`../app.json\` \`ios.sdk\` is the exact \`lecodes-ios-sdk\` release this shell links —
|
|
3668
|
+
\`lecodes app init\` pinned the newest one at the time, so builds stay reproducible.
|
|
3669
|
+
|
|
3670
|
+
\`\`\`
|
|
3671
|
+
lecodes app update # → newest SDK release + newest plugin tags, then sync
|
|
3672
|
+
lecodes app update --check # what's available, writes nothing
|
|
3673
|
+
lecodes app update --sdk 1.3.0
|
|
3674
|
+
\`\`\`
|
|
3675
|
+
|
|
3676
|
+
Set \`"sdk": "latest"\` instead of a version to re-resolve on every sync (needs network;
|
|
3677
|
+
offline it keeps whatever the shell already links). The pin is exact rather than a range
|
|
3678
|
+
because the CLI also downloads that release's filament resources bundle into
|
|
3679
|
+
\`App/Resources/\` — the two halves of one SDK must not drift apart.
|
|
3680
|
+
|
|
3656
3681
|
## Who owns what
|
|
3657
3682
|
|
|
3658
3683
|
Yours — \`lecodes app sync\` never touches these:
|
|
@@ -3700,7 +3725,8 @@ buildNumber) — edit it there, then re-run \`lecodes app sync\`. Plugins are ma
|
|
|
3700
3725
|
`;
|
|
3701
3726
|
|
|
3702
3727
|
// src/commands/app.ts
|
|
3703
|
-
var
|
|
3728
|
+
var SDK_REPO = "letary/lecodes-ios-sdk";
|
|
3729
|
+
var SDK_MIN_VERSION = "1.2.0";
|
|
3704
3730
|
var SDK_VARIANTS = ["core", "2d", "3d", "full"];
|
|
3705
3731
|
var FIRST_PARTY_PLUGINS = {
|
|
3706
3732
|
camera: "letary/lecodes-plugins",
|
|
@@ -3725,6 +3751,51 @@ var projectRoot = () => {
|
|
|
3725
3751
|
}
|
|
3726
3752
|
};
|
|
3727
3753
|
var git = (cwd, ...args) => execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
3754
|
+
var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
3755
|
+
var cmpVersion = (a, b) => {
|
|
3756
|
+
const pa = a.split(".").map(Number);
|
|
3757
|
+
const pb = b.split(".").map(Number);
|
|
3758
|
+
for (let i = 0;i < 3; i++) {
|
|
3759
|
+
if ((pa[i] ?? 0) !== (pb[i] ?? 0))
|
|
3760
|
+
return (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
3761
|
+
}
|
|
3762
|
+
return 0;
|
|
3763
|
+
};
|
|
3764
|
+
var fetchLatestSdkVersion = async () => {
|
|
3765
|
+
const url = `https://api.github.com/repos/${SDK_REPO}/releases/latest`;
|
|
3766
|
+
const res = await fetch(url, { headers: { accept: "application/vnd.github+json", "user-agent": "lecodes-cli" } });
|
|
3767
|
+
if (!res.ok)
|
|
3768
|
+
throw new CliError(`Could not read the latest ${SDK_REPO} release (HTTP ${res.status}) — ${url}`);
|
|
3769
|
+
const tag = (await res.json()).tag_name?.replace(/^v/, "") ?? "";
|
|
3770
|
+
if (!VERSION_RE.test(tag))
|
|
3771
|
+
throw new CliError(`${SDK_REPO} latest release has no x.y.z tag (got "${tag}")`);
|
|
3772
|
+
return tag;
|
|
3773
|
+
};
|
|
3774
|
+
var shellSdkVersion = (iosDir) => {
|
|
3775
|
+
const path = join11(iosDir, "LeCodesRuntime", "Package.swift");
|
|
3776
|
+
if (!existsSync8(path))
|
|
3777
|
+
return null;
|
|
3778
|
+
const m = /lecodes-ios-sdk\.git",\s*(?:exact|from):\s*"([^"]+)"/.exec(readFileSync10(path, "utf8"));
|
|
3779
|
+
return m && VERSION_RE.test(m[1]) ? m[1] : null;
|
|
3780
|
+
};
|
|
3781
|
+
var resolveSdkVersion = async (pin, fallback) => {
|
|
3782
|
+
if (pin !== "latest") {
|
|
3783
|
+
if (!VERSION_RE.test(pin)) {
|
|
3784
|
+
throw new CliError(`app.json ios.sdk must be "latest" or an exact x.y.z release (got "${pin}").`);
|
|
3785
|
+
}
|
|
3786
|
+
if (cmpVersion(pin, SDK_MIN_VERSION) < 0) {
|
|
3787
|
+
throw new CliError(`app.json ios.sdk ${pin} is older than the ${SDK_MIN_VERSION} these generated shells need.`);
|
|
3788
|
+
}
|
|
3789
|
+
return pin;
|
|
3790
|
+
}
|
|
3791
|
+
try {
|
|
3792
|
+
return await fetchLatestSdkVersion();
|
|
3793
|
+
} catch (e) {
|
|
3794
|
+
const kept = fallback ?? SDK_MIN_VERSION;
|
|
3795
|
+
warn(`Could not resolve the latest SDK (${e.message}) — staying on ${kept}.`);
|
|
3796
|
+
return kept;
|
|
3797
|
+
}
|
|
3798
|
+
};
|
|
3728
3799
|
var ensureRepo = (repo, wantTag, lock) => {
|
|
3729
3800
|
const override = process.env.LECODES_PLUGINS_DIR;
|
|
3730
3801
|
if (override) {
|
|
@@ -3802,7 +3873,7 @@ var slugify = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+
|
|
|
3802
3873
|
var appInit = async (args) => {
|
|
3803
3874
|
const platform = args._[1];
|
|
3804
3875
|
if (platform !== "ios") {
|
|
3805
|
-
throw new CliError(platform ? `Unsupported platform "${platform}" — only "ios" for now.` : "Usage: lecodes app init ios [--name <display name>] [--bundle-id <id>] [--team <team id>] [--variant auto|core|2d|3d|full]");
|
|
3876
|
+
throw new CliError(platform ? `Unsupported platform "${platform}" — only "ios" for now.` : "Usage: lecodes app init ios [--name <display name>] [--bundle-id <id>] [--team <team id>] [--variant auto|core|2d|3d|full] [--sdk <version|latest>]");
|
|
3806
3877
|
}
|
|
3807
3878
|
const root = projectRoot();
|
|
3808
3879
|
const iosDir = join11(root, "ios");
|
|
@@ -3814,6 +3885,9 @@ var appInit = async (args) => {
|
|
|
3814
3885
|
if (variant !== "auto" && !SDK_VARIANTS.includes(variant)) {
|
|
3815
3886
|
throw new CliError(`--variant must be auto, ${SDK_VARIANTS.join(", ")} (got "${variant}")`);
|
|
3816
3887
|
}
|
|
3888
|
+
const sdkFlag = flagStr(args, "sdk") ?? existing.ios?.sdk;
|
|
3889
|
+
const sdkVersion = await resolveSdkVersion(process.env.LECODES_IOS_SDK ?? sdkFlag ?? "latest", null);
|
|
3890
|
+
const sdk = sdkFlag === "latest" ? "latest" : sdkVersion;
|
|
3817
3891
|
const name = flagStr(args, "name") ?? existing.name ?? basename4(root);
|
|
3818
3892
|
const config = {
|
|
3819
3893
|
...existing,
|
|
@@ -3824,6 +3898,7 @@ var appInit = async (args) => {
|
|
|
3824
3898
|
bundleId: flagStr(args, "bundle-id") ?? existing.ios?.bundleId ?? `com.example.${slugify(name)}`,
|
|
3825
3899
|
teamId: flagStr(args, "team") ?? existing.ios?.teamId ?? "",
|
|
3826
3900
|
variant,
|
|
3901
|
+
sdk,
|
|
3827
3902
|
buildNumber: existing.ios?.buildNumber ?? 1
|
|
3828
3903
|
}
|
|
3829
3904
|
};
|
|
@@ -3863,9 +3938,10 @@ ios/
|
|
|
3863
3938
|
write("App/Assets.xcassets/Contents.json", ASSETS_ROOT_JSON);
|
|
3864
3939
|
write("App/Assets.xcassets/AccentColor.colorset/Contents.json", ACCENT_COLOR_JSON);
|
|
3865
3940
|
write("App/Assets.xcassets/AppIcon.appiconset/Contents.json", APP_ICON_JSON);
|
|
3866
|
-
write("LeCodesRuntime/Package.swift", runtimePackageTemplate(
|
|
3941
|
+
write("LeCodesRuntime/Package.swift", runtimePackageTemplate(sdkVersion, variant === "auto" ? "core" : variant, []));
|
|
3867
3942
|
write("LeCodesRuntime/Sources/LeCodesRuntime/LeCodesRuntime.swift", runtimeSwiftTemplate([]));
|
|
3868
3943
|
log("");
|
|
3944
|
+
log(`${c.green("sdk")} lecodes-ios-sdk ${sdkVersion}${sdk === "latest" ? c.dim(" (ios.sdk: latest — re-resolved on every sync)") : c.dim(" (pinned in app.json ios.sdk — `lecodes app update` moves it)")}`);
|
|
3869
3945
|
success(`iOS shell for ${c.bold(name)} ready.`);
|
|
3870
3946
|
log(` ${c.bold("lecodes app sync")} embed the compiled bundle (+ plugins) into the shell`);
|
|
3871
3947
|
log(` ${c.bold("open ios/App.xcodeproj")} then run on your device from Xcode`);
|
|
@@ -3898,18 +3974,18 @@ var resolveVariant = (requested, bundleJs) => {
|
|
|
3898
3974
|
}
|
|
3899
3975
|
return { variant, detected };
|
|
3900
3976
|
};
|
|
3901
|
-
var ensureSdkResourcesBundle = async () => {
|
|
3977
|
+
var ensureSdkResourcesBundle = async (sdkVersion) => {
|
|
3902
3978
|
const override = process.env.LECODES_SDK_RESOURCES;
|
|
3903
3979
|
if (override) {
|
|
3904
3980
|
note(`Using local resources bundle ${override} (LECODES_SDK_RESOURCES)`);
|
|
3905
3981
|
return resolve2(override);
|
|
3906
3982
|
}
|
|
3907
|
-
const dir = join11(homedir2(), ".lecodes", "sdk-resources",
|
|
3983
|
+
const dir = join11(homedir2(), ".lecodes", "sdk-resources", sdkVersion);
|
|
3908
3984
|
const bundle = join11(dir, "LeCodesSDKResources.bundle");
|
|
3909
3985
|
if (existsSync8(bundle))
|
|
3910
3986
|
return bundle;
|
|
3911
|
-
const url = `https://github.com/
|
|
3912
|
-
note(`Downloading LeCodesSDKResources.bundle (SDK ${
|
|
3987
|
+
const url = `https://github.com/${SDK_REPO}/releases/download/${sdkVersion}/LeCodesSDKResources.bundle.zip`;
|
|
3988
|
+
note(`Downloading LeCodesSDKResources.bundle (SDK ${sdkVersion})…`);
|
|
3913
3989
|
const res = await fetch(url);
|
|
3914
3990
|
if (!res.ok)
|
|
3915
3991
|
throw new CliError(`Resources bundle download failed (HTTP ${res.status}) — ${url}`);
|
|
@@ -3952,6 +4028,9 @@ var appSync = async (_args) => {
|
|
|
3952
4028
|
if (requestedVariant !== "auto" && !SDK_VARIANTS.includes(requestedVariant)) {
|
|
3953
4029
|
throw new CliError(`app.json ios.variant must be "auto" or one of ${SDK_VARIANTS.join(", ")} (got "${requestedVariant}").`);
|
|
3954
4030
|
}
|
|
4031
|
+
const linkedSdk = shellSdkVersion(iosDir);
|
|
4032
|
+
const sdkPin = process.env.LECODES_IOS_SDK ?? config.ios.sdk ?? linkedSdk ?? "latest";
|
|
4033
|
+
const sdkVersion = await resolveSdkVersion(sdkPin, linkedSdk);
|
|
3955
4034
|
const lockPath = join11(root, "plugins.lock.json");
|
|
3956
4035
|
const oldLock = readJson(lockPath)?.plugins ?? {};
|
|
3957
4036
|
const pluginSpecs = Object.entries(config.plugins ?? {});
|
|
@@ -3972,6 +4051,9 @@ var appSync = async (_args) => {
|
|
|
3972
4051
|
const dest = join11(sourcesDir, module);
|
|
3973
4052
|
rmSync(dest, { recursive: true, force: true });
|
|
3974
4053
|
cpSync(src, dest, { recursive: true });
|
|
4054
|
+
if (ios.minSdk && VERSION_RE.test(ios.minSdk) && cmpVersion(sdkVersion, ios.minSdk) < 0) {
|
|
4055
|
+
warn(`Plugin "${id}" needs lecodes-ios-sdk ${ios.minSdk} but the shell links ${sdkVersion} — run \`lecodes app update\` (or set app.json ios.sdk).`);
|
|
4056
|
+
}
|
|
3975
4057
|
for (const [key, value] of Object.entries(ios.infoPlist ?? {})) {
|
|
3976
4058
|
(infoValues[key] ??= []).push(value);
|
|
3977
4059
|
}
|
|
@@ -4003,9 +4085,11 @@ var appSync = async (_args) => {
|
|
|
4003
4085
|
full: "uses the 3D + 2D engines"
|
|
4004
4086
|
};
|
|
4005
4087
|
log(`${c.green("variant")} ${variant}${requestedVariant === "auto" ? ` (auto: ${WHY[detected]})` : " (forced by ios.variant)"}`);
|
|
4088
|
+
const sdkWhy = sdkPin === "latest" ? "latest" : config.ios.sdk ? "pinned by ios.sdk" : "from the shell — set ios.sdk or run `lecodes app update`";
|
|
4089
|
+
log(`${c.green("sdk")} lecodes-ios-sdk ${sdkVersion}${c.dim(` (${sdkWhy})`)}`);
|
|
4006
4090
|
const sdkProvided = new Set;
|
|
4007
4091
|
if (variant === "3d" || variant === "full") {
|
|
4008
|
-
const sdkResources = await ensureSdkResourcesBundle();
|
|
4092
|
+
const sdkResources = await ensureSdkResourcesBundle(sdkVersion);
|
|
4009
4093
|
cpSync(sdkResources, join11(resourcesDir, "LeCodesSDKResources.bundle"), { recursive: true });
|
|
4010
4094
|
for (const f2 of readdirSync4(sdkResources))
|
|
4011
4095
|
if (f2 !== "Info.plist")
|
|
@@ -4043,7 +4127,7 @@ var appSync = async (_args) => {
|
|
|
4043
4127
|
if (streamed.length > 0)
|
|
4044
4128
|
log(`${c.green("remote")} ${streamed.length} resource(s) stay on the server (resources.remote): ${streamed.map((r) => r.path).join(", ")}`);
|
|
4045
4129
|
writeFileSync7(join11(iosDir, "README.md"), IOS_README);
|
|
4046
|
-
writeFileSync7(join11(iosDir, "LeCodesRuntime", "Package.swift"), runtimePackageTemplate(
|
|
4130
|
+
writeFileSync7(join11(iosDir, "LeCodesRuntime", "Package.swift"), runtimePackageTemplate(sdkVersion, variant, runtimePlugins));
|
|
4047
4131
|
writeFileSync7(join11(sourcesDir, "LeCodesRuntime", "LeCodesRuntime.swift"), runtimeSwiftTemplate(runtimePlugins));
|
|
4048
4132
|
const mergedKeys = {};
|
|
4049
4133
|
for (const [key, values] of Object.entries(infoValues))
|
|
@@ -4058,7 +4142,7 @@ var appSync = async (_args) => {
|
|
|
4058
4142
|
const pbx = readFileSync10(pbxPath, "utf8").replace(/MARKETING_VERSION = [^;]+;/g, `MARKETING_VERSION = ${config.version ?? "1.0.0"};`).replace(/CURRENT_PROJECT_VERSION = [^;]+;/g, `CURRENT_PROJECT_VERSION = ${config.ios.buildNumber ?? 1};`);
|
|
4059
4143
|
writeFileSync7(pbxPath, pbx);
|
|
4060
4144
|
log("");
|
|
4061
|
-
success(`Shell synced: app.js + ${runtimePlugins.length} plugin(s), variant ${variant}, v${config.version ?? "1.0.0"} (${config.ios.buildNumber ?? 1}).`);
|
|
4145
|
+
success(`Shell synced: app.js + ${runtimePlugins.length} plugin(s), SDK ${sdkVersion}, variant ${variant}, v${config.version ?? "1.0.0"} (${config.ios.buildNumber ?? 1}).`);
|
|
4062
4146
|
note("Build & run from Xcode (open ios/App.xcodeproj) — user files in ios/App/*.swift were not touched.");
|
|
4063
4147
|
};
|
|
4064
4148
|
var appPlugin = async (args) => {
|
|
@@ -4147,6 +4231,95 @@ var requireShell = () => {
|
|
|
4147
4231
|
}
|
|
4148
4232
|
return { root, config };
|
|
4149
4233
|
};
|
|
4234
|
+
var latestPluginTag = (repo) => {
|
|
4235
|
+
try {
|
|
4236
|
+
return ensureRepo(repo, null, undefined).tag;
|
|
4237
|
+
} catch {
|
|
4238
|
+
return null;
|
|
4239
|
+
}
|
|
4240
|
+
};
|
|
4241
|
+
var appUpdate = async (args) => {
|
|
4242
|
+
const { root, config } = requireShell();
|
|
4243
|
+
const iosDir = join11(root, "ios");
|
|
4244
|
+
const checkOnly = flagBool(args, "check");
|
|
4245
|
+
const wanted = flagStr(args, "sdk");
|
|
4246
|
+
let changed = false;
|
|
4247
|
+
const pin = config.ios.sdk ?? shellSdkVersion(iosDir) ?? "latest";
|
|
4248
|
+
const current = pin === "latest" ? shellSdkVersion(iosDir) ?? SDK_MIN_VERSION : pin;
|
|
4249
|
+
let nextPin;
|
|
4250
|
+
if (wanted === "latest") {
|
|
4251
|
+
nextPin = "latest";
|
|
4252
|
+
} else if (wanted) {
|
|
4253
|
+
if (!VERSION_RE.test(wanted))
|
|
4254
|
+
throw new CliError(`--sdk must be "latest" or an exact x.y.z release (got "${wanted}").`);
|
|
4255
|
+
if (cmpVersion(wanted, SDK_MIN_VERSION) < 0)
|
|
4256
|
+
throw new CliError(`--sdk ${wanted} is older than the ${SDK_MIN_VERSION} these generated shells need.`);
|
|
4257
|
+
nextPin = wanted;
|
|
4258
|
+
} else {
|
|
4259
|
+
nextPin = await fetchLatestSdkVersion();
|
|
4260
|
+
}
|
|
4261
|
+
const nextVersion = nextPin === "latest" ? await fetchLatestSdkVersion() : nextPin;
|
|
4262
|
+
if (nextVersion === current && nextPin === pin) {
|
|
4263
|
+
log(`${c.green("sdk")} lecodes-ios-sdk ${current} ${c.dim("(already current)")}`);
|
|
4264
|
+
} else if (nextVersion === current) {
|
|
4265
|
+
log(`${c.green("sdk")} lecodes-ios-sdk ${current} → ios.sdk ${nextPin} ${c.dim("(same version, pin mode changed)")}`);
|
|
4266
|
+
changed = true;
|
|
4267
|
+
} else {
|
|
4268
|
+
const verb = cmpVersion(nextVersion, current) < 0 ? c.yellow("downgrade") : c.green("update");
|
|
4269
|
+
log(`${c.green("sdk")} lecodes-ios-sdk ${current} → ${c.bold(nextVersion)} ${c.dim(`(${verb})`)}`);
|
|
4270
|
+
changed = true;
|
|
4271
|
+
}
|
|
4272
|
+
if (!checkOnly && nextPin !== pin) {
|
|
4273
|
+
config.ios.sdk = nextPin;
|
|
4274
|
+
writeJson(join11(root, "app.json"), config);
|
|
4275
|
+
}
|
|
4276
|
+
const lockPath = join11(root, "plugins.lock.json");
|
|
4277
|
+
const lock = readJson(lockPath)?.plugins ?? {};
|
|
4278
|
+
const specs = Object.entries(config.plugins ?? {});
|
|
4279
|
+
if (specs.length > 0 && !flagBool(args, "no-plugins")) {
|
|
4280
|
+
const kept = {};
|
|
4281
|
+
for (const [id, spec] of specs) {
|
|
4282
|
+
const entry = lock[id];
|
|
4283
|
+
if (spec.startsWith("github:") && spec.includes("@")) {
|
|
4284
|
+
log(`${c.green("plugin")} ${id} ${c.dim(`pinned by app.json (${spec}) — not updated`)}`);
|
|
4285
|
+
if (entry)
|
|
4286
|
+
kept[id] = entry;
|
|
4287
|
+
continue;
|
|
4288
|
+
}
|
|
4289
|
+
const repo = spec.startsWith("github:") ? spec.slice("github:".length) : FIRST_PARTY_PLUGINS[id];
|
|
4290
|
+
const tag = repo ? latestPluginTag(repo) : null;
|
|
4291
|
+
if (!tag) {
|
|
4292
|
+
log(`${c.yellow("plugin")} ${id} ${c.dim("could not read its latest tag — left as is")}`);
|
|
4293
|
+
if (entry)
|
|
4294
|
+
kept[id] = entry;
|
|
4295
|
+
continue;
|
|
4296
|
+
}
|
|
4297
|
+
if (entry && entry.tag === tag) {
|
|
4298
|
+
log(`${c.green("plugin")} ${id} ${entry.tag} ${c.dim("(already current)")}`);
|
|
4299
|
+
kept[id] = entry;
|
|
4300
|
+
continue;
|
|
4301
|
+
}
|
|
4302
|
+
log(`${c.green("plugin")} ${id} ${entry?.tag ?? "—"} → ${c.bold(tag)}`);
|
|
4303
|
+
changed = true;
|
|
4304
|
+
}
|
|
4305
|
+
if (!checkOnly && existsSync8(lockPath))
|
|
4306
|
+
writeJson(lockPath, { plugins: kept });
|
|
4307
|
+
}
|
|
4308
|
+
log("");
|
|
4309
|
+
if (checkOnly) {
|
|
4310
|
+
note(changed ? "Run `lecodes app update` to apply." : "Everything is up to date.");
|
|
4311
|
+
return;
|
|
4312
|
+
}
|
|
4313
|
+
if (!changed) {
|
|
4314
|
+
success("Already up to date.");
|
|
4315
|
+
return;
|
|
4316
|
+
}
|
|
4317
|
+
if (flagBool(args, "no-sync")) {
|
|
4318
|
+
success("Pins updated — run `lecodes app sync` to apply them to the shell.");
|
|
4319
|
+
return;
|
|
4320
|
+
}
|
|
4321
|
+
await appSync({ _: [], flags: {} });
|
|
4322
|
+
};
|
|
4150
4323
|
var appDoctor = async (_args) => {
|
|
4151
4324
|
const { root, config } = requireShell();
|
|
4152
4325
|
let failed = 0;
|
|
@@ -4177,6 +4350,18 @@ var appDoctor = async (_args) => {
|
|
|
4177
4350
|
const hasResources = existsSync8(join11(root, "ios", "App", "Resources", "LeCodesSDKResources.bundle"));
|
|
4178
4351
|
check(pkg.includes(`"LeCodesSDK-${variant}"`) && (!needsResources || hasResources), `SDK variant ${variant}${needsResources ? " (+ filament resources)" : ""}`, "run `lecodes app sync`");
|
|
4179
4352
|
}
|
|
4353
|
+
const linked = shellSdkVersion(join11(root, "ios"));
|
|
4354
|
+
const pin = config.ios.sdk ?? "latest";
|
|
4355
|
+
check(!!linked, `SDK ${linked ?? "version unreadable"}`, "run `lecodes app sync`");
|
|
4356
|
+
if (linked && pin !== "latest") {
|
|
4357
|
+
check(linked === pin, `SDK matches app.json ios.sdk (${pin})`, "run `lecodes app sync`");
|
|
4358
|
+
}
|
|
4359
|
+
if (linked) {
|
|
4360
|
+
try {
|
|
4361
|
+
const latest = await fetchLatestSdkVersion();
|
|
4362
|
+
check(cmpVersion(linked, latest) >= 0, `SDK is the latest release (${latest})`, "run `lecodes app update`", false);
|
|
4363
|
+
} catch {}
|
|
4364
|
+
}
|
|
4180
4365
|
const lock = readJson(join11(root, "plugins.lock.json"))?.plugins ?? {};
|
|
4181
4366
|
const wanted = Object.keys(config.plugins ?? {});
|
|
4182
4367
|
const inSync = wanted.every((id) => lock[id]) && Object.keys(lock).every((id) => wanted.includes(id));
|
|
@@ -4303,6 +4488,8 @@ var app = async (args) => {
|
|
|
4303
4488
|
return appInit(args);
|
|
4304
4489
|
if (sub === "sync")
|
|
4305
4490
|
return appSync(args);
|
|
4491
|
+
if (sub === "update")
|
|
4492
|
+
return appUpdate(args);
|
|
4306
4493
|
if (sub === "plugin")
|
|
4307
4494
|
return appPlugin(args);
|
|
4308
4495
|
if (sub === "doctor")
|
|
@@ -4313,16 +4500,16 @@ var app = async (args) => {
|
|
|
4313
4500
|
return appUpload(args);
|
|
4314
4501
|
if (sub === "open")
|
|
4315
4502
|
return appOpen(args);
|
|
4316
|
-
throw new CliError("Usage: lecodes app <init ios | sync | open | plugin list|add|remove | doctor | build ios | upload ios> — see `lecodes help`.");
|
|
4503
|
+
throw new CliError("Usage: lecodes app <init ios | sync | update | open | plugin list|add|remove | doctor | build ios | upload ios> — see `lecodes help`.");
|
|
4317
4504
|
};
|
|
4318
4505
|
|
|
4319
4506
|
// src/commands/clone.ts
|
|
4320
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync7, readdirSync as
|
|
4507
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, readdirSync as readdirSync6 } from "node:fs";
|
|
4321
4508
|
import { resolve as resolve3 } from "node:path";
|
|
4322
4509
|
|
|
4323
4510
|
// src/types.ts
|
|
4324
|
-
import { dirname as dirname8, join as join12 } from "node:path";
|
|
4325
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "node:fs";
|
|
4511
|
+
import { dirname as dirname8, join as join12, relative as relative3, sep } from "node:path";
|
|
4512
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readdirSync as readdirSync5, readFileSync as readFileSync11, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync8 } from "node:fs";
|
|
4326
4513
|
var TSCONFIG = {
|
|
4327
4514
|
compilerOptions: {
|
|
4328
4515
|
target: "ES2020",
|
|
@@ -4339,17 +4526,60 @@ var TSCONFIG = {
|
|
|
4339
4526
|
include: [`${LECODES_DIR}/types/**/*.d.ts`, "**/*.ts", "**/*.js"],
|
|
4340
4527
|
exclude: ["node_modules"]
|
|
4341
4528
|
};
|
|
4529
|
+
var listBundleFiles = (dir, base = dir) => {
|
|
4530
|
+
if (!existsSync9(dir))
|
|
4531
|
+
return [];
|
|
4532
|
+
const out = [];
|
|
4533
|
+
for (const name of readdirSync5(dir)) {
|
|
4534
|
+
const path = join12(dir, name);
|
|
4535
|
+
if (statSync3(path).isDirectory())
|
|
4536
|
+
out.push(...listBundleFiles(path, base));
|
|
4537
|
+
else
|
|
4538
|
+
out.push(relative3(base, path).split(sep).join("/"));
|
|
4539
|
+
}
|
|
4540
|
+
return out;
|
|
4541
|
+
};
|
|
4542
|
+
var pruneEmptyDirs = (dir, root) => {
|
|
4543
|
+
if (!existsSync9(dir) || !statSync3(dir).isDirectory())
|
|
4544
|
+
return;
|
|
4545
|
+
for (const name of readdirSync5(dir))
|
|
4546
|
+
pruneEmptyDirs(join12(dir, name), root);
|
|
4547
|
+
if (dir !== root && readdirSync5(dir).length === 0)
|
|
4548
|
+
rmSync2(dir, { recursive: true });
|
|
4549
|
+
};
|
|
4342
4550
|
var writeTypeBundle = (root, bundle) => {
|
|
4343
4551
|
const typesDir = join12(root, LECODES_DIR, "types");
|
|
4552
|
+
const before = new Set(listBundleFiles(typesDir));
|
|
4553
|
+
const stats = { added: 0, updated: 0, removed: 0, total: Object.keys(bundle).length };
|
|
4344
4554
|
for (const [path, text] of Object.entries(bundle)) {
|
|
4345
4555
|
const dest = join12(typesDir, path);
|
|
4556
|
+
if (!before.has(path))
|
|
4557
|
+
stats.added++;
|
|
4558
|
+
else if (readFileSync11(dest, "utf8") !== text)
|
|
4559
|
+
stats.updated++;
|
|
4560
|
+
else
|
|
4561
|
+
continue;
|
|
4346
4562
|
mkdirSync6(dirname8(dest), { recursive: true });
|
|
4347
4563
|
writeFileSync8(dest, text);
|
|
4348
4564
|
}
|
|
4565
|
+
for (const path of before) {
|
|
4566
|
+
if (path in bundle)
|
|
4567
|
+
continue;
|
|
4568
|
+
rmSync2(join12(typesDir, path));
|
|
4569
|
+
stats.removed++;
|
|
4570
|
+
}
|
|
4571
|
+
pruneEmptyDirs(typesDir, typesDir);
|
|
4572
|
+
return stats;
|
|
4349
4573
|
};
|
|
4350
|
-
var
|
|
4351
|
-
|
|
4574
|
+
var bundleHash = (bundle) => sha256(Object.keys(bundle).sort().map((k) => `${k}\x00${bundle[k]}`).join("\x00"));
|
|
4575
|
+
var readProjectTypeBundle = (root) => {
|
|
4576
|
+
const typesDir = join12(root, LECODES_DIR, "types");
|
|
4577
|
+
const bundle = {};
|
|
4578
|
+
for (const path of listBundleFiles(typesDir))
|
|
4579
|
+
bundle[path] = readFileSync11(join12(typesDir, path), "utf8");
|
|
4580
|
+
return bundle;
|
|
4352
4581
|
};
|
|
4582
|
+
var materializeTypes = async (root, apiUrl) => writeTypeBundle(root, await getTypes(apiUrl));
|
|
4353
4583
|
var readLocalTypeBundle = () => {
|
|
4354
4584
|
const candidates = [
|
|
4355
4585
|
join12(distRoot, "runtime", "sdk-types.json"),
|
|
@@ -4394,7 +4624,7 @@ var clone = async (args) => {
|
|
|
4394
4624
|
const project = await getProject(apiUrl, token, uuid);
|
|
4395
4625
|
const dir = args._[1] ?? slug(project.name);
|
|
4396
4626
|
const root = resolve3(dir);
|
|
4397
|
-
if (existsSync10(root) &&
|
|
4627
|
+
if (existsSync10(root) && readdirSync6(root).length > 0) {
|
|
4398
4628
|
throw new CliError(`Directory "${dir}" already exists and is not empty.`);
|
|
4399
4629
|
}
|
|
4400
4630
|
mkdirSync7(root, { recursive: true });
|
|
@@ -4414,7 +4644,7 @@ var clone = async (args) => {
|
|
|
4414
4644
|
};
|
|
4415
4645
|
|
|
4416
4646
|
// src/commands/create.ts
|
|
4417
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readdirSync as
|
|
4647
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readdirSync as readdirSync7, writeFileSync as writeFileSync9 } from "node:fs";
|
|
4418
4648
|
import { join as join13, resolve as resolve4 } from "node:path";
|
|
4419
4649
|
|
|
4420
4650
|
// src/commands/projectTemplates.ts
|
|
@@ -4501,7 +4731,7 @@ var create = async (args) => {
|
|
|
4501
4731
|
throw new CliError("Usage: lecodes create <name> [directory]");
|
|
4502
4732
|
const dir = args._[1] ?? slug2(name);
|
|
4503
4733
|
const root = resolve4(dir);
|
|
4504
|
-
if (existsSync11(root) &&
|
|
4734
|
+
if (existsSync11(root) && readdirSync7(root).length > 0) {
|
|
4505
4735
|
throw new CliError(`Directory "${dir}" already exists and is not empty.`);
|
|
4506
4736
|
}
|
|
4507
4737
|
mkdirSync8(root, { recursive: true });
|
|
@@ -4520,7 +4750,7 @@ var create = async (args) => {
|
|
|
4520
4750
|
};
|
|
4521
4751
|
|
|
4522
4752
|
// src/commands/design.ts
|
|
4523
|
-
import { existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync14, readdirSync as
|
|
4753
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync14, readdirSync as readdirSync8, writeFileSync as writeFileSync11, appendFileSync } from "node:fs";
|
|
4524
4754
|
import { basename as basename6, join as join15 } from "node:path";
|
|
4525
4755
|
|
|
4526
4756
|
// src/compile/designCompile.ts
|
|
@@ -5507,7 +5737,7 @@ var listScreens = (designDir) => {
|
|
|
5507
5737
|
const dir = join15(designDir, "screens");
|
|
5508
5738
|
if (!existsSync13(dir))
|
|
5509
5739
|
return [];
|
|
5510
|
-
return
|
|
5740
|
+
return readdirSync8(dir).filter((f2) => f2.endsWith(".ts") && !f2.startsWith(".")).map((f2) => f2.slice(0, -3)).sort();
|
|
5511
5741
|
};
|
|
5512
5742
|
var readMetaSizes = (designDir) => {
|
|
5513
5743
|
try {
|
|
@@ -5876,7 +6106,7 @@ import { basename as basename7, join as join17 } from "node:path";
|
|
|
5876
6106
|
|
|
5877
6107
|
// src/dev/devServer.ts
|
|
5878
6108
|
import { createServer as createServer2 } from "node:http";
|
|
5879
|
-
import { createReadStream, existsSync as existsSync14, statSync as
|
|
6109
|
+
import { createReadStream, existsSync as existsSync14, statSync as statSync4 } from "node:fs";
|
|
5880
6110
|
import { join as join16 } from "node:path";
|
|
5881
6111
|
|
|
5882
6112
|
// src/dev/clientTemplates.ts
|
|
@@ -6247,7 +6477,7 @@ var startDevServer = async (options) => {
|
|
|
6247
6477
|
return sendText(res, 400, "text/plain", "Bad path");
|
|
6248
6478
|
}
|
|
6249
6479
|
const abs = join16(root, key.slice(1));
|
|
6250
|
-
if (!existsSync14(abs) || !
|
|
6480
|
+
if (!existsSync14(abs) || !statSync4(abs).isFile())
|
|
6251
6481
|
return sendText(res, 404, "text/plain", "Not found");
|
|
6252
6482
|
res.writeHead(200, { "content-type": contentType(key), "cache-control": "no-store" });
|
|
6253
6483
|
createReadStream(abs).pipe(res);
|
|
@@ -6706,6 +6936,19 @@ lecodes test # run tests/*.flow.json — the starter ships w
|
|
|
6706
6936
|
Scenario format + conventions: see [CLAUDE.md](./CLAUDE.md) (written for AI assistants, useful
|
|
6707
6937
|
for humans too).
|
|
6708
6938
|
|
|
6939
|
+
## Put it on the platform
|
|
6940
|
+
|
|
6941
|
+
This project is local-only. To give it a home on le.codes (web editor, sharing, OTA updates):
|
|
6942
|
+
|
|
6943
|
+
\`\`\`
|
|
6944
|
+
lecodes login # once per machine
|
|
6945
|
+
lecodes link # create a project from this folder and connect it
|
|
6946
|
+
lecodes push -m "initial" # upload it as the first checkpoint (--publish to go live)
|
|
6947
|
+
\`\`\`
|
|
6948
|
+
|
|
6949
|
+
Already made the project in the web editor? \`lecodes link --project <uuid>\` adopts it — it
|
|
6950
|
+
shows what your next push would change there before connecting anything.
|
|
6951
|
+
|
|
6709
6952
|
## Go native
|
|
6710
6953
|
|
|
6711
6954
|
\`\`\`
|
|
@@ -6722,7 +6965,8 @@ lecodes app open # open it in Xcode, build & run
|
|
|
6722
6965
|
\`tests/.artifacts/\`
|
|
6723
6966
|
- \`CLAUDE.md\` — guidance for AI assistants (the headless feedback loop + testability conventions)
|
|
6724
6967
|
- \`assets/\` — images/fonts the app references (created on demand)
|
|
6725
|
-
- \`tsconfig.json\` + \`.lecodes/types/\` — generated IDE types (rewritten by the CLI; don't edit)
|
|
6968
|
+
- \`tsconfig.json\` + \`.lecodes/types/\` — generated IDE types (rewritten by the CLI; don't edit).
|
|
6969
|
+
They come from the SDK bundled in the CLI; \`lecodes types\` refreshes them from the platform.
|
|
6726
6970
|
- \`.lecodesignore\` — files a future \`lecodes push\` would skip
|
|
6727
6971
|
|
|
6728
6972
|
Full command list: \`lecodes --help\`.
|
|
@@ -6763,6 +7007,231 @@ var init2 = async (args) => {
|
|
|
6763
7007
|
log(` ${c.bold(cd + "lecodes compile -o app.js")} bundle it for LeCodes Desktop`);
|
|
6764
7008
|
};
|
|
6765
7009
|
|
|
7010
|
+
// src/commands/link.ts
|
|
7011
|
+
import { existsSync as existsSync17 } from "node:fs";
|
|
7012
|
+
import { basename as basename11, join as join19, resolve as resolve6 } from "node:path";
|
|
7013
|
+
|
|
7014
|
+
// src/commands/push.ts
|
|
7015
|
+
import { basename as basename10 } from "node:path";
|
|
7016
|
+
import { readFileSync as readFileSync17 } from "node:fs";
|
|
7017
|
+
var push = async (args) => {
|
|
7018
|
+
const root = findProjectRoot(process.cwd());
|
|
7019
|
+
const manifest = readManifest(root);
|
|
7020
|
+
const config = loadConfig();
|
|
7021
|
+
const apiUrl = requireApiUrl(config);
|
|
7022
|
+
const token = requireToken(config);
|
|
7023
|
+
const publish = flagBool(args, "publish");
|
|
7024
|
+
const scan = scanProject(root, manifest);
|
|
7025
|
+
const diff2 = diffLocal(manifest, scan);
|
|
7026
|
+
if (!hasChanges(diff2)) {
|
|
7027
|
+
if (publish) {
|
|
7028
|
+
await publishProject(apiUrl, token, manifest.uuid);
|
|
7029
|
+
success("No file changes — published the current project state.");
|
|
7030
|
+
} else {
|
|
7031
|
+
info(c.dim("Working tree clean — nothing to push."));
|
|
7032
|
+
}
|
|
7033
|
+
return;
|
|
7034
|
+
}
|
|
7035
|
+
const force = flagBool(args, "force");
|
|
7036
|
+
if (!force && manifest.baseHeadId !== null) {
|
|
7037
|
+
const commits = await getCommits(apiUrl, token, manifest.uuid);
|
|
7038
|
+
if (commits.headId !== manifest.baseHeadId) {
|
|
7039
|
+
throw new CliError("The project changed on the server since your last sync. Run `lecodes diff` to compare, `lecodes pull` to take the server's version, or `lecodes push --force` to overwrite.");
|
|
7040
|
+
}
|
|
7041
|
+
if (commits.dirty) {
|
|
7042
|
+
const drift = serverPathDrift(await getProject(apiUrl, token, manifest.uuid), manifest);
|
|
7043
|
+
if (drift) {
|
|
7044
|
+
throw new CliError(`The project has uncommitted changes in the web editor your checkout doesn't have (${drift}). Run \`lecodes pull\` to take them, or \`lecodes push --force\` to overwrite the server.`);
|
|
7045
|
+
}
|
|
7046
|
+
}
|
|
7047
|
+
}
|
|
7048
|
+
if (force) {
|
|
7049
|
+
const project = await getProject(apiUrl, token, manifest.uuid);
|
|
7050
|
+
const serverDiff = computeServerDiff(project, scan);
|
|
7051
|
+
if (serverDiff.length) {
|
|
7052
|
+
info(c.yellow(`--force will overwrite the server. ${serverDiff.length} file${serverDiff.length === 1 ? "" : "s"} change:`));
|
|
7053
|
+
const label = { added: c.green(" create "), removed: c.red(" delete "), modified: c.yellow(" update ") };
|
|
7054
|
+
for (const e of serverDiff)
|
|
7055
|
+
log(label[e.status] + e.path + (e.binary ? c.dim(" (binary)") : ""));
|
|
7056
|
+
if (process.stdin.isTTY && !flagBool(args, "y", "yes")) {
|
|
7057
|
+
if (!await confirm("Overwrite the server with your local copy?", false)) {
|
|
7058
|
+
info(c.dim("Aborted."));
|
|
7059
|
+
return;
|
|
7060
|
+
}
|
|
7061
|
+
}
|
|
7062
|
+
}
|
|
7063
|
+
}
|
|
7064
|
+
const message = (flagStr(args, "m", "message") ?? await prompt("Checkpoint message")) || "Update from lecodes CLI";
|
|
7065
|
+
const sourceFor = (path) => manifest.files[path] ?? (diff2.moves.has(path) ? manifest.files[diff2.moves.get(path)] : undefined);
|
|
7066
|
+
const files = [];
|
|
7067
|
+
let uploaded = 0;
|
|
7068
|
+
for (const file of scan.files) {
|
|
7069
|
+
const source = sourceFor(file.path);
|
|
7070
|
+
const kind = source?.type ?? classifyKind(basename10(file.path));
|
|
7071
|
+
const entry = { path: file.path, type: kind };
|
|
7072
|
+
if (source)
|
|
7073
|
+
entry.assetId = source.id;
|
|
7074
|
+
if (isTextKind(kind)) {
|
|
7075
|
+
entry.text = readFileSync17(file.absPath, "utf8");
|
|
7076
|
+
} else if (!source || source.sha !== file.sha) {
|
|
7077
|
+
const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename10(file.path), readFileSync17(file.absPath));
|
|
7078
|
+
entry.blobFileId = blob.id;
|
|
7079
|
+
uploaded++;
|
|
7080
|
+
}
|
|
7081
|
+
files.push(entry);
|
|
7082
|
+
}
|
|
7083
|
+
const folders = scan.dirs.map((path) => ({ path }));
|
|
7084
|
+
if (uploaded > 0)
|
|
7085
|
+
info(c.dim(`Uploaded ${uploaded} binary file${uploaded === 1 ? "" : "s"}.`));
|
|
7086
|
+
const res = await sync(apiUrl, token, manifest.uuid, { message, files, folders, publish });
|
|
7087
|
+
if (!res.created) {
|
|
7088
|
+
info(c.dim("Server reported no changes."));
|
|
7089
|
+
if (publish)
|
|
7090
|
+
reportPublish(res);
|
|
7091
|
+
return;
|
|
7092
|
+
}
|
|
7093
|
+
const shaByPath = new Map(scan.files.map((f2) => [f2.path, f2.sha]));
|
|
7094
|
+
const newFiles = {};
|
|
7095
|
+
for (const node of res.nodes) {
|
|
7096
|
+
newFiles[node.path] = { id: node.id, type: node.type, sha: shaByPath.get(node.path) ?? "" };
|
|
7097
|
+
}
|
|
7098
|
+
writeManifest(root, { ...manifest, baseHeadId: res.headId, files: newFiles });
|
|
7099
|
+
const counts = [
|
|
7100
|
+
diff2.added.length - diff2.moves.size && `${diff2.added.length - diff2.moves.size} added`,
|
|
7101
|
+
diff2.modified.length && `${diff2.modified.length} modified`,
|
|
7102
|
+
diff2.moves.size && `${diff2.moves.size} moved`,
|
|
7103
|
+
diff2.deleted.length - diff2.moves.size && `${diff2.deleted.length - diff2.moves.size} deleted`
|
|
7104
|
+
].filter(Boolean).join(", ");
|
|
7105
|
+
success(`Pushed checkpoint${res.seq ? ` #${res.seq}` : ""}${counts ? ` (${counts})` : ""}: ${message}`);
|
|
7106
|
+
if (publish)
|
|
7107
|
+
reportPublish(res);
|
|
7108
|
+
};
|
|
7109
|
+
var serverPathDrift = (project, manifest) => {
|
|
7110
|
+
const pathById = buildPaths(project.assets);
|
|
7111
|
+
const serverPaths = new Set(project.assets.filter((a) => a.type !== "folder").map((a) => pathById.get(a.id)));
|
|
7112
|
+
const manifestPaths = new Set(Object.keys(manifest.files));
|
|
7113
|
+
const added = [...serverPaths].filter((p) => !manifestPaths.has(p));
|
|
7114
|
+
const removed = [...manifestPaths].filter((p) => !serverPaths.has(p));
|
|
7115
|
+
const describe = (label, paths) => `${paths.length} ${label} on the server: ${paths.slice(0, 3).join(", ")}${paths.length > 3 ? ", …" : ""}`;
|
|
7116
|
+
const parts = [
|
|
7117
|
+
added.length ? describe("added", added) : "",
|
|
7118
|
+
removed.length ? describe("removed", removed) : ""
|
|
7119
|
+
].filter(Boolean);
|
|
7120
|
+
return parts.length ? parts.join("; ") : null;
|
|
7121
|
+
};
|
|
7122
|
+
var reportPublish = (res) => {
|
|
7123
|
+
if (res.published)
|
|
7124
|
+
success("Published — the project is now live.");
|
|
7125
|
+
else
|
|
7126
|
+
warn(`Push landed, but publish failed: ${res.publishError ?? "unknown error"}`);
|
|
7127
|
+
};
|
|
7128
|
+
|
|
7129
|
+
// src/commands/link.ts
|
|
7130
|
+
var kindOf2 = (type) => type === "file" ? "resource" : type;
|
|
7131
|
+
var manifestFromServer = (project) => {
|
|
7132
|
+
const pathById = buildPaths(project.assets);
|
|
7133
|
+
const files = {};
|
|
7134
|
+
for (const asset of project.assets) {
|
|
7135
|
+
if (asset.type === "folder")
|
|
7136
|
+
continue;
|
|
7137
|
+
files[pathById.get(asset.id)] = {
|
|
7138
|
+
id: asset.id,
|
|
7139
|
+
type: kindOf2(asset.type),
|
|
7140
|
+
sha: asset.type === "file" ? "" : sha256(asset.text ?? "")
|
|
7141
|
+
};
|
|
7142
|
+
}
|
|
7143
|
+
return files;
|
|
7144
|
+
};
|
|
7145
|
+
var confirmAdoption = async (root, project, files, args) => {
|
|
7146
|
+
const count = Object.keys(files).length;
|
|
7147
|
+
if (count === 0) {
|
|
7148
|
+
info(c.dim(`Project ${c.bold(project.name)} is empty on the server — nothing to overwrite.`));
|
|
7149
|
+
return true;
|
|
7150
|
+
}
|
|
7151
|
+
const scan = scanProject(root, { files });
|
|
7152
|
+
const diff2 = computeServerDiff(project, scan);
|
|
7153
|
+
if (diff2.length === 0) {
|
|
7154
|
+
info(c.dim(`Project ${c.bold(project.name)} (${count} file(s)) already matches this folder.`));
|
|
7155
|
+
return true;
|
|
7156
|
+
}
|
|
7157
|
+
warn(`${c.bold(project.name)} already has ${count} file(s). Your next push would change the server:`);
|
|
7158
|
+
const label = { added: c.green(" create "), removed: c.red(" delete "), modified: c.yellow(" update ") };
|
|
7159
|
+
for (const e of diff2)
|
|
7160
|
+
log(label[e.status] + e.path + (e.binary ? c.dim(" (binary)") : ""));
|
|
7161
|
+
log("");
|
|
7162
|
+
note("`lecodes pull` after linking takes the server's version instead.");
|
|
7163
|
+
if (flagBool(args, "y", "yes") || flagBool(args, "force") || !process.stdin.isTTY)
|
|
7164
|
+
return true;
|
|
7165
|
+
return confirm("Link this folder to that project?", false);
|
|
7166
|
+
};
|
|
7167
|
+
var link = async (args) => {
|
|
7168
|
+
const config = loadConfig();
|
|
7169
|
+
const apiUrl = requireApiUrl(config);
|
|
7170
|
+
const token = requireToken(config);
|
|
7171
|
+
const dirArg = args._[0];
|
|
7172
|
+
const root = dirArg ? resolve6(dirArg) : process.cwd();
|
|
7173
|
+
if (!existsSync17(root))
|
|
7174
|
+
throw new CliError(`No such directory: ${root}`);
|
|
7175
|
+
if (existsSync17(join19(root, LECODES_DIR, "manifest.json")) && !flagBool(args, "force")) {
|
|
7176
|
+
throw new CliError("This folder is already linked (.lecodes/manifest.json exists) — use `lecodes push`, or `lecodes link --force` to re-link it to another project.");
|
|
7177
|
+
}
|
|
7178
|
+
let enclosing = null;
|
|
7179
|
+
try {
|
|
7180
|
+
enclosing = findProjectRoot(root);
|
|
7181
|
+
} catch {}
|
|
7182
|
+
if (enclosing && enclosing !== root) {
|
|
7183
|
+
throw new CliError(`This folder is inside the linked project at ${enclosing} — push from there instead.`);
|
|
7184
|
+
}
|
|
7185
|
+
const projectRef = flagStr(args, "project");
|
|
7186
|
+
let uuid;
|
|
7187
|
+
let name;
|
|
7188
|
+
let files;
|
|
7189
|
+
if (projectRef) {
|
|
7190
|
+
uuid = parseUuid(projectRef);
|
|
7191
|
+
const project = await getProject(apiUrl, token, uuid);
|
|
7192
|
+
if (project.role === "viewer") {
|
|
7193
|
+
throw new CliError(`You have view-only access to ${project.name} — linking would give you a checkout you can't push.`);
|
|
7194
|
+
}
|
|
7195
|
+
files = manifestFromServer(project);
|
|
7196
|
+
if (!await confirmAdoption(root, project, files, args)) {
|
|
7197
|
+
info(c.dim("Aborted — nothing was linked."));
|
|
7198
|
+
return;
|
|
7199
|
+
}
|
|
7200
|
+
name = project.name;
|
|
7201
|
+
} else {
|
|
7202
|
+
name = flagStr(args, "name") ?? basename11(root);
|
|
7203
|
+
info(c.dim(`Creating project ${c.bold(name)}…`));
|
|
7204
|
+
uuid = (await createProject(apiUrl, token, name, flagStr(args, "team"))).uuid;
|
|
7205
|
+
files = {};
|
|
7206
|
+
}
|
|
7207
|
+
const commits = await getCommits(apiUrl, token, uuid);
|
|
7208
|
+
writeManifest(root, { uuid, apiUrl, name, baseHeadId: commits.headId, files });
|
|
7209
|
+
log(`${c.green("linked")} ${c.bold(name)} ${c.dim(uuid)}`);
|
|
7210
|
+
if (!flagBool(args, "no-types")) {
|
|
7211
|
+
await materializeTypes(root, apiUrl);
|
|
7212
|
+
writeTsconfig(root);
|
|
7213
|
+
log(`${c.green("written")} ${LECODES_DIR}/types/ + tsconfig.json ${c.dim("(from the server)")}`);
|
|
7214
|
+
}
|
|
7215
|
+
writeDefaultIgnore(root);
|
|
7216
|
+
if (flagBool(args, "push")) {
|
|
7217
|
+
log("");
|
|
7218
|
+
if (root !== process.cwd())
|
|
7219
|
+
process.chdir(root);
|
|
7220
|
+
await push(args);
|
|
7221
|
+
return;
|
|
7222
|
+
}
|
|
7223
|
+
const pending = scanProject(root, { files }).files.filter((f2) => {
|
|
7224
|
+
const known = files[f2.path];
|
|
7225
|
+
return !known || known.sha !== f2.sha;
|
|
7226
|
+
}).length;
|
|
7227
|
+
log("");
|
|
7228
|
+
success(`Connected to ${c.bold(name)}.`);
|
|
7229
|
+
const cd = dirArg ? `cd ${dirArg} && ` : "";
|
|
7230
|
+
log(` ${c.bold(cd + "lecodes status")} ${pending} file(s) would go up`);
|
|
7231
|
+
log(` ${c.bold(cd + 'lecodes push -m "initial"')} push them as the first checkpoint`);
|
|
7232
|
+
log(` ${c.dim("(add --publish to deploy it live in the same step)")}`);
|
|
7233
|
+
};
|
|
7234
|
+
|
|
6766
7235
|
// src/commands/login.ts
|
|
6767
7236
|
var login = async (args) => {
|
|
6768
7237
|
const config = loadConfig();
|
|
@@ -6775,8 +7244,8 @@ var login = async (args) => {
|
|
|
6775
7244
|
};
|
|
6776
7245
|
|
|
6777
7246
|
// src/commands/render.ts
|
|
6778
|
-
import { mkdirSync as mkdirSync13, readFileSync as
|
|
6779
|
-
import { dirname as dirname11, resolve as
|
|
7247
|
+
import { mkdirSync as mkdirSync13, readFileSync as readFileSync18, writeFileSync as writeFileSync14 } from "node:fs";
|
|
7248
|
+
import { dirname as dirname11, resolve as resolve7 } from "node:path";
|
|
6780
7249
|
|
|
6781
7250
|
// src/compile/headlessBundle.ts
|
|
6782
7251
|
var LOCAL_ASSET_SCHEME2 = "lecodesfile://";
|
|
@@ -6877,7 +7346,7 @@ var requireRenderer = async () => {
|
|
|
6877
7346
|
};
|
|
6878
7347
|
var fileScenarioIO = (dir) => ({
|
|
6879
7348
|
write: (relPath, data) => {
|
|
6880
|
-
const abs =
|
|
7349
|
+
const abs = resolve7(dir, relPath);
|
|
6881
7350
|
mkdirSync13(dirname11(abs), { recursive: true });
|
|
6882
7351
|
writeFileSync14(abs, data);
|
|
6883
7352
|
}
|
|
@@ -6887,9 +7356,9 @@ var resolveFixtures = (value, baseDir) => {
|
|
|
6887
7356
|
return;
|
|
6888
7357
|
let map = value;
|
|
6889
7358
|
if (typeof value === "string") {
|
|
6890
|
-
const abs =
|
|
7359
|
+
const abs = resolve7(baseDir, value);
|
|
6891
7360
|
try {
|
|
6892
|
-
map = JSON.parse(
|
|
7361
|
+
map = JSON.parse(readFileSync18(abs, "utf8"));
|
|
6893
7362
|
} catch (e) {
|
|
6894
7363
|
throw new CliError(`Can't read fixtures "${value}": ${e instanceof Error ? e.message : String(e)}`);
|
|
6895
7364
|
}
|
|
@@ -6902,7 +7371,7 @@ var resolveFixtures = (value, baseDir) => {
|
|
|
6902
7371
|
var readScenarioFile = (path) => {
|
|
6903
7372
|
let raw;
|
|
6904
7373
|
try {
|
|
6905
|
-
raw = JSON.parse(
|
|
7374
|
+
raw = JSON.parse(readFileSync18(resolve7(path), "utf8"));
|
|
6906
7375
|
} catch (e) {
|
|
6907
7376
|
throw new CliError(`Can't read scenario "${path}": ${e instanceof Error ? e.message : String(e)}`);
|
|
6908
7377
|
}
|
|
@@ -6932,7 +7401,7 @@ var render = async (args) => {
|
|
|
6932
7401
|
const settleStr = flagStr(args, "settle") ?? (scenario?.settle !== undefined ? String(scenario.settle) : undefined);
|
|
6933
7402
|
const timeStr = flagStr(args, "time");
|
|
6934
7403
|
const onConsole = flagBool(args, "logs") ? (level, text2) => logErr(`${c.dim(`[${level}]`)} ${text2}`) : undefined;
|
|
6935
|
-
const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname11(
|
|
7404
|
+
const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname11(resolve7(scriptFlag)) : ".");
|
|
6936
7405
|
const flagFixtures = resolveFixtures(flagStr(args, "fixtures"), ".");
|
|
6937
7406
|
const fixtures = envelopeFixtures || flagFixtures ? { ...envelopeFixtures, ...flagFixtures } : undefined;
|
|
6938
7407
|
if (fixtures) {
|
|
@@ -6971,7 +7440,7 @@ var render = async (args) => {
|
|
|
6971
7440
|
${c.red(st.error ?? "failed")}`);
|
|
6972
7441
|
}
|
|
6973
7442
|
if (!result2.ok && result2.failureArtifacts.length > 0) {
|
|
6974
|
-
logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p) =>
|
|
7443
|
+
logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p) => resolve7(outDir, p)).join(", ")}`));
|
|
6975
7444
|
}
|
|
6976
7445
|
logErr(result2.ok ? c.green(`✓ ${result2.steps.length}/${steps.length} steps passed`) : c.red(`✗ failed at step ${result2.steps.length} of ${steps.length}`));
|
|
6977
7446
|
return done((cb) => cb(), result2.ok ? 0 : 1);
|
|
@@ -6993,7 +7462,7 @@ var render = async (args) => {
|
|
|
6993
7462
|
if (isNullSink(pngFlag)) {
|
|
6994
7463
|
note(`Discarded PNG (${pngFlag})`);
|
|
6995
7464
|
} else {
|
|
6996
|
-
writeFileSync14(
|
|
7465
|
+
writeFileSync14(resolve7(pngFlag), png);
|
|
6997
7466
|
note(`Wrote ${pngFlag} (${png.length} bytes, ${width}x${height})`);
|
|
6998
7467
|
}
|
|
6999
7468
|
return done((cb) => cb());
|
|
@@ -7008,7 +7477,7 @@ var render = async (args) => {
|
|
|
7008
7477
|
return done((cb) => cb());
|
|
7009
7478
|
}
|
|
7010
7479
|
if (out) {
|
|
7011
|
-
writeFileSync14(
|
|
7480
|
+
writeFileSync14(resolve7(out), result.text);
|
|
7012
7481
|
note(`Wrote ${out} (${result.text.length} bytes)`);
|
|
7013
7482
|
return done((cb) => cb());
|
|
7014
7483
|
}
|
|
@@ -7019,8 +7488,8 @@ var render = async (args) => {
|
|
|
7019
7488
|
};
|
|
7020
7489
|
|
|
7021
7490
|
// src/commands/pull.ts
|
|
7022
|
-
import { existsSync as
|
|
7023
|
-
import { dirname as dirname12, join as
|
|
7491
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync14, rmSync as rmSync3, writeFileSync as writeFileSync15 } from "node:fs";
|
|
7492
|
+
import { dirname as dirname12, join as join20 } from "node:path";
|
|
7024
7493
|
var pull = async (args) => {
|
|
7025
7494
|
const root = findProjectRoot(process.cwd());
|
|
7026
7495
|
const manifest = readManifest(root);
|
|
@@ -7040,9 +7509,9 @@ var pull = async (args) => {
|
|
|
7040
7509
|
const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a) => a.id === id)?.type !== "folder").map(([, path]) => path));
|
|
7041
7510
|
for (const path of Object.keys(manifest.files)) {
|
|
7042
7511
|
if (!remotePaths.has(path)) {
|
|
7043
|
-
const abs =
|
|
7044
|
-
if (
|
|
7045
|
-
|
|
7512
|
+
const abs = join20(root, ...path.replace(/^\//, "").split("/"));
|
|
7513
|
+
if (existsSync18(abs))
|
|
7514
|
+
rmSync3(abs);
|
|
7046
7515
|
}
|
|
7047
7516
|
}
|
|
7048
7517
|
const files = await materializeAssets(root, project, apiUrl, token, progress);
|
|
@@ -7055,7 +7524,7 @@ var pull = async (args) => {
|
|
|
7055
7524
|
writeManifest(root, { ...manifest, name: project.name, baseHeadId: commits.headId, files });
|
|
7056
7525
|
success(`Pulled latest into ${c.bold(manifest.name)}.`);
|
|
7057
7526
|
};
|
|
7058
|
-
var
|
|
7527
|
+
var kindOf3 = (type) => type === "file" ? "resource" : type;
|
|
7059
7528
|
var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
7060
7529
|
const pathById = buildPaths(project.assets);
|
|
7061
7530
|
const byPath = new Map;
|
|
@@ -7079,7 +7548,7 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
|
7079
7548
|
const files = { ...manifest.files };
|
|
7080
7549
|
for (const asset of targets) {
|
|
7081
7550
|
const key = pathById.get(asset.id);
|
|
7082
|
-
const abs =
|
|
7551
|
+
const abs = join20(root, ...key.replace(/^\//, "").split("/"));
|
|
7083
7552
|
mkdirSync14(dirname12(abs), { recursive: true });
|
|
7084
7553
|
let sha;
|
|
7085
7554
|
if (asset.type === "file") {
|
|
@@ -7093,7 +7562,7 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
|
7093
7562
|
writeFileSync15(abs, text);
|
|
7094
7563
|
sha = sha256(text);
|
|
7095
7564
|
}
|
|
7096
|
-
files[key] = { id: asset.id, type:
|
|
7565
|
+
files[key] = { id: asset.id, type: kindOf3(asset.type), sha };
|
|
7097
7566
|
info(c.green(` pulled ${key}`));
|
|
7098
7567
|
}
|
|
7099
7568
|
writeManifest(root, { ...manifest, files });
|
|
@@ -7101,137 +7570,22 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
|
7101
7570
|
info(c.dim("Local edits to other files are untouched. Run `lecodes diff` to review, then `lecodes push --force`."));
|
|
7102
7571
|
};
|
|
7103
7572
|
|
|
7104
|
-
// src/commands/push.ts
|
|
7105
|
-
import { basename as basename10 } from "node:path";
|
|
7106
|
-
import { readFileSync as readFileSync18 } from "node:fs";
|
|
7107
|
-
var push = async (args) => {
|
|
7108
|
-
const root = findProjectRoot(process.cwd());
|
|
7109
|
-
const manifest = readManifest(root);
|
|
7110
|
-
const config = loadConfig();
|
|
7111
|
-
const apiUrl = requireApiUrl(config);
|
|
7112
|
-
const token = requireToken(config);
|
|
7113
|
-
const publish = flagBool(args, "publish");
|
|
7114
|
-
const scan = scanProject(root, manifest);
|
|
7115
|
-
const diff2 = diffLocal(manifest, scan);
|
|
7116
|
-
if (!hasChanges(diff2)) {
|
|
7117
|
-
if (publish) {
|
|
7118
|
-
await publishProject(apiUrl, token, manifest.uuid);
|
|
7119
|
-
success("No file changes — published the current project state.");
|
|
7120
|
-
} else {
|
|
7121
|
-
info(c.dim("Working tree clean — nothing to push."));
|
|
7122
|
-
}
|
|
7123
|
-
return;
|
|
7124
|
-
}
|
|
7125
|
-
const force = flagBool(args, "force");
|
|
7126
|
-
if (!force && manifest.baseHeadId !== null) {
|
|
7127
|
-
const commits = await getCommits(apiUrl, token, manifest.uuid);
|
|
7128
|
-
if (commits.headId !== manifest.baseHeadId) {
|
|
7129
|
-
throw new CliError("The project changed on the server since your last sync. Run `lecodes diff` to compare, `lecodes pull` to take the server's version, or `lecodes push --force` to overwrite.");
|
|
7130
|
-
}
|
|
7131
|
-
if (commits.dirty) {
|
|
7132
|
-
const drift = serverPathDrift(await getProject(apiUrl, token, manifest.uuid), manifest);
|
|
7133
|
-
if (drift) {
|
|
7134
|
-
throw new CliError(`The project has uncommitted changes in the web editor your checkout doesn't have (${drift}). Run \`lecodes pull\` to take them, or \`lecodes push --force\` to overwrite the server.`);
|
|
7135
|
-
}
|
|
7136
|
-
}
|
|
7137
|
-
}
|
|
7138
|
-
if (force) {
|
|
7139
|
-
const project = await getProject(apiUrl, token, manifest.uuid);
|
|
7140
|
-
const serverDiff = computeServerDiff(project, scan);
|
|
7141
|
-
if (serverDiff.length) {
|
|
7142
|
-
info(c.yellow(`--force will overwrite the server. ${serverDiff.length} file${serverDiff.length === 1 ? "" : "s"} change:`));
|
|
7143
|
-
const label = { added: c.green(" create "), removed: c.red(" delete "), modified: c.yellow(" update ") };
|
|
7144
|
-
for (const e of serverDiff)
|
|
7145
|
-
log(label[e.status] + e.path + (e.binary ? c.dim(" (binary)") : ""));
|
|
7146
|
-
if (process.stdin.isTTY && !flagBool(args, "y", "yes")) {
|
|
7147
|
-
if (!await confirm("Overwrite the server with your local copy?", false)) {
|
|
7148
|
-
info(c.dim("Aborted."));
|
|
7149
|
-
return;
|
|
7150
|
-
}
|
|
7151
|
-
}
|
|
7152
|
-
}
|
|
7153
|
-
}
|
|
7154
|
-
const message = (flagStr(args, "m", "message") ?? await prompt("Checkpoint message")) || "Update from lecodes CLI";
|
|
7155
|
-
const sourceFor = (path) => manifest.files[path] ?? (diff2.moves.has(path) ? manifest.files[diff2.moves.get(path)] : undefined);
|
|
7156
|
-
const files = [];
|
|
7157
|
-
let uploaded = 0;
|
|
7158
|
-
for (const file of scan.files) {
|
|
7159
|
-
const source = sourceFor(file.path);
|
|
7160
|
-
const kind = source?.type ?? classifyKind(basename10(file.path));
|
|
7161
|
-
const entry = { path: file.path, type: kind };
|
|
7162
|
-
if (source)
|
|
7163
|
-
entry.assetId = source.id;
|
|
7164
|
-
if (isTextKind(kind)) {
|
|
7165
|
-
entry.text = readFileSync18(file.absPath, "utf8");
|
|
7166
|
-
} else if (!source || source.sha !== file.sha) {
|
|
7167
|
-
const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename10(file.path), readFileSync18(file.absPath));
|
|
7168
|
-
entry.blobFileId = blob.id;
|
|
7169
|
-
uploaded++;
|
|
7170
|
-
}
|
|
7171
|
-
files.push(entry);
|
|
7172
|
-
}
|
|
7173
|
-
const folders = scan.dirs.map((path) => ({ path }));
|
|
7174
|
-
if (uploaded > 0)
|
|
7175
|
-
info(c.dim(`Uploaded ${uploaded} binary file${uploaded === 1 ? "" : "s"}.`));
|
|
7176
|
-
const res = await sync(apiUrl, token, manifest.uuid, { message, files, folders, publish });
|
|
7177
|
-
if (!res.created) {
|
|
7178
|
-
info(c.dim("Server reported no changes."));
|
|
7179
|
-
if (publish)
|
|
7180
|
-
reportPublish(res);
|
|
7181
|
-
return;
|
|
7182
|
-
}
|
|
7183
|
-
const shaByPath = new Map(scan.files.map((f2) => [f2.path, f2.sha]));
|
|
7184
|
-
const newFiles = {};
|
|
7185
|
-
for (const node of res.nodes) {
|
|
7186
|
-
newFiles[node.path] = { id: node.id, type: node.type, sha: shaByPath.get(node.path) ?? "" };
|
|
7187
|
-
}
|
|
7188
|
-
writeManifest(root, { ...manifest, baseHeadId: res.headId, files: newFiles });
|
|
7189
|
-
const counts = [
|
|
7190
|
-
diff2.added.length - diff2.moves.size && `${diff2.added.length - diff2.moves.size} added`,
|
|
7191
|
-
diff2.modified.length && `${diff2.modified.length} modified`,
|
|
7192
|
-
diff2.moves.size && `${diff2.moves.size} moved`,
|
|
7193
|
-
diff2.deleted.length - diff2.moves.size && `${diff2.deleted.length - diff2.moves.size} deleted`
|
|
7194
|
-
].filter(Boolean).join(", ");
|
|
7195
|
-
success(`Pushed checkpoint${res.seq ? ` #${res.seq}` : ""}${counts ? ` (${counts})` : ""}: ${message}`);
|
|
7196
|
-
if (publish)
|
|
7197
|
-
reportPublish(res);
|
|
7198
|
-
};
|
|
7199
|
-
var serverPathDrift = (project, manifest) => {
|
|
7200
|
-
const pathById = buildPaths(project.assets);
|
|
7201
|
-
const serverPaths = new Set(project.assets.filter((a) => a.type !== "folder").map((a) => pathById.get(a.id)));
|
|
7202
|
-
const manifestPaths = new Set(Object.keys(manifest.files));
|
|
7203
|
-
const added = [...serverPaths].filter((p) => !manifestPaths.has(p));
|
|
7204
|
-
const removed = [...manifestPaths].filter((p) => !serverPaths.has(p));
|
|
7205
|
-
const describe = (label, paths) => `${paths.length} ${label} on the server: ${paths.slice(0, 3).join(", ")}${paths.length > 3 ? ", …" : ""}`;
|
|
7206
|
-
const parts = [
|
|
7207
|
-
added.length ? describe("added", added) : "",
|
|
7208
|
-
removed.length ? describe("removed", removed) : ""
|
|
7209
|
-
].filter(Boolean);
|
|
7210
|
-
return parts.length ? parts.join("; ") : null;
|
|
7211
|
-
};
|
|
7212
|
-
var reportPublish = (res) => {
|
|
7213
|
-
if (res.published)
|
|
7214
|
-
success("Published — the project is now live.");
|
|
7215
|
-
else
|
|
7216
|
-
warn(`Push landed, but publish failed: ${res.publishError ?? "unknown error"}`);
|
|
7217
|
-
};
|
|
7218
|
-
|
|
7219
7573
|
// src/commands/scene.ts
|
|
7220
|
-
import { existsSync as
|
|
7221
|
-
import { basename as
|
|
7574
|
+
import { existsSync as existsSync20, readFileSync as readFileSync20, readdirSync as readdirSync9, statSync as statSync5, writeFileSync as writeFileSync16 } from "node:fs";
|
|
7575
|
+
import { basename as basename12, dirname as dirname13, join as join22, resolve as resolve8 } from "node:path";
|
|
7222
7576
|
|
|
7223
7577
|
// src/compile/sceneCompile.ts
|
|
7224
|
-
import { existsSync as
|
|
7225
|
-
import { join as
|
|
7578
|
+
import { existsSync as existsSync19, readFileSync as readFileSync19 } from "node:fs";
|
|
7579
|
+
import { join as join21 } from "node:path";
|
|
7226
7580
|
var TEXT_EXT = /\.(ts|js|json|svg|txt|md)$/i;
|
|
7227
7581
|
var rel = (key) => key.replace(/^\//, "");
|
|
7228
7582
|
var resolveHarness = (loadSceneHarness2) => {
|
|
7229
|
-
const inRepo =
|
|
7583
|
+
const inRepo = join21(distRoot, "..", "projects", "src", "scene-editor");
|
|
7230
7584
|
try {
|
|
7231
|
-
return loadSceneHarness2(
|
|
7585
|
+
return loadSceneHarness2(existsSync19(inRepo) ? inRepo : undefined);
|
|
7232
7586
|
} catch {
|
|
7233
|
-
const vendored =
|
|
7234
|
-
if (
|
|
7587
|
+
const vendored = join21(distRoot, "runtime", "scene-harness.json");
|
|
7588
|
+
if (existsSync19(vendored))
|
|
7235
7589
|
return JSON.parse(readFileSync19(vendored, "utf8"));
|
|
7236
7590
|
throw new CliError("scene-editor harness sources not found — reinstall the CLI or set LECODES_SCENE_HARNESS_DIR.");
|
|
7237
7591
|
}
|
|
@@ -7284,22 +7638,22 @@ var getContext2 = (args) => {
|
|
|
7284
7638
|
manifest = null;
|
|
7285
7639
|
const pathArg = args._[0];
|
|
7286
7640
|
if (pathArg) {
|
|
7287
|
-
const abs =
|
|
7288
|
-
if (
|
|
7641
|
+
const abs = resolve8(process.cwd(), pathArg);
|
|
7642
|
+
if (existsSync20(abs) && statSync5(abs).isDirectory())
|
|
7289
7643
|
root = abs;
|
|
7290
|
-
else if (abs.endsWith(".scene.ts") &&
|
|
7644
|
+
else if (abs.endsWith(".scene.ts") && existsSync20(abs))
|
|
7291
7645
|
root = dirname13(abs);
|
|
7292
7646
|
else
|
|
7293
7647
|
throw new CliError(`No such folder or .scene.ts: ${pathArg}`);
|
|
7294
7648
|
}
|
|
7295
7649
|
}
|
|
7296
|
-
return { root, manifest, name: manifest?.name ??
|
|
7650
|
+
return { root, manifest, name: manifest?.name ?? basename12(root) };
|
|
7297
7651
|
};
|
|
7298
7652
|
var writeMcpConfig2 = (root, port) => {
|
|
7299
|
-
const path =
|
|
7653
|
+
const path = join22(root, ".mcp.json");
|
|
7300
7654
|
const entry = { type: "http", url: `http://localhost:${port}/mcp` };
|
|
7301
7655
|
let config = {};
|
|
7302
|
-
if (
|
|
7656
|
+
if (existsSync20(path)) {
|
|
7303
7657
|
try {
|
|
7304
7658
|
config = JSON.parse(readFileSync20(path, "utf8"));
|
|
7305
7659
|
} catch {
|
|
@@ -7320,11 +7674,11 @@ var writeMcpConfig2 = (root, port) => {
|
|
|
7320
7674
|
};
|
|
7321
7675
|
var countScenes = (dir) => {
|
|
7322
7676
|
let n = 0;
|
|
7323
|
-
for (const name of
|
|
7677
|
+
for (const name of readdirSync9(dir)) {
|
|
7324
7678
|
if (name === "node_modules" || name.startsWith("."))
|
|
7325
7679
|
continue;
|
|
7326
|
-
const full =
|
|
7327
|
-
if (
|
|
7680
|
+
const full = join22(dir, name);
|
|
7681
|
+
if (statSync5(full).isDirectory())
|
|
7328
7682
|
n += countScenes(full);
|
|
7329
7683
|
else if (name.endsWith(".scene.ts"))
|
|
7330
7684
|
n++;
|
|
@@ -7394,8 +7748,8 @@ var status = async (_args) => {
|
|
|
7394
7748
|
};
|
|
7395
7749
|
|
|
7396
7750
|
// src/commands/test.ts
|
|
7397
|
-
import { existsSync as
|
|
7398
|
-
import { dirname as dirname14, resolve as
|
|
7751
|
+
import { existsSync as existsSync21, readFileSync as readFileSync21, readdirSync as readdirSync10 } from "node:fs";
|
|
7752
|
+
import { dirname as dirname14, resolve as resolve9 } from "node:path";
|
|
7399
7753
|
var deriveDesignFlows = (meta, entryScreen) => {
|
|
7400
7754
|
const edges = meta.edges ?? [];
|
|
7401
7755
|
const isState = (id) => id.includes("@");
|
|
@@ -7481,34 +7835,34 @@ var readFlowFile = (abs, displayName) => {
|
|
|
7481
7835
|
var test = async (args) => {
|
|
7482
7836
|
const { js, warnings, useLocalAssets, root } = await compileHeadlessBundle(args);
|
|
7483
7837
|
const renderer = await requireRenderer();
|
|
7484
|
-
const testsDir =
|
|
7485
|
-
const designMetaPath =
|
|
7838
|
+
const testsDir = resolve9(root, flagStr(args, "dir") ?? "tests");
|
|
7839
|
+
const designMetaPath = resolve9(root, flagStr(args, "design-dir") ?? "design", "meta.json");
|
|
7486
7840
|
const asJson = flagBool(args, "json");
|
|
7487
7841
|
const out = asJson ? logErr : log;
|
|
7488
7842
|
const cases = [];
|
|
7489
7843
|
if (args._.length > 0) {
|
|
7490
7844
|
for (const given of args._) {
|
|
7491
|
-
const candidates = [
|
|
7492
|
-
const abs = candidates.find((p) =>
|
|
7845
|
+
const candidates = [resolve9(given), resolve9(testsDir, given), resolve9(testsDir, `${given}.flow.json`)];
|
|
7846
|
+
const abs = candidates.find((p) => existsSync21(p));
|
|
7493
7847
|
if (!abs)
|
|
7494
7848
|
throw new CliError(`No such test: "${given}" (looked in ${testsDir}).`);
|
|
7495
7849
|
cases.push(readFlowFile(abs, given));
|
|
7496
7850
|
}
|
|
7497
|
-
} else if (
|
|
7498
|
-
for (const f2 of
|
|
7499
|
-
cases.push(readFlowFile(
|
|
7851
|
+
} else if (existsSync21(testsDir)) {
|
|
7852
|
+
for (const f2 of readdirSync10(testsDir).filter((f3) => f3.endsWith(".flow.json")).sort()) {
|
|
7853
|
+
cases.push(readFlowFile(resolve9(testsDir, f2), f2));
|
|
7500
7854
|
}
|
|
7501
7855
|
}
|
|
7502
7856
|
const fixturesFlag = flagStr(args, "fixtures");
|
|
7503
|
-
const defaultPack =
|
|
7504
|
-
const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") :
|
|
7857
|
+
const defaultPack = resolve9(testsDir, "fixtures.json");
|
|
7858
|
+
const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") : existsSync21(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
|
|
7505
7859
|
const flowsOnly = flagBool(args, "flows");
|
|
7506
7860
|
const wantFlows = !flagBool(args, "no-flows") && args._.length === 0;
|
|
7507
7861
|
if (flowsOnly)
|
|
7508
7862
|
cases.length = 0;
|
|
7509
7863
|
let skipped = [];
|
|
7510
7864
|
let entryScreen = null;
|
|
7511
|
-
if ((wantFlows || flowsOnly) &&
|
|
7865
|
+
if ((wantFlows || flowsOnly) && existsSync21(designMetaPath)) {
|
|
7512
7866
|
let meta;
|
|
7513
7867
|
try {
|
|
7514
7868
|
meta = JSON.parse(readFileSync21(designMetaPath, "utf8"));
|
|
@@ -7543,7 +7897,7 @@ var test = async (args) => {
|
|
|
7543
7897
|
}, timeoutMs);
|
|
7544
7898
|
killer.unref?.();
|
|
7545
7899
|
const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
|
|
7546
|
-
const artifactsRoot =
|
|
7900
|
+
const artifactsRoot = resolve9(root, flagStr(args, "artifacts") ?? resolve9(testsDir, ".artifacts"));
|
|
7547
7901
|
const results = [];
|
|
7548
7902
|
for (const kase of cases) {
|
|
7549
7903
|
const flagSettle = flagStr(args, "settle");
|
|
@@ -7561,14 +7915,14 @@ var test = async (args) => {
|
|
|
7561
7915
|
onConsole
|
|
7562
7916
|
});
|
|
7563
7917
|
try {
|
|
7564
|
-
const io = fileScenarioIO(
|
|
7918
|
+
const io = fileScenarioIO(resolve9(artifactsRoot, slug3(kase.name)));
|
|
7565
7919
|
const outcome = await renderer.runScenario(session, steps, io);
|
|
7566
7920
|
result = {
|
|
7567
7921
|
name: kase.name,
|
|
7568
7922
|
kind: kase.kind,
|
|
7569
7923
|
ok: outcome.ok,
|
|
7570
7924
|
steps: outcome.steps.map((s) => ({ desc: s.desc, ok: s.ok, error: s.error })),
|
|
7571
|
-
artifacts: outcome.failureArtifacts.map((p) =>
|
|
7925
|
+
artifacts: outcome.failureArtifacts.map((p) => resolve9(artifactsRoot, slug3(kase.name), p))
|
|
7572
7926
|
};
|
|
7573
7927
|
} finally {
|
|
7574
7928
|
session.dispose();
|
|
@@ -7624,23 +7978,86 @@ var test = async (args) => {
|
|
|
7624
7978
|
process.exit(failed > 0 ? 1 : 0);
|
|
7625
7979
|
};
|
|
7626
7980
|
|
|
7981
|
+
// src/commands/types.ts
|
|
7982
|
+
import { resolve as resolve10 } from "node:path";
|
|
7983
|
+
var label = (bundle) => `${Object.keys(bundle).length} file(s)`;
|
|
7984
|
+
var types = async (args) => {
|
|
7985
|
+
const dirArg = args._[0];
|
|
7986
|
+
const root = dirArg ? resolve10(dirArg) : (() => {
|
|
7987
|
+
try {
|
|
7988
|
+
return findProjectRoot(process.cwd());
|
|
7989
|
+
} catch {
|
|
7990
|
+
return process.cwd();
|
|
7991
|
+
}
|
|
7992
|
+
})();
|
|
7993
|
+
const useLocal = flagBool(args, "local");
|
|
7994
|
+
const checkOnly = flagBool(args, "check");
|
|
7995
|
+
const local = readLocalTypeBundle();
|
|
7996
|
+
let bundle = null;
|
|
7997
|
+
let source = "";
|
|
7998
|
+
if (useLocal) {
|
|
7999
|
+
if (!local)
|
|
8000
|
+
throw new CliError("This CLI ships no vendored type bundle — drop --local to fetch them from the server.");
|
|
8001
|
+
bundle = local;
|
|
8002
|
+
source = "the CLI's bundled SDK";
|
|
8003
|
+
} else {
|
|
8004
|
+
const apiUrl = normalizeApiUrl(flagStr(args, "api") ?? loadConfig().apiUrl ?? DEFAULT_API);
|
|
8005
|
+
try {
|
|
8006
|
+
bundle = await getTypes(apiUrl);
|
|
8007
|
+
source = apiUrl;
|
|
8008
|
+
} catch (e) {
|
|
8009
|
+
if (!local || checkOnly)
|
|
8010
|
+
throw e;
|
|
8011
|
+
warn(`${e.message}`);
|
|
8012
|
+
bundle = local;
|
|
8013
|
+
source = "the CLI's bundled SDK (server unreachable)";
|
|
8014
|
+
}
|
|
8015
|
+
}
|
|
8016
|
+
log(`${c.green("source")} ${source} — ${label(bundle)}`);
|
|
8017
|
+
const onDisk = readProjectTypeBundle(root);
|
|
8018
|
+
const same = bundleHash(onDisk) === bundleHash(bundle);
|
|
8019
|
+
if (checkOnly) {
|
|
8020
|
+
if (Object.keys(onDisk).length === 0)
|
|
8021
|
+
throw new CliError(`No types in ${LECODES_DIR}/types/ — run \`lecodes types\`.`);
|
|
8022
|
+
if (!same)
|
|
8023
|
+
throw new CliError(`Types are out of date (${label(onDisk)} on disk) — run \`lecodes types\`.`);
|
|
8024
|
+
success(`Types are up to date (${label(onDisk)}).`);
|
|
8025
|
+
return;
|
|
8026
|
+
}
|
|
8027
|
+
const stats = writeTypeBundle(root, bundle);
|
|
8028
|
+
writeTsconfig(root);
|
|
8029
|
+
const changes = [
|
|
8030
|
+
stats.added ? `${stats.added} added` : null,
|
|
8031
|
+
stats.updated ? `${stats.updated} updated` : null,
|
|
8032
|
+
stats.removed ? `${stats.removed} removed` : null
|
|
8033
|
+
].filter(Boolean);
|
|
8034
|
+
log(`${c.green("written")} ${LECODES_DIR}/types/ + tsconfig.json ${c.dim(changes.length ? `(${changes.join(", ")})` : "(already current)")}`);
|
|
8035
|
+
if (!useLocal && local && bundleHash(local) !== bundleHash(bundle)) {
|
|
8036
|
+
warn("The server's SDK surface differs from the one this CLI compiles with — if something autocompletes but fails to compile, update the CLI (`npm i -g lecodes-cli@latest`) or re-run with --local.");
|
|
8037
|
+
}
|
|
8038
|
+
log("");
|
|
8039
|
+
success(stats.added + stats.updated + stats.removed > 0 ? "Types synced." : "Types already current.");
|
|
8040
|
+
if (!dirArg)
|
|
8041
|
+
note("Reload the TypeScript server in your editor to pick them up.");
|
|
8042
|
+
};
|
|
8043
|
+
|
|
7627
8044
|
// src/index.ts
|
|
7628
|
-
import { existsSync as
|
|
8045
|
+
import { existsSync as existsSync23, readFileSync as readFileSync23, realpathSync as realpathSync2 } from "node:fs";
|
|
7629
8046
|
import { createRequire as createRequire4 } from "node:module";
|
|
7630
|
-
import { delimiter as delimiter2, dirname as dirname16, join as
|
|
8047
|
+
import { delimiter as delimiter2, dirname as dirname16, join as join24 } from "node:path";
|
|
7631
8048
|
|
|
7632
8049
|
// src/commands/install.ts
|
|
7633
8050
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
7634
|
-
import { appendFileSync as appendFileSync2, chmodSync, cpSync as cpSync2, existsSync as
|
|
8051
|
+
import { appendFileSync as appendFileSync2, chmodSync, cpSync as cpSync2, existsSync as existsSync22, mkdirSync as mkdirSync15, readFileSync as readFileSync22, realpathSync, rmSync as rmSync4, symlinkSync } from "node:fs";
|
|
7635
8052
|
import { homedir as homedir3 } from "node:os";
|
|
7636
|
-
import { basename as
|
|
8053
|
+
import { basename as basename13, delimiter, dirname as dirname15, join as join23, resolve as resolve11, sep as sep2 } from "node:path";
|
|
7637
8054
|
var isWin = process.platform === "win32";
|
|
7638
8055
|
var HOME = homedir3();
|
|
7639
8056
|
var safeRealpath = (p) => {
|
|
7640
8057
|
try {
|
|
7641
8058
|
return realpathSync(p);
|
|
7642
8059
|
} catch {
|
|
7643
|
-
return
|
|
8060
|
+
return resolve11(p);
|
|
7644
8061
|
}
|
|
7645
8062
|
};
|
|
7646
8063
|
var onPath = (dir) => {
|
|
@@ -7657,12 +8074,12 @@ var onPath = (dir) => {
|
|
|
7657
8074
|
};
|
|
7658
8075
|
var install = async (args) => {
|
|
7659
8076
|
const bundle = dirname15(process.execPath);
|
|
7660
|
-
const exeName =
|
|
8077
|
+
const exeName = basename13(process.execPath);
|
|
7661
8078
|
const chiselName = isWin ? "chisel.exe" : "chisel";
|
|
7662
|
-
if (!
|
|
8079
|
+
if (!existsSync22(join23(bundle, "runtime")) || !existsSync22(join23(bundle, chiselName))) {
|
|
7663
8080
|
throw new CliError("`lecodes install` only works from the packaged release build (the archive containing lecodes + " + "chisel + runtime/). You appear to be running from source or an npm install — install globally " + "with your package manager there instead.");
|
|
7664
8081
|
}
|
|
7665
|
-
const companions = [exeName, chiselName, "runtime", "README.txt"].filter((f2) =>
|
|
8082
|
+
const companions = [exeName, chiselName, "runtime", "README.txt"].filter((f2) => existsSync22(join23(bundle, f2)));
|
|
7666
8083
|
if (isWin)
|
|
7667
8084
|
return installWindows(args, bundle, companions);
|
|
7668
8085
|
return installUnix(args, bundle, companions);
|
|
@@ -7672,9 +8089,9 @@ var copyBundle = (bundle, target, companions) => {
|
|
|
7672
8089
|
if (safeRealpath(target) === safeRealpath(bundle))
|
|
7673
8090
|
return;
|
|
7674
8091
|
for (const f2 of companions) {
|
|
7675
|
-
const dest =
|
|
7676
|
-
|
|
7677
|
-
cpSync2(
|
|
8092
|
+
const dest = join23(target, f2);
|
|
8093
|
+
rmSync4(dest, { recursive: true, force: true });
|
|
8094
|
+
cpSync2(join23(bundle, f2), dest, { recursive: true });
|
|
7678
8095
|
}
|
|
7679
8096
|
};
|
|
7680
8097
|
var permError = (e, dir, system) => {
|
|
@@ -7686,41 +8103,41 @@ var permError = (e, dir, system) => {
|
|
|
7686
8103
|
};
|
|
7687
8104
|
var installUnix = (args, bundle, companions) => {
|
|
7688
8105
|
const system = flagBool(args, "system");
|
|
7689
|
-
const libDir =
|
|
7690
|
-
const binDir =
|
|
7691
|
-
const
|
|
8106
|
+
const libDir = resolve11(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join23(HOME, ".local", "lib", "lecodes-cli")));
|
|
8107
|
+
const binDir = resolve11(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join23(HOME, ".local", "bin")));
|
|
8108
|
+
const link2 = join23(binDir, "lecodes");
|
|
7692
8109
|
try {
|
|
7693
8110
|
copyBundle(bundle, libDir, companions);
|
|
7694
|
-
chmodSync(
|
|
7695
|
-
chmodSync(
|
|
8111
|
+
chmodSync(join23(libDir, "lecodes"), 493);
|
|
8112
|
+
chmodSync(join23(libDir, "chisel"), 493);
|
|
7696
8113
|
} catch (e) {
|
|
7697
8114
|
throw permError(e, libDir, system);
|
|
7698
8115
|
}
|
|
7699
8116
|
try {
|
|
7700
8117
|
mkdirSync15(binDir, { recursive: true });
|
|
7701
|
-
|
|
7702
|
-
symlinkSync(
|
|
8118
|
+
rmSync4(link2, { force: true });
|
|
8119
|
+
symlinkSync(join23(libDir, "lecodes"), link2);
|
|
7703
8120
|
} catch (e) {
|
|
7704
8121
|
throw permError(e, binDir, system);
|
|
7705
8122
|
}
|
|
7706
8123
|
success(`Installed to ${c.bold(libDir)}`);
|
|
7707
|
-
log(` ${c.dim(
|
|
8124
|
+
log(` ${c.dim(link2 + " → " + join23(libDir, "lecodes"))}`);
|
|
7708
8125
|
finishPathUnix(args, binDir);
|
|
7709
8126
|
};
|
|
7710
8127
|
var shellRc = () => {
|
|
7711
|
-
const shell =
|
|
8128
|
+
const shell = basename13(process.env.SHELL ?? "");
|
|
7712
8129
|
if (shell === "zsh")
|
|
7713
|
-
return { file:
|
|
8130
|
+
return { file: join23(HOME, ".zshrc"), name: "~/.zshrc" };
|
|
7714
8131
|
if (shell === "bash")
|
|
7715
|
-
return { file:
|
|
7716
|
-
return { file:
|
|
8132
|
+
return { file: join23(HOME, ".bashrc"), name: "~/.bashrc" };
|
|
8133
|
+
return { file: join23(HOME, ".profile"), name: "~/.profile" };
|
|
7717
8134
|
};
|
|
7718
8135
|
var PATH_MARKER = "# Added by `lecodes install`";
|
|
7719
8136
|
var appendPathToShellRc = (binDir) => {
|
|
7720
8137
|
const { file, name } = shellRc();
|
|
7721
8138
|
const line = `export PATH="${binDir}:$PATH"`;
|
|
7722
8139
|
try {
|
|
7723
|
-
const existing =
|
|
8140
|
+
const existing = existsSync22(file) ? readFileSync22(file, "utf8") : "";
|
|
7724
8141
|
if (existing.includes(line))
|
|
7725
8142
|
return name;
|
|
7726
8143
|
appendFileSync2(file, `${existing && !existing.endsWith(`
|
|
@@ -7758,7 +8175,7 @@ var finishPathUnix = (args, binDir) => {
|
|
|
7758
8175
|
log(` • or install system-wide (no PATH edit): ${c.bold("sudo lecodes install --system")}`);
|
|
7759
8176
|
};
|
|
7760
8177
|
var installWindows = (args, bundle, companions) => {
|
|
7761
|
-
const target =
|
|
8178
|
+
const target = resolve11(flagStr(args, "dir") ?? join23(process.env.LOCALAPPDATA ?? join23(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
|
|
7762
8179
|
try {
|
|
7763
8180
|
copyBundle(bundle, target, companions);
|
|
7764
8181
|
} catch (e) {
|
|
@@ -7797,7 +8214,7 @@ var addToUserPathWindows = (dir) => {
|
|
|
7797
8214
|
var STANDALONE = (() => {
|
|
7798
8215
|
const dir = dirname16(process.execPath);
|
|
7799
8216
|
const chisel = process.platform === "win32" ? "chisel.exe" : "chisel";
|
|
7800
|
-
return
|
|
8217
|
+
return existsSync23(join24(dir, "runtime")) && existsSync23(join24(dir, chisel));
|
|
7801
8218
|
})();
|
|
7802
8219
|
var INSTALLED = STANDALONE && (() => {
|
|
7803
8220
|
const exe = process.platform === "win32" ? "lecodes.exe" : "lecodes";
|
|
@@ -7812,8 +8229,8 @@ var INSTALLED = STANDALONE && (() => {
|
|
|
7812
8229
|
if (!d)
|
|
7813
8230
|
return false;
|
|
7814
8231
|
try {
|
|
7815
|
-
const cand =
|
|
7816
|
-
return
|
|
8232
|
+
const cand = join24(d, exe);
|
|
8233
|
+
return existsSync23(cand) && realpathSync2(cand) === self;
|
|
7817
8234
|
} catch {
|
|
7818
8235
|
return false;
|
|
7819
8236
|
}
|
|
@@ -7844,6 +8261,19 @@ ${c.bold("Commands:")}
|
|
|
7844
8261
|
--team <uuid> create it inside a team
|
|
7845
8262
|
clone <uuid|url> [dir] Download a project's files + types into a new folder
|
|
7846
8263
|
--no-types skip the .d.ts / tsconfig generation
|
|
8264
|
+
link [dir] Connect an existing local folder (e.g. one made by 'lecodes
|
|
8265
|
+
init') to the platform — the reverse of clone. Creates a
|
|
8266
|
+
project on the server, writes .lecodes/manifest.json and
|
|
8267
|
+
refreshes the types; nothing is uploaded until you push
|
|
8268
|
+
--name <s> project name (default: folder name)
|
|
8269
|
+
--team <uuid> create it inside a team
|
|
8270
|
+
--project <uuid|url> adopt an EXISTING project instead —
|
|
8271
|
+
prints what your next push would change
|
|
8272
|
+
on its server tree, and confirms first
|
|
8273
|
+
--push [-m <msg>] push right after linking (--publish to
|
|
8274
|
+
deploy it live in the same step)
|
|
8275
|
+
--force re-link a folder that's already linked
|
|
8276
|
+
--no-types skip the .d.ts / tsconfig refresh
|
|
7847
8277
|
status Show local changes vs the last sync
|
|
7848
8278
|
diff [path…] Show a unified diff of local vs the current server state
|
|
7849
8279
|
--name-only list changed paths + status only
|
|
@@ -7907,6 +8337,13 @@ ${c.bold("Commands:")}
|
|
|
7907
8337
|
--artifacts <dir> failure-artifact folder override
|
|
7908
8338
|
(+ the render timing flags: --width/--height, --settle,
|
|
7909
8339
|
--device, --safe-area, --wait-network, --logs, --entry)
|
|
8340
|
+
types [dir] Refresh the IDE types (.lecodes/types/ + tsconfig.json) — by
|
|
8341
|
+
default from the server, so a project scaffolded offline picks
|
|
8342
|
+
up the SDK the platform actually runs
|
|
8343
|
+
--local use the SDK bundled in this CLI instead
|
|
8344
|
+
--check report drift only, don't write (non-zero exit)
|
|
8345
|
+
--api <url> server to fetch from (default: the logged-in
|
|
8346
|
+
one, else https://le.codes)
|
|
7910
8347
|
design [serve] LeCodes Design — a live canvas of screens for AI-driven prototyping
|
|
7911
8348
|
(needs the optional 'lecodes-design' package; works inside a
|
|
7912
8349
|
cloned project or standalone in any folder)
|
|
@@ -7933,6 +8370,9 @@ ${c.bold("Commands:")}
|
|
|
7933
8370
|
--variant <v> SDK variant: auto (default — sync picks the
|
|
7934
8371
|
smallest of core/2d/3d/full covering what
|
|
7935
8372
|
the bundle uses), or one of those to pin
|
|
8373
|
+
--sdk <v> native SDK release (default: the newest one,
|
|
8374
|
+
pinned in app.json ios.sdk); 'latest' floats
|
|
8375
|
+
it — every sync re-resolves
|
|
7936
8376
|
app sync Make the shell match the project: compile the bundle + embed
|
|
7937
8377
|
its assets into ios/App/Resources/ (offline by default, also
|
|
7938
8378
|
for server projects; app.json "resources": {"remote": [globs]}
|
|
@@ -7941,6 +8381,13 @@ ${c.bold("Commands:")}
|
|
|
7941
8381
|
plugins (pinned in plugins.lock.json), regenerate the
|
|
7942
8382
|
CLI-owned shell files. app.json "update": true (or
|
|
7943
8383
|
{"url": …}) enables native OTA JS updates at launch
|
|
8384
|
+
app update Move the shell's pins forward — app.json ios.sdk to the newest
|
|
8385
|
+
lecodes-ios-sdk release, plugins.lock.json to each plugin's
|
|
8386
|
+
newest tag — then re-sync
|
|
8387
|
+
--sdk <v|latest> pin that release instead of the newest
|
|
8388
|
+
--check report available updates, write nothing
|
|
8389
|
+
--no-plugins only move the SDK pin
|
|
8390
|
+
--no-sync update the pins without re-syncing
|
|
7944
8391
|
app plugin <list|add|remove> Manage native plugins in app.json (then run 'lecodes app sync')
|
|
7945
8392
|
add takes a known id (camera, qr-scanner) or github:owner/repo[@tag]
|
|
7946
8393
|
app open [ios] Open the native shell in its IDE (Xcode; Android Studio later)
|
|
@@ -7955,7 +8402,7 @@ ${c.bold("Commands:")}
|
|
|
7955
8402
|
ASC API key; stored in ~/.lecodes/asc.json
|
|
7956
8403
|
|
|
7957
8404
|
Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKEN).`;
|
|
7958
|
-
var commands = { init: init2, login, create, clone, status, diff, pull, push, compile: compile2, preview: previewLegacy, render, test, design, scene, dev, app };
|
|
8405
|
+
var commands = { init: init2, login, create, clone, link, status, diff, pull, push, compile: compile2, preview: previewLegacy, render, test, types, design, scene, dev, app };
|
|
7959
8406
|
if (STANDALONE)
|
|
7960
8407
|
commands.install = install;
|
|
7961
8408
|
var readVersion = () => package_default.version ?? "unknown";
|
|
@@ -7969,8 +8416,8 @@ var peerVersion = (name, probe) => {
|
|
|
7969
8416
|
const require2 = createRequire4(import.meta.url);
|
|
7970
8417
|
let dir = dirname16(require2.resolve(probe));
|
|
7971
8418
|
for (let depth = 0;depth < 16; depth++) {
|
|
7972
|
-
const manifest =
|
|
7973
|
-
if (
|
|
8419
|
+
const manifest = join24(dir, "package.json");
|
|
8420
|
+
if (existsSync23(manifest)) {
|
|
7974
8421
|
const parsed = JSON.parse(readFileSync23(manifest, "utf8"));
|
|
7975
8422
|
if (parsed.name === name)
|
|
7976
8423
|
return parsed.version ?? "unknown";
|
|
@@ -7989,9 +8436,9 @@ var printVersion = () => {
|
|
|
7989
8436
|
const version = peerVersion(name, probe);
|
|
7990
8437
|
rows.push([name, version ?? c.dim("not installed")]);
|
|
7991
8438
|
}
|
|
7992
|
-
const width = Math.max(...rows.map(([
|
|
7993
|
-
for (const [
|
|
7994
|
-
log(`${
|
|
8439
|
+
const width = Math.max(...rows.map(([label2]) => label2.length));
|
|
8440
|
+
for (const [label2, value] of rows)
|
|
8441
|
+
log(`${label2.padEnd(width)} ${value}`);
|
|
7995
8442
|
};
|
|
7996
8443
|
var main = async () => {
|
|
7997
8444
|
const argv = process.argv.slice(2);
|