@1agh/maude 0.18.2 → 0.19.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/cli/bin/maude.mjs +16 -0
- package/cli/lib/update-check.mjs +145 -0
- package/cli/lib/update-check.test.mjs +32 -0
- package/package.json +8 -8
- package/plugins/design/dev-server/api.ts +2 -2
- package/plugins/design/dev-server/canvas-lib-resolver.ts +10 -1
- package/plugins/design/dev-server/client/app.jsx +6 -6
- package/plugins/design/dev-server/dist/client.bundle.js +12 -9
- package/plugins/design/dev-server/server.mjs +2 -2
- package/plugins/design/dev-server/server.ts +76 -30
package/cli/bin/maude.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { dirname, resolve } from 'node:path';
|
|
3
4
|
// maude — Maude CLI. Scaffold .ai workspace, run dev servers, manage config.
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { runUpdateCheck } from '../lib/update-check.mjs';
|
|
5
7
|
|
|
6
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
9
|
const __dirname = dirname(__filename);
|
|
@@ -16,10 +18,24 @@ const COMMANDS = {
|
|
|
16
18
|
version: () => import('../commands/version.mjs'),
|
|
17
19
|
};
|
|
18
20
|
|
|
21
|
+
function readCurrentVersion() {
|
|
22
|
+
try {
|
|
23
|
+
const raw = readFileSync(resolve(PKG_ROOT, 'package.json'), 'utf8');
|
|
24
|
+
return JSON.parse(raw).version || null;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
19
30
|
async function main(argv) {
|
|
20
31
|
const args = argv.slice(2);
|
|
21
32
|
const cmd = args[0];
|
|
22
33
|
|
|
34
|
+
// Print "update available" notice (from cached registry data) before
|
|
35
|
+
// dispatch, so it lands on stderr ahead of any subcommand output. Hot
|
|
36
|
+
// path is sync + non-blocking — the stale-cache refresh is detached.
|
|
37
|
+
runUpdateCheck(readCurrentVersion());
|
|
38
|
+
|
|
23
39
|
if (!cmd || cmd === '--help' || cmd === '-h') {
|
|
24
40
|
const { run } = await COMMANDS.help();
|
|
25
41
|
return run({ args: args.slice(1), pkgRoot: PKG_ROOT });
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Update-availability notifier for the `maude` CLI.
|
|
2
|
+
//
|
|
3
|
+
// Design (intentional, see PR discussion):
|
|
4
|
+
// • Hot path reads ONLY the local cache. Never blocks on a network fetch.
|
|
5
|
+
// • A detached child process refreshes the cache when stale (>24h). The
|
|
6
|
+
// notice for a freshly published version therefore appears on the run
|
|
7
|
+
// AFTER the cache rolls — same pattern as `update-notifier`.
|
|
8
|
+
// • Opt-out via MAUDE_NO_UPDATE_CHECK=1, NO_UPDATE_NOTIFIER=1, CI=true,
|
|
9
|
+
// or any non-TTY stderr (pipes, CI logs, etc.).
|
|
10
|
+
// • Best-effort everywhere: cache read/write, registry fetch, and spawn
|
|
11
|
+
// all swallow errors. The CLI must never fail because of this module.
|
|
12
|
+
|
|
13
|
+
import { spawn } from 'node:child_process';
|
|
14
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { homedir } from 'node:os';
|
|
16
|
+
import { dirname, join } from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const PKG_NAME = '@1agh/maude';
|
|
20
|
+
const REGISTRY_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
|
|
21
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
22
|
+
const FETCH_TIMEOUT_MS = 3000;
|
|
23
|
+
|
|
24
|
+
function cachePath() {
|
|
25
|
+
const base = process.env.XDG_CACHE_HOME || join(homedir(), '.cache');
|
|
26
|
+
return join(base, 'maude', 'update-check.json');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function shouldSkip() {
|
|
30
|
+
if (process.env.MAUDE_NO_UPDATE_CHECK) return true;
|
|
31
|
+
if (process.env.NO_UPDATE_NOTIFIER) return true;
|
|
32
|
+
if (process.env.CI) return true;
|
|
33
|
+
if (!process.stderr.isTTY) return true;
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Compare semver x.y.z (ignoring prerelease/build metadata). Returns
|
|
38
|
+
// positive if a > b, negative if a < b, 0 if equal.
|
|
39
|
+
export function cmpSemver(a, b) {
|
|
40
|
+
const parse = (v) =>
|
|
41
|
+
String(v)
|
|
42
|
+
.split(/[-+]/)[0]
|
|
43
|
+
.split('.')
|
|
44
|
+
.map((n) => Number(n) || 0);
|
|
45
|
+
const pa = parse(a);
|
|
46
|
+
const pb = parse(b);
|
|
47
|
+
for (let i = 0; i < 3; i += 1) {
|
|
48
|
+
const x = pa[i] || 0;
|
|
49
|
+
const y = pb[i] || 0;
|
|
50
|
+
if (x !== y) return x - y;
|
|
51
|
+
}
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readCacheSync() {
|
|
56
|
+
try {
|
|
57
|
+
const raw = readFileSync(cachePath(), 'utf8');
|
|
58
|
+
const parsed = JSON.parse(raw);
|
|
59
|
+
if (typeof parsed?.latest === 'string' && typeof parsed?.checkedAt === 'number') {
|
|
60
|
+
return parsed;
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
/* missing or corrupt — treat as no cache */
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function writeCacheSync(data) {
|
|
69
|
+
const path = cachePath();
|
|
70
|
+
try {
|
|
71
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
72
|
+
writeFileSync(path, JSON.stringify(data), 'utf8');
|
|
73
|
+
} catch {
|
|
74
|
+
/* read-only fs / no permission — non-fatal */
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function printNotice(current, latest) {
|
|
79
|
+
const msg = [
|
|
80
|
+
'',
|
|
81
|
+
` ⚠ maude update available: ${current} → ${latest}`,
|
|
82
|
+
` Run: npm i -g ${PKG_NAME}@latest (or pnpm add -g / bun add -g)`,
|
|
83
|
+
'',
|
|
84
|
+
'',
|
|
85
|
+
].join('\n');
|
|
86
|
+
process.stderr.write(msg);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function spawnDetachedRefresh() {
|
|
90
|
+
try {
|
|
91
|
+
const self = fileURLToPath(import.meta.url);
|
|
92
|
+
const child = spawn(process.execPath, [self, '--refresh'], {
|
|
93
|
+
detached: true,
|
|
94
|
+
stdio: 'ignore',
|
|
95
|
+
windowsHide: true,
|
|
96
|
+
env: { ...process.env, MAUDE_UPDATE_CHECK_CHILD: '1' },
|
|
97
|
+
});
|
|
98
|
+
child.unref();
|
|
99
|
+
} catch {
|
|
100
|
+
/* spawn failed — skip the refresh, will retry next run */
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function refreshCache() {
|
|
105
|
+
const ac = new AbortController();
|
|
106
|
+
const timer = setTimeout(() => ac.abort(), FETCH_TIMEOUT_MS);
|
|
107
|
+
try {
|
|
108
|
+
const res = await fetch(REGISTRY_URL, {
|
|
109
|
+
headers: { accept: 'application/json' },
|
|
110
|
+
signal: ac.signal,
|
|
111
|
+
});
|
|
112
|
+
if (!res.ok) return;
|
|
113
|
+
const json = await res.json();
|
|
114
|
+
if (typeof json?.version === 'string') {
|
|
115
|
+
writeCacheSync({ latest: json.version, checkedAt: Date.now() });
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
/* offline, DNS failure, registry down — ignore */
|
|
119
|
+
} finally {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Run the update check from the CLI entry. Synchronous-fast: reads cache,
|
|
126
|
+
* prints notice if outdated, kicks off a detached refresh when the cache
|
|
127
|
+
* is stale, returns immediately.
|
|
128
|
+
*/
|
|
129
|
+
export function runUpdateCheck(currentVersion) {
|
|
130
|
+
if (shouldSkip()) return;
|
|
131
|
+
if (!currentVersion) return;
|
|
132
|
+
|
|
133
|
+
const cache = readCacheSync();
|
|
134
|
+
if (cache?.latest && cmpSemver(cache.latest, currentVersion) > 0) {
|
|
135
|
+
printNotice(currentVersion, cache.latest);
|
|
136
|
+
}
|
|
137
|
+
const isStale = !cache || Date.now() - cache.checkedAt > CACHE_TTL_MS;
|
|
138
|
+
if (isStale) spawnDetachedRefresh();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Entrypoint when invoked as a detached refresh child:
|
|
142
|
+
// node cli/lib/update-check.mjs --refresh
|
|
143
|
+
if (process.argv[1]?.endsWith('update-check.mjs') && process.argv.includes('--refresh')) {
|
|
144
|
+
refreshCache().catch(() => process.exit(0));
|
|
145
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
import { cmpSemver } from './update-check.mjs';
|
|
4
|
+
|
|
5
|
+
test('cmpSemver: equal versions return 0', () => {
|
|
6
|
+
assert.equal(cmpSemver('0.18.2', '0.18.2'), 0);
|
|
7
|
+
assert.equal(cmpSemver('1.0.0', '1.0.0'), 0);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('cmpSemver: newer patch > older', () => {
|
|
11
|
+
assert.ok(cmpSemver('0.18.3', '0.18.2') > 0);
|
|
12
|
+
assert.ok(cmpSemver('0.18.2', '0.18.3') < 0);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('cmpSemver: newer minor > older', () => {
|
|
16
|
+
assert.ok(cmpSemver('0.19.0', '0.18.99') > 0);
|
|
17
|
+
assert.ok(cmpSemver('0.18.99', '0.19.0') < 0);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('cmpSemver: newer major > older', () => {
|
|
21
|
+
assert.ok(cmpSemver('1.0.0', '0.99.99') > 0);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('cmpSemver: prerelease tags stripped (compares core only)', () => {
|
|
25
|
+
assert.equal(cmpSemver('0.18.2-beta.1', '0.18.2'), 0);
|
|
26
|
+
assert.ok(cmpSemver('0.19.0-rc.0', '0.18.2') > 0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('cmpSemver: missing components default to 0', () => {
|
|
30
|
+
assert.equal(cmpSemver('1', '1.0.0'), 0);
|
|
31
|
+
assert.equal(cmpSemver('1.2', '1.2.0'), 0);
|
|
32
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1agh/maude",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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": {
|
|
@@ -41,13 +41,13 @@
|
|
|
41
41
|
"prepublishOnly": "bash scripts/check-version-parity.sh"
|
|
42
42
|
},
|
|
43
43
|
"optionalDependencies": {
|
|
44
|
-
"@1agh/maude-darwin-arm64": "0.
|
|
45
|
-
"@1agh/maude-darwin-x64": "0.
|
|
46
|
-
"@1agh/maude-linux-arm64": "0.
|
|
47
|
-
"@1agh/maude-linux-arm64-musl": "0.
|
|
48
|
-
"@1agh/maude-linux-x64": "0.
|
|
49
|
-
"@1agh/maude-linux-x64-musl": "0.
|
|
50
|
-
"@1agh/maude-win32-x64": "0.
|
|
44
|
+
"@1agh/maude-darwin-arm64": "0.19.0",
|
|
45
|
+
"@1agh/maude-darwin-x64": "0.19.0",
|
|
46
|
+
"@1agh/maude-linux-arm64": "0.19.0",
|
|
47
|
+
"@1agh/maude-linux-arm64-musl": "0.19.0",
|
|
48
|
+
"@1agh/maude-linux-x64": "0.19.0",
|
|
49
|
+
"@1agh/maude-linux-x64-musl": "0.19.0",
|
|
50
|
+
"@1agh/maude-win32-x64": "0.19.0"
|
|
51
51
|
},
|
|
52
52
|
"files": [
|
|
53
53
|
"cli",
|
|
@@ -839,12 +839,12 @@ export function createApi(ctx: Context, onCommentsChanged: (file: string) => voi
|
|
|
839
839
|
}
|
|
840
840
|
const items: { label: string; path: string; group: string }[] = [];
|
|
841
841
|
for (const m of matches) {
|
|
842
|
-
const files = await findFiles(m.abs, m.rel, ['.html']);
|
|
842
|
+
const files = await findFiles(m.abs, m.rel, ['.tsx', '.html']);
|
|
843
843
|
for (const f of files) {
|
|
844
844
|
const fname = f
|
|
845
845
|
.split('/')
|
|
846
846
|
.pop()
|
|
847
|
-
?.replace(/\.html$/i, '');
|
|
847
|
+
?.replace(/\.(tsx|html)$/i, '');
|
|
848
848
|
const group = f.split('/').slice(-2, -1)[0] || folderName;
|
|
849
849
|
const label = fname.toLowerCase() === 'index' ? group : fname;
|
|
850
850
|
items.push({ label, path: f, group });
|
|
@@ -27,15 +27,24 @@ import path from 'node:path';
|
|
|
27
27
|
|
|
28
28
|
import type { BunPlugin } from 'bun';
|
|
29
29
|
|
|
30
|
+
import { DEV_SERVER_ROOT } from './paths.ts';
|
|
31
|
+
|
|
30
32
|
export const CANVAS_LIB_SPECIFIER = '@maude/canvas-lib';
|
|
31
33
|
|
|
32
34
|
/**
|
|
33
35
|
* Returns the dev-server-internal canvas-lib path. The `_designRoot` parameter
|
|
34
36
|
* is retained for one minor (back-compat with callers we don't control) but
|
|
35
37
|
* ignored — canvas-lib now ships with the dev-server install (DDR-025).
|
|
38
|
+
*
|
|
39
|
+
* Uses DEV_SERVER_ROOT (paths.ts) instead of `import.meta.dir` per DDR-045 —
|
|
40
|
+
* inside `bun --compile` standalone binaries `import.meta.dir` resolves to the
|
|
41
|
+
* virtual `/$bunfs/root` and any subsequent `existsSync` / `fs.watch` against
|
|
42
|
+
* that path silently fails. Same bug class as v0.18.0/0.18.1; this one surfaces
|
|
43
|
+
* as `[canvas-lib] failed to watch ... '/$bunfs/root/canvas-lib.tsx'` at boot
|
|
44
|
+
* and disables canvas-lib HMR.
|
|
36
45
|
*/
|
|
37
46
|
export function canvasLibPath(_designRoot?: string): string {
|
|
38
|
-
return path.join(
|
|
47
|
+
return path.join(DEV_SERVER_ROOT, 'canvas-lib.tsx');
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
export interface CanvasLibResolverOptions {
|
|
@@ -1073,7 +1073,7 @@ function Viewport({ tabs, activePath, registerIframe, systemData, onOpenFromSyst
|
|
|
1073
1073
|
if (t.path === SYSTEM_TAB) {
|
|
1074
1074
|
return (
|
|
1075
1075
|
<div key={t.path} className={'system-view' + (t.path === activePath ? ' active' : '')}>
|
|
1076
|
-
<SystemView data={systemData} onOpen={onOpenFromSystem} />
|
|
1076
|
+
<SystemView data={systemData} onOpen={onOpenFromSystem} cfg={cfg} />
|
|
1077
1077
|
</div>
|
|
1078
1078
|
);
|
|
1079
1079
|
}
|
|
@@ -1152,7 +1152,7 @@ function TypeLadder() {
|
|
|
1152
1152
|
);
|
|
1153
1153
|
}
|
|
1154
1154
|
|
|
1155
|
-
function SystemView({ data, onOpen }) {
|
|
1155
|
+
function SystemView({ data, onOpen, cfg }) {
|
|
1156
1156
|
if (!data) {
|
|
1157
1157
|
return <div className="sv-empty"><p>Loading design system…</p></div>;
|
|
1158
1158
|
}
|
|
@@ -1176,15 +1176,15 @@ function SystemView({ data, onOpen }) {
|
|
|
1176
1176
|
</div>
|
|
1177
1177
|
) : (
|
|
1178
1178
|
<>
|
|
1179
|
-
<Gallery title="preview" items={previewGallery} onOpen={onOpen} kind="preview" />
|
|
1180
|
-
<Gallery title="ui kits" items={uiKitsGallery} onOpen={onOpen} kind="ui_kits" />
|
|
1179
|
+
<Gallery title="preview" items={previewGallery} onOpen={onOpen} kind="preview" cfg={cfg} />
|
|
1180
|
+
<Gallery title="ui kits" items={uiKitsGallery} onOpen={onOpen} kind="ui_kits" cfg={cfg} />
|
|
1181
1181
|
</>
|
|
1182
1182
|
)}
|
|
1183
1183
|
</div>
|
|
1184
1184
|
);
|
|
1185
1185
|
}
|
|
1186
1186
|
|
|
1187
|
-
function Gallery({ title, items, onOpen, kind }) {
|
|
1187
|
+
function Gallery({ title, items, onOpen, kind, cfg }) {
|
|
1188
1188
|
if (!items || items.length === 0) return null;
|
|
1189
1189
|
return (
|
|
1190
1190
|
<section className="sv-section">
|
|
@@ -1193,7 +1193,7 @@ function Gallery({ title, items, onOpen, kind }) {
|
|
|
1193
1193
|
{items.map(p => (
|
|
1194
1194
|
<article key={p.path} className="sv-preview-card" onClick={() => onOpen(p.path)}>
|
|
1195
1195
|
<div className="sv-preview-frame">
|
|
1196
|
-
<iframe src={
|
|
1196
|
+
<iframe src={canvasUrl(p.path, cfg)} title={p.label} scrolling="no" />
|
|
1197
1197
|
</div>
|
|
1198
1198
|
<div className="sv-preview-foot">
|
|
1199
1199
|
<strong>{p.label}</strong>
|