@petercjl/procli 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 petercjl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # procli
2
+
3
+ `procli` 是自建项目管理系统面向人和 Agent 的稳定 CLI。npm 包同时分发唯一真源的 `project-management` Skill。
4
+
5
+ ```bash
6
+ npm install -g @petercjl/procli@latest
7
+ procli profile add nas --url http://NAS地址:14317 --environment production
8
+ procli auth login --profile nas
9
+ procli skill install --agent codex
10
+ ```
11
+
12
+ 默认 Profile 是 `nas`。开发时可持久切换到本地,或只覆盖一条命令:
13
+
14
+ ```bash
15
+ procli profile add local --url http://127.0.0.1:4317 --environment development
16
+ procli profile use local
17
+ procli --profile nas project create --name "NAS 测试项目" --goal "验证最终服务" --yes
18
+ ```
19
+
20
+ 创建生产项目支持目标确认、幂等和写后回读:
21
+
22
+ ```bash
23
+ procli project create --name "新品项目" --goal "完成市场验证与上市" --yes
24
+ ```
25
+
26
+ 配置和每个 Profile 的个人凭证保存在用户配置目录,不进入 npm 包或项目仓库。运行 `procli doctor --json` 查看当前 Profile、服务指纹和认证状态。
package/bin/procli.mjs ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "../src/main.mjs";
3
+
4
+ main(process.argv.slice(2)).catch((error) => {
5
+ const result = {
6
+ ok: false,
7
+ error: {
8
+ code: error.code || "CLI_ERROR",
9
+ message: error.message || String(error),
10
+ ...(error.details ? { details: error.details } : {}),
11
+ },
12
+ };
13
+ console.log(JSON.stringify(result, null, 2));
14
+ process.exitCode = Number(error.exitCode || 1);
15
+ });
@@ -0,0 +1,18 @@
1
+ {
2
+ "schemaVersion": "1",
3
+ "name": "procli",
4
+ "version": "0.1.0",
5
+ "defaultProfile": "nas",
6
+ "capabilities": [
7
+ {
8
+ "id": "project.create",
9
+ "command": "procli project create",
10
+ "effect": "write",
11
+ "risk": "medium",
12
+ "requiredInputs": ["name"],
13
+ "optionalInputs": ["goal", "template", "mode", "profile", "idempotencyKey"],
14
+ "supports": ["dryRun", "idempotency", "targetVerification", "postWriteReadback"],
15
+ "productionConfirmation": true
16
+ }
17
+ ]
18
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@petercjl/procli",
3
+ "version": "0.1.0",
4
+ "description": "Agent-first CLI and portable Skill for SealSeek project management",
5
+ "type": "module",
6
+ "bin": {
7
+ "procli": "bin/procli.mjs"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "skill",
13
+ "capabilities.json",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "scripts": {
18
+ "test": "node --test tests/*.test.mjs",
19
+ "prepack": "npm test"
20
+ },
21
+ "engines": {
22
+ "node": ">=20"
23
+ },
24
+ "keywords": [
25
+ "agent",
26
+ "project-management",
27
+ "cli",
28
+ "dingtalk",
29
+ "skill"
30
+ ],
31
+ "author": "petercjl",
32
+ "license": "MIT",
33
+ "publishConfig": {
34
+ "access": "public",
35
+ "registry": "https://registry.npmjs.org/"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/petercjl/agent-project-lab.git",
40
+ "directory": "packages/procli"
41
+ }
42
+ }
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: project-management
3
+ description: Create and manage projects in a procli-compatible project management service. Use when a user asks to create a new product project, choose between local development and NAS production, or verify which project environment an Agent will modify.
4
+ ---
5
+
6
+ # Project Management
7
+
8
+ Use the bundled `procli` executable as the only execution surface. Do not call the service API directly or infer success from prose.
9
+
10
+ ## Input → Strategy → Output
11
+
12
+ - Input: project name is required; goal is optional; profile is optional and defaults through procli to `nas`.
13
+ - Strategy: discover the live target, preview the write, obtain confirmation for production, create with one idempotency key, then require the CLI's write-after-readback result.
14
+ - Output: the selected target, project record, generated task and Wiki summary, idempotency key, and an explicit success or structured error.
15
+
16
+ ## Main line: create a project
17
+
18
+ 1. Run `procli profile current --json`, then `procli doctor --json`. If the selected Profile is missing, unreachable, mismatched, or unauthenticated, stop and report the structured error.
19
+ 2. Collect the project name. Use the user's stated business outcome as `--goal`; do not invent a detailed goal when it would change project intent.
20
+ 3. Generate one UUID for this logical creation attempt and retain it across retries as `--idempotency-key`.
21
+ 4. Run a preview:
22
+
23
+ ```bash
24
+ procli project create --name "<name>" --goal "<goal>" --idempotency-key "<uuid>" --dry-run --json
25
+ ```
26
+
27
+ 5. State the previewed Profile, URL, environment and project name. A production write requires the user's explicit creation intent; once present, execute the same request with `--yes`. A local development write does not require `--yes`.
28
+ 6. Accept success only when the CLI returns `ok=true`, the target matches the preview, and the result contains the created project plus generated task and Wiki counts.
29
+ 7. Report the project name, ID, target Profile/environment and generated structure. Never expose stored credentials.
30
+
31
+ ## Profile routing
32
+
33
+ The default Profile is `nas`. Respect the CLI precedence exactly:
34
+
35
+ ```text
36
+ --profile > PROCLI_PROFILE > saved current Profile > nas
37
+ ```
38
+
39
+ Use `--profile local` for a one-command development override. Use `procli profile use local` only when the user asks to persistently change the default. Never replace a named Profile with a raw URL during normal execution.
40
+
41
+ ## Failure branches
42
+
43
+ - `PROFILE_NOT_CONFIGURED`: show the required `procli profile add` command and stop.
44
+ - `TARGET_MISMATCH` or `API_VERSION_UNSUPPORTED`: stop without writing; do not bypass instance verification.
45
+ - `AUTH_REQUIRED`: run `procli auth login --profile <name>` only when interactive browser authorization is available; otherwise return the login command.
46
+ - `CONFIRMATION_REQUIRED`: show the target and wait for explicit authorization; then return to main-line step 5 with the same idempotency key.
47
+ - `SERVICE_UNAVAILABLE`: do not switch from local to NAS or from NAS to local automatically. Report the selected Profile and stop.
48
+ - Unknown write result: retry only with the same idempotency key. Never create a second logical attempt merely because the response was interrupted.
49
+
50
+ ## Capability contract
51
+
52
+ Before execution, load `capabilities.json` and the adapter matching the advertised Agent platform. The required logical capability is `project.service.project.create`. If its mapping is unresolved or unsupported, return `CAPABILITY_UNAVAILABLE`; do not guess another API or CLI.
53
+
54
+ After a reported execution failure, preserve the failed command, structured error code and target metadata for a separately authorized plugin update. Runtime execution must not edit this Skill or its adapters.
@@ -0,0 +1,30 @@
1
+ {
2
+ "schema": "portable-skill-adapter",
3
+ "schema_version": "1.0.0",
4
+ "platform": "codex",
5
+ "mappings": [
6
+ {
7
+ "capability_id": "project.service.project.create",
8
+ "implementation": {
9
+ "kind": "stable-cli",
10
+ "discovery": "Resolve procli from PATH and verify it with procli version and procli capabilities --json.",
11
+ "execution": "Run procli project create with the selected Profile, one idempotency key, preview and required confirmation.",
12
+ "normalization": "Use the procli JSON envelope without reinterpretation."
13
+ },
14
+ "features": [
15
+ "profile_selection",
16
+ "target_identity_verification",
17
+ "dry_run",
18
+ "production_confirmation",
19
+ "idempotent_write",
20
+ "post_write_readback",
21
+ "structured_json"
22
+ ],
23
+ "status": "tested",
24
+ "permissions": ["Network access to the selected service", "Permission to create a project"],
25
+ "normalization": "Use the procli JSON envelope without reinterpretation.",
26
+ "tested_date": "2026-09-15",
27
+ "evidence": ["Codex resolved the installed procli 0.1.0 entry point, verified authenticated DingTalk identities for local development and NAS production Profiles, confirmed an unacknowledged production write was rejected, then created one project in each environment and received matching write-after-readback counts for seven tasks and eight Wiki paths."]
28
+ }
29
+ ]
30
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "schema": "portable-skill-adapter",
3
+ "schema_version": "1.0.0",
4
+ "platform": "sealseek",
5
+ "mappings": [
6
+ {
7
+ "capability_id": "project.service.project.create",
8
+ "implementation": {
9
+ "kind": "stable-cli",
10
+ "discovery": "Resolve procli from PATH and verify it with procli version and procli capabilities --json.",
11
+ "execution": "Run procli project create with the selected Profile, one idempotency key, preview and required confirmation.",
12
+ "normalization": "Use the procli JSON envelope without reinterpretation."
13
+ },
14
+ "features": [
15
+ "profile_selection",
16
+ "target_identity_verification",
17
+ "dry_run",
18
+ "production_confirmation",
19
+ "idempotent_write",
20
+ "post_write_readback",
21
+ "structured_json"
22
+ ],
23
+ "status": "implemented",
24
+ "permissions": ["Network access to the selected service", "Permission to create a project"],
25
+ "normalization": "Use the procli JSON envelope without reinterpretation."
26
+ }
27
+ ]
28
+ }
@@ -0,0 +1,6 @@
1
+ interface:
2
+ display_name: "项目管理"
3
+ short_description: "让 Agent 安全创建并管理商品项目及项目知识"
4
+ default_prompt: "Use $project-management to create a new product project in the selected procli profile."
5
+ policy:
6
+ allow_implicit_invocation: true
@@ -0,0 +1,36 @@
1
+ {
2
+ "schema": "portable-skill-capabilities",
3
+ "schema_version": "1.0.0",
4
+ "skill": "project-management",
5
+ "target_platforms": ["codex", "sealseek"],
6
+ "capabilities": [
7
+ {
8
+ "id": "project.service.project.create",
9
+ "purpose": "Create one project in the selected project-management service and verify its generated structure",
10
+ "required": true,
11
+ "required_features": [
12
+ "profile_selection",
13
+ "target_identity_verification",
14
+ "dry_run",
15
+ "production_confirmation",
16
+ "idempotent_write",
17
+ "post_write_readback",
18
+ "structured_json"
19
+ ],
20
+ "input_fields": ["profile", "name", "goal", "template", "mode", "idempotency_key"],
21
+ "output_fields": ["target", "project", "tasks", "wiki", "idempotency_key"],
22
+ "side_effects": {
23
+ "external_mutation": true,
24
+ "may_cost_money": false,
25
+ "authorization": "The user must explicitly intend to create the project; production additionally requires CLI confirmation."
26
+ },
27
+ "errors": [
28
+ "CAPABILITY_UNAVAILABLE",
29
+ "AUTH_REQUIRED",
30
+ "PERMISSION_REQUIRED",
31
+ "PROVIDER_FAILURE",
32
+ "OUTPUT_CONTRACT_FAILED"
33
+ ]
34
+ }
35
+ ]
36
+ }
package/src/client.mjs ADDED
@@ -0,0 +1,65 @@
1
+ import { CliError, readToken } from "./config.mjs";
2
+
3
+ export async function request(profile, pathname, options = {}) {
4
+ const token = options.token === undefined ? await readToken(profile.name) : options.token;
5
+ let response;
6
+ try {
7
+ response = await fetch(profile.url + pathname, {
8
+ method: options.method || "GET",
9
+ headers: {
10
+ accept: "application/json",
11
+ ...(options.body !== undefined ? { "content-type": "application/json" } : {}),
12
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
13
+ ...(options.idempotencyKey ? { "idempotency-key": options.idempotencyKey } : {}),
14
+ },
15
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
16
+ signal: AbortSignal.timeout(options.timeoutMs || 15000),
17
+ redirect: options.redirect || "follow",
18
+ });
19
+ } catch (error) {
20
+ throw new CliError("SERVICE_UNAVAILABLE", `无法连接 Profile “${profile.name}”:${profile.url}`, {
21
+ cause: error.name,
22
+ });
23
+ }
24
+ let payload;
25
+ try {
26
+ payload = await response.json();
27
+ } catch {
28
+ throw new CliError("INVALID_RESPONSE", `服务返回了非 JSON 响应(HTTP ${response.status})`);
29
+ }
30
+ if (!response.ok || payload.ok === false) {
31
+ const upstream = payload?.error || {};
32
+ throw new CliError(upstream.code || `HTTP_${response.status}`, upstream.message || `请求失败(HTTP ${response.status})`, {
33
+ status: response.status,
34
+ profile: profile.name,
35
+ });
36
+ }
37
+ return { status: response.status, data: payload.data };
38
+ }
39
+
40
+ export async function inspectTarget(profile) {
41
+ const { data } = await request(profile, "/api/health", { token: "" });
42
+ const target = {
43
+ profile: profile.name,
44
+ url: profile.url,
45
+ environment: data.environment || "unknown",
46
+ instanceId: data.instanceId || "unknown",
47
+ apiVersion: data.apiVersion || "unknown",
48
+ commit: data.commit || "unknown",
49
+ };
50
+ if (profile.environment && target.environment !== profile.environment)
51
+ throw new CliError("TARGET_MISMATCH", "Profile 环境与服务端环境不一致", {
52
+ expected: profile.environment,
53
+ actual: target.environment,
54
+ profile: profile.name,
55
+ });
56
+ if (profile.instanceId && target.instanceId !== profile.instanceId)
57
+ throw new CliError("TARGET_MISMATCH", "Profile 实例指纹与服务端不一致", {
58
+ expected: profile.instanceId,
59
+ actual: target.instanceId,
60
+ profile: profile.name,
61
+ });
62
+ if (String(target.apiVersion) !== "1")
63
+ throw new CliError("API_VERSION_UNSUPPORTED", `服务端 API 版本不受支持:${target.apiVersion}`);
64
+ return target;
65
+ }
package/src/config.mjs ADDED
@@ -0,0 +1,121 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ export class CliError extends Error {
6
+ constructor(code, message, details, exitCode = 1) {
7
+ super(message);
8
+ this.code = code;
9
+ this.details = details;
10
+ this.exitCode = exitCode;
11
+ }
12
+ }
13
+
14
+ export function configPath(env = process.env, platform = process.platform) {
15
+ if (env.PROCLI_CONFIG_PATH) return path.resolve(env.PROCLI_CONFIG_PATH);
16
+ const base =
17
+ platform === "win32"
18
+ ? env.APPDATA || path.join(os.homedir(), "AppData", "Roaming")
19
+ : env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
20
+ return path.join(base, "procli", "config.json");
21
+ }
22
+
23
+ export async function readConfig(file = configPath()) {
24
+ try {
25
+ const data = JSON.parse(await fs.readFile(file, "utf8"));
26
+ return {
27
+ schemaVersion: 1,
28
+ currentProfile: "nas",
29
+ profiles: {},
30
+ ...data,
31
+ profiles: data.profiles || {},
32
+ };
33
+ } catch (error) {
34
+ if (error.code === "ENOENT")
35
+ return { schemaVersion: 1, currentProfile: "nas", profiles: {} };
36
+ if (error instanceof SyntaxError)
37
+ throw new CliError("CONFIG_INVALID", `配置文件不是有效 JSON:${file}`);
38
+ throw error;
39
+ }
40
+ }
41
+
42
+ export async function writeConfig(data, file = configPath()) {
43
+ const dir = path.dirname(file);
44
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
45
+ const temp = path.join(dir, `.config.${process.pid}.${Date.now()}.tmp`);
46
+ await fs.writeFile(temp, JSON.stringify(data, null, 2) + "\n", {
47
+ flag: "wx",
48
+ mode: 0o600,
49
+ });
50
+ await fs.rename(temp, file);
51
+ await fs.chmod(file, 0o600).catch(() => {});
52
+ }
53
+
54
+ export function normalizeUrl(value) {
55
+ let parsed;
56
+ try {
57
+ parsed = new URL(value);
58
+ } catch {
59
+ throw new CliError("ARGUMENT", "--url 必须是有效的 HTTP(S) 地址");
60
+ }
61
+ if (!['http:', 'https:'].includes(parsed.protocol))
62
+ throw new CliError("ARGUMENT", "--url 只支持 HTTP(S)");
63
+ parsed.pathname = parsed.pathname.replace(/\/+$/, "");
64
+ parsed.search = "";
65
+ parsed.hash = "";
66
+ return parsed.toString().replace(/\/$/, "");
67
+ }
68
+
69
+ export function selectedProfileName(options, config, env = process.env) {
70
+ return String(options.profile || env.PROCLI_PROFILE || config.currentProfile || "nas");
71
+ }
72
+
73
+ export function resolveProfile(options, config, env = process.env) {
74
+ const name = selectedProfileName(options, config, env);
75
+ const profile = config.profiles[name];
76
+ if (!profile)
77
+ throw new CliError(
78
+ "PROFILE_NOT_CONFIGURED",
79
+ `Profile “${name}”尚未配置,请先运行 procli profile add ${name} --url <服务地址> --environment development|production`,
80
+ { profile: name },
81
+ );
82
+ return { name, ...profile };
83
+ }
84
+
85
+ function credentialPath(profileName, file = configPath()) {
86
+ if (!/^[a-zA-Z0-9._-]+$/.test(profileName))
87
+ throw new CliError("ARGUMENT", "Profile 名称只能包含字母、数字、点、下划线和连字符");
88
+ return path.join(path.dirname(file), "credentials", `${profileName}.token`);
89
+ }
90
+
91
+ export async function saveToken(profileName, token, file = configPath()) {
92
+ if (!token || token.length < 24)
93
+ throw new CliError("AUTH_FAILED", "服务端没有返回有效的 CLI 凭证");
94
+ const dest = credentialPath(profileName, file);
95
+ await fs.mkdir(path.dirname(dest), { recursive: true, mode: 0o700 });
96
+ const temp = `${dest}.${process.pid}.${Date.now()}.tmp`;
97
+ await fs.writeFile(temp, token, { flag: "wx", mode: 0o600 });
98
+ await fs.rename(temp, dest);
99
+ await fs.chmod(dest, 0o600).catch(() => {});
100
+ return dest;
101
+ }
102
+
103
+ export async function readToken(profileName, file = configPath()) {
104
+ try {
105
+ return (await fs.readFile(credentialPath(profileName, file), "utf8")).trim();
106
+ } catch (error) {
107
+ if (error.code === "ENOENT") return "";
108
+ throw error;
109
+ }
110
+ }
111
+
112
+ export async function deleteToken(profileName, file = configPath()) {
113
+ const dest = credentialPath(profileName, file);
114
+ try {
115
+ await fs.unlink(dest);
116
+ return true;
117
+ } catch (error) {
118
+ if (error.code === "ENOENT") return false;
119
+ throw error;
120
+ }
121
+ }
package/src/main.mjs ADDED
@@ -0,0 +1,204 @@
1
+ import crypto from "node:crypto";
2
+ import { spawn } from "node:child_process";
3
+ import fs from "node:fs/promises";
4
+ import { CliError, configPath, deleteToken, normalizeUrl, readConfig, readToken, resolveProfile, saveToken, selectedProfileName, writeConfig } from "./config.mjs";
5
+ import { inspectTarget, request } from "./client.mjs";
6
+ import { installSkill, skillSource, skillStatus } from "./skill.mjs";
7
+
8
+ const VERSION = "0.1.0";
9
+ const help = `procli ${VERSION} — Agent-first project management CLI
10
+
11
+ Profile(默认 nas):
12
+ profile add NAME --url URL --environment development|production
13
+ profile list | current | use NAME | remove NAME
14
+
15
+ 认证:
16
+ auth login [--profile NAME] [--no-open] [--timeout-ms 300000]
17
+ auth status|logout [--profile NAME]
18
+
19
+ 项目:
20
+ project create --name NAME [--goal TEXT] [--template ecommerce-v1]
21
+ [--mode agent] [--idempotency-key KEY] [--dry-run] [--yes]
22
+
23
+ 发现与安装:
24
+ capabilities [--profile NAME]
25
+ doctor [--profile NAME]
26
+ skill source
27
+ skill status|install|update --agent codex|agents|sealseek|openclaw [--mode auto|link|copy]
28
+
29
+ 全局:--profile NAME --json
30
+ Profile 选择顺序:--profile > PROCLI_PROFILE > profile use > nas。`;
31
+
32
+ export function parseArgs(argv) {
33
+ const options = {};
34
+ const positional = [];
35
+ for (let i = 0; i < argv.length; i++) {
36
+ const value = argv[i];
37
+ if (value.startsWith("--")) {
38
+ const key = value.slice(2);
39
+ if (["json", "yes", "dry-run", "no-open", "help", "version"].includes(key)) options[key] = true;
40
+ else {
41
+ if (!argv[i + 1] || argv[i + 1].startsWith("--")) throw new CliError("ARGUMENT", `缺少 --${key} 的值`);
42
+ options[key] = argv[++i];
43
+ }
44
+ } else positional.push(value);
45
+ }
46
+ return { options, positional };
47
+ }
48
+
49
+ function required(options, key) {
50
+ if (typeof options[key] !== "string" || !options[key].trim()) throw new CliError("ARGUMENT", `缺少 --${key}`);
51
+ return options[key].trim();
52
+ }
53
+
54
+ function output(data, ok = true) {
55
+ console.log(JSON.stringify({ ok, data }, null, 2));
56
+ if (!ok) process.exitCode = 1;
57
+ }
58
+
59
+ async function openUrl(url) {
60
+ const command = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
61
+ const child = spawn(command[0], command[1], { detached: true, stdio: "ignore" });
62
+ child.unref();
63
+ }
64
+
65
+ async function authLogin(profile, options) {
66
+ const verifier = crypto.randomBytes(32).toString("base64url");
67
+ const verifierHash = crypto.createHash("sha256").update(verifier).digest("hex");
68
+ const started = await request(profile, "/api/auth/cli/start", { method: "POST", body: { verifierHash, clientName: `procli/${VERSION}` }, token: "" });
69
+ if (!options["no-open"]) await openUrl(started.data.verificationUrl);
70
+ process.stderr.write(`请在浏览器完成钉钉授权:${started.data.verificationUrl}\n`);
71
+ const timeoutMs = Math.max(10000, Number(options["timeout-ms"] || 300000));
72
+ const deadline = Date.now() + timeoutMs;
73
+ while (Date.now() < deadline) {
74
+ await new Promise((resolve) => setTimeout(resolve, Number(started.data.intervalMs || 1500)));
75
+ const response = await fetch(profile.url + "/api/auth/cli/poll", {
76
+ method: "POST",
77
+ headers: { "content-type": "application/json" },
78
+ body: JSON.stringify({ requestId: started.data.requestId, verifier }),
79
+ signal: AbortSignal.timeout(15000),
80
+ });
81
+ const payload = await response.json();
82
+ if (response.status === 202) continue;
83
+ if (!response.ok || !payload.ok) throw new CliError(payload?.error?.code || "AUTH_FAILED", payload?.error?.message || "CLI 授权失败");
84
+ await saveToken(profile.name, payload.data.token);
85
+ return { profile: profile.name, url: profile.url, authenticated: true, member: payload.data.member, expiresAt: payload.data.expiresAt };
86
+ }
87
+ throw new CliError("AUTH_TIMEOUT", "等待钉钉授权超时,请重新运行 auth login");
88
+ }
89
+
90
+ export async function main(argv) {
91
+ const { options, positional } = parseArgs(argv);
92
+ const [group, command] = positional;
93
+ if (options.version || group === "version") return output({ version: VERSION });
94
+ if (!group || group === "help" || options.help) return console.log(help);
95
+ const file = configPath();
96
+ const config = await readConfig(file);
97
+
98
+ if (group === "profile") {
99
+ if (command === "add") {
100
+ const name = positional[2];
101
+ if (!name || !/^[a-zA-Z0-9._-]+$/.test(name)) throw new CliError("ARGUMENT", "请提供有效 Profile 名称");
102
+ const environment = required(options, "environment");
103
+ if (!['development', 'production'].includes(environment)) throw new CliError("ARGUMENT", "--environment 必须是 development 或 production");
104
+ const candidate = { name, url: normalizeUrl(required(options, "url")), environment };
105
+ const target = await inspectTarget(candidate);
106
+ config.profiles[name] = { url: candidate.url, environment, instanceId: target.instanceId };
107
+ await writeConfig(config, file);
108
+ return output({ profile: name, ...config.profiles[name], verified: true });
109
+ }
110
+ if (command === "list") return output({ currentProfile: config.currentProfile || "nas", profiles: config.profiles });
111
+ if (command === "current") {
112
+ const name = selectedProfileName(options, config);
113
+ return output({ selectedBy: options.profile ? "flag" : process.env.PROCLI_PROFILE ? "environment" : "default", profile: name, ...(config.profiles[name] || { configured: false }) });
114
+ }
115
+ if (command === "use") {
116
+ const name = positional[2];
117
+ if (!config.profiles[name]) throw new CliError("PROFILE_NOT_CONFIGURED", `Profile “${name}”尚未配置`);
118
+ config.currentProfile = name;
119
+ await writeConfig(config, file);
120
+ return output({ currentProfile: name, ...config.profiles[name] });
121
+ }
122
+ if (command === "remove") {
123
+ const name = positional[2];
124
+ if (!config.profiles[name]) throw new CliError("PROFILE_NOT_CONFIGURED", `Profile “${name}”尚未配置`);
125
+ delete config.profiles[name];
126
+ if (config.currentProfile === name) config.currentProfile = "nas";
127
+ await writeConfig(config, file);
128
+ await deleteToken(name, file);
129
+ return output({ removed: name, currentProfile: config.currentProfile });
130
+ }
131
+ throw new CliError("UNKNOWN_COMMAND", "未知 profile 命令");
132
+ }
133
+
134
+ if (group === "skill") {
135
+ if (command === "source") return output({ skill: "project-management", source: skillSource });
136
+ const agent = options.agent || "";
137
+ if (command === "status") return output(await skillStatus(agent, options["target-dir"]));
138
+ if (command === "install") return output(await installSkill({ agent, customRoot: options["target-dir"], mode: options.mode || "auto" }));
139
+ if (command === "update") return output(await installSkill({ agent, customRoot: options["target-dir"], mode: options.mode || "auto", update: true }));
140
+ throw new CliError("UNKNOWN_COMMAND", "未知 skill 命令");
141
+ }
142
+
143
+ const profile = resolveProfile(options, config);
144
+ if (group === "doctor") {
145
+ const checks = { node: { ok: Number(process.versions.node.split('.')[0]) >= 20, version: process.versions.node }, config: { ok: true, path: file }, profile: { ok: true, ...profile } };
146
+ try { checks.server = { ok: true, ...(await inspectTarget(profile)) }; } catch (error) { checks.server = { ok: false, code: error.code, message: error.message }; }
147
+ const token = await readToken(profile.name, file);
148
+ if (!token) checks.auth = { ok: false, code: "AUTH_REQUIRED" };
149
+ else {
150
+ try { checks.auth = { ok: true, ...(await request(profile, "/api/auth/cli/status", { token })).data }; }
151
+ catch (error) { checks.auth = { ok: false, code: error.code, message: error.message }; }
152
+ }
153
+ const ok = Object.values(checks).every((item) => item.ok);
154
+ return output({ checks }, ok);
155
+ }
156
+ if (group === "capabilities") {
157
+ const manifest = JSON.parse(await fs.readFile(new URL("../capabilities.json", import.meta.url), "utf8"));
158
+ let server;
159
+ try { server = (await request(profile, "/api/capabilities", { token: "" })).data; } catch (error) { server = { available: false, code: error.code }; }
160
+ return output({ cli: manifest, server, target: await inspectTarget(profile) });
161
+ }
162
+ if (group === "auth") {
163
+ if (command === "login") return output(await authLogin(profile, options));
164
+ if (command === "status") {
165
+ const token = await readToken(profile.name, file);
166
+ if (!token) throw new CliError("AUTH_REQUIRED", `Profile “${profile.name}”尚未登录`);
167
+ return output({ profile: profile.name, url: profile.url, ...(await request(profile, "/api/auth/cli/status", { token })).data });
168
+ }
169
+ if (command === "logout") {
170
+ const token = await readToken(profile.name, file);
171
+ if (token) await request(profile, "/api/auth/cli/revoke", { method: "POST", token }).catch(() => {});
172
+ const removed = await deleteToken(profile.name, file);
173
+ return output({ profile: profile.name, loggedOut: true, localCredentialRemoved: removed });
174
+ }
175
+ throw new CliError("UNKNOWN_COMMAND", "未知 auth 命令");
176
+ }
177
+ if (group === "project" && command === "create") {
178
+ const target = await inspectTarget(profile);
179
+ const input = { name: required(options, "name"), goal: options.goal || "", template: options.template || "ecommerce-v1", mode: options.mode || "agent" };
180
+ if (input.template !== "ecommerce-v1") throw new CliError("ARGUMENT", "当前只支持模板 ecommerce-v1");
181
+ const idempotencyKey = options["idempotency-key"] || crypto.randomUUID();
182
+ if (options["dry-run"]) return output({ dryRun: true, target, input, idempotencyKey });
183
+ if (target.environment === "production" && !options.yes)
184
+ throw new CliError("CONFIRMATION_REQUIRED", "目标是生产环境;确认创建后请增加 --yes", { target, input });
185
+ const result = await request(profile, "/api/v1/projects", { method: "POST", body: input, idempotencyKey });
186
+ const readback = await request(profile, `/api/v1/projects/${encodeURIComponent(result.data.project.id)}`);
187
+ if (
188
+ readback.data.project.id !== result.data.project.id ||
189
+ readback.data.tasks.count !== result.data.tasks.count ||
190
+ readback.data.wiki.count !== result.data.wiki.count
191
+ )
192
+ throw new CliError("OUTPUT_CONTRACT_FAILED", "项目写入后的回读结果与创建结果不一致", {
193
+ projectId: result.data.project.id,
194
+ });
195
+ return output({
196
+ target,
197
+ idempotencyKey,
198
+ ...readback.data,
199
+ audit: result.data.audit,
200
+ verification: "write-after-readback",
201
+ });
202
+ }
203
+ throw new CliError("UNKNOWN_COMMAND", "未知命令,运行 procli help 查看用法");
204
+ }
package/src/skill.mjs ADDED
@@ -0,0 +1,73 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { CliError } from "./config.mjs";
7
+
8
+ export const skillSource = fileURLToPath(new URL("../skill/project-management/", import.meta.url));
9
+
10
+ function targetRoot(agent) {
11
+ const roots = {
12
+ codex: path.join(os.homedir(), ".codex", "skills"),
13
+ agents: path.join(os.homedir(), ".agents", "skills"),
14
+ sealseek: path.join(os.homedir(), ".sealseek", "skill_pool"),
15
+ openclaw: path.join(os.homedir(), ".openclaw", "skills"),
16
+ };
17
+ if (!roots[agent]) throw new CliError("ARGUMENT", "--agent 必须是 codex、agents、sealseek 或 openclaw");
18
+ return roots[agent];
19
+ }
20
+
21
+ async function digest(root) {
22
+ const hash = crypto.createHash("sha256");
23
+ async function walk(dir) {
24
+ for (const entry of (await fs.readdir(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
25
+ const full = path.join(dir, entry.name);
26
+ if (entry.isDirectory()) await walk(full);
27
+ else if (entry.isFile()) {
28
+ hash.update(path.relative(root, full));
29
+ hash.update(await fs.readFile(full));
30
+ }
31
+ }
32
+ }
33
+ await walk(root);
34
+ return hash.digest("hex");
35
+ }
36
+
37
+ export async function skillStatus(agent, customRoot) {
38
+ const root = customRoot ? path.resolve(customRoot) : targetRoot(agent);
39
+ const destination = path.join(root, "project-management");
40
+ const sourceDigest = await digest(skillSource);
41
+ try {
42
+ const stat = await fs.lstat(destination);
43
+ if (stat.isSymbolicLink()) {
44
+ const resolved = await fs.realpath(destination);
45
+ return { skill: "project-management", source: skillSource, sourceDigest, targetRoot: root, destination, state: resolved === await fs.realpath(skillSource) ? "current" : "foreign-link", mode: "link", managed: resolved === await fs.realpath(skillSource), current: resolved === await fs.realpath(skillSource), resolved };
46
+ }
47
+ const marker = JSON.parse(await fs.readFile(path.join(destination, ".procli-managed.json"), "utf8"));
48
+ const current = marker.sourceDigest === sourceDigest;
49
+ return { skill: "project-management", source: skillSource, sourceDigest, targetRoot: root, destination, state: current ? "current" : "outdated", mode: "copy", managed: true, current };
50
+ } catch (error) {
51
+ if (error.code === "ENOENT") return { skill: "project-management", source: skillSource, sourceDigest, targetRoot: root, destination, state: "absent", managed: false, current: false };
52
+ if (error instanceof SyntaxError) return { skill: "project-management", source: skillSource, sourceDigest, targetRoot: root, destination, state: "foreign-directory", managed: false, current: false };
53
+ throw error;
54
+ }
55
+ }
56
+
57
+ export async function installSkill({ agent, customRoot, mode = "auto", update = false }) {
58
+ const status = await skillStatus(agent, customRoot);
59
+ if (status.current) return status;
60
+ if (status.state !== "absent" && !status.managed)
61
+ throw new CliError("SKILL_TARGET_OCCUPIED", `目标已有非 procli 管理的 Skill:${status.destination}`);
62
+ const selectedMode = mode === "auto" ? (process.platform === "win32" ? "copy" : "link") : mode;
63
+ if (!['link', 'copy'].includes(selectedMode)) throw new CliError("ARGUMENT", "--mode 必须是 auto、link 或 copy");
64
+ await fs.mkdir(status.targetRoot, { recursive: true });
65
+ if (status.managed && update) await fs.rm(status.destination, { recursive: true, force: true });
66
+ else if (status.state !== "absent") throw new CliError("SKILL_UPDATE_REQUIRED", "Skill 已存在,请使用 procli skill update");
67
+ if (selectedMode === "link") await fs.symlink(skillSource, status.destination, process.platform === "win32" ? "junction" : "dir");
68
+ else {
69
+ await fs.cp(skillSource, status.destination, { recursive: true, errorOnExist: true });
70
+ await fs.writeFile(path.join(status.destination, ".procli-managed.json"), JSON.stringify({ sourceDigest: status.sourceDigest, package: "@petercjl/procli" }, null, 2) + "\n");
71
+ }
72
+ return skillStatus(agent, customRoot);
73
+ }