@illuminis/comprism 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.
Files changed (80) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +281 -0
  3. package/out/agent/command.d.ts +86 -0
  4. package/out/agent/command.js +259 -0
  5. package/out/agent/render.d.ts +97 -0
  6. package/out/agent/render.js +255 -0
  7. package/out/agent/session.d.ts +175 -0
  8. package/out/agent/session.js +573 -0
  9. package/out/commands/ask.d.ts +1 -0
  10. package/out/commands/ask.js +146 -0
  11. package/out/commands/codemap.d.ts +2 -0
  12. package/out/commands/codemap.js +151 -0
  13. package/out/commands/commands-thin.d.ts +39 -0
  14. package/out/commands/commands-thin.js +182 -0
  15. package/out/commands/install.d.ts +163 -0
  16. package/out/commands/install.js +543 -0
  17. package/out/commands/keys.d.ts +55 -0
  18. package/out/commands/keys.js +344 -0
  19. package/out/commands/login.d.ts +9 -0
  20. package/out/commands/login.js +384 -0
  21. package/out/commands/repl.d.ts +1 -0
  22. package/out/commands/repl.js +752 -0
  23. package/out/commands/settings.d.ts +21 -0
  24. package/out/commands/settings.js +244 -0
  25. package/out/commands/welcome.d.ts +1 -0
  26. package/out/commands/welcome.js +196 -0
  27. package/out/executor/documents.d.ts +40 -0
  28. package/out/executor/documents.js +170 -0
  29. package/out/executor/files.d.ts +2 -0
  30. package/out/executor/files.js +360 -0
  31. package/out/executor/git.d.ts +48 -0
  32. package/out/executor/git.js +132 -0
  33. package/out/executor/hooks.d.ts +67 -0
  34. package/out/executor/hooks.js +247 -0
  35. package/out/executor/index.d.ts +29 -0
  36. package/out/executor/index.js +221 -0
  37. package/out/executor/notebook.d.ts +2 -0
  38. package/out/executor/notebook.js +147 -0
  39. package/out/executor/paths.d.ts +15 -0
  40. package/out/executor/paths.js +126 -0
  41. package/out/executor/shell.d.ts +41 -0
  42. package/out/executor/shell.js +336 -0
  43. package/out/graph/build.d.ts +45 -0
  44. package/out/graph/build.js +91 -0
  45. package/out/graph/facts.d.ts +47 -0
  46. package/out/graph/facts.js +12 -0
  47. package/out/graph/files.d.ts +45 -0
  48. package/out/graph/files.js +207 -0
  49. package/out/graph/read-locales.d.ts +29 -0
  50. package/out/graph/read-locales.js +246 -0
  51. package/out/graph/read-python.d.ts +11 -0
  52. package/out/graph/read-python.js +115 -0
  53. package/out/graph/read-typescript.d.ts +16 -0
  54. package/out/graph/read-typescript.js +292 -0
  55. package/out/graph/sync.d.ts +66 -0
  56. package/out/graph/sync.js +242 -0
  57. package/out/lib/attach.d.ts +62 -0
  58. package/out/lib/attach.js +228 -0
  59. package/out/lib/config.d.ts +93 -0
  60. package/out/lib/config.js +198 -0
  61. package/out/lib/connection.d.ts +73 -0
  62. package/out/lib/connection.js +188 -0
  63. package/out/lib/gateway.d.ts +239 -0
  64. package/out/lib/gateway.js +171 -0
  65. package/out/lib/prompt.d.ts +34 -0
  66. package/out/lib/prompt.js +108 -0
  67. package/out/lib/types.d.ts +417 -0
  68. package/out/lib/types.js +21 -0
  69. package/out/lib/ui.d.ts +114 -0
  70. package/out/lib/ui.js +265 -0
  71. package/out/lib/version.d.ts +24 -0
  72. package/out/lib/version.js +27 -0
  73. package/out/lib/voice.d.ts +50 -0
  74. package/out/lib/voice.js +218 -0
  75. package/out/postinstall.d.ts +2 -0
  76. package/out/postinstall.js +92 -0
  77. package/out/thin.d.ts +2 -0
  78. package/out/thin.js +259 -0
  79. package/package.json +101 -0
  80. package/scripts/read_python.py +270 -0
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The one shape both readers emit, and the only thing that ever leaves the
3
+ * machine.
4
+ *
5
+ * An object and a link, each carrying a file and a line. Nothing else. No file
6
+ * contents, no source text, no string a person wrote. That is what makes
7
+ * sending the map to the service a different act from sending the code: the
8
+ * service learns that a function called `price_of` exists at line 40 of a named
9
+ * file and what calls it, and it never learns what that function says.
10
+ */
11
+ export interface GraphObject {
12
+ /** Unique within a project. A file is its own path; a symbol is `file::name`. */
13
+ id: string;
14
+ /** file, function, method, class, component, model, table, route. */
15
+ kind: string;
16
+ name: string;
17
+ file: string;
18
+ line: number;
19
+ [extra: string]: unknown;
20
+ }
21
+ export interface GraphLink {
22
+ from: string;
23
+ /** imports, defines, calls, renders, inherits, references, maps_to,
24
+ * handled_by, declares, calls_endpoint, browser_route. */
25
+ kind: string;
26
+ /** Set when the reader could resolve the far end itself. */
27
+ to?: string;
28
+ /** Set when it could not, and the service joins it up. */
29
+ to_name?: string;
30
+ [extra: string]: unknown;
31
+ }
32
+ export interface FactSet {
33
+ objects: GraphObject[];
34
+ links: GraphLink[];
35
+ /** A file that declares routers, and what each is mounted under. Python only:
36
+ * an endpoint's path is written relative to a router that is mounted with a
37
+ * prefix in a different file, and without both halves the front end and the
38
+ * back end never join up. */
39
+ routers?: {
40
+ prefixes: Record<string, string>;
41
+ mounts: {
42
+ target: string;
43
+ prefix: string;
44
+ line: number;
45
+ }[];
46
+ };
47
+ }
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ /**
3
+ * The one shape both readers emit, and the only thing that ever leaves the
4
+ * machine.
5
+ *
6
+ * An object and a link, each carrying a file and a line. Nothing else. No file
7
+ * contents, no source text, no string a person wrote. That is what makes
8
+ * sending the map to the service a different act from sending the code: the
9
+ * service learns that a function called `price_of` exists at line 40 of a named
10
+ * file and what calls it, and it never learns what that function says.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,45 @@
1
+ /** How many files one project may have indexed. A ceiling, not a guess: a
2
+ * monorepo of forty thousand files would take minutes to read and produce a
3
+ * map too large to be worth sending, and half a map that claims to be whole is
4
+ * the failure this whole feature exists to avoid. Past this the map says
5
+ * plainly that it covers part of the project. */
6
+ export declare const MAX_FILES = 6000;
7
+ /** How large a single file may be before it is skipped. A generated bundle or
8
+ * a checked-in minified file is not code anybody navigates. */
9
+ export declare const MAX_FILE_BYTES = 1500000;
10
+ export declare const PY_EXT: Set<string>;
11
+ export declare const TS_EXT: Set<string>;
12
+ export interface IndexedFile {
13
+ /** Path relative to the project root, forward slashes, always. */
14
+ rel: string;
15
+ size: number;
16
+ mtimeMs: number;
17
+ lang: "python" | "typescript" | "translations";
18
+ }
19
+ /** Every file the map covers, in a stable order.
20
+ *
21
+ * Sorted, and that is not cosmetic: the fingerprint below is a hash over this
22
+ * list, so an unstable order would report the code had moved every time the
23
+ * filesystem felt like returning entries differently. */
24
+ export declare function indexedFiles(root: string): IndexedFile[];
25
+ /** One short string per file that changes whenever that file does.
26
+ *
27
+ * Size and modified time rather than a hash of the contents: reading six
28
+ * thousand files to find out whether any of them changed costs more than
29
+ * rebuilding the map. A file edited back to its original contents within the
30
+ * same millisecond would be missed, which is a case that does not occur in
31
+ * practice and is corrected by the next ordinary edit. */
32
+ export declare function stampOf(file: IndexedFile): string;
33
+ /** One short string for the whole project. Saying the code has not moved is a
34
+ * claim, and this is the evidence for it. */
35
+ export declare function fingerprint(files: IndexedFile[]): {
36
+ hash: string;
37
+ files: number;
38
+ };
39
+ /** The commit the project is standing on, when it is a repository at all.
40
+ *
41
+ * Read with a plain file read rather than by running git, because this is
42
+ * called at the start of every job and spawning a process there is a cost paid
43
+ * on work that has nothing to do with the map. A project with no git still
44
+ * gets a usable map; it simply cannot stamp an answer with a commit. */
45
+ export declare function headCommit(root: string): string;
@@ -0,0 +1,207 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.TS_EXT = exports.PY_EXT = exports.MAX_FILE_BYTES = exports.MAX_FILES = void 0;
37
+ exports.indexedFiles = indexedFiles;
38
+ exports.stampOf = stampOf;
39
+ exports.fingerprint = fingerprint;
40
+ exports.headCommit = headCommit;
41
+ /**
42
+ * What the code map covers, and how to tell in milliseconds whether the code
43
+ * has moved since it was last read.
44
+ *
45
+ * Everything here runs on the person's own machine, because it is the one place
46
+ * their source files exist. Nothing in this folder decides, prices or
47
+ * classifies anything: it reads files and produces facts. That is the division
48
+ * the whole product rests on, and a function here that formed an opinion would
49
+ * be the first crack in it.
50
+ */
51
+ const crypto = __importStar(require("crypto"));
52
+ const fs = __importStar(require("fs"));
53
+ const path = __importStar(require("path"));
54
+ const read_locales_1 = require("./read-locales");
55
+ /** Folders never worth reading: somebody else's code, or build output. */
56
+ const SKIP_DIRS = new Set([
57
+ "node_modules", ".git", "__pycache__", ".venv", "venv", "env",
58
+ "site-packages", "dist", "build", ".pytest_cache", ".mypy_cache",
59
+ "htmlcov", ".ruff_cache", "coverage", ".vite", ".next", ".turbo",
60
+ "playwright-report", "test-results", "vendor", "target", ".cache",
61
+ "scratchpad", ".comprism", ".completionprism",
62
+ ]);
63
+ /** Folder names that are build output only when they sit beside a package
64
+ * manifest. `out` is where TypeScript writes its JavaScript by convention, but
65
+ * it is also a perfectly ordinary name for a source folder, so skipping it
66
+ * everywhere would quietly remove real code from somebody's map. Next to a
67
+ * package.json it is almost always generated.
68
+ *
69
+ * Found by mapping this repository and getting 83 changed files from one edit:
70
+ * the terminal client's own compiled output was in the map, so every rebuild
71
+ * looked like eighty three source files moving. */
72
+ const BUILT_BESIDE_MANIFEST = new Set(["out", "output", "lib", "es", "esm"]);
73
+ /** How many files one project may have indexed. A ceiling, not a guess: a
74
+ * monorepo of forty thousand files would take minutes to read and produce a
75
+ * map too large to be worth sending, and half a map that claims to be whole is
76
+ * the failure this whole feature exists to avoid. Past this the map says
77
+ * plainly that it covers part of the project. */
78
+ exports.MAX_FILES = 6000;
79
+ /** How large a single file may be before it is skipped. A generated bundle or
80
+ * a checked-in minified file is not code anybody navigates. */
81
+ exports.MAX_FILE_BYTES = 1_500_000;
82
+ exports.PY_EXT = new Set([".py"]);
83
+ exports.TS_EXT = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
84
+ function isCode(rel, name) {
85
+ if (name.endsWith(".d.ts"))
86
+ return null;
87
+ if (name.endsWith(".min.js"))
88
+ return null;
89
+ const ext = path.extname(name);
90
+ if (exports.PY_EXT.has(ext))
91
+ return "python";
92
+ if (exports.TS_EXT.has(ext))
93
+ return "typescript";
94
+ // Translation files. Not code, and in the map for a reason no code parser can
95
+ // serve: the label a person sees is a key here and a sentence in ten language
96
+ // files, and "which languages are missing this one" is the question asked
97
+ // about it. See `read-locales.ts`.
98
+ if ((0, read_locales_1.looksLikeTranslations)(rel))
99
+ return "translations";
100
+ return null;
101
+ }
102
+ /** Every file the map covers, in a stable order.
103
+ *
104
+ * Sorted, and that is not cosmetic: the fingerprint below is a hash over this
105
+ * list, so an unstable order would report the code had moved every time the
106
+ * filesystem felt like returning entries differently. */
107
+ function indexedFiles(root) {
108
+ const out = [];
109
+ const walk = (dir) => {
110
+ if (out.length >= exports.MAX_FILES)
111
+ return;
112
+ let entries;
113
+ try {
114
+ entries = fs.readdirSync(dir, { withFileTypes: true });
115
+ }
116
+ catch {
117
+ return;
118
+ }
119
+ for (const entry of entries) {
120
+ if (out.length >= exports.MAX_FILES)
121
+ return;
122
+ const full = path.join(dir, entry.name);
123
+ if (entry.isDirectory()) {
124
+ if (entry.name.startsWith(".") && entry.name !== ".github")
125
+ continue;
126
+ if (SKIP_DIRS.has(entry.name))
127
+ continue;
128
+ if (BUILT_BESIDE_MANIFEST.has(entry.name)
129
+ && fs.existsSync(path.join(dir, "package.json")))
130
+ continue;
131
+ walk(full);
132
+ continue;
133
+ }
134
+ if (!entry.isFile())
135
+ continue;
136
+ const rel = path.relative(root, full).split(path.sep).join("/");
137
+ const lang = isCode(rel, entry.name);
138
+ if (!lang)
139
+ continue;
140
+ let stat;
141
+ try {
142
+ stat = fs.statSync(full);
143
+ }
144
+ catch {
145
+ continue;
146
+ }
147
+ if (stat.size > exports.MAX_FILE_BYTES)
148
+ continue;
149
+ out.push({
150
+ rel,
151
+ size: stat.size,
152
+ mtimeMs: Math.round(stat.mtimeMs),
153
+ lang,
154
+ });
155
+ }
156
+ };
157
+ walk(root);
158
+ out.sort((a, b) => (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0));
159
+ return out;
160
+ }
161
+ /** One short string per file that changes whenever that file does.
162
+ *
163
+ * Size and modified time rather than a hash of the contents: reading six
164
+ * thousand files to find out whether any of them changed costs more than
165
+ * rebuilding the map. A file edited back to its original contents within the
166
+ * same millisecond would be missed, which is a case that does not occur in
167
+ * practice and is corrected by the next ordinary edit. */
168
+ function stampOf(file) {
169
+ return `${file.size}:${file.mtimeMs}`;
170
+ }
171
+ /** One short string for the whole project. Saying the code has not moved is a
172
+ * claim, and this is the evidence for it. */
173
+ function fingerprint(files) {
174
+ const hash = crypto.createHash("sha1");
175
+ for (const file of files)
176
+ hash.update(`${file.rel}:${stampOf(file)}\n`);
177
+ return { hash: hash.digest("hex").slice(0, 16), files: files.length };
178
+ }
179
+ /** The commit the project is standing on, when it is a repository at all.
180
+ *
181
+ * Read with a plain file read rather than by running git, because this is
182
+ * called at the start of every job and spawning a process there is a cost paid
183
+ * on work that has nothing to do with the map. A project with no git still
184
+ * gets a usable map; it simply cannot stamp an answer with a commit. */
185
+ function headCommit(root) {
186
+ try {
187
+ const head = fs.readFileSync(path.join(root, ".git", "HEAD"), "utf8").trim();
188
+ if (!head.startsWith("ref:"))
189
+ return head.slice(0, 40);
190
+ const ref = head.slice(4).trim();
191
+ const direct = path.join(root, ".git", ref);
192
+ if (fs.existsSync(direct))
193
+ return fs.readFileSync(direct, "utf8").trim().slice(0, 40);
194
+ const packed = path.join(root, ".git", "packed-refs");
195
+ if (fs.existsSync(packed)) {
196
+ for (const line of fs.readFileSync(packed, "utf8").split("\n")) {
197
+ const [sha, name] = line.trim().split(" ");
198
+ if (name === ref && sha)
199
+ return sha.slice(0, 40);
200
+ }
201
+ }
202
+ }
203
+ catch {
204
+ /* not a repository, or an unusual layout. Neither is an error. */
205
+ }
206
+ return "";
207
+ }
@@ -0,0 +1,29 @@
1
+ import type { FactSet } from "./facts";
2
+ /** Is this JSON file a translation file?
3
+ *
4
+ * Both conditions, not either: a `data/en.json` of sample records is not a
5
+ * translation file, and a `locales/schema.json` is not one either. Indexing
6
+ * the wrong JSON would fill the map with things nobody asked about. */
7
+ export declare function looksLikeTranslations(rel: string): boolean;
8
+ /** The language a translation file is for, from its own name. */
9
+ export declare function languageOf(rel: string): string;
10
+ interface KeyAt {
11
+ key: string;
12
+ line: number;
13
+ }
14
+ /**
15
+ * Every leaf key in a translation file, with the line it sits on.
16
+ *
17
+ * A single pass over the text, tracking strings, escapes, nesting and lines. It
18
+ * reads the structure rather than the values: what a translation SAYS never
19
+ * leaves the machine, only that the key exists and where.
20
+ */
21
+ export declare function scanKeys(text: string): KeyAt[];
22
+ export declare function readLocales(root: string, rels: string[]): {
23
+ facts: Record<string, FactSet>;
24
+ failures: {
25
+ file: string;
26
+ reason: string;
27
+ }[];
28
+ };
29
+ export {};
@@ -0,0 +1,246 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.looksLikeTranslations = looksLikeTranslations;
37
+ exports.languageOf = languageOf;
38
+ exports.scanKeys = scanKeys;
39
+ exports.readLocales = readLocales;
40
+ /**
41
+ * Read translation files into code map facts.
42
+ *
43
+ * ## Why these are in the map at all
44
+ *
45
+ * A label a person sees on screen is not written in the code. It is a key in
46
+ * the code and a sentence in ten language files, and the question people
47
+ * actually ask about it is one no code parser can answer: *where is this label,
48
+ * and which languages are missing it.* An agent asked to add a field to a form
49
+ * has to put the same line in ten files, and a map that indexes only Python and
50
+ * TypeScript sends it to search for them one at a time.
51
+ *
52
+ * ## One object per key, not one per key per file
53
+ *
54
+ * Ten language files times three thousand keys is thirty one thousand objects,
55
+ * and almost all of that is the same key repeated. Each file reports its own
56
+ * keys here, because facts are read per file and a file has to be re-readable
57
+ * on its own. The service then collapses them into ONE object per key carrying
58
+ * which languages have it and which do not, which is both far smaller and the
59
+ * answer somebody wanted.
60
+ *
61
+ * ## Line numbers are tracked by hand, and that is deliberate
62
+ *
63
+ * `JSON.parse` gives no positions, and "the key is in this file somewhere" is
64
+ * not a useful answer when the file is three thousand lines. So the text is
65
+ * scanned once, tracking the line of every key as it goes. It is a scanner, not
66
+ * a guess: the same file always produces the same lines.
67
+ */
68
+ const fs = __importStar(require("fs"));
69
+ const path = __importStar(require("path"));
70
+ /** Folders whose JSON files are translations. */
71
+ const LOCALE_DIRS = new Set(["locales", "locale", "i18n", "lang", "translations"]);
72
+ /** A file named like a language: `en`, `en-US`, `pt_BR`. */
73
+ const LANGUAGE_NAME = /^[a-z]{2}([-_][A-Za-z]{2,4})?$/;
74
+ /** Is this JSON file a translation file?
75
+ *
76
+ * Both conditions, not either: a `data/en.json` of sample records is not a
77
+ * translation file, and a `locales/schema.json` is not one either. Indexing
78
+ * the wrong JSON would fill the map with things nobody asked about. */
79
+ function looksLikeTranslations(rel) {
80
+ if (!rel.endsWith(".json"))
81
+ return false;
82
+ const parts = rel.split("/");
83
+ const base = path.basename(rel, ".json");
84
+ const parent = parts.length > 1 ? parts[parts.length - 2] ?? "" : "";
85
+ return LOCALE_DIRS.has(parent.toLowerCase()) && LANGUAGE_NAME.test(base);
86
+ }
87
+ /** The language a translation file is for, from its own name. */
88
+ function languageOf(rel) {
89
+ return path.basename(rel, ".json");
90
+ }
91
+ /**
92
+ * Every leaf key in a translation file, with the line it sits on.
93
+ *
94
+ * A single pass over the text, tracking strings, escapes, nesting and lines. It
95
+ * reads the structure rather than the values: what a translation SAYS never
96
+ * leaves the machine, only that the key exists and where.
97
+ */
98
+ function scanKeys(text) {
99
+ const out = [];
100
+ const stack = [];
101
+ let line = 1;
102
+ let i = 0;
103
+ /** The key whose value has not been seen yet. Until the value turns up we do
104
+ * not know whether this key is a group or a leaf. */
105
+ let pending = null;
106
+ let pendingLine = 0;
107
+ const at = (n) => text[n] ?? "";
108
+ /** Consume a string starting at the opening quote, counting any lines in it. */
109
+ const readString = () => {
110
+ i += 1;
111
+ let value = "";
112
+ while (i < text.length) {
113
+ const ch = at(i);
114
+ if (ch === "\\") {
115
+ i += 2;
116
+ continue;
117
+ }
118
+ if (ch === '"') {
119
+ i += 1;
120
+ return value;
121
+ }
122
+ if (ch === "\n")
123
+ line += 1;
124
+ value += ch;
125
+ i += 1;
126
+ }
127
+ return value;
128
+ };
129
+ /** Consume an array, whatever is nested in it. Arrays hold no keys we want. */
130
+ const skipArray = () => {
131
+ let depth = 0;
132
+ while (i < text.length) {
133
+ const ch = at(i);
134
+ if (ch === '"') {
135
+ readString();
136
+ continue;
137
+ }
138
+ if (ch === "\n")
139
+ line += 1;
140
+ if (ch === "[")
141
+ depth += 1;
142
+ if (ch === "]") {
143
+ depth -= 1;
144
+ i += 1;
145
+ if (depth === 0)
146
+ return;
147
+ continue;
148
+ }
149
+ i += 1;
150
+ }
151
+ };
152
+ while (i < text.length) {
153
+ const ch = at(i);
154
+ if (ch === "\n") {
155
+ line += 1;
156
+ i += 1;
157
+ continue;
158
+ }
159
+ if (ch === " " || ch === "\t" || ch === "\r" || ch === ",") {
160
+ i += 1;
161
+ continue;
162
+ }
163
+ // A key is waiting, so whatever comes next is its VALUE. This is checked
164
+ // first, and that is the whole correctness of the scanner: a string value
165
+ // looks exactly like a key until you know which position you are in.
166
+ if (pending !== null) {
167
+ if (ch === "{") {
168
+ stack.push(pending);
169
+ pending = null;
170
+ i += 1;
171
+ continue;
172
+ }
173
+ out.push({ key: [...stack, pending].join("."), line: pendingLine });
174
+ pending = null;
175
+ if (ch === '"') {
176
+ readString();
177
+ continue;
178
+ }
179
+ if (ch === "[") {
180
+ skipArray();
181
+ continue;
182
+ }
183
+ while (i < text.length && !/[,}\]\s]/.test(at(i)))
184
+ i += 1;
185
+ continue;
186
+ }
187
+ if (ch === '"') {
188
+ const startLine = line;
189
+ const name = readString();
190
+ let j = i;
191
+ while (j < text.length && /\s/.test(at(j))) {
192
+ if (at(j) === "\n")
193
+ line += 1;
194
+ j += 1;
195
+ }
196
+ if (at(j) === ":") {
197
+ pending = name;
198
+ pendingLine = startLine;
199
+ i = j + 1;
200
+ }
201
+ continue;
202
+ }
203
+ if (ch === "}") {
204
+ stack.pop();
205
+ i += 1;
206
+ continue;
207
+ }
208
+ i += 1;
209
+ }
210
+ return out;
211
+ }
212
+ function readOne(root, rel) {
213
+ const text = fs.readFileSync(path.join(root, rel), "utf8");
214
+ const language = languageOf(rel);
215
+ const objects = [{
216
+ id: rel, kind: "file", name: path.basename(rel), file: rel, line: 1,
217
+ lang: "translations", language,
218
+ lines: text.split("\n").length,
219
+ }];
220
+ for (const { key, line } of scanKeys(text)) {
221
+ objects.push({
222
+ // Unique per file, because facts are stored per file. The service
223
+ // collapses every file's copy of one key into a single object.
224
+ id: `i18n:${key}@${rel}`,
225
+ kind: "translation_key",
226
+ name: key,
227
+ file: rel,
228
+ line,
229
+ language,
230
+ });
231
+ }
232
+ return { objects, links: [] };
233
+ }
234
+ function readLocales(root, rels) {
235
+ const facts = {};
236
+ const failures = [];
237
+ for (const rel of rels) {
238
+ try {
239
+ facts[rel] = readOne(root, rel);
240
+ }
241
+ catch (err) {
242
+ failures.push({ file: rel, reason: err?.name || "Error" });
243
+ }
244
+ }
245
+ return { facts, failures };
246
+ }
@@ -0,0 +1,11 @@
1
+ import type { FactSet } from "./facts";
2
+ /** Which Python this machine has, or null. Asked once per process. */
3
+ export declare function findPython(): string | null;
4
+ export declare function readPython(root: string, rels: string[]): {
5
+ facts: Record<string, FactSet>;
6
+ failures: {
7
+ file: string;
8
+ reason: string;
9
+ }[];
10
+ available: boolean;
11
+ };