@danypops/papyrus 0.44.8 → 0.44.10
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/package.json +3 -2
- package/src/{ports → artifact}/artifact-event-reader.ts +1 -1
- package/src/{artifact-relationship-view.ts → artifact/artifact-relationship-view.ts} +3 -3
- package/src/{ports → artifact}/artifact-store.ts +2 -2
- package/src/{artifact-subtree.ts → artifact/artifact-subtree.ts} +4 -4
- package/src/{ports → artifact}/artifact-trash-store.ts +2 -2
- package/src/{adapters → artifact}/sqlite-artifact-scope-store.ts +1 -1
- package/src/{adapters → artifact}/sqlite-artifact-store.ts +15 -15
- package/src/authority-registry.ts +3 -3
- package/src/cli/artifact-command.ts +210 -0
- package/src/cli/discuss-command.ts +256 -0
- package/src/cli/docs-command.ts +192 -0
- package/src/cli/gates-command.ts +46 -0
- package/src/cli/graph-command.ts +171 -0
- package/src/cli/graph-projection-command.ts +79 -0
- package/src/cli/log-command.ts +101 -0
- package/src/cli/migration-command.ts +40 -0
- package/src/cli/note-command.ts +186 -0
- package/src/cli/playbooks-command.ts +313 -0
- package/src/cli/rules-command.ts +220 -0
- package/src/cli/session-identity-command.ts +58 -0
- package/src/cli/shared.ts +5 -0
- package/src/cli/stricli-run.ts +30 -0
- package/src/cli/task-command.ts +892 -0
- package/src/cli.ts +32 -1914
- package/src/client.ts +2 -2
- package/src/{daemon-state.ts → daemon/daemon-state.ts} +1 -1
- package/src/{daemon.ts → daemon/daemon.ts} +3 -3
- package/src/db.ts +1 -1
- package/src/{discussion-service.ts → discussion/discussion-service.ts} +7 -6
- package/src/domain-services.ts +5 -5
- package/src/{graph-projection-service.ts → graph-projection/graph-projection-service.ts} +6 -6
- package/src/{vehicle/artifact-trash-vehicle.ts → handlers/artifact-trash.ts} +4 -4
- package/src/{vehicle/discuss-vehicle.ts → handlers/discuss.ts} +20 -25
- package/src/{vehicle/docs-vehicle.ts → handlers/docs.ts} +4 -37
- package/src/{vehicle/notes-vehicle.ts → handlers/notes.ts} +4 -38
- package/src/{vehicle/playbooks-vehicle.ts → handlers/playbooks.ts} +34 -100
- package/src/{vehicle/papyrus-vehicle.ts → handlers/registry.ts} +16 -16
- package/src/{vehicle/rules-vehicle.ts → handlers/rules.ts} +4 -37
- package/src/{vehicle/artifact-vehicle-shared.ts → handlers/shared.ts} +96 -6
- package/src/{vehicle/tasks-vehicle.ts → handlers/tasks.ts} +37 -124
- package/src/index.ts +13 -13
- package/src/{log-service.ts → log/log-service.ts} +2 -2
- package/src/modules/discuss.ts +7 -28
- package/src/modules/docs.ts +4 -24
- package/src/modules/graph-projection.ts +5 -11
- package/src/modules/logs.ts +2 -23
- package/src/modules/notes.ts +2 -23
- package/src/modules/operation-input.ts +35 -0
- package/src/modules/playbooks.ts +13 -33
- package/src/modules/rules.ts +4 -24
- package/src/modules/session-identity.ts +2 -16
- package/src/modules/tasks.ts +11 -38
- package/src/{note-service.ts → note/note-service.ts} +6 -6
- package/src/ops.ts +6 -6
- package/src/{playbook-definition.ts → playbook/playbook-definition.ts} +7 -6
- package/src/{playbook-execution.ts → playbook/playbook-execution.ts} +5 -4
- package/src/{workflow-execution.ts → playbook/workflow-execution.ts} +12 -12
- package/src/service.ts +30 -57
- package/src/{session-identity-service.ts → session-identity/session-identity-service.ts} +2 -2
- package/src/{ports → stores}/session-identity-store.ts +1 -1
- package/src/{adapters → stores}/sqlite-discussion-round-store.ts +1 -1
- package/src/{adapters → stores}/sqlite-gate-runner.ts +1 -1
- package/src/{adapters → stores}/sqlite-graph-projection-store.ts +1 -1
- package/src/{adapters → stores}/sqlite-log-store.ts +1 -1
- package/src/{adapters → stores}/sqlite-note-event-store.ts +1 -1
- package/src/{adapters → stores}/sqlite-session-identity-store.ts +1 -1
- package/src/{adapters → stores}/sqlite-task-event-store.ts +1 -1
- package/src/{adapters → stores}/sqlite-task-focus-store.ts +1 -1
- package/src/{adapters → stores}/sqlite-task-lease-store.ts +1 -1
- package/src/{adapters → stores}/sqlite-task-scope-store.ts +1 -1
- package/src/{task-context.ts → task/task-context.ts} +4 -4
- package/src/{task-execution.ts → task/task-execution.ts} +1 -1
- package/src/{task-graph-view.ts → task/task-graph-view.ts} +1 -1
- package/src/{task-relationship-view.ts → task/task-relationship-view.ts} +3 -3
- package/src/{task-service.ts → task/task-service.ts} +14 -14
- /package/src/{domain → artifact}/artifact-alias.ts +0 -0
- /package/src/{domain → artifact}/artifact-event.ts +0 -0
- /package/src/{ports → artifact}/artifact-scope-store.ts +0 -0
- /package/src/{domain → artifact}/artifact-trash.ts +0 -0
- /package/src/{domain → artifact}/artifact.ts +0 -0
- /package/src/{ports → artifact}/atomic-artifact-store.ts +0 -0
- /package/src/{log.ts → log/log.ts} +0 -0
- /package/src/{ports → stores}/discussion-round-store.ts +0 -0
- /package/src/{ports → stores}/gate-runner.ts +0 -0
- /package/src/{ports → stores}/graph-projection-store.ts +0 -0
- /package/src/{ports → stores}/graph-renderer.ts +0 -0
- /package/src/{ports → stores}/log-store.ts +0 -0
- /package/src/{ports → stores}/note-event-store.ts +0 -0
- /package/src/{ports → stores}/task-event-store.ts +0 -0
- /package/src/{ports → stores}/task-focus-store.ts +0 -0
- /package/src/{ports → stores}/task-lease-store.ts +0 -0
- /package/src/{ports → stores}/task-scope-store.ts +0 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { CommandContext } from "@stricli/core";
|
|
2
|
+
import { buildApplication, buildCommand, buildRouteMap, numberParser } from "@stricli/core";
|
|
3
|
+
import type { PapyrusClient } from "../client.ts";
|
|
4
|
+
import type { NoteHistoryPage } from "../domain/note-event.ts";
|
|
5
|
+
import { artifactLabel, type CliArtifact } from "./shared.ts";
|
|
6
|
+
import { runStricliToString } from "./stricli-run.ts";
|
|
7
|
+
|
|
8
|
+
type NoteClient = Pick<PapyrusClient, "call">;
|
|
9
|
+
|
|
10
|
+
interface NoteContext extends CommandContext {
|
|
11
|
+
readonly client: NoteClient;
|
|
12
|
+
readonly json: boolean;
|
|
13
|
+
readonly projectRoot: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function renderResult(this: NoteContext, result: unknown, human: string): void {
|
|
17
|
+
this.process.stdout.write(this.json ? JSON.stringify(result) : human);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const captureCommand = buildCommand({
|
|
21
|
+
func: async function (this: NoteContext, flags: { title?: string }, request: string) {
|
|
22
|
+
const result = (await this.client.call("notes.capture", {
|
|
23
|
+
body: request,
|
|
24
|
+
...(flags.title ? { title: flags.title } : {}),
|
|
25
|
+
project_root: this.projectRoot,
|
|
26
|
+
actor: "human",
|
|
27
|
+
source: "cli",
|
|
28
|
+
})) as CliArtifact;
|
|
29
|
+
renderResult.call(this, result, `Captured: ${artifactLabel(result)}`);
|
|
30
|
+
},
|
|
31
|
+
parameters: {
|
|
32
|
+
flags: { title: { brief: "Optional title", kind: "parsed", parse: String, placeholder: "text", optional: true } },
|
|
33
|
+
positional: { kind: "tuple", parameters: [{ brief: "Request text to capture", parse: String, placeholder: "request" }] },
|
|
34
|
+
},
|
|
35
|
+
docs: { brief: "Capture a deferred request as a note, without creating work" },
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const listCommand = buildCommand({
|
|
39
|
+
func: async function (this: NoteContext, flags: { status?: string; text?: string; limit?: number }) {
|
|
40
|
+
const result = (await this.client.call("notes.list", {
|
|
41
|
+
project_root: this.projectRoot,
|
|
42
|
+
...(flags.status ? { status: flags.status } : {}),
|
|
43
|
+
...(flags.text ? { text: flags.text } : {}),
|
|
44
|
+
...(flags.limit === undefined ? {} : { limit: flags.limit }),
|
|
45
|
+
})) as CliArtifact[];
|
|
46
|
+
renderResult.call(
|
|
47
|
+
this,
|
|
48
|
+
result,
|
|
49
|
+
result.length > 0 ? result.map((note) => `[${note.status}] ${artifactLabel(note)}`).join("\n") : "No open notes.",
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
parameters: {
|
|
53
|
+
flags: {
|
|
54
|
+
status: { brief: "Filter by status (draft|active|archived)", kind: "parsed", parse: String, placeholder: "status", optional: true },
|
|
55
|
+
text: { brief: "Substring match against title/body", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
56
|
+
limit: { brief: "Maximum notes to return", kind: "parsed", parse: numberParser, optional: true },
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
docs: { brief: "List open (draft/active) notes, or a specific status" },
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const showCommand = buildCommand({
|
|
63
|
+
func: async function (this: NoteContext, _flags: Record<string, never>, id: string) {
|
|
64
|
+
const result = (await this.client.call("notes.show", { id, project_root: this.projectRoot })) as CliArtifact;
|
|
65
|
+
renderResult.call(this, result, `${artifactLabel(result)}\n\n${result.body ?? ""}`.trimEnd());
|
|
66
|
+
},
|
|
67
|
+
parameters: { flags: {}, positional: { kind: "tuple", parameters: [{ brief: "Note id", parse: String, placeholder: "id" }] } },
|
|
68
|
+
docs: { brief: "Show one note" },
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const historyCommand = buildCommand({
|
|
72
|
+
func: async function (this: NoteContext, flags: { limit?: number }, id: string) {
|
|
73
|
+
const page = await this.client.call<Record<string, unknown>, NoteHistoryPage>("notes.history", {
|
|
74
|
+
id,
|
|
75
|
+
project_root: this.projectRoot,
|
|
76
|
+
direction: "desc",
|
|
77
|
+
...(flags.limit === undefined ? {} : { limit: flags.limit }),
|
|
78
|
+
});
|
|
79
|
+
const human =
|
|
80
|
+
page.events.length === 0
|
|
81
|
+
? `No recorded history for ${id}.`
|
|
82
|
+
: [...page.events]
|
|
83
|
+
.reverse()
|
|
84
|
+
.map(
|
|
85
|
+
(event) =>
|
|
86
|
+
`${event.occurredAt} ${event.type} · ${event.actor}/${event.source}${event.relatedId ? ` · ${event.relatedId}` : ""}${event.disposition ? ` · ${event.disposition}` : ""}${event.reason ? ` · ${event.reason}` : ""}`,
|
|
87
|
+
)
|
|
88
|
+
.join("\n");
|
|
89
|
+
renderResult.call(this, page, human);
|
|
90
|
+
},
|
|
91
|
+
parameters: {
|
|
92
|
+
flags: { limit: { brief: "Maximum events to return", kind: "parsed", parse: numberParser, optional: true } },
|
|
93
|
+
positional: { kind: "tuple", parameters: [{ brief: "Note id", parse: String, placeholder: "id" }] },
|
|
94
|
+
},
|
|
95
|
+
docs: { brief: "This note's own event history" },
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const consumeCommand = buildCommand({
|
|
99
|
+
func: async function (this: NoteContext, flags: { reason?: string }, id: string) {
|
|
100
|
+
const result = (await this.client.call("notes.consume", {
|
|
101
|
+
id,
|
|
102
|
+
project_root: this.projectRoot,
|
|
103
|
+
actor: "agent",
|
|
104
|
+
source: "cli",
|
|
105
|
+
...(flags.reason ? { reason: flags.reason } : {}),
|
|
106
|
+
})) as CliArtifact;
|
|
107
|
+
renderResult.call(this, result, `Consumed: ${artifactLabel(result)}`);
|
|
108
|
+
},
|
|
109
|
+
parameters: {
|
|
110
|
+
flags: { reason: { brief: "Why this note was considered", kind: "parsed", parse: String, placeholder: "text", optional: true } },
|
|
111
|
+
positional: { kind: "tuple", parameters: [{ brief: "Note id", parse: String, placeholder: "id" }] },
|
|
112
|
+
},
|
|
113
|
+
docs: { brief: "Mark a note as considered" },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const promoteCommand = buildCommand({
|
|
117
|
+
func: async function (this: NoteContext, flags: { reason?: string }, id: string, targetId: string) {
|
|
118
|
+
const result = (await this.client.call("notes.promote", {
|
|
119
|
+
id,
|
|
120
|
+
target_id: targetId,
|
|
121
|
+
project_root: this.projectRoot,
|
|
122
|
+
actor: "agent",
|
|
123
|
+
source: "cli",
|
|
124
|
+
...(flags.reason ? { reason: flags.reason } : {}),
|
|
125
|
+
})) as CliArtifact;
|
|
126
|
+
renderResult.call(this, result, `Promoted: ${artifactLabel(result)} → ${targetId}`);
|
|
127
|
+
},
|
|
128
|
+
parameters: {
|
|
129
|
+
flags: { reason: { brief: "Why this note was promoted", kind: "parsed", parse: String, placeholder: "text", optional: true } },
|
|
130
|
+
positional: {
|
|
131
|
+
kind: "tuple",
|
|
132
|
+
parameters: [
|
|
133
|
+
{ brief: "Note id", parse: String, placeholder: "id" },
|
|
134
|
+
{ brief: "Target artifact id it was promoted into", parse: String, placeholder: "target-id" },
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
docs: { brief: "Link a note to the artifact it was promoted into, then archive it" },
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const archiveCommand = buildCommand({
|
|
142
|
+
func: async function (this: NoteContext, flags: { reason?: string }, id: string, disposition: string) {
|
|
143
|
+
const result = (await this.client.call("notes.archive", {
|
|
144
|
+
id,
|
|
145
|
+
disposition,
|
|
146
|
+
project_root: this.projectRoot,
|
|
147
|
+
actor: "human",
|
|
148
|
+
source: "cli",
|
|
149
|
+
...(flags.reason ? { reason: flags.reason } : {}),
|
|
150
|
+
})) as CliArtifact;
|
|
151
|
+
renderResult.call(this, result, `Archived: ${artifactLabel(result)} · ${disposition}`);
|
|
152
|
+
},
|
|
153
|
+
parameters: {
|
|
154
|
+
flags: { reason: { brief: "Why this note was archived", kind: "parsed", parse: String, placeholder: "text", optional: true } },
|
|
155
|
+
positional: {
|
|
156
|
+
kind: "tuple",
|
|
157
|
+
parameters: [
|
|
158
|
+
{ brief: "Note id", parse: String, placeholder: "id" },
|
|
159
|
+
{ brief: "completed|duplicate|declined|superseded", parse: String, placeholder: "disposition" },
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
docs: { brief: "Archive a note with an explicit disposition" },
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const app = buildApplication(
|
|
167
|
+
buildRouteMap({
|
|
168
|
+
routes: {
|
|
169
|
+
capture: captureCommand,
|
|
170
|
+
list: listCommand,
|
|
171
|
+
show: showCommand,
|
|
172
|
+
history: historyCommand,
|
|
173
|
+
consume: consumeCommand,
|
|
174
|
+
promote: promoteCommand,
|
|
175
|
+
archive: archiveCommand,
|
|
176
|
+
},
|
|
177
|
+
docs: { brief: "Note operations" },
|
|
178
|
+
}),
|
|
179
|
+
{ name: "notes", scanner: { caseStyle: "allow-kebab-for-camel" } },
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
export async function runNoteCli(args: string[], client: NoteClient, projectRoot: string = process.cwd()): Promise<string> {
|
|
183
|
+
const json = args.includes("--json");
|
|
184
|
+
const positional = args.filter((arg) => arg !== "--json");
|
|
185
|
+
return runStricliToString(app, positional, { client, json, projectRoot });
|
|
186
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import type { CommandContext } from "@stricli/core";
|
|
2
|
+
import { buildApplication, buildCommand, buildRouteMap, numberParser } from "@stricli/core";
|
|
3
|
+
import type { PapyrusClient } from "../client.ts";
|
|
4
|
+
import type { OperationName } from "../service.ts";
|
|
5
|
+
import { artifactLabel, type CliArtifact } from "./shared.ts";
|
|
6
|
+
import { runStricliToString } from "./stricli-run.ts";
|
|
7
|
+
|
|
8
|
+
type PlaybooksClient = Pick<PapyrusClient, "call">;
|
|
9
|
+
|
|
10
|
+
interface PlaybooksContext extends CommandContext {
|
|
11
|
+
readonly client: PlaybooksClient;
|
|
12
|
+
readonly json: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseStringArray(value: string): string[] {
|
|
16
|
+
const parsed = JSON.parse(value) as unknown;
|
|
17
|
+
if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== "string")) throw new Error("value must be a JSON string array");
|
|
18
|
+
return parsed as string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseObject(value: string): Record<string, unknown> {
|
|
22
|
+
const parsed = JSON.parse(value) as unknown;
|
|
23
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("value must be a JSON object");
|
|
24
|
+
return parsed as Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* No further shape assertion here -- --arguments-json is genuinely polymorphic (an array on
|
|
29
|
+
* create, a {name: value} map on invoke) and --steps-json accepts a mix of plain prose strings
|
|
30
|
+
* and structured step objects that a single string-array assertion would wrongly reject. The
|
|
31
|
+
* service validates the real shape for whichever operation actually receives it. Typed as a
|
|
32
|
+
* concrete array-or-object union rather than bare `unknown` -- Stricli's flag-type inference
|
|
33
|
+
* doesn't correctly propagate a parser returning `unknown` through to the command function.
|
|
34
|
+
*/
|
|
35
|
+
function parseAny(value: string): unknown[] | Record<string, unknown> {
|
|
36
|
+
return JSON.parse(value) as unknown[] | Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function render(this: PlaybooksContext, result: unknown, human: string): void {
|
|
40
|
+
this.process.stdout.write(this.json ? JSON.stringify(result) : human);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const createCommand = buildCommand({
|
|
44
|
+
func: async function (
|
|
45
|
+
this: PlaybooksContext,
|
|
46
|
+
flags: {
|
|
47
|
+
title: string;
|
|
48
|
+
body?: string;
|
|
49
|
+
trigger?: string;
|
|
50
|
+
stepsJson?: unknown[] | Record<string, unknown>;
|
|
51
|
+
toolsJson?: string[];
|
|
52
|
+
labelsJson?: string[];
|
|
53
|
+
extraJson?: Record<string, unknown>;
|
|
54
|
+
argumentsJson?: unknown[] | Record<string, unknown>;
|
|
55
|
+
projectRoot?: string;
|
|
56
|
+
},
|
|
57
|
+
) {
|
|
58
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("playbooks.create", {
|
|
59
|
+
title: flags.title,
|
|
60
|
+
body: flags.body,
|
|
61
|
+
trigger: flags.trigger,
|
|
62
|
+
steps: flags.stepsJson,
|
|
63
|
+
tools: flags.toolsJson,
|
|
64
|
+
labels: flags.labelsJson,
|
|
65
|
+
extra: flags.extraJson,
|
|
66
|
+
arguments: flags.argumentsJson,
|
|
67
|
+
project_root: flags.projectRoot,
|
|
68
|
+
});
|
|
69
|
+
render.call(this, artifact, `Created playbook: ${artifactLabel(artifact)}`);
|
|
70
|
+
},
|
|
71
|
+
parameters: {
|
|
72
|
+
flags: {
|
|
73
|
+
title: { brief: "Playbook title", kind: "parsed", parse: String, placeholder: "text" },
|
|
74
|
+
body: { brief: "Playbook body", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
75
|
+
trigger: { brief: "When this playbook applies", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
76
|
+
stepsJson: {
|
|
77
|
+
brief: "JSON array of steps (strings and/or {kind:'doc'|'rule'|'call'|'task',...} objects)",
|
|
78
|
+
kind: "parsed",
|
|
79
|
+
parse: parseAny,
|
|
80
|
+
placeholder: "json",
|
|
81
|
+
optional: true,
|
|
82
|
+
},
|
|
83
|
+
toolsJson: { brief: "JSON string array of tool names", kind: "parsed", parse: parseStringArray, placeholder: "json", optional: true },
|
|
84
|
+
labelsJson: { brief: "JSON string array of labels", kind: "parsed", parse: parseStringArray, placeholder: "json", optional: true },
|
|
85
|
+
extraJson: { brief: "JSON object of extra fields", kind: "parsed", parse: parseObject, placeholder: "json", optional: true },
|
|
86
|
+
argumentsJson: {
|
|
87
|
+
brief: "JSON array of declared argument definitions",
|
|
88
|
+
kind: "parsed",
|
|
89
|
+
parse: parseAny,
|
|
90
|
+
placeholder: "json",
|
|
91
|
+
optional: true,
|
|
92
|
+
},
|
|
93
|
+
projectRoot: { brief: "Project scope", kind: "parsed", parse: String, placeholder: "path", optional: true },
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
docs: { brief: "Create a Playbook" },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const listCommand = buildCommand({
|
|
100
|
+
func: async function (this: PlaybooksContext, flags: { status?: string; text?: string; limit?: number; projectRoot?: string }) {
|
|
101
|
+
const rows = await this.client.call<Record<string, unknown>, CliArtifact[]>("playbooks.list", {
|
|
102
|
+
status: flags.status,
|
|
103
|
+
text: flags.text,
|
|
104
|
+
limit: flags.limit,
|
|
105
|
+
project_root: flags.projectRoot,
|
|
106
|
+
});
|
|
107
|
+
render.call(this, rows, rows.length === 0 ? "No playbooks found." : rows.map((row) => artifactLabel(row)).join("\n"));
|
|
108
|
+
},
|
|
109
|
+
parameters: {
|
|
110
|
+
flags: {
|
|
111
|
+
status: { brief: "Filter by status", kind: "parsed", parse: String, placeholder: "status", optional: true },
|
|
112
|
+
text: { brief: "Substring match against title/body", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
113
|
+
limit: { brief: "Maximum playbooks to return", kind: "parsed", parse: numberParser, optional: true },
|
|
114
|
+
projectRoot: { brief: "Project scope", kind: "parsed", parse: String, placeholder: "path", optional: true },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
docs: { brief: "List Playbooks" },
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const showCommand = buildCommand({
|
|
121
|
+
func: async function (this: PlaybooksContext, _flags: Record<string, never>, id: string) {
|
|
122
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("playbooks.show", { id });
|
|
123
|
+
render.call(this, artifact, `${artifactLabel(artifact)}\n\n${artifact.body ?? ""}`);
|
|
124
|
+
},
|
|
125
|
+
parameters: { flags: {}, positional: { kind: "tuple", parameters: [{ brief: "Playbook id", parse: String, placeholder: "id" }] } },
|
|
126
|
+
docs: { brief: "Show one Playbook" },
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const previewCommand = buildCommand({
|
|
130
|
+
func: async function (this: PlaybooksContext, flags: { argumentsJson?: unknown[] | Record<string, unknown> }, id: string) {
|
|
131
|
+
const rendered = await this.client.call<Record<string, unknown>, string>("playbooks.preview", { id, arguments: flags.argumentsJson });
|
|
132
|
+
render.call(this, rendered, rendered);
|
|
133
|
+
},
|
|
134
|
+
parameters: {
|
|
135
|
+
flags: {
|
|
136
|
+
argumentsJson: {
|
|
137
|
+
brief: "JSON object supplying argument values",
|
|
138
|
+
kind: "parsed",
|
|
139
|
+
parse: parseAny,
|
|
140
|
+
placeholder: "json",
|
|
141
|
+
optional: true,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
positional: { kind: "tuple", parameters: [{ brief: "Playbook id", parse: String, placeholder: "id" }] },
|
|
145
|
+
},
|
|
146
|
+
docs: { brief: "Render a Playbook's whole composition tree as text, creating nothing" },
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const invokeCommand = buildCommand({
|
|
150
|
+
func: async function (
|
|
151
|
+
this: PlaybooksContext,
|
|
152
|
+
flags: { argumentsJson?: unknown[] | Record<string, unknown>; runId?: string; projectRoot?: string },
|
|
153
|
+
id: string,
|
|
154
|
+
) {
|
|
155
|
+
const invocation = await this.client.call<Record<string, unknown>, { entryTaskId: string; missingArguments?: string[] }>(
|
|
156
|
+
"playbooks.invoke",
|
|
157
|
+
{ id, arguments: flags.argumentsJson, run_id: flags.runId, project_root: flags.projectRoot },
|
|
158
|
+
);
|
|
159
|
+
render.call(
|
|
160
|
+
this,
|
|
161
|
+
invocation,
|
|
162
|
+
invocation.missingArguments
|
|
163
|
+
? `Missing required argument(s): ${invocation.missingArguments.join(", ")}.`
|
|
164
|
+
: `Invoked: entry task ${invocation.entryTaskId} focused. Drive it forward with \`tasks start/submit/complete\` like any other task.`,
|
|
165
|
+
);
|
|
166
|
+
},
|
|
167
|
+
parameters: {
|
|
168
|
+
flags: {
|
|
169
|
+
argumentsJson: {
|
|
170
|
+
brief: "JSON object supplying argument values",
|
|
171
|
+
kind: "parsed",
|
|
172
|
+
parse: parseAny,
|
|
173
|
+
placeholder: "json",
|
|
174
|
+
optional: true,
|
|
175
|
+
},
|
|
176
|
+
runId: { brief: "Run id to associate with this invocation", kind: "parsed", parse: String, placeholder: "id", optional: true },
|
|
177
|
+
projectRoot: { brief: "Project scope", kind: "parsed", parse: String, placeholder: "path", optional: true },
|
|
178
|
+
},
|
|
179
|
+
positional: { kind: "tuple", parameters: [{ brief: "Playbook id", parse: String, placeholder: "id" }] },
|
|
180
|
+
},
|
|
181
|
+
docs: { brief: "Compile a Playbook's steps into real Tasks and focus the first one" },
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
function buildEnableDisableCommand(action: "enable" | "disable") {
|
|
185
|
+
return buildCommand({
|
|
186
|
+
func: async function (this: PlaybooksContext, _flags: Record<string, never>, id: string) {
|
|
187
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>(`playbooks.${action}` as OperationName, { id });
|
|
188
|
+
render.call(this, artifact, artifactLabel(artifact));
|
|
189
|
+
},
|
|
190
|
+
parameters: { flags: {}, positional: { kind: "tuple", parameters: [{ brief: "Playbook id", parse: String, placeholder: "id" }] } },
|
|
191
|
+
docs: { brief: `${action[0]!.toUpperCase()}${action.slice(1)} a Playbook` },
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const assignProjectCommand = buildCommand({
|
|
196
|
+
func: async function (this: PlaybooksContext, _flags: Record<string, never>, id: string, projectRoot?: string) {
|
|
197
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("playbooks.assign_project", {
|
|
198
|
+
id,
|
|
199
|
+
project_root: projectRoot,
|
|
200
|
+
});
|
|
201
|
+
render.call(this, artifact, projectRoot ? `Assigned ${id} to ${projectRoot}` : `Unscoped ${id}`);
|
|
202
|
+
},
|
|
203
|
+
parameters: {
|
|
204
|
+
flags: {},
|
|
205
|
+
positional: {
|
|
206
|
+
kind: "tuple",
|
|
207
|
+
parameters: [
|
|
208
|
+
{ brief: "Playbook id", parse: String, placeholder: "id" },
|
|
209
|
+
{ brief: "New project scope, omit to unscope", parse: String, placeholder: "project-root", optional: true },
|
|
210
|
+
],
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
docs: { brief: "Reassign a Playbook's project scope, or unscope it" },
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const updateCommand = buildCommand({
|
|
217
|
+
func: async function (this: PlaybooksContext, flags: { title?: string; body?: string; labelsJson?: string[] }, id: string) {
|
|
218
|
+
if (flags.title === undefined && flags.body === undefined && flags.labelsJson === undefined)
|
|
219
|
+
throw new Error("playbooks update requires --title, --body, or --labels-json");
|
|
220
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("playbooks.update", {
|
|
221
|
+
id,
|
|
222
|
+
title: flags.title,
|
|
223
|
+
body: flags.body,
|
|
224
|
+
labels: flags.labelsJson,
|
|
225
|
+
});
|
|
226
|
+
render.call(this, artifact, artifactLabel(artifact));
|
|
227
|
+
},
|
|
228
|
+
parameters: {
|
|
229
|
+
flags: {
|
|
230
|
+
title: { brief: "New title", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
231
|
+
body: { brief: "New body", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
232
|
+
labelsJson: { brief: "JSON string array of labels", kind: "parsed", parse: parseStringArray, placeholder: "json", optional: true },
|
|
233
|
+
},
|
|
234
|
+
positional: { kind: "tuple", parameters: [{ brief: "Playbook id", parse: String, placeholder: "id" }] },
|
|
235
|
+
},
|
|
236
|
+
docs: { brief: "Change a Playbook's title/body/labels" },
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
function buildPairedIdCommand(
|
|
240
|
+
operation: OperationName,
|
|
241
|
+
fields: readonly [string, string],
|
|
242
|
+
human: (artifact: CliArtifact, second: string) => string,
|
|
243
|
+
brief: string,
|
|
244
|
+
) {
|
|
245
|
+
return buildCommand({
|
|
246
|
+
func: async function (this: PlaybooksContext, _flags: Record<string, never>, first: string, second: string) {
|
|
247
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>(operation, {
|
|
248
|
+
[fields[0]]: first,
|
|
249
|
+
[fields[1]]: second,
|
|
250
|
+
});
|
|
251
|
+
render.call(this, artifact, human(artifact, second));
|
|
252
|
+
},
|
|
253
|
+
parameters: {
|
|
254
|
+
flags: {},
|
|
255
|
+
positional: {
|
|
256
|
+
kind: "tuple",
|
|
257
|
+
parameters: [
|
|
258
|
+
{ brief: "First playbook id", parse: String, placeholder: "id" },
|
|
259
|
+
{ brief: "Second playbook id", parse: String, placeholder: "id" },
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
docs: { brief },
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const app = buildApplication(
|
|
268
|
+
buildRouteMap({
|
|
269
|
+
routes: {
|
|
270
|
+
create: createCommand,
|
|
271
|
+
list: listCommand,
|
|
272
|
+
show: showCommand,
|
|
273
|
+
preview: previewCommand,
|
|
274
|
+
invoke: invokeCommand,
|
|
275
|
+
enable: buildEnableDisableCommand("enable"),
|
|
276
|
+
disable: buildEnableDisableCommand("disable"),
|
|
277
|
+
"assign-project": assignProjectCommand,
|
|
278
|
+
update: updateCommand,
|
|
279
|
+
contain: buildPairedIdCommand(
|
|
280
|
+
"playbooks.contain",
|
|
281
|
+
["parent_id", "child_id"],
|
|
282
|
+
(artifact, second) => `Nested: ${second} → ${artifactLabel(artifact)}`,
|
|
283
|
+
"Nest a child Playbook inside a parent",
|
|
284
|
+
),
|
|
285
|
+
uncontain: buildPairedIdCommand(
|
|
286
|
+
"playbooks.uncontain",
|
|
287
|
+
["parent_id", "child_id"],
|
|
288
|
+
(artifact, second) => `Removed ${second} from ${artifactLabel(artifact)}`,
|
|
289
|
+
"Remove a parent/child Playbook nesting",
|
|
290
|
+
),
|
|
291
|
+
depend: buildPairedIdCommand(
|
|
292
|
+
"playbooks.depend",
|
|
293
|
+
["id", "dependency_id"],
|
|
294
|
+
(artifact, second) => `Dependency added: ${artifactLabel(artifact)} waits for ${second}`,
|
|
295
|
+
"Chain a prerequisite Playbook before another",
|
|
296
|
+
),
|
|
297
|
+
undepend: buildPairedIdCommand(
|
|
298
|
+
"playbooks.undepend",
|
|
299
|
+
["id", "dependency_id"],
|
|
300
|
+
(artifact, second) => `Dependency removed: ${artifactLabel(artifact)} no longer waits for ${second}`,
|
|
301
|
+
"Remove a Playbook dependency",
|
|
302
|
+
),
|
|
303
|
+
},
|
|
304
|
+
docs: { brief: "Playbook operations" },
|
|
305
|
+
}),
|
|
306
|
+
{ name: "playbooks", scanner: { caseStyle: "allow-kebab-for-camel" } },
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
export async function runPlaybooksCli(args: string[], client: PlaybooksClient): Promise<string> {
|
|
310
|
+
const json = args.includes("--json");
|
|
311
|
+
const positional = args.filter((arg) => arg !== "--json");
|
|
312
|
+
return runStricliToString(app, positional, { client, json });
|
|
313
|
+
}
|