@1agh/maude 0.43.0 → 0.45.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/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 +396 -46
- package/apps/studio/api.ts +395 -2
- 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/_transcribe.mjs +419 -0
- package/apps/studio/bin/_transcribe.test.mjs +80 -0
- 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/photo-bg-remove.sh +119 -21
- package/apps/studio/bin/transcribe.sh +39 -0
- package/apps/studio/canvas-edit.ts +197 -2
- package/apps/studio/canvas-lib.tsx +115 -6
- package/apps/studio/client/app.jsx +1954 -685
- package/apps/studio/client/generate-dialog.jsx +352 -0
- package/apps/studio/client/github.js +14 -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/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/SettingsPanel.jsx +828 -0
- package/apps/studio/client/panels/SetupChecklist.jsx +223 -0
- package/apps/studio/client/photo-knobs.jsx +167 -172
- package/apps/studio/client/styles/3-shell-maude.css +334 -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/config.schema.json +70 -0
- package/apps/studio/context-menu.tsx +11 -2
- package/apps/studio/context.ts +16 -0
- package/apps/studio/design-setup-readiness.ts +115 -0
- package/apps/studio/dist/client.bundle.js +2097 -2088
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/draw/palette.ts +34 -0
- package/apps/studio/footage/schema.test.ts +92 -0
- package/apps/studio/footage/schema.ts +179 -1
- 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/service.ts +3 -1
- package/apps/studio/http.ts +867 -9
- package/apps/studio/media/intro.mp4 +0 -0
- package/apps/studio/media-commit-chain.ts +119 -0
- package/apps/studio/paths.ts +10 -0
- package/apps/studio/readiness.ts +108 -20
- package/apps/studio/scaffold-design.ts +1145 -0
- package/apps/studio/server.ts +13 -1
- package/apps/studio/test/_helpers.ts +15 -2
- package/apps/studio/test/acp-bridge.test.ts +34 -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/canvas-origin-gate.test.ts +37 -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/element-structural-edit.test.ts +108 -0
- package/apps/studio/test/fixtures/fake-claude-auth.mjs +13 -0
- package/apps/studio/test/generate-route.test.ts +106 -0
- 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/media-commit-chain.test.ts +210 -0
- package/apps/studio/test/photo-bg-remove-validation.test.ts +71 -0
- package/apps/studio/test/readiness.test.ts +13 -11
- package/apps/studio/test/scaffold-design.test.ts +122 -0
- package/apps/studio/test/tour-overlay.test.tsx +16 -0
- package/apps/studio/tool-palette.tsx +11 -6
- package/apps/studio/ui-prefs.ts +130 -0
- package/apps/studio/use-canvas-media-drop.tsx +4 -2
- package/apps/studio/use-chrome-visibility.tsx +8 -2
- package/apps/studio/whats-new.json +96 -0
- package/apps/studio/ws.ts +5 -0
- package/cli/bin/maude.mjs +6 -6
- package/cli/commands/design.mjs +33 -0
- package/cli/commands/init.mjs +7 -1
- package/cli/lib/gitignore-block.mjs +1 -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 +12 -10
- package/plugins/design/dependencies.json +18 -0
- package/plugins/flow/.claude-plugin/config.schema.json +5 -0
- package/plugins/flow/templates/ai-skeleton/scenario-guide.md +39 -0
- package/plugins/flow/templates/ai-skeleton/scenarios/README.md +2 -0
|
@@ -42,9 +42,15 @@ export function ChromeVisibilityProvider({
|
|
|
42
42
|
children: ReactNode;
|
|
43
43
|
initial?: Partial<ChromeVisibilityState>;
|
|
44
44
|
}) {
|
|
45
|
+
// Defaults MUST match the app-shell's own default view-prefs (app.jsx
|
|
46
|
+
// MINIMAP_STORE / ZOOMCTL_STORE both default false) — otherwise a freshly
|
|
47
|
+
// loaded canvas shows the minimap/zoom before (or instead of, when the seed
|
|
48
|
+
// races the early inline-script `dgn:'loaded'`) the shell's OFF state is
|
|
49
|
+
// applied, so a user with both toggled off still sees them. `present` is a
|
|
50
|
+
// transient overlay, never persisted, so it stays false.
|
|
45
51
|
const [state, setState] = useState<ChromeVisibilityState>({
|
|
46
|
-
minimap: initial?.minimap ??
|
|
47
|
-
zoom: initial?.zoom ??
|
|
52
|
+
minimap: initial?.minimap ?? false,
|
|
53
|
+
zoom: initial?.zoom ?? false,
|
|
48
54
|
present: initial?.present ?? false,
|
|
49
55
|
});
|
|
50
56
|
const setChrome = useCallback((patch: Partial<ChromeVisibilityState>) => {
|
|
@@ -1,6 +1,102 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./whats-new.schema.json",
|
|
3
3
|
"entries": [
|
|
4
|
+
{
|
|
5
|
+
"id": "bring-your-brand",
|
|
6
|
+
"version": "0.44.0",
|
|
7
|
+
"date": "2026-07-14",
|
|
8
|
+
"kind": "feature",
|
|
9
|
+
"title": "Bring your existing brand into Maude",
|
|
10
|
+
"summary": "Have a logo already? Upload it from the Quick setup checklist and Maude pulls out its color palette (and any recognizable font names) to seed a new design system — nothing is applied automatically, you confirm every choice during setup. Works with SVG logos; safety-checked before anything touches your project.",
|
|
11
|
+
"surface": "design-ui",
|
|
12
|
+
"tour": [
|
|
13
|
+
{
|
|
14
|
+
"target": "[data-testid=\"onboarding-bring-brand\"]",
|
|
15
|
+
"title": "Bring my existing brand",
|
|
16
|
+
"body": "Upload a logo SVG to seed a new design system's colors and fonts from it."
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "inspector-designer-mode",
|
|
22
|
+
"version": "0.44.0",
|
|
23
|
+
"date": "2026-07-14",
|
|
24
|
+
"kind": "feature",
|
|
25
|
+
"title": "A Figma-friendly view of the CSS panel",
|
|
26
|
+
"summary": "The Inspector now speaks two languages. Advanced keeps the real CSS property names; Designer regroups the exact same controls into Figma-familiar sections — Fill, Stroke, Corner radius, Auto layout, Effects, Opacity, Text — and relabels the rows, so you can style without thinking in CSS. Flip between them from the small toggle in the panel's top-right corner, or set a default in Settings → Appearance. Same live edits underneath, and a value set in one view reads back correctly in the other. New in both views: Blur and Blend controls, plus a single 9-point pad for auto-layout alignment.",
|
|
27
|
+
"surface": "design-ui",
|
|
28
|
+
"tour": [
|
|
29
|
+
{
|
|
30
|
+
"target": "[data-tour=\"cp-mode\"]",
|
|
31
|
+
"title": "Advanced or Designer",
|
|
32
|
+
"body": "Toggle the CSS panel between raw property names and Figma-style vocabulary. Your choice is remembered — you can also set it in Settings → Appearance.",
|
|
33
|
+
"placement": "left",
|
|
34
|
+
"inspector": true,
|
|
35
|
+
"tab": "css",
|
|
36
|
+
"requireSelection": true
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "watch-the-intro",
|
|
42
|
+
"version": "0.44.0",
|
|
43
|
+
"date": "2026-07-14",
|
|
44
|
+
"kind": "feature",
|
|
45
|
+
"title": "Watch the intro, right inside Maude",
|
|
46
|
+
"summary": "There's now a short product showreel (about a minute and a half) you can watch without leaving the app — Help → Watch the intro, or the same button on the welcome screen the first time you open Maude. It walks through the whole loop: a design system built from a conversation, the infinite canvas, pointing and commenting to direct an AI edit, live multiplayer, and shipping the result. Also lands as its own canvas (Maude Explainer) if you want to open it in the canvas browser like anything else.",
|
|
47
|
+
"surface": "design-ui",
|
|
48
|
+
"tour": [
|
|
49
|
+
{
|
|
50
|
+
"target": "[data-tour=\"help\"]",
|
|
51
|
+
"title": "Watch the intro",
|
|
52
|
+
"body": "Help → Watch the intro plays the full showreel without leaving Maude."
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"id": "bulk-media-insert",
|
|
58
|
+
"version": "0.44.0",
|
|
59
|
+
"date": "2026-07-14",
|
|
60
|
+
"kind": "feature",
|
|
61
|
+
"title": "Insert several photos at once",
|
|
62
|
+
"summary": "The media picker now supports multi-select — pick several photos (or a mix of photos, videos, and audio) in one go instead of one at a time. Choose whether they land inside your artboard or float on the canvas as annotations; the picker figures out the sensible default (video and audio always go on the canvas, since only images can drop into an artboard) and lets you switch. Dragging several files from Finder onto the canvas at once is also more reliable now — every dropped file lands, even in a large batch.",
|
|
63
|
+
"surface": "design-ui"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "ai-video-generation",
|
|
67
|
+
"version": "0.44.0",
|
|
68
|
+
"date": "2026-07-14",
|
|
69
|
+
"kind": "feature",
|
|
70
|
+
"title": "Generate video clips for your reels",
|
|
71
|
+
"summary": "The BYOK generator now makes video. Bring your own Google key and generate a clip with Veo — a text prompt (\"slow drone push over an alpine lake at dawn\"), or seed it from a still you already generated so the clip matches your hero's look. The clip lands in your assets like any other footage, and Maude treats it as first-class: when you build a reel it watches the generated clip alongside your real footage and places it as a beat, so you can fill a missing shot without hand-sourcing one. Video takes a few minutes and runs in the background — you'll get the finished clip when it's ready. Add your key under File → Settings — AI generation (⌘,); it stays on your machine.",
|
|
72
|
+
"surface": "design-ui"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"id": "ai-audio-generation",
|
|
76
|
+
"version": "0.44.0",
|
|
77
|
+
"date": "2026-07-14",
|
|
78
|
+
"kind": "feature",
|
|
79
|
+
"title": "Generate music, voiceover & subtitles",
|
|
80
|
+
"summary": "The BYOK generator now does audio. Bring your own ElevenLabs key and generate a music bed, sound effects, or a text-to-speech voiceover — one key covers the whole stack — and the track lands in your assets ready to drop under a reel. Before you spend credits on a music or SFX track, Maude searches audio you've already made (and your own ElevenLabs history — re-downloading it costs nothing) and offers a match to reuse. Subtitles are free and need no key at all: `maude design transcribe` runs whisper.cpp locally on any clip and writes word-timed SRT/VTT captions. Local setup is one click — Settings → Subtitles → Download model — and Maude auto-converts video containers (no ffmpeg wrangling) and picks the model for you. You pick the subtitle engine — local whisper, ElevenLabs Scribe, or Groq — in Settings; it's an explicit choice, so Maude never silently switches to a paid cloud engine behind your back. In a reel, generated music/voiceover become layered audio tracks (the music ducks under the voice) and the subtitles become a caption track that renders right on the video. Add your keys under File → Settings — AI generation (⌘,); they're stored only on your machine.",
|
|
81
|
+
"surface": "design-ui"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"id": "ai-media-generation",
|
|
85
|
+
"version": "0.44.0",
|
|
86
|
+
"date": "2026-07-14",
|
|
87
|
+
"kind": "feature",
|
|
88
|
+
"title": "Generate images with your own AI key",
|
|
89
|
+
"summary": "Bring your own Google (Nano Banana) API key and generate images right inside Maude — and the image lands straight on your canvas, not in a dead-end dialog. Use the Generate action (⌘K → \"Generate with AI\"), the command line, or just ask in the chat panel (\"generate a hero image of a mountain lake\"); the finished picture drops onto the artboard you're looking at. Add your key under File → Settings — AI generation (⌘,); it's stored only on your machine, sent straight to the provider, and never touches a canvas or your git history. Want to change a generated image? Ask to edit it (\"make the sky purple\") for a fresh AI-edited version, or fine-tune it in the Photo tab — every result is a normal asset you can place, edit, and export. Audio and video generation are on the way.",
|
|
90
|
+
"surface": "design-ui",
|
|
91
|
+
"tour": [
|
|
92
|
+
{
|
|
93
|
+
"target": "[data-tour=\"menubar-file\"]",
|
|
94
|
+
"title": "AI generation settings",
|
|
95
|
+
"body": "Open File → Settings — AI generation (⌘,) to add your provider key, then Generate with AI from the ⌘K palette.",
|
|
96
|
+
"placement": "bottom"
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
},
|
|
4
100
|
{
|
|
5
101
|
"id": "photo-editor",
|
|
6
102
|
"version": "0.43.0",
|
package/apps/studio/ws.ts
CHANGED
|
@@ -222,6 +222,11 @@ export function createWs(
|
|
|
222
222
|
// clients only — same privileged-data class as the rest of this feed.
|
|
223
223
|
ctx.bus.on('export:job', (job: unknown) => broadcast({ type: 'export:job', payload: job }));
|
|
224
224
|
|
|
225
|
+
// feature-ai-media-generation (DDR-16x) — generation job queue state changes
|
|
226
|
+
// (queued → running → done/failed). Same privileged-data class + snapshot
|
|
227
|
+
// shape as export:job; the notification center reuses the export-center chrome.
|
|
228
|
+
ctx.bus.on('generate:job', (job: unknown) => broadcast({ type: 'generate:job', payload: job }));
|
|
229
|
+
|
|
225
230
|
// HMR broadcaster — turns fs:any change events into `canvas-hmr` messages.
|
|
226
231
|
// The iframe-side client (in _shell.html) decides reload strategy from `mode`.
|
|
227
232
|
// Uses broadcastHmr so the segregated canvas origin's HMR-only sockets get it.
|
package/cli/bin/maude.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
|
-
import {
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { resolvePkgRoot } from '../lib/pkg-root.mjs';
|
|
4
5
|
// maude — Maude CLI. Scaffold .ai workspace, run dev servers, manage config.
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { runUpdateCheck } from '../lib/update-check.mjs';
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const PKG_ROOT =
|
|
8
|
+
// DDR-166 T0b — real-disk resolution, safe inside a `bun build --compile`
|
|
9
|
+
// standalone binary (see pkg-root.mjs's own doc comment for why the previous
|
|
10
|
+
// `dirname(fileURLToPath(import.meta.url))` walk-up broke there).
|
|
11
|
+
const PKG_ROOT = resolvePkgRoot();
|
|
12
12
|
|
|
13
13
|
const COMMANDS = {
|
|
14
14
|
init: () => import('../commands/init.mjs'),
|
package/cli/commands/design.mjs
CHANGED
|
@@ -47,6 +47,23 @@ const BIN_VERBS = new Set([
|
|
|
47
47
|
'chat-open',
|
|
48
48
|
'ensure-browser',
|
|
49
49
|
'fetch-asset',
|
|
50
|
+
// DDR-167 (Phase 3 / T10). `import-asset` is the LOCAL-file sibling of the
|
|
51
|
+
// URL-only `fetch-asset`: hardened SVG ingestion (allowlist DOM-sanitize +
|
|
52
|
+
// a real-browser execution canary). PDF import is wired but not yet
|
|
53
|
+
// available — see the DDR's addendum on why the planned rasterization
|
|
54
|
+
// mechanism doesn't work under browser automation.
|
|
55
|
+
'import-asset',
|
|
56
|
+
// DDR-172 (Phase 3 / T11). `import-tokens` is the token-file sibling of
|
|
57
|
+
// `import-asset`: parses W3C design-tokens / Style-Dictionary JSON or raw
|
|
58
|
+
// CSS custom properties, maps recognized tokens onto the DS CSS-variable
|
|
59
|
+
// contract, and patches (theme-block-scoped) or scaffolds the target
|
|
60
|
+
// design system. CLI-only — no HTTP route, no in-app panel.
|
|
61
|
+
'import-tokens',
|
|
62
|
+
// DDR-173 (Phase 3 / T12). `import-brand` extracts typed palette/font
|
|
63
|
+
// cues + a hardened logo asset from an already-DDR-167-sanitized SVG for
|
|
64
|
+
// `/design:setup-ds --from-brand` — never re-reads or re-sanitizes the
|
|
65
|
+
// original brand file (Decision 2: no parallel, ungated read path).
|
|
66
|
+
'import-brand',
|
|
50
67
|
// feature-photo-editor (Stage G). `photo-adjust` is the thin non-browser
|
|
51
68
|
// parametric verb (curl → /_api/photo-edit). `photo-bg-remove` is the
|
|
52
69
|
// client-side @imgly ML harness (throwaway proof canvas + agent-browser
|
|
@@ -60,6 +77,21 @@ const BIN_VERBS = new Set([
|
|
|
60
77
|
// reads the clip as a file:// resource).
|
|
61
78
|
'ingest-footage',
|
|
62
79
|
'probe-footage',
|
|
80
|
+
// feature-ai-media-generation (Phase 0, DDR-16x). `generate` is the thin
|
|
81
|
+
// non-browser BYOK verb (curl → /_api/generate-jobs → poll → print the
|
|
82
|
+
// produced /assets/<sha8>.<ext>). The provider call happens server-side (the
|
|
83
|
+
// sidecar resolves the key from the keychain / ~/.config/maude/keys.json).
|
|
84
|
+
'generate',
|
|
85
|
+
// feature-ai-media-generation (Phase 2, DDR-164). `transcribe` is the local,
|
|
86
|
+
// no-key subtitle verb: spawn whisper.cpp on an audio/video file → SRT/VTT via
|
|
87
|
+
// the shared captions.ts reflow. No dev server; whisper.cpp is a SOFT dep with
|
|
88
|
+
// ElevenLabs Scribe / Groq Whisper as the cloud fallback.
|
|
89
|
+
'transcribe',
|
|
90
|
+
// feature-ai-media-generation (Task 2.5, DDR-164). `audio-search` is the
|
|
91
|
+
// reuse-before-you-pay verb: search the project's own generated audio + the
|
|
92
|
+
// user's ElevenLabs history before spending credits on a new music/SFX/VO
|
|
93
|
+
// track. Runs server-side (key resolved by the sidecar); needs a dev server.
|
|
94
|
+
'audio-search',
|
|
63
95
|
]);
|
|
64
96
|
|
|
65
97
|
// Bin verbs that boot the dev-server (directly, or by shelling into server-up.sh).
|
|
@@ -154,6 +186,7 @@ Dev-tooling (dispatch to the dev-server bash helpers — DDR-062):
|
|
|
154
186
|
smoke · canvas-edit · handoff · asset-sweep · visual-sanity · fetch-asset
|
|
155
187
|
draw-build · draw-proof · svg-optimize · to-lottie · read-annotations · annotate
|
|
156
188
|
canvas-rects · ingest-footage · probe-footage
|
|
189
|
+
generate · transcribe · audio-search
|
|
157
190
|
Invoke the bundled helper of the same name. maude resolves it from its
|
|
158
191
|
own package root and sets CLAUDE_PLUGIN_ROOT for the child; stdout,
|
|
159
192
|
stderr, and exit code pass straight through (so command-substitution
|
package/cli/commands/init.mjs
CHANGED
|
@@ -6,7 +6,13 @@ import { copyTree } from '../lib/copy-tree.mjs';
|
|
|
6
6
|
const PLACEHOLDER = 'PROJECT_NAME';
|
|
7
7
|
// Files in the skeleton that contain the project-name placeholder and should
|
|
8
8
|
// be templated on copy.
|
|
9
|
-
const TEMPLATED = [
|
|
9
|
+
const TEMPLATED = [
|
|
10
|
+
'workflows.config.json',
|
|
11
|
+
'README.md',
|
|
12
|
+
'INDEX.md',
|
|
13
|
+
'release-guide.md',
|
|
14
|
+
'scenario-guide.md',
|
|
15
|
+
];
|
|
10
16
|
|
|
11
17
|
// Per-provider command substitutions for release-guide.md. Keys map to the
|
|
12
18
|
// `# CHANGELOG_PROVIDER_*_CMD` placeholders inside the skeleton's bash blocks.
|
|
@@ -38,6 +38,7 @@ export function buildBlock(designRel = '.design') {
|
|
|
38
38
|
`${root}/_preflight.json`,
|
|
39
39
|
`${root}/_locator.json`, // regenerable slug→path index
|
|
40
40
|
`${root}/_export-history.json`,
|
|
41
|
+
`${root}/_generate-history.json`, // AI-media generation job ledger (regenerable — feature-ai-media-generation, DDR-16x)
|
|
41
42
|
// Per-machine / per-user dirs.
|
|
42
43
|
`${root}/_export-jobs/`, // background-export job byte store (regenerable — feature-background-export-notification-center)
|
|
43
44
|
`${root}/_state/`, // binary CRDT logs (regenerable from hub)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// pkg-root.mjs — real-disk `maude` package-root resolution, safe inside a
|
|
2
|
+
// `bun build --compile` standalone binary (DDR-166 T0b).
|
|
3
|
+
//
|
|
4
|
+
// `cli/bin/maude.mjs` originally computed PKG_ROOT via
|
|
5
|
+
// `dirname(fileURLToPath(import.meta.url))` walk-up — correct for the plain
|
|
6
|
+
// npm/node install, but the exact DDR-045 trap: inside a compiled binary,
|
|
7
|
+
// `import.meta.url` resolves to Bun's virtual filesystem (`/$bunfs/root` on
|
|
8
|
+
// POSIX, `B:/~BUN/root` on Windows), not the real on-disk location. Two
|
|
9
|
+
// dev-server releases (v0.18.0/v0.18.1) shipped broken because of this exact
|
|
10
|
+
// bug class before DDR-045 fixed it there. This module mirrors that fix
|
|
11
|
+
// (`apps/studio/paths.ts` `resolveDevServerRoot`) for the CLI, independently
|
|
12
|
+
// — not by importing paths.ts directly, to keep the CLI's compiled binary
|
|
13
|
+
// decoupled from the dev-server's module graph.
|
|
14
|
+
|
|
15
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
16
|
+
import { dirname, join } from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
function isVirtualBunfsPath(p) {
|
|
20
|
+
return p !== null && (p.startsWith('/$bunfs') || p.startsWith('B:/~BUN'));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** True inside a `bun build --compile` standalone binary. Mirrors `apps/studio/paths.ts`'s `IS_COMPILED_BINARY`. */
|
|
24
|
+
export function isCompiledBinary() {
|
|
25
|
+
return isVirtualBunfsPath(getImportMetaDir());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getImportMetaDir() {
|
|
29
|
+
try {
|
|
30
|
+
return dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Anchor: `apps/studio/bin/screenshot.sh` is present in every real maude
|
|
38
|
+
* package root (dev tree, npm tarball, and the Tauri desktop `Resources/`
|
|
39
|
+
* bundle all ship it) and nowhere else — unique enough to identify the
|
|
40
|
+
* package root without also requiring `package.json` (npm tarballs can drop
|
|
41
|
+
* nested workspace `package.json` files, the exact false-negative DDR-045's
|
|
42
|
+
* own retro already documents for the dev-server's analogous check).
|
|
43
|
+
*/
|
|
44
|
+
export function isPkgRoot(dir) {
|
|
45
|
+
// Both anchors, not just one: `apps/studio/bin/screenshot.sh` ALONE is a
|
|
46
|
+
// false-positive inside the Tauri desktop build — `stage-resources.mjs`
|
|
47
|
+
// stages a full working copy of `apps/studio` (dev-server + bin scripts)
|
|
48
|
+
// into `target/debug/apps/studio/` for the sidecar's own runtime
|
|
49
|
+
// resolution, but that staged copy has no `cli/` — walking up from a
|
|
50
|
+
// compiled `maude` binary placed at `target/debug/maude` matched THAT
|
|
51
|
+
// copy first and stopped one level too early (caught by live testing
|
|
52
|
+
// against the real Tauri build, not a synthetic path). `cli/commands/
|
|
53
|
+
// design.mjs` only exists in a genuine package root.
|
|
54
|
+
return (
|
|
55
|
+
existsSync(join(dir, 'apps', 'studio', 'bin', 'screenshot.sh')) &&
|
|
56
|
+
existsSync(join(dir, 'cli', 'commands', 'design.mjs'))
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolve maude's own package root — real disk path, safe inside a compiled
|
|
62
|
+
* binary. Priority: (0) explicit override, (1) dev-mode `import.meta.url`
|
|
63
|
+
* (plain `node`/`bun run cli/bin/maude.mjs`), (2) compiled-binary walk-up
|
|
64
|
+
* from `process.execPath` (matches the npm-install layout: binary at
|
|
65
|
+
* `@1agh/maude-<platform>/maude` walks up to `@1agh/maude/`, which ships
|
|
66
|
+
* `apps/studio/` via `package.json` `files`; matches the Tauri desktop layout
|
|
67
|
+
* too, where the bundled `maude` binary and the `Resources/apps/studio`
|
|
68
|
+
* bundle share a common ancestor within the walk-up depth).
|
|
69
|
+
*/
|
|
70
|
+
export function resolvePkgRoot() {
|
|
71
|
+
const override = process.env.MAUDE_PKG_ROOT;
|
|
72
|
+
if (override && !isVirtualBunfsPath(override) && isPkgRoot(override)) return override;
|
|
73
|
+
|
|
74
|
+
const importDir = getImportMetaDir();
|
|
75
|
+
if (importDir && !isVirtualBunfsPath(importDir)) {
|
|
76
|
+
// Dev-mode: cli/bin/maude.mjs is 2 levels under the package root.
|
|
77
|
+
const candidate = join(importDir, '..', '..');
|
|
78
|
+
if (isPkgRoot(candidate)) return candidate;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// `process.execPath` is the path AS INVOKED — when launched through a
|
|
82
|
+
// symlink (DDR-166 T0b stages exactly this: sidecar.rs's narrow bin-link
|
|
83
|
+
// dir), it's the symlink's own path, not the real target, and walking up
|
|
84
|
+
// from there (e.g. ~/Library/Caches/…/bin-link/) never reaches the repo
|
|
85
|
+
// root. Dereference first. Falls back to the raw path if the file somehow
|
|
86
|
+
// doesn't exist (shouldn't happen — we ARE that running process).
|
|
87
|
+
let cur;
|
|
88
|
+
try {
|
|
89
|
+
cur = dirname(realpathSync(process.execPath));
|
|
90
|
+
} catch {
|
|
91
|
+
cur = dirname(process.execPath);
|
|
92
|
+
}
|
|
93
|
+
for (let i = 0; i < 10; i++) {
|
|
94
|
+
if (isPkgRoot(cur)) return cur;
|
|
95
|
+
const parent = dirname(cur);
|
|
96
|
+
if (parent === cur) break;
|
|
97
|
+
cur = parent;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Final fallback for unanchored test contexts — callers should expect
|
|
101
|
+
// existsSync-based lookups to fail and surface a clear error, same
|
|
102
|
+
// contract as paths.ts's own resolver. Never join a VIRTUAL importDir here
|
|
103
|
+
// (`/$bunfs/root/../..` collapses to `/` via path.join — a real bug an
|
|
104
|
+
// earlier draft of this function had, caught by testing against an
|
|
105
|
+
// unanchored compile output before wiring this into Tauri).
|
|
106
|
+
return dirname(process.execPath);
|
|
107
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// pkg-root.mjs tests (DDR-166 T0b).
|
|
2
|
+
//
|
|
3
|
+
// The regression this locks down: a naive single-anchor check
|
|
4
|
+
// (`apps/studio/bin/screenshot.sh` alone) false-positives inside the Tauri
|
|
5
|
+
// desktop build, because `stage-resources.mjs` stages a full working copy of
|
|
6
|
+
// `apps/studio` into `target/debug/apps/studio/` for the sidecar's own
|
|
7
|
+
// runtime resolution — that staged copy has no `cli/`, so a compiled `maude`
|
|
8
|
+
// binary walking up from `target/debug/maude` matched it and stopped one
|
|
9
|
+
// level too early, resolving PKG_ROOT to `target/debug` instead of the real
|
|
10
|
+
// repo root. Caught by live testing against the real Tauri build (not a
|
|
11
|
+
// synthetic path), not by static review — worth a regression test.
|
|
12
|
+
|
|
13
|
+
import assert from 'node:assert/strict';
|
|
14
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { tmpdir } from 'node:os';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { test } from 'node:test';
|
|
18
|
+
|
|
19
|
+
import { isPkgRoot, resolvePkgRoot } from './pkg-root.mjs';
|
|
20
|
+
|
|
21
|
+
function makeRealRoot(base) {
|
|
22
|
+
mkdirSync(join(base, 'apps', 'studio', 'bin'), { recursive: true });
|
|
23
|
+
writeFileSync(join(base, 'apps', 'studio', 'bin', 'screenshot.sh'), '#!/bin/sh\n');
|
|
24
|
+
mkdirSync(join(base, 'cli', 'commands'), { recursive: true });
|
|
25
|
+
writeFileSync(join(base, 'cli', 'commands', 'design.mjs'), 'export const run = () => {};\n');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test('isPkgRoot requires BOTH anchors — the staged-resources false-positive is rejected', () => {
|
|
29
|
+
const tmp = mkdtempSync(join(tmpdir(), 'maude-pkgroot-'));
|
|
30
|
+
try {
|
|
31
|
+
// The exact shape of the bug: apps/studio/bin/screenshot.sh present
|
|
32
|
+
// (mirrors stage-resources.mjs's staged copy), no cli/ at all.
|
|
33
|
+
mkdirSync(join(tmp, 'apps', 'studio', 'bin'), { recursive: true });
|
|
34
|
+
writeFileSync(join(tmp, 'apps', 'studio', 'bin', 'screenshot.sh'), '#!/bin/sh\n');
|
|
35
|
+
assert.equal(isPkgRoot(tmp), false);
|
|
36
|
+
|
|
37
|
+
// cli/ alone, no apps/studio — also not a real root.
|
|
38
|
+
const tmp2 = mkdtempSync(join(tmpdir(), 'maude-pkgroot-'));
|
|
39
|
+
try {
|
|
40
|
+
mkdirSync(join(tmp2, 'cli', 'commands'), { recursive: true });
|
|
41
|
+
writeFileSync(join(tmp2, 'cli', 'commands', 'design.mjs'), '');
|
|
42
|
+
assert.equal(isPkgRoot(tmp2), false);
|
|
43
|
+
} finally {
|
|
44
|
+
rmSync(tmp2, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Both anchors together — a genuine package root.
|
|
48
|
+
makeRealRoot(tmp);
|
|
49
|
+
assert.equal(isPkgRoot(tmp), true);
|
|
50
|
+
} finally {
|
|
51
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('resolvePkgRoot honors MAUDE_PKG_ROOT only when it actually satisfies both anchors', () => {
|
|
56
|
+
const real = mkdtempSync(join(tmpdir(), 'maude-pkgroot-real-'));
|
|
57
|
+
const fake = mkdtempSync(join(tmpdir(), 'maude-pkgroot-fake-'));
|
|
58
|
+
const prevOverride = process.env.MAUDE_PKG_ROOT;
|
|
59
|
+
try {
|
|
60
|
+
makeRealRoot(real);
|
|
61
|
+
// Fake: only the staged-resources-shaped anchor, same false-positive shape.
|
|
62
|
+
mkdirSync(join(fake, 'apps', 'studio', 'bin'), { recursive: true });
|
|
63
|
+
writeFileSync(join(fake, 'apps', 'studio', 'bin', 'screenshot.sh'), '#!/bin/sh\n');
|
|
64
|
+
|
|
65
|
+
process.env.MAUDE_PKG_ROOT = real;
|
|
66
|
+
assert.equal(resolvePkgRoot(), real);
|
|
67
|
+
|
|
68
|
+
process.env.MAUDE_PKG_ROOT = fake;
|
|
69
|
+
// Falls through past the (rejected) override to whatever the real
|
|
70
|
+
// walk-up finds in THIS test process — just assert it's NOT the fake
|
|
71
|
+
// dir, since accepting it would be exactly the regression.
|
|
72
|
+
assert.notEqual(resolvePkgRoot(), fake);
|
|
73
|
+
} finally {
|
|
74
|
+
if (prevOverride === undefined) delete process.env.MAUDE_PKG_ROOT;
|
|
75
|
+
else process.env.MAUDE_PKG_ROOT = prevOverride;
|
|
76
|
+
rmSync(real, { recursive: true, force: true });
|
|
77
|
+
rmSync(fake, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// DDR-174 regression guard (T15).
|
|
2
|
+
//
|
|
3
|
+
// The whole security architecture of `/design:import --reconstruct` rests on
|
|
4
|
+
// ONE fact: every agent turn that ever reads the untrusted source image —
|
|
5
|
+
// `reconstruct-agent` (authoring) AND `reconstruct-critic` (the reality-check
|
|
6
|
+
// comparator) — has `tools: Read, Write, Glob, Grep` and NEVER `Bash`,
|
|
7
|
+
// `WebSearch`, or `WebFetch`. DDR-174's own Consequences section calls for
|
|
8
|
+
// exactly this test: "a targeted grep-based test mirroring
|
|
9
|
+
// plugin-cli-reachability.test.mjs's pattern should assert BOTH the authoring
|
|
10
|
+
// agent's AND the comparator agent's frontmatter never gain
|
|
11
|
+
// Bash/WebSearch/WebFetch, not just this DDR's prose." A future maintainer
|
|
12
|
+
// "helpfully" widening either agent's toolset (e.g. "just give it Bash so it
|
|
13
|
+
// can screenshot itself") would silently reopen DDR-174's entire closure —
|
|
14
|
+
// this fails loudly instead.
|
|
15
|
+
|
|
16
|
+
import assert from 'node:assert/strict';
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
import { test } from 'node:test';
|
|
19
|
+
|
|
20
|
+
const AGENTS = [
|
|
21
|
+
'plugins/design/agents/reconstruct-agent.md',
|
|
22
|
+
'plugins/design/agents/reconstruct-critic.md',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---/;
|
|
26
|
+
|
|
27
|
+
function readFrontmatter(path) {
|
|
28
|
+
const text = readFileSync(path, 'utf8');
|
|
29
|
+
const fm = FRONTMATTER_RE.exec(text);
|
|
30
|
+
assert.ok(fm, `${path}: no frontmatter block found`);
|
|
31
|
+
return fm[1];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readToolsLine(path) {
|
|
35
|
+
const fm = readFrontmatter(path);
|
|
36
|
+
const toolsLine = fm.split('\n').find((l) => l.startsWith('tools:'));
|
|
37
|
+
assert.ok(toolsLine, `${path}: frontmatter has no tools: line`);
|
|
38
|
+
return toolsLine;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const path of AGENTS) {
|
|
42
|
+
test(`${path}: tools: line never grants Bash`, () => {
|
|
43
|
+
const line = readToolsLine(path);
|
|
44
|
+
assert.ok(
|
|
45
|
+
!/\bBash\b/.test(line),
|
|
46
|
+
`${path} declares Bash — this reopens DDR-174's core closure (the agent reads an untrusted image and must never hold a shell). Line: ${line}`
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test(`${path}: tools: line never grants WebSearch or WebFetch`, () => {
|
|
51
|
+
const line = readToolsLine(path);
|
|
52
|
+
assert.ok(
|
|
53
|
+
!/\bWebSearch\b/.test(line) && !/\bWebFetch\b/.test(line),
|
|
54
|
+
`${path} declares WebSearch/WebFetch — DDR-174 requires these agents have no tool call that reaches the network. Line: ${line}`
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test(`${path}: tools: line is exactly Read, Write, Glob, Grep`, () => {
|
|
59
|
+
const line = readToolsLine(path);
|
|
60
|
+
const tools = line
|
|
61
|
+
.replace(/^tools:\s*/, '')
|
|
62
|
+
.split(',')
|
|
63
|
+
.map((t) => t.trim())
|
|
64
|
+
.filter(Boolean);
|
|
65
|
+
assert.deepEqual(
|
|
66
|
+
[...tools].sort(),
|
|
67
|
+
['Glob', 'Grep', 'Read', 'Write'],
|
|
68
|
+
`${path}: expected exactly Read/Write/Glob/Grep per DDR-174 Decision 1, got: ${line}`
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// DDR-174 Addendum (post-implementation adversarial review) — Write has no
|
|
73
|
+
// path restriction (Claude Code has no per-subagent path-scoping mechanism,
|
|
74
|
+
// verified authoritatively, not assumed) and the only real closure for a
|
|
75
|
+
// non-bypassPermissions session is a human approval prompt on any write.
|
|
76
|
+
// `permissionMode: default` is what makes that prompt happen instead of
|
|
77
|
+
// silently inheriting the parent session's (possibly bypassPermissions)
|
|
78
|
+
// mode. A future maintainer removing this line silently re-widens the
|
|
79
|
+
// agent's effective write reach for every downstream consumer NOT running
|
|
80
|
+
// bypassPermissions.
|
|
81
|
+
test(`${path}: frontmatter declares permissionMode: default (DDR-174 Addendum)`, () => {
|
|
82
|
+
const fm = readFrontmatter(path);
|
|
83
|
+
const line = fm.split('\n').find((l) => l.startsWith('permissionMode:'));
|
|
84
|
+
assert.ok(
|
|
85
|
+
line,
|
|
86
|
+
`${path}: missing permissionMode: default — without it this agent silently inherits the parent session's permission mode (e.g. bypassPermissions), losing the one real per-write approval gate available for its unrestricted Write tool (DDR-174 Addendum)`
|
|
87
|
+
);
|
|
88
|
+
assert.equal(
|
|
89
|
+
line.replace(/^permissionMode:\s*/, '').trim(),
|
|
90
|
+
'default',
|
|
91
|
+
`${path}: permissionMode must be exactly "default" (prompts on every write), not a looser mode. Line: ${line}`
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
test('import.md diff-check is whole-repo scoped, not just $DESIGN_ROOT (DDR-174 Addendum)', () => {
|
|
97
|
+
// The first implementation scoped `git status --porcelain -- "$DESIGN_ROOT"`,
|
|
98
|
+
// which made a write to .claude/settings.json, CLAUDE.md, or plugins/**
|
|
99
|
+
// structurally invisible to the check — an adversarial review built a full
|
|
100
|
+
// exploit chain out of exactly that gap. Assert the pathspec was actually
|
|
101
|
+
// widened, not just documented as widened in prose.
|
|
102
|
+
const importMd = readFileSync('plugins/design/commands/import.md', 'utf8');
|
|
103
|
+
assert.ok(
|
|
104
|
+
!/git -C "\$REPO" status --porcelain -- "\$DESIGN_ROOT"/.test(importMd),
|
|
105
|
+
'import.md must NOT scope the diff-check to $DESIGN_ROOT only — this was the exact gap the DDR-174 Addendum closes (a write outside $DESIGN_ROOT, e.g. to .claude/settings.json, would be invisible to a $DESIGN_ROOT-scoped check)'
|
|
106
|
+
);
|
|
107
|
+
const wholeRepoOccurrences = (importMd.match(/git -C "\$REPO" status --porcelain\b/g) || [])
|
|
108
|
+
.length;
|
|
109
|
+
assert.ok(
|
|
110
|
+
wholeRepoOccurrences >= 2,
|
|
111
|
+
`import.md must run an UNSCOPED \`git -C "$REPO" status --porcelain\` for both the BEFORE (step 4) and AFTER (step 5b) snapshots — found ${wholeRepoOccurrences} occurrence(s)`
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('import.md snapshots the two well-known global Claude Code config paths (DDR-174 Addendum)', () => {
|
|
116
|
+
// A repo-scoped git diff can never see $HOME/.claude/** — these are the two
|
|
117
|
+
// specific files an adversarial review named as attack targets (a planted
|
|
118
|
+
// hook, or a poisoned global CLAUDE.md loaded into every future session).
|
|
119
|
+
const importMd = readFileSync('plugins/design/commands/import.md', 'utf8');
|
|
120
|
+
assert.ok(
|
|
121
|
+
importMd.includes('$HOME/.claude/settings.json') &&
|
|
122
|
+
importMd.includes('$HOME/.claude/CLAUDE.md'),
|
|
123
|
+
'import.md must snapshot $HOME/.claude/settings.json and $HOME/.claude/CLAUDE.md before/after each round and hard-fail on any change (DDR-174 Addendum) — these are outside any repo-scoped diff check'
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('import.md stat calls are portable (GNU-first, BSD-fallback), not BSD-only (DDR-174 Addendum confirmation pass)', () => {
|
|
128
|
+
// The first Addendum revision used BSD-only `stat -f` unconditionally — it
|
|
129
|
+
// silently no-ops on Linux (a platform this project ships a .deb for),
|
|
130
|
+
// because GNU `stat -f` means something else entirely and errors out,
|
|
131
|
+
// swallowed by the trailing `2>/dev/null`. Both empty strings compare
|
|
132
|
+
// equal, so the check trivially "passes" without ever having checked
|
|
133
|
+
// anything. Assert every stat call tries GNU syntax (`stat -c`) first.
|
|
134
|
+
const importMd = readFileSync('plugins/design/commands/import.md', 'utf8');
|
|
135
|
+
// Only scan actual shell inside fenced ```bash blocks — prose elsewhere
|
|
136
|
+
// legitimately mentions `stat -f` by name when explaining the old bug.
|
|
137
|
+
const codeBlocks = [...importMd.matchAll(/```bash\n([\s\S]*?)```/g)].map((m) => m[1]).join('\n');
|
|
138
|
+
const bareStatF = codeBlocks.match(/(?<!\|\| )\bstat -f\b/g) || [];
|
|
139
|
+
assert.deepEqual(
|
|
140
|
+
bareStatF,
|
|
141
|
+
[],
|
|
142
|
+
`import.md has a \`stat -f\` call in executable shell with no \`stat -c\` GNU fallback tried first — this silently no-ops on Linux (DDR-174 Addendum confirmation-pass finding). Found ${bareStatF.length} bare occurrence(s).`
|
|
143
|
+
);
|
|
144
|
+
const statCOccurrences = (codeBlocks.match(/stat -c '/g) || []).length;
|
|
145
|
+
assert.ok(
|
|
146
|
+
statCOccurrences >= 4,
|
|
147
|
+
`expected at least 4 GNU-first \`stat -c\` attempts (2 for the non-git repo-wide fallback, 2 for the global-config snapshot) — found ${statCOccurrences}`
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('import.md non-git fallback excludes _history/ (DDR-174 Addendum confirmation pass)', () => {
|
|
152
|
+
// The reconstruct loop writes its own screenshots/verdict files under
|
|
153
|
+
// _history/_reconstruct/<slug>/ every round. A git-repo target gets this
|
|
154
|
+
// excluded for free via .gitignore; the non-git `find`-based fallback has
|
|
155
|
+
// no such filter and would false-positive-abort on its own round-1 output
|
|
156
|
+
// the moment round 2's diff check runs, in a non-git target repo.
|
|
157
|
+
const importMd = readFileSync('plugins/design/commands/import.md', 'utf8');
|
|
158
|
+
const findOccurrences = importMd.match(/find "\$REPO" -type f[^\n]*/g) || [];
|
|
159
|
+
assert.ok(
|
|
160
|
+
findOccurrences.length >= 2,
|
|
161
|
+
'expected the non-git fallback find command in both step 4 and step 5b'
|
|
162
|
+
);
|
|
163
|
+
for (const line of findOccurrences) {
|
|
164
|
+
assert.ok(
|
|
165
|
+
line.includes("-not -path '*/_history/*'"),
|
|
166
|
+
`non-git fallback find command must exclude _history/ (its own round-N artifacts) or every multi-round run in a non-git target repo false-positive-aborts: ${line}`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('import.md uses mktemp for the _active.json patch, not a fixed /tmp filename (DDR-174 Addendum)', () => {
|
|
172
|
+
const importMd = readFileSync('plugins/design/commands/import.md', 'utf8');
|
|
173
|
+
assert.ok(
|
|
174
|
+
!/\/tmp\/active\.json\.tmp/.test(importMd),
|
|
175
|
+
'import.md must not use a fixed, predictable /tmp filename for the _active.json patch — a symlink race target on a shared machine (DDR-174 Addendum adversarial finding)'
|
|
176
|
+
);
|
|
177
|
+
assert.ok(/mktemp/.test(importMd), 'import.md must use mktemp for its temp file(s)');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('design-critic.md is never repurposed as the reconstruct comparator (DDR-174 Decision 1 Round-2)', () => {
|
|
181
|
+
// The comparator must be reconstruct-critic (Bash-free), never the default
|
|
182
|
+
// Bash-capable design-critic — assert import.md's orchestrator spawns the
|
|
183
|
+
// dedicated agent, not design-critic, for the reality-check step.
|
|
184
|
+
const importMd = readFileSync('plugins/design/commands/import.md', 'utf8');
|
|
185
|
+
const reconstructSection = importMd.slice(importMd.indexOf('## `--reconstruct'));
|
|
186
|
+
assert.ok(
|
|
187
|
+
reconstructSection.includes('design:reconstruct-critic'),
|
|
188
|
+
'import.md --reconstruct mode must spawn design:reconstruct-critic for the reality-check step'
|
|
189
|
+
);
|
|
190
|
+
assert.ok(
|
|
191
|
+
!/subagent_type:\s*"design:design-critic"/.test(reconstructSection),
|
|
192
|
+
'import.md --reconstruct mode must NOT spawn the default design-critic as its comparator (DDR-174 Decision 1 Round-2) — it is Bash-capable and reopens the trifecta this DDR closes'
|
|
193
|
+
);
|
|
194
|
+
});
|
package/cli/lib/update-check.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
15
15
|
import { homedir } from 'node:os';
|
|
16
16
|
import { dirname, join } from 'node:path';
|
|
17
17
|
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { isCompiledBinary } from './pkg-root.mjs';
|
|
18
19
|
|
|
19
20
|
const PKG_NAME = '@1agh/maude';
|
|
20
21
|
const REGISTRY_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
|
|
@@ -31,6 +32,13 @@ function shouldSkip() {
|
|
|
31
32
|
if (process.env.NO_UPDATE_NOTIFIER) return true;
|
|
32
33
|
if (process.env.CI) return true;
|
|
33
34
|
if (!process.stderr.isTTY) return true;
|
|
35
|
+
// DDR-166 T0b — a Tauri-bundled `maude` binary updates via the app's own
|
|
36
|
+
// auto-updater (DDR-126), not `npm i -g @1agh/maude@latest`; the notice
|
|
37
|
+
// would be actively wrong advice. Also sidesteps spawnDetachedRefresh()'s
|
|
38
|
+
// self-respawn, which re-invokes `import.meta.url` as a script path — a
|
|
39
|
+
// real file:// path in dev/node, but Bun's virtual `/$bunfs/root` inside
|
|
40
|
+
// this exact compiled binary (DDR-045-class trap).
|
|
41
|
+
if (isCompiledBinary()) return true;
|
|
34
42
|
return false;
|
|
35
43
|
}
|
|
36
44
|
|