modelos-secure 0.3.2 → 0.3.4
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/assets/{login-DeAJGhiN.js → login-DeGa63BQ.js} +19 -9
- package/assets/login-Dz4o-1cM.js +1 -0
- package/cli.js +240 -65
- package/package.json +1 -1
- package/assets/login-CvPyhFJ7.js +0 -1
|
@@ -49,11 +49,26 @@ function step(name, detail, state = "done") {
|
|
|
49
49
|
const right = state === "done" ? "done" : "";
|
|
50
50
|
say(` ${bold(name.padEnd(STEP_WIDTH))}${detail}${right ? ` ${grey(right)}` : ""}`);
|
|
51
51
|
}
|
|
52
|
+
var RECOMMENDED = " (recommended)";
|
|
53
|
+
/**
|
|
54
|
+
* The choices as the lines they take on screen, one each and none wider than the window. The
|
|
55
|
+
* picker redraws by moving up exactly this many lines, so a line that wrapped would leave the one
|
|
56
|
+
* above it standing: seen on an 80-column window, where "Yes" appeared twice.
|
|
57
|
+
*/
|
|
58
|
+
function choiceLines(choices, index, columns) {
|
|
59
|
+
return choices.map((choice, i) => {
|
|
60
|
+
const tail = choice.recommended ? RECOMMENDED : "";
|
|
61
|
+
const room = Math.max(8, columns - 1 - 4 - tail.length);
|
|
62
|
+
const label = choice.label.length > room ? `${choice.label.slice(0, room - 1)}${unicode ? "…" : "~"}` : choice.label;
|
|
63
|
+
return ` ${i === index ? blue(pointer) : " "} ${label}${tail ? grey(tail) : ""}`;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
52
66
|
/** One question, one reason, a default. Arrows, Enter and the first letter of each choice. */
|
|
53
67
|
async function ask(question, why, choices) {
|
|
54
68
|
const start = Math.max(0, choices.findIndex((c) => c.recommended));
|
|
55
69
|
say();
|
|
56
70
|
say(` ${question}`);
|
|
71
|
+
if (why) say(` ${grey(`Why we ask: ${why}`)}`);
|
|
57
72
|
say();
|
|
58
73
|
if (!interactive()) {
|
|
59
74
|
const chosen = choices[start];
|
|
@@ -63,14 +78,10 @@ async function ask(question, why, choices) {
|
|
|
63
78
|
let index = start;
|
|
64
79
|
const render = (first) => {
|
|
65
80
|
if (!first) process.stdout.write(`\u001b[${choices.length.toString()}A`);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
process.stdout.write(`\u001b[2K ${mark} ${choice.label}${choice.recommended ? grey(" (recommended)") : ""}\n`);
|
|
69
|
-
}
|
|
81
|
+
const columns = process.stdout.columns || 80;
|
|
82
|
+
for (const line of choiceLines(choices, index, columns)) process.stdout.write(`\u001b[2K${line}\n`);
|
|
70
83
|
};
|
|
71
84
|
render(true);
|
|
72
|
-
if (why) say(`\n ${grey(`Why we ask: ${why}`)}`);
|
|
73
|
-
if (why) process.stdout.write(`\u001b[2A`);
|
|
74
85
|
return new Promise((resolve, reject) => {
|
|
75
86
|
emitKeypressEvents(process.stdin);
|
|
76
87
|
process.stdin.setRawMode(true);
|
|
@@ -79,7 +90,6 @@ async function ask(question, why, choices) {
|
|
|
79
90
|
process.stdin.off("keypress", onKey);
|
|
80
91
|
process.stdin.setRawMode(false);
|
|
81
92
|
process.stdin.pause();
|
|
82
|
-
if (why) process.stdout.write(`\u001b[2B\n`);
|
|
83
93
|
};
|
|
84
94
|
const onKey = (text, key) => {
|
|
85
95
|
if (key.ctrl && key.name === "c") {
|
|
@@ -89,7 +99,7 @@ async function ask(question, why, choices) {
|
|
|
89
99
|
}
|
|
90
100
|
if (key.name === "up") index = (index - 1 + choices.length) % choices.length;
|
|
91
101
|
else if (key.name === "down") index = (index + 1) % choices.length;
|
|
92
|
-
else if (key.name === "return") {
|
|
102
|
+
else if (key.name === "return" || key.name === "enter") {
|
|
93
103
|
done();
|
|
94
104
|
resolve(choices[index].value);
|
|
95
105
|
return;
|
|
@@ -120,7 +130,7 @@ async function askText(question, secret = false) {
|
|
|
120
130
|
reject(new Interrupted());
|
|
121
131
|
return;
|
|
122
132
|
}
|
|
123
|
-
if (key.name === "return") {
|
|
133
|
+
if (key.name === "return" || key.name === "enter") {
|
|
124
134
|
process.stdin.off("keypress", onKey);
|
|
125
135
|
process.stdin.setRawMode(false);
|
|
126
136
|
process.stdin.pause();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { t as loginCommand } from "./login-DeGa63BQ.js";
|
package/cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { _ as lastFinishedStep, a as apiClient, b as status, c as Interrupted, d as beforeStopping, f as blue, g as interactive, h as grey, i as IapRefusal, l as ask, m as finishBeforeStopping, o as atAddress, p as bold, r as ApiError$1, s as readConnection, t as loginCommand, u as askText, v as red, x as step, y as say } from "./assets/login-
|
|
2
|
+
import { _ as lastFinishedStep, a as apiClient, b as status, c as Interrupted, d as beforeStopping, f as blue, g as interactive, h as grey, i as IapRefusal, l as ask, m as finishBeforeStopping, o as atAddress, p as bold, r as ApiError$1, s as readConnection, t as loginCommand, u as askText, v as red, x as step, y as say } from "./assets/login-DeGa63BQ.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
-
import { basename, delimiter, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
6
|
+
import { basename, delimiter, dirname, extname, join, posix, relative, resolve, sep } from "node:path";
|
|
7
7
|
import { parseArgs } from "node:util";
|
|
8
|
-
import { access, appendFile, constants, glob, mkdir, readFile, readdir, realpath, rename, stat, writeFile } from "node:fs/promises";
|
|
8
|
+
import { access, appendFile, constants, glob, lstat, mkdir, readFile, readdir, realpath, rename, stat, writeFile } from "node:fs/promises";
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
import { spawn } from "node:child_process";
|
|
11
11
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -23,7 +23,7 @@ if (existsSync(resolve(here, "wasm"))) {
|
|
|
23
23
|
}
|
|
24
24
|
// package.json
|
|
25
25
|
var name = "modelos-secure";
|
|
26
|
-
var version$1 = "0.3.
|
|
26
|
+
var version$1 = "0.3.4";
|
|
27
27
|
var type = "module";
|
|
28
28
|
var bin = { "modelos-secure": "dist/cli.js" };
|
|
29
29
|
var exports$1 = { "./hook": "./dist/hook/hook.mjs" };
|
|
@@ -335,7 +335,17 @@ var SKIPPED = /* @__PURE__ */ new Set([
|
|
|
335
335
|
"e2e",
|
|
336
336
|
"coverage",
|
|
337
337
|
".turbo",
|
|
338
|
-
".cache"
|
|
338
|
+
".cache",
|
|
339
|
+
".idea",
|
|
340
|
+
".vscode",
|
|
341
|
+
".vs",
|
|
342
|
+
".terraform",
|
|
343
|
+
".ruff_cache",
|
|
344
|
+
".nox",
|
|
345
|
+
".gradle",
|
|
346
|
+
".yarn",
|
|
347
|
+
".svelte-kit",
|
|
348
|
+
".angular"
|
|
339
349
|
]);
|
|
340
350
|
var MAX_FILE_BYTES = 1048576;
|
|
341
351
|
var DOCUMENT = /* @__PURE__ */ new Set([
|
|
@@ -414,8 +424,10 @@ async function walk$1(root) {
|
|
|
414
424
|
};
|
|
415
425
|
}
|
|
416
426
|
/**
|
|
417
|
-
* The folders whose manifests describe this one: itself,
|
|
418
|
-
* repository's root, for an app kept in a folder of a larger repository
|
|
427
|
+
* The folders whose manifests describe this one: itself, each folder above it up to the
|
|
428
|
+
* repository's root, for an app kept in a folder of a larger repository, and the folders below
|
|
429
|
+
* it that hold a manifest, for a repository whose app sits in `backend/` or `sidecar/`. Without
|
|
430
|
+
* the ones below, such a repository showed no library, no framework and no route. ADR 0094.
|
|
419
431
|
*/
|
|
420
432
|
async function manifestFolders(root) {
|
|
421
433
|
const folders = [root];
|
|
@@ -427,6 +439,23 @@ async function manifestFolders(root) {
|
|
|
427
439
|
folders.push(up);
|
|
428
440
|
at = up;
|
|
429
441
|
}
|
|
442
|
+
const below = async (dir, depth) => {
|
|
443
|
+
if (depth > 3) return;
|
|
444
|
+
for (const entry of await readdir(dir, { withFileTypes: true }).catch(() => [])) {
|
|
445
|
+
if (!entry.isDirectory() || SKIPPED.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
446
|
+
const full = join(dir, entry.name);
|
|
447
|
+
for (const manifest of [
|
|
448
|
+
"package.json",
|
|
449
|
+
"pyproject.toml",
|
|
450
|
+
"Pipfile"
|
|
451
|
+
]) if (await access(join(full, manifest)).then(() => true, () => false)) {
|
|
452
|
+
folders.push(full);
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
await below(full, depth + 1);
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
await below(root, 1);
|
|
430
459
|
return folders;
|
|
431
460
|
}
|
|
432
461
|
/** A TOML file's sections by name, each with its body. Enough for the manifests read here. */
|
|
@@ -532,15 +561,25 @@ async function look(root) {
|
|
|
532
561
|
command: web,
|
|
533
562
|
from: "Procfile"
|
|
534
563
|
});
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
564
|
+
for (const name of [
|
|
565
|
+
"docker-compose.yml",
|
|
566
|
+
"docker-compose.yaml",
|
|
567
|
+
"compose.yml",
|
|
568
|
+
"compose.yaml"
|
|
569
|
+
]) {
|
|
570
|
+
const composed = (await read(name))?.match(/^\s*command:\s*(.+)$/m)?.[1];
|
|
571
|
+
if (composed) {
|
|
572
|
+
startCommands.push({
|
|
573
|
+
command: composed.replace(/^["']|["']$/g, ""),
|
|
574
|
+
from: name
|
|
575
|
+
});
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
const target = (await read("Makefile"))?.match(/^(run|start|serve|dev|dev-api|api|server):[^\n]*\n\t(.+)$/m);
|
|
580
|
+
if (target) startCommands.push({
|
|
581
|
+
command: `make ${target[1]}`,
|
|
582
|
+
from: `Makefile, whose ${target[1]} target runs ${target[2].trim().split(/\s+/).slice(0, 3).join(" ")}`
|
|
544
583
|
});
|
|
545
584
|
const settingsFiles = [];
|
|
546
585
|
const settingNames = /* @__PURE__ */ new Set();
|
|
@@ -5621,46 +5660,128 @@ function docOf(node, file) {
|
|
|
5621
5660
|
// ../../packages/scan/src/start-candidates.ts
|
|
5622
5661
|
/** Words a README command starts with that start an app, not install or test one. */
|
|
5623
5662
|
var STARTS = /^(?:\$\s*)?((?:uvicorn|gunicorn|hypercorn|fastapi (?:dev|run)|flask (?:--app \S+ )?run|streamlit run|chainlit run|gradio|python3? (?:-m )?\S+\.py|python3? -m \S+|(?:npm|pnpm|yarn|bun) (?:run )?(?:dev|start|serve)|node \S+\.(?:m?js|ts)|npx tsx \S+|docker compose up|docker-compose up)\b.*)$/;
|
|
5663
|
+
/** The words of an instruction in its list form, or null when it is in the shell form. */
|
|
5664
|
+
function listForm(body) {
|
|
5665
|
+
try {
|
|
5666
|
+
const parts = JSON.parse(body);
|
|
5667
|
+
return Array.isArray(parts) ? parts.map(String) : null;
|
|
5668
|
+
} catch {
|
|
5669
|
+
return null;
|
|
5670
|
+
}
|
|
5671
|
+
}
|
|
5672
|
+
/**
|
|
5673
|
+
* What a Dockerfile runs, as Docker reads it: the last stage's entrypoint with its command as the
|
|
5674
|
+
* arguments, unless the entrypoint is in the shell form, which ignores them. A health check's own
|
|
5675
|
+
* CMD is not it. Only arguments, starting with a dash, go to a program in the base image, which is
|
|
5676
|
+
* not here to name, so they are no command at all.
|
|
5677
|
+
*/
|
|
5678
|
+
function dockerCommand(dockerfile) {
|
|
5679
|
+
const lines = dockerfile.replace(/\\\r?\n/g, " ").split(/\r?\n/).map((l) => l.trim());
|
|
5680
|
+
const stage = lines.slice(lines.findLastIndex((l) => /^FROM\s/i.test(l)) + 1);
|
|
5681
|
+
const last = (word) => stage.findLast((l) => new RegExp(`^${word}\\s`, "i").test(l))?.slice(word.length).trim();
|
|
5682
|
+
const entrypoint = last("ENTRYPOINT");
|
|
5683
|
+
if (entrypoint && !listForm(entrypoint)) return entrypoint;
|
|
5684
|
+
const full = [entrypoint, last("CMD")].filter((body) => body !== void 0).map((body) => listForm(body)?.join(" ") ?? body).join(" ");
|
|
5685
|
+
return full && !full.startsWith("-") ? full : null;
|
|
5686
|
+
}
|
|
5687
|
+
/**
|
|
5688
|
+
* Scripts beside the app, not the app: a main block in scripts/, tests/ or eval/, or in a file
|
|
5689
|
+
* named for a check, a benchmark or a one-off job, starts nothing a review could talk to.
|
|
5690
|
+
*/
|
|
5691
|
+
var ASIDE_FOLDERS = /(^|\/)(scripts?|tests?|evals?|benchmarks?|training|tools|migrations|examples?|notebooks?|docs?|bin|hooks|experiments?|utils?)(\/|$)/;
|
|
5692
|
+
var ASIDE_NAMES = /(^|\/)(benchmark|check|backfill|stress|verify|test|dump|audit|import|export|migrate|seed|setup|install|build|deploy|smoke|demo|debug|fixture|generate|convert|download|upload|sync|clean|classify|investigate|analy[sz]e|report)([_-][^/]*)?\.py$|(^|\/)conftest\.py$|_(test|tests|audit|smoke|check|benchmark|demo|gates?)\.py$/;
|
|
5693
|
+
function isAside(path) {
|
|
5694
|
+
return ASIDE_FOLDERS.test(path.replace(/\/[^/]+$/, "")) || ASIDE_NAMES.test(path);
|
|
5695
|
+
}
|
|
5696
|
+
/**
|
|
5697
|
+
* The folder a Python file's absolute imports resolve from, which is where the app runs: for
|
|
5698
|
+
* `from app.db import x` in backend/app/main.py, that is backend. Empty for the repository's
|
|
5699
|
+
* root, or when nothing it imports is here.
|
|
5700
|
+
*/
|
|
5701
|
+
function importRoot(path, imports, files) {
|
|
5702
|
+
const parts = path.split("/").slice(0, -1);
|
|
5703
|
+
const own = imports.filter((name) => !name.startsWith("."));
|
|
5704
|
+
for (let i = parts.length; i >= 0; i--) {
|
|
5705
|
+
const dir = parts.slice(0, i).join("/");
|
|
5706
|
+
const under = (rest) => dir ? `${dir}/${rest}` : rest;
|
|
5707
|
+
if (own.some((name) => {
|
|
5708
|
+
const head = name.split(".")[0];
|
|
5709
|
+
return files.has(under(`${head}.py`)) || files.has(under(`${head}/__init__.py`));
|
|
5710
|
+
})) return dir;
|
|
5711
|
+
}
|
|
5712
|
+
return "";
|
|
5713
|
+
}
|
|
5714
|
+
/** What is offered first: the framework's own way, then the build, the README, the scripts. */
|
|
5715
|
+
var RANK = [
|
|
5716
|
+
[/^the (FastAPI|Starlette|Flask) (app|factory) /, 0],
|
|
5717
|
+
[/^the Dockerfile's command$/, 1],
|
|
5718
|
+
[/^README/i, 2],
|
|
5719
|
+
[/^(package\.json|pyproject\.toml|Procfile|docker-compose|compose\.|Makefile)/, 3],
|
|
5720
|
+
[/^the main block at ([^/]+\/)*(main|app|server|api|cli|run|serve|wsgi|asgi)\.py:/, 4]
|
|
5721
|
+
];
|
|
5722
|
+
function rankStart(candidates) {
|
|
5723
|
+
const rank = (c) => RANK.find(([pattern]) => pattern.test(c.from))?.[1] ?? 5;
|
|
5724
|
+
return candidates.map((c, i) => ({
|
|
5725
|
+
c,
|
|
5726
|
+
i
|
|
5727
|
+
})).sort((a, b) => rank(a.c) - rank(b.c) || a.i - b.i).map(({ c }) => c);
|
|
5728
|
+
}
|
|
5624
5729
|
async function startCandidates(look, project) {
|
|
5625
5730
|
const files = new Set(project.files.map((f) => f.path));
|
|
5626
5731
|
const read = (file) => readFile(join(look.root, file), "utf8").catch(() => null);
|
|
5627
5732
|
const found = [];
|
|
5628
5733
|
for (const file of project.files) {
|
|
5629
5734
|
if (file.language !== "python") continue;
|
|
5630
|
-
const
|
|
5735
|
+
const symbols = symbolsOf(file);
|
|
5736
|
+
const appDir = importRoot(file.path, symbols.imports, files);
|
|
5737
|
+
const module = file.path.slice(appDir ? appDir.length + 1 : 0).replace(/\.py$/, "").replace(/\//g, ".");
|
|
5738
|
+
const dirOption = appDir ? ` --app-dir ${appDir}` : "";
|
|
5631
5739
|
const standsAlone = module.includes(".") || !/^from \.+\w*\s+import/m.test(file.text);
|
|
5632
|
-
|
|
5740
|
+
const made = /* @__PURE__ */ new Set();
|
|
5741
|
+
for (const d of symbols.declarations) {
|
|
5633
5742
|
const callee = d.callee.split(".").at(-1) ?? "";
|
|
5634
5743
|
if (!d.name) continue;
|
|
5635
5744
|
if (!standsAlone) continue;
|
|
5636
|
-
if (/^(FastAPI|Starlette)$/.test(callee))
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5745
|
+
if (/^(FastAPI|Starlette)$/.test(callee)) {
|
|
5746
|
+
made.add(callee);
|
|
5747
|
+
found.push({
|
|
5748
|
+
command: `uvicorn ${module}:${d.name}${dirOption}`,
|
|
5749
|
+
from: `the ${callee} app ${d.name} at ${file.path}:${d.line.toString()}`
|
|
5750
|
+
});
|
|
5751
|
+
} else if (callee === "Flask") {
|
|
5752
|
+
made.add(callee);
|
|
5753
|
+
found.push({
|
|
5754
|
+
command: `flask --app ${module}:${d.name} run${dirOption}`,
|
|
5755
|
+
from: `the Flask app ${d.name} at ${file.path}:${d.line.toString()}`
|
|
5756
|
+
});
|
|
5757
|
+
}
|
|
5758
|
+
}
|
|
5759
|
+
const lines = file.text.split("\n");
|
|
5760
|
+
if (standsAlone) for (const s of symbols.symbols) {
|
|
5761
|
+
if (s.kind !== "function") continue;
|
|
5762
|
+
const body = lines.slice(s.line - 1, s.end).join("\n");
|
|
5763
|
+
const callee = /\b(FastAPI|Starlette|Flask)\(/.exec(body)?.[1];
|
|
5764
|
+
if (!callee || made.has(callee) || !/^\s+return\s/m.test(body)) continue;
|
|
5765
|
+
found.push(callee === "Flask" ? {
|
|
5766
|
+
command: `flask --app ${module}:${s.name} run${dirOption}`,
|
|
5767
|
+
from: `the Flask factory ${s.name} at ${file.path}:${s.line.toString()}`
|
|
5768
|
+
} : {
|
|
5769
|
+
command: `uvicorn ${module}:${s.name} --factory${dirOption}`,
|
|
5770
|
+
from: `the ${callee} factory ${s.name} at ${file.path}:${s.line.toString()}`
|
|
5643
5771
|
});
|
|
5644
5772
|
}
|
|
5645
|
-
const main =
|
|
5646
|
-
if (main >= 0) found.push({
|
|
5773
|
+
const main = lines.findIndex((l) => /^if __name__\s*==\s*["']__main__["']/.test(l));
|
|
5774
|
+
if (main >= 0 && !isAside(file.path)) found.push({
|
|
5647
5775
|
command: `python ${file.path}`,
|
|
5648
5776
|
from: `the main block at ${file.path}:${(main + 1).toString()}`
|
|
5649
5777
|
});
|
|
5650
5778
|
}
|
|
5651
|
-
const
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
if (Array.isArray(parts)) command = parts.map(String).join(" ");
|
|
5658
|
-
} catch {}
|
|
5659
|
-
if (command) found.push({
|
|
5660
|
-
command,
|
|
5661
|
-
from: "the Dockerfile's command"
|
|
5662
|
-
});
|
|
5663
|
-
}
|
|
5779
|
+
const dockerfile = await read("Dockerfile");
|
|
5780
|
+
const command = dockerfile ? dockerCommand(dockerfile) : null;
|
|
5781
|
+
if (command) found.push({
|
|
5782
|
+
command,
|
|
5783
|
+
from: "the Dockerfile's command"
|
|
5784
|
+
});
|
|
5664
5785
|
for (const name of [
|
|
5665
5786
|
"README.md",
|
|
5666
5787
|
"readme.md",
|
|
@@ -5680,8 +5801,10 @@ async function startCandidates(look, project) {
|
|
|
5680
5801
|
return found.filter((c) => {
|
|
5681
5802
|
if (seen.has(c.command)) return false;
|
|
5682
5803
|
seen.add(c.command);
|
|
5683
|
-
const file = c.command.split(/\s+/).find((w) => /\.(py|m?js|ts)$/.test(w));
|
|
5684
|
-
|
|
5804
|
+
const file = c.command.split(/\s+/).find((w) => /\.(py|m?js|ts)$/.test(w))?.replace(/^\.\//, "");
|
|
5805
|
+
if (!file) return true;
|
|
5806
|
+
if (/^README/i.test(c.from) && isAside(file)) return false;
|
|
5807
|
+
return files.has(file);
|
|
5685
5808
|
});
|
|
5686
5809
|
}
|
|
5687
5810
|
// ../../packages/scan/src/outline.ts
|
|
@@ -5722,7 +5845,8 @@ var where$1 = (f) => ({
|
|
|
5722
5845
|
file: f.file,
|
|
5723
5846
|
line: f.line
|
|
5724
5847
|
});
|
|
5725
|
-
async function buildOutline(look, facts, project) {
|
|
5848
|
+
async function buildOutline(look, facts, project, options = {}) {
|
|
5849
|
+
const budget = options.budget ?? BUDGET;
|
|
5726
5850
|
const outlines = project.files.map(symbolsOf);
|
|
5727
5851
|
const known = await localValues(look.root, look.settingsFiles);
|
|
5728
5852
|
const ranked = [...outlines].sort((a, b) => priority(b) - priority(a));
|
|
@@ -5731,7 +5855,7 @@ async function buildOutline(look, facts, project) {
|
|
|
5731
5855
|
let size = 0;
|
|
5732
5856
|
for (const file of ranked) {
|
|
5733
5857
|
const bytes = JSON.stringify(file).length;
|
|
5734
|
-
if (size + bytes >
|
|
5858
|
+
if (size + bytes > budget) omitted.push(file.file);
|
|
5735
5859
|
else {
|
|
5736
5860
|
code.push(file);
|
|
5737
5861
|
size += bytes;
|
|
@@ -6135,7 +6259,7 @@ function draftConfig(input) {
|
|
|
6135
6259
|
trigger: previous?.ways_in.find((w) => w.kind === "conversation")?.trigger ?? "Hello, can you help me?"
|
|
6136
6260
|
});
|
|
6137
6261
|
for (const fact of wayFacts) {
|
|
6138
|
-
const before = previous?.ways_in.find((w) => w.name === fact.name || w.function === fact.function);
|
|
6262
|
+
const before = previous?.ways_in.find((w) => w.name === fact.name || !!fact.function && w.function === fact.function);
|
|
6139
6263
|
const understood = understanding?.waysIn.find((w) => w.name === fact.name);
|
|
6140
6264
|
if (before) {
|
|
6141
6265
|
waysIn.push({
|
|
@@ -6368,11 +6492,15 @@ function resolveImport(from, name, known) {
|
|
|
6368
6492
|
base = up.slice(0, Math.max(0, up.length - (dots - 1))).join("/");
|
|
6369
6493
|
} else base = "";
|
|
6370
6494
|
const path = rest ? base ? `${base}/${rest}` : rest : base;
|
|
6371
|
-
|
|
6495
|
+
const parts = dir.split("/").filter(Boolean);
|
|
6496
|
+
const ancestors = parts.map((_, i) => `${parts.slice(0, parts.length - i).join("/")}/`);
|
|
6497
|
+
return first((dots > 0 ? [""] : [.../* @__PURE__ */ new Set([
|
|
6498
|
+
...ancestors,
|
|
6372
6499
|
"",
|
|
6373
6500
|
"src/",
|
|
6374
|
-
"app/"
|
|
6375
|
-
|
|
6501
|
+
"app/",
|
|
6502
|
+
...ancestors.map((a) => `${a}src/`)
|
|
6503
|
+
])]).flatMap((root) => [`${root}${path}.py`, `${root}${path}/__init__.py`]));
|
|
6376
6504
|
}
|
|
6377
6505
|
function normalise(path) {
|
|
6378
6506
|
const out = [];
|
|
@@ -6395,15 +6523,36 @@ var LIMITS = {
|
|
|
6395
6523
|
maxFileBytes: 1e6
|
|
6396
6524
|
};
|
|
6397
6525
|
var CODE_TOOLS = ["grep", "read"];
|
|
6398
|
-
|
|
6526
|
+
var NOT_OPENED = "the scan never opens that folder";
|
|
6527
|
+
/** Whether the scan opens a folder at all: no part of its path is one it never walks. */
|
|
6528
|
+
var opensFolder = (dir) => !dir.split("/").some((part) => SKIPPED.has(part.toLowerCase()));
|
|
6529
|
+
/**
|
|
6530
|
+
* Every file in the repository the tools could see, and so what --explain counts. All of it by hand
|
|
6531
|
+
* rather than by glob, whose `*` leaves out names starting with a dot, which is where settings files
|
|
6532
|
+
* are. A link is not a file here: it could lead outside the repository. ADR 0093.
|
|
6533
|
+
*/
|
|
6399
6534
|
async function repositoryFiles(root, pattern = "**/*", most = 5e3) {
|
|
6400
6535
|
const found = [];
|
|
6536
|
+
if (pattern === "**/*") {
|
|
6537
|
+
const walk = async (dir) => {
|
|
6538
|
+
const entries = await readdir(join(root, dir), { withFileTypes: true }).catch(() => []);
|
|
6539
|
+
for (const entry of entries) {
|
|
6540
|
+
if (found.length >= most) return;
|
|
6541
|
+
const path = posix.join(dir, entry.name);
|
|
6542
|
+
if (entry.isFile()) found.push(path);
|
|
6543
|
+
else if (entry.isDirectory() && opensFolder(entry.name)) await walk(path);
|
|
6544
|
+
}
|
|
6545
|
+
};
|
|
6546
|
+
await walk("");
|
|
6547
|
+
return found.sort();
|
|
6548
|
+
}
|
|
6401
6549
|
for await (const entry of glob(pattern, {
|
|
6402
6550
|
cwd: root,
|
|
6403
6551
|
exclude: (name) => SKIPPED.has(String(name).split(/[\\/]/).at(-1) ?? "")
|
|
6404
6552
|
})) {
|
|
6405
6553
|
const path = String(entry).split("\\").join("/");
|
|
6406
|
-
if ((
|
|
6554
|
+
if (!opensFolder(posix.dirname(path))) continue;
|
|
6555
|
+
if ((await lstat(join(root, path)).catch(() => null))?.isFile()) found.push(path);
|
|
6407
6556
|
if (found.length >= most) break;
|
|
6408
6557
|
}
|
|
6409
6558
|
return found.sort();
|
|
@@ -6419,6 +6568,7 @@ function createTools(input) {
|
|
|
6419
6568
|
const dir = String(args.dir ?? ".");
|
|
6420
6569
|
const inRepo = await insideRepository(root, dir);
|
|
6421
6570
|
if (inRepo === null) return refused(dir, "it is not a folder in this repository");
|
|
6571
|
+
if (!opensFolder(inRepo)) return refused(inRepo, NOT_OPENED);
|
|
6422
6572
|
const entries = await readdir(join(root, inRepo), { withFileTypes: true }).catch(() => null);
|
|
6423
6573
|
if (!entries) return refused(dir, "it is not a folder in this repository");
|
|
6424
6574
|
return {
|
|
@@ -6463,6 +6613,7 @@ function createTools(input) {
|
|
|
6463
6613
|
if (inRepo === null) return refused(path, "it is not a file in this repository");
|
|
6464
6614
|
const why = neverRead(inRepo);
|
|
6465
6615
|
if (why) return refused(inRepo, why);
|
|
6616
|
+
if (!opensFolder(posix.dirname(inRepo))) return refused(inRepo, NOT_OPENED);
|
|
6466
6617
|
if ((await stat(join(root, inRepo))).size > LIMITS.maxFileBytes) return refused(inRepo, "it is over 1 MB");
|
|
6467
6618
|
const all = (await readFile(join(root, inRepo), "utf8")).split(/\r?\n/);
|
|
6468
6619
|
const from = Math.max(1, Number(args.from ?? (args.to === void 0 ? 1 : args.to - LIMITS.readLines + 1)));
|
|
@@ -6924,7 +7075,7 @@ async function answerQuestion(input) {
|
|
|
6924
7075
|
async function scanFolder(root) {
|
|
6925
7076
|
const found = await look(root);
|
|
6926
7077
|
const { facts, adapters, project } = await readFacts(found);
|
|
6927
|
-
found.startCommands
|
|
7078
|
+
found.startCommands = rankStart([...found.startCommands, ...await startCandidates(found, project)]);
|
|
6928
7079
|
return {
|
|
6929
7080
|
look: found,
|
|
6930
7081
|
facts,
|
|
@@ -20713,6 +20864,23 @@ async function scriptsIn(dir) {
|
|
|
20713
20864
|
return {};
|
|
20714
20865
|
}
|
|
20715
20866
|
}
|
|
20867
|
+
var SAME_INTERPRETER = {
|
|
20868
|
+
python: "python3",
|
|
20869
|
+
python3: "python",
|
|
20870
|
+
pip: "pip3",
|
|
20871
|
+
pip3: "pip"
|
|
20872
|
+
};
|
|
20873
|
+
/**
|
|
20874
|
+
* The same command under the interpreter's name this machine has: `python main.py` where only
|
|
20875
|
+
* `python3` is installed, as on most Linux and Mac machines, or the reverse on Windows. A command
|
|
20876
|
+
* whose first word is here already is returned as it is.
|
|
20877
|
+
*/
|
|
20878
|
+
async function interpreterHere(command) {
|
|
20879
|
+
const [first = "", ...rest] = command.trim().split(/\s+/);
|
|
20880
|
+
const other = SAME_INTERPRETER[first];
|
|
20881
|
+
if (!other || await onPath(first) || !await onPath(other)) return command;
|
|
20882
|
+
return [other, ...rest].join(" ");
|
|
20883
|
+
}
|
|
20716
20884
|
/**
|
|
20717
20885
|
* Why `command` cannot start the app in `dir`, in one sentence, or null when it can as far as can
|
|
20718
20886
|
* be told without running it.
|
|
@@ -21305,7 +21473,7 @@ function toFix(ctx, fix) {
|
|
|
21305
21473
|
}
|
|
21306
21474
|
};
|
|
21307
21475
|
case "run-command": return {
|
|
21308
|
-
label: `${fix.label}: ${fix.command}`,
|
|
21476
|
+
label: fix.label.includes(fix.command) ? fix.label : `${fix.label}: ${fix.command}`,
|
|
21309
21477
|
needsPerson: true,
|
|
21310
21478
|
apply: async () => {
|
|
21311
21479
|
if (!interactive()) return;
|
|
@@ -21350,7 +21518,7 @@ var DONT_KNOW = /^(i\s+)?(don'?t|do not)\s+know|^not\s+sure|^no\s+idea|^\?$/i;
|
|
|
21350
21518
|
*/
|
|
21351
21519
|
async function typeTheCommand(ctx) {
|
|
21352
21520
|
for (let tries = 0; tries < 3; tries++) {
|
|
21353
|
-
const typed = (await askText("How do you start the app?")).trim();
|
|
21521
|
+
const typed = await interpreterHere((await askText("How do you start the app?")).trim());
|
|
21354
21522
|
if (!typed || DONT_KNOW.test(typed)) return;
|
|
21355
21523
|
const why = await whyNotRunnable(ctx.dir, typed);
|
|
21356
21524
|
if (!why) {
|
|
@@ -21367,13 +21535,14 @@ async function typeTheCommand(ctx) {
|
|
|
21367
21535
|
async function candidateFixes(ctx) {
|
|
21368
21536
|
const fixes = [];
|
|
21369
21537
|
for (const c of ctx.startCommands) {
|
|
21370
|
-
|
|
21371
|
-
if (
|
|
21538
|
+
const command = await interpreterHere(c.command);
|
|
21539
|
+
if (command === ctx.config.start?.command) continue;
|
|
21540
|
+
if (await whyNotRunnable(ctx.dir, command)) continue;
|
|
21372
21541
|
fixes.push({
|
|
21373
|
-
label: `${
|
|
21542
|
+
label: `${command} (from ${c.from})`,
|
|
21374
21543
|
apply: () => void (ctx.config.start = {
|
|
21375
21544
|
...ctx.config.start,
|
|
21376
|
-
command
|
|
21545
|
+
command
|
|
21377
21546
|
})
|
|
21378
21547
|
});
|
|
21379
21548
|
}
|
|
@@ -21389,7 +21558,11 @@ var notKnown = {
|
|
|
21389
21558
|
/** Starts the pool, or fails with what happened and how to fix it. */
|
|
21390
21559
|
async function startApp(ctx, instances) {
|
|
21391
21560
|
const { config } = ctx;
|
|
21392
|
-
const command = config.start?.command;
|
|
21561
|
+
const command = config.start?.command ? await interpreterHere(config.start.command) : void 0;
|
|
21562
|
+
if (command && config.start && command !== config.start.command) config.start = {
|
|
21563
|
+
...config.start,
|
|
21564
|
+
command
|
|
21565
|
+
};
|
|
21393
21566
|
const typeIt = {
|
|
21394
21567
|
label: "Type the command",
|
|
21395
21568
|
apply: () => typeTheCommand(ctx)
|
|
@@ -22103,6 +22276,7 @@ async function scanCommand(dir, options) {
|
|
|
22103
22276
|
say(`\nNever sent, whatever is asked (${neverSent.length.toString()}):`);
|
|
22104
22277
|
for (const n of neverSent) say(` ${n.path}, because ${n.why}`);
|
|
22105
22278
|
if (neverSent.length === 0) say(" nothing here matches");
|
|
22279
|
+
say("\nThe scan never opens folders such as .git, node_modules and tests, so nothing in them is listed or read.");
|
|
22106
22280
|
say(`\nWhat was actually sent on earlier runs is in .modelos-secure/sent.jsonl.`);
|
|
22107
22281
|
return;
|
|
22108
22282
|
}
|
|
@@ -22157,7 +22331,7 @@ async function scanCommand(dir, options) {
|
|
|
22157
22331
|
label: "Enter a token now",
|
|
22158
22332
|
recommended: true,
|
|
22159
22333
|
apply: async () => {
|
|
22160
|
-
const { loginCommand } = await import("./assets/login-
|
|
22334
|
+
const { loginCommand } = await import("./assets/login-Dz4o-1cM.js");
|
|
22161
22335
|
await loginCommand();
|
|
22162
22336
|
const fresh = await readConnection();
|
|
22163
22337
|
api = fresh ? apiClient(fresh) : null;
|
|
@@ -22275,7 +22449,7 @@ async function scanCommand(dir, options) {
|
|
|
22275
22449
|
config = treatToolAs(config, tool.name, state.answers[id]);
|
|
22276
22450
|
}
|
|
22277
22451
|
if (drafted.config.decided_for_you?.length) say(` ${grey(`I hit the limit of 7 questions, so I made the safer choice on ${plural(drafted.config.decided_for_you.length, "tool")} and recorded it for review in modelos-secure.yaml.`)}`);
|
|
22278
|
-
const candidates = [...new Map([...reading?.verified.start ?? [], ...result.look.startCommands].map((c) => [c.command, c])).values()];
|
|
22452
|
+
const candidates = [...new Map([...reading?.verified.start ?? [], ...result.look.startCommands].map((c) => [c.command, c])).values()].slice(0, 6);
|
|
22279
22453
|
if (!previous?.start?.command && candidates.length > 1 && !options.ci) {
|
|
22280
22454
|
const command = (state.answers.start ?? (options.yes ? candidates[0].command : await ask("How should the scan start the app?", "It starts the app on this machine, with the hooks attached, to check the connection.", [...candidates.map((c, i) => ({
|
|
22281
22455
|
label: `${c.command} (${c.from})`,
|
|
@@ -22327,12 +22501,13 @@ async function scanCommand(dir, options) {
|
|
|
22327
22501
|
const paths = (config.ways_in.length || 1) * (config.rules.length || 1);
|
|
22328
22502
|
say(`\n ${bold(`${config.system} is connected.`)}\n`);
|
|
22329
22503
|
say(` ${`${plural(config.ways_in.length, "way")} in`.padEnd(14)} ${config.ways_in.map((w) => w.name).join(", ")}`);
|
|
22330
|
-
|
|
22504
|
+
const heldNow = held(config);
|
|
22505
|
+
say(` ${plural(config.tools.length, "tool").padEnd(14)} ${config.tools.length === 0 ? "none to hold" : heldNow.length === 0 ? "none held during reviews" : `${heldNow.length.toString()} held during reviews: ${heldNow.join(", ")}`}`);
|
|
22331
22506
|
say(` ${plural(paths, "path").padEnd(14)} none tested yet\n`);
|
|
22332
22507
|
const url = connection ? `${connection.url}/systems/${system.id}` : "";
|
|
22333
22508
|
say(` Map: ${url}`);
|
|
22334
|
-
if (options.noReview || options.ci) return;
|
|
22335
|
-
if (
|
|
22509
|
+
if (options.noReview || options.ci || options.yes || !interactive()) return;
|
|
22510
|
+
if (await ask("Start a quick first review now? About 15 minutes, 1,200 attempts.", "It starts the app on this machine with the hooks attached, checks they hold what they should, then reviews it.", [{
|
|
22336
22511
|
label: "Yes, start it",
|
|
22337
22512
|
value: true,
|
|
22338
22513
|
recommended: true
|
|
@@ -22401,8 +22576,8 @@ async function sessionCommand(dir, run) {
|
|
|
22401
22576
|
for (;;) {
|
|
22402
22577
|
say();
|
|
22403
22578
|
const line = await askText(">");
|
|
22404
|
-
const word = line.toLowerCase();
|
|
22405
|
-
if (!line || word === "quit" || word === "exit") return;
|
|
22579
|
+
const word = line.toLowerCase().replace(/\(\)$/, "");
|
|
22580
|
+
if (!line || word === "quit" || word === "exit" || word === "q") return;
|
|
22406
22581
|
if (word === "help") {
|
|
22407
22582
|
run.help();
|
|
22408
22583
|
continue;
|
package/package.json
CHANGED
package/assets/login-CvPyhFJ7.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { t as loginCommand } from "./login-DeAJGhiN.js";
|