@1agh/maude 0.36.0 → 0.37.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.
@@ -107,6 +107,7 @@ export interface GitHubEndpoints {
107
107
  repos(): Promise<GitHubEndpointResult>;
108
108
  createRepo(body: unknown): Promise<GitHubEndpointResult>;
109
109
  createProject(body: unknown): Promise<GitHubEndpointResult>;
110
+ createLocalProject(body: unknown): Promise<GitHubEndpointResult>;
110
111
  invite(body: unknown): Promise<GitHubEndpointResult>;
111
112
  clone(body: unknown): Promise<GitHubEndpointResult>;
112
113
  initDesign(body: unknown): Promise<GitHubEndpointResult>;
@@ -346,6 +347,46 @@ export function createGitHubEndpoints(ctx: Context): GitHubEndpoints {
346
347
  });
347
348
  }
348
349
 
350
+ // "Just a local git repo, no GitHub remote" — same as createProject minus the
351
+ // network: mkdir + git init (defaultBranch main) + .design scaffold, NO token, NO
352
+ // createRepo, NO setRemote. The user can publish it later. Reuses the same slug +
353
+ // parent-dir validation as createProject, and the route is main-origin/loopback
354
+ // gated (http.ts) + absent from CANVAS_SAFE_API (dual-allowlist) like its sibling.
355
+ async function createLocalProject(body: unknown): Promise<GitHubEndpointResult> {
356
+ const b = (body ?? {}) as { name?: unknown; parentDir?: unknown };
357
+ const slug = slugifyRepoName(b.name);
358
+ if (!slug) return bad('Enter a project name (letters, numbers, hyphens).');
359
+ if (!validParentDir(b.parentDir))
360
+ return bad('Pick a folder on your computer to save the project in.');
361
+ const target = join(b.parentDir, slug);
362
+ if (existsSync(target)) {
363
+ return {
364
+ status: 409,
365
+ json: { ok: false, error: `A folder named “${slug}” already exists there.` },
366
+ };
367
+ }
368
+ const humanName = typeof b.name === 'string' ? b.name.trim() : slug;
369
+ try {
370
+ mkdirSync(target, { recursive: true });
371
+ await git.init({ fs, dir: target, defaultBranch: 'main' });
372
+ const s = scaffoldDesign(target, humanName);
373
+ if (!s.ok)
374
+ return {
375
+ status: 500,
376
+ json: { ok: false, error: s.error ?? 'Could not set up the project.' },
377
+ };
378
+ } catch (e) {
379
+ return {
380
+ status: 500,
381
+ json: {
382
+ ok: false,
383
+ error: e instanceof Error ? e.message : 'Could not set up the project.',
384
+ },
385
+ };
386
+ }
387
+ return { status: 200, json: { ok: true, path: target } };
388
+ }
389
+
349
390
  // Fallback for "open a folder/repo that isn't a Maude project yet" — scaffold a
350
391
  // bootable .design/ into an existing folder so it can open (no GitHub, no token).
351
392
  async function initDesign(body: unknown): Promise<GitHubEndpointResult> {
@@ -368,6 +409,7 @@ export function createGitHubEndpoints(ctx: Context): GitHubEndpoints {
368
409
  repos,
369
410
  createRepo: createRepoHandler,
370
411
  createProject,
412
+ createLocalProject,
371
413
  invite,
372
414
  clone,
373
415
  initDesign,
@@ -1050,6 +1050,21 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
1050
1050
  return gitJson(await githubApi.createProject(body));
1051
1051
  },
1052
1052
 
1053
+ // Create a NEW *local-only* project: mkdir + git init + .design scaffold, NO
1054
+ // GitHub / no token (the "just local git, no remote" path). Same main-origin +
1055
+ // loopback + POST CSRF gate as create-project, and MAIN-ORIGIN ONLY — absent
1056
+ // from CANVAS_SAFE_API + startCanvasServer routes (dual-allowlist), so the
1057
+ // untrusted canvas iframe can't drive disk writes.
1058
+ '/_api/project/create-local': async (req: Request) => {
1059
+ if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
1060
+ if (!sameOriginWrite(req))
1061
+ return new Response('cross-origin write rejected', { status: 403 });
1062
+ if (!isLoopbackHost(req.headers.get('host')))
1063
+ return new Response('local request required', { status: 403 });
1064
+ const body = await readJson<unknown>(req, 8 * 1024);
1065
+ return gitJson(await githubApi.createLocalProject(body));
1066
+ },
1067
+
1053
1068
  // Scaffold a bootable .design/ into an existing folder (the "open a non-Maude
1054
1069
  // repo → set it up?" fallback). No token / no GitHub — local FS only, but still
1055
1070
  // main-origin + loopback gated (it writes to disk).
@@ -13,6 +13,93 @@ import { basename, join } from 'node:path';
13
13
  const CONFIG_SCHEMA =
14
14
  'https://raw.githubusercontent.com/1aGh/maude/main/apps/studio/config.schema.json';
15
15
 
16
+ // A neutral, token-free starter canvas seeded into `ui/` so a freshly-created project
17
+ // opens to a real artboard instead of an empty studio (the "empty-studio after Start a
18
+ // new project" onboarding gap). It depends ONLY on @maude/canvas-lib + inline styles —
19
+ // no design-system tokens — so it renders before /design:setup-ds exists. The button
20
+ // vocabulary mirrors the shipped GitPanel (Save version · Publish · Get latest).
21
+ const STARTER_CANVAS_TSX = `/**
22
+ * @canvas welcome · Your first canvas — seeded when a new Maude project is created.
23
+ * @platform desktop
24
+ * @stack React 19 · TSX · Bun.build
25
+ *
26
+ * Neutral + token-free so it renders the moment the project is created — before
27
+ * /design:setup-ds builds a design system. Edit it, replace it, or delete it once you
28
+ * have your own canvases.
29
+ */
30
+ import { DCArtboard, DCSection, DesignCanvas } from "@maude/canvas-lib";
31
+
32
+ export default function Welcome() {
33
+ return (
34
+ <DesignCanvas>
35
+ <DCSection id="welcome" title="Welcome to Maude" subtitle="START HERE">
36
+ <DCArtboard id="welcome" label="WELCOME/01" width={680} height={440}>
37
+ <div
38
+ data-testid="welcome-artboard-content"
39
+ style={{
40
+ height: "100%",
41
+ boxSizing: "border-box",
42
+ padding: 48,
43
+ display: "flex",
44
+ flexDirection: "column",
45
+ gap: 24,
46
+ fontFamily: "system-ui, -apple-system, sans-serif",
47
+ color: "#16181d",
48
+ background: "#fbfbfc",
49
+ }}
50
+ >
51
+ <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
52
+ <span style={{ fontSize: 12, letterSpacing: "0.12em", textTransform: "uppercase", color: "#8a8f98" }}>
53
+ Your first canvas
54
+ </span>
55
+ <h1 style={{ margin: 0, fontSize: 30, lineHeight: 1.1, fontWeight: 650 }}>
56
+ You're in. This is a canvas.
57
+ </h1>
58
+ <p style={{ margin: 0, fontSize: 15, lineHeight: 1.5, color: "#4a4f57", maxWidth: 460 }}>
59
+ Everything you design lives on canvases like this one. Edit it, or start fresh —
60
+ your work saves on its own, and you can version and share it without ever opening a terminal.
61
+ </p>
62
+ </div>
63
+ <div style={{ display: "flex", gap: 12 }}>
64
+ {[
65
+ ["Save version", "keep a checkpoint, just for you"],
66
+ ["Publish", "share it with your team"],
67
+ ["Get latest", "pull in everyone else's work"],
68
+ ].map(([title, desc]) => (
69
+ <div key={title} style={{ flex: 1, padding: 16, border: "1px solid #e6e7ea", borderRadius: 10, background: "#fff" }}>
70
+ <div style={{ fontSize: 14, fontWeight: 600, marginBottom: 4 }}>{title}</div>
71
+ <div style={{ fontSize: 12.5, lineHeight: 1.4, color: "#6b7078" }}>{desc}</div>
72
+ </div>
73
+ ))}
74
+ </div>
75
+ <p style={{ margin: 0, fontSize: 13, lineHeight: 1.5, color: "#8a8f98" }}>
76
+ Next: build a design system, or just start editing this canvas.
77
+ </p>
78
+ </div>
79
+ </DCArtboard>
80
+ </DCSection>
81
+ </DesignCanvas>
82
+ );
83
+ }
84
+ `;
85
+
86
+ const STARTER_CANVAS_META = {
87
+ title: 'Welcome',
88
+ subtitle: 'Your first canvas — seeded on project creation',
89
+ brief:
90
+ 'Neutral, token-free welcome canvas written by scaffoldDesign() so a new project opens to a real artboard instead of an empty studio. Safe to edit or delete.',
91
+ platform: 'desktop',
92
+ sections: [
93
+ {
94
+ id: 'welcome',
95
+ title: 'Welcome to Maude · START HERE',
96
+ artboards: [{ id: 'welcome', label: 'WELCOME/01', width: 680, height: 440 }],
97
+ },
98
+ ],
99
+ css_mode: 'inline',
100
+ layout: { artboards: [{ id: 'welcome', x: 0, y: 0 }] },
101
+ };
102
+
16
103
  /** Whether `dir` is already a Maude project (has `.design/config.json`). */
17
104
  export function hasDesign(dir: string): boolean {
18
105
  return existsSync(join(dir, '.design', 'config.json'));
@@ -47,8 +134,14 @@ export function scaffoldDesign(dir: string, name?: string): ScaffoldResult {
47
134
  completenessProfile: 'standard',
48
135
  };
49
136
  writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
50
- // Keep the empty group dirs in git so a fresh clone still has the structure.
51
- writeFileSync(join(designDir, 'ui', '.gitkeep'), '', 'utf8');
137
+ // Seed a starter canvas so the studio opens to a real artboard, not an empty list.
138
+ writeFileSync(join(designDir, 'ui', 'Welcome.tsx'), STARTER_CANVAS_TSX, 'utf8');
139
+ writeFileSync(
140
+ join(designDir, 'ui', 'Welcome.meta.json'),
141
+ `${JSON.stringify(STARTER_CANVAS_META, null, 2)}\n`,
142
+ 'utf8'
143
+ );
144
+ // `system/` is genuinely empty until /design:setup-ds — keep it in git.
52
145
  writeFileSync(join(designDir, 'system', '.gitkeep'), '', 'utf8');
53
146
  return { ok: true };
54
147
  } catch (e) {
@@ -99,6 +99,9 @@ describe('canvas-origin gate — A1/A2 traversal + privilege containment', () =>
99
99
  '/_api/github/invite',
100
100
  '/_api/github/clone',
101
101
  '/_api/github/create-project',
102
+ // Local-only project create (mkdir + git init + scaffold) — writes to disk,
103
+ // no token; MAIN-ORIGIN ONLY, so the canvas origin must 403 at the gate.
104
+ '/_api/project/create-local',
102
105
  '/_api/design/init',
103
106
  // Phase 29 (E4) Door C — the hub-link credential write is MAIN-ORIGIN ONLY;
104
107
  // the untrusted canvas origin must never reach it (it writes the global
@@ -0,0 +1,119 @@
1
+ import { beforeAll, describe, expect, mock, test } from 'bun:test';
2
+
3
+ import {
4
+ buildEditSourceRecord,
5
+ createEditSourceCommand,
6
+ EDIT_SOURCE_KIND,
7
+ type EditSourceApplyFn,
8
+ type EditSourcePayload,
9
+ } from '../commands/edit-source-command.ts';
10
+ import { type CommandSinks, rebuildCommand, registerCommand } from '../undo-stack.ts';
11
+
12
+ const cssEdit: EditSourcePayload = {
13
+ op: 'css',
14
+ canvas: '.design/ui/Foo.tsx',
15
+ id: 'cd-1',
16
+ key: 'color',
17
+ before: 'red',
18
+ after: 'blue',
19
+ };
20
+
21
+ describe('createEditSourceCommand', () => {
22
+ test('do() applies the AFTER value', async () => {
23
+ const applyFn = mock(() => {});
24
+ const cmd = createEditSourceCommand({ payload: cssEdit, applyFn });
25
+ await cmd.do();
26
+ expect(applyFn).toHaveBeenCalledTimes(1);
27
+ expect(applyFn.mock.calls[0]?.[0]).toEqual({
28
+ op: 'css',
29
+ canvas: '.design/ui/Foo.tsx',
30
+ id: 'cd-1',
31
+ key: 'color',
32
+ value: 'blue',
33
+ });
34
+ });
35
+
36
+ test('undo() applies the BEFORE value', async () => {
37
+ const applyFn = mock(() => {});
38
+ const cmd = createEditSourceCommand({ payload: cssEdit, applyFn });
39
+ await cmd.undo();
40
+ expect(applyFn.mock.calls[0]?.[0]?.value).toBe('red');
41
+ });
42
+
43
+ test('undo() of a NEW prop (before:null) applies null = reset', async () => {
44
+ const applyFn = mock(() => {});
45
+ const cmd = createEditSourceCommand({
46
+ payload: { ...cssEdit, before: null, after: 'blue' },
47
+ applyFn,
48
+ });
49
+ await cmd.undo();
50
+ expect(applyFn.mock.calls[0]?.[0]?.value).toBeNull();
51
+ });
52
+
53
+ test('do() of a RESET (after:null) applies null = remove the prop', async () => {
54
+ const applyFn = mock(() => {});
55
+ const cmd = createEditSourceCommand({
56
+ payload: { ...cssEdit, before: 'red', after: null },
57
+ applyFn,
58
+ });
59
+ await cmd.do();
60
+ expect(applyFn.mock.calls[0]?.[0]?.value).toBeNull();
61
+ });
62
+
63
+ test('label reflects op + key', () => {
64
+ expect(createEditSourceCommand({ payload: cssEdit, applyFn: () => {} }).label).toBe(
65
+ 'edit color'
66
+ );
67
+ expect(
68
+ createEditSourceCommand({ payload: { ...cssEdit, after: null }, applyFn: () => {} }).label
69
+ ).toBe('reset color');
70
+ expect(
71
+ createEditSourceCommand({
72
+ payload: { op: 'attr', canvas: 'x', id: 'y', key: 'data-x', before: null, after: '1' },
73
+ applyFn: () => {},
74
+ }).label
75
+ ).toBe('edit @data-x');
76
+ expect(
77
+ createEditSourceCommand({
78
+ payload: { op: 'text', canvas: 'x', id: 'y', key: '', before: 'a', after: 'b' },
79
+ applyFn: () => {},
80
+ }).label
81
+ ).toBe('edit text');
82
+ });
83
+ });
84
+
85
+ describe('buildEditSourceRecord', () => {
86
+ test('produces a serializable record under the edit-source kind', () => {
87
+ const rec = buildEditSourceRecord(cssEdit);
88
+ expect(rec.kind).toBe(EDIT_SOURCE_KIND);
89
+ expect(rec.payload).toEqual(cssEdit);
90
+ expect(JSON.parse(JSON.stringify(rec))).toEqual(rec); // round-trips
91
+ });
92
+ });
93
+
94
+ describe('edit-source registry', () => {
95
+ // The module self-registers on import, but sibling test files wipe the shared
96
+ // builder registry in their beforeEach — re-register here so this block is
97
+ // order-independent. Mirrors the real registration in edit-source-command.ts.
98
+ beforeAll(() => {
99
+ registerCommand<EditSourcePayload>(EDIT_SOURCE_KIND, (record, sinks) => {
100
+ const applyFn = sinks.editSourceApplyFn as EditSourceApplyFn | undefined;
101
+ if (!applyFn) return null;
102
+ return createEditSourceCommand({ payload: record.payload, applyFn, label: record.label });
103
+ });
104
+ });
105
+
106
+ test('rebuildCommand returns null when the editSourceApplyFn sink is unbound', () => {
107
+ expect(rebuildCommand(buildEditSourceRecord(cssEdit), {} as CommandSinks)).toBeNull();
108
+ });
109
+
110
+ test('rebuildCommand wires the bound sink into a runnable command', async () => {
111
+ const applyFn = mock(() => {});
112
+ const cmd = rebuildCommand(buildEditSourceRecord(cssEdit), {
113
+ editSourceApplyFn: applyFn,
114
+ } as CommandSinks);
115
+ expect(cmd).not.toBeNull();
116
+ await cmd?.do();
117
+ expect(applyFn.mock.calls[0]?.[0]?.value).toBe('blue');
118
+ });
119
+ });
@@ -190,14 +190,21 @@ describe('CanvasSyncAgent — cold start reconciliation', () => {
190
190
  agent = makeAgent({ adopt: true });
191
191
  await agent.reconcile();
192
192
 
193
- // Hub changes the value.
193
+ // Hub changes the value — stamp it explicitly newer than the disk so the
194
+ // newest-wins reconcile is deterministic. An un-pinned `applyHtmlToDoc` stamp
195
+ // is `Date.now()` (ms-truncated) while the disk mtime is sub-ms `statSync`:
196
+ // in isolation they land in the same ms → tie → hub; under parallel-suite
197
+ // load they straddle a ms boundary → disk looks newer → local, flaking this
198
+ // assertion. Sibling tests pin timestamps for exactly this reason.
194
199
  applyHtmlToDoc(docA, '<button>hub-v2</button>');
200
+ docA.getMap('syncMeta').set('bodyEditAt', Date.now() + 60_000);
195
201
 
196
202
  // Disk reverts to a stale local-only state.
197
203
  writeFileSync(paths().html, '<button>local-v3</button>');
198
204
  await agent.reconcile();
199
205
 
200
- // Hub state won this time.
206
+ // Hub state won this time — adopt was consumed, so the second reconcile runs
207
+ // normal newest-wins, and the (explicitly newer) hub body wins.
201
208
  expect(htmlFromDoc(docB)).toBe('<button>hub-v2</button>');
202
209
  });
203
210
 
@@ -155,6 +155,36 @@ describe('use-undo-stack / runner side-effects', () => {
155
155
  });
156
156
  });
157
157
 
158
+ describe('use-undo-stack / record (already-applied edits)', () => {
159
+ test('record() appends WITHOUT running do()', async () => {
160
+ const v = captureProvider(undefined, { layoutPatchFn: () => {} });
161
+ v.record(rec('inline-edit', 'X'));
162
+ // undo() is enqueued after record(), so awaiting it flushes record first.
163
+ await v.undo();
164
+ expect(doSpy).toHaveBeenCalledTimes(0); // record never runs do()
165
+ expect(undoSpy).toHaveBeenCalledTimes(1);
166
+ expect(undoSpy.mock.calls[0]?.[0]).toBe('X');
167
+ });
168
+
169
+ test('record() then redo() (after undo) re-runs do()', async () => {
170
+ const v = captureProvider(undefined, { layoutPatchFn: () => {} });
171
+ v.record(rec('e', 'Y'));
172
+ await v.undo();
173
+ await v.redo();
174
+ expect(doSpy).toHaveBeenCalledTimes(1); // only redo's do(), not the record
175
+ expect(doSpy.mock.calls[0]?.[0]).toBe('Y');
176
+ });
177
+
178
+ test('record() clears the redo future', async () => {
179
+ const v = captureProvider(undefined, { layoutPatchFn: () => {} });
180
+ await v.push(rec('a', 'A')); // doSpy: 1
181
+ await v.undo(); // future = [a]
182
+ v.record(rec('b', 'B')); // clears future
183
+ await v.redo(); // future empty → no-op, no extra do()
184
+ expect(doSpy).toHaveBeenCalledTimes(1);
185
+ });
186
+ });
187
+
158
188
  describe('use-undo-stack / cross-canvas persistence', () => {
159
189
  test('history under a canvasFile survives a fresh provider mount with the same canvasFile', async () => {
160
190
  // First mount: push 2 records.
@@ -71,6 +71,16 @@ export interface CommandSinks {
71
71
  layoutPatchFn?: unknown;
72
72
  /** Wired by `AnnotationsLayer`. */
73
73
  strokesPutFn?: unknown;
74
+ /**
75
+ * Wired by `CanvasShell` (canvas-shell.tsx). Applies a single inline source
76
+ * edit (CSS / text / attr) by posting `dgn:'apply-edit'` to the parent shell,
77
+ * which owns the main-origin-only `/_api/edit-*` write (DDR-054 — the canvas
78
+ * iframe can't call those routes directly). Drives undo/redo of inspector
79
+ * CSS edits, inline text rewrites, and custom-attribute edits, which write
80
+ * the canvas `.tsx` source and were previously unrecoverable (no command, no
81
+ * snapshot). See `commands/edit-source-command.ts`.
82
+ */
83
+ editSourceApplyFn?: unknown;
74
84
  }
75
85
 
76
86
  export type CommandBuilder<P = unknown> = (
@@ -51,6 +51,14 @@ export interface UndoStackValue {
51
51
  * Awaiting the returned promise is optional.
52
52
  */
53
53
  push: (record: CommandRecord) => Promise<void>;
54
+ /**
55
+ * Record an ALREADY-APPLIED command — appends to `past` + clears `future`
56
+ * WITHOUT running `do()`. Use when the side-effect happened outside the
57
+ * stack (e.g. an inspector CSS commit / inline text edit that already POSTed
58
+ * `/_api/edit-*`), so re-running `do()` would double-apply. `undo()` / `redo()`
59
+ * still drive the command's `undo()` / `do()` normally afterwards.
60
+ */
61
+ record: (record: CommandRecord) => void;
54
62
  /** Undo the top of `past`. No-op when empty. */
55
63
  undo: () => Promise<void>;
56
64
  /** Redo the top of `future`. No-op when empty. */
@@ -69,6 +77,7 @@ const UndoStackContext = createContext<UndoStackValue | null>(null);
69
77
 
70
78
  const NOOP_VALUE: UndoStackValue = {
71
79
  push: () => Promise.resolve(),
80
+ record: () => {},
72
81
  undo: () => Promise.resolve(),
73
82
  redo: () => Promise.resolve(),
74
83
  clear: () => {},
@@ -213,6 +222,16 @@ export function UndoStackProvider({
213
222
  [enqueue, build, reportError, bumpLabel, writeState]
214
223
  );
215
224
 
225
+ const record = useCallback(
226
+ (rec: CommandRecord): void => {
227
+ void enqueue(async () => {
228
+ writeState(undoReducer(stateRef.current, { type: 'push', record: rec }));
229
+ bumpLabel(rec.label);
230
+ });
231
+ },
232
+ [enqueue, writeState, bumpLabel]
233
+ );
234
+
216
235
  const undo = useCallback(
217
236
  (): Promise<void> =>
218
237
  enqueue(async () => {
@@ -306,6 +325,7 @@ export function UndoStackProvider({
306
325
  const value = useMemo<UndoStackValue>(
307
326
  () => ({
308
327
  push,
328
+ record,
309
329
  undo,
310
330
  redo,
311
331
  clear,
@@ -318,7 +338,7 @@ export function UndoStackProvider({
318
338
  // `lastTick` (bumped by every push/undo/redo/clear) as the proxy so the
319
339
  // memoized canUndo/canRedo readouts re-evaluate on each transition.
320
340
  // eslint-disable-next-line react-hooks/exhaustive-deps
321
- [push, undo, redo, clear, lastLabel, lastTick]
341
+ [push, record, undo, redo, clear, lastLabel, lastTick]
322
342
  );
323
343
 
324
344
  return (
@@ -1,6 +1,24 @@
1
1
  {
2
2
  "$schema": "./whats-new.schema.json",
3
3
  "entries": [
4
+ {
5
+ "id": "new-project-menu-and-local",
6
+ "version": "0.37.0",
7
+ "date": "2026-07-01",
8
+ "kind": "feature",
9
+ "title": "Start a project from the menu — or fully local",
10
+ "summary": "New Project… is now in the File menu (⌘N), not just onboarding. And you can create a project on This computer only — a plain local git repo with no GitHub and no remote, that you can publish later. It works even when you're signed out.",
11
+ "surface": "design-ui"
12
+ },
13
+ {
14
+ "id": "desktop-onboarding-fixes",
15
+ "version": "0.36.2",
16
+ "date": "2026-06-30",
17
+ "kind": "fix",
18
+ "title": "A smoother first run",
19
+ "summary": "Onboarding got a polish. If you're already signed in, the GitHub step now says Continue instead of asking you to sign in again, and your signed-in status shows reliably. Cancelling the sign-in code screen frees the button right away, “Merge this branch → main” shows progress while it runs, and a brand-new project opens to a starter canvas instead of an empty workspace.",
20
+ "surface": "design-ui"
21
+ },
4
22
  {
5
23
  "id": "git-native-switcher",
6
24
  "version": "0.36.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1agh/maude",
3
- "version": "0.36.0",
3
+ "version": "0.37.0",
4
4
  "description": "Marketplace of Claude Code plugins by Michal Dovrtěl: `design` (canvas-first design iteration) + `flow` (generic agentic workflow loop with .ai second brain). Ships the `maude` CLI (with `mdcc` legacy alias) to scaffold workspace, run the design dev server, and manage configs.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -21,6 +21,7 @@
21
21
  "dev": "bun run apps/studio/server.ts --port 4555",
22
22
  "dev:site": "pnpm --filter @maude/site dev --port 4398",
23
23
  "dev:desktop": "PATH=\"$HOME/.cargo/bin:$PATH\" pnpm --filter @maude/desktop tauri dev",
24
+ "dev:desktop:onboarding": "PATH=\"$HOME/.cargo/bin:$PATH\" MAUDE_FORCE_ONBOARDING=1 pnpm --filter @maude/desktop tauri dev",
24
25
  "build:desktop": "PATH=\"$HOME/.cargo/bin:$PATH\" pnpm --filter @maude/desktop tauri build",
25
26
  "test:e2e:desktop:build": "PATH=\"$HOME/.cargo/bin:$PATH\" pnpm --filter @maude/desktop tauri build --debug --config src-tauri/tauri.e2e.conf.json",
26
27
  "test:e2e:desktop": "pnpm --filter @maude/desktop-e2e e2e",
@@ -45,16 +46,17 @@
45
46
  "postinstall": "node cli/install.cjs",
46
47
  "prepublishOnly": "bash scripts/check-version-parity.sh && bash apps/studio/bin/check-runtime-bundles.sh",
47
48
  "test:e2e:desktop:lifecycle": "pnpm --filter @maude/desktop-e2e e2e:lifecycle",
48
- "test:e2e:desktop:switchrepos": "pnpm --filter @maude/desktop-e2e e2e:switchrepos"
49
+ "test:e2e:desktop:switchrepos": "pnpm --filter @maude/desktop-e2e e2e:switchrepos",
50
+ "test:e2e:desktop:onboarding": "pnpm --filter @maude/desktop-e2e e2e:onboarding"
49
51
  },
50
52
  "optionalDependencies": {
51
- "@1agh/maude-darwin-arm64": "0.36.0",
52
- "@1agh/maude-darwin-x64": "0.36.0",
53
- "@1agh/maude-linux-arm64": "0.36.0",
54
- "@1agh/maude-linux-arm64-musl": "0.36.0",
55
- "@1agh/maude-linux-x64": "0.36.0",
56
- "@1agh/maude-linux-x64-musl": "0.36.0",
57
- "@1agh/maude-win32-x64": "0.36.0"
53
+ "@1agh/maude-darwin-arm64": "0.37.0",
54
+ "@1agh/maude-darwin-x64": "0.37.0",
55
+ "@1agh/maude-linux-arm64": "0.37.0",
56
+ "@1agh/maude-linux-arm64-musl": "0.37.0",
57
+ "@1agh/maude-linux-x64": "0.37.0",
58
+ "@1agh/maude-linux-x64-musl": "0.37.0",
59
+ "@1agh/maude-win32-x64": "0.37.0"
58
60
  },
59
61
  "files": [
60
62
  "cli",