@procedures/mark 0.0.0-stage → 0.1.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/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/README.md +799 -2
- package/dist/.tsbuildinfo +1 -0
- package/dist/args.d.ts +77 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +351 -0
- package/dist/args.js.map +1 -0
- package/dist/ask.d.ts +48 -0
- package/dist/ask.d.ts.map +1 -0
- package/dist/ask.js +91 -0
- package/dist/ask.js.map +1 -0
- package/dist/bin.d.ts +9 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +10 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli.d.ts +61 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +530 -0
- package/dist/cli.js.map +1 -0
- package/dist/client-mode.d.ts +21 -0
- package/dist/client-mode.d.ts.map +1 -0
- package/dist/client-mode.js +75 -0
- package/dist/client-mode.js.map +1 -0
- package/dist/ecosystem.d.ts +66 -0
- package/dist/ecosystem.d.ts.map +1 -0
- package/dist/ecosystem.js +211 -0
- package/dist/ecosystem.js.map +1 -0
- package/dist/failure.d.ts +16 -0
- package/dist/failure.d.ts.map +1 -0
- package/dist/failure.js +47 -0
- package/dist/failure.js.map +1 -0
- package/dist/format.d.ts +30 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +125 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/lockfile.d.ts +26 -0
- package/dist/lockfile.d.ts.map +1 -0
- package/dist/lockfile.js +68 -0
- package/dist/lockfile.js.map +1 -0
- package/dist/manifest-build.d.ts +7 -0
- package/dist/manifest-build.d.ts.map +1 -0
- package/dist/manifest-build.js +11 -0
- package/dist/manifest-build.js.map +1 -0
- package/dist/manifest.d.ts +57 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +140 -0
- package/dist/manifest.js.map +1 -0
- package/dist/manifest.json +20885 -0
- package/dist/parse.d.ts +143 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +509 -0
- package/dist/parse.js.map +1 -0
- package/dist/print.d.ts +42 -0
- package/dist/print.d.ts.map +1 -0
- package/dist/print.js +80 -0
- package/dist/print.js.map +1 -0
- package/dist/prompts.d.ts +58 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +124 -0
- package/dist/prompts.js.map +1 -0
- package/dist/repl.d.ts +34 -0
- package/dist/repl.d.ts.map +1 -0
- package/dist/repl.js +101 -0
- package/dist/repl.js.map +1 -0
- package/dist/server-mode.d.ts +37 -0
- package/dist/server-mode.d.ts.map +1 -0
- package/dist/server-mode.js +167 -0
- package/dist/server-mode.js.map +1 -0
- package/package.json +97 -3
- package/src/args.ts +440 -0
- package/src/ask.ts +119 -0
- package/src/bin.ts +11 -0
- package/src/cli.ts +596 -0
- package/src/client-mode.ts +90 -0
- package/src/ecosystem.ts +258 -0
- package/src/failure.ts +47 -0
- package/src/format.ts +177 -0
- package/src/index.ts +31 -0
- package/src/lockfile.ts +85 -0
- package/src/manifest-build.ts +12 -0
- package/src/manifest.ts +162 -0
- package/src/parse.ts +670 -0
- package/src/print.ts +108 -0
- package/src/prompts.ts +181 -0
- package/src/repl.ts +121 -0
- package/src/server-mode.ts +205 -0
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The command manifest of mark (architecture review 3.6, roadmap 1.3).
|
|
3
|
+
*
|
|
4
|
+
* Before, each run of mark imported the procedure modules of every package (about 4 s), also to
|
|
5
|
+
* show the help or to send a command to the CLI server. The manifest holds what the command line
|
|
6
|
+
* needs without the procedures: the path, the CLI metadata, the fields of the input (with the
|
|
7
|
+
* types that the flag parser converts to) and the JSON Schema of the input. mark reads it first.
|
|
8
|
+
* It imports the procedures only to run a command in this process.
|
|
9
|
+
*
|
|
10
|
+
* The build of mark writes `dist/manifest.json`. A run checks the fingerprint of the manifest:
|
|
11
|
+
* the packages that declare `client.procedures` and the build information of each one, of the
|
|
12
|
+
* core, and of mark. When a package was built again, or added, or removed, the fingerprint
|
|
13
|
+
* changes: mark then loads the procedures as before and writes the manifest again.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
17
|
+
import * as fs from "node:fs";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { jsonSchemaOf, type AnyProcedure } from "@procedures/client";
|
|
21
|
+
import { discoverFromEcosystem, findWorkspaceRoot } from "./ecosystem.js";
|
|
22
|
+
import { extractSchemaFields, resultFieldsOfSchema, type CLIMeta, type SchemaFieldInfo } from "./parse.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The format of the manifest. A change of the format makes old manifests stale.
|
|
26
|
+
* Version 2 adds `resultFields`.
|
|
27
|
+
*/
|
|
28
|
+
export const MANIFEST_VERSION = 2;
|
|
29
|
+
|
|
30
|
+
/** One command of the manifest */
|
|
31
|
+
export interface ManifestCommand {
|
|
32
|
+
path: string[];
|
|
33
|
+
/** The CLI metadata of the procedure (description, args, shorts, output, interactive) */
|
|
34
|
+
metadata: CLIMeta;
|
|
35
|
+
/** The fields of the input, as the flag parser and the help read them */
|
|
36
|
+
fields: SchemaFieldInfo[];
|
|
37
|
+
/** The JSON Schema of the input */
|
|
38
|
+
inputSchema: unknown;
|
|
39
|
+
/** The fields of the result, for the help (none when the result is not an object) */
|
|
40
|
+
resultFields: SchemaFieldInfo[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface Manifest {
|
|
44
|
+
version: number;
|
|
45
|
+
/** The state of the builds that the manifest describes (see `computeFingerprint`) */
|
|
46
|
+
fingerprint: string;
|
|
47
|
+
generatedAt: string;
|
|
48
|
+
commands: ManifestCommand[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The folder of the mark package: the nearest folder above this module with a package.json */
|
|
52
|
+
function markRoot(): string {
|
|
53
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
54
|
+
while (!fs.existsSync(path.join(dir, "package.json"))) {
|
|
55
|
+
const parent = path.dirname(dir);
|
|
56
|
+
if (parent === dir) break;
|
|
57
|
+
dir = parent;
|
|
58
|
+
}
|
|
59
|
+
return dir;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The path of the manifest: in the dist folder of mark (also when this module runs from src) */
|
|
63
|
+
export function manifestPath(): string {
|
|
64
|
+
return path.join(markRoot(), "dist", "manifest.json");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The size and the modification time of a file, or "-" when it does not exist */
|
|
68
|
+
function stamp(file: string): string {
|
|
69
|
+
try {
|
|
70
|
+
const stat = fs.statSync(file);
|
|
71
|
+
return `${stat.size}:${Math.round(stat.mtimeMs)}`;
|
|
72
|
+
} catch {
|
|
73
|
+
return "-";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The fingerprint of the builds that the manifest depends on. `tsc -b` writes
|
|
79
|
+
* `dist/.tsbuildinfo` each time it builds a package, so a new build of any package changes it.
|
|
80
|
+
* For a package without that file, the procedures file counts. `tsc --noEmit` (the typecheck
|
|
81
|
+
* script) also writes that file when the sources changed after the last build: the manifest is
|
|
82
|
+
* then stale, and the next run of mark loads the procedures and writes it again.
|
|
83
|
+
*/
|
|
84
|
+
export function computeFingerprint(root: string | null = findWorkspaceRoot(), markDir: string = markRoot()): string {
|
|
85
|
+
const parts: string[] = [`v${MANIFEST_VERSION}`];
|
|
86
|
+
if (root) {
|
|
87
|
+
parts.push(`client:${stamp(path.join(root, "packages", "client", "dist", ".tsbuildinfo"))}`);
|
|
88
|
+
}
|
|
89
|
+
// The field extraction is code of mark: a new build of mark can read the schemas differently
|
|
90
|
+
parts.push(`mark:${stamp(path.join(markDir, "dist", ".tsbuildinfo"))}`);
|
|
91
|
+
for (const pkg of root ? discoverFromEcosystem(root) : []) {
|
|
92
|
+
const info = stamp(path.join(pkg.path, "dist", ".tsbuildinfo"));
|
|
93
|
+
const procedures = info === "-" ? stamp(path.join(pkg.path, pkg.proceduresPath)) : info;
|
|
94
|
+
parts.push(`${pkg.name}:${pkg.proceduresPath}:${procedures}`);
|
|
95
|
+
}
|
|
96
|
+
return createHash("sha256").update(parts.join("\n")).digest("hex");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The CLI metadata of a procedure: only the fields that mark reads, so the manifest is plain JSON */
|
|
100
|
+
function cliMetadata(procedure: AnyProcedure): CLIMeta {
|
|
101
|
+
const meta = (procedure.metadata ?? {}) as Record<string, unknown>;
|
|
102
|
+
const result: CLIMeta = {};
|
|
103
|
+
if (typeof meta["description"] === "string") result.description = meta["description"];
|
|
104
|
+
if (Array.isArray(meta["args"])) result.args = (meta["args"] as unknown[]).filter((arg): arg is string => typeof arg === "string");
|
|
105
|
+
if (meta["shorts"] && typeof meta["shorts"] === "object") result.shorts = { ...(meta["shorts"] as Record<string, string>) };
|
|
106
|
+
if (typeof meta["output"] === "string") result.output = meta["output"] as NonNullable<CLIMeta["output"]>;
|
|
107
|
+
if (typeof meta["interactive"] === "boolean") result.interactive = meta["interactive"];
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The commands of the manifest, from the procedures of the registry */
|
|
112
|
+
export async function commandsOf(procedures: readonly AnyProcedure[]): Promise<ManifestCommand[]> {
|
|
113
|
+
return procedures.map((procedure) => {
|
|
114
|
+
// One JSON Schema for each command: the fields come from it too (roadmap 2.1)
|
|
115
|
+
const inputSchema = jsonSchemaOf(procedure.input) ?? {};
|
|
116
|
+
return {
|
|
117
|
+
path: [...procedure.path],
|
|
118
|
+
metadata: cliMetadata(procedure),
|
|
119
|
+
fields: extractSchemaFields(inputSchema),
|
|
120
|
+
inputSchema,
|
|
121
|
+
resultFields: resultFieldsOfSchema(procedure.output),
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** JSON of a manifest: a bigint (a default or a literal) becomes its text */
|
|
127
|
+
function toJson(manifest: Manifest): string {
|
|
128
|
+
return JSON.stringify(manifest, (_key, value: unknown) => (typeof value === "bigint" ? value.toString() : value), 1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Write the manifest of these procedures for the current fingerprint (a temporary file, then a rename) */
|
|
132
|
+
export async function writeManifest(procedures: readonly AnyProcedure[], file: string = manifestPath()): Promise<Manifest> {
|
|
133
|
+
const manifest: Manifest = {
|
|
134
|
+
version: MANIFEST_VERSION,
|
|
135
|
+
fingerprint: computeFingerprint(),
|
|
136
|
+
generatedAt: new Date().toISOString(),
|
|
137
|
+
commands: await commandsOf(procedures),
|
|
138
|
+
};
|
|
139
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
140
|
+
fs.writeFileSync(temporary, toJson(manifest));
|
|
141
|
+
try {
|
|
142
|
+
fs.renameSync(temporary, file);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
fs.rmSync(temporary, { force: true });
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
return manifest;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The manifest when it exists and matches the current builds, or null */
|
|
151
|
+
export function readFreshManifest(file: string = manifestPath()): Manifest | null {
|
|
152
|
+
let manifest: Manifest;
|
|
153
|
+
try {
|
|
154
|
+
manifest = JSON.parse(fs.readFileSync(file, "utf8")) as Manifest;
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
if (manifest.version !== MANIFEST_VERSION || !Array.isArray(manifest.commands)) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
return manifest.fingerprint === computeFingerprint() ? manifest : null;
|
|
162
|
+
}
|