@danypops/papyrus 0.44.9 → 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/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 +30 -1912
- package/src/handlers/discuss.ts +22 -20
- package/src/handlers/docs.ts +2 -28
- package/src/handlers/notes.ts +2 -29
- package/src/handlers/playbooks.ts +24 -90
- package/src/handlers/rules.ts +2 -28
- package/src/handlers/shared.ts +91 -1
- package/src/handlers/tasks.ts +31 -118
- package/src/modules/discuss.ts +5 -26
- package/src/modules/docs.ts +1 -22
- package/src/modules/graph-projection.ts +1 -8
- package/src/modules/logs.ts +1 -22
- package/src/modules/notes.ts +1 -22
- package/src/modules/operation-input.ts +35 -0
- package/src/modules/playbooks.ts +1 -22
- package/src/modules/rules.ts +1 -22
- package/src/modules/session-identity.ts +1 -15
- package/src/modules/tasks.ts +5 -33
- package/src/service.ts +1 -28
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { CommandContext } from "@stricli/core";
|
|
2
|
+
import { buildApplication, buildCommand, buildRouteMap } from "@stricli/core";
|
|
3
|
+
import type { PapyrusClient } from "../client.ts";
|
|
4
|
+
import type { GateResult } from "../domain/gate.ts";
|
|
5
|
+
import { runStricliToString } from "./stricli-run.ts";
|
|
6
|
+
|
|
7
|
+
type GatesClient = Pick<PapyrusClient, "call">;
|
|
8
|
+
|
|
9
|
+
interface GatesContext extends CommandContext {
|
|
10
|
+
readonly client: GatesClient;
|
|
11
|
+
readonly json: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const runGateCommand = buildCommand({
|
|
15
|
+
func: async function (this: GatesContext, _flags: Record<string, never>, id: string) {
|
|
16
|
+
const results = await this.client.call<Record<string, unknown>, GateResult[]>("gates.run", { id });
|
|
17
|
+
if (this.json) {
|
|
18
|
+
this.process.stdout.write(JSON.stringify(results));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
this.process.stdout.write(
|
|
22
|
+
results.length === 0
|
|
23
|
+
? "No gates configured."
|
|
24
|
+
: results.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target} — ${gate.output}`).join("\n"),
|
|
25
|
+
);
|
|
26
|
+
},
|
|
27
|
+
parameters: {
|
|
28
|
+
flags: {},
|
|
29
|
+
positional: {
|
|
30
|
+
kind: "tuple",
|
|
31
|
+
parameters: [{ brief: "Artifact id to run gates against", parse: String, placeholder: "id" }],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
docs: { brief: "Run every gate configured on an artifact" },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const app = buildApplication(buildRouteMap({ routes: { run: runGateCommand }, docs: { brief: "Gate operations" } }), {
|
|
38
|
+
name: "gates",
|
|
39
|
+
scanner: { caseStyle: "allow-kebab-for-camel" },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export async function runGatesCli(args: string[], client: GatesClient): Promise<string> {
|
|
43
|
+
const json = args.includes("--json");
|
|
44
|
+
const positional = args.filter((arg) => arg !== "--json");
|
|
45
|
+
return runStricliToString(app, positional, { client, json });
|
|
46
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { CommandContext } from "@stricli/core";
|
|
2
|
+
import { buildApplication, buildCommand, buildRouteMap, numberParser } from "@stricli/core";
|
|
3
|
+
import type { ArtifactEventPage } from "../artifact/artifact-event.ts";
|
|
4
|
+
import type { PapyrusClient } from "../client.ts";
|
|
5
|
+
import { artifactLabel, type CliArtifact } from "./shared.ts";
|
|
6
|
+
import { runStricliToString } from "./stricli-run.ts";
|
|
7
|
+
|
|
8
|
+
type GraphClient = Pick<PapyrusClient, "call">;
|
|
9
|
+
|
|
10
|
+
interface GraphContext extends CommandContext {
|
|
11
|
+
readonly client: GraphClient;
|
|
12
|
+
readonly json: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const linkCommand = buildCommand({
|
|
16
|
+
func: async function (this: GraphContext, _flags: Record<string, never>, from: string, relation: string, to: string) {
|
|
17
|
+
const result = await this.client.call<Record<string, unknown>, { ok: boolean }>("graph.link", { from, relation, to });
|
|
18
|
+
this.process.stdout.write(this.json ? JSON.stringify(result) : `Linked ${from} --${relation}--> ${to}`);
|
|
19
|
+
},
|
|
20
|
+
parameters: {
|
|
21
|
+
flags: {},
|
|
22
|
+
positional: {
|
|
23
|
+
kind: "tuple",
|
|
24
|
+
parameters: [
|
|
25
|
+
{ brief: "Source artifact id", parse: String, placeholder: "from" },
|
|
26
|
+
{ brief: "Relation name", parse: String, placeholder: "relation" },
|
|
27
|
+
{ brief: "Target artifact id", parse: String, placeholder: "to" },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
docs: { brief: "Link two artifacts" },
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const unlinkCommand = buildCommand({
|
|
35
|
+
func: async function (this: GraphContext, _flags: Record<string, never>, from: string, relation: string, to: string) {
|
|
36
|
+
const result = await this.client.call<Record<string, unknown>, { removed: boolean }>("graph.unlink", { from, relation, to });
|
|
37
|
+
if (this.json) {
|
|
38
|
+
this.process.stdout.write(JSON.stringify(result));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
this.process.stdout.write(
|
|
42
|
+
result.removed ? `Unlinked ${from} --${relation}--> ${to}` : `No such relationship: ${from} --${relation}--> ${to}`,
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
parameters: {
|
|
46
|
+
flags: {},
|
|
47
|
+
positional: {
|
|
48
|
+
kind: "tuple",
|
|
49
|
+
parameters: [
|
|
50
|
+
{ brief: "Source artifact id", parse: String, placeholder: "from" },
|
|
51
|
+
{ brief: "Relation name", parse: String, placeholder: "relation" },
|
|
52
|
+
{ brief: "Target artifact id", parse: String, placeholder: "to" },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
docs: { brief: "Remove a relationship between two artifacts" },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const treeCommand = buildCommand({
|
|
60
|
+
func: async function (this: GraphContext, flags: { depth?: number; maxNodes?: number }, id: string) {
|
|
61
|
+
const artifact = await this.client.call<
|
|
62
|
+
Record<string, unknown>,
|
|
63
|
+
CliArtifact & { edges?: Array<{ from: string; relation: string; to: string }> }
|
|
64
|
+
>("graph.tree", { id, depth: flags.depth, max_nodes: flags.maxNodes });
|
|
65
|
+
if (this.json) {
|
|
66
|
+
this.process.stdout.write(JSON.stringify(artifact));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const edges = artifact.edges ?? [];
|
|
70
|
+
this.process.stdout.write(
|
|
71
|
+
edges.length === 0
|
|
72
|
+
? `${artifactLabel(artifact)} — no edges`
|
|
73
|
+
: `${artifactLabel(artifact)}\n${edges.map((edge) => ` ${edge.from} --${edge.relation}--> ${edge.to}`).join("\n")}`,
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
parameters: {
|
|
77
|
+
flags: {
|
|
78
|
+
depth: { brief: "Traversal depth", kind: "parsed", parse: numberParser, optional: true },
|
|
79
|
+
maxNodes: { brief: "Maximum nodes to traverse", kind: "parsed", parse: numberParser, optional: true },
|
|
80
|
+
},
|
|
81
|
+
positional: { kind: "tuple", parameters: [{ brief: "Artifact id", parse: String, placeholder: "id" }] },
|
|
82
|
+
},
|
|
83
|
+
docs: { brief: "Show an artifact's relationship tree" },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const statusCommand = buildCommand({
|
|
87
|
+
func: async function (this: GraphContext, _flags: Record<string, never>, id: string, status: string) {
|
|
88
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("graph.status", { id, status });
|
|
89
|
+
this.process.stdout.write(this.json ? JSON.stringify(artifact) : `Updated ${artifact.id} → [${artifact.status}]`);
|
|
90
|
+
},
|
|
91
|
+
parameters: {
|
|
92
|
+
flags: {},
|
|
93
|
+
positional: {
|
|
94
|
+
kind: "tuple",
|
|
95
|
+
parameters: [
|
|
96
|
+
{ brief: "Artifact id", parse: String, placeholder: "id" },
|
|
97
|
+
{ brief: "New status", parse: String, placeholder: "status" },
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
docs: { brief: "Transition an artifact's status" },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const historyCommand = buildCommand({
|
|
105
|
+
func: async function (
|
|
106
|
+
this: GraphContext,
|
|
107
|
+
flags: {
|
|
108
|
+
id?: string;
|
|
109
|
+
actor?: string;
|
|
110
|
+
sessionId?: string;
|
|
111
|
+
since?: string;
|
|
112
|
+
limit?: number;
|
|
113
|
+
cursor?: number;
|
|
114
|
+
direction?: string;
|
|
115
|
+
},
|
|
116
|
+
) {
|
|
117
|
+
const page = await this.client.call<Record<string, unknown>, ArtifactEventPage>("graph.history", {
|
|
118
|
+
id: flags.id,
|
|
119
|
+
actor: flags.actor,
|
|
120
|
+
session_id: flags.sessionId,
|
|
121
|
+
since: flags.since,
|
|
122
|
+
limit: flags.limit,
|
|
123
|
+
cursor: flags.cursor,
|
|
124
|
+
direction: flags.direction,
|
|
125
|
+
});
|
|
126
|
+
if (this.json) {
|
|
127
|
+
this.process.stdout.write(JSON.stringify(page));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (page.events.length === 0) {
|
|
131
|
+
this.process.stdout.write("No recorded events.");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
this.process.stdout.write(
|
|
135
|
+
page.events
|
|
136
|
+
.map((event) => {
|
|
137
|
+
const transition =
|
|
138
|
+
event.fromStatus || event.toStatus ? ` ${event.fromStatus ?? "\u2205"} \u2192 ${event.toStatus ?? "\u2205"}` : "";
|
|
139
|
+
const relation = event.relation ? ` ${event.relation} \u2192 ${event.relatedId}` : "";
|
|
140
|
+
return `${event.occurredAt} ${event.artifactId} ${event.type}${transition}${relation} \u00b7 ${event.actor}/${event.source}${event.sessionId ? ` \u00b7 ${event.sessionId}` : ""}`;
|
|
141
|
+
})
|
|
142
|
+
.join("\n"),
|
|
143
|
+
);
|
|
144
|
+
},
|
|
145
|
+
parameters: {
|
|
146
|
+
flags: {
|
|
147
|
+
id: { brief: "Filter to one artifact id", kind: "parsed", parse: String, optional: true },
|
|
148
|
+
actor: { brief: "Filter to one actor", kind: "parsed", parse: String, optional: true },
|
|
149
|
+
sessionId: { brief: "Filter to one session id", kind: "parsed", parse: String, optional: true },
|
|
150
|
+
since: { brief: "Filter to events at or after this RFC3339 timestamp", kind: "parsed", parse: String, optional: true },
|
|
151
|
+
limit: { brief: "Maximum events to return", kind: "parsed", parse: numberParser, optional: true },
|
|
152
|
+
cursor: { brief: "Pagination cursor", kind: "parsed", parse: numberParser, optional: true },
|
|
153
|
+
direction: { brief: "Sort direction (asc|desc)", kind: "parsed", parse: String, optional: true },
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
docs: { brief: "Query the mutation event history" },
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const app = buildApplication(
|
|
160
|
+
buildRouteMap({
|
|
161
|
+
routes: { link: linkCommand, unlink: unlinkCommand, tree: treeCommand, status: statusCommand, history: historyCommand },
|
|
162
|
+
docs: { brief: "Graph operations" },
|
|
163
|
+
}),
|
|
164
|
+
{ name: "graph", scanner: { caseStyle: "allow-kebab-for-camel" } },
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
export async function runGraphCli(args: string[], client: GraphClient): Promise<string> {
|
|
168
|
+
const json = args.includes("--json");
|
|
169
|
+
const positional = args.filter((arg) => arg !== "--json");
|
|
170
|
+
return runStricliToString(app, positional, { client, json });
|
|
171
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { CommandContext } from "@stricli/core";
|
|
2
|
+
import { buildApplication, buildCommand, buildRouteMap } from "@stricli/core";
|
|
3
|
+
import type { PapyrusClient } from "../client.ts";
|
|
4
|
+
import { runStricliToString } from "./stricli-run.ts";
|
|
5
|
+
|
|
6
|
+
type GraphProjectionClient = Pick<PapyrusClient, "call">;
|
|
7
|
+
|
|
8
|
+
interface GraphProjectionContext extends CommandContext {
|
|
9
|
+
readonly client: GraphProjectionClient;
|
|
10
|
+
readonly json: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseBatchJson(value: string): Record<string, unknown> {
|
|
14
|
+
const parsed = JSON.parse(value) as unknown;
|
|
15
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("--batch-json must be a JSON object");
|
|
16
|
+
return parsed as Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const applyCommand = buildCommand({
|
|
20
|
+
func: async function (this: GraphProjectionContext, flags: { batchJson: Record<string, unknown> }) {
|
|
21
|
+
const result = await this.client.call<Record<string, unknown>, unknown>("graph_projection.apply", flags.batchJson);
|
|
22
|
+
this.process.stdout.write(this.json ? JSON.stringify(result) : JSON.stringify(result, null, 2));
|
|
23
|
+
},
|
|
24
|
+
parameters: {
|
|
25
|
+
flags: {
|
|
26
|
+
batchJson: {
|
|
27
|
+
brief: "JSON-encoded projection batch to apply",
|
|
28
|
+
kind: "parsed",
|
|
29
|
+
parse: parseBatchJson,
|
|
30
|
+
placeholder: "json",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
docs: {
|
|
35
|
+
brief: "Apply a projection batch",
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const checkpointCommand = buildCommand({
|
|
40
|
+
func: async function (this: GraphProjectionContext, flags: { producerId: string }) {
|
|
41
|
+
const result = await this.client.call<Record<string, unknown>, unknown>("graph_projection.checkpoint", {
|
|
42
|
+
producer_id: flags.producerId,
|
|
43
|
+
});
|
|
44
|
+
if (this.json) {
|
|
45
|
+
this.process.stdout.write(JSON.stringify(result));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this.process.stdout.write(
|
|
49
|
+
result === null ? `No projection checkpoint for producer "${flags.producerId}".` : JSON.stringify(result, null, 2),
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
parameters: {
|
|
53
|
+
flags: {
|
|
54
|
+
producerId: {
|
|
55
|
+
brief: "Producer id to look up the checkpoint for",
|
|
56
|
+
kind: "parsed",
|
|
57
|
+
parse: String,
|
|
58
|
+
placeholder: "id",
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
docs: {
|
|
63
|
+
brief: "Look up a producer's projection checkpoint",
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const app = buildApplication(
|
|
68
|
+
buildRouteMap({
|
|
69
|
+
routes: { apply: applyCommand, checkpoint: checkpointCommand },
|
|
70
|
+
docs: { brief: "Graph projection operations" },
|
|
71
|
+
}),
|
|
72
|
+
{ name: "graph-projection", scanner: { caseStyle: "allow-kebab-for-camel" } },
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
export async function runGraphProjectionCli(args: string[], client: GraphProjectionClient): Promise<string> {
|
|
76
|
+
const json = args.includes("--json");
|
|
77
|
+
const positional = args.filter((arg) => arg !== "--json");
|
|
78
|
+
return runStricliToString(app, positional, { client, json });
|
|
79
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
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 { runStricliToString } from "./stricli-run.ts";
|
|
5
|
+
|
|
6
|
+
type LogClient = Pick<PapyrusClient, "call">;
|
|
7
|
+
|
|
8
|
+
interface LogContext extends CommandContext {
|
|
9
|
+
readonly client: LogClient;
|
|
10
|
+
readonly json: boolean;
|
|
11
|
+
readonly projectRoot: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseFieldsJson(value: string): Record<string, unknown> {
|
|
15
|
+
const parsed = JSON.parse(value) as unknown;
|
|
16
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("--fields-json must be a JSON object");
|
|
17
|
+
return parsed as Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const appendCommand = buildCommand({
|
|
21
|
+
func: async function (
|
|
22
|
+
this: LogContext,
|
|
23
|
+
flags: {
|
|
24
|
+
source: string;
|
|
25
|
+
sourceLabel?: string;
|
|
26
|
+
level: string;
|
|
27
|
+
message: string;
|
|
28
|
+
operationId: string;
|
|
29
|
+
sessionId?: string;
|
|
30
|
+
occurredAt?: string;
|
|
31
|
+
fieldsJson?: Record<string, unknown>;
|
|
32
|
+
global: boolean;
|
|
33
|
+
},
|
|
34
|
+
) {
|
|
35
|
+
const result = await this.client.call("logs.append", {
|
|
36
|
+
source_id: flags.source,
|
|
37
|
+
...(flags.sourceLabel ? { source_label: flags.sourceLabel } : {}),
|
|
38
|
+
...(flags.global ? {} : { project_root: this.projectRoot }),
|
|
39
|
+
level: flags.level,
|
|
40
|
+
message: flags.message,
|
|
41
|
+
operation_id: flags.operationId,
|
|
42
|
+
...(flags.fieldsJson ? { fields: flags.fieldsJson } : {}),
|
|
43
|
+
...(flags.sessionId ? { session_id: flags.sessionId } : {}),
|
|
44
|
+
...(flags.occurredAt ? { occurred_at: flags.occurredAt } : {}),
|
|
45
|
+
});
|
|
46
|
+
this.process.stdout.write(this.json ? JSON.stringify(result) : JSON.stringify(result, null, 2));
|
|
47
|
+
},
|
|
48
|
+
parameters: {
|
|
49
|
+
flags: {
|
|
50
|
+
source: { brief: "Log source id", kind: "parsed", parse: String, placeholder: "id" },
|
|
51
|
+
sourceLabel: { brief: "Human-readable source label", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
52
|
+
level: { brief: "Log level (debug|info|warning|error)", kind: "parsed", parse: String, placeholder: "level" },
|
|
53
|
+
message: { brief: "Log message text", kind: "parsed", parse: String, placeholder: "text" },
|
|
54
|
+
operationId: { brief: "Idempotency key for this append", kind: "parsed", parse: String, placeholder: "id" },
|
|
55
|
+
sessionId: { brief: "Session id to attribute this entry to", kind: "parsed", parse: String, placeholder: "id", optional: true },
|
|
56
|
+
occurredAt: { brief: "ISO timestamp override", kind: "parsed", parse: String, placeholder: "iso", optional: true },
|
|
57
|
+
fieldsJson: { brief: "JSON-encoded structured fields", kind: "parsed", parse: parseFieldsJson, placeholder: "json", optional: true },
|
|
58
|
+
global: { brief: "Append outside any project scope", kind: "boolean" },
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
docs: { brief: "Append a log entry" },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const queryCommand = buildCommand({
|
|
65
|
+
func: async function (this: LogContext, flags: { source: string; since?: string; level?: string; limit?: number }) {
|
|
66
|
+
const result = await this.client.call<Record<string, unknown>, { entries: unknown[]; truncated: boolean }>("logs.query", {
|
|
67
|
+
source_id: flags.source,
|
|
68
|
+
...(flags.since ? { since: flags.since } : {}),
|
|
69
|
+
...(flags.level ? { level: flags.level } : {}),
|
|
70
|
+
...(flags.limit === undefined ? {} : { limit: flags.limit }),
|
|
71
|
+
});
|
|
72
|
+
if (this.json) {
|
|
73
|
+
this.process.stdout.write(JSON.stringify(result));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const lines = result.entries.map((entry) => JSON.stringify(entry));
|
|
77
|
+
this.process.stdout.write(
|
|
78
|
+
[...lines, result.truncated ? "(truncated -- more entries exist beyond this page)" : `(${lines.length} entries)`].join("\n"),
|
|
79
|
+
);
|
|
80
|
+
},
|
|
81
|
+
parameters: {
|
|
82
|
+
flags: {
|
|
83
|
+
source: { brief: "Log source id", kind: "parsed", parse: String, placeholder: "id" },
|
|
84
|
+
since: { brief: "Only entries at or after this ISO timestamp", kind: "parsed", parse: String, placeholder: "iso", optional: true },
|
|
85
|
+
level: { brief: "Filter to one level", kind: "parsed", parse: String, placeholder: "level", optional: true },
|
|
86
|
+
limit: { brief: "Maximum entries to return", kind: "parsed", parse: numberParser, optional: true },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
docs: { brief: "Query log entries" },
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const app = buildApplication(buildRouteMap({ routes: { append: appendCommand, query: queryCommand }, docs: { brief: "Log operations" } }), {
|
|
93
|
+
name: "log",
|
|
94
|
+
scanner: { caseStyle: "allow-kebab-for-camel" },
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
export async function runLogCli(args: string[], client: LogClient, projectRoot: string = process.cwd()): Promise<string> {
|
|
98
|
+
const json = args.includes("--json");
|
|
99
|
+
const positional = args.filter((arg) => arg !== "--json");
|
|
100
|
+
return runStricliToString(app, positional, { client, json, projectRoot });
|
|
101
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { CommandContext } from "@stricli/core";
|
|
2
|
+
import { buildApplication, buildCommand, buildRouteMap } from "@stricli/core";
|
|
3
|
+
import type { PapyrusClient } from "../client.ts";
|
|
4
|
+
import { runStricliToString } from "./stricli-run.ts";
|
|
5
|
+
|
|
6
|
+
type MigrationClient = Pick<PapyrusClient, "call">;
|
|
7
|
+
type MigrationResult = { from: number; to: number; applied: string[] };
|
|
8
|
+
|
|
9
|
+
interface MigrationContext extends CommandContext {
|
|
10
|
+
readonly client: MigrationClient;
|
|
11
|
+
readonly json: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const schemaCommand = buildCommand({
|
|
15
|
+
func: async function (this: MigrationContext) {
|
|
16
|
+
const result = await this.client.call<Record<string, never>, MigrationResult>("system.migrate", {});
|
|
17
|
+
if (this.json) {
|
|
18
|
+
this.process.stdout.write(JSON.stringify(result));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
this.process.stdout.write(
|
|
22
|
+
result.applied.length === 0
|
|
23
|
+
? `Schema already current at version ${result.to}.`
|
|
24
|
+
: `Migrated schema ${result.from} → ${result.to}: ${result.applied.join(", ")}`,
|
|
25
|
+
);
|
|
26
|
+
},
|
|
27
|
+
parameters: { flags: {} },
|
|
28
|
+
docs: { brief: "Run any pending schema migrations" },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const app = buildApplication(buildRouteMap({ routes: { schema: schemaCommand }, docs: { brief: "Migration operations" } }), {
|
|
32
|
+
name: "migrate",
|
|
33
|
+
scanner: { caseStyle: "allow-kebab-for-camel" },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export async function runMigrationCli(args: string[], client: MigrationClient): Promise<string> {
|
|
37
|
+
const json = args.includes("--json");
|
|
38
|
+
const positional = args.filter((arg) => arg !== "--json");
|
|
39
|
+
return runStricliToString(app, positional, { client, json });
|
|
40
|
+
}
|
|
@@ -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
|
+
}
|