@ijfw/memory-server 1.3.0 → 1.4.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 +67 -0
- package/fixtures/team/book.json +47 -0
- package/fixtures/team/business.json +47 -0
- package/fixtures/team/content.json +47 -0
- package/fixtures/team/design.json +47 -0
- package/fixtures/team/mixed.json +59 -0
- package/fixtures/team/research.json +47 -0
- package/fixtures/team/software.json +47 -0
- package/package.json +1 -9
- package/src/.registry-meta-key.pem +3 -0
- package/src/active-extension-writer.js +142 -0
- package/src/blackboard.js +360 -0
- package/src/cli-run.js +91 -0
- package/src/codex-agents.js +177 -0
- package/src/compute/extract.js +3 -0
- package/src/compute/fts5.js +4 -4
- package/src/compute/graph-lock.js +0 -2
- package/src/compute/migrations/003-tier-semantic.js +3 -3
- package/src/compute/runner.js +44 -15
- package/src/compute/schema.sql +1 -1
- package/src/cross-orchestrator-cli.js +974 -13
- package/src/cross-orchestrator.js +9 -1
- package/src/dashboard-client.html +353 -1
- package/src/dashboard-server.js +318 -2
- package/src/design-intelligence.js +721 -0
- package/src/dispatch/colon-syntax.js +31 -3
- package/src/dispatch/domain-manifest.js +251 -0
- package/src/dispatch/extension.js +637 -0
- package/src/dispatch/override.js +221 -0
- package/src/dispatch-planner.js +1 -0
- package/src/dream/runner.mjs +3 -3
- package/src/extension-installer.js +1269 -0
- package/src/extension-manifest-schema.js +301 -0
- package/src/extension-permission-check.mjs +79 -0
- package/src/extension-registry.js +619 -0
- package/src/extension-signer.js +905 -0
- package/src/gate-result-formatter.js +95 -0
- package/src/gate-result-schema.js +274 -0
- package/src/gate-result.js +195 -0
- package/src/intent-router.js +2 -0
- package/src/lib/npm-view.js +1 -0
- package/src/memory/fts5.js +3 -3
- package/src/memory/migrations/002-tier-semantic.js +2 -2
- package/src/memory/staleness.js +1 -1
- package/src/memory/tier-promotion.js +6 -6
- package/src/memory/tokenize.js +1 -1
- package/src/memory-feedback.js +372 -0
- package/src/override-manifest-schema.js +146 -0
- package/src/override-resolver.js +699 -0
- package/src/override-use-registry.js +307 -0
- package/src/overrides/presets/academic.md +101 -0
- package/src/overrides/presets/book.md +87 -0
- package/src/overrides/presets/campaign.md +95 -0
- package/src/overrides/presets/screenplay.md +99 -0
- package/src/recovery/checkpoint.js +191 -0
- package/src/redactor.js +2 -0
- package/src/runtime-mediator.js +207 -0
- package/src/sandbox.js +17 -3
- package/src/server.js +94 -2
- package/src/swarm/dispatch-prompt.js +154 -0
- package/src/swarm/planner.js +399 -0
- package/src/swarm/review.js +136 -0
- package/src/swarm/worktree.js +239 -0
- package/src/team/generator.js +119 -0
- package/src/team/schemas.js +341 -0
- package/src/trident/dispatch.js +47 -0
- package/src/update-check.js +1 -1
- package/src/vectors.js +7 -8
package/src/dashboard-server.js
CHANGED
|
@@ -20,6 +20,8 @@ import { computeValueDelivered } from './cost/savings.js';
|
|
|
20
20
|
import { listMemoryFiles, listKnownProjects } from './memory/reader.js';
|
|
21
21
|
import { searchMemory } from './memory/search.js';
|
|
22
22
|
import { buildRecallCounts, mergeRecallCounts, topRecalled } from './memory/recall-counter.js';
|
|
23
|
+
import { PLACEHOLDER_HTML } from './design-companion.js';
|
|
24
|
+
import { listExtensions } from './extension-installer.js';
|
|
23
25
|
|
|
24
26
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
27
|
// REPO_ROOT: IJFW_PROJECT_ROOT override > user's interactive shell cwd (PWD) > process.cwd() fallback.
|
|
@@ -132,6 +134,24 @@ const PORT_WALK_MAX = 10; // walk up to 37891+PORT_WALK_MAX (37900)
|
|
|
132
134
|
const BACKFILL_DEFAULT = 200;
|
|
133
135
|
const BACKFILL_CAP = 50; // max observations sent on fresh connect (W4.6)
|
|
134
136
|
|
|
137
|
+
const DESIGN_LIVE_RELOAD_SCRIPT = `
|
|
138
|
+
<script>
|
|
139
|
+
(function(){
|
|
140
|
+
if (window.__ijfwDesignLiveReload) return;
|
|
141
|
+
window.__ijfwDesignLiveReload = true;
|
|
142
|
+
try {
|
|
143
|
+
var events = new EventSource('/design/stream');
|
|
144
|
+
events.addEventListener('reload', function(){ window.location.reload(); });
|
|
145
|
+
} catch (_) {}
|
|
146
|
+
})();
|
|
147
|
+
</script>`;
|
|
148
|
+
|
|
149
|
+
function injectDesignLiveReload(html) {
|
|
150
|
+
if (typeof html !== 'string' || html.includes('__ijfwDesignLiveReload')) return html;
|
|
151
|
+
if (/<\/body>/i.test(html)) return html.replace(/<\/body>/i, DESIGN_LIVE_RELOAD_SCRIPT + '\n</body>');
|
|
152
|
+
return html + DESIGN_LIVE_RELOAD_SCRIPT;
|
|
153
|
+
}
|
|
154
|
+
|
|
135
155
|
// ---------- integer param validator ----------
|
|
136
156
|
// Rejects numeric-prefix garbage like "10xyz" that parseInt accepts (W9-M2).
|
|
137
157
|
// Returns null on invalid input; caller should respond with 400.
|
|
@@ -668,6 +688,278 @@ export async function startServer(options = {}) {
|
|
|
668
688
|
res.end(JSON.stringify(config));
|
|
669
689
|
}],
|
|
670
690
|
|
|
691
|
+
// ---------- extensions: installed (B9) ----------
|
|
692
|
+
// Enumerate ~/.ijfw/state-org/extension-registry.json,
|
|
693
|
+
// ~/.ijfw/state-user/extension-registry.json, and project-scope registry.
|
|
694
|
+
// Returns JSON list with name, scope, version, publisher_keyId, permissions,
|
|
695
|
+
// last_activated_time. Path-traversal defence: resolve + assert under HOME.
|
|
696
|
+
['/api/extensions/installed', async (req, res) => {
|
|
697
|
+
try {
|
|
698
|
+
const home = homedir();
|
|
699
|
+
// realpath both sides — on macOS /var/folders -> /private/var/folders is a symlink,
|
|
700
|
+
// so the registry's realpathed path won't show as under un-realpathed HOME.
|
|
701
|
+
let homeCanon;
|
|
702
|
+
try { homeCanon = realpathSync(home); } catch { homeCanon = home; }
|
|
703
|
+
function isUnderHome(p) {
|
|
704
|
+
try {
|
|
705
|
+
const canon = realpathSync(p);
|
|
706
|
+
const rel = relative(homeCanon, canon);
|
|
707
|
+
return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel);
|
|
708
|
+
} catch { return false; }
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const REGISTRY_FILENAME = 'extension-registry.json';
|
|
712
|
+
const registryPaths = [
|
|
713
|
+
{ scope: 'org', path: join(home, '.ijfw', 'state-org', REGISTRY_FILENAME) },
|
|
714
|
+
{ scope: 'user', path: join(home, '.ijfw', 'state-user', REGISTRY_FILENAME) },
|
|
715
|
+
{ scope: 'project', path: join(REPO_ROOT, '.ijfw', 'state', REGISTRY_FILENAME) },
|
|
716
|
+
];
|
|
717
|
+
|
|
718
|
+
const seen = new Map();
|
|
719
|
+
for (const { scope, path: regPath } of registryPaths) {
|
|
720
|
+
// Path-traversal check on each registry path.
|
|
721
|
+
const resolvedReg = resolve(regPath);
|
|
722
|
+
const underHome = isUnderHome(resolvedReg) ||
|
|
723
|
+
resolvedReg.startsWith(resolve(REPO_ROOT));
|
|
724
|
+
if (!underHome) continue;
|
|
725
|
+
if (!existsSync(resolvedReg)) continue;
|
|
726
|
+
let registry;
|
|
727
|
+
try {
|
|
728
|
+
const raw = readFileSync(resolvedReg, 'utf8');
|
|
729
|
+
const parsed = JSON.parse(raw);
|
|
730
|
+
registry = Array.isArray(parsed.extensions) ? parsed.extensions : [];
|
|
731
|
+
} catch { continue; }
|
|
732
|
+
|
|
733
|
+
for (const e of registry) {
|
|
734
|
+
if (!e || !e.name || !e.version) continue;
|
|
735
|
+
const key = `${e.name}@${e.version}`;
|
|
736
|
+
if (seen.has(key)) continue;
|
|
737
|
+
const manifest = (e.manifest && typeof e.manifest === 'object') ? e.manifest : null;
|
|
738
|
+
seen.set(key, {
|
|
739
|
+
name: e.name,
|
|
740
|
+
scope,
|
|
741
|
+
version: e.version,
|
|
742
|
+
publisher_keyId: manifest ? (manifest.publisher_keyId || null) : null,
|
|
743
|
+
permissions: manifest ? (manifest.permissions || null) : null,
|
|
744
|
+
last_activated_time: e.last_activated_time || null,
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
749
|
+
res.end(JSON.stringify({ extensions: Array.from(seen.values()) }));
|
|
750
|
+
} catch (err) {
|
|
751
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
752
|
+
res.end(JSON.stringify({ extensions: [], error: err.message }));
|
|
753
|
+
}
|
|
754
|
+
}],
|
|
755
|
+
|
|
756
|
+
// ---------- extensions: active (B9) ----------
|
|
757
|
+
['/api/extensions/active', async (req, res) => {
|
|
758
|
+
try {
|
|
759
|
+
const home = homedir();
|
|
760
|
+
const activePath = join(home, '.ijfw', 'state', 'active-extension.json');
|
|
761
|
+
const resolvedActive = resolve(activePath);
|
|
762
|
+
// Return {active:null} BEFORE realpath — realpathSync throws on non-existent paths.
|
|
763
|
+
if (!existsSync(resolvedActive)) {
|
|
764
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
765
|
+
res.end(JSON.stringify({ active: null }));
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
// Path-traversal: realpath BOTH home and target so macOS /private symlinks
|
|
769
|
+
// (e.g. /var/folders -> /private/var/folders) resolve correctly AND symlinks
|
|
770
|
+
// pointing outside HOME are rejected.
|
|
771
|
+
let homeCanon;
|
|
772
|
+
try { homeCanon = realpathSync(home); } catch { homeCanon = home; }
|
|
773
|
+
let activeCanon;
|
|
774
|
+
try {
|
|
775
|
+
activeCanon = realpathSync(resolvedActive);
|
|
776
|
+
} catch {
|
|
777
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
778
|
+
res.end(JSON.stringify({ error: 'path traversal rejected' }));
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
const rel = relative(homeCanon, activeCanon);
|
|
782
|
+
if (rel.startsWith('..') || isAbsolute(rel)) {
|
|
783
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
784
|
+
res.end(JSON.stringify({ error: 'path traversal rejected' }));
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
const raw = readFileSync(activeCanon, 'utf8');
|
|
788
|
+
const parsed = JSON.parse(raw);
|
|
789
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
790
|
+
res.end(JSON.stringify({ active: parsed }));
|
|
791
|
+
} catch (err) {
|
|
792
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
793
|
+
res.end(JSON.stringify({ active: null, error: err.message }));
|
|
794
|
+
}
|
|
795
|
+
}],
|
|
796
|
+
|
|
797
|
+
// ---------- extensions: events (B9) ----------
|
|
798
|
+
// Tail-streams ~/.ijfw/state/permission-events.jsonl with optional filters.
|
|
799
|
+
// Allowlisted query params: limit, extension, tool, denied.
|
|
800
|
+
// SSE mode: Accept: text/event-stream. JSON array otherwise.
|
|
801
|
+
// Never reads full file into memory: streams line-by-line.
|
|
802
|
+
['/api/extensions/events', async (req, res, url) => {
|
|
803
|
+
const ALLOWED_FILTER_KEYS = new Set(['limit', 'extension', 'tool', 'denied']);
|
|
804
|
+
// Reject any non-allowlisted filter key.
|
|
805
|
+
for (const key of url.searchParams.keys()) {
|
|
806
|
+
if (!ALLOWED_FILTER_KEYS.has(key)) {
|
|
807
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
808
|
+
res.end(JSON.stringify({ error: `unknown filter parameter: ${key}` }));
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const home = homedir();
|
|
814
|
+
const eventsPath = join(home, '.ijfw', 'state', 'permission-events.jsonl');
|
|
815
|
+
|
|
816
|
+
const rawLimit = url.searchParams.get('limit');
|
|
817
|
+
let limit = 200;
|
|
818
|
+
if (rawLimit !== null) {
|
|
819
|
+
const n = safeIntegerParam(rawLimit, 10_000);
|
|
820
|
+
if (n === null) {
|
|
821
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
822
|
+
res.end(JSON.stringify({ error: 'invalid limit' }));
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
limit = n;
|
|
826
|
+
}
|
|
827
|
+
const filterExtension = url.searchParams.get('extension') || null;
|
|
828
|
+
const filterTool = url.searchParams.get('tool') || null;
|
|
829
|
+
const filterDenied = url.searchParams.has('denied')
|
|
830
|
+
? (url.searchParams.get('denied') !== 'false')
|
|
831
|
+
: null;
|
|
832
|
+
|
|
833
|
+
// Stream-tail: read only the last TAIL_CHUNK bytes, never slurp the full file.
|
|
834
|
+
// For permission-events.jsonl which rotates at 10_000 lines, 2MB covers
|
|
835
|
+
// many thousands of events without loading the entire file into memory.
|
|
836
|
+
const TAIL_CHUNK = 2 * 1024 * 1024; // 2MB
|
|
837
|
+
function tailEvents() {
|
|
838
|
+
if (!existsSync(eventsPath)) return [];
|
|
839
|
+
let st;
|
|
840
|
+
try { st = statSync(eventsPath); } catch { return []; }
|
|
841
|
+
if (st.size === 0) return [];
|
|
842
|
+
let lines = [];
|
|
843
|
+
try {
|
|
844
|
+
const fullBuf = readFileSync(eventsPath);
|
|
845
|
+
const slice = fullBuf.subarray(Math.max(0, fullBuf.length - TAIL_CHUNK));
|
|
846
|
+
const text = slice.toString('utf8');
|
|
847
|
+
lines = text.split('\n').filter(Boolean);
|
|
848
|
+
// If we sliced mid-line, the first element may be truncated — drop it.
|
|
849
|
+
if (fullBuf.length > TAIL_CHUNK) lines = lines.slice(1);
|
|
850
|
+
} catch { return []; }
|
|
851
|
+
|
|
852
|
+
const results = [];
|
|
853
|
+
for (let i = lines.length - 1; i >= 0 && results.length < limit; i--) {
|
|
854
|
+
let obj;
|
|
855
|
+
try { obj = JSON.parse(lines[i]); } catch { continue; }
|
|
856
|
+
if (filterExtension !== null && obj.extension !== filterExtension) continue;
|
|
857
|
+
if (filterTool !== null && obj.tool !== filterTool) continue;
|
|
858
|
+
if (filterDenied !== null && Boolean(!obj.allowed) !== filterDenied) continue;
|
|
859
|
+
results.unshift(obj);
|
|
860
|
+
}
|
|
861
|
+
return results;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const isSSE = (req.headers['accept'] || '').includes('text/event-stream');
|
|
865
|
+
if (isSSE) {
|
|
866
|
+
res.writeHead(200, {
|
|
867
|
+
'Content-Type': 'text/event-stream',
|
|
868
|
+
'Cache-Control': 'no-cache',
|
|
869
|
+
'Connection': 'keep-alive',
|
|
870
|
+
'X-Accel-Buffering': 'no',
|
|
871
|
+
});
|
|
872
|
+
res.write(': connected\n\n');
|
|
873
|
+
// Send current tail as initial batch.
|
|
874
|
+
const initial = tailEvents();
|
|
875
|
+
for (const evt of initial) {
|
|
876
|
+
try { res.write(`data: ${JSON.stringify(evt)}\n\n`); } catch { break; }
|
|
877
|
+
}
|
|
878
|
+
// Watch for new events. lastLineCount must match what the watcher
|
|
879
|
+
// measures (tail-chunk lines), NOT the limited initial batch length —
|
|
880
|
+
// mismatching them causes a replay storm when the file is bigger than
|
|
881
|
+
// the limit.
|
|
882
|
+
let evtWatcher = null;
|
|
883
|
+
let lastLineCount = 0;
|
|
884
|
+
try {
|
|
885
|
+
const buf0 = readFileSync(eventsPath);
|
|
886
|
+
const slice0 = buf0.subarray(Math.max(0, buf0.length - TAIL_CHUNK));
|
|
887
|
+
let lines0 = slice0.toString('utf8').split('\n').filter(Boolean);
|
|
888
|
+
if (buf0.length > TAIL_CHUNK) lines0 = lines0.slice(1);
|
|
889
|
+
lastLineCount = lines0.length;
|
|
890
|
+
} catch { /* eventsPath missing or unreadable — start from 0 */ }
|
|
891
|
+
try {
|
|
892
|
+
evtWatcher = watch(existsSync(eventsPath) ? eventsPath : join(home, '.ijfw', 'state'), () => {
|
|
893
|
+
if (!existsSync(eventsPath)) return;
|
|
894
|
+
try {
|
|
895
|
+
// Use the tail-chunk reader (bounded read) rather than slurping the
|
|
896
|
+
// full file. At 10K lines × ~1-2KB each = 10-20MB sync read per watch
|
|
897
|
+
// event, which is unacceptable for a long-lived SSE connection.
|
|
898
|
+
try { statSync(eventsPath); } catch { return; }
|
|
899
|
+
const buf = (() => {
|
|
900
|
+
try { return readFileSync(eventsPath); } catch { return null; }
|
|
901
|
+
})();
|
|
902
|
+
if (!buf) return;
|
|
903
|
+
const slice = buf.subarray(Math.max(0, buf.length - TAIL_CHUNK));
|
|
904
|
+
const text = slice.toString('utf8');
|
|
905
|
+
let lines = text.split('\n').filter(Boolean);
|
|
906
|
+
if (buf.length > TAIL_CHUNK) lines = lines.slice(1);
|
|
907
|
+
if (lines.length > lastLineCount) {
|
|
908
|
+
const newLines = lines.slice(lastLineCount);
|
|
909
|
+
lastLineCount = lines.length;
|
|
910
|
+
for (const line of newLines) {
|
|
911
|
+
let obj; try { obj = JSON.parse(line); } catch { continue; }
|
|
912
|
+
if (filterExtension !== null && obj.extension !== filterExtension) continue;
|
|
913
|
+
if (filterTool !== null && obj.tool !== filterTool) continue;
|
|
914
|
+
if (filterDenied !== null && Boolean(!obj.allowed) !== filterDenied) continue;
|
|
915
|
+
try { res.write(`data: ${JSON.stringify(obj)}\n\n`); } catch {}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
} catch {}
|
|
919
|
+
});
|
|
920
|
+
if (evtWatcher) evtWatcher.on('error', () => {});
|
|
921
|
+
} catch {}
|
|
922
|
+
req.on('close', () => {
|
|
923
|
+
if (evtWatcher) { try { evtWatcher.close(); } catch {} }
|
|
924
|
+
});
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// JSON array response.
|
|
929
|
+
const events = tailEvents();
|
|
930
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
931
|
+
res.end(JSON.stringify(events));
|
|
932
|
+
}],
|
|
933
|
+
|
|
934
|
+
// ---------- extensions health (W3/t15) ----------
|
|
935
|
+
// Reads .ijfw/state/extension-registry.json (project) plus org/user via
|
|
936
|
+
// listExtensions(). Missing or malformed registry yields {extensions: []}
|
|
937
|
+
// (day-1 protection). ENOENT and JSON.parse errors are swallowed inside
|
|
938
|
+
// readRegistry(); this handler adds an outer try/catch as a final safety
|
|
939
|
+
// net so a malformed registry can never crash the dashboard.
|
|
940
|
+
['/api/extensions/health', async (req, res) => {
|
|
941
|
+
try {
|
|
942
|
+
const extensions = await listExtensions(REPO_ROOT);
|
|
943
|
+
// Strip embedded manifest blobs -- the dashboard only needs the
|
|
944
|
+
// status summary fields, not the full manifest.
|
|
945
|
+
const out = (Array.isArray(extensions) ? extensions : []).map((e) => ({
|
|
946
|
+
name: e.name,
|
|
947
|
+
version: e.version,
|
|
948
|
+
scope: e.scope,
|
|
949
|
+
installed_at: e.installed_at || null,
|
|
950
|
+
status: e.status || 'stale',
|
|
951
|
+
last_trident_verdict: e.last_trident_verdict ?? null,
|
|
952
|
+
}));
|
|
953
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
954
|
+
res.end(JSON.stringify({ extensions: out }));
|
|
955
|
+
} catch (err) {
|
|
956
|
+
// ENOENT / missing / malformed -> 200 with empty list (day-1).
|
|
957
|
+
process.stderr.write(`[ijfw-mcp] /api/extensions/health: ${err && err.message ? err.message : err}\n`);
|
|
958
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
959
|
+
res.end(JSON.stringify({ extensions: [], error: 'malformed registry' }));
|
|
960
|
+
}
|
|
961
|
+
}],
|
|
962
|
+
|
|
671
963
|
['/api/value-delivered', (req, res, url) => {
|
|
672
964
|
try {
|
|
673
965
|
const platform = url.searchParams.get('platform') || 'claude';
|
|
@@ -699,6 +991,29 @@ export async function startServer(options = {}) {
|
|
|
699
991
|
}],
|
|
700
992
|
|
|
701
993
|
// ---------- design companion ----------
|
|
994
|
+
[/^\/design\/files\/[^/]+\.html$/, async (req, res, url) => {
|
|
995
|
+
const contentDir = join(homedir(), '.ijfw', 'design-companion', 'content');
|
|
996
|
+
mkdirSync(contentDir, { recursive: true });
|
|
997
|
+
const name = url.pathname.split('/').pop();
|
|
998
|
+
const filePath = join(contentDir, name);
|
|
999
|
+
let html = null;
|
|
1000
|
+
try {
|
|
1001
|
+
if (existsSync(filePath)) html = readFileSync(filePath, 'utf8');
|
|
1002
|
+
} catch {}
|
|
1003
|
+
if (!html) {
|
|
1004
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
1005
|
+
res.end('404 Not Found');
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
html = injectDesignLiveReload(html);
|
|
1009
|
+
res.writeHead(200, {
|
|
1010
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
1011
|
+
'Cache-Control': 'no-store',
|
|
1012
|
+
'Content-Security-Policy': "default-src 'self' https: data:; style-src 'self' 'unsafe-inline' https:; script-src 'self' 'unsafe-inline' https:; img-src 'self' data: https:; font-src 'self' data: https:; connect-src 'self'",
|
|
1013
|
+
});
|
|
1014
|
+
res.end(html);
|
|
1015
|
+
}],
|
|
1016
|
+
|
|
702
1017
|
['/design', async (req, res) => {
|
|
703
1018
|
const contentDir = join(homedir(), '.ijfw', 'design-companion', 'content');
|
|
704
1019
|
mkdirSync(contentDir, { recursive: true });
|
|
@@ -714,12 +1029,13 @@ export async function startServer(options = {}) {
|
|
|
714
1029
|
}
|
|
715
1030
|
} catch {}
|
|
716
1031
|
if (!html) {
|
|
717
|
-
html =
|
|
1032
|
+
html = PLACEHOLDER_HTML;
|
|
718
1033
|
}
|
|
1034
|
+
html = injectDesignLiveReload(html);
|
|
719
1035
|
res.writeHead(200, {
|
|
720
1036
|
'Content-Type': 'text/html; charset=utf-8',
|
|
721
1037
|
'Cache-Control': 'no-store',
|
|
722
|
-
'Content-Security-Policy': "default-src 'self'
|
|
1038
|
+
'Content-Security-Policy': "default-src 'self' https: data:; style-src 'self' 'unsafe-inline' https:; script-src 'self' 'unsafe-inline' https:; img-src 'self' data: https:; font-src 'self' data: https:; connect-src 'self'",
|
|
723
1039
|
});
|
|
724
1040
|
res.end(html);
|
|
725
1041
|
}],
|