@thehumanpatternlab/hpl 0.0.1-alpha.5 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +182 -42
- package/dist/bin/hpl.js +20 -145
- package/dist/src/commands/capabilities.js +24 -0
- package/dist/src/commands/health.js +26 -0
- package/dist/src/commands/notes/create.js +183 -0
- package/dist/src/commands/notes/get.js +52 -12
- package/dist/src/commands/notes/list.js +68 -12
- package/dist/src/commands/notes/notes.js +36 -0
- package/dist/src/commands/notes/notesSync.js +221 -0
- package/dist/src/commands/notes/update.js +217 -0
- package/dist/src/commands/notesSync.js +1 -1
- package/dist/src/commands/version.js +17 -0
- package/dist/src/contract/exitCodes.js +2 -0
- package/dist/src/contract/intents.js +14 -0
- package/dist/src/http/client.js +26 -0
- package/dist/src/index.js +8 -7
- package/dist/src/lib/config.js +8 -8
- package/dist/src/lib/contentRepo.js +49 -0
- package/dist/src/render/table.js +7 -0
- package/dist/src/render/text.js +68 -10
- package/dist/src/types/labNotes.js +96 -12
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -1,42 +1,182 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
1
|
+
<!--
|
|
2
|
+
HPL CLI
|
|
3
|
+
The Human Pattern Lab
|
|
4
|
+
|
|
5
|
+
This README is written for humans.
|
|
6
|
+
Design rationale lives in DESIGN.md.
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
# HPL CLI (Alpha) 🧭🦊
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+

|
|
13
|
+

|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
> **Status:** Alpha
|
|
17
|
+
> A modern, automation-safe CLI for The Human Pattern Lab.
|
|
18
|
+
|
|
19
|
+
**HPL** is the official command-line interface for **The Human Pattern Lab**.
|
|
20
|
+
|
|
21
|
+
Formerly developed under the codename **Skulk**, HPL is built to work just as well for humans at the keyboard as it does for automation, CI, and agent-driven workflows.
|
|
22
|
+
|
|
23
|
+
This package is in **active alpha development**. Interfaces are stabilizing, but iteration is expected.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## What HPL Connects To
|
|
28
|
+
|
|
29
|
+
HPL is a deterministic bridge between:
|
|
30
|
+
|
|
31
|
+
- the **Human Pattern Lab Content Repository** (source of truth)
|
|
32
|
+
- the **Human Pattern Lab API** (runtime index and operations)
|
|
33
|
+
|
|
34
|
+
Written content lives as Markdown in a dedicated content repository.
|
|
35
|
+
The API syncs and indexes that content so it can be rendered by user interfaces.
|
|
36
|
+
|
|
37
|
+
By default, HPL targets a Human Pattern Lab API instance. You can override the API endpoint with `--base-url` to use staging or a self-hosted deployment of the same API.
|
|
38
|
+
|
|
39
|
+
> Note: `--base-url` is intended for alternate deployments of the Human Pattern Lab API, not arbitrary third-party APIs.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Authentication
|
|
44
|
+
|
|
45
|
+
HPL supports token-based authentication via the `HPL_TOKEN` environment variable.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
export HPL_TOKEN="your-api-token"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
(Optional) Override the API endpoint:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
export HPL_BASE_URL="https://api.thehumanpatternlab.com"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
> `HPL_BASE_URL` should point to the **root** of a Human Pattern Lab API deployment.
|
|
58
|
+
> Do not include additional path segments.
|
|
59
|
+
|
|
60
|
+
Some API endpoints may require authentication depending on server configuration.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Quick Start
|
|
65
|
+
|
|
66
|
+
### Install (alpha)
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm install -g @thehumanpatternlab/hpl@alpha
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Sync Lab Notes from the content repository
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
hpl notes sync --content-repo AdaInTheLab/the-human-pattern-lab-content
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
This pulls structured Markdown content from the repository and synchronizes it into the Human Pattern Lab system.
|
|
79
|
+
|
|
80
|
+
### Machine-readable output
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
hpl --json notes sync
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Content Source Configuration (Optional)
|
|
89
|
+
|
|
90
|
+
By default, `notes sync` expects a content repository with the following structure:
|
|
91
|
+
|
|
92
|
+
```text
|
|
93
|
+
labnotes/
|
|
94
|
+
en/
|
|
95
|
+
*.md
|
|
96
|
+
ko/
|
|
97
|
+
*.md
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
You may pin a default content repository using an environment variable:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
export HPL_CONTENT_REPO="AdaInTheLab/the-human-pattern-lab-content"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
This allows `hpl notes sync` to run without explicitly passing `--content-repo`.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Commands
|
|
111
|
+
|
|
112
|
+
```text
|
|
113
|
+
hpl <domain> <action> [options]
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### notes
|
|
117
|
+
|
|
118
|
+
**Read operations:**
|
|
119
|
+
- `hpl notes list` - List all published Lab Notes
|
|
120
|
+
- `hpl notes get <slug>` - Get a specific Lab Note by slug
|
|
121
|
+
|
|
122
|
+
**Write operations:** (requires `HPL_TOKEN`)
|
|
123
|
+
- `hpl notes create --title "..." --slug "..." --file note.md` - Create a new Lab Note
|
|
124
|
+
- `hpl notes update <slug> --title "..." --file note.md` - Update an existing Lab Note
|
|
125
|
+
|
|
126
|
+
**Bulk operations:**
|
|
127
|
+
- `hpl notes sync --content-repo <owner/name|url>` - Sync from content repository
|
|
128
|
+
- `hpl notes sync --dir <path>` - Sync from local directory (advanced / local development)
|
|
129
|
+
|
|
130
|
+
See [WRITE_OPERATIONS_GUIDE.md](./WRITE_OPERATIONS_GUIDE.md) for detailed write operations documentation.
|
|
131
|
+
|
|
132
|
+
### health
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
hpl health
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### version
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
hpl version
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## JSON Output Contract
|
|
147
|
+
|
|
148
|
+
Structured output is treated as a **contract**, not a courtesy.
|
|
149
|
+
|
|
150
|
+
When `--json` is provided:
|
|
151
|
+
|
|
152
|
+
- stdout contains **only valid JSON**
|
|
153
|
+
- stderr is used for logs and diagnostics
|
|
154
|
+
- exit codes are deterministic
|
|
155
|
+
|
|
156
|
+
A verification step is included:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
npm run json:check
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
This command fails if any non-JSON output appears on stdout.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## What HPL Is Not
|
|
167
|
+
|
|
168
|
+
HPL is not:
|
|
169
|
+
- a chatbot interface
|
|
170
|
+
- an agent framework
|
|
171
|
+
- a memory system
|
|
172
|
+
- an inference layer
|
|
173
|
+
|
|
174
|
+
It is a command-line tool for interacting with Human Pattern Lab systems in a predictable, human-owned way.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
**The Human Pattern Lab**
|
|
179
|
+
https://thehumanpatternlab.com
|
|
180
|
+
|
|
181
|
+
*The lantern is lit.
|
|
182
|
+
The foxes are watching.*
|
package/dist/bin/hpl.js
CHANGED
|
@@ -2,157 +2,32 @@
|
|
|
2
2
|
/* ===========================================================
|
|
3
3
|
🌌 HUMAN PATTERN LAB — CLI ENTRYPOINT
|
|
4
4
|
-----------------------------------------------------------
|
|
5
|
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Contract: --json => JSON only on stdout
|
|
5
|
+
Purpose:
|
|
6
|
+
- Register top-level commands
|
|
7
|
+
- Define global flags (--json)
|
|
8
|
+
- Parse argv
|
|
9
|
+
Contract:
|
|
10
|
+
--json => JSON only on stdout (enforced in command handlers)
|
|
12
11
|
Notes:
|
|
13
|
-
|
|
12
|
+
Avoid process.exit() inside handlers (Windows + tsx stability).
|
|
14
13
|
=========================================================== */
|
|
15
14
|
import { Command } from "commander";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import { runNotesList } from "../src/commands/notes/list";
|
|
22
|
-
import { runNotesGet } from "../src/commands/notes/get";
|
|
23
|
-
import { renderTable } from "../src/render/table";
|
|
24
|
-
import { formatTags, safeLine, stripHtml } from "../src/render/text";
|
|
15
|
+
import { versionCommand } from "../src/commands/version.js";
|
|
16
|
+
import { capabilitiesCommand } from "../src/commands/capabilities.js";
|
|
17
|
+
import { healthCommand } from "../src/commands/health.js";
|
|
18
|
+
import { notesCommand } from "../src/commands/notes/notes.js";
|
|
19
|
+
import { EXIT } from "../src/contract/exitCodes.js";
|
|
25
20
|
const program = new Command();
|
|
26
21
|
program
|
|
27
22
|
.name("hpl")
|
|
28
23
|
.description("Human Pattern Lab CLI (alpha)")
|
|
29
24
|
.option("--json", "Emit contract JSON only on stdout")
|
|
30
|
-
.showHelpAfterError()
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
program
|
|
36
|
-
|
|
37
|
-
.
|
|
38
|
-
.action(() => {
|
|
39
|
-
const opts = program.opts();
|
|
40
|
-
const envelope = runVersion("version");
|
|
41
|
-
if (opts.json)
|
|
42
|
-
writeJson(envelope);
|
|
43
|
-
else
|
|
44
|
-
writeHuman(`${envelope.data.name} ${envelope.data.version}`);
|
|
45
|
-
setExit(EXIT.OK);
|
|
46
|
-
});
|
|
47
|
-
program
|
|
48
|
-
.command("capabilities")
|
|
49
|
-
.description("Show CLI capabilities for agents (contract: show_capabilities)")
|
|
50
|
-
.action(() => {
|
|
51
|
-
const opts = program.opts();
|
|
52
|
-
const envelope = runCapabilities("capabilities");
|
|
53
|
-
if (opts.json)
|
|
54
|
-
writeJson(envelope);
|
|
55
|
-
else {
|
|
56
|
-
writeHuman(`intentTier: ${envelope.data.intentTier}`);
|
|
57
|
-
writeHuman(`schemaVersions: ${envelope.data.schemaVersions.join(", ")}`);
|
|
58
|
-
writeHuman(`supportedIntents:`);
|
|
59
|
-
for (const i of envelope.data.supportedIntents)
|
|
60
|
-
writeHuman(` - ${i}`);
|
|
61
|
-
}
|
|
62
|
-
setExit(EXIT.OK);
|
|
63
|
-
});
|
|
64
|
-
program
|
|
65
|
-
.command("health")
|
|
66
|
-
.description("Check API health (contract: check_health)")
|
|
67
|
-
.action(async () => {
|
|
68
|
-
const opts = program.opts();
|
|
69
|
-
const result = await runHealth("health");
|
|
70
|
-
if (opts.json) {
|
|
71
|
-
writeJson(result.envelope);
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
if (result.envelope.status === "ok") {
|
|
75
|
-
const d = result.envelope.data;
|
|
76
|
-
const db = d.dbPath ? ` (db: ${d.dbPath})` : "";
|
|
77
|
-
writeHuman(`ok${db}`);
|
|
78
|
-
}
|
|
79
|
-
else {
|
|
80
|
-
const e = result.envelope.error;
|
|
81
|
-
writeHuman(`error: ${e.code} — ${e.message}`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
setExit(result.exitCode);
|
|
85
|
-
});
|
|
86
|
-
const notes = program.command("notes").description("Lab Notes commands");
|
|
87
|
-
notes
|
|
88
|
-
.command("list")
|
|
89
|
-
.description("List lab notes (contract: render_lab_note)")
|
|
90
|
-
.option("--limit <n>", "Limit number of rows (client-side)", (v) => parseInt(v, 10))
|
|
91
|
-
.action(async (cmdOpts) => {
|
|
92
|
-
const opts = program.opts();
|
|
93
|
-
const result = await runNotesList("notes list");
|
|
94
|
-
if (opts.json) {
|
|
95
|
-
writeJson(result.envelope);
|
|
96
|
-
setExit(result.exitCode);
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
if (result.envelope.status !== "ok") {
|
|
100
|
-
const e = result.envelope.error;
|
|
101
|
-
writeHuman(`error: ${e.code} — ${e.message}`);
|
|
102
|
-
setExit(result.exitCode);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
const data = result.envelope.data;
|
|
106
|
-
const rows = data.notes ?? [];
|
|
107
|
-
const limit = Number.isFinite(cmdOpts.limit) && cmdOpts.limit > 0 ? cmdOpts.limit : rows.length;
|
|
108
|
-
const slice = rows.slice(0, limit);
|
|
109
|
-
const table = renderTable(slice, [
|
|
110
|
-
{ header: "slug", width: 28, value: (n) => safeLine(String(n.slug ?? "")) },
|
|
111
|
-
{ header: "title", width: 34, value: (n) => safeLine(String(n.title ?? "")) },
|
|
112
|
-
{ header: "status", width: 10, value: (n) => safeLine(String(n.status ?? "-")) },
|
|
113
|
-
{ header: "dept", width: 8, value: (n) => safeLine(String(n.department_id ?? "-")) },
|
|
114
|
-
{ header: "tags", width: 22, value: (n) => formatTags(n.tags) },
|
|
115
|
-
]);
|
|
116
|
-
writeHuman(table);
|
|
117
|
-
writeHuman(`\ncount: ${data.count}`);
|
|
118
|
-
setExit(result.exitCode);
|
|
119
|
-
});
|
|
120
|
-
notes
|
|
121
|
-
.command("get")
|
|
122
|
-
.description("Get a lab note by slug (contract: render_lab_note)")
|
|
123
|
-
.argument("<slug>", "Lab Note slug")
|
|
124
|
-
.option("--raw", "Print raw contentHtml (no HTML stripping)")
|
|
125
|
-
.action(async (slug, cmdOpts) => {
|
|
126
|
-
const opts = program.opts();
|
|
127
|
-
const result = await runNotesGet(slug, "notes get");
|
|
128
|
-
if (opts.json) {
|
|
129
|
-
writeJson(result.envelope);
|
|
130
|
-
setExit(result.exitCode);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
if (result.envelope.status !== "ok") {
|
|
134
|
-
const e = result.envelope.error;
|
|
135
|
-
writeHuman(`error: ${e.code} — ${e.message}`);
|
|
136
|
-
setExit(result.exitCode);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
const n = result.envelope.data;
|
|
140
|
-
writeHuman(`# ${n.title}`);
|
|
141
|
-
writeHuman(`slug: ${n.slug}`);
|
|
142
|
-
if (n.status)
|
|
143
|
-
writeHuman(`status: ${n.status}`);
|
|
144
|
-
if (n.type)
|
|
145
|
-
writeHuman(`type: ${n.type}`);
|
|
146
|
-
if (n.department_id)
|
|
147
|
-
writeHuman(`department_id: ${n.department_id}`);
|
|
148
|
-
if (n.published)
|
|
149
|
-
writeHuman(`published: ${n.published}`);
|
|
150
|
-
if (Array.isArray(n.tags))
|
|
151
|
-
writeHuman(`tags: ${formatTags(n.tags)}`);
|
|
152
|
-
writeHuman("");
|
|
153
|
-
const body = cmdOpts.raw ? String(n.contentHtml ?? "") : stripHtml(String(n.contentHtml ?? ""));
|
|
154
|
-
writeHuman(body || "(no content)");
|
|
155
|
-
setExit(result.exitCode);
|
|
25
|
+
.showHelpAfterError()
|
|
26
|
+
.configureHelp({ helpWidth: 100 });
|
|
27
|
+
program.addCommand(versionCommand());
|
|
28
|
+
program.addCommand(capabilitiesCommand());
|
|
29
|
+
program.addCommand(healthCommand());
|
|
30
|
+
program.addCommand(notesCommand());
|
|
31
|
+
program.parseAsync(process.argv).catch(() => {
|
|
32
|
+
process.exitCode = EXIT.UNKNOWN;
|
|
156
33
|
});
|
|
157
|
-
// Let commander handle errors; set exit code without hard exit.
|
|
158
|
-
program.parseAsync(process.argv).catch(() => setExit(EXIT.UNKNOWN));
|
|
@@ -1,9 +1,33 @@
|
|
|
1
1
|
/* ===========================================================
|
|
2
2
|
🌌 HUMAN PATTERN LAB — COMMAND: capabilities
|
|
3
3
|
=========================================================== */
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { writeHuman, writeJson } from "../io.js";
|
|
6
|
+
import { EXIT } from "../contract/exitCodes.js";
|
|
4
7
|
import { getAlphaIntent } from "../contract/intents";
|
|
5
8
|
import { ok } from "../contract/envelope";
|
|
6
9
|
import { getCapabilitiesAlpha } from "../contract/capabilities";
|
|
10
|
+
export function capabilitiesCommand() {
|
|
11
|
+
return new Command("capabilities")
|
|
12
|
+
.description("Show CLI capabilities for agents (contract: show_capabilities)")
|
|
13
|
+
.action((...args) => {
|
|
14
|
+
const cmd = args[args.length - 1];
|
|
15
|
+
const rootOpts = (cmd.parent?.opts?.() ?? {});
|
|
16
|
+
const envelope = runCapabilities("capabilities");
|
|
17
|
+
if (rootOpts.json) {
|
|
18
|
+
writeJson(envelope);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
const d = envelope.data ?? {};
|
|
22
|
+
writeHuman(`intentTier: ${d.intentTier ?? "-"}`);
|
|
23
|
+
writeHuman(`schemaVersions: ${(d.schemaVersions ?? []).join(", ")}`);
|
|
24
|
+
writeHuman(`supportedIntents:`);
|
|
25
|
+
for (const i of d.supportedIntents ?? [])
|
|
26
|
+
writeHuman(` - ${i}`);
|
|
27
|
+
}
|
|
28
|
+
process.exitCode = EXIT.OK;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
7
31
|
export function runCapabilities(commandName = "capabilities") {
|
|
8
32
|
const intent = getAlphaIntent("show_capabilities");
|
|
9
33
|
return ok(commandName, intent, getCapabilitiesAlpha());
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/* ===========================================================
|
|
2
2
|
🌌 HUMAN PATTERN LAB — COMMAND: health
|
|
3
3
|
=========================================================== */
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { writeHuman, writeJson } from "../io.js";
|
|
4
6
|
import { z } from "zod";
|
|
5
7
|
import { getAlphaIntent } from "../contract/intents";
|
|
6
8
|
import { ok, err } from "../contract/envelope";
|
|
@@ -10,6 +12,30 @@ const HealthSchema = z.object({
|
|
|
10
12
|
status: z.string(),
|
|
11
13
|
dbPath: z.string().optional(),
|
|
12
14
|
});
|
|
15
|
+
export function healthCommand() {
|
|
16
|
+
return new Command("health")
|
|
17
|
+
.description("Check API health (contract: check_health)")
|
|
18
|
+
.action(async (...args) => {
|
|
19
|
+
const cmd = args[args.length - 1];
|
|
20
|
+
const rootOpts = (cmd.parent?.opts?.() ?? {});
|
|
21
|
+
const result = await runHealth("health");
|
|
22
|
+
if (rootOpts.json) {
|
|
23
|
+
writeJson(result.envelope);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
if (result.envelope.status === "ok") {
|
|
27
|
+
const d = result.envelope.data ?? {};
|
|
28
|
+
const db = d.dbPath ? ` (db: ${d.dbPath})` : "";
|
|
29
|
+
writeHuman(`ok${db}`);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const e = result.envelope.error ?? {};
|
|
33
|
+
writeHuman(`error: ${e.code ?? "E_UNKNOWN"} — ${e.message ?? "unknown"}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
process.exitCode = result.exitCode ?? EXIT.UNKNOWN;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
13
39
|
export async function runHealth(commandName = "health") {
|
|
14
40
|
const intent = getAlphaIntent("check_health");
|
|
15
41
|
try {
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/* ===========================================================
|
|
2
|
+
🦊 THE HUMAN PATTERN LAB — HPL CLI
|
|
3
|
+
-----------------------------------------------------------
|
|
4
|
+
File: create.ts
|
|
5
|
+
Role: Notes subcommand: `hpl notes create`
|
|
6
|
+
Author: Ada (The Human Pattern Lab)
|
|
7
|
+
Assistant: Claude
|
|
8
|
+
Lab Unit: SCMS — Systems & Code Management Suite
|
|
9
|
+
Status: Active
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
Purpose:
|
|
12
|
+
Create a new Lab Note via API using the upsert endpoint.
|
|
13
|
+
Supports both markdown file input and inline content.
|
|
14
|
+
-----------------------------------------------------------
|
|
15
|
+
Design:
|
|
16
|
+
- Core function returns { envelope, exitCode }
|
|
17
|
+
- Commander adapter decides json vs human rendering
|
|
18
|
+
- Requires authentication via HPL_TOKEN
|
|
19
|
+
=========================================================== */
|
|
20
|
+
import fs from "node:fs";
|
|
21
|
+
import { Command } from "commander";
|
|
22
|
+
import { getOutputMode, printJson } from "../../cli/output.js";
|
|
23
|
+
import { renderText } from "../../render/text.js";
|
|
24
|
+
import { LabNoteUpsertSchema } from "../../types/labNotes.js";
|
|
25
|
+
import { HPL_TOKEN } from "../../lib/config.js";
|
|
26
|
+
import { getAlphaIntent } from "../../contract/intents.js";
|
|
27
|
+
import { ok, err } from "../../contract/envelope.js";
|
|
28
|
+
import { EXIT } from "../../contract/exitCodes.js";
|
|
29
|
+
import { postJson, HttpError } from "../../http/client.js";
|
|
30
|
+
/**
|
|
31
|
+
* Core: create a new Lab Note.
|
|
32
|
+
* Returns structured envelope + exitCode (no printing here).
|
|
33
|
+
*/
|
|
34
|
+
export async function runNotesCreate(options, commandName = "notes.create") {
|
|
35
|
+
const intent = getAlphaIntent("create_lab_note");
|
|
36
|
+
// Authentication check
|
|
37
|
+
const token = HPL_TOKEN();
|
|
38
|
+
if (!token) {
|
|
39
|
+
return {
|
|
40
|
+
envelope: err(commandName, intent, {
|
|
41
|
+
code: "E_AUTH",
|
|
42
|
+
message: "Authentication required. Set HPL_TOKEN environment variable or configure token in ~/.humanpatternlab/hpl.json",
|
|
43
|
+
}),
|
|
44
|
+
exitCode: EXIT.AUTH,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// Get markdown content
|
|
48
|
+
let markdown;
|
|
49
|
+
if (options.file) {
|
|
50
|
+
if (!fs.existsSync(options.file)) {
|
|
51
|
+
return {
|
|
52
|
+
envelope: err(commandName, intent, {
|
|
53
|
+
code: "E_NOT_FOUND",
|
|
54
|
+
message: `File not found: ${options.file}`,
|
|
55
|
+
}),
|
|
56
|
+
exitCode: EXIT.NOT_FOUND,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
markdown = fs.readFileSync(options.file, "utf-8");
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
64
|
+
return {
|
|
65
|
+
envelope: err(commandName, intent, {
|
|
66
|
+
code: "E_IO",
|
|
67
|
+
message: `Failed to read file: ${msg}`,
|
|
68
|
+
}),
|
|
69
|
+
exitCode: EXIT.IO,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else if (options.markdown) {
|
|
74
|
+
markdown = options.markdown;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
return {
|
|
78
|
+
envelope: err(commandName, intent, {
|
|
79
|
+
code: "E_VALIDATION",
|
|
80
|
+
message: "Either --markdown or --file is required",
|
|
81
|
+
}),
|
|
82
|
+
exitCode: EXIT.VALIDATION,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// Build payload
|
|
86
|
+
const payload = {
|
|
87
|
+
slug: options.slug,
|
|
88
|
+
title: options.title,
|
|
89
|
+
markdown,
|
|
90
|
+
locale: options.locale,
|
|
91
|
+
subtitle: options.subtitle,
|
|
92
|
+
summary: options.summary,
|
|
93
|
+
tags: options.tags,
|
|
94
|
+
published: options.published,
|
|
95
|
+
status: options.status,
|
|
96
|
+
type: options.type,
|
|
97
|
+
dept: options.dept,
|
|
98
|
+
};
|
|
99
|
+
// Validate payload
|
|
100
|
+
const parsed = LabNoteUpsertSchema.safeParse(payload);
|
|
101
|
+
if (!parsed.success) {
|
|
102
|
+
return {
|
|
103
|
+
envelope: err(commandName, intent, {
|
|
104
|
+
code: "E_VALIDATION",
|
|
105
|
+
message: "Invalid note data",
|
|
106
|
+
details: parsed.error.flatten(),
|
|
107
|
+
}),
|
|
108
|
+
exitCode: EXIT.VALIDATION,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
// Make API request
|
|
112
|
+
try {
|
|
113
|
+
const response = await postJson("/lab-notes/upsert", parsed.data, token);
|
|
114
|
+
return {
|
|
115
|
+
envelope: ok(commandName, intent, {
|
|
116
|
+
slug: response.slug,
|
|
117
|
+
action: response.action ?? "created",
|
|
118
|
+
message: `Lab Note ${response.action ?? "created"}: ${response.slug}`,
|
|
119
|
+
}),
|
|
120
|
+
exitCode: EXIT.OK,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
if (e instanceof HttpError) {
|
|
125
|
+
if (e.status === 401 || e.status === 403) {
|
|
126
|
+
return {
|
|
127
|
+
envelope: err(commandName, intent, {
|
|
128
|
+
code: "E_AUTH",
|
|
129
|
+
message: "Authentication failed. Check your HPL_TOKEN.",
|
|
130
|
+
}),
|
|
131
|
+
exitCode: EXIT.AUTH,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const code = e.status && e.status >= 500 ? "E_SERVER" : "E_HTTP";
|
|
135
|
+
return {
|
|
136
|
+
envelope: err(commandName, intent, {
|
|
137
|
+
code,
|
|
138
|
+
message: `API request failed (${e.status ?? "unknown"})`,
|
|
139
|
+
details: e.body ? e.body.slice(0, 500) : undefined,
|
|
140
|
+
}),
|
|
141
|
+
exitCode: e.status && e.status >= 500 ? EXIT.SERVER : EXIT.NETWORK,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
145
|
+
return {
|
|
146
|
+
envelope: err(commandName, intent, {
|
|
147
|
+
code: "E_UNKNOWN",
|
|
148
|
+
message: msg,
|
|
149
|
+
}),
|
|
150
|
+
exitCode: EXIT.UNKNOWN,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Commander: `hpl notes create`
|
|
156
|
+
*/
|
|
157
|
+
export function notesCreateSubcommand() {
|
|
158
|
+
return new Command("create")
|
|
159
|
+
.description("Create a new Lab Note (contract: create_lab_note)")
|
|
160
|
+
.requiredOption("--title <title>", "Note title")
|
|
161
|
+
.requiredOption("--slug <slug>", "Note slug (unique identifier)")
|
|
162
|
+
.option("--markdown <text>", "Markdown content (inline)")
|
|
163
|
+
.option("--file <path>", "Path to markdown file")
|
|
164
|
+
.option("--locale <code>", "Locale code (default: en)", "en")
|
|
165
|
+
.option("--subtitle <text>", "Note subtitle")
|
|
166
|
+
.option("--summary <text>", "Note summary")
|
|
167
|
+
.option("--tags <tags>", "Comma-separated tags", (val) => val.split(",").map((t) => t.trim()))
|
|
168
|
+
.option("--published <date>", "Publication date (ISO format)")
|
|
169
|
+
.option("--status <status>", "Note status (draft|published|archived)", "draft")
|
|
170
|
+
.option("--type <type>", "Note type (labnote|paper|memo|lore|weather)", "labnote")
|
|
171
|
+
.option("--dept <dept>", "Department code")
|
|
172
|
+
.action(async (opts, cmd) => {
|
|
173
|
+
const mode = getOutputMode(cmd);
|
|
174
|
+
const { envelope, exitCode } = await runNotesCreate(opts, "notes.create");
|
|
175
|
+
if (mode === "json") {
|
|
176
|
+
printJson(envelope);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
renderText(envelope);
|
|
180
|
+
}
|
|
181
|
+
process.exitCode = exitCode;
|
|
182
|
+
});
|
|
183
|
+
}
|