@thehumanpatternlab/hpl 0.0.1-alpha.5
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 +42 -0
- package/dist/__tests__/config.test.js +22 -0
- package/dist/__tests__/outputContract.test.js +58 -0
- package/dist/bin/hpl.js +158 -0
- package/dist/cli/output.js +21 -0
- package/dist/cli/outputContract.js +32 -0
- package/dist/commands/notesSync.js +191 -0
- package/dist/commands/publish.js +36 -0
- package/dist/index.js +36 -0
- package/dist/lab.js +10 -0
- package/dist/lib/config.js +52 -0
- package/dist/lib/http.js +18 -0
- package/dist/lib/notes.js +62 -0
- package/dist/lib/output.js +21 -0
- package/dist/sdk/LabClient.js +94 -0
- package/dist/src/__tests__/config.test.js +22 -0
- package/dist/src/__tests__/outputContract.test.js +58 -0
- package/dist/src/cli/output.js +21 -0
- package/dist/src/cli/outputContract.js +32 -0
- package/dist/src/commands/capabilities.js +10 -0
- package/dist/src/commands/health.js +48 -0
- package/dist/src/commands/notes/get.js +48 -0
- package/dist/src/commands/notes/list.js +43 -0
- package/dist/src/commands/notesSync.js +191 -0
- package/dist/src/commands/version.js +12 -0
- package/dist/src/config.js +10 -0
- package/dist/src/contract/capabilities.js +15 -0
- package/dist/src/contract/envelope.js +16 -0
- package/dist/src/contract/exitCodes.js +21 -0
- package/dist/src/contract/intents.js +49 -0
- package/dist/src/contract/schema.js +59 -0
- package/dist/src/http/client.js +39 -0
- package/dist/src/index.js +36 -0
- package/dist/src/io.js +15 -0
- package/dist/src/lib/config.js +52 -0
- package/dist/src/lib/http.js +18 -0
- package/dist/src/lib/notes.js +62 -0
- package/dist/src/render/table.js +17 -0
- package/dist/src/render/text.js +40 -0
- package/dist/src/sdk/ApiError.js +10 -0
- package/dist/src/sdk/LabClient.js +101 -0
- package/dist/src/sync/summary.js +25 -0
- package/dist/src/sync/types.js +2 -0
- package/dist/src/types/labNotes.js +27 -0
- package/dist/src/utils/loadMarkdown.js +10 -0
- package/dist/sync/summary.js +25 -0
- package/dist/sync/types.js +2 -0
- package/dist/utils/loadMarkdown.js +10 -0
- package/package.json +70 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/* ===========================================================
|
|
2
|
+
🌌 HUMAN PATTERN LAB — CLI CONFIG
|
|
3
|
+
-----------------------------------------------------------
|
|
4
|
+
Purpose: Single source of truth for config resolution.
|
|
5
|
+
Contract: Deterministic defaults.
|
|
6
|
+
=========================================================== */
|
|
7
|
+
export function getConfig() {
|
|
8
|
+
const apiBaseUrl = (process.env.HPL_API_BASE_URL || "https://api.thehumanpatternlab.com").replace(/\/+$/, "");
|
|
9
|
+
return { apiBaseUrl };
|
|
10
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/* ===========================================================
|
|
2
|
+
🌌 HUMAN PATTERN LAB — CLI CAPABILITIES (Alpha Tier)
|
|
3
|
+
-----------------------------------------------------------
|
|
4
|
+
Purpose: Capability disclosure for AI agents (no assumptions).
|
|
5
|
+
Contract: show_capabilities MUST emit tier, intents, schema versions.
|
|
6
|
+
=========================================================== */
|
|
7
|
+
import { CLI_SCHEMA_VERSION } from "./schema";
|
|
8
|
+
import { listAlphaIntents } from "./intents";
|
|
9
|
+
export function getCapabilitiesAlpha() {
|
|
10
|
+
return {
|
|
11
|
+
intentTier: "alpha",
|
|
12
|
+
supportedIntents: listAlphaIntents().map((i) => i.intent),
|
|
13
|
+
schemaVersions: [CLI_SCHEMA_VERSION],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/* ===========================================================
|
|
2
|
+
🌌 HUMAN PATTERN LAB — CLI ENVELOPE BUILDERS (Alpha Tier)
|
|
3
|
+
-----------------------------------------------------------
|
|
4
|
+
Purpose: Single source of truth for contract-compliant JSON output.
|
|
5
|
+
Guarantee: When --json, stdout emits JSON only (no logs).
|
|
6
|
+
=========================================================== */
|
|
7
|
+
import { CLI_SCHEMA_VERSION } from "./schema";
|
|
8
|
+
export function ok(command, intent, data) {
|
|
9
|
+
return { schemaVersion: CLI_SCHEMA_VERSION, command, status: "ok", intent, data };
|
|
10
|
+
}
|
|
11
|
+
export function warn(command, intent, warnings, data) {
|
|
12
|
+
return { schemaVersion: CLI_SCHEMA_VERSION, command, status: "warn", intent, warnings, data };
|
|
13
|
+
}
|
|
14
|
+
export function err(command, intent, error) {
|
|
15
|
+
return { schemaVersion: CLI_SCHEMA_VERSION, command, status: "error", intent, error };
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/* ===========================================================
|
|
2
|
+
🌌 HUMAN PATTERN LAB — CLI EXIT CODES (Alpha Tier)
|
|
3
|
+
-----------------------------------------------------------
|
|
4
|
+
Purpose: Consistent meanings across commands (machine-parseable).
|
|
5
|
+
Contract: "Errors & Exit Codes" predictable and structured.
|
|
6
|
+
=========================================================== */
|
|
7
|
+
/**
|
|
8
|
+
* Exit code meanings are stable across commands.
|
|
9
|
+
* Additive only; do not repurpose existing numeric values.
|
|
10
|
+
*/
|
|
11
|
+
export const EXIT = {
|
|
12
|
+
OK: 0,
|
|
13
|
+
USAGE: 2, // bad args / invalid flags
|
|
14
|
+
NOT_FOUND: 3, // 404 semantics
|
|
15
|
+
AUTH: 4, // auth required / invalid token
|
|
16
|
+
FORBIDDEN: 5, // insufficient scope/permission
|
|
17
|
+
NETWORK: 10, // DNS/timeout/unreachable
|
|
18
|
+
SERVER: 11, // 5xx or unexpected response
|
|
19
|
+
CONTRACT: 12, // schema mismatch / invalid JSON contract
|
|
20
|
+
UNKNOWN: 1,
|
|
21
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/* ===========================================================
|
|
2
|
+
🌌 HUMAN PATTERN LAB — CLI INTENT REGISTRY (Alpha Tier)
|
|
3
|
+
-----------------------------------------------------------
|
|
4
|
+
Purpose: Machine-legible, stable intent identifiers for CLI commands.
|
|
5
|
+
Contract: Lab CLI MVP Contract v0.x (Intent Registry + Explicit Intent)
|
|
6
|
+
Notes:
|
|
7
|
+
- Intent IDs are snake_case and treated as contractual identifiers.
|
|
8
|
+
- Changing the meaning of an intent is breaking, even in v0.x.
|
|
9
|
+
=========================================================== */
|
|
10
|
+
/**
|
|
11
|
+
* Alpha Tier intents (MVP).
|
|
12
|
+
* Additive only. Do not change semantics of existing IDs.
|
|
13
|
+
*/
|
|
14
|
+
export const INTENTS_ALPHA = {
|
|
15
|
+
show_version: {
|
|
16
|
+
intent: "show_version",
|
|
17
|
+
intentVersion: "1",
|
|
18
|
+
scope: ["cli"],
|
|
19
|
+
sideEffects: [],
|
|
20
|
+
reversible: true,
|
|
21
|
+
},
|
|
22
|
+
show_capabilities: {
|
|
23
|
+
intent: "show_capabilities",
|
|
24
|
+
intentVersion: "1",
|
|
25
|
+
scope: ["cli"],
|
|
26
|
+
sideEffects: [],
|
|
27
|
+
reversible: true,
|
|
28
|
+
},
|
|
29
|
+
check_health: {
|
|
30
|
+
intent: "check_health",
|
|
31
|
+
intentVersion: "1",
|
|
32
|
+
scope: ["remote_api"],
|
|
33
|
+
sideEffects: [],
|
|
34
|
+
reversible: true,
|
|
35
|
+
},
|
|
36
|
+
render_lab_note: {
|
|
37
|
+
intent: "render_lab_note",
|
|
38
|
+
intentVersion: "1",
|
|
39
|
+
scope: ["lab_notes", "remote_api"],
|
|
40
|
+
sideEffects: [],
|
|
41
|
+
reversible: true,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
export function getAlphaIntent(id) {
|
|
45
|
+
return INTENTS_ALPHA[id];
|
|
46
|
+
}
|
|
47
|
+
export function listAlphaIntents() {
|
|
48
|
+
return Object.values(INTENTS_ALPHA);
|
|
49
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/* ===========================================================
|
|
2
|
+
🌌 HUMAN PATTERN LAB — CLI OUTPUT SCHEMAS (Alpha Tier)
|
|
3
|
+
-----------------------------------------------------------
|
|
4
|
+
Purpose: Enforce the "JSON is a Contract" guarantee for --json mode.
|
|
5
|
+
Contract: Lab CLI MVP Contract v0.x (Schema Versioning + Determinism)
|
|
6
|
+
=========================================================== */
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
/** Global structured output schema version (CLI contract layer). */
|
|
9
|
+
export const CLI_SCHEMA_VERSION = "0.1";
|
|
10
|
+
export const StatusSchema = z.enum(["ok", "warn", "error"]);
|
|
11
|
+
export const IntentBlockSchema = z.object({
|
|
12
|
+
intent: z.string(),
|
|
13
|
+
intentVersion: z.literal("1"),
|
|
14
|
+
scope: z.array(z.string()),
|
|
15
|
+
sideEffects: z.array(z.string()),
|
|
16
|
+
reversible: z.boolean(),
|
|
17
|
+
});
|
|
18
|
+
/**
|
|
19
|
+
* Base envelope required by the contract for all --json outputs.
|
|
20
|
+
* Commands extend this with typed `data` for success, or `error` for failures.
|
|
21
|
+
*/
|
|
22
|
+
export const BaseEnvelopeSchema = z.object({
|
|
23
|
+
schemaVersion: z.literal(CLI_SCHEMA_VERSION),
|
|
24
|
+
command: z.string(),
|
|
25
|
+
status: StatusSchema,
|
|
26
|
+
intent: IntentBlockSchema,
|
|
27
|
+
});
|
|
28
|
+
/** Standard error payload (machine-parseable). */
|
|
29
|
+
export const ErrorPayloadSchema = z.object({
|
|
30
|
+
code: z.string(), // stable error code (e.g., "E_NETWORK", "E_AUTH")
|
|
31
|
+
message: z.string(), // short human-readable
|
|
32
|
+
details: z.unknown().optional() // optional machine-parseable context
|
|
33
|
+
});
|
|
34
|
+
/**
|
|
35
|
+
* Success envelope: { ...base, data: T }
|
|
36
|
+
*/
|
|
37
|
+
export function successSchema(dataSchema) {
|
|
38
|
+
return BaseEnvelopeSchema.extend({
|
|
39
|
+
status: z.literal("ok"),
|
|
40
|
+
data: dataSchema,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Warning envelope: { ...base, warnings: [...], data?: T }
|
|
45
|
+
*/
|
|
46
|
+
export function warnSchema(dataSchema) {
|
|
47
|
+
const base = BaseEnvelopeSchema.extend({
|
|
48
|
+
status: z.literal("warn"),
|
|
49
|
+
warnings: z.array(z.string()).min(1),
|
|
50
|
+
});
|
|
51
|
+
return dataSchema ? base.extend({ data: dataSchema }) : base;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Error envelope: { ...base, error: { code, message, details? } }
|
|
55
|
+
*/
|
|
56
|
+
export const ErrorEnvelopeSchema = BaseEnvelopeSchema.extend({
|
|
57
|
+
status: z.literal("error"),
|
|
58
|
+
error: ErrorPayloadSchema,
|
|
59
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/* ===========================================================
|
|
2
|
+
🌌 HUMAN PATTERN LAB — HTTP CLIENT (minimal)
|
|
3
|
+
-----------------------------------------------------------
|
|
4
|
+
Purpose: Fetch wrapper with consistent error shaping.
|
|
5
|
+
Notes:
|
|
6
|
+
- Supports both raw API payloads and envelope form { ok: true, data: ... }.
|
|
7
|
+
=========================================================== */
|
|
8
|
+
import { getConfig } from "../config";
|
|
9
|
+
export class HttpError extends Error {
|
|
10
|
+
status;
|
|
11
|
+
body;
|
|
12
|
+
constructor(message, status, body) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "HttpError";
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.body = body;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function unwrap(payload) {
|
|
20
|
+
if (payload && typeof payload === "object" && payload.ok === true) {
|
|
21
|
+
return payload.data;
|
|
22
|
+
}
|
|
23
|
+
return payload;
|
|
24
|
+
}
|
|
25
|
+
export async function getJson(path, signal) {
|
|
26
|
+
const { apiBaseUrl } = getConfig();
|
|
27
|
+
const url = apiBaseUrl + path;
|
|
28
|
+
const res = await fetch(url, { method: "GET", signal });
|
|
29
|
+
if (!res.ok) {
|
|
30
|
+
let body = "";
|
|
31
|
+
try {
|
|
32
|
+
body = await res.text();
|
|
33
|
+
}
|
|
34
|
+
catch { /* ignore */ }
|
|
35
|
+
throw new HttpError(`GET ${path} failed`, res.status, body);
|
|
36
|
+
}
|
|
37
|
+
const payload = (await res.json());
|
|
38
|
+
return unwrap(payload);
|
|
39
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* ===========================================================
|
|
3
|
+
🦊 THE HUMAN PATTERN LAB — SKULK CLI
|
|
4
|
+
-----------------------------------------------------------
|
|
5
|
+
File: notesSync.ts
|
|
6
|
+
Role: Command Implementation
|
|
7
|
+
Author: Ada (The Human Pattern Lab)
|
|
8
|
+
Assistant: Lyric
|
|
9
|
+
Status: Active
|
|
10
|
+
Description:
|
|
11
|
+
Implements the `skulk notes sync` command.
|
|
12
|
+
Handles human-readable and machine-readable output modes
|
|
13
|
+
with enforced JSON purity for automation safety.
|
|
14
|
+
-----------------------------------------------------------
|
|
15
|
+
Design Notes:
|
|
16
|
+
- Output format is a contract
|
|
17
|
+
- JSON mode emits stdout-only structured data
|
|
18
|
+
- Errors are written to stderr
|
|
19
|
+
- Exit codes are deterministic
|
|
20
|
+
=========================================================== */
|
|
21
|
+
import { Command } from "commander";
|
|
22
|
+
import { notesSyncCommand } from "./commands/notesSync.js";
|
|
23
|
+
const program = new Command();
|
|
24
|
+
program
|
|
25
|
+
.name("skulk")
|
|
26
|
+
.description("Skulk CLI for The Human Pattern Lab")
|
|
27
|
+
.version("0.1.0")
|
|
28
|
+
.option("--json", "Output machine-readable JSON")
|
|
29
|
+
.configureHelp({ helpWidth: 100 });
|
|
30
|
+
const argv = process.argv.slice(2);
|
|
31
|
+
if (argv.length === 0) {
|
|
32
|
+
program.outputHelp();
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
program.addCommand(notesSyncCommand());
|
|
36
|
+
program.parse(process.argv);
|
package/dist/src/io.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/* ===========================================================
|
|
2
|
+
🌌 HUMAN PATTERN LAB — CLI IO GATE
|
|
3
|
+
-----------------------------------------------------------
|
|
4
|
+
Purpose: Enforce "--json means JSON only on stdout".
|
|
5
|
+
=========================================================== */
|
|
6
|
+
export function writeJson(obj) {
|
|
7
|
+
process.stdout.write(JSON.stringify(obj, null, 2) + "\n");
|
|
8
|
+
}
|
|
9
|
+
export function writeHuman(text) {
|
|
10
|
+
process.stdout.write(text.endsWith("\n") ? text : text + "\n");
|
|
11
|
+
}
|
|
12
|
+
/** Logs to stderr only. Safe to call even in --json mode (but we avoid it by policy). */
|
|
13
|
+
export function logErr(message) {
|
|
14
|
+
process.stderr.write(message.endsWith("\n") ? message : message + "\n");
|
|
15
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
/**
|
|
6
|
+
* Skulk CLI configuration schema
|
|
7
|
+
* Stored in ~/.humanpatternlab/skulk.json
|
|
8
|
+
*/
|
|
9
|
+
const ConfigSchema = z.object({
|
|
10
|
+
apiBaseUrl: z.string().url().default('https://thehumanpatternlab.com/api'),
|
|
11
|
+
token: z.string().optional(),
|
|
12
|
+
});
|
|
13
|
+
function getConfigPath() {
|
|
14
|
+
return path.join(os.homedir(), '.humanpatternlab', 'skulk.json');
|
|
15
|
+
}
|
|
16
|
+
export function loadConfig() {
|
|
17
|
+
const p = getConfigPath();
|
|
18
|
+
if (!fs.existsSync(p)) {
|
|
19
|
+
return ConfigSchema.parse({});
|
|
20
|
+
}
|
|
21
|
+
const raw = fs.readFileSync(p, 'utf-8');
|
|
22
|
+
return ConfigSchema.parse(JSON.parse(raw));
|
|
23
|
+
}
|
|
24
|
+
export function saveConfig(partial) {
|
|
25
|
+
const p = getConfigPath();
|
|
26
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
27
|
+
const current = loadConfig();
|
|
28
|
+
const next = ConfigSchema.parse({ ...current, ...partial });
|
|
29
|
+
fs.writeFileSync(p, JSON.stringify(next, null, 2), 'utf-8');
|
|
30
|
+
}
|
|
31
|
+
export function SKULK_BASE_URL(override) {
|
|
32
|
+
if (override?.trim())
|
|
33
|
+
return override.trim();
|
|
34
|
+
// NEW official env var
|
|
35
|
+
const env = process.env.SKULK_BASE_URL?.trim();
|
|
36
|
+
if (env)
|
|
37
|
+
return env;
|
|
38
|
+
// optional legacy support (remove later if you want)
|
|
39
|
+
const legacy = process.env.HPL_API_BASE_URL?.trim();
|
|
40
|
+
if (legacy)
|
|
41
|
+
return legacy;
|
|
42
|
+
return loadConfig().apiBaseUrl;
|
|
43
|
+
}
|
|
44
|
+
export function SKULK_TOKEN() {
|
|
45
|
+
const env = process.env.SKULK_TOKEN?.trim();
|
|
46
|
+
if (env)
|
|
47
|
+
return env;
|
|
48
|
+
const legacy = process.env.HPL_TOKEN?.trim();
|
|
49
|
+
if (legacy)
|
|
50
|
+
return legacy;
|
|
51
|
+
return loadConfig().token;
|
|
52
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export async function httpJson(opts, method, path, body) {
|
|
2
|
+
const url = `${opts.baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
|
|
3
|
+
const headers = {
|
|
4
|
+
"Content-Type": "application/json"
|
|
5
|
+
};
|
|
6
|
+
if (opts.token)
|
|
7
|
+
headers["Authorization"] = `Bearer ${opts.token}`;
|
|
8
|
+
const res = await fetch(url, {
|
|
9
|
+
method,
|
|
10
|
+
headers,
|
|
11
|
+
body: body ? JSON.stringify(body) : undefined
|
|
12
|
+
});
|
|
13
|
+
const text = await res.text();
|
|
14
|
+
if (!res.ok) {
|
|
15
|
+
throw new Error(`HTTP ${res.status} ${res.statusText} @ ${url}\n${text}`);
|
|
16
|
+
}
|
|
17
|
+
return text ? JSON.parse(text) : {};
|
|
18
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import matter from "gray-matter";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
const FrontmatterSchema = z.object({
|
|
6
|
+
id: z.string().optional(),
|
|
7
|
+
type: z.string().optional(),
|
|
8
|
+
title: z.string(),
|
|
9
|
+
subtitle: z.string().optional(),
|
|
10
|
+
published: z.string().optional(),
|
|
11
|
+
tags: z.array(z.string()).optional(),
|
|
12
|
+
summary: z.string().optional(),
|
|
13
|
+
readingTime: z.number().optional(),
|
|
14
|
+
status: z.enum(["published", "draft", "archived"]).optional(),
|
|
15
|
+
dept: z.string().optional(),
|
|
16
|
+
department_id: z.string().optional(),
|
|
17
|
+
shadow_density: z.number().optional(),
|
|
18
|
+
safer_landing: z.boolean().optional(),
|
|
19
|
+
slug: z.string().optional()
|
|
20
|
+
});
|
|
21
|
+
function slugFromFilename(filePath) {
|
|
22
|
+
const base = path.basename(filePath);
|
|
23
|
+
return base.replace(/\.mdx?$/i, "");
|
|
24
|
+
}
|
|
25
|
+
export function readNote(filePath, locale) {
|
|
26
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
27
|
+
const parsed = matter(raw);
|
|
28
|
+
const fm = FrontmatterSchema.parse(parsed.data ?? {});
|
|
29
|
+
const slug = (fm.slug && String(fm.slug).trim()) || slugFromFilename(filePath);
|
|
30
|
+
const body = parsed.content.trim();
|
|
31
|
+
const markdown = body.length > 0 ? body : raw.trim();
|
|
32
|
+
// Keep full attributes, but ensure key stuff exists
|
|
33
|
+
const title = (fm.title && String(fm.title).trim()) || "";
|
|
34
|
+
if (!title) {
|
|
35
|
+
throw new Error(`Missing required frontmatter: title (${filePath})`);
|
|
36
|
+
}
|
|
37
|
+
if (!markdown) {
|
|
38
|
+
throw new Error(`Missing required markdown content (${filePath})`);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
slug,
|
|
42
|
+
locale,
|
|
43
|
+
attributes: { ...fm, slug },
|
|
44
|
+
markdown
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function listMarkdownFiles(dir) {
|
|
48
|
+
const out = [];
|
|
49
|
+
const stack = [dir];
|
|
50
|
+
while (stack.length) {
|
|
51
|
+
const d = stack.pop();
|
|
52
|
+
const entries = fs.readdirSync(d, { withFileTypes: true });
|
|
53
|
+
for (const e of entries) {
|
|
54
|
+
const p = path.join(d, e.name);
|
|
55
|
+
if (e.isDirectory())
|
|
56
|
+
stack.push(p);
|
|
57
|
+
else if (e.isFile() && /\.mdx?$/i.test(e.name))
|
|
58
|
+
out.push(p);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return out.sort();
|
|
62
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* ===========================================================
|
|
2
|
+
🌌 HUMAN PATTERN LAB — TABLE RENDERER (tiny)
|
|
3
|
+
-----------------------------------------------------------
|
|
4
|
+
Purpose: Deterministic fixed-width table output.
|
|
5
|
+
=========================================================== */
|
|
6
|
+
function pad(s, width) {
|
|
7
|
+
const str = (s ?? "").toString();
|
|
8
|
+
if (str.length >= width)
|
|
9
|
+
return str.slice(0, Math.max(0, width - 1)) + "…";
|
|
10
|
+
return str + " ".repeat(width - str.length);
|
|
11
|
+
}
|
|
12
|
+
export function renderTable(rows, cols) {
|
|
13
|
+
const header = cols.map((c) => pad(c.header, c.width)).join(" ");
|
|
14
|
+
const sep = cols.map((c) => "-".repeat(c.width)).join(" ");
|
|
15
|
+
const body = rows.map((r) => cols.map((c) => pad(c.value(r), c.width)).join(" ")).join("\n");
|
|
16
|
+
return [header, sep, body].filter(Boolean).join("\n");
|
|
17
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/* ===========================================================
|
|
2
|
+
🌌 HUMAN PATTERN LAB — TEXT RENDER UTILS
|
|
3
|
+
-----------------------------------------------------------
|
|
4
|
+
Purpose: Deterministic, dependency-free formatting for terminals.
|
|
5
|
+
=========================================================== */
|
|
6
|
+
export function stripHtml(input) {
|
|
7
|
+
const s = (input || "");
|
|
8
|
+
// Convert common structure to deterministic newlines first
|
|
9
|
+
const withBreaks = s
|
|
10
|
+
.replace(/<\s*br\s*\/?>/gi, "\n")
|
|
11
|
+
.replace(/<\/\s*p\s*>/gi, "\n\n")
|
|
12
|
+
.replace(/<\s*p[^>]*>/gi, "")
|
|
13
|
+
.replace(/<\s*hr\s*\/?>/gi, "\n---\n")
|
|
14
|
+
.replace(/<\/\s*li\s*>/gi, "\n")
|
|
15
|
+
.replace(/<\s*li[^>]*>/gi, "- ")
|
|
16
|
+
.replace(/<\/\s*ul\s*>/gi, "\n")
|
|
17
|
+
.replace(/<\s*ul[^>]*>/gi, "");
|
|
18
|
+
// Strip scripts/styles then remaining tags
|
|
19
|
+
const stripped = withBreaks
|
|
20
|
+
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "")
|
|
21
|
+
.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, "")
|
|
22
|
+
.replace(/<[^>]+>/g, "");
|
|
23
|
+
// Decode a few entities + normalize whitespace
|
|
24
|
+
return stripped
|
|
25
|
+
.replace(/ /g, " ")
|
|
26
|
+
.replace(/&/g, "&")
|
|
27
|
+
.replace(/</g, "<")
|
|
28
|
+
.replace(/>/g, ">")
|
|
29
|
+
.replace(/\r\n/g, "\n")
|
|
30
|
+
.replace(/[ \t]+\n/g, "\n")
|
|
31
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
32
|
+
.trim();
|
|
33
|
+
}
|
|
34
|
+
export function safeLine(s) {
|
|
35
|
+
return (s ?? "").replace(/\s+/g, " ").trim();
|
|
36
|
+
}
|
|
37
|
+
export function formatTags(tags) {
|
|
38
|
+
const t = (tags ?? []).filter(Boolean);
|
|
39
|
+
return t.length ? t.join(", ") : "-";
|
|
40
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// src/sdk/LabClient.ts
|
|
2
|
+
export class LabClient {
|
|
3
|
+
baseUrl;
|
|
4
|
+
token;
|
|
5
|
+
constructor(baseUrl, token) {
|
|
6
|
+
// baseUrl should be like: https://api.thehumanpatternlab.com
|
|
7
|
+
// (no trailing slash, no /api)
|
|
8
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
9
|
+
this.token = token;
|
|
10
|
+
}
|
|
11
|
+
setToken(token) {
|
|
12
|
+
this.token = token;
|
|
13
|
+
}
|
|
14
|
+
buildHeaders(extra, auth = "none") {
|
|
15
|
+
const h = { ...(extra ?? {}) };
|
|
16
|
+
// Only set JSON content-type when we actually send a JSON body
|
|
17
|
+
// (some servers get weird on GETs with Content-Type set)
|
|
18
|
+
if (!h["Content-Type"])
|
|
19
|
+
h["Content-Type"] = "application/json";
|
|
20
|
+
if (auth === "bearer" && this.token) {
|
|
21
|
+
h["Authorization"] = `Bearer ${this.token}`;
|
|
22
|
+
}
|
|
23
|
+
return h;
|
|
24
|
+
}
|
|
25
|
+
async request(path, opts = {}) {
|
|
26
|
+
const url = `${this.baseUrl}${path.startsWith("/") ? "" : "/"}${path}`;
|
|
27
|
+
const auth = opts.auth ?? "none";
|
|
28
|
+
const headers = this.buildHeaders(opts.headers, auth);
|
|
29
|
+
const res = await fetch(url, {
|
|
30
|
+
...opts,
|
|
31
|
+
headers,
|
|
32
|
+
credentials: auth === "cookie" ? "include" : opts.credentials,
|
|
33
|
+
});
|
|
34
|
+
// Best-effort parse
|
|
35
|
+
const payload = await res.json().catch(() => null);
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
const msg = typeof payload === "object" &&
|
|
38
|
+
payload !== null &&
|
|
39
|
+
("error" in payload || "message" in payload) &&
|
|
40
|
+
typeof payload.error === "string"
|
|
41
|
+
? payload.error
|
|
42
|
+
: typeof payload?.message === "string"
|
|
43
|
+
? payload.message
|
|
44
|
+
: `Request failed (${res.status})`;
|
|
45
|
+
throw new Error(msg);
|
|
46
|
+
}
|
|
47
|
+
return payload;
|
|
48
|
+
}
|
|
49
|
+
//
|
|
50
|
+
// ────────────────────────────────────────────────
|
|
51
|
+
// PUBLIC ROUTES
|
|
52
|
+
// ────────────────────────────────────────────────
|
|
53
|
+
//
|
|
54
|
+
// ✅ was GET `${baseUrl}/`
|
|
55
|
+
async getAllNotes() {
|
|
56
|
+
return this.request("/lab-notes", { method: "GET", auth: "none" });
|
|
57
|
+
}
|
|
58
|
+
// ✅ was GET `${baseUrl}/notes/${slug}`
|
|
59
|
+
async getNoteBySlug(slug) {
|
|
60
|
+
return this.request(`/lab-notes/${encodeURIComponent(slug)}`, {
|
|
61
|
+
method: "GET",
|
|
62
|
+
auth: "none",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
//
|
|
66
|
+
// ────────────────────────────────────────────────
|
|
67
|
+
// ADMIN ROUTES
|
|
68
|
+
// ────────────────────────────────────────────────
|
|
69
|
+
//
|
|
70
|
+
// ✅ was `/api/admin/notes`
|
|
71
|
+
async getAdminNotes() {
|
|
72
|
+
return this.request("/admin/notes", {
|
|
73
|
+
method: "GET",
|
|
74
|
+
auth: "cookie",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// ✅ was `/api/admin/notes`
|
|
78
|
+
async createOrUpdateNote(input) {
|
|
79
|
+
return this.request("/admin/notes", {
|
|
80
|
+
method: "POST",
|
|
81
|
+
auth: "cookie",
|
|
82
|
+
body: JSON.stringify(input),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
// ✅ was `/api/admin/notes/${id}`
|
|
86
|
+
async deleteNote(id) {
|
|
87
|
+
return this.request(`/admin/notes/${encodeURIComponent(id)}`, {
|
|
88
|
+
method: "DELETE",
|
|
89
|
+
auth: "cookie",
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
//
|
|
93
|
+
// ────────────────────────────────────────────────
|
|
94
|
+
// HEALTH
|
|
95
|
+
// ────────────────────────────────────────────────
|
|
96
|
+
//
|
|
97
|
+
// ✅ was `/api/health`
|
|
98
|
+
async health() {
|
|
99
|
+
return this.request("/health", { method: "GET", auth: "none" });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function buildSyncSummary(results, dryRunMode) {
|
|
2
|
+
let synced = 0;
|
|
3
|
+
let dryRun = 0;
|
|
4
|
+
let failed = 0;
|
|
5
|
+
for (const r of results) {
|
|
6
|
+
if (r.status === "failed") {
|
|
7
|
+
failed++;
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
if (dryRunMode)
|
|
11
|
+
dryRun++;
|
|
12
|
+
else
|
|
13
|
+
synced++;
|
|
14
|
+
}
|
|
15
|
+
const total = results.length;
|
|
16
|
+
// Invariant: dry-run must never report synced writes
|
|
17
|
+
if (dryRunMode && synced > 0) {
|
|
18
|
+
throw new Error("Invariant violation: dry-run mode cannot produce synced > 0");
|
|
19
|
+
}
|
|
20
|
+
// Invariant: accounting must balance
|
|
21
|
+
if (synced + dryRun + failed !== total) {
|
|
22
|
+
throw new Error("SyncSummary invariant failed: counts do not add up to total");
|
|
23
|
+
}
|
|
24
|
+
return { synced, dryRun, failed, total };
|
|
25
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/* ===========================================================
|
|
2
|
+
🌌 HUMAN PATTERN LAB — TYPES: Lab Notes (CLI)
|
|
3
|
+
-----------------------------------------------------------
|
|
4
|
+
Purpose: API contract types for Lab Notes used by the CLI.
|
|
5
|
+
Notes:
|
|
6
|
+
- Keep permissive: API may add fields (additive).
|
|
7
|
+
=========================================================== */
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
export const LabNoteSchema = z.object({
|
|
10
|
+
id: z.string(),
|
|
11
|
+
slug: z.string(),
|
|
12
|
+
title: z.string(),
|
|
13
|
+
summary: z.string().optional().default(""),
|
|
14
|
+
contentHtml: z.string().optional().default(""),
|
|
15
|
+
published: z.string().optional().nullable(),
|
|
16
|
+
status: z.string().optional(),
|
|
17
|
+
type: z.string().optional(),
|
|
18
|
+
locale: z.string().optional(),
|
|
19
|
+
department_id: z.string().optional(),
|
|
20
|
+
shadow_density: z.number().optional(),
|
|
21
|
+
safer_landing: z.boolean().optional(),
|
|
22
|
+
tags: z.array(z.string()).optional().default([]),
|
|
23
|
+
readingTime: z.number().optional(),
|
|
24
|
+
created_at: z.string().optional(),
|
|
25
|
+
updated_at: z.string().optional(),
|
|
26
|
+
}).passthrough();
|
|
27
|
+
export const LabNoteListSchema = z.array(LabNoteSchema);
|