@arshdelight/practi 0.9.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/LICENSE +21 -0
- package/README.md +81 -0
- package/dist/client.js +84 -0
- package/dist/client.js.map +1 -0
- package/dist/cmd/blob.js +97 -0
- package/dist/cmd/blob.js.map +1 -0
- package/dist/cmd/clone.js +76 -0
- package/dist/cmd/clone.js.map +1 -0
- package/dist/cmd/comment.js +210 -0
- package/dist/cmd/comment.js.map +1 -0
- package/dist/cmd/config.js +30 -0
- package/dist/cmd/config.js.map +1 -0
- package/dist/cmd/edit.js +122 -0
- package/dist/cmd/edit.js.map +1 -0
- package/dist/cmd/init.js +11 -0
- package/dist/cmd/init.js.map +1 -0
- package/dist/cmd/lifecycle.js +69 -0
- package/dist/cmd/lifecycle.js.map +1 -0
- package/dist/cmd/login.js +125 -0
- package/dist/cmd/login.js.map +1 -0
- package/dist/cmd/ls.js +59 -0
- package/dist/cmd/ls.js.map +1 -0
- package/dist/cmd/new.js +57 -0
- package/dist/cmd/new.js.map +1 -0
- package/dist/cmd/pull.js +94 -0
- package/dist/cmd/pull.js.map +1 -0
- package/dist/cmd/push.js +84 -0
- package/dist/cmd/push.js.map +1 -0
- package/dist/cmd/remote.js +48 -0
- package/dist/cmd/remote.js.map +1 -0
- package/dist/cmd/search.js +137 -0
- package/dist/cmd/search.js.map +1 -0
- package/dist/cmd/show.js +61 -0
- package/dist/cmd/show.js.map +1 -0
- package/dist/cmd/skill.js +198 -0
- package/dist/cmd/skill.js.map +1 -0
- package/dist/cmd/spec.js +10 -0
- package/dist/cmd/spec.js.map +1 -0
- package/dist/cmd/update.js +60 -0
- package/dist/cmd/update.js.map +1 -0
- package/dist/credentials.js +65 -0
- package/dist/credentials.js.map +1 -0
- package/dist/index.js +412 -0
- package/dist/index.js.map +1 -0
- package/dist/oauth.js +244 -0
- package/dist/oauth.js.map +1 -0
- package/dist/render.js +25 -0
- package/dist/render.js.map +1 -0
- package/dist/state.js +74 -0
- package/dist/state.js.map +1 -0
- package/dist/version.js +12 -0
- package/dist/version.js.map +1 -0
- package/dist/web.js +243 -0
- package/dist/web.js.map +1 -0
- package/dist/workspace.js +38 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +48 -0
- package/skills/use-practi/SKILL.md +186 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 arsh tech
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# practi
|
|
2
|
+
|
|
3
|
+
The `practi` command (formerly `pop` / `@arshdelight/pop-cli`): a local registry for POP (Protocol of Practice) documents — a personal, content-addressed collection of practice documents that syncs to [PractiHub](https://practihub.com), built on [@arshdelight/pop-sdk](https://www.npmjs.com/package/@arshdelight/pop-sdk).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @arshdelight/practi
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
practi version | --version show CLI + pop-spec versions
|
|
15
|
+
practi spec print the bundled pop-spec.md (no network fetch)
|
|
16
|
+
practi update self-update via npm (checks the registry's latest)
|
|
17
|
+
practi init [path] initialize a data directory (default: ~/.practi)
|
|
18
|
+
practi config show data dir, remote, registry summary
|
|
19
|
+
practi remote set <url> set the remote provider (e.g. https://practihub.com)
|
|
20
|
+
practi remote show | remove inspect / clear the remote
|
|
21
|
+
practi ls [-a] [--json] list direct POPs (-a also lists indirect nodes)
|
|
22
|
+
practi new <file.json> create a POP from a JSON document (or --json '<text>', or stdin)
|
|
23
|
+
practi clone <hash> fetch a public POP and claim it (local direct + remote claim)
|
|
24
|
+
practi edit <hash> <file.json> replace a direct POP with new content (new hash; auto-revision
|
|
25
|
+
+ GC of nodes unreachable from any direct root)
|
|
26
|
+
practi show <hash> [--json] [--doc] inspect one node (hash prefix OK)
|
|
27
|
+
practi web [--port 4317] browse direct POPs in a local web UI
|
|
28
|
+
practi login [--no-open] OAuth login to the remote (browser authorize; --no-open prints the URL)
|
|
29
|
+
practi logout clear stored credentials (revokes on the server)
|
|
30
|
+
practi me show the authenticated remote user
|
|
31
|
+
practi push [hash] push new local claims to the remote (only new ones; stored PRIVATE)
|
|
32
|
+
practi pull [hash] sync YOUR claims from the remote (default: all of mine)
|
|
33
|
+
practi search [query...] search POPs on the remote (title-first; empty = browse)
|
|
34
|
+
[--scope public|me|all] [--limit N] [--json]
|
|
35
|
+
practi submit [hash] submit POPs for public review (default: all direct)
|
|
36
|
+
practi unpublish [hash] withdraw a submission / take one back out of public
|
|
37
|
+
practi delete <hash> remove your direct claim on the remote (hash required)
|
|
38
|
+
practi blob add <file-or-url> stage an attachment; emits the attachment entry
|
|
39
|
+
(hashes the bytes, stores local blobs in the workspace)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The data directory is a POP workspace (nodes content-addressed under `nodes/*.md`); `practi.json` records the remote provider and the registered **direct** roots (indirect = every other node the direct POPs reference).
|
|
43
|
+
|
|
44
|
+
### Login to PractiHub
|
|
45
|
+
|
|
46
|
+
The CLI defaults its remote to **https://practihub.com** — `practi login` works out of the box; run `practi remote set <url>` only when pointing at another hub (e.g. a local dev server).
|
|
47
|
+
|
|
48
|
+
`practi login` authenticates via **OAuth 2.1 Authorization Code + PKCE** (loopback redirect, RFC 8252): it opens your browser to PractiHub's consent page, exchanges the code for tokens, and stores them in `practi.auth.json` (next to `practi.json`). Credentials are deliberately **kept out of `practi.json`** so the workspace stays commit-safe; on POSIX the file is `chmod 600`.
|
|
49
|
+
|
|
50
|
+
- Credentials are long-lived but auto-refreshing: commands use the refresh token (rotation) to renew the 1-hour access token, so once logged in, an agent can keep using `practi` without interactive prompts.
|
|
51
|
+
- `practi logout` revokes the refresh token on the server (best-effort) and deletes `practi.auth.json`.
|
|
52
|
+
- `practi me` verifies the current session and prints the authenticated user / profile / granted scopes.
|
|
53
|
+
- Headless / agent environments: use `practi login --no-open`, then open the printed URL manually.
|
|
54
|
+
- Dead credentials never block a re-login: `practi login` probes the stored session — if the remote rejects it (or it was issued by a different hub), it is auto-cleared and the login proceeds; if the hub is merely unreachable, credentials are kept and the error says `cannot reach <remote>` instead of blaming the session. `practi remote set` revokes (best-effort) and clears credentials issued by the previous hub.
|
|
55
|
+
|
|
56
|
+
### Edit
|
|
57
|
+
|
|
58
|
+
Editing is replacing: content addressing means an edit produces a **new root hash**. `practi edit <hash> <file.json>` (or `--json` / stdin) validates and stores the new tree, swaps the old root out of the direct registration, and auto-appends a revision record on the root (`from` = old root hash — a history pointer that may dangle by design, never validated). Nodes no longer reachable from any direct root are garbage-collected — the local counterpart of the hub's claim reconciliation: content still referenced by another direct POP survives, exclusive descendants go (`--keep` preserves them; `--message` sets the revision note; `--no-revision` skips it). Purely local — the hub still holds the old version: sync with `practi push` + `practi delete <old-root>`.
|
|
59
|
+
|
|
60
|
+
### Push, pull & clone
|
|
61
|
+
|
|
62
|
+
- `practi push [hash]` pushes **only the local claims the remote doesn't already have for you** (git-style): it first fetches your claim list (`/mine`, paginated), diffs against your local **direct** roots, and uploads just the new ones via `POST /api/v1/pop` (content-addressed, idempotent). Documents are stored **PRIVATE**; making one public is a separate review step (submit → auto review), keeping "record first, publish later" intact.
|
|
63
|
+
- `practi pull [hash]` syncs **your own** claims from the remote: it fetches your claim list (`/mine`, paginated), diffs against your local direct roots, and pulls the missing documents (registered as **direct**). It never touches the public library — there is no ownership guessing; anything outside your claims is public content for `practi clone`.
|
|
64
|
+
- `practi clone <hash>` fetches a **public** document and claims it (fork semantics): the document lands in your workspace, is registered as a local **direct** root, and is pushed back to the remote so the claim is consistent both ways. Cloning something you already claim is idempotent.
|
|
65
|
+
- Attachments: the hub does not store attachment bytes, so push sends the document with attachment pointers only; blobs stay local (or external `url` if provided).
|
|
66
|
+
|
|
67
|
+
### Search
|
|
68
|
+
|
|
69
|
+
- `practi search <query...>` searches the remote via `GET /api/v1/pop/search` — both title and content match, **title hits rank first**; an empty query browses the newest documents.
|
|
70
|
+
- `--scope public` (default) searches the published library; `--scope me` searches your own documents in **any status** (including PRIVATE); `--scope all` is the union — your visible universe.
|
|
71
|
+
- `--json` dumps the raw API response. Fetch any result with `practi clone <hash>`.
|
|
72
|
+
|
|
73
|
+
### Lifecycle (submit / unpublish / delete)
|
|
74
|
+
|
|
75
|
+
- `practi submit [hash]` submits your direct pops for public review (`PRIVATE → PENDING_REVIEW`; an auto review passes before they go public — already-approved content skips re-review). Omit the hash to submit all direct pops — non-PRIVATE ones are skipped and counted as failures.
|
|
76
|
+
- `practi unpublish [hash]` withdraws a pending submission or takes a published POP back out of public distribution (`→ PRIVATE`).
|
|
77
|
+
- `practi delete <hash>` removes your direct claim on the remote (children's indirect claims are reclaimed; a document with no claims left is hard-deleted). Explicit hash required — there is no delete-all. Pure remote operation: your local workspace is untouched, and pushing the same content again recreates it (fresh row, PRIVATE).
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT © 2026 arsh tech
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { loadCredentials, saveCredentials, isAccessTokenFresh, } from './credentials.js';
|
|
2
|
+
import { refreshAccessToken } from './oauth.js';
|
|
3
|
+
/** refresh 被服务端拒绝(token 端点有应答但 HTTP 报错)——凭据已死,需重新 login */
|
|
4
|
+
export class RefreshRejectedError extends Error {
|
|
5
|
+
}
|
|
6
|
+
/** token 端点不可达(网络/超时)——凭据生死未知,报"连不上"而非"已过期" */
|
|
7
|
+
export class HubUnreachableError extends Error {
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 登录态之上的远程请求层:保证请求带有效 Bearer access token。
|
|
11
|
+
* - access token 已过期 → 用 refresh token 换新对(rotation,回写 practi.auth.json)
|
|
12
|
+
* - 401(token 被服务端撤销/aud 不符)→ 刷新一次并重试
|
|
13
|
+
* 刷新失败(refresh 也被撤销/过期)→ 抛错提示重新 `pop login`。
|
|
14
|
+
*/
|
|
15
|
+
export function requireCredentials(dataDir) {
|
|
16
|
+
const creds = loadCredentials(dataDir);
|
|
17
|
+
if (!creds) {
|
|
18
|
+
throw new Error('not logged in — run `pop login` first');
|
|
19
|
+
}
|
|
20
|
+
return creds;
|
|
21
|
+
}
|
|
22
|
+
async function refreshAndSave(dataDir, remote, creds) {
|
|
23
|
+
let next;
|
|
24
|
+
try {
|
|
25
|
+
next = await refreshAccessToken(remote, creds.client_id, creds.refresh_token);
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
const msg = e.message;
|
|
29
|
+
if (/^token endpoint failed \(HTTP/.test(msg)) {
|
|
30
|
+
throw new RefreshRejectedError(`session expired or revoked — run \`pop login\` again (${msg})`);
|
|
31
|
+
}
|
|
32
|
+
throw new HubUnreachableError(`cannot reach ${remote} — is the hub running? (${msg})`);
|
|
33
|
+
}
|
|
34
|
+
const updated = {
|
|
35
|
+
...creds,
|
|
36
|
+
access_token: next.access_token,
|
|
37
|
+
refresh_token: next.refresh_token,
|
|
38
|
+
expires_at: Date.now() + next.expires_in * 1000,
|
|
39
|
+
scope: next.scope || creds.scope,
|
|
40
|
+
};
|
|
41
|
+
saveCredentials(dataDir, updated);
|
|
42
|
+
return updated;
|
|
43
|
+
}
|
|
44
|
+
/** 拿到一个未过期的 access token(必要时自动刷新) */
|
|
45
|
+
export async function validAccessToken(dataDir, remote) {
|
|
46
|
+
const creds = requireCredentials(dataDir);
|
|
47
|
+
if (isAccessTokenFresh(creds))
|
|
48
|
+
return creds;
|
|
49
|
+
return refreshAndSave(dataDir, remote, creds);
|
|
50
|
+
}
|
|
51
|
+
/** 向 remote 发带 Bearer 的请求;401 时刷新一次并重试 */
|
|
52
|
+
export async function authedFetch(dataDir, remote, path, init) {
|
|
53
|
+
const creds = await validAccessToken(dataDir, remote);
|
|
54
|
+
const url = /^https?:\/\//i.test(path) ? path : `${remote}${path}`;
|
|
55
|
+
const call = (token) => fetch(url, {
|
|
56
|
+
...init,
|
|
57
|
+
headers: { ...(init?.headers ?? {}), authorization: `Bearer ${token}` },
|
|
58
|
+
});
|
|
59
|
+
let res = await call(creds.access_token);
|
|
60
|
+
if (res.status === 401) {
|
|
61
|
+
const refreshed = await refreshAndSave(dataDir, remote, creds);
|
|
62
|
+
res = await call(refreshed.access_token);
|
|
63
|
+
}
|
|
64
|
+
return res;
|
|
65
|
+
}
|
|
66
|
+
/** hub 上「我的」认领列表(DIRECT 根),按 total 分页拉全——push/pull 做认领表 diff 的基准。 */
|
|
67
|
+
export async function fetchMine(dataDir, remote) {
|
|
68
|
+
const out = [];
|
|
69
|
+
const limit = 100;
|
|
70
|
+
for (let page = 1;; page++) {
|
|
71
|
+
const res = await authedFetch(dataDir, remote, `/api/v1/pop/mine?page=${page}&limit=${limit}`);
|
|
72
|
+
if (!res.ok) {
|
|
73
|
+
const body = (await res.json().catch(() => ({})));
|
|
74
|
+
throw new Error(`listing own documents failed — ${body.error ?? `HTTP ${res.status}`}`);
|
|
75
|
+
}
|
|
76
|
+
const body = (await res.json());
|
|
77
|
+
const results = body.results ?? [];
|
|
78
|
+
out.push(...results);
|
|
79
|
+
if (results.length === 0 || out.length >= (body.total ?? out.length))
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/cmd/blob.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { storeBlob } from '@arshdelight/pop-sdk';
|
|
5
|
+
import { defaultDataDir } from '../state.js';
|
|
6
|
+
import { isInitialized } from '../workspace.js';
|
|
7
|
+
/**
|
|
8
|
+
* Stage an attachment and emit the ready-to-paste attachment entry.
|
|
9
|
+
* - Local file: hashed + stored into the workspace blob channel (when the data
|
|
10
|
+
* dir is initialized), entry emitted without url.
|
|
11
|
+
* - http(s) URL: fetched once to compute the identity hash, entry emitted WITH
|
|
12
|
+
* the source url (bytes stay external, spec §5 — identity remains the hash).
|
|
13
|
+
*
|
|
14
|
+
* Attachments are immutable content: the emitted entry goes into the author's
|
|
15
|
+
* document JSON (then `practi new`), never mutated onto an existing node.
|
|
16
|
+
*/
|
|
17
|
+
export async function runBlobAdd(opts) {
|
|
18
|
+
const target = opts.positional[0];
|
|
19
|
+
if (!target) {
|
|
20
|
+
console.error('usage: practi blob add <file-or-url> [--name <name>]');
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
const dataDir = opts.dataDir ?? defaultDataDir();
|
|
24
|
+
if (/^https?:\/\//i.test(target))
|
|
25
|
+
return blobFromUrl(target, opts.name);
|
|
26
|
+
return blobFromFile(target, opts.name, dataDir);
|
|
27
|
+
}
|
|
28
|
+
function blobFromFile(target, name, dataDir) {
|
|
29
|
+
const bytes = fs.readFileSync(target);
|
|
30
|
+
const base = path.basename(target);
|
|
31
|
+
const hash = sha256(bytes);
|
|
32
|
+
if (isInitialized(dataDir)) {
|
|
33
|
+
const stored = storeBlob(dataDir, bytes);
|
|
34
|
+
if (stored !== hash)
|
|
35
|
+
throw new Error('internal error: stored blob hash mismatch');
|
|
36
|
+
console.log(`stored: ${stored} (in ${dataDir})`);
|
|
37
|
+
}
|
|
38
|
+
printEntry({
|
|
39
|
+
name: name ?? base,
|
|
40
|
+
hash,
|
|
41
|
+
mime: mimeFromName(base),
|
|
42
|
+
size: bytes.length,
|
|
43
|
+
});
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
const MAX_URL_BYTES = 25 * 1024 * 1024;
|
|
47
|
+
async function blobFromUrl(url, name) {
|
|
48
|
+
const res = await fetch(url, { redirect: 'follow' });
|
|
49
|
+
if (!res.ok)
|
|
50
|
+
throw new Error(`fetch ${url} → HTTP ${res.status}`);
|
|
51
|
+
const contentType = res.headers.get('content-type')?.split(';')[0].trim().toLowerCase();
|
|
52
|
+
const declared = Number(res.headers.get('content-length') ?? 0);
|
|
53
|
+
if (Number.isFinite(declared) && declared > MAX_URL_BYTES) {
|
|
54
|
+
throw new Error(`file too large: ${declared} bytes (limit ${MAX_URL_BYTES})`);
|
|
55
|
+
}
|
|
56
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
57
|
+
if (bytes.length > MAX_URL_BYTES) {
|
|
58
|
+
throw new Error(`file too large: ${bytes.length} bytes (limit ${MAX_URL_BYTES})`);
|
|
59
|
+
}
|
|
60
|
+
let base = '';
|
|
61
|
+
try {
|
|
62
|
+
base = path.posix.basename(new URL(url).pathname);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
base = '';
|
|
66
|
+
}
|
|
67
|
+
const mimeExt = mimeFromName(base);
|
|
68
|
+
printEntry({
|
|
69
|
+
name: name ?? (base && base !== '/' ? decodeURIComponent(base) : 'attachment'),
|
|
70
|
+
hash: sha256(bytes),
|
|
71
|
+
mime: contentType && contentType !== 'application/octet-stream' ? contentType : mimeExt,
|
|
72
|
+
size: bytes.length,
|
|
73
|
+
url,
|
|
74
|
+
});
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
function printEntry(entry) {
|
|
78
|
+
console.log(JSON.stringify(entry, null, 2));
|
|
79
|
+
console.log('\npaste this object into an action\'s "attachments", then reference it in content as ');
|
|
80
|
+
}
|
|
81
|
+
function sha256(bytes) {
|
|
82
|
+
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
83
|
+
}
|
|
84
|
+
const MIME = {
|
|
85
|
+
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif',
|
|
86
|
+
webp: 'image/webp', svg: 'image/svg+xml', avif: 'image/avif', ico: 'image/x-icon',
|
|
87
|
+
bmp: 'image/bmp', mp4: 'video/mp4', webm: 'video/webm', mov: 'video/quicktime',
|
|
88
|
+
mp3: 'audio/mpeg', wav: 'audio/wav', pdf: 'application/pdf',
|
|
89
|
+
md: 'text/markdown', txt: 'text/plain', json: 'application/json',
|
|
90
|
+
csv: 'text/csv', zip: 'application/zip', gz: 'application/gzip',
|
|
91
|
+
yaml: 'text/yaml', yml: 'text/yaml', wasm: 'application/wasm',
|
|
92
|
+
};
|
|
93
|
+
function mimeFromName(name) {
|
|
94
|
+
const ext = path.extname(name).slice(1).toLowerCase();
|
|
95
|
+
return MIME[ext] ?? 'application/octet-stream';
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=blob.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blob.js","sourceRoot":"","sources":["../../src/cmd/blob.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createFromDoc, parseDocument } from '@arshdelight/pop-sdk';
|
|
2
|
+
import { defaultDataDir, loadState, saveState } from '../state.js';
|
|
3
|
+
import { openWorkspace } from '../workspace.js';
|
|
4
|
+
import { authedFetch } from '../client.js';
|
|
5
|
+
/**
|
|
6
|
+
* practi clone <hash>:从 hub 取一个文档并认领它(git clone / fork 语义)。
|
|
7
|
+
* - 只用于公共(PUBLISHED)或你本就可读的内容;POP 认领制「能拿到即可认领」。
|
|
8
|
+
* - 本地:落 workspace + 注册为 direct;远端:POST /api/v1/pop(内容寻址幂等)→
|
|
9
|
+
* 你也获得 hub 认领——本地/hub 双向一致,之后 `practi pull` 能拉回。
|
|
10
|
+
* - 已在你的认领里 → 幂等返回。
|
|
11
|
+
*/
|
|
12
|
+
export async function runClone(opts) {
|
|
13
|
+
const dataDir = opts.dataDir ?? defaultDataDir();
|
|
14
|
+
const state = loadState(dataDir);
|
|
15
|
+
if (!state.remote) {
|
|
16
|
+
console.error('error: no remote configured — run `practi remote set <url>` first');
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
const ref = opts.positional[0];
|
|
20
|
+
if (!ref) {
|
|
21
|
+
console.error('usage: practi clone <hash>');
|
|
22
|
+
return 1;
|
|
23
|
+
}
|
|
24
|
+
const remote = state.remote.url;
|
|
25
|
+
// 取文档体(公开可读;私有且非本人会被 hub 拒)
|
|
26
|
+
const res = await authedFetch(dataDir, remote, `/api/v1/pop/${encodeURIComponent(ref)}`);
|
|
27
|
+
const body = (await res.json().catch(() => ({})));
|
|
28
|
+
if (!res.ok) {
|
|
29
|
+
const detail = typeof body.message === 'string' ? body.message : typeof body.error === 'string' ? body.error : `HTTP ${res.status}`;
|
|
30
|
+
console.error(`clone failed: ${ref} — ${detail}`);
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
if (body.document === undefined || body.document === null) {
|
|
34
|
+
console.error(`clone failed: ${ref} — response missing document`);
|
|
35
|
+
return 1;
|
|
36
|
+
}
|
|
37
|
+
// 入库前验算(内容寻址的客户端责任):本地算出的根哈希必须等于请求地址(也是服务器
|
|
38
|
+
// 声称的 root_hash)——注册与推送一律用本地算的值,不采信服务器声称的哈希
|
|
39
|
+
let computedRoot;
|
|
40
|
+
try {
|
|
41
|
+
computedRoot = parseDocument(body.document).rootHash;
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
console.error(`clone failed: ${ref} — invalid document: ${e.message}`);
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
if (computedRoot !== ref || (body.root_hash !== undefined && body.root_hash !== computedRoot)) {
|
|
48
|
+
console.error(`clone failed: ${ref} — hash mismatch: content hashes to ${computedRoot}`);
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
// 落本地 workspace + 注册 direct
|
|
52
|
+
const ws = openWorkspace(dataDir);
|
|
53
|
+
createFromDoc(ws, body.document);
|
|
54
|
+
const rootHash = computedRoot;
|
|
55
|
+
const alreadyMine = body.ownership?.mine === true;
|
|
56
|
+
const localState = loadState(dataDir);
|
|
57
|
+
if (!localState.direct.includes(rootHash)) {
|
|
58
|
+
localState.direct.push(rootHash);
|
|
59
|
+
saveState(dataDir, localState);
|
|
60
|
+
}
|
|
61
|
+
// 同步到 hub 认领(双向一致;幂等)
|
|
62
|
+
const pushRes = await authedFetch(dataDir, remote, '/api/v1/pop', {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: { 'content-type': 'application/json' },
|
|
65
|
+
body: JSON.stringify(body.document),
|
|
66
|
+
});
|
|
67
|
+
const pushBody = (await pushRes.json().catch(() => ({})));
|
|
68
|
+
if (!pushRes.ok) {
|
|
69
|
+
const detail = typeof pushBody.message === 'string' ? pushBody.message : typeof pushBody.error === 'string' ? pushBody.error : `HTTP ${pushRes.status}`;
|
|
70
|
+
console.error(`clone failed: ${rootHash} — could not claim on remote: ${detail}`);
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
console.log(`cloned: ${rootHash} (${body.status ?? 'PUBLISHED'}) [direct]${alreadyMine ? ' (already yours on the remote)' : ' (claimed on remote)'}`);
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=clone.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clone.js","sourceRoot":"","sources":["../../src/cmd/clone.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAO3C;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAe;IAC5C,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,cAAc,EAAE,CAAC;IACjD,MAAM,KAAK,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;IACjC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QAClB,OAAO,CAAC,KAAK,CAAC,mEAAmE,CAAC,CAAC;QACnF,OAAO,CAAC,CAAC;IACX,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IAC/B,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,CAAC,KAAK,CAAC,4BAA4B,CAAC,CAAC;QAC5C,OAAO,CAAC,CAAC;IACX,CAAC;IACD,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC;IAEhC,4BAA4B;IAC5B,MAAM,GAAG,GAAG,MAAM,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,eAAe,kBAAkB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACzF,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAO/C,CAAC;IACF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,MAAM,GAAG,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,MAAM,EAAE,CAAC;QACpI,OAAO,CAAC,KAAK,CAAC,iBAAiB,GAAG,MAAM,MAAM,EAAE,CAAC,CAAC;QAClD,OAAO,CAAC,CAAC;IACX,CAAC;IACD,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;QAC1D,OAAO,CAAC,KAAK,CAAC,iBAAiB,GAAG,8BAA8B,CAAC,CAAC;QAClE,OAAO,CAAC,CAAC;IACX,CAAC;IAED,2CAA2C;IAC3C,4CAA4C;IAC5C,IAAI,YAAoB,CAAC;IACzB,IAAI,CAAC;QACH,YAAY,GAAG,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;IACvD,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,CAAC,KAAK,CAAC,iBAAiB,GAAG,wBAAyB,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;QAClF,OAAO,CAAC,CAAC;IACX,CAAC;IACD,IAAI,YAAY,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,KAAK,SAAS,IAAI,IAAI,CAAC,SAAS,KAAK,YAAY,CAAC,EAAE,CAAC;QAC9F,OAAO,CAAC,KAAK,CAAC,iBAAiB,GAAG,uCAAuC,YAAY,EAAE,CAAC,CAAC;QACzF,OAAO,CAAC,CAAC;IACX,CAAC;IAED,4BAA4B;IAC5B,MAAM,EAAE,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IAClC,aAAa,CAAC,EAAE,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IACjC,MAAM,QAAQ,GAAG,YAAY,CAAC;IAC9B,MAAM,WAAW,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,KAAK,IAAI,CAAC;IAElD,MAAM,UAAU,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;IACtC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1C,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACjC,SAAS,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IACjC,CAAC;IAED,sBAAsB;IACtB,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE;QAChE,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC;KACpC,CAAC,CAAC;IACH,MAAM,QAAQ,GAAG,CAAC,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAA4B,CAAC;IACrF,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;QAChB,MAAM,MAAM,GACV,OAAO,QAAQ,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,QAAQ,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,OAAO,CAAC,MAAM,EAAE,CAAC;QAC3I,OAAO,CAAC,KAAK,CAAC,iBAAiB,QAAQ,iCAAiC,MAAM,EAAE,CAAC,CAAC;QAClF,OAAO,CAAC,CAAC;IACX,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,YAAY,QAAQ,MAAM,IAAI,CAAC,MAAM,IAAI,WAAW,cAAc,WAAW,CAAC,CAAC,CAAC,iCAAiC,CAAC,CAAC,CAAC,uBAAuB,EAAE,CAAC,CAAC;IAC3J,OAAO,CAAC,CAAC;AACX,CAAC"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { defaultDataDir, loadState } from '../state.js';
|
|
2
|
+
import { authedFetch } from '../client.js';
|
|
3
|
+
import { shortHash } from '../render.js';
|
|
4
|
+
import { COMMENT_VALENCES, COMMENT_REPORT_REASONS, } from '@arshdelight/pop-sdk';
|
|
5
|
+
const VALENCE_BADGE = {
|
|
6
|
+
SUPPORT: '[支持]',
|
|
7
|
+
NEUTRAL: '[中立]',
|
|
8
|
+
OPPOSE: '[反对]',
|
|
9
|
+
};
|
|
10
|
+
const USAGE = `usage: practi comment list|tally|add|edit|delete|report
|
|
11
|
+
practi comment list <hash> [--node <hash>] [--cursor <c>] [--limit N] [--json]
|
|
12
|
+
看评论:整篇=该文档自己的评论;--node=某节点全网评论(含正反态度分布)
|
|
13
|
+
practi comment tally <hash> [--node <hash>] [--json]
|
|
14
|
+
只看正反态度分布(支持/中立/反对计数)
|
|
15
|
+
practi comment add <hash> --node <hash> --valence support|neutral|oppose -m "<评论内容>"
|
|
16
|
+
practi comment edit <comment-id> -m "<新内容>"
|
|
17
|
+
practi comment delete <comment-id>
|
|
18
|
+
practi comment report <comment-id> --reason illegal|infringement|spam|other [--detail "..."]`;
|
|
19
|
+
function errDetail(body, status) {
|
|
20
|
+
return typeof body.message === 'string'
|
|
21
|
+
? body.message
|
|
22
|
+
: typeof body.error === 'string'
|
|
23
|
+
? body.error
|
|
24
|
+
: `HTTP ${status}`;
|
|
25
|
+
}
|
|
26
|
+
export async function runComment(opts) {
|
|
27
|
+
const dataDir = opts.dataDir ?? defaultDataDir();
|
|
28
|
+
const state = loadState(dataDir);
|
|
29
|
+
if (!state.remote) {
|
|
30
|
+
console.error('error: no remote configured — run `practi remote set <url>` first');
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
const sub = opts.positional[0];
|
|
34
|
+
const target = opts.positional[1];
|
|
35
|
+
switch (sub) {
|
|
36
|
+
case 'list':
|
|
37
|
+
return listComments(opts, dataDir, state.remote.url, target);
|
|
38
|
+
case 'tally':
|
|
39
|
+
return tallyComments(opts, dataDir, state.remote.url, target);
|
|
40
|
+
case 'add':
|
|
41
|
+
return addComment(opts, dataDir, state.remote.url, target);
|
|
42
|
+
case 'edit':
|
|
43
|
+
return editComment(opts, dataDir, state.remote.url, target);
|
|
44
|
+
case 'delete':
|
|
45
|
+
return removeComment(opts, dataDir, state.remote.url, target);
|
|
46
|
+
case 'report':
|
|
47
|
+
return reportComment(opts, dataDir, state.remote.url, target);
|
|
48
|
+
default:
|
|
49
|
+
console.error(USAGE);
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function listComments(opts, dataDir, remote, hash) {
|
|
54
|
+
if (!hash) {
|
|
55
|
+
console.error('usage: practi comment list <hash> [--node <hash>] [--json]');
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
const params = new URLSearchParams();
|
|
59
|
+
if (opts.node)
|
|
60
|
+
params.set('node', opts.node);
|
|
61
|
+
if (opts.cursor)
|
|
62
|
+
params.set('cursor', opts.cursor);
|
|
63
|
+
if (opts.limit)
|
|
64
|
+
params.set('limit', opts.limit);
|
|
65
|
+
const res = await authedFetch(dataDir, remote, `/api/v1/pop/${encodeURIComponent(hash)}/comments?${params}`);
|
|
66
|
+
const body = (await res.json().catch(() => ({})));
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
console.error(`comments failed: ${errDetail(body, res.status)}`);
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
if (opts.json) {
|
|
72
|
+
console.log(JSON.stringify(body, null, 2));
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
printTally(body.tally, hash);
|
|
76
|
+
if (body.items.length === 0) {
|
|
77
|
+
console.log('no comments');
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
for (const c of body.items)
|
|
81
|
+
printComment(c);
|
|
82
|
+
if (body.nextCursor) {
|
|
83
|
+
console.log(`\nmore — continue with --cursor ${body.nextCursor}`);
|
|
84
|
+
}
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
async function tallyComments(opts, dataDir, remote, hash) {
|
|
88
|
+
if (!hash) {
|
|
89
|
+
console.error('usage: practi comment tally <hash> [--node <hash>] [--json]');
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
const params = new URLSearchParams({ limit: '1' });
|
|
93
|
+
if (opts.node)
|
|
94
|
+
params.set('node', opts.node);
|
|
95
|
+
const res = await authedFetch(dataDir, remote, `/api/v1/pop/${encodeURIComponent(hash)}/comments?${params}`);
|
|
96
|
+
const body = (await res.json().catch(() => ({})));
|
|
97
|
+
if (!res.ok) {
|
|
98
|
+
console.error(`tally failed: ${errDetail(body, res.status)}`);
|
|
99
|
+
return 1;
|
|
100
|
+
}
|
|
101
|
+
if (opts.json) {
|
|
102
|
+
console.log(JSON.stringify(body.tally, null, 2));
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
printTally(body.tally, hash);
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
function printTally(tally, hash) {
|
|
109
|
+
const scope = tally.total === 0 ? '' : `(正反:${tally.SUPPORT} 支持 / ${tally.NEUTRAL} 中立 / ${tally.OPPOSE} 反对)`;
|
|
110
|
+
console.log(`${shortHash(hash)} 共 ${tally.total} 条评论${scope}`);
|
|
111
|
+
}
|
|
112
|
+
function printComment(c) {
|
|
113
|
+
const author = c.author.username ?? `${c.author.id.slice(0, 8)}…`;
|
|
114
|
+
const hidden = c.reviewStatus === 'reject' ? '(已隐藏,可编辑后重发)' : '';
|
|
115
|
+
console.log(` ${shortHash(c.id)} ${VALENCE_BADGE[c.valence] ?? c.valence} @${author} ${shortHash(c.targetHash)}${hidden}`);
|
|
116
|
+
console.log(` ${c.body}`);
|
|
117
|
+
}
|
|
118
|
+
async function addComment(opts, dataDir, remote, hash) {
|
|
119
|
+
if (!hash || !opts.node || !opts.message) {
|
|
120
|
+
console.error('usage: practi comment add <hash> --node <hash> --valence support|neutral|oppose -m "<评论内容>"');
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
const valence = (opts.valence ?? 'neutral').toUpperCase();
|
|
124
|
+
if (!COMMENT_VALENCES.includes(valence)) {
|
|
125
|
+
console.error('error: valence must be support | neutral | oppose');
|
|
126
|
+
return 1;
|
|
127
|
+
}
|
|
128
|
+
const res = await authedFetch(dataDir, remote, `/api/v1/pop/${encodeURIComponent(hash)}/comments`, {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: { 'content-type': 'application/json' },
|
|
131
|
+
body: JSON.stringify({ targetHash: opts.node, valence, body: opts.message }),
|
|
132
|
+
});
|
|
133
|
+
const body = (await res.json().catch(() => ({})));
|
|
134
|
+
if (!res.ok) {
|
|
135
|
+
console.error(`comment failed: ${errDetail(body, res.status)}`);
|
|
136
|
+
return 1;
|
|
137
|
+
}
|
|
138
|
+
if (opts.json) {
|
|
139
|
+
console.log(JSON.stringify(body, null, 2));
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
console.log(`commented on ${shortHash(opts.node)} [${valence}] — id ${body.id}`);
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
async function editComment(opts, dataDir, remote, commentId) {
|
|
146
|
+
if (!commentId || !opts.message) {
|
|
147
|
+
console.error('usage: practi comment edit <comment-id> -m "<新内容>"');
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
const res = await authedFetch(dataDir, remote, `/api/v1/comments/${encodeURIComponent(commentId)}`, {
|
|
151
|
+
method: 'PATCH',
|
|
152
|
+
headers: { 'content-type': 'application/json' },
|
|
153
|
+
body: JSON.stringify({ body: opts.message }),
|
|
154
|
+
});
|
|
155
|
+
const body = (await res.json().catch(() => ({})));
|
|
156
|
+
if (!res.ok) {
|
|
157
|
+
console.error(`edit failed: ${errDetail(body, res.status)}`);
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
160
|
+
if (opts.json) {
|
|
161
|
+
console.log(JSON.stringify(body, null, 2));
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
console.log(`comment ${commentId} edited`);
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
async function removeComment(opts, dataDir, remote, commentId) {
|
|
168
|
+
if (!commentId) {
|
|
169
|
+
console.error('usage: practi comment delete <comment-id>');
|
|
170
|
+
return 1;
|
|
171
|
+
}
|
|
172
|
+
const res = await authedFetch(dataDir, remote, `/api/v1/comments/${encodeURIComponent(commentId)}`, {
|
|
173
|
+
method: 'DELETE',
|
|
174
|
+
});
|
|
175
|
+
const body = (await res.json().catch(() => ({})));
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
console.error(`delete failed: ${errDetail(body, res.status)}`);
|
|
178
|
+
return 1;
|
|
179
|
+
}
|
|
180
|
+
if (opts.json) {
|
|
181
|
+
console.log(JSON.stringify(body, null, 2));
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
console.log(`comment ${commentId} deleted`);
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
async function reportComment(opts, dataDir, remote, commentId) {
|
|
188
|
+
if (!commentId || !opts.reason) {
|
|
189
|
+
console.error('usage: practi comment report <comment-id> --reason illegal|infringement|spam|other [--detail "..."]');
|
|
190
|
+
return 1;
|
|
191
|
+
}
|
|
192
|
+
const reason = opts.reason.toLowerCase();
|
|
193
|
+
if (!COMMENT_REPORT_REASONS.includes(reason)) {
|
|
194
|
+
console.error(`error: reason must be one of: ${COMMENT_REPORT_REASONS.join(' | ')}`);
|
|
195
|
+
return 1;
|
|
196
|
+
}
|
|
197
|
+
const res = await authedFetch(dataDir, remote, `/api/v1/comments/${encodeURIComponent(commentId)}/report`, {
|
|
198
|
+
method: 'POST',
|
|
199
|
+
headers: { 'content-type': 'application/json' },
|
|
200
|
+
body: JSON.stringify({ reason, detail: opts.detail ?? null }),
|
|
201
|
+
});
|
|
202
|
+
const body = (await res.json().catch(() => ({})));
|
|
203
|
+
if (!res.ok) {
|
|
204
|
+
console.error(`report failed: ${errDetail(body, res.status)}`);
|
|
205
|
+
return 1;
|
|
206
|
+
}
|
|
207
|
+
console.log(body.updated ? `report updated for comment ${commentId}` : `comment ${commentId} reported`);
|
|
208
|
+
return 0;
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=comment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comment.js","sourceRoot":"","sources":["../../src/cmd/comment.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { defaultDataDir, loadState } from '../state.js';
|
|
3
|
+
import { openWorkspace } from '../workspace.js';
|
|
4
|
+
import { loadCredentials, existingAuthFile, authPath } from '../credentials.js';
|
|
5
|
+
import { CLI_VERSION, POP_SPEC_VERSION } from '../version.js';
|
|
6
|
+
export function runConfig(opts) {
|
|
7
|
+
const dataDir = opts.dataDir ?? defaultDataDir();
|
|
8
|
+
const state = loadState(dataDir);
|
|
9
|
+
let ws;
|
|
10
|
+
try {
|
|
11
|
+
ws = openWorkspace(dataDir);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
ws = null;
|
|
15
|
+
}
|
|
16
|
+
const direct = state.direct.length;
|
|
17
|
+
const indirect = ws ? ws.nodes.size - direct : 0;
|
|
18
|
+
const creds = loadCredentials(dataDir);
|
|
19
|
+
console.log(`practi: ${CLI_VERSION} pop-spec: ${POP_SPEC_VERSION}`);
|
|
20
|
+
console.log(`data dir: ${path.resolve(dataDir)}`);
|
|
21
|
+
console.log(`remote: ${state.remote ? state.remote.url : '(not set)'}`);
|
|
22
|
+
console.log(`auth: ${creds ? `logged in as ${creds.client_name ?? 'practi cli'} (${existingAuthFile(dataDir) ?? authPath(dataDir)})` : '(not logged in)'}`);
|
|
23
|
+
console.log(`direct: ${direct}`);
|
|
24
|
+
console.log(`indirect: ${indirect}`);
|
|
25
|
+
if (ws && ws.parseIssues.length > 0) {
|
|
26
|
+
console.log(`warnings: ${ws.parseIssues.length} parse issue(s) — run \`practi ls\` to see them`);
|
|
27
|
+
}
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=config.js.map
|