@drhomet/mithaq 0.0.0-stage → 0.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.
Files changed (152) hide show
  1. package/LICENSE +22 -0
  2. package/LICENSES/openstatus-time-picker.txt +21 -0
  3. package/LICENSES/payload.txt +22 -0
  4. package/LICENSES/ponytail.txt +21 -0
  5. package/README.md +160 -2
  6. package/dist/asas/actor.d.ts +96 -0
  7. package/dist/asas/actor.js +114 -0
  8. package/dist/asas/build.d.ts +239 -0
  9. package/dist/asas/build.js +834 -0
  10. package/dist/asas/cli/mithaq.d.ts +2 -0
  11. package/dist/asas/cli/mithaq.js +177 -0
  12. package/dist/asas/cli/upgrade.d.ts +33 -0
  13. package/dist/asas/cli/upgrade.js +256 -0
  14. package/dist/asas/http/fetch.d.ts +41 -0
  15. package/dist/asas/http/fetch.js +178 -0
  16. package/dist/asas/http/orpc.d.ts +45 -0
  17. package/dist/asas/http/orpc.js +153 -0
  18. package/dist/asas/runner/bulk.d.ts +20 -0
  19. package/dist/asas/runner/bulk.js +196 -0
  20. package/dist/asas/runner/errors.d.ts +63 -0
  21. package/dist/asas/runner/errors.js +75 -0
  22. package/dist/asas/runner/read.d.ts +72 -0
  23. package/dist/asas/runner/read.js +601 -0
  24. package/dist/asas/runner/runtime.d.ts +21 -0
  25. package/dist/asas/runner/runtime.js +8 -0
  26. package/dist/asas/runner/steps.d.ts +418 -0
  27. package/dist/asas/runner/steps.js +277 -0
  28. package/dist/asas/runner/store.d.ts +4 -0
  29. package/dist/asas/runner/store.js +21 -0
  30. package/dist/asas/runner/tenant.d.ts +54 -0
  31. package/dist/asas/runner/tenant.js +275 -0
  32. package/dist/asas/runner/write.d.ts +606 -0
  33. package/dist/asas/runner/write.js +1019 -0
  34. package/dist/auth/better-auth/access.d.ts +29 -0
  35. package/dist/auth/better-auth/access.js +103 -0
  36. package/dist/auth/better-auth/generate.d.ts +31 -0
  37. package/dist/auth/better-auth/generate.js +328 -0
  38. package/dist/auth/better-auth/index.d.ts +94 -0
  39. package/dist/auth/better-auth/index.js +81 -0
  40. package/dist/auth/better-auth/tenant.d.ts +26 -0
  41. package/dist/auth/better-auth/tenant.js +34 -0
  42. package/dist/auth/local/index.d.ts +285 -0
  43. package/dist/auth/local/index.js +738 -0
  44. package/dist/base/decimal.d.ts +9 -0
  45. package/dist/base/decimal.js +16 -0
  46. package/dist/base/fields.d.ts +124 -0
  47. package/dist/base/fields.js +52 -0
  48. package/dist/base/index.d.ts +47 -0
  49. package/dist/base/index.js +5 -0
  50. package/dist/base/json-schema.d.ts +7 -0
  51. package/dist/base/json-schema.js +35 -0
  52. package/dist/base/kinds.d.ts +75 -0
  53. package/dist/base/kinds.js +123 -0
  54. package/dist/base/rules.d.ts +73 -0
  55. package/dist/base/rules.js +81 -0
  56. package/dist/drizzle/index.d.ts +308 -0
  57. package/dist/drizzle/index.js +951 -0
  58. package/dist/drizzle/kinds.d.ts +28 -0
  59. package/dist/drizzle/kinds.js +43 -0
  60. package/dist/drizzle/migrate.d.ts +159 -0
  61. package/dist/drizzle/migrate.js +1328 -0
  62. package/dist/drizzle/proxy/index.d.ts +1 -0
  63. package/dist/drizzle/proxy/index.js +1 -0
  64. package/dist/drizzle/proxy/node-postgres.d.ts +1 -0
  65. package/dist/drizzle/proxy/node-postgres.js +1 -0
  66. package/dist/drizzle/proxy/pg-core.d.ts +1 -0
  67. package/dist/drizzle/proxy/pg-core.js +1 -0
  68. package/dist/drizzle/rules.d.ts +336 -0
  69. package/dist/drizzle/rules.js +44 -0
  70. package/dist/index.d.ts +35 -0
  71. package/dist/index.js +26 -0
  72. package/dist/mithaq/define.d.ts +335 -0
  73. package/dist/mithaq/define.js +57 -0
  74. package/dist/mithaq/generate-messages.d.ts +38 -0
  75. package/dist/mithaq/generate-messages.js +235 -0
  76. package/dist/mithaq/language.d.ts +374 -0
  77. package/dist/mithaq/language.js +76 -0
  78. package/dist/mithaq/lock.d.ts +71 -0
  79. package/dist/mithaq/lock.js +329 -0
  80. package/dist/mithaq/message-file.d.ts +13 -0
  81. package/dist/mithaq/message-file.js +29 -0
  82. package/dist/mithaq/messages.d.ts +55 -0
  83. package/dist/mithaq/messages.js +111 -0
  84. package/dist/mithaq/refusal.d.ts +6 -0
  85. package/dist/mithaq/refusal.js +6 -0
  86. package/dist/mithaq/resolve.d.ts +10 -0
  87. package/dist/mithaq/resolve.js +1014 -0
  88. package/dist/mithaq/rows.d.ts +87 -0
  89. package/dist/mithaq/rows.js +174 -0
  90. package/dist/mithaq/rukn.d.ts +72 -0
  91. package/dist/mithaq/rukn.js +15 -0
  92. package/dist/mithaq/standard-schema.d.ts +38 -0
  93. package/dist/mithaq/standard-schema.js +2 -0
  94. package/dist/mithaq/types.d.ts +386 -0
  95. package/dist/mithaq/types.js +15 -0
  96. package/dist/mithaq/user.d.ts +19 -0
  97. package/dist/mithaq/user.js +16 -0
  98. package/dist/mithaq/where.d.ts +32 -0
  99. package/dist/mithaq/where.js +87 -0
  100. package/dist/rbac/index.d.ts +105 -0
  101. package/dist/rbac/index.js +182 -0
  102. package/dist/rbac/steps.d.ts +22 -0
  103. package/dist/rbac/steps.js +161 -0
  104. package/dist/rich-text/index.d.ts +28 -0
  105. package/dist/rich-text/index.js +59 -0
  106. package/dist/rich-text/rich-text.d.ts +106 -0
  107. package/dist/rich-text/rich-text.js +249 -0
  108. package/dist/rich-text/value.d.ts +55 -0
  109. package/dist/rich-text/value.js +126 -0
  110. package/dist/screen/client.d.ts +156 -0
  111. package/dist/screen/client.js +71 -0
  112. package/dist/screen/generate.d.ts +14 -0
  113. package/dist/screen/generate.js +268 -0
  114. package/dist/screen/index.d.ts +496 -0
  115. package/dist/screen/index.js +663 -0
  116. package/dist/screen/react.d.ts +97 -0
  117. package/dist/screen/react.js +58 -0
  118. package/dist/screen/studio.d.ts +24 -0
  119. package/dist/screen/studio.js +68 -0
  120. package/dist/service/index.d.ts +63 -0
  121. package/dist/service/index.js +15 -0
  122. package/dist/storage/allowed-type.d.ts +5 -0
  123. package/dist/storage/allowed-type.js +7 -0
  124. package/dist/storage/index.d.ts +42 -0
  125. package/dist/storage/index.js +43 -0
  126. package/dist/storage/storage.d.ts +386 -0
  127. package/dist/storage/storage.js +412 -0
  128. package/dist/targets.d.ts +14 -0
  129. package/dist/targets.js +16 -0
  130. package/dist/versions/index.d.ts +37 -0
  131. package/dist/versions/index.js +8 -0
  132. package/dist/versions/versions.d.ts +104 -0
  133. package/dist/versions/versions.js +160 -0
  134. package/dist/vite.d.ts +7 -0
  135. package/dist/vite.js +81 -0
  136. package/dist/zod/index.d.ts +193 -0
  137. package/dist/zod/index.js +122 -0
  138. package/dist/zod/kinds.d.ts +36 -0
  139. package/dist/zod/kinds.js +58 -0
  140. package/dist/zod/rules.d.ts +117 -0
  141. package/dist/zod/rules.js +67 -0
  142. package/docs/packages/mithaq/README.md +962 -0
  143. package/docs/packages/mithaq/src/asas/cli/README.md +252 -0
  144. package/docs/packages/mithaq/src/asas/runner/README.md +472 -0
  145. package/docs/packages/mithaq/src/drizzle/README.md +39 -0
  146. package/docs/packages/mithaq-admin/README.md +312 -0
  147. package/docs/templates/asas/README.md +1056 -0
  148. package/package.json +92 -4
  149. package/skills/add-field/SKILL.md +27 -0
  150. package/skills/add-kayan/SKILL.md +26 -0
  151. package/skills/eject-admin-piece/SKILL.md +28 -0
  152. package/skills/upgrade-mithaq/SKILL.md +35 -0
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
3
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
4
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
5
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
6
+ });
7
+ }
8
+ return path;
9
+ };
10
+ // Each generator is imported when its word is asked for, so `better-auth/db` loads
11
+ // only for `generate auth` and the `generate --check` sweep.
12
+ import { realpathSync } from 'node:fs';
13
+ import * as modules from 'node:module';
14
+ import { pathToFileURL } from 'node:url';
15
+ import { Refusal } from '../../mithaq/refusal.js';
16
+ const verbs = ['eject', 'generate', 'lock', 'migrate', 'screens', 'upgrade'];
17
+ const nouns = ['auth', 'messages', 'screens'];
18
+ const usage = 'Usage: mithaq generate auth [--check] | mithaq generate messages [--check] [--no-data-scan] | mithaq generate screens [--check] | mithaq generate --check | mithaq lock [--check] | mithaq migrate [--check] [--name <slug>] | mithaq screens | mithaq upgrade --check | mithaq eject <item> | mithaq eject --prune | mithaq <target> <command> [--check]';
19
+ // Run as a command, not imported. The bin is a symlink, so compare real paths.
20
+ if (process.argv[1] && realpathSync(process.argv[1]) === import.meta.filename) {
21
+ // A project's sources may import each other with no extension, as Vite, Bun and drizzle-kit
22
+ // resolve them and Node does not, so a relative import that names no file is tried as `.ts`,
23
+ // then as a folder's `index.ts`. Only after Node's own resolution failed, never under node_modules.
24
+ modules.registerHooks?.({
25
+ resolve: (specifier, context, next) => {
26
+ try {
27
+ return next(specifier, context);
28
+ }
29
+ catch (error) {
30
+ const code = error.code;
31
+ if (!/^\.\.?\//.test(specifier) || context.parentURL?.includes('/node_modules/') || (code !== 'ERR_MODULE_NOT_FOUND' && code !== 'ERR_UNSUPPORTED_DIR_IMPORT'))
32
+ throw error;
33
+ for (const tail of ['.ts', '/index.ts']) {
34
+ try {
35
+ return next(`${specifier}${tail}`, context);
36
+ }
37
+ catch { }
38
+ }
39
+ throw error;
40
+ }
41
+ },
42
+ });
43
+ try {
44
+ const [, , verb, noun, ...rest] = process.argv;
45
+ // Before anything reads the project: outside one there is no `src/asas.ts` to import.
46
+ if (verb === '--help' || verb === '-h') {
47
+ console.log(usage);
48
+ process.exit(0);
49
+ }
50
+ // `generate --check` takes no noun, so the word in the noun's place is a flag.
51
+ const flags = noun?.startsWith('--') ? [noun, ...rest] : rest;
52
+ const sweeping = verb === 'generate' && flags.includes('--check') && !nouns.includes(noun ?? '');
53
+ // `migrate` is a word of the bin's own wherever it stands first, so it takes flags
54
+ // and never a second word; a target named `migrate` is reached by no spelling.
55
+ const misplaced = (verb === 'migrate' || verb === 'upgrade') && noun !== undefined && !noun.startsWith('--');
56
+ const unchecked = verb === 'upgrade' && !flags.includes('--check');
57
+ // `eject` takes one item or `--prune`, never both and never neither.
58
+ const ejecting = noun !== undefined && !noun.startsWith('--');
59
+ const unejected = verb === 'eject' && (ejecting ? flags.length > 0 : flags.join(' ') !== '--prune');
60
+ if (verb === undefined || misplaced || unchecked || unejected || (verb === 'generate' && !(sweeping || nouns.includes(noun ?? '')))) {
61
+ console.error(usage);
62
+ process.exit(1);
63
+ }
64
+ const project = pathToFileURL(`${process.cwd()}/`);
65
+ const check = flags.includes('--check');
66
+ // The project saying it has no database to scan, which a check otherwise refuses to pass without.
67
+ const scan = !flags.includes('--no-data-scan');
68
+ const auth = async (checking) => (await import('../../auth/better-auth/generate.js')).run(project, checking);
69
+ const messages = async () => (await import('../../mithaq/generate-messages.js')).run({ project, check, dataScan: scan });
70
+ const screens = async () => (await import('../../screen/generate.js')).run(project, check);
71
+ const upgrade = async () => (await import('./upgrade.js')).run(project);
72
+ const lock = async (word, words) => (await import('../../mithaq/lock.js')).run({ project, noun: word, flags: words });
73
+ // Imported only where a target's command was asked for: `generate auth` runs against
74
+ // a project whose `src/asas.ts` may not import at all.
75
+ const asasOf = async () => (await import(__rewriteRelativeImportExtension(new URL('src/asas.ts', project).href))).asas;
76
+ // `Object.hasOwn`: a typed word would otherwise reach `Object.prototype`, and
77
+ // `mithaq drizzle constructor` would call `Object`.
78
+ const commandsOf = (asas, name) => (Object.hasOwn(asas.targets, name) ? asas.targets[name]?.commands : undefined) ?? {};
79
+ const commandIn = (commands, name) => Object.hasOwn(commands, name) ? commands[name] : undefined;
80
+ const everyCommand = (asas) => Object.entries(asas.targets).flatMap(([name, target]) => Object.entries(target?.commands ?? {}).map(([command, one]) => [`${name} ${command}`, one]));
81
+ const stopped = (name, code) => {
82
+ console.error(`\`mithaq ${name} --check\` answered ${code}, so the sweep stopped there.`);
83
+ process.exit(code);
84
+ };
85
+ // Every command but the lock's own and the sweep shows an unsigned language change first.
86
+ if (verb !== 'lock' && !(verb === 'generate' && sweeping))
87
+ await (await import('../../mithaq/lock.js')).warnLock(project);
88
+ if (verb === 'generate' && sweeping) {
89
+ // Stops at the first non-zero, so a pipeline reads which check failed. The lock goes first,
90
+ // and `migrate` before anything that reads rows, so an unmigrated database stops it by name.
91
+ const step = async (name, one) => {
92
+ const code = await one();
93
+ if (code)
94
+ stopped(name, code);
95
+ };
96
+ await step('lock', () => lock(undefined, ['--check']));
97
+ await step('generate auth', () => auth(true));
98
+ // A command with no check to make answers 0 for the flag it does not know.
99
+ const asas = await asasOf();
100
+ const commands = everyCommand(asas);
101
+ const migrates = commands.filter(([name]) => name.endsWith(' migrate'));
102
+ const others = commands.filter(([name]) => !name.endsWith(' migrate'));
103
+ for (const [name, one] of migrates)
104
+ await step(name, () => one({ asas, flags }));
105
+ await step('generate messages', messages);
106
+ await step('generate screens', screens);
107
+ for (const [name, one] of others)
108
+ await step(name, () => one({ asas, flags }));
109
+ // A report, never a gate: a copy that differs from the registry is a file the project owns.
110
+ await upgrade().catch((error) => console.error(`upgrade --check could not read the copies: ${error.message}`));
111
+ }
112
+ else if (verb === 'lock') {
113
+ const code = await lock(noun?.startsWith('--') ? undefined : noun, flags);
114
+ if (code)
115
+ process.exit(code);
116
+ }
117
+ else if (verb === 'eject') {
118
+ const { eject, prune } = await import('./upgrade.js');
119
+ const code = ejecting ? await eject(project, noun) : await prune(project);
120
+ if (code)
121
+ process.exit(code);
122
+ }
123
+ else if (verb === 'upgrade') {
124
+ const code = await upgrade();
125
+ if (code)
126
+ process.exit(code);
127
+ }
128
+ else if (verb === 'generate') {
129
+ const code = await (noun === 'messages' ? messages() : noun === 'screens' ? screens() : auth(check));
130
+ if (code)
131
+ process.exit(code);
132
+ }
133
+ else if (verb === 'screens') {
134
+ const { studioText, studioTree } = await import('../../screen/studio.js');
135
+ const targets = (await asasOf()).targets;
136
+ const trees = Object.entries(targets).flatMap(([name, target]) => (target?.capability === 'screen' ? [studioText(studioTree(name, target.client))] : []));
137
+ console.log(trees.join('\n\n'));
138
+ }
139
+ else {
140
+ const asas = await asasOf();
141
+ // Two stores that both declare `migrate` cannot be told apart by the bare word, so it is refused.
142
+ const aliased = verb === 'migrate';
143
+ const declared = aliased ? Object.keys(asas.targets).filter((name) => commandsOf(asas, name).migrate) : [];
144
+ if (aliased && declared.length === 0) {
145
+ throw new Refusal('No target of this language declares a `migrate` command, and that is what `mithaq migrate` runs. Name a store that declares one in `targets`, or migrate that store with its own tool.');
146
+ }
147
+ if (aliased && declared.length > 1) {
148
+ throw new Refusal(`${declared.join(', ')} each declare a \`migrate\` command, so \`mithaq migrate\` cannot say which one to run. Name the target: ${declared.map((name) => `\`mithaq ${name} migrate\``).join(', ')}.`);
149
+ }
150
+ const target = aliased ? declared[0] : verb;
151
+ const command = aliased ? 'migrate' : noun;
152
+ if (!Object.hasOwn(asas.targets, target)) {
153
+ throw new Refusal(`"${verb}" is neither a command of mithaq nor a target of this language. The commands are ${verbs.join(' and ')}; the targets are ${Object.keys(asas.targets).join(', ')}.`);
154
+ }
155
+ if (command === undefined || command.startsWith('--')) {
156
+ console.error(usage);
157
+ process.exit(1);
158
+ }
159
+ const one = commandIn(commandsOf(asas, target), command);
160
+ if (!one) {
161
+ const has = Object.keys(commandsOf(asas, target)).map((name) => `\`${name}\``);
162
+ throw new Refusal(`Target "${target}" declares no \`${command}\` command. It declares ${has.length ? has.join(', ') : 'none at all'}.`);
163
+ }
164
+ const code = await one({ asas, flags });
165
+ if (code)
166
+ process.exit(code);
167
+ }
168
+ }
169
+ catch (error) {
170
+ // Anything that is not a refusal is rethrown, so Node prints its stack, which is the whole
171
+ // diagnosis of a project's `src/asas.ts` failing to import.
172
+ if (!(error instanceof Refusal))
173
+ throw error;
174
+ console.error(error.message);
175
+ process.exit(1);
176
+ }
177
+ }
@@ -0,0 +1,33 @@
1
+ /** The hash of a copy `shadcn add` made, without its marker and the leading comments it dropped. */
2
+ export declare const hashOf: (text: string) => string;
3
+ /** A held file's state against the package's text of it, read as `upgrade --check` reads it. */
4
+ export declare const stateOf: (text: string, source: string) => 'up to date' | 'up to date, edited' | 'differs' | 'differs, edited' | 'unknown';
5
+ export type Copied = {
6
+ /** The file, relative to the project, or the target or package path that resolved outside it. */
7
+ path: string;
8
+ state: 'up to date' | 'up to date, edited' | 'differs' | 'differs, edited' | 'unknown' | 'outside' | 'removed';
9
+ /** The version or commit the file's marker names, when it carries one. */
10
+ from?: string;
11
+ /** The installed mithaq-admin's version. */
12
+ to?: string;
13
+ /** The item of a file the project always holds, which it never deletes: it ejects the item again instead. */
14
+ item?: string;
15
+ };
16
+ /**
17
+ * Every package file the project holds at its target, ejected or always the project's, with its state. Reads only;
18
+ * a path that climbs out of the project or the package is listed as outside, never followed.
19
+ */
20
+ export declare const upgradeCheck: (project: URL) => Promise<Copied[] | undefined>;
21
+ /** `mithaq upgrade --check`: one line per file the project holds, and 1 where any differs. It never writes. */
22
+ export declare const run: (project: URL) => Promise<number>;
23
+ /**
24
+ * `mithaq eject <item>`: writes the item's own files where the project keeps them, each byte for byte with a marker,
25
+ * and never over a file already there. It copies no other item and installs nothing: the package serves every
26
+ * sibling, so it only names a primitive or an npm package the project lacks.
27
+ */
28
+ export declare const eject: (project: URL, name: string) => Promise<number>;
29
+ /**
30
+ * `mithaq eject --prune`: deletes every marked file in the folders pieces are ejected into whose text still hashes
31
+ * to its marker, so the package draws it again. Never an edited, unmarked or `~/` file.
32
+ */
33
+ export declare const prune: (project: URL) => Promise<number>;
@@ -0,0 +1,256 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync } from 'node:fs';
3
+ import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
4
+ import { createRequire } from 'node:module';
5
+ import { pathToFileURL } from 'node:url';
6
+ // The last line of every ejected file: the version (or, for a copy `shadcn add` made, the commit) it came from and the
7
+ // hash of the file above. Trailing, because `shadcn add` dropped a file's leading comments only.
8
+ const marker = /\n\/\/ mithaq-admin (\S+) ([0-9a-f]{64})\n?$/;
9
+ // What `shadcn add` dropped, so a hash is the same over the package's text and a copy it made.
10
+ const leading = /^(?:\s+|\/\/[^\n]*|\/\*[\s\S]*?\*\/)*/;
11
+ /** What an eject writes above the marker: the file's text, ending in a newline. */
12
+ const bodyOf = (text) => {
13
+ const body = text.replace(marker, '\n');
14
+ return body.endsWith('\n') ? body : `${body}\n`;
15
+ };
16
+ const sha = (text) => createHash('sha256').update(text).digest('hex');
17
+ /** The hash of a copy `shadcn add` made, without its marker and the leading comments it dropped. */
18
+ export const hashOf = (text) => sha(bodyOf(text).replace(leading, ''));
19
+ /**
20
+ * The hash a marker from `from` holds: of the whole text above it for an eject, which writes byte for byte, so an edit
21
+ * to a leading pragma is an edit; without leading comments only for a copy made at a commit, as `shadcn add` wrote it.
22
+ */
23
+ const madeByShadcn = (from) => /^(?:[0-9a-f]{40}|unknown)$/.test(from);
24
+ const hashFrom = (from) => (text) => (madeByShadcn(from) ? hashOf(text) : sha(bodyOf(text)));
25
+ /** A held file's state against the package's text of it, read as `upgrade --check` reads it. */
26
+ export const stateOf = (text, source) => {
27
+ const [, from, hash] = marker.exec(text) ?? [];
28
+ if (!hash)
29
+ return 'unknown';
30
+ const hashed = hashFrom(from);
31
+ return `${hash !== hashed(source) ? 'differs' : 'up to date'}${hash !== hashed(text) ? ', edited' : ''}`;
32
+ };
33
+ /** The `mithaq-admin` the project installed: its folder, its version and its items, or none. */
34
+ const installedOf = async (project) => {
35
+ let manifest;
36
+ try {
37
+ manifest = pathToFileURL(createRequire(new URL('package.json', project)).resolve('mithaq-admin/package.json'));
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ const root = new URL('./', manifest);
43
+ const { version } = JSON.parse(await readFile(manifest, 'utf8'));
44
+ const { items } = JSON.parse(await readFile(new URL('registry.json', root), 'utf8'));
45
+ return { root, version, items };
46
+ };
47
+ // `new URL` resolves every `..`, encoded or not, so a prefix is the whole test.
48
+ const within = (url, folder) => url.href.startsWith(folder.href);
49
+ /** Where the project keeps each target: `@<alias>/…` through `components.json`, `~/…` from the project's root. */
50
+ const targetsOf = async (project) => {
51
+ const components = JSON.parse(await readFile(new URL('components.json', project), 'utf8'));
52
+ const src = existsSync(new URL('src/', project)) ? 'src/' : '';
53
+ return (target) => {
54
+ if (target.startsWith('~/'))
55
+ return target.slice(2);
56
+ const [, alias = '', rest] = /^@([^/]+)\/(.+)$/.exec(target) ?? [];
57
+ // shadcn resolves an alias through the app's tsconfig `paths`; this reads `@/` as `src/`
58
+ // when the project has one and as its root otherwise, shadcn's own fallback.
59
+ const at = components.aliases?.[alias];
60
+ return at?.startsWith('@/') ? `${src}${at.slice(2)}/${rest}` : undefined;
61
+ };
62
+ };
63
+ const folderOf = (path) => path.slice(0, path.lastIndexOf('/') + 1);
64
+ /** The folders a piece the package serves (`registry/<file>`) is ejected into: what `eject --prune` sweeps. */
65
+ const ejectedFolders = (items, pathOf) => new Set(items.flatMap((item) => item.files.filter((file) => !file.path.slice('registry/'.length).includes('/')).flatMap((file) => { const path = pathOf(file.target); return path ? folderOf(path) : []; })));
66
+ /**
67
+ * Every package file the project holds at its target, ejected or always the project's, with its state. Reads only;
68
+ * a path that climbs out of the project or the package is listed as outside, never followed.
69
+ */
70
+ export const upgradeCheck = async (project) => {
71
+ const pathOf = await targetsOf(project);
72
+ const installed = await installedOf(project);
73
+ if (!installed)
74
+ return undefined;
75
+ const copied = [];
76
+ /** Every path an item names, held or not, and the folders they sit in, which a file no item names any more is looked for in. */
77
+ const named = new Set();
78
+ const folders = new Set();
79
+ const ejected = ejectedFolders(installed.items, pathOf);
80
+ for (const { name, files } of installed.items) {
81
+ for (const file of files) {
82
+ const source = new URL(file.path, installed.root);
83
+ const path = pathOf(file.target);
84
+ if (!within(source, installed.root)) {
85
+ copied.push({ path: file.path, state: 'outside' });
86
+ continue;
87
+ }
88
+ if (path && !within(new URL(path, project), project)) {
89
+ copied.push({ path: file.target, state: 'outside' });
90
+ continue;
91
+ }
92
+ if (path) {
93
+ named.add(path);
94
+ folders.add(folderOf(path));
95
+ }
96
+ const text = path && (await readFile(new URL(path, project), 'utf8').catch(() => undefined));
97
+ if (!path || text === undefined)
98
+ continue;
99
+ const state = stateOf(text, await readFile(source, 'utf8'));
100
+ const to = installed.version;
101
+ copied.push(state === 'unknown' ? { path, state, to } : { path, state, from: marker.exec(text)[1], to, ...(!ejected.has(folderOf(path)) && { item: name }) });
102
+ }
103
+ }
104
+ // A marked file no item names any more: an item the package dropped, which the project deletes.
105
+ for (const folder of folders) {
106
+ const entries = await readdir(new URL(folder || './', project), { withFileTypes: true }).catch(() => []);
107
+ for (const entry of entries) {
108
+ const path = `${folder}${entry.name}`;
109
+ if (!entry.isFile() || named.has(path))
110
+ continue;
111
+ const from = marker.exec(await readFile(new URL(path, project), 'utf8'))?.[1];
112
+ if (from)
113
+ copied.push({ path, state: 'removed', from });
114
+ }
115
+ }
116
+ return copied;
117
+ };
118
+ const sentence = ({ path, state, from = '', to = 'unknown', item }) => ({
119
+ 'up to date': `${path} is up to date.`,
120
+ 'up to date, edited': `${path} is up to date, and edited since it was ejected.`,
121
+ differs: `${path} differs from the installed mithaq-admin's file (${to}), and is not edited since it was ejected from ${from.slice(0, 12)}. A hash shows a difference, not which side is newer: diff the two before replacing it, or ${item ? `delete it and run \`mithaq eject ${item}\` to take the package's` : 'delete it to draw the package\'s'}.`,
122
+ 'differs, edited': `${path} differs from the installed mithaq-admin's file (${to}), and is edited since it was ejected from ${from.slice(0, 12)}: diff the two and merge by hand.`,
123
+ unknown: `${path} carries no mithaq-admin marker on its last line, so nothing says what it was ejected from.`,
124
+ outside: `${path} resolves outside this project or the package, so it was not read.`,
125
+ removed: `${path} was ejected from mithaq-admin ${from.slice(0, 12)}, and no item of the installed package names it any more: it was removed from the package, so delete it.`,
126
+ })[state];
127
+ const noComponents = 'No components.json here, so nothing says where mithaq-admin files go.';
128
+ const noPackage = 'No mithaq-admin is installed here.';
129
+ /** `mithaq upgrade --check`: one line per file the project holds, and 1 where any differs. It never writes. */
130
+ export const run = async (project) => {
131
+ if (!existsSync(new URL('components.json', project))) {
132
+ console.log(noComponents);
133
+ return 0;
134
+ }
135
+ const copied = await upgradeCheck(project);
136
+ if (!copied) {
137
+ console.log(`${noPackage} Nothing to check the files against.`);
138
+ return 0;
139
+ }
140
+ if (!copied.length)
141
+ console.log('Nothing from mithaq-admin here.');
142
+ for (const one of copied)
143
+ console.log(sentence(one));
144
+ const differing = copied.filter((one) => one.state.startsWith('differs')).length;
145
+ if (differing) {
146
+ console.error(`${differing} ${differing === 1 ? 'file differs' : 'files differ'} from the installed mithaq-admin.`);
147
+ return 1;
148
+ }
149
+ return 0;
150
+ };
151
+ /**
152
+ * `mithaq eject <item>`: writes the item's own files where the project keeps them, each byte for byte with a marker,
153
+ * and never over a file already there. It copies no other item and installs nothing: the package serves every
154
+ * sibling, so it only names a primitive or an npm package the project lacks.
155
+ */
156
+ export const eject = async (project, name) => {
157
+ if (!existsSync(new URL('components.json', project))) {
158
+ console.error(noComponents);
159
+ return 1;
160
+ }
161
+ const pathOf = await targetsOf(project);
162
+ const installed = await installedOf(project);
163
+ if (!installed) {
164
+ console.error(`${noPackage} Install it at the version of mithaq, then eject.`);
165
+ return 1;
166
+ }
167
+ const wanted = `mithaq-${name.replace(/\.tsx?$/, '').replace(/^mithaq-/, '')}`;
168
+ const item = installed.items.find((one) => one.name === wanted);
169
+ if (!item) {
170
+ console.error(`mithaq-admin has no item "${name}". Its items are: ${installed.items.map((one) => one.name).join(', ')}.`);
171
+ return 1;
172
+ }
173
+ const files = item.files.map((file) => ({ source: new URL(file.path, installed.root), path: pathOf(file.target), target: file.target }));
174
+ const refused = files.filter(({ source, path }) => !within(source, installed.root) || !path || !within(new URL(path, project), project));
175
+ if (refused.length) {
176
+ console.error(`${refused.map((one) => one.target).join(', ')} resolves outside this project or the package, so nothing was written.`);
177
+ return 1;
178
+ }
179
+ const ejected = ejectedFolders(installed.items, pathOf);
180
+ for (const { source, path } of files) {
181
+ const at = new URL(path, project);
182
+ const text = await readFile(source, 'utf8');
183
+ await mkdir(new URL('./', at), { recursive: true });
184
+ // `wx` fails on a file already there, so an eject never overwrites, whatever raced it.
185
+ const body = bodyOf(text);
186
+ const written = await writeFile(at, `${body}// mithaq-admin ${installed.version} ${sha(body)}\n`, { flag: 'wx' }).then(() => true, (error) => {
187
+ if (error.code === 'EEXIST')
188
+ return false;
189
+ throw error;
190
+ });
191
+ // A file the project always holds (the host, storage, a step) is never deleted: the app needs it.
192
+ const drawn = ejected.has(folderOf(path)) ? '; delete it to draw mithaq-admin\'s again' : '';
193
+ console.log(written ? `${path} is ejected. Edit it in place${drawn}.` : `${path} is already ejected, so it was left as it is.`);
194
+ }
195
+ for (const primitive of item.registryDependencies ?? []) {
196
+ const path = pathOf(`@ui/${primitive}.tsx`);
197
+ if (path && !existsSync(new URL(path, project)))
198
+ console.log(`${path} is missing: run shadcn add ${primitive}`);
199
+ }
200
+ const require = createRequire(new URL('package.json', project));
201
+ for (const dependency of item.dependencies ?? []) {
202
+ const npm = /^(@?[^@]+)/.exec(dependency)[1];
203
+ try {
204
+ require.resolve(npm);
205
+ }
206
+ catch (error) {
207
+ if (error.code === 'MODULE_NOT_FOUND')
208
+ console.log(`${npm} is not installed: install ${dependency}`);
209
+ }
210
+ }
211
+ return 0;
212
+ };
213
+ /**
214
+ * `mithaq eject --prune`: deletes every marked file in the folders pieces are ejected into whose text still hashes
215
+ * to its marker, so the package draws it again. Never an edited, unmarked or `~/` file.
216
+ */
217
+ export const prune = async (project) => {
218
+ if (!existsSync(new URL('components.json', project))) {
219
+ console.error(noComponents);
220
+ return 1;
221
+ }
222
+ const pathOf = await targetsOf(project);
223
+ const installed = await installedOf(project);
224
+ if (!installed) {
225
+ console.error(noPackage);
226
+ return 1;
227
+ }
228
+ // The package's file at each path, whose leading comments a copy `shadcn add` made may hold and no more.
229
+ const sources = new Map(installed.items.flatMap((item) => item.files.flatMap((file) => {
230
+ const path = pathOf(file.target);
231
+ return path ? [[path, new URL(file.path, installed.root)]] : [];
232
+ })));
233
+ const leadOf = (text) => leading.exec(bodyOf(text))[0];
234
+ for (const folder of ejectedFolders(installed.items, pathOf)) {
235
+ if (!within(new URL(folder, project), project))
236
+ continue;
237
+ const entries = await readdir(new URL(folder, project), { withFileTypes: true }).catch(() => []);
238
+ for (const entry of entries) {
239
+ if (!entry.isFile())
240
+ continue;
241
+ const path = `${folder}${entry.name}`;
242
+ const text = await readFile(new URL(path, project), 'utf8');
243
+ const [, from, hash] = marker.exec(text) ?? [];
244
+ if (!hash || hash !== hashFrom(from)(text))
245
+ continue;
246
+ // A commit's hash leaves leading comments out, so a pragma written above one is an edit the hash cannot see.
247
+ const source = sources.get(path);
248
+ const lead = leadOf(text);
249
+ if (madeByShadcn(from) && lead && lead !== (source && within(source, installed.root) ? leadOf(await readFile(source, 'utf8').catch(() => '')) : ''))
250
+ continue;
251
+ await rm(new URL(path, project));
252
+ console.log(`${path} was not edited since it was ejected, so it is deleted and mithaq-admin draws its own.`);
253
+ }
254
+ }
255
+ return 0;
256
+ };
@@ -0,0 +1,41 @@
1
+ import type { HttpPlug, OperationTable } from '../build.ts';
2
+ import type { StandardSchemaV1 } from '../../mithaq/standard-schema.ts';
3
+ /** The Zod input decides which JSON strings represent dates on either HTTP plug. */
4
+ export declare const decodeJson: (value: unknown, schema: StandardSchemaV1) => unknown;
5
+ export declare const draftOptions: (request: Request) => {
6
+ draft?: boolean | undefined;
7
+ autosave?: boolean | undefined;
8
+ tenant?: string | undefined;
9
+ };
10
+ /**
11
+ * The largest request body a plug reads, which a project names as `maxBodyBytes`: room for a
12
+ * json field's `defaultJsonMaxBytes` written in any script. A file never comes this way.
13
+ */
14
+ export declare const defaultMaxBodyBytes: number;
15
+ /**
16
+ * The body, or the answer that refuses it: 413 once it passes `limit` bytes, and 499 to a client
17
+ * that went away mid-body, which is no fault of the server's. Counted as it arrives, since a
18
+ * `content-length` may be absent or wrong, and the rest is never read.
19
+ */
20
+ export declare const boundedBody: (request: Request, limit: number) => Promise<Blob | Response>;
21
+ /** Refused by name, since core holds no limit of its own. */
22
+ export declare const assertMaxBodyBytes: (owner: string, value: unknown) => void;
23
+ /** Refused by name, as `auth.onError` and `storage.onError` are: core reports no fault of its own. */
24
+ export declare const assertOnError: (owner: string, value: unknown) => void;
25
+ export declare const tooLarge: (limit: number) => Response;
26
+ export type FetchHttpOptions = {
27
+ /** The largest body a call takes, answered 413 past it: `defaultMaxBodyBytes` or the project's own. Required. */
28
+ maxBodyBytes: number;
29
+ /**
30
+ * Builds the context every step of a served call reads, from the request and the actor.
31
+ * The plug reads no header or body key into it; a throw is answered 500.
32
+ */
33
+ context?: (request: Request, actor: unknown) => Promise<Record<string, unknown>> | Record<string, unknown>;
34
+ /** Handed every error that is not an `AsasError`, before the 500 goes out with its message replaced. Required. */
35
+ onError: (error: unknown) => void;
36
+ };
37
+ /**
38
+ * The default HTTP plug: `POST /api/<kayan>/<operation>` with a JSON body, over
39
+ * `Request` and `Response` alone. The runner parses each operation's input.
40
+ */
41
+ export declare const fetchHttp: (options: FetchHttpOptions) => <View>(operations: OperationTable<View>) => HttpPlug;