@awak-app/simy-cli 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -4
- package/package.json +16 -3
- package/src/agent.js +713 -41
- package/src/backend-executable.js +44 -0
- package/src/browser.js +59 -0
- package/src/console/app.js +1042 -0
- package/src/console/commands.js +100 -0
- package/src/console/index.js +25 -0
- package/src/index.js +22 -1
- package/src/local-attachments.js +270 -0
- package/src/orchestrator/contract.js +1 -0
- package/src/orchestrator/independent-audit.js +25 -0
- package/src/orchestrator/index.js +1 -1
- package/src/orchestrator/instruction.js +27 -1
- package/src/orchestrator/loop.js +61 -25
- package/src/orchestrator/presentation.js +189 -0
- package/src/orchestrator/result.js +11 -0
- package/src/provider-stream.js +310 -0
- package/src/repository-inventory.js +186 -0
- package/src/run-registry.js +44 -0
- package/src/runner.js +525 -64
- package/src/web-api.js +66 -0
- package/src/workspace-context.js +37 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdir, readFile, readdir, realpath, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
|
|
7
|
+
import { normalizeGitHubRemote } from "./workspace-context.js";
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
const INVENTORY_VERSION = 1;
|
|
11
|
+
const DEFAULT_MAX_DEPTH = 12;
|
|
12
|
+
const DEFAULT_MAX_REPOSITORIES = 1_000;
|
|
13
|
+
const SKIPPED_DIRECTORIES = new Set([
|
|
14
|
+
".git",
|
|
15
|
+
".cache",
|
|
16
|
+
".npm",
|
|
17
|
+
".pnpm-store",
|
|
18
|
+
".Trash",
|
|
19
|
+
".yarn",
|
|
20
|
+
"Library",
|
|
21
|
+
"node_modules",
|
|
22
|
+
"vendor",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
export function defaultRepositoryScanRoot() {
|
|
26
|
+
return homedir();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function repositoryInventoryPath(inventoryRoot = defaultInventoryRoot()) {
|
|
30
|
+
return join(inventoryRoot, "repository-inventory.json");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function scanGitRepositories(
|
|
34
|
+
requestedRoot,
|
|
35
|
+
{ maxDepth = DEFAULT_MAX_DEPTH, maxRepositories = DEFAULT_MAX_REPOSITORIES } = {},
|
|
36
|
+
) {
|
|
37
|
+
const root = await realpath(resolve(String(requestedRoot || defaultRepositoryScanRoot())));
|
|
38
|
+
const repositories = [];
|
|
39
|
+
const visited = new Set();
|
|
40
|
+
const pending = [{ directory: root, depth: 0 }];
|
|
41
|
+
|
|
42
|
+
while (pending.length > 0 && repositories.length < maxRepositories) {
|
|
43
|
+
const current = pending.shift();
|
|
44
|
+
if (!current || visited.has(current.directory)) continue;
|
|
45
|
+
visited.add(current.directory);
|
|
46
|
+
|
|
47
|
+
let entries;
|
|
48
|
+
try {
|
|
49
|
+
entries = await readdir(current.directory, { withFileTypes: true });
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (isSkippableFilesystemError(error)) continue;
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (entries.some((entry) => entry.name === ".git")) {
|
|
56
|
+
const repository = await inspectGitRepository(current.directory);
|
|
57
|
+
if (repository) repositories.push(repository);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (current.depth >= maxDepth) continue;
|
|
61
|
+
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || shouldSkipDirectory(entry.name)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
pending.push({
|
|
67
|
+
directory: join(current.directory, entry.name),
|
|
68
|
+
depth: current.depth + 1,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
root,
|
|
75
|
+
scannedAt: new Date().toISOString(),
|
|
76
|
+
repositories: mergeRepositoryInventory(repositories),
|
|
77
|
+
truncated: repositories.length >= maxRepositories,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function readRepositoryInventory(inventoryRoot = defaultInventoryRoot()) {
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(await readFile(repositoryInventoryPath(inventoryRoot), "utf8"));
|
|
84
|
+
if (parsed?.version !== INVENTORY_VERSION) return emptyInventory();
|
|
85
|
+
return {
|
|
86
|
+
version: INVENTORY_VERSION,
|
|
87
|
+
authorizedRoots: uniqueStrings(parsed.authorized_roots),
|
|
88
|
+
repositories: mergeRepositoryInventory(parsed.repositories),
|
|
89
|
+
scannedAt: typeof parsed.scanned_at === "string" ? parsed.scanned_at : null,
|
|
90
|
+
};
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error?.code === "ENOENT" || error instanceof SyntaxError) return emptyInventory();
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function writeRepositoryInventory(
|
|
98
|
+
{ authorizedRoots = [], repositories = [], scannedAt = new Date().toISOString() },
|
|
99
|
+
inventoryRoot = defaultInventoryRoot(),
|
|
100
|
+
) {
|
|
101
|
+
const target = repositoryInventoryPath(inventoryRoot);
|
|
102
|
+
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
103
|
+
await writeFile(
|
|
104
|
+
target,
|
|
105
|
+
`${JSON.stringify(
|
|
106
|
+
{
|
|
107
|
+
version: INVENTORY_VERSION,
|
|
108
|
+
authorized_roots: uniqueStrings(authorizedRoots),
|
|
109
|
+
scanned_at: scannedAt,
|
|
110
|
+
repositories: mergeRepositoryInventory(repositories).map((item) => ({
|
|
111
|
+
repository: item.repository,
|
|
112
|
+
branch: item.branch,
|
|
113
|
+
local_path: item.local_path,
|
|
114
|
+
})),
|
|
115
|
+
},
|
|
116
|
+
null,
|
|
117
|
+
2,
|
|
118
|
+
)}\n`,
|
|
119
|
+
{ encoding: "utf8", mode: 0o600 },
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function mergeRepositoryInventory(...inventories) {
|
|
124
|
+
const byPath = new Map();
|
|
125
|
+
for (const item of inventories.flat()) {
|
|
126
|
+
const repository = normalizeGitHubRemote(item?.repository);
|
|
127
|
+
const localPath = String(item?.local_path || item?.localPath || "").trim();
|
|
128
|
+
if (!repository || !localPath) continue;
|
|
129
|
+
byPath.set(resolve(localPath), {
|
|
130
|
+
repository,
|
|
131
|
+
branch: String(item?.branch || "").trim() || "dev",
|
|
132
|
+
local_path: resolve(localPath),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return [...byPath.values()].sort((left, right) =>
|
|
136
|
+
left.repository.localeCompare(right.repository),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function findRepository(inventory, repository) {
|
|
141
|
+
const expected = normalizeGitHubRemote(repository)?.toLowerCase();
|
|
142
|
+
if (!expected) return null;
|
|
143
|
+
return (
|
|
144
|
+
inventory.find((item) => normalizeGitHubRemote(item?.repository)?.toLowerCase() === expected) ||
|
|
145
|
+
null
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function inspectGitRepository(directory) {
|
|
150
|
+
try {
|
|
151
|
+
const [{ stdout: root }, { stdout: remote }, { stdout: branch }] = await Promise.all([
|
|
152
|
+
execFileAsync("git", ["rev-parse", "--show-toplevel"], { cwd: directory }),
|
|
153
|
+
execFileAsync("git", ["remote", "get-url", "origin"], { cwd: directory }),
|
|
154
|
+
execFileAsync("git", ["branch", "--show-current"], { cwd: directory }),
|
|
155
|
+
]);
|
|
156
|
+
const repository = normalizeGitHubRemote(remote);
|
|
157
|
+
if (!repository) return null;
|
|
158
|
+
return {
|
|
159
|
+
repository,
|
|
160
|
+
branch: String(branch || "").trim() || "dev",
|
|
161
|
+
local_path: String(root || "").trim(),
|
|
162
|
+
};
|
|
163
|
+
} catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function shouldSkipDirectory(name) {
|
|
169
|
+
return name.startsWith(".") || SKIPPED_DIRECTORIES.has(name);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isSkippableFilesystemError(error) {
|
|
173
|
+
return error?.code === "EACCES" || error?.code === "EPERM" || error?.code === "ENOENT";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function uniqueStrings(values) {
|
|
177
|
+
return [...new Set((Array.isArray(values) ? values : []).map(String).filter(Boolean))];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function emptyInventory() {
|
|
181
|
+
return { version: INVENTORY_VERSION, authorizedRoots: [], repositories: [], scannedAt: null };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function defaultInventoryRoot() {
|
|
185
|
+
return process.env.SIMY_HOME?.trim() || join(homedir(), ".simy");
|
|
186
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
|
|
3
|
+
export class LocalRunRegistry extends EventEmitter {
|
|
4
|
+
#runs = new Map();
|
|
5
|
+
#listeners = new Map();
|
|
6
|
+
|
|
7
|
+
create(run) {
|
|
8
|
+
if (this.#runs.has(run.id)) throw new Error(`Run ${run.id} already exists.`);
|
|
9
|
+
this.#runs.set(run.id, run);
|
|
10
|
+
const listener = (event) => {
|
|
11
|
+
this.emit("event", { run, event });
|
|
12
|
+
this.emit("change", this.list());
|
|
13
|
+
};
|
|
14
|
+
this.#listeners.set(run.id, listener);
|
|
15
|
+
run.emitter.on("event", listener);
|
|
16
|
+
this.emit("event", { run, event: { type: "created", run_id: run.id } });
|
|
17
|
+
this.emit("change", this.list());
|
|
18
|
+
return run;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get(runId) {
|
|
22
|
+
return this.#runs.get(runId) ?? null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
has(runId) {
|
|
26
|
+
return this.#runs.has(runId);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
list() {
|
|
30
|
+
return [...this.#runs.values()].sort((left, right) => {
|
|
31
|
+
const leftTime = Date.parse(left.snapshot?.updated_at || left.startedAt || 0) || 0;
|
|
32
|
+
const rightTime = Date.parse(right.snapshot?.updated_at || right.startedAt || 0) || 0;
|
|
33
|
+
return rightTime - leftTime;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
close() {
|
|
38
|
+
for (const [runId, listener] of this.#listeners) {
|
|
39
|
+
this.#runs.get(runId)?.emitter.off("event", listener);
|
|
40
|
+
}
|
|
41
|
+
this.#listeners.clear();
|
|
42
|
+
this.removeAllListeners();
|
|
43
|
+
}
|
|
44
|
+
}
|