@hawkeyexl/manni 2.1.0 → 2.2.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/README.md CHANGED
@@ -98,7 +98,7 @@ actually reads rather than hand-written:
98
98
  ```yaml
99
99
  repos:
100
100
  - repo: https://github.com/hawkeyexl/manni
101
- rev: v2.1.0
101
+ rev: v2.2.0
102
102
  hooks:
103
103
  - id: manni-meta
104
104
  ```
@@ -8,13 +8,20 @@ import {
8
8
  DocmetaError,
9
9
  FILL_FORMATS,
10
10
  FILL_FORMAT_LIST,
11
+ LOCATION_EXTERNAL_SCHEMA,
12
+ LOCATION_PAGE_SCHEMA,
11
13
  OMITTED_WHEN_CLEAN,
12
14
  QUERY_FORMATS,
13
15
  QUERY_FORMAT_LIST,
14
16
  REPORT_FORMATS,
15
17
  REPORT_FORMAT_LIST,
18
+ STDIN_LABEL,
19
+ Validator,
20
+ applyRelocation,
21
+ collectSchemaPins,
16
22
  deriveFailed,
17
23
  fail,
24
+ formatList,
18
25
  getSchemasInfo,
19
26
  isCommonFormat,
20
27
  isDeriveFormat,
@@ -24,28 +31,37 @@ import {
24
31
  isQueryFindingsFormat,
25
32
  isQueryFormat,
26
33
  isReportFormat,
34
+ keyHome,
27
35
  notice,
36
+ offerPrompt,
28
37
  package_default,
29
38
  palette,
39
+ planRelocation,
40
+ programName,
41
+ relocateFailed,
42
+ relocationContext,
30
43
  render,
31
44
  renderDerive,
32
45
  renderFill,
33
46
  renderGet,
34
47
  renderQuery,
35
48
  renderQueryCsv,
49
+ resolveRunConfig,
36
50
  rowsToFindings,
37
51
  runDerive,
38
52
  runFill,
39
53
  runGet,
40
54
  runInferSchema,
41
55
  runQuery,
56
+ runRelocate,
42
57
  runValidate,
43
58
  runVendorSchema,
59
+ schemaLoadOptions,
44
60
  shouldColor,
45
61
  supportedExtensions,
46
62
  terminalConfirm,
47
63
  toJsonText
48
- } from "./chunk-2DC2GSRH.js";
64
+ } from "./chunk-5O4FIGDJ.js";
49
65
 
50
66
  // src/meta/cli.ts
51
67
  import { existsSync } from "node:fs";
@@ -82,6 +98,238 @@ function reportConfig(toStdout, cwd) {
82
98
  };
83
99
  }
84
100
 
101
+ // src/meta/reporters/relocate.ts
102
+ var RELOCATE_FORMATS = ["pretty", "json"];
103
+ var RELOCATE_FORMAT_LIST = formatList(RELOCATE_FORMATS);
104
+ function isRelocateFormat(value) {
105
+ return RELOCATE_FORMATS.includes(value);
106
+ }
107
+ function renderRelocate(result, format, opts = {}) {
108
+ switch (format) {
109
+ case "json":
110
+ return JSON.stringify(relocateJson(result), null, 2);
111
+ case "pretty":
112
+ return renderRelocatePretty(result, opts);
113
+ default: {
114
+ const unreachable = format;
115
+ throw new DocmetaError(
116
+ `relocate --format must be pretty or json; got ${JSON.stringify(unreachable)}.`
117
+ );
118
+ }
119
+ }
120
+ }
121
+ function relocateJson(result) {
122
+ return {
123
+ dryRun: result.dryRun,
124
+ config: {
125
+ file: result.config.file,
126
+ created: result.config.created,
127
+ collectionsCreated: result.config.collectionsCreated,
128
+ pathsAdded: result.config.pathsAdded
129
+ },
130
+ manifests: result.manifests.map((m) => ({
131
+ file: m.file,
132
+ collection: m.collection,
133
+ created: m.created,
134
+ keysAdded: m.keysAdded,
135
+ keysRemoved: m.keysRemoved,
136
+ undeclared: m.undeclared
137
+ })),
138
+ files: result.files.map((f) => ({
139
+ file: f.file,
140
+ moved: f.moved,
141
+ stayed: f.stayed.map((s) => ({ key: s.key, reason: s.reason }))
142
+ })),
143
+ summary: result.summary
144
+ };
145
+ }
146
+ var plural = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
147
+ var list = (xs) => xs.join(", ");
148
+ function renderRelocatePretty(result, opts = {}) {
149
+ const c = palette(opts.color ?? false);
150
+ const dry = result.dryRun;
151
+ const lines = [];
152
+ if (result.config.created && result.config.file !== null) {
153
+ lines.push(dry ? `Would create ${result.config.file}.` : `Created ${result.config.file}.`);
154
+ }
155
+ const pathsOf = (collection) => result.config.pathsAdded.filter((p) => p.collection === collection).map((p) => p.path);
156
+ const said = /* @__PURE__ */ new Set();
157
+ for (const created of result.config.collectionsCreated) {
158
+ said.add(created.name);
159
+ const made = result.manifests.filter((m) => m.collection === created.name && m.created);
160
+ const head = `${dry ? "Would create" : "Created"} collection ${created.name} (paths: ${list(created.paths)})`;
161
+ if (made.length === 0) {
162
+ lines.push(`${head}.`);
163
+ continue;
164
+ }
165
+ const owns = list(made.flatMap((m) => m.keys));
166
+ lines.push(`${head} and ${list(made.map((m) => m.file))}; it ${dry ? "would own" : "owns"} ${owns}.`);
167
+ }
168
+ for (const m of result.manifests) {
169
+ if (said.has(m.collection) && m.created) continue;
170
+ const added = said.has(m.collection) ? [] : pathsOf(m.collection);
171
+ if (added.length > 0) said.add(m.collection);
172
+ const keys = list(m.keys);
173
+ if (m.created) {
174
+ if (added.length > 0) {
175
+ lines.push(
176
+ dry ? `Would add ${list(added)} to collection ${m.collection}'s paths and create ${m.file}; it would own ${keys}.` : `Added ${list(added)} to collection ${m.collection}'s paths and created ${m.file}; it owns ${keys}.`
177
+ );
178
+ } else {
179
+ lines.push(
180
+ dry ? `Would create ${m.file}; collections[${m.collection}].externalMetadata[${m.index}] would own ${keys}.` : `Created ${m.file}; collections[${m.collection}].externalMetadata[${m.index}] owns ${keys}.`
181
+ );
182
+ }
183
+ continue;
184
+ }
185
+ const beyond = m.beyond > 0 ? `, ${m.beyond} beyond the paths you named` : "";
186
+ const pronoun = (n) => n === 1 ? "it" : "them";
187
+ if (m.keysRemoved.length > 0) {
188
+ const what = `${list(m.keysRemoved)} from ${m.file}'s keys`;
189
+ const it = pronoun(m.keysRemoved.length);
190
+ const where = `into every page in collection ${m.collection}${beyond}`;
191
+ lines.push(dry ? `Would remove ${what}, moving ${it} ${where}.` : `Removing ${what} moves ${it} ${where}.`);
192
+ }
193
+ if (added.length > 0) {
194
+ lines.push(
195
+ dry ? `Would add ${list(added)} to collection ${m.collection}'s paths; ${m.file} would own ${keys}.` : `Added ${list(added)} to collection ${m.collection}'s paths; ${m.file} now owns ${keys}.`
196
+ );
197
+ } else if (m.keysAdded.length > 0) {
198
+ const what = `${list(m.keysAdded)} to ${m.file}'s keys`;
199
+ const it = pronoun(m.keysAdded.length);
200
+ const where = `out of every page in collection ${m.collection}${beyond}`;
201
+ lines.push(dry ? `Would add ${what}, moving ${it} ${where}.` : `Adding ${what} moves ${it} ${where}.`);
202
+ }
203
+ if (m.undeclared) {
204
+ lines.push(
205
+ dry ? `${m.file} would no longer own any keys and would no longer be declared; delete it when you are ready.` : `${m.file} no longer owns any keys and is no longer declared; delete it when you are ready.`
206
+ );
207
+ }
208
+ }
209
+ for (const p of result.config.pathsAdded) {
210
+ if (said.has(p.collection)) continue;
211
+ said.add(p.collection);
212
+ lines.push(`${dry ? "Would add" : "Added"} ${list(pathsOf(p.collection))} to collection ${p.collection}'s paths.`);
213
+ }
214
+ const width = Math.max(
215
+ 7,
216
+ ...result.files.flatMap((f) => [...f.moved, ...f.stayed].map((v) => v.key.length))
217
+ );
218
+ const shown = result.files.filter((f) => !f.beyond || f.stayed.length > 0);
219
+ for (const f of shown) {
220
+ lines.push(f.file);
221
+ for (const mv of f.moved) {
222
+ const name = c.cyan(mv.key.padEnd(width));
223
+ if (mv.to === "page") {
224
+ lines.push(` ${name} \u2190 ${mv.from}`);
225
+ } else {
226
+ lines.push(` ${name} \u2192 ${mv.manifest}${mv.line !== void 0 ? `:${mv.line}` : ""}`);
227
+ }
228
+ }
229
+ for (const st of f.stayed) {
230
+ lines.push(` ${c.cyan(st.key.padEnd(width))} ${c.yellow(`stays: ${st.detail ?? st.reason}`)}`);
231
+ }
232
+ }
233
+ const hidden = result.files.length - shown.length;
234
+ if (hidden > 0) lines.push(`\u2026 (${plural(hidden, "more file")})`);
235
+ const s = result.summary;
236
+ if (s.files === 0) {
237
+ lines.push("0 files, nothing to move");
238
+ return lines.join("\n");
239
+ }
240
+ const moves = result.files.flatMap((f) => f.moved);
241
+ const toPage = moves.filter((m) => m.to === "page").length;
242
+ const toManifest = moves.flatMap((m) => m.to === "manifest" ? [m.manifest] : []);
243
+ const manifests = plural(new Set(toManifest).size, "manifest");
244
+ let footer = `${plural(s.files, "file")}, ${plural(s.moved, "value")} ${dry ? "would move" : "moved"}`;
245
+ if (toPage > 0 && toManifest.length > 0) {
246
+ footer += `: ${toPage} into pages, ${toManifest.length} into ${manifests}`;
247
+ } else if (toManifest.length > 0) {
248
+ footer += ` to ${manifests}`;
249
+ } else if (toPage > 0) {
250
+ footer += " into pages";
251
+ }
252
+ if (s.stayed > 0) footer += `, ${s.stayed} stayed`;
253
+ lines.push(s.stayed > 0 ? c.yellow(footer) : footer);
254
+ return lines.join("\n");
255
+ }
256
+
257
+ // src/meta/commands/validate-offer.ts
258
+ async function offerRelocation(opts) {
259
+ const { confirm, output } = opts;
260
+ if (confirm === void 0 || opts.noConfig === true) return [];
261
+ const findings = opts.results.flatMap(
262
+ (r) => r.file === STDIN_LABEL ? [] : r.errors.filter((e) => e.schema === LOCATION_EXTERNAL_SCHEMA || e.schema === LOCATION_PAGE_SCHEMA).flatMap((e) => e.subject === void 0 ? [] : [{ file: r.file, key: e.subject, schema: e.schema }])
263
+ );
264
+ if (findings.length === 0) return [];
265
+ const cwd = opts.cwd ?? process.cwd();
266
+ const run = await resolveRunConfig({
267
+ cwd,
268
+ ...opts.configPath !== void 0 ? { configPath: opts.configPath } : {},
269
+ inputs: opts.inputs,
270
+ ...opts.collections !== void 0 ? { collections: opts.collections } : {}
271
+ });
272
+ const validator = new Validator(
273
+ schemaLoadOptions({
274
+ root: run.configDir ?? cwd,
275
+ fileBase: cwd,
276
+ ttlHours: run.config?.schemaCache?.ttlHours,
277
+ offline: opts.offline ?? run.config?.offline,
278
+ pins: collectSchemaPins(run.config)
279
+ })
280
+ );
281
+ const ctx = relocationContext(run, {
282
+ cwd,
283
+ targets: run.fromCollections ? [] : opts.inputs,
284
+ validator,
285
+ ...opts.cliSchemas !== void 0 && opts.cliSchemas.length > 0 ? { cliSchemas: opts.cliSchemas } : {},
286
+ ...opts.as !== void 0 ? { as: opts.as } : {},
287
+ ...opts.respectGitignore !== void 0 ? { respectGitignore: opts.respectGitignore } : {},
288
+ ...opts.env !== void 0 ? { env: opts.env } : {}
289
+ });
290
+ const groups = /* @__PURE__ */ new Map();
291
+ for (const f of findings) {
292
+ const home = await keyHome(ctx, f.file, {}, f.key);
293
+ let collection;
294
+ if (f.schema === LOCATION_PAGE_SCHEMA) {
295
+ if (home.kind === "manifest") collection = home.collection;
296
+ } else if (home.kind === "unowned" && home.home.kind === "collection") {
297
+ collection = home.home.collection;
298
+ }
299
+ if (collection === void 0) continue;
300
+ const group = groups.get(collection) ?? { files: /* @__PURE__ */ new Set(), keys: /* @__PURE__ */ new Set() };
301
+ group.files.add(f.file);
302
+ group.keys.add(f.key);
303
+ groups.set(collection, group);
304
+ }
305
+ const applied = [];
306
+ const say = (message) => {
307
+ output.write(`${programName()}: ${message}
308
+ `);
309
+ };
310
+ for (const [collection, group] of groups) {
311
+ let plan;
312
+ try {
313
+ plan = await planRelocation(ctx, { files: [...group.files], fields: [...group.keys] });
314
+ } catch (err) {
315
+ if (!(err instanceof DocmetaError)) throw err;
316
+ say(err.message);
317
+ continue;
318
+ }
319
+ if (plan.writes.length === 0) continue;
320
+ const offer = plan.offers.find((o) => o.collection === collection);
321
+ if (offer === void 0) continue;
322
+ const { notice: notice2, question } = offerPrompt(offer, "validate");
323
+ say(notice2);
324
+ if (!await confirm(question)) continue;
325
+ const result = await applyRelocation(ctx, plan);
326
+ output.write(`${renderRelocatePretty(result, { color: opts.color ?? false })}
327
+ `);
328
+ applied.push(result);
329
+ }
330
+ return applied;
331
+ }
332
+
85
333
  // src/meta/reporters/infer.ts
86
334
  var OUTLIERS_SHOWN = 5;
87
335
  var ENUM_SHOWN = 3;
@@ -496,6 +744,22 @@ function buildProgram() {
496
744
  `);
497
745
  }
498
746
  process.exitCode = summary.failed > 0 ? 1 : 0;
747
+ await offerRelocation({
748
+ results,
749
+ inputs: paths,
750
+ cliSchemas: options.schema,
751
+ as: options.as,
752
+ collections: options.collection,
753
+ ...configOption(options.config),
754
+ respectGitignore: explicitFalse(options.gitignore),
755
+ offline: options.offline ? true : void 0,
756
+ confirm: paths.includes(STDIN) ? void 0 : terminalConfirm(),
757
+ output: process.stderr,
758
+ color: shouldColor({
759
+ noColor: (command.parent ?? command).opts().color === false,
760
+ isTTY: process.stderr.isTTY
761
+ })
762
+ });
499
763
  } catch (err) {
500
764
  fail(err);
501
765
  }
@@ -716,7 +980,17 @@ function buildProgram() {
716
980
  onNotice: notice,
717
981
  // A write to a column marked x-manni-encrypt with no key asks
718
982
  // for one on a terminal, and refuses off one (proposal 0045).
719
- confirm: terminalConfirm()
983
+ // The same Confirm asks P1 (proposal 0047).
984
+ confirm: terminalConfirm(),
985
+ // An accepted P1 offer prints relocate's report: on stdout
986
+ // beside a pretty report, on stderr when stdout must parse.
987
+ onRelocated: (result) => {
988
+ const text = renderRelocate(result, "pretty", {
989
+ color: format === "pretty" && resolveColor(command.parent ?? command)
990
+ });
991
+ (format === "pretty" ? process.stdout : process.stderr).write(`${text}
992
+ `);
993
+ }
720
994
  });
721
995
  if (run.db && sql === "") {
722
996
  process.stdout.write(
@@ -880,8 +1154,18 @@ function buildProgram() {
880
1154
  includeContent: usingStdin,
881
1155
  // A field marked x-manni-encrypt with no key asks for one on a
882
1156
  // terminal, and refuses off one (proposal 0045). Reading the page
883
- // from stdin leaves no terminal to ask on.
884
- confirm: usingStdin ? void 0 : terminalConfirm()
1157
+ // from stdin leaves no terminal to ask on. The same Confirm asks
1158
+ // P1 (proposal 0047).
1159
+ confirm: usingStdin ? void 0 : terminalConfirm(),
1160
+ // An accepted P1 offer prints relocate's report: on stdout beside a
1161
+ // pretty report, on stderr when stdout must parse.
1162
+ onRelocated: (result) => {
1163
+ const text = renderRelocate(result, "pretty", {
1164
+ color: format === "pretty" && resolveColor(command.parent ?? command)
1165
+ });
1166
+ (format === "pretty" ? process.stdout : process.stderr).write(`${text}
1167
+ `);
1168
+ }
885
1169
  });
886
1170
  const color = resolveColor(command.parent ?? command);
887
1171
  const report = renderFill(format, run, {
@@ -974,8 +1258,18 @@ function buildProgram() {
974
1258
  respectGitignore: explicitFalse(options.gitignore),
975
1259
  onNotice: notice,
976
1260
  // A field its schema marks x-manni-encrypt (proposal 0045) with no
977
- // key yet: asked on a terminal, refused off one.
978
- confirm: terminalConfirm()
1261
+ // key yet: asked on a terminal, refused off one. The same Confirm
1262
+ // asks P1 (proposal 0047).
1263
+ confirm: terminalConfirm(),
1264
+ // An accepted P1 offer prints relocate's report: on stdout beside a
1265
+ // pretty report, on stderr when stdout must parse.
1266
+ onRelocated: (result) => {
1267
+ const text2 = renderRelocate(result, "pretty", {
1268
+ color: format === "pretty" && resolveColor(command.parent ?? command)
1269
+ });
1270
+ (format === "pretty" ? process.stdout : process.stderr).write(`${text2}
1271
+ `);
1272
+ }
979
1273
  });
980
1274
  const text = renderDerive(run, format, {
981
1275
  color: resolveColor(command.parent ?? command),
@@ -991,6 +1285,72 @@ function buildProgram() {
991
1285
  fail(err);
992
1286
  }
993
1287
  });
1288
+ program.command("relocate").description(
1289
+ "Move each value to where its schema's x-manni-location and the config's manifests say it belongs"
1290
+ ).argument(
1291
+ "[paths...]",
1292
+ "files, directories, or globs to relocate (stdin is refused)"
1293
+ ).option(
1294
+ "--fields <list>",
1295
+ "comma-separated fields to move; each must be marked x-manni-location or manifest-owned"
1296
+ ).option(
1297
+ "--collection <name>",
1298
+ "configured collection to run over; repeatable",
1299
+ collect,
1300
+ []
1301
+ ).option(
1302
+ "-s, --schema <ref>",
1303
+ "schema to judge preferences against; repeatable; overrides $schema/config",
1304
+ collect,
1305
+ []
1306
+ ).option("--dry-run", "report what would move and be created, and write nothing").option(
1307
+ "-f, --format <format>",
1308
+ `output: ${RELOCATE_FORMATS.join(" | ")}`,
1309
+ "pretty"
1310
+ ).option("--ext <list>", "comma-separated extensions for directory walks").option("--exclude <glob>", "glob to exclude; repeatable", collect, []).option("--as <format>", "force an input format (e.g. markdown, mdx)").option("-c, --config <path>", "path to a manni config file").option("--no-config", "ignore any discovered config file (refused: relocate writes the config)").option("--allow-empty", "treat zero matched files as success").option("--no-gitignore", "relocate files .gitignore covers").addHelpText(
1311
+ "after",
1312
+ [
1313
+ "",
1314
+ "Examples:",
1315
+ " manni meta relocate # every collection, as config declares it",
1316
+ " manni meta relocate --dry-run # what would move and be created",
1317
+ " manni meta relocate docs/install.md --fields owner",
1318
+ " manni meta relocate docs/ guides/intro.md # no collections yet: creates collection default",
1319
+ " manni meta relocate -f json --dry-run # the scripting form"
1320
+ ].join("\n")
1321
+ ).action(async (paths, options, command) => {
1322
+ try {
1323
+ const format = options.format;
1324
+ if (!isRelocateFormat(format)) {
1325
+ throw new DocmetaError(
1326
+ `relocate --format must be pretty or json; got "${format}".`
1327
+ );
1328
+ }
1329
+ const result = await runRelocate({
1330
+ inputs: paths,
1331
+ fields: options.fields ? splitList(options.fields) : void 0,
1332
+ collections: options.collection,
1333
+ cliSchemas: options.schema,
1334
+ dryRun: Boolean(options.dryRun),
1335
+ exts: options.ext ? splitList(options.ext) : void 0,
1336
+ exclude: options.exclude,
1337
+ as: options.as,
1338
+ ...configOption(options.config),
1339
+ onConfigLoaded: reportConfig(format === "pretty", process.cwd()),
1340
+ allowEmpty: options.allowEmpty ? true : void 0,
1341
+ respectGitignore: explicitFalse(options.gitignore),
1342
+ onNotice: notice
1343
+ });
1344
+ const text = renderRelocate(result, format, {
1345
+ color: resolveColor(command.parent ?? command)
1346
+ });
1347
+ process.stdout.write(`${text}
1348
+ `);
1349
+ process.exitCode = relocateFailed(result) ? 1 : 0;
1350
+ } catch (err) {
1351
+ fail(err);
1352
+ }
1353
+ });
994
1354
  const schemas = program.command("schemas").description("List built-in schemas and supported input formats").option(
995
1355
  "-f, --format <format>",
996
1356
  `output: ${COMMON_FORMATS.join(" | ")}`,
@@ -1164,4 +1524,4 @@ export {
1164
1524
  reportConfig,
1165
1525
  buildProgram
1166
1526
  };
1167
- //# sourceMappingURL=chunk-HNPRXGD7.js.map
1527
+ //# sourceMappingURL=chunk-2Q5JUEIO.js.map