lecodes-cli 0.10.2 → 0.10.4
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 +1 -0
- package/dist/index.js +480 -69
- package/package.json +1 -1
- package/runtime/sdk/inject.ts +1 -0
- package/runtime/sdk/plugins/push.ts +132 -0
- package/runtime/sdk/runtime/app.ts +13 -0
- package/runtime/sdk/runtime/channel.ts +14 -3
- package/runtime/sdk/runtime/service.ts +1 -1
- package/runtime/sdk/ui/NativeView.ts +1 -1
- package/runtime/sdk/ui/UIVirtualizedList.ts +7 -1
- package/runtime/sdk/ui/UIWidget.ts +2 -1
- package/runtime/sdk-types.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2129,7 +2129,7 @@ var require_main = __commonJS((exports, module) => {
|
|
|
2129
2129
|
// package.json
|
|
2130
2130
|
var package_default = {
|
|
2131
2131
|
name: "lecodes-cli",
|
|
2132
|
-
version: "0.10.
|
|
2132
|
+
version: "0.10.4",
|
|
2133
2133
|
dependencies: {
|
|
2134
2134
|
"@letary/chisel": "^0.6.0"
|
|
2135
2135
|
},
|
|
@@ -2210,8 +2210,10 @@ var c = {
|
|
|
2210
2210
|
yellow: (s) => paint("33", s),
|
|
2211
2211
|
cyan: (s) => paint("36", s)
|
|
2212
2212
|
};
|
|
2213
|
-
var log = (msg = "") =>
|
|
2213
|
+
var log = (msg = "") => {
|
|
2214
|
+
process.stdout.write(msg + `
|
|
2214
2215
|
`);
|
|
2216
|
+
};
|
|
2215
2217
|
var info = (msg) => log(msg);
|
|
2216
2218
|
var success = (msg) => log(c.green("✓ ") + msg);
|
|
2217
2219
|
var warn = (msg) => log(c.yellow("! ") + msg);
|
|
@@ -2523,6 +2525,12 @@ var sync = (apiUrl, token, uuid, payload) => apiRequest(apiUrl, `/projects/${uui
|
|
|
2523
2525
|
var publishProject = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}/publish`, { token, body: {} });
|
|
2524
2526
|
var enableDesignShare = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}/design/share`, { token, body: {} });
|
|
2525
2527
|
var disableDesignShare = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}/design/share`, { token, method: "DELETE" });
|
|
2528
|
+
var sendPush = (apiUrl, token, uuid, payload) => apiRequest(apiUrl, `/projects/${uuid}/push/send`, { token, body: payload });
|
|
2529
|
+
var listPushDevices = (apiUrl, token, uuid, user) => apiRequest(apiUrl, `/projects/${uuid}/push/registrations${user ? `?user=${encodeURIComponent(user)}` : ""}`, { token });
|
|
2530
|
+
var forgetPushDevice = (apiUrl, token, uuid, address) => apiRequest(apiUrl, `/projects/${uuid}/push/registrations/${encodeURIComponent(address)}`, { token, method: "DELETE" });
|
|
2531
|
+
var listPushKeys = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}/push/keys`, { token });
|
|
2532
|
+
var createPushKey = (apiUrl, token, uuid, name) => apiRequest(apiUrl, `/projects/${uuid}/push/keys`, { token, body: { name } });
|
|
2533
|
+
var revokePushKey = (apiUrl, token, uuid, id) => apiRequest(apiUrl, `/projects/${uuid}/push/keys/${id}`, { token, method: "DELETE" });
|
|
2526
2534
|
var downloadBlob = async (apiUrl, src) => {
|
|
2527
2535
|
const resp = await fetch(`${apiUrl}${src}`);
|
|
2528
2536
|
if (!resp.ok)
|
|
@@ -3047,6 +3055,7 @@ var pbxprojTemplate = ({ displayName, bundleId, teamId, version, buildNumber })
|
|
|
3047
3055
|
const target = `
|
|
3048
3056
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
3049
3057
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
|
3058
|
+
CODE_SIGN_ENTITLEMENTS = "App/App.entitlements";
|
|
3050
3059
|
CODE_SIGN_STYLE = Automatic;
|
|
3051
3060
|
CURRENT_PROJECT_VERSION = ${buildNumber};
|
|
3052
3061
|
DEVELOPMENT_TEAM = "${pbxStr(teamId)}";
|
|
@@ -3154,6 +3163,7 @@ var pbxprojTemplate = ({ displayName, bundleId, teamId, version, buildNumber })
|
|
|
3154
3163
|
B73550792FEBAC100005B4CC /* Exceptions for "App" folder in "App" target */ = {
|
|
3155
3164
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
|
3156
3165
|
membershipExceptions = (
|
|
3166
|
+
App.entitlements,
|
|
3157
3167
|
Info.plist,
|
|
3158
3168
|
);
|
|
3159
3169
|
target = B73550662FEBAC0F0005B4CC /* App */;
|
|
@@ -3433,14 +3443,30 @@ var APP_DELEGATE = `//
|
|
|
3433
3443
|
//
|
|
3434
3444
|
|
|
3435
3445
|
import UIKit
|
|
3446
|
+
import LeCodesRuntime
|
|
3436
3447
|
|
|
3437
3448
|
@main
|
|
3438
3449
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
|
3439
3450
|
|
|
3440
3451
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
|
3452
|
+
// Plugin hook surface: installs the notification-center delegate (must be in
|
|
3453
|
+
// place before the app finishes launching) and buffers launch-time events until
|
|
3454
|
+
// a plugin that needs them registers. Inert if no installed plugin consumes it.
|
|
3455
|
+
LeCodesAppHooks.install()
|
|
3441
3456
|
return true
|
|
3442
3457
|
}
|
|
3443
3458
|
|
|
3459
|
+
// APNs token callbacks → the plugin hook surface (consumed by the push plugin).
|
|
3460
|
+
func application(_ application: UIApplication,
|
|
3461
|
+
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
|
3462
|
+
LeCodesAppHooks.remoteNotificationsToken(deviceToken)
|
|
3463
|
+
}
|
|
3464
|
+
|
|
3465
|
+
func application(_ application: UIApplication,
|
|
3466
|
+
didFailToRegisterForRemoteNotificationsWithError error: Error) {
|
|
3467
|
+
LeCodesAppHooks.remoteNotificationsError(error)
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3444
3470
|
// MARK: UISceneSession Lifecycle
|
|
3445
3471
|
|
|
3446
3472
|
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
|
@@ -3454,6 +3480,7 @@ var SCENE_DELEGATE = `//
|
|
|
3454
3480
|
//
|
|
3455
3481
|
|
|
3456
3482
|
import UIKit
|
|
3483
|
+
import LeCodesRuntime
|
|
3457
3484
|
|
|
3458
3485
|
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|
3459
3486
|
|
|
@@ -3466,6 +3493,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|
|
3466
3493
|
) {
|
|
3467
3494
|
guard let windowScene = (scene as? UIWindowScene) else { return }
|
|
3468
3495
|
|
|
3496
|
+
// Cold-started by a notification tap: buffered by the hooks; a plugin that
|
|
3497
|
+
// consumes it (push) drains the buffer when it registers — before the JS world
|
|
3498
|
+
// boots, because makeKeyAndVisible() runs registerBundledPlugins via viewDidLoad.
|
|
3499
|
+
if let response = connectionOptions.notificationResponse {
|
|
3500
|
+
LeCodesAppHooks.coldStartNotification(response)
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3469
3503
|
let window = UIWindow(windowScene: windowScene)
|
|
3470
3504
|
window.rootViewController = ViewController()
|
|
3471
3505
|
self.window = window
|
|
@@ -3519,6 +3553,47 @@ class ViewController: LeCodesViewController {
|
|
|
3519
3553
|
}
|
|
3520
3554
|
}
|
|
3521
3555
|
`;
|
|
3556
|
+
var plistValue = (value, indent) => {
|
|
3557
|
+
if (typeof value === "string")
|
|
3558
|
+
return `<string>${value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")}</string>`;
|
|
3559
|
+
if (typeof value === "boolean")
|
|
3560
|
+
return value ? "<true/>" : "<false/>";
|
|
3561
|
+
if (typeof value === "number")
|
|
3562
|
+
return Number.isInteger(value) ? `<integer>${value}</integer>` : `<real>${value}</real>`;
|
|
3563
|
+
if (Array.isArray(value)) {
|
|
3564
|
+
if (value.length === 0)
|
|
3565
|
+
return "<array/>";
|
|
3566
|
+
return `<array>
|
|
3567
|
+
${value.map((v) => `${indent} ${plistValue(v, indent + "\t")}`).join(`
|
|
3568
|
+
`)}
|
|
3569
|
+
${indent}</array>`;
|
|
3570
|
+
}
|
|
3571
|
+
if (value && typeof value === "object") {
|
|
3572
|
+
const entries = Object.entries(value);
|
|
3573
|
+
if (entries.length === 0)
|
|
3574
|
+
return "<dict/>";
|
|
3575
|
+
return `<dict>
|
|
3576
|
+
${entries.map(([k, v]) => `${indent} <key>${k}</key>
|
|
3577
|
+
${indent} ${plistValue(v, indent + "\t")}`).join(`
|
|
3578
|
+
`)}
|
|
3579
|
+
${indent}</dict>`;
|
|
3580
|
+
}
|
|
3581
|
+
return "<string></string>";
|
|
3582
|
+
};
|
|
3583
|
+
var entitlementsTemplate = (entries) => {
|
|
3584
|
+
const keys = Object.keys(entries).sort();
|
|
3585
|
+
const body = keys.length === 0 ? "" : keys.map((k) => ` <key>${k}</key>
|
|
3586
|
+
${plistValue(entries[k], "\t")}`).join(`
|
|
3587
|
+
`) + `
|
|
3588
|
+
`;
|
|
3589
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
3590
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3591
|
+
<plist version="1.0">
|
|
3592
|
+
<dict>
|
|
3593
|
+
${body}</dict>
|
|
3594
|
+
</plist>
|
|
3595
|
+
`;
|
|
3596
|
+
};
|
|
3522
3597
|
var infoPlistTemplate = (pluginKeys, updateUrl) => {
|
|
3523
3598
|
const xml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3524
3599
|
const keys = Object.keys(pluginKeys).sort();
|
|
@@ -3684,12 +3759,17 @@ let package = Package(
|
|
|
3684
3759
|
)
|
|
3685
3760
|
`;
|
|
3686
3761
|
};
|
|
3687
|
-
var runtimeSwiftTemplate = (plugins) => {
|
|
3762
|
+
var runtimeSwiftTemplate = (plugins, projectUuid) => {
|
|
3688
3763
|
const imports = plugins.map((p) => `import ${p.module}
|
|
3689
3764
|
`).join("");
|
|
3690
|
-
const
|
|
3691
|
-
|
|
3765
|
+
const identity = !projectUuid ? "" : ` // World identity: this shell IS the project — the engine attributes the boot world so
|
|
3766
|
+
// identity-gated services (push registration) see the app's uuid from its first
|
|
3767
|
+
// synchronous statement. Baked from .lecodes/manifest.json by \`lecodes app sync\`.
|
|
3768
|
+
engine.bootProjectUuid = "${projectUuid}"
|
|
3769
|
+
`;
|
|
3770
|
+
const calls = plugins.map((p) => ` ${p.register}.register(in: engine)
|
|
3692
3771
|
`).join("");
|
|
3772
|
+
const body = identity + calls;
|
|
3693
3773
|
return `//
|
|
3694
3774
|
// LeCodesRuntime.swift
|
|
3695
3775
|
// GENERATED by lecodes — regenerated by \`lecodes app sync\`; do not edit.
|
|
@@ -3701,7 +3781,8 @@ var runtimeSwiftTemplate = (plugins) => {
|
|
|
3701
3781
|
@_exported import LeCodesSDK
|
|
3702
3782
|
${imports}
|
|
3703
3783
|
public func registerBundledPlugins(in engine: LeCodesEngine) {
|
|
3704
|
-
${
|
|
3784
|
+
${body === "" ? `
|
|
3785
|
+
` : body}}
|
|
3705
3786
|
`;
|
|
3706
3787
|
};
|
|
3707
3788
|
var IOS_README = `# ios/ — native Xcode shell
|
|
@@ -3754,6 +3835,7 @@ CLI-owned — regenerated on every sync, hand edits are overwritten:
|
|
|
3754
3835
|
the app runs fully offline. Globs in app.json \`"resources": {"remote": [...]}\` opt heavy
|
|
3755
3836
|
assets (videos, big models) back into streaming from the server instead.
|
|
3756
3837
|
- \`App/Info.plist\` — template + the installed plugins' usage keys
|
|
3838
|
+
- \`App/App.entitlements\` — the capabilities the installed plugins declare (e.g. push)
|
|
3757
3839
|
- \`LeCodesRuntime/\` — local Swift package: the LeCodesSDK dependency, vendored plugin
|
|
3758
3840
|
sources (Sources/<Module>/), and the registerBundledPlugins entry point
|
|
3759
3841
|
- the version values inside \`App.xcodeproj/project.pbxproj\` (from app.json)
|
|
@@ -3788,9 +3870,12 @@ buildNumber) — edit it there, then re-run \`lecodes app sync\`. Plugins are ma
|
|
|
3788
3870
|
var SDK_REPO = "letary/lecodes-ios-sdk";
|
|
3789
3871
|
var SDK_MIN_VERSION = "1.2.0";
|
|
3790
3872
|
var SDK_VARIANTS = ["core", "2d", "3d", "full"];
|
|
3873
|
+
var SDK_IDENTITY_VERSION = "1.6.0";
|
|
3791
3874
|
var FIRST_PARTY_PLUGINS = {
|
|
3792
3875
|
camera: "letary/lecodes-plugins",
|
|
3793
|
-
"qr-scanner": "letary/lecodes-plugins"
|
|
3876
|
+
"qr-scanner": "letary/lecodes-plugins",
|
|
3877
|
+
geolocation: "letary/lecodes-plugins",
|
|
3878
|
+
push: "letary/lecodes-plugins"
|
|
3794
3879
|
};
|
|
3795
3880
|
var readJson = (path) => {
|
|
3796
3881
|
if (!existsSync9(path))
|
|
@@ -3994,6 +4079,7 @@ ios/
|
|
|
3994
4079
|
write("App/SceneDelegate.swift", SCENE_DELEGATE);
|
|
3995
4080
|
write("App/ViewController.swift", VIEW_CONTROLLER);
|
|
3996
4081
|
write("App/Info.plist", infoPlistTemplate({}));
|
|
4082
|
+
write("App/App.entitlements", entitlementsTemplate({}));
|
|
3997
4083
|
write("App/Base.lproj/LaunchScreen.storyboard", LAUNCH_STORYBOARD);
|
|
3998
4084
|
write("App/Assets.xcassets/Contents.json", ASSETS_ROOT_JSON);
|
|
3999
4085
|
write("App/Assets.xcassets/AccentColor.colorset/Contents.json", ACCENT_COLOR_JSON);
|
|
@@ -4096,6 +4182,7 @@ var appSync = async (_args) => {
|
|
|
4096
4182
|
const pluginSpecs = Object.entries(config.plugins ?? {});
|
|
4097
4183
|
const runtimePlugins = [];
|
|
4098
4184
|
const infoValues = {};
|
|
4185
|
+
const entitlementValues = {};
|
|
4099
4186
|
const newLock = {};
|
|
4100
4187
|
const sourcesDir = join11(iosDir, "LeCodesRuntime", "Sources");
|
|
4101
4188
|
for (const [id, spec] of pluginSpecs) {
|
|
@@ -4112,11 +4199,14 @@ var appSync = async (_args) => {
|
|
|
4112
4199
|
rmSync(dest, { recursive: true, force: true });
|
|
4113
4200
|
cpSync(src, dest, { recursive: true });
|
|
4114
4201
|
if (ios.minSdk && VERSION_RE.test(ios.minSdk) && cmpVersion(sdkVersion, ios.minSdk) < 0) {
|
|
4115
|
-
|
|
4202
|
+
throw new CliError(`Plugin "${id}" needs lecodes-ios-sdk ${ios.minSdk} but the shell links ${sdkVersion} — run \`lecodes app update\` (or set app.json ios.sdk).`);
|
|
4116
4203
|
}
|
|
4117
4204
|
for (const [key, value] of Object.entries(ios.infoPlist ?? {})) {
|
|
4118
4205
|
(infoValues[key] ??= []).push(value);
|
|
4119
4206
|
}
|
|
4207
|
+
for (const [key, value] of Object.entries(ios.entitlements ?? {})) {
|
|
4208
|
+
(entitlementValues[key] ??= []).push(value);
|
|
4209
|
+
}
|
|
4120
4210
|
runtimePlugins.push({ id, module, register });
|
|
4121
4211
|
newLock[id] = plugin.lock;
|
|
4122
4212
|
log(`${c.green("plugin")} ${id}@${plugin.lock.version} (${plugin.lock.tag} → Sources/${module})`);
|
|
@@ -4186,9 +4276,21 @@ var appSync = async (_args) => {
|
|
|
4186
4276
|
const streamed = resources.filter((r) => r.remote);
|
|
4187
4277
|
if (streamed.length > 0)
|
|
4188
4278
|
log(`${c.green("remote")} ${streamed.length} resource(s) stay on the server (resources.remote): ${streamed.map((r) => r.path).join(", ")}`);
|
|
4279
|
+
let shellUuid;
|
|
4280
|
+
try {
|
|
4281
|
+
shellUuid = readManifest(root).uuid;
|
|
4282
|
+
} catch {}
|
|
4283
|
+
if (shellUuid && cmpVersion(sdkVersion, SDK_IDENTITY_VERSION) < 0) {
|
|
4284
|
+
if (runtimePlugins.some((p) => p.id === "push")) {
|
|
4285
|
+
warn(`push needs the shell's world identity, which lecodes-ios-sdk ${SDK_IDENTITY_VERSION}+ carries — this shell links ${sdkVersion}, so Push.register() will reject "unavailable". Run \`lecodes app update\` when ${SDK_IDENTITY_VERSION} is out.`);
|
|
4286
|
+
}
|
|
4287
|
+
shellUuid = undefined;
|
|
4288
|
+
}
|
|
4289
|
+
if (shellUuid)
|
|
4290
|
+
log(`${c.green("identity")} project ${shellUuid} baked into the shell (world identity)`);
|
|
4189
4291
|
writeFileSync7(join11(iosDir, "README.md"), IOS_README);
|
|
4190
4292
|
writeFileSync7(join11(iosDir, "LeCodesRuntime", "Package.swift"), runtimePackageTemplate(sdkVersion, variant, runtimePlugins));
|
|
4191
|
-
writeFileSync7(join11(sourcesDir, "LeCodesRuntime", "LeCodesRuntime.swift"), runtimeSwiftTemplate(runtimePlugins));
|
|
4293
|
+
writeFileSync7(join11(sourcesDir, "LeCodesRuntime", "LeCodesRuntime.swift"), runtimeSwiftTemplate(runtimePlugins, shellUuid));
|
|
4192
4294
|
const mergedKeys = {};
|
|
4193
4295
|
for (const [key, values] of Object.entries(infoValues))
|
|
4194
4296
|
mergedKeys[key] = [...new Set(values)].join(" ");
|
|
@@ -4199,7 +4301,43 @@ var appSync = async (_args) => {
|
|
|
4199
4301
|
if (pluginSpecs.length > 0 || existsSync9(lockPath))
|
|
4200
4302
|
writeJson(lockPath, { plugins: newLock });
|
|
4201
4303
|
const pbxPath = join11(iosDir, "App.xcodeproj", "project.pbxproj");
|
|
4202
|
-
|
|
4304
|
+
let 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};`);
|
|
4305
|
+
const mergedEntitlements = {};
|
|
4306
|
+
for (const [key, values] of Object.entries(entitlementValues)) {
|
|
4307
|
+
if (values.every((v) => Array.isArray(v))) {
|
|
4308
|
+
const seen = new Set;
|
|
4309
|
+
mergedEntitlements[key] = values.flat().filter((v) => {
|
|
4310
|
+
const s = JSON.stringify(v);
|
|
4311
|
+
if (seen.has(s))
|
|
4312
|
+
return false;
|
|
4313
|
+
seen.add(s);
|
|
4314
|
+
return true;
|
|
4315
|
+
});
|
|
4316
|
+
} else {
|
|
4317
|
+
if (new Set(values.map((v) => JSON.stringify(v))).size > 1) {
|
|
4318
|
+
warn(`entitlement ${key}: plugins declare conflicting values — keeping ${JSON.stringify(values[0])}`);
|
|
4319
|
+
}
|
|
4320
|
+
mergedEntitlements[key] = values[0];
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4323
|
+
const hasEntitlements = Object.keys(mergedEntitlements).length > 0;
|
|
4324
|
+
if (pbx.includes("CODE_SIGN_ENTITLEMENTS") || hasEntitlements) {
|
|
4325
|
+
writeFileSync7(join11(iosDir, "App", "App.entitlements"), entitlementsTemplate(mergedEntitlements));
|
|
4326
|
+
if (hasEntitlements)
|
|
4327
|
+
log(`${c.green("caps")} App/App.entitlements: ${Object.keys(mergedEntitlements).sort().join(", ")}`);
|
|
4328
|
+
if (!pbx.includes("CODE_SIGN_ENTITLEMENTS")) {
|
|
4329
|
+
const patched = pbx.replace(/^(\s*)CODE_SIGN_STYLE = /gm, `$1CODE_SIGN_ENTITLEMENTS = "App/App.entitlements";
|
|
4330
|
+
$1CODE_SIGN_STYLE = `);
|
|
4331
|
+
if (patched === pbx) {
|
|
4332
|
+
warn(`couldn't wire App.entitlements into the pbxproj — add CODE_SIGN_ENTITLEMENTS = "App/App.entitlements" to the App target's build settings in Xcode.`);
|
|
4333
|
+
} else {
|
|
4334
|
+
pbx = patched.replace(/^(\s*)membershipExceptions = \(\n(\s*)Info\.plist,/m, `$1membershipExceptions = (
|
|
4335
|
+
$2App.entitlements,
|
|
4336
|
+
$2Info.plist,`);
|
|
4337
|
+
log(`${c.green("updated")} App.xcodeproj: CODE_SIGN_ENTITLEMENTS → App/App.entitlements`);
|
|
4338
|
+
}
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4203
4341
|
writeFileSync7(pbxPath, pbx);
|
|
4204
4342
|
log("");
|
|
4205
4343
|
success(`Shell synced: app.js + ${runtimePlugins.length} plugin(s), SDK ${sdkVersion}, variant ${variant}, v${config.version ?? "1.0.0"} (${config.ios.buildNumber ?? 1}).`);
|
|
@@ -4426,6 +4564,15 @@ var appDoctor = async (_args) => {
|
|
|
4426
4564
|
const wanted = Object.keys(config.plugins ?? {});
|
|
4427
4565
|
const inSync = wanted.every((id) => lock[id]) && Object.keys(lock).every((id) => wanted.includes(id));
|
|
4428
4566
|
check(wanted.length === 0 || inSync, "Plugins synced", "run `lecodes app sync`");
|
|
4567
|
+
const pbxFile = join11(root, "ios", "App.xcodeproj", "project.pbxproj");
|
|
4568
|
+
if (existsSync9(pbxFile) && readFileSync10(pbxFile, "utf8").includes("CODE_SIGN_ENTITLEMENTS")) {
|
|
4569
|
+
check(existsSync9(join11(root, "ios", "App", "App.entitlements")), "App/App.entitlements present", "run `lecodes app sync`");
|
|
4570
|
+
}
|
|
4571
|
+
if (wanted.includes("push")) {
|
|
4572
|
+
const runtimeSwift = join11(root, "ios", "LeCodesRuntime", "Sources", "LeCodesRuntime", "LeCodesRuntime.swift");
|
|
4573
|
+
const baked = existsSync9(runtimeSwift) && readFileSync10(runtimeSwift, "utf8").includes("bootProjectUuid");
|
|
4574
|
+
check(baked, "Push world identity baked", `without it Push.register() rejects "unavailable" — needs a pushed project (uuid), lecodes-ios-sdk ${SDK_IDENTITY_VERSION}+ and \`lecodes app sync\``, false);
|
|
4575
|
+
}
|
|
4429
4576
|
if (config.update) {
|
|
4430
4577
|
const plist = readFileSync10(join11(root, "ios", "App", "Info.plist"), "utf8");
|
|
4431
4578
|
check(plist.includes("<key>LeCodesUpdateURL</key>"), "OTA update URL in Info.plist", "run `lecodes app sync`", false);
|
|
@@ -7546,9 +7693,264 @@ var render = async (args) => {
|
|
|
7546
7693
|
return done((cb) => process.stdout.write(text, cb));
|
|
7547
7694
|
};
|
|
7548
7695
|
|
|
7696
|
+
// src/commands/pn.ts
|
|
7697
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync18, readFileSync as readFileSync19, writeFileSync as writeFileSync15 } from "node:fs";
|
|
7698
|
+
import { join as join19 } from "node:path";
|
|
7699
|
+
var ENV_VAR = "LECODES_PUSH_KEY";
|
|
7700
|
+
var context = () => {
|
|
7701
|
+
const root = findProjectRoot(process.cwd());
|
|
7702
|
+
const manifest = readManifest(root);
|
|
7703
|
+
const config = loadConfig();
|
|
7704
|
+
return { root, uuid: manifest.uuid, name: manifest.name, apiUrl: requireApiUrl(config), token: requireToken(config) };
|
|
7705
|
+
};
|
|
7706
|
+
var ago = (iso) => {
|
|
7707
|
+
if (!iso)
|
|
7708
|
+
return c.dim("never");
|
|
7709
|
+
const ms = Date.now() - new Date(iso).getTime();
|
|
7710
|
+
if (!Number.isFinite(ms))
|
|
7711
|
+
return c.dim("—");
|
|
7712
|
+
const mins = Math.round(ms / 60000);
|
|
7713
|
+
if (mins < 1)
|
|
7714
|
+
return "just now";
|
|
7715
|
+
if (mins < 60)
|
|
7716
|
+
return `${mins}m ago`;
|
|
7717
|
+
const hours = Math.round(mins / 60);
|
|
7718
|
+
if (hours < 24)
|
|
7719
|
+
return `${hours}h ago`;
|
|
7720
|
+
return `${Math.round(hours / 24)}d ago`;
|
|
7721
|
+
};
|
|
7722
|
+
var ANSI = /\x1b\[[0-9;]*m/g;
|
|
7723
|
+
var printed = (s) => s.replace(ANSI, "").length;
|
|
7724
|
+
var table = (rows) => {
|
|
7725
|
+
const widths = rows[0].map((_, i) => Math.max(...rows.map((r) => printed(r[i] ?? ""))));
|
|
7726
|
+
for (const row of rows) {
|
|
7727
|
+
const cells = row.map((cell, i) => i === row.length - 1 ? cell : cell + " ".repeat(Math.max(0, widths[i] - printed(cell))));
|
|
7728
|
+
log(" " + cells.join(" "));
|
|
7729
|
+
}
|
|
7730
|
+
};
|
|
7731
|
+
var listFlag = (args, ...keys) => {
|
|
7732
|
+
const raw = flagStr(args, ...keys);
|
|
7733
|
+
if (!raw)
|
|
7734
|
+
return [];
|
|
7735
|
+
return [...new Set(raw.split(",").map((s) => s.trim()).filter(Boolean))];
|
|
7736
|
+
};
|
|
7737
|
+
var pnSend = async (args) => {
|
|
7738
|
+
const ctx = context();
|
|
7739
|
+
const title = flagStr(args, "title", "t") ?? args._[1];
|
|
7740
|
+
if (!title)
|
|
7741
|
+
throw new CliError('A title is required: lecodes pn send --title "Order shipped" --user u42');
|
|
7742
|
+
const users = listFlag(args, "user", "users", "u");
|
|
7743
|
+
const to = listFlag(args, "to", "address", "addresses");
|
|
7744
|
+
if (users.length === 0 && to.length === 0) {
|
|
7745
|
+
throw new CliError("Nothing to send to — pass --user <id> (from Push.register({ user })) or --to <pa_…>. `lecodes pn devices` lists both.");
|
|
7746
|
+
}
|
|
7747
|
+
const badgeRaw = flagStr(args, "badge");
|
|
7748
|
+
const badge = badgeRaw === undefined ? undefined : Number(badgeRaw);
|
|
7749
|
+
if (badge !== undefined && !Number.isInteger(badge))
|
|
7750
|
+
throw new CliError(`--badge must be a whole number (got "${badgeRaw}")`);
|
|
7751
|
+
const dataRaw = flagStr(args, "data");
|
|
7752
|
+
let data;
|
|
7753
|
+
if (dataRaw !== undefined) {
|
|
7754
|
+
try {
|
|
7755
|
+
data = JSON.parse(dataRaw);
|
|
7756
|
+
} catch {
|
|
7757
|
+
throw new CliError(`--data must be a JSON object, e.g. --data '{"orderId":42}'`);
|
|
7758
|
+
}
|
|
7759
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
7760
|
+
throw new CliError(`--data must be a JSON object, e.g. --data '{"orderId":42}'`);
|
|
7761
|
+
}
|
|
7762
|
+
const payload = {
|
|
7763
|
+
title,
|
|
7764
|
+
body: flagStr(args, "body", "b"),
|
|
7765
|
+
url: flagStr(args, "url"),
|
|
7766
|
+
users: users.length ? users : undefined,
|
|
7767
|
+
to: to.length ? to : undefined,
|
|
7768
|
+
data,
|
|
7769
|
+
badge
|
|
7770
|
+
};
|
|
7771
|
+
const result = await sendPush(ctx.apiUrl, ctx.token, ctx.uuid, payload);
|
|
7772
|
+
if (result.sent > 0)
|
|
7773
|
+
success(`Sent to ${result.sent} device${result.sent === 1 ? "" : "s"}.`);
|
|
7774
|
+
if (result.failed.length === 0) {
|
|
7775
|
+
if (result.sent === 0) {
|
|
7776
|
+
warn("Matched 0 devices — nobody with that user id has registered. Run `lecodes pn devices` to see who has.");
|
|
7777
|
+
}
|
|
7778
|
+
return;
|
|
7779
|
+
}
|
|
7780
|
+
warn(`${result.failed.length} failed:`);
|
|
7781
|
+
for (const failure of result.failed)
|
|
7782
|
+
log(` ${c.dim(failure.address)} ${c.red(failure.reason)}`);
|
|
7783
|
+
};
|
|
7784
|
+
var deviceLabel = (device) => {
|
|
7785
|
+
const env = device.environment === "sandbox" ? c.yellow(" sandbox") : "";
|
|
7786
|
+
return `${device.platform}${env}`;
|
|
7787
|
+
};
|
|
7788
|
+
var pnDevices = async (args) => {
|
|
7789
|
+
const ctx = context();
|
|
7790
|
+
const user = flagStr(args, "user", "u");
|
|
7791
|
+
const forget = flagStr(args, "forget");
|
|
7792
|
+
if (forget) {
|
|
7793
|
+
if (!flagBool(args, "yes", "y")) {
|
|
7794
|
+
warn(`Forgetting ${c.bold(forget)} — sends to this address stop immediately. The device gets a NEW address on its next Push.register().`);
|
|
7795
|
+
if (!await confirm("Forget it?"))
|
|
7796
|
+
return info("Cancelled.");
|
|
7797
|
+
}
|
|
7798
|
+
await forgetPushDevice(ctx.apiUrl, ctx.token, ctx.uuid, forget);
|
|
7799
|
+
success(`Forgot ${c.bold(forget)}.`);
|
|
7800
|
+
return;
|
|
7801
|
+
}
|
|
7802
|
+
const { devices, truncated } = await listPushDevices(ctx.apiUrl, ctx.token, ctx.uuid, user);
|
|
7803
|
+
if (devices.length === 0) {
|
|
7804
|
+
info(user ? `No devices registered for user ${c.bold(user)} in ${c.bold(ctx.name)}.` : `No devices registered in ${c.bold(ctx.name)} yet.`);
|
|
7805
|
+
info(c.dim("A device appears here after the app calls Push.register(). Note that `lecodes dev`"));
|
|
7806
|
+
info(c.dim("bundles are unattributed — register() rejects there; open a PUBLISHED project via QR."));
|
|
7807
|
+
return;
|
|
7808
|
+
}
|
|
7809
|
+
info(`${devices.length} device${devices.length === 1 ? "" : "s"} in ${c.bold(ctx.name)}${user ? ` for user ${c.bold(user)}` : ""}:`);
|
|
7810
|
+
table([
|
|
7811
|
+
[c.dim("ADDRESS"), c.dim("PLATFORM"), c.dim("USER"), c.dim("HOST"), c.dim("LAST SEEN")],
|
|
7812
|
+
...devices.map((device) => [
|
|
7813
|
+
device.disabledAt ? c.dim(device.address) : device.address,
|
|
7814
|
+
deviceLabel(device),
|
|
7815
|
+
device.user ?? c.dim("—"),
|
|
7816
|
+
device.hostApp,
|
|
7817
|
+
device.disabledAt ? c.red("disabled") : ago(device.lastSeenAt)
|
|
7818
|
+
])
|
|
7819
|
+
]);
|
|
7820
|
+
if (devices.some((d) => d.disabledAt)) {
|
|
7821
|
+
info(c.dim("Disabled = the provider reported a dead token; re-registering from the device revives it."));
|
|
7822
|
+
}
|
|
7823
|
+
if (truncated)
|
|
7824
|
+
warn("Showing the 200 most recently seen — there are more.");
|
|
7825
|
+
};
|
|
7826
|
+
var pnKeysList = async (ctx) => {
|
|
7827
|
+
const keys = await listPushKeys(ctx.apiUrl, ctx.token, ctx.uuid);
|
|
7828
|
+
if (keys.length === 0) {
|
|
7829
|
+
info(`No push keys in ${c.bold(ctx.name)}.`);
|
|
7830
|
+
info(c.dim("You only need one to send from your OWN server — `lecodes pn send` uses your login."));
|
|
7831
|
+
info(c.dim('Mint one with `lecodes pn keys new "prod server" --env`.'));
|
|
7832
|
+
return;
|
|
7833
|
+
}
|
|
7834
|
+
info(`${keys.length} push key${keys.length === 1 ? "" : "s"} in ${c.bold(ctx.name)}:`);
|
|
7835
|
+
table([
|
|
7836
|
+
[c.dim("ID"), c.dim("NAME"), c.dim("KEY"), c.dim("LAST USED")],
|
|
7837
|
+
...keys.map((key) => [String(key.id), key.name, `${key.prefix}${c.dim("…")}`, ago(key.lastUsedAt)])
|
|
7838
|
+
]);
|
|
7839
|
+
};
|
|
7840
|
+
var writeEnv = (root, key) => {
|
|
7841
|
+
const touched = [];
|
|
7842
|
+
const envPath = join19(root, ".env");
|
|
7843
|
+
const line = `${ENV_VAR}=${key}`;
|
|
7844
|
+
const existing = existsSync18(envPath) ? readFileSync19(envPath, "utf8") : "";
|
|
7845
|
+
if (new RegExp(`^${ENV_VAR}=`, "m").test(existing)) {
|
|
7846
|
+
writeFileSync15(envPath, existing.replace(new RegExp(`^${ENV_VAR}=.*$`, "m"), line));
|
|
7847
|
+
} else {
|
|
7848
|
+
const separator = !existing || existing.endsWith(`
|
|
7849
|
+
`) ? "" : `
|
|
7850
|
+
`;
|
|
7851
|
+
appendFileSync2(envPath, `${separator}${line}
|
|
7852
|
+
`);
|
|
7853
|
+
}
|
|
7854
|
+
touched.push(".env");
|
|
7855
|
+
const ignorePath = join19(root, ".gitignore");
|
|
7856
|
+
const ignore = existsSync18(ignorePath) ? readFileSync19(ignorePath, "utf8") : "";
|
|
7857
|
+
if (!ignore.split(`
|
|
7858
|
+
`).some((l) => l.trim() === ".env")) {
|
|
7859
|
+
const separator = !ignore || ignore.endsWith(`
|
|
7860
|
+
`) ? "" : `
|
|
7861
|
+
`;
|
|
7862
|
+
appendFileSync2(ignorePath, `${separator}.env
|
|
7863
|
+
`);
|
|
7864
|
+
touched.push(".gitignore");
|
|
7865
|
+
}
|
|
7866
|
+
return touched;
|
|
7867
|
+
};
|
|
7868
|
+
var pnKeysNew = async (ctx, args) => {
|
|
7869
|
+
const name = args._[2] ?? flagStr(args, "name");
|
|
7870
|
+
const created = await createPushKey(ctx.apiUrl, ctx.token, ctx.uuid, name);
|
|
7871
|
+
success(`Created push key ${c.bold(created.name)} (id ${created.id}).`);
|
|
7872
|
+
log();
|
|
7873
|
+
log(` ${c.bold(created.key)}`);
|
|
7874
|
+
log();
|
|
7875
|
+
if (flagBool(args, "env")) {
|
|
7876
|
+
const touched = writeEnv(ctx.root, created.key);
|
|
7877
|
+
success(`Wrote ${ENV_VAR} to ${touched.join(" + ")}.`);
|
|
7878
|
+
} else {
|
|
7879
|
+
warn("Copy it now — it is never shown again. Lose it and you mint a new one.");
|
|
7880
|
+
info(c.dim(` Tip: \`lecodes pn keys new --env\` writes it straight into .env as ${ENV_VAR}.`));
|
|
7881
|
+
}
|
|
7882
|
+
log();
|
|
7883
|
+
info(c.dim("Send from your server with:"));
|
|
7884
|
+
log(c.dim(` curl -X POST ${ctx.apiUrl}/api/projects/${ctx.uuid}/push/send \\`));
|
|
7885
|
+
log(c.dim(` -H "Authorization: Bearer $${ENV_VAR}" -H "Content-Type: application/json" \\`));
|
|
7886
|
+
log(c.dim(` -d '{"title":"Order shipped","users":["u42"]}'`));
|
|
7887
|
+
};
|
|
7888
|
+
var pnKeysRevoke = async (ctx, args) => {
|
|
7889
|
+
const raw = args._[2];
|
|
7890
|
+
if (!raw)
|
|
7891
|
+
throw new CliError("Which key? `lecodes pn keys` lists their ids, then `lecodes pn keys revoke <id>`.");
|
|
7892
|
+
const id = Number(raw);
|
|
7893
|
+
if (!Number.isInteger(id))
|
|
7894
|
+
throw new CliError(`"${raw}" is not a key id — run \`lecodes pn keys\` to see them.`);
|
|
7895
|
+
const keys = await listPushKeys(ctx.apiUrl, ctx.token, ctx.uuid);
|
|
7896
|
+
const key = keys.find((k) => k.id === id);
|
|
7897
|
+
if (!key)
|
|
7898
|
+
throw new CliError(`No push key with id ${id} in ${ctx.name}.`);
|
|
7899
|
+
if (!flagBool(args, "yes", "y")) {
|
|
7900
|
+
warn(`Revoking ${c.bold(key.name)} (${key.prefix}…) — any server still using it stops sending immediately.`);
|
|
7901
|
+
if (!await confirm("Revoke it?"))
|
|
7902
|
+
return info("Cancelled.");
|
|
7903
|
+
}
|
|
7904
|
+
await revokePushKey(ctx.apiUrl, ctx.token, ctx.uuid, id);
|
|
7905
|
+
success(`Revoked push key ${c.bold(key.name)}.`);
|
|
7906
|
+
};
|
|
7907
|
+
var pnKeys = async (args) => {
|
|
7908
|
+
const ctx = context();
|
|
7909
|
+
const sub = args._[1];
|
|
7910
|
+
if (!sub || sub === "list")
|
|
7911
|
+
return pnKeysList(ctx);
|
|
7912
|
+
if (sub === "new" || sub === "create")
|
|
7913
|
+
return pnKeysNew(ctx, args);
|
|
7914
|
+
if (sub === "revoke" || sub === "delete" || sub === "rm")
|
|
7915
|
+
return pnKeysRevoke(ctx, args);
|
|
7916
|
+
throw new CliError(`Unknown keys command "${sub}" — use: lecodes pn keys [list | new [name] | revoke <id>]`);
|
|
7917
|
+
};
|
|
7918
|
+
var PN_HELP = `${c.bold("lecodes pn")} — push notifications for this project.
|
|
7919
|
+
|
|
7920
|
+
${c.bold("Commands:")}
|
|
7921
|
+
pn send --title <s> Send a notification (uses your login — no key needed)
|
|
7922
|
+
--body <s> second line
|
|
7923
|
+
--url <s> deep link (also arrives as app.launchUrl)
|
|
7924
|
+
--user <ids> target Push.register({ user }) ids, comma-separated
|
|
7925
|
+
--to <pa_…> target addresses, comma-separated
|
|
7926
|
+
--badge <n> app icon badge
|
|
7927
|
+
--data <json> your JSON payload (≤ 2 KB)
|
|
7928
|
+
pn devices List the devices that registered for this project
|
|
7929
|
+
--user <id> only that user's devices
|
|
7930
|
+
--forget <pa_…> drop a dead registration (-y to skip confirm)
|
|
7931
|
+
pn keys List this project's push keys
|
|
7932
|
+
pn keys new [name] Mint one for YOUR server (owner only; shown once)
|
|
7933
|
+
--env write it to .env as ${ENV_VAR} (+ gitignore)
|
|
7934
|
+
pn keys revoke <id> Revoke a key (-y to skip the confirmation)
|
|
7935
|
+
|
|
7936
|
+
${c.dim("A push key is generated BY the server — you can't choose or recover one. It only sends")}
|
|
7937
|
+
${c.dim("for this project, which is why deploying it beats deploying a personal access token.")}`;
|
|
7938
|
+
var pn = async (args) => {
|
|
7939
|
+
const sub = args._[0];
|
|
7940
|
+
if (!sub || sub === "help")
|
|
7941
|
+
return log(PN_HELP);
|
|
7942
|
+
if (sub === "send")
|
|
7943
|
+
return pnSend(args);
|
|
7944
|
+
if (sub === "devices" || sub === "device")
|
|
7945
|
+
return pnDevices(args);
|
|
7946
|
+
if (sub === "keys" || sub === "key")
|
|
7947
|
+
return pnKeys(args);
|
|
7948
|
+
throw new CliError(`Unknown pn command "${sub}" — use: lecodes pn <send | devices | keys>. Run \`lecodes pn\` for the full list.`);
|
|
7949
|
+
};
|
|
7950
|
+
|
|
7549
7951
|
// src/commands/pull.ts
|
|
7550
|
-
import { existsSync as
|
|
7551
|
-
import { dirname as dirname12, join as
|
|
7952
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync14, rmSync as rmSync3, writeFileSync as writeFileSync16 } from "node:fs";
|
|
7953
|
+
import { dirname as dirname12, join as join20 } from "node:path";
|
|
7552
7954
|
var pull = async (args) => {
|
|
7553
7955
|
const root = findProjectRoot(process.cwd());
|
|
7554
7956
|
const manifest = readManifest(root);
|
|
@@ -7568,8 +7970,8 @@ var pull = async (args) => {
|
|
|
7568
7970
|
const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a) => a.id === id)?.type !== "folder").map(([, path]) => path));
|
|
7569
7971
|
for (const path of Object.keys(manifest.files)) {
|
|
7570
7972
|
if (!remotePaths.has(path)) {
|
|
7571
|
-
const abs =
|
|
7572
|
-
if (
|
|
7973
|
+
const abs = join20(root, ...path.replace(/^\//, "").split("/"));
|
|
7974
|
+
if (existsSync19(abs))
|
|
7573
7975
|
rmSync3(abs);
|
|
7574
7976
|
}
|
|
7575
7977
|
}
|
|
@@ -7607,18 +8009,18 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
|
7607
8009
|
const files = { ...manifest.files };
|
|
7608
8010
|
for (const asset of targets) {
|
|
7609
8011
|
const key = pathById.get(asset.id);
|
|
7610
|
-
const abs =
|
|
8012
|
+
const abs = join20(root, ...key.replace(/^\//, "").split("/"));
|
|
7611
8013
|
mkdirSync14(dirname12(abs), { recursive: true });
|
|
7612
8014
|
let sha;
|
|
7613
8015
|
if (asset.type === "file") {
|
|
7614
8016
|
if (!asset.file?.src)
|
|
7615
8017
|
continue;
|
|
7616
8018
|
const bytes = await downloadBlob(apiUrl, asset.file.src);
|
|
7617
|
-
|
|
8019
|
+
writeFileSync16(abs, bytes);
|
|
7618
8020
|
sha = sha256(bytes);
|
|
7619
8021
|
} else {
|
|
7620
8022
|
const text = asset.text ?? "";
|
|
7621
|
-
|
|
8023
|
+
writeFileSync16(abs, text);
|
|
7622
8024
|
sha = sha256(text);
|
|
7623
8025
|
}
|
|
7624
8026
|
files[key] = { id: asset.id, type: kindOf3(asset.type), sha };
|
|
@@ -7630,22 +8032,22 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
|
7630
8032
|
};
|
|
7631
8033
|
|
|
7632
8034
|
// src/commands/scene.ts
|
|
7633
|
-
import { existsSync as
|
|
7634
|
-
import { basename as basename12, dirname as dirname13, join as
|
|
8035
|
+
import { existsSync as existsSync21, readFileSync as readFileSync21, readdirSync as readdirSync9, statSync as statSync5, writeFileSync as writeFileSync17 } from "node:fs";
|
|
8036
|
+
import { basename as basename12, dirname as dirname13, join as join22, resolve as resolve8 } from "node:path";
|
|
7635
8037
|
|
|
7636
8038
|
// src/compile/sceneCompile.ts
|
|
7637
|
-
import { existsSync as
|
|
7638
|
-
import { join as
|
|
8039
|
+
import { existsSync as existsSync20, readFileSync as readFileSync20 } from "node:fs";
|
|
8040
|
+
import { join as join21 } from "node:path";
|
|
7639
8041
|
var TEXT_EXT = /\.(ts|js|json|svg|txt|md)$/i;
|
|
7640
8042
|
var rel = (key) => key.replace(/^\//, "");
|
|
7641
8043
|
var resolveHarness = (loadSceneHarness2) => {
|
|
7642
|
-
const inRepo =
|
|
8044
|
+
const inRepo = join21(distRoot, "..", "projects", "src", "scene-editor");
|
|
7643
8045
|
try {
|
|
7644
|
-
return loadSceneHarness2(
|
|
8046
|
+
return loadSceneHarness2(existsSync20(inRepo) ? inRepo : undefined);
|
|
7645
8047
|
} catch {
|
|
7646
|
-
const vendored =
|
|
7647
|
-
if (
|
|
7648
|
-
return JSON.parse(
|
|
8048
|
+
const vendored = join21(distRoot, "runtime", "scene-harness.json");
|
|
8049
|
+
if (existsSync20(vendored))
|
|
8050
|
+
return JSON.parse(readFileSync20(vendored, "utf8"));
|
|
7649
8051
|
throw new CliError("scene-editor harness sources not found — reinstall the CLI or set LECODES_SCENE_HARNESS_DIR.");
|
|
7650
8052
|
}
|
|
7651
8053
|
};
|
|
@@ -7657,7 +8059,7 @@ var compileSceneEditor = async (opts) => {
|
|
|
7657
8059
|
for (const file of scan.files) {
|
|
7658
8060
|
const path = "./" + rel(file.path);
|
|
7659
8061
|
if (TEXT_EXT.test(path)) {
|
|
7660
|
-
entries.push({ path, type: "text", text:
|
|
8062
|
+
entries.push({ path, type: "text", text: readFileSync20(file.absPath, "utf8") });
|
|
7661
8063
|
} else {
|
|
7662
8064
|
entries.push({ path, type: "resource", fileSrc: "/project/" + rel(file.path) });
|
|
7663
8065
|
}
|
|
@@ -7698,9 +8100,9 @@ var getContext2 = (args) => {
|
|
|
7698
8100
|
const pathArg = args._[0];
|
|
7699
8101
|
if (pathArg) {
|
|
7700
8102
|
const abs = resolve8(process.cwd(), pathArg);
|
|
7701
|
-
if (
|
|
8103
|
+
if (existsSync21(abs) && statSync5(abs).isDirectory())
|
|
7702
8104
|
root = abs;
|
|
7703
|
-
else if (abs.endsWith(".scene.ts") &&
|
|
8105
|
+
else if (abs.endsWith(".scene.ts") && existsSync21(abs))
|
|
7704
8106
|
root = dirname13(abs);
|
|
7705
8107
|
else
|
|
7706
8108
|
throw new CliError(`No such folder or .scene.ts: ${pathArg}`);
|
|
@@ -7709,12 +8111,12 @@ var getContext2 = (args) => {
|
|
|
7709
8111
|
return { root, manifest, name: manifest?.name ?? basename12(root) };
|
|
7710
8112
|
};
|
|
7711
8113
|
var writeMcpConfig2 = (root, port) => {
|
|
7712
|
-
const path =
|
|
8114
|
+
const path = join22(root, ".mcp.json");
|
|
7713
8115
|
const entry = { type: "http", url: `http://localhost:${port}/mcp` };
|
|
7714
8116
|
let config = {};
|
|
7715
|
-
if (
|
|
8117
|
+
if (existsSync21(path)) {
|
|
7716
8118
|
try {
|
|
7717
|
-
config = JSON.parse(
|
|
8119
|
+
config = JSON.parse(readFileSync21(path, "utf8"));
|
|
7718
8120
|
} catch {
|
|
7719
8121
|
warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-scene server by hand.");
|
|
7720
8122
|
return;
|
|
@@ -7727,7 +8129,7 @@ var writeMcpConfig2 = (root, port) => {
|
|
|
7727
8129
|
return;
|
|
7728
8130
|
}
|
|
7729
8131
|
config.mcpServers["lecodes-scene"] = entry;
|
|
7730
|
-
|
|
8132
|
+
writeFileSync17(path, JSON.stringify(config, null, 2) + `
|
|
7731
8133
|
`);
|
|
7732
8134
|
note(`${existing ? "updated" : "created"} .mcp.json (lecodes-scene MCP → ${entry.url})`);
|
|
7733
8135
|
};
|
|
@@ -7736,7 +8138,7 @@ var countScenes = (dir) => {
|
|
|
7736
8138
|
for (const name of readdirSync9(dir)) {
|
|
7737
8139
|
if (name === "node_modules" || name.startsWith("."))
|
|
7738
8140
|
continue;
|
|
7739
|
-
const full =
|
|
8141
|
+
const full = join22(dir, name);
|
|
7740
8142
|
if (statSync5(full).isDirectory())
|
|
7741
8143
|
n += countScenes(full);
|
|
7742
8144
|
else if (name.endsWith(".scene.ts"))
|
|
@@ -7807,7 +8209,7 @@ var status = async (_args) => {
|
|
|
7807
8209
|
};
|
|
7808
8210
|
|
|
7809
8211
|
// src/commands/test.ts
|
|
7810
|
-
import { existsSync as
|
|
8212
|
+
import { existsSync as existsSync22, readFileSync as readFileSync22, readdirSync as readdirSync10 } from "node:fs";
|
|
7811
8213
|
import { dirname as dirname14, resolve as resolve9 } from "node:path";
|
|
7812
8214
|
var deriveDesignFlows = (meta, entryScreen) => {
|
|
7813
8215
|
const edges = meta.edges ?? [];
|
|
@@ -7869,7 +8271,7 @@ var slug3 = (s) => s.replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "").sl
|
|
|
7869
8271
|
var readFlowFile = (abs, displayName) => {
|
|
7870
8272
|
let raw;
|
|
7871
8273
|
try {
|
|
7872
|
-
raw = JSON.parse(
|
|
8274
|
+
raw = JSON.parse(readFileSync22(abs, "utf8"));
|
|
7873
8275
|
} catch (e) {
|
|
7874
8276
|
throw new CliError(`Can't read "${displayName}": ${e instanceof Error ? e.message : String(e)}`);
|
|
7875
8277
|
}
|
|
@@ -7902,29 +8304,29 @@ var test = async (args) => {
|
|
|
7902
8304
|
if (args._.length > 0) {
|
|
7903
8305
|
for (const given of args._) {
|
|
7904
8306
|
const candidates = [resolve9(given), resolve9(testsDir, given), resolve9(testsDir, `${given}.flow.json`)];
|
|
7905
|
-
const abs = candidates.find((p) =>
|
|
8307
|
+
const abs = candidates.find((p) => existsSync22(p));
|
|
7906
8308
|
if (!abs)
|
|
7907
8309
|
throw new CliError(`No such test: "${given}" (looked in ${testsDir}).`);
|
|
7908
8310
|
cases.push(readFlowFile(abs, given));
|
|
7909
8311
|
}
|
|
7910
|
-
} else if (
|
|
8312
|
+
} else if (existsSync22(testsDir)) {
|
|
7911
8313
|
for (const f2 of readdirSync10(testsDir).filter((f3) => f3.endsWith(".flow.json")).sort()) {
|
|
7912
8314
|
cases.push(readFlowFile(resolve9(testsDir, f2), f2));
|
|
7913
8315
|
}
|
|
7914
8316
|
}
|
|
7915
8317
|
const fixturesFlag = flagStr(args, "fixtures");
|
|
7916
8318
|
const defaultPack = resolve9(testsDir, "fixtures.json");
|
|
7917
|
-
const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") :
|
|
8319
|
+
const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") : existsSync22(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
|
|
7918
8320
|
const flowsOnly = flagBool(args, "flows");
|
|
7919
8321
|
const wantFlows = !flagBool(args, "no-flows") && args._.length === 0;
|
|
7920
8322
|
if (flowsOnly)
|
|
7921
8323
|
cases.length = 0;
|
|
7922
8324
|
let skipped = [];
|
|
7923
8325
|
let entryScreen = null;
|
|
7924
|
-
if ((wantFlows || flowsOnly) &&
|
|
8326
|
+
if ((wantFlows || flowsOnly) && existsSync22(designMetaPath)) {
|
|
7925
8327
|
let meta;
|
|
7926
8328
|
try {
|
|
7927
|
-
meta = JSON.parse(
|
|
8329
|
+
meta = JSON.parse(readFileSync22(designMetaPath, "utf8"));
|
|
7928
8330
|
} catch (e) {
|
|
7929
8331
|
throw new CliError(`Can't read ${designMetaPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
7930
8332
|
}
|
|
@@ -8101,15 +8503,15 @@ var types = async (args) => {
|
|
|
8101
8503
|
};
|
|
8102
8504
|
|
|
8103
8505
|
// src/index.ts
|
|
8104
|
-
import { existsSync as
|
|
8506
|
+
import { existsSync as existsSync24, readFileSync as readFileSync24, realpathSync as realpathSync2 } from "node:fs";
|
|
8105
8507
|
import { createRequire as createRequire4 } from "node:module";
|
|
8106
|
-
import { delimiter as delimiter2, dirname as dirname16, join as
|
|
8508
|
+
import { delimiter as delimiter2, dirname as dirname16, join as join24 } from "node:path";
|
|
8107
8509
|
|
|
8108
8510
|
// src/commands/install.ts
|
|
8109
8511
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
8110
|
-
import { appendFileSync as
|
|
8512
|
+
import { appendFileSync as appendFileSync3, chmodSync, cpSync as cpSync2, existsSync as existsSync23, mkdirSync as mkdirSync15, readFileSync as readFileSync23, realpathSync, rmSync as rmSync4, symlinkSync } from "node:fs";
|
|
8111
8513
|
import { homedir as homedir3 } from "node:os";
|
|
8112
|
-
import { basename as basename13, delimiter, dirname as dirname15, join as
|
|
8514
|
+
import { basename as basename13, delimiter, dirname as dirname15, join as join23, resolve as resolve11, sep as sep2 } from "node:path";
|
|
8113
8515
|
var isWin = process.platform === "win32";
|
|
8114
8516
|
var HOME = homedir3();
|
|
8115
8517
|
var safeRealpath = (p) => {
|
|
@@ -8135,10 +8537,10 @@ var install = async (args) => {
|
|
|
8135
8537
|
const bundle = dirname15(process.execPath);
|
|
8136
8538
|
const exeName = basename13(process.execPath);
|
|
8137
8539
|
const chiselName = isWin ? "chisel.exe" : "chisel";
|
|
8138
|
-
if (!
|
|
8540
|
+
if (!existsSync23(join23(bundle, "runtime")) || !existsSync23(join23(bundle, chiselName))) {
|
|
8139
8541
|
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.");
|
|
8140
8542
|
}
|
|
8141
|
-
const companions = [exeName, chiselName, "runtime", "README.txt"].filter((f2) =>
|
|
8543
|
+
const companions = [exeName, chiselName, "runtime", "README.txt"].filter((f2) => existsSync23(join23(bundle, f2)));
|
|
8142
8544
|
if (isWin)
|
|
8143
8545
|
return installWindows(args, bundle, companions);
|
|
8144
8546
|
return installUnix(args, bundle, companions);
|
|
@@ -8148,9 +8550,9 @@ var copyBundle = (bundle, target, companions) => {
|
|
|
8148
8550
|
if (safeRealpath(target) === safeRealpath(bundle))
|
|
8149
8551
|
return;
|
|
8150
8552
|
for (const f2 of companions) {
|
|
8151
|
-
const dest =
|
|
8553
|
+
const dest = join23(target, f2);
|
|
8152
8554
|
rmSync4(dest, { recursive: true, force: true });
|
|
8153
|
-
cpSync2(
|
|
8555
|
+
cpSync2(join23(bundle, f2), dest, { recursive: true });
|
|
8154
8556
|
}
|
|
8155
8557
|
};
|
|
8156
8558
|
var permError = (e, dir, system) => {
|
|
@@ -8162,44 +8564,44 @@ var permError = (e, dir, system) => {
|
|
|
8162
8564
|
};
|
|
8163
8565
|
var installUnix = (args, bundle, companions) => {
|
|
8164
8566
|
const system = flagBool(args, "system");
|
|
8165
|
-
const libDir = resolve11(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" :
|
|
8166
|
-
const binDir = resolve11(flagStr(args, "bin") ?? (system ? "/usr/local/bin" :
|
|
8167
|
-
const link2 =
|
|
8567
|
+
const libDir = resolve11(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join23(HOME, ".local", "lib", "lecodes-cli")));
|
|
8568
|
+
const binDir = resolve11(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join23(HOME, ".local", "bin")));
|
|
8569
|
+
const link2 = join23(binDir, "lecodes");
|
|
8168
8570
|
try {
|
|
8169
8571
|
copyBundle(bundle, libDir, companions);
|
|
8170
|
-
chmodSync(
|
|
8171
|
-
chmodSync(
|
|
8572
|
+
chmodSync(join23(libDir, "lecodes"), 493);
|
|
8573
|
+
chmodSync(join23(libDir, "chisel"), 493);
|
|
8172
8574
|
} catch (e) {
|
|
8173
8575
|
throw permError(e, libDir, system);
|
|
8174
8576
|
}
|
|
8175
8577
|
try {
|
|
8176
8578
|
mkdirSync15(binDir, { recursive: true });
|
|
8177
8579
|
rmSync4(link2, { force: true });
|
|
8178
|
-
symlinkSync(
|
|
8580
|
+
symlinkSync(join23(libDir, "lecodes"), link2);
|
|
8179
8581
|
} catch (e) {
|
|
8180
8582
|
throw permError(e, binDir, system);
|
|
8181
8583
|
}
|
|
8182
8584
|
success(`Installed to ${c.bold(libDir)}`);
|
|
8183
|
-
log(` ${c.dim(link2 + " → " +
|
|
8585
|
+
log(` ${c.dim(link2 + " → " + join23(libDir, "lecodes"))}`);
|
|
8184
8586
|
finishPathUnix(args, binDir);
|
|
8185
8587
|
};
|
|
8186
8588
|
var shellRc = () => {
|
|
8187
8589
|
const shell = basename13(process.env.SHELL ?? "");
|
|
8188
8590
|
if (shell === "zsh")
|
|
8189
|
-
return { file:
|
|
8591
|
+
return { file: join23(HOME, ".zshrc"), name: "~/.zshrc" };
|
|
8190
8592
|
if (shell === "bash")
|
|
8191
|
-
return { file:
|
|
8192
|
-
return { file:
|
|
8593
|
+
return { file: join23(HOME, ".bashrc"), name: "~/.bashrc" };
|
|
8594
|
+
return { file: join23(HOME, ".profile"), name: "~/.profile" };
|
|
8193
8595
|
};
|
|
8194
8596
|
var PATH_MARKER = "# Added by `lecodes install`";
|
|
8195
8597
|
var appendPathToShellRc = (binDir) => {
|
|
8196
8598
|
const { file, name } = shellRc();
|
|
8197
8599
|
const line = `export PATH="${binDir}:$PATH"`;
|
|
8198
8600
|
try {
|
|
8199
|
-
const existing =
|
|
8601
|
+
const existing = existsSync23(file) ? readFileSync23(file, "utf8") : "";
|
|
8200
8602
|
if (existing.includes(line))
|
|
8201
8603
|
return name;
|
|
8202
|
-
|
|
8604
|
+
appendFileSync3(file, `${existing && !existing.endsWith(`
|
|
8203
8605
|
`) ? `
|
|
8204
8606
|
` : ""}
|
|
8205
8607
|
${PATH_MARKER}
|
|
@@ -8234,7 +8636,7 @@ var finishPathUnix = (args, binDir) => {
|
|
|
8234
8636
|
log(` • or install system-wide (no PATH edit): ${c.bold("sudo lecodes install --system")}`);
|
|
8235
8637
|
};
|
|
8236
8638
|
var installWindows = (args, bundle, companions) => {
|
|
8237
|
-
const target = resolve11(flagStr(args, "dir") ??
|
|
8639
|
+
const target = resolve11(flagStr(args, "dir") ?? join23(process.env.LOCALAPPDATA ?? join23(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
|
|
8238
8640
|
try {
|
|
8239
8641
|
copyBundle(bundle, target, companions);
|
|
8240
8642
|
} catch (e) {
|
|
@@ -8273,7 +8675,7 @@ var addToUserPathWindows = (dir) => {
|
|
|
8273
8675
|
var STANDALONE = (() => {
|
|
8274
8676
|
const dir = dirname16(process.execPath);
|
|
8275
8677
|
const chisel = process.platform === "win32" ? "chisel.exe" : "chisel";
|
|
8276
|
-
return
|
|
8678
|
+
return existsSync24(join24(dir, "runtime")) && existsSync24(join24(dir, chisel));
|
|
8277
8679
|
})();
|
|
8278
8680
|
var INSTALLED = STANDALONE && (() => {
|
|
8279
8681
|
const exe = process.platform === "win32" ? "lecodes.exe" : "lecodes";
|
|
@@ -8288,8 +8690,8 @@ var INSTALLED = STANDALONE && (() => {
|
|
|
8288
8690
|
if (!d)
|
|
8289
8691
|
return false;
|
|
8290
8692
|
try {
|
|
8291
|
-
const cand =
|
|
8292
|
-
return
|
|
8693
|
+
const cand = join24(d, exe);
|
|
8694
|
+
return existsSync24(cand) && realpathSync2(cand) === self;
|
|
8293
8695
|
} catch {
|
|
8294
8696
|
return false;
|
|
8295
8697
|
}
|
|
@@ -8459,9 +8861,18 @@ ${c.bold("Commands:")}
|
|
|
8459
8861
|
app upload ios Upload the archive to App Store Connect (then bumps buildNumber)
|
|
8460
8862
|
--key-path <p8> --key-id <id> --issuer-id <id>
|
|
8461
8863
|
ASC API key; stored in ~/.lecodes/asc.json
|
|
8864
|
+
pn send --title <s> Push notifications for this project ('lecodes pn' for the group).
|
|
8865
|
+
Sends with your login — no push key needed to test.
|
|
8866
|
+
--user <ids> target Push.register({ user }) ids (comma-sep)
|
|
8867
|
+
--to <pa_…> target addresses (comma-separated)
|
|
8868
|
+
--body/--url/--badge/--data payload fields
|
|
8869
|
+
pn devices Which devices registered (the 'why didn't it arrive?' command)
|
|
8870
|
+
--user <id> only that user's devices
|
|
8871
|
+
pn keys [new|revoke] Push keys for YOUR server: list, mint (owner only, shown once
|
|
8872
|
+
— 'new --env' banks it in .env), revoke <id>
|
|
8462
8873
|
|
|
8463
8874
|
Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKEN).`;
|
|
8464
|
-
var commands = { init: init2, login, create, clone, link, status, diff, pull, push, compile: compile2, preview: previewLegacy, render, test, types, design, scene, dev, app };
|
|
8875
|
+
var commands = { init: init2, login, create, clone, link, status, diff, pull, push, compile: compile2, preview: previewLegacy, render, test, types, design, scene, dev, app, pn };
|
|
8465
8876
|
if (STANDALONE)
|
|
8466
8877
|
commands.install = install;
|
|
8467
8878
|
var readVersion = () => package_default.version ?? "unknown";
|
|
@@ -8475,9 +8886,9 @@ var peerVersion = (name, probe) => {
|
|
|
8475
8886
|
const require2 = createRequire4(import.meta.url);
|
|
8476
8887
|
let dir = dirname16(require2.resolve(probe));
|
|
8477
8888
|
for (let depth = 0;depth < 16; depth++) {
|
|
8478
|
-
const manifest =
|
|
8479
|
-
if (
|
|
8480
|
-
const parsed = JSON.parse(
|
|
8889
|
+
const manifest = join24(dir, "package.json");
|
|
8890
|
+
if (existsSync24(manifest)) {
|
|
8891
|
+
const parsed = JSON.parse(readFileSync24(manifest, "utf8"));
|
|
8481
8892
|
if (parsed.name === name)
|
|
8482
8893
|
return parsed.version ?? "unknown";
|
|
8483
8894
|
}
|