lecodes-cli 0.9.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/index.js +666 -227
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2088,7 +2088,7 @@ var require_main = __commonJS((exports, module) => {
|
|
|
2088
2088
|
// package.json
|
|
2089
2089
|
var package_default = {
|
|
2090
2090
|
name: "lecodes-cli",
|
|
2091
|
-
version: "0.
|
|
2091
|
+
version: "0.10.0",
|
|
2092
2092
|
dependencies: {
|
|
2093
2093
|
"@letary/chisel": "^0.5.1"
|
|
2094
2094
|
},
|
|
@@ -3588,8 +3588,9 @@ var runtimePackageTemplate = (sdkVersion, variant, plugins) => {
|
|
|
3588
3588
|
//
|
|
3589
3589
|
// GENERATED by lecodes — regenerated by \`lecodes app sync\`; do not edit.
|
|
3590
3590
|
// 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
|
-
//
|
|
3591
|
+
// the compiled bundle uses, or forced via app.json ios.variant), the exact SDK release
|
|
3592
|
+
// (app.json ios.sdk — move it with \`lecodes app update\`) and one vendored target per
|
|
3593
|
+
// installed plugin (see plugins.lock.json).
|
|
3593
3594
|
|
|
3594
3595
|
import PackageDescription
|
|
3595
3596
|
|
|
@@ -3600,7 +3601,7 @@ let package = Package(
|
|
|
3600
3601
|
.library(name: "LeCodesRuntime", targets: ["LeCodesRuntime"]),
|
|
3601
3602
|
],
|
|
3602
3603
|
dependencies: [
|
|
3603
|
-
.package(url: "https://github.com/letary/lecodes-ios-sdk.git",
|
|
3604
|
+
.package(url: "https://github.com/letary/lecodes-ios-sdk.git", exact: "${sdkVersion}"),
|
|
3604
3605
|
],
|
|
3605
3606
|
targets: [
|
|
3606
3607
|
.target(
|
|
@@ -3653,6 +3654,22 @@ lecodes app open # open App.xcodeproj in Xcode — build & run from there
|
|
|
3653
3654
|
\`lecodes app doctor\` preflights builds; \`lecodes app build ios\` archives and exports an
|
|
3654
3655
|
.ipa; \`lecodes app upload ios\` sends the archive to App Store Connect.
|
|
3655
3656
|
|
|
3657
|
+
## SDK version
|
|
3658
|
+
|
|
3659
|
+
\`../app.json\` \`ios.sdk\` is the exact \`lecodes-ios-sdk\` release this shell links —
|
|
3660
|
+
\`lecodes app init\` pinned the newest one at the time, so builds stay reproducible.
|
|
3661
|
+
|
|
3662
|
+
\`\`\`
|
|
3663
|
+
lecodes app update # → newest SDK release + newest plugin tags, then sync
|
|
3664
|
+
lecodes app update --check # what's available, writes nothing
|
|
3665
|
+
lecodes app update --sdk 1.3.0
|
|
3666
|
+
\`\`\`
|
|
3667
|
+
|
|
3668
|
+
Set \`"sdk": "latest"\` instead of a version to re-resolve on every sync (needs network;
|
|
3669
|
+
offline it keeps whatever the shell already links). The pin is exact rather than a range
|
|
3670
|
+
because the CLI also downloads that release's filament resources bundle into
|
|
3671
|
+
\`App/Resources/\` — the two halves of one SDK must not drift apart.
|
|
3672
|
+
|
|
3656
3673
|
## Who owns what
|
|
3657
3674
|
|
|
3658
3675
|
Yours — \`lecodes app sync\` never touches these:
|
|
@@ -3700,7 +3717,8 @@ buildNumber) — edit it there, then re-run \`lecodes app sync\`. Plugins are ma
|
|
|
3700
3717
|
`;
|
|
3701
3718
|
|
|
3702
3719
|
// src/commands/app.ts
|
|
3703
|
-
var
|
|
3720
|
+
var SDK_REPO = "letary/lecodes-ios-sdk";
|
|
3721
|
+
var SDK_MIN_VERSION = "1.2.0";
|
|
3704
3722
|
var SDK_VARIANTS = ["core", "2d", "3d", "full"];
|
|
3705
3723
|
var FIRST_PARTY_PLUGINS = {
|
|
3706
3724
|
camera: "letary/lecodes-plugins",
|
|
@@ -3725,6 +3743,51 @@ var projectRoot = () => {
|
|
|
3725
3743
|
}
|
|
3726
3744
|
};
|
|
3727
3745
|
var git = (cwd, ...args) => execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
3746
|
+
var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
3747
|
+
var cmpVersion = (a, b) => {
|
|
3748
|
+
const pa = a.split(".").map(Number);
|
|
3749
|
+
const pb = b.split(".").map(Number);
|
|
3750
|
+
for (let i = 0;i < 3; i++) {
|
|
3751
|
+
if ((pa[i] ?? 0) !== (pb[i] ?? 0))
|
|
3752
|
+
return (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
3753
|
+
}
|
|
3754
|
+
return 0;
|
|
3755
|
+
};
|
|
3756
|
+
var fetchLatestSdkVersion = async () => {
|
|
3757
|
+
const url = `https://api.github.com/repos/${SDK_REPO}/releases/latest`;
|
|
3758
|
+
const res = await fetch(url, { headers: { accept: "application/vnd.github+json", "user-agent": "lecodes-cli" } });
|
|
3759
|
+
if (!res.ok)
|
|
3760
|
+
throw new CliError(`Could not read the latest ${SDK_REPO} release (HTTP ${res.status}) — ${url}`);
|
|
3761
|
+
const tag = (await res.json()).tag_name?.replace(/^v/, "") ?? "";
|
|
3762
|
+
if (!VERSION_RE.test(tag))
|
|
3763
|
+
throw new CliError(`${SDK_REPO} latest release has no x.y.z tag (got "${tag}")`);
|
|
3764
|
+
return tag;
|
|
3765
|
+
};
|
|
3766
|
+
var shellSdkVersion = (iosDir) => {
|
|
3767
|
+
const path = join11(iosDir, "LeCodesRuntime", "Package.swift");
|
|
3768
|
+
if (!existsSync8(path))
|
|
3769
|
+
return null;
|
|
3770
|
+
const m = /lecodes-ios-sdk\.git",\s*(?:exact|from):\s*"([^"]+)"/.exec(readFileSync10(path, "utf8"));
|
|
3771
|
+
return m && VERSION_RE.test(m[1]) ? m[1] : null;
|
|
3772
|
+
};
|
|
3773
|
+
var resolveSdkVersion = async (pin, fallback) => {
|
|
3774
|
+
if (pin !== "latest") {
|
|
3775
|
+
if (!VERSION_RE.test(pin)) {
|
|
3776
|
+
throw new CliError(`app.json ios.sdk must be "latest" or an exact x.y.z release (got "${pin}").`);
|
|
3777
|
+
}
|
|
3778
|
+
if (cmpVersion(pin, SDK_MIN_VERSION) < 0) {
|
|
3779
|
+
throw new CliError(`app.json ios.sdk ${pin} is older than the ${SDK_MIN_VERSION} these generated shells need.`);
|
|
3780
|
+
}
|
|
3781
|
+
return pin;
|
|
3782
|
+
}
|
|
3783
|
+
try {
|
|
3784
|
+
return await fetchLatestSdkVersion();
|
|
3785
|
+
} catch (e) {
|
|
3786
|
+
const kept = fallback ?? SDK_MIN_VERSION;
|
|
3787
|
+
warn(`Could not resolve the latest SDK (${e.message}) — staying on ${kept}.`);
|
|
3788
|
+
return kept;
|
|
3789
|
+
}
|
|
3790
|
+
};
|
|
3728
3791
|
var ensureRepo = (repo, wantTag, lock) => {
|
|
3729
3792
|
const override = process.env.LECODES_PLUGINS_DIR;
|
|
3730
3793
|
if (override) {
|
|
@@ -3802,7 +3865,7 @@ var slugify = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+
|
|
|
3802
3865
|
var appInit = async (args) => {
|
|
3803
3866
|
const platform = args._[1];
|
|
3804
3867
|
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]");
|
|
3868
|
+
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
3869
|
}
|
|
3807
3870
|
const root = projectRoot();
|
|
3808
3871
|
const iosDir = join11(root, "ios");
|
|
@@ -3814,6 +3877,9 @@ var appInit = async (args) => {
|
|
|
3814
3877
|
if (variant !== "auto" && !SDK_VARIANTS.includes(variant)) {
|
|
3815
3878
|
throw new CliError(`--variant must be auto, ${SDK_VARIANTS.join(", ")} (got "${variant}")`);
|
|
3816
3879
|
}
|
|
3880
|
+
const sdkFlag = flagStr(args, "sdk") ?? existing.ios?.sdk;
|
|
3881
|
+
const sdkVersion = await resolveSdkVersion(process.env.LECODES_IOS_SDK ?? sdkFlag ?? "latest", null);
|
|
3882
|
+
const sdk = sdkFlag === "latest" ? "latest" : sdkVersion;
|
|
3817
3883
|
const name = flagStr(args, "name") ?? existing.name ?? basename4(root);
|
|
3818
3884
|
const config = {
|
|
3819
3885
|
...existing,
|
|
@@ -3824,6 +3890,7 @@ var appInit = async (args) => {
|
|
|
3824
3890
|
bundleId: flagStr(args, "bundle-id") ?? existing.ios?.bundleId ?? `com.example.${slugify(name)}`,
|
|
3825
3891
|
teamId: flagStr(args, "team") ?? existing.ios?.teamId ?? "",
|
|
3826
3892
|
variant,
|
|
3893
|
+
sdk,
|
|
3827
3894
|
buildNumber: existing.ios?.buildNumber ?? 1
|
|
3828
3895
|
}
|
|
3829
3896
|
};
|
|
@@ -3863,9 +3930,10 @@ ios/
|
|
|
3863
3930
|
write("App/Assets.xcassets/Contents.json", ASSETS_ROOT_JSON);
|
|
3864
3931
|
write("App/Assets.xcassets/AccentColor.colorset/Contents.json", ACCENT_COLOR_JSON);
|
|
3865
3932
|
write("App/Assets.xcassets/AppIcon.appiconset/Contents.json", APP_ICON_JSON);
|
|
3866
|
-
write("LeCodesRuntime/Package.swift", runtimePackageTemplate(
|
|
3933
|
+
write("LeCodesRuntime/Package.swift", runtimePackageTemplate(sdkVersion, variant === "auto" ? "core" : variant, []));
|
|
3867
3934
|
write("LeCodesRuntime/Sources/LeCodesRuntime/LeCodesRuntime.swift", runtimeSwiftTemplate([]));
|
|
3868
3935
|
log("");
|
|
3936
|
+
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
3937
|
success(`iOS shell for ${c.bold(name)} ready.`);
|
|
3870
3938
|
log(` ${c.bold("lecodes app sync")} embed the compiled bundle (+ plugins) into the shell`);
|
|
3871
3939
|
log(` ${c.bold("open ios/App.xcodeproj")} then run on your device from Xcode`);
|
|
@@ -3898,18 +3966,18 @@ var resolveVariant = (requested, bundleJs) => {
|
|
|
3898
3966
|
}
|
|
3899
3967
|
return { variant, detected };
|
|
3900
3968
|
};
|
|
3901
|
-
var ensureSdkResourcesBundle = async () => {
|
|
3969
|
+
var ensureSdkResourcesBundle = async (sdkVersion) => {
|
|
3902
3970
|
const override = process.env.LECODES_SDK_RESOURCES;
|
|
3903
3971
|
if (override) {
|
|
3904
3972
|
note(`Using local resources bundle ${override} (LECODES_SDK_RESOURCES)`);
|
|
3905
3973
|
return resolve2(override);
|
|
3906
3974
|
}
|
|
3907
|
-
const dir = join11(homedir2(), ".lecodes", "sdk-resources",
|
|
3975
|
+
const dir = join11(homedir2(), ".lecodes", "sdk-resources", sdkVersion);
|
|
3908
3976
|
const bundle = join11(dir, "LeCodesSDKResources.bundle");
|
|
3909
3977
|
if (existsSync8(bundle))
|
|
3910
3978
|
return bundle;
|
|
3911
|
-
const url = `https://github.com/
|
|
3912
|
-
note(`Downloading LeCodesSDKResources.bundle (SDK ${
|
|
3979
|
+
const url = `https://github.com/${SDK_REPO}/releases/download/${sdkVersion}/LeCodesSDKResources.bundle.zip`;
|
|
3980
|
+
note(`Downloading LeCodesSDKResources.bundle (SDK ${sdkVersion})…`);
|
|
3913
3981
|
const res = await fetch(url);
|
|
3914
3982
|
if (!res.ok)
|
|
3915
3983
|
throw new CliError(`Resources bundle download failed (HTTP ${res.status}) — ${url}`);
|
|
@@ -3952,6 +4020,9 @@ var appSync = async (_args) => {
|
|
|
3952
4020
|
if (requestedVariant !== "auto" && !SDK_VARIANTS.includes(requestedVariant)) {
|
|
3953
4021
|
throw new CliError(`app.json ios.variant must be "auto" or one of ${SDK_VARIANTS.join(", ")} (got "${requestedVariant}").`);
|
|
3954
4022
|
}
|
|
4023
|
+
const linkedSdk = shellSdkVersion(iosDir);
|
|
4024
|
+
const sdkPin = process.env.LECODES_IOS_SDK ?? config.ios.sdk ?? linkedSdk ?? "latest";
|
|
4025
|
+
const sdkVersion = await resolveSdkVersion(sdkPin, linkedSdk);
|
|
3955
4026
|
const lockPath = join11(root, "plugins.lock.json");
|
|
3956
4027
|
const oldLock = readJson(lockPath)?.plugins ?? {};
|
|
3957
4028
|
const pluginSpecs = Object.entries(config.plugins ?? {});
|
|
@@ -3972,6 +4043,9 @@ var appSync = async (_args) => {
|
|
|
3972
4043
|
const dest = join11(sourcesDir, module);
|
|
3973
4044
|
rmSync(dest, { recursive: true, force: true });
|
|
3974
4045
|
cpSync(src, dest, { recursive: true });
|
|
4046
|
+
if (ios.minSdk && VERSION_RE.test(ios.minSdk) && cmpVersion(sdkVersion, ios.minSdk) < 0) {
|
|
4047
|
+
warn(`Plugin "${id}" needs lecodes-ios-sdk ${ios.minSdk} but the shell links ${sdkVersion} — run \`lecodes app update\` (or set app.json ios.sdk).`);
|
|
4048
|
+
}
|
|
3975
4049
|
for (const [key, value] of Object.entries(ios.infoPlist ?? {})) {
|
|
3976
4050
|
(infoValues[key] ??= []).push(value);
|
|
3977
4051
|
}
|
|
@@ -4003,9 +4077,11 @@ var appSync = async (_args) => {
|
|
|
4003
4077
|
full: "uses the 3D + 2D engines"
|
|
4004
4078
|
};
|
|
4005
4079
|
log(`${c.green("variant")} ${variant}${requestedVariant === "auto" ? ` (auto: ${WHY[detected]})` : " (forced by ios.variant)"}`);
|
|
4080
|
+
const sdkWhy = sdkPin === "latest" ? "latest" : config.ios.sdk ? "pinned by ios.sdk" : "from the shell — set ios.sdk or run `lecodes app update`";
|
|
4081
|
+
log(`${c.green("sdk")} lecodes-ios-sdk ${sdkVersion}${c.dim(` (${sdkWhy})`)}`);
|
|
4006
4082
|
const sdkProvided = new Set;
|
|
4007
4083
|
if (variant === "3d" || variant === "full") {
|
|
4008
|
-
const sdkResources = await ensureSdkResourcesBundle();
|
|
4084
|
+
const sdkResources = await ensureSdkResourcesBundle(sdkVersion);
|
|
4009
4085
|
cpSync(sdkResources, join11(resourcesDir, "LeCodesSDKResources.bundle"), { recursive: true });
|
|
4010
4086
|
for (const f2 of readdirSync4(sdkResources))
|
|
4011
4087
|
if (f2 !== "Info.plist")
|
|
@@ -4043,7 +4119,7 @@ var appSync = async (_args) => {
|
|
|
4043
4119
|
if (streamed.length > 0)
|
|
4044
4120
|
log(`${c.green("remote")} ${streamed.length} resource(s) stay on the server (resources.remote): ${streamed.map((r) => r.path).join(", ")}`);
|
|
4045
4121
|
writeFileSync7(join11(iosDir, "README.md"), IOS_README);
|
|
4046
|
-
writeFileSync7(join11(iosDir, "LeCodesRuntime", "Package.swift"), runtimePackageTemplate(
|
|
4122
|
+
writeFileSync7(join11(iosDir, "LeCodesRuntime", "Package.swift"), runtimePackageTemplate(sdkVersion, variant, runtimePlugins));
|
|
4047
4123
|
writeFileSync7(join11(sourcesDir, "LeCodesRuntime", "LeCodesRuntime.swift"), runtimeSwiftTemplate(runtimePlugins));
|
|
4048
4124
|
const mergedKeys = {};
|
|
4049
4125
|
for (const [key, values] of Object.entries(infoValues))
|
|
@@ -4058,7 +4134,7 @@ var appSync = async (_args) => {
|
|
|
4058
4134
|
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
4135
|
writeFileSync7(pbxPath, pbx);
|
|
4060
4136
|
log("");
|
|
4061
|
-
success(`Shell synced: app.js + ${runtimePlugins.length} plugin(s), variant ${variant}, v${config.version ?? "1.0.0"} (${config.ios.buildNumber ?? 1}).`);
|
|
4137
|
+
success(`Shell synced: app.js + ${runtimePlugins.length} plugin(s), SDK ${sdkVersion}, variant ${variant}, v${config.version ?? "1.0.0"} (${config.ios.buildNumber ?? 1}).`);
|
|
4062
4138
|
note("Build & run from Xcode (open ios/App.xcodeproj) — user files in ios/App/*.swift were not touched.");
|
|
4063
4139
|
};
|
|
4064
4140
|
var appPlugin = async (args) => {
|
|
@@ -4147,6 +4223,95 @@ var requireShell = () => {
|
|
|
4147
4223
|
}
|
|
4148
4224
|
return { root, config };
|
|
4149
4225
|
};
|
|
4226
|
+
var latestPluginTag = (repo) => {
|
|
4227
|
+
try {
|
|
4228
|
+
return ensureRepo(repo, null, undefined).tag;
|
|
4229
|
+
} catch {
|
|
4230
|
+
return null;
|
|
4231
|
+
}
|
|
4232
|
+
};
|
|
4233
|
+
var appUpdate = async (args) => {
|
|
4234
|
+
const { root, config } = requireShell();
|
|
4235
|
+
const iosDir = join11(root, "ios");
|
|
4236
|
+
const checkOnly = flagBool(args, "check");
|
|
4237
|
+
const wanted = flagStr(args, "sdk");
|
|
4238
|
+
let changed = false;
|
|
4239
|
+
const pin = config.ios.sdk ?? shellSdkVersion(iosDir) ?? "latest";
|
|
4240
|
+
const current = pin === "latest" ? shellSdkVersion(iosDir) ?? SDK_MIN_VERSION : pin;
|
|
4241
|
+
let nextPin;
|
|
4242
|
+
if (wanted === "latest") {
|
|
4243
|
+
nextPin = "latest";
|
|
4244
|
+
} else if (wanted) {
|
|
4245
|
+
if (!VERSION_RE.test(wanted))
|
|
4246
|
+
throw new CliError(`--sdk must be "latest" or an exact x.y.z release (got "${wanted}").`);
|
|
4247
|
+
if (cmpVersion(wanted, SDK_MIN_VERSION) < 0)
|
|
4248
|
+
throw new CliError(`--sdk ${wanted} is older than the ${SDK_MIN_VERSION} these generated shells need.`);
|
|
4249
|
+
nextPin = wanted;
|
|
4250
|
+
} else {
|
|
4251
|
+
nextPin = await fetchLatestSdkVersion();
|
|
4252
|
+
}
|
|
4253
|
+
const nextVersion = nextPin === "latest" ? await fetchLatestSdkVersion() : nextPin;
|
|
4254
|
+
if (nextVersion === current && nextPin === pin) {
|
|
4255
|
+
log(`${c.green("sdk")} lecodes-ios-sdk ${current} ${c.dim("(already current)")}`);
|
|
4256
|
+
} else if (nextVersion === current) {
|
|
4257
|
+
log(`${c.green("sdk")} lecodes-ios-sdk ${current} → ios.sdk ${nextPin} ${c.dim("(same version, pin mode changed)")}`);
|
|
4258
|
+
changed = true;
|
|
4259
|
+
} else {
|
|
4260
|
+
const verb = cmpVersion(nextVersion, current) < 0 ? c.yellow("downgrade") : c.green("update");
|
|
4261
|
+
log(`${c.green("sdk")} lecodes-ios-sdk ${current} → ${c.bold(nextVersion)} ${c.dim(`(${verb})`)}`);
|
|
4262
|
+
changed = true;
|
|
4263
|
+
}
|
|
4264
|
+
if (!checkOnly && nextPin !== pin) {
|
|
4265
|
+
config.ios.sdk = nextPin;
|
|
4266
|
+
writeJson(join11(root, "app.json"), config);
|
|
4267
|
+
}
|
|
4268
|
+
const lockPath = join11(root, "plugins.lock.json");
|
|
4269
|
+
const lock = readJson(lockPath)?.plugins ?? {};
|
|
4270
|
+
const specs = Object.entries(config.plugins ?? {});
|
|
4271
|
+
if (specs.length > 0 && !flagBool(args, "no-plugins")) {
|
|
4272
|
+
const kept = {};
|
|
4273
|
+
for (const [id, spec] of specs) {
|
|
4274
|
+
const entry = lock[id];
|
|
4275
|
+
if (spec.startsWith("github:") && spec.includes("@")) {
|
|
4276
|
+
log(`${c.green("plugin")} ${id} ${c.dim(`pinned by app.json (${spec}) — not updated`)}`);
|
|
4277
|
+
if (entry)
|
|
4278
|
+
kept[id] = entry;
|
|
4279
|
+
continue;
|
|
4280
|
+
}
|
|
4281
|
+
const repo = spec.startsWith("github:") ? spec.slice("github:".length) : FIRST_PARTY_PLUGINS[id];
|
|
4282
|
+
const tag = repo ? latestPluginTag(repo) : null;
|
|
4283
|
+
if (!tag) {
|
|
4284
|
+
log(`${c.yellow("plugin")} ${id} ${c.dim("could not read its latest tag — left as is")}`);
|
|
4285
|
+
if (entry)
|
|
4286
|
+
kept[id] = entry;
|
|
4287
|
+
continue;
|
|
4288
|
+
}
|
|
4289
|
+
if (entry && entry.tag === tag) {
|
|
4290
|
+
log(`${c.green("plugin")} ${id} ${entry.tag} ${c.dim("(already current)")}`);
|
|
4291
|
+
kept[id] = entry;
|
|
4292
|
+
continue;
|
|
4293
|
+
}
|
|
4294
|
+
log(`${c.green("plugin")} ${id} ${entry?.tag ?? "—"} → ${c.bold(tag)}`);
|
|
4295
|
+
changed = true;
|
|
4296
|
+
}
|
|
4297
|
+
if (!checkOnly && existsSync8(lockPath))
|
|
4298
|
+
writeJson(lockPath, { plugins: kept });
|
|
4299
|
+
}
|
|
4300
|
+
log("");
|
|
4301
|
+
if (checkOnly) {
|
|
4302
|
+
note(changed ? "Run `lecodes app update` to apply." : "Everything is up to date.");
|
|
4303
|
+
return;
|
|
4304
|
+
}
|
|
4305
|
+
if (!changed) {
|
|
4306
|
+
success("Already up to date.");
|
|
4307
|
+
return;
|
|
4308
|
+
}
|
|
4309
|
+
if (flagBool(args, "no-sync")) {
|
|
4310
|
+
success("Pins updated — run `lecodes app sync` to apply them to the shell.");
|
|
4311
|
+
return;
|
|
4312
|
+
}
|
|
4313
|
+
await appSync({ _: [], flags: {} });
|
|
4314
|
+
};
|
|
4150
4315
|
var appDoctor = async (_args) => {
|
|
4151
4316
|
const { root, config } = requireShell();
|
|
4152
4317
|
let failed = 0;
|
|
@@ -4177,6 +4342,18 @@ var appDoctor = async (_args) => {
|
|
|
4177
4342
|
const hasResources = existsSync8(join11(root, "ios", "App", "Resources", "LeCodesSDKResources.bundle"));
|
|
4178
4343
|
check(pkg.includes(`"LeCodesSDK-${variant}"`) && (!needsResources || hasResources), `SDK variant ${variant}${needsResources ? " (+ filament resources)" : ""}`, "run `lecodes app sync`");
|
|
4179
4344
|
}
|
|
4345
|
+
const linked = shellSdkVersion(join11(root, "ios"));
|
|
4346
|
+
const pin = config.ios.sdk ?? "latest";
|
|
4347
|
+
check(!!linked, `SDK ${linked ?? "version unreadable"}`, "run `lecodes app sync`");
|
|
4348
|
+
if (linked && pin !== "latest") {
|
|
4349
|
+
check(linked === pin, `SDK matches app.json ios.sdk (${pin})`, "run `lecodes app sync`");
|
|
4350
|
+
}
|
|
4351
|
+
if (linked) {
|
|
4352
|
+
try {
|
|
4353
|
+
const latest = await fetchLatestSdkVersion();
|
|
4354
|
+
check(cmpVersion(linked, latest) >= 0, `SDK is the latest release (${latest})`, "run `lecodes app update`", false);
|
|
4355
|
+
} catch {}
|
|
4356
|
+
}
|
|
4180
4357
|
const lock = readJson(join11(root, "plugins.lock.json"))?.plugins ?? {};
|
|
4181
4358
|
const wanted = Object.keys(config.plugins ?? {});
|
|
4182
4359
|
const inSync = wanted.every((id) => lock[id]) && Object.keys(lock).every((id) => wanted.includes(id));
|
|
@@ -4303,6 +4480,8 @@ var app = async (args) => {
|
|
|
4303
4480
|
return appInit(args);
|
|
4304
4481
|
if (sub === "sync")
|
|
4305
4482
|
return appSync(args);
|
|
4483
|
+
if (sub === "update")
|
|
4484
|
+
return appUpdate(args);
|
|
4306
4485
|
if (sub === "plugin")
|
|
4307
4486
|
return appPlugin(args);
|
|
4308
4487
|
if (sub === "doctor")
|
|
@@ -4313,16 +4492,16 @@ var app = async (args) => {
|
|
|
4313
4492
|
return appUpload(args);
|
|
4314
4493
|
if (sub === "open")
|
|
4315
4494
|
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`.");
|
|
4495
|
+
throw new CliError("Usage: lecodes app <init ios | sync | update | open | plugin list|add|remove | doctor | build ios | upload ios> — see `lecodes help`.");
|
|
4317
4496
|
};
|
|
4318
4497
|
|
|
4319
4498
|
// src/commands/clone.ts
|
|
4320
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync7, readdirSync as
|
|
4499
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, readdirSync as readdirSync6 } from "node:fs";
|
|
4321
4500
|
import { resolve as resolve3 } from "node:path";
|
|
4322
4501
|
|
|
4323
4502
|
// 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";
|
|
4503
|
+
import { dirname as dirname8, join as join12, relative as relative3, sep } from "node:path";
|
|
4504
|
+
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
4505
|
var TSCONFIG = {
|
|
4327
4506
|
compilerOptions: {
|
|
4328
4507
|
target: "ES2020",
|
|
@@ -4339,17 +4518,60 @@ var TSCONFIG = {
|
|
|
4339
4518
|
include: [`${LECODES_DIR}/types/**/*.d.ts`, "**/*.ts", "**/*.js"],
|
|
4340
4519
|
exclude: ["node_modules"]
|
|
4341
4520
|
};
|
|
4521
|
+
var listBundleFiles = (dir, base = dir) => {
|
|
4522
|
+
if (!existsSync9(dir))
|
|
4523
|
+
return [];
|
|
4524
|
+
const out = [];
|
|
4525
|
+
for (const name of readdirSync5(dir)) {
|
|
4526
|
+
const path = join12(dir, name);
|
|
4527
|
+
if (statSync3(path).isDirectory())
|
|
4528
|
+
out.push(...listBundleFiles(path, base));
|
|
4529
|
+
else
|
|
4530
|
+
out.push(relative3(base, path).split(sep).join("/"));
|
|
4531
|
+
}
|
|
4532
|
+
return out;
|
|
4533
|
+
};
|
|
4534
|
+
var pruneEmptyDirs = (dir, root) => {
|
|
4535
|
+
if (!existsSync9(dir) || !statSync3(dir).isDirectory())
|
|
4536
|
+
return;
|
|
4537
|
+
for (const name of readdirSync5(dir))
|
|
4538
|
+
pruneEmptyDirs(join12(dir, name), root);
|
|
4539
|
+
if (dir !== root && readdirSync5(dir).length === 0)
|
|
4540
|
+
rmSync2(dir, { recursive: true });
|
|
4541
|
+
};
|
|
4342
4542
|
var writeTypeBundle = (root, bundle) => {
|
|
4343
4543
|
const typesDir = join12(root, LECODES_DIR, "types");
|
|
4544
|
+
const before = new Set(listBundleFiles(typesDir));
|
|
4545
|
+
const stats = { added: 0, updated: 0, removed: 0, total: Object.keys(bundle).length };
|
|
4344
4546
|
for (const [path, text] of Object.entries(bundle)) {
|
|
4345
4547
|
const dest = join12(typesDir, path);
|
|
4548
|
+
if (!before.has(path))
|
|
4549
|
+
stats.added++;
|
|
4550
|
+
else if (readFileSync11(dest, "utf8") !== text)
|
|
4551
|
+
stats.updated++;
|
|
4552
|
+
else
|
|
4553
|
+
continue;
|
|
4346
4554
|
mkdirSync6(dirname8(dest), { recursive: true });
|
|
4347
4555
|
writeFileSync8(dest, text);
|
|
4348
4556
|
}
|
|
4557
|
+
for (const path of before) {
|
|
4558
|
+
if (path in bundle)
|
|
4559
|
+
continue;
|
|
4560
|
+
rmSync2(join12(typesDir, path));
|
|
4561
|
+
stats.removed++;
|
|
4562
|
+
}
|
|
4563
|
+
pruneEmptyDirs(typesDir, typesDir);
|
|
4564
|
+
return stats;
|
|
4349
4565
|
};
|
|
4350
|
-
var
|
|
4351
|
-
|
|
4566
|
+
var bundleHash = (bundle) => sha256(Object.keys(bundle).sort().map((k) => `${k}\x00${bundle[k]}`).join("\x00"));
|
|
4567
|
+
var readProjectTypeBundle = (root) => {
|
|
4568
|
+
const typesDir = join12(root, LECODES_DIR, "types");
|
|
4569
|
+
const bundle = {};
|
|
4570
|
+
for (const path of listBundleFiles(typesDir))
|
|
4571
|
+
bundle[path] = readFileSync11(join12(typesDir, path), "utf8");
|
|
4572
|
+
return bundle;
|
|
4352
4573
|
};
|
|
4574
|
+
var materializeTypes = async (root, apiUrl) => writeTypeBundle(root, await getTypes(apiUrl));
|
|
4353
4575
|
var readLocalTypeBundle = () => {
|
|
4354
4576
|
const candidates = [
|
|
4355
4577
|
join12(distRoot, "runtime", "sdk-types.json"),
|
|
@@ -4394,7 +4616,7 @@ var clone = async (args) => {
|
|
|
4394
4616
|
const project = await getProject(apiUrl, token, uuid);
|
|
4395
4617
|
const dir = args._[1] ?? slug(project.name);
|
|
4396
4618
|
const root = resolve3(dir);
|
|
4397
|
-
if (existsSync10(root) &&
|
|
4619
|
+
if (existsSync10(root) && readdirSync6(root).length > 0) {
|
|
4398
4620
|
throw new CliError(`Directory "${dir}" already exists and is not empty.`);
|
|
4399
4621
|
}
|
|
4400
4622
|
mkdirSync7(root, { recursive: true });
|
|
@@ -4414,7 +4636,7 @@ var clone = async (args) => {
|
|
|
4414
4636
|
};
|
|
4415
4637
|
|
|
4416
4638
|
// src/commands/create.ts
|
|
4417
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readdirSync as
|
|
4639
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readdirSync as readdirSync7, writeFileSync as writeFileSync9 } from "node:fs";
|
|
4418
4640
|
import { join as join13, resolve as resolve4 } from "node:path";
|
|
4419
4641
|
|
|
4420
4642
|
// src/commands/projectTemplates.ts
|
|
@@ -4501,7 +4723,7 @@ var create = async (args) => {
|
|
|
4501
4723
|
throw new CliError("Usage: lecodes create <name> [directory]");
|
|
4502
4724
|
const dir = args._[1] ?? slug2(name);
|
|
4503
4725
|
const root = resolve4(dir);
|
|
4504
|
-
if (existsSync11(root) &&
|
|
4726
|
+
if (existsSync11(root) && readdirSync7(root).length > 0) {
|
|
4505
4727
|
throw new CliError(`Directory "${dir}" already exists and is not empty.`);
|
|
4506
4728
|
}
|
|
4507
4729
|
mkdirSync8(root, { recursive: true });
|
|
@@ -4520,7 +4742,7 @@ var create = async (args) => {
|
|
|
4520
4742
|
};
|
|
4521
4743
|
|
|
4522
4744
|
// src/commands/design.ts
|
|
4523
|
-
import { existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync14, readdirSync as
|
|
4745
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync14, readdirSync as readdirSync8, writeFileSync as writeFileSync11, appendFileSync } from "node:fs";
|
|
4524
4746
|
import { basename as basename6, join as join15 } from "node:path";
|
|
4525
4747
|
|
|
4526
4748
|
// src/compile/designCompile.ts
|
|
@@ -5507,7 +5729,7 @@ var listScreens = (designDir) => {
|
|
|
5507
5729
|
const dir = join15(designDir, "screens");
|
|
5508
5730
|
if (!existsSync13(dir))
|
|
5509
5731
|
return [];
|
|
5510
|
-
return
|
|
5732
|
+
return readdirSync8(dir).filter((f2) => f2.endsWith(".ts") && !f2.startsWith(".")).map((f2) => f2.slice(0, -3)).sort();
|
|
5511
5733
|
};
|
|
5512
5734
|
var readMetaSizes = (designDir) => {
|
|
5513
5735
|
try {
|
|
@@ -5876,7 +6098,7 @@ import { basename as basename7, join as join17 } from "node:path";
|
|
|
5876
6098
|
|
|
5877
6099
|
// src/dev/devServer.ts
|
|
5878
6100
|
import { createServer as createServer2 } from "node:http";
|
|
5879
|
-
import { createReadStream, existsSync as existsSync14, statSync as
|
|
6101
|
+
import { createReadStream, existsSync as existsSync14, statSync as statSync4 } from "node:fs";
|
|
5880
6102
|
import { join as join16 } from "node:path";
|
|
5881
6103
|
|
|
5882
6104
|
// src/dev/clientTemplates.ts
|
|
@@ -6247,7 +6469,7 @@ var startDevServer = async (options) => {
|
|
|
6247
6469
|
return sendText(res, 400, "text/plain", "Bad path");
|
|
6248
6470
|
}
|
|
6249
6471
|
const abs = join16(root, key.slice(1));
|
|
6250
|
-
if (!existsSync14(abs) || !
|
|
6472
|
+
if (!existsSync14(abs) || !statSync4(abs).isFile())
|
|
6251
6473
|
return sendText(res, 404, "text/plain", "Not found");
|
|
6252
6474
|
res.writeHead(200, { "content-type": contentType(key), "cache-control": "no-store" });
|
|
6253
6475
|
createReadStream(abs).pipe(res);
|
|
@@ -6706,6 +6928,19 @@ lecodes test # run tests/*.flow.json — the starter ships w
|
|
|
6706
6928
|
Scenario format + conventions: see [CLAUDE.md](./CLAUDE.md) (written for AI assistants, useful
|
|
6707
6929
|
for humans too).
|
|
6708
6930
|
|
|
6931
|
+
## Put it on the platform
|
|
6932
|
+
|
|
6933
|
+
This project is local-only. To give it a home on le.codes (web editor, sharing, OTA updates):
|
|
6934
|
+
|
|
6935
|
+
\`\`\`
|
|
6936
|
+
lecodes login # once per machine
|
|
6937
|
+
lecodes link # create a project from this folder and connect it
|
|
6938
|
+
lecodes push -m "initial" # upload it as the first checkpoint (--publish to go live)
|
|
6939
|
+
\`\`\`
|
|
6940
|
+
|
|
6941
|
+
Already made the project in the web editor? \`lecodes link --project <uuid>\` adopts it — it
|
|
6942
|
+
shows what your next push would change there before connecting anything.
|
|
6943
|
+
|
|
6709
6944
|
## Go native
|
|
6710
6945
|
|
|
6711
6946
|
\`\`\`
|
|
@@ -6722,7 +6957,8 @@ lecodes app open # open it in Xcode, build & run
|
|
|
6722
6957
|
\`tests/.artifacts/\`
|
|
6723
6958
|
- \`CLAUDE.md\` — guidance for AI assistants (the headless feedback loop + testability conventions)
|
|
6724
6959
|
- \`assets/\` — images/fonts the app references (created on demand)
|
|
6725
|
-
- \`tsconfig.json\` + \`.lecodes/types/\` — generated IDE types (rewritten by the CLI; don't edit)
|
|
6960
|
+
- \`tsconfig.json\` + \`.lecodes/types/\` — generated IDE types (rewritten by the CLI; don't edit).
|
|
6961
|
+
They come from the SDK bundled in the CLI; \`lecodes types\` refreshes them from the platform.
|
|
6726
6962
|
- \`.lecodesignore\` — files a future \`lecodes push\` would skip
|
|
6727
6963
|
|
|
6728
6964
|
Full command list: \`lecodes --help\`.
|
|
@@ -6763,6 +6999,231 @@ var init2 = async (args) => {
|
|
|
6763
6999
|
log(` ${c.bold(cd + "lecodes compile -o app.js")} bundle it for LeCodes Desktop`);
|
|
6764
7000
|
};
|
|
6765
7001
|
|
|
7002
|
+
// src/commands/link.ts
|
|
7003
|
+
import { existsSync as existsSync17 } from "node:fs";
|
|
7004
|
+
import { basename as basename11, join as join19, resolve as resolve6 } from "node:path";
|
|
7005
|
+
|
|
7006
|
+
// src/commands/push.ts
|
|
7007
|
+
import { basename as basename10 } from "node:path";
|
|
7008
|
+
import { readFileSync as readFileSync17 } from "node:fs";
|
|
7009
|
+
var push = async (args) => {
|
|
7010
|
+
const root = findProjectRoot(process.cwd());
|
|
7011
|
+
const manifest = readManifest(root);
|
|
7012
|
+
const config = loadConfig();
|
|
7013
|
+
const apiUrl = requireApiUrl(config);
|
|
7014
|
+
const token = requireToken(config);
|
|
7015
|
+
const publish = flagBool(args, "publish");
|
|
7016
|
+
const scan = scanProject(root, manifest);
|
|
7017
|
+
const diff2 = diffLocal(manifest, scan);
|
|
7018
|
+
if (!hasChanges(diff2)) {
|
|
7019
|
+
if (publish) {
|
|
7020
|
+
await publishProject(apiUrl, token, manifest.uuid);
|
|
7021
|
+
success("No file changes — published the current project state.");
|
|
7022
|
+
} else {
|
|
7023
|
+
info(c.dim("Working tree clean — nothing to push."));
|
|
7024
|
+
}
|
|
7025
|
+
return;
|
|
7026
|
+
}
|
|
7027
|
+
const force = flagBool(args, "force");
|
|
7028
|
+
if (!force && manifest.baseHeadId !== null) {
|
|
7029
|
+
const commits = await getCommits(apiUrl, token, manifest.uuid);
|
|
7030
|
+
if (commits.headId !== manifest.baseHeadId) {
|
|
7031
|
+
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.");
|
|
7032
|
+
}
|
|
7033
|
+
if (commits.dirty) {
|
|
7034
|
+
const drift = serverPathDrift(await getProject(apiUrl, token, manifest.uuid), manifest);
|
|
7035
|
+
if (drift) {
|
|
7036
|
+
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.`);
|
|
7037
|
+
}
|
|
7038
|
+
}
|
|
7039
|
+
}
|
|
7040
|
+
if (force) {
|
|
7041
|
+
const project = await getProject(apiUrl, token, manifest.uuid);
|
|
7042
|
+
const serverDiff = computeServerDiff(project, scan);
|
|
7043
|
+
if (serverDiff.length) {
|
|
7044
|
+
info(c.yellow(`--force will overwrite the server. ${serverDiff.length} file${serverDiff.length === 1 ? "" : "s"} change:`));
|
|
7045
|
+
const label = { added: c.green(" create "), removed: c.red(" delete "), modified: c.yellow(" update ") };
|
|
7046
|
+
for (const e of serverDiff)
|
|
7047
|
+
log(label[e.status] + e.path + (e.binary ? c.dim(" (binary)") : ""));
|
|
7048
|
+
if (process.stdin.isTTY && !flagBool(args, "y", "yes")) {
|
|
7049
|
+
if (!await confirm("Overwrite the server with your local copy?", false)) {
|
|
7050
|
+
info(c.dim("Aborted."));
|
|
7051
|
+
return;
|
|
7052
|
+
}
|
|
7053
|
+
}
|
|
7054
|
+
}
|
|
7055
|
+
}
|
|
7056
|
+
const message = (flagStr(args, "m", "message") ?? await prompt("Checkpoint message")) || "Update from lecodes CLI";
|
|
7057
|
+
const sourceFor = (path) => manifest.files[path] ?? (diff2.moves.has(path) ? manifest.files[diff2.moves.get(path)] : undefined);
|
|
7058
|
+
const files = [];
|
|
7059
|
+
let uploaded = 0;
|
|
7060
|
+
for (const file of scan.files) {
|
|
7061
|
+
const source = sourceFor(file.path);
|
|
7062
|
+
const kind = source?.type ?? classifyKind(basename10(file.path));
|
|
7063
|
+
const entry = { path: file.path, type: kind };
|
|
7064
|
+
if (source)
|
|
7065
|
+
entry.assetId = source.id;
|
|
7066
|
+
if (isTextKind(kind)) {
|
|
7067
|
+
entry.text = readFileSync17(file.absPath, "utf8");
|
|
7068
|
+
} else if (!source || source.sha !== file.sha) {
|
|
7069
|
+
const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename10(file.path), readFileSync17(file.absPath));
|
|
7070
|
+
entry.blobFileId = blob.id;
|
|
7071
|
+
uploaded++;
|
|
7072
|
+
}
|
|
7073
|
+
files.push(entry);
|
|
7074
|
+
}
|
|
7075
|
+
const folders = scan.dirs.map((path) => ({ path }));
|
|
7076
|
+
if (uploaded > 0)
|
|
7077
|
+
info(c.dim(`Uploaded ${uploaded} binary file${uploaded === 1 ? "" : "s"}.`));
|
|
7078
|
+
const res = await sync(apiUrl, token, manifest.uuid, { message, files, folders, publish });
|
|
7079
|
+
if (!res.created) {
|
|
7080
|
+
info(c.dim("Server reported no changes."));
|
|
7081
|
+
if (publish)
|
|
7082
|
+
reportPublish(res);
|
|
7083
|
+
return;
|
|
7084
|
+
}
|
|
7085
|
+
const shaByPath = new Map(scan.files.map((f2) => [f2.path, f2.sha]));
|
|
7086
|
+
const newFiles = {};
|
|
7087
|
+
for (const node of res.nodes) {
|
|
7088
|
+
newFiles[node.path] = { id: node.id, type: node.type, sha: shaByPath.get(node.path) ?? "" };
|
|
7089
|
+
}
|
|
7090
|
+
writeManifest(root, { ...manifest, baseHeadId: res.headId, files: newFiles });
|
|
7091
|
+
const counts = [
|
|
7092
|
+
diff2.added.length - diff2.moves.size && `${diff2.added.length - diff2.moves.size} added`,
|
|
7093
|
+
diff2.modified.length && `${diff2.modified.length} modified`,
|
|
7094
|
+
diff2.moves.size && `${diff2.moves.size} moved`,
|
|
7095
|
+
diff2.deleted.length - diff2.moves.size && `${diff2.deleted.length - diff2.moves.size} deleted`
|
|
7096
|
+
].filter(Boolean).join(", ");
|
|
7097
|
+
success(`Pushed checkpoint${res.seq ? ` #${res.seq}` : ""}${counts ? ` (${counts})` : ""}: ${message}`);
|
|
7098
|
+
if (publish)
|
|
7099
|
+
reportPublish(res);
|
|
7100
|
+
};
|
|
7101
|
+
var serverPathDrift = (project, manifest) => {
|
|
7102
|
+
const pathById = buildPaths(project.assets);
|
|
7103
|
+
const serverPaths = new Set(project.assets.filter((a) => a.type !== "folder").map((a) => pathById.get(a.id)));
|
|
7104
|
+
const manifestPaths = new Set(Object.keys(manifest.files));
|
|
7105
|
+
const added = [...serverPaths].filter((p) => !manifestPaths.has(p));
|
|
7106
|
+
const removed = [...manifestPaths].filter((p) => !serverPaths.has(p));
|
|
7107
|
+
const describe = (label, paths) => `${paths.length} ${label} on the server: ${paths.slice(0, 3).join(", ")}${paths.length > 3 ? ", …" : ""}`;
|
|
7108
|
+
const parts = [
|
|
7109
|
+
added.length ? describe("added", added) : "",
|
|
7110
|
+
removed.length ? describe("removed", removed) : ""
|
|
7111
|
+
].filter(Boolean);
|
|
7112
|
+
return parts.length ? parts.join("; ") : null;
|
|
7113
|
+
};
|
|
7114
|
+
var reportPublish = (res) => {
|
|
7115
|
+
if (res.published)
|
|
7116
|
+
success("Published — the project is now live.");
|
|
7117
|
+
else
|
|
7118
|
+
warn(`Push landed, but publish failed: ${res.publishError ?? "unknown error"}`);
|
|
7119
|
+
};
|
|
7120
|
+
|
|
7121
|
+
// src/commands/link.ts
|
|
7122
|
+
var kindOf2 = (type) => type === "file" ? "resource" : type;
|
|
7123
|
+
var manifestFromServer = (project) => {
|
|
7124
|
+
const pathById = buildPaths(project.assets);
|
|
7125
|
+
const files = {};
|
|
7126
|
+
for (const asset of project.assets) {
|
|
7127
|
+
if (asset.type === "folder")
|
|
7128
|
+
continue;
|
|
7129
|
+
files[pathById.get(asset.id)] = {
|
|
7130
|
+
id: asset.id,
|
|
7131
|
+
type: kindOf2(asset.type),
|
|
7132
|
+
sha: asset.type === "file" ? "" : sha256(asset.text ?? "")
|
|
7133
|
+
};
|
|
7134
|
+
}
|
|
7135
|
+
return files;
|
|
7136
|
+
};
|
|
7137
|
+
var confirmAdoption = async (root, project, files, args) => {
|
|
7138
|
+
const count = Object.keys(files).length;
|
|
7139
|
+
if (count === 0) {
|
|
7140
|
+
info(c.dim(`Project ${c.bold(project.name)} is empty on the server — nothing to overwrite.`));
|
|
7141
|
+
return true;
|
|
7142
|
+
}
|
|
7143
|
+
const scan = scanProject(root, { files });
|
|
7144
|
+
const diff2 = computeServerDiff(project, scan);
|
|
7145
|
+
if (diff2.length === 0) {
|
|
7146
|
+
info(c.dim(`Project ${c.bold(project.name)} (${count} file(s)) already matches this folder.`));
|
|
7147
|
+
return true;
|
|
7148
|
+
}
|
|
7149
|
+
warn(`${c.bold(project.name)} already has ${count} file(s). Your next push would change the server:`);
|
|
7150
|
+
const label = { added: c.green(" create "), removed: c.red(" delete "), modified: c.yellow(" update ") };
|
|
7151
|
+
for (const e of diff2)
|
|
7152
|
+
log(label[e.status] + e.path + (e.binary ? c.dim(" (binary)") : ""));
|
|
7153
|
+
log("");
|
|
7154
|
+
note("`lecodes pull` after linking takes the server's version instead.");
|
|
7155
|
+
if (flagBool(args, "y", "yes") || flagBool(args, "force") || !process.stdin.isTTY)
|
|
7156
|
+
return true;
|
|
7157
|
+
return confirm("Link this folder to that project?", false);
|
|
7158
|
+
};
|
|
7159
|
+
var link = async (args) => {
|
|
7160
|
+
const config = loadConfig();
|
|
7161
|
+
const apiUrl = requireApiUrl(config);
|
|
7162
|
+
const token = requireToken(config);
|
|
7163
|
+
const dirArg = args._[0];
|
|
7164
|
+
const root = dirArg ? resolve6(dirArg) : process.cwd();
|
|
7165
|
+
if (!existsSync17(root))
|
|
7166
|
+
throw new CliError(`No such directory: ${root}`);
|
|
7167
|
+
if (existsSync17(join19(root, LECODES_DIR, "manifest.json")) && !flagBool(args, "force")) {
|
|
7168
|
+
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.");
|
|
7169
|
+
}
|
|
7170
|
+
let enclosing = null;
|
|
7171
|
+
try {
|
|
7172
|
+
enclosing = findProjectRoot(root);
|
|
7173
|
+
} catch {}
|
|
7174
|
+
if (enclosing && enclosing !== root) {
|
|
7175
|
+
throw new CliError(`This folder is inside the linked project at ${enclosing} — push from there instead.`);
|
|
7176
|
+
}
|
|
7177
|
+
const projectRef = flagStr(args, "project");
|
|
7178
|
+
let uuid;
|
|
7179
|
+
let name;
|
|
7180
|
+
let files;
|
|
7181
|
+
if (projectRef) {
|
|
7182
|
+
uuid = parseUuid(projectRef);
|
|
7183
|
+
const project = await getProject(apiUrl, token, uuid);
|
|
7184
|
+
if (project.role === "viewer") {
|
|
7185
|
+
throw new CliError(`You have view-only access to ${project.name} — linking would give you a checkout you can't push.`);
|
|
7186
|
+
}
|
|
7187
|
+
files = manifestFromServer(project);
|
|
7188
|
+
if (!await confirmAdoption(root, project, files, args)) {
|
|
7189
|
+
info(c.dim("Aborted — nothing was linked."));
|
|
7190
|
+
return;
|
|
7191
|
+
}
|
|
7192
|
+
name = project.name;
|
|
7193
|
+
} else {
|
|
7194
|
+
name = flagStr(args, "name") ?? basename11(root);
|
|
7195
|
+
info(c.dim(`Creating project ${c.bold(name)}…`));
|
|
7196
|
+
uuid = (await createProject(apiUrl, token, name, flagStr(args, "team"))).uuid;
|
|
7197
|
+
files = {};
|
|
7198
|
+
}
|
|
7199
|
+
const commits = await getCommits(apiUrl, token, uuid);
|
|
7200
|
+
writeManifest(root, { uuid, apiUrl, name, baseHeadId: commits.headId, files });
|
|
7201
|
+
log(`${c.green("linked")} ${c.bold(name)} ${c.dim(uuid)}`);
|
|
7202
|
+
if (!flagBool(args, "no-types")) {
|
|
7203
|
+
await materializeTypes(root, apiUrl);
|
|
7204
|
+
writeTsconfig(root);
|
|
7205
|
+
log(`${c.green("written")} ${LECODES_DIR}/types/ + tsconfig.json ${c.dim("(from the server)")}`);
|
|
7206
|
+
}
|
|
7207
|
+
writeDefaultIgnore(root);
|
|
7208
|
+
if (flagBool(args, "push")) {
|
|
7209
|
+
log("");
|
|
7210
|
+
if (root !== process.cwd())
|
|
7211
|
+
process.chdir(root);
|
|
7212
|
+
await push(args);
|
|
7213
|
+
return;
|
|
7214
|
+
}
|
|
7215
|
+
const pending = scanProject(root, { files }).files.filter((f2) => {
|
|
7216
|
+
const known = files[f2.path];
|
|
7217
|
+
return !known || known.sha !== f2.sha;
|
|
7218
|
+
}).length;
|
|
7219
|
+
log("");
|
|
7220
|
+
success(`Connected to ${c.bold(name)}.`);
|
|
7221
|
+
const cd = dirArg ? `cd ${dirArg} && ` : "";
|
|
7222
|
+
log(` ${c.bold(cd + "lecodes status")} ${pending} file(s) would go up`);
|
|
7223
|
+
log(` ${c.bold(cd + 'lecodes push -m "initial"')} push them as the first checkpoint`);
|
|
7224
|
+
log(` ${c.dim("(add --publish to deploy it live in the same step)")}`);
|
|
7225
|
+
};
|
|
7226
|
+
|
|
6766
7227
|
// src/commands/login.ts
|
|
6767
7228
|
var login = async (args) => {
|
|
6768
7229
|
const config = loadConfig();
|
|
@@ -6775,8 +7236,8 @@ var login = async (args) => {
|
|
|
6775
7236
|
};
|
|
6776
7237
|
|
|
6777
7238
|
// src/commands/render.ts
|
|
6778
|
-
import { mkdirSync as mkdirSync13, readFileSync as
|
|
6779
|
-
import { dirname as dirname11, resolve as
|
|
7239
|
+
import { mkdirSync as mkdirSync13, readFileSync as readFileSync18, writeFileSync as writeFileSync14 } from "node:fs";
|
|
7240
|
+
import { dirname as dirname11, resolve as resolve7 } from "node:path";
|
|
6780
7241
|
|
|
6781
7242
|
// src/compile/headlessBundle.ts
|
|
6782
7243
|
var LOCAL_ASSET_SCHEME2 = "lecodesfile://";
|
|
@@ -6877,7 +7338,7 @@ var requireRenderer = async () => {
|
|
|
6877
7338
|
};
|
|
6878
7339
|
var fileScenarioIO = (dir) => ({
|
|
6879
7340
|
write: (relPath, data) => {
|
|
6880
|
-
const abs =
|
|
7341
|
+
const abs = resolve7(dir, relPath);
|
|
6881
7342
|
mkdirSync13(dirname11(abs), { recursive: true });
|
|
6882
7343
|
writeFileSync14(abs, data);
|
|
6883
7344
|
}
|
|
@@ -6887,9 +7348,9 @@ var resolveFixtures = (value, baseDir) => {
|
|
|
6887
7348
|
return;
|
|
6888
7349
|
let map = value;
|
|
6889
7350
|
if (typeof value === "string") {
|
|
6890
|
-
const abs =
|
|
7351
|
+
const abs = resolve7(baseDir, value);
|
|
6891
7352
|
try {
|
|
6892
|
-
map = JSON.parse(
|
|
7353
|
+
map = JSON.parse(readFileSync18(abs, "utf8"));
|
|
6893
7354
|
} catch (e) {
|
|
6894
7355
|
throw new CliError(`Can't read fixtures "${value}": ${e instanceof Error ? e.message : String(e)}`);
|
|
6895
7356
|
}
|
|
@@ -6902,7 +7363,7 @@ var resolveFixtures = (value, baseDir) => {
|
|
|
6902
7363
|
var readScenarioFile = (path) => {
|
|
6903
7364
|
let raw;
|
|
6904
7365
|
try {
|
|
6905
|
-
raw = JSON.parse(
|
|
7366
|
+
raw = JSON.parse(readFileSync18(resolve7(path), "utf8"));
|
|
6906
7367
|
} catch (e) {
|
|
6907
7368
|
throw new CliError(`Can't read scenario "${path}": ${e instanceof Error ? e.message : String(e)}`);
|
|
6908
7369
|
}
|
|
@@ -6932,7 +7393,7 @@ var render = async (args) => {
|
|
|
6932
7393
|
const settleStr = flagStr(args, "settle") ?? (scenario?.settle !== undefined ? String(scenario.settle) : undefined);
|
|
6933
7394
|
const timeStr = flagStr(args, "time");
|
|
6934
7395
|
const onConsole = flagBool(args, "logs") ? (level, text2) => logErr(`${c.dim(`[${level}]`)} ${text2}`) : undefined;
|
|
6935
|
-
const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname11(
|
|
7396
|
+
const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname11(resolve7(scriptFlag)) : ".");
|
|
6936
7397
|
const flagFixtures = resolveFixtures(flagStr(args, "fixtures"), ".");
|
|
6937
7398
|
const fixtures = envelopeFixtures || flagFixtures ? { ...envelopeFixtures, ...flagFixtures } : undefined;
|
|
6938
7399
|
if (fixtures) {
|
|
@@ -6971,7 +7432,7 @@ var render = async (args) => {
|
|
|
6971
7432
|
${c.red(st.error ?? "failed")}`);
|
|
6972
7433
|
}
|
|
6973
7434
|
if (!result2.ok && result2.failureArtifacts.length > 0) {
|
|
6974
|
-
logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p) =>
|
|
7435
|
+
logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p) => resolve7(outDir, p)).join(", ")}`));
|
|
6975
7436
|
}
|
|
6976
7437
|
logErr(result2.ok ? c.green(`✓ ${result2.steps.length}/${steps.length} steps passed`) : c.red(`✗ failed at step ${result2.steps.length} of ${steps.length}`));
|
|
6977
7438
|
return done((cb) => cb(), result2.ok ? 0 : 1);
|
|
@@ -6993,7 +7454,7 @@ var render = async (args) => {
|
|
|
6993
7454
|
if (isNullSink(pngFlag)) {
|
|
6994
7455
|
note(`Discarded PNG (${pngFlag})`);
|
|
6995
7456
|
} else {
|
|
6996
|
-
writeFileSync14(
|
|
7457
|
+
writeFileSync14(resolve7(pngFlag), png);
|
|
6997
7458
|
note(`Wrote ${pngFlag} (${png.length} bytes, ${width}x${height})`);
|
|
6998
7459
|
}
|
|
6999
7460
|
return done((cb) => cb());
|
|
@@ -7008,7 +7469,7 @@ var render = async (args) => {
|
|
|
7008
7469
|
return done((cb) => cb());
|
|
7009
7470
|
}
|
|
7010
7471
|
if (out) {
|
|
7011
|
-
writeFileSync14(
|
|
7472
|
+
writeFileSync14(resolve7(out), result.text);
|
|
7012
7473
|
note(`Wrote ${out} (${result.text.length} bytes)`);
|
|
7013
7474
|
return done((cb) => cb());
|
|
7014
7475
|
}
|
|
@@ -7019,8 +7480,8 @@ var render = async (args) => {
|
|
|
7019
7480
|
};
|
|
7020
7481
|
|
|
7021
7482
|
// src/commands/pull.ts
|
|
7022
|
-
import { existsSync as
|
|
7023
|
-
import { dirname as dirname12, join as
|
|
7483
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync14, rmSync as rmSync3, writeFileSync as writeFileSync15 } from "node:fs";
|
|
7484
|
+
import { dirname as dirname12, join as join20 } from "node:path";
|
|
7024
7485
|
var pull = async (args) => {
|
|
7025
7486
|
const root = findProjectRoot(process.cwd());
|
|
7026
7487
|
const manifest = readManifest(root);
|
|
@@ -7040,9 +7501,9 @@ var pull = async (args) => {
|
|
|
7040
7501
|
const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a) => a.id === id)?.type !== "folder").map(([, path]) => path));
|
|
7041
7502
|
for (const path of Object.keys(manifest.files)) {
|
|
7042
7503
|
if (!remotePaths.has(path)) {
|
|
7043
|
-
const abs =
|
|
7044
|
-
if (
|
|
7045
|
-
|
|
7504
|
+
const abs = join20(root, ...path.replace(/^\//, "").split("/"));
|
|
7505
|
+
if (existsSync18(abs))
|
|
7506
|
+
rmSync3(abs);
|
|
7046
7507
|
}
|
|
7047
7508
|
}
|
|
7048
7509
|
const files = await materializeAssets(root, project, apiUrl, token, progress);
|
|
@@ -7055,7 +7516,7 @@ var pull = async (args) => {
|
|
|
7055
7516
|
writeManifest(root, { ...manifest, name: project.name, baseHeadId: commits.headId, files });
|
|
7056
7517
|
success(`Pulled latest into ${c.bold(manifest.name)}.`);
|
|
7057
7518
|
};
|
|
7058
|
-
var
|
|
7519
|
+
var kindOf3 = (type) => type === "file" ? "resource" : type;
|
|
7059
7520
|
var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
7060
7521
|
const pathById = buildPaths(project.assets);
|
|
7061
7522
|
const byPath = new Map;
|
|
@@ -7079,7 +7540,7 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
|
7079
7540
|
const files = { ...manifest.files };
|
|
7080
7541
|
for (const asset of targets) {
|
|
7081
7542
|
const key = pathById.get(asset.id);
|
|
7082
|
-
const abs =
|
|
7543
|
+
const abs = join20(root, ...key.replace(/^\//, "").split("/"));
|
|
7083
7544
|
mkdirSync14(dirname12(abs), { recursive: true });
|
|
7084
7545
|
let sha;
|
|
7085
7546
|
if (asset.type === "file") {
|
|
@@ -7093,7 +7554,7 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
|
7093
7554
|
writeFileSync15(abs, text);
|
|
7094
7555
|
sha = sha256(text);
|
|
7095
7556
|
}
|
|
7096
|
-
files[key] = { id: asset.id, type:
|
|
7557
|
+
files[key] = { id: asset.id, type: kindOf3(asset.type), sha };
|
|
7097
7558
|
info(c.green(` pulled ${key}`));
|
|
7098
7559
|
}
|
|
7099
7560
|
writeManifest(root, { ...manifest, files });
|
|
@@ -7101,137 +7562,22 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
|
7101
7562
|
info(c.dim("Local edits to other files are untouched. Run `lecodes diff` to review, then `lecodes push --force`."));
|
|
7102
7563
|
};
|
|
7103
7564
|
|
|
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
7565
|
// src/commands/scene.ts
|
|
7220
|
-
import { existsSync as
|
|
7221
|
-
import { basename as
|
|
7566
|
+
import { existsSync as existsSync20, readFileSync as readFileSync20, readdirSync as readdirSync9, statSync as statSync5, writeFileSync as writeFileSync16 } from "node:fs";
|
|
7567
|
+
import { basename as basename12, dirname as dirname13, join as join22, resolve as resolve8 } from "node:path";
|
|
7222
7568
|
|
|
7223
7569
|
// src/compile/sceneCompile.ts
|
|
7224
|
-
import { existsSync as
|
|
7225
|
-
import { join as
|
|
7570
|
+
import { existsSync as existsSync19, readFileSync as readFileSync19 } from "node:fs";
|
|
7571
|
+
import { join as join21 } from "node:path";
|
|
7226
7572
|
var TEXT_EXT = /\.(ts|js|json|svg|txt|md)$/i;
|
|
7227
7573
|
var rel = (key) => key.replace(/^\//, "");
|
|
7228
7574
|
var resolveHarness = (loadSceneHarness2) => {
|
|
7229
|
-
const inRepo =
|
|
7575
|
+
const inRepo = join21(distRoot, "..", "projects", "src", "scene-editor");
|
|
7230
7576
|
try {
|
|
7231
|
-
return loadSceneHarness2(
|
|
7577
|
+
return loadSceneHarness2(existsSync19(inRepo) ? inRepo : undefined);
|
|
7232
7578
|
} catch {
|
|
7233
|
-
const vendored =
|
|
7234
|
-
if (
|
|
7579
|
+
const vendored = join21(distRoot, "runtime", "scene-harness.json");
|
|
7580
|
+
if (existsSync19(vendored))
|
|
7235
7581
|
return JSON.parse(readFileSync19(vendored, "utf8"));
|
|
7236
7582
|
throw new CliError("scene-editor harness sources not found — reinstall the CLI or set LECODES_SCENE_HARNESS_DIR.");
|
|
7237
7583
|
}
|
|
@@ -7284,22 +7630,22 @@ var getContext2 = (args) => {
|
|
|
7284
7630
|
manifest = null;
|
|
7285
7631
|
const pathArg = args._[0];
|
|
7286
7632
|
if (pathArg) {
|
|
7287
|
-
const abs =
|
|
7288
|
-
if (
|
|
7633
|
+
const abs = resolve8(process.cwd(), pathArg);
|
|
7634
|
+
if (existsSync20(abs) && statSync5(abs).isDirectory())
|
|
7289
7635
|
root = abs;
|
|
7290
|
-
else if (abs.endsWith(".scene.ts") &&
|
|
7636
|
+
else if (abs.endsWith(".scene.ts") && existsSync20(abs))
|
|
7291
7637
|
root = dirname13(abs);
|
|
7292
7638
|
else
|
|
7293
7639
|
throw new CliError(`No such folder or .scene.ts: ${pathArg}`);
|
|
7294
7640
|
}
|
|
7295
7641
|
}
|
|
7296
|
-
return { root, manifest, name: manifest?.name ??
|
|
7642
|
+
return { root, manifest, name: manifest?.name ?? basename12(root) };
|
|
7297
7643
|
};
|
|
7298
7644
|
var writeMcpConfig2 = (root, port) => {
|
|
7299
|
-
const path =
|
|
7645
|
+
const path = join22(root, ".mcp.json");
|
|
7300
7646
|
const entry = { type: "http", url: `http://localhost:${port}/mcp` };
|
|
7301
7647
|
let config = {};
|
|
7302
|
-
if (
|
|
7648
|
+
if (existsSync20(path)) {
|
|
7303
7649
|
try {
|
|
7304
7650
|
config = JSON.parse(readFileSync20(path, "utf8"));
|
|
7305
7651
|
} catch {
|
|
@@ -7320,11 +7666,11 @@ var writeMcpConfig2 = (root, port) => {
|
|
|
7320
7666
|
};
|
|
7321
7667
|
var countScenes = (dir) => {
|
|
7322
7668
|
let n = 0;
|
|
7323
|
-
for (const name of
|
|
7669
|
+
for (const name of readdirSync9(dir)) {
|
|
7324
7670
|
if (name === "node_modules" || name.startsWith("."))
|
|
7325
7671
|
continue;
|
|
7326
|
-
const full =
|
|
7327
|
-
if (
|
|
7672
|
+
const full = join22(dir, name);
|
|
7673
|
+
if (statSync5(full).isDirectory())
|
|
7328
7674
|
n += countScenes(full);
|
|
7329
7675
|
else if (name.endsWith(".scene.ts"))
|
|
7330
7676
|
n++;
|
|
@@ -7394,8 +7740,8 @@ var status = async (_args) => {
|
|
|
7394
7740
|
};
|
|
7395
7741
|
|
|
7396
7742
|
// src/commands/test.ts
|
|
7397
|
-
import { existsSync as
|
|
7398
|
-
import { dirname as dirname14, resolve as
|
|
7743
|
+
import { existsSync as existsSync21, readFileSync as readFileSync21, readdirSync as readdirSync10 } from "node:fs";
|
|
7744
|
+
import { dirname as dirname14, resolve as resolve9 } from "node:path";
|
|
7399
7745
|
var deriveDesignFlows = (meta, entryScreen) => {
|
|
7400
7746
|
const edges = meta.edges ?? [];
|
|
7401
7747
|
const isState = (id) => id.includes("@");
|
|
@@ -7481,34 +7827,34 @@ var readFlowFile = (abs, displayName) => {
|
|
|
7481
7827
|
var test = async (args) => {
|
|
7482
7828
|
const { js, warnings, useLocalAssets, root } = await compileHeadlessBundle(args);
|
|
7483
7829
|
const renderer = await requireRenderer();
|
|
7484
|
-
const testsDir =
|
|
7485
|
-
const designMetaPath =
|
|
7830
|
+
const testsDir = resolve9(root, flagStr(args, "dir") ?? "tests");
|
|
7831
|
+
const designMetaPath = resolve9(root, flagStr(args, "design-dir") ?? "design", "meta.json");
|
|
7486
7832
|
const asJson = flagBool(args, "json");
|
|
7487
7833
|
const out = asJson ? logErr : log;
|
|
7488
7834
|
const cases = [];
|
|
7489
7835
|
if (args._.length > 0) {
|
|
7490
7836
|
for (const given of args._) {
|
|
7491
|
-
const candidates = [
|
|
7492
|
-
const abs = candidates.find((p) =>
|
|
7837
|
+
const candidates = [resolve9(given), resolve9(testsDir, given), resolve9(testsDir, `${given}.flow.json`)];
|
|
7838
|
+
const abs = candidates.find((p) => existsSync21(p));
|
|
7493
7839
|
if (!abs)
|
|
7494
7840
|
throw new CliError(`No such test: "${given}" (looked in ${testsDir}).`);
|
|
7495
7841
|
cases.push(readFlowFile(abs, given));
|
|
7496
7842
|
}
|
|
7497
|
-
} else if (
|
|
7498
|
-
for (const f2 of
|
|
7499
|
-
cases.push(readFlowFile(
|
|
7843
|
+
} else if (existsSync21(testsDir)) {
|
|
7844
|
+
for (const f2 of readdirSync10(testsDir).filter((f3) => f3.endsWith(".flow.json")).sort()) {
|
|
7845
|
+
cases.push(readFlowFile(resolve9(testsDir, f2), f2));
|
|
7500
7846
|
}
|
|
7501
7847
|
}
|
|
7502
7848
|
const fixturesFlag = flagStr(args, "fixtures");
|
|
7503
|
-
const defaultPack =
|
|
7504
|
-
const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") :
|
|
7849
|
+
const defaultPack = resolve9(testsDir, "fixtures.json");
|
|
7850
|
+
const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") : existsSync21(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
|
|
7505
7851
|
const flowsOnly = flagBool(args, "flows");
|
|
7506
7852
|
const wantFlows = !flagBool(args, "no-flows") && args._.length === 0;
|
|
7507
7853
|
if (flowsOnly)
|
|
7508
7854
|
cases.length = 0;
|
|
7509
7855
|
let skipped = [];
|
|
7510
7856
|
let entryScreen = null;
|
|
7511
|
-
if ((wantFlows || flowsOnly) &&
|
|
7857
|
+
if ((wantFlows || flowsOnly) && existsSync21(designMetaPath)) {
|
|
7512
7858
|
let meta;
|
|
7513
7859
|
try {
|
|
7514
7860
|
meta = JSON.parse(readFileSync21(designMetaPath, "utf8"));
|
|
@@ -7543,7 +7889,7 @@ var test = async (args) => {
|
|
|
7543
7889
|
}, timeoutMs);
|
|
7544
7890
|
killer.unref?.();
|
|
7545
7891
|
const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
|
|
7546
|
-
const artifactsRoot =
|
|
7892
|
+
const artifactsRoot = resolve9(root, flagStr(args, "artifacts") ?? resolve9(testsDir, ".artifacts"));
|
|
7547
7893
|
const results = [];
|
|
7548
7894
|
for (const kase of cases) {
|
|
7549
7895
|
const flagSettle = flagStr(args, "settle");
|
|
@@ -7561,14 +7907,14 @@ var test = async (args) => {
|
|
|
7561
7907
|
onConsole
|
|
7562
7908
|
});
|
|
7563
7909
|
try {
|
|
7564
|
-
const io = fileScenarioIO(
|
|
7910
|
+
const io = fileScenarioIO(resolve9(artifactsRoot, slug3(kase.name)));
|
|
7565
7911
|
const outcome = await renderer.runScenario(session, steps, io);
|
|
7566
7912
|
result = {
|
|
7567
7913
|
name: kase.name,
|
|
7568
7914
|
kind: kase.kind,
|
|
7569
7915
|
ok: outcome.ok,
|
|
7570
7916
|
steps: outcome.steps.map((s) => ({ desc: s.desc, ok: s.ok, error: s.error })),
|
|
7571
|
-
artifacts: outcome.failureArtifacts.map((p) =>
|
|
7917
|
+
artifacts: outcome.failureArtifacts.map((p) => resolve9(artifactsRoot, slug3(kase.name), p))
|
|
7572
7918
|
};
|
|
7573
7919
|
} finally {
|
|
7574
7920
|
session.dispose();
|
|
@@ -7624,23 +7970,86 @@ var test = async (args) => {
|
|
|
7624
7970
|
process.exit(failed > 0 ? 1 : 0);
|
|
7625
7971
|
};
|
|
7626
7972
|
|
|
7973
|
+
// src/commands/types.ts
|
|
7974
|
+
import { resolve as resolve10 } from "node:path";
|
|
7975
|
+
var label = (bundle) => `${Object.keys(bundle).length} file(s)`;
|
|
7976
|
+
var types = async (args) => {
|
|
7977
|
+
const dirArg = args._[0];
|
|
7978
|
+
const root = dirArg ? resolve10(dirArg) : (() => {
|
|
7979
|
+
try {
|
|
7980
|
+
return findProjectRoot(process.cwd());
|
|
7981
|
+
} catch {
|
|
7982
|
+
return process.cwd();
|
|
7983
|
+
}
|
|
7984
|
+
})();
|
|
7985
|
+
const useLocal = flagBool(args, "local");
|
|
7986
|
+
const checkOnly = flagBool(args, "check");
|
|
7987
|
+
const local = readLocalTypeBundle();
|
|
7988
|
+
let bundle = null;
|
|
7989
|
+
let source = "";
|
|
7990
|
+
if (useLocal) {
|
|
7991
|
+
if (!local)
|
|
7992
|
+
throw new CliError("This CLI ships no vendored type bundle — drop --local to fetch them from the server.");
|
|
7993
|
+
bundle = local;
|
|
7994
|
+
source = "the CLI's bundled SDK";
|
|
7995
|
+
} else {
|
|
7996
|
+
const apiUrl = normalizeApiUrl(flagStr(args, "api") ?? loadConfig().apiUrl ?? DEFAULT_API);
|
|
7997
|
+
try {
|
|
7998
|
+
bundle = await getTypes(apiUrl);
|
|
7999
|
+
source = apiUrl;
|
|
8000
|
+
} catch (e) {
|
|
8001
|
+
if (!local || checkOnly)
|
|
8002
|
+
throw e;
|
|
8003
|
+
warn(`${e.message}`);
|
|
8004
|
+
bundle = local;
|
|
8005
|
+
source = "the CLI's bundled SDK (server unreachable)";
|
|
8006
|
+
}
|
|
8007
|
+
}
|
|
8008
|
+
log(`${c.green("source")} ${source} — ${label(bundle)}`);
|
|
8009
|
+
const onDisk = readProjectTypeBundle(root);
|
|
8010
|
+
const same = bundleHash(onDisk) === bundleHash(bundle);
|
|
8011
|
+
if (checkOnly) {
|
|
8012
|
+
if (Object.keys(onDisk).length === 0)
|
|
8013
|
+
throw new CliError(`No types in ${LECODES_DIR}/types/ — run \`lecodes types\`.`);
|
|
8014
|
+
if (!same)
|
|
8015
|
+
throw new CliError(`Types are out of date (${label(onDisk)} on disk) — run \`lecodes types\`.`);
|
|
8016
|
+
success(`Types are up to date (${label(onDisk)}).`);
|
|
8017
|
+
return;
|
|
8018
|
+
}
|
|
8019
|
+
const stats = writeTypeBundle(root, bundle);
|
|
8020
|
+
writeTsconfig(root);
|
|
8021
|
+
const changes = [
|
|
8022
|
+
stats.added ? `${stats.added} added` : null,
|
|
8023
|
+
stats.updated ? `${stats.updated} updated` : null,
|
|
8024
|
+
stats.removed ? `${stats.removed} removed` : null
|
|
8025
|
+
].filter(Boolean);
|
|
8026
|
+
log(`${c.green("written")} ${LECODES_DIR}/types/ + tsconfig.json ${c.dim(changes.length ? `(${changes.join(", ")})` : "(already current)")}`);
|
|
8027
|
+
if (!useLocal && local && bundleHash(local) !== bundleHash(bundle)) {
|
|
8028
|
+
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.");
|
|
8029
|
+
}
|
|
8030
|
+
log("");
|
|
8031
|
+
success(stats.added + stats.updated + stats.removed > 0 ? "Types synced." : "Types already current.");
|
|
8032
|
+
if (!dirArg)
|
|
8033
|
+
note("Reload the TypeScript server in your editor to pick them up.");
|
|
8034
|
+
};
|
|
8035
|
+
|
|
7627
8036
|
// src/index.ts
|
|
7628
|
-
import { existsSync as
|
|
8037
|
+
import { existsSync as existsSync23, readFileSync as readFileSync23, realpathSync as realpathSync2 } from "node:fs";
|
|
7629
8038
|
import { createRequire as createRequire4 } from "node:module";
|
|
7630
|
-
import { delimiter as delimiter2, dirname as dirname16, join as
|
|
8039
|
+
import { delimiter as delimiter2, dirname as dirname16, join as join24 } from "node:path";
|
|
7631
8040
|
|
|
7632
8041
|
// src/commands/install.ts
|
|
7633
8042
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
7634
|
-
import { appendFileSync as appendFileSync2, chmodSync, cpSync as cpSync2, existsSync as
|
|
8043
|
+
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
8044
|
import { homedir as homedir3 } from "node:os";
|
|
7636
|
-
import { basename as
|
|
8045
|
+
import { basename as basename13, delimiter, dirname as dirname15, join as join23, resolve as resolve11, sep as sep2 } from "node:path";
|
|
7637
8046
|
var isWin = process.platform === "win32";
|
|
7638
8047
|
var HOME = homedir3();
|
|
7639
8048
|
var safeRealpath = (p) => {
|
|
7640
8049
|
try {
|
|
7641
8050
|
return realpathSync(p);
|
|
7642
8051
|
} catch {
|
|
7643
|
-
return
|
|
8052
|
+
return resolve11(p);
|
|
7644
8053
|
}
|
|
7645
8054
|
};
|
|
7646
8055
|
var onPath = (dir) => {
|
|
@@ -7657,12 +8066,12 @@ var onPath = (dir) => {
|
|
|
7657
8066
|
};
|
|
7658
8067
|
var install = async (args) => {
|
|
7659
8068
|
const bundle = dirname15(process.execPath);
|
|
7660
|
-
const exeName =
|
|
8069
|
+
const exeName = basename13(process.execPath);
|
|
7661
8070
|
const chiselName = isWin ? "chisel.exe" : "chisel";
|
|
7662
|
-
if (!
|
|
8071
|
+
if (!existsSync22(join23(bundle, "runtime")) || !existsSync22(join23(bundle, chiselName))) {
|
|
7663
8072
|
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
8073
|
}
|
|
7665
|
-
const companions = [exeName, chiselName, "runtime", "README.txt"].filter((f2) =>
|
|
8074
|
+
const companions = [exeName, chiselName, "runtime", "README.txt"].filter((f2) => existsSync22(join23(bundle, f2)));
|
|
7666
8075
|
if (isWin)
|
|
7667
8076
|
return installWindows(args, bundle, companions);
|
|
7668
8077
|
return installUnix(args, bundle, companions);
|
|
@@ -7672,9 +8081,9 @@ var copyBundle = (bundle, target, companions) => {
|
|
|
7672
8081
|
if (safeRealpath(target) === safeRealpath(bundle))
|
|
7673
8082
|
return;
|
|
7674
8083
|
for (const f2 of companions) {
|
|
7675
|
-
const dest =
|
|
7676
|
-
|
|
7677
|
-
cpSync2(
|
|
8084
|
+
const dest = join23(target, f2);
|
|
8085
|
+
rmSync4(dest, { recursive: true, force: true });
|
|
8086
|
+
cpSync2(join23(bundle, f2), dest, { recursive: true });
|
|
7678
8087
|
}
|
|
7679
8088
|
};
|
|
7680
8089
|
var permError = (e, dir, system) => {
|
|
@@ -7686,41 +8095,41 @@ var permError = (e, dir, system) => {
|
|
|
7686
8095
|
};
|
|
7687
8096
|
var installUnix = (args, bundle, companions) => {
|
|
7688
8097
|
const system = flagBool(args, "system");
|
|
7689
|
-
const libDir =
|
|
7690
|
-
const binDir =
|
|
7691
|
-
const
|
|
8098
|
+
const libDir = resolve11(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join23(HOME, ".local", "lib", "lecodes-cli")));
|
|
8099
|
+
const binDir = resolve11(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join23(HOME, ".local", "bin")));
|
|
8100
|
+
const link2 = join23(binDir, "lecodes");
|
|
7692
8101
|
try {
|
|
7693
8102
|
copyBundle(bundle, libDir, companions);
|
|
7694
|
-
chmodSync(
|
|
7695
|
-
chmodSync(
|
|
8103
|
+
chmodSync(join23(libDir, "lecodes"), 493);
|
|
8104
|
+
chmodSync(join23(libDir, "chisel"), 493);
|
|
7696
8105
|
} catch (e) {
|
|
7697
8106
|
throw permError(e, libDir, system);
|
|
7698
8107
|
}
|
|
7699
8108
|
try {
|
|
7700
8109
|
mkdirSync15(binDir, { recursive: true });
|
|
7701
|
-
|
|
7702
|
-
symlinkSync(
|
|
8110
|
+
rmSync4(link2, { force: true });
|
|
8111
|
+
symlinkSync(join23(libDir, "lecodes"), link2);
|
|
7703
8112
|
} catch (e) {
|
|
7704
8113
|
throw permError(e, binDir, system);
|
|
7705
8114
|
}
|
|
7706
8115
|
success(`Installed to ${c.bold(libDir)}`);
|
|
7707
|
-
log(` ${c.dim(
|
|
8116
|
+
log(` ${c.dim(link2 + " → " + join23(libDir, "lecodes"))}`);
|
|
7708
8117
|
finishPathUnix(args, binDir);
|
|
7709
8118
|
};
|
|
7710
8119
|
var shellRc = () => {
|
|
7711
|
-
const shell =
|
|
8120
|
+
const shell = basename13(process.env.SHELL ?? "");
|
|
7712
8121
|
if (shell === "zsh")
|
|
7713
|
-
return { file:
|
|
8122
|
+
return { file: join23(HOME, ".zshrc"), name: "~/.zshrc" };
|
|
7714
8123
|
if (shell === "bash")
|
|
7715
|
-
return { file:
|
|
7716
|
-
return { file:
|
|
8124
|
+
return { file: join23(HOME, ".bashrc"), name: "~/.bashrc" };
|
|
8125
|
+
return { file: join23(HOME, ".profile"), name: "~/.profile" };
|
|
7717
8126
|
};
|
|
7718
8127
|
var PATH_MARKER = "# Added by `lecodes install`";
|
|
7719
8128
|
var appendPathToShellRc = (binDir) => {
|
|
7720
8129
|
const { file, name } = shellRc();
|
|
7721
8130
|
const line = `export PATH="${binDir}:$PATH"`;
|
|
7722
8131
|
try {
|
|
7723
|
-
const existing =
|
|
8132
|
+
const existing = existsSync22(file) ? readFileSync22(file, "utf8") : "";
|
|
7724
8133
|
if (existing.includes(line))
|
|
7725
8134
|
return name;
|
|
7726
8135
|
appendFileSync2(file, `${existing && !existing.endsWith(`
|
|
@@ -7758,7 +8167,7 @@ var finishPathUnix = (args, binDir) => {
|
|
|
7758
8167
|
log(` • or install system-wide (no PATH edit): ${c.bold("sudo lecodes install --system")}`);
|
|
7759
8168
|
};
|
|
7760
8169
|
var installWindows = (args, bundle, companions) => {
|
|
7761
|
-
const target =
|
|
8170
|
+
const target = resolve11(flagStr(args, "dir") ?? join23(process.env.LOCALAPPDATA ?? join23(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
|
|
7762
8171
|
try {
|
|
7763
8172
|
copyBundle(bundle, target, companions);
|
|
7764
8173
|
} catch (e) {
|
|
@@ -7797,7 +8206,7 @@ var addToUserPathWindows = (dir) => {
|
|
|
7797
8206
|
var STANDALONE = (() => {
|
|
7798
8207
|
const dir = dirname16(process.execPath);
|
|
7799
8208
|
const chisel = process.platform === "win32" ? "chisel.exe" : "chisel";
|
|
7800
|
-
return
|
|
8209
|
+
return existsSync23(join24(dir, "runtime")) && existsSync23(join24(dir, chisel));
|
|
7801
8210
|
})();
|
|
7802
8211
|
var INSTALLED = STANDALONE && (() => {
|
|
7803
8212
|
const exe = process.platform === "win32" ? "lecodes.exe" : "lecodes";
|
|
@@ -7812,8 +8221,8 @@ var INSTALLED = STANDALONE && (() => {
|
|
|
7812
8221
|
if (!d)
|
|
7813
8222
|
return false;
|
|
7814
8223
|
try {
|
|
7815
|
-
const cand =
|
|
7816
|
-
return
|
|
8224
|
+
const cand = join24(d, exe);
|
|
8225
|
+
return existsSync23(cand) && realpathSync2(cand) === self;
|
|
7817
8226
|
} catch {
|
|
7818
8227
|
return false;
|
|
7819
8228
|
}
|
|
@@ -7844,6 +8253,19 @@ ${c.bold("Commands:")}
|
|
|
7844
8253
|
--team <uuid> create it inside a team
|
|
7845
8254
|
clone <uuid|url> [dir] Download a project's files + types into a new folder
|
|
7846
8255
|
--no-types skip the .d.ts / tsconfig generation
|
|
8256
|
+
link [dir] Connect an existing local folder (e.g. one made by 'lecodes
|
|
8257
|
+
init') to the platform — the reverse of clone. Creates a
|
|
8258
|
+
project on the server, writes .lecodes/manifest.json and
|
|
8259
|
+
refreshes the types; nothing is uploaded until you push
|
|
8260
|
+
--name <s> project name (default: folder name)
|
|
8261
|
+
--team <uuid> create it inside a team
|
|
8262
|
+
--project <uuid|url> adopt an EXISTING project instead —
|
|
8263
|
+
prints what your next push would change
|
|
8264
|
+
on its server tree, and confirms first
|
|
8265
|
+
--push [-m <msg>] push right after linking (--publish to
|
|
8266
|
+
deploy it live in the same step)
|
|
8267
|
+
--force re-link a folder that's already linked
|
|
8268
|
+
--no-types skip the .d.ts / tsconfig refresh
|
|
7847
8269
|
status Show local changes vs the last sync
|
|
7848
8270
|
diff [path…] Show a unified diff of local vs the current server state
|
|
7849
8271
|
--name-only list changed paths + status only
|
|
@@ -7907,6 +8329,13 @@ ${c.bold("Commands:")}
|
|
|
7907
8329
|
--artifacts <dir> failure-artifact folder override
|
|
7908
8330
|
(+ the render timing flags: --width/--height, --settle,
|
|
7909
8331
|
--device, --safe-area, --wait-network, --logs, --entry)
|
|
8332
|
+
types [dir] Refresh the IDE types (.lecodes/types/ + tsconfig.json) — by
|
|
8333
|
+
default from the server, so a project scaffolded offline picks
|
|
8334
|
+
up the SDK the platform actually runs
|
|
8335
|
+
--local use the SDK bundled in this CLI instead
|
|
8336
|
+
--check report drift only, don't write (non-zero exit)
|
|
8337
|
+
--api <url> server to fetch from (default: the logged-in
|
|
8338
|
+
one, else https://le.codes)
|
|
7910
8339
|
design [serve] LeCodes Design — a live canvas of screens for AI-driven prototyping
|
|
7911
8340
|
(needs the optional 'lecodes-design' package; works inside a
|
|
7912
8341
|
cloned project or standalone in any folder)
|
|
@@ -7933,6 +8362,9 @@ ${c.bold("Commands:")}
|
|
|
7933
8362
|
--variant <v> SDK variant: auto (default — sync picks the
|
|
7934
8363
|
smallest of core/2d/3d/full covering what
|
|
7935
8364
|
the bundle uses), or one of those to pin
|
|
8365
|
+
--sdk <v> native SDK release (default: the newest one,
|
|
8366
|
+
pinned in app.json ios.sdk); 'latest' floats
|
|
8367
|
+
it — every sync re-resolves
|
|
7936
8368
|
app sync Make the shell match the project: compile the bundle + embed
|
|
7937
8369
|
its assets into ios/App/Resources/ (offline by default, also
|
|
7938
8370
|
for server projects; app.json "resources": {"remote": [globs]}
|
|
@@ -7941,6 +8373,13 @@ ${c.bold("Commands:")}
|
|
|
7941
8373
|
plugins (pinned in plugins.lock.json), regenerate the
|
|
7942
8374
|
CLI-owned shell files. app.json "update": true (or
|
|
7943
8375
|
{"url": …}) enables native OTA JS updates at launch
|
|
8376
|
+
app update Move the shell's pins forward — app.json ios.sdk to the newest
|
|
8377
|
+
lecodes-ios-sdk release, plugins.lock.json to each plugin's
|
|
8378
|
+
newest tag — then re-sync
|
|
8379
|
+
--sdk <v|latest> pin that release instead of the newest
|
|
8380
|
+
--check report available updates, write nothing
|
|
8381
|
+
--no-plugins only move the SDK pin
|
|
8382
|
+
--no-sync update the pins without re-syncing
|
|
7944
8383
|
app plugin <list|add|remove> Manage native plugins in app.json (then run 'lecodes app sync')
|
|
7945
8384
|
add takes a known id (camera, qr-scanner) or github:owner/repo[@tag]
|
|
7946
8385
|
app open [ios] Open the native shell in its IDE (Xcode; Android Studio later)
|
|
@@ -7955,7 +8394,7 @@ ${c.bold("Commands:")}
|
|
|
7955
8394
|
ASC API key; stored in ~/.lecodes/asc.json
|
|
7956
8395
|
|
|
7957
8396
|
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 };
|
|
8397
|
+
var commands = { init: init2, login, create, clone, link, status, diff, pull, push, compile: compile2, preview: previewLegacy, render, test, types, design, scene, dev, app };
|
|
7959
8398
|
if (STANDALONE)
|
|
7960
8399
|
commands.install = install;
|
|
7961
8400
|
var readVersion = () => package_default.version ?? "unknown";
|
|
@@ -7969,8 +8408,8 @@ var peerVersion = (name, probe) => {
|
|
|
7969
8408
|
const require2 = createRequire4(import.meta.url);
|
|
7970
8409
|
let dir = dirname16(require2.resolve(probe));
|
|
7971
8410
|
for (let depth = 0;depth < 16; depth++) {
|
|
7972
|
-
const manifest =
|
|
7973
|
-
if (
|
|
8411
|
+
const manifest = join24(dir, "package.json");
|
|
8412
|
+
if (existsSync23(manifest)) {
|
|
7974
8413
|
const parsed = JSON.parse(readFileSync23(manifest, "utf8"));
|
|
7975
8414
|
if (parsed.name === name)
|
|
7976
8415
|
return parsed.version ?? "unknown";
|
|
@@ -7989,9 +8428,9 @@ var printVersion = () => {
|
|
|
7989
8428
|
const version = peerVersion(name, probe);
|
|
7990
8429
|
rows.push([name, version ?? c.dim("not installed")]);
|
|
7991
8430
|
}
|
|
7992
|
-
const width = Math.max(...rows.map(([
|
|
7993
|
-
for (const [
|
|
7994
|
-
log(`${
|
|
8431
|
+
const width = Math.max(...rows.map(([label2]) => label2.length));
|
|
8432
|
+
for (const [label2, value] of rows)
|
|
8433
|
+
log(`${label2.padEnd(width)} ${value}`);
|
|
7995
8434
|
};
|
|
7996
8435
|
var main = async () => {
|
|
7997
8436
|
const argv = process.argv.slice(2);
|