quiver-cli 0.8.0 → 1.1.0
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 +127 -45
- package/bin/quiver-cli.mjs +3 -1
- package/dist/cli.js +666 -436
- package/package.json +2 -2
- package/template/.agents/AGENTS.md +4 -3
- package/template/.agents/config.json +7 -0
- package/template/.agents/plugins/opencode/rtk.ts +34 -0
- package/template/.agents/skills/agent-browser/SKILL.md +1 -0
- package/template/.agents/skills/apps/skybridge/SKILL.md +4 -0
- package/template/.agents/skills/design/impeccable/SKILL.md +36 -118
- package/template/.agents/skills/design/impeccable/reference/adapt.md +1 -0
- package/template/.agents/skills/design/impeccable/reference/adapt.native.md +58 -0
- package/template/.agents/skills/design/impeccable/reference/android.md +40 -0
- package/template/.agents/skills/design/impeccable/reference/animate.md +73 -188
- package/template/.agents/skills/design/impeccable/reference/audit.md +12 -9
- package/template/.agents/skills/design/impeccable/reference/audit.native.md +139 -0
- package/template/.agents/skills/design/impeccable/reference/bolder.md +19 -101
- package/template/.agents/skills/design/impeccable/reference/clarify.md +59 -253
- package/template/.agents/skills/design/impeccable/reference/colorize.md +51 -222
- package/template/.agents/skills/design/impeccable/reference/craft-floor.md +42 -0
- package/template/.agents/skills/design/impeccable/reference/craft.md +3 -121
- package/template/.agents/skills/design/impeccable/reference/critique.md +44 -23
- package/template/.agents/skills/design/impeccable/reference/degraded/asset-producer.md +90 -0
- package/template/.agents/skills/design/impeccable/reference/degraded/documenter.md +24 -0
- package/template/.agents/skills/design/impeccable/reference/degraded/finish-reviewer.md +37 -0
- package/template/.agents/skills/design/impeccable/reference/degraded/manual-edit-applier.md +92 -0
- package/template/.agents/skills/design/impeccable/reference/delight.md +47 -279
- package/template/.agents/skills/design/impeccable/reference/distill.md +2 -2
- package/template/.agents/skills/design/impeccable/reference/doctor.md +53 -0
- package/template/.agents/skills/design/impeccable/reference/document.md +60 -73
- package/template/.agents/skills/design/impeccable/reference/harden.md +1 -12
- package/template/.agents/skills/design/impeccable/reference/hooks.md +20 -5
- package/template/.agents/skills/design/impeccable/reference/init.md +72 -119
- package/template/.agents/skills/design/impeccable/reference/ios.md +45 -0
- package/template/.agents/skills/design/impeccable/reference/layout.md +54 -131
- package/template/.agents/skills/design/impeccable/reference/live-setup.md +102 -0
- package/template/.agents/skills/design/impeccable/reference/live.md +116 -511
- package/template/.agents/skills/design/impeccable/reference/new-work.md +105 -0
- package/template/.agents/skills/design/impeccable/reference/{product.md → operate.md} +6 -5
- package/template/.agents/skills/design/impeccable/reference/optimize.md +4 -4
- package/template/.agents/skills/design/impeccable/reference/overdrive.md +1 -4
- package/template/.agents/skills/design/impeccable/reference/polish.md +68 -212
- package/template/.agents/skills/design/impeccable/reference/quieter.md +3 -3
- package/template/.agents/skills/design/impeccable/reference/routing.md +18 -0
- package/template/.agents/skills/design/impeccable/reference/shape.md +38 -144
- package/template/.agents/skills/design/impeccable/reference/typeset.md +51 -250
- package/template/.agents/skills/design/impeccable/reference/visualize.md +47 -0
- package/template/.agents/skills/design/impeccable/scripts/command-metadata.json +1 -1
- package/template/.agents/skills/design/impeccable/scripts/concept-seed.mjs +558 -0
- package/template/.agents/skills/design/impeccable/scripts/context-signals.mjs +119 -10
- package/template/.agents/skills/design/impeccable/scripts/context.mjs +534 -45
- package/template/.agents/skills/design/impeccable/scripts/critique-storage.mjs +18 -47
- package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs +96 -10
- package/template/.agents/skills/design/impeccable/scripts/detector/cli/main.mjs +174 -26
- package/template/.agents/skills/design/impeccable/scripts/detector/design-system.mjs +233 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns-browser.js +3348 -203
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/browser/detect-url.mjs +102 -7
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/regex/detect-text.mjs +297 -97
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +187 -16
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/detect-html.mjs +41 -11
- package/template/.agents/skills/design/impeccable/scripts/detector/findings.mjs +7 -1
- package/template/.agents/skills/design/impeccable/scripts/detector/node/file-system.mjs +16 -2
- package/template/.agents/skills/design/impeccable/scripts/detector/registry/antipatterns.mjs +207 -38
- package/template/.agents/skills/design/impeccable/scripts/detector/rules/checks.mjs +3082 -173
- package/template/.agents/skills/design/impeccable/scripts/detector/shared/constants.mjs +11 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/shared/fonts.mjs +30 -0
- package/template/.agents/skills/design/impeccable/scripts/doctor.mjs +336 -0
- package/template/.agents/skills/design/impeccable/scripts/embed-prompt.mjs +133 -0
- package/template/.agents/skills/design/impeccable/scripts/generate-image.mjs +240 -0
- package/template/.agents/skills/design/impeccable/scripts/hook-admin.mjs +98 -18
- package/template/.agents/skills/design/impeccable/scripts/hook-before-edit.mjs +46 -6
- package/template/.agents/skills/design/impeccable/scripts/hook-lib.mjs +518 -50
- package/template/.agents/skills/design/impeccable/scripts/hook.mjs +25 -8
- package/template/.agents/skills/design/impeccable/scripts/lib/artifact-schema.mjs +93 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/composition-catalog.mjs +200 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/concept-catalog.mjs +357 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-config.mjs +27 -7
- package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-paths.mjs +17 -8
- package/template/.agents/skills/design/impeccable/scripts/lib/provider.mjs +5 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/roll-selection.mjs +362 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/staleness-deep.mjs +457 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/staleness-notice.mjs +169 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/staleness.mjs +457 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/surface-briefs.mjs +151 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/target-slug.mjs +33 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/template-extensions.mjs +146 -0
- package/template/.agents/skills/design/impeccable/scripts/live/accept-css.mjs +617 -0
- package/template/.agents/skills/design/impeccable/scripts/live/accept-verify.mjs +60 -0
- package/template/.agents/skills/design/impeccable/scripts/live/browser-script-parts.mjs +7 -1
- package/template/.agents/skills/design/impeccable/scripts/live/completion.mjs +10 -1
- package/template/.agents/skills/design/impeccable/scripts/live/event-validation.mjs +67 -5
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/astro.mjs +47 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/detect-utils.mjs +73 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/index.mjs +143 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/journal.mjs +197 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/nextjs.mjs +49 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/nuxt.mjs +161 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/script-src.mjs +17 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/static-html.mjs +26 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/sveltekit.mjs +71 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/tag-strategy.mjs +247 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/tanstack-start.mjs +70 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/vite-generic.mjs +42 -0
- package/template/.agents/skills/design/impeccable/scripts/live/generation-preflight.mjs +149 -0
- package/template/.agents/skills/design/impeccable/scripts/live/instructions.mjs +142 -0
- package/template/.agents/skills/design/impeccable/scripts/live/poll-lanes.mjs +14 -0
- package/template/.agents/skills/design/impeccable/scripts/live/roots.mjs +508 -0
- package/template/.agents/skills/design/impeccable/scripts/live/session-store.mjs +324 -50
- package/template/.agents/skills/design/impeccable/scripts/live/source-lock.mjs +105 -0
- package/template/.agents/skills/design/impeccable/scripts/live/source-search.mjs +105 -0
- package/template/.agents/skills/design/impeccable/scripts/live/svelte-ast.mjs +961 -0
- package/template/.agents/skills/design/impeccable/scripts/live/svelte-component.mjs +588 -72
- package/template/.agents/skills/design/impeccable/scripts/live/sveltekit-adapter.mjs +59 -17
- package/template/.agents/skills/design/impeccable/scripts/live/tanstack-adapter.mjs +280 -0
- package/template/.agents/skills/design/impeccable/scripts/live/vocabulary.mjs +135 -0
- package/template/.agents/skills/design/impeccable/scripts/live-accept.mjs +210 -68
- package/template/.agents/skills/design/impeccable/scripts/live-browser.js +1562 -223
- package/template/.agents/skills/design/impeccable/scripts/live-commit-manual-edits.mjs +3 -0
- package/template/.agents/skills/design/impeccable/scripts/live-complete.mjs +33 -1
- package/template/.agents/skills/design/impeccable/scripts/live-inject.mjs +175 -255
- package/template/.agents/skills/design/impeccable/scripts/live-insert.mjs +26 -6
- package/template/.agents/skills/design/impeccable/scripts/live-manual-edit-evidence.mjs +6 -1
- package/template/.agents/skills/design/impeccable/scripts/live-poll.mjs +61 -16
- package/template/.agents/skills/design/impeccable/scripts/live-resume.mjs +39 -10
- package/template/.agents/skills/design/impeccable/scripts/live-server.mjs +573 -47
- package/template/.agents/skills/design/impeccable/scripts/live-status.mjs +17 -7
- package/template/.agents/skills/design/impeccable/scripts/live-wrap.mjs +124 -91
- package/template/.agents/skills/design/impeccable/scripts/live.mjs +88 -26
- package/template/.agents/skills/design/impeccable/scripts/palette.mjs +76 -81
- package/template/.agents/skills/design/impeccable/scripts/pin.mjs +18 -11
- package/template/.agents/skills/design/impeccable/scripts/serve-question.mjs +932 -0
- package/template/.agents/skills/design/impeccable/scripts/surface-brief.mjs +74 -0
- package/template/.agents/skills/design/shadcn/SKILL.md +46 -11
- package/template/.agents/skills/design/shadcn/cli.md +49 -16
- package/template/.agents/skills/design/shadcn/customization.md +14 -7
- package/template/.agents/skills/design/shadcn/evals/evals.json +30 -0
- package/template/.agents/skills/design/shadcn/mcp.md +27 -16
- package/template/.agents/skills/design/shadcn/registry.md +277 -0
- package/template/.agents/skills/design/shadcn/rules/chat.md +224 -0
- package/template/.agents/skills/design/shadcn/rules/composition.md +20 -2
- package/template/.agents/skills/design/shadcn/rules/styling.md +23 -0
- package/template/.agents/skills/find-skills/SKILL.md +2 -3
- package/template/.agents/skills/hono/SKILL.md +579 -0
- package/template/.agents/skills/integrations/langfuse/SKILL.md +10 -6
- package/template/.agents/skills/integrations/langfuse/references/ci-cd.md +41 -0
- package/template/.agents/skills/integrations/langfuse/references/cli.md +8 -0
- package/template/.agents/skills/integrations/langfuse/references/error-analysis.md +15 -27
- package/template/.agents/skills/integrations/langfuse/references/instrumentation.md +39 -52
- package/template/.agents/skills/integrations/langfuse/references/judge-calibration.md +5 -2
- package/template/.agents/skills/integrations/langfuse/references/prompt-engineering.md +35 -0
- package/template/.agents/skills/integrations/langfuse/references/prompt-migration.md +41 -196
- package/template/.agents/skills/integrations/langfuse/references/skill-feedback.md +3 -0
- package/template/.agents/skills/integrations/langfuse/references/trace-evaluator-upgrade.md +76 -0
- package/template/.agents/skills/integrations/langfuse/references/user-feedback.md +4 -0
- package/template/.agents/skills/integrations/langfuse/references/v4-project-migration.md +73 -0
- package/template/.agents/skills/supabase/CHANGELOG.md +71 -0
- package/template/.agents/skills/supabase/SKILL.md +145 -0
- package/template/.agents/skills/supabase/assets/feedback-issue-template.md +17 -0
- package/template/.agents/skills/supabase/references/skill-feedback.md +17 -0
- package/template/.agents/skills/supabase-postgres-best-practices/CHANGELOG.md +73 -0
- package/template/.agents/skills/supabase-postgres-best-practices/SKILL.md +64 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/_contributing.md +170 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/_sections.md +39 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/_template.md +34 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/advanced-full-text-search.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/advanced-jsonb-indexing.md +49 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/conn-idle-timeout.md +46 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/conn-limits.md +44 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/conn-pooling.md +41 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/conn-prepared-statements.md +46 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/data-batch-inserts.md +54 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/data-n-plus-one.md +53 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/data-pagination.md +50 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/data-upsert.md +50 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/lock-advisory.md +56 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/lock-deadlock-prevention.md +68 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/lock-short-transactions.md +50 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/lock-skip-locked.md +54 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-explain-analyze.md +45 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-pg-stat-statements.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-vacuum-analyze.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-composite-indexes.md +44 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-covering-indexes.md +40 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-index-types.md +48 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-missing-indexes.md +43 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-partial-indexes.md +45 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-constraints.md +80 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-data-types.md +46 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-foreign-key-indexes.md +59 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-lowercase-identifiers.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-partitioning.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-primary-keys.md +61 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/security-privileges.md +54 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/security-rls-basics.md +50 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/security-rls-performance.md +63 -0
- package/template/.agents/upstreams.json +34 -12
- package/template/.agents/skills/design/impeccable/reference/brand.md +0 -108
- package/template/.agents/skills/design/impeccable/reference/codex.md +0 -105
- package/template/.agents/skills/design/impeccable/reference/interaction-design.md +0 -189
- package/template/.agents/skills/integrations/langfuse/references/sdk-upgrade.md +0 -175
|
@@ -3,8 +3,12 @@
|
|
|
3
3
|
* Canonical durable completion acknowledgement for Impeccable live sessions.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
6
8
|
import { createLiveSessionStore } from './live/session-store.mjs';
|
|
7
9
|
import { readLiveServerInfo } from './lib/impeccable-paths.mjs';
|
|
10
|
+
import { enterLiveRoot } from './live/roots.mjs';
|
|
11
|
+
import { verifyAcceptedFile } from './live/accept-verify.mjs';
|
|
8
12
|
|
|
9
13
|
function parseArgs(argv) {
|
|
10
14
|
const out = { status: 'complete' };
|
|
@@ -15,6 +19,7 @@ function parseArgs(argv) {
|
|
|
15
19
|
else if (arg === '--discarded' || arg === '--discard') out.status = 'discarded';
|
|
16
20
|
else if (arg === '--error') { out.status = 'agent_error'; out.message = argv[++i] || 'unknown error'; }
|
|
17
21
|
else if (arg.startsWith('--error=')) { out.status = 'agent_error'; out.message = arg.slice('--error='.length); }
|
|
22
|
+
else if (arg === '--force') out.force = true;
|
|
18
23
|
else if (arg === '--help' || arg === '-h') out.help = true;
|
|
19
24
|
}
|
|
20
25
|
return out;
|
|
@@ -23,10 +28,36 @@ function parseArgs(argv) {
|
|
|
23
28
|
export async function completeCli() {
|
|
24
29
|
const args = parseArgs(process.argv.slice(2));
|
|
25
30
|
if (args.help || !args.id) {
|
|
26
|
-
console.log(`Usage: node live-complete.mjs --id SESSION_ID [--discarded|--error MESSAGE]\n\nAppend the final durable session acknowledgement. Use after accept/discard cleanup is verified.`);
|
|
31
|
+
console.log(`Usage: node live-complete.mjs --id SESSION_ID [--discarded|--error MESSAGE] [--force]\n\nAppend the final durable session acknowledgement. Use after accept/discard cleanup is verified.\nCompletion is refused while the session's source file still carries live-mode leftovers\n(markers, data-p-* attributes, unbaked --p-* vars); fix the file or pass --force.`);
|
|
27
32
|
process.exit(args.help ? 0 : 1);
|
|
28
33
|
}
|
|
29
34
|
|
|
35
|
+
// The carbonize contract used to be prose; this makes it mechanical. A
|
|
36
|
+
// "complete" while the source still carries live plumbing is how markers
|
|
37
|
+
// and dead param branches accumulated across sessions.
|
|
38
|
+
if (args.status === 'complete' && !args.force) {
|
|
39
|
+
const store = createLiveSessionStore({ cwd: process.cwd(), sessionId: args.id });
|
|
40
|
+
const snapshot = store.getSnapshot(args.id, { includeCompleted: true });
|
|
41
|
+
const sourceFile = snapshot?.sourceFile;
|
|
42
|
+
const absSource = sourceFile ? path.resolve(process.cwd(), sourceFile) : null;
|
|
43
|
+
const relSource = absSource ? path.relative(process.cwd(), absSource) : null;
|
|
44
|
+
const insideProject = relSource !== null && relSource !== '' && !relSource.startsWith('..') && !path.isAbsolute(relSource);
|
|
45
|
+
if (insideProject && !relSource.startsWith('node_modules' + path.sep) && !relSource.startsWith('node_modules/')) {
|
|
46
|
+
const verify = verifyAcceptedFile(fs, absSource);
|
|
47
|
+
if (!verify.clean) {
|
|
48
|
+
console.log(JSON.stringify({
|
|
49
|
+
ok: false,
|
|
50
|
+
error: 'source_dirty',
|
|
51
|
+
id: args.id,
|
|
52
|
+
file: sourceFile,
|
|
53
|
+
findings: verify.findings,
|
|
54
|
+
hint: 'The accepted source still carries live-mode leftovers. Finish the carbonize cleanup (bake params, remove markers and data-p-* attributes), then run live-complete again. Use --force only if a finding is a false positive.',
|
|
55
|
+
}, null, 2));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
30
61
|
const serverInfo = readServerInfo();
|
|
31
62
|
const serverResult = serverInfo ? await completeThroughServer(serverInfo, args) : null;
|
|
32
63
|
if (serverResult?.ok) {
|
|
@@ -71,5 +102,6 @@ async function completeThroughServer(info, args) {
|
|
|
71
102
|
|
|
72
103
|
const _running = process.argv[1];
|
|
73
104
|
if (_running?.endsWith('live-complete.mjs') || _running?.endsWith('live-complete.mjs/')) {
|
|
105
|
+
enterLiveRoot();
|
|
74
106
|
completeCli();
|
|
75
107
|
}
|
|
@@ -7,10 +7,20 @@
|
|
|
7
7
|
* every subsequent run, this script handles insert/remove deterministically
|
|
8
8
|
* with zero LLM involvement.
|
|
9
9
|
*
|
|
10
|
+
* Framework knowledge lives in `live/frameworks/` — detection order, adapters,
|
|
11
|
+
* the generic tag strategy, and the per-extension authoring traits live-wrap
|
|
12
|
+
* reads. This file is the CLI around it: resolve config, resolve the
|
|
13
|
+
* framework, heal orphaned artifacts, apply or remove, record the journal.
|
|
14
|
+
*
|
|
10
15
|
* Usage:
|
|
11
|
-
* node live-inject.mjs --port PORT
|
|
12
|
-
* node live-inject.mjs --remove
|
|
13
|
-
* node live-inject.mjs --check
|
|
16
|
+
* node live-inject.mjs --port PORT [--token TOKEN] # Insert the live script tag
|
|
17
|
+
* node live-inject.mjs --remove # Remove the live script tag
|
|
18
|
+
* node live-inject.mjs --check # Check whether live config exists
|
|
19
|
+
*
|
|
20
|
+
* When --token is supplied, it is appended to the /live.js src as `?token=...`
|
|
21
|
+
* so the server's token-gated /live.js handler will serve the bundle. Omitting
|
|
22
|
+
* the token yields a bare `/live.js` src (legacy behavior; the server returns
|
|
23
|
+
* 401 for it under the current gate).
|
|
14
24
|
*/
|
|
15
25
|
|
|
16
26
|
import fs from 'node:fs';
|
|
@@ -18,15 +28,36 @@ import path from 'node:path';
|
|
|
18
28
|
import { fileURLToPath } from 'node:url';
|
|
19
29
|
import { resolveLiveConfigPath } from './lib/impeccable-paths.mjs';
|
|
20
30
|
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
describeInjectArtifacts,
|
|
32
|
+
frameworkIgnorePatterns,
|
|
33
|
+
resolveFramework,
|
|
34
|
+
resolveSourceTraits,
|
|
35
|
+
} from './live/frameworks/index.mjs';
|
|
36
|
+
import {
|
|
37
|
+
clearInjectJournal,
|
|
38
|
+
healInjectJournal,
|
|
39
|
+
recordInjection,
|
|
40
|
+
} from './live/frameworks/journal.mjs';
|
|
41
|
+
import {
|
|
42
|
+
buildTagBlock,
|
|
43
|
+
insertTag,
|
|
44
|
+
patchCspMeta,
|
|
45
|
+
removeTag,
|
|
46
|
+
revertCspMeta,
|
|
47
|
+
} from './live/frameworks/tag-strategy.mjs';
|
|
48
|
+
import { buildLiveScriptSrc } from './live/frameworks/script-src.mjs';
|
|
49
|
+
import { enterLiveRoot } from './live/roots.mjs';
|
|
25
50
|
|
|
26
51
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
52
|
+
// Resolved lazily so the enterLiveRoot() chdir in the CLI guard below takes
|
|
53
|
+
// effect first; module scope runs before the guard.
|
|
54
|
+
let CONFIG_PATH_CACHED = null;
|
|
55
|
+
function CONFIG_PATH_GET() {
|
|
56
|
+
if (!CONFIG_PATH_CACHED) {
|
|
57
|
+
CONFIG_PATH_CACHED = resolveLiveConfigPath({ cwd: process.cwd(), scriptsDir: __dirname });
|
|
58
|
+
}
|
|
59
|
+
return CONFIG_PATH_CACHED;
|
|
60
|
+
}
|
|
30
61
|
const IGNORE_MARKER_OPEN = '# impeccable-live-ignore-start';
|
|
31
62
|
const IGNORE_MARKER_CLOSE = '# impeccable-live-ignore-end';
|
|
32
63
|
|
|
@@ -35,9 +66,15 @@ export const LIVE_IGNORE_PATTERNS = Object.freeze([
|
|
|
35
66
|
'.impeccable/hook.pending.json',
|
|
36
67
|
'.impeccable/config.local.json',
|
|
37
68
|
'.impeccable/live/server.json',
|
|
69
|
+
'.impeccable/live/roots.json',
|
|
70
|
+
'.impeccable/live/app-root.json',
|
|
71
|
+
'.impeccable/live/inject-journal.json',
|
|
38
72
|
'.impeccable/live/sessions/',
|
|
39
73
|
'.impeccable/live/previews/',
|
|
40
74
|
'.impeccable/live/annotations/',
|
|
75
|
+
'.impeccable/live/artifacts/',
|
|
76
|
+
'.impeccable/live/accept-receipts/',
|
|
77
|
+
'.impeccable/live/locks/',
|
|
41
78
|
'.impeccable/live/cache/',
|
|
42
79
|
'.impeccable/live/manual-edit-apply-transaction.json',
|
|
43
80
|
'.impeccable/live/manual-edit-events.jsonl',
|
|
@@ -46,10 +83,15 @@ export const LIVE_IGNORE_PATTERNS = Object.freeze([
|
|
|
46
83
|
'.impeccable/live/deferred-svelte-component-accepts.json',
|
|
47
84
|
'.impeccable-live.json',
|
|
48
85
|
'.impeccable-live/',
|
|
86
|
+
'app/.impeccable-live/',
|
|
87
|
+
'src/.impeccable-live/',
|
|
49
88
|
'node_modules/.impeccable-live/',
|
|
50
89
|
'src/lib/impeccable/ImpeccableLiveRoot.svelte',
|
|
51
90
|
'src/lib/impeccable/__runtime.js',
|
|
52
91
|
'src/lib/impeccable/[0-9a-f]*/',
|
|
92
|
+
'plugins/impeccable-live.client.ts',
|
|
93
|
+
'app/plugins/impeccable-live.client.ts',
|
|
94
|
+
'src/plugins/impeccable-live.client.ts',
|
|
53
95
|
]);
|
|
54
96
|
|
|
55
97
|
/**
|
|
@@ -82,46 +124,61 @@ Output (JSON):
|
|
|
82
124
|
}
|
|
83
125
|
|
|
84
126
|
if (args.includes('--check')) {
|
|
85
|
-
|
|
86
|
-
|
|
127
|
+
// Deliberately read-only: --check runs from status paths and must never
|
|
128
|
+
// mutate the tree. Journal reconciliation happens on the inject run.
|
|
129
|
+
if (!fs.existsSync(CONFIG_PATH_GET())) {
|
|
130
|
+
console.log(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH_GET() }));
|
|
87
131
|
process.exit(0);
|
|
88
132
|
}
|
|
89
133
|
let cfg;
|
|
90
134
|
try {
|
|
91
|
-
cfg = JSON.parse(fs.readFileSync(
|
|
135
|
+
cfg = JSON.parse(fs.readFileSync(CONFIG_PATH_GET(), 'utf-8'));
|
|
92
136
|
} catch (err) {
|
|
93
|
-
console.log(JSON.stringify({ ok: false, error: 'config_invalid', message: err.message, path:
|
|
137
|
+
console.log(JSON.stringify({ ok: false, error: 'config_invalid', message: err.message, path: CONFIG_PATH_GET() }));
|
|
94
138
|
return;
|
|
95
139
|
}
|
|
96
140
|
try {
|
|
97
141
|
validateConfig(cfg);
|
|
98
142
|
} catch (err) {
|
|
99
|
-
console.log(JSON.stringify({ ok: false, error: 'config_invalid', message: err.message, path:
|
|
143
|
+
console.log(JSON.stringify({ ok: false, error: 'config_invalid', message: err.message, path: CONFIG_PATH_GET() }));
|
|
100
144
|
return;
|
|
101
145
|
}
|
|
102
|
-
console.log(JSON.stringify({ ok: true, config: cfg, path:
|
|
146
|
+
console.log(JSON.stringify({ ok: true, config: cfg, path: CONFIG_PATH_GET() }));
|
|
103
147
|
return;
|
|
104
148
|
}
|
|
105
149
|
|
|
106
150
|
// Load config
|
|
107
|
-
if (!fs.existsSync(
|
|
108
|
-
console.error(JSON.stringify({ ok: false, error: 'config_missing', path:
|
|
151
|
+
if (!fs.existsSync(CONFIG_PATH_GET())) {
|
|
152
|
+
console.error(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH_GET() }));
|
|
109
153
|
process.exit(1);
|
|
110
154
|
}
|
|
111
|
-
const config = JSON.parse(fs.readFileSync(
|
|
155
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH_GET(), 'utf-8'));
|
|
112
156
|
validateConfig(config);
|
|
113
157
|
|
|
114
|
-
const
|
|
115
|
-
const
|
|
158
|
+
const cwd = process.cwd();
|
|
159
|
+
const resolvedFiles = resolveFiles(cwd, config);
|
|
160
|
+
const resolved = resolveFramework(cwd, config);
|
|
161
|
+
const isAdapter = resolved?.framework.inject.kind === 'adapter';
|
|
116
162
|
|
|
117
163
|
if (args.includes('--remove')) {
|
|
118
|
-
if (
|
|
119
|
-
const adapterResult =
|
|
120
|
-
|
|
164
|
+
if (isAdapter) {
|
|
165
|
+
const adapterResult = resolved.framework.inject.remove({ cwd, config, project: resolved.project });
|
|
166
|
+
const ok = !(adapterResult && adapterResult.error);
|
|
167
|
+
// Anything the adapter could not reach (its detection may have shifted
|
|
168
|
+
// since the session started) is still on the journal.
|
|
169
|
+
const { healed } = healInjectJournal(cwd);
|
|
170
|
+
clearInjectJournal(cwd);
|
|
171
|
+
console.log(JSON.stringify({
|
|
172
|
+
ok,
|
|
173
|
+
adapter: resolved.framework.name,
|
|
174
|
+
results: [adapterResult],
|
|
175
|
+
healed: healed.length ? healed : undefined,
|
|
176
|
+
}));
|
|
177
|
+
if (!ok) process.exitCode = 1;
|
|
121
178
|
return;
|
|
122
179
|
}
|
|
123
180
|
const results = resolvedFiles.map((relFile) => {
|
|
124
|
-
const absFile = path.resolve(
|
|
181
|
+
const absFile = path.resolve(cwd, relFile);
|
|
125
182
|
if (!fs.existsSync(absFile)) return { file: relFile, error: 'file_not_found' };
|
|
126
183
|
const content = fs.readFileSync(absFile, 'utf-8');
|
|
127
184
|
const detagged = removeTag(content, config.commentSyntax);
|
|
@@ -134,7 +191,9 @@ Output (JSON):
|
|
|
134
191
|
cspReverted: updated !== detagged,
|
|
135
192
|
};
|
|
136
193
|
});
|
|
137
|
-
|
|
194
|
+
const { healed } = healInjectJournal(cwd);
|
|
195
|
+
clearInjectJournal(cwd);
|
|
196
|
+
console.log(JSON.stringify({ ok: true, results, healed: healed.length ? healed : undefined }));
|
|
138
197
|
return;
|
|
139
198
|
}
|
|
140
199
|
|
|
@@ -145,20 +204,69 @@ Output (JSON):
|
|
|
145
204
|
console.error(JSON.stringify({ ok: false, error: 'missing_port' }));
|
|
146
205
|
process.exit(1);
|
|
147
206
|
}
|
|
148
|
-
|
|
207
|
+
// Optional server token: appended to the /live.js src so the token-gated
|
|
208
|
+
// /live.js handler authorizes the browser fetch. `live.mjs` always passes
|
|
209
|
+
// it; a manual `--port`-only invocation reads the running helper's token
|
|
210
|
+
// from server.json instead of writing an unauthenticated URL that 401s.
|
|
211
|
+
const tokenIdx = args.indexOf('--token');
|
|
212
|
+
let token = tokenIdx !== -1 ? args[tokenIdx + 1] : undefined;
|
|
213
|
+
if (!token) {
|
|
214
|
+
try {
|
|
215
|
+
const info = JSON.parse(fs.readFileSync(path.join(cwd, '.impeccable', 'live', 'server.json'), 'utf-8'));
|
|
216
|
+
// A record for a DIFFERENT port is a stale or foreign helper; its token
|
|
217
|
+
// would 401 just the same, so only adopt a matching one.
|
|
218
|
+
if (info?.token && Number(info.port) === port) token = info.token;
|
|
219
|
+
} catch { /* no running helper recorded; keep legacy tokenless behavior */ }
|
|
220
|
+
}
|
|
149
221
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
222
|
+
// Reconcile before writing anything. Artifacts this run is about to own are
|
|
223
|
+
// kept (so a repeat inject stays byte-idempotent); artifacts left behind by
|
|
224
|
+
// a session that never got to stop are healed.
|
|
225
|
+
const plannedArtifacts = describeInjectArtifacts(resolved, { cwd, files: resolvedFiles });
|
|
226
|
+
const { healed } = healInjectJournal(cwd, { keep: plannedArtifacts.map((a) => a.path) });
|
|
227
|
+
|
|
228
|
+
const gitIgnore = ensureLiveGitIgnores(cwd, frameworkIgnorePatterns(resolved));
|
|
229
|
+
// In a nested-app repo the roots pointer lives at the REPO root, outside the
|
|
230
|
+
// reach of the appRoot-relative ignore block above; give that directory its
|
|
231
|
+
// own local excludes so the pointer (absolute host paths) never gets staged.
|
|
232
|
+
try {
|
|
233
|
+
const rootsManifest = JSON.parse(fs.readFileSync(path.join(cwd, '.impeccable', 'live', 'roots.json'), 'utf-8'));
|
|
234
|
+
if (rootsManifest?.repoRoot && path.resolve(rootsManifest.repoRoot) !== path.resolve(cwd)) {
|
|
235
|
+
ensureLiveGitIgnores(rootsManifest.repoRoot);
|
|
236
|
+
}
|
|
237
|
+
} catch { /* no manifest: single-root project */ }
|
|
238
|
+
|
|
239
|
+
if (isAdapter) {
|
|
240
|
+
const adapterResult = resolved.framework.inject.apply({
|
|
241
|
+
cwd,
|
|
242
|
+
port,
|
|
243
|
+
token,
|
|
244
|
+
config,
|
|
245
|
+
project: resolved.project,
|
|
246
|
+
});
|
|
247
|
+
const ok = !(adapterResult && adapterResult.error);
|
|
248
|
+
if (ok) recordInjection(cwd, { framework: resolved.framework.name, port, artifacts: plannedArtifacts });
|
|
249
|
+
console.log(JSON.stringify({
|
|
250
|
+
ok,
|
|
251
|
+
port,
|
|
252
|
+
adapter: resolved.framework.name,
|
|
253
|
+
gitIgnore,
|
|
254
|
+
results: [adapterResult],
|
|
255
|
+
healed: healed.length ? healed : undefined,
|
|
256
|
+
}));
|
|
257
|
+
if (!ok) process.exitCode = 1;
|
|
153
258
|
return;
|
|
154
259
|
}
|
|
155
260
|
|
|
156
261
|
const results = resolvedFiles.map((relFile) => {
|
|
157
|
-
const absFile = path.resolve(
|
|
262
|
+
const absFile = path.resolve(cwd, relFile);
|
|
158
263
|
if (!fs.existsSync(absFile)) return { file: relFile, error: 'file_not_found' };
|
|
159
264
|
const content = fs.readFileSync(absFile, 'utf-8');
|
|
160
265
|
const withoutOld = revertCspMeta(removeTag(content, config.commentSyntax));
|
|
161
|
-
|
|
266
|
+
// Per-file, not per-project: a Vite app can hold an .astro partial, and a
|
|
267
|
+
// framework project's entry template is often plain HTML.
|
|
268
|
+
const scriptAttrs = resolveSourceTraits(relFile).injectScriptAttrs;
|
|
269
|
+
const withTag = insertTag(withoutOld, config, port, token, scriptAttrs);
|
|
162
270
|
if (withTag === withoutOld) {
|
|
163
271
|
return { file: relFile, error: 'insertion_point_not_found', anchor: config.insertBefore || config.insertAfter };
|
|
164
272
|
}
|
|
@@ -171,16 +279,28 @@ Output (JSON):
|
|
|
171
279
|
};
|
|
172
280
|
});
|
|
173
281
|
const anyInserted = results.some((r) => r.inserted);
|
|
174
|
-
|
|
282
|
+
const writtenFiles = new Set(results.filter((r) => r.inserted).map((r) => r.file));
|
|
283
|
+
recordInjection(cwd, {
|
|
284
|
+
framework: resolved?.framework.name,
|
|
285
|
+
port,
|
|
286
|
+
artifacts: plannedArtifacts.filter((a) => writtenFiles.has(a.path)),
|
|
287
|
+
});
|
|
288
|
+
console.log(JSON.stringify({
|
|
289
|
+
ok: anyInserted,
|
|
290
|
+
port,
|
|
291
|
+
gitIgnore,
|
|
292
|
+
results,
|
|
293
|
+
healed: healed.length ? healed : undefined,
|
|
294
|
+
}));
|
|
175
295
|
if (!anyInserted) process.exit(1);
|
|
176
296
|
}
|
|
177
297
|
|
|
178
|
-
export function ensureLiveGitIgnores(cwd = process.cwd()) {
|
|
298
|
+
export function ensureLiveGitIgnores(cwd = process.cwd(), extraPatterns = []) {
|
|
179
299
|
const target = resolveIgnoreTarget(cwd);
|
|
180
300
|
const existing = fs.existsSync(target.path) ? fs.readFileSync(target.path, 'utf-8') : '';
|
|
181
301
|
const block = [
|
|
182
302
|
IGNORE_MARKER_OPEN,
|
|
183
|
-
...LIVE_IGNORE_PATTERNS,
|
|
303
|
+
...new Set([...LIVE_IGNORE_PATTERNS, ...extraPatterns]),
|
|
184
304
|
IGNORE_MARKER_CLOSE,
|
|
185
305
|
].join('\n');
|
|
186
306
|
const markerRe = new RegExp(`${escapeRegExp(IGNORE_MARKER_OPEN)}[\\s\\S]*?${escapeRegExp(IGNORE_MARKER_CLOSE)}`);
|
|
@@ -202,7 +322,7 @@ export function ensureLiveGitIgnores(cwd = process.cwd()) {
|
|
|
202
322
|
file: path.relative(cwd, target.path).split(path.sep).join('/'),
|
|
203
323
|
mode: target.mode,
|
|
204
324
|
changed: updated !== existing,
|
|
205
|
-
patterns: [...LIVE_IGNORE_PATTERNS],
|
|
325
|
+
patterns: [...new Set([...LIVE_IGNORE_PATTERNS, ...extraPatterns])],
|
|
206
326
|
};
|
|
207
327
|
}
|
|
208
328
|
|
|
@@ -353,231 +473,31 @@ function validateConfig(cfg) {
|
|
|
353
473
|
}
|
|
354
474
|
}
|
|
355
475
|
|
|
356
|
-
function commentOpen(syntax) { return syntax === 'jsx' ? '{/*' : '<!--'; }
|
|
357
|
-
function commentClose(syntax) { return syntax === 'jsx' ? '*/}' : '-->'; }
|
|
358
|
-
|
|
359
|
-
function buildTagBlock(syntax, port, filePath) {
|
|
360
|
-
const open = commentOpen(syntax);
|
|
361
|
-
const close = commentClose(syntax);
|
|
362
|
-
// Astro processes <script> tags by default and rewrites src to its own
|
|
363
|
-
// bundled URL. is:inline opts out so the literal external src survives.
|
|
364
|
-
const isAstro = typeof filePath === 'string' && filePath.endsWith('.astro');
|
|
365
|
-
const scriptAttrs = isAstro ? 'is:inline ' : '';
|
|
366
|
-
return (
|
|
367
|
-
open + ' ' + MARKER_OPEN_TEXT + ' ' + close + '\n' +
|
|
368
|
-
'<script ' + scriptAttrs + 'src="http://localhost:' + port + '/live.js"></script>\n' +
|
|
369
|
-
open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n'
|
|
370
|
-
);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function detectLineEnding(content) {
|
|
374
|
-
if (content.includes('\r\n')) return '\r\n';
|
|
375
|
-
if (content.includes('\r')) return '\r';
|
|
376
|
-
return '\n';
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function normalizeLineEndings(content, lineEnding) {
|
|
380
|
-
return lineEnding === '\n' ? content : content.replace(/\n/g, lineEnding);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function readLineEndingAt(content, index) {
|
|
384
|
-
if (content[index] === '\r' && content[index + 1] === '\n') return '\r\n';
|
|
385
|
-
if (content[index] === '\n') return '\n';
|
|
386
|
-
if (content[index] === '\r') return '\r';
|
|
387
|
-
return '';
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function insertTag(content, config, port, filePath) {
|
|
391
|
-
const lineEnding = detectLineEnding(content);
|
|
392
|
-
const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath), lineEnding);
|
|
393
|
-
// insertBefore: match the LAST occurrence. Anchors like `</body>` naturally
|
|
394
|
-
// belong at the end, and the same literal can appear earlier in code blocks
|
|
395
|
-
// within rendered documentation pages.
|
|
396
|
-
if (config.insertBefore) {
|
|
397
|
-
const idx = content.lastIndexOf(config.insertBefore);
|
|
398
|
-
if (idx === -1) return content;
|
|
399
|
-
return content.slice(0, idx) + block + content.slice(idx);
|
|
400
|
-
}
|
|
401
|
-
// insertAfter: match the FIRST occurrence — typical anchors like `<head>` or
|
|
402
|
-
// `<body>` open near the top of the document.
|
|
403
|
-
const idx = content.indexOf(config.insertAfter);
|
|
404
|
-
if (idx === -1) return content;
|
|
405
|
-
const after = idx + config.insertAfter.length;
|
|
406
|
-
// Preserve an existing trailing newline if the anchor already has one.
|
|
407
|
-
// Slice the remainder from the original anchor offset, not prefix.length:
|
|
408
|
-
// in the no-newline case prefix is one char longer than the anchor (the
|
|
409
|
-
// appended '\n'), so slicing by prefix.length would drop the first real
|
|
410
|
-
// character after the anchor (#227).
|
|
411
|
-
const existingNewline = readLineEndingAt(content, after);
|
|
412
|
-
const prefix = content.slice(0, after) + (existingNewline || lineEnding);
|
|
413
|
-
const rest = content.slice(after + existingNewline.length);
|
|
414
|
-
return prefix + block + rest;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* Remove the live script block. Matches either HTML or JSX comment markers
|
|
419
|
-
* regardless of config (so stale tags from a wrong config can still be cleaned).
|
|
420
|
-
*
|
|
421
|
-
* Indent-preserving: captures any whitespace immediately preceding the opener
|
|
422
|
-
* marker and re-emits it in place of the removed block. `insertTag` inserted
|
|
423
|
-
* the block *after* the original line's indent and *before* the anchor (e.g.
|
|
424
|
-
* `</body>`), which moved the indent onto the opener line and left the anchor
|
|
425
|
-
* unindented. Replacing the whole block (plus its trailing newline) with just
|
|
426
|
-
* the captured indent hands the indent back to the anchor that follows.
|
|
427
|
-
*/
|
|
428
|
-
function removeTag(content, _syntax) {
|
|
429
|
-
const patterns = [
|
|
430
|
-
/([ \t]*)<!--\s*impeccable-live-start\s*-->[\s\S]*?<!--\s*impeccable-live-end\s*-->([ \t]*(?:\r\n|\n|\r|$)?)/,
|
|
431
|
-
/([ \t]*)\{\/\*\s*impeccable-live-start\s*\*\/\}[\s\S]*?\{\/\*\s*impeccable-live-end\s*\*\/\}([ \t]*(?:\r\n|\n|\r|$)?)/,
|
|
432
|
-
];
|
|
433
|
-
for (const pat of patterns) {
|
|
434
|
-
let changed = false;
|
|
435
|
-
let next = content;
|
|
436
|
-
do {
|
|
437
|
-
content = next;
|
|
438
|
-
next = content.replace(pat, (_match, leadingIndent, trailing = '') => {
|
|
439
|
-
if (/[\r\n]/.test(trailing)) return leadingIndent;
|
|
440
|
-
return leadingIndent || trailing || '';
|
|
441
|
-
});
|
|
442
|
-
if (next !== content) changed = true;
|
|
443
|
-
} while (next !== content);
|
|
444
|
-
if (changed) return next;
|
|
445
|
-
}
|
|
446
|
-
return content;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// ---------------------------------------------------------------------------
|
|
450
|
-
// Content-Security-Policy meta-tag patcher
|
|
451
|
-
//
|
|
452
|
-
// When the user's HTML carries `<meta http-equiv="Content-Security-Policy">`,
|
|
453
|
-
// the cross-origin load of /live.js (and the SSE/POST connection back to
|
|
454
|
-
// localhost:PORT) is blocked unless the CSP explicitly allows that origin.
|
|
455
|
-
//
|
|
456
|
-
// On insert: append `http://localhost:PORT` to `script-src` and `connect-src`,
|
|
457
|
-
// and stash the original `content` value in a `data-impeccable-csp-original`
|
|
458
|
-
// attribute (base64) so revert is exact.
|
|
459
|
-
//
|
|
460
|
-
// On remove: detect the marker attribute, decode it, restore the original
|
|
461
|
-
// content value verbatim, drop the marker.
|
|
462
|
-
//
|
|
463
|
-
// Header-based CSP (Next.js headers, Nuxt routeRules, SvelteKit kit.csp,
|
|
464
|
-
// shared helpers) is NOT patched here — those need framework-specific config
|
|
465
|
-
// edits and are handled via the existing detect-csp.mjs reference output.
|
|
466
|
-
// Only the in-source meta-tag form gets the auto-patch.
|
|
467
|
-
// ---------------------------------------------------------------------------
|
|
468
|
-
|
|
469
|
-
const CSP_MARKER_ATTR = 'data-impeccable-csp-original';
|
|
470
|
-
|
|
471
|
-
function findCspMetaTags(content) {
|
|
472
|
-
const out = [];
|
|
473
|
-
const tagRe = /<meta\s+([^>]*?)\/?>/gis;
|
|
474
|
-
let m;
|
|
475
|
-
while ((m = tagRe.exec(content)) !== null) {
|
|
476
|
-
const attrs = m[1];
|
|
477
|
-
if (!/(http-equiv|httpEquiv)\s*=\s*(['"])Content-Security-Policy\2/i.test(attrs)) continue;
|
|
478
|
-
out.push({ start: m.index, end: m.index + m[0].length, full: m[0], attrs });
|
|
479
|
-
}
|
|
480
|
-
return out;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
function getAttr(attrs, name) {
|
|
484
|
-
const re = new RegExp(`\\b${name}\\s*=\\s*(['"])([\\s\\S]*?)\\1`, 'i');
|
|
485
|
-
const m = attrs.match(re);
|
|
486
|
-
return m ? { quote: m[1], value: m[2], full: m[0] } : null;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
function appendOriginToDirective(csp, directive, origin) {
|
|
490
|
-
const re = new RegExp(`(^|;)(\\s*)(${directive})\\s+([^;]*)`, 'i');
|
|
491
|
-
const m = csp.match(re);
|
|
492
|
-
if (m) {
|
|
493
|
-
const tokens = m[4].trim().split(/\s+/);
|
|
494
|
-
if (tokens.includes(origin)) return csp;
|
|
495
|
-
return csp.replace(re, `${m[1]}${m[2]}${m[3]} ${[...tokens, origin].join(' ')}`);
|
|
496
|
-
}
|
|
497
|
-
// Directive missing — add it. Use 'self' + origin so we don't inadvertently
|
|
498
|
-
// narrow the policy compared to the default-src fallback (most users with
|
|
499
|
-
// an explicit CSP have 'self' there).
|
|
500
|
-
return csp.trim().replace(/;?\s*$/, '') + `; ${directive} 'self' ${origin}`;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
export function patchCspMeta(content, port) {
|
|
504
|
-
const tags = findCspMetaTags(content);
|
|
505
|
-
if (tags.length === 0) return content;
|
|
506
|
-
const origin = `http://localhost:${port}`;
|
|
507
|
-
|
|
508
|
-
// Walk last-to-first so prior splices don't invalidate later indices.
|
|
509
|
-
let result = content;
|
|
510
|
-
for (let i = tags.length - 1; i >= 0; i--) {
|
|
511
|
-
const tag = tags[i];
|
|
512
|
-
const attrs = tag.attrs;
|
|
513
|
-
if (getAttr(attrs, CSP_MARKER_ATTR)) continue; // already patched
|
|
514
|
-
const contentAttr = getAttr(attrs, 'content');
|
|
515
|
-
if (!contentAttr) continue;
|
|
516
|
-
|
|
517
|
-
const original = contentAttr.value;
|
|
518
|
-
let patched = original;
|
|
519
|
-
patched = appendOriginToDirective(patched, 'script-src', origin);
|
|
520
|
-
patched = appendOriginToDirective(patched, 'connect-src', origin);
|
|
521
|
-
// The shader overlay during 'generating' creates a screenshot via
|
|
522
|
-
// URL.createObjectURL, producing a `blob:` URL — img-src 'self' rejects
|
|
523
|
-
// those. Add `blob:` so the overlay doesn't throw a CSP violation.
|
|
524
|
-
patched = appendOriginToDirective(patched, 'img-src', 'blob:');
|
|
525
|
-
if (patched === original) continue;
|
|
526
|
-
|
|
527
|
-
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
|
|
528
|
-
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
|
|
529
|
-
// The tagRe captures any whitespace between the last attribute and the
|
|
530
|
-
// closing `/>` as part of `attrs`. Naively appending ` ${marker}` after
|
|
531
|
-
// a replace would land it BEFORE that trailing space, leaving a double
|
|
532
|
-
// space inside attrs and clobbering the space before `/>`. Split off
|
|
533
|
-
// the trailing whitespace, splice the marker into the attribute body,
|
|
534
|
-
// and re-append the original trailing whitespace so a self-closing
|
|
535
|
-
// `<meta … />` round-trips byte-for-byte.
|
|
536
|
-
const trailingWs = (attrs.match(/[ \t]*$/) || [''])[0];
|
|
537
|
-
const attrsBody = attrs.slice(0, attrs.length - trailingWs.length);
|
|
538
|
-
const newAttrs = attrsBody.replace(contentAttr.full, newContentAttr) + ' ' + marker + trailingWs;
|
|
539
|
-
const newTag = tag.full.replace(attrs, newAttrs);
|
|
540
|
-
|
|
541
|
-
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
|
542
|
-
}
|
|
543
|
-
return result;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
export function revertCspMeta(content) {
|
|
547
|
-
const tags = findCspMetaTags(content);
|
|
548
|
-
if (tags.length === 0) return content;
|
|
549
|
-
|
|
550
|
-
let result = content;
|
|
551
|
-
for (let i = tags.length - 1; i >= 0; i--) {
|
|
552
|
-
const tag = tags[i];
|
|
553
|
-
const origAttr = getAttr(tag.attrs, CSP_MARKER_ATTR);
|
|
554
|
-
if (!origAttr) continue;
|
|
555
|
-
const contentAttr = getAttr(tag.attrs, 'content');
|
|
556
|
-
if (!contentAttr) continue;
|
|
557
|
-
|
|
558
|
-
let originalValue;
|
|
559
|
-
try { originalValue = Buffer.from(origAttr.value, 'base64').toString('utf-8'); }
|
|
560
|
-
catch { continue; }
|
|
561
|
-
|
|
562
|
-
const newContentAttr = `content=${contentAttr.quote}${originalValue}${contentAttr.quote}`;
|
|
563
|
-
let newAttrs = tag.attrs.replace(contentAttr.full, newContentAttr);
|
|
564
|
-
// Drop the marker attribute and any single space immediately preceding it.
|
|
565
|
-
newAttrs = newAttrs.replace(new RegExp(`\\s*${origAttr.full}`), '');
|
|
566
|
-
const newTag = tag.full.replace(tag.attrs, newAttrs);
|
|
567
|
-
|
|
568
|
-
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
|
569
|
-
}
|
|
570
|
-
return result;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
476
|
// ---------------------------------------------------------------------------
|
|
574
477
|
// Auto-execute
|
|
575
478
|
// ---------------------------------------------------------------------------
|
|
576
479
|
|
|
577
480
|
const _running = process.argv[1];
|
|
578
481
|
if (_running?.endsWith('live-inject.mjs') || _running?.endsWith('live-inject.mjs/')) {
|
|
482
|
+
enterLiveRoot();
|
|
579
483
|
injectCli();
|
|
580
484
|
}
|
|
581
485
|
|
|
582
|
-
|
|
583
|
-
//
|
|
486
|
+
// Re-exported so long-standing importers (live.mjs, the adapter modules, the
|
|
487
|
+
// test suites) keep their entry points while the implementations live in
|
|
488
|
+
// live/frameworks/.
|
|
489
|
+
export {
|
|
490
|
+
buildLiveScriptSrc,
|
|
491
|
+
buildTagBlock,
|
|
492
|
+
insertTag,
|
|
493
|
+
patchCspMeta,
|
|
494
|
+
removeTag,
|
|
495
|
+
revertCspMeta,
|
|
496
|
+
validateConfig,
|
|
497
|
+
};
|
|
498
|
+
export {
|
|
499
|
+
applyNuxtLiveAdapter,
|
|
500
|
+
buildNuxtPlugin,
|
|
501
|
+
detectNuxtProject,
|
|
502
|
+
removeNuxtLiveAdapter,
|
|
503
|
+
} from './live/frameworks/nuxt.mjs';
|