@1agh/maude 0.49.2 → 0.51.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.
Files changed (39) hide show
  1. package/apps/studio/api.ts +751 -52
  2. package/apps/studio/canvas-artifacts.ts +153 -0
  3. package/apps/studio/canvas-create.ts +23 -0
  4. package/apps/studio/canvas-slug.ts +30 -0
  5. package/apps/studio/client/app.jsx +547 -90
  6. package/apps/studio/client/panels/CloudBar.jsx +388 -0
  7. package/apps/studio/client/styles/3-shell-maude.css +28 -0
  8. package/apps/studio/client/tree-row-menu.jsx +132 -0
  9. package/apps/studio/client/use-tree-drag.js +120 -0
  10. package/apps/studio/cloud/endpoints.ts +284 -0
  11. package/apps/studio/collab/registry.ts +23 -0
  12. package/apps/studio/debug-bundle.ts +144 -0
  13. package/apps/studio/dist/client.bundle.js +1235 -1228
  14. package/apps/studio/dist/styles.css +1 -1
  15. package/apps/studio/http.ts +205 -1
  16. package/apps/studio/inspect.ts +49 -0
  17. package/apps/studio/server.ts +15 -0
  18. package/apps/studio/sync/workspace-signin.ts +15 -0
  19. package/apps/studio/test/canvas-artifacts.test.ts +125 -0
  20. package/apps/studio/test/canvas-create-api.test.ts +159 -1
  21. package/apps/studio/test/canvas-move-api.test.ts +556 -0
  22. package/apps/studio/test/canvas-origin-gate.test.ts +6 -0
  23. package/apps/studio/test/cloud-endpoints.test.ts +130 -0
  24. package/apps/studio/test/debug-bundle.test.ts +105 -0
  25. package/apps/studio/test/fs-mkdir-api.test.ts +248 -0
  26. package/apps/studio/test/workspace-signin.test.ts +19 -0
  27. package/apps/studio/whats-new.json +27 -0
  28. package/cli/bin/maude.mjs +1 -0
  29. package/cli/commands/hub-workspace.mjs +294 -7
  30. package/cli/commands/init.mjs +33 -2
  31. package/cli/commands/kg.mjs +129 -4
  32. package/cli/commands/share.mjs +184 -0
  33. package/cli/lib/ddr-to-kgai.mjs +310 -82
  34. package/cli/lib/ddr-to-kgai.test.mjs +20 -0
  35. package/cli/lib/share-plan.mjs +96 -0
  36. package/cli/lib/share-plan.test.mjs +57 -0
  37. package/cli/lib/workspace-plan.mjs +130 -12
  38. package/cli/lib/workspace-plan.test.mjs +160 -0
  39. package/package.json +9 -8
@@ -0,0 +1,184 @@
1
+ // `maude share` — publish a read-only view of a project. Cloud Phase 18.
2
+ //
3
+ // The effects half; every decision lives in cli/lib/share-plan.mjs.
4
+ //
5
+ // RENDERING HAPPENS HERE, ON THIS MACHINE. That is the containment invariant
6
+ // (DDR-193 §2, narrowed by DDR-197), not an implementation detail: the vendor
7
+ // serves finished pictures it never produced and never interprets. Moving this
8
+ // to the server would be a one-line convenience that deletes the guarantee.
9
+ //
10
+ // maude share publish --project alligators --from <dir> [--name "Brno Alligators"]
11
+ // maude share off --project alligators
12
+ //
13
+ // `--from` is a directory of screenshots — typically what `maude design smoke
14
+ // --out-dir <dir>` just wrote, which is already one PNG per canvas.
15
+
16
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
17
+ import { join, resolve } from 'node:path';
18
+
19
+ import { parseArgs } from '../lib/argv.mjs';
20
+ import { publishPlan, publishSummary, shareMarker, validProjectId } from '../lib/share-plan.mjs';
21
+
22
+ export function usage() {
23
+ return `maude share <publish|off> [options]
24
+
25
+ Publish a read-only view of a design project, so someone can look at it in a
26
+ browser without installing anything.
27
+
28
+ publish upload the pictures in --from and turn sharing ON
29
+ off turn sharing off (the pictures stay; nothing serves them)
30
+
31
+ --project ID the project (lowercase, hyphens)
32
+ --from DIR directory of screenshots to publish (publish only)
33
+ --name "Title" what the view calls itself (default: the project id)
34
+ --zone HOST default cloud.maude.sh
35
+ --dry-run print the plan, upload nothing
36
+ --json machine-readable result
37
+
38
+ Storage credentials come from the environment:
39
+ MAUDE_R2_ENDPOINT MAUDE_R2_ACCESS_KEY_ID MAUDE_R2_SECRET_ACCESS_KEY
40
+ MAUDE_R2_BUCKET (default maude-cloud-assets)
41
+
42
+ The pictures are taken on THIS machine and uploaded as finished images. The
43
+ shared view never renders anything — that is what makes it safe to hand to
44
+ someone who is not on your team.
45
+ `;
46
+ }
47
+
48
+ /** Every file under `dir`, relative to it. */
49
+ function listFiles(dir, prefix = '', out = []) {
50
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
51
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
52
+ if (entry.isDirectory()) listFiles(join(dir, entry.name), rel, out);
53
+ else if (entry.isFile()) out.push(rel);
54
+ }
55
+ return out;
56
+ }
57
+
58
+ function s3ConfigFromEnv(env = process.env) {
59
+ const missing = ['MAUDE_R2_ENDPOINT', 'MAUDE_R2_ACCESS_KEY_ID', 'MAUDE_R2_SECRET_ACCESS_KEY'].filter(
60
+ (k) => !env[k]
61
+ );
62
+ if (missing.length) {
63
+ throw new Error(
64
+ `object storage is not configured — missing ${missing.join(', ')}.\n` +
65
+ 'A publish needs somewhere to put the pictures.'
66
+ );
67
+ }
68
+ return {
69
+ endpoint: env.MAUDE_R2_ENDPOINT,
70
+ bucket: env.MAUDE_R2_BUCKET ?? 'maude-cloud-assets',
71
+ accessKeyId: env.MAUDE_R2_ACCESS_KEY_ID,
72
+ secretAccessKey: env.MAUDE_R2_SECRET_ACCESS_KEY,
73
+ region: 'auto',
74
+ };
75
+ }
76
+
77
+ async function loadS3(pkgRoot) {
78
+ for (const candidate of [
79
+ resolve(pkgRoot, 'apps/hub/src/s3.mjs'),
80
+ resolve(pkgRoot, '../apps/hub/src/s3.mjs'),
81
+ ]) {
82
+ const mod = await import(`file://${candidate}`).catch(() => null);
83
+ if (mod) return mod;
84
+ }
85
+ throw new Error('the storage client (apps/hub/src/s3.mjs) was not found in this install');
86
+ }
87
+
88
+ export async function run({ args, pkgRoot }) {
89
+ const { positional, flags } = parseArgs(args, { booleans: ['help', 'dry-run', 'json'] });
90
+ const sub = positional[0];
91
+ if (flags.help || !sub) {
92
+ process.stdout.write(usage());
93
+ return;
94
+ }
95
+
96
+ const project = validProjectId(flags.project);
97
+ if (!project) {
98
+ process.stderr.write('maude share: --project is required (lowercase letters, digits, hyphens)\n');
99
+ process.exit(2);
100
+ }
101
+ const zone = String(flags.zone ?? 'cloud.maude.sh');
102
+
103
+ if (sub === 'off') {
104
+ const s3 = await loadS3(pkgRoot);
105
+ const marker = shareMarker(project, { enabled: false, name: flags.name });
106
+ if (flags['dry-run']) {
107
+ process.stdout.write(`Would turn sharing OFF for ${project}.\n`);
108
+ return;
109
+ }
110
+ await s3.putObject(s3ConfigFromEnv(), marker.key, Buffer.from(marker.body));
111
+ process.stdout.write(
112
+ `Sharing is off for ${project}. The link no longer shows anything.\n` +
113
+ 'The pictures are still in storage — nothing serves them.\n'
114
+ );
115
+ return;
116
+ }
117
+
118
+ if (sub !== 'publish') {
119
+ process.stderr.write(`maude share: unknown command "${sub}"\n`);
120
+ process.exit(2);
121
+ }
122
+
123
+ const from = flags.from ? resolve(String(flags.from)) : null;
124
+ if (!from) {
125
+ process.stderr.write('maude share publish: --from <dir> is required\n');
126
+ process.exit(2);
127
+ }
128
+ try {
129
+ if (!statSync(from).isDirectory()) throw new Error('not a directory');
130
+ } catch {
131
+ process.stderr.write(`maude share publish: ${from} is not a directory\n`);
132
+ process.exit(2);
133
+ }
134
+
135
+ const { uploads, skipped } = publishPlan(listFiles(from), project);
136
+ if (uploads.length === 0) {
137
+ process.stderr.write(
138
+ `maude share publish: nothing to publish in ${from}.\n` +
139
+ 'The shared view serves PNG, JPEG, WebP and AVIF. Take screenshots first\n' +
140
+ '(`maude design smoke --out-dir <dir>` writes one per canvas).\n'
141
+ );
142
+ process.exit(1);
143
+ }
144
+
145
+ if (flags['dry-run']) {
146
+ process.stdout.write(`Would publish ${uploads.length} file(s) for ${project}:\n`);
147
+ for (const u of uploads.slice(0, 20)) process.stdout.write(` ${u.from} → ${u.key}\n`);
148
+ if (uploads.length > 20) process.stdout.write(` …and ${uploads.length - 20} more\n`);
149
+ if (skipped.length) process.stdout.write(`Would skip ${skipped.length} unshareable file(s).\n`);
150
+ return;
151
+ }
152
+
153
+ const s3 = await loadS3(pkgRoot);
154
+ const cfg = s3ConfigFromEnv();
155
+ let uploaded = 0;
156
+ const failed = [];
157
+ for (const u of uploads) {
158
+ try {
159
+ await s3.putObject(cfg, u.key, readFileSync(join(from, u.from)));
160
+ uploaded += 1;
161
+ } catch (err) {
162
+ failed.push({ file: u.from, reason: err.message.slice(0, 120) });
163
+ }
164
+ }
165
+
166
+ // The marker goes LAST. Its presence is what turns sharing on, so writing it
167
+ // first would expose a half-published gallery — the same reasoning as the
168
+ // backup manifest.
169
+ const marker = shareMarker(project, { enabled: true, name: flags.name });
170
+ await s3.putObject(cfg, marker.key, Buffer.from(marker.body));
171
+
172
+ if (flags.json) {
173
+ process.stdout.write(
174
+ `${JSON.stringify({ ok: failed.length === 0, project, uploaded, skipped: skipped.length, failed, url: `https://view-${project}.${zone}` }, null, 2)}\n`
175
+ );
176
+ return;
177
+ }
178
+ process.stdout.write(`${publishSummary({ project, uploaded, skipped: skipped.length, zone })}\n`);
179
+ if (failed.length) {
180
+ process.stderr.write(`\n${failed.length} file(s) failed to upload:\n`);
181
+ for (const f of failed.slice(0, 5)) process.stderr.write(` ${f.file} — ${f.reason}\n`);
182
+ process.exit(1);
183
+ }
184
+ }