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
|
@@ -1,9 +1,18 @@
|
|
|
1
|
+
// A preview whose variants live in component modules rather than in the user's
|
|
2
|
+
// source. These leave no markers in the real file, so a failed accept gives the
|
|
3
|
+
// agent nothing to hand-edit and must be reported as a failure rather than
|
|
4
|
+
// reference/live.md's manual-cleanup handoff. Kept as a set: any future
|
|
5
|
+
// component-module preview mode belongs here the day it lands.
|
|
6
|
+
const PREVIEW_MODES_WITHOUT_SOURCE_MARKERS = new Set([
|
|
7
|
+
'svelte-component',
|
|
8
|
+
]);
|
|
9
|
+
|
|
1
10
|
export function completionTypeForAcceptResult(eventType, acceptResult) {
|
|
2
11
|
if (eventType === 'discard') return acceptResult?.handled === true ? 'discarded' : 'error';
|
|
3
12
|
if (acceptResult?.handled === true && acceptResult?.carbonize === true) return 'agent_done';
|
|
4
13
|
if (acceptResult?.handled === true) return 'complete';
|
|
5
14
|
if (acceptResult?.mode === 'error') return 'error';
|
|
6
|
-
if (eventType === 'accept' && acceptResult?.previewMode
|
|
15
|
+
if (eventType === 'accept' && PREVIEW_MODES_WITHOUT_SOURCE_MARKERS.has(acceptResult?.previewMode)) return 'error';
|
|
7
16
|
return 'agent_done';
|
|
8
17
|
}
|
|
9
18
|
|
|
@@ -5,17 +5,26 @@
|
|
|
5
5
|
|
|
6
6
|
import { canCreateInsert } from './insert-ui.mjs';
|
|
7
7
|
|
|
8
|
-
// The accepted
|
|
9
|
-
// validator, the
|
|
10
|
-
// just re-exported) so
|
|
11
|
-
import { VISUAL_ACTIONS } from './vocabulary.mjs';
|
|
12
|
-
export { VISUAL_ACTIONS };
|
|
8
|
+
// The accepted protocol values come from the canonical vocabulary so the
|
|
9
|
+
// validator, the store, the server, and the picker UI never drift. Imported
|
|
10
|
+
// (not just re-exported) so they are also in scope for the validators below.
|
|
11
|
+
import { AGENT_PHASES, CLIENT_EVENT_TYPES, VISUAL_ACTIONS } from './vocabulary.mjs';
|
|
12
|
+
export { AGENT_PHASES, CLIENT_EVENT_TYPES, VISUAL_ACTIONS };
|
|
13
|
+
|
|
14
|
+
const AGENT_PHASE_SET = new Set(AGENT_PHASES);
|
|
13
15
|
|
|
14
16
|
const ID_PATTERN = /^[0-9a-f]{8}$/;
|
|
15
17
|
const VARIANT_ID_PATTERN = /^[0-9]{1,3}$/;
|
|
16
18
|
const INSERT_POSITIONS = new Set(['before', 'after']);
|
|
17
19
|
const FORBIDDEN_MANUAL_EDIT_TEXT_CHARS = ['<', '{', '}', '`'];
|
|
18
20
|
|
|
21
|
+
// Mount acknowledgements carry a module URL and a raw exception message from
|
|
22
|
+
// the page. Both are attacker-adjacent (any script on the page can POST them
|
|
23
|
+
// with the token it can already read), so they are length-capped before they
|
|
24
|
+
// reach the journal.
|
|
25
|
+
export const MOUNT_URL_MAX_LENGTH = 2000;
|
|
26
|
+
export const MOUNT_ERROR_MAX_LENGTH = 1000;
|
|
27
|
+
|
|
19
28
|
function isValidId(v) { return typeof v === 'string' && ID_PATTERN.test(v); }
|
|
20
29
|
function isValidVariantId(v) { return typeof v === 'string' && VARIANT_ID_PATTERN.test(v); }
|
|
21
30
|
|
|
@@ -92,6 +101,36 @@ function validateManualEditEvent(msg, label) {
|
|
|
92
101
|
return null;
|
|
93
102
|
}
|
|
94
103
|
|
|
104
|
+
function isValidMountVariant(value) {
|
|
105
|
+
return Number.isInteger(value) && value >= 1 && value <= 999;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Mount acknowledgements are the browser's answer to "did the thing you
|
|
110
|
+
* published actually render". They are validated strictly because the render
|
|
111
|
+
* truth in the session snapshot is built from them: a malformed ack that slid
|
|
112
|
+
* through would report a variant as mounted that never was.
|
|
113
|
+
*/
|
|
114
|
+
function validateMountAck(msg) {
|
|
115
|
+
if (!isValidId(msg.id)) return 'variant_mounted: missing or malformed id';
|
|
116
|
+
if (!isValidMountVariant(msg.variant)) return 'variant_mounted: variant must be an integer 1-999';
|
|
117
|
+
if (msg.url !== undefined) {
|
|
118
|
+
if (typeof msg.url !== 'string') return 'variant_mounted: url must be string';
|
|
119
|
+
if (msg.url.length > MOUNT_URL_MAX_LENGTH) return 'variant_mounted: url too long';
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function validateMountFailure(msg) {
|
|
125
|
+
if (!isValidId(msg.id)) return 'variant_mount_failed: missing or malformed id';
|
|
126
|
+
if (!isValidMountVariant(msg.variant)) return 'variant_mount_failed: variant must be an integer 1-999';
|
|
127
|
+
if (typeof msg.url !== 'string' || !msg.url.trim()) return 'variant_mount_failed: url required';
|
|
128
|
+
if (msg.url.length > MOUNT_URL_MAX_LENGTH) return 'variant_mount_failed: url too long';
|
|
129
|
+
if (typeof msg.error !== 'string' || !msg.error.trim()) return 'variant_mount_failed: error required';
|
|
130
|
+
if (msg.error.length > MOUNT_ERROR_MAX_LENGTH) return 'variant_mount_failed: error too long';
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
95
134
|
export function validateEvent(msg) {
|
|
96
135
|
if (!msg || typeof msg !== 'object' || !msg.type) return 'Missing or invalid message';
|
|
97
136
|
switch (msg.type) {
|
|
@@ -118,6 +157,23 @@ export function validateEvent(msg) {
|
|
|
118
157
|
return 'checkpoint: paramValues must be an object';
|
|
119
158
|
}
|
|
120
159
|
return null;
|
|
160
|
+
case 'agent_phase':
|
|
161
|
+
if (!isValidId(msg.id)) return 'agent_phase: missing or malformed id';
|
|
162
|
+
if (typeof msg.phase !== 'string' || !msg.phase) return 'agent_phase: missing phase';
|
|
163
|
+
// The enum, not a shape pattern. A phase the browser cannot rank is a
|
|
164
|
+
// phase the progress bar cannot show, so accepting an arbitrary
|
|
165
|
+
// lowercase word only defers the failure to the UI.
|
|
166
|
+
if (!AGENT_PHASE_SET.has(msg.phase)) {
|
|
167
|
+
return 'agent_phase: unknown phase ' + msg.phase + ' (expected one of ' + AGENT_PHASES.join(', ') + ')';
|
|
168
|
+
}
|
|
169
|
+
if (msg.durationMs !== undefined && (!Number.isFinite(msg.durationMs) || msg.durationMs < 0)) {
|
|
170
|
+
return 'agent_phase: durationMs must be a non-negative number';
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
case 'variant_mounted':
|
|
174
|
+
return validateMountAck(msg);
|
|
175
|
+
case 'variant_mount_failed':
|
|
176
|
+
return validateMountFailure(msg);
|
|
121
177
|
case 'exit':
|
|
122
178
|
return null;
|
|
123
179
|
case 'prefetch':
|
|
@@ -131,6 +187,12 @@ export function validateEvent(msg) {
|
|
|
131
187
|
if (msg.message.length > 4000) return 'steer: message too long';
|
|
132
188
|
if (msg.pageUrl !== undefined && typeof msg.pageUrl !== 'string') return 'steer: pageUrl must be string';
|
|
133
189
|
return null;
|
|
190
|
+
case 'carbonize_cleanup':
|
|
191
|
+
if (!isValidId(msg.id)) return 'carbonize_cleanup: missing or malformed id';
|
|
192
|
+
if (!isValidId(msg.sessionId)) return 'carbonize_cleanup: missing or malformed sessionId';
|
|
193
|
+
if (!msg.file || typeof msg.file !== 'string') return 'carbonize_cleanup: missing file';
|
|
194
|
+
if (!isValidVariantId(String(msg.variantId))) return 'carbonize_cleanup: missing or malformed variantId';
|
|
195
|
+
return null;
|
|
134
196
|
default:
|
|
135
197
|
return 'Unknown event type: ' + msg.type;
|
|
136
198
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Astro registry entry.
|
|
3
|
+
*
|
|
4
|
+
* Astro takes the generic tag strategy, with two Astro-specific values that
|
|
5
|
+
* used to sit as inline `endsWith('.astro')` branches in live-inject.mjs and
|
|
6
|
+
* live-wrap.mjs:
|
|
7
|
+
*
|
|
8
|
+
* injectScriptAttrs Astro processes <script> tags by default and rewrites
|
|
9
|
+
* src to its own bundled URL; is:inline opts out.
|
|
10
|
+
* styleMode Astro scopes component styles, which strips preview CSS
|
|
11
|
+
* off the generated variant wrappers, so preview rules are
|
|
12
|
+
* authored global and prefixed instead of @scope'd.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { findConfigFile, hasAnyDependency, literalConfigFiles } from './detect-utils.mjs';
|
|
16
|
+
|
|
17
|
+
const ASTRO_CONFIG_RE = /^astro\.config\.(?:js|mjs|cjs|ts|mts|cts)$/;
|
|
18
|
+
|
|
19
|
+
export function detectAstroProject(cwd = process.cwd(), config = null) {
|
|
20
|
+
const configFile = findConfigFile(cwd, ASTRO_CONFIG_RE);
|
|
21
|
+
if (configFile) return { configFile, via: 'config' };
|
|
22
|
+
if (hasAnyDependency(cwd, ['astro'])) return { configFile: null, via: 'package' };
|
|
23
|
+
// A tree of .astro entry templates with no astro.config still belongs to
|
|
24
|
+
// Astro; the configured injection target names it.
|
|
25
|
+
const entry = literalConfigFiles(cwd, config).find((rel) => rel.endsWith('.astro'));
|
|
26
|
+
if (entry) return { configFile: null, via: 'config-files', entry };
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const astro = {
|
|
31
|
+
name: 'astro',
|
|
32
|
+
|
|
33
|
+
detect(cwd, config) {
|
|
34
|
+
return detectAstroProject(cwd, config);
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
inject: { kind: 'tag' },
|
|
38
|
+
|
|
39
|
+
source: {
|
|
40
|
+
extensions: ['.astro'],
|
|
41
|
+
preview: 'source',
|
|
42
|
+
styleMode: 'astro-global-prefixed',
|
|
43
|
+
styleTag: '<style is:inline data-impeccable-css="SESSION_ID">',
|
|
44
|
+
commentSyntax: 'html',
|
|
45
|
+
injectScriptAttrs: 'is:inline ',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small read-only probes the framework entries share.
|
|
3
|
+
*
|
|
4
|
+
* Every helper here is cheap and failure-tolerant: detection runs on every
|
|
5
|
+
* inject, against project trees that may be half-installed, so a missing or
|
|
6
|
+
* malformed file means "not this framework", never a throw.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
/** Merged dependency names from package.json, or an empty object. */
|
|
13
|
+
export function readPackageDeps(cwd) {
|
|
14
|
+
const file = path.join(cwd, 'package.json');
|
|
15
|
+
try {
|
|
16
|
+
const pkg = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
17
|
+
return {
|
|
18
|
+
...(pkg.dependencies || {}),
|
|
19
|
+
...(pkg.devDependencies || {}),
|
|
20
|
+
...(pkg.peerDependencies || {}),
|
|
21
|
+
};
|
|
22
|
+
} catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function hasAnyDependency(cwd, names) {
|
|
28
|
+
const deps = readPackageDeps(cwd);
|
|
29
|
+
return names.some((name) => Boolean(deps[name]));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** First top-level file name matching `re`, or null. */
|
|
33
|
+
export function findConfigFile(cwd, re) {
|
|
34
|
+
try {
|
|
35
|
+
return fs.readdirSync(cwd, { withFileTypes: true })
|
|
36
|
+
.find((entry) => entry.isFile() && re.test(entry.name))
|
|
37
|
+
?.name ?? null;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function fileExists(cwd, rel) {
|
|
44
|
+
try {
|
|
45
|
+
return fs.existsSync(path.join(cwd, rel));
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function firstExistingFile(cwd, candidates) {
|
|
52
|
+
for (const rel of candidates) {
|
|
53
|
+
if (fileExists(cwd, rel)) return rel;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Literal (non-glob) entries of `config.files` that exist on disk. Several
|
|
60
|
+
* detectors read the configured injection target as a signal, which is how the
|
|
61
|
+
* bare fixtures — a tree of `.astro` files with no astro.config — still resolve
|
|
62
|
+
* to the framework that authored them.
|
|
63
|
+
*/
|
|
64
|
+
export function literalConfigFiles(cwd, config) {
|
|
65
|
+
const files = Array.isArray(config?.files) ? config.files : [];
|
|
66
|
+
const out = [];
|
|
67
|
+
for (const rel of files) {
|
|
68
|
+
if (typeof rel !== 'string' || rel.includes('*') || rel.includes('?')) continue;
|
|
69
|
+
const normalized = rel.split(path.sep).join('/');
|
|
70
|
+
if (fileExists(cwd, normalized)) out.push(normalized);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live-mode framework registry.
|
|
3
|
+
*
|
|
4
|
+
* Before this existed, framework knowledge was smeared across live-inject.mjs
|
|
5
|
+
* (detection order, the Nuxt adapter, the Astro `is:inline` branch), the two
|
|
6
|
+
* adapter modules, and live-wrap.mjs (which extension gets component preview,
|
|
7
|
+
* which gets Astro's global-prefixed CSS, which gets JSX comments). Adding or
|
|
8
|
+
* fixing a framework meant reading all of them.
|
|
9
|
+
*
|
|
10
|
+
* One entry per framework now declares everything the live scripts need:
|
|
11
|
+
*
|
|
12
|
+
* name stable identifier; also the `adapter` value in inject JSON.
|
|
13
|
+
* detect (cwd, config) → falsy when this is not the project, otherwise
|
|
14
|
+
* a truthy project descriptor that apply/remove/artifacts read.
|
|
15
|
+
* Order in FRAMEWORKS is priority order; first truthy wins.
|
|
16
|
+
* inject { kind: 'adapter', apply, remove, ignorePatterns, artifacts,
|
|
17
|
+
* unpatch } for frameworks that server-render their document
|
|
18
|
+
* shell, or { kind: 'tag' } for the generic marker-wrapped
|
|
19
|
+
* <script src> block.
|
|
20
|
+
* source how live-wrap treats files this framework authors:
|
|
21
|
+
* extensions, preview ('source' | 'component'), styleMode,
|
|
22
|
+
* styleTag, commentSyntax, injectScriptAttrs. Anything omitted
|
|
23
|
+
* falls back to SOURCE_TRAIT_DEFAULTS.
|
|
24
|
+
*
|
|
25
|
+
* Two rules hold the thing together:
|
|
26
|
+
*
|
|
27
|
+
* 1. **Detection order is injection priority.** SvelteKit → Nuxt → TanStack
|
|
28
|
+
* Start → Astro → Next → Vite → static HTML, exactly the order
|
|
29
|
+
* live-inject.mjs used to hard-code. static-html always matches, so
|
|
30
|
+
* resolveFramework never returns null.
|
|
31
|
+
* 2. **Source traits resolve by file extension, not by project.** A SvelteKit
|
|
32
|
+
* project's injection target is `src/app.html`; a Vite app can contain
|
|
33
|
+
* `.astro` partials. live-wrap has always keyed these off the target file,
|
|
34
|
+
* and resolveSourceTraits keeps it that way. Several entries may claim the
|
|
35
|
+
* same extension (`.tsx` belongs to three); when they do, the values must
|
|
36
|
+
* agree, which tests/live-frameworks.test.mjs asserts.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import path from 'node:path';
|
|
40
|
+
|
|
41
|
+
import { sveltekit } from './sveltekit.mjs';
|
|
42
|
+
import { nuxt } from './nuxt.mjs';
|
|
43
|
+
import { tanstackStart } from './tanstack-start.mjs';
|
|
44
|
+
import { astro } from './astro.mjs';
|
|
45
|
+
import { nextjs } from './nextjs.mjs';
|
|
46
|
+
import { viteGeneric } from './vite-generic.mjs';
|
|
47
|
+
import { staticHtml } from './static-html.mjs';
|
|
48
|
+
import { TAG_PATCH_MARKERS, unpatchTagFile } from './tag-strategy.mjs';
|
|
49
|
+
|
|
50
|
+
/** Priority order. Do not reorder without re-reading rule 1 above. */
|
|
51
|
+
export const FRAMEWORKS = Object.freeze([
|
|
52
|
+
sveltekit,
|
|
53
|
+
nuxt,
|
|
54
|
+
tanstackStart,
|
|
55
|
+
astro,
|
|
56
|
+
nextjs,
|
|
57
|
+
viteGeneric,
|
|
58
|
+
staticHtml,
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
export const PREVIEW_MODES = Object.freeze(['source', 'component']);
|
|
62
|
+
export const STYLE_MODES = Object.freeze(['scoped', 'astro-global-prefixed']);
|
|
63
|
+
export const COMMENT_SYNTAXES = Object.freeze(['html', 'jsx']);
|
|
64
|
+
export const INJECT_KINDS = Object.freeze(['adapter', 'tag']);
|
|
65
|
+
|
|
66
|
+
export const SOURCE_TRAIT_DEFAULTS = Object.freeze({
|
|
67
|
+
preview: 'source',
|
|
68
|
+
styleMode: 'scoped',
|
|
69
|
+
styleTag: '<style data-impeccable-css="SESSION_ID">',
|
|
70
|
+
commentSyntax: 'html',
|
|
71
|
+
injectScriptAttrs: '',
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/** The patch kind the generic tag strategy records in the journal. */
|
|
75
|
+
export const TAG_PATCH_KIND = 'live-tag';
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Undo functions keyed by the `patch` value an artifact carries. Built from
|
|
79
|
+
* the entries so a new adapter registers its own undo alongside its apply.
|
|
80
|
+
*/
|
|
81
|
+
export const PATCH_UNDOERS = Object.freeze(Object.assign(
|
|
82
|
+
{ [TAG_PATCH_KIND]: unpatchTagFile },
|
|
83
|
+
...FRAMEWORKS.map((framework) => framework.inject.unpatch || {}),
|
|
84
|
+
));
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* First entry whose detect() matches. Returns { framework, project } where
|
|
88
|
+
* project is the detector's descriptor (adapters read it; tag frameworks
|
|
89
|
+
* mostly ignore it).
|
|
90
|
+
*/
|
|
91
|
+
export function resolveFramework(cwd = process.cwd(), config = null) {
|
|
92
|
+
for (const framework of FRAMEWORKS) {
|
|
93
|
+
const project = framework.detect(cwd, config);
|
|
94
|
+
if (project) return { framework, project };
|
|
95
|
+
}
|
|
96
|
+
// Unreachable while static-html stays terminal, but a caller that reorders
|
|
97
|
+
// the array should get a diagnosable null rather than a silent tag inject.
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Source-authoring traits for one file, merged over SOURCE_TRAIT_DEFAULTS.
|
|
103
|
+
* `framework` names the entry that claimed the extension, or null.
|
|
104
|
+
*/
|
|
105
|
+
export function resolveSourceTraits(filePath) {
|
|
106
|
+
const ext = path.extname(String(filePath || '')).toLowerCase();
|
|
107
|
+
for (const framework of FRAMEWORKS) {
|
|
108
|
+
const source = framework.source;
|
|
109
|
+
if (!source || !source.extensions.includes(ext)) continue;
|
|
110
|
+
const { extensions, ...traits } = source;
|
|
111
|
+
return { framework: framework.name, ...SOURCE_TRAIT_DEFAULTS, ...traits };
|
|
112
|
+
}
|
|
113
|
+
return { framework: null, ...SOURCE_TRAIT_DEFAULTS };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Extra gitignore patterns the resolved framework needs beyond the static
|
|
118
|
+
* LIVE_IGNORE_PATTERNS list (paths that depend on a detected srcDir or file
|
|
119
|
+
* extension and so cannot be written down ahead of time).
|
|
120
|
+
*/
|
|
121
|
+
export function frameworkIgnorePatterns(resolved) {
|
|
122
|
+
const fn = resolved?.framework?.inject?.ignorePatterns;
|
|
123
|
+
return typeof fn === 'function' ? (fn(resolved.project) || []) : [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The files this injection will create or patch, in journal-artifact form.
|
|
128
|
+
* Adapters declare their own; the tag strategy patches exactly the resolved
|
|
129
|
+
* config files.
|
|
130
|
+
*/
|
|
131
|
+
export function describeInjectArtifacts(resolved, { cwd = process.cwd(), files = [] } = {}) {
|
|
132
|
+
if (!resolved) return [];
|
|
133
|
+
const { framework, project } = resolved;
|
|
134
|
+
if (framework.inject.kind === 'adapter') {
|
|
135
|
+
return (framework.inject.artifacts?.({ cwd, project }) || []).filter((a) => a && a.path);
|
|
136
|
+
}
|
|
137
|
+
return files.map((file) => ({
|
|
138
|
+
kind: 'patched',
|
|
139
|
+
path: file,
|
|
140
|
+
patch: TAG_PATCH_KIND,
|
|
141
|
+
markers: [...TAG_PATCH_MARKERS],
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crash-safe injection journal.
|
|
3
|
+
*
|
|
4
|
+
* Injection writes into the user's source tree: generated components, a Nuxt
|
|
5
|
+
* client plugin, marker blocks inside a layout, a patched CSP meta tag. The
|
|
6
|
+
* clean path removes all of it on stop. The unclean paths do not:
|
|
7
|
+
*
|
|
8
|
+
* - the dev server is SIGKILLed, so `--remove` never runs;
|
|
9
|
+
* - the project changes shape between start and stop (a nuxt.config appears,
|
|
10
|
+
* a package.json is edited), so detection resolves a different framework
|
|
11
|
+
* and the old framework's artifacts are nobody's business;
|
|
12
|
+
* - stop runs from a different directory than start did.
|
|
13
|
+
*
|
|
14
|
+
* So every inject records what it wrote to `.impeccable/live/inject-journal.json`
|
|
15
|
+
* before the next one runs, and both inject and `--remove` reconcile that
|
|
16
|
+
* record against the tree.
|
|
17
|
+
*
|
|
18
|
+
* **The journal is a claim of ownership, not a to-do list.** Healing an
|
|
19
|
+
* artifact only ever removes what still carries our marker; a generated file
|
|
20
|
+
* the user has since replaced, or a layout they have since un-patched by hand,
|
|
21
|
+
* is dropped from the journal untouched.
|
|
22
|
+
*
|
|
23
|
+
* **Path resolution is appRoot-relative.** Live entry scripts chdir onto the
|
|
24
|
+
* roots manifest (`enterLiveRoot`) before doing anything, so a journal written
|
|
25
|
+
* by a session started in the app root is found by a stop issued from any
|
|
26
|
+
* directory inside the repo.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import fs from 'node:fs';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import { PATCH_UNDOERS } from './index.mjs';
|
|
32
|
+
|
|
33
|
+
export const INJECT_JOURNAL_VERSION = 1;
|
|
34
|
+
export const INJECT_JOURNAL_RELPATH = '.impeccable/live/inject-journal.json';
|
|
35
|
+
|
|
36
|
+
export function injectJournalPath(cwd = process.cwd()) {
|
|
37
|
+
return path.join(cwd, ...INJECT_JOURNAL_RELPATH.split('/'));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function readInjectJournal(cwd = process.cwd()) {
|
|
41
|
+
const file = injectJournalPath(cwd);
|
|
42
|
+
let raw;
|
|
43
|
+
try {
|
|
44
|
+
raw = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (!raw || typeof raw !== 'object' || !Array.isArray(raw.artifacts)) return null;
|
|
49
|
+
return raw;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function clearInjectJournal(cwd = process.cwd()) {
|
|
53
|
+
try { fs.unlinkSync(injectJournalPath(cwd)); } catch { /* already gone */ }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function writeInjectJournal(cwd, journal) {
|
|
57
|
+
const file = injectJournalPath(cwd);
|
|
58
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
59
|
+
fs.writeFileSync(file, JSON.stringify(journal, null, 2) + '\n', 'utf-8');
|
|
60
|
+
return file;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Record the artifacts an injection just wrote. Replaces any previous record:
|
|
65
|
+
* callers heal first (see healInjectJournal), so nothing survivable is lost.
|
|
66
|
+
*/
|
|
67
|
+
export function recordInjection(cwd = process.cwd(), { framework, port, artifacts = [] } = {}) {
|
|
68
|
+
if (!artifacts.length) {
|
|
69
|
+
clearInjectJournal(cwd);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return writeInjectJournal(cwd, {
|
|
73
|
+
version: INJECT_JOURNAL_VERSION,
|
|
74
|
+
appRoot: path.resolve(cwd),
|
|
75
|
+
framework: framework || null,
|
|
76
|
+
port: Number.isFinite(Number(port)) ? Number(port) : null,
|
|
77
|
+
pid: process.pid,
|
|
78
|
+
recordedAt: new Date().toISOString(),
|
|
79
|
+
artifacts,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeRel(cwd, rel) {
|
|
84
|
+
return path.resolve(cwd, String(rel || '')).split(path.sep).join('/');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readIfPresent(abs) {
|
|
88
|
+
try {
|
|
89
|
+
return fs.readFileSync(abs, 'utf-8');
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function pruneEmptyDirs(dir, stopDir) {
|
|
96
|
+
let current = path.resolve(dir);
|
|
97
|
+
const stop = path.resolve(stopDir);
|
|
98
|
+
while (current !== stop && current.startsWith(stop + path.sep)) {
|
|
99
|
+
try {
|
|
100
|
+
if (fs.readdirSync(current).length > 0) return;
|
|
101
|
+
fs.rmdirSync(current);
|
|
102
|
+
} catch {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
current = path.dirname(current);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function insideProject(cwd, abs) {
|
|
110
|
+
const rel = path.relative(path.resolve(cwd), path.resolve(abs));
|
|
111
|
+
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function healArtifact(cwd, artifact, undoers) {
|
|
115
|
+
const abs = path.resolve(cwd, artifact.path);
|
|
116
|
+
// The journal is a project-local file, i.e. attacker-writable input in a
|
|
117
|
+
// cloned repo. Never touch anything outside the project tree, whatever the
|
|
118
|
+
// journal claims to own.
|
|
119
|
+
if (!insideProject(cwd, abs)) return { path: artifact.path, action: 'refused_outside_project' };
|
|
120
|
+
const content = readIfPresent(abs);
|
|
121
|
+
if (content === null) return { path: artifact.path, action: 'absent' };
|
|
122
|
+
|
|
123
|
+
if (artifact.kind === 'created') {
|
|
124
|
+
// Only reclaim a generated file that still carries our marker; a created
|
|
125
|
+
// artifact with no marker at all is unverifiable and stays untouched.
|
|
126
|
+
if (!artifact.marker || !content.includes(artifact.marker)) {
|
|
127
|
+
return { path: artifact.path, action: 'disowned' };
|
|
128
|
+
}
|
|
129
|
+
try { fs.rmSync(abs, { force: true }); } catch { return null; }
|
|
130
|
+
if (artifact.pruneTo !== undefined) {
|
|
131
|
+
const pruneRoot = path.resolve(cwd, artifact.pruneTo || '.');
|
|
132
|
+
if (insideProject(cwd, pruneRoot) || pruneRoot === path.resolve(cwd)) {
|
|
133
|
+
pruneEmptyDirs(path.dirname(abs), pruneRoot);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return { path: artifact.path, action: 'removed' };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (artifact.kind === 'patched') {
|
|
140
|
+
const markers = Array.isArray(artifact.markers) ? artifact.markers : [];
|
|
141
|
+
// No marker left means the patch is already gone; never run an undo over
|
|
142
|
+
// a file we no longer recognize (the undoers normalize whitespace).
|
|
143
|
+
if (markers.length && !markers.some((marker) => content.includes(marker))) {
|
|
144
|
+
return { path: artifact.path, action: 'disowned' };
|
|
145
|
+
}
|
|
146
|
+
const undo = undoers[artifact.patch];
|
|
147
|
+
if (typeof undo !== 'function') return null;
|
|
148
|
+
const next = undo(content);
|
|
149
|
+
if (next === content) return { path: artifact.path, action: 'disowned' };
|
|
150
|
+
try { fs.writeFileSync(abs, next, 'utf-8'); } catch { return null; }
|
|
151
|
+
return { path: artifact.path, action: 'unpatched' };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Reconcile the journal against the tree.
|
|
159
|
+
*
|
|
160
|
+
* `keep` is the set of paths the current operation legitimately owns — the
|
|
161
|
+
* artifacts an inject is about to (re)write. Everything else in the journal is
|
|
162
|
+
* an orphan of a session that is gone, and gets healed. This keeps a repeat
|
|
163
|
+
* inject byte-idempotent: the artifacts it is about to rewrite are kept, not
|
|
164
|
+
* torn down and rebuilt.
|
|
165
|
+
*
|
|
166
|
+
* Returns `{ healed, kept }`. `healed` lists only artifacts whose file was
|
|
167
|
+
* actually changed or removed, so callers can stay silent when nothing was
|
|
168
|
+
* orphaned. Idempotent: a second call finds an empty journal.
|
|
169
|
+
*/
|
|
170
|
+
export function healInjectJournal(cwd = process.cwd(), { keep = [], undoers = PATCH_UNDOERS } = {}) {
|
|
171
|
+
const journal = readInjectJournal(cwd);
|
|
172
|
+
if (!journal) return { healed: [], kept: [] };
|
|
173
|
+
|
|
174
|
+
const keepSet = new Set(keep.map((rel) => normalizeRel(cwd, rel)));
|
|
175
|
+
const healed = [];
|
|
176
|
+
const kept = [];
|
|
177
|
+
|
|
178
|
+
for (const artifact of journal.artifacts) {
|
|
179
|
+
if (!artifact || typeof artifact.path !== 'string') continue;
|
|
180
|
+
if (keepSet.has(normalizeRel(cwd, artifact.path))) {
|
|
181
|
+
kept.push(artifact);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const outcome = healArtifact(cwd, artifact, undoers);
|
|
185
|
+
if (outcome && (outcome.action === 'removed' || outcome.action === 'unpatched')) {
|
|
186
|
+
healed.push(outcome);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (kept.length) {
|
|
191
|
+
writeInjectJournal(cwd, { ...journal, artifacts: kept });
|
|
192
|
+
} else {
|
|
193
|
+
clearInjectJournal(cwd);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return { healed, kept };
|
|
197
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js registry entry.
|
|
3
|
+
*
|
|
4
|
+
* Next takes the generic tag strategy: the App Router's root layout renders
|
|
5
|
+
* `<html>…<body>` in JSX, so the marker-wrapped script block goes in there
|
|
6
|
+
* verbatim. Nothing about injection differs from a plain Vite app, which is
|
|
7
|
+
* why live-inject.mjs never had a Next branch. The entry exists so the
|
|
8
|
+
* registry can name what it is looking at.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { fileExists, findConfigFile, hasAnyDependency } from './detect-utils.mjs';
|
|
12
|
+
|
|
13
|
+
const NEXT_CONFIG_RE = /^next\.config\.(?:js|mjs|cjs|ts|mts|cts)$/;
|
|
14
|
+
|
|
15
|
+
const ROUTER_ENTRY_CANDIDATES = [
|
|
16
|
+
'app/layout.tsx', 'app/layout.jsx', 'app/layout.ts', 'app/layout.js',
|
|
17
|
+
'src/app/layout.tsx', 'src/app/layout.jsx', 'src/app/layout.ts', 'src/app/layout.js',
|
|
18
|
+
'pages/_app.tsx', 'pages/_app.jsx', 'pages/_app.ts', 'pages/_app.js',
|
|
19
|
+
'pages/_document.tsx', 'pages/_document.jsx',
|
|
20
|
+
'src/pages/_app.tsx', 'src/pages/_app.jsx',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export function detectNextProject(cwd = process.cwd()) {
|
|
24
|
+
const configFile = findConfigFile(cwd, NEXT_CONFIG_RE);
|
|
25
|
+
if (configFile) return { configFile, via: 'config' };
|
|
26
|
+
if (hasAnyDependency(cwd, ['next'])) return { configFile: null, via: 'package' };
|
|
27
|
+
// Next's file conventions are distinctive enough to stand alone: a root
|
|
28
|
+
// `app/layout.*` or `pages/_app.*` is not a shape other bundlers produce.
|
|
29
|
+
const entry = ROUTER_ENTRY_CANDIDATES.find((rel) => fileExists(cwd, rel));
|
|
30
|
+
if (entry) return { configFile: null, via: 'router-entry', entry };
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const nextjs = {
|
|
35
|
+
name: 'nextjs',
|
|
36
|
+
|
|
37
|
+
detect(cwd) {
|
|
38
|
+
return detectNextProject(cwd);
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
inject: { kind: 'tag' },
|
|
42
|
+
|
|
43
|
+
source: {
|
|
44
|
+
extensions: ['.tsx', '.jsx'],
|
|
45
|
+
preview: 'source',
|
|
46
|
+
styleMode: 'scoped',
|
|
47
|
+
commentSyntax: 'jsx',
|
|
48
|
+
},
|
|
49
|
+
};
|