@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,192 @@
|
|
|
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 DocsClient = Pick<PapyrusClient, "call">;
|
|
9
|
+
|
|
10
|
+
interface DocsContext extends CommandContext {
|
|
11
|
+
readonly client: DocsClient;
|
|
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
|
+
function render(this: DocsContext, result: unknown, human: string): void {
|
|
28
|
+
this.process.stdout.write(this.json ? JSON.stringify(result) : human);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const createCommand = buildCommand({
|
|
32
|
+
func: async function (
|
|
33
|
+
this: DocsContext,
|
|
34
|
+
flags: {
|
|
35
|
+
title: string;
|
|
36
|
+
body?: string;
|
|
37
|
+
subtype?: string;
|
|
38
|
+
labelsJson?: string[];
|
|
39
|
+
extraJson?: Record<string, unknown>;
|
|
40
|
+
templateId?: string;
|
|
41
|
+
projectRoot?: string;
|
|
42
|
+
},
|
|
43
|
+
) {
|
|
44
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("docs.create", {
|
|
45
|
+
title: flags.title,
|
|
46
|
+
body: flags.body,
|
|
47
|
+
subtype: flags.subtype,
|
|
48
|
+
labels: flags.labelsJson,
|
|
49
|
+
extra: flags.extraJson,
|
|
50
|
+
template_id: flags.templateId,
|
|
51
|
+
project_root: flags.projectRoot,
|
|
52
|
+
});
|
|
53
|
+
render.call(this, artifact, `Created document: ${artifactLabel(artifact)}`);
|
|
54
|
+
},
|
|
55
|
+
parameters: {
|
|
56
|
+
flags: {
|
|
57
|
+
title: { brief: "Document title", kind: "parsed", parse: String, placeholder: "text" },
|
|
58
|
+
body: { brief: "Document body", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
59
|
+
subtype: { brief: "Document subtype", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
60
|
+
labelsJson: { brief: "JSON string array of labels", kind: "parsed", parse: parseStringArray, placeholder: "json", optional: true },
|
|
61
|
+
extraJson: { brief: "JSON object of extra fields", kind: "parsed", parse: parseObject, placeholder: "json", optional: true },
|
|
62
|
+
templateId: { brief: "Template to instantiate from", kind: "parsed", parse: String, placeholder: "id", optional: true },
|
|
63
|
+
projectRoot: { brief: "Project scope", kind: "parsed", parse: String, placeholder: "path", optional: true },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
docs: { brief: "Create a Doc" },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const listCommand = buildCommand({
|
|
70
|
+
func: async function (this: DocsContext, flags: { status?: string; text?: string; limit?: number; projectRoot?: string }) {
|
|
71
|
+
const rows = await this.client.call<Record<string, unknown>, CliArtifact[]>("docs.list", {
|
|
72
|
+
status: flags.status,
|
|
73
|
+
text: flags.text,
|
|
74
|
+
limit: flags.limit,
|
|
75
|
+
project_root: flags.projectRoot,
|
|
76
|
+
});
|
|
77
|
+
render.call(this, rows, rows.length === 0 ? "No documents found." : rows.map((row) => artifactLabel(row)).join("\n"));
|
|
78
|
+
},
|
|
79
|
+
parameters: {
|
|
80
|
+
flags: {
|
|
81
|
+
status: { brief: "Filter by status", kind: "parsed", parse: String, placeholder: "status", optional: true },
|
|
82
|
+
text: { brief: "Substring match against title/body", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
83
|
+
limit: { brief: "Maximum docs to return", kind: "parsed", parse: numberParser, optional: true },
|
|
84
|
+
projectRoot: { brief: "Project scope", kind: "parsed", parse: String, placeholder: "path", optional: true },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
docs: { brief: "List Docs" },
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const assignProjectCommand = buildCommand({
|
|
91
|
+
func: async function (this: DocsContext, _flags: Record<string, never>, id: string, projectRoot?: string) {
|
|
92
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("docs.assign_project", { id, project_root: projectRoot });
|
|
93
|
+
render.call(this, artifact, projectRoot ? `Assigned ${id} to ${projectRoot}` : `Unscoped ${id}`);
|
|
94
|
+
},
|
|
95
|
+
parameters: {
|
|
96
|
+
flags: {},
|
|
97
|
+
positional: {
|
|
98
|
+
kind: "tuple",
|
|
99
|
+
parameters: [
|
|
100
|
+
{ brief: "Document id", parse: String, placeholder: "id" },
|
|
101
|
+
{ brief: "New project scope, omit to unscope", parse: String, placeholder: "project-root", optional: true },
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
docs: { brief: "Reassign a Doc's project scope, or unscope it" },
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const showCommand = buildCommand({
|
|
109
|
+
func: async function (this: DocsContext, _flags: Record<string, never>, id: string) {
|
|
110
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("docs.show", { id });
|
|
111
|
+
render.call(this, artifact, `${artifactLabel(artifact)}\n\n${artifact.body ?? ""}`);
|
|
112
|
+
},
|
|
113
|
+
parameters: { flags: {}, positional: { kind: "tuple", parameters: [{ brief: "Document id", parse: String, placeholder: "id" }] } },
|
|
114
|
+
docs: { brief: "Show one Doc" },
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
function buildStatusTransitionCommand(action: "activate" | "archive" | "reopen") {
|
|
118
|
+
return buildCommand({
|
|
119
|
+
func: async function (this: DocsContext, _flags: Record<string, never>, id: string) {
|
|
120
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>(`docs.${action}` as OperationName, { id });
|
|
121
|
+
render.call(this, artifact, artifactLabel(artifact));
|
|
122
|
+
},
|
|
123
|
+
parameters: { flags: {}, positional: { kind: "tuple", parameters: [{ brief: "Document id", parse: String, placeholder: "id" }] } },
|
|
124
|
+
docs: { brief: `${action[0]!.toUpperCase()}${action.slice(1)} a Doc` },
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const linkCommand = buildCommand({
|
|
129
|
+
func: async function (this: DocsContext, _flags: Record<string, never>, id: string, relation: string, targetId: string) {
|
|
130
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("docs.link", { id, relation, target_id: targetId });
|
|
131
|
+
render.call(this, artifact, `Linked ${id} --${relation}--> ${targetId}`);
|
|
132
|
+
},
|
|
133
|
+
parameters: {
|
|
134
|
+
flags: {},
|
|
135
|
+
positional: {
|
|
136
|
+
kind: "tuple",
|
|
137
|
+
parameters: [
|
|
138
|
+
{ brief: "Document id", parse: String, placeholder: "id" },
|
|
139
|
+
{ brief: "Relation name", parse: String, placeholder: "relation" },
|
|
140
|
+
{ brief: "Target artifact id", parse: String, placeholder: "target-id" },
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
docs: { brief: "Link a Doc to another artifact" },
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const updateCommand = buildCommand({
|
|
148
|
+
func: async function (this: DocsContext, flags: { title?: string; body?: string; labelsJson?: string[] }, id: string) {
|
|
149
|
+
if (flags.title === undefined && flags.body === undefined && flags.labelsJson === undefined)
|
|
150
|
+
throw new Error("docs update requires --title, --body, or --labels-json");
|
|
151
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("docs.update", {
|
|
152
|
+
id,
|
|
153
|
+
title: flags.title,
|
|
154
|
+
body: flags.body,
|
|
155
|
+
labels: flags.labelsJson,
|
|
156
|
+
});
|
|
157
|
+
render.call(this, artifact, artifactLabel(artifact));
|
|
158
|
+
},
|
|
159
|
+
parameters: {
|
|
160
|
+
flags: {
|
|
161
|
+
title: { brief: "New title", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
162
|
+
body: { brief: "New body", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
163
|
+
labelsJson: { brief: "JSON string array of labels", kind: "parsed", parse: parseStringArray, placeholder: "json", optional: true },
|
|
164
|
+
},
|
|
165
|
+
positional: { kind: "tuple", parameters: [{ brief: "Document id", parse: String, placeholder: "id" }] },
|
|
166
|
+
},
|
|
167
|
+
docs: { brief: "Change a Doc's title/body/labels" },
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const app = buildApplication(
|
|
171
|
+
buildRouteMap({
|
|
172
|
+
routes: {
|
|
173
|
+
create: createCommand,
|
|
174
|
+
list: listCommand,
|
|
175
|
+
"assign-project": assignProjectCommand,
|
|
176
|
+
show: showCommand,
|
|
177
|
+
activate: buildStatusTransitionCommand("activate"),
|
|
178
|
+
archive: buildStatusTransitionCommand("archive"),
|
|
179
|
+
reopen: buildStatusTransitionCommand("reopen"),
|
|
180
|
+
link: linkCommand,
|
|
181
|
+
update: updateCommand,
|
|
182
|
+
},
|
|
183
|
+
docs: { brief: "Doc operations" },
|
|
184
|
+
}),
|
|
185
|
+
{ name: "docs", scanner: { caseStyle: "allow-kebab-for-camel" } },
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
export async function runDocsCli(args: string[], client: DocsClient): Promise<string> {
|
|
189
|
+
const json = args.includes("--json");
|
|
190
|
+
const positional = args.filter((arg) => arg !== "--json");
|
|
191
|
+
return runStricliToString(app, positional, { client, json });
|
|
192
|
+
}
|
|
@@ -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
|
+
}
|