@notis_ai/cli 0.2.4 → 0.2.6
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 +47 -5
- package/dist/scaffolds/notis-database/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-database/notis.config.ts +0 -1
- package/dist/scaffolds/notis-database/src/mock-runtime.ts +1 -0
- package/dist/scaffolds/notis-journal/CHANGELOG.md +25 -0
- package/dist/scaffolds/notis-journal/app/globals.css +37 -0
- package/dist/scaffolds/notis-journal/app/insights/page.tsx +513 -0
- package/dist/scaffolds/notis-journal/app/journal-core.tsx +362 -0
- package/dist/scaffolds/notis-journal/app/journal-ui.tsx +337 -0
- package/dist/scaffolds/notis-journal/app/layout.tsx +6 -0
- package/dist/scaffolds/notis-journal/app/page.tsx +485 -0
- package/dist/scaffolds/notis-journal/components/ui/badge.tsx +28 -0
- package/dist/scaffolds/notis-journal/components/ui/button.tsx +53 -0
- package/dist/scaffolds/notis-journal/components/ui/card.tsx +56 -0
- package/dist/scaffolds/notis-journal/components.json +20 -0
- package/dist/scaffolds/notis-journal/index.html +12 -0
- package/dist/scaffolds/notis-journal/lib/utils.ts +6 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +132 -0
- package/dist/scaffolds/notis-journal/notis.config.ts +93 -0
- package/dist/scaffolds/notis-journal/package.json +34 -0
- package/dist/scaffolds/notis-journal/packages/sdk/package.json +36 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +60 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +145 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +229 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +83 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +220 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +186 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-journal/postcss.config.mjs +8 -0
- package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +120 -0
- package/dist/scaffolds/notis-journal/src/dev-main.tsx +58 -0
- package/dist/scaffolds/notis-journal/src/mock-runtime.ts +197 -0
- package/dist/scaffolds/notis-journal/tailwind.config.ts +58 -0
- package/dist/scaffolds/notis-journal/tsconfig.json +23 -0
- package/dist/scaffolds/notis-journal/vite.config.ts +10 -0
- package/dist/scaffolds/notis-notes/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-notes/app/page.tsx +325 -543
- package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-notes/notis.config.ts +0 -1
- package/dist/scaffolds/notis-random/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-random/notis.config.ts +0 -1
- package/dist/scaffolds/notis-random/src/mock-runtime.ts +1 -0
- package/dist/scaffolds.json +11 -0
- package/package.json +3 -3
- package/skills/notis-apps/SKILL.md +64 -6
- package/skills/notis-apps/cli.md +38 -2
- package/skills/notis-cli/SKILL.md +20 -2
- package/skills/notis-query/cli.md +1 -1
- package/src/command-specs/apps.js +529 -16
- package/src/command-specs/index.js +1 -1
- package/src/command-specs/meta.js +8 -2
- package/src/command-specs/tools.js +50 -37
- package/src/runtime/agent-browser.js +273 -1
- package/src/runtime/app-changelog.js +79 -0
- package/src/runtime/app-dev-server.js +113 -6
- package/src/runtime/app-dev-sessions.js +176 -12
- package/src/runtime/app-platform.js +200 -39
- package/src/runtime/assets/store-screenshot-dark.png +0 -0
- package/src/runtime/desktop-auth.js +93 -0
- package/src/runtime/profiles.js +37 -15
- package/src/runtime/store-screenshot.js +138 -0
- package/src/runtime/transport.js +21 -82
- package/template/.harness/index.html.tmpl +128 -6
- package/template/CHANGELOG.md +5 -0
- package/template/app/page.tsx +11 -41
- package/template/notis.config.ts +0 -1
- package/template/package-lock.json +4137 -0
- package/template/package.json +1 -0
- package/template/packages/sdk/package.json +4 -0
- package/template/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/template/packages/sdk/src/components/Markdown.tsx +60 -0
- package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +7 -2
- package/template/packages/sdk/src/config.ts +74 -0
- package/template/packages/sdk/src/documents.ts +229 -0
- package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/template/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/template/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/template/packages/sdk/src/hooks/useMultiSelect.ts +38 -2
- package/template/packages/sdk/src/hooks/useUpsertDocument.ts +54 -9
- package/template/packages/sdk/src/index.ts +30 -1
- package/template/packages/sdk/src/runtime.ts +76 -17
- package/template/packages/sdk/src/styles.css +148 -0
- package/dist/scaffolds/notis-database/metadata/cover.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/cover.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-random/metadata/cover.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
- package/template/metadata/cover.png +0 -0
- package/template/packages/sdk/src/hooks/useDatabase.ts +0 -76
|
@@ -25,13 +25,17 @@ import {
|
|
|
25
25
|
loadAppConfig,
|
|
26
26
|
prepareArtifactBuild,
|
|
27
27
|
readManifest,
|
|
28
|
+
readLinkedState,
|
|
29
|
+
writeLinkedState,
|
|
28
30
|
} from './app-platform.js';
|
|
31
|
+
import { linkAppDevSessionTarget } from './app-dev-sessions.js';
|
|
29
32
|
|
|
30
33
|
const CONTENT_TYPES = {
|
|
31
34
|
'.js': 'application/javascript; charset=utf-8',
|
|
32
35
|
'.css': 'text/css; charset=utf-8',
|
|
33
36
|
'.map': 'application/json; charset=utf-8',
|
|
34
37
|
};
|
|
38
|
+
const MAX_JSON_BODY_BYTES = 64 * 1024;
|
|
35
39
|
const RUNTIME_DIR = dirname(fileURLToPath(import.meta.url));
|
|
36
40
|
const CLI_ROOT = resolve(RUNTIME_DIR, '../..');
|
|
37
41
|
const REPO_ROOT = resolve(RUNTIME_DIR, '../../../..');
|
|
@@ -59,7 +63,7 @@ function corsHeaders(origin) {
|
|
|
59
63
|
const allowOrigin = origin && isAllowedOrigin(origin) ? origin : '*';
|
|
60
64
|
return {
|
|
61
65
|
'Access-Control-Allow-Origin': allowOrigin,
|
|
62
|
-
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
|
66
|
+
'Access-Control-Allow-Methods': 'GET, HEAD, POST, OPTIONS',
|
|
63
67
|
'Access-Control-Allow-Headers': 'Content-Type, Cache-Control',
|
|
64
68
|
'Cache-Control': 'no-store',
|
|
65
69
|
};
|
|
@@ -97,6 +101,32 @@ function readJsonFile(path) {
|
|
|
97
101
|
}
|
|
98
102
|
}
|
|
99
103
|
|
|
104
|
+
function readRequestJson(req) {
|
|
105
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
106
|
+
let body = '';
|
|
107
|
+
req.setEncoding('utf8');
|
|
108
|
+
req.on('data', (chunk) => {
|
|
109
|
+
body += chunk;
|
|
110
|
+
if (Buffer.byteLength(body, 'utf8') > MAX_JSON_BODY_BYTES) {
|
|
111
|
+
rejectPromise(new Error('request body too large'));
|
|
112
|
+
req.destroy();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
req.on('end', () => {
|
|
116
|
+
if (!body.trim()) {
|
|
117
|
+
resolvePromise({});
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
resolvePromise(JSON.parse(body));
|
|
122
|
+
} catch {
|
|
123
|
+
rejectPromise(new Error('invalid JSON body'));
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
req.on('error', rejectPromise);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
100
130
|
function reactVersionFromPeer(peerRange) {
|
|
101
131
|
if (typeof peerRange !== 'string' || !peerRange) {
|
|
102
132
|
return FALLBACK_REACT_VERSION;
|
|
@@ -186,7 +216,7 @@ function findHarnessRoute(manifest, routeSlug) {
|
|
|
186
216
|
return routes.find((route) => route?.slug === routeSlug) || null;
|
|
187
217
|
}
|
|
188
218
|
|
|
189
|
-
function buildHarnessDescriptor({ state, manifest, appConfig, route }) {
|
|
219
|
+
function buildHarnessDescriptor({ state, manifest, appConfig, route, scenario = null }) {
|
|
190
220
|
const databases = normalizeDatabaseDescriptors(
|
|
191
221
|
Array.isArray(appConfig?.databases) && appConfig.databases.length
|
|
192
222
|
? appConfig.databases
|
|
@@ -216,14 +246,28 @@ function buildHarnessDescriptor({ state, manifest, appConfig, route }) {
|
|
|
216
246
|
collection: route.collection || null,
|
|
217
247
|
},
|
|
218
248
|
databases,
|
|
219
|
-
context: { collectionItem: null },
|
|
249
|
+
context: { collectionItem: null, screenshotScenario: scenario },
|
|
220
250
|
tools,
|
|
221
251
|
};
|
|
222
252
|
}
|
|
223
253
|
|
|
224
|
-
function
|
|
254
|
+
function harnessFixtures(projectDir, scenario) {
|
|
255
|
+
const fixtureConfig = readJsonFile(join(projectDir, 'metadata', 'screenshot-fixtures.json')) || {};
|
|
256
|
+
const scenarios = fixtureConfig.scenarios && typeof fixtureConfig.scenarios === 'object'
|
|
257
|
+
? fixtureConfig.scenarios
|
|
258
|
+
: {};
|
|
259
|
+
return {
|
|
260
|
+
tools: fixtureConfig.tools && typeof fixtureConfig.tools === 'object' ? fixtureConfig.tools : {},
|
|
261
|
+
requests: fixtureConfig.requests && typeof fixtureConfig.requests === 'object' ? fixtureConfig.requests : {},
|
|
262
|
+
scenario: scenario && scenarios[scenario] && typeof scenarios[scenario] === 'object'
|
|
263
|
+
? scenarios[scenario]
|
|
264
|
+
: null,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function renderHarnessHtml({ state, manifest, appConfig, route, harnessOptions, scenario = null }) {
|
|
225
269
|
const template = readFileSync(HARNESS_TEMPLATE_PATH, 'utf-8');
|
|
226
|
-
const descriptor = buildHarnessDescriptor({ state, manifest, appConfig, route });
|
|
270
|
+
const descriptor = buildHarnessDescriptor({ state, manifest, appConfig, route, scenario });
|
|
227
271
|
const routeExport = route.export_name || route.exportName || exportNameFromPath(route.path || '/');
|
|
228
272
|
const replacements = {
|
|
229
273
|
'{{REACT_VERSION}}': resolveHarnessReactVersion(state.projectDir),
|
|
@@ -232,6 +276,7 @@ function renderHarnessHtml({ state, manifest, appConfig, route, harnessOptions }
|
|
|
232
276
|
'{{MODE}}': scriptJson(harnessOptions.mode || 'stub'),
|
|
233
277
|
'{{API_BASE}}': scriptJson(harnessOptions.apiBase || null),
|
|
234
278
|
'{{JWT}}': scriptJson(harnessOptions.jwt || null),
|
|
279
|
+
'{{FIXTURES}}': scriptJson(harnessFixtures(state.projectDir, scenario)),
|
|
235
280
|
};
|
|
236
281
|
let html = template;
|
|
237
282
|
for (const [token, value] of Object.entries(replacements)) {
|
|
@@ -243,7 +288,7 @@ function renderHarnessHtml({ state, manifest, appConfig, route, harnessOptions }
|
|
|
243
288
|
/**
|
|
244
289
|
* Start the dev server for one or more apps.
|
|
245
290
|
*
|
|
246
|
-
* @param {{apps: Array<{slug: string, projectDir: string, appId?: string, userId?: string}>, port: number, watch?: boolean, harness?: { mode?: string, apiBase?: string, jwt?: string }, log?: (m: string) => void, logError?: (m: string) => void}} options
|
|
291
|
+
* @param {{apps: Array<{slug: string, projectDir: string, appId?: string, targetAppId?: string, userId?: string}>, port: number, watch?: boolean, harness?: { mode?: string, apiBase?: string, jwt?: string }, log?: (m: string) => void, logError?: (m: string) => void}} options
|
|
247
292
|
*/
|
|
248
293
|
export async function startAppDevServer({
|
|
249
294
|
apps,
|
|
@@ -263,6 +308,7 @@ export async function startAppDevServer({
|
|
|
263
308
|
slug: app.slug,
|
|
264
309
|
projectDir: app.projectDir,
|
|
265
310
|
appId: app.appId || null,
|
|
311
|
+
targetAppId: app.targetAppId || null,
|
|
266
312
|
userId: app.userId || null,
|
|
267
313
|
canonicalBundleDir: getBundleDir(app.projectDir),
|
|
268
314
|
bundleDir: null,
|
|
@@ -339,6 +385,7 @@ export async function startAppDevServer({
|
|
|
339
385
|
}
|
|
340
386
|
|
|
341
387
|
const routeSlug = url.searchParams.get('route') || '';
|
|
388
|
+
const scenario = url.searchParams.get('scenario') || null;
|
|
342
389
|
const route = findHarnessRoute(manifest, routeSlug);
|
|
343
390
|
if (!route) {
|
|
344
391
|
res.writeHead(404, {
|
|
@@ -355,6 +402,7 @@ export async function startAppDevServer({
|
|
|
355
402
|
appConfig,
|
|
356
403
|
route,
|
|
357
404
|
harnessOptions: harness,
|
|
405
|
+
scenario,
|
|
358
406
|
});
|
|
359
407
|
res.writeHead(200, {
|
|
360
408
|
...headers,
|
|
@@ -385,6 +433,63 @@ export async function startAppDevServer({
|
|
|
385
433
|
}
|
|
386
434
|
|
|
387
435
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
436
|
+
if (req.method === 'POST') {
|
|
437
|
+
const routed = matchAppRoute(url.pathname);
|
|
438
|
+
const state = routed ? appState.get(routed.slug) : null;
|
|
439
|
+
if (state && routed.rest === 'link') {
|
|
440
|
+
void (async () => {
|
|
441
|
+
try {
|
|
442
|
+
const body = await readRequestJson(req);
|
|
443
|
+
const appId = typeof body.app_id === 'string' ? body.app_id.trim() : '';
|
|
444
|
+
if (!appId) {
|
|
445
|
+
res.writeHead(400, {
|
|
446
|
+
...headers,
|
|
447
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
448
|
+
});
|
|
449
|
+
res.end(JSON.stringify({ error: 'app_id is required' }));
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const currentState = readLinkedState(state.projectDir) || {};
|
|
454
|
+
const now = new Date().toISOString();
|
|
455
|
+
writeLinkedState(state.projectDir, {
|
|
456
|
+
...currentState,
|
|
457
|
+
app_id: appId,
|
|
458
|
+
dev_app_id: state.appId || currentState.dev_app_id,
|
|
459
|
+
linked_at: currentState.linked_at || now,
|
|
460
|
+
dev_linked_at: currentState.dev_linked_at || now,
|
|
461
|
+
updated_at: now,
|
|
462
|
+
});
|
|
463
|
+
state.targetAppId = appId;
|
|
464
|
+
linkAppDevSessionTarget({
|
|
465
|
+
appId: state.appId,
|
|
466
|
+
devSlug: state.slug,
|
|
467
|
+
targetAppId: appId,
|
|
468
|
+
lastHeartbeatAt: now,
|
|
469
|
+
});
|
|
470
|
+
const response = {
|
|
471
|
+
ok: true,
|
|
472
|
+
app_id: appId,
|
|
473
|
+
dev_app_id: state.appId || null,
|
|
474
|
+
target_app_id: appId,
|
|
475
|
+
};
|
|
476
|
+
res.writeHead(200, {
|
|
477
|
+
...headers,
|
|
478
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
479
|
+
});
|
|
480
|
+
res.end(JSON.stringify(response));
|
|
481
|
+
} catch (error) {
|
|
482
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
483
|
+
res.writeHead(400, {
|
|
484
|
+
...headers,
|
|
485
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
486
|
+
});
|
|
487
|
+
res.end(JSON.stringify({ error: message }));
|
|
488
|
+
}
|
|
489
|
+
})();
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
388
493
|
res.writeHead(405, headers);
|
|
389
494
|
res.end('method not allowed');
|
|
390
495
|
return;
|
|
@@ -394,6 +499,7 @@ export async function startAppDevServer({
|
|
|
394
499
|
const now = new Date().toISOString();
|
|
395
500
|
const sessions = Array.from(appState.values()).map((state) => ({
|
|
396
501
|
appId: state.appId,
|
|
502
|
+
targetAppId: state.targetAppId || undefined,
|
|
397
503
|
userId: state.userId,
|
|
398
504
|
devSlug: state.slug,
|
|
399
505
|
bundleBaseUrl: `http://127.0.0.1:${port}/a/${state.slug}`,
|
|
@@ -446,6 +552,7 @@ export async function startAppDevServer({
|
|
|
446
552
|
const manifest = readManifest(state.projectDir);
|
|
447
553
|
const payload = {
|
|
448
554
|
app_id: state.appId,
|
|
555
|
+
target_app_id: state.targetAppId || null,
|
|
449
556
|
dev_slug: state.slug,
|
|
450
557
|
manifest,
|
|
451
558
|
files: collectArtifactFiles(state.projectDir),
|
|
@@ -1,33 +1,85 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import {
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join, parse as parsePath } from 'node:path';
|
|
4
5
|
import { CONFIG_DIR } from './profiles.js';
|
|
5
6
|
|
|
6
7
|
export const DEFAULT_APP_DEV_SESSIONS_FILE = join(CONFIG_DIR, 'app-dev-sessions.json');
|
|
7
8
|
export const APP_DEV_SESSIONS_FILE = DEFAULT_APP_DEV_SESSIONS_FILE;
|
|
8
9
|
export const APP_DEV_SESSIONS_VERSION = 1;
|
|
10
|
+
export const APP_DEV_SESSION_MOUNT_ACKS_VERSION = 1;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Walk up from `startDir` looking for a workspace-scoped dev-sessions registry.
|
|
14
|
+
*
|
|
15
|
+
* The Notis desktop (and Conductor) reads a PER-WORKSPACE registry at
|
|
16
|
+
* `<workspace>/.context/app-dev-sessions.json` (it points its own `apps dev`
|
|
17
|
+
* at it via `NOTIS_APP_DEV_SESSIONS_FILE`). When a coding agent runs
|
|
18
|
+
* `apps dev` manually it does not inherit that env var, so its session would
|
|
19
|
+
* otherwise land in the global `~/.notis` registry and never appear in the
|
|
20
|
+
* desktop's Local development sidebar. Resolving the nearest `.context`
|
|
21
|
+
* registry keeps agent-run and desktop-run sessions in the same file.
|
|
22
|
+
*
|
|
23
|
+
* Returns the registry path if a `.context/` directory is found in `startDir`
|
|
24
|
+
* or an ancestor (stopping at the home directory / filesystem root), else null.
|
|
25
|
+
*/
|
|
26
|
+
export function findWorkspaceAppDevSessionsFile(startDir = process.cwd()) {
|
|
27
|
+
let dir = startDir;
|
|
28
|
+
const home = homedir();
|
|
29
|
+
const { root } = parsePath(dir);
|
|
30
|
+
// Bounded walk; `.context` lives at the workspace root, never above $HOME.
|
|
31
|
+
for (let depth = 0; depth < 64; depth += 1) {
|
|
32
|
+
if (existsSync(join(dir, '.context'))) {
|
|
33
|
+
return join(dir, '.context', 'app-dev-sessions.json');
|
|
34
|
+
}
|
|
35
|
+
if (dir === home || dir === root) break;
|
|
36
|
+
const parent = dirname(dir);
|
|
37
|
+
if (parent === dir) break;
|
|
38
|
+
dir = parent;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
9
42
|
|
|
10
43
|
export function getAppDevSessionsFile(filePath) {
|
|
11
44
|
if (filePath) {
|
|
12
45
|
return filePath;
|
|
13
46
|
}
|
|
14
47
|
const envPath = process.env.NOTIS_APP_DEV_SESSIONS_FILE;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
48
|
+
if (typeof envPath === 'string' && envPath.trim()) {
|
|
49
|
+
return envPath.trim();
|
|
50
|
+
}
|
|
51
|
+
const workspaceFile = findWorkspaceAppDevSessionsFile();
|
|
52
|
+
if (workspaceFile) {
|
|
53
|
+
return workspaceFile;
|
|
54
|
+
}
|
|
55
|
+
return DEFAULT_APP_DEV_SESSIONS_FILE;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getAppDevSessionMountAcksFile(sessionsFilePath) {
|
|
59
|
+
const resolvedSessionsFile = getAppDevSessionsFile(sessionsFilePath);
|
|
60
|
+
const parsed = parsePath(resolvedSessionsFile);
|
|
61
|
+
return join(parsed.dir, `${parsed.name}-mount-acks${parsed.ext || '.json'}`);
|
|
18
62
|
}
|
|
19
63
|
|
|
20
64
|
function normalizeSessions(raw) {
|
|
21
65
|
if (!raw || typeof raw !== 'object' || !Array.isArray(raw.sessions)) {
|
|
22
66
|
return [];
|
|
23
67
|
}
|
|
24
|
-
return raw.sessions
|
|
25
|
-
session
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
68
|
+
return raw.sessions
|
|
69
|
+
.filter((session) =>
|
|
70
|
+
session &&
|
|
71
|
+
typeof session === 'object' &&
|
|
72
|
+
typeof session.sessionId === 'string' &&
|
|
73
|
+
typeof session.appId === 'string' &&
|
|
74
|
+
typeof session.bundleBaseUrl === 'string',
|
|
75
|
+
)
|
|
76
|
+
.map((session) => {
|
|
77
|
+
if (typeof session.targetAppId === 'string' && session.targetAppId.trim()) {
|
|
78
|
+
return { ...session, targetAppId: session.targetAppId.trim() };
|
|
79
|
+
}
|
|
80
|
+
const { targetAppId: _targetAppId, ...rest } = session;
|
|
81
|
+
return rest;
|
|
82
|
+
});
|
|
31
83
|
}
|
|
32
84
|
|
|
33
85
|
export function readAppDevSessions(filePath) {
|
|
@@ -80,8 +132,120 @@ export function heartbeatAppDevSession(sessionId, lastHeartbeatAt, filePath) {
|
|
|
80
132
|
return writeAppDevSessions(registry, filePath);
|
|
81
133
|
}
|
|
82
134
|
|
|
135
|
+
export function linkAppDevSessionTarget({ appId, devSlug, targetAppId, lastHeartbeatAt = new Date().toISOString() }, filePath) {
|
|
136
|
+
const target = typeof targetAppId === 'string' ? targetAppId.trim() : '';
|
|
137
|
+
if (!target) {
|
|
138
|
+
return readAppDevSessions(filePath);
|
|
139
|
+
}
|
|
140
|
+
const registry = readAppDevSessions(filePath);
|
|
141
|
+
registry.sessions = registry.sessions.map((session) => {
|
|
142
|
+
const matchesApp = appId && session.appId === appId;
|
|
143
|
+
const matchesSlug = devSlug && session.devSlug === devSlug;
|
|
144
|
+
if (!matchesApp || !matchesSlug) {
|
|
145
|
+
return session;
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
...session,
|
|
149
|
+
targetAppId: target,
|
|
150
|
+
lastHeartbeatAt,
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
return writeAppDevSessions(registry, filePath);
|
|
154
|
+
}
|
|
155
|
+
|
|
83
156
|
export function removeAppDevSession(sessionId, filePath) {
|
|
84
157
|
const registry = readAppDevSessions(filePath);
|
|
85
158
|
registry.sessions = registry.sessions.filter((session) => session.sessionId !== sessionId);
|
|
86
|
-
|
|
159
|
+
const result = writeAppDevSessions(registry, filePath);
|
|
160
|
+
removeAppDevSessionMountAcknowledgements(sessionId, filePath);
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function normalizeMountAcknowledgements(raw) {
|
|
165
|
+
if (!raw || typeof raw !== 'object' || !Array.isArray(raw.acknowledgements)) {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
return raw.acknowledgements.filter((acknowledgement) =>
|
|
169
|
+
acknowledgement &&
|
|
170
|
+
typeof acknowledgement === 'object' &&
|
|
171
|
+
typeof acknowledgement.sessionId === 'string' &&
|
|
172
|
+
typeof acknowledgement.appId === 'string' &&
|
|
173
|
+
typeof acknowledgement.devSlug === 'string' &&
|
|
174
|
+
typeof acknowledgement.mountNonce === 'string' &&
|
|
175
|
+
typeof acknowledgement.acknowledgedAt === 'string' &&
|
|
176
|
+
acknowledgement.stage === 'listed',
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function readAppDevSessionMountAcknowledgements(sessionsFilePath) {
|
|
181
|
+
const filePath = getAppDevSessionMountAcksFile(sessionsFilePath);
|
|
182
|
+
if (!existsSync(filePath)) {
|
|
183
|
+
return { version: APP_DEV_SESSION_MOUNT_ACKS_VERSION, acknowledgements: [] };
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const raw = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
187
|
+
return {
|
|
188
|
+
version: APP_DEV_SESSION_MOUNT_ACKS_VERSION,
|
|
189
|
+
acknowledgements: normalizeMountAcknowledgements(raw),
|
|
190
|
+
};
|
|
191
|
+
} catch {
|
|
192
|
+
return { version: APP_DEV_SESSION_MOUNT_ACKS_VERSION, acknowledgements: [] };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function writeAppDevSessionMountAcknowledgements(registry, sessionsFilePath) {
|
|
197
|
+
const filePath = getAppDevSessionMountAcksFile(sessionsFilePath);
|
|
198
|
+
mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
|
|
199
|
+
const normalized = {
|
|
200
|
+
version: APP_DEV_SESSION_MOUNT_ACKS_VERSION,
|
|
201
|
+
acknowledgements: normalizeMountAcknowledgements(registry),
|
|
202
|
+
};
|
|
203
|
+
const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
204
|
+
writeFileSync(tempPath, JSON.stringify(normalized, null, 2), { mode: 0o600 });
|
|
205
|
+
renameSync(tempPath, filePath);
|
|
206
|
+
return normalized;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function removeAppDevSessionMountAcknowledgements(sessionId, sessionsFilePath) {
|
|
210
|
+
const filePath = getAppDevSessionMountAcksFile(sessionsFilePath);
|
|
211
|
+
if (!existsSync(filePath)) {
|
|
212
|
+
return { version: APP_DEV_SESSION_MOUNT_ACKS_VERSION, acknowledgements: [] };
|
|
213
|
+
}
|
|
214
|
+
const registry = readAppDevSessionMountAcknowledgements(sessionsFilePath);
|
|
215
|
+
registry.acknowledgements = registry.acknowledgements.filter(
|
|
216
|
+
(acknowledgement) => acknowledgement.sessionId !== sessionId,
|
|
217
|
+
);
|
|
218
|
+
return writeAppDevSessionMountAcknowledgements(registry, sessionsFilePath);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function mountAcknowledgementKey(value) {
|
|
222
|
+
return `${value.sessionId}:${value.appId}:${value.devSlug}:${value.mountNonce}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export async function waitForAppDevSessionMountAcknowledgements(expectedSessions, options = {}) {
|
|
226
|
+
const expected = Array.isArray(expectedSessions) ? expectedSessions : [expectedSessions];
|
|
227
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? Math.max(0, options.timeoutMs) : 15_000;
|
|
228
|
+
const pollIntervalMs = Number.isFinite(options.pollIntervalMs)
|
|
229
|
+
? Math.max(1, options.pollIntervalMs)
|
|
230
|
+
: 100;
|
|
231
|
+
const now = typeof options.now === 'function' ? options.now : Date.now;
|
|
232
|
+
const sleep = typeof options.sleep === 'function'
|
|
233
|
+
? options.sleep
|
|
234
|
+
: (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
235
|
+
const startedAt = now();
|
|
236
|
+
|
|
237
|
+
while (true) {
|
|
238
|
+
const registry = readAppDevSessionMountAcknowledgements(options.sessionsFilePath);
|
|
239
|
+
const acknowledgedKeys = new Set(registry.acknowledgements.map(mountAcknowledgementKey));
|
|
240
|
+
const missing = expected.filter((session) => !acknowledgedKeys.has(mountAcknowledgementKey(session)));
|
|
241
|
+
if (missing.length === 0) {
|
|
242
|
+
return { mounted: true, missing: [], acknowledgements: registry.acknowledgements };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const elapsedMs = now() - startedAt;
|
|
246
|
+
if (elapsedMs >= timeoutMs) {
|
|
247
|
+
return { mounted: false, missing, acknowledgements: registry.acknowledgements };
|
|
248
|
+
}
|
|
249
|
+
await sleep(Math.min(pollIntervalMs, timeoutMs - elapsedMs));
|
|
250
|
+
}
|
|
87
251
|
}
|