@1agh/maude 0.42.0 → 0.44.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/apps/studio/.ai/cache/_stats.json +7 -0
- package/apps/studio/acp/bootstrap-brief.ts +12 -2
- package/apps/studio/acp/bridge.ts +44 -10
- package/apps/studio/acp/env.ts +23 -0
- package/apps/studio/acp/login-state.ts +263 -0
- package/apps/studio/acp/plugin-bootstrap.ts +31 -25
- package/apps/studio/acp/probe.ts +152 -1
- package/apps/studio/annotations-layer.tsx +1451 -324
- package/apps/studio/annotations-model.ts +58 -0
- package/apps/studio/api.ts +568 -24
- package/apps/studio/bin/_audio-search.mjs +140 -0
- package/apps/studio/bin/_import-asset-pdf-worker.mjs +34 -0
- package/apps/studio/bin/_import-asset.mjs +815 -0
- package/apps/studio/bin/_import-brand.mjs +635 -0
- package/apps/studio/bin/_import-tokens-alias-resolver.mjs +95 -0
- package/apps/studio/bin/_import-tokens.mjs +1201 -0
- package/apps/studio/bin/_ingest-footage.mjs +386 -0
- package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
- package/apps/studio/bin/_transcribe.mjs +419 -0
- package/apps/studio/bin/_transcribe.test.mjs +80 -0
- package/apps/studio/bin/_video-playwright.mjs +141 -71
- package/apps/studio/bin/audio-search.sh +28 -0
- package/apps/studio/bin/generate.sh +154 -0
- package/apps/studio/bin/import-asset.sh +34 -0
- package/apps/studio/bin/import-brand.sh +29 -0
- package/apps/studio/bin/import-tokens.sh +33 -0
- package/apps/studio/bin/ingest-footage.sh +27 -0
- package/apps/studio/bin/photo-adjust.sh +163 -0
- package/apps/studio/bin/photo-bg-remove.sh +258 -0
- package/apps/studio/bin/probe-footage.sh +28 -0
- package/apps/studio/bin/read-annotations.mjs +125 -2
- package/apps/studio/bin/transcribe.sh +39 -0
- package/apps/studio/canvas-edit.ts +656 -12
- package/apps/studio/canvas-lib.tsx +637 -10
- package/apps/studio/canvas-pipeline.ts +65 -7
- package/apps/studio/canvas-shell.tsx +156 -16
- package/apps/studio/client/app.jsx +2385 -699
- package/apps/studio/client/export-center.jsx +35 -1
- package/apps/studio/client/generate-dialog.jsx +352 -0
- package/apps/studio/client/github.js +18 -0
- package/apps/studio/client/inspector-controls.jsx +781 -0
- package/apps/studio/client/panels/BrandUploadPanel.jsx +233 -0
- package/apps/studio/client/panels/ChatPanel.jsx +15 -33
- package/apps/studio/client/panels/GitPanel.jsx +8 -0
- package/apps/studio/client/panels/IntroVideoDialog.jsx +44 -0
- package/apps/studio/client/panels/OnboardingWizard.jsx +12 -0
- package/apps/studio/client/panels/ReadinessList.jsx +227 -7
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
- package/apps/studio/client/panels/SettingsPanel.jsx +828 -0
- package/apps/studio/client/panels/SetupChecklist.jsx +223 -0
- package/apps/studio/client/panels/StickerPicker.jsx +160 -0
- package/apps/studio/client/panels/TimelinePanel.jsx +15 -2
- package/apps/studio/client/photo-knobs.jsx +432 -0
- package/apps/studio/client/styles/3-shell-maude.css +376 -28
- package/apps/studio/client/styles/4-components.css +223 -0
- package/apps/studio/client/styles/6-acp-chat.css +86 -0
- package/apps/studio/client/tour/quick-setup-tour.js +39 -0
- package/apps/studio/commands/annotation-strokes-command.ts +13 -3
- package/apps/studio/commands/edit-source-command.ts +31 -4
- package/apps/studio/config.schema.json +70 -0
- package/apps/studio/context-menu.tsx +42 -8
- package/apps/studio/context.ts +16 -0
- package/apps/studio/design-setup-readiness.ts +115 -0
- package/apps/studio/dist/client.bundle.js +5572 -21
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/runtime/.min-sizes.json +1 -0
- package/apps/studio/dist/runtime/@imgly_background-removal.js +5543 -0
- package/apps/studio/dist/runtime/pixi-js.js +519 -519
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/dom-selection.ts +25 -0
- package/apps/studio/draw/palette.ts +34 -0
- package/apps/studio/exporters/jobs.ts +47 -5
- package/apps/studio/exporters/video.ts +24 -4
- package/apps/studio/footage/schema.test.ts +287 -0
- package/apps/studio/footage/schema.ts +662 -0
- package/apps/studio/footage-store.ts +235 -0
- package/apps/studio/fs-watch.ts +10 -0
- package/apps/studio/generation/adapters/elevenlabs.ts +370 -0
- package/apps/studio/generation/adapters/gemini.ts +542 -0
- package/apps/studio/generation/adapters/groq.test.ts +80 -0
- package/apps/studio/generation/adapters/groq.ts +247 -0
- package/apps/studio/generation/audio-library.test.ts +95 -0
- package/apps/studio/generation/audio-library.ts +156 -0
- package/apps/studio/generation/captions.test.ts +78 -0
- package/apps/studio/generation/captions.ts +132 -0
- package/apps/studio/generation/download.ts +76 -0
- package/apps/studio/generation/elevenlabs.test.ts +223 -0
- package/apps/studio/generation/gemini.test.ts +350 -0
- package/apps/studio/generation/jobs.test.ts +128 -0
- package/apps/studio/generation/jobs.ts +243 -0
- package/apps/studio/generation/keys.test.ts +75 -0
- package/apps/studio/generation/keys.ts +162 -0
- package/apps/studio/generation/prefs.test.ts +95 -0
- package/apps/studio/generation/prefs.ts +82 -0
- package/apps/studio/generation/registry.ts +88 -0
- package/apps/studio/generation/types.test.ts +123 -0
- package/apps/studio/generation/types.ts +266 -0
- package/apps/studio/generation/whisper-models.test.ts +152 -0
- package/apps/studio/generation/whisper-models.ts +257 -0
- package/apps/studio/generation/whisper-spike-results.md +61 -0
- package/apps/studio/git/endpoints.ts +68 -7
- package/apps/studio/git/service.ts +199 -21
- package/apps/studio/github/service.ts +67 -0
- package/apps/studio/handoff.ts +3 -2
- package/apps/studio/http.ts +1039 -22
- package/apps/studio/input-router.tsx +10 -0
- package/apps/studio/inspect.ts +53 -1
- package/apps/studio/media/intro.mp4 +0 -0
- package/apps/studio/media-commit-chain.ts +119 -0
- package/apps/studio/paths.ts +20 -0
- package/apps/studio/photo/filters.ts +285 -0
- package/apps/studio/photo/pipeline.ts +607 -0
- package/apps/studio/photo/schema.ts +367 -0
- package/apps/studio/photo-store.ts +154 -0
- package/apps/studio/readiness.ts +108 -20
- package/apps/studio/runtime-bundle.ts +29 -6
- package/apps/studio/scaffold-design.ts +1145 -0
- package/apps/studio/server.ts +18 -2
- package/apps/studio/stickers/figjam-doodle/manifest.json +104 -0
- package/apps/studio/stickers/figjam-doodle/slice1.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice10.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice11.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice12.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice13.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice14.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice15.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice16.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice17.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice18.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice19.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice2.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice20.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice21.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice22.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice23.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice24.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice3.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice4.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice5.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice6.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice7.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice8.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice9.png +0 -0
- package/apps/studio/stickers/life-style/best.png +0 -0
- package/apps/studio/stickers/life-style/come-on.png +0 -0
- package/apps/studio/stickers/life-style/cut.png +0 -0
- package/apps/studio/stickers/life-style/detox.png +0 -0
- package/apps/studio/stickers/life-style/just.png +0 -0
- package/apps/studio/stickers/life-style/manifest.json +68 -0
- package/apps/studio/stickers/life-style/mirror.png +0 -0
- package/apps/studio/stickers/life-style/nice.png +0 -0
- package/apps/studio/stickers/life-style/objects.png +0 -0
- package/apps/studio/stickers/life-style/ok.png +0 -0
- package/apps/studio/stickers/life-style/queen.png +0 -0
- package/apps/studio/stickers/life-style/star.png +0 -0
- package/apps/studio/stickers/life-style/sun.png +0 -0
- package/apps/studio/stickers/life-style/water.png +0 -0
- package/apps/studio/stickers/life-style/yeah.png +0 -0
- package/apps/studio/stickers/life-style/you-can.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/check-the-deets.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/cut-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/dope.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/fresh.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/hot.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/idea.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/keep-exploring.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/killed-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/love-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/manifest.json +88 -0
- package/apps/studio/stickers/opposing-thoughts/not-sure.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/ok.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/pure-gold.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/save-for-later.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/steal-this.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/take-a-peek.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/this-or-that.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/thoughts.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/vibes.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/winner.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/wip.png +0 -0
- package/apps/studio/stickers/project-status/group-100.png +0 -0
- package/apps/studio/stickers/project-status/group-101.png +0 -0
- package/apps/studio/stickers/project-status/group-102.png +0 -0
- package/apps/studio/stickers/project-status/group-103.png +0 -0
- package/apps/studio/stickers/project-status/group-104.png +0 -0
- package/apps/studio/stickers/project-status/group-105.png +0 -0
- package/apps/studio/stickers/project-status/group-106.png +0 -0
- package/apps/studio/stickers/project-status/group-107.png +0 -0
- package/apps/studio/stickers/project-status/group-108.png +0 -0
- package/apps/studio/stickers/project-status/group-109.png +0 -0
- package/apps/studio/stickers/project-status/group-110.png +0 -0
- package/apps/studio/stickers/project-status/group-111.png +0 -0
- package/apps/studio/stickers/project-status/group-112.png +0 -0
- package/apps/studio/stickers/project-status/group-113.png +0 -0
- package/apps/studio/stickers/project-status/group-114.png +0 -0
- package/apps/studio/stickers/project-status/group-115.png +0 -0
- package/apps/studio/stickers/project-status/group-117.png +0 -0
- package/apps/studio/stickers/project-status/group-118.png +0 -0
- package/apps/studio/stickers/project-status/group-119.png +0 -0
- package/apps/studio/stickers/project-status/group-120.png +0 -0
- package/apps/studio/stickers/project-status/group-121.png +0 -0
- package/apps/studio/stickers/project-status/group-122.png +0 -0
- package/apps/studio/stickers/project-status/group-41.png +0 -0
- package/apps/studio/stickers/project-status/group-42.png +0 -0
- package/apps/studio/stickers/project-status/group-43.png +0 -0
- package/apps/studio/stickers/project-status/group-44.png +0 -0
- package/apps/studio/stickers/project-status/group-45.png +0 -0
- package/apps/studio/stickers/project-status/group-46.png +0 -0
- package/apps/studio/stickers/project-status/group-47.png +0 -0
- package/apps/studio/stickers/project-status/group-48.png +0 -0
- package/apps/studio/stickers/project-status/group-49.png +0 -0
- package/apps/studio/stickers/project-status/group-50.png +0 -0
- package/apps/studio/stickers/project-status/group-51.png +0 -0
- package/apps/studio/stickers/project-status/group-52.png +0 -0
- package/apps/studio/stickers/project-status/group-53.png +0 -0
- package/apps/studio/stickers/project-status/group-54.png +0 -0
- package/apps/studio/stickers/project-status/group-55.png +0 -0
- package/apps/studio/stickers/project-status/group-56.png +0 -0
- package/apps/studio/stickers/project-status/group-57.png +0 -0
- package/apps/studio/stickers/project-status/group-58.png +0 -0
- package/apps/studio/stickers/project-status/group-59.png +0 -0
- package/apps/studio/stickers/project-status/group-60.png +0 -0
- package/apps/studio/stickers/project-status/group-61.png +0 -0
- package/apps/studio/stickers/project-status/group-62.png +0 -0
- package/apps/studio/stickers/project-status/group-63.png +0 -0
- package/apps/studio/stickers/project-status/group-64.png +0 -0
- package/apps/studio/stickers/project-status/group-65.png +0 -0
- package/apps/studio/stickers/project-status/group-66.png +0 -0
- package/apps/studio/stickers/project-status/group-67.png +0 -0
- package/apps/studio/stickers/project-status/group-68.png +0 -0
- package/apps/studio/stickers/project-status/group-69.png +0 -0
- package/apps/studio/stickers/project-status/group-70.png +0 -0
- package/apps/studio/stickers/project-status/group-71.png +0 -0
- package/apps/studio/stickers/project-status/group-72.png +0 -0
- package/apps/studio/stickers/project-status/group-73.png +0 -0
- package/apps/studio/stickers/project-status/group-74.png +0 -0
- package/apps/studio/stickers/project-status/group-75.png +0 -0
- package/apps/studio/stickers/project-status/group-76.png +0 -0
- package/apps/studio/stickers/project-status/group-77.png +0 -0
- package/apps/studio/stickers/project-status/group-78.png +0 -0
- package/apps/studio/stickers/project-status/group-79.png +0 -0
- package/apps/studio/stickers/project-status/group-80.png +0 -0
- package/apps/studio/stickers/project-status/group-81.png +0 -0
- package/apps/studio/stickers/project-status/group-82.png +0 -0
- package/apps/studio/stickers/project-status/group-83.png +0 -0
- package/apps/studio/stickers/project-status/group-84.png +0 -0
- package/apps/studio/stickers/project-status/group-85.png +0 -0
- package/apps/studio/stickers/project-status/group-86.png +0 -0
- package/apps/studio/stickers/project-status/group-87.png +0 -0
- package/apps/studio/stickers/project-status/group-88.png +0 -0
- package/apps/studio/stickers/project-status/group-89.png +0 -0
- package/apps/studio/stickers/project-status/group-90.png +0 -0
- package/apps/studio/stickers/project-status/group-91.png +0 -0
- package/apps/studio/stickers/project-status/group-92.png +0 -0
- package/apps/studio/stickers/project-status/group-93.png +0 -0
- package/apps/studio/stickers/project-status/group-94.png +0 -0
- package/apps/studio/stickers/project-status/group-96.png +0 -0
- package/apps/studio/stickers/project-status/group-97.png +0 -0
- package/apps/studio/stickers/project-status/group-98.png +0 -0
- package/apps/studio/stickers/project-status/group-99.png +0 -0
- package/apps/studio/stickers/project-status/manifest.json +328 -0
- package/apps/studio/test/_helpers.ts +15 -2
- package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
- package/apps/studio/test/acp-bridge.test.ts +34 -1
- package/apps/studio/test/acp-commands.test.ts +2 -1
- package/apps/studio/test/acp-env.test.ts +30 -0
- package/apps/studio/test/acp-plugin-bootstrap.test.ts +23 -57
- package/apps/studio/test/acp-session-plugins.test.ts +72 -1
- package/apps/studio/test/annotation-strokes-command.test.ts +17 -0
- package/apps/studio/test/annotations-layer.test.ts +174 -0
- package/apps/studio/test/asset-api.test.ts +3 -3
- package/apps/studio/test/canvas-edit.test.ts +17 -6
- package/apps/studio/test/canvas-origin-gate.test.ts +50 -0
- package/apps/studio/test/csp-canvas-shell.test.ts +11 -0
- package/apps/studio/test/csrf-write-guard.test.ts +23 -1
- package/apps/studio/test/design-setup-readiness.test.ts +155 -0
- package/apps/studio/test/dynamic-text-edit.test.ts +162 -0
- package/apps/studio/test/edit-source-command.test.ts +23 -0
- package/apps/studio/test/element-structural-edit.test.ts +154 -0
- package/apps/studio/test/fixtures/fake-claude-auth.mjs +13 -0
- package/apps/studio/test/footage-store.test.ts +100 -0
- package/apps/studio/test/generate-route.test.ts +106 -0
- package/apps/studio/test/git-branches.test.ts +97 -0
- package/apps/studio/test/github-api.test.ts +56 -1
- package/apps/studio/test/import-asset-browser.test.ts +64 -0
- package/apps/studio/test/import-asset-route.test.ts +71 -0
- package/apps/studio/test/import-asset.test.ts +399 -0
- package/apps/studio/test/import-brand-browser.test.ts +88 -0
- package/apps/studio/test/import-brand-route.test.ts +120 -0
- package/apps/studio/test/import-brand.test.ts +206 -0
- package/apps/studio/test/import-tokens.test.ts +722 -0
- package/apps/studio/test/input-router.test.ts +33 -0
- package/apps/studio/test/inspect-script-syntax.test.ts +53 -0
- package/apps/studio/test/media-commit-chain.test.ts +210 -0
- package/apps/studio/test/photo-bg-remove-validation.test.ts +71 -0
- package/apps/studio/test/photo-canvas-bundle.test.ts +61 -0
- package/apps/studio/test/photo-edit-api.test.ts +201 -0
- package/apps/studio/test/photo-filters.test.ts +149 -0
- package/apps/studio/test/photo-pipeline.test.ts +106 -0
- package/apps/studio/test/photo-taxonomy.test.ts +96 -0
- package/apps/studio/test/read-annotations.test.ts +164 -0
- package/apps/studio/test/readiness.test.ts +13 -11
- package/apps/studio/test/scaffold-design.test.ts +122 -0
- package/apps/studio/test/shell-importmap.test.ts +49 -0
- package/apps/studio/test/text-editability-stamp.test.ts +131 -0
- package/apps/studio/test/timeline-parse.test.ts +57 -0
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +5 -3
- package/apps/studio/test/tour-overlay.test.tsx +16 -0
- package/apps/studio/test/undo-stack.test.ts +33 -0
- package/apps/studio/test/video-asset.test.ts +5 -5
- package/apps/studio/test/video-render-bridge.test.ts +23 -1
- package/apps/studio/text-caret.ts +239 -0
- package/apps/studio/tool-palette.tsx +49 -21
- package/apps/studio/ui-prefs.ts +130 -0
- package/apps/studio/undo-stack.ts +94 -2
- package/apps/studio/use-annotation-resize.tsx +4 -4
- package/apps/studio/use-canvas-media-drop.tsx +40 -1
- package/apps/studio/use-chrome-visibility.tsx +8 -2
- package/apps/studio/use-selection-set.tsx +21 -0
- package/apps/studio/video-comp.tsx +48 -7
- package/apps/studio/whats-new.json +149 -0
- package/apps/studio/ws.ts +5 -0
- package/cli/bin/maude.mjs +6 -6
- package/cli/commands/design.mjs +67 -3
- package/cli/lib/gitignore-block.mjs +2 -0
- package/cli/lib/pkg-root.mjs +107 -0
- package/cli/lib/pkg-root.test.mjs +79 -0
- package/cli/lib/reconstruct-toolset.test.mjs +194 -0
- package/cli/lib/update-check.mjs +8 -0
- package/package.json +13 -11
- package/plugins/design/dependencies.json +18 -0
- package/plugins/design/templates/_shell.html +1 -0
|
@@ -209,9 +209,11 @@ function underPrefix(filepath: string, prefix: string): boolean {
|
|
|
209
209
|
* double-transports through git. */
|
|
210
210
|
function isMaudeRuntimeState(p: string): boolean {
|
|
211
211
|
return (
|
|
212
|
-
/(^|\/)_(?:server|active|sync|preflight|locator|export-history)\.json$/.test(
|
|
212
|
+
/(^|\/)_(?:server|active|sync|preflight|locator|export-history|generate-history)\.json$/.test(
|
|
213
|
+
p
|
|
214
|
+
) ||
|
|
213
215
|
/(^|\/)_server\.(?:lock|log)$/.test(p) ||
|
|
214
|
-
/(^|\/)_(?:history|trash|draw|smoke|canvas-state|state|chat|comments|untrusted|export-jobs)(?:\/|$)/.test(
|
|
216
|
+
/(^|\/)_(?:history|trash|draw|photo|smoke|canvas-state|state|chat|comments|untrusted|export-jobs)(?:\/|$)/.test(
|
|
215
217
|
p
|
|
216
218
|
)
|
|
217
219
|
);
|
|
@@ -541,6 +543,16 @@ function classifyRemoteUrl(url: string): RemoteTransport {
|
|
|
541
543
|
return 'unsafe';
|
|
542
544
|
}
|
|
543
545
|
|
|
546
|
+
/** True when a remote URL points at github.com (https or the scp-like ssh form).
|
|
547
|
+
* Decides the fold path (DDR-162): a GitHub remote gets the PR flow (push draft +
|
|
548
|
+
* open a pull request via the endpoint); anything else (no remote, a local/file
|
|
549
|
+
* remote) keeps the local-merge path. Only a routing decision — the authoritative,
|
|
550
|
+
* security-anchored owner/repo parse is `parseGitHubRemote` at the endpoint, which
|
|
551
|
+
* rejects an embedded `evil.com/github.com/…` even if this heuristic didn't. */
|
|
552
|
+
function isGitHubRemote(url: string): boolean {
|
|
553
|
+
return /(?:^|\/\/|@)github\.com[:/]/i.test((url || '').trim());
|
|
554
|
+
}
|
|
555
|
+
|
|
544
556
|
/** Read a remote's configured URL (empty string when missing). */
|
|
545
557
|
async function readRemoteUrl(dir: string, remote: string): Promise<string> {
|
|
546
558
|
return (await git.getConfig({ fs, dir, path: `remote.${remote}.url` }).catch(() => null)) || '';
|
|
@@ -550,14 +562,42 @@ async function readRemoteUrl(dir: string, remote: string): Promise<string> {
|
|
|
550
562
|
* never to an arbitrary HTTPS host an attacker put in `remote.origin.url` (PAT
|
|
551
563
|
* exfil / SSRF). HTTP(S) urls only; ssh carries no token regardless. */
|
|
552
564
|
function isTrustedTokenHost(url: string): boolean {
|
|
565
|
+
const u = (url || '').trim();
|
|
566
|
+
// SECURITY (F1 — parser-differential PAT exfil): the token-attach decision must NOT
|
|
567
|
+
// trust a `new URL()` parse that git+libcurl will REDO with a different grammar. A
|
|
568
|
+
// `https://github.com\@attacker/x` reads as host github.com under WHATWG but as host
|
|
569
|
+
// `attacker` under curl (backslash = path/userinfo char). Reject any byte that could
|
|
570
|
+
// re-open the authority — backslash, userinfo `@`, whitespace, control chars — then
|
|
571
|
+
// require the byte-exact canonical github.com https prefix, so the host resolves to
|
|
572
|
+
// github.com under BOTH parsers before the PAT is ever lent.
|
|
573
|
+
// Only printable ASCII (rejects whitespace, control, and non-ASCII homoglyphs),
|
|
574
|
+
// and no backslash / userinfo `@` — the two bytes that let curl re-resolve the host.
|
|
575
|
+
if (/[^\x21-\x7e]/.test(u) || u.includes('\\') || u.includes('@')) return false;
|
|
576
|
+
if (!/^https:\/\/github\.com\//i.test(u)) return false;
|
|
553
577
|
try {
|
|
554
|
-
|
|
555
|
-
return h === 'github.com' || h.endsWith('.github.com');
|
|
578
|
+
return new URL(u).hostname.toLowerCase() === 'github.com';
|
|
556
579
|
} catch {
|
|
557
580
|
return false;
|
|
558
581
|
}
|
|
559
582
|
}
|
|
560
583
|
|
|
584
|
+
/** The URL git will ACTUALLY dial for `remote`, after any `url.<base>.insteadOf`
|
|
585
|
+
* rewrite — the URL that must be host-validated, NOT the raw `remote.url`. A poisoned
|
|
586
|
+
* `.git/config` `url.<attacker>.insteadOf = https://github.com/` (rides a clone/folder,
|
|
587
|
+
* no file-review sees it) makes the SYSTEM engine dial the attacker even though
|
|
588
|
+
* `remote.url` is a clean github URL; this is the surviving instance of the F1
|
|
589
|
+
* "validate-here / connect-there" class (verify re-review). `git ls-remote --get-url`
|
|
590
|
+
* applies insteadOf and prints WITHOUT touching the network. iso-git ignores insteadOf,
|
|
591
|
+
* so when there's no system git the raw URL IS the effective one. */
|
|
592
|
+
async function effectiveRemoteUrl(dir: string, remote: string): Promise<string> {
|
|
593
|
+
const raw = await readRemoteUrl(dir, remote);
|
|
594
|
+
if (!raw) return ''; // no remote → 'none'
|
|
595
|
+
if (!(await systemGitAvailable())) return raw;
|
|
596
|
+
const r = await runGit(dir, [...HARDENED_REMOTE_FLAGS, 'ls-remote', '--get-url', remote]);
|
|
597
|
+
const eff = r.code === 0 ? r.stdout.trim() : '';
|
|
598
|
+
return eff || raw;
|
|
599
|
+
}
|
|
600
|
+
|
|
561
601
|
/** Defense-in-depth for any `runGit` that resolves a config remote URL: disable the
|
|
562
602
|
* command-EXECUTING transports at the git layer too (`classifyRemoteUrl` already
|
|
563
603
|
* refuses them before we spawn — this is the backstop). Deliberately does NOT
|
|
@@ -767,8 +807,15 @@ export interface GitFoldResult {
|
|
|
767
807
|
conflict?: boolean;
|
|
768
808
|
authRequired?: boolean;
|
|
769
809
|
error?: string;
|
|
770
|
-
/** The Shared-version branch the draft was added to. */
|
|
810
|
+
/** The Shared-version branch the draft was added to (local merge) or targeted (PR). */
|
|
771
811
|
shared?: string;
|
|
812
|
+
/** PR flow (DDR-162): a GitHub remote exists, the draft branch was pushed, and the
|
|
813
|
+
* endpoint should open a pull request `head → base`. When set, no local merge or
|
|
814
|
+
* push of the Shared version happened — the merge lands on GitHub, post-review. */
|
|
815
|
+
prReady?: boolean;
|
|
816
|
+
head?: string;
|
|
817
|
+
base?: string;
|
|
818
|
+
remoteUrl?: string;
|
|
772
819
|
}
|
|
773
820
|
|
|
774
821
|
/** "Add this draft to the Shared version" (phase-29 / E4, Task 7): merge the draft
|
|
@@ -793,6 +840,29 @@ export async function gitFoldDraft(
|
|
|
793
840
|
if (!branches.some((b) => b.name === draftName))
|
|
794
841
|
return { ok: false, error: "That draft doesn't exist." };
|
|
795
842
|
|
|
843
|
+
// A GitHub remote → PR flow (DDR-162): publish the DRAFT branch (branch protection
|
|
844
|
+
// guards the Shared version, not the draft) and signal the endpoint to open a pull
|
|
845
|
+
// request draft→shared. We deliberately do NOT merge or push the Shared version here
|
|
846
|
+
// — pushing a protected `main` is exactly what GitHub forbids, and the reason the PR
|
|
847
|
+
// exists. The merge lands on GitHub after review.
|
|
848
|
+
const remoteUrl = await readRemoteUrl(dir, remote);
|
|
849
|
+
if (isGitHubRemote(remoteUrl)) {
|
|
850
|
+
const push = await gitPush(dir, token, { remote, ref: draftName });
|
|
851
|
+
if (!push.ok) {
|
|
852
|
+
if (push.authRequired) return { ok: false, authRequired: true, error: push.error };
|
|
853
|
+
if (push.conflict)
|
|
854
|
+
return {
|
|
855
|
+
ok: false,
|
|
856
|
+
conflict: true,
|
|
857
|
+
error: 'Your draft moved on the server — Get latest first, then add it.',
|
|
858
|
+
};
|
|
859
|
+
return { ok: false, error: push.error ?? 'Could not publish your draft.' };
|
|
860
|
+
}
|
|
861
|
+
return { ok: true, shared, prReady: true, head: draftName, base: shared, remoteUrl };
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// No remote (or a non-GitHub local remote) → merge the draft into the Shared version
|
|
865
|
+
// locally; there's no PR host. Unchanged pre-PR-flow behavior.
|
|
796
866
|
// Merge the draft into the Shared version (FF when possible, else a merge commit).
|
|
797
867
|
try {
|
|
798
868
|
if (USE_SYSTEM_GIT) {
|
|
@@ -866,11 +936,59 @@ export async function gitFoldDraft(
|
|
|
866
936
|
|
|
867
937
|
// ── push (Publish) ─────────────────────────────────────────────────────────
|
|
868
938
|
|
|
939
|
+
/** Publish / Get-latest transport routing — brings the network WRITE paths up to the
|
|
940
|
+
* same DDR-131/DDR-133 gate the network READ paths already enforce (gitFetchRemote
|
|
941
|
+
* `:1099`, remoteAheadBehind `:1628`). iso-git speaks HTTP(S) ONLY, so an ssh/git
|
|
942
|
+
* remote MUST run through the system binary (the "unrecognized transport protocol:
|
|
943
|
+
* ssh" bug was push/pull skipping this); a command-executing / non-github URL is
|
|
944
|
+
* REFUSED before any spawn; and the keychain PAT rides only a trusted-host HTTPS
|
|
945
|
+
* request. `none` (no remote configured) keeps the pre-gate routing so a local-only
|
|
946
|
+
* project's tokenless publish still short-circuits to "sign in" unchanged. */
|
|
947
|
+
type NetWriteRoute =
|
|
948
|
+
| { via: 'system'; tokenForSystem: string | undefined }
|
|
949
|
+
| { via: 'iso' }
|
|
950
|
+
| { via: 'legacy' }
|
|
951
|
+
| { via: 'authRequired' }
|
|
952
|
+
| { via: 'reject'; error: string };
|
|
953
|
+
|
|
954
|
+
async function resolveNetWriteRoute(
|
|
955
|
+
dir: string,
|
|
956
|
+
remote: string,
|
|
957
|
+
token: string | undefined
|
|
958
|
+
): Promise<NetWriteRoute> {
|
|
959
|
+
const url = await effectiveRemoteUrl(dir, remote); // post-insteadOf — the URL git dials
|
|
960
|
+
const transport = classifyRemoteUrl(url);
|
|
961
|
+
if (transport === 'none') return { via: 'legacy' };
|
|
962
|
+
// Command-executing transports (ext::/fd::/transport:: — the `::` helpers) are RCE:
|
|
963
|
+
// refuse BEFORE any spawn so neither engine ever resolves them. A plain file/local
|
|
964
|
+
// remote is NOT rejected here — it's a legitimate explicit local-repo transfer
|
|
965
|
+
// (handled by the system branch below), matching HARDENED_REMOTE_FLAGS' stance that
|
|
966
|
+
// only shell-spawning helpers are blocked, not file object transfer.
|
|
967
|
+
if (url.includes('::'))
|
|
968
|
+
return { via: 'reject', error: 'Maude can only sync github.com (HTTPS or SSH) projects.' };
|
|
969
|
+
const trustedHttp = transport === 'http' && isTrustedTokenHost(url);
|
|
970
|
+
if (transport === 'http' && !trustedHttp)
|
|
971
|
+
return { via: 'reject', error: 'Maude can only sync github.com projects.' };
|
|
972
|
+
// Tokenless github HTTPS with no system git to fall back on: iso can't authenticate
|
|
973
|
+
// → ask the user to sign in (mirrors gitFetchRemote `:1096`).
|
|
974
|
+
if (transport === 'http' && !token && !(await systemGitAvailable()))
|
|
975
|
+
return { via: 'authRequired' };
|
|
976
|
+
// System engine for: ssh (iso can't speak it), a file/local remote ('unsafe' minus
|
|
977
|
+
// the `::` helpers rejected above — e.g. a bare-repo path), or ANY remote once a git
|
|
978
|
+
// binary exists. The PAT rides only a trusted-host HTTPS request; ssh/local use the
|
|
979
|
+
// user's own key / on-disk path.
|
|
980
|
+
if ((await systemGitAvailable()) || transport === 'ssh' || transport === 'unsafe')
|
|
981
|
+
return { via: 'system', tokenForSystem: trustedHttp ? token : undefined };
|
|
982
|
+
return { via: 'iso' }; // github HTTPS + token, no system git present
|
|
983
|
+
}
|
|
984
|
+
|
|
869
985
|
/** Publish. `token` is optional in phase-27: the system-git engine falls back to
|
|
870
986
|
* the user's configured credential helper / SSH, so a developer-ish user who
|
|
871
987
|
* cloned with system git can publish today (no in-UI token). The iso-git default
|
|
872
988
|
* engine needs the token (no helper integration) → `authRequired` when absent,
|
|
873
|
-
* which the UI renders as "Sign in to publish" (phase-28 keychain fills it).
|
|
989
|
+
* which the UI renders as "Sign in to publish" (phase-28 keychain fills it).
|
|
990
|
+
* Engine choice goes through resolveNetWriteRoute so an ssh remote reaches the git
|
|
991
|
+
* binary instead of iso's HTTP-only transport (DDR-131/DDR-133 parity). */
|
|
874
992
|
export async function gitPush(
|
|
875
993
|
dir: string,
|
|
876
994
|
token: string | undefined,
|
|
@@ -879,9 +997,21 @@ export async function gitPush(
|
|
|
879
997
|
if (!isRepo(dir)) return { ok: false, error: 'This project is not versioned yet.' };
|
|
880
998
|
invalidateRemoteProbe(dir); // a publish changes ahead/behind — re-probe next status
|
|
881
999
|
const remote = opts.remote || 'origin';
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
:
|
|
1000
|
+
const route = await resolveNetWriteRoute(dir, remote, token);
|
|
1001
|
+
switch (route.via) {
|
|
1002
|
+
case 'reject':
|
|
1003
|
+
return { ok: false, error: route.error };
|
|
1004
|
+
case 'authRequired':
|
|
1005
|
+
return { ok: false, authRequired: true, error: 'Sign in to publish.' };
|
|
1006
|
+
case 'system':
|
|
1007
|
+
return pushSystem(dir, route.tokenForSystem, remote, opts.ref);
|
|
1008
|
+
case 'iso':
|
|
1009
|
+
return pushIso(dir, token, remote, opts.ref);
|
|
1010
|
+
case 'legacy':
|
|
1011
|
+
return USE_SYSTEM_GIT
|
|
1012
|
+
? pushSystem(dir, token, remote, opts.ref)
|
|
1013
|
+
: pushIso(dir, token, remote, opts.ref);
|
|
1014
|
+
}
|
|
885
1015
|
}
|
|
886
1016
|
|
|
887
1017
|
async function pushIso(
|
|
@@ -926,6 +1056,11 @@ async function pushIso(
|
|
|
926
1056
|
} catch (e) {
|
|
927
1057
|
const msg = errMsg(e);
|
|
928
1058
|
if (isNonFastForward(msg)) return { ok: false, conflict: true };
|
|
1059
|
+
if (isTransportError(msg))
|
|
1060
|
+
return {
|
|
1061
|
+
ok: false,
|
|
1062
|
+
error: 'Publishing needs the git command-line tool for this project’s connection.',
|
|
1063
|
+
};
|
|
929
1064
|
return { ok: false, error: msg };
|
|
930
1065
|
}
|
|
931
1066
|
}
|
|
@@ -944,9 +1079,15 @@ async function pushSystem(
|
|
|
944
1079
|
// disk). Without one, fall through to the user's configured credential helper /
|
|
945
1080
|
// SSH agent — the phase-27 "I cloned with system git" publish path.
|
|
946
1081
|
const args = tokenHeaderArgs(token);
|
|
947
|
-
args.push('push', remote, branch || 'HEAD');
|
|
1082
|
+
args.push(...HARDENED_REMOTE_FLAGS, 'push', remote, branch || 'HEAD');
|
|
948
1083
|
const r = await runGit(dir, args);
|
|
949
1084
|
if (r.code === 0) return { ok: true };
|
|
1085
|
+
// 127 = no git binary on PATH (the ssh-remote-but-no-CLI case; ssh always routes here).
|
|
1086
|
+
if (r.code === 127)
|
|
1087
|
+
return {
|
|
1088
|
+
ok: false,
|
|
1089
|
+
error: 'Publishing needs the git command-line tool for this project’s connection.',
|
|
1090
|
+
};
|
|
950
1091
|
if (isNonFastForward(`${r.stderr} ${r.stdout}`.toLowerCase()))
|
|
951
1092
|
return { ok: false, conflict: true };
|
|
952
1093
|
return { ok: false, error: r.stderr.trim() || 'Publish failed.' };
|
|
@@ -962,13 +1103,17 @@ function isSafeGitPositional(v: string): boolean {
|
|
|
962
1103
|
return SAFE_GIT_POSITIONAL.test(v);
|
|
963
1104
|
}
|
|
964
1105
|
|
|
965
|
-
/** Ephemeral `git -c http
|
|
966
|
-
* auth, or `[]` when no token (fall back to the user's credential helper). The
|
|
967
|
-
* header is per-invocation so the PAT never lands in the on-disk remote URL
|
|
1106
|
+
/** Ephemeral `git -c http.<github>.extraheader=…` args carrying a token as HTTPS
|
|
1107
|
+
* basic auth, or `[]` when no token (fall back to the user's credential helper). The
|
|
1108
|
+
* header is per-invocation so the PAT never lands in the on-disk remote URL — AND it
|
|
1109
|
+
* is SCOPED to `https://github.com/` (not the global `http.extraheader`): git attaches
|
|
1110
|
+
* it only to a request whose curl-RESOLVED host is github.com, so a poisoned remote
|
|
1111
|
+
* that WHATWG reads as github.com but curl dials elsewhere never receives the PAT
|
|
1112
|
+
* (F1 defense-in-depth, on top of the isTrustedTokenHost strict-validate). */
|
|
968
1113
|
function tokenHeaderArgs(token: string | undefined): string[] {
|
|
969
1114
|
if (!token) return [];
|
|
970
1115
|
const auth = Buffer.from(`x-access-token:${token}`).toString('base64');
|
|
971
|
-
return ['-c', `http.extraheader=Authorization: Basic ${auth}`];
|
|
1116
|
+
return ['-c', `http.https://github.com/.extraheader=Authorization: Basic ${auth}`];
|
|
972
1117
|
}
|
|
973
1118
|
|
|
974
1119
|
function isNonFastForward(blob: string): boolean {
|
|
@@ -981,6 +1126,15 @@ function isNonFastForward(blob: string): boolean {
|
|
|
981
1126
|
);
|
|
982
1127
|
}
|
|
983
1128
|
|
|
1129
|
+
/** iso-git speaks HTTP(S) only and throws "unrecognized transport protocol" on an
|
|
1130
|
+
* ssh/git remote. Routing now sends those to system git (resolveNetWriteRoute), so
|
|
1131
|
+
* this is a BELT: if a transport error ever still reaches an iso catch, map it to the
|
|
1132
|
+
* same "use the git CLI" copy gitFetchRemote shows (`:1153`) instead of leaking the
|
|
1133
|
+
* raw isomorphic-git string to the UI. */
|
|
1134
|
+
function isTransportError(blob: string): boolean {
|
|
1135
|
+
return /unrecognized transport|unsupported|protocol/i.test(blob);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
984
1138
|
// ── pull (Get latest) ─────────────────────────────────────────────────────
|
|
985
1139
|
|
|
986
1140
|
/** Get latest. Same optional-token model as gitPush (see its doc comment). */
|
|
@@ -991,9 +1145,22 @@ export async function gitPull(
|
|
|
991
1145
|
): Promise<GitPullResult> {
|
|
992
1146
|
if (!isRepo(dir)) return { ok: false, error: 'This project is not versioned yet.' };
|
|
993
1147
|
invalidateRemoteProbe(dir); // a pull changes ahead/behind — re-probe next status
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1148
|
+
const remote = opts.remote || 'origin';
|
|
1149
|
+
const route = await resolveNetWriteRoute(dir, remote, token);
|
|
1150
|
+
switch (route.via) {
|
|
1151
|
+
case 'reject':
|
|
1152
|
+
return { ok: false, error: route.error };
|
|
1153
|
+
case 'authRequired':
|
|
1154
|
+
return { ok: false, authRequired: true, error: 'Sign in to get the latest.' };
|
|
1155
|
+
case 'system':
|
|
1156
|
+
return pullSystem(dir, route.tokenForSystem, remote, opts.ref);
|
|
1157
|
+
case 'iso':
|
|
1158
|
+
return pullIso(dir, token, remote, opts.ref);
|
|
1159
|
+
case 'legacy':
|
|
1160
|
+
return USE_SYSTEM_GIT
|
|
1161
|
+
? pullSystem(dir, token, remote, opts.ref)
|
|
1162
|
+
: pullIso(dir, token, remote, opts.ref);
|
|
1163
|
+
}
|
|
997
1164
|
}
|
|
998
1165
|
|
|
999
1166
|
async function pullIso(
|
|
@@ -1022,7 +1189,13 @@ async function pullIso(
|
|
|
1022
1189
|
// `data` is the list of conflicted filepaths. DiffView opens on these.
|
|
1023
1190
|
const conflictFiles = mergeConflictFiles(e);
|
|
1024
1191
|
if (conflictFiles) return { ok: false, conflict: true, files: conflictFiles };
|
|
1025
|
-
|
|
1192
|
+
const msg = errMsg(e);
|
|
1193
|
+
if (isTransportError(msg))
|
|
1194
|
+
return {
|
|
1195
|
+
ok: false,
|
|
1196
|
+
error: 'Getting the latest needs the git command-line tool for this project’s connection.',
|
|
1197
|
+
};
|
|
1198
|
+
return { ok: false, error: msg };
|
|
1026
1199
|
}
|
|
1027
1200
|
}
|
|
1028
1201
|
|
|
@@ -1037,9 +1210,14 @@ async function pullSystem(
|
|
|
1037
1210
|
return { ok: false, error: 'Invalid remote or draft name.' };
|
|
1038
1211
|
}
|
|
1039
1212
|
const args = tokenHeaderArgs(token);
|
|
1040
|
-
args.push('pull', '--no-rebase', remote, branch || 'HEAD');
|
|
1213
|
+
args.push(...HARDENED_REMOTE_FLAGS, 'pull', '--no-rebase', remote, branch || 'HEAD');
|
|
1041
1214
|
const r = await runGit(dir, args);
|
|
1042
1215
|
if (r.code === 0) return { ok: true };
|
|
1216
|
+
if (r.code === 127)
|
|
1217
|
+
return {
|
|
1218
|
+
ok: false,
|
|
1219
|
+
error: 'Getting the latest needs the git command-line tool for this project’s connection.',
|
|
1220
|
+
};
|
|
1043
1221
|
const blob = `${r.stderr}\n${r.stdout}`;
|
|
1044
1222
|
if (/conflict/i.test(blob)) {
|
|
1045
1223
|
// Parse `CONFLICT (content): Merge conflict in <path>` lines.
|
|
@@ -1078,7 +1256,7 @@ export async function gitFetchRemote(
|
|
|
1078
1256
|
// binary — but a command-executing (`ext::`) / local (`file://`) URL must be
|
|
1079
1257
|
// REFUSED, never handed to `git fetch` (it would run as the user). And the GitHub
|
|
1080
1258
|
// token may only ride a github.com HTTPS request, never an attacker-chosen host.
|
|
1081
|
-
const url = await
|
|
1259
|
+
const url = await effectiveRemoteUrl(dir, remote); // post-insteadOf — the URL git dials
|
|
1082
1260
|
const transport = classifyRemoteUrl(url);
|
|
1083
1261
|
if (transport === 'none') return { ok: false, error: 'This project has no remote to refresh.' };
|
|
1084
1262
|
if (transport === 'unsafe')
|
|
@@ -1613,7 +1791,7 @@ async function remoteAheadBehindUncached(
|
|
|
1613
1791
|
// remote URL before touching the git binary. A command-executing (`ext::`) or
|
|
1614
1792
|
// local (`file://`) URL is refused (return 0/0, no spawn); the token rides only a
|
|
1615
1793
|
// github.com HTTPS request, never an attacker-chosen host.
|
|
1616
|
-
const url = await
|
|
1794
|
+
const url = await effectiveRemoteUrl(dir, remote); // post-insteadOf — the URL git dials
|
|
1617
1795
|
const transport = classifyRemoteUrl(url);
|
|
1618
1796
|
if (transport === 'none' || transport === 'unsafe') return { ahead: 0, behind: 0 };
|
|
1619
1797
|
const trustedHttp = transport === 'http' && isTrustedTokenHost(url);
|
|
@@ -164,6 +164,73 @@ export async function createRepo(token: string, input: CreateRepoInput): Promise
|
|
|
164
164
|
};
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
export interface PullRequest {
|
|
168
|
+
number: number;
|
|
169
|
+
html_url: string;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface CreatePullRequestInput {
|
|
173
|
+
/** The draft branch (same-repo head — a bare branch name). */
|
|
174
|
+
head: string;
|
|
175
|
+
/** The Shared-version branch the PR targets (main/master). */
|
|
176
|
+
base: string;
|
|
177
|
+
title: string;
|
|
178
|
+
body?: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Open a pull request `head → base` in the given repo. GitHub returns 422 both when
|
|
182
|
+
* a PR for this head/base already exists AND when there are no commits between them;
|
|
183
|
+
* we can't tell from the (friendly-mapped) message, so we probe for an already-open
|
|
184
|
+
* PR — if one exists, re-running "Add to Shared version" links it instead of erroring;
|
|
185
|
+
* otherwise it's a genuine no-op and we surface a friendly message. */
|
|
186
|
+
export async function createPullRequest(
|
|
187
|
+
token: string,
|
|
188
|
+
owner: string,
|
|
189
|
+
repo: string,
|
|
190
|
+
input: CreatePullRequestInput
|
|
191
|
+
): Promise<PullRequest> {
|
|
192
|
+
const path = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls`;
|
|
193
|
+
try {
|
|
194
|
+
const res = await api(token, path, {
|
|
195
|
+
method: 'POST',
|
|
196
|
+
body: { title: input.title, head: input.head, base: input.base, body: input.body ?? '' },
|
|
197
|
+
});
|
|
198
|
+
const r = (await res.json()) as { number: number; html_url: string };
|
|
199
|
+
return { number: r.number, html_url: r.html_url };
|
|
200
|
+
} catch (e) {
|
|
201
|
+
if (e instanceof GitHubApiError && e.status === 422) {
|
|
202
|
+
const existing = await findOpenPullRequest(token, owner, repo, input.head, input.base);
|
|
203
|
+
if (existing) return existing;
|
|
204
|
+
throw new GitHubApiError(
|
|
205
|
+
422,
|
|
206
|
+
'Nothing new to add — this draft already matches the Shared version.'
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
throw e;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** The open PR for `head → base`, if any (recovers a link from the 422 above). */
|
|
214
|
+
async function findOpenPullRequest(
|
|
215
|
+
token: string,
|
|
216
|
+
owner: string,
|
|
217
|
+
repo: string,
|
|
218
|
+
head: string,
|
|
219
|
+
base: string
|
|
220
|
+
): Promise<PullRequest | null> {
|
|
221
|
+
const q =
|
|
222
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls` +
|
|
223
|
+
`?state=open&head=${encodeURIComponent(`${owner}:${head}`)}&base=${encodeURIComponent(base)}`;
|
|
224
|
+
const res = await api(token, q).catch(() => null);
|
|
225
|
+
if (!res) return null;
|
|
226
|
+
const list = (await res.json().catch(() => null)) as Array<{
|
|
227
|
+
number: number;
|
|
228
|
+
html_url: string;
|
|
229
|
+
}> | null;
|
|
230
|
+
if (!Array.isArray(list) || list.length === 0) return null;
|
|
231
|
+
return { number: list[0].number, html_url: list[0].html_url };
|
|
232
|
+
}
|
|
233
|
+
|
|
167
234
|
/**
|
|
168
235
|
* Point the local project's `origin` at `remoteUrl` (iso-git, no network). Replaces
|
|
169
236
|
* any existing remote of the same name so re-running "create project" is idempotent.
|
package/apps/studio/handoff.ts
CHANGED
|
@@ -107,7 +107,8 @@ export interface EmitOptions {
|
|
|
107
107
|
// Strip data-cd-id from source — the inverse of canvas-pipeline.ts pass 1.
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
|
-
* Remove every ` data-cd-id="<hex>"`
|
|
110
|
+
* Remove every pipeline-emitted attribute — ` data-cd-id="<hex>"` and the
|
|
111
|
+
* Phase-6 ` data-cd-editable="text"` marker — from a TSX source string.
|
|
111
112
|
* Pure: caller persists. Uses the same oxc-parser + magic-string toolchain as
|
|
112
113
|
* the pipeline that emitted them.
|
|
113
114
|
*/
|
|
@@ -136,7 +137,7 @@ export function stripDataCdId(canvasAbsPath: string, source: string): string {
|
|
|
136
137
|
if (
|
|
137
138
|
a?.type === 'JSXAttribute' &&
|
|
138
139
|
a.name?.type === 'JSXIdentifier' &&
|
|
139
|
-
a.name.name === 'data-cd-id' &&
|
|
140
|
+
(a.name.name === 'data-cd-id' || a.name.name === 'data-cd-editable') &&
|
|
140
141
|
typeof a.start === 'number' &&
|
|
141
142
|
typeof a.end === 'number'
|
|
142
143
|
) {
|