@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 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.1.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": ["goal", "template", "mode", "profile", "idempotencyKey"],
14
- "supports": ["dryRun", "idempotency", "targetVerification", "postWriteReadback"],
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@petercjl/procli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Agent-first CLI and portable Skill for SealSeek project management",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,54 +1,76 @@
1
1
  ---
2
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.
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. Do not call the service API directly or infer success from prose.
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
- ## Input Strategy Output
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
- - 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.
12
+ ## Target routing
15
13
 
16
- ## Main line: create a project
14
+ The default Profile is `nas`. Respect this precedence exactly:
17
15
 
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:
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
- 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.
39
+ Omit `--goal` or `--sop-file` when unused.
30
40
 
31
- ## Profile routing
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
- The default Profile is `nas`. Respect the CLI precedence exactly:
44
+ ## Delete a project
34
45
 
35
- ```text
36
- --profile > PROCLI_PROFILE > saved current Profile > nas
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
- 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.
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`: 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.
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
- ## Capability contract
72
+ ## Output contract
51
73
 
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.
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": ["Network access to the selected service", "Permission to create a project"],
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": ["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."]
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": ["Network access to the selected service", "Permission to create a project"],
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": ["profile", "name", "goal", "template", "mode", "idempotency_key"],
21
- "output_fields": ["target", "project", "tasks", "wiki", "idempotency_key"],
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 { CliError, configPath, deleteToken, normalizeUrl, readConfig, readToken, resolveProfile, saveToken, selectedProfileName, writeConfig } from "./config.mjs";
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.1.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] [--template ecommerce-v1]
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 (["json", "yes", "dry-run", "no-open", "help", "version"].includes(key)) options[key] = true;
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("--")) throw new CliError("ARGUMENT", `缺少 --${key} 的值`);
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()) throw new CliError("ARGUMENT", `缺少 --${key}`);
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 = 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" });
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.createHash("sha256").update(verifier).digest("hex");
68
- const started = await request(profile, "/api/auth/cli/start", { method: "POST", body: { verifierHash, clientName: `procli/${VERSION}` }, token: "" });
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(`请在浏览器完成钉钉授权:${started.data.verificationUrl}\n`);
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) => setTimeout(resolve, Number(started.data.intervalMs || 1500)));
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) throw new CliError(payload?.error?.code || "AUTH_FAILED", payload?.error?.message || "CLI 授权失败");
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 { profile: profile.name, url: profile.url, authenticated: true, member: payload.data.member, expiresAt: payload.data.expiresAt };
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") return output({ version: 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)) throw new CliError("ARGUMENT", "请提供有效 Profile 名称");
184
+ if (!name || !/^[a-zA-Z0-9._-]+$/.test(name))
185
+ throw new CliError("ARGUMENT", "请提供有效 Profile 名称");
102
186
  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 };
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] = { url: candidate.url, environment, instanceId: target.instanceId };
198
+ config.profiles[name] = {
199
+ url: candidate.url,
200
+ environment,
201
+ instanceId: target.instanceId,
202
+ };
107
203
  await writeConfig(config, file);
108
- return output({ profile: name, ...config.profiles[name], verified: true });
204
+ return output({
205
+ profile: name,
206
+ ...config.profiles[name],
207
+ verified: true,
208
+ });
109
209
  }
110
- if (command === "list") return output({ currentProfile: config.currentProfile || "nas", profiles: config.profiles });
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({ selectedBy: options.profile ? "flag" : process.env.PROCLI_PROFILE ? "environment" : "default", profile: name, ...(config.profiles[name] || { configured: false }) });
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]) throw new CliError("PROFILE_NOT_CONFIGURED", `Profile “${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]) throw new CliError("PROFILE_NOT_CONFIGURED", `Profile “${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") return output({ skill: "project-management", source: skillSource });
255
+ if (command === "source")
256
+ return output({ skill: "project-management", source: skillSource });
136
257
  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 }));
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 = { 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 }; }
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 { 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 }; }
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(await fs.readFile(new URL("../capabilities.json", import.meta.url), "utf8"));
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 { 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) });
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) 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 });
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) await request(profile, "/api/auth/cli/revoke", { method: "POST", token }).catch(() => {});
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({ profile: profile.name, loggedOut: true, localCredentialRemoved: removed });
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 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");
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"]) return output({ dryRun: true, target, input, idempotencyKey });
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("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)}`);
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("OUTPUT_CONTRACT_FAILED", "项目写入后的回读结果与创建结果不一致", {
193
- projectId: result.data.project.id,
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
  }