@evanbobo/colco 0.12.0 → 0.18.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/README.md +14 -0
- package/package.json +1 -1
- package/src/cli.mjs +9 -3
- package/src/project.mjs +78 -0
package/README.md
CHANGED
|
@@ -59,6 +59,20 @@ How the numbers are counted:
|
|
|
59
59
|
Token fields: `input` excludes cache reads and writes, `output` includes reasoning, and `total` is input,
|
|
60
60
|
output, cache read and cache write added together.
|
|
61
61
|
|
|
62
|
+
## Finding the project for a code directory
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
colco project # projects that use this directory's repository; * marks the current branch
|
|
66
|
+
colco project --json # the same as JSON
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The command reads the directory's git remote (`origin`, or the first remote when there is no origin) and
|
|
70
|
+
the current branch, removes any user name, password or token from the remote, and asks Colco which of your
|
|
71
|
+
projects use that repository. Projects that use the current branch come first. Repositories belong to the
|
|
72
|
+
whole organization, so one repository can serve several projects, each on its own branch. A project owner
|
|
73
|
+
links them on the project's 仓库 tab or with `link_repository`. When nothing matches, the command says so
|
|
74
|
+
and points to `list_projects`.
|
|
75
|
+
|
|
62
76
|
## Other commands
|
|
63
77
|
|
|
64
78
|
```bash
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { callTool, ColcoError, VERSION } from "./api.mjs";
|
|
|
5
5
|
import { configPath, normaliseServer, readConfig, resolveConnection, TOKEN_ENV, writeConfig } from "./config.mjs";
|
|
6
6
|
import { homeDir } from "./home.mjs";
|
|
7
7
|
import { applyInstall, applyRemove, inspect, planInstall } from "./install.mjs";
|
|
8
|
+
import { project, runGit } from "./project.mjs";
|
|
8
9
|
import { cliConfirmation, readMarkers, recordMarker, TASK_STATUSES } from "./usage/markers.mjs";
|
|
9
10
|
import { buildReports, sendReports, totalTokens } from "./usage/report.mjs";
|
|
10
11
|
import { ask, askSecret, choose, multiSelect } from "./prompt.mjs";
|
|
@@ -13,7 +14,7 @@ import { ask, askSecret, choose, multiSelect } from "./prompt.mjs";
|
|
|
13
14
|
* The colco command. `add`, `list` and `remove` manage the skill for the chosen agents; `task start|done`
|
|
14
15
|
* move a task, record where it starts or ends in Colco's marker file and, on done, report the session's
|
|
15
16
|
* usage; `report` reports (or previews) without moving anything; `call` runs any Colco tool with the saved
|
|
16
|
-
* API key.
|
|
17
|
+
* API key; `project` finds the projects that use this directory's git repository.
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
20
|
const HELP = `colco ${VERSION} — Colco(叩叩)协作的 Agent Skill 与用量上报
|
|
@@ -30,6 +31,7 @@ const HELP = `colco ${VERSION} — Colco(叩叩)协作的 Agent Skill 与用
|
|
|
30
31
|
colco report [--task <id>] [--dry-run] [--agent <name> --session <id>]
|
|
31
32
|
重新统计本会话并上报;--dry-run 只预览
|
|
32
33
|
colco call <tool> ['<json>'] 用已保存的 API Key 调用任意 Colco 工具
|
|
34
|
+
colco project [--json] 按当前目录的 git 远程地址和分支找到对应的 Colco 项目
|
|
33
35
|
colco whoami 检查连接
|
|
34
36
|
|
|
35
37
|
选项:
|
|
@@ -38,6 +40,7 @@ const HELP = `colco ${VERSION} — Colco(叩叩)协作的 Agent Skill 与用
|
|
|
38
40
|
--copy 复制文件而不是创建链接
|
|
39
41
|
-y, --yes 不提问,使用默认值
|
|
40
42
|
--server <url> Colco 服务地址(也可设置 COLCO_SERVER)
|
|
43
|
+
--json colco project 以 JSON 输出
|
|
41
44
|
API Key 读取 ${TOKEN_ENV},或 colco add 时保存在 ~/.colco/config.json(仅本人可读)。`;
|
|
42
45
|
|
|
43
46
|
/** Splits argv into positionals and flags; repeated flags collect into arrays. */
|
|
@@ -45,7 +48,7 @@ export function parseArgs(argv) {
|
|
|
45
48
|
const positionals = [];
|
|
46
49
|
const flags = { agent: [] };
|
|
47
50
|
const valued = new Map([["-a", "agent"], ["--agent", "agent"], ["--server", "server"], ["--task", "task"], ["--session", "session"], ["--status", "status"]]);
|
|
48
|
-
const booleans = new Map([["-p", "project"], ["--project", "project"], ["-g", "global"], ["--global", "global"], ["--copy", "copy"], ["-y", "yes"], ["--yes", "yes"], ["--force", "force"], ["--dry-run", "dryRun"], ["-h", "help"], ["--help", "help"], ["-v", "version"], ["--version", "version"]]);
|
|
51
|
+
const booleans = new Map([["-p", "project"], ["--project", "project"], ["-g", "global"], ["--global", "global"], ["--copy", "copy"], ["-y", "yes"], ["--yes", "yes"], ["--force", "force"], ["--dry-run", "dryRun"], ["--json", "json"], ["-h", "help"], ["--help", "help"], ["-v", "version"], ["--version", "version"]]);
|
|
49
52
|
for (let index = 0; index < argv.length; index += 1) {
|
|
50
53
|
const arg = argv[index];
|
|
51
54
|
const [name, inline] = arg.startsWith("--") && arg.includes("=") ? [arg.slice(0, arg.indexOf("=")), arg.slice(arg.indexOf("=") + 1)] : [arg, undefined];
|
|
@@ -236,12 +239,13 @@ async function task(positionals, flags, io) {
|
|
|
236
239
|
return report(flags, io, { found });
|
|
237
240
|
}
|
|
238
241
|
|
|
239
|
-
/** Runs the CLI; returns the exit code. `io` carries output
|
|
242
|
+
/** Runs the CLI; returns the exit code. `io` carries output, the fetch used for every request and the git runner. */
|
|
240
243
|
export async function main(argv, output = {}) {
|
|
241
244
|
const io = {
|
|
242
245
|
out: (line) => process.stdout.write(`${line}\n`),
|
|
243
246
|
err: (line) => process.stderr.write(`${line}\n`),
|
|
244
247
|
fetch: (...args) => fetch(...args),
|
|
248
|
+
git: runGit,
|
|
245
249
|
...output,
|
|
246
250
|
};
|
|
247
251
|
try {
|
|
@@ -268,6 +272,8 @@ export async function main(argv, output = {}) {
|
|
|
268
272
|
return await task(rest, flags, io);
|
|
269
273
|
case "report":
|
|
270
274
|
return await report(flags, io, { onlyTask: flags.task === undefined ? null : taskIdOf(flags.task) });
|
|
275
|
+
case "project":
|
|
276
|
+
return await project(flags, io, resolveConnection());
|
|
271
277
|
case "whoami": {
|
|
272
278
|
const me = await callTool(resolveConnection(), "whoami", {}, io.fetch);
|
|
273
279
|
io.out(`${me.displayName || me.username}(${me.username})`);
|
package/src/project.mjs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { callTool, ColcoError } from "./api.mjs";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* `colco project`: which Colco projects use the code in this directory. It reads the git remote (origin,
|
|
6
|
+
* else the first remote) and the current branch, and asks Colco's find_projects_by_repository. Git runs
|
|
7
|
+
* through execFile with an argument list, never a shell, so nothing in a remote or branch name is executed.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const NOT_A_REPOSITORY = "当前目录不是 git 仓库:请在代码目录中运行 colco project";
|
|
11
|
+
|
|
12
|
+
/** Runs git with `args` in the current directory; resolves trimmed stdout, rejects with git's error. */
|
|
13
|
+
export function runGit(args) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
execFile("git", args, { encoding: "utf8", timeout: 10_000, maxBuffer: 1024 * 1024, windowsHide: true }, (error, stdout) => {
|
|
16
|
+
if (error) reject(error);
|
|
17
|
+
else resolve(String(stdout).trim());
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Removes `user:password@`, query and fragment from a URL-style remote, so no credential leaves the machine or
|
|
24
|
+
* reaches the screen. scp-style remotes (`git@host:path`) are left as they are; their user is not a secret.
|
|
25
|
+
*/
|
|
26
|
+
export function stripCredentials(remote) {
|
|
27
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(remote)) return remote;
|
|
28
|
+
return remote.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/]*@/i, "$1").replace(/[?#].*$/, "");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The directory's remote URL and current branch (null when HEAD is detached or has no commit yet). */
|
|
32
|
+
export async function readGitContext(git) {
|
|
33
|
+
let inside;
|
|
34
|
+
try {
|
|
35
|
+
inside = await git(["rev-parse", "--is-inside-work-tree"]);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error?.code === "ENOENT") throw new ColcoError("没有找到 git 命令:请先安装 git");
|
|
38
|
+
throw new ColcoError(NOT_A_REPOSITORY);
|
|
39
|
+
}
|
|
40
|
+
if (inside !== "true") throw new ColcoError(NOT_A_REPOSITORY);
|
|
41
|
+
let remote = await git(["remote", "get-url", "origin"]).catch(() => "");
|
|
42
|
+
if (!remote) {
|
|
43
|
+
const [first] = (await git(["remote"]).catch(() => "")).split("\n").map((name) => name.trim()).filter(Boolean);
|
|
44
|
+
if (first) remote = await git(["remote", "get-url", "--", first]).catch(() => "");
|
|
45
|
+
}
|
|
46
|
+
if (!remote) throw new ColcoError("这个 git 仓库还没有远程地址(git remote),Colco 无法据此找到项目");
|
|
47
|
+
const head = await git(["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "");
|
|
48
|
+
return { remote: stripCredentials(remote), branch: head && head !== "HEAD" ? head : null };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Prints the projects that use this directory's repository, or why there are none; `--json` prints the data. */
|
|
52
|
+
export async function project(flags, io, connection) {
|
|
53
|
+
const { remote, branch } = await readGitContext(io.git);
|
|
54
|
+
const data = await callTool(connection, "find_projects_by_repository", { url: remote, ...(branch ? { branch } : {}) }, io.fetch);
|
|
55
|
+
if (flags.json) {
|
|
56
|
+
io.out(JSON.stringify({ remote, branch, ...data }, null, 2));
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
io.out(`仓库:${data.repository?.url ?? remote}${branch ? `(当前分支 ${branch})` : ""}`);
|
|
60
|
+
if (!data.repository) {
|
|
61
|
+
io.out("这个仓库还没有登记到 Colco,没有项目使用它。");
|
|
62
|
+
io.out("用 colco call list_projects 查看你的项目;项目负责人可以用 link_repository 把仓库关联到项目。");
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
if (data.projects.length === 0) {
|
|
66
|
+
io.out(`仓库「${data.repository.name}」已登记,但你能访问的项目都没有使用它。`);
|
|
67
|
+
io.out("用 colco call list_projects 查看你的项目。");
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
const idWidth = Math.max(...data.projects.map((item) => String(item.projectId).length));
|
|
71
|
+
const nameWidth = Math.max(...data.projects.map((item) => item.projectName.length));
|
|
72
|
+
for (const item of data.projects) {
|
|
73
|
+
const archived = item.projectStatus === "archived" ? "(已归档)" : "";
|
|
74
|
+
io.out(`${item.branchMatched ? "*" : " "} ${String(item.projectId).padStart(idWidth)} ${item.projectName.padEnd(nameWidth)} ${item.branch}${archived}`);
|
|
75
|
+
}
|
|
76
|
+
if (data.projects.some((item) => item.branchMatched)) io.out("* 项目使用的就是当前分支");
|
|
77
|
+
return 0;
|
|
78
|
+
}
|