pi-crew 0.6.1 → 0.6.4
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/CHANGELOG.md +194 -0
- package/README.md +81 -33
- package/docs/issue-29-analysis.md +189 -0
- package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +1268 -0
- package/docs/ui-optimization-plan.md +447 -0
- package/package.json +2 -2
- package/src/config/config.ts +106 -15
- package/src/errors.ts +107 -0
- package/src/extension/async-notifier.ts +6 -2
- package/src/extension/crew-cleanup.ts +8 -5
- package/src/extension/management.ts +464 -109
- package/src/extension/register.ts +213 -35
- package/src/extension/registration/brief-tool-overrides.ts +400 -0
- package/src/extension/registration/commands.ts +27 -2
- package/src/extension/registration/subagent-helpers.ts +2 -2
- package/src/extension/registration/subagent-tools.ts +9 -4
- package/src/extension/registration/team-tool.ts +17 -10
- package/src/extension/registration/viewers.ts +2 -2
- package/src/extension/team-tool/api.ts +3 -3
- package/src/extension/team-tool/cancel.ts +3 -3
- package/src/extension/team-tool/explain.ts +1 -1
- package/src/extension/team-tool/handle-schedule.ts +40 -0
- package/src/extension/team-tool/inspect.ts +3 -3
- package/src/extension/team-tool/lifecycle-actions.ts +4 -4
- package/src/extension/team-tool/respond.ts +2 -2
- package/src/extension/team-tool/run.ts +64 -14
- package/src/extension/team-tool/status.ts +1 -1
- package/src/extension/team-tool-types.ts +2 -0
- package/src/extension/team-tool.ts +173 -46
- package/src/hooks/registry.ts +77 -13
- package/src/hooks/types.ts +9 -0
- package/src/plugins/plugin-define.ts +6 -0
- package/src/plugins/plugin-registry.ts +32 -0
- package/src/plugins/plugins/index.ts +3 -0
- package/src/plugins/plugins/nextjs.ts +19 -0
- package/src/plugins/plugins/vite.ts +14 -0
- package/src/plugins/plugins/vitest.ts +14 -0
- package/src/runtime/async-runner.ts +35 -7
- package/src/runtime/background-runner.ts +529 -144
- package/src/runtime/chain-parser.ts +5 -1
- package/src/runtime/chain-runner.ts +9 -3
- package/src/runtime/checkpoint.ts +262 -180
- package/src/runtime/child-pi.ts +164 -37
- package/src/runtime/crash-recovery.ts +25 -14
- package/src/runtime/crew-agent-records.ts +2 -0
- package/src/runtime/diagnostic-export.ts +1 -1
- package/src/runtime/dynamic-script-runner.ts +10 -7
- package/src/runtime/foreground-watchdog.ts +1 -1
- package/src/runtime/heartbeat-watcher.ts +19 -2
- package/src/runtime/intercom-bridge.ts +7 -0
- package/src/runtime/iteration-hooks.ts +1 -1
- package/src/runtime/manifest-cache.ts +4 -0
- package/src/runtime/orphan-worker-registry.ts +444 -0
- package/src/runtime/parent-guard.ts +70 -16
- package/src/runtime/pi-args.ts +437 -14
- package/src/runtime/pi-spawn.ts +1 -0
- package/src/runtime/post-checks.ts +1 -1
- package/src/runtime/retry-runner.ts +9 -3
- package/src/runtime/run-tracker.ts +38 -10
- package/src/runtime/sandbox.ts +17 -2
- package/src/runtime/scheduler.ts +25 -2
- package/src/runtime/skill-effectiveness.ts +105 -62
- package/src/runtime/skill-instructions.ts +110 -32
- package/src/runtime/stale-reconciler.ts +310 -69
- package/src/runtime/subagent-manager.ts +251 -57
- package/src/runtime/task-health.ts +76 -0
- package/src/runtime/task-id.ts +20 -0
- package/src/runtime/task-runner/live-executor.ts +1 -1
- package/src/runtime/task-runner/progress.ts +1 -1
- package/src/runtime/task-runner/state-helpers.ts +110 -6
- package/src/runtime/task-runner.ts +92 -70
- package/src/runtime/team-runner.ts +186 -20
- package/src/schema/team-tool-schema.ts +27 -9
- package/src/skills/discover-skills.ts +9 -3
- package/src/state/active-run-registry.ts +170 -38
- package/src/state/artifact-store.ts +25 -13
- package/src/state/atomic-write-v2.ts +86 -0
- package/src/state/atomic-write.ts +346 -55
- package/src/state/blob-store.ts +178 -10
- package/src/state/crew-init.ts +161 -28
- package/src/state/decision-ledger.ts +172 -111
- package/src/state/event-log-rotation.ts +82 -52
- package/src/state/event-log.ts +254 -70
- package/src/state/health-store.ts +71 -0
- package/src/state/locks.ts +102 -20
- package/src/state/mailbox.ts +45 -7
- package/src/state/observation-store.ts +4 -1
- package/src/state/run-graph.ts +141 -130
- package/src/state/run-metrics.ts +24 -8
- package/src/state/session-state-map.ts +51 -0
- package/src/state/state-store.ts +330 -43
- package/src/ui/live-run-sidebar.ts +1 -1
- package/src/ui/loaders.ts +4 -0
- package/src/ui/overlays/agent-picker-overlay.ts +1 -1
- package/src/ui/overlays/mailbox-detail-overlay.ts +1 -1
- package/src/ui/powerbar-publisher.ts +1 -1
- package/src/ui/run-action-dispatcher.ts +2 -2
- package/src/ui/run-snapshot-cache.ts +1 -1
- package/src/ui/status-colors.ts +5 -1
- package/src/ui/theme-adapter.ts +80 -1
- package/src/ui/tool-progress-formatter.ts +9 -5
- package/src/ui/tool-render.ts +4 -0
- package/src/ui/tool-renderers/brief-mode.ts +207 -0
- package/src/ui/tool-renderers/index.ts +627 -0
- package/src/ui/widget/index.ts +224 -0
- package/src/ui/widget/widget-formatters.ts +148 -0
- package/src/ui/widget/widget-model.ts +90 -0
- package/src/ui/widget/widget-renderer.ts +130 -0
- package/src/ui/widget/widget-types.ts +37 -0
- package/src/utils/env-filter.ts +66 -0
- package/src/utils/file-coalescer.ts +9 -1
- package/src/utils/guards.ts +110 -0
- package/src/utils/paths.ts +80 -5
- package/src/utils/redaction.ts +6 -1
- package/src/utils/safe-paths.ts +281 -10
- package/src/worktree/cleanup.ts +112 -24
- package/src/worktree/worktree-manager.ts +128 -15
- package/test-bugs-all.mjs +10 -6
- package/test-integration-check.ts +4 -0
|
@@ -1,22 +1,37 @@
|
|
|
1
1
|
import * as crypto from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
AgentConfig,
|
|
6
|
+
ResourceSource,
|
|
7
|
+
RoutingMetadata,
|
|
8
|
+
} from "../agents/agent-config.ts";
|
|
5
9
|
import { serializeAgent } from "../agents/agent-serializer.ts";
|
|
6
10
|
import { discoverAgents } from "../agents/discover-agents.ts";
|
|
7
|
-
import type { TeamToolDetails } from "./team-tool-types.ts";
|
|
8
|
-
import { toolResult, type PiTeamsToolResult } from "./tool-result.ts";
|
|
9
|
-
import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
|
|
10
11
|
import type { PiTeamsConfig } from "../config/config.ts";
|
|
11
|
-
import {
|
|
12
|
+
import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
|
|
13
|
+
import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
|
|
12
14
|
import type { TeamConfig, TeamRole } from "../teams/team-config.ts";
|
|
13
15
|
import { serializeTeam } from "../teams/team-serializer.ts";
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
import {
|
|
17
|
+
hasOwn,
|
|
18
|
+
parseConfigObject,
|
|
19
|
+
requireString,
|
|
20
|
+
sanitizeName,
|
|
21
|
+
} from "../utils/names.ts";
|
|
18
22
|
import { projectCrewRoot, userPiRoot } from "../utils/paths.ts";
|
|
19
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
allWorkflows,
|
|
25
|
+
discoverWorkflows,
|
|
26
|
+
} from "../workflows/discover-workflows.ts";
|
|
27
|
+
import type {
|
|
28
|
+
WorkflowConfig,
|
|
29
|
+
WorkflowStep,
|
|
30
|
+
} from "../workflows/workflow-config.ts";
|
|
31
|
+
import { serializeWorkflow } from "../workflows/workflow-serializer.ts";
|
|
32
|
+
import { enforceDestructiveIntent } from "./team-tool/intent-policy.ts";
|
|
33
|
+
import type { TeamToolDetails } from "./team-tool-types.ts";
|
|
34
|
+
import { type PiTeamsToolResult, toolResult } from "./tool-result.ts";
|
|
20
35
|
|
|
21
36
|
interface ManagementContext {
|
|
22
37
|
cwd: string;
|
|
@@ -27,11 +42,19 @@ type MutableSource = "user" | "project";
|
|
|
27
42
|
|
|
28
43
|
type MutableResource = AgentConfig | TeamConfig | WorkflowConfig;
|
|
29
44
|
|
|
30
|
-
function result(
|
|
45
|
+
function result(
|
|
46
|
+
text: string,
|
|
47
|
+
status: TeamToolDetails["status"] = "ok",
|
|
48
|
+
isError = false,
|
|
49
|
+
): PiTeamsToolResult {
|
|
31
50
|
return toolResult(text, { action: "management", status }, isError);
|
|
32
51
|
}
|
|
33
52
|
|
|
34
|
-
function scopeDir(
|
|
53
|
+
function scopeDir(
|
|
54
|
+
ctx: ManagementContext,
|
|
55
|
+
resource: "agent" | "team" | "workflow",
|
|
56
|
+
scope: MutableSource,
|
|
57
|
+
): string {
|
|
35
58
|
const base = scope === "user" ? userPiRoot() : projectCrewRoot(ctx.cwd);
|
|
36
59
|
if (resource === "agent") return path.join(base, "agents");
|
|
37
60
|
if (resource === "team") return path.join(base, "teams");
|
|
@@ -54,33 +77,77 @@ function backupFile(filePath: string): string {
|
|
|
54
77
|
return backupPath;
|
|
55
78
|
}
|
|
56
79
|
|
|
57
|
-
function targetPath(
|
|
58
|
-
|
|
80
|
+
function targetPath(
|
|
81
|
+
ctx: ManagementContext,
|
|
82
|
+
resource: "agent" | "team" | "workflow",
|
|
83
|
+
scope: MutableSource,
|
|
84
|
+
name: string,
|
|
85
|
+
): string {
|
|
86
|
+
return path.join(
|
|
87
|
+
scopeDir(ctx, resource, scope),
|
|
88
|
+
`${name}${extensionFor(resource)}`,
|
|
89
|
+
);
|
|
59
90
|
}
|
|
60
91
|
|
|
61
92
|
function parseStringArray(value: unknown): string[] | undefined {
|
|
62
|
-
if (typeof value === "string")
|
|
63
|
-
|
|
93
|
+
if (typeof value === "string")
|
|
94
|
+
return value
|
|
95
|
+
.split(",")
|
|
96
|
+
.map((entry) => entry.trim())
|
|
97
|
+
.filter(Boolean);
|
|
98
|
+
if (Array.isArray(value))
|
|
99
|
+
return value
|
|
100
|
+
.filter(
|
|
101
|
+
(entry): entry is string =>
|
|
102
|
+
typeof entry === "string" && entry.trim().length > 0,
|
|
103
|
+
)
|
|
104
|
+
.map((entry) => entry.trim());
|
|
64
105
|
return undefined;
|
|
65
106
|
}
|
|
66
107
|
|
|
67
|
-
function parseRouting(
|
|
108
|
+
function parseRouting(
|
|
109
|
+
value: Record<string, unknown>,
|
|
110
|
+
fallback?: RoutingMetadata,
|
|
111
|
+
): RoutingMetadata | undefined {
|
|
68
112
|
const routing = {
|
|
69
|
-
triggers: hasOwn(value, "triggers")
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
113
|
+
triggers: hasOwn(value, "triggers")
|
|
114
|
+
? parseStringArray(value.triggers)
|
|
115
|
+
: fallback?.triggers,
|
|
116
|
+
useWhen: hasOwn(value, "useWhen")
|
|
117
|
+
? parseStringArray(value.useWhen)
|
|
118
|
+
: fallback?.useWhen,
|
|
119
|
+
avoidWhen: hasOwn(value, "avoidWhen")
|
|
120
|
+
? parseStringArray(value.avoidWhen)
|
|
121
|
+
: fallback?.avoidWhen,
|
|
122
|
+
cost:
|
|
123
|
+
value.cost === "free" ||
|
|
124
|
+
value.cost === "cheap" ||
|
|
125
|
+
value.cost === "expensive"
|
|
126
|
+
? value.cost
|
|
127
|
+
: fallback?.cost,
|
|
128
|
+
category: hasOwn(value, "category")
|
|
129
|
+
? typeof value.category === "string" && value.category.trim()
|
|
130
|
+
? value.category.trim()
|
|
131
|
+
: undefined
|
|
132
|
+
: fallback?.category,
|
|
74
133
|
};
|
|
75
|
-
return routing.triggers ||
|
|
134
|
+
return routing.triggers ||
|
|
135
|
+
routing.useWhen ||
|
|
136
|
+
routing.avoidWhen ||
|
|
137
|
+
routing.cost ||
|
|
138
|
+
routing.category
|
|
139
|
+
? routing
|
|
140
|
+
: undefined;
|
|
76
141
|
}
|
|
77
142
|
|
|
78
143
|
function parseRoles(value: unknown): { roles?: TeamRole[]; error?: string } {
|
|
79
|
-
if (!Array.isArray(value) || value.length === 0)
|
|
144
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
145
|
+
return { error: "config.roles must be a non-empty array." };
|
|
80
146
|
const roles: TeamRole[] = [];
|
|
81
147
|
for (let i = 0; i < value.length; i++) {
|
|
82
148
|
const item = value[i];
|
|
83
|
-
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
149
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
150
|
+
return { error: `config.roles[${i}] must be an object.` };
|
|
84
151
|
const obj = item as Record<string, unknown>;
|
|
85
152
|
const name = requireString(obj.name, `config.roles[${i}].name`);
|
|
86
153
|
if (name.error) return { error: name.error };
|
|
@@ -89,20 +156,33 @@ function parseRoles(value: unknown): { roles?: TeamRole[]; error?: string } {
|
|
|
89
156
|
roles.push({
|
|
90
157
|
name: sanitizeName(name.value!),
|
|
91
158
|
agent: sanitizeName(agent.value!),
|
|
92
|
-
description:
|
|
159
|
+
description:
|
|
160
|
+
typeof obj.description === "string"
|
|
161
|
+
? obj.description.trim()
|
|
162
|
+
: undefined,
|
|
93
163
|
model: typeof obj.model === "string" ? obj.model.trim() : undefined,
|
|
94
|
-
maxConcurrency:
|
|
164
|
+
maxConcurrency:
|
|
165
|
+
typeof obj.maxConcurrency === "number" &&
|
|
166
|
+
Number.isInteger(obj.maxConcurrency) &&
|
|
167
|
+
obj.maxConcurrency > 0
|
|
168
|
+
? obj.maxConcurrency
|
|
169
|
+
: undefined,
|
|
95
170
|
});
|
|
96
171
|
}
|
|
97
172
|
return { roles };
|
|
98
173
|
}
|
|
99
174
|
|
|
100
|
-
function parseSteps(value: unknown): {
|
|
101
|
-
|
|
175
|
+
function parseSteps(value: unknown): {
|
|
176
|
+
steps?: WorkflowStep[];
|
|
177
|
+
error?: string;
|
|
178
|
+
} {
|
|
179
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
180
|
+
return { error: "config.steps must be a non-empty array." };
|
|
102
181
|
const steps: WorkflowStep[] = [];
|
|
103
182
|
for (let i = 0; i < value.length; i++) {
|
|
104
183
|
const item = value[i];
|
|
105
|
-
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
184
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
185
|
+
return { error: `config.steps[${i}] must be an object.` };
|
|
106
186
|
const obj = item as Record<string, unknown>;
|
|
107
187
|
const id = requireString(obj.id, `config.steps[${i}].id`);
|
|
108
188
|
if (id.error) return { error: id.error };
|
|
@@ -112,21 +192,36 @@ function parseSteps(value: unknown): { steps?: WorkflowStep[]; error?: string }
|
|
|
112
192
|
// Strip characters that could create headings, code blocks, or links.
|
|
113
193
|
// Defense-in-depth — the task field is stored in YAML frontmatter, not executed.
|
|
114
194
|
const rawTask = obj.task;
|
|
115
|
-
const task =
|
|
116
|
-
|
|
117
|
-
|
|
195
|
+
const task =
|
|
196
|
+
typeof rawTask === "string"
|
|
197
|
+
? rawTask
|
|
198
|
+
.replace(/^#{1,6}\s+/gm, "")
|
|
199
|
+
.replace(/```/g, "``")
|
|
200
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
201
|
+
.slice(0, 4000)
|
|
202
|
+
: "{goal}";
|
|
118
203
|
steps.push({
|
|
119
204
|
id: sanitizeName(id.value!),
|
|
120
205
|
role: sanitizeName(role.value!),
|
|
121
206
|
task,
|
|
122
207
|
dependsOn: parseStringArray(obj.dependsOn),
|
|
123
|
-
parallelGroup:
|
|
124
|
-
|
|
208
|
+
parallelGroup:
|
|
209
|
+
typeof obj.parallelGroup === "string"
|
|
210
|
+
? obj.parallelGroup.trim()
|
|
211
|
+
: undefined,
|
|
212
|
+
output:
|
|
213
|
+
obj.output === false
|
|
214
|
+
? false
|
|
215
|
+
: typeof obj.output === "string"
|
|
216
|
+
? obj.output.trim()
|
|
217
|
+
: undefined,
|
|
125
218
|
reads: obj.reads === false ? false : parseStringArray(obj.reads),
|
|
126
219
|
model: typeof obj.model === "string" ? obj.model.trim() : undefined,
|
|
127
220
|
skills: obj.skills === false ? false : parseStringArray(obj.skills),
|
|
128
|
-
progress:
|
|
129
|
-
|
|
221
|
+
progress:
|
|
222
|
+
typeof obj.progress === "boolean" ? obj.progress : undefined,
|
|
223
|
+
worktree:
|
|
224
|
+
typeof obj.worktree === "boolean" ? obj.worktree : undefined,
|
|
130
225
|
verify: typeof obj.verify === "boolean" ? obj.verify : undefined,
|
|
131
226
|
});
|
|
132
227
|
}
|
|
@@ -134,45 +229,75 @@ function parseSteps(value: unknown): { steps?: WorkflowStep[]; error?: string }
|
|
|
134
229
|
}
|
|
135
230
|
|
|
136
231
|
function parseWorkflowMaxConcurrency(value: unknown): number | undefined {
|
|
137
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value < 1)
|
|
232
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1)
|
|
233
|
+
return undefined;
|
|
138
234
|
return value;
|
|
139
235
|
}
|
|
140
236
|
|
|
141
|
-
function findResource(
|
|
237
|
+
function findResource(
|
|
238
|
+
ctx: ManagementContext,
|
|
239
|
+
resource: "agent" | "team" | "workflow",
|
|
240
|
+
name: string,
|
|
241
|
+
scope?: string,
|
|
242
|
+
): MutableResource[] {
|
|
142
243
|
const normalized = sanitizeName(name);
|
|
143
|
-
const sourceMatches = (item: { name: string; source: ResourceSource }) =>
|
|
244
|
+
const sourceMatches = (item: { name: string; source: ResourceSource }) =>
|
|
245
|
+
(scope === "user" || scope === "project"
|
|
246
|
+
? item.source === scope
|
|
247
|
+
: item.source !== "builtin") && item.name === normalized;
|
|
144
248
|
// Search in the correct scope array directly to avoid allAgents shadowing issue.
|
|
145
249
|
if (resource === "agent") {
|
|
146
250
|
const discovery = discoverAgents(ctx.cwd);
|
|
147
|
-
const pool =
|
|
251
|
+
const pool =
|
|
252
|
+
scope === "user"
|
|
253
|
+
? discovery.user
|
|
254
|
+
: scope === "project"
|
|
255
|
+
? discovery.project
|
|
256
|
+
: [...discovery.builtin, ...discovery.user];
|
|
148
257
|
return pool.filter(sourceMatches);
|
|
149
258
|
}
|
|
150
259
|
if (resource === "team") {
|
|
151
260
|
const discovery = discoverTeams(ctx.cwd);
|
|
152
|
-
const pool =
|
|
261
|
+
const pool =
|
|
262
|
+
scope === "user"
|
|
263
|
+
? discovery.user
|
|
264
|
+
: scope === "project"
|
|
265
|
+
? discovery.project
|
|
266
|
+
: [...discovery.builtin, ...discovery.user];
|
|
153
267
|
return pool.filter(sourceMatches);
|
|
154
268
|
}
|
|
155
269
|
{
|
|
156
270
|
const discovery = discoverWorkflows(ctx.cwd);
|
|
157
|
-
const pool =
|
|
271
|
+
const pool =
|
|
272
|
+
scope === "user"
|
|
273
|
+
? discovery.user
|
|
274
|
+
: scope === "project"
|
|
275
|
+
? discovery.project
|
|
276
|
+
: [...discovery.builtin, ...discovery.user];
|
|
158
277
|
return pool.filter(sourceMatches);
|
|
159
278
|
}
|
|
160
279
|
}
|
|
161
280
|
|
|
162
281
|
// Note: only checks agent→team references and defaultWorkflow. Does not detect
|
|
163
282
|
// workflow-step→agent/team references or team name in workflow metadata.
|
|
164
|
-
function findReferences(
|
|
283
|
+
function findReferences(
|
|
284
|
+
ctx: ManagementContext,
|
|
285
|
+
resource: "agent" | "team" | "workflow",
|
|
286
|
+
name: string,
|
|
287
|
+
): string[] {
|
|
165
288
|
const refs: string[] = [];
|
|
166
289
|
if (resource === "agent") {
|
|
167
290
|
for (const team of allTeams(discoverTeams(ctx.cwd))) {
|
|
168
291
|
for (const role of team.roles) {
|
|
169
|
-
if (role.agent === name)
|
|
292
|
+
if (role.agent === name)
|
|
293
|
+
refs.push(`team '${team.name}' role '${role.name}'`);
|
|
170
294
|
}
|
|
171
295
|
}
|
|
172
296
|
}
|
|
173
297
|
if (resource === "workflow") {
|
|
174
298
|
for (const team of allTeams(discoverTeams(ctx.cwd))) {
|
|
175
|
-
if (team.defaultWorkflow === name)
|
|
299
|
+
if (team.defaultWorkflow === name)
|
|
300
|
+
refs.push(`team '${team.name}' defaultWorkflow`);
|
|
176
301
|
}
|
|
177
302
|
}
|
|
178
303
|
return refs;
|
|
@@ -196,16 +321,29 @@ function walkTsFiles(dir: string): string[] {
|
|
|
196
321
|
return results;
|
|
197
322
|
}
|
|
198
323
|
|
|
199
|
-
function updateReferencesForRename(
|
|
324
|
+
function updateReferencesForRename(
|
|
325
|
+
ctx: ManagementContext,
|
|
326
|
+
resource: "agent" | "team" | "workflow",
|
|
327
|
+
oldName: string,
|
|
328
|
+
newName: string,
|
|
329
|
+
scope: MutableSource,
|
|
330
|
+
dryRun: boolean,
|
|
331
|
+
): string[] {
|
|
200
332
|
if (oldName === newName) return [];
|
|
201
333
|
if (resource !== "agent" && resource !== "workflow") return [];
|
|
202
334
|
const changed: string[] = [];
|
|
203
|
-
for (const team of allTeams(discoverTeams(ctx.cwd)).filter(
|
|
335
|
+
for (const team of allTeams(discoverTeams(ctx.cwd)).filter(
|
|
336
|
+
(candidate) => candidate.source === scope,
|
|
337
|
+
)) {
|
|
204
338
|
let updated = false;
|
|
205
339
|
let nextTeam = team;
|
|
206
340
|
if (resource === "agent") {
|
|
207
|
-
const roles = team.roles.map((role) =>
|
|
208
|
-
|
|
341
|
+
const roles = team.roles.map((role) =>
|
|
342
|
+
role.agent === oldName ? { ...role, agent: newName } : role,
|
|
343
|
+
);
|
|
344
|
+
updated = roles.some(
|
|
345
|
+
(role, index) => role.agent !== team.roles[index]!.agent,
|
|
346
|
+
);
|
|
209
347
|
nextTeam = { ...team, roles };
|
|
210
348
|
}
|
|
211
349
|
if (resource === "workflow" && team.defaultWorkflow === oldName) {
|
|
@@ -221,7 +359,9 @@ function updateReferencesForRename(ctx: ManagementContext, resource: "agent" | "
|
|
|
221
359
|
}
|
|
222
360
|
// L12 fix: also update workflow step role references when renaming agents.
|
|
223
361
|
// Workflow files use `role:` to reference agent roles, not agent names.
|
|
224
|
-
for (const workflow of allWorkflows(discoverWorkflows(ctx.cwd)).filter(
|
|
362
|
+
for (const workflow of allWorkflows(discoverWorkflows(ctx.cwd)).filter(
|
|
363
|
+
(w) => w.source === scope,
|
|
364
|
+
)) {
|
|
225
365
|
let updated = false;
|
|
226
366
|
const newSteps = workflow.steps.map((step) => {
|
|
227
367
|
if (step.role === oldName) {
|
|
@@ -234,16 +374,28 @@ function updateReferencesForRename(ctx: ManagementContext, resource: "agent" | "
|
|
|
234
374
|
changed.push(workflow.filePath);
|
|
235
375
|
if (!dryRun) {
|
|
236
376
|
backupFile(workflow.filePath);
|
|
237
|
-
fs.writeFileSync(
|
|
377
|
+
fs.writeFileSync(
|
|
378
|
+
workflow.filePath,
|
|
379
|
+
serializeWorkflow({ ...workflow, steps: newSteps }),
|
|
380
|
+
"utf-8",
|
|
381
|
+
);
|
|
238
382
|
}
|
|
239
383
|
}
|
|
240
384
|
// L12 fix: update agent references in test fixtures.
|
|
241
|
-
const testDir =
|
|
385
|
+
const testDir =
|
|
386
|
+
scope === "user"
|
|
387
|
+
? path.join(projectCrewRoot(ctx.cwd), "test")
|
|
388
|
+
: path.join(ctx.cwd, "test", "fixtures");
|
|
242
389
|
if (fs.existsSync(testDir)) {
|
|
243
390
|
for (const fixture of walkTsFiles(testDir)) {
|
|
244
391
|
const content = fs.readFileSync(fixture, "utf-8");
|
|
245
392
|
if (!content.includes(oldName)) continue;
|
|
246
|
-
const agentPattern = new RegExp(
|
|
393
|
+
const agentPattern = new RegExp(
|
|
394
|
+
"([\"'\\`]agent[=\"':\\s]*)" +
|
|
395
|
+
escapeRegex(oldName) +
|
|
396
|
+
"([\"'\\`]|\\s)",
|
|
397
|
+
"g",
|
|
398
|
+
);
|
|
247
399
|
const newContent = content.replace(agentPattern, `$1${newName}$2`);
|
|
248
400
|
if (newContent !== content) {
|
|
249
401
|
changed.push(fixture);
|
|
@@ -256,30 +408,80 @@ function updateReferencesForRename(ctx: ManagementContext, resource: "agent" | "
|
|
|
256
408
|
return changed;
|
|
257
409
|
}
|
|
258
410
|
|
|
259
|
-
function resolveMutable(
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
411
|
+
function resolveMutable(
|
|
412
|
+
ctx: ManagementContext,
|
|
413
|
+
params: TeamToolParamsValue,
|
|
414
|
+
): { resource?: MutableResource; error?: PiTeamsToolResult } {
|
|
415
|
+
if (!params.resource)
|
|
416
|
+
return {
|
|
417
|
+
error: result(
|
|
418
|
+
"resource is required for update/delete.",
|
|
419
|
+
"error",
|
|
420
|
+
true,
|
|
421
|
+
),
|
|
422
|
+
};
|
|
423
|
+
const name =
|
|
424
|
+
params.resource === "agent"
|
|
425
|
+
? params.agent
|
|
426
|
+
: params.resource === "team"
|
|
427
|
+
? params.team
|
|
428
|
+
: params.workflow;
|
|
429
|
+
if (!name)
|
|
430
|
+
return {
|
|
431
|
+
error: result(
|
|
432
|
+
`${params.resource} name is required.`,
|
|
433
|
+
"error",
|
|
434
|
+
true,
|
|
435
|
+
),
|
|
436
|
+
};
|
|
263
437
|
const matches = findResource(ctx, params.resource, name, params.scope);
|
|
264
|
-
if (matches.length === 0)
|
|
265
|
-
|
|
438
|
+
if (matches.length === 0)
|
|
439
|
+
return {
|
|
440
|
+
error: result(
|
|
441
|
+
`${params.resource} '${name}' not found in mutable user/project scopes.`,
|
|
442
|
+
"error",
|
|
443
|
+
true,
|
|
444
|
+
),
|
|
445
|
+
};
|
|
446
|
+
if (matches.length > 1)
|
|
447
|
+
return {
|
|
448
|
+
error: result(
|
|
449
|
+
`${params.resource} '${name}' exists in multiple scopes. Specify scope: 'user' or 'project'.`,
|
|
450
|
+
"error",
|
|
451
|
+
true,
|
|
452
|
+
),
|
|
453
|
+
};
|
|
266
454
|
return { resource: matches[0] };
|
|
267
455
|
}
|
|
268
456
|
|
|
269
|
-
export function handleCreate(
|
|
270
|
-
|
|
457
|
+
export function handleCreate(
|
|
458
|
+
params: TeamToolParamsValue,
|
|
459
|
+
ctx: ManagementContext,
|
|
460
|
+
): PiTeamsToolResult {
|
|
461
|
+
if (!params.resource)
|
|
462
|
+
return result("resource is required for create.", "error", true);
|
|
271
463
|
const parsed = parseConfigObject(params.config);
|
|
272
464
|
if (parsed.error) return result(parsed.error, "error", true);
|
|
273
465
|
const cfg = parsed.value!;
|
|
274
466
|
const nameValue = requireString(cfg.name, "config.name");
|
|
275
467
|
if (nameValue.error) return result(nameValue.error, "error", true);
|
|
276
|
-
const descriptionValue = requireString(
|
|
277
|
-
|
|
468
|
+
const descriptionValue = requireString(
|
|
469
|
+
cfg.description,
|
|
470
|
+
"config.description",
|
|
471
|
+
);
|
|
472
|
+
if (descriptionValue.error)
|
|
473
|
+
return result(descriptionValue.error, "error", true);
|
|
278
474
|
const name = sanitizeName(nameValue.value!);
|
|
279
|
-
if (!name)
|
|
475
|
+
if (!name)
|
|
476
|
+
return result(
|
|
477
|
+
"config.name is invalid after sanitization.",
|
|
478
|
+
"error",
|
|
479
|
+
true,
|
|
480
|
+
);
|
|
280
481
|
const scope = cfg.scope === "project" ? "project" : "user";
|
|
281
482
|
const filePath = targetPath(ctx, params.resource, scope, name);
|
|
282
|
-
if (fs.existsSync(filePath))
|
|
483
|
+
if (fs.existsSync(filePath))
|
|
484
|
+
return result(`File already exists: ${filePath}`, "error", true);
|
|
283
485
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
284
486
|
|
|
285
487
|
let content: string;
|
|
@@ -289,14 +491,19 @@ export function handleCreate(params: TeamToolParamsValue, ctx: ManagementContext
|
|
|
289
491
|
description: descriptionValue.value!,
|
|
290
492
|
source: scope,
|
|
291
493
|
filePath,
|
|
292
|
-
systemPrompt:
|
|
494
|
+
systemPrompt:
|
|
495
|
+
typeof cfg.systemPrompt === "string" ? cfg.systemPrompt : "",
|
|
293
496
|
model: typeof cfg.model === "string" ? cfg.model : undefined,
|
|
294
497
|
fallbackModels: parseStringArray(cfg.fallbackModels),
|
|
295
|
-
thinking:
|
|
498
|
+
thinking:
|
|
499
|
+
typeof cfg.thinking === "string" ? cfg.thinking : undefined,
|
|
296
500
|
tools: parseStringArray(cfg.tools),
|
|
297
|
-
extensions: hasOwn(cfg, "extensions")
|
|
501
|
+
extensions: hasOwn(cfg, "extensions")
|
|
502
|
+
? (parseStringArray(cfg.extensions) ?? [])
|
|
503
|
+
: undefined,
|
|
298
504
|
skills: parseStringArray(cfg.skills),
|
|
299
|
-
systemPromptMode:
|
|
505
|
+
systemPromptMode:
|
|
506
|
+
cfg.systemPromptMode === "append" ? "append" : "replace",
|
|
300
507
|
inheritProjectContext: cfg.inheritProjectContext === true,
|
|
301
508
|
inheritSkills: cfg.inheritSkills === true,
|
|
302
509
|
routing: parseRouting(cfg),
|
|
@@ -311,9 +518,18 @@ export function handleCreate(params: TeamToolParamsValue, ctx: ManagementContext
|
|
|
311
518
|
source: scope,
|
|
312
519
|
filePath,
|
|
313
520
|
roles: parsedRoles.roles!,
|
|
314
|
-
defaultWorkflow:
|
|
315
|
-
|
|
316
|
-
|
|
521
|
+
defaultWorkflow:
|
|
522
|
+
typeof cfg.defaultWorkflow === "string"
|
|
523
|
+
? sanitizeName(cfg.defaultWorkflow)
|
|
524
|
+
: undefined,
|
|
525
|
+
workspaceMode:
|
|
526
|
+
cfg.workspaceMode === "worktree" ? "worktree" : "single",
|
|
527
|
+
maxConcurrency:
|
|
528
|
+
typeof cfg.maxConcurrency === "number" &&
|
|
529
|
+
Number.isInteger(cfg.maxConcurrency) &&
|
|
530
|
+
cfg.maxConcurrency > 0
|
|
531
|
+
? cfg.maxConcurrency
|
|
532
|
+
: undefined,
|
|
317
533
|
routing: parseRouting(cfg),
|
|
318
534
|
});
|
|
319
535
|
} else {
|
|
@@ -329,27 +545,45 @@ export function handleCreate(params: TeamToolParamsValue, ctx: ManagementContext
|
|
|
329
545
|
});
|
|
330
546
|
}
|
|
331
547
|
|
|
332
|
-
if (params.dryRun)
|
|
548
|
+
if (params.dryRun)
|
|
549
|
+
return result(
|
|
550
|
+
`[dry-run] Would create ${params.resource} '${name}' at ${filePath}:\n\n${content}`,
|
|
551
|
+
);
|
|
333
552
|
try {
|
|
334
553
|
fs.writeFileSync(filePath, content, "utf-8");
|
|
335
554
|
} catch (writeError) {
|
|
336
|
-
return result(
|
|
555
|
+
return result(
|
|
556
|
+
`Failed to create ${params.resource}: ${writeError instanceof Error ? writeError.message : String(writeError)}`,
|
|
557
|
+
"error",
|
|
558
|
+
true,
|
|
559
|
+
);
|
|
337
560
|
}
|
|
338
561
|
return result(`Created ${params.resource} '${name}' at ${filePath}.`);
|
|
339
562
|
}
|
|
340
563
|
|
|
341
|
-
export function handleUpdate(
|
|
564
|
+
export function handleUpdate(
|
|
565
|
+
params: TeamToolParamsValue,
|
|
566
|
+
ctx: ManagementContext,
|
|
567
|
+
): PiTeamsToolResult {
|
|
342
568
|
const resolved = resolveMutable(ctx, params);
|
|
343
569
|
if (resolved.error) return resolved.error;
|
|
344
570
|
const parsed = parseConfigObject(params.config);
|
|
345
571
|
if (parsed.error) return result(parsed.error, "error", true);
|
|
346
572
|
const cfg = parsed.value!;
|
|
347
573
|
const current = resolved.resource!;
|
|
348
|
-
const nextName = hasOwn(cfg, "name")
|
|
349
|
-
|
|
574
|
+
const nextName = hasOwn(cfg, "name")
|
|
575
|
+
? sanitizeName(String(cfg.name ?? ""))
|
|
576
|
+
: current.name;
|
|
577
|
+
if (!nextName)
|
|
578
|
+
return result(
|
|
579
|
+
"config.name is invalid after sanitization.",
|
|
580
|
+
"error",
|
|
581
|
+
true,
|
|
582
|
+
);
|
|
350
583
|
const source = current.source === "project" ? "project" : "user";
|
|
351
584
|
const nextPath = targetPath(ctx, params.resource!, source, nextName);
|
|
352
|
-
if (nextPath !== current.filePath && fs.existsSync(nextPath))
|
|
585
|
+
if (nextPath !== current.filePath && fs.existsSync(nextPath))
|
|
586
|
+
return result(`Target file already exists: ${nextPath}`, "error", true);
|
|
353
587
|
|
|
354
588
|
let content: string;
|
|
355
589
|
if (params.resource === "agent") {
|
|
@@ -358,17 +592,50 @@ export function handleUpdate(params: TeamToolParamsValue, ctx: ManagementContext
|
|
|
358
592
|
...agent,
|
|
359
593
|
name: nextName,
|
|
360
594
|
filePath: nextPath,
|
|
361
|
-
description:
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
595
|
+
description:
|
|
596
|
+
typeof cfg.description === "string" && cfg.description.trim()
|
|
597
|
+
? cfg.description.trim()
|
|
598
|
+
: agent.description,
|
|
599
|
+
systemPrompt:
|
|
600
|
+
typeof cfg.systemPrompt === "string"
|
|
601
|
+
? cfg.systemPrompt
|
|
602
|
+
: agent.systemPrompt,
|
|
603
|
+
model: hasOwn(cfg, "model")
|
|
604
|
+
? typeof cfg.model === "string" && cfg.model.trim()
|
|
605
|
+
? cfg.model.trim()
|
|
606
|
+
: undefined
|
|
607
|
+
: agent.model,
|
|
608
|
+
fallbackModels: hasOwn(cfg, "fallbackModels")
|
|
609
|
+
? parseStringArray(cfg.fallbackModels)
|
|
610
|
+
: agent.fallbackModels,
|
|
611
|
+
thinking: hasOwn(cfg, "thinking")
|
|
612
|
+
? typeof cfg.thinking === "string" && cfg.thinking.trim()
|
|
613
|
+
? cfg.thinking.trim()
|
|
614
|
+
: undefined
|
|
615
|
+
: agent.thinking,
|
|
616
|
+
tools: hasOwn(cfg, "tools")
|
|
617
|
+
? parseStringArray(cfg.tools)
|
|
618
|
+
: agent.tools,
|
|
619
|
+
extensions: hasOwn(cfg, "extensions")
|
|
620
|
+
? (parseStringArray(cfg.extensions) ?? [])
|
|
621
|
+
: agent.extensions,
|
|
622
|
+
skills: hasOwn(cfg, "skills")
|
|
623
|
+
? parseStringArray(cfg.skills)
|
|
624
|
+
: agent.skills,
|
|
625
|
+
systemPromptMode:
|
|
626
|
+
cfg.systemPromptMode === "append"
|
|
627
|
+
? "append"
|
|
628
|
+
: cfg.systemPromptMode === "replace"
|
|
629
|
+
? "replace"
|
|
630
|
+
: agent.systemPromptMode,
|
|
631
|
+
inheritProjectContext:
|
|
632
|
+
typeof cfg.inheritProjectContext === "boolean"
|
|
633
|
+
? cfg.inheritProjectContext
|
|
634
|
+
: agent.inheritProjectContext,
|
|
635
|
+
inheritSkills:
|
|
636
|
+
typeof cfg.inheritSkills === "boolean"
|
|
637
|
+
? cfg.inheritSkills
|
|
638
|
+
: agent.inheritSkills,
|
|
372
639
|
routing: parseRouting(cfg, agent.routing),
|
|
373
640
|
});
|
|
374
641
|
} else if (params.resource === "team") {
|
|
@@ -376,18 +643,36 @@ export function handleUpdate(params: TeamToolParamsValue, ctx: ManagementContext
|
|
|
376
643
|
let roles = team.roles;
|
|
377
644
|
if (hasOwn(cfg, "roles")) {
|
|
378
645
|
const parsedRoles = parseRoles(cfg.roles);
|
|
379
|
-
if (parsedRoles.error)
|
|
646
|
+
if (parsedRoles.error)
|
|
647
|
+
return result(parsedRoles.error, "error", true);
|
|
380
648
|
roles = parsedRoles.roles!;
|
|
381
649
|
}
|
|
382
650
|
content = serializeTeam({
|
|
383
651
|
...team,
|
|
384
652
|
name: nextName,
|
|
385
653
|
filePath: nextPath,
|
|
386
|
-
description:
|
|
654
|
+
description:
|
|
655
|
+
typeof cfg.description === "string" && cfg.description.trim()
|
|
656
|
+
? cfg.description.trim()
|
|
657
|
+
: team.description,
|
|
387
658
|
roles,
|
|
388
|
-
defaultWorkflow: hasOwn(cfg, "defaultWorkflow")
|
|
389
|
-
|
|
390
|
-
|
|
659
|
+
defaultWorkflow: hasOwn(cfg, "defaultWorkflow")
|
|
660
|
+
? typeof cfg.defaultWorkflow === "string"
|
|
661
|
+
? sanitizeName(cfg.defaultWorkflow)
|
|
662
|
+
: undefined
|
|
663
|
+
: team.defaultWorkflow,
|
|
664
|
+
workspaceMode:
|
|
665
|
+
cfg.workspaceMode === "worktree"
|
|
666
|
+
? "worktree"
|
|
667
|
+
: cfg.workspaceMode === "single"
|
|
668
|
+
? "single"
|
|
669
|
+
: team.workspaceMode,
|
|
670
|
+
maxConcurrency:
|
|
671
|
+
typeof cfg.maxConcurrency === "number" &&
|
|
672
|
+
Number.isInteger(cfg.maxConcurrency) &&
|
|
673
|
+
cfg.maxConcurrency > 0
|
|
674
|
+
? cfg.maxConcurrency
|
|
675
|
+
: team.maxConcurrency,
|
|
391
676
|
routing: parseRouting(cfg, team.routing),
|
|
392
677
|
});
|
|
393
678
|
} else {
|
|
@@ -395,22 +680,52 @@ export function handleUpdate(params: TeamToolParamsValue, ctx: ManagementContext
|
|
|
395
680
|
let steps = workflow.steps;
|
|
396
681
|
if (hasOwn(cfg, "steps")) {
|
|
397
682
|
const parsedSteps = parseSteps(cfg.steps);
|
|
398
|
-
if (parsedSteps.error)
|
|
683
|
+
if (parsedSteps.error)
|
|
684
|
+
return result(parsedSteps.error, "error", true);
|
|
399
685
|
steps = parsedSteps.steps!;
|
|
400
686
|
}
|
|
401
687
|
content = serializeWorkflow({
|
|
402
688
|
...workflow,
|
|
403
689
|
name: nextName,
|
|
404
690
|
filePath: nextPath,
|
|
405
|
-
description:
|
|
406
|
-
|
|
691
|
+
description:
|
|
692
|
+
typeof cfg.description === "string" && cfg.description.trim()
|
|
693
|
+
? cfg.description.trim()
|
|
694
|
+
: workflow.description,
|
|
695
|
+
maxConcurrency: hasOwn(cfg, "maxConcurrency")
|
|
696
|
+
? parseWorkflowMaxConcurrency(cfg.maxConcurrency)
|
|
697
|
+
: workflow.maxConcurrency,
|
|
407
698
|
steps,
|
|
408
699
|
});
|
|
409
700
|
}
|
|
410
701
|
|
|
411
|
-
const referenceUpdates = params.updateReferences
|
|
702
|
+
const referenceUpdates = params.updateReferences
|
|
703
|
+
? updateReferencesForRename(
|
|
704
|
+
ctx,
|
|
705
|
+
params.resource!,
|
|
706
|
+
current.name,
|
|
707
|
+
nextName,
|
|
708
|
+
source,
|
|
709
|
+
true,
|
|
710
|
+
)
|
|
711
|
+
: [];
|
|
412
712
|
if (params.dryRun) {
|
|
413
|
-
return result(
|
|
713
|
+
return result(
|
|
714
|
+
[
|
|
715
|
+
`[dry-run] Would update ${params.resource} at ${current.filePath}:`,
|
|
716
|
+
"",
|
|
717
|
+
content,
|
|
718
|
+
...(referenceUpdates.length
|
|
719
|
+
? [
|
|
720
|
+
"",
|
|
721
|
+
"Would update references in:",
|
|
722
|
+
...referenceUpdates.map(
|
|
723
|
+
(filePath) => `- ${filePath}`,
|
|
724
|
+
),
|
|
725
|
+
]
|
|
726
|
+
: []),
|
|
727
|
+
].join("\n"),
|
|
728
|
+
);
|
|
414
729
|
}
|
|
415
730
|
const backupPath = backupFile(current.filePath);
|
|
416
731
|
try {
|
|
@@ -428,28 +743,68 @@ export function handleUpdate(params: TeamToolParamsValue, ctx: ManagementContext
|
|
|
428
743
|
}
|
|
429
744
|
fs.writeFileSync(nextPath, content, "utf-8");
|
|
430
745
|
} catch (updateError) {
|
|
431
|
-
return result(
|
|
746
|
+
return result(
|
|
747
|
+
`Failed to update ${params.resource}: ${updateError instanceof Error ? updateError.message : String(updateError)}`,
|
|
748
|
+
"error",
|
|
749
|
+
true,
|
|
750
|
+
);
|
|
432
751
|
}
|
|
433
|
-
const updatedRefs = params.updateReferences
|
|
434
|
-
|
|
752
|
+
const updatedRefs = params.updateReferences
|
|
753
|
+
? updateReferencesForRename(
|
|
754
|
+
ctx,
|
|
755
|
+
params.resource!,
|
|
756
|
+
current.name,
|
|
757
|
+
nextName,
|
|
758
|
+
source,
|
|
759
|
+
false,
|
|
760
|
+
)
|
|
761
|
+
: [];
|
|
762
|
+
return result(
|
|
763
|
+
[
|
|
764
|
+
`Updated ${params.resource} at ${nextPath}. Backup: ${backupPath}.`,
|
|
765
|
+
...(updatedRefs.length
|
|
766
|
+
? [
|
|
767
|
+
"Updated references:",
|
|
768
|
+
...updatedRefs.map((filePath) => `- ${filePath}`),
|
|
769
|
+
]
|
|
770
|
+
: []),
|
|
771
|
+
].join("\n"),
|
|
772
|
+
);
|
|
435
773
|
}
|
|
436
774
|
|
|
437
|
-
export function handleDelete(
|
|
775
|
+
export function handleDelete(
|
|
776
|
+
params: TeamToolParamsValue,
|
|
777
|
+
ctx: ManagementContext,
|
|
778
|
+
): PiTeamsToolResult {
|
|
438
779
|
const intentError = enforceDestructiveIntent("delete", params, ctx.config);
|
|
439
780
|
if (intentError) return intentError;
|
|
440
|
-
if (!params.confirm)
|
|
781
|
+
if (!params.confirm)
|
|
782
|
+
return result("delete requires confirm: true.", "error", true);
|
|
441
783
|
const resolved = resolveMutable(ctx, params);
|
|
442
784
|
if (resolved.error) return resolved.error;
|
|
443
785
|
const refs = findReferences(ctx, params.resource!, resolved.resource!.name);
|
|
444
786
|
if (refs.length > 0 && !params.force) {
|
|
445
|
-
return result(
|
|
787
|
+
return result(
|
|
788
|
+
`${params.resource} '${resolved.resource!.name}' is still referenced. Use force: true to delete anyway.\n${refs.map((ref) => `- ${ref}`).join("\n")}`,
|
|
789
|
+
"error",
|
|
790
|
+
true,
|
|
791
|
+
);
|
|
446
792
|
}
|
|
447
|
-
if (params.dryRun)
|
|
793
|
+
if (params.dryRun)
|
|
794
|
+
return result(
|
|
795
|
+
`[dry-run] Would delete ${params.resource} at ${resolved.resource!.filePath}.${refs.length ? `\nReferences:\n${refs.map((ref) => `- ${ref}`).join("\n")}` : ""}`,
|
|
796
|
+
);
|
|
448
797
|
const backupPath = backupFile(resolved.resource!.filePath);
|
|
449
798
|
try {
|
|
450
799
|
fs.unlinkSync(resolved.resource!.filePath);
|
|
451
800
|
} catch (deleteError) {
|
|
452
|
-
return result(
|
|
801
|
+
return result(
|
|
802
|
+
`Failed to delete ${params.resource}: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`,
|
|
803
|
+
"error",
|
|
804
|
+
true,
|
|
805
|
+
);
|
|
453
806
|
}
|
|
454
|
-
return result(
|
|
807
|
+
return result(
|
|
808
|
+
`Deleted ${params.resource} at ${resolved.resource!.filePath}. Backup: ${backupPath}.`,
|
|
809
|
+
);
|
|
455
810
|
}
|