@thehumanpatternlab/hpl 0.0.1-alpha.6 → 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 -173
- package/dist/src/commands/notes/create.js +183 -0
- package/dist/src/commands/notes/notes.js +4 -0
- package/dist/src/commands/notes/update.js +217 -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/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,173 +1,182 @@
|
|
|
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
|
-
|
|
119
|
-
- `hpl notes
|
|
120
|
-
- `hpl notes
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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.*
|
|
@@ -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
|
+
}
|
|
@@ -22,11 +22,15 @@ import { Command } from "commander";
|
|
|
22
22
|
import { notesListSubcommand } from "./list.js";
|
|
23
23
|
import { notesGetSubcommand } from "./get.js";
|
|
24
24
|
import { notesSyncSubcommand } from "./notesSync.js";
|
|
25
|
+
import { notesCreateSubcommand } from "./create.js";
|
|
26
|
+
import { notesUpdateSubcommand } from "./update.js";
|
|
25
27
|
export function notesCommand() {
|
|
26
28
|
const notes = new Command("notes").description("Lab Notes commands");
|
|
27
29
|
// Subcommands
|
|
28
30
|
notes.addCommand(notesListSubcommand());
|
|
29
31
|
notes.addCommand(notesGetSubcommand());
|
|
32
|
+
notes.addCommand(notesCreateSubcommand());
|
|
33
|
+
notes.addCommand(notesUpdateSubcommand());
|
|
30
34
|
notes.addCommand(notesSyncSubcommand());
|
|
31
35
|
return notes;
|
|
32
36
|
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/* ===========================================================
|
|
2
|
+
🦊 THE HUMAN PATTERN LAB — HPL CLI
|
|
3
|
+
-----------------------------------------------------------
|
|
4
|
+
File: update.ts
|
|
5
|
+
Role: Notes subcommand: `hpl notes update`
|
|
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
|
+
Update an existing 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
|
+
- Uses upsert endpoint (same as create)
|
|
20
|
+
=========================================================== */
|
|
21
|
+
import fs from "node:fs";
|
|
22
|
+
import { Command } from "commander";
|
|
23
|
+
import { getOutputMode, printJson } from "../../cli/output.js";
|
|
24
|
+
import { renderText } from "../../render/text.js";
|
|
25
|
+
import { LabNoteUpsertSchema } from "../../types/labNotes.js";
|
|
26
|
+
import { HPL_TOKEN } from "../../lib/config.js";
|
|
27
|
+
import { getAlphaIntent } from "../../contract/intents.js";
|
|
28
|
+
import { ok, err } from "../../contract/envelope.js";
|
|
29
|
+
import { EXIT } from "../../contract/exitCodes.js";
|
|
30
|
+
import { postJson, HttpError } from "../../http/client.js";
|
|
31
|
+
/**
|
|
32
|
+
* Core: update an existing Lab Note.
|
|
33
|
+
* Returns structured envelope + exitCode (no printing here).
|
|
34
|
+
*/
|
|
35
|
+
export async function runNotesUpdate(options, commandName = "notes.update") {
|
|
36
|
+
const intent = getAlphaIntent("update_lab_note");
|
|
37
|
+
// Authentication check
|
|
38
|
+
const token = HPL_TOKEN();
|
|
39
|
+
if (!token) {
|
|
40
|
+
return {
|
|
41
|
+
envelope: err(commandName, intent, {
|
|
42
|
+
code: "E_AUTH",
|
|
43
|
+
message: "Authentication required. Set HPL_TOKEN environment variable or configure token in ~/.humanpatternlab/hpl.json",
|
|
44
|
+
}),
|
|
45
|
+
exitCode: EXIT.AUTH,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// Get markdown content if provided
|
|
49
|
+
let markdown;
|
|
50
|
+
if (options.file) {
|
|
51
|
+
if (!fs.existsSync(options.file)) {
|
|
52
|
+
return {
|
|
53
|
+
envelope: err(commandName, intent, {
|
|
54
|
+
code: "E_NOT_FOUND",
|
|
55
|
+
message: `File not found: ${options.file}`,
|
|
56
|
+
}),
|
|
57
|
+
exitCode: EXIT.NOT_FOUND,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
markdown = fs.readFileSync(options.file, "utf-8");
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
65
|
+
return {
|
|
66
|
+
envelope: err(commandName, intent, {
|
|
67
|
+
code: "E_IO",
|
|
68
|
+
message: `Failed to read file: ${msg}`,
|
|
69
|
+
}),
|
|
70
|
+
exitCode: EXIT.IO,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else if (options.markdown) {
|
|
75
|
+
markdown = options.markdown;
|
|
76
|
+
}
|
|
77
|
+
// For updates, we need at least title OR markdown
|
|
78
|
+
if (!options.title && !markdown) {
|
|
79
|
+
return {
|
|
80
|
+
envelope: err(commandName, intent, {
|
|
81
|
+
code: "E_VALIDATION",
|
|
82
|
+
message: "Must provide at least --title or --markdown/--file for update",
|
|
83
|
+
}),
|
|
84
|
+
exitCode: EXIT.VALIDATION,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// Build payload - use required fields from what's provided
|
|
88
|
+
// The API will handle partial updates if it supports them,
|
|
89
|
+
// or we provide what we have
|
|
90
|
+
const payload = {
|
|
91
|
+
slug: options.slug,
|
|
92
|
+
};
|
|
93
|
+
if (options.title)
|
|
94
|
+
payload.title = options.title;
|
|
95
|
+
if (markdown)
|
|
96
|
+
payload.markdown = markdown;
|
|
97
|
+
if (options.locale)
|
|
98
|
+
payload.locale = options.locale;
|
|
99
|
+
if (options.subtitle)
|
|
100
|
+
payload.subtitle = options.subtitle;
|
|
101
|
+
if (options.summary)
|
|
102
|
+
payload.summary = options.summary;
|
|
103
|
+
if (options.tags)
|
|
104
|
+
payload.tags = options.tags;
|
|
105
|
+
if (options.published)
|
|
106
|
+
payload.published = options.published;
|
|
107
|
+
if (options.status)
|
|
108
|
+
payload.status = options.status;
|
|
109
|
+
if (options.type)
|
|
110
|
+
payload.type = options.type;
|
|
111
|
+
if (options.dept)
|
|
112
|
+
payload.dept = options.dept;
|
|
113
|
+
// The upsert endpoint requires title and markdown
|
|
114
|
+
// For an update operation, if these aren't provided, we should fetch the existing note first
|
|
115
|
+
if (!payload.title || !payload.markdown) {
|
|
116
|
+
return {
|
|
117
|
+
envelope: err(commandName, intent, {
|
|
118
|
+
code: "E_VALIDATION",
|
|
119
|
+
message: "Update requires both --title and (--markdown or --file). For partial updates, use the API directly or fetch the existing note first.",
|
|
120
|
+
}),
|
|
121
|
+
exitCode: EXIT.VALIDATION,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
// Validate payload
|
|
125
|
+
const parsed = LabNoteUpsertSchema.safeParse(payload);
|
|
126
|
+
if (!parsed.success) {
|
|
127
|
+
return {
|
|
128
|
+
envelope: err(commandName, intent, {
|
|
129
|
+
code: "E_VALIDATION",
|
|
130
|
+
message: "Invalid note data",
|
|
131
|
+
details: parsed.error.flatten(),
|
|
132
|
+
}),
|
|
133
|
+
exitCode: EXIT.VALIDATION,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
// Make API request
|
|
137
|
+
try {
|
|
138
|
+
const response = await postJson("/lab-notes/upsert", parsed.data, token);
|
|
139
|
+
return {
|
|
140
|
+
envelope: ok(commandName, intent, {
|
|
141
|
+
slug: response.slug,
|
|
142
|
+
action: response.action ?? "updated",
|
|
143
|
+
message: `Lab Note ${response.action ?? "updated"}: ${response.slug}`,
|
|
144
|
+
}),
|
|
145
|
+
exitCode: EXIT.OK,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
if (e instanceof HttpError) {
|
|
150
|
+
if (e.status === 401 || e.status === 403) {
|
|
151
|
+
return {
|
|
152
|
+
envelope: err(commandName, intent, {
|
|
153
|
+
code: "E_AUTH",
|
|
154
|
+
message: "Authentication failed. Check your HPL_TOKEN.",
|
|
155
|
+
}),
|
|
156
|
+
exitCode: EXIT.AUTH,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (e.status === 404) {
|
|
160
|
+
return {
|
|
161
|
+
envelope: err(commandName, intent, {
|
|
162
|
+
code: "E_NOT_FOUND",
|
|
163
|
+
message: `No lab note found for slug: ${options.slug}`,
|
|
164
|
+
}),
|
|
165
|
+
exitCode: EXIT.NOT_FOUND,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const code = e.status && e.status >= 500 ? "E_SERVER" : "E_HTTP";
|
|
169
|
+
return {
|
|
170
|
+
envelope: err(commandName, intent, {
|
|
171
|
+
code,
|
|
172
|
+
message: `API request failed (${e.status ?? "unknown"})`,
|
|
173
|
+
details: e.body ? e.body.slice(0, 500) : undefined,
|
|
174
|
+
}),
|
|
175
|
+
exitCode: e.status && e.status >= 500 ? EXIT.SERVER : EXIT.NETWORK,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
179
|
+
return {
|
|
180
|
+
envelope: err(commandName, intent, {
|
|
181
|
+
code: "E_UNKNOWN",
|
|
182
|
+
message: msg,
|
|
183
|
+
}),
|
|
184
|
+
exitCode: EXIT.UNKNOWN,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Commander: `hpl notes update`
|
|
190
|
+
*/
|
|
191
|
+
export function notesUpdateSubcommand() {
|
|
192
|
+
return new Command("update")
|
|
193
|
+
.description("Update an existing Lab Note (contract: update_lab_note)")
|
|
194
|
+
.argument("<slug>", "Note slug to update")
|
|
195
|
+
.option("--title <title>", "Note title")
|
|
196
|
+
.option("--markdown <text>", "Markdown content (inline)")
|
|
197
|
+
.option("--file <path>", "Path to markdown file")
|
|
198
|
+
.option("--locale <code>", "Locale code")
|
|
199
|
+
.option("--subtitle <text>", "Note subtitle")
|
|
200
|
+
.option("--summary <text>", "Note summary")
|
|
201
|
+
.option("--tags <tags>", "Comma-separated tags", (val) => val.split(",").map((t) => t.trim()))
|
|
202
|
+
.option("--published <date>", "Publication date (ISO format)")
|
|
203
|
+
.option("--status <status>", "Note status (draft|published|archived)")
|
|
204
|
+
.option("--type <type>", "Note type (labnote|paper|memo|lore|weather)")
|
|
205
|
+
.option("--dept <dept>", "Department code")
|
|
206
|
+
.action(async (slug, opts, cmd) => {
|
|
207
|
+
const mode = getOutputMode(cmd);
|
|
208
|
+
const { envelope, exitCode } = await runNotesUpdate({ ...opts, slug }, "notes.update");
|
|
209
|
+
if (mode === "json") {
|
|
210
|
+
printJson(envelope);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
renderText(envelope);
|
|
214
|
+
}
|
|
215
|
+
process.exitCode = exitCode;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
@@ -14,6 +14,8 @@ export const EXIT = {
|
|
|
14
14
|
NOT_FOUND: 3, // 404 semantics
|
|
15
15
|
AUTH: 4, // auth required / invalid token
|
|
16
16
|
FORBIDDEN: 5, // insufficient scope/permission
|
|
17
|
+
VALIDATION: 6, // data validation failed
|
|
18
|
+
IO: 7, // file I/O errors
|
|
17
19
|
NETWORK: 10, // DNS/timeout/unreachable
|
|
18
20
|
SERVER: 11, // 5xx or unexpected response
|
|
19
21
|
CONTRACT: 12, // schema mismatch / invalid JSON contract
|
|
@@ -40,6 +40,20 @@ export const INTENTS_ALPHA = {
|
|
|
40
40
|
sideEffects: [],
|
|
41
41
|
reversible: true,
|
|
42
42
|
},
|
|
43
|
+
create_lab_note: {
|
|
44
|
+
intent: "create_lab_note",
|
|
45
|
+
intentVersion: "1",
|
|
46
|
+
scope: ["lab_notes", "remote_api"],
|
|
47
|
+
sideEffects: ["write_remote"],
|
|
48
|
+
reversible: false,
|
|
49
|
+
},
|
|
50
|
+
update_lab_note: {
|
|
51
|
+
intent: "update_lab_note",
|
|
52
|
+
intentVersion: "1",
|
|
53
|
+
scope: ["lab_notes", "remote_api"],
|
|
54
|
+
sideEffects: ["write_remote"],
|
|
55
|
+
reversible: false,
|
|
56
|
+
},
|
|
43
57
|
};
|
|
44
58
|
export function getAlphaIntent(id) {
|
|
45
59
|
return INTENTS_ALPHA[id];
|
package/dist/src/http/client.js
CHANGED
|
@@ -37,3 +37,29 @@ export async function getJson(path, signal) {
|
|
|
37
37
|
const payload = (await res.json());
|
|
38
38
|
return unwrap(payload);
|
|
39
39
|
}
|
|
40
|
+
export async function postJson(path, body, token, signal) {
|
|
41
|
+
const { apiBaseUrl } = getConfig();
|
|
42
|
+
const url = apiBaseUrl + path;
|
|
43
|
+
const headers = {
|
|
44
|
+
"Content-Type": "application/json",
|
|
45
|
+
};
|
|
46
|
+
if (token) {
|
|
47
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
48
|
+
}
|
|
49
|
+
const res = await fetch(url, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers,
|
|
52
|
+
body: JSON.stringify(body),
|
|
53
|
+
signal,
|
|
54
|
+
});
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
let responseBody = "";
|
|
57
|
+
try {
|
|
58
|
+
responseBody = await res.text();
|
|
59
|
+
}
|
|
60
|
+
catch { /* ignore */ }
|
|
61
|
+
throw new HttpError(`POST ${path} failed`, res.status, responseBody);
|
|
62
|
+
}
|
|
63
|
+
const payload = (await res.json());
|
|
64
|
+
return unwrap(payload);
|
|
65
|
+
}
|