notioncode 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -8
- package/bin/nocode-local.js +2 -8
- package/lib/certs.js +332 -0
- package/lib/install.js +15 -2
- package/lib/start.js +199 -14
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
|
-
#
|
|
1
|
+
# notioncode
|
|
2
2
|
|
|
3
|
-
Run the
|
|
3
|
+
Run the local companion runtime behind the `npx create notioncode` flow.
|
|
4
4
|
|
|
5
5
|
## Intended usage
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx
|
|
8
|
+
npx create notioncode
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
Options:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npx
|
|
15
|
-
npx
|
|
16
|
-
npx
|
|
14
|
+
npx notioncode install
|
|
15
|
+
npx notioncode doctor
|
|
16
|
+
npx notioncode start
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
## What it starts
|
|
20
20
|
|
|
21
21
|
- localhost bridge on `127.0.0.1:3456`
|
|
22
|
-
-
|
|
22
|
+
- local UI on `http://127.0.0.1:1420` for non-Safari default browsers
|
|
23
|
+
- local UI on `https://localhost:1420` when the default browser is Safari
|
|
23
24
|
- browser handoff to `https://www.notioncode.live`
|
|
25
|
+
- trusted localhost certificate provisioning with `mkcert` only when Safari-compatible HTTPS is needed
|
|
24
26
|
|
|
25
27
|
## Bridge binaries
|
|
26
28
|
|
|
@@ -33,6 +35,11 @@ Environment variables:
|
|
|
33
35
|
- `NOCODE_CLOUD_URL`
|
|
34
36
|
- `NOCODE_NO_OPEN=1`
|
|
35
37
|
|
|
38
|
+
## Safari support
|
|
39
|
+
|
|
40
|
+
When the system default browser is Safari, this package switches the local UI to `https://localhost:1420`.
|
|
41
|
+
That path requires a trusted localhost certificate, so the setup flow will install or download `mkcert` and ask the OS to trust a local CA when needed.
|
|
42
|
+
|
|
36
43
|
## Current limitation
|
|
37
44
|
|
|
38
|
-
This package
|
|
45
|
+
This package still expects local runtime assets from this repository. It is close to publication-ready in interface and metadata, but the final standalone distribution still needs packaged runtime assets for the agent sidecar.
|
package/bin/nocode-local.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import process from 'node:process';
|
|
4
4
|
|
|
5
5
|
import { ensureBridgeBinary, buildLocalBridgeAsset } from '../lib/install.js';
|
|
6
|
-
import { startLocalCompanion } from '../lib/start.js';
|
|
6
|
+
import { runDoctor, startLocalCompanion } from '../lib/start.js';
|
|
7
7
|
|
|
8
8
|
async function main() {
|
|
9
9
|
const args = process.argv.slice(2);
|
|
@@ -14,13 +14,7 @@ async function main() {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
if (command === 'doctor') {
|
|
17
|
-
|
|
18
|
-
['node', process.version],
|
|
19
|
-
['platform', `${process.platform}/${process.arch}`],
|
|
20
|
-
];
|
|
21
|
-
for (const [label, detail] of checks) {
|
|
22
|
-
console.log(`[OK] ${label}: ${detail}`);
|
|
23
|
-
}
|
|
17
|
+
await runDoctor();
|
|
24
18
|
return;
|
|
25
19
|
}
|
|
26
20
|
|
package/lib/certs.js
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { chmodSync, createWriteStream, existsSync, mkdirSync } from 'node:fs';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import https from 'node:https';
|
|
6
|
+
|
|
7
|
+
import { defaultCacheDir } from './platform.js';
|
|
8
|
+
|
|
9
|
+
function appDataDir() {
|
|
10
|
+
return path.join(defaultCacheDir(), 'local-https');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function localBinDir() {
|
|
14
|
+
return path.join(appDataDir(), 'bin');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ensureDir(dirPath) {
|
|
18
|
+
mkdirSync(dirPath, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function pathEntries() {
|
|
22
|
+
return (process.env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveCommand(command) {
|
|
26
|
+
const names = process.platform === 'win32' ? [command, `${command}.exe`, `${command}.cmd`] : [command];
|
|
27
|
+
const extraDirs = process.platform === 'darwin'
|
|
28
|
+
? ['/opt/homebrew/bin', '/usr/local/bin', localBinDir()]
|
|
29
|
+
: process.platform === 'win32'
|
|
30
|
+
? [
|
|
31
|
+
'C:\\ProgramData\\chocolatey\\bin',
|
|
32
|
+
path.join(os.homedir(), 'scoop', 'shims'),
|
|
33
|
+
localBinDir(),
|
|
34
|
+
]
|
|
35
|
+
: [localBinDir()];
|
|
36
|
+
|
|
37
|
+
for (const dir of [...pathEntries(), ...extraDirs]) {
|
|
38
|
+
for (const name of names) {
|
|
39
|
+
const fullPath = path.join(dir, name);
|
|
40
|
+
if (existsSync(fullPath)) {
|
|
41
|
+
return fullPath;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function hasCommand(command) {
|
|
50
|
+
return Boolean(resolveCommand(command));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function runResolvedCommand(command, args, options = {}) {
|
|
54
|
+
const resolved = resolveCommand(command);
|
|
55
|
+
if (!resolved) {
|
|
56
|
+
throw new Error(`Command not found: ${command}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return execFileSync(resolved, args, {
|
|
60
|
+
stdio: 'inherit',
|
|
61
|
+
...options,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function certificatePaths() {
|
|
66
|
+
const dir = path.join(appDataDir(), 'certs');
|
|
67
|
+
return {
|
|
68
|
+
dir,
|
|
69
|
+
certFile: path.join(dir, 'localhost.pem'),
|
|
70
|
+
keyFile: path.join(dir, 'localhost-key.pem'),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function hasCertificateFiles(paths) {
|
|
75
|
+
return existsSync(paths.certFile) && existsSync(paths.keyFile);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function mkcertInstallInstructions() {
|
|
79
|
+
if (process.platform === 'darwin') {
|
|
80
|
+
return ['brew install mkcert', 'mkcert -install'];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (process.platform === 'win32') {
|
|
84
|
+
return ['choco install mkcert', 'mkcert -install'];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return ['install mkcert with your package manager', 'mkcert -install'];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function mkcertAssetMatcher() {
|
|
91
|
+
if (process.platform === 'darwin' && process.arch === 'arm64') {
|
|
92
|
+
return /darwin-arm64$/;
|
|
93
|
+
}
|
|
94
|
+
if (process.platform === 'darwin' && process.arch === 'x64') {
|
|
95
|
+
return /darwin-amd64$|darwin-x86_64$|darwin-x64$/;
|
|
96
|
+
}
|
|
97
|
+
if (process.platform === 'linux' && process.arch === 'x64') {
|
|
98
|
+
return /linux-amd64$|linux-x86_64$|linux-x64$/;
|
|
99
|
+
}
|
|
100
|
+
if (process.platform === 'win32' && process.arch === 'x64') {
|
|
101
|
+
return /windows-amd64\.exe$|windows-x86_64\.exe$|windows-x64\.exe$/;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
throw new Error(`Unsupported platform for automatic mkcert download: ${process.platform}/${process.arch}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function requestJson(url) {
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
https
|
|
110
|
+
.get(
|
|
111
|
+
url,
|
|
112
|
+
{
|
|
113
|
+
headers: {
|
|
114
|
+
'User-Agent': 'notioncode-local',
|
|
115
|
+
Accept: 'application/vnd.github+json',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
(response) => {
|
|
119
|
+
if ((response.statusCode || 0) >= 300 && (response.statusCode || 0) < 400 && response.headers.location) {
|
|
120
|
+
response.resume();
|
|
121
|
+
requestJson(response.headers.location).then(resolve, reject);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if ((response.statusCode || 0) >= 400) {
|
|
126
|
+
response.resume();
|
|
127
|
+
reject(new Error(`GitHub API request failed with HTTP ${response.statusCode}`));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let body = '';
|
|
132
|
+
response.setEncoding('utf8');
|
|
133
|
+
response.on('data', (chunk) => {
|
|
134
|
+
body += chunk;
|
|
135
|
+
});
|
|
136
|
+
response.on('end', () => {
|
|
137
|
+
try {
|
|
138
|
+
resolve(JSON.parse(body));
|
|
139
|
+
} catch (error) {
|
|
140
|
+
reject(error);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
)
|
|
145
|
+
.on('error', reject);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function downloadFile(url, destination) {
|
|
150
|
+
return new Promise((resolve, reject) => {
|
|
151
|
+
https
|
|
152
|
+
.get(
|
|
153
|
+
url,
|
|
154
|
+
{
|
|
155
|
+
headers: {
|
|
156
|
+
'User-Agent': 'notioncode-local',
|
|
157
|
+
Accept: 'application/octet-stream',
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
(response) => {
|
|
161
|
+
if ((response.statusCode || 0) >= 300 && (response.statusCode || 0) < 400 && response.headers.location) {
|
|
162
|
+
response.resume();
|
|
163
|
+
downloadFile(response.headers.location, destination).then(resolve, reject);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if ((response.statusCode || 0) >= 400) {
|
|
168
|
+
response.resume();
|
|
169
|
+
reject(new Error(`mkcert download failed with HTTP ${response.statusCode}`));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const output = createWriteStream(destination, { mode: 0o755 });
|
|
174
|
+
response.pipe(output);
|
|
175
|
+
output.on('finish', () => {
|
|
176
|
+
output.close(() => resolve(destination));
|
|
177
|
+
});
|
|
178
|
+
output.on('error', reject);
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
.on('error', reject);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function installStandaloneMkcert() {
|
|
186
|
+
ensureDir(localBinDir());
|
|
187
|
+
|
|
188
|
+
const latestRelease = await requestJson('https://api.github.com/repos/FiloSottile/mkcert/releases/latest');
|
|
189
|
+
const matcher = mkcertAssetMatcher();
|
|
190
|
+
const asset = Array.isArray(latestRelease.assets)
|
|
191
|
+
? latestRelease.assets.find((entry) => typeof entry?.name === 'string' && matcher.test(entry.name))
|
|
192
|
+
: null;
|
|
193
|
+
|
|
194
|
+
if (!asset?.browser_download_url) {
|
|
195
|
+
throw new Error(`Could not find a mkcert download for ${process.platform}/${process.arch}.`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const targetName = process.platform === 'win32' ? 'mkcert.exe' : 'mkcert';
|
|
199
|
+
const targetPath = path.join(localBinDir(), targetName);
|
|
200
|
+
console.log(`[notioncode] Downloading mkcert to ${targetPath} ...`);
|
|
201
|
+
await downloadFile(asset.browser_download_url, targetPath);
|
|
202
|
+
if (process.platform !== 'win32') {
|
|
203
|
+
chmodSync(targetPath, 0o755);
|
|
204
|
+
}
|
|
205
|
+
return targetPath;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function installMkcertIfPossible() {
|
|
209
|
+
if (hasCommand('mkcert')) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (process.platform === 'darwin' && hasCommand('brew')) {
|
|
214
|
+
console.log('[notioncode] Installing mkcert with Homebrew...');
|
|
215
|
+
runResolvedCommand('brew', ['install', 'mkcert']);
|
|
216
|
+
if (!hasCommand('mkcert')) {
|
|
217
|
+
throw new Error('Homebrew completed, but mkcert is still not available on PATH.');
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (process.platform === 'win32' && hasCommand('choco')) {
|
|
223
|
+
console.log('[notioncode] Installing mkcert with Chocolatey...');
|
|
224
|
+
runResolvedCommand('choco', ['install', 'mkcert', '-y']);
|
|
225
|
+
if (!hasCommand('mkcert')) {
|
|
226
|
+
throw new Error('Chocolatey completed, but mkcert is still not available on PATH.');
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (process.platform === 'win32' && hasCommand('scoop')) {
|
|
232
|
+
console.log('[notioncode] Installing mkcert with Scoop...');
|
|
233
|
+
runResolvedCommand('scoop', ['install', 'mkcert']);
|
|
234
|
+
if (!hasCommand('mkcert')) {
|
|
235
|
+
throw new Error('Scoop completed, but mkcert is still not available on PATH.');
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
console.log('[notioncode] No package-manager mkcert installation path was detected. Falling back to a direct mkcert download...');
|
|
241
|
+
throw new Error('__DOWNLOAD_MKCERT__');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function ensureMkcertAvailable() {
|
|
245
|
+
try {
|
|
246
|
+
installMkcertIfPossible();
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (error instanceof Error && error.message === '__DOWNLOAD_MKCERT__') {
|
|
249
|
+
return installStandaloneMkcert();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function waitForMkcertOnPath() {
|
|
257
|
+
const resolved = resolveCommand('mkcert');
|
|
258
|
+
if (!resolved) {
|
|
259
|
+
throw new Error('mkcert installation completed, but the binary is still unavailable.');
|
|
260
|
+
}
|
|
261
|
+
return resolved;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function ensureMkcertReady() {
|
|
265
|
+
await ensureMkcertAvailable();
|
|
266
|
+
return waitForMkcertOnPath();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function installMkcertAndTrust() {
|
|
270
|
+
await ensureMkcertReady();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function installTrustedLocalCa() {
|
|
274
|
+
await installMkcertAndTrust();
|
|
275
|
+
|
|
276
|
+
console.log(
|
|
277
|
+
'[notioncode] NotionCode needs to install a local certificate authority so Safari can trust https://localhost:1420.'
|
|
278
|
+
);
|
|
279
|
+
console.log(
|
|
280
|
+
'[notioncode] Your OS may ask for approval or your system password because this updates the local trust store.'
|
|
281
|
+
);
|
|
282
|
+
runResolvedCommand('mkcert', ['-install']);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function generateTrustedLocalhostCert(paths) {
|
|
286
|
+
console.log('[notioncode] Generating trusted localhost certificate...');
|
|
287
|
+
runResolvedCommand('mkcert', [
|
|
288
|
+
'-cert-file',
|
|
289
|
+
paths.certFile,
|
|
290
|
+
'-key-file',
|
|
291
|
+
paths.keyFile,
|
|
292
|
+
'localhost',
|
|
293
|
+
'127.0.0.1',
|
|
294
|
+
'::1',
|
|
295
|
+
]);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export async function ensureTrustedLocalhostCert() {
|
|
299
|
+
const paths = certificatePaths();
|
|
300
|
+
ensureDir(paths.dir);
|
|
301
|
+
|
|
302
|
+
await installTrustedLocalCa();
|
|
303
|
+
|
|
304
|
+
if (!hasCertificateFiles(paths)) {
|
|
305
|
+
generateTrustedLocalhostCert(paths);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
certFile: paths.certFile,
|
|
310
|
+
keyFile: paths.keyFile,
|
|
311
|
+
certDir: paths.dir,
|
|
312
|
+
appDataDir: appDataDir(),
|
|
313
|
+
localBinDir: localBinDir(),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function diagnoseTrustedLocalhostCert() {
|
|
318
|
+
const paths = certificatePaths();
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
appDataDir: appDataDir(),
|
|
322
|
+
certDir: paths.dir,
|
|
323
|
+
certFile: paths.certFile,
|
|
324
|
+
keyFile: paths.keyFile,
|
|
325
|
+
mkcertInstalled: hasCommand('mkcert'),
|
|
326
|
+
mkcertPath: resolveCommand('mkcert'),
|
|
327
|
+
brewInstalled: hasCommand('brew'),
|
|
328
|
+
brewPath: resolveCommand('brew'),
|
|
329
|
+
certExists: existsSync(paths.certFile),
|
|
330
|
+
keyExists: existsSync(paths.keyFile),
|
|
331
|
+
};
|
|
332
|
+
}
|
package/lib/install.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chmodSync, copyFileSync, createWriteStream, existsSync, mkdirSync } from 'node:fs';
|
|
1
|
+
import { chmodSync, copyFileSync, createWriteStream, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import { pipeline } from 'node:stream/promises';
|
|
@@ -7,11 +7,24 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
|
|
8
8
|
import { commandForPlatform, defaultCacheDir, resolvePlatformTarget } from './platform.js';
|
|
9
9
|
|
|
10
|
-
const DEFAULT_VERSION = '0.1.0';
|
|
11
10
|
const DEFAULT_ASSET_BASE =
|
|
12
11
|
process.env.NOCODE_COMPANION_ASSET_BASE_URL ||
|
|
13
12
|
'https://github.com/tadkt/nocode/releases/download';
|
|
14
13
|
|
|
14
|
+
function packageVersion() {
|
|
15
|
+
try {
|
|
16
|
+
const packageJsonPath = path.join(packageRootDir(), 'package.json');
|
|
17
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
18
|
+
return typeof packageJson.version === 'string' && packageJson.version.trim()
|
|
19
|
+
? packageJson.version.trim()
|
|
20
|
+
: '0.1.0';
|
|
21
|
+
} catch {
|
|
22
|
+
return '0.1.0';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DEFAULT_VERSION = packageVersion();
|
|
27
|
+
|
|
15
28
|
function packageRootDir() {
|
|
16
29
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
17
30
|
}
|
package/lib/start.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import https from 'node:https';
|
|
1
3
|
import path from 'node:path';
|
|
2
4
|
import { spawn, spawnSync } from 'node:child_process';
|
|
3
5
|
import { existsSync } from 'node:fs';
|
|
4
6
|
import { fileURLToPath } from 'node:url';
|
|
5
7
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
6
8
|
|
|
9
|
+
import defaultBrowserId from 'default-browser-id';
|
|
10
|
+
|
|
7
11
|
import { commandForPlatform } from './platform.js';
|
|
8
12
|
import { ensureBridgeBinary } from './install.js';
|
|
13
|
+
import { diagnoseTrustedLocalhostCert, ensureTrustedLocalhostCert } from './certs.js';
|
|
9
14
|
|
|
10
15
|
const BRIDGE_HEALTH_URL = 'http://127.0.0.1:3456/healthz';
|
|
11
16
|
const LOCAL_UI_URL = 'http://127.0.0.1:1420/';
|
|
17
|
+
const LOCAL_UI_HTTPS_URL = 'https://localhost:1420/';
|
|
12
18
|
const CLOUD_URL = process.env.NOCODE_CLOUD_URL || 'https://www.notioncode.live';
|
|
13
19
|
|
|
14
20
|
function packageRootDir() {
|
|
@@ -44,10 +50,34 @@ function spawnLogged(cmd, args, options = {}) {
|
|
|
44
50
|
});
|
|
45
51
|
}
|
|
46
52
|
|
|
53
|
+
function requestReady(url, options = {}) {
|
|
54
|
+
const target = new URL(url);
|
|
55
|
+
const client = target.protocol === 'https:' ? https : http;
|
|
56
|
+
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const request = client.request(
|
|
59
|
+
target,
|
|
60
|
+
{
|
|
61
|
+
method: 'GET',
|
|
62
|
+
...(target.protocol === 'https:' ? { rejectUnauthorized: options.rejectUnauthorized !== false } : {}),
|
|
63
|
+
},
|
|
64
|
+
(response) => {
|
|
65
|
+
response.resume();
|
|
66
|
+
resolve(response.statusCode >= 200 && response.statusCode < 400);
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
request.on('error', reject);
|
|
71
|
+
request.setTimeout(2_000, () => {
|
|
72
|
+
request.destroy(new Error(`Timed out waiting for ${url}`));
|
|
73
|
+
});
|
|
74
|
+
request.end();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
47
78
|
async function isReady(url) {
|
|
48
79
|
try {
|
|
49
|
-
|
|
50
|
-
return response.ok;
|
|
80
|
+
return await requestReady(url);
|
|
51
81
|
} catch {
|
|
52
82
|
return false;
|
|
53
83
|
}
|
|
@@ -58,9 +88,9 @@ async function waitFor(url, timeoutMs, label) {
|
|
|
58
88
|
let lastError = null;
|
|
59
89
|
while (Date.now() < deadline) {
|
|
60
90
|
try {
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
63
|
-
lastError = new Error(`${label} returned
|
|
91
|
+
const ready = await requestReady(url);
|
|
92
|
+
if (ready) return;
|
|
93
|
+
lastError = new Error(`${label} returned a non-ready response`);
|
|
64
94
|
} catch (error) {
|
|
65
95
|
lastError = error;
|
|
66
96
|
}
|
|
@@ -69,6 +99,26 @@ async function waitFor(url, timeoutMs, label) {
|
|
|
69
99
|
throw lastError || new Error(`${label} did not become ready.`);
|
|
70
100
|
}
|
|
71
101
|
|
|
102
|
+
async function verifyTrustedBridge(url) {
|
|
103
|
+
const deadline = Date.now() + 10_000;
|
|
104
|
+
let lastError = null;
|
|
105
|
+
|
|
106
|
+
while (Date.now() < deadline) {
|
|
107
|
+
try {
|
|
108
|
+
const ready = await requestReady(url, { rejectUnauthorized: true });
|
|
109
|
+
if (ready) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
lastError = new Error(`Trusted bridge check returned a non-ready response for ${url}`);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
lastError = error;
|
|
115
|
+
}
|
|
116
|
+
await sleep(300);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
throw lastError || new Error(`Timed out verifying trusted bridge at ${url}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
72
122
|
function openBrowser(url) {
|
|
73
123
|
if (process.env.NOCODE_NO_OPEN === '1') return;
|
|
74
124
|
if (process.platform === 'darwin') {
|
|
@@ -82,6 +132,73 @@ function openBrowser(url) {
|
|
|
82
132
|
spawn(commandForPlatform('xdg-open'), [url], { stdio: 'ignore', detached: true }).unref();
|
|
83
133
|
}
|
|
84
134
|
|
|
135
|
+
function trimOutput(value) {
|
|
136
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function detectDefaultBrowser() {
|
|
140
|
+
if (process.env.NOCODE_DEFAULT_BROWSER?.trim()) {
|
|
141
|
+
return process.env.NOCODE_DEFAULT_BROWSER.trim();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
if (process.platform === 'darwin') {
|
|
146
|
+
const bundleId = defaultBrowserId();
|
|
147
|
+
if (bundleId) {
|
|
148
|
+
return trimOutput(bundleId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (process.platform === 'win32') {
|
|
153
|
+
const result = spawnSync(
|
|
154
|
+
'reg',
|
|
155
|
+
[
|
|
156
|
+
'query',
|
|
157
|
+
'HKCU\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\https\\UserChoice',
|
|
158
|
+
'/v',
|
|
159
|
+
'ProgId',
|
|
160
|
+
],
|
|
161
|
+
{ encoding: 'utf8' }
|
|
162
|
+
);
|
|
163
|
+
if (result.status === 0) {
|
|
164
|
+
return trimOutput(result.stdout);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const result = spawnSync('xdg-settings', ['get', 'default-web-browser'], {
|
|
169
|
+
encoding: 'utf8',
|
|
170
|
+
});
|
|
171
|
+
if (result.status === 0) {
|
|
172
|
+
return trimOutput(result.stdout);
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
// Ignore detection failures and fall back to HTTP.
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return '';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function isSafariDefaultBrowser(defaultBrowser) {
|
|
182
|
+
const normalized = defaultBrowser.toLowerCase();
|
|
183
|
+
return (
|
|
184
|
+
normalized.includes('com.apple.safari') ||
|
|
185
|
+
normalized.includes('safari.app') ||
|
|
186
|
+
normalized.includes('safari technology preview')
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function shouldUseHttpsLocalUi() {
|
|
191
|
+
const explicit = process.env.NOCODE_LOCAL_UI_HTTPS?.trim().toLowerCase();
|
|
192
|
+
if (explicit === '1' || explicit === 'true' || explicit === 'https') {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
if (explicit === '0' || explicit === 'false' || explicit === 'http') {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return isSafariDefaultBrowser(detectDefaultBrowser());
|
|
200
|
+
}
|
|
201
|
+
|
|
85
202
|
function hasLocalUiRepo() {
|
|
86
203
|
const runtimeRoot = detectRuntimeRoot();
|
|
87
204
|
return existsSync(path.join(runtimeRoot, 'package.json')) && existsSync(path.join(runtimeRoot, 'node_modules', 'vite', 'bin', 'vite.js'));
|
|
@@ -119,27 +236,44 @@ export async function startLocalCompanion(options = {}) {
|
|
|
119
236
|
const requestedLocalUi = options.withLocalUi !== false;
|
|
120
237
|
const shouldStartLocalUi = requestedLocalUi && hasLocalUiRepo();
|
|
121
238
|
const useCargoBridge = hasCargoBridgeSource(runtimeRoot) && canUseCargo();
|
|
122
|
-
const
|
|
239
|
+
const useHttpsLocalUi =
|
|
240
|
+
typeof options.localUiHttps === 'boolean' ? options.localUiHttps : shouldUseHttpsLocalUi();
|
|
241
|
+
const localUiUrl = useHttpsLocalUi ? LOCAL_UI_HTTPS_URL : LOCAL_UI_URL;
|
|
242
|
+
const localUiHost = useHttpsLocalUi ? 'localhost' : '127.0.0.1';
|
|
243
|
+
let localHttps = null;
|
|
244
|
+
|
|
245
|
+
if (shouldStartLocalUi && useHttpsLocalUi) {
|
|
246
|
+
localHttps = await ensureTrustedLocalhostCert();
|
|
247
|
+
}
|
|
123
248
|
|
|
124
249
|
ensureSidecarInstallIfRepoPresent();
|
|
125
250
|
|
|
126
251
|
let vite = null;
|
|
127
252
|
if (shouldStartLocalUi) {
|
|
128
|
-
if (await isReady(
|
|
129
|
-
console.log(
|
|
253
|
+
if (await isReady(localUiUrl)) {
|
|
254
|
+
console.log(`[notioncode] Reusing existing local UI on ${localUiUrl} .`);
|
|
130
255
|
} else {
|
|
131
|
-
console.log(
|
|
132
|
-
vite = spawnLogged(commandForPlatform('node'), ['node_modules/vite/bin/vite.js', '--host',
|
|
256
|
+
console.log(`[notioncode] Starting local UI on ${localUiUrl} ...`);
|
|
257
|
+
vite = spawnLogged(commandForPlatform('node'), ['node_modules/vite/bin/vite.js', '--host', localUiHost], {
|
|
133
258
|
cwd: runtimeRoot,
|
|
134
259
|
env: {
|
|
135
260
|
VITE_SINGLE_USER_MODE: 'true',
|
|
261
|
+
NOCODE_LOCAL_UI_USE_HTTPS: useHttpsLocalUi ? '1' : '0',
|
|
262
|
+
NOCODE_LOCAL_UI_HOST: localUiHost,
|
|
263
|
+
...(localHttps
|
|
264
|
+
? {
|
|
265
|
+
NOCODE_LOCAL_UI_CERT_FILE: localHttps.certFile,
|
|
266
|
+
NOCODE_LOCAL_UI_KEY_FILE: localHttps.keyFile,
|
|
267
|
+
}
|
|
268
|
+
: {}),
|
|
136
269
|
},
|
|
137
270
|
});
|
|
138
271
|
}
|
|
139
272
|
}
|
|
140
273
|
|
|
141
274
|
let bridge = null;
|
|
142
|
-
|
|
275
|
+
const bridgeAlreadyReady = await isReady(BRIDGE_HEALTH_URL);
|
|
276
|
+
if (bridgeAlreadyReady) {
|
|
143
277
|
console.log('[notioncode] Reusing existing bridge on http://127.0.0.1:3456 .');
|
|
144
278
|
} else {
|
|
145
279
|
console.log('[notioncode] Starting local bridge on http://127.0.0.1:3456 ...');
|
|
@@ -150,6 +284,7 @@ export async function startLocalCompanion(options = {}) {
|
|
|
150
284
|
{ cwd: runtimeRoot }
|
|
151
285
|
);
|
|
152
286
|
} else {
|
|
287
|
+
const bridgeExecutable = await ensureBridgeBinary(options.version);
|
|
153
288
|
bridge = spawnLogged(bridgeExecutable, [], {
|
|
154
289
|
cwd: runtimeRoot,
|
|
155
290
|
env: {
|
|
@@ -180,15 +315,65 @@ export async function startLocalCompanion(options = {}) {
|
|
|
180
315
|
const bridgeTimeoutMs = useCargoBridge ? 180_000 : 15_000;
|
|
181
316
|
await waitFor(BRIDGE_HEALTH_URL, bridgeTimeoutMs, 'Bridge');
|
|
182
317
|
if (shouldStartLocalUi) {
|
|
183
|
-
await waitFor(
|
|
318
|
+
await waitFor(localUiUrl, 15_000, 'Local UI');
|
|
319
|
+
if (useHttpsLocalUi) {
|
|
320
|
+
try {
|
|
321
|
+
await verifyTrustedBridge(localUiUrl);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
throw new Error(
|
|
324
|
+
`Safari-compatible local bridge could not be initialized.\n\nReason:\n ${error instanceof Error ? error.message : String(error)}\n\nFix:\n Install mkcert and trust the local certificate authority, then rerun:\n mkcert -install\n npx create notioncode`
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
184
328
|
}
|
|
185
329
|
|
|
186
330
|
console.log('[notioncode] Ready.');
|
|
187
331
|
console.log(`[notioncode] Cloud UI: ${CLOUD_URL}`);
|
|
188
332
|
if (shouldStartLocalUi) {
|
|
189
|
-
console.log(`[notioncode] Local UI: ${
|
|
333
|
+
console.log(`[notioncode] Local UI: ${localUiUrl}`);
|
|
190
334
|
}
|
|
191
|
-
const entryUrl =
|
|
335
|
+
const entryUrl = CLOUD_URL;
|
|
192
336
|
console.log(`[notioncode] Opening: ${entryUrl}`);
|
|
193
337
|
openBrowser(entryUrl);
|
|
194
338
|
}
|
|
339
|
+
|
|
340
|
+
export async function runDoctor() {
|
|
341
|
+
const certs = diagnoseTrustedLocalhostCert();
|
|
342
|
+
const defaultBrowser = detectDefaultBrowser();
|
|
343
|
+
const useHttpsLocalUi = shouldUseHttpsLocalUi();
|
|
344
|
+
const checks = [
|
|
345
|
+
['node', process.version],
|
|
346
|
+
['platform', `${process.platform}/${process.arch}`],
|
|
347
|
+
['default browser', defaultBrowser || 'unknown'],
|
|
348
|
+
['local UI scheme', useHttpsLocalUi ? 'https' : 'http'],
|
|
349
|
+
['mkcert', certs.mkcertInstalled ? 'installed' : 'missing'],
|
|
350
|
+
['localhost cert', certs.certExists ? certs.certFile : 'missing'],
|
|
351
|
+
['localhost key', certs.keyExists ? certs.keyFile : 'missing'],
|
|
352
|
+
];
|
|
353
|
+
|
|
354
|
+
for (const [label, detail] of checks) {
|
|
355
|
+
console.log(`[OK] ${label}: ${detail}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const bridgeReady = await isReady(BRIDGE_HEALTH_URL);
|
|
359
|
+
console.log(`[${bridgeReady ? 'OK' : 'WARN'}] bridge healthz: ${bridgeReady ? BRIDGE_HEALTH_URL : 'not reachable'}`);
|
|
360
|
+
|
|
361
|
+
const selectedLocalUiUrl = useHttpsLocalUi ? LOCAL_UI_HTTPS_URL : LOCAL_UI_URL;
|
|
362
|
+
const localUiReachable = await isReady(selectedLocalUiUrl);
|
|
363
|
+
console.log(
|
|
364
|
+
`[${localUiReachable ? 'OK' : 'WARN'}] selected local UI: ${
|
|
365
|
+
localUiReachable ? selectedLocalUiUrl : 'not reachable'
|
|
366
|
+
}`
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
if (useHttpsLocalUi && localUiReachable) {
|
|
370
|
+
try {
|
|
371
|
+
await verifyTrustedBridge(LOCAL_UI_HTTPS_URL);
|
|
372
|
+
console.log(`[OK] trusted TLS: ${LOCAL_UI_HTTPS_URL}`);
|
|
373
|
+
} catch (error) {
|
|
374
|
+
console.log(
|
|
375
|
+
`[WARN] trusted TLS: ${error instanceof Error ? error.message : String(error)}`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "notioncode",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Local companion runtime used by the `npx create notioncode` setup flow.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"default-browser-id": "^5.0.1"
|
|
9
|
+
},
|
|
7
10
|
"bin": {
|
|
8
11
|
"notioncode": "bin/nocode-local.js"
|
|
9
12
|
},
|