@1agh/maude 0.21.0 → 0.22.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.
@@ -252,96 +252,107 @@ function printReport({ depsByPlugin, configReport, summary }) {
252
252
  async function applyFixes({ repoRoot, configFsPath, depsByPlugin, configReport }) {
253
253
  let configChanged = false;
254
254
 
255
- // ── Dependency installs ────────────────────────────────────────────────
256
- const installable = [];
257
- for (const env of Object.values(depsByPlugin)) {
258
- if (!env.results) continue;
259
- for (const r of env.results) {
260
- if (r.status !== 'missing') continue;
261
- // Find the original dep entry to read autoInstall + install commands.
262
- // checkAll dropped the manifest entry but kept r.install — we use that.
263
- installable.push(r);
264
- }
265
- }
266
- // Per-dep prompt — no silent installs.
267
- if (installable.length > 0) {
268
- process.stdout.write('\n--- Dependency installs (per-dep prompt) ---\n');
269
- const rl = createInterface({ input: stdin, output: stdout });
270
- for (const r of installable) {
271
- const cmd = (r.install && (r.install[process.platform] || r.install.preferred)) || null;
272
- if (!cmd) {
273
- process.stdout.write(` skip ${r.id} — no install command declared\n`);
274
- continue;
275
- }
276
- // eslint-disable-next-line no-await-in-loop
277
- const ans = await rl.question(` Install ${r.id} via \`${cmd}\`? [y/N] `);
278
- if (ans.trim().toLowerCase() === 'y' || ans.trim().toLowerCase() === 'yes') {
279
- process.stdout.write(` running: ${cmd}\n`);
280
- const res = spawnSync('bash', ['-c', cmd], { stdio: 'inherit' });
281
- if (res.status !== 0) process.stdout.write(` ✗ install failed (exit ${res.status})\n`);
282
- } else {
283
- process.stdout.write(` skipped ${r.id}\n`);
255
+ // ── Config edits ───────────────────────────────────────────────────────
256
+ // Run FIRST — deterministic (filesystem only) and must not be blocked by
257
+ // an interactive prompt that may never resolve on a non-TTY stdin (CI,
258
+ // scripted spawns with empty input). Dep prompts come AFTER.
259
+ if (configReport.exists) {
260
+ const config = JSON.parse(readFileSync(configFsPath, 'utf8'));
261
+
262
+ // Drop unknown additionalProperties errors.
263
+ for (const e of configReport.schema.errors) {
264
+ if (e.message.startsWith('unknown property')) {
265
+ const parts = e.path.split('/').filter(Boolean);
266
+ let cur = config;
267
+ for (let i = 0; i < parts.length - 1; i++) cur = cur?.[parts[i]];
268
+ if (cur && parts.length > 0) {
269
+ delete cur[parts[parts.length - 1]];
270
+ configChanged = true;
271
+ }
284
272
  }
285
273
  }
286
- rl.close();
287
- }
288
274
 
289
- // ── Config edits ────────────────────────────────────────────────────────
290
- if (!configReport.exists) return;
291
- const config = JSON.parse(readFileSync(configFsPath, 'utf8'));
292
-
293
- // Drop unknown additionalProperties errors.
294
- for (const e of configReport.schema.errors) {
295
- if (e.message.startsWith('unknown property')) {
296
- const parts = e.path.split('/').filter(Boolean);
297
- let cur = config;
298
- for (let i = 0; i < parts.length - 1; i++) cur = cur?.[parts[i]];
299
- if (cur && parts.length > 0) {
300
- delete cur[parts[parts.length - 1]];
275
+ // Apply drift silent (detector returned a concrete value, so we know
276
+ // declared is wrong or stale).
277
+ if (configReport.drift.length > 0) {
278
+ if (!config.stack) config.stack = {};
279
+ for (const d of configReport.drift) {
280
+ config.stack[d.key] = d.detected;
301
281
  configChanged = true;
302
282
  }
303
283
  }
304
- }
305
284
 
306
- // Apply drift — silent (detector returned a concrete value, so we know
307
- // declared is wrong or stale).
308
- if (configReport.drift.length > 0) {
309
- if (!config.stack) config.stack = {};
310
- for (const d of configReport.drift) {
311
- config.stack[d.key] = d.detected;
312
- configChanged = true;
285
+ // Add missing quality gates — silent additive merge.
286
+ if (configReport.qualityAdditions.length > 0) {
287
+ if (!config.quality) config.quality = {};
288
+ for (const a of configReport.qualityAdditions) {
289
+ if (!config.quality[a.gate]) {
290
+ config.quality[a.gate] = a.command;
291
+ configChanged = true;
292
+ }
293
+ }
313
294
  }
314
- }
315
295
 
316
- // Add missing quality gates — silent additive merge.
317
- if (configReport.qualityAdditions.length > 0) {
318
- if (!config.quality) config.quality = {};
319
- for (const a of configReport.qualityAdditions) {
320
- if (!config.quality[a.gate]) {
321
- config.quality[a.gate] = a.command;
322
- configChanged = true;
296
+ if (configChanged) {
297
+ writeFileSync(configFsPath, `${JSON.stringify(config, null, 2)}\n`);
298
+ process.stdout.write(`\n ✓ wrote ${CONFIG_PATH}\n`);
299
+ } else {
300
+ process.stdout.write('\n (no config edits applied)\n');
301
+ }
302
+
303
+ // Note: invalid-enum errors (e.g. tests: "node-test") are NEVER auto-
304
+ // fixed — the user picks the migration target. Surface the leftover.
305
+ const remaining = configReport.schema.errors.filter(
306
+ (e) => !e.message.startsWith('unknown property')
307
+ );
308
+ if (remaining.length > 0) {
309
+ process.stdout.write(
310
+ `\n ⚠ ${remaining.length} schema error${remaining.length === 1 ? '' : 's'} not auto-fixed — user decision required:\n`
311
+ );
312
+ for (const e of remaining) {
313
+ process.stdout.write(` ${e.path}: ${e.message}\n`);
323
314
  }
324
315
  }
325
316
  }
326
317
 
327
- if (configChanged) {
328
- writeFileSync(configFsPath, `${JSON.stringify(config, null, 2)}\n`);
329
- process.stdout.write(`\n ✓ wrote ${CONFIG_PATH}\n`);
330
- } else {
331
- process.stdout.write('\n (no config edits applied)\n');
318
+ // ── Dependency installs ────────────────────────────────────────────────
319
+ const installable = [];
320
+ for (const env of Object.values(depsByPlugin)) {
321
+ if (!env.results) continue;
322
+ for (const r of env.results) {
323
+ if (r.status !== 'missing') continue;
324
+ installable.push(r);
325
+ }
332
326
  }
333
-
334
- // Note: invalid-enum errors (e.g. tests: "node-test") are NEVER auto-
335
- // fixed the user picks the migration target. Surface the leftover.
336
- const remaining = configReport.schema.errors.filter(
337
- (e) => !e.message.startsWith('unknown property')
338
- );
339
- if (remaining.length > 0) {
340
- process.stdout.write(
341
- `\n ⚠ ${remaining.length} schema error${remaining.length === 1 ? '' : 's'} not auto-fixed — user decision required:\n`
342
- );
343
- for (const e of remaining) {
344
- process.stdout.write(` ${e.path}: ${e.message}\n`);
327
+ // Per-dep prompt — no silent installs. Skip when stdin is not a TTY
328
+ // (CI runners, scripted spawns) so config edits above are never bypassed
329
+ // by a never-resolving readline prompt against closed stdin.
330
+ if (installable.length > 0) {
331
+ if (!stdin.isTTY) {
332
+ process.stdout.write(
333
+ `\n--- Dependency installs skipped (non-interactive stdin; ${installable.length} missing) ---\n`
334
+ );
335
+ } else {
336
+ process.stdout.write('\n--- Dependency installs (per-dep prompt) ---\n');
337
+ const rl = createInterface({ input: stdin, output: stdout });
338
+ for (const r of installable) {
339
+ const cmd = (r.install && (r.install[process.platform] || r.install.preferred)) || null;
340
+ if (!cmd) {
341
+ process.stdout.write(` skip ${r.id} — no install command declared\n`);
342
+ continue;
343
+ }
344
+ // eslint-disable-next-line no-await-in-loop
345
+ const ans = await rl.question(` Install ${r.id} via \`${cmd}\`? [y/N] `);
346
+ const norm = (ans || '').trim().toLowerCase();
347
+ if (norm === 'y' || norm === 'yes') {
348
+ process.stdout.write(` running: ${cmd}\n`);
349
+ const res = spawnSync('bash', ['-c', cmd], { stdio: 'inherit' });
350
+ if (res.status !== 0) process.stdout.write(` ✗ install failed (exit ${res.status})\n`);
351
+ } else {
352
+ process.stdout.write(` skipped ${r.id}\n`);
353
+ }
354
+ }
355
+ rl.close();
345
356
  }
346
357
  }
347
358
 
@@ -37,13 +37,18 @@ function usage() {
37
37
  Start the self-hostable Yjs sync hub in the current process tree.
38
38
  --port listen port (default 1234, env PORT)
39
39
  --data hub.db + tokens.json dir (default ./data, env DATA_DIR)
40
- --secret HUB_SECRET escape-hatch token (env HUB_SECRET)
40
+ --secret HUB_SECRET escape-hatch token (env HUB_SECRET).
41
+ If unset on an empty hub, a one-time bootstrap link is
42
+ printed to logs — open it in a browser to claim admin.
41
43
  --insecure-http cosmetic log-only flag for non-TLS dev
42
44
  --dev generate a one-shot mau_dev_<hex> token, print the
43
45
  connect command, then run the hub. Convenience for
44
46
  contributor onboarding — drops tokens.json on exit
45
47
  is NOT performed (tokens persist; clean by hand).
46
48
 
49
+ On boot the hub prints its /admin URL. The admin UI generates invite
50
+ tokens, lists peers, and rotates tokens without shelling into the host.
51
+
47
52
  token generate --label NAME [--data PATH] [--dev]
48
53
  Generate a new mau_<32hex> token and append it to
49
54
  <data>/tokens.json with the given label. Prints the raw token ONCE,
@@ -51,9 +56,12 @@ function usage() {
51
56
 
52
57
  --dev produces a mau_dev_<hex> token (convention only — same auth).
53
58
 
59
+ Equivalent to the "Generate invite" button in the /admin UI — use
60
+ whichever is more convenient for the deploy.
61
+
54
62
  status [URL] [--json]
55
- HTTP GET <url>/health, print uptime/version/token-count. URL defaults
56
- to http://localhost:1234. --json emits the raw response.
63
+ HTTP GET <url>/health, print uptime/version/token-count/peers. URL
64
+ defaults to http://localhost:1234. --json emits the raw response.
57
65
 
58
66
  NOTES
59
67
  Local dev only in v1.1 Task 2 slice — production-install packaging
@@ -64,6 +72,12 @@ EXAMPLES
64
72
  maude hub serve --port 4400
65
73
  maude hub token generate --label alice
66
74
  maude hub status http://localhost:4400
75
+
76
+ # Recommended flow on a fresh deploy:
77
+ # 1. maude hub serve → copy bootstrap link from logs
78
+ # 2. open the link in browser → first-run wizard, mint admin secret
79
+ # 3. click "Generate invite" → copy 'maude design link …' command
80
+ # 4. paste on the peer machine → linked
67
81
  `;
68
82
  }
69
83
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1agh/maude",
3
- "version": "0.21.0",
3
+ "version": "0.22.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.21.0",
45
- "@1agh/maude-darwin-x64": "0.21.0",
46
- "@1agh/maude-linux-arm64": "0.21.0",
47
- "@1agh/maude-linux-arm64-musl": "0.21.0",
48
- "@1agh/maude-linux-x64": "0.21.0",
49
- "@1agh/maude-linux-x64-musl": "0.21.0",
50
- "@1agh/maude-win32-x64": "0.21.0"
44
+ "@1agh/maude-darwin-arm64": "0.22.0",
45
+ "@1agh/maude-darwin-x64": "0.22.0",
46
+ "@1agh/maude-linux-arm64": "0.22.0",
47
+ "@1agh/maude-linux-arm64-musl": "0.22.0",
48
+ "@1agh/maude-linux-x64": "0.22.0",
49
+ "@1agh/maude-linux-x64-musl": "0.22.0",
50
+ "@1agh/maude-win32-x64": "0.22.0"
51
51
  },
52
52
  "files": [
53
53
  "cli",