@unbrained/pm-cli 2026.3.9
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/.pi/extensions/pm-cli/index.ts +778 -0
- package/AGENTS.md +475 -0
- package/LICENSE +21 -0
- package/PRD.md +1390 -0
- package/README.md +655 -0
- package/dist/cli/commands/activity.d.ts +14 -0
- package/dist/cli/commands/activity.js +80 -0
- package/dist/cli/commands/activity.js.map +1 -0
- package/dist/cli/commands/append.d.ts +13 -0
- package/dist/cli/commands/append.js +46 -0
- package/dist/cli/commands/append.js.map +1 -0
- package/dist/cli/commands/beads.d.ts +15 -0
- package/dist/cli/commands/beads.js +475 -0
- package/dist/cli/commands/beads.js.map +1 -0
- package/dist/cli/commands/claim.d.ts +19 -0
- package/dist/cli/commands/claim.js +79 -0
- package/dist/cli/commands/claim.js.map +1 -0
- package/dist/cli/commands/close.d.ts +12 -0
- package/dist/cli/commands/close.js +58 -0
- package/dist/cli/commands/close.js.map +1 -0
- package/dist/cli/commands/comments.d.ts +15 -0
- package/dist/cli/commands/comments.js +80 -0
- package/dist/cli/commands/comments.js.map +1 -0
- package/dist/cli/commands/completion.d.ts +10 -0
- package/dist/cli/commands/completion.js +469 -0
- package/dist/cli/commands/completion.js.map +1 -0
- package/dist/cli/commands/config.d.ts +15 -0
- package/dist/cli/commands/config.js +72 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/create.d.ts +60 -0
- package/dist/cli/commands/create.js +456 -0
- package/dist/cli/commands/create.js.map +1 -0
- package/dist/cli/commands/delete.d.ts +12 -0
- package/dist/cli/commands/delete.js +33 -0
- package/dist/cli/commands/delete.js.map +1 -0
- package/dist/cli/commands/docs.d.ts +16 -0
- package/dist/cli/commands/docs.js +113 -0
- package/dist/cli/commands/docs.js.map +1 -0
- package/dist/cli/commands/files.d.ts +17 -0
- package/dist/cli/commands/files.js +113 -0
- package/dist/cli/commands/files.js.map +1 -0
- package/dist/cli/commands/gc.d.ts +9 -0
- package/dist/cli/commands/gc.js +80 -0
- package/dist/cli/commands/gc.js.map +1 -0
- package/dist/cli/commands/get.d.ts +12 -0
- package/dist/cli/commands/get.js +28 -0
- package/dist/cli/commands/get.js.map +1 -0
- package/dist/cli/commands/health.d.ts +15 -0
- package/dist/cli/commands/health.js +288 -0
- package/dist/cli/commands/health.js.map +1 -0
- package/dist/cli/commands/history.d.ts +13 -0
- package/dist/cli/commands/history.js +72 -0
- package/dist/cli/commands/history.js.map +1 -0
- package/dist/cli/commands/index.d.ts +26 -0
- package/dist/cli/commands/index.js +27 -0
- package/dist/cli/commands/index.js.map +1 -0
- package/dist/cli/commands/init.d.ts +10 -0
- package/dist/cli/commands/init.js +59 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/install.d.ts +18 -0
- package/dist/cli/commands/install.js +87 -0
- package/dist/cli/commands/install.js.map +1 -0
- package/dist/cli/commands/list.d.ts +21 -0
- package/dist/cli/commands/list.js +137 -0
- package/dist/cli/commands/list.js.map +1 -0
- package/dist/cli/commands/reindex.d.ts +16 -0
- package/dist/cli/commands/reindex.js +154 -0
- package/dist/cli/commands/reindex.js.map +1 -0
- package/dist/cli/commands/restore.d.ts +20 -0
- package/dist/cli/commands/restore.js +208 -0
- package/dist/cli/commands/restore.js.map +1 -0
- package/dist/cli/commands/search.d.ts +45 -0
- package/dist/cli/commands/search.js +531 -0
- package/dist/cli/commands/search.js.map +1 -0
- package/dist/cli/commands/stats.d.ts +13 -0
- package/dist/cli/commands/stats.js +88 -0
- package/dist/cli/commands/stats.js.map +1 -0
- package/dist/cli/commands/test-all.d.ts +30 -0
- package/dist/cli/commands/test-all.js +157 -0
- package/dist/cli/commands/test-all.js.map +1 -0
- package/dist/cli/commands/test.d.ts +29 -0
- package/dist/cli/commands/test.js +492 -0
- package/dist/cli/commands/test.js.map +1 -0
- package/dist/cli/commands/update.d.ts +52 -0
- package/dist/cli/commands/update.js +467 -0
- package/dist/cli/commands/update.js.map +1 -0
- package/dist/cli/extension-command-options.d.ts +1 -0
- package/dist/cli/extension-command-options.js +76 -0
- package/dist/cli/extension-command-options.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +1494 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +3 -0
- package/dist/cli.js.map +1 -0
- package/dist/command-types.d.ts +1 -0
- package/dist/command-types.js +2 -0
- package/dist/command-types.js.map +1 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +2 -0
- package/dist/constants.js.map +1 -0
- package/dist/core/extensions/builtins.d.ts +3 -0
- package/dist/core/extensions/builtins.js +47 -0
- package/dist/core/extensions/builtins.js.map +1 -0
- package/dist/core/extensions/index.d.ts +13 -0
- package/dist/core/extensions/index.js +88 -0
- package/dist/core/extensions/index.js.map +1 -0
- package/dist/core/extensions/loader.d.ts +301 -0
- package/dist/core/extensions/loader.js +917 -0
- package/dist/core/extensions/loader.js.map +1 -0
- package/dist/core/fs/fs-utils.d.ts +6 -0
- package/dist/core/fs/fs-utils.js +58 -0
- package/dist/core/fs/fs-utils.js.map +1 -0
- package/dist/core/fs/index.d.ts +1 -0
- package/dist/core/fs/index.js +2 -0
- package/dist/core/fs/index.js.map +1 -0
- package/dist/core/history/history.d.ts +12 -0
- package/dist/core/history/history.js +44 -0
- package/dist/core/history/history.js.map +1 -0
- package/dist/core/history/index.d.ts +1 -0
- package/dist/core/history/index.js +2 -0
- package/dist/core/history/index.js.map +1 -0
- package/dist/core/item/id.d.ts +3 -0
- package/dist/core/item/id.js +54 -0
- package/dist/core/item/id.js.map +1 -0
- package/dist/core/item/index.d.ts +3 -0
- package/dist/core/item/index.js +4 -0
- package/dist/core/item/index.js.map +1 -0
- package/dist/core/item/item-format.d.ts +9 -0
- package/dist/core/item/item-format.js +363 -0
- package/dist/core/item/item-format.js.map +1 -0
- package/dist/core/item/parse.d.ts +3 -0
- package/dist/core/item/parse.js +72 -0
- package/dist/core/item/parse.js.map +1 -0
- package/dist/core/lock/index.d.ts +1 -0
- package/dist/core/lock/index.js +2 -0
- package/dist/core/lock/index.js.map +1 -0
- package/dist/core/lock/lock.d.ts +1 -0
- package/dist/core/lock/lock.js +100 -0
- package/dist/core/lock/lock.js.map +1 -0
- package/dist/core/output/output.d.ts +7 -0
- package/dist/core/output/output.js +79 -0
- package/dist/core/output/output.js.map +1 -0
- package/dist/core/search/cache.d.ts +17 -0
- package/dist/core/search/cache.js +212 -0
- package/dist/core/search/cache.js.map +1 -0
- package/dist/core/search/embedding-batches.d.ts +7 -0
- package/dist/core/search/embedding-batches.js +54 -0
- package/dist/core/search/embedding-batches.js.map +1 -0
- package/dist/core/search/providers.d.ts +59 -0
- package/dist/core/search/providers.js +265 -0
- package/dist/core/search/providers.js.map +1 -0
- package/dist/core/search/vector-stores.d.ts +89 -0
- package/dist/core/search/vector-stores.js +546 -0
- package/dist/core/search/vector-stores.js.map +1 -0
- package/dist/core/shared/command-types.d.ts +7 -0
- package/dist/core/shared/command-types.js +2 -0
- package/dist/core/shared/command-types.js.map +1 -0
- package/dist/core/shared/constants.d.ts +19 -0
- package/dist/core/shared/constants.js +134 -0
- package/dist/core/shared/constants.js.map +1 -0
- package/dist/core/shared/errors.d.ts +4 -0
- package/dist/core/shared/errors.js +9 -0
- package/dist/core/shared/errors.js.map +1 -0
- package/dist/core/shared/index.d.ts +3 -0
- package/dist/core/shared/index.js +4 -0
- package/dist/core/shared/index.js.map +1 -0
- package/dist/core/shared/serialization.d.ts +3 -0
- package/dist/core/shared/serialization.js +70 -0
- package/dist/core/shared/serialization.js.map +1 -0
- package/dist/core/shared/time.d.ts +3 -0
- package/dist/core/shared/time.js +28 -0
- package/dist/core/shared/time.js.map +1 -0
- package/dist/core/store/index.d.ts +3 -0
- package/dist/core/store/index.js +4 -0
- package/dist/core/store/index.js.map +1 -0
- package/dist/core/store/item-store.d.ts +42 -0
- package/dist/core/store/item-store.js +186 -0
- package/dist/core/store/item-store.js.map +1 -0
- package/dist/core/store/paths.d.ts +8 -0
- package/dist/core/store/paths.js +29 -0
- package/dist/core/store/paths.js.map +1 -0
- package/dist/core/store/settings.d.ts +4 -0
- package/dist/core/store/settings.js +148 -0
- package/dist/core/store/settings.js.map +1 -0
- package/dist/errors.d.ts +1 -0
- package/dist/errors.js +2 -0
- package/dist/errors.js.map +1 -0
- package/dist/extensions/builtins/beads/index.d.ts +8 -0
- package/dist/extensions/builtins/beads/index.js +29 -0
- package/dist/extensions/builtins/beads/index.js.map +1 -0
- package/dist/extensions/builtins/todos/import-export.d.ts +26 -0
- package/dist/extensions/builtins/todos/import-export.js +460 -0
- package/dist/extensions/builtins/todos/import-export.js.map +1 -0
- package/dist/extensions/builtins/todos/index.d.ts +8 -0
- package/dist/extensions/builtins/todos/index.js +38 -0
- package/dist/extensions/builtins/todos/index.js.map +1 -0
- package/dist/fs-utils.d.ts +1 -0
- package/dist/fs-utils.js +2 -0
- package/dist/fs-utils.js.map +1 -0
- package/dist/history.d.ts +1 -0
- package/dist/history.js +2 -0
- package/dist/history.js.map +1 -0
- package/dist/id.d.ts +1 -0
- package/dist/id.js +2 -0
- package/dist/id.js.map +1 -0
- package/dist/item-format.d.ts +1 -0
- package/dist/item-format.js +2 -0
- package/dist/item-format.js.map +1 -0
- package/dist/item-store.d.ts +1 -0
- package/dist/item-store.js +2 -0
- package/dist/item-store.js.map +1 -0
- package/dist/lock.d.ts +1 -0
- package/dist/lock.js +2 -0
- package/dist/lock.js.map +1 -0
- package/dist/output.d.ts +1 -0
- package/dist/output.js +2 -0
- package/dist/output.js.map +1 -0
- package/dist/parse.d.ts +1 -0
- package/dist/parse.js +2 -0
- package/dist/parse.js.map +1 -0
- package/dist/paths.d.ts +1 -0
- package/dist/paths.js +2 -0
- package/dist/paths.js.map +1 -0
- package/dist/serialization.d.ts +1 -0
- package/dist/serialization.js +2 -0
- package/dist/serialization.js.map +1 -0
- package/dist/settings.d.ts +1 -0
- package/dist/settings.js +2 -0
- package/dist/settings.js.map +1 -0
- package/dist/time.d.ts +1 -0
- package/dist/time.js +2 -0
- package/dist/time.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types.d.ts +179 -0
- package/dist/types.js +21 -0
- package/dist/types.js.map +1 -0
- package/docs/ARCHITECTURE.md +246 -0
- package/docs/EXTENSIONS.md +329 -0
- package/docs/RELEASING.md +65 -0
- package/package.json +79 -0
- package/scripts/install.ps1 +112 -0
- package/scripts/install.sh +113 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# pm-cli Extension Development Guide
|
|
2
|
+
|
|
3
|
+
Extensions let you add commands, renderers, importers, exporters, schema fields, search providers, and lifecycle hooks to `pm-cli` without modifying core.
|
|
4
|
+
|
|
5
|
+
## Extension Locations
|
|
6
|
+
|
|
7
|
+
| Scope | Path |
|
|
8
|
+
|-------|------|
|
|
9
|
+
| Global | `~/.pm-cli/extensions/<name>/` (override: `PM_GLOBAL_PATH/extensions/<name>/`) |
|
|
10
|
+
| Project | `.agents/pm/extensions/<name>/` (override: `PM_PATH/extensions/<name>/`) |
|
|
11
|
+
|
|
12
|
+
**Load order:** core built-ins → global → project. Project-local extensions take precedence over global when they declare the same command name or renderer key.
|
|
13
|
+
|
|
14
|
+
## Manifest
|
|
15
|
+
|
|
16
|
+
Every extension directory must contain a `manifest.json`:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"name": "pm-ext-example",
|
|
21
|
+
"version": "0.1.0",
|
|
22
|
+
"entry": "./dist/index.js",
|
|
23
|
+
"priority": 100,
|
|
24
|
+
"capabilities": [
|
|
25
|
+
"commands",
|
|
26
|
+
"renderers",
|
|
27
|
+
"hooks",
|
|
28
|
+
"schema",
|
|
29
|
+
"importers",
|
|
30
|
+
"search"
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- `entry` must resolve inside the extension directory (no path traversal).
|
|
36
|
+
- `capabilities` declares what the extension will register. API calls that exceed declared capabilities fail activation deterministically.
|
|
37
|
+
- Unknown capability names are silently ignored for gating but emit discovery diagnostics.
|
|
38
|
+
|
|
39
|
+
## Extension Module
|
|
40
|
+
|
|
41
|
+
The entry module must export an `activate` function:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import type { ExtensionApi } from "pm-cli";
|
|
45
|
+
|
|
46
|
+
export function activate(api: ExtensionApi): void {
|
|
47
|
+
// register commands, hooks, renderers, etc.
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`activate` may be synchronous or return `Promise<void>`.
|
|
52
|
+
|
|
53
|
+
## API Reference
|
|
54
|
+
|
|
55
|
+
### `api.registerCommand(def)`
|
|
56
|
+
|
|
57
|
+
Register a new command or override an existing core command's result.
|
|
58
|
+
|
|
59
|
+
**New command path:**
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
api.registerCommand({
|
|
63
|
+
name: "acme sync",
|
|
64
|
+
run: async (args, options, global) => {
|
|
65
|
+
// args: string[] — positional CLI arguments
|
|
66
|
+
// options: Record<string,unknown> — parsed flags
|
|
67
|
+
// global: GlobalOptions — --json, --quiet, --path, etc.
|
|
68
|
+
return { ok: true, synced: 42 };
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The command name is canonicalized (trimmed, lowercased, repeated whitespace collapsed). The handler receives cloned snapshots so mutation cannot leak into caller state.
|
|
74
|
+
|
|
75
|
+
**Override existing core command result:**
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
api.registerCommand("list", (priorResult, args, options, global, pmRoot) => {
|
|
79
|
+
// priorResult: the core command's output object (cloned)
|
|
80
|
+
// return a modified result object, or undefined to use priorResult as-is
|
|
81
|
+
return { ...priorResult, _ext: "annotated" };
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### `api.registerFlags(targetCommand, flags)`
|
|
86
|
+
|
|
87
|
+
Declare flags for a command (displayed in `--help` for dynamic extension commands):
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
api.registerFlags("acme sync", [
|
|
91
|
+
{ name: "--dry-run", description: "Simulate without writing" },
|
|
92
|
+
{ name: "--org <name>", description: "Organization name" },
|
|
93
|
+
]);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### `api.registerRenderer(format, renderer)`
|
|
97
|
+
|
|
98
|
+
Override TOON or JSON output for a command:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
api.registerRenderer("toon", (command, result, args, options, global, pmRoot) => {
|
|
102
|
+
if (command !== "stats") return undefined; // pass through
|
|
103
|
+
return customToonFormat(result);
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Return `undefined` to fall back to the built-in renderer.
|
|
108
|
+
|
|
109
|
+
### `api.registerImporter(name, importer)`
|
|
110
|
+
|
|
111
|
+
Register an importer (also wires `<name> import` command path):
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
api.registerImporter("jira", async (options, global) => {
|
|
115
|
+
// options: parsed flags from `pm jira import ...`
|
|
116
|
+
return { ok: true, imported: 5, skipped: 0, ids: ["pm-xxxx"], warnings: [] };
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### `api.registerExporter(name, exporter)`
|
|
121
|
+
|
|
122
|
+
Register an exporter (also wires `<name> export` command path):
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
api.registerExporter("jira", async (options, global) => {
|
|
126
|
+
return { ok: true, exported: 5, ids: ["pm-xxxx"], warnings: [] };
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### `api.registerItemFields(fields)`
|
|
131
|
+
|
|
132
|
+
Declare additional front-matter fields for schema-awareness:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
api.registerItemFields([
|
|
136
|
+
{ name: "acme_epic_id", type: "string", optional: true },
|
|
137
|
+
]);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### `api.registerMigration(def)`
|
|
141
|
+
|
|
142
|
+
Declare a schema migration (tracked in `pm health`):
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
api.registerMigration({
|
|
146
|
+
id: "add-acme-epic-id",
|
|
147
|
+
description: "Add acme_epic_id field to existing items",
|
|
148
|
+
mandatory: false,
|
|
149
|
+
status: "pending",
|
|
150
|
+
run: async (pmRoot) => {
|
|
151
|
+
// migrate items; update status to "applied" when done
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Migrations with `mandatory: true` and `status` not `"applied"` block write commands until resolved (bypass with `--force`).
|
|
157
|
+
|
|
158
|
+
### `api.registerSearchProvider(provider)`
|
|
159
|
+
|
|
160
|
+
Register a custom search provider:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
api.registerSearchProvider({
|
|
164
|
+
name: "elastic",
|
|
165
|
+
query: async (query, options, settings) => {
|
|
166
|
+
return [{ id: "pm-xxxx", score: 0.95 }];
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### `api.registerVectorStoreAdapter(adapter)`
|
|
172
|
+
|
|
173
|
+
Register a custom vector store:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
api.registerVectorStoreAdapter({
|
|
177
|
+
name: "pinecone",
|
|
178
|
+
upsert: async (records, settings) => { ... },
|
|
179
|
+
query: async (vector, topK, settings) => { ... },
|
|
180
|
+
delete: async (ids, settings) => { ... },
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Lifecycle Hooks
|
|
185
|
+
|
|
186
|
+
Hooks run for every applicable core operation. Hook handlers receive cloned context snapshots — mutations do not leak back into caller state.
|
|
187
|
+
|
|
188
|
+
### `api.hooks.beforeCommand(hook)`
|
|
189
|
+
|
|
190
|
+
Runs before any command executes:
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
api.hooks.beforeCommand((ctx) => {
|
|
194
|
+
// ctx.command: string
|
|
195
|
+
// ctx.args: string[]
|
|
196
|
+
// ctx.options: Record<string,unknown>
|
|
197
|
+
// ctx.global: GlobalOptions
|
|
198
|
+
// ctx.pm_root: string
|
|
199
|
+
console.log(`[ext] before: ${ctx.command}`);
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### `api.hooks.afterCommand(hook)`
|
|
204
|
+
|
|
205
|
+
Runs after a command completes (even on failure):
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
api.hooks.afterCommand((ctx) => {
|
|
209
|
+
// ctx.ok: boolean
|
|
210
|
+
// ctx.result?: unknown
|
|
211
|
+
// ctx.error?: unknown
|
|
212
|
+
// same fields as beforeCommand
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### `api.hooks.onWrite(hook)`
|
|
217
|
+
|
|
218
|
+
Runs before each item file write:
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
api.hooks.onWrite((ctx) => {
|
|
222
|
+
// ctx.path: string
|
|
223
|
+
// ctx.op: string (create, update, restore, etc.)
|
|
224
|
+
// ctx.item_id: string
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### `api.hooks.onRead(hook)`
|
|
229
|
+
|
|
230
|
+
Runs after each item file read:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
api.hooks.onRead((ctx) => {
|
|
234
|
+
// ctx.path: string
|
|
235
|
+
// ctx.item_id: string
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### `api.hooks.onIndex(hook)`
|
|
240
|
+
|
|
241
|
+
Runs during reindex/gc operations:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
api.hooks.onIndex((ctx) => {
|
|
245
|
+
// ctx.mode: "keyword" | "semantic" | "hybrid" | "gc"
|
|
246
|
+
// ctx.total?: number
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Health and Diagnostics
|
|
251
|
+
|
|
252
|
+
`pm health` probes all loaded extensions and surfaces:
|
|
253
|
+
|
|
254
|
+
- `extension_load_failed:<layer>:<name>` — manifest parse or module import error
|
|
255
|
+
- `extension_activate_failed:<layer>:<name>` — exception in `activate()`
|
|
256
|
+
- `extension_entry_outside_extension:<layer>:<name>` — entry path escapes directory
|
|
257
|
+
- `extension_capability_unknown:<layer>:<name>:<capability>` — unknown capability in manifest
|
|
258
|
+
|
|
259
|
+
Use `pm health --json` to parse diagnostics programmatically.
|
|
260
|
+
|
|
261
|
+
## Disabling Extensions
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
pm --no-extensions list-open # disable all extensions for this invocation
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Or configure per-project in `.agents/pm/settings.json`:
|
|
268
|
+
|
|
269
|
+
```json
|
|
270
|
+
{
|
|
271
|
+
"extensions": {
|
|
272
|
+
"disabled": ["pm-ext-example"]
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Example: Minimal Custom Command
|
|
278
|
+
|
|
279
|
+
**`~/.pm-cli/extensions/hello/manifest.json`:**
|
|
280
|
+
|
|
281
|
+
```json
|
|
282
|
+
{
|
|
283
|
+
"name": "hello",
|
|
284
|
+
"version": "0.1.0",
|
|
285
|
+
"entry": "./index.js",
|
|
286
|
+
"priority": 100,
|
|
287
|
+
"capabilities": ["commands"]
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**`~/.pm-cli/extensions/hello/index.js`:**
|
|
292
|
+
|
|
293
|
+
```js
|
|
294
|
+
export function activate(api) {
|
|
295
|
+
api.registerCommand({
|
|
296
|
+
name: "hello",
|
|
297
|
+
run: async (_args, _options, _global) => {
|
|
298
|
+
return { message: "Hello from extension!" };
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
pm hello
|
|
306
|
+
# => message: Hello from extension!
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Built-in Extensions
|
|
310
|
+
|
|
311
|
+
`pm-cli` ships two built-in extensions compiled into the package:
|
|
312
|
+
|
|
313
|
+
| Extension | Commands | Purpose |
|
|
314
|
+
|-----------|----------|---------|
|
|
315
|
+
| `builtin-beads-import` | `pm beads import` | Import Beads JSONL records into pm items |
|
|
316
|
+
| `builtin-todos-import-export` | `pm todos import`, `pm todos export` | Round-trip todos markdown format |
|
|
317
|
+
|
|
318
|
+
Built-in extensions are loaded automatically and cannot be disabled via settings (use `--no-extensions` to disable all extensions including built-ins).
|
|
319
|
+
|
|
320
|
+
## Pi Agent Extension
|
|
321
|
+
|
|
322
|
+
The bundled Pi tool wrapper lives at `.pi/extensions/pm-cli/index.ts` and is a Pi agent extension (not a pm-cli extension). Install it with:
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
pm install pi # to current project .pi/extensions/pm-cli/index.ts
|
|
326
|
+
pm install pi --global # to PI_CODING_AGENT_DIR/extensions/pm-cli/index.ts
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
See [AGENTS.md](../AGENTS.md) section 9 for full usage details.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Releasing `@unbrained/pm-cli`
|
|
2
|
+
|
|
3
|
+
This repository uses a tag-driven GitHub Actions release pipeline that publishes to npm and creates a GitHub Release.
|
|
4
|
+
|
|
5
|
+
## Version policy
|
|
6
|
+
|
|
7
|
+
`pm-cli` uses a calendar SemVer-compatible scheme:
|
|
8
|
+
|
|
9
|
+
- `YYYY.M.D` for the first release on a date
|
|
10
|
+
- `YYYY.M.D-N` for additional releases on the same date (`N >= 2`)
|
|
11
|
+
|
|
12
|
+
Examples:
|
|
13
|
+
|
|
14
|
+
- First release on 2026-03-09: `2026.3.9`
|
|
15
|
+
- Second release on 2026-03-09: `2026.3.9-2`
|
|
16
|
+
|
|
17
|
+
Use:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm version:next
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
to compute the next expected release version from the npm registry.
|
|
24
|
+
|
|
25
|
+
## One-time GitHub setup
|
|
26
|
+
|
|
27
|
+
- Create GitHub Environment `release`
|
|
28
|
+
- Add environment secret `NPM_TOKEN` (npm automation token with publish rights for `@unbrained/pm-cli`)
|
|
29
|
+
|
|
30
|
+
## Release checklist
|
|
31
|
+
|
|
32
|
+
1) Update `package.json` version and `CHANGELOG.md`
|
|
33
|
+
|
|
34
|
+
2) Run release gates locally:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pnpm install
|
|
38
|
+
pnpm build
|
|
39
|
+
pnpm typecheck
|
|
40
|
+
pnpm test
|
|
41
|
+
pnpm test:coverage
|
|
42
|
+
pnpm version:check
|
|
43
|
+
pnpm security:scan
|
|
44
|
+
pnpm smoke:npx
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
3) Commit and tag:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
git tag v<version>
|
|
51
|
+
git push origin main
|
|
52
|
+
git push origin v<version>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
4) Verify Actions:
|
|
56
|
+
|
|
57
|
+
- `CI` passes for commit
|
|
58
|
+
- `Release` workflow passes for tag
|
|
59
|
+
- npm package published at `@unbrained/pm-cli`
|
|
60
|
+
- GitHub Release created with generated notes
|
|
61
|
+
|
|
62
|
+
## Notes
|
|
63
|
+
|
|
64
|
+
- Do not run manual `npm publish`; publishing is owned by `.github/workflows/release.yml`.
|
|
65
|
+
- Release workflow enforces tag/version alignment and calendar version sequencing before publish.
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@unbrained/pm-cli",
|
|
3
|
+
"version": "2026.3.9",
|
|
4
|
+
"description": "Git-native project management CLI for humans and agents.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "unbrained",
|
|
7
|
+
"homepage": "https://github.com/unbraind/pm-cli#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/unbraind/pm-cli.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/unbraind/pm-cli/issues"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"pm": "dist/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist/**",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"AGENTS.md",
|
|
23
|
+
"PRD.md",
|
|
24
|
+
"docs/**",
|
|
25
|
+
".pi/extensions/pm-cli/index.ts",
|
|
26
|
+
"scripts/install.sh",
|
|
27
|
+
"scripts/install.ps1"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc -p tsconfig.json",
|
|
31
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
32
|
+
"start": "node dist/cli.js",
|
|
33
|
+
"dev": "tsx src/cli.ts",
|
|
34
|
+
"test": "pnpm build && vitest run",
|
|
35
|
+
"test:coverage": "pnpm build && vitest run --coverage",
|
|
36
|
+
"version:check": "node scripts/release-version.mjs check",
|
|
37
|
+
"version:next": "node scripts/release-version.mjs next",
|
|
38
|
+
"security:scan": "node scripts/check-secrets.mjs",
|
|
39
|
+
"smoke:npx": "node scripts/smoke-npx-from-pack.mjs",
|
|
40
|
+
"prepublishOnly": "pnpm build"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"cli",
|
|
44
|
+
"project-management",
|
|
45
|
+
"agents",
|
|
46
|
+
"ai",
|
|
47
|
+
"git-native",
|
|
48
|
+
"typescript",
|
|
49
|
+
"task-tracker",
|
|
50
|
+
"coding-agents"
|
|
51
|
+
],
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
},
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": ">=20"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@toon-format/toon": "latest",
|
|
61
|
+
"commander": "latest",
|
|
62
|
+
"fast-json-patch": "latest",
|
|
63
|
+
"undici": "latest",
|
|
64
|
+
"zod": "latest"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@types/node": "latest",
|
|
68
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
69
|
+
"c8": "^11.0.0",
|
|
70
|
+
"tsx": "latest",
|
|
71
|
+
"typescript": "latest",
|
|
72
|
+
"vitest": "^4.0.18"
|
|
73
|
+
},
|
|
74
|
+
"pnpm": {
|
|
75
|
+
"overrides": {
|
|
76
|
+
"rollup": ">=4.59.0"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[string]$Version = "latest",
|
|
3
|
+
[string]$Prefix = "",
|
|
4
|
+
[string]$PackageName = ""
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
Set-StrictMode -Version Latest
|
|
8
|
+
$ErrorActionPreference = "Stop"
|
|
9
|
+
|
|
10
|
+
$envPackageName = $env:PM_CLI_PACKAGE
|
|
11
|
+
if ([string]::IsNullOrWhiteSpace($PackageName)) {
|
|
12
|
+
if ([string]::IsNullOrWhiteSpace($envPackageName)) {
|
|
13
|
+
$PackageName = "@unbrained/pm-cli"
|
|
14
|
+
} else {
|
|
15
|
+
$PackageName = $envPackageName
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function Require-Command {
|
|
20
|
+
param([string]$Name)
|
|
21
|
+
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
|
|
22
|
+
throw "Required command not found: $Name"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function Use-LiteralInstallSpec {
|
|
27
|
+
param([string]$Name)
|
|
28
|
+
|
|
29
|
+
if ([string]::IsNullOrWhiteSpace($Name)) {
|
|
30
|
+
return $false
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if ($Name -match "^(file:|https?://|git\+|npm:)") {
|
|
34
|
+
return $true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if ($Name.StartsWith("./") -or $Name.StartsWith("../") -or $Name.StartsWith("/") -or $Name.StartsWith("~/")) {
|
|
38
|
+
return $true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if ($Name.Contains('\') -or $Name -match "^[A-Za-z]:[\\/]") {
|
|
42
|
+
return $true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if ($Name.EndsWith(".tgz") -or $Name.EndsWith(".tar.gz")) {
|
|
46
|
+
return $true
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if ($Name.StartsWith("@")) {
|
|
50
|
+
return $Name -match "^@[^/]+/[^@]+@.+$"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return $Name.Contains("@")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
Require-Command "node"
|
|
57
|
+
Require-Command "npm"
|
|
58
|
+
|
|
59
|
+
$installSpec = $null
|
|
60
|
+
if (Use-LiteralInstallSpec $PackageName) {
|
|
61
|
+
$installSpec = $PackageName
|
|
62
|
+
} else {
|
|
63
|
+
$installSpec = "$PackageName@$Version"
|
|
64
|
+
}
|
|
65
|
+
Write-Host "Installing or updating $installSpec..."
|
|
66
|
+
$npmArgs = @("install", "-g", $installSpec)
|
|
67
|
+
if ($Prefix -ne "") {
|
|
68
|
+
$npmArgs += @("--prefix", $Prefix)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
& npm @npmArgs
|
|
72
|
+
if ($LASTEXITCODE -ne 0) {
|
|
73
|
+
throw "npm install failed with exit code $LASTEXITCODE"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
$pmCommand = Get-Command "pm" -ErrorAction SilentlyContinue
|
|
77
|
+
if ($pmCommand) {
|
|
78
|
+
$pmExecutable = $pmCommand.Source
|
|
79
|
+
} else {
|
|
80
|
+
$candidates = @()
|
|
81
|
+
if ($Prefix -ne "") {
|
|
82
|
+
$candidates += (Join-Path $Prefix "pm.cmd")
|
|
83
|
+
$candidates += (Join-Path $Prefix "bin/pm.cmd")
|
|
84
|
+
$candidates += (Join-Path $Prefix "pm")
|
|
85
|
+
$candidates += (Join-Path $Prefix "bin/pm")
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
$resolved = $null
|
|
89
|
+
foreach ($candidate in $candidates) {
|
|
90
|
+
if (Test-Path $candidate) {
|
|
91
|
+
$resolved = $candidate
|
|
92
|
+
break
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (-not $resolved) {
|
|
97
|
+
$hint = ""
|
|
98
|
+
if ($Prefix -ne "") {
|
|
99
|
+
$hint = " Add '$Prefix' (or '$Prefix\bin') to PATH and retry."
|
|
100
|
+
}
|
|
101
|
+
throw "pm binary not found after install.$hint"
|
|
102
|
+
}
|
|
103
|
+
$pmExecutable = $resolved
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
$versionOutput = & $pmExecutable --version
|
|
107
|
+
if ($LASTEXITCODE -ne 0) {
|
|
108
|
+
throw "pm --version failed with exit code $LASTEXITCODE"
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
Write-Host "Installed pm version: $versionOutput"
|
|
112
|
+
Write-Host "Done."
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
PACKAGE_NAME="${PM_CLI_PACKAGE:-@unbrained/pm-cli}"
|
|
5
|
+
TARGET_VERSION="latest"
|
|
6
|
+
PREFIX=""
|
|
7
|
+
|
|
8
|
+
usage() {
|
|
9
|
+
cat <<'EOF'
|
|
10
|
+
Install or update @unbrained/pm-cli globally via npm.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
bash scripts/install.sh [--version <tag>] [--prefix <dir>]
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
--version <tag> Package tag/version to install (default: latest)
|
|
17
|
+
--prefix <dir> npm global prefix override
|
|
18
|
+
-h, --help Show this help message
|
|
19
|
+
EOF
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
require_command() {
|
|
23
|
+
local cmd="$1"
|
|
24
|
+
if ! command -v "$cmd" >/dev/null 2>&1; then
|
|
25
|
+
echo "error: required command not found: $cmd" >&2
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
is_literal_install_spec() {
|
|
31
|
+
local name="$1"
|
|
32
|
+
|
|
33
|
+
if [[ "$name" == file:* || "$name" == http://* || "$name" == https://* || "$name" == git+* || "$name" == npm:* ]]; then
|
|
34
|
+
return 0
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
if [[ "$name" == ./* || "$name" == ../* || "$name" == /* || "$name" == ~/* || "$name" == *.tgz || "$name" == *.tar.gz || "$name" == *\\* ]]; then
|
|
38
|
+
return 0
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Scoped names use @scope/pkg; only treat them as literal when explicitly versioned.
|
|
42
|
+
if [[ "$name" == @*/*@* ]]; then
|
|
43
|
+
return 0
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
if [[ "$name" != @* && "$name" == *@* ]]; then
|
|
47
|
+
return 0
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
return 1
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
while [[ $# -gt 0 ]]; do
|
|
54
|
+
case "$1" in
|
|
55
|
+
--version)
|
|
56
|
+
if [[ $# -lt 2 ]]; then
|
|
57
|
+
echo "error: --version requires a value" >&2
|
|
58
|
+
exit 2
|
|
59
|
+
fi
|
|
60
|
+
TARGET_VERSION="$2"
|
|
61
|
+
shift 2
|
|
62
|
+
;;
|
|
63
|
+
--prefix)
|
|
64
|
+
if [[ $# -lt 2 ]]; then
|
|
65
|
+
echo "error: --prefix requires a value" >&2
|
|
66
|
+
exit 2
|
|
67
|
+
fi
|
|
68
|
+
PREFIX="$2"
|
|
69
|
+
shift 2
|
|
70
|
+
;;
|
|
71
|
+
-h|--help)
|
|
72
|
+
usage
|
|
73
|
+
exit 0
|
|
74
|
+
;;
|
|
75
|
+
*)
|
|
76
|
+
echo "error: unknown argument: $1" >&2
|
|
77
|
+
usage
|
|
78
|
+
exit 2
|
|
79
|
+
;;
|
|
80
|
+
esac
|
|
81
|
+
done
|
|
82
|
+
|
|
83
|
+
require_command node
|
|
84
|
+
require_command npm
|
|
85
|
+
|
|
86
|
+
if is_literal_install_spec "$PACKAGE_NAME"; then
|
|
87
|
+
INSTALL_SPEC="$PACKAGE_NAME"
|
|
88
|
+
else
|
|
89
|
+
INSTALL_SPEC="${PACKAGE_NAME}@${TARGET_VERSION}"
|
|
90
|
+
fi
|
|
91
|
+
INSTALL_CMD=(npm install -g "$INSTALL_SPEC")
|
|
92
|
+
if [[ -n "$PREFIX" ]]; then
|
|
93
|
+
INSTALL_CMD+=(--prefix "$PREFIX")
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
echo "Installing ${INSTALL_SPEC}..."
|
|
97
|
+
"${INSTALL_CMD[@]}"
|
|
98
|
+
|
|
99
|
+
PM_BIN="pm"
|
|
100
|
+
if ! command -v "$PM_BIN" >/dev/null 2>&1; then
|
|
101
|
+
if [[ -n "$PREFIX" && -x "${PREFIX}/bin/pm" ]]; then
|
|
102
|
+
PM_BIN="${PREFIX}/bin/pm"
|
|
103
|
+
else
|
|
104
|
+
echo "error: pm binary not found on PATH after install." >&2
|
|
105
|
+
if [[ -n "$PREFIX" ]]; then
|
|
106
|
+
echo "hint: add ${PREFIX}/bin to your PATH or rerun without --prefix." >&2
|
|
107
|
+
fi
|
|
108
|
+
exit 1
|
|
109
|
+
fi
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
echo "Installed pm version: $($PM_BIN --version)"
|
|
113
|
+
echo "Done."
|