@1agh/maude 0.27.0 → 0.28.1
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/cli/commands/design-link.test.mjs +46 -0
- package/cli/commands/design.mjs +69 -3
- package/cli/commands/design.test.mjs +97 -1
- package/cli/commands/doctor.mjs +110 -5
- package/cli/commands/doctor.test.mjs +52 -0
- package/cli/lib/cache.mjs +1 -1
- package/cli/lib/config-lint.mjs +1 -1
- package/cli/lib/copy-tree.mjs +1 -1
- package/cli/lib/design-link.mjs +39 -9
- package/cli/lib/flow-design-integration.test.mjs +1 -1
- package/cli/lib/gitignore-block.test.mjs +2 -2
- package/cli/lib/stack-detect.mjs +1 -1
- package/package.json +9 -9
- package/plugins/design/dev-server/activity.ts +256 -0
- package/plugins/design/dev-server/ai-banner.tsx +4 -0
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +1 -1
- package/plugins/design/dev-server/annotations-layer.tsx +3 -3
- package/plugins/design/dev-server/api.ts +1 -1
- package/plugins/design/dev-server/artboard-activity-overlay.tsx +67 -0
- package/plugins/design/dev-server/bin/scenario-report.mjs +1 -1
- package/plugins/design/dev-server/bin/server-up.sh +102 -9
- package/plugins/design/dev-server/bin/visual-sanity.sh +19 -3
- package/plugins/design/dev-server/canvas-comment-mount.tsx +165 -10
- package/plugins/design/dev-server/canvas-cursors.ts +1 -1
- package/plugins/design/dev-server/canvas-lib.tsx +72 -36
- package/plugins/design/dev-server/canvas-shell.tsx +1 -1
- package/plugins/design/dev-server/collab/awareness-bridge.ts +1 -1
- package/plugins/design/dev-server/collab/index.ts +4 -4
- package/plugins/design/dev-server/collab/persistence.ts +1 -1
- package/plugins/design/dev-server/config.schema.json +2 -1
- package/plugins/design/dev-server/context-menu.tsx +1 -1
- package/plugins/design/dev-server/dist/client.bundle.js +19 -19
- package/plugins/design/dev-server/dist/comment-mount.js +84 -6
- package/plugins/design/dev-server/dist/runtime/lib0_decoding.js +1 -620
- package/plugins/design/dev-server/dist/runtime/lib0_encoding.js +1 -604
- package/plugins/design/dev-server/dist/runtime/motion.js +4 -4944
- package/plugins/design/dev-server/dist/runtime/motion_react.js +10 -10054
- package/plugins/design/dev-server/dist/runtime/pixi-js.js +2775 -52482
- package/plugins/design/dev-server/dist/runtime/react-dom.js +1 -231
- package/plugins/design/dev-server/dist/runtime/react-dom_client.js +9 -10357
- package/plugins/design/dev-server/dist/runtime/react.js +1 -535
- package/plugins/design/dev-server/dist/runtime/react_jsx-dev-runtime.js +1 -60
- package/plugins/design/dev-server/dist/runtime/react_jsx-runtime.js +1 -85
- package/plugins/design/dev-server/dist/runtime/y-protocols_awareness.js +1 -376
- package/plugins/design/dev-server/dist/runtime/y-protocols_sync.js +1 -100
- package/plugins/design/dev-server/dist/runtime/yjs.js +5 -6687
- package/plugins/design/dev-server/draw/brush.ts +3 -3
- package/plugins/design/dev-server/draw/index.ts +5 -5
- package/plugins/design/dev-server/draw/layout.ts +1 -1
- package/plugins/design/dev-server/draw/test/geometry.test.ts +1 -1
- package/plugins/design/dev-server/draw/test/layout.test.ts +1 -1
- package/plugins/design/dev-server/draw/test/primitives.test.ts +1 -1
- package/plugins/design/dev-server/draw/test/serialize.test.ts +1 -1
- package/plugins/design/dev-server/equal-spacing-handles.tsx +1 -1
- package/plugins/design/dev-server/examples/perf-100-artboards.tsx +1 -2
- package/plugins/design/dev-server/export-dialog.tsx +1 -1
- package/plugins/design/dev-server/exporters/html.ts +1 -1
- package/plugins/design/dev-server/exporters/index.ts +2 -2
- package/plugins/design/dev-server/exporters/pdf.ts +1 -1
- package/plugins/design/dev-server/exporters/png.ts +1 -1
- package/plugins/design/dev-server/exporters/pptx.ts +1 -1
- package/plugins/design/dev-server/exporters/svg.ts +1 -1
- package/plugins/design/dev-server/http.ts +1 -1
- package/plugins/design/dev-server/inspect.ts +31 -1
- package/plugins/design/dev-server/marquee-overlay.tsx +1 -1
- package/plugins/design/dev-server/participants-chrome.tsx +86 -1
- package/plugins/design/dev-server/server.ts +11 -2
- package/plugins/design/dev-server/sync/agent.ts +1 -1
- package/plugins/design/dev-server/sync/index.ts +29 -24
- package/plugins/design/dev-server/sync/materialize.ts +2 -2
- package/plugins/design/dev-server/sync/migrate-seed.ts +1 -1
- package/plugins/design/dev-server/sync/projection.ts +3 -3
- package/plugins/design/dev-server/test/activity.test.ts +195 -0
- package/plugins/design/dev-server/test/ai-activity.test.ts +1 -1
- package/plugins/design/dev-server/test/annotations-api.test.ts +1 -2
- package/plugins/design/dev-server/test/annotations-draw-modifiers.test.ts +2 -2
- package/plugins/design/dev-server/test/annotations-layer.test.ts +6 -6
- package/plugins/design/dev-server/test/annotations-roundtrip.test.ts +1 -1
- package/plugins/design/dev-server/test/artboard-activity-overlay.test.tsx +56 -0
- package/plugins/design/dev-server/test/boot-self-heal.test.ts +1 -1
- package/plugins/design/dev-server/test/canvas-cursors.test.ts +1 -1
- package/plugins/design/dev-server/test/canvas-edit.test.ts +1 -1
- package/plugins/design/dev-server/test/canvas-header.test.ts +1 -1
- package/plugins/design/dev-server/test/canvas-hmr-runtime.test.tsx +114 -0
- package/plugins/design/dev-server/test/canvas-lib-inline.test.ts +1 -1
- package/plugins/design/dev-server/test/canvas-lib-resolver.test.ts +1 -2
- package/plugins/design/dev-server/test/canvas-meta-api.test.ts +1 -2
- package/plugins/design/dev-server/test/canvas-origin-gate.test.ts +1 -2
- package/plugins/design/dev-server/test/collab-annotations-bridge.test.ts +1 -1
- package/plugins/design/dev-server/test/collab-bridge.test.ts +1 -1
- package/plugins/design/dev-server/test/collab-protocol.test.ts +2 -2
- package/plugins/design/dev-server/test/collab-room.test.ts +1 -1
- package/plugins/design/dev-server/test/collab-stress.test.ts +1 -1
- package/plugins/design/dev-server/test/comments-api.test.ts +1 -2
- package/plugins/design/dev-server/test/compile-entry.test.ts +1 -2
- package/plugins/design/dev-server/test/exporters/canva.test.ts +1 -2
- package/plugins/design/dev-server/test/exporters/history.test.ts +1 -2
- package/plugins/design/dev-server/test/exporters/pw-launch.test.ts +1 -1
- package/plugins/design/dev-server/test/exporters/scope.test.ts +1 -2
- package/plugins/design/dev-server/test/exporters/zip.test.ts +1 -2
- package/plugins/design/dev-server/test/git-lifecycle.test.ts +1 -2
- package/plugins/design/dev-server/test/handoff-static-frames.test.ts +1 -1
- package/plugins/design/dev-server/test/handoff.test.ts +1 -1
- package/plugins/design/dev-server/test/hmr-broadcast.test.ts +1 -1
- package/plugins/design/dev-server/test/input-router.test.ts +2 -2
- package/plugins/design/dev-server/test/locator.test.ts +1 -1
- package/plugins/design/dev-server/test/runtime-bundle.test.ts +1 -1
- package/plugins/design/dev-server/test/shared-doc-convergence.test.ts +1 -1
- package/plugins/design/dev-server/test/shared-doc-migrate.test.ts +2 -3
- package/plugins/design/dev-server/test/shared-doc-projection.test.ts +1 -1
- package/plugins/design/dev-server/test/snap-distance-pill.test.ts +1 -1
- package/plugins/design/dev-server/test/sync-agent.test.ts +2 -3
- package/plugins/design/dev-server/test/sync-atomic-write.test.ts +2 -3
- package/plugins/design/dev-server/test/sync-codec.test.ts +1 -1
- package/plugins/design/dev-server/test/sync-connection-state.test.ts +1 -1
- package/plugins/design/dev-server/test/sync-echo-guard.test.ts +1 -1
- package/plugins/design/dev-server/test/sync-fs-mirror.test.ts +1 -2
- package/plugins/design/dev-server/test/sync-hardening.test.ts +4 -5
- package/plugins/design/dev-server/test/sync-hubs-config.test.ts +1 -2
- package/plugins/design/dev-server/test/sync-meta-codec.test.ts +1 -1
- package/plugins/design/dev-server/test/sync-runtime.test.ts +37 -14
- package/plugins/design/dev-server/test/sync-status.test.ts +1 -1
- package/plugins/design/dev-server/test/sync-untrusted.test.ts +1 -2
- package/plugins/design/dev-server/test/undo-stack.test.ts +2 -2
- package/plugins/design/dev-server/test/use-agent-presence.test.tsx +117 -0
- package/plugins/design/dev-server/test/use-artboard-drag.test.ts +3 -3
- package/plugins/design/dev-server/test/use-canvas-activity.test.tsx +206 -0
- package/plugins/design/dev-server/test/use-selection-set.test.tsx +1 -1
- package/plugins/design/dev-server/test/use-snap-guides.test.ts +1 -1
- package/plugins/design/dev-server/test/use-undo-stack.test.tsx +2 -2
- package/plugins/design/dev-server/use-agent-presence.tsx +244 -0
- package/plugins/design/dev-server/use-annotation-resize.tsx +1 -5
- package/plugins/design/dev-server/use-annotation-selection.tsx +2 -6
- package/plugins/design/dev-server/use-annotations-visibility.tsx +1 -1
- package/plugins/design/dev-server/use-artboard-drag.tsx +1 -1
- package/plugins/design/dev-server/use-canvas-activity.tsx +252 -0
- package/plugins/design/dev-server/use-collab.tsx +3 -4
- package/plugins/design/dev-server/use-selection-set.tsx +1 -1
- package/plugins/design/dev-server/use-tool-mode.tsx +1 -1
- package/plugins/design/dev-server/use-undo-stack.tsx +4 -5
- package/plugins/design/dev-server/ws.ts +21 -2
- package/plugins/design/templates/_shell.html +108 -3
- package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +18 -3
|
@@ -136,6 +136,52 @@ test('adopt subcommand is an alias of link --adopt', async () => {
|
|
|
136
136
|
cleanup();
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
+
test('DDR-079: link without a flag omits syncTsx (= default ON, restore-proof)', async () => {
|
|
140
|
+
const res = await runCli(['design', 'link', URL, '--token', 'mau_test']);
|
|
141
|
+
assert.equal(res.status, 0, res.stderr);
|
|
142
|
+
const cfg = JSON.parse(readFileSync(join(workspace, '.design/config.json'), 'utf8'));
|
|
143
|
+
// We never write syncTsx:true to encode the default — absence means on.
|
|
144
|
+
assert.equal(cfg.linkedHub.syncTsx, undefined);
|
|
145
|
+
assert.match(res.stdout, /TSX sync: on by default/);
|
|
146
|
+
cleanup();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('DDR-079: link --no-sync-tsx records syncTsx:false (project opt-out)', async () => {
|
|
150
|
+
const res = await runCli(['design', 'link', URL, '--token', 'mau_test', '--no-sync-tsx']);
|
|
151
|
+
assert.equal(res.status, 0, res.stderr);
|
|
152
|
+
const cfg = JSON.parse(readFileSync(join(workspace, '.design/config.json'), 'utf8'));
|
|
153
|
+
assert.equal(cfg.linkedHub.syncTsx, false);
|
|
154
|
+
assert.match(res.stdout, /TSX sync: off \(opted out/);
|
|
155
|
+
cleanup();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('DDR-079: link --sync-tsx pins syncTsx:true explicitly', async () => {
|
|
159
|
+
const res = await runCli(['design', 'link', URL, '--token', 'mau_test', '--sync-tsx']);
|
|
160
|
+
assert.equal(res.status, 0, res.stderr);
|
|
161
|
+
const cfg = JSON.parse(readFileSync(join(workspace, '.design/config.json'), 'utf8'));
|
|
162
|
+
assert.equal(cfg.linkedHub.syncTsx, true);
|
|
163
|
+
cleanup();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('DDR-079: status surfaces the migration advisory when syncTsx is unset', async () => {
|
|
167
|
+
await runCli(['design', 'link', URL, '--token', 'mau_test']);
|
|
168
|
+
const res = await runCli(['design', 'status']);
|
|
169
|
+
assert.equal(res.status, 0, res.stderr);
|
|
170
|
+
assert.match(res.stdout, /TSX sync:\s+on \(default/);
|
|
171
|
+
assert.match(res.stdout, /syncTsx is not set/);
|
|
172
|
+
cleanup();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('DDR-079: status --json reports effective + explicit syncTsx', async () => {
|
|
176
|
+
await runCli(['design', 'link', URL, '--token', 'mau_test', '--no-sync-tsx']);
|
|
177
|
+
const res = await runCli(['design', 'status', '--json']);
|
|
178
|
+
assert.equal(res.status, 0, res.stderr);
|
|
179
|
+
const payload = JSON.parse(res.stdout);
|
|
180
|
+
assert.equal(payload.syncTsx, false);
|
|
181
|
+
assert.equal(payload.syncTsxExplicit, false);
|
|
182
|
+
cleanup();
|
|
183
|
+
});
|
|
184
|
+
|
|
139
185
|
test('link against unreachable URL exits 1 unless --force', async () => {
|
|
140
186
|
const dead = 'http://127.0.0.1:1';
|
|
141
187
|
const fail = await runCli(['design', 'link', dead, '--token', 'mau_test']);
|
package/cli/commands/design.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execSync, spawn, spawnSync } from 'node:child_process';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
3
|
-
import { mkdir,
|
|
2
|
+
import { chmodSync, existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
6
6
|
import { parseArgs } from '../lib/argv.mjs';
|
|
@@ -42,6 +42,11 @@ const BIN_VERBS = new Set([
|
|
|
42
42
|
'svg-optimize',
|
|
43
43
|
]);
|
|
44
44
|
|
|
45
|
+
// Bin verbs that boot the dev-server (directly, or by shelling into server-up.sh).
|
|
46
|
+
// For these we resolve the compiled platform binary and hand its path to the
|
|
47
|
+
// helper via MAUDE_DEV_SERVER_BIN — see runBinDispatch + DDR-084.
|
|
48
|
+
const BOOT_VERBS = new Set(['server-up', 'visual-sanity', 'smoke']);
|
|
49
|
+
|
|
45
50
|
export async function run({ args, pkgRoot }) {
|
|
46
51
|
const { positional } = parseArgs(args);
|
|
47
52
|
const sub = positional[0];
|
|
@@ -90,9 +95,21 @@ function runBinDispatch(verb, { args, pkgRoot }) {
|
|
|
90
95
|
process.exit(1);
|
|
91
96
|
}
|
|
92
97
|
const rest = args.slice(args.indexOf(verb) + 1); // everything after the verb token
|
|
98
|
+
const childEnv = { ...process.env, CLAUDE_PLUGIN_ROOT: pluginRoot };
|
|
99
|
+
// For verbs that boot the dev-server, resolve the compiled platform binary the
|
|
100
|
+
// SAME way `maude design serve` does and hand the path down. The binary embeds
|
|
101
|
+
// yjs + every runtime dep; `bun server.ts` from source does NOT (the deps live
|
|
102
|
+
// in a nested package.json the npm tarball excludes), so on a production install
|
|
103
|
+
// server.ts crashes at boot on `yjs`. server-up.sh prefers MAUDE_DEV_SERVER_BIN
|
|
104
|
+
// when set; absent ⇒ dev tree ⇒ it falls back to `bun server.ts`. Helpers that
|
|
105
|
+
// shell into server-up.sh (visual-sanity, smoke) inherit it. DDR-084.
|
|
106
|
+
if (BOOT_VERBS.has(verb) && !process.env.MAUDE_DEV_SERVER_BIN) {
|
|
107
|
+
const bin = resolveServerBinary({ pkgRoot });
|
|
108
|
+
if (bin) childEnv.MAUDE_DEV_SERVER_BIN = bin;
|
|
109
|
+
}
|
|
93
110
|
const child = spawnSync('bash', [script, ...rest], {
|
|
94
111
|
stdio: 'inherit',
|
|
95
|
-
env:
|
|
112
|
+
env: childEnv,
|
|
96
113
|
});
|
|
97
114
|
if (child.error) {
|
|
98
115
|
process.stderr.write(`maude design ${verb}: ${child.error.message}\n`);
|
|
@@ -295,6 +312,55 @@ function detectPlatformSlug() {
|
|
|
295
312
|
return null;
|
|
296
313
|
}
|
|
297
314
|
|
|
315
|
+
// Read-only resolution of the compiled platform binary the dev-server should
|
|
316
|
+
// boot as in a production install (it embeds yjs + every runtime dep). Mirrors
|
|
317
|
+
// runServe's order — side-channel cache (postinstall) → lazy resolve of the
|
|
318
|
+
// @1agh/maude-<slug> sibling/nested package — but performs NO caching and NEVER
|
|
319
|
+
// hard-fails: returns the path, or null when none is found. A null result means
|
|
320
|
+
// "fall back to `bun server.ts` from source" (correct in the local dev tree).
|
|
321
|
+
// Honors MAUDE_FORCE_SOURCE=1 so maintainers hacking on the dev-server source
|
|
322
|
+
// still boot their working copy. Used by runBinDispatch to hand the path to
|
|
323
|
+
// server-up.sh; runServe keeps its own inline copy (it also caches + hard-fails).
|
|
324
|
+
// Structural allowlist for a path we're about to spawn (DDR-084 hardening). The
|
|
325
|
+
// compiled binary is ALWAYS named `maude` / `maude.exe` inside a `maude-<slug>/`
|
|
326
|
+
// dir (the @1agh/maude-<slug> package, or the dev-tree `packages/maude-<slug>/`).
|
|
327
|
+
// The side-channel file's content is the one attacker-influenceable input in a
|
|
328
|
+
// shared-prefix / poisoned-clone layout — a path outside this shape is ignored
|
|
329
|
+
// (caller falls back to source), so the file can only DENY the binary, never
|
|
330
|
+
// redirect the spawn to an arbitrary executable. Paths `lazyResolveBinary`
|
|
331
|
+
// constructs always conform, so this never rejects a legit resolve.
|
|
332
|
+
export function isPlausiblePlatformBinary(p) {
|
|
333
|
+
if (!p) return false;
|
|
334
|
+
const b = basename(p);
|
|
335
|
+
if (b !== 'maude' && b !== 'maude.exe') return false;
|
|
336
|
+
return /^maude-[a-z0-9-]+$/.test(basename(dirname(p)));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function resolveServerBinary({ pkgRoot }) {
|
|
340
|
+
if (process.env.MAUDE_FORCE_SOURCE === '1') return null;
|
|
341
|
+
// In the local source checkout, return null so server-up.sh boots
|
|
342
|
+
// `bun server.ts` from source — the maintainer is editing the dev-server and
|
|
343
|
+
// needs their working copy live, not a stale compiled binary that happens to
|
|
344
|
+
// be installed via pnpm. A production install has no `packages/` dir, so this
|
|
345
|
+
// never short-circuits there. (Preserves server-up's historic dev-tree
|
|
346
|
+
// behavior — it always ran source before DDR-084.)
|
|
347
|
+
if (isLocalDevTree(pkgRoot, { existsSync })) return null;
|
|
348
|
+
const sideChannel = resolve(pkgRoot, 'cli', '.platform-binary-path');
|
|
349
|
+
try {
|
|
350
|
+
if (existsSync(sideChannel)) {
|
|
351
|
+
const candidate = readFileSync(sideChannel, 'utf8').trim();
|
|
352
|
+
// Existence AND the structural allowlist — a poisoned side-channel pointing
|
|
353
|
+
// at an arbitrary executable is ignored (falls through to lazy resolve).
|
|
354
|
+
if (candidate && existsSync(candidate) && isPlausiblePlatformBinary(candidate)) {
|
|
355
|
+
return candidate;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
} catch {
|
|
359
|
+
/* fall through to lazy resolve */
|
|
360
|
+
}
|
|
361
|
+
return lazyResolveBinary({ pkgRoot, fs: { existsSync, chmodSync } }).binPath;
|
|
362
|
+
}
|
|
363
|
+
|
|
298
364
|
function lazyResolveBinary({ pkgRoot, fs }) {
|
|
299
365
|
const slug = detectPlatformSlug();
|
|
300
366
|
if (!slug) return { binPath: null, slug: null };
|
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
|
|
9
9
|
import assert from 'node:assert/strict';
|
|
10
10
|
import { spawnSync } from 'node:child_process';
|
|
11
|
-
import { mkdtempSync, rmSync } from 'node:fs';
|
|
11
|
+
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
12
12
|
import { tmpdir } from 'node:os';
|
|
13
13
|
import { dirname, join, resolve } from 'node:path';
|
|
14
14
|
import { test } from 'node:test';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { isPlausiblePlatformBinary, resolveServerBinary } from './design.mjs';
|
|
16
17
|
|
|
17
18
|
const BIN = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'maude.mjs');
|
|
18
19
|
|
|
@@ -54,3 +55,98 @@ test('design help lists the dev-tooling verbs', () => {
|
|
|
54
55
|
assert.match(res.stdout, new RegExp(verb), `usage should mention ${verb}`);
|
|
55
56
|
}
|
|
56
57
|
});
|
|
58
|
+
|
|
59
|
+
// resolveServerBinary — the dev-server boot-runtime resolver (DDR-084). server-up
|
|
60
|
+
// must boot the compiled platform binary on a production install (it embeds yjs +
|
|
61
|
+
// every dep; `bun server.ts` from source can't resolve them — the historic
|
|
62
|
+
// yjs-at-boot crash), but keep using SOURCE in the local dev tree so a maintainer's
|
|
63
|
+
// edits are live.
|
|
64
|
+
function fakePkgRoot({ devTree = false, sideChannelBin = null } = {}) {
|
|
65
|
+
const root = mkdtempSync(join(tmpdir(), 'maude-pkgroot-'));
|
|
66
|
+
if (devTree) {
|
|
67
|
+
mkdirSync(join(root, 'packages', 'maude-darwin-arm64'), { recursive: true });
|
|
68
|
+
writeFileSync(join(root, 'packages', 'maude-darwin-arm64', 'package.json'), '{}');
|
|
69
|
+
}
|
|
70
|
+
if (sideChannelBin) {
|
|
71
|
+
mkdirSync(join(root, 'cli'), { recursive: true });
|
|
72
|
+
writeFileSync(join(root, 'cli', '.platform-binary-path'), sideChannelBin);
|
|
73
|
+
}
|
|
74
|
+
return root;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
test('resolveServerBinary → null in the local dev tree (packages/ present) — boots source', () => {
|
|
78
|
+
const root = fakePkgRoot({ devTree: true });
|
|
79
|
+
try {
|
|
80
|
+
delete process.env.MAUDE_FORCE_SOURCE;
|
|
81
|
+
assert.equal(resolveServerBinary({ pkgRoot: root }), null);
|
|
82
|
+
} finally {
|
|
83
|
+
rmSync(root, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Build an executable in a layout the allowlist accepts (`maude-<slug>/maude`)
|
|
88
|
+
// or rejects (anything else), under a throwaway dir.
|
|
89
|
+
function fakeBinary({ conforming = true } = {}) {
|
|
90
|
+
const home = mkdtempSync(join(tmpdir(), 'maude-bin-'));
|
|
91
|
+
const dir = join(home, conforming ? 'maude-darwin-arm64' : 'evil');
|
|
92
|
+
mkdirSync(dir, { recursive: true });
|
|
93
|
+
const bin = join(dir, 'maude');
|
|
94
|
+
writeFileSync(bin, '#!/bin/sh\n');
|
|
95
|
+
chmodSync(bin, 0o755);
|
|
96
|
+
return { home, bin };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
test('resolveServerBinary → null when MAUDE_FORCE_SOURCE=1 (maintainer override)', () => {
|
|
100
|
+
const { home, bin } = fakeBinary();
|
|
101
|
+
const root = fakePkgRoot({ sideChannelBin: bin }); // NOT a dev tree
|
|
102
|
+
const prev = process.env.MAUDE_FORCE_SOURCE;
|
|
103
|
+
try {
|
|
104
|
+
process.env.MAUDE_FORCE_SOURCE = '1';
|
|
105
|
+
assert.equal(resolveServerBinary({ pkgRoot: root }), null);
|
|
106
|
+
} finally {
|
|
107
|
+
if (prev === undefined) delete process.env.MAUDE_FORCE_SOURCE;
|
|
108
|
+
else process.env.MAUDE_FORCE_SOURCE = prev;
|
|
109
|
+
rmSync(root, { recursive: true, force: true });
|
|
110
|
+
rmSync(home, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('resolveServerBinary → side-channel path on a production install (no packages/)', () => {
|
|
115
|
+
const { home, bin } = fakeBinary(); // conforming maude-<slug>/maude layout
|
|
116
|
+
const root = fakePkgRoot({ sideChannelBin: bin }); // production shape
|
|
117
|
+
const prev = process.env.MAUDE_FORCE_SOURCE;
|
|
118
|
+
try {
|
|
119
|
+
delete process.env.MAUDE_FORCE_SOURCE;
|
|
120
|
+
assert.equal(resolveServerBinary({ pkgRoot: root }), bin);
|
|
121
|
+
} finally {
|
|
122
|
+
if (prev !== undefined) process.env.MAUDE_FORCE_SOURCE = prev;
|
|
123
|
+
rmSync(root, { recursive: true, force: true });
|
|
124
|
+
rmSync(home, { recursive: true, force: true });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('resolveServerBinary → REJECTS a side-channel pointing outside the maude-<slug>/ layout (DDR-084 allowlist)', () => {
|
|
129
|
+
const { home, bin } = fakeBinary({ conforming: false }); // <tmp>/evil/maude
|
|
130
|
+
const root = fakePkgRoot({ sideChannelBin: bin }); // production shape, no real platform pkg
|
|
131
|
+
const prev = process.env.MAUDE_FORCE_SOURCE;
|
|
132
|
+
try {
|
|
133
|
+
delete process.env.MAUDE_FORCE_SOURCE;
|
|
134
|
+
// Poisoned side-channel is ignored → falls through to lazyResolve, which finds
|
|
135
|
+
// nothing in the fake root → null (server-up then boots source, not the planted bin).
|
|
136
|
+
assert.equal(resolveServerBinary({ pkgRoot: root }), null);
|
|
137
|
+
} finally {
|
|
138
|
+
if (prev !== undefined) process.env.MAUDE_FORCE_SOURCE = prev;
|
|
139
|
+
rmSync(root, { recursive: true, force: true });
|
|
140
|
+
rmSync(home, { recursive: true, force: true });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('isPlausiblePlatformBinary — accepts maude-<slug>/maude, rejects everything else', () => {
|
|
145
|
+
assert.equal(isPlausiblePlatformBinary('/x/node_modules/@1agh/maude-darwin-arm64/maude'), true);
|
|
146
|
+
assert.equal(isPlausiblePlatformBinary('/x/packages/maude-linux-x64-musl/maude'), true);
|
|
147
|
+
assert.equal(isPlausiblePlatformBinary('/x/maude-win32-x64/maude.exe'), true);
|
|
148
|
+
assert.equal(isPlausiblePlatformBinary('/tmp/evil/maude'), false); // parent not maude-<slug>
|
|
149
|
+
assert.equal(isPlausiblePlatformBinary('/x/maude-darwin-arm64/evil'), false); // basename not maude
|
|
150
|
+
assert.equal(isPlausiblePlatformBinary(''), false);
|
|
151
|
+
assert.equal(isPlausiblePlatformBinary(null), false);
|
|
152
|
+
});
|
package/cli/commands/doctor.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// `maude doctor` — unified workspace diagnostic.
|
|
2
2
|
// Combines (1) plugin dependency preflight, (2) workflows.config.json schema
|
|
3
|
-
// validation, (3) stack drift, (4) quality-gate additions
|
|
3
|
+
// validation, (3) stack drift, (4) quality-gate additions, and (5) linked
|
|
4
|
+
// design-hub / TSX-sync health (report-only, local signals — no network) into
|
|
5
|
+
// one report.
|
|
4
6
|
//
|
|
5
7
|
// Flags:
|
|
6
8
|
// --plugin <name> Scope deps section to one plugin. Config section is
|
|
@@ -23,6 +25,7 @@ import { stdin, stdout } from 'node:process';
|
|
|
23
25
|
import { createInterface } from 'node:readline/promises';
|
|
24
26
|
import { parseArgs } from '../lib/argv.mjs';
|
|
25
27
|
import { lintConfig } from '../lib/config-lint.mjs';
|
|
28
|
+
import { getHub } from '../lib/hubs-config.mjs';
|
|
26
29
|
import { checkAll } from '../lib/preflight.mjs';
|
|
27
30
|
import { detectQualityGates, detectStack } from '../lib/stack-detect.mjs';
|
|
28
31
|
|
|
@@ -115,6 +118,60 @@ export async function run({ args, pkgRoot }) {
|
|
|
115
118
|
};
|
|
116
119
|
}
|
|
117
120
|
|
|
121
|
+
// ── Design config: linked-hub + TSX-sync health (report-only) ────────────
|
|
122
|
+
// doctor's primary domain is .ai/workflows.config.json, but a linked design
|
|
123
|
+
// project's .design/config.json carries the one knob that has bitten users
|
|
124
|
+
// repeatedly — `linkedHub.syncTsx`. Surface it here so doctor is the one-stop
|
|
125
|
+
// health check. Report-only: under DDR-079 (default ON) every state is valid,
|
|
126
|
+
// so there is nothing to --fix and we NEVER impose an opt-out.
|
|
127
|
+
let designReport = { exists: false };
|
|
128
|
+
const designFsPath = resolve(repoRoot, '.design/config.json');
|
|
129
|
+
if (existsSync(designFsPath)) {
|
|
130
|
+
try {
|
|
131
|
+
const dcfg = JSON.parse(readFileSync(designFsPath, 'utf8'));
|
|
132
|
+
const linkedHub = dcfg.linkedHub;
|
|
133
|
+
if (!linkedHub) {
|
|
134
|
+
designReport = { exists: true, linked: false };
|
|
135
|
+
} else {
|
|
136
|
+
const syncTsx = linkedHub.syncTsx;
|
|
137
|
+
const tsxSync =
|
|
138
|
+
syncTsx === false
|
|
139
|
+
? 'off (opted out)'
|
|
140
|
+
: syncTsx === true
|
|
141
|
+
? 'on (explicit)'
|
|
142
|
+
: 'on (default)';
|
|
143
|
+
// Local-only signals — no network. `getHub` reads ~/.config/maude/
|
|
144
|
+
// hubs.json; the sync-agent state comes from the serve-written
|
|
145
|
+
// `_sync.json`. We deliberately DO NOT probe the hub's /health here:
|
|
146
|
+
// doctor must stay fast + offline (it runs in the release pre-flight),
|
|
147
|
+
// so live hub reachability stays in `maude design status`.
|
|
148
|
+
let tokenStored = false;
|
|
149
|
+
try {
|
|
150
|
+
tokenStored = !!getHub(linkedHub.url);
|
|
151
|
+
} catch {
|
|
152
|
+
/* hubs.json absent/unreadable on this machine → no token */
|
|
153
|
+
}
|
|
154
|
+
designReport = {
|
|
155
|
+
exists: true,
|
|
156
|
+
linked: true,
|
|
157
|
+
hubUrl: linkedHub.url,
|
|
158
|
+
adopt: !!linkedHub.adopt,
|
|
159
|
+
tsxSync,
|
|
160
|
+
tokenStored,
|
|
161
|
+
syncAgent: readSyncAgentState(resolve(repoRoot, '.design', '_sync.json')),
|
|
162
|
+
// DDR-079 migration advisory: a linked config with no explicit
|
|
163
|
+
// syncTsx rides the default, which flipped off→on in maude 0.27→0.28.
|
|
164
|
+
advisory:
|
|
165
|
+
syncTsx === undefined
|
|
166
|
+
? 'TSX sync defaults ON (DDR-079) — every .tsx syncs to this hub. Opt out with linkedHub.syncTsx:false or `maude design link … --no-sync-tsx`.'
|
|
167
|
+
: null,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
} catch (err) {
|
|
171
|
+
designReport = { exists: true, error: `invalid JSON: ${err.message}` };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
118
175
|
// ── Summary ─────────────────────────────────────────────────────────────
|
|
119
176
|
const hardDepsMissing = Object.values(depsByPlugin)
|
|
120
177
|
.filter((r) => r.summary)
|
|
@@ -132,12 +189,12 @@ export async function run({ args, pkgRoot }) {
|
|
|
132
189
|
|
|
133
190
|
if (jsonMode) {
|
|
134
191
|
process.stdout.write(
|
|
135
|
-
`${JSON.stringify({ deps: depsByPlugin, config: configReport, summary }, null, 2)}\n`
|
|
192
|
+
`${JSON.stringify({ deps: depsByPlugin, config: configReport, design: designReport, summary }, null, 2)}\n`
|
|
136
193
|
);
|
|
137
194
|
process.exit(summary.healthy ? 0 : 1);
|
|
138
195
|
}
|
|
139
196
|
|
|
140
|
-
printReport({ depsByPlugin, configReport, summary });
|
|
197
|
+
printReport({ depsByPlugin, configReport, designReport, summary });
|
|
141
198
|
|
|
142
199
|
// ── Fix path ────────────────────────────────────────────────────────────
|
|
143
200
|
if (fix) {
|
|
@@ -151,11 +208,33 @@ export async function run({ args, pkgRoot }) {
|
|
|
151
208
|
process.exit(summary.healthy ? 0 : 1);
|
|
152
209
|
}
|
|
153
210
|
|
|
211
|
+
// Read the serve-written `.design/_sync.json` and condense it to one line.
|
|
212
|
+
// Local file only — never network. Returns null when the agent hasn't run.
|
|
213
|
+
function readSyncAgentState(p) {
|
|
214
|
+
if (!existsSync(p)) return null;
|
|
215
|
+
try {
|
|
216
|
+
const s = JSON.parse(readFileSync(p, 'utf8'));
|
|
217
|
+
if (s.notSyncable) return `0 syncable — ${s.reason || 'see `maude design status`'}`;
|
|
218
|
+
if (s.state) {
|
|
219
|
+
const bits = [String(s.state), `${s.canvases ?? 0} canvas(es)`];
|
|
220
|
+
if (s.queuedOps) bits.push(`${s.queuedOps} queued`);
|
|
221
|
+
if (s.conflicts?.length) bits.push(`${s.conflicts.length} conflict notice(s)`);
|
|
222
|
+
return bits.join(', ');
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
} catch {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
154
230
|
function usage() {
|
|
155
231
|
return `maude doctor [--plugin <name>] [--fix] [--json]
|
|
156
232
|
|
|
157
233
|
Unified workspace diagnostic. Reports missing dependencies, config schema
|
|
158
|
-
errors, stack drift,
|
|
234
|
+
errors, stack drift, missing quality-gate declarations, and — when a
|
|
235
|
+
.design/config.json is linked — design-hub + TSX-sync health, in one shot.
|
|
236
|
+
(The design section is report-only and local; live hub reachability is
|
|
237
|
+
\`maude design status\`.)
|
|
159
238
|
|
|
160
239
|
Flags:
|
|
161
240
|
--plugin <name> Scope dependency section to one plugin (design | flow).
|
|
@@ -170,7 +249,7 @@ function usage() {
|
|
|
170
249
|
`;
|
|
171
250
|
}
|
|
172
251
|
|
|
173
|
-
function printReport({ depsByPlugin, configReport, summary }) {
|
|
252
|
+
function printReport({ depsByPlugin, configReport, designReport, summary }) {
|
|
174
253
|
process.stdout.write('maude doctor\n\n');
|
|
175
254
|
for (const [name, env] of Object.entries(depsByPlugin)) {
|
|
176
255
|
process.stdout.write(` Dependencies (plugins/${name}):\n`);
|
|
@@ -235,6 +314,32 @@ function printReport({ depsByPlugin, configReport, summary }) {
|
|
|
235
314
|
}
|
|
236
315
|
}
|
|
237
316
|
|
|
317
|
+
// Design / linked-hub health — report-only, local signals only (no network).
|
|
318
|
+
if (designReport?.exists) {
|
|
319
|
+
process.stdout.write(' Design hub (.design/config.json):\n');
|
|
320
|
+
if (designReport.error) {
|
|
321
|
+
process.stdout.write(` ✗ ${designReport.error}\n`);
|
|
322
|
+
} else if (!designReport.linked) {
|
|
323
|
+
process.stdout.write(' solo — not linked to a hub.\n');
|
|
324
|
+
} else {
|
|
325
|
+
process.stdout.write(` ✓ linked: ${designReport.hubUrl}\n`);
|
|
326
|
+
process.stdout.write(
|
|
327
|
+
` token stored: ${designReport.tokenStored ? 'yes' : 'NO — run `maude design link`'}\n`
|
|
328
|
+
);
|
|
329
|
+
process.stdout.write(` TSX sync: ${designReport.tsxSync}\n`);
|
|
330
|
+
if (designReport.adopt)
|
|
331
|
+
process.stdout.write(' adopt mode: yes (push-on-first-sync)\n');
|
|
332
|
+
process.stdout.write(
|
|
333
|
+
` sync agent: ${designReport.syncAgent || 'idle (start `maude design serve`)'}\n`
|
|
334
|
+
);
|
|
335
|
+
if (designReport.advisory) process.stdout.write(` ℹ ${designReport.advisory}\n`);
|
|
336
|
+
process.stdout.write(
|
|
337
|
+
' (live hub reachability + full sync detail: `maude design status`)\n'
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
process.stdout.write('\n');
|
|
341
|
+
}
|
|
342
|
+
|
|
238
343
|
const parts = [];
|
|
239
344
|
if (summary.hardDepsMissing) parts.push(`${summary.hardDepsMissing} hard dep missing`);
|
|
240
345
|
if (summary.schemaErrors)
|
|
@@ -183,3 +183,55 @@ test('summary line reflects counts (drift/additions tracked via package.json)',
|
|
|
183
183
|
const lintAdd = env.config.qualityAdditions.find((a) => a.gate === 'lint');
|
|
184
184
|
assert.ok(lintAdd);
|
|
185
185
|
});
|
|
186
|
+
|
|
187
|
+
// ── DDR-079: design-hub / TSX-sync section (report-only, local) ──────────────
|
|
188
|
+
function writeDesign(root, linkedHub) {
|
|
189
|
+
mkdirSync(join(root, '.design'), { recursive: true });
|
|
190
|
+
writeFileSync(
|
|
191
|
+
join(root, '.design/config.json'),
|
|
192
|
+
JSON.stringify({ name: 't', designRoot: '.design', ...(linkedHub ? { linkedHub } : {}) })
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
test('DDR-079: linked design config with no syncTsx → on (default) + advisory', () => {
|
|
197
|
+
const root = makeTempRepo();
|
|
198
|
+
writeFileSync(join(root, '.ai/workflows.config.json'), `${JSON.stringify({ name: 'x' })}\n`);
|
|
199
|
+
writeDesign(root, { url: 'https://hub.example.com', linkedAt: 1 });
|
|
200
|
+
const env = JSON.parse(spawnDoctor(['--json'], root).stdout);
|
|
201
|
+
assert.equal(env.design.linked, true);
|
|
202
|
+
assert.equal(env.design.hubUrl, 'https://hub.example.com');
|
|
203
|
+
assert.equal(env.design.tsxSync, 'on (default)');
|
|
204
|
+
assert.match(env.design.advisory, /defaults ON/);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('DDR-079: syncTsx:false → off (opted out), no advisory', () => {
|
|
208
|
+
const root = makeTempRepo();
|
|
209
|
+
writeFileSync(join(root, '.ai/workflows.config.json'), `${JSON.stringify({ name: 'x' })}\n`);
|
|
210
|
+
writeDesign(root, { url: 'https://hub.example.com', linkedAt: 1, syncTsx: false });
|
|
211
|
+
const env = JSON.parse(spawnDoctor(['--json'], root).stdout);
|
|
212
|
+
assert.equal(env.design.tsxSync, 'off (opted out)');
|
|
213
|
+
assert.equal(env.design.advisory, null);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('DDR-079: syncTsx:true → on (explicit), no advisory', () => {
|
|
217
|
+
const root = makeTempRepo();
|
|
218
|
+
writeFileSync(join(root, '.ai/workflows.config.json'), `${JSON.stringify({ name: 'x' })}\n`);
|
|
219
|
+
writeDesign(root, { url: 'https://hub.example.com', linkedAt: 1, syncTsx: true });
|
|
220
|
+
const env = JSON.parse(spawnDoctor(['--json'], root).stdout);
|
|
221
|
+
assert.equal(env.design.tsxSync, 'on (explicit)');
|
|
222
|
+
assert.equal(env.design.advisory, null);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('design section reports solo when .design/config.json has no linkedHub', () => {
|
|
226
|
+
const root = makeTempRepo();
|
|
227
|
+
writeDesign(root, null);
|
|
228
|
+
const env = JSON.parse(spawnDoctor(['--json'], root).stdout);
|
|
229
|
+
assert.equal(env.design.exists, true);
|
|
230
|
+
assert.equal(env.design.linked, false);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('design section absent when no .design/config.json', () => {
|
|
234
|
+
const root = makeTempRepo();
|
|
235
|
+
const env = JSON.parse(spawnDoctor(['--json'], root).stdout);
|
|
236
|
+
assert.equal(env.design.exists, false);
|
|
237
|
+
});
|
package/cli/lib/cache.mjs
CHANGED
package/cli/lib/config-lint.mjs
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
import { readFile } from 'node:fs/promises';
|
|
18
18
|
import { resolve } from 'node:path';
|
|
19
|
-
import addFormats from 'ajv-formats';
|
|
20
19
|
import Ajv2020 from 'ajv/dist/2020.js';
|
|
20
|
+
import addFormats from 'ajv-formats';
|
|
21
21
|
|
|
22
22
|
function levenshtein(a, b) {
|
|
23
23
|
if (a === b) return 0;
|
package/cli/lib/copy-tree.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { copyFile, mkdir,
|
|
1
|
+
import { copyFile, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { basename, join, relative } from 'node:path';
|
|
3
3
|
|
|
4
4
|
// Recursively copy `src` → `dest`. Returns { created, skipped, replaced }.
|
package/cli/lib/design-link.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
//
|
|
11
11
|
// Token NEVER lands in .design/config.json — that's git-committed.
|
|
12
12
|
|
|
13
|
-
import { existsSync,
|
|
13
|
+
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
14
14
|
import { dirname, resolve } from 'node:path';
|
|
15
15
|
import { createInterface } from 'node:readline';
|
|
16
16
|
|
|
@@ -25,7 +25,9 @@ const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
|
|
|
25
25
|
|
|
26
26
|
export async function runLink({ args, cwd = process.cwd(), forceAdopt = false }) {
|
|
27
27
|
const tail = args.slice(args.indexOf(forceAdopt ? 'adopt' : 'link') + 1);
|
|
28
|
-
const { flags, positional } = parseArgs(tail, {
|
|
28
|
+
const { flags, positional } = parseArgs(tail, {
|
|
29
|
+
booleans: ['adopt', 'force', 'yes', 'sync-tsx', 'no-sync-tsx'],
|
|
30
|
+
});
|
|
29
31
|
const url = positional[0];
|
|
30
32
|
const token = flags.token;
|
|
31
33
|
|
|
@@ -116,15 +118,27 @@ export async function runLink({ args, cwd = process.cwd(), forceAdopt = false })
|
|
|
116
118
|
`[design link] note: replacing existing link to ${existing.url} (was added ${new Date(existing.linkedAt).toISOString()}).\n`
|
|
117
119
|
);
|
|
118
120
|
}
|
|
119
|
-
// DDR-
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
|
|
121
|
+
// DDR-079 — TSX sync defaults ON, so we only PERSIST `syncTsx` when it's an
|
|
122
|
+
// explicit choice; absence means "the default", and we never write
|
|
123
|
+
// `syncTsx: true` just to encode the default (that's what bit us — a value
|
|
124
|
+
// that git-restore could wipe to silently flip behavior). Precedence:
|
|
125
|
+
// --no-sync-tsx → false (project-wide opt-out)
|
|
126
|
+
// --sync-tsx → true (explicit, == default; useful to pin against a future
|
|
127
|
+
// default change or to self-document)
|
|
128
|
+
// else, re-link to the SAME hub → carry the existing explicit value forward
|
|
129
|
+
// (a NEW hub URL is a fresh trust decision — don't inherit, DDR-054 F2)
|
|
130
|
+
// else → omit (= on by default)
|
|
131
|
+
let syncTsx;
|
|
132
|
+
if (flags['no-sync-tsx']) syncTsx = false;
|
|
133
|
+
else if (flags['sync-tsx']) syncTsx = true;
|
|
134
|
+
else if (existing && existing.url === normUrl && typeof existing.syncTsx === 'boolean') {
|
|
135
|
+
syncTsx = existing.syncTsx;
|
|
136
|
+
}
|
|
123
137
|
cfg.linkedHub = {
|
|
124
138
|
url: normUrl,
|
|
125
139
|
linkedAt: hubRecord.linkedAt,
|
|
126
140
|
...(adopt ? { adopt: true } : {}),
|
|
127
|
-
...(
|
|
141
|
+
...(syncTsx !== undefined ? { syncTsx } : {}),
|
|
128
142
|
};
|
|
129
143
|
writeDesignConfig(designConfigPath, cfg);
|
|
130
144
|
|
|
@@ -140,8 +154,12 @@ export async function runLink({ args, cwd = process.cwd(), forceAdopt = false })
|
|
|
140
154
|
await maybeWriteGitignoreBlock(cwd, !!flags.yes);
|
|
141
155
|
}
|
|
142
156
|
|
|
157
|
+
const tsxSyncLine =
|
|
158
|
+
syncTsx === false
|
|
159
|
+
? 'off (opted out — linkedHub.syncTsx: false)'
|
|
160
|
+
: 'on by default (DDR-079) — every .tsx body syncs; opt out with --no-sync-tsx or a canvas .meta.json "syncable": false';
|
|
143
161
|
process.stdout.write(
|
|
144
|
-
`[design link] linked ${cwd} to ${normUrl}.\n token:
|
|
162
|
+
`[design link] linked ${cwd} to ${normUrl}.\n token: stored in ~/.config/maude/hubs.json (per-machine, never committed)\n config: .design/config.json.linkedHub = { url, linkedAt${adopt ? ', adopt: true' : ''}${syncTsx !== undefined ? `, syncTsx: ${syncTsx}` : ''} }\n TSX sync: ${tsxSyncLine}\n hub: ${probe.ok ? `v${probe.version}, uptime ${Math.round((probe.uptimeMs ?? 0) / 1000)}s, ${probe.tokenCount} token(s) (${probe.authMode})` : 'NOT REACHED — linked anyway (--force)'}\n\nNext step: start 'maude design serve' — the linked sync agent ${adopt ? 'will push local state up to the hub on first connect' : 'will mirror hub state to disk on first connect'}.\n`
|
|
145
163
|
);
|
|
146
164
|
if (!loopback) process.stderr.write(linkedModeBanner());
|
|
147
165
|
}
|
|
@@ -220,6 +238,9 @@ export async function runStatus({ args, cwd = process.cwd() }) {
|
|
|
220
238
|
url,
|
|
221
239
|
linkedAt: cfg.linkedHub.linkedAt,
|
|
222
240
|
adopt: !!cfg.linkedHub.adopt,
|
|
241
|
+
// DDR-079 — effective TSX-sync state: on unless explicitly opted out.
|
|
242
|
+
syncTsx: cfg.linkedHub.syncTsx !== false,
|
|
243
|
+
syncTsxExplicit: typeof cfg.linkedHub.syncTsx === 'boolean' ? cfg.linkedHub.syncTsx : null,
|
|
223
244
|
tokenStored: !!hubRecord,
|
|
224
245
|
hub: probe.ok
|
|
225
246
|
? {
|
|
@@ -249,8 +270,17 @@ export async function runStatus({ args, cwd = process.cwd() }) {
|
|
|
249
270
|
}${sync.conflicts?.length ? `, ${sync.conflicts.length} conflict notice(s)` : ''}`
|
|
250
271
|
: 'idle (start `maude design serve` in linked mode)';
|
|
251
272
|
process.stdout.write(
|
|
252
|
-
`Maude design — linked mode\n hub URL: ${url}\n linked at: ${new Date(cfg.linkedHub.linkedAt).toISOString()}\n adopt mode: ${cfg.linkedHub.adopt ? 'yes (push-on-first-sync)' : 'no (hub-wins)'}\n token stored: ${hubRecord ? 'yes (~/.config/maude/hubs.json)' : "NO — re-run 'maude design link'"}\n hub status: ${probe.ok ? `up — v${probe.version}, ${uptimeS}s uptime, ${probe.tokenCount} token(s), ${probe.authMode}` : `UNREACHABLE — ${probe.error}`}\n sync agent: ${syncLine}\n`
|
|
273
|
+
`Maude design — linked mode\n hub URL: ${url}\n linked at: ${new Date(cfg.linkedHub.linkedAt).toISOString()}\n adopt mode: ${cfg.linkedHub.adopt ? 'yes (push-on-first-sync)' : 'no (hub-wins)'}\n TSX sync: ${cfg.linkedHub.syncTsx === false ? 'off (opted out — linkedHub.syncTsx: false)' : 'on (default — DDR-079)'}\n token stored: ${hubRecord ? 'yes (~/.config/maude/hubs.json)' : "NO — re-run 'maude design link'"}\n hub status: ${probe.ok ? `up — v${probe.version}, ${uptimeS}s uptime, ${probe.tokenCount} token(s), ${probe.authMode}` : `UNREACHABLE — ${probe.error}`}\n sync agent: ${syncLine}\n`
|
|
253
274
|
);
|
|
275
|
+
// DDR-079 migration advisory: a config with no explicit `syncTsx` rides the
|
|
276
|
+
// default, which FLIPPED from off→on in maude 0.27. Surface it so an upgrader
|
|
277
|
+
// who relied on the old "TSX never syncs" behavior isn't surprised that every
|
|
278
|
+
// .tsx now goes to the hub.
|
|
279
|
+
if (cfg.linkedHub.syncTsx === undefined) {
|
|
280
|
+
process.stdout.write(
|
|
281
|
+
'\n ℹ syncTsx is not set, so it uses the DEFAULT — which is now ON (DDR-079, maude ≥ 0.27): every .tsx syncs to this hub.\n Upgraded from an older maude and want the pre-0.27 behavior (no TSX sync)? Set .design/config.json → linkedHub.syncTsx: false (or maude design link <url> --token … --no-sync-tsx).\n'
|
|
282
|
+
);
|
|
283
|
+
}
|
|
254
284
|
}
|
|
255
285
|
|
|
256
286
|
/**
|
|
@@ -19,8 +19,8 @@ import assert from 'node:assert/strict';
|
|
|
19
19
|
import { readFileSync } from 'node:fs';
|
|
20
20
|
import { resolve } from 'node:path';
|
|
21
21
|
import { test } from 'node:test';
|
|
22
|
-
import addFormats from 'ajv-formats';
|
|
23
22
|
import Ajv2020 from 'ajv/dist/2020.js';
|
|
23
|
+
import addFormats from 'ajv-formats';
|
|
24
24
|
import { lintConfig } from './config-lint.mjs';
|
|
25
25
|
|
|
26
26
|
const read = (p) => readFileSync(resolve(p), 'utf8');
|
package/cli/lib/stack-detect.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
//
|
|
11
11
|
// Consumers: cli/commands/doctor.mjs and the drift-aware /flow:init re-run.
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
14
14
|
import { join } from 'node:path';
|
|
15
15
|
|
|
16
16
|
async function fileExists(path) {
|