@petercjl/procli 0.1.0 → 0.2.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 +21 -0
- package/capabilities.json +33 -3
- package/package.json +1 -1
- package/skill/project-management/SKILL.md +52 -30
- package/skill/project-management/adapters/codex.json +37 -3
- package/skill/project-management/adapters/sealseek.json +30 -2
- package/skill/project-management/capabilities.json +57 -2
- package/src/main.mjs +318 -48
package/README.md
CHANGED
|
@@ -23,4 +23,25 @@ procli --profile nas project create --name "NAS 测试项目" --goal "验证最
|
|
|
23
23
|
procli project create --name "新品项目" --goal "完成市场验证与上市" --yes
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
商品项目默认生成标准生命周期 SOP。Agent 也可以提交自定义 SOP;每个节点都必须有名称和工作说明:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"nodes": [{ "name": "验证需求", "description": "取得真实用户证据并形成结论" }]
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
procli project create --name "新品验证" --sop-file ./sop.json
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
删除项目仅允许系统管理员执行,必须先预览、精确复述项目名称并显式确认。删除是可恢复的软删除:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
procli project delete --name "新品验证" --confirm-name "新品验证" --dry-run
|
|
42
|
+
procli project delete --name "新品验证" --confirm-name "新品验证" --yes
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
店铺项目和企业项目已保留为后续能力,目前 CLI 会返回 `FEATURE_UNSUPPORTED`。
|
|
46
|
+
|
|
26
47
|
配置和每个 Profile 的个人凭证保存在用户配置目录,不进入 npm 包或项目仓库。运行 `procli doctor --json` 查看当前 Profile、服务指纹和认证状态。
|
package/capabilities.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "1",
|
|
3
3
|
"name": "procli",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"defaultProfile": "nas",
|
|
6
6
|
"capabilities": [
|
|
7
7
|
{
|
|
@@ -10,9 +10,39 @@
|
|
|
10
10
|
"effect": "write",
|
|
11
11
|
"risk": "medium",
|
|
12
12
|
"requiredInputs": ["name"],
|
|
13
|
-
"optionalInputs": [
|
|
14
|
-
|
|
13
|
+
"optionalInputs": [
|
|
14
|
+
"goal",
|
|
15
|
+
"type",
|
|
16
|
+
"template",
|
|
17
|
+
"sopFile",
|
|
18
|
+
"mode",
|
|
19
|
+
"profile",
|
|
20
|
+
"idempotencyKey"
|
|
21
|
+
],
|
|
22
|
+
"supports": [
|
|
23
|
+
"dryRun",
|
|
24
|
+
"idempotency",
|
|
25
|
+
"targetVerification",
|
|
26
|
+
"postWriteReadback"
|
|
27
|
+
],
|
|
15
28
|
"productionConfirmation": true
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "project.delete",
|
|
32
|
+
"command": "procli project delete",
|
|
33
|
+
"effect": "soft-delete",
|
|
34
|
+
"risk": "high",
|
|
35
|
+
"requiredInputs": ["name", "confirmName", "yes"],
|
|
36
|
+
"optionalInputs": ["profile", "idempotencyKey"],
|
|
37
|
+
"supports": [
|
|
38
|
+
"dryRun",
|
|
39
|
+
"idempotency",
|
|
40
|
+
"targetVerification",
|
|
41
|
+
"postWriteReadback",
|
|
42
|
+
"recoverableDeletion"
|
|
43
|
+
],
|
|
44
|
+
"requiredSystemRole": "admin",
|
|
45
|
+
"confirmationRequired": true
|
|
16
46
|
}
|
|
17
47
|
]
|
|
18
48
|
}
|
package/package.json
CHANGED
|
@@ -1,54 +1,76 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: project-management
|
|
3
|
-
description: Create and
|
|
3
|
+
description: Create and delete projects in a procli-compatible project management service. Use when a user asks an Agent to initialize a product project with the default or a custom SOP, delete a project, choose local or NAS, or verify the target environment.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Project Management
|
|
7
7
|
|
|
8
|
-
Use the bundled `procli` executable as the only execution surface.
|
|
8
|
+
Use the bundled `procli` executable as the only execution surface. Load the current `capabilities.json` and the adapter matching the advertised Agent platform before acting. Resolve the live CLI version and capabilities at runtime; do not call service APIs directly.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
The logical capabilities are `project.service.project.create` and `project.service.project.delete`. If the matching adapter mapping is unresolved or unsupported, return `CAPABILITY_UNAVAILABLE`.
|
|
11
11
|
|
|
12
|
-
|
|
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.
|
|
12
|
+
## Target routing
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
The default Profile is `nas`. Respect this precedence exactly:
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
```text
|
|
17
|
+
--profile > PROCLI_PROFILE > saved current Profile > nas
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Run `procli profile current --json` and `procli doctor --json` before a write. Use `--profile local` for a one-command development override. Change the saved Profile only when the user asks for a persistent switch. Never silently switch environments after a connection or authentication failure.
|
|
21
|
+
|
|
22
|
+
## Create a product project
|
|
23
|
+
|
|
24
|
+
1. Collect the project name; it is the only required business input. Use the user's stated outcome as `--goal` when available.
|
|
25
|
+
2. Project type defaults to `product`. Report shop and company project creation as `FEATURE_UNSUPPORTED` until the CLI advertises those capabilities.
|
|
26
|
+
3. If the user wants the standard product lifecycle, omit `--sop-file`; the service initializes the default SOP, sequential tasks, and their Wiki pages.
|
|
27
|
+
4. If the user supplies a custom SOP, obtain both `name` and `description` for every node. Ask the user for missing meaning, or offer a clearly labeled draft for approval. Never send an incomplete node. Save the approved structure as JSON in this form:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{ "nodes": [{ "name": "节点名称", "description": "节点工作说明" }] }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
5. Generate one UUID for the logical attempt and preserve it across retries. Preview with the same arguments and idempotency key:
|
|
22
34
|
|
|
23
35
|
```bash
|
|
24
|
-
procli project create --name "<name>" --goal "<goal>" --idempotency-key "<uuid>" --dry-run --json
|
|
36
|
+
procli project create --name "<name>" --goal "<goal>" --sop-file "<file>" --idempotency-key "<uuid>" --dry-run --json
|
|
25
37
|
```
|
|
26
38
|
|
|
27
|
-
|
|
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.
|
|
39
|
+
Omit `--goal` or `--sop-file` when unused.
|
|
30
40
|
|
|
31
|
-
|
|
41
|
+
6. Show the selected Profile, URL, environment, project name, SOP mode, and node count. The user's request to create is authorization for the local write. Production additionally requires `--yes`.
|
|
42
|
+
7. Execute the matching command. Accept success only when `ok=true`, the target matches the preview, and the CLI returns a write-after-readback project, task count, and Wiki count.
|
|
32
43
|
|
|
33
|
-
|
|
44
|
+
## Delete a project
|
|
34
45
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
Deletion is a recoverable soft delete, but it is a high-risk administrator action.
|
|
47
|
+
|
|
48
|
+
1. Collect the exact project name and generate one UUID for the logical attempt.
|
|
49
|
+
2. Preview the target and affected counts:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
procli project delete --name "<exact name>" --confirm-name "<exact name>" --idempotency-key "<uuid>" --dry-run --json
|
|
53
|
+
```
|
|
38
54
|
|
|
39
|
-
|
|
55
|
+
3. Present the Profile/environment, exact name, task count, Wiki count, administrator requirement, and recoverable status. Obtain explicit confirmation if it is not already present in the current request.
|
|
56
|
+
4. Execute the identical request with `--yes`. Both name arguments must match exactly. The authenticated identity must have `systemRole=admin`.
|
|
57
|
+
5. Accept success only when the CLI returns `verification=soft-delete-readback`, `deletedAt`, and `recoverable=true`.
|
|
40
58
|
|
|
41
59
|
## Failure branches
|
|
42
60
|
|
|
43
|
-
- `PROFILE_NOT_CONFIGURED`:
|
|
44
|
-
- `TARGET_MISMATCH` or `API_VERSION_UNSUPPORTED`: stop without writing
|
|
45
|
-
- `AUTH_REQUIRED`: run `procli auth login --profile <name>` only when interactive browser authorization is available; otherwise
|
|
46
|
-
- `
|
|
47
|
-
- `
|
|
48
|
-
-
|
|
61
|
+
- `PROFILE_NOT_CONFIGURED`: provide the required `procli profile add` command and stop.
|
|
62
|
+
- `TARGET_MISMATCH` or `API_VERSION_UNSUPPORTED`: stop without writing.
|
|
63
|
+
- `AUTH_REQUIRED`: run `procli auth login --profile <name>` only when interactive browser authorization is available; otherwise provide the command.
|
|
64
|
+
- `PROJECT_NAME_CONFLICT`: ask for a distinct active-project name.
|
|
65
|
+
- `SOP_NODE_INCOMPLETE`: return to custom-SOP collection and obtain both required fields.
|
|
66
|
+
- `CONFIRMATION_REQUIRED`: preserve the idempotency key, obtain confirmation, then resume the matching write.
|
|
67
|
+
- `ADMIN_REQUIRED`: tell the user to contact the system administrator; do not seek a bypass.
|
|
68
|
+
- `DELETE_CONFIRMATION_MISMATCH`: request the exact project name and rebuild the preview.
|
|
69
|
+
- `SERVICE_UNAVAILABLE`: report the selected Profile and stop without changing environments.
|
|
70
|
+
- Unknown write result: retry only with the same idempotency key.
|
|
49
71
|
|
|
50
|
-
##
|
|
72
|
+
## Output contract
|
|
51
73
|
|
|
52
|
-
|
|
74
|
+
For creation, report the project name and ID, target Profile/environment, SOP mode, task count, Wiki count, and verification. For deletion, report the exact project name and ID, deletion time, actor, recoverable status, target, and verification. Never expose credentials or local token storage.
|
|
53
75
|
|
|
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.
|
|
76
|
+
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.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"implementation": {
|
|
9
9
|
"kind": "stable-cli",
|
|
10
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.",
|
|
11
|
+
"execution": "Run procli project create with the selected Profile, optional custom SOP file, one idempotency key, preview and required confirmation.",
|
|
12
12
|
"normalization": "Use the procli JSON envelope without reinterpretation."
|
|
13
13
|
},
|
|
14
14
|
"features": [
|
|
@@ -21,10 +21,44 @@
|
|
|
21
21
|
"structured_json"
|
|
22
22
|
],
|
|
23
23
|
"status": "tested",
|
|
24
|
-
"permissions": [
|
|
24
|
+
"permissions": [
|
|
25
|
+
"Network access to the selected service",
|
|
26
|
+
"Permission to create a project"
|
|
27
|
+
],
|
|
28
|
+
"normalization": "Use the procli JSON envelope without reinterpretation.",
|
|
29
|
+
"tested_date": "2026-09-15",
|
|
30
|
+
"evidence": [
|
|
31
|
+
"Codex resolved procli 0.2.0, verified the authenticated local Profile, created a two-node custom-SOP product project, and received matching project, task, dependency, and Wiki write-after-readback results."
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"capability_id": "project.service.project.delete",
|
|
36
|
+
"implementation": {
|
|
37
|
+
"kind": "stable-cli",
|
|
38
|
+
"discovery": "Resolve procli from PATH and verify it with procli version and procli capabilities --json.",
|
|
39
|
+
"execution": "Run procli project delete with the selected Profile, exact-name confirmation, one idempotency key, preview, explicit --yes, and administrator authorization.",
|
|
40
|
+
"normalization": "Use the procli JSON envelope without reinterpretation."
|
|
41
|
+
},
|
|
42
|
+
"features": [
|
|
43
|
+
"profile_selection",
|
|
44
|
+
"target_identity_verification",
|
|
45
|
+
"dry_run",
|
|
46
|
+
"admin_authorization",
|
|
47
|
+
"exact_name_confirmation",
|
|
48
|
+
"idempotent_write",
|
|
49
|
+
"post_write_readback",
|
|
50
|
+
"structured_json"
|
|
51
|
+
],
|
|
52
|
+
"status": "tested",
|
|
53
|
+
"permissions": [
|
|
54
|
+
"Network access to the selected service",
|
|
55
|
+
"System administrator permission to delete a project"
|
|
56
|
+
],
|
|
25
57
|
"normalization": "Use the procli JSON envelope without reinterpretation.",
|
|
26
58
|
"tested_date": "2026-09-15",
|
|
27
|
-
"evidence": [
|
|
59
|
+
"evidence": [
|
|
60
|
+
"Codex previewed the two-node local test project, deleted it as the authenticated system administrator with exact-name confirmation, and received soft-delete-readback with the project id, actor, timestamp, counts, and recoverable=true."
|
|
61
|
+
]
|
|
28
62
|
}
|
|
29
63
|
]
|
|
30
64
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"implementation": {
|
|
9
9
|
"kind": "stable-cli",
|
|
10
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.",
|
|
11
|
+
"execution": "Run procli project create with the selected Profile, optional custom SOP file, one idempotency key, preview and required confirmation.",
|
|
12
12
|
"normalization": "Use the procli JSON envelope without reinterpretation."
|
|
13
13
|
},
|
|
14
14
|
"features": [
|
|
@@ -21,7 +21,35 @@
|
|
|
21
21
|
"structured_json"
|
|
22
22
|
],
|
|
23
23
|
"status": "implemented",
|
|
24
|
-
"permissions": [
|
|
24
|
+
"permissions": [
|
|
25
|
+
"Network access to the selected service",
|
|
26
|
+
"Permission to create a project"
|
|
27
|
+
],
|
|
28
|
+
"normalization": "Use the procli JSON envelope without reinterpretation."
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"capability_id": "project.service.project.delete",
|
|
32
|
+
"implementation": {
|
|
33
|
+
"kind": "stable-cli",
|
|
34
|
+
"discovery": "Resolve procli from PATH and verify it with procli version and procli capabilities --json.",
|
|
35
|
+
"execution": "Run procli project delete with exact-name confirmation, one idempotency key, preview, explicit --yes, and administrator authorization.",
|
|
36
|
+
"normalization": "Use the procli JSON envelope without reinterpretation."
|
|
37
|
+
},
|
|
38
|
+
"features": [
|
|
39
|
+
"profile_selection",
|
|
40
|
+
"target_identity_verification",
|
|
41
|
+
"dry_run",
|
|
42
|
+
"admin_authorization",
|
|
43
|
+
"exact_name_confirmation",
|
|
44
|
+
"idempotent_write",
|
|
45
|
+
"post_write_readback",
|
|
46
|
+
"structured_json"
|
|
47
|
+
],
|
|
48
|
+
"status": "implemented",
|
|
49
|
+
"permissions": [
|
|
50
|
+
"Network access to the selected service",
|
|
51
|
+
"System administrator permission to delete a project"
|
|
52
|
+
],
|
|
25
53
|
"normalization": "Use the procli JSON envelope without reinterpretation."
|
|
26
54
|
}
|
|
27
55
|
]
|
|
@@ -17,8 +17,23 @@
|
|
|
17
17
|
"post_write_readback",
|
|
18
18
|
"structured_json"
|
|
19
19
|
],
|
|
20
|
-
"input_fields": [
|
|
21
|
-
|
|
20
|
+
"input_fields": [
|
|
21
|
+
"profile",
|
|
22
|
+
"name",
|
|
23
|
+
"goal",
|
|
24
|
+
"type",
|
|
25
|
+
"template",
|
|
26
|
+
"sop_file",
|
|
27
|
+
"mode",
|
|
28
|
+
"idempotency_key"
|
|
29
|
+
],
|
|
30
|
+
"output_fields": [
|
|
31
|
+
"target",
|
|
32
|
+
"project",
|
|
33
|
+
"tasks",
|
|
34
|
+
"wiki",
|
|
35
|
+
"idempotency_key"
|
|
36
|
+
],
|
|
22
37
|
"side_effects": {
|
|
23
38
|
"external_mutation": true,
|
|
24
39
|
"may_cost_money": false,
|
|
@@ -29,6 +44,46 @@
|
|
|
29
44
|
"AUTH_REQUIRED",
|
|
30
45
|
"PERMISSION_REQUIRED",
|
|
31
46
|
"PROVIDER_FAILURE",
|
|
47
|
+
"OUTPUT_CONTRACT_FAILED",
|
|
48
|
+
"PROJECT_NAME_CONFLICT",
|
|
49
|
+
"SOP_NODE_INCOMPLETE",
|
|
50
|
+
"FEATURE_UNSUPPORTED"
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "project.service.project.delete",
|
|
55
|
+
"purpose": "Soft-delete one exactly named project as a system administrator and verify the deletion",
|
|
56
|
+
"required": true,
|
|
57
|
+
"required_features": [
|
|
58
|
+
"profile_selection",
|
|
59
|
+
"target_identity_verification",
|
|
60
|
+
"dry_run",
|
|
61
|
+
"admin_authorization",
|
|
62
|
+
"exact_name_confirmation",
|
|
63
|
+
"idempotent_write",
|
|
64
|
+
"post_write_readback",
|
|
65
|
+
"structured_json"
|
|
66
|
+
],
|
|
67
|
+
"input_fields": ["profile", "name", "confirm_name", "idempotency_key"],
|
|
68
|
+
"output_fields": [
|
|
69
|
+
"target",
|
|
70
|
+
"project_id",
|
|
71
|
+
"project_name",
|
|
72
|
+
"deleted_at",
|
|
73
|
+
"deleted_by",
|
|
74
|
+
"recoverable",
|
|
75
|
+
"idempotency_key"
|
|
76
|
+
],
|
|
77
|
+
"side_effects": {
|
|
78
|
+
"external_mutation": true,
|
|
79
|
+
"may_cost_money": false,
|
|
80
|
+
"authorization": "The user must explicitly intend to delete the project and the authenticated identity must have the system admin role."
|
|
81
|
+
},
|
|
82
|
+
"errors": [
|
|
83
|
+
"AUTH_REQUIRED",
|
|
84
|
+
"ADMIN_REQUIRED",
|
|
85
|
+
"DELETE_CONFIRMATION_MISMATCH",
|
|
86
|
+
"NOT_FOUND",
|
|
32
87
|
"OUTPUT_CONTRACT_FAILED"
|
|
33
88
|
]
|
|
34
89
|
}
|
package/src/main.mjs
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
CliError,
|
|
6
|
+
configPath,
|
|
7
|
+
deleteToken,
|
|
8
|
+
normalizeUrl,
|
|
9
|
+
readConfig,
|
|
10
|
+
readToken,
|
|
11
|
+
resolveProfile,
|
|
12
|
+
saveToken,
|
|
13
|
+
selectedProfileName,
|
|
14
|
+
writeConfig,
|
|
15
|
+
} from "./config.mjs";
|
|
5
16
|
import { inspectTarget, request } from "./client.mjs";
|
|
6
17
|
import { installSkill, skillSource, skillStatus } from "./skill.mjs";
|
|
7
18
|
|
|
8
|
-
const VERSION = "0.
|
|
19
|
+
const VERSION = "0.2.0";
|
|
9
20
|
const help = `procli ${VERSION} — Agent-first project management CLI
|
|
10
21
|
|
|
11
22
|
Profile(默认 nas):
|
|
@@ -17,8 +28,10 @@ Profile(默认 nas):
|
|
|
17
28
|
auth status|logout [--profile NAME]
|
|
18
29
|
|
|
19
30
|
项目:
|
|
20
|
-
project create --name NAME [--goal TEXT] [--
|
|
31
|
+
project create --name NAME [--goal TEXT] [--type product] [--sop-file FILE]
|
|
21
32
|
[--mode agent] [--idempotency-key KEY] [--dry-run] [--yes]
|
|
33
|
+
project delete --name NAME --confirm-name NAME --yes
|
|
34
|
+
[--idempotency-key KEY] [--dry-run]
|
|
22
35
|
|
|
23
36
|
发现与安装:
|
|
24
37
|
capabilities [--profile NAME]
|
|
@@ -36,9 +49,13 @@ export function parseArgs(argv) {
|
|
|
36
49
|
const value = argv[i];
|
|
37
50
|
if (value.startsWith("--")) {
|
|
38
51
|
const key = value.slice(2);
|
|
39
|
-
if (
|
|
52
|
+
if (
|
|
53
|
+
["json", "yes", "dry-run", "no-open", "help", "version"].includes(key)
|
|
54
|
+
)
|
|
55
|
+
options[key] = true;
|
|
40
56
|
else {
|
|
41
|
-
if (!argv[i + 1] || argv[i + 1].startsWith("--"))
|
|
57
|
+
if (!argv[i + 1] || argv[i + 1].startsWith("--"))
|
|
58
|
+
throw new CliError("ARGUMENT", `缺少 --${key} 的值`);
|
|
42
59
|
options[key] = argv[++i];
|
|
43
60
|
}
|
|
44
61
|
} else positional.push(value);
|
|
@@ -47,7 +64,8 @@ export function parseArgs(argv) {
|
|
|
47
64
|
}
|
|
48
65
|
|
|
49
66
|
function required(options, key) {
|
|
50
|
-
if (typeof options[key] !== "string" || !options[key].trim())
|
|
67
|
+
if (typeof options[key] !== "string" || !options[key].trim())
|
|
68
|
+
throw new CliError("ARGUMENT", `缺少 --${key}`);
|
|
51
69
|
return options[key].trim();
|
|
52
70
|
}
|
|
53
71
|
|
|
@@ -56,22 +74,76 @@ function output(data, ok = true) {
|
|
|
56
74
|
if (!ok) process.exitCode = 1;
|
|
57
75
|
}
|
|
58
76
|
|
|
77
|
+
async function readSopFile(file) {
|
|
78
|
+
let parsed;
|
|
79
|
+
try {
|
|
80
|
+
parsed = JSON.parse(await fs.readFile(file, "utf8"));
|
|
81
|
+
} catch (error) {
|
|
82
|
+
throw new CliError("SOP_FILE_INVALID", `无法读取 SOP JSON:${file}`, {
|
|
83
|
+
cause: error.message,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const nodes = Array.isArray(parsed) ? parsed : parsed?.nodes;
|
|
87
|
+
if (!Array.isArray(nodes) || nodes.length === 0)
|
|
88
|
+
throw new CliError(
|
|
89
|
+
"SOP_FILE_INVALID",
|
|
90
|
+
"SOP 文件必须是非空节点数组,或包含 nodes 数组",
|
|
91
|
+
);
|
|
92
|
+
nodes.forEach((node, index) => {
|
|
93
|
+
if (typeof node?.name !== "string" || !node.name.trim())
|
|
94
|
+
throw new CliError(
|
|
95
|
+
"SOP_NODE_INCOMPLETE",
|
|
96
|
+
`SOP 第 ${index + 1} 个节点缺少 name`,
|
|
97
|
+
);
|
|
98
|
+
if (typeof node?.description !== "string" || !node.description.trim())
|
|
99
|
+
throw new CliError(
|
|
100
|
+
"SOP_NODE_INCOMPLETE",
|
|
101
|
+
`SOP 节点“${node.name}”缺少 description`,
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
return {
|
|
105
|
+
nodes: nodes.map(({ name, description }) => ({
|
|
106
|
+
name: name.trim(),
|
|
107
|
+
description: description.trim(),
|
|
108
|
+
})),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
59
112
|
async function openUrl(url) {
|
|
60
|
-
const command =
|
|
61
|
-
|
|
113
|
+
const command =
|
|
114
|
+
process.platform === "darwin"
|
|
115
|
+
? ["open", [url]]
|
|
116
|
+
: process.platform === "win32"
|
|
117
|
+
? ["cmd", ["/c", "start", "", url]]
|
|
118
|
+
: ["xdg-open", [url]];
|
|
119
|
+
const child = spawn(command[0], command[1], {
|
|
120
|
+
detached: true,
|
|
121
|
+
stdio: "ignore",
|
|
122
|
+
});
|
|
62
123
|
child.unref();
|
|
63
124
|
}
|
|
64
125
|
|
|
65
126
|
async function authLogin(profile, options) {
|
|
66
127
|
const verifier = crypto.randomBytes(32).toString("base64url");
|
|
67
|
-
const verifierHash = crypto
|
|
68
|
-
|
|
128
|
+
const verifierHash = crypto
|
|
129
|
+
.createHash("sha256")
|
|
130
|
+
.update(verifier)
|
|
131
|
+
.digest("hex");
|
|
132
|
+
const started = await request(profile, "/api/auth/cli/start", {
|
|
133
|
+
method: "POST",
|
|
134
|
+
body: { verifierHash, clientName: `procli/${VERSION}` },
|
|
135
|
+
token: "",
|
|
136
|
+
});
|
|
69
137
|
if (!options["no-open"]) await openUrl(started.data.verificationUrl);
|
|
70
|
-
process.stderr.write(
|
|
138
|
+
process.stderr.write(
|
|
139
|
+
`请在浏览器完成钉钉授权:${started.data.verificationUrl}\n`,
|
|
140
|
+
);
|
|
71
141
|
const timeoutMs = Math.max(10000, Number(options["timeout-ms"] || 300000));
|
|
72
142
|
const deadline = Date.now() + timeoutMs;
|
|
73
143
|
while (Date.now() < deadline) {
|
|
74
|
-
await new Promise((resolve) =>
|
|
144
|
+
await new Promise((resolve) =>
|
|
145
|
+
setTimeout(resolve, Number(started.data.intervalMs || 1500)),
|
|
146
|
+
);
|
|
75
147
|
const response = await fetch(profile.url + "/api/auth/cli/poll", {
|
|
76
148
|
method: "POST",
|
|
77
149
|
headers: { "content-type": "application/json" },
|
|
@@ -80,9 +152,19 @@ async function authLogin(profile, options) {
|
|
|
80
152
|
});
|
|
81
153
|
const payload = await response.json();
|
|
82
154
|
if (response.status === 202) continue;
|
|
83
|
-
if (!response.ok || !payload.ok)
|
|
155
|
+
if (!response.ok || !payload.ok)
|
|
156
|
+
throw new CliError(
|
|
157
|
+
payload?.error?.code || "AUTH_FAILED",
|
|
158
|
+
payload?.error?.message || "CLI 授权失败",
|
|
159
|
+
);
|
|
84
160
|
await saveToken(profile.name, payload.data.token);
|
|
85
|
-
return {
|
|
161
|
+
return {
|
|
162
|
+
profile: profile.name,
|
|
163
|
+
url: profile.url,
|
|
164
|
+
authenticated: true,
|
|
165
|
+
member: payload.data.member,
|
|
166
|
+
expiresAt: payload.data.expiresAt,
|
|
167
|
+
};
|
|
86
168
|
}
|
|
87
169
|
throw new CliError("AUTH_TIMEOUT", "等待钉钉授权超时,请重新运行 auth login");
|
|
88
170
|
}
|
|
@@ -90,7 +172,8 @@ async function authLogin(profile, options) {
|
|
|
90
172
|
export async function main(argv) {
|
|
91
173
|
const { options, positional } = parseArgs(argv);
|
|
92
174
|
const [group, command] = positional;
|
|
93
|
-
if (options.version || group === "version")
|
|
175
|
+
if (options.version || group === "version")
|
|
176
|
+
return output({ version: VERSION });
|
|
94
177
|
if (!group || group === "help" || options.help) return console.log(help);
|
|
95
178
|
const file = configPath();
|
|
96
179
|
const config = await readConfig(file);
|
|
@@ -98,30 +181,67 @@ export async function main(argv) {
|
|
|
98
181
|
if (group === "profile") {
|
|
99
182
|
if (command === "add") {
|
|
100
183
|
const name = positional[2];
|
|
101
|
-
if (!name || !/^[a-zA-Z0-9._-]+$/.test(name))
|
|
184
|
+
if (!name || !/^[a-zA-Z0-9._-]+$/.test(name))
|
|
185
|
+
throw new CliError("ARGUMENT", "请提供有效 Profile 名称");
|
|
102
186
|
const environment = required(options, "environment");
|
|
103
|
-
if (![
|
|
104
|
-
|
|
187
|
+
if (!["development", "production"].includes(environment))
|
|
188
|
+
throw new CliError(
|
|
189
|
+
"ARGUMENT",
|
|
190
|
+
"--environment 必须是 development 或 production",
|
|
191
|
+
);
|
|
192
|
+
const candidate = {
|
|
193
|
+
name,
|
|
194
|
+
url: normalizeUrl(required(options, "url")),
|
|
195
|
+
environment,
|
|
196
|
+
};
|
|
105
197
|
const target = await inspectTarget(candidate);
|
|
106
|
-
config.profiles[name] = {
|
|
198
|
+
config.profiles[name] = {
|
|
199
|
+
url: candidate.url,
|
|
200
|
+
environment,
|
|
201
|
+
instanceId: target.instanceId,
|
|
202
|
+
};
|
|
107
203
|
await writeConfig(config, file);
|
|
108
|
-
return output({
|
|
204
|
+
return output({
|
|
205
|
+
profile: name,
|
|
206
|
+
...config.profiles[name],
|
|
207
|
+
verified: true,
|
|
208
|
+
});
|
|
109
209
|
}
|
|
110
|
-
if (command === "list")
|
|
210
|
+
if (command === "list")
|
|
211
|
+
return output({
|
|
212
|
+
currentProfile: config.currentProfile || "nas",
|
|
213
|
+
profiles: config.profiles,
|
|
214
|
+
});
|
|
111
215
|
if (command === "current") {
|
|
112
216
|
const name = selectedProfileName(options, config);
|
|
113
|
-
return output({
|
|
217
|
+
return output({
|
|
218
|
+
selectedBy: options.profile
|
|
219
|
+
? "flag"
|
|
220
|
+
: process.env.PROCLI_PROFILE
|
|
221
|
+
? "environment"
|
|
222
|
+
: "default",
|
|
223
|
+
profile: name,
|
|
224
|
+
...(config.profiles[name] || { configured: false }),
|
|
225
|
+
});
|
|
114
226
|
}
|
|
115
227
|
if (command === "use") {
|
|
116
228
|
const name = positional[2];
|
|
117
|
-
if (!config.profiles[name])
|
|
229
|
+
if (!config.profiles[name])
|
|
230
|
+
throw new CliError(
|
|
231
|
+
"PROFILE_NOT_CONFIGURED",
|
|
232
|
+
`Profile “${name}”尚未配置`,
|
|
233
|
+
);
|
|
118
234
|
config.currentProfile = name;
|
|
119
235
|
await writeConfig(config, file);
|
|
120
236
|
return output({ currentProfile: name, ...config.profiles[name] });
|
|
121
237
|
}
|
|
122
238
|
if (command === "remove") {
|
|
123
239
|
const name = positional[2];
|
|
124
|
-
if (!config.profiles[name])
|
|
240
|
+
if (!config.profiles[name])
|
|
241
|
+
throw new CliError(
|
|
242
|
+
"PROFILE_NOT_CONFIGURED",
|
|
243
|
+
`Profile “${name}”尚未配置`,
|
|
244
|
+
);
|
|
125
245
|
delete config.profiles[name];
|
|
126
246
|
if (config.currentProfile === name) config.currentProfile = "nas";
|
|
127
247
|
await writeConfig(config, file);
|
|
@@ -132,66 +252,160 @@ export async function main(argv) {
|
|
|
132
252
|
}
|
|
133
253
|
|
|
134
254
|
if (group === "skill") {
|
|
135
|
-
if (command === "source")
|
|
255
|
+
if (command === "source")
|
|
256
|
+
return output({ skill: "project-management", source: skillSource });
|
|
136
257
|
const agent = options.agent || "";
|
|
137
|
-
if (command === "status")
|
|
138
|
-
|
|
139
|
-
if (command === "
|
|
258
|
+
if (command === "status")
|
|
259
|
+
return output(await skillStatus(agent, options["target-dir"]));
|
|
260
|
+
if (command === "install")
|
|
261
|
+
return output(
|
|
262
|
+
await installSkill({
|
|
263
|
+
agent,
|
|
264
|
+
customRoot: options["target-dir"],
|
|
265
|
+
mode: options.mode || "auto",
|
|
266
|
+
}),
|
|
267
|
+
);
|
|
268
|
+
if (command === "update")
|
|
269
|
+
return output(
|
|
270
|
+
await installSkill({
|
|
271
|
+
agent,
|
|
272
|
+
customRoot: options["target-dir"],
|
|
273
|
+
mode: options.mode || "auto",
|
|
274
|
+
update: true,
|
|
275
|
+
}),
|
|
276
|
+
);
|
|
140
277
|
throw new CliError("UNKNOWN_COMMAND", "未知 skill 命令");
|
|
141
278
|
}
|
|
142
279
|
|
|
143
280
|
const profile = resolveProfile(options, config);
|
|
144
281
|
if (group === "doctor") {
|
|
145
|
-
const checks = {
|
|
146
|
-
|
|
282
|
+
const checks = {
|
|
283
|
+
node: {
|
|
284
|
+
ok: Number(process.versions.node.split(".")[0]) >= 20,
|
|
285
|
+
version: process.versions.node,
|
|
286
|
+
},
|
|
287
|
+
config: { ok: true, path: file },
|
|
288
|
+
profile: { ok: true, ...profile },
|
|
289
|
+
};
|
|
290
|
+
try {
|
|
291
|
+
checks.server = { ok: true, ...(await inspectTarget(profile)) };
|
|
292
|
+
} catch (error) {
|
|
293
|
+
checks.server = { ok: false, code: error.code, message: error.message };
|
|
294
|
+
}
|
|
147
295
|
const token = await readToken(profile.name, file);
|
|
148
296
|
if (!token) checks.auth = { ok: false, code: "AUTH_REQUIRED" };
|
|
149
297
|
else {
|
|
150
|
-
try {
|
|
151
|
-
|
|
298
|
+
try {
|
|
299
|
+
checks.auth = {
|
|
300
|
+
ok: true,
|
|
301
|
+
...(await request(profile, "/api/auth/cli/status", { token })).data,
|
|
302
|
+
};
|
|
303
|
+
} catch (error) {
|
|
304
|
+
checks.auth = { ok: false, code: error.code, message: error.message };
|
|
305
|
+
}
|
|
152
306
|
}
|
|
153
307
|
const ok = Object.values(checks).every((item) => item.ok);
|
|
154
308
|
return output({ checks }, ok);
|
|
155
309
|
}
|
|
156
310
|
if (group === "capabilities") {
|
|
157
|
-
const manifest = JSON.parse(
|
|
311
|
+
const manifest = JSON.parse(
|
|
312
|
+
await fs.readFile(
|
|
313
|
+
new URL("../capabilities.json", import.meta.url),
|
|
314
|
+
"utf8",
|
|
315
|
+
),
|
|
316
|
+
);
|
|
158
317
|
let server;
|
|
159
|
-
try {
|
|
160
|
-
|
|
318
|
+
try {
|
|
319
|
+
server = (await request(profile, "/api/capabilities", { token: "" }))
|
|
320
|
+
.data;
|
|
321
|
+
} catch (error) {
|
|
322
|
+
server = { available: false, code: error.code };
|
|
323
|
+
}
|
|
324
|
+
return output({
|
|
325
|
+
cli: manifest,
|
|
326
|
+
server,
|
|
327
|
+
target: await inspectTarget(profile),
|
|
328
|
+
});
|
|
161
329
|
}
|
|
162
330
|
if (group === "auth") {
|
|
163
331
|
if (command === "login") return output(await authLogin(profile, options));
|
|
164
332
|
if (command === "status") {
|
|
165
333
|
const token = await readToken(profile.name, file);
|
|
166
|
-
if (!token)
|
|
167
|
-
|
|
334
|
+
if (!token)
|
|
335
|
+
throw new CliError(
|
|
336
|
+
"AUTH_REQUIRED",
|
|
337
|
+
`Profile “${profile.name}”尚未登录`,
|
|
338
|
+
);
|
|
339
|
+
return output({
|
|
340
|
+
profile: profile.name,
|
|
341
|
+
url: profile.url,
|
|
342
|
+
...(await request(profile, "/api/auth/cli/status", { token })).data,
|
|
343
|
+
});
|
|
168
344
|
}
|
|
169
345
|
if (command === "logout") {
|
|
170
346
|
const token = await readToken(profile.name, file);
|
|
171
|
-
if (token)
|
|
347
|
+
if (token)
|
|
348
|
+
await request(profile, "/api/auth/cli/revoke", {
|
|
349
|
+
method: "POST",
|
|
350
|
+
token,
|
|
351
|
+
}).catch(() => {});
|
|
172
352
|
const removed = await deleteToken(profile.name, file);
|
|
173
|
-
return output({
|
|
353
|
+
return output({
|
|
354
|
+
profile: profile.name,
|
|
355
|
+
loggedOut: true,
|
|
356
|
+
localCredentialRemoved: removed,
|
|
357
|
+
});
|
|
174
358
|
}
|
|
175
359
|
throw new CliError("UNKNOWN_COMMAND", "未知 auth 命令");
|
|
176
360
|
}
|
|
177
361
|
if (group === "project" && command === "create") {
|
|
178
362
|
const target = await inspectTarget(profile);
|
|
179
|
-
const
|
|
180
|
-
if (
|
|
363
|
+
const type = options.type || "product";
|
|
364
|
+
if (type !== "product")
|
|
365
|
+
throw new CliError(
|
|
366
|
+
"FEATURE_UNSUPPORTED",
|
|
367
|
+
`${type === "shop" ? "店铺" : type === "company" ? "企业" : "该类型"}项目尚未开放`,
|
|
368
|
+
);
|
|
369
|
+
const input = {
|
|
370
|
+
name: required(options, "name"),
|
|
371
|
+
goal: options.goal || "",
|
|
372
|
+
type,
|
|
373
|
+
template: options.template || "product-sop-v1",
|
|
374
|
+
mode: options.mode || "agent",
|
|
375
|
+
};
|
|
376
|
+
if (options["sop-file"]) input.sop = await readSopFile(options["sop-file"]);
|
|
377
|
+
if (!["product-sop-v1", "ecommerce-v1"].includes(input.template))
|
|
378
|
+
throw new CliError("ARGUMENT", "当前只支持商品项目 SOP 模板");
|
|
181
379
|
const idempotencyKey = options["idempotency-key"] || crypto.randomUUID();
|
|
182
|
-
if (options["dry-run"])
|
|
380
|
+
if (options["dry-run"])
|
|
381
|
+
return output({ dryRun: true, target, input, idempotencyKey });
|
|
183
382
|
if (target.environment === "production" && !options.yes)
|
|
184
|
-
throw new CliError(
|
|
185
|
-
|
|
186
|
-
|
|
383
|
+
throw new CliError(
|
|
384
|
+
"CONFIRMATION_REQUIRED",
|
|
385
|
+
"目标是生产环境;确认创建后请增加 --yes",
|
|
386
|
+
{ target, input },
|
|
387
|
+
);
|
|
388
|
+
const result = await request(profile, "/api/v1/projects", {
|
|
389
|
+
method: "POST",
|
|
390
|
+
body: input,
|
|
391
|
+
idempotencyKey,
|
|
392
|
+
});
|
|
393
|
+
const readback = await request(
|
|
394
|
+
profile,
|
|
395
|
+
`/api/v1/projects/${encodeURIComponent(result.data.project.id)}`,
|
|
396
|
+
);
|
|
187
397
|
if (
|
|
188
398
|
readback.data.project.id !== result.data.project.id ||
|
|
189
399
|
readback.data.tasks.count !== result.data.tasks.count ||
|
|
190
400
|
readback.data.wiki.count !== result.data.wiki.count
|
|
191
401
|
)
|
|
192
|
-
throw new CliError(
|
|
193
|
-
|
|
194
|
-
|
|
402
|
+
throw new CliError(
|
|
403
|
+
"OUTPUT_CONTRACT_FAILED",
|
|
404
|
+
"项目写入后的回读结果与创建结果不一致",
|
|
405
|
+
{
|
|
406
|
+
projectId: result.data.project.id,
|
|
407
|
+
},
|
|
408
|
+
);
|
|
195
409
|
return output({
|
|
196
410
|
target,
|
|
197
411
|
idempotencyKey,
|
|
@@ -200,5 +414,61 @@ export async function main(argv) {
|
|
|
200
414
|
verification: "write-after-readback",
|
|
201
415
|
});
|
|
202
416
|
}
|
|
417
|
+
if (group === "project" && command === "delete") {
|
|
418
|
+
const target = await inspectTarget(profile);
|
|
419
|
+
const name = required(options, "name");
|
|
420
|
+
const confirmedName = required(options, "confirm-name");
|
|
421
|
+
if (name !== confirmedName)
|
|
422
|
+
throw new CliError(
|
|
423
|
+
"DELETE_CONFIRMATION_MISMATCH",
|
|
424
|
+
"--confirm-name 必须与 --name 完全一致",
|
|
425
|
+
);
|
|
426
|
+
const idempotencyKey = options["idempotency-key"] || crypto.randomUUID();
|
|
427
|
+
if (options["dry-run"]) {
|
|
428
|
+
const found = await request(
|
|
429
|
+
profile,
|
|
430
|
+
`/api/v1/projects/resolve/by-name?name=${encodeURIComponent(name)}`,
|
|
431
|
+
);
|
|
432
|
+
return output({
|
|
433
|
+
dryRun: true,
|
|
434
|
+
target,
|
|
435
|
+
preview: {
|
|
436
|
+
project: { id: found.data.project.id, name: found.data.project.name },
|
|
437
|
+
taskCount: found.data.taskCount,
|
|
438
|
+
wikiCount: found.data.wikiCount,
|
|
439
|
+
recoverable: true,
|
|
440
|
+
},
|
|
441
|
+
idempotencyKey,
|
|
442
|
+
requiresAdmin: true,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
if (!options.yes)
|
|
446
|
+
throw new CliError(
|
|
447
|
+
"CONFIRMATION_REQUIRED",
|
|
448
|
+
"删除项目必须增加 --yes,并提供完全一致的 --confirm-name",
|
|
449
|
+
{ target, projectName: name },
|
|
450
|
+
);
|
|
451
|
+
const result = await request(profile, "/api/v1/projects/by-name", {
|
|
452
|
+
method: "DELETE",
|
|
453
|
+
body: { expectedName: name },
|
|
454
|
+
idempotencyKey,
|
|
455
|
+
});
|
|
456
|
+
const readback = await request(
|
|
457
|
+
profile,
|
|
458
|
+
`/api/v1/deleted-projects/${encodeURIComponent(result.data.projectId)}`,
|
|
459
|
+
);
|
|
460
|
+
if (!readback.data.project.deletedAt || readback.data.project.name !== name)
|
|
461
|
+
throw new CliError(
|
|
462
|
+
"OUTPUT_CONTRACT_FAILED",
|
|
463
|
+
"删除后的回读结果与请求不一致",
|
|
464
|
+
{ projectId: result.data.projectId },
|
|
465
|
+
);
|
|
466
|
+
return output({
|
|
467
|
+
target,
|
|
468
|
+
idempotencyKey,
|
|
469
|
+
...result.data,
|
|
470
|
+
verification: "soft-delete-readback",
|
|
471
|
+
});
|
|
472
|
+
}
|
|
203
473
|
throw new CliError("UNKNOWN_COMMAND", "未知命令,运行 procli help 查看用法");
|
|
204
474
|
}
|