@dfosco/storyboard 0.7.1 → 0.9.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/dist/storyboard-ui.css +1 -1
- package/dist/storyboard-ui.js +14128 -10953
- package/dist/storyboard-ui.js.map +1 -1
- package/dist/tailwind.css +1 -1
- package/dist/tiny-canvas.css +1 -1
- package/package.json +138 -128
- package/scaffold/AGENTS.md +9 -7
- package/scaffold/README.md +18 -0
- package/scaffold/deploy.yml +21 -0
- package/scaffold/gitignore +8 -14
- package/scaffold/preview.yml +40 -0
- package/scaffold/{manifest.json → scaffold.config.json} +73 -3
- package/scaffold/scripts/aggregate-artifacts.mjs +257 -0
- package/scaffold/skills/canvas/SKILL.md +1 -1
- package/scaffold/skills/migrate-0.5.0/SKILL.md +1 -1
- package/scaffold/skills/ship/SKILL.md +2 -2
- package/scaffold/skills/worktree/SKILL.md +1 -1
- package/scaffold/src/_prototype.jsx +17 -0
- package/scaffold/src/_story.jsx +13 -0
- package/scaffold/src/library/README.md +34 -0
- package/scaffold/src/library/_app.jsx +41 -0
- package/scaffold/src/library/_app.module.css +2 -0
- package/scaffold/src/library/create.jsx +10 -0
- package/scaffold/src/library/index.jsx +14 -0
- package/scaffold/src/library/mount.jsx +51 -0
- package/scaffold/src/library/prototypes-entry.jsx +57 -0
- package/scaffold/src/library/routes.jsx +205 -0
- package/scaffold/src/library/viewfinder.jsx +13 -0
- package/scaffold/src/library/workspace.jsx +17 -0
- package/scripts/aggregate-artifacts.mjs +257 -0
- package/src/canvas/style.css +16 -16
- package/src/core/artifact/operations.js +54 -15
- package/src/core/artifact/schemas/canvas.schema.json +7 -1
- package/src/core/autosync/server.test.js +1 -1
- package/src/core/canvas/hot-pool.js +21 -33
- package/src/core/canvas/materializer.js +39 -13
- package/src/core/canvas/selectedWidgets.js +1 -1
- package/src/core/canvas/server.js +376 -100
- package/src/core/canvas/server.test.js +472 -2
- package/src/core/canvas/terminal-server.js +144 -57
- package/src/core/canvas/undoRedo.js +174 -0
- package/src/core/canvas/undoRedo.test.js +250 -0
- package/src/core/cli/artifact.js +2 -0
- package/src/core/cli/buildManifest.js +34 -0
- package/src/core/cli/canvasRedo.js +44 -0
- package/src/core/cli/canvasUndo.js +40 -0
- package/src/core/cli/create.js +151 -23
- package/src/core/cli/dev.js +23 -35
- package/src/core/cli/index.js +21 -0
- package/src/core/cli/schemas.js +8 -4
- package/src/core/cli/serverUrl.js +24 -0
- package/src/core/cli/setup.js +14 -25
- package/src/core/cli/terminal-welcome.js +12 -6
- package/src/core/cli/updateVersion.js +42 -2
- package/src/core/cli/workleaf.js +140 -0
- package/src/core/comments/api.js +1 -1
- package/src/core/comments/queries.js +3 -0
- package/src/core/comments/ui/CommentWindow.jsx +3 -2
- package/src/core/comments/ui/comment-layout.css +14 -14
- package/src/core/data/artifactIndex.js +235 -0
- package/src/core/data/artifactIndex.test.js +203 -0
- package/src/core/data/artifactManifest.js +290 -0
- package/src/core/data/artifactManifest.test.js +207 -0
- package/src/core/data/viewfinder.js +47 -7
- package/src/core/index.js +24 -1
- package/src/core/inspector/highlighter.js +1 -1
- package/src/core/inspector/mouseMode.js +2 -2
- package/src/core/lib/components/ui/avatar/avatar-fallback.jsx +2 -2
- package/src/core/lib/components/ui/avatar/avatar-image.jsx +2 -2
- package/src/core/lib/components/ui/avatar/avatar.jsx +2 -2
- package/src/core/lib/components/ui/checkbox/checkbox.jsx +12 -9
- package/src/core/lib/components/ui/dialog/dialog-content.jsx +45 -31
- package/src/core/lib/components/ui/dialog/dialog-trigger.jsx +9 -5
- package/src/core/lib/components/ui/dialog/dialog.jsx +12 -3
- package/src/core/lib/components/ui/dialog/dialog.module.css +118 -0
- package/src/core/lib/components/ui/dialog/index.js +4 -34
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.jsx +3 -3
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.jsx +19 -9
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-content.jsx +8 -8
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.jsx +2 -2
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-group.jsx +2 -2
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-item.jsx +15 -4
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-portal.jsx +2 -2
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.jsx +2 -2
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.jsx +14 -7
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-separator.jsx +2 -2
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.jsx +8 -4
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.jsx +4 -4
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub.jsx +2 -2
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-trigger.jsx +8 -2
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu.jsx +2 -2
- package/src/core/lib/components/ui/label/label.jsx +1 -2
- package/src/core/lib/components/ui/panel/panel-close.jsx +7 -4
- package/src/core/lib/components/ui/panel/panel-content.jsx +7 -9
- package/src/core/lib/components/ui/panel/panel-title.jsx +3 -3
- package/src/core/lib/components/ui/panel/panel.jsx +11 -2
- package/src/core/lib/components/ui/separator/separator.jsx +6 -4
- package/src/core/lib/components/ui/tooltip/tooltip-content.jsx +7 -6
- package/src/core/lib/components/ui/tooltip/tooltip-trigger.jsx +13 -6
- package/src/core/lib/components/ui/tooltip/tooltip.jsx +5 -5
- package/src/core/modes/modes.css +4 -4
- package/src/core/mountStoryboardCore.js +287 -83
- package/src/core/mountStoryboardCore.test.js +198 -0
- package/src/core/rename-watcher/config.json +1 -1
- package/src/core/rename-watcher/watcher.js +330 -37
- package/src/core/rename-watcher/watcher.test.js +253 -0
- package/src/core/scaffold/fragments.js +287 -0
- package/src/core/scaffold/fragments.test.js +401 -0
- package/src/core/scaffold/index.js +291 -0
- package/src/core/scaffold/migrateLegacyGitignore.js +157 -0
- package/src/core/scaffold/scaffold-integration.test.js +247 -0
- package/src/core/scaffold.js +4 -97
- package/src/core/stores/canvasInteractionStore.js +120 -0
- package/src/core/stores/canvasInteractionStore.test.js +126 -0
- package/src/core/stores/configSchema.js +64 -8
- package/src/core/stores/configSchema.test.js +3 -0
- package/src/core/stores/customerModeConfig.js +24 -3
- package/src/core/stores/customerModeConfig.test.js +46 -0
- package/src/core/stores/presentationStore.js +118 -0
- package/src/core/stores/presentationStore.test.js +114 -0
- package/src/core/stores/recentArtifacts.js +49 -2
- package/src/core/stores/recentArtifacts.test.js +59 -6
- package/src/core/stores/themeBootstrap.js +264 -0
- package/src/core/stores/themeBootstrap.test.js +211 -0
- package/src/core/stores/themeStore.test.js +154 -0
- package/src/core/stores/themeStore.ts +235 -220
- package/src/core/stores/uiConfig.js +10 -2
- package/src/core/styles/generateThemesCss.js +63 -0
- package/src/core/styles/tailwind.css +150 -94
- package/src/core/styles/workspace-chrome.css +13 -0
- package/src/core/tools/handlers/paletteTheme.js +11 -8
- package/src/core/ui/ActionMenuButton.jsx +2 -1
- package/src/core/ui/AutosyncMenuButton.css +7 -7
- package/src/core/ui/BranchSelect.module.css +5 -5
- package/src/core/ui/CanvasCreateMenu.jsx +1 -19
- package/src/core/ui/ChromeSlot.jsx +92 -0
- package/src/core/ui/ChromeSlot.test.jsx +140 -0
- package/src/core/ui/CoreUIBar.css +5 -5
- package/src/core/ui/CoreUIBar.jsx +170 -142
- package/src/core/ui/InspectorPanel.css +5 -5
- package/src/core/ui/InspectorPanel.jsx +9 -9
- package/src/core/ui/PwaInstallBanner.css +5 -5
- package/src/core/ui/PwaInstallBanner.jsx +5 -5
- package/src/core/ui/ThemeMenuButton.jsx +45 -30
- package/src/core/ui/core-ui-colors.css +28 -66
- package/src/core/ui/sidepanel.css +12 -12
- package/src/core/vite/server-plugin.js +13 -0
- package/src/core/workleaf/cleanup.js +102 -0
- package/src/core/workleaf/create.js +199 -0
- package/src/core/workleaf/delete.js +101 -0
- package/src/core/workleaf/list.js +57 -0
- package/src/core/workleaf/merge.js +238 -0
- package/src/core/workleaf/paths.js +146 -0
- package/src/core/workleaf/paths.test.js +230 -0
- package/src/core/workshop/features/createCanvas/CreateCanvasForm.jsx +130 -34
- package/src/core/workshop/features/createFlow/server.js +5 -1
- package/src/core/workshop/features/createPrototype/server.js +5 -1
- package/src/core/workshop/features/templateIndex.js +28 -1
- package/src/core/worktree/port.js +48 -5
- package/src/core/worktree/port.test.js +42 -0
- package/src/internals/ArtifactForm/ArtifactForm.jsx +173 -112
- package/src/internals/ArtifactForm/ArtifactForm.module.css +51 -69
- package/src/internals/ArtifactForm/artifactSchemas.js +40 -5
- package/src/internals/AuthModal/AuthModal.module.css +34 -34
- package/src/internals/BaseUiForm/BaseUiForm.jsx +293 -0
- package/src/internals/BaseUiForm/BaseUiForm.module.css +542 -0
- package/src/internals/BranchBar/BranchBar.module.css +4 -4
- package/src/internals/BranchesDropdown/BranchesDropdown.jsx +69 -0
- package/src/internals/BranchesDropdown/BranchesDropdown.module.css +84 -0
- package/src/internals/CommandPalette/CommandPalette.jsx +41 -29
- package/src/internals/CommandPalette/CreateArtifactForm.jsx +150 -0
- package/src/internals/CommandPalette/CreateDialog.jsx +20 -110
- package/src/internals/CommandPalette/WidgetArtifactDialog.css +16 -16
- package/src/internals/CommandPalette/WidgetArtifactDialog.jsx +1 -1
- package/src/internals/CommandPalette/command-palette.css +14 -14
- package/src/internals/CommandPalette/createHelpers.js +30 -0
- package/src/internals/CreatePage/CreatePage.jsx +57 -0
- package/src/internals/CreatePage/CreatePage.module.css +34 -0
- package/src/internals/Dialog.jsx +51 -0
- package/src/internals/FlowError.module.css +5 -5
- package/src/internals/PrototypeErrorBoundary.module.css +15 -15
- package/src/internals/SimpleWorkspace/SimpleWorkspace.jsx +534 -0
- package/src/internals/SimpleWorkspace/SimpleWorkspace.module.css +323 -0
- package/src/internals/Tooltip.jsx +39 -0
- package/src/internals/Viewfinder.jsx +229 -64
- package/src/internals/Viewfinder.module.css +253 -182
- package/src/internals/canvas/CanvasControls.module.css +11 -11
- package/src/internals/canvas/CanvasPage.bridge.test.jsx +11 -37
- package/src/internals/canvas/CanvasPage.jsx +358 -217
- package/src/internals/canvas/CanvasPage.module.css +20 -19
- package/src/internals/canvas/CanvasPage.multiselect.test.jsx +39 -26
- package/src/internals/canvas/CanvasToolbar.module.css +11 -11
- package/src/internals/canvas/ConnectorLayer.jsx +39 -2
- package/src/internals/canvas/ConnectorLayer.module.css +10 -10
- package/src/internals/canvas/PageSelector.jsx +15 -1
- package/src/internals/canvas/PageSelector.module.css +41 -27
- package/src/internals/canvas/canvasApi.js +40 -5
- package/src/internals/canvas/canvasTheme.js +22 -111
- package/src/internals/canvas/isolation/createIsolationMiddleware.js +68 -0
- package/src/internals/canvas/isolation/mountIsolationApp.jsx +88 -0
- package/src/internals/canvas/isolation/prototypeRoutes.jsx +140 -0
- package/src/internals/canvas/isolation/prototypesEntry.jsx +28 -0
- package/src/internals/canvas/isolation/storiesEntry.jsx +20 -0
- package/src/internals/canvas/isolation/storyRoutes.jsx +55 -0
- package/src/internals/canvas/useUndoRedo.js +75 -60
- package/src/internals/canvas/useUndoRedo.test.js +46 -178
- package/src/internals/canvas/widgets/CodePenEmbed.jsx +16 -36
- package/src/internals/canvas/widgets/CodePenEmbed.module.css +12 -12
- package/src/internals/canvas/widgets/CropOverlay.module.css +23 -23
- package/src/internals/canvas/widgets/ExpandedPane.module.css +8 -8
- package/src/internals/canvas/widgets/ExpandedPaneTopBar.jsx +1 -1
- package/src/internals/canvas/widgets/FigmaEmbed.jsx +15 -36
- package/src/internals/canvas/widgets/FigmaEmbed.module.css +15 -15
- package/src/internals/canvas/widgets/FrozenTerminalOverlay.jsx +30 -21
- package/src/internals/canvas/widgets/FrozenTerminalOverlay.module.css +5 -5
- package/src/internals/canvas/widgets/ImageWidget.jsx +7 -2
- package/src/internals/canvas/widgets/ImageWidget.module.css +3 -3
- package/src/internals/canvas/widgets/LinkPreview.module.css +79 -80
- package/src/internals/canvas/widgets/MarkdownBlock.module.css +31 -31
- package/src/internals/canvas/widgets/PromptWidget.jsx +2 -16
- package/src/internals/canvas/widgets/PromptWidget.module.css +33 -33
- package/src/internals/canvas/widgets/PrototypeEmbed.jsx +92 -17
- package/src/internals/canvas/widgets/PrototypeEmbed.module.css +58 -56
- package/src/internals/canvas/widgets/PrototypeEmbed.test.jsx +118 -4
- package/src/internals/canvas/widgets/ResizeHandle.module.css +4 -4
- package/src/internals/canvas/widgets/StorySetWidget.jsx +53 -25
- package/src/internals/canvas/widgets/StoryWidget.jsx +71 -56
- package/src/internals/canvas/widgets/StoryWidget.module.css +22 -20
- package/src/internals/canvas/widgets/TerminalReadWidget.jsx +13 -15
- package/src/internals/canvas/widgets/TerminalReadWidget.module.css +4 -4
- package/src/internals/canvas/widgets/TerminalWidget.jsx +56 -45
- package/src/internals/canvas/widgets/TerminalWidget.module.css +10 -10
- package/src/internals/canvas/widgets/TilesWidget.module.css +17 -17
- package/src/internals/canvas/widgets/WidgetChrome.jsx +1 -1
- package/src/internals/canvas/widgets/WidgetChrome.module.css +76 -61
- package/src/internals/canvas/widgets/WidgetWrapper.module.css +2 -2
- package/src/internals/canvas/widgets/embedOverlay.module.css +14 -4
- package/src/internals/canvas/widgets/embedTheme.js +12 -104
- package/src/internals/canvas/widgets/expandUtils.js +19 -19
- package/src/internals/canvas/widgets/normalizeLegacyEmbedSrc.js +74 -0
- package/src/internals/canvas/widgets/widgetConfig.js +5 -5
- package/src/internals/context.jsx +22 -1
- package/src/internals/context.test.jsx +47 -0
- package/src/internals/hooks/useArtifactIndex.js +31 -0
- package/src/internals/hooks/useArtifactIndex.test.js +45 -0
- package/src/internals/hooks/usePrototypeReloadGuard.js +14 -8
- package/src/internals/index.js +6 -0
- package/src/internals/loadConsumerWrapper.js +60 -0
- package/src/internals/story/ComponentSetPage.jsx +75 -34
- package/src/internals/story/ComponentSetPage.module.css +14 -14
- package/src/internals/story/StoryPage.jsx +16 -25
- package/src/internals/story/StoryPage.module.css +2 -2
- package/src/internals/vite/data-plugin.js +317 -149
- package/src/internals/vite/data-plugin.test.js +11 -11
- package/{scaffold/storyboard.config.json → storyboard.config.json} +25 -0
- package/terminal.config.json +1 -1
- package/src/core/lib/components/ui/card/card-action.jsx +0 -11
- package/src/core/lib/components/ui/card/card-content.jsx +0 -11
- package/src/core/lib/components/ui/card/card-description.jsx +0 -11
- package/src/core/lib/components/ui/card/card-footer.jsx +0 -11
- package/src/core/lib/components/ui/card/card-header.jsx +0 -19
- package/src/core/lib/components/ui/card/card-title.jsx +0 -11
- package/src/core/lib/components/ui/card/card.jsx +0 -17
- package/src/core/lib/components/ui/card/index.js +0 -25
- package/src/core/lib/components/ui/collapsible/collapsible-content.jsx +0 -7
- package/src/core/lib/components/ui/collapsible/collapsible-trigger.jsx +0 -7
- package/src/core/lib/components/ui/collapsible/collapsible.jsx +0 -7
- package/src/core/lib/components/ui/collapsible/index.js +0 -13
- package/src/core/lib/components/ui/dialog/dialog-close.jsx +0 -7
- package/src/core/lib/components/ui/dialog/dialog-description.jsx +0 -15
- package/src/core/lib/components/ui/dialog/dialog-footer.jsx +0 -23
- package/src/core/lib/components/ui/dialog/dialog-header.jsx +0 -11
- package/src/core/lib/components/ui/dialog/dialog-overlay.jsx +0 -15
- package/src/core/lib/components/ui/dialog/dialog-portal.jsx +0 -4
- package/src/core/lib/components/ui/dialog/dialog-title.jsx +0 -15
- package/src/core/lib/components/ui/popover/index.js +0 -28
- package/src/core/lib/components/ui/popover/popover-close.jsx +0 -7
- package/src/core/lib/components/ui/popover/popover-content.jsx +0 -22
- package/src/core/lib/components/ui/popover/popover-description.jsx +0 -11
- package/src/core/lib/components/ui/popover/popover-header.jsx +0 -11
- package/src/core/lib/components/ui/popover/popover-portal.jsx +0 -4
- package/src/core/lib/components/ui/popover/popover-title.jsx +0 -11
- package/src/core/lib/components/ui/popover/popover-trigger.jsx +0 -8
- package/src/core/lib/components/ui/popover/popover.jsx +0 -4
- package/src/core/lib/components/ui/select/index.js +0 -37
- package/src/core/lib/components/ui/select/select-content.jsx +0 -30
- package/src/core/lib/components/ui/select/select-group-heading.jsx +0 -17
- package/src/core/lib/components/ui/select/select-group.jsx +0 -15
- package/src/core/lib/components/ui/select/select-item.jsx +0 -26
- package/src/core/lib/components/ui/select/select-label.jsx +0 -11
- package/src/core/lib/components/ui/select/select-portal.jsx +0 -4
- package/src/core/lib/components/ui/select/select-scroll-down-button.jsx +0 -18
- package/src/core/lib/components/ui/select/select-scroll-up-button.jsx +0 -18
- package/src/core/lib/components/ui/select/select-separator.jsx +0 -15
- package/src/core/lib/components/ui/select/select-trigger.jsx +0 -25
- package/src/core/lib/components/ui/select/select.jsx +0 -4
- package/src/core/lib/components/ui/sheet/index.js +0 -34
- package/src/core/lib/components/ui/sheet/sheet-close.jsx +0 -7
- package/src/core/lib/components/ui/sheet/sheet-content.jsx +0 -35
- package/src/core/lib/components/ui/sheet/sheet-description.jsx +0 -15
- package/src/core/lib/components/ui/sheet/sheet-footer.jsx +0 -11
- package/src/core/lib/components/ui/sheet/sheet-header.jsx +0 -11
- package/src/core/lib/components/ui/sheet/sheet-overlay.jsx +0 -15
- package/src/core/lib/components/ui/sheet/sheet-portal.jsx +0 -4
- package/src/core/lib/components/ui/sheet/sheet-title.jsx +0 -15
- package/src/core/lib/components/ui/sheet/sheet-trigger.jsx +0 -7
- package/src/core/lib/components/ui/sheet/sheet.jsx +0 -4
- package/src/core/lib/components/ui/toggle/index.js +0 -8
- package/src/core/lib/components/ui/toggle/toggle.jsx +0 -36
- package/src/core/lib/components/ui/toggle-group/index.js +0 -10
- package/src/core/lib/components/ui/toggle-group/toggle-group-item.jsx +0 -29
- package/src/core/lib/components/ui/toggle-group/toggle-group.jsx +0 -43
- package/src/internals/canvas/StoryErrorBoundary.jsx +0 -47
- package/src/internals/canvas/WebGLContextPool.jsx +0 -291
- package/src/internals/canvas/WebGLContextPool.test.jsx +0 -165
- package/src/internals/canvas/componentIsolate.jsx +0 -144
- package/src/internals/canvas/componentSetIsolate.jsx +0 -300
- package/src/internals/canvas/widgets/ComponentSetWidget.module.css +0 -89
- package/src/internals/canvas/widgets/InlineStoryRenderer.jsx +0 -129
- package/src/internals/canvas/widgets/InlineStoryRenderer.test.jsx +0 -68
- package/src/internals/canvas/widgets/StorySetWidget.module.css +0 -89
- package/src/primer/ThemeSync.jsx +0 -73
- package/src/primer/index.js +0 -1
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a new workleaf.
|
|
3
|
+
*
|
|
4
|
+
* A workleaf is an ephemeral git worktree owned by a parent worktree
|
|
5
|
+
* (or main). The full lifecycle:
|
|
6
|
+
*
|
|
7
|
+
* 1. Refuse if the current cwd is already inside a workleaf
|
|
8
|
+
* (workleaves cannot be nested).
|
|
9
|
+
* 2. Derive the parent worktree name from cwd.
|
|
10
|
+
* 3. `git worktree add <dir> -b <branch> <parent-branch>` so the
|
|
11
|
+
* workleaf branches off the parent's CURRENT HEAD.
|
|
12
|
+
* 4. Run `npm install` so the workleaf has a working dependency tree.
|
|
13
|
+
* 5. Allocate a stable port via the shared worktrees/ports.json.
|
|
14
|
+
* 6. Spawn `storyboard dev` as a fully detached child process so the
|
|
15
|
+
* Vite dev server stays alive after this CLI exits.
|
|
16
|
+
* 7. Poll the server registry for self-registration and print the URL.
|
|
17
|
+
*
|
|
18
|
+
* The detached server registers itself in .storyboard/servers.json on
|
|
19
|
+
* startup, which is how `workleaf merge` / `workleaf delete` later
|
|
20
|
+
* locates and stops it.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import * as p from '@clack/prompts'
|
|
24
|
+
import { execFileSync, spawn } from 'node:child_process'
|
|
25
|
+
import { mkdirSync, existsSync } from 'node:fs'
|
|
26
|
+
import { dirname, resolve } from 'node:path'
|
|
27
|
+
import { detectWorktreeName, repoRoot, worktreeDir, getPort, slugify } from '../worktree/port.js'
|
|
28
|
+
import { findByWorktree } from '../worktree/serverRegistry.js'
|
|
29
|
+
import { detectWorkleafContext, workleafDir, workleafBranchName, WORKLEAF_PREFIX } from './paths.js'
|
|
30
|
+
import { dim, bold, green } from '../cli/intro.js'
|
|
31
|
+
import { parseFlags } from '../cli/flags.js'
|
|
32
|
+
|
|
33
|
+
const flagSchema = {
|
|
34
|
+
parent: { type: 'string', description: 'Override parent worktree (default: current)' },
|
|
35
|
+
'no-dev': { type: 'boolean', default: false, description: 'Skip starting the dev server' },
|
|
36
|
+
'no-install': { type: 'boolean', default: false, description: 'Skip npm install (use parent node_modules at your own risk)' },
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Validate that `name` is a usable workleaf identifier.
|
|
41
|
+
*/
|
|
42
|
+
function validateName(name) {
|
|
43
|
+
if (!name || typeof name !== 'string') return 'A workleaf name is required'
|
|
44
|
+
const trimmed = name.trim()
|
|
45
|
+
if (trimmed.length === 0) return 'Workleaf name cannot be empty'
|
|
46
|
+
if (trimmed.startsWith(WORKLEAF_PREFIX)) {
|
|
47
|
+
return `Don't prefix with "${WORKLEAF_PREFIX}" — it's added automatically`
|
|
48
|
+
}
|
|
49
|
+
if (/[\s~^:\\]/.test(trimmed)) return 'Workleaf name cannot contain spaces or ~ ^ : \\'
|
|
50
|
+
if (trimmed.startsWith('-')) return 'Workleaf name cannot start with "-"'
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the parent worktree's on-disk path. Errors out if it's gone.
|
|
56
|
+
*/
|
|
57
|
+
function resolveParentDir(parent, root) {
|
|
58
|
+
if (parent === 'main') return root
|
|
59
|
+
const dir = worktreeDir(parent, root)
|
|
60
|
+
if (!existsSync(resolve(dir, '.git'))) {
|
|
61
|
+
p.log.error(`Parent worktree "${parent}" not found at ${dim(dir)}`)
|
|
62
|
+
p.log.info(`Create it first with ${green('npx storyboard branch ' + parent)}`)
|
|
63
|
+
process.exit(1)
|
|
64
|
+
}
|
|
65
|
+
return dir
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Public entry — runnable as a subcommand or imported.
|
|
70
|
+
*/
|
|
71
|
+
export async function runCreate(argv = process.argv.slice(4)) {
|
|
72
|
+
const { flags, positional } = parseFlags(argv, flagSchema)
|
|
73
|
+
const rawName = positional[0]
|
|
74
|
+
|
|
75
|
+
const nameError = validateName(rawName)
|
|
76
|
+
if (nameError) {
|
|
77
|
+
p.log.error(nameError)
|
|
78
|
+
p.log.info(`Usage: ${green('npx storyboard workleaf create <name>')}`)
|
|
79
|
+
process.exit(1)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const slug = slugify(rawName.trim())
|
|
83
|
+
|
|
84
|
+
// 1. Nesting check — refuse if cwd is already a workleaf
|
|
85
|
+
const ctx = detectWorkleafContext()
|
|
86
|
+
if (ctx.inWorkleaf) {
|
|
87
|
+
p.log.error('Workleaves cannot be nested.')
|
|
88
|
+
p.log.info(`You're inside ${bold(ctx.branch)} — cd to its parent worktree first.`)
|
|
89
|
+
process.exit(1)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 2. Derive parent from cwd (or explicit --parent)
|
|
93
|
+
const parent = flags.parent || detectWorktreeName()
|
|
94
|
+
const root = repoRoot()
|
|
95
|
+
const parentDir = resolveParentDir(parent, root)
|
|
96
|
+
const parentBranch = currentBranch(parentDir)
|
|
97
|
+
|
|
98
|
+
const dir = workleafDir(parent, slug)
|
|
99
|
+
const branch = workleafBranchName(parent, slug)
|
|
100
|
+
|
|
101
|
+
// Refuse if the workleaf already exists
|
|
102
|
+
if (existsSync(resolve(dir, '.git'))) {
|
|
103
|
+
p.log.error(`Workleaf already exists at ${dim(dir)}`)
|
|
104
|
+
p.log.info(`Open it with ${green(`cd ${dir}`)}`)
|
|
105
|
+
process.exit(1)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
p.intro('storyboard workleaf create')
|
|
109
|
+
p.log.info(`${dim('parent:')} ${bold(parent)} ${dim('(branch:')} ${parentBranch}${dim(')')}`)
|
|
110
|
+
p.log.info(`${dim('branch:')} ${bold(branch)}`)
|
|
111
|
+
p.log.info(`${dim('dir:')} ${dim(dir.replace(root + '/', ''))}`)
|
|
112
|
+
|
|
113
|
+
// 3. Create the worktree
|
|
114
|
+
mkdirSync(dirname(dir), { recursive: true })
|
|
115
|
+
|
|
116
|
+
const spin = p.spinner()
|
|
117
|
+
spin.start(`Creating worktree`)
|
|
118
|
+
try {
|
|
119
|
+
execFileSync(
|
|
120
|
+
'git',
|
|
121
|
+
['worktree', 'add', dir, '-b', branch, parentBranch],
|
|
122
|
+
{ cwd: root, stdio: 'pipe' }
|
|
123
|
+
)
|
|
124
|
+
spin.stop(`Worktree created`)
|
|
125
|
+
} catch (err) {
|
|
126
|
+
spin.stop('Failed to create worktree')
|
|
127
|
+
p.log.error(err.message || 'git worktree add failed')
|
|
128
|
+
process.exit(1)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 4. npm install (skippable)
|
|
132
|
+
if (!flags['no-install']) {
|
|
133
|
+
try {
|
|
134
|
+
spin.start('Installing dependencies (this is the slow part)')
|
|
135
|
+
const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
|
136
|
+
execFileSync(npmBin, ['install'], { cwd: dir, stdio: 'pipe' })
|
|
137
|
+
spin.stop('Dependencies installed')
|
|
138
|
+
} catch {
|
|
139
|
+
spin.stop('npm install failed — run it manually inside the workleaf')
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 5. Allocate the port (persisted in shared worktrees/ports.json)
|
|
144
|
+
const port = getPort(branch)
|
|
145
|
+
p.log.info(`${dim('port:')} ${bold(port)}`)
|
|
146
|
+
|
|
147
|
+
// 6. Spawn detached dev server
|
|
148
|
+
if (flags['no-dev']) {
|
|
149
|
+
p.outro(`Workleaf ready at ${green(dir.replace(root + '/', ''))} — start dev with ${green('npx storyboard dev')}`)
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
spin.start('Spawning dev server')
|
|
154
|
+
const npmBin = process.platform === 'win32' ? 'npx.cmd' : 'npx'
|
|
155
|
+
const child = spawn(npmBin, ['storyboard', 'dev'], {
|
|
156
|
+
cwd: dir,
|
|
157
|
+
detached: true,
|
|
158
|
+
stdio: 'ignore',
|
|
159
|
+
env: { ...process.env, STORYBOARD_NO_BUDDY: '1' },
|
|
160
|
+
})
|
|
161
|
+
child.unref()
|
|
162
|
+
|
|
163
|
+
// 7. Poll registry for self-registration
|
|
164
|
+
const start = Date.now()
|
|
165
|
+
let entry = null
|
|
166
|
+
while (Date.now() - start < 30000) {
|
|
167
|
+
await new Promise((r) => setTimeout(r, 500))
|
|
168
|
+
const matches = findByWorktree(branch)
|
|
169
|
+
if (matches.length > 0) { entry = matches[matches.length - 1]; break }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (entry) {
|
|
173
|
+
spin.stop(`Dev server ready`)
|
|
174
|
+
const url = `http://localhost:${entry.port}/storyboard/`
|
|
175
|
+
p.note(
|
|
176
|
+
[
|
|
177
|
+
` ${green(url)}`,
|
|
178
|
+
'',
|
|
179
|
+
` ${dim('cd into it:')} ${green(`cd ${dir.replace(root + '/', '')}`)}`,
|
|
180
|
+
` ${dim('stop it:')} ${green(`npx storyboard workleaf delete ${slug}`)}`,
|
|
181
|
+
` ${dim('merge & clean:')} ${green(`npx storyboard workleaf merge ${slug}`)}`,
|
|
182
|
+
].join('\n'),
|
|
183
|
+
`${bold(branch)} is alive`
|
|
184
|
+
)
|
|
185
|
+
} else {
|
|
186
|
+
spin.stop('Spawned but did not self-register within 30s')
|
|
187
|
+
p.log.warn(`Check the workleaf manually: ${dim(`cd ${dir} && npx storyboard dev`)}`)
|
|
188
|
+
}
|
|
189
|
+
p.outro('')
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Read the current branch of a directory. */
|
|
193
|
+
function currentBranch(cwd) {
|
|
194
|
+
try {
|
|
195
|
+
return execFileSync('git', ['branch', '--show-current'], { cwd, encoding: 'utf8' }).trim()
|
|
196
|
+
} catch {
|
|
197
|
+
return 'HEAD'
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delete a workleaf without merging.
|
|
3
|
+
*
|
|
4
|
+
* Identical to `workleaf merge` minus the squash + commit steps. Used
|
|
5
|
+
* to abandon a workleaf entirely — useful when an experiment didn't
|
|
6
|
+
* pan out or when you want to free a port without preserving the work.
|
|
7
|
+
*
|
|
8
|
+
* Prompts for confirmation unless --yes is passed.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as p from '@clack/prompts'
|
|
12
|
+
import { existsSync } from 'node:fs'
|
|
13
|
+
import { resolve } from 'node:path'
|
|
14
|
+
import { repoRoot } from '../worktree/port.js'
|
|
15
|
+
import { detectWorkleafContext, workleafDir, listWorkleafs, parentOfWorkleaf } from './paths.js'
|
|
16
|
+
import { cleanupWorkleaf, hasDirtyWorkingTree } from './cleanup.js'
|
|
17
|
+
import { dim, bold, green, yellow } from '../cli/intro.js'
|
|
18
|
+
import { parseFlags } from '../cli/flags.js'
|
|
19
|
+
|
|
20
|
+
const flagSchema = {
|
|
21
|
+
yes: { type: 'boolean', default: false, description: 'Skip interactive confirmation' },
|
|
22
|
+
force: { type: 'boolean', default: false, description: 'Delete even if the workleaf has uncommitted changes' },
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveTarget(positionalName, root) {
|
|
26
|
+
if (positionalName) {
|
|
27
|
+
const branchInfo = parentOfWorkleaf(positionalName)
|
|
28
|
+
if (branchInfo) {
|
|
29
|
+
const dir = workleafDir(branchInfo.parent, branchInfo.slug, root)
|
|
30
|
+
return { parent: branchInfo.parent, slug: branchInfo.slug, branch: positionalName, dir, root }
|
|
31
|
+
}
|
|
32
|
+
const matches = listWorkleafs(root).filter((w) => w.slug === positionalName)
|
|
33
|
+
if (matches.length === 0) {
|
|
34
|
+
p.log.error(`No workleaf named "${positionalName}" found.`)
|
|
35
|
+
process.exit(1)
|
|
36
|
+
}
|
|
37
|
+
if (matches.length > 1) {
|
|
38
|
+
p.log.error(`Multiple workleafs match "${positionalName}":`)
|
|
39
|
+
for (const m of matches) console.log(` ${m.branch}`)
|
|
40
|
+
process.exit(1)
|
|
41
|
+
}
|
|
42
|
+
const m = matches[0]
|
|
43
|
+
return { parent: m.parent, slug: m.slug, branch: m.branch, dir: m.dir, root }
|
|
44
|
+
}
|
|
45
|
+
const ctx = detectWorkleafContext()
|
|
46
|
+
if (!ctx.inWorkleaf) {
|
|
47
|
+
p.log.error('Not inside a workleaf — pass a name or cd into one.')
|
|
48
|
+
p.log.info(`Usage: ${green('npx storyboard workleaf delete <name>')}`)
|
|
49
|
+
process.exit(1)
|
|
50
|
+
}
|
|
51
|
+
return { parent: ctx.parent, slug: ctx.slug, branch: ctx.branch, dir: ctx.dir, root }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function runDelete(argv = process.argv.slice(4)) {
|
|
55
|
+
const { flags, positional } = parseFlags(argv, flagSchema)
|
|
56
|
+
const root = repoRoot()
|
|
57
|
+
const target = resolveTarget(positional[0], root)
|
|
58
|
+
const { branch, dir } = target
|
|
59
|
+
|
|
60
|
+
p.intro('storyboard workleaf delete')
|
|
61
|
+
p.log.info(`${dim('workleaf:')} ${bold(branch)}`)
|
|
62
|
+
|
|
63
|
+
if (!existsSync(resolve(dir, '.git'))) {
|
|
64
|
+
p.log.warn(`Workleaf directory missing: ${dim(dir)} — cleaning up registry anyway.`)
|
|
65
|
+
} else if (hasDirtyWorkingTree(dir) && !flags.force) {
|
|
66
|
+
p.log.error(`Workleaf has uncommitted changes — pass ${green('--force')} to discard them.`)
|
|
67
|
+
process.exit(1)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!flags.yes) {
|
|
71
|
+
const confirm = await p.confirm({
|
|
72
|
+
message: `Permanently delete ${yellow(branch)}? This discards any unmerged commits.`,
|
|
73
|
+
initialValue: false,
|
|
74
|
+
})
|
|
75
|
+
if (p.isCancel(confirm) || !confirm) {
|
|
76
|
+
p.cancel('Cancelled')
|
|
77
|
+
process.exit(0)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// chdir out of the workleaf BEFORE cleanup so helpers that call
|
|
82
|
+
// realpathSync(cwd) don't ENOENT when the dir is gone.
|
|
83
|
+
try {
|
|
84
|
+
const safeParent = target.parent === 'main' ? root : resolve(root, 'worktrees', target.parent)
|
|
85
|
+
if (existsSync(safeParent)) process.chdir(safeParent)
|
|
86
|
+
else process.chdir(root)
|
|
87
|
+
} catch { /* cwd will stay where it is */ }
|
|
88
|
+
|
|
89
|
+
p.log.step('Cleaning up workleaf')
|
|
90
|
+
const { stopped } = cleanupWorkleaf({ branch, dir, root })
|
|
91
|
+
|
|
92
|
+
p.note(
|
|
93
|
+
[
|
|
94
|
+
` ${green('✓')} stopped ${stopped} dev server${stopped === 1 ? '' : 's'}`,
|
|
95
|
+
` ${green('✓')} removed ${dim(dir.replace(root + '/', ''))}`,
|
|
96
|
+
` ${green('✓')} deleted branch ${dim(branch)}`,
|
|
97
|
+
].join('\n'),
|
|
98
|
+
`${bold(branch)} deleted`
|
|
99
|
+
)
|
|
100
|
+
p.outro('')
|
|
101
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List workleafs across the repo, grouped by parent, with live server info.
|
|
3
|
+
*
|
|
4
|
+
* Output shape:
|
|
5
|
+
*
|
|
6
|
+
* storyboard workleaf list
|
|
7
|
+
*
|
|
8
|
+
* main
|
|
9
|
+
* ● workleaf--quick-fix :1235 http://localhost:1235/storyboard/ (pid 12345)
|
|
10
|
+
* ○ workleaf--archive (no server)
|
|
11
|
+
*
|
|
12
|
+
* 4.3.0
|
|
13
|
+
* ● workleaf--bench :1236 http://localhost:1236/storyboard/ (pid 23456)
|
|
14
|
+
*
|
|
15
|
+
* `●` = live server, `○` = no live server.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as p from '@clack/prompts'
|
|
19
|
+
import { listWorkleafs } from './paths.js'
|
|
20
|
+
import { findByWorktree } from '../worktree/serverRegistry.js'
|
|
21
|
+
import { dim, bold, green } from '../cli/intro.js'
|
|
22
|
+
|
|
23
|
+
export function runList() {
|
|
24
|
+
const all = listWorkleafs()
|
|
25
|
+
|
|
26
|
+
if (all.length === 0) {
|
|
27
|
+
p.log.info('No workleafs yet.')
|
|
28
|
+
p.log.info(dim('Create one with: npx storyboard workleaf create <name>'))
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Group by parent, preserving insertion order from listWorkleafs (already
|
|
33
|
+
// grouped by readdir order which is parent-first).
|
|
34
|
+
const byParent = new Map()
|
|
35
|
+
for (const w of all) {
|
|
36
|
+
if (!byParent.has(w.parent)) byParent.set(w.parent, [])
|
|
37
|
+
byParent.get(w.parent).push(w)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log()
|
|
41
|
+
for (const [parent, leaves] of byParent) {
|
|
42
|
+
console.log(` ${bold(parent)}`)
|
|
43
|
+
for (const w of leaves) {
|
|
44
|
+
const servers = findByWorktree(w.branch)
|
|
45
|
+
if (servers.length > 0) {
|
|
46
|
+
const s = servers[0]
|
|
47
|
+
const url = `http://localhost:${s.port}/storyboard/`
|
|
48
|
+
console.log(` ${green('●')} workleaf--${w.slug.padEnd(28)} :${String(s.port).padEnd(5)} ${green(url)} ${dim(`(pid ${s.pid})`)}`)
|
|
49
|
+
} else {
|
|
50
|
+
console.log(` ${dim('○')} workleaf--${w.slug.padEnd(28)} ${dim('(no server)')}`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
console.log()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
p.log.info(dim('Tip: storyboard workleaf merge <name> squashes & cleans up'))
|
|
57
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge a workleaf into its parent (squash) and clean it up.
|
|
3
|
+
*
|
|
4
|
+
* Lifecycle:
|
|
5
|
+
* 1. Resolve the target workleaf — explicit name positional OR auto-detect
|
|
6
|
+
* from cwd via detectWorkleafContext.
|
|
7
|
+
* 2. Refuse if the workleaf has uncommitted changes.
|
|
8
|
+
* 3. Verify the parent worktree still exists.
|
|
9
|
+
* 4. Preflight a squash-merge probe in the parent (and bail cleanly
|
|
10
|
+
* on conflict — never leave the parent in MERGING state).
|
|
11
|
+
* 5. Real `git merge --squash <branch>` in the parent.
|
|
12
|
+
* 6. Commit with a generated message + Copilot co-author trailer.
|
|
13
|
+
* 7. Cleanup: stop dev server, remove worktree, delete branch, release port.
|
|
14
|
+
*
|
|
15
|
+
* If we're inside the workleaf being merged, we move cwd to the parent
|
|
16
|
+
* directory after merge so the user isn't left in a deleted directory.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import * as p from '@clack/prompts'
|
|
20
|
+
import { execFileSync } from 'node:child_process'
|
|
21
|
+
import { existsSync } from 'node:fs'
|
|
22
|
+
import { resolve } from 'node:path'
|
|
23
|
+
import { repoRoot, worktreeDir } from '../worktree/port.js'
|
|
24
|
+
import {
|
|
25
|
+
detectWorkleafContext,
|
|
26
|
+
workleafBranchName,
|
|
27
|
+
workleafDir,
|
|
28
|
+
listWorkleafs,
|
|
29
|
+
parentOfWorkleaf,
|
|
30
|
+
} from './paths.js'
|
|
31
|
+
import { cleanupWorkleaf, currentBranch, hasDirtyWorkingTree } from './cleanup.js'
|
|
32
|
+
import { dim, bold, green } from '../cli/intro.js'
|
|
33
|
+
import { parseFlags } from '../cli/flags.js'
|
|
34
|
+
|
|
35
|
+
const flagSchema = {
|
|
36
|
+
message: { type: 'string', description: 'Override the squash commit message' },
|
|
37
|
+
'no-commit': { type: 'boolean', default: false, description: 'Stage the squash but do not commit (caller commits)' },
|
|
38
|
+
yes: { type: 'boolean', default: false, description: 'Skip interactive confirmation' },
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const COAUTHOR_TRAILER = 'Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>'
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Locate the target workleaf by name (positional) OR by inspecting cwd.
|
|
45
|
+
* Returns { parent, slug, branch, dir, root } or exits with an error.
|
|
46
|
+
*/
|
|
47
|
+
function resolveTarget(positionalName, root) {
|
|
48
|
+
// Explicit positional wins
|
|
49
|
+
if (positionalName) {
|
|
50
|
+
// The user can pass either a slug OR a full branch name. Disambiguate.
|
|
51
|
+
const branchInfo = parentOfWorkleaf(positionalName)
|
|
52
|
+
if (branchInfo) {
|
|
53
|
+
const dir = workleafDir(branchInfo.parent, branchInfo.slug, root)
|
|
54
|
+
return {
|
|
55
|
+
parent: branchInfo.parent,
|
|
56
|
+
slug: branchInfo.slug,
|
|
57
|
+
branch: positionalName,
|
|
58
|
+
dir,
|
|
59
|
+
root,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Treat as slug — find a matching workleaf, prefer one parented by
|
|
63
|
+
// the current worktree if multiple match.
|
|
64
|
+
const matches = listWorkleafs(root).filter((w) => w.slug === positionalName)
|
|
65
|
+
if (matches.length === 0) {
|
|
66
|
+
p.log.error(`No workleaf named "${positionalName}" found.`)
|
|
67
|
+
p.log.info(`List workleafs with: ${green('npx storyboard workleaf list')}`)
|
|
68
|
+
process.exit(1)
|
|
69
|
+
}
|
|
70
|
+
if (matches.length > 1) {
|
|
71
|
+
p.log.error(`Multiple workleafs match "${positionalName}":`)
|
|
72
|
+
for (const m of matches) console.log(` ${m.branch} ${dim(m.dir)}`)
|
|
73
|
+
p.log.info(`Pass the full branch name: ${green('npx storyboard workleaf merge <parent>--workleaf--<slug>')}`)
|
|
74
|
+
process.exit(1)
|
|
75
|
+
}
|
|
76
|
+
const m = matches[0]
|
|
77
|
+
return { parent: m.parent, slug: m.slug, branch: m.branch, dir: m.dir, root }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Auto-detect from cwd
|
|
81
|
+
const ctx = detectWorkleafContext()
|
|
82
|
+
if (!ctx.inWorkleaf) {
|
|
83
|
+
p.log.error('Not inside a workleaf — pass a name or cd into one.')
|
|
84
|
+
p.log.info(`Usage: ${green('npx storyboard workleaf merge <name>')}`)
|
|
85
|
+
process.exit(1)
|
|
86
|
+
}
|
|
87
|
+
return { parent: ctx.parent, slug: ctx.slug, branch: ctx.branch, dir: ctx.dir, root }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Preflight: probe whether `git merge --squash <branch>` in the parent
|
|
92
|
+
* would succeed. We do the probe by running the actual merge command,
|
|
93
|
+
* checking the result, and aborting the merge state if anything was
|
|
94
|
+
* staged (i.e. we always leave the parent in its pre-probe state).
|
|
95
|
+
*
|
|
96
|
+
* Returns { ok: true } on clean merge OR { ok: false, conflicts: [...] }
|
|
97
|
+
* on conflict. The parent is reset either way.
|
|
98
|
+
*/
|
|
99
|
+
function preflightSquash(parentDir, branch) {
|
|
100
|
+
try {
|
|
101
|
+
// --squash leaves the changes staged but does NOT create a merge commit
|
|
102
|
+
// and does NOT set MERGE_HEAD, so on success we just need to reset.
|
|
103
|
+
execFileSync('git', ['merge', '--squash', '--no-commit', branch], {
|
|
104
|
+
cwd: parentDir,
|
|
105
|
+
stdio: 'pipe',
|
|
106
|
+
})
|
|
107
|
+
// Reset the staged changes so the probe is non-destructive.
|
|
108
|
+
execFileSync('git', ['reset', '--hard', 'HEAD'], { cwd: parentDir, stdio: 'pipe' })
|
|
109
|
+
return { ok: true }
|
|
110
|
+
} catch (err) {
|
|
111
|
+
// Capture conflict markers, then reset state.
|
|
112
|
+
let conflicts = []
|
|
113
|
+
try {
|
|
114
|
+
const out = execFileSync('git', ['diff', '--name-only', '--diff-filter=U'], {
|
|
115
|
+
cwd: parentDir,
|
|
116
|
+
encoding: 'utf8',
|
|
117
|
+
}).trim()
|
|
118
|
+
conflicts = out ? out.split('\n') : []
|
|
119
|
+
} catch { /* ignore */ }
|
|
120
|
+
// Always reset to leave the parent clean.
|
|
121
|
+
try { execFileSync('git', ['reset', '--hard', 'HEAD'], { cwd: parentDir, stdio: 'pipe' }) } catch { /* ignore */ }
|
|
122
|
+
return { ok: false, conflicts, error: err.message }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function runMerge(argv = process.argv.slice(4)) {
|
|
127
|
+
const { flags, positional } = parseFlags(argv, flagSchema)
|
|
128
|
+
const root = repoRoot()
|
|
129
|
+
const target = resolveTarget(positional[0], root)
|
|
130
|
+
const { parent, slug, branch, dir } = target
|
|
131
|
+
|
|
132
|
+
p.intro('storyboard workleaf merge')
|
|
133
|
+
p.log.info(`${dim('workleaf:')} ${bold(branch)}`)
|
|
134
|
+
|
|
135
|
+
// Verify workleaf exists on disk
|
|
136
|
+
if (!existsSync(resolve(dir, '.git'))) {
|
|
137
|
+
p.log.error(`Workleaf directory missing: ${dim(dir)}`)
|
|
138
|
+
process.exit(1)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Refuse on dirty workleaf
|
|
142
|
+
if (hasDirtyWorkingTree(dir)) {
|
|
143
|
+
p.log.error(`Workleaf has uncommitted changes — commit or stash them first.`)
|
|
144
|
+
p.log.info(` ${dim(`cd ${dir}`)}`)
|
|
145
|
+
p.log.info(` ${green('git status')}`)
|
|
146
|
+
process.exit(1)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Verify parent worktree
|
|
150
|
+
const parentDir = parent === 'main' ? root : worktreeDir(parent, root)
|
|
151
|
+
if (!existsSync(resolve(parentDir, '.git'))) {
|
|
152
|
+
p.log.error(`Parent worktree "${parent}" is gone.`)
|
|
153
|
+
p.log.info(`Recreate it (${green('npx storyboard branch ' + parent)}) and re-run, or abandon with`)
|
|
154
|
+
p.log.info(` ${green(`npx storyboard workleaf delete ${slug}`)}`)
|
|
155
|
+
process.exit(1)
|
|
156
|
+
}
|
|
157
|
+
// Refuse if parent is on a different branch than expected — that
|
|
158
|
+
// means the user is intentionally on a wildcard branch and probably
|
|
159
|
+
// doesn't want a squash here.
|
|
160
|
+
const parentBranch = currentBranch(parentDir)
|
|
161
|
+
if (!parentBranch) {
|
|
162
|
+
p.log.error(`Could not determine current branch of parent worktree at ${dim(parentDir)}`)
|
|
163
|
+
process.exit(1)
|
|
164
|
+
}
|
|
165
|
+
if (hasDirtyWorkingTree(parentDir)) {
|
|
166
|
+
p.log.error(`Parent worktree "${parent}" has uncommitted changes — commit or stash them first.`)
|
|
167
|
+
p.log.info(` ${dim(`cd ${parentDir}`)}`)
|
|
168
|
+
p.log.info(` ${green('git status')}`)
|
|
169
|
+
process.exit(1)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Preflight squash
|
|
173
|
+
p.log.step('Checking that the squash will apply cleanly…')
|
|
174
|
+
const probe = preflightSquash(parentDir, branch)
|
|
175
|
+
if (!probe.ok) {
|
|
176
|
+
p.log.error('Squash would conflict with the parent.')
|
|
177
|
+
if (probe.conflicts && probe.conflicts.length > 0) {
|
|
178
|
+
for (const c of probe.conflicts) console.log(` - ${c}`)
|
|
179
|
+
}
|
|
180
|
+
p.log.info(`Resolve manually with:`)
|
|
181
|
+
p.log.info(` ${dim(`cd ${parentDir} && git merge --squash ${branch}`)}`)
|
|
182
|
+
process.exit(1)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Real merge
|
|
186
|
+
p.log.step(`Squash-merging into ${bold(parentBranch)}`)
|
|
187
|
+
try {
|
|
188
|
+
execFileSync('git', ['merge', '--squash', branch], { cwd: parentDir, stdio: 'pipe' })
|
|
189
|
+
} catch (err) {
|
|
190
|
+
p.log.error(`Real squash failed (this shouldn't happen after a clean preflight): ${err.message}`)
|
|
191
|
+
process.exit(1)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Commit (or stage-only)
|
|
195
|
+
let sha = null
|
|
196
|
+
if (flags['no-commit']) {
|
|
197
|
+
p.log.success(`Squash staged in ${bold(parentBranch)} — commit it yourself`)
|
|
198
|
+
} else {
|
|
199
|
+
const message = flags.message || `feat: ${slug} (workleaf squash)`
|
|
200
|
+
try {
|
|
201
|
+
execFileSync('git', ['commit', '-m', `${message}\n\n${COAUTHOR_TRAILER}`], {
|
|
202
|
+
cwd: parentDir,
|
|
203
|
+
stdio: 'pipe',
|
|
204
|
+
})
|
|
205
|
+
sha = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd: parentDir, encoding: 'utf8' }).trim()
|
|
206
|
+
p.log.success(`Committed as ${green(sha)}: ${dim(message)}`)
|
|
207
|
+
} catch (err) {
|
|
208
|
+
p.log.error(`Commit failed: ${err.message}`)
|
|
209
|
+
p.log.warn(`Squash is staged in ${parentDir} but uncommitted. Resolve manually before re-running.`)
|
|
210
|
+
process.exit(1)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// CRITICAL: chdir to the parent BEFORE cleanup so cwd is not inside
|
|
215
|
+
// the directory we're about to delete. Without this, every subsequent
|
|
216
|
+
// call into helpers that touch cwd (releasePort -> portsFilePath ->
|
|
217
|
+
// realpathSync) hits ENOENT.
|
|
218
|
+
try { process.chdir(parentDir) } catch { /* parent may also be gone */ }
|
|
219
|
+
|
|
220
|
+
p.log.step('Cleaning up workleaf')
|
|
221
|
+
const { stopped } = cleanupWorkleaf({ branch, dir, root })
|
|
222
|
+
|
|
223
|
+
p.note(
|
|
224
|
+
[
|
|
225
|
+
` ${green('✓')} squashed into ${bold(parentBranch)}${sha ? ` as ${green(sha)}` : ''}`,
|
|
226
|
+
` ${green('✓')} stopped ${stopped} dev server${stopped === 1 ? '' : 's'}`,
|
|
227
|
+
` ${green('✓')} removed ${dim(dir.replace(root + '/', ''))}`,
|
|
228
|
+
` ${green('✓')} deleted branch ${dim(branch)}`,
|
|
229
|
+
'',
|
|
230
|
+
` cd to the parent: ${green(`cd ${parentDir.replace(root + '/', '') || '.'}`)}`,
|
|
231
|
+
].join('\n'),
|
|
232
|
+
`${bold(branch)} merged`
|
|
233
|
+
)
|
|
234
|
+
p.outro('')
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Re-export the branch-name helper for symmetry
|
|
238
|
+
export { workleafBranchName }
|