parley-live 0.4.2 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +8 -2
  3. package/dist/lens.js +252 -0
  4. package/kb/index/bm25.json +1 -0
  5. package/kb/index/chunks-meta.jsonl +11523 -0
  6. package/lens-builder/build-from-prompt.mjs +19 -0
  7. package/lens-builder/build.mjs +147 -0
  8. package/lens-builder/cli.mjs +103 -0
  9. package/lens-builder/kb/lib-fetch.mjs +35 -0
  10. package/lens-builder/kb/search.mjs +61 -0
  11. package/lens-builder/kb/show.mjs +4 -0
  12. package/lens-builder/kb/tokenize.mjs +27 -0
  13. package/lens-builder/lib/bridge.mjs +186 -0
  14. package/lens-builder/lib/submission.mjs +246 -0
  15. package/lens-builder/mcp-client.mjs +101 -0
  16. package/lens-builder/pitch/one-pager.mjs +70 -0
  17. package/lens-builder/planner.mjs +108 -0
  18. package/lens-builder/publish.mjs +127 -0
  19. package/lens-builder/training/author-scene.ts +77 -0
  20. package/lens-builder/training/compile.mjs +258 -0
  21. package/lens-builder/vendor/specs-publish/LICENSE +208 -0
  22. package/lens-builder/vendor/specs-publish/NOTICE +13 -0
  23. package/lens-builder/vendor/specs-publish/scripts/make-eec-script.py +85 -0
  24. package/lens-builder/vendor/specs-publish/scripts/package.ts +189 -0
  25. package/lens-builder/vendor/specs-publish/scripts/preflight.ts +852 -0
  26. package/lens-builder/vendor/specs-publish/scripts/set-lens-icon.ts +107 -0
  27. package/lens-builder/vendor/specs-publish/scripts/set-lens-name.ts +121 -0
  28. package/lens-builder/vendor/specs-publish/scripts/set-signing-key.ts +129 -0
  29. package/lens-builder/vendor/specs-publish/scripts/signing-key.js +290 -0
  30. package/lens-builder/verify.mjs +186 -0
  31. package/lens-builder/webxr/generate.mjs +238 -0
  32. package/package.json +11 -5
@@ -0,0 +1,107 @@
1
+ // Copyright 2026 Specs Inc.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // publish · SET LENS ICON (Editor API)
5
+ // Reads a generated external PNG/JPEG, copies it into the open project through
6
+ // MetaInfo.setIcon(), saves, and verifies that Lens Studio recognizes the new icon.
7
+ //
8
+ // HOW TO RUN: do NOT read this file, and do NOT hand-write the CONFIG consts.
9
+ // Run `make-eec-script.py` against it — it JSON-encodes each value (a bare
10
+ // quote or a Windows backslash otherwise corrupts the emitted TypeScript), keeps the
11
+ // declarations' type annotations, and prints a unique temp path. Pass that path to
12
+ // ExecuteEditorCode as `path`, then delete it. Never edit this file on disk.
13
+ //
14
+ // Result shapes:
15
+ // { status: "ICON_SET", projectPath, iconPath, alreadySet }
16
+ // { status: "FAILED", reason, message, ... }
17
+
18
+ // ===================== CONFIG — replace per call =====================
19
+ const ICON_PATH = "";
20
+ const EXPECTED_PROJECT_PATH = "";
21
+ // =====================================================================
22
+
23
+ const FileSystem = await import("LensStudio:FileSystem");
24
+
25
+ function errorToString(error: unknown): string {
26
+ return error instanceof Error ? error.message : String(error);
27
+ }
28
+ function failed(reason: string, message: string, extra: Record<string, unknown> = {}): any {
29
+ return { status: "FAILED", stage: "lens_icon", reason, message, ...extra };
30
+ }
31
+
32
+ try {
33
+ const project = pluginSystem.findInterface(Editor.Model.IModel).project;
34
+ const projectPath = project.projectFile.toString();
35
+ const configuredProjectPath = EXPECTED_PROJECT_PATH.trim();
36
+ if (!configuredProjectPath) {
37
+ return failed("missing_expected_project_path", "Set EXPECTED_PROJECT_PATH before assigning a Lens icon.", {
38
+ projectPath,
39
+ });
40
+ }
41
+
42
+ const expectedProjectPath = new Editor.Path(configuredProjectPath);
43
+ if (!FileSystem.exists(expectedProjectPath) || !FileSystem.isFile(expectedProjectPath)) {
44
+ return failed("expected_project_missing", "The expected Lens Studio project file is missing or unreadable.", {
45
+ projectPath,
46
+ expectedProjectPath: configuredProjectPath,
47
+ });
48
+ }
49
+
50
+ const canonicalProjectPath = FileSystem.realPath(project.projectFile).toString();
51
+ const canonicalExpectedProjectPath = FileSystem.realPath(expectedProjectPath).toString();
52
+ if (canonicalProjectPath !== canonicalExpectedProjectPath) {
53
+ return failed("wrong_project_open", "Lens Studio has a different project open. Refusing to assign the Lens icon.", {
54
+ projectPath,
55
+ expectedProjectPath: configuredProjectPath,
56
+ });
57
+ }
58
+
59
+ const metaInfo = project.metaInfo;
60
+ if (metaInfo.isIconSet) {
61
+ return {
62
+ status: "ICON_SET",
63
+ stage: "lens_icon",
64
+ projectPath,
65
+ iconPath: metaInfo.iconPath.toString(),
66
+ alreadySet: true,
67
+ };
68
+ }
69
+
70
+ const configuredPath = ICON_PATH.trim();
71
+ if (!configuredPath) {
72
+ return failed("missing_icon_path", "Set ICON_PATH to the generated Lens icon before running this script.", {
73
+ projectPath,
74
+ });
75
+ }
76
+ if (!/\.(png|jpe?g)$/i.test(configuredPath)) {
77
+ return failed("unsupported_icon_type", "The generated Lens icon must be a PNG or JPEG file.", { projectPath });
78
+ }
79
+
80
+ const externalIconPath = new Editor.Path(configuredPath);
81
+ if (!FileSystem.exists(externalIconPath) || !FileSystem.isFile(externalIconPath)) {
82
+ return failed("icon_file_missing", "The generated Lens icon file is missing or unreadable.", { projectPath });
83
+ }
84
+
85
+ // setIcon copies the external file into project-managed metadata storage.
86
+ metaInfo.setIcon(externalIconPath);
87
+ project.metaInfo = metaInfo;
88
+ project.save();
89
+
90
+ const committedMetaInfo = project.metaInfo;
91
+ const copiedIconPath = committedMetaInfo.iconPath;
92
+ if (!committedMetaInfo.isIconSet || !copiedIconPath || !FileSystem.exists(copiedIconPath)) {
93
+ return failed("lens_icon_not_set", "Lens Studio did not retain the generated Lens icon after saving.", {
94
+ projectPath,
95
+ });
96
+ }
97
+
98
+ return {
99
+ status: "ICON_SET",
100
+ stage: "lens_icon",
101
+ projectPath,
102
+ iconPath: copiedIconPath.toString(),
103
+ alreadySet: false,
104
+ };
105
+ } catch (error) {
106
+ return failed("set_lens_icon_failed", errorToString(error), { stack: (error as any)?.stack });
107
+ }
@@ -0,0 +1,121 @@
1
+ // Copyright 2026 Specs Inc.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // publish · SET LENS NAME (Editor API)
5
+ // Assigns a confirmed generated name only when the intended open project has
6
+ // no name or Lens Studio's default "Untitled" placeholder, then saves and
7
+ // verifies that Lens Studio retained it.
8
+ //
9
+ // HOW TO RUN: do NOT read this file, and do NOT hand-write the CONFIG consts.
10
+ // Run `make-eec-script.py` against it — it JSON-encodes each value (a bare
11
+ // quote or a Windows backslash otherwise corrupts the emitted TypeScript), keeps the
12
+ // declarations' type annotations, and prints a unique temp path. Pass that path to
13
+ // ExecuteEditorCode as `path`, then delete it. Never edit this file on disk.
14
+ //
15
+ // Result shapes:
16
+ // { status: "LENS_NAME_SET", projectPath, lensName, alreadySet }
17
+ // { status: "FAILED", reason, message, ... }
18
+
19
+ // ===================== CONFIG — replace per call =====================
20
+ const LENS_NAME = "";
21
+ const EXPECTED_PROJECT_PATH = "";
22
+ // =====================================================================
23
+
24
+ const FileSystem = await import("LensStudio:FileSystem");
25
+
26
+ function errorToString(error: unknown): string {
27
+ return error instanceof Error ? error.message : String(error);
28
+ }
29
+ function isDefaultLensName(value: string): boolean {
30
+ return value.trim().toLowerCase() === "untitled";
31
+ }
32
+ function failed(reason: string, message: string, extra: Record<string, unknown> = {}): any {
33
+ return { status: "FAILED", stage: "lens_name", reason, message, ...extra };
34
+ }
35
+
36
+ try {
37
+ const project = pluginSystem.findInterface(Editor.Model.IModel).project;
38
+ const projectPath = project.projectFile.toString();
39
+ const configuredProjectPath = EXPECTED_PROJECT_PATH.trim();
40
+ if (!configuredProjectPath) {
41
+ return failed("missing_expected_project_path", "Set EXPECTED_PROJECT_PATH before assigning a Lens name.", {
42
+ projectPath,
43
+ });
44
+ }
45
+
46
+ const expectedProjectPath = new Editor.Path(configuredProjectPath);
47
+ if (!FileSystem.exists(expectedProjectPath) || !FileSystem.isFile(expectedProjectPath)) {
48
+ return failed("expected_project_missing", "The expected Lens Studio project file is missing or unreadable.", {
49
+ projectPath,
50
+ expectedProjectPath: configuredProjectPath,
51
+ });
52
+ }
53
+
54
+ const canonicalProjectPath = FileSystem.realPath(project.projectFile).toString();
55
+ const canonicalExpectedProjectPath = FileSystem.realPath(expectedProjectPath).toString();
56
+ if (canonicalProjectPath !== canonicalExpectedProjectPath) {
57
+ return failed("wrong_project_open", "Lens Studio has a different project open. Refusing to assign the Lens name.", {
58
+ projectPath,
59
+ expectedProjectPath: configuredProjectPath,
60
+ });
61
+ }
62
+
63
+ const configuredLensName = LENS_NAME.trim();
64
+ if (!configuredLensName) {
65
+ return failed("missing_lens_name", "Set LENS_NAME to the confirmed Lens name before running this script.", {
66
+ projectPath,
67
+ });
68
+ }
69
+ if (isDefaultLensName(configuredLensName)) {
70
+ return failed("default_lens_name", "Generate a descriptive Lens name instead of the default \"Untitled\".", {
71
+ projectPath,
72
+ lensName: configuredLensName,
73
+ });
74
+ }
75
+ if (!Editor.Model.MetaInfo.isLensNameValid(configuredLensName)) {
76
+ return failed("invalid_lens_name", "Lens Studio rejected the proposed Lens name.", {
77
+ projectPath,
78
+ lensName: configuredLensName,
79
+ });
80
+ }
81
+
82
+ const metaInfo = project.metaInfo;
83
+ const currentLensName = String(metaInfo.lensName || "").trim();
84
+ if (currentLensName && !isDefaultLensName(currentLensName)) {
85
+ if (currentLensName === configuredLensName) {
86
+ return {
87
+ status: "LENS_NAME_SET",
88
+ stage: "lens_name",
89
+ projectPath,
90
+ lensName: currentLensName,
91
+ alreadySet: true,
92
+ };
93
+ }
94
+ return failed("lens_name_already_set", "The project already has a different Lens name.", {
95
+ projectPath,
96
+ lensName: currentLensName,
97
+ });
98
+ }
99
+
100
+ metaInfo.lensName = configuredLensName;
101
+ project.metaInfo = metaInfo;
102
+ project.save();
103
+
104
+ const committedLensName = String(project.metaInfo.lensName || "").trim();
105
+ if (committedLensName !== configuredLensName) {
106
+ return failed("lens_name_not_set", "Lens Studio did not retain the confirmed Lens name after saving.", {
107
+ projectPath,
108
+ lensName: committedLensName,
109
+ });
110
+ }
111
+
112
+ return {
113
+ status: "LENS_NAME_SET",
114
+ stage: "lens_name",
115
+ projectPath,
116
+ lensName: committedLensName,
117
+ alreadySet: false,
118
+ };
119
+ } catch (error) {
120
+ return failed("set_lens_name_failed", errorToString(error), { stack: (error as any)?.stack });
121
+ }
@@ -0,0 +1,129 @@
1
+ // Copyright 2026 Specs Inc.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // publish · SET SIGNING KEY (Editor API)
5
+ // Assigns an existing Ed25519 PKCS#8 PEM to the intended open project, saves,
6
+ // and verifies that Lens Studio retained the selected key path.
7
+ //
8
+ // HOW TO RUN: do NOT read this file, and do NOT hand-write the CONFIG consts.
9
+ // Run `make-eec-script.py` against it — it JSON-encodes each value (a bare
10
+ // quote or a Windows backslash otherwise corrupts the emitted TypeScript), keeps the
11
+ // declarations' type annotations, and prints a unique temp path. Pass that path to
12
+ // ExecuteEditorCode as `path`, then delete it. Never edit this file on disk.
13
+ //
14
+ // Result shapes:
15
+ // { status: "SIGNING_KEY_SET", projectPath, signingKeyPath, alreadySet }
16
+ // { status: "FAILED", reason, message, ... }
17
+
18
+ // ===================== CONFIG — replace per call =====================
19
+ const SIGNING_KEY_PATH = "";
20
+ const EXPECTED_PROJECT_PATH = "";
21
+ // =====================================================================
22
+
23
+ const FileSystem = await import("LensStudio:FileSystem");
24
+
25
+ function errorToString(error: unknown): string {
26
+ return error instanceof Error ? error.message : String(error);
27
+ }
28
+ function failed(reason: string, message: string, extra: Record<string, unknown> = {}): any {
29
+ return { status: "FAILED", stage: "signing_key", reason, message, ...extra };
30
+ }
31
+
32
+ try {
33
+ const project = pluginSystem.findInterface(Editor.Model.IModel).project;
34
+ const projectPath = project.projectFile.toString();
35
+ const configuredProjectPath = EXPECTED_PROJECT_PATH.trim();
36
+ if (!configuredProjectPath) {
37
+ return failed("missing_expected_project_path", "Set EXPECTED_PROJECT_PATH before assigning a signing key.", {
38
+ projectPath,
39
+ });
40
+ }
41
+
42
+ const expectedProjectPath = new Editor.Path(configuredProjectPath);
43
+ if (!FileSystem.exists(expectedProjectPath) || !FileSystem.isFile(expectedProjectPath)) {
44
+ return failed("expected_project_missing", "The expected Lens Studio project file is missing or unreadable.", {
45
+ projectPath,
46
+ expectedProjectPath: configuredProjectPath,
47
+ });
48
+ }
49
+
50
+ const canonicalProjectPath = FileSystem.realPath(project.projectFile).toString();
51
+ const canonicalExpectedProjectPath = FileSystem.realPath(expectedProjectPath).toString();
52
+ if (canonicalProjectPath !== canonicalExpectedProjectPath) {
53
+ return failed(
54
+ "wrong_project_open",
55
+ "Lens Studio has a different project open. Refusing to assign the signing key.",
56
+ {
57
+ projectPath,
58
+ expectedProjectPath: configuredProjectPath,
59
+ },
60
+ );
61
+ }
62
+
63
+ const configuredKeyPath = SIGNING_KEY_PATH.trim();
64
+ if (!configuredKeyPath) {
65
+ return failed("missing_signing_key_path", "Set SIGNING_KEY_PATH before assigning a signing key.", { projectPath });
66
+ }
67
+
68
+ const signingKeyPath = new Editor.Path(configuredKeyPath);
69
+ if (!FileSystem.exists(signingKeyPath) || !FileSystem.isFile(signingKeyPath)) {
70
+ return failed("signing_key_missing", "The selected production signing key is missing or unreadable.", {
71
+ projectPath,
72
+ signingKeyPath: configuredKeyPath,
73
+ });
74
+ }
75
+
76
+ const canonicalSigningKeyPath = FileSystem.realPath(signingKeyPath).toString();
77
+ const canonicalSigningKey = new Editor.Path(canonicalSigningKeyPath);
78
+ const metaInfo = project.metaInfo;
79
+ const currentSigningKeyPath = metaInfo.spkProductionKeyPath;
80
+ if (
81
+ currentSigningKeyPath &&
82
+ !currentSigningKeyPath.isEmpty &&
83
+ FileSystem.exists(currentSigningKeyPath) &&
84
+ FileSystem.isFile(currentSigningKeyPath)
85
+ ) {
86
+ const canonicalCurrentPath = FileSystem.realPath(currentSigningKeyPath).toString();
87
+ if (canonicalCurrentPath === canonicalSigningKeyPath) {
88
+ return {
89
+ status: "SIGNING_KEY_SET",
90
+ stage: "signing_key",
91
+ projectPath,
92
+ signingKeyPath: canonicalSigningKeyPath,
93
+ alreadySet: true,
94
+ };
95
+ }
96
+ return failed("signing_key_already_set", "The project already has a different valid production signing key.", {
97
+ projectPath,
98
+ signingKeyPath: currentSigningKeyPath.toString(),
99
+ });
100
+ }
101
+
102
+ metaInfo.spkProductionKeyPath = canonicalSigningKey;
103
+ project.metaInfo = metaInfo;
104
+ project.save();
105
+
106
+ const committedPath = project.metaInfo.spkProductionKeyPath;
107
+ if (!committedPath || committedPath.isEmpty || !FileSystem.exists(committedPath)) {
108
+ return failed("signing_key_not_set", "Lens Studio did not retain the selected signing key after saving.", {
109
+ projectPath,
110
+ });
111
+ }
112
+ const canonicalCommittedPath = FileSystem.realPath(committedPath).toString();
113
+ if (canonicalCommittedPath !== canonicalSigningKeyPath) {
114
+ return failed("signing_key_path_changed", "Lens Studio retained a different signing key path after saving.", {
115
+ projectPath,
116
+ signingKeyPath: canonicalCommittedPath,
117
+ });
118
+ }
119
+
120
+ return {
121
+ status: "SIGNING_KEY_SET",
122
+ stage: "signing_key",
123
+ projectPath,
124
+ signingKeyPath: canonicalCommittedPath,
125
+ alreadySet: false,
126
+ };
127
+ } catch (error) {
128
+ return failed("set_signing_key_failed", errorToString(error), { stack: (error as any)?.stack });
129
+ }
@@ -0,0 +1,290 @@
1
+ #!/usr/bin/env node
2
+ // Copyright 2026 Specs Inc.
3
+ // SPDX-License-Identifier: Apache-2.0
4
+
5
+ "use strict";
6
+
7
+ // Probe, validate, or generate the production signing key used by the
8
+ // specs-publish skill. Uses only Node.js built-ins and never prints key material.
9
+
10
+ const crypto = require("node:crypto");
11
+ const fs = require("node:fs");
12
+ const path = require("node:path");
13
+
14
+ const GENERATED_KEY_FILENAME = "specs_signing_key.pem";
15
+
16
+ function result(status, extra = {}) {
17
+ process.stdout.write(`${JSON.stringify({ status, stage: "signing_key", ...extra })}\n`);
18
+ }
19
+
20
+ function fail(reason, message, extra = {}) {
21
+ result("FAILED", { reason, message, ...extra });
22
+ }
23
+
24
+ function parseArgs(argv) {
25
+ const parsed = { generate: false, keyPath: "", projectPath: "" };
26
+ for (let index = 0; index < argv.length; index += 1) {
27
+ const argument = argv[index];
28
+ if (argument === "--generate") {
29
+ parsed.generate = true;
30
+ } else if (argument === "--path") {
31
+ index += 1;
32
+ if (index >= argv.length || !argv[index].trim()) throw new Error("--path requires a value");
33
+ parsed.keyPath = argv[index];
34
+ } else if (argument === "--project") {
35
+ index += 1;
36
+ if (index >= argv.length || !argv[index].trim()) throw new Error("--project requires a value");
37
+ parsed.projectPath = argv[index];
38
+ } else {
39
+ throw new Error(`Unknown argument: ${argument}`);
40
+ }
41
+ }
42
+ if (Boolean(parsed.keyPath) === Boolean(parsed.projectPath)) {
43
+ throw new Error("Specify exactly one of --project or --path");
44
+ }
45
+ if (parsed.generate && parsed.keyPath) {
46
+ throw new Error("--generate requires --project so generated keys stay in the Lens Studio project root");
47
+ }
48
+ return parsed;
49
+ }
50
+
51
+ function octalMode(stat) {
52
+ return `0${(stat.mode & 0o777).toString(8).padStart(3, "0")}`;
53
+ }
54
+
55
+ function pathEntryExists(entryPath) {
56
+ try {
57
+ fs.lstatSync(entryPath);
58
+ return true;
59
+ } catch (error) {
60
+ if (error && error.code === "ENOENT") return false;
61
+ throw error;
62
+ }
63
+ }
64
+
65
+ function inspectSigningKey(keyPath) {
66
+ let descriptor;
67
+ let canonicalKeyPath;
68
+ let stat;
69
+ let pem;
70
+ try {
71
+ const entryStat = fs.lstatSync(keyPath);
72
+ if (entryStat.isSymbolicLink()) {
73
+ return {
74
+ ok: false,
75
+ reason: "signing_key_symlink",
76
+ message: "The signing key path must not be a symbolic link.",
77
+ };
78
+ }
79
+ canonicalKeyPath = fs.realpathSync(keyPath);
80
+ descriptor = fs.openSync(canonicalKeyPath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
81
+ stat = fs.fstatSync(descriptor);
82
+ if (!stat.isFile()) {
83
+ return {
84
+ ok: false,
85
+ reason: "signing_key_not_file",
86
+ message: "The signing key path is not a file.",
87
+ };
88
+ }
89
+ pem = fs.readFileSync(descriptor);
90
+ } catch (error) {
91
+ return {
92
+ ok: false,
93
+ reason: "signing_key_unreadable",
94
+ message: "The signing key could not be read.",
95
+ error: error instanceof Error ? error.message : String(error),
96
+ };
97
+ } finally {
98
+ if (descriptor !== undefined) fs.closeSync(descriptor);
99
+ }
100
+
101
+ try {
102
+ if (!pem.toString("utf8").trimStart().startsWith("-----BEGIN PRIVATE KEY-----")) {
103
+ return {
104
+ ok: false,
105
+ reason: "unsupported_signing_key_encoding",
106
+ message: "The signing key must use unencrypted PKCS#8 PEM encoding.",
107
+ };
108
+ }
109
+ const privateKey = crypto.createPrivateKey({ key: pem, format: "pem" });
110
+ if (privateKey.asymmetricKeyType !== "ed25519") {
111
+ return {
112
+ ok: false,
113
+ reason: "unsupported_signing_key",
114
+ message: "The signing key must be an Ed25519 PKCS#8 PEM private key.",
115
+ keyType: privateKey.asymmetricKeyType || "unknown",
116
+ };
117
+ }
118
+
119
+ // Exporting as PKCS#8 proves the private key is usable in the format Lens Studio generates.
120
+ privateKey.export({ type: "pkcs8", format: "pem" });
121
+ const publicDer = crypto.createPublicKey(privateKey).export({ type: "spki", format: "der" });
122
+ const fingerprint = crypto.createHash("sha256").update(publicDer).digest("hex");
123
+ const insecurePermissions = process.platform !== "win32" && (stat.mode & 0o077) !== 0;
124
+ return {
125
+ ok: true,
126
+ path: canonicalKeyPath,
127
+ fingerprint: `SHA256:${fingerprint}`,
128
+ permissions: octalMode(stat),
129
+ permissionsSecure: !insecurePermissions,
130
+ };
131
+ } catch (error) {
132
+ return {
133
+ ok: false,
134
+ reason: "invalid_signing_key",
135
+ message: "The signing key is not a valid Ed25519 PKCS#8 PEM private key.",
136
+ error: error instanceof Error ? error.message : String(error),
137
+ };
138
+ }
139
+ }
140
+
141
+ function validateSigningKey(keyPath, created) {
142
+ const inspection = inspectSigningKey(keyPath);
143
+ if (!inspection.ok) {
144
+ const { reason, message, ...extra } = inspection;
145
+ fail(reason, message, { path: keyPath, ...extra });
146
+ return;
147
+ }
148
+ result("SIGNING_KEY_READY", {
149
+ path: inspection.path,
150
+ created,
151
+ fingerprint: inspection.fingerprint,
152
+ permissions: inspection.permissions,
153
+ permissionsSecure: inspection.permissionsSecure,
154
+ });
155
+ }
156
+
157
+ function validateProjectPathDirectories(projectRoot) {
158
+ const directories = [];
159
+ let directory = path.resolve(projectRoot);
160
+ while (true) {
161
+ directories.unshift(directory);
162
+ const parent = path.dirname(directory);
163
+ if (parent === directory) break;
164
+ directory = parent;
165
+ }
166
+
167
+ for (const component of directories) {
168
+ let stat;
169
+ try {
170
+ stat = fs.lstatSync(component);
171
+ } catch (error) {
172
+ fail("project_path_unreadable", "A Lens Studio project path directory could not be read.", {
173
+ projectRoot,
174
+ path: component,
175
+ error: error instanceof Error ? error.message : String(error),
176
+ });
177
+ return false;
178
+ }
179
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
180
+ fail("project_path_unsafe", "The Lens Studio project path must contain only regular directories.", {
181
+ projectRoot,
182
+ path: component,
183
+ });
184
+ return false;
185
+ }
186
+ if (process.platform !== "win32" && (stat.mode & 0o022) !== 0) {
187
+ fail("project_path_permissions", "The Lens Studio project path contains a directory writable by other users.", {
188
+ projectRoot,
189
+ path: component,
190
+ permissions: octalMode(stat),
191
+ });
192
+ return false;
193
+ }
194
+ }
195
+ return true;
196
+ }
197
+
198
+ function projectSigningKeyPath(projectPath) {
199
+ const resolvedProjectPath = path.resolve(projectPath);
200
+ let projectStat;
201
+ try {
202
+ projectStat = fs.lstatSync(resolvedProjectPath);
203
+ } catch (error) {
204
+ fail("project_unreadable", "The Lens Studio project file could not be read.", {
205
+ projectPath: resolvedProjectPath,
206
+ error: error instanceof Error ? error.message : String(error),
207
+ });
208
+ return "";
209
+ }
210
+ if (projectStat.isSymbolicLink() || !projectStat.isFile()) {
211
+ fail("project_not_regular_file", "The Lens Studio project path must be a regular file, not a symbolic link.", {
212
+ projectPath: resolvedProjectPath,
213
+ });
214
+ return "";
215
+ }
216
+
217
+ const canonicalProjectPath = fs.realpathSync(resolvedProjectPath);
218
+ const projectRoot = path.dirname(canonicalProjectPath);
219
+ if (!validateProjectPathDirectories(projectRoot)) return "";
220
+ return path.join(projectRoot, GENERATED_KEY_FILENAME);
221
+ }
222
+
223
+ function generateSigningKey(keyPath) {
224
+ const directory = path.dirname(keyPath);
225
+ if (!validateProjectPathDirectories(directory)) return;
226
+
227
+ // Never write through an existing path, including an invalid file or symlink.
228
+ if (pathEntryExists(keyPath)) {
229
+ validateSigningKey(keyPath, false);
230
+ return;
231
+ }
232
+
233
+ const { privateKey } = crypto.generateKeyPairSync("ed25519");
234
+ const pem = privateKey.export({ type: "pkcs8", format: "pem" });
235
+ const temporaryPath = path.join(
236
+ directory,
237
+ `.${path.basename(keyPath)}.${process.pid}.${crypto.randomBytes(12).toString("hex")}.tmp`,
238
+ );
239
+ let descriptor;
240
+ let temporaryCreated = false;
241
+ let installed = false;
242
+ try {
243
+ descriptor = fs.openSync(temporaryPath, "wx", 0o600);
244
+ temporaryCreated = true;
245
+ if (process.platform !== "win32") fs.fchmodSync(descriptor, 0o600);
246
+ fs.writeFileSync(descriptor, pem);
247
+ fs.fsyncSync(descriptor);
248
+ const descriptorToClose = descriptor;
249
+ descriptor = undefined;
250
+ fs.closeSync(descriptorToClose);
251
+
252
+ const temporaryInspection = inspectSigningKey(temporaryPath);
253
+ if (!temporaryInspection.ok) {
254
+ throw new Error(`Generated signing key validation failed: ${temporaryInspection.message}`);
255
+ }
256
+
257
+ if (!validateProjectPathDirectories(directory)) return;
258
+ try {
259
+ // A hard link publishes the complete key atomically and fails rather than
260
+ // replacing a path created by another process after the initial probe.
261
+ fs.linkSync(temporaryPath, keyPath);
262
+ installed = true;
263
+ } catch (error) {
264
+ if (!error || error.code !== "EEXIST") throw error;
265
+ }
266
+ } finally {
267
+ if (descriptor !== undefined) fs.closeSync(descriptor);
268
+ if (temporaryCreated && pathEntryExists(temporaryPath)) fs.unlinkSync(temporaryPath);
269
+ }
270
+ validateSigningKey(keyPath, installed);
271
+ }
272
+
273
+ try {
274
+ const args = parseArgs(process.argv.slice(2));
275
+ const keyPath = args.projectPath ? projectSigningKeyPath(args.projectPath) : path.resolve(args.keyPath);
276
+ if (!keyPath) {
277
+ // projectSigningKeyPath already emitted the structured failure.
278
+ } else if (pathEntryExists(keyPath)) {
279
+ validateSigningKey(keyPath, false);
280
+ } else if (args.generate) {
281
+ generateSigningKey(keyPath);
282
+ } else {
283
+ result("SIGNING_KEY_MISSING", {
284
+ path: keyPath,
285
+ message: "No Specs production signing key exists at the selected path.",
286
+ });
287
+ }
288
+ } catch (error) {
289
+ fail("signing_key_setup_failed", error instanceof Error ? error.message : String(error));
290
+ }