@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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -0
  3. package/dist/client.js +84 -0
  4. package/dist/client.js.map +1 -0
  5. package/dist/cmd/blob.js +97 -0
  6. package/dist/cmd/blob.js.map +1 -0
  7. package/dist/cmd/clone.js +76 -0
  8. package/dist/cmd/clone.js.map +1 -0
  9. package/dist/cmd/comment.js +210 -0
  10. package/dist/cmd/comment.js.map +1 -0
  11. package/dist/cmd/config.js +30 -0
  12. package/dist/cmd/config.js.map +1 -0
  13. package/dist/cmd/edit.js +122 -0
  14. package/dist/cmd/edit.js.map +1 -0
  15. package/dist/cmd/init.js +11 -0
  16. package/dist/cmd/init.js.map +1 -0
  17. package/dist/cmd/lifecycle.js +69 -0
  18. package/dist/cmd/lifecycle.js.map +1 -0
  19. package/dist/cmd/login.js +125 -0
  20. package/dist/cmd/login.js.map +1 -0
  21. package/dist/cmd/ls.js +59 -0
  22. package/dist/cmd/ls.js.map +1 -0
  23. package/dist/cmd/new.js +57 -0
  24. package/dist/cmd/new.js.map +1 -0
  25. package/dist/cmd/pull.js +94 -0
  26. package/dist/cmd/pull.js.map +1 -0
  27. package/dist/cmd/push.js +84 -0
  28. package/dist/cmd/push.js.map +1 -0
  29. package/dist/cmd/remote.js +48 -0
  30. package/dist/cmd/remote.js.map +1 -0
  31. package/dist/cmd/search.js +137 -0
  32. package/dist/cmd/search.js.map +1 -0
  33. package/dist/cmd/show.js +61 -0
  34. package/dist/cmd/show.js.map +1 -0
  35. package/dist/cmd/skill.js +198 -0
  36. package/dist/cmd/skill.js.map +1 -0
  37. package/dist/cmd/spec.js +10 -0
  38. package/dist/cmd/spec.js.map +1 -0
  39. package/dist/cmd/update.js +60 -0
  40. package/dist/cmd/update.js.map +1 -0
  41. package/dist/credentials.js +65 -0
  42. package/dist/credentials.js.map +1 -0
  43. package/dist/index.js +412 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/oauth.js +244 -0
  46. package/dist/oauth.js.map +1 -0
  47. package/dist/render.js +25 -0
  48. package/dist/render.js.map +1 -0
  49. package/dist/state.js +74 -0
  50. package/dist/state.js.map +1 -0
  51. package/dist/version.js +12 -0
  52. package/dist/version.js.map +1 -0
  53. package/dist/web.js +243 -0
  54. package/dist/web.js.map +1 -0
  55. package/dist/workspace.js +38 -0
  56. package/dist/workspace.js.map +1 -0
  57. package/package.json +48 -0
  58. package/skills/use-practi/SKILL.md +186 -0
package/dist/oauth.js ADDED
@@ -0,0 +1,244 @@
1
+ import http from 'node:http';
2
+ import { createHash, randomBytes } from 'node:crypto';
3
+ import { openBrowser } from './web.js';
4
+ /**
5
+ * OAuth 2.1 client(practi cli ↔ practihub 的 Authorization Server):
6
+ * 发现 AS metadata → DCR 注册 loopback client → PKCE S256 → 浏览器授权 →
7
+ * loopback 回调收 code → token 端点换 access/refresh。全部走标准端点。
8
+ *
9
+ * 登录流程分步组合(login.ts):
10
+ * session = createLoopbackSession() // 绑定端口,得到 redirectUri + state
11
+ * { client_id } = registerClient(remote, session.redirectUri)
12
+ * authorizeUrl = buildAuthorizeUrl(...) // 含 PKCE challenge + resource + state
13
+ * code = await session.waitForCode() // 浏览器授权回调
14
+ * tokens = await exchangeCode(...) // code + verifier → access/refresh
15
+ */
16
+ export const LOGIN_SCOPES = 'pop:read pop:write pop:publish pop:delete';
17
+ const CALLBACK_PATH = '/callback';
18
+ const TIMEOUT_MS = 5 * 60 * 1000; // 授权页等待上限
19
+ /** CLI 的 resource(RFC 8707 audience)= remote origin + /cli */
20
+ export function cliResource(remote) {
21
+ return `${new URL(remote).origin}/cli`;
22
+ }
23
+ /** 从 AS metadata 或约定路径发现端点;缺失时按 practihub 约定回退 */
24
+ export async function discoverAs(remote) {
25
+ const origin = new URL(remote).origin;
26
+ const defaults = {
27
+ authorization_endpoint: `${origin}/oauth/authorize`,
28
+ token_endpoint: `${origin}/api/auth/oauth/token`,
29
+ registration_endpoint: `${origin}/api/auth/oauth/register`,
30
+ revocation_endpoint: `${origin}/api/auth/oauth/revoke`,
31
+ };
32
+ try {
33
+ const res = await fetch(`${origin}/.well-known/oauth-authorization-server`, {
34
+ headers: { accept: 'application/json' },
35
+ signal: AbortSignal.timeout(10_000),
36
+ });
37
+ if (!res.ok)
38
+ return defaults;
39
+ const meta = (await res.json());
40
+ return {
41
+ authorization_endpoint: meta.authorization_endpoint ?? defaults.authorization_endpoint,
42
+ token_endpoint: meta.token_endpoint ?? defaults.token_endpoint,
43
+ registration_endpoint: meta.registration_endpoint ?? defaults.registration_endpoint,
44
+ revocation_endpoint: meta.revocation_endpoint ?? defaults.revocation_endpoint,
45
+ };
46
+ }
47
+ catch {
48
+ return defaults;
49
+ }
50
+ }
51
+ /** DCR(RFC 7591):注册一个 loopback 回调的 public client */
52
+ export async function registerClient(remote, redirectUri) {
53
+ const meta = await discoverAs(remote);
54
+ const res = await fetch(meta.registration_endpoint, {
55
+ method: 'POST',
56
+ headers: { 'content-type': 'application/json' },
57
+ body: JSON.stringify({
58
+ client_name: 'practi cli',
59
+ redirect_uris: [redirectUri],
60
+ token_endpoint_auth_method: 'none',
61
+ grant_types: ['authorization_code', 'refresh_token'],
62
+ response_types: ['code'],
63
+ scope: LOGIN_SCOPES,
64
+ }),
65
+ signal: AbortSignal.timeout(15_000),
66
+ });
67
+ if (!res.ok) {
68
+ throw new Error(`client registration failed (HTTP ${res.status}): ${await res.text()}`);
69
+ }
70
+ return (await res.json());
71
+ }
72
+ /** PKCE S256:verifier + challenge */
73
+ export function generatePkcePair() {
74
+ const verifier = randomBytes(32).toString('base64url');
75
+ const challenge = createHash('sha256').update(verifier).digest('base64url');
76
+ return { verifier, challenge };
77
+ }
78
+ /**
79
+ * 起 loopback server(RFC 8252)并绑定端口。
80
+ * 必须在 DCR 注册前创建(redirectUri 含端口,注册与授权必须一致)。
81
+ */
82
+ export function createLoopbackSession() {
83
+ let settled = false;
84
+ let timer;
85
+ const state = randomBytes(16).toString('hex');
86
+ let resolveCode = null;
87
+ let rejectCode = null;
88
+ const codePromise = new Promise((res, rej) => {
89
+ resolveCode = res;
90
+ rejectCode = rej;
91
+ });
92
+ let port = 0;
93
+ const server = http.createServer((req, res) => {
94
+ const url = new URL(req.url ?? '/', 'http://127.0.0.1');
95
+ if (url.pathname !== CALLBACK_PATH) {
96
+ res.writeHead(404, { 'content-type': 'text/plain' });
97
+ res.end('not found');
98
+ return;
99
+ }
100
+ const error = url.searchParams.get('error');
101
+ if (error) {
102
+ res.writeHead(400, { 'content-type': 'text/html' });
103
+ res.end(simplePage('Authorization failed', `The authorization server returned an error: ${escapeHtml(error)}`));
104
+ finish(new Error(`authorization denied: ${error}`));
105
+ return;
106
+ }
107
+ const code = url.searchParams.get('code');
108
+ const stateParam = url.searchParams.get('state');
109
+ if (!code || stateParam !== state) {
110
+ res.writeHead(400, { 'content-type': 'text/html' });
111
+ res.end(simplePage('Authorization failed', 'Missing or mismatched code/state.'));
112
+ finish(new Error('authorization callback missing code or state mismatch'));
113
+ return;
114
+ }
115
+ res.writeHead(200, { 'content-type': 'text/html' });
116
+ res.end(simplePage('Logged in', 'You can close this window and return to the terminal.'));
117
+ finish(null, code);
118
+ });
119
+ server.on('error', (err) => finish(err));
120
+ server.listen(0, '127.0.0.1', () => {
121
+ const addr = server.address();
122
+ port = typeof addr === 'object' && addr ? addr.port : 0;
123
+ timer = setTimeout(() => finish(new Error('authorization timed out — no browser callback received')), TIMEOUT_MS);
124
+ });
125
+ function finish(err, code) {
126
+ if (settled)
127
+ return;
128
+ settled = true;
129
+ if (timer)
130
+ clearTimeout(timer);
131
+ server.close(() => { });
132
+ if (err)
133
+ rejectCode?.(err);
134
+ else
135
+ resolveCode?.(code ?? '');
136
+ }
137
+ return {
138
+ get redirectUri() {
139
+ return `http://127.0.0.1:${port}${CALLBACK_PATH}`;
140
+ },
141
+ state,
142
+ waitForCode: () => codePromise,
143
+ close: () => finish(new Error('login aborted')),
144
+ };
145
+ }
146
+ /** 组装授权 URL(浏览器打开用) */
147
+ export function buildAuthorizeUrl(opts) {
148
+ const url = new URL(opts.authorizationEndpoint);
149
+ url.searchParams.set('response_type', 'code');
150
+ url.searchParams.set('client_id', opts.clientId);
151
+ url.searchParams.set('redirect_uri', opts.redirectUri);
152
+ url.searchParams.set('code_challenge', opts.codeChallenge);
153
+ url.searchParams.set('code_challenge_method', 'S256');
154
+ url.searchParams.set('state', opts.state);
155
+ url.searchParams.set('scope', LOGIN_SCOPES);
156
+ url.searchParams.set('resource', opts.resource);
157
+ return url.toString();
158
+ }
159
+ /** 打开浏览器(或打印 URL 供手动打开) */
160
+ export function openAuthorizeUrl(url, open) {
161
+ if (open)
162
+ openBrowser(url);
163
+ else
164
+ console.log(`open this URL in your browser:\n ${url}`);
165
+ }
166
+ /** authorization_code grant:code + verifier → access/refresh */
167
+ export async function exchangeCode(remote, opts) {
168
+ const meta = await discoverAs(remote);
169
+ const body = new URLSearchParams({
170
+ grant_type: 'authorization_code',
171
+ client_id: opts.clientId,
172
+ redirect_uri: opts.redirectUri,
173
+ code: opts.code,
174
+ code_verifier: opts.verifier,
175
+ });
176
+ return postToken(meta.token_endpoint, body);
177
+ }
178
+ /** 用 refresh token 换新的 access/refresh 对(rotation) */
179
+ export async function refreshAccessToken(remote, clientId, refreshToken) {
180
+ const meta = await discoverAs(remote);
181
+ const body = new URLSearchParams({
182
+ grant_type: 'refresh_token',
183
+ client_id: clientId,
184
+ refresh_token: refreshToken,
185
+ });
186
+ return postToken(meta.token_endpoint, body);
187
+ }
188
+ async function postToken(tokenEndpoint, body) {
189
+ const res = await fetch(tokenEndpoint, {
190
+ method: 'POST',
191
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
192
+ body: body.toString(),
193
+ signal: AbortSignal.timeout(15_000),
194
+ });
195
+ if (!res.ok) {
196
+ const text = await res.text();
197
+ let detail = text;
198
+ try {
199
+ const j = JSON.parse(text);
200
+ detail = j.error_description ?? j.error ?? text;
201
+ }
202
+ catch {
203
+ // 保留原文
204
+ }
205
+ throw new Error(`token endpoint failed (HTTP ${res.status}): ${detail}`);
206
+ }
207
+ const data = (await res.json());
208
+ if (!data.access_token || !data.refresh_token) {
209
+ throw new Error('token endpoint returned an invalid response (missing access_token/refresh_token)');
210
+ }
211
+ return {
212
+ access_token: data.access_token,
213
+ refresh_token: data.refresh_token,
214
+ expires_in: data.expires_in ?? 3600,
215
+ scope: data.scope ?? '',
216
+ };
217
+ }
218
+ /** 撤销 token(RFC 7009,best-effort:失败不抛错) */
219
+ export async function revokeToken(remote, clientId, token, tokenTypeHint) {
220
+ try {
221
+ const meta = await discoverAs(remote);
222
+ const body = new URLSearchParams({ token, client_id: clientId });
223
+ if (tokenTypeHint)
224
+ body.set('token_type_hint', tokenTypeHint);
225
+ await fetch(meta.revocation_endpoint, {
226
+ method: 'POST',
227
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
228
+ body: body.toString(),
229
+ signal: AbortSignal.timeout(10_000),
230
+ });
231
+ }
232
+ catch {
233
+ // best-effort:本地凭证已删,revoke 失败不阻塞 logout
234
+ }
235
+ }
236
+ function simplePage(title, body) {
237
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${title}</title></head>
238
+ <body style="font:16px/1.6 system-ui,sans-serif;margin:40px auto;max-width:480px;text-align:center">
239
+ <h1>${title}</h1><p>${body}</p></body></html>`;
240
+ }
241
+ function escapeHtml(s) {
242
+ return s.replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
243
+ }
244
+ //# sourceMappingURL=oauth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.js","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"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"}
package/dist/render.js ADDED
@@ -0,0 +1,25 @@
1
+ /** Compact display hash: first 12 hex chars of the payload */
2
+ export function shortHash(hash) {
3
+ return hash.slice('sha256:'.length, 'sha256:'.length + 12);
4
+ }
5
+ export function nodeTag(node) {
6
+ return node.type === 'practice' ? `practice·${node.op}` : 'action';
7
+ }
8
+ /** Render a standard view's header line, e.g. "● Name [practice·seq] abc123def456" */
9
+ export function viewHeader(view) {
10
+ const tag = view.type === 'practice' ? `practice·${view.op}` : 'action';
11
+ return `● ${view.name} [${tag}] ${shortHash(view.hash)}`;
12
+ }
13
+ /** Render aggregate steps as an indented tree (depth from the view, normalized to 0) */
14
+ export function renderSteps(steps, nodes) {
15
+ const lines = [];
16
+ for (const s of steps) {
17
+ const n = nodes.get(s.refHash);
18
+ const tag = n ? `[${nodeTag(n)}]` : '';
19
+ const note = s.note ? ` (${s.note})` : '';
20
+ const desc = s.description !== undefined ? ` — ${s.description}` : '';
21
+ lines.push(`${' '.repeat(s.depth)}${s.name} ${tag} ${shortHash(s.refHash)}${note}${desc}`);
22
+ }
23
+ return lines;
24
+ }
25
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAEA,8DAA8D;AAC9D,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAC7D,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,IAAW;IACjC,OAAO,IAAI,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,YAAY,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC;AACrE,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,UAAU,CAAC,IAAkB;IAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,YAAY,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC;IACxE,OAAO,KAAK,IAAI,CAAC,IAAI,MAAM,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;AAC7D,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,WAAW,CAAC,KAAiB,EAAE,KAAyB;IACtE,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,MAAM,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QAC/B,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,GAAG,CAAC,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACvE,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC,CAAC;IAChG,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
package/dist/state.js ADDED
@@ -0,0 +1,74 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ /** CLI bookkeeping file inside the data directory (the data dir IS a POP workspace).
5
+ * 旧名 pop.json 仍可读(读旧写新),写入一律走 practi.json。 */
6
+ export const STATE_FILE = 'practi.json';
7
+ const LEGACY_STATE_FILE = 'pop.json';
8
+ export const HASH_RE = /^sha256:[0-9a-f]{64}$/;
9
+ /** 默认 remote:开箱即连官方 hub;自建/本地 hub 用 `practi remote set <url>` 覆盖 */
10
+ export const DEFAULT_REMOTE_URL = 'https://practihub.com';
11
+ /** Default data directory: $PRACTI_HOME (legacy $POP_HOME still honored), else
12
+ * ~/.practi on every platform. Deliberately NOT %APPDATA%\practi — that path
13
+ * belongs to the unrelated "practi" Electron app's data on Windows. When
14
+ * ~/.practi doesn't exist yet but a pre-rename workspace does (%APPDATA%\pop
15
+ * or ~/.pop), adopt it silently. */
16
+ export function defaultDataDir() {
17
+ if (process.env.PRACTI_HOME)
18
+ return path.resolve(process.env.PRACTI_HOME);
19
+ if (process.env.POP_HOME)
20
+ return path.resolve(process.env.POP_HOME);
21
+ const fresh = path.join(os.homedir(), '.practi');
22
+ if (fs.existsSync(fresh))
23
+ return fresh;
24
+ const legacyCandidates = [
25
+ process.env.APPDATA ? path.join(process.env.APPDATA, 'pop') : null,
26
+ path.join(os.homedir(), '.pop'),
27
+ ].filter((p) => p !== null);
28
+ for (const legacy of legacyCandidates) {
29
+ if (fs.existsSync(legacy))
30
+ return legacy;
31
+ }
32
+ return fresh;
33
+ }
34
+ export function statePath(dataDir) {
35
+ return path.join(dataDir, STATE_FILE);
36
+ }
37
+ /** 旧版(改名前)状态文件路径;存在则优先级低于 practi.json */
38
+ export function legacyStatePath(dataDir) {
39
+ return path.join(dataDir, LEGACY_STATE_FILE);
40
+ }
41
+ function asRecord(v) {
42
+ return typeof v === 'object' && v !== null && !Array.isArray(v) ? v : null;
43
+ }
44
+ function asString(v) {
45
+ return typeof v === 'string' ? v : undefined;
46
+ }
47
+ export function loadState(dataDir) {
48
+ // 未配置(或 practi.json 不存在/损坏)时回落默认 remote:开箱即连官方 hub
49
+ const base = { schema: 1, direct: [], remote: { url: DEFAULT_REMOTE_URL } };
50
+ const p = statePath(dataDir);
51
+ const legacy = legacyStatePath(dataDir);
52
+ const file = fs.existsSync(p) ? p : fs.existsSync(legacy) ? legacy : null;
53
+ if (file === null)
54
+ return base;
55
+ try {
56
+ const rec = asRecord(JSON.parse(fs.readFileSync(file, 'utf8')));
57
+ if (rec === null)
58
+ return base;
59
+ const state = { schema: 1, direct: [] };
60
+ const remoteUrl = asString(asRecord(rec.remote)?.url);
61
+ state.remote = remoteUrl ? { url: remoteUrl } : { url: DEFAULT_REMOTE_URL };
62
+ if (Array.isArray(rec.direct)) {
63
+ state.direct = rec.direct.filter((x) => typeof x === 'string' && HASH_RE.test(x));
64
+ }
65
+ return state;
66
+ }
67
+ catch {
68
+ return base;
69
+ }
70
+ }
71
+ export function saveState(dataDir, state) {
72
+ fs.writeFileSync(statePath(dataDir), JSON.stringify(state, null, 2) + '\n', 'utf8');
73
+ }
74
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import { createRequire } from 'node:module';
2
+ import { POP_SPEC_VERSION } from '@arshdelight/pop-sdk';
3
+ /**
4
+ * 版本单一来源:CLI 版本运行时读 package.json(不硬编码,防漂移),
5
+ * 协议版本来自 SDK 导出的 POP_SPEC_VERSION(spec 升版只改 SDK 一处)。
6
+ * src/ 与 dist/ 同深(../package.json 均指向 cli/package.json)。
7
+ */
8
+ const require = createRequire(import.meta.url);
9
+ const pkg = require('../package.json');
10
+ export const CLI_VERSION = pkg.version;
11
+ export { POP_SPEC_VERSION };
12
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAExD;;;;GAIG;AACH,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC/C,MAAM,GAAG,GAAG,OAAO,CAAC,iBAAiB,CAAwB,CAAC;AAE9D,MAAM,CAAC,MAAM,WAAW,GAAG,GAAG,CAAC,OAAO,CAAC;AACvC,OAAO,EAAE,gBAAgB,EAAE,CAAC"}
package/dist/web.js ADDED
@@ -0,0 +1,243 @@
1
+ import http from 'node:http';
2
+ import { spawn } from 'node:child_process';
3
+ import { aggregateView, exportSubtree, readBlob, resolveNodeRef, PracticeError } from '@arshdelight/pop-sdk';
4
+ import { defaultDataDir, loadState } from './state.js';
5
+ import { openWorkspace } from './workspace.js';
6
+ import { nodeTag, shortHash } from './render.js';
7
+ export function runWeb(opts) {
8
+ const dataDir = opts.dataDir ?? defaultDataDir();
9
+ // validate the data dir up front so a typo fails fast instead of on first request
10
+ const ws = openWorkspace(dataDir);
11
+ const url = `http://127.0.0.1:${opts.port}/`;
12
+ const server = http.createServer((req, res) => {
13
+ try {
14
+ handle(req, res, dataDir, ws);
15
+ }
16
+ catch (e) {
17
+ if (e instanceof PracticeError) {
18
+ send(res, 404, 'text/plain; charset=utf-8', `${e.code}: ${e.message}`);
19
+ }
20
+ else {
21
+ res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' });
22
+ res.end(e instanceof Error ? e.message : String(e));
23
+ }
24
+ }
25
+ });
26
+ server.listen(opts.port, '127.0.0.1', () => {
27
+ console.log(`practi web: ${url}`);
28
+ console.log(`data dir: ${dataDir}`);
29
+ console.log('press Ctrl-C to stop');
30
+ if (opts.open)
31
+ openBrowser(url);
32
+ });
33
+ return 0;
34
+ }
35
+ function handle(req, res, dataDir, ws) {
36
+ const pathname = (req.url ?? '/').split('?')[0];
37
+ const state = loadState(dataDir);
38
+ if (pathname === '/' || pathname === '/index.html') {
39
+ send(res, 200, 'text/html; charset=utf-8', indexHtml(ws, state.direct));
40
+ return;
41
+ }
42
+ const pop = pathname.match(/^\/pop\/(.+)$/);
43
+ if (pop) {
44
+ const hash = decodeURIComponent(pop[1]).replace(/\.json$/, '');
45
+ const resolved = resolveNodeRef(ws, hash);
46
+ if (pathname.endsWith('.json')) {
47
+ const view = aggregateView(resolved, ws.nodes);
48
+ send(res, 200, 'application/json; charset=utf-8', JSON.stringify(view, null, 2));
49
+ return;
50
+ }
51
+ const view = aggregateView(resolved, ws.nodes, { full: true });
52
+ send(res, 200, 'text/html; charset=utf-8', detailHtml(ws, view));
53
+ return;
54
+ }
55
+ const blob = pathname.match(/^\/blobs\/(sha256:[0-9a-f]{64})$/);
56
+ if (blob) {
57
+ const bytes = readBlob(ws.root, blob[1]);
58
+ if (bytes === null) {
59
+ send(res, 404, 'text/plain; charset=utf-8', 'E_BLOB_MISSING: blob not present in this workspace');
60
+ return;
61
+ }
62
+ sendBytes(res, mimeFor(ws, blob[1]), bytes);
63
+ return;
64
+ }
65
+ const doc = pathname.match(/^\/doc\/(.+)$/);
66
+ if (doc) {
67
+ const hash = decodeURIComponent(doc[1]).replace(/\.json$/, '');
68
+ const node = ws.nodes.get(resolveNodeRef(ws, hash));
69
+ if (node) {
70
+ send(res, 200, 'application/json; charset=utf-8', JSON.stringify(exportSubtree(node, ws.nodes), null, 2));
71
+ return;
72
+ }
73
+ }
74
+ if (pathname === '/healthz') {
75
+ send(res, 200, 'text/plain', 'ok');
76
+ return;
77
+ }
78
+ send(res, 404, 'text/plain; charset=utf-8', 'not found');
79
+ }
80
+ function send(res, code, type, body) {
81
+ res.writeHead(code, { 'content-type': type });
82
+ res.end(body);
83
+ }
84
+ function sendBytes(res, type, body) {
85
+ res.writeHead(200, { 'content-type': type, 'cache-control': 'public, max-age=31536000, immutable' });
86
+ res.end(body);
87
+ }
88
+ /** Best-effort mime for a local blob, looked up from any action that references it */
89
+ function mimeFor(ws, hash) {
90
+ for (const n of ws.nodes.values()) {
91
+ if (n.type !== 'action')
92
+ continue;
93
+ for (const a of n.attachments ?? []) {
94
+ if (a.hash === hash && a.mime)
95
+ return a.mime;
96
+ }
97
+ }
98
+ return 'application/octet-stream';
99
+ }
100
+ export function openBrowser(url) {
101
+ if (process.platform === 'win32') {
102
+ // cmd 把 & 当命令分隔符,URL 必须整体加引号(否则带 query 的 OAuth 授权 URL 被截断在首个 &);
103
+ // start 的首个引号参数是窗口标题占位;/d /s + verbatim 让 cmd 按原样解析这条命令行
104
+ spawn('cmd', ['/d', '/s', '/c', `start "" "${url}"`], {
105
+ windowsVerbatimArguments: true,
106
+ stdio: 'ignore',
107
+ detached: true,
108
+ }).unref();
109
+ return;
110
+ }
111
+ const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
112
+ spawn(cmd, [url], { stdio: 'ignore', detached: true }).unref();
113
+ }
114
+ const CSS = `
115
+ :root { color-scheme: light; }
116
+ * { box-sizing: border-box; }
117
+ body { font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; margin: 0; color: #1a1a1a; background: #fafafa; }
118
+ header { background: #111; color: #fff; padding: 14px 24px; display: flex; gap: 16px; align-items: baseline; }
119
+ header .brand { font-weight: 700; letter-spacing: .5px; }
120
+ header .sub { color: #999; font-size: 13px; }
121
+ main { max-width: 860px; margin: 24px auto; padding: 0 24px 60px; }
122
+ a { color: #0b57d0; text-decoration: none; }
123
+ a:hover { text-decoration: underline; }
124
+ .card { background: #fff; border: 1px solid #e3e3e3; border-radius: 8px; padding: 14px 18px; margin-bottom: 12px; }
125
+ .card .title { font-weight: 600; font-size: 16px; }
126
+ .tag { font-size: 12px; color: #666; background: #f0f0f0; border-radius: 4px; padding: 1px 6px; margin-left: 6px; }
127
+ .hash { font-family: ui-monospace, "Cascadia Code", Consolas, monospace; font-size: 12px; color: #888; }
128
+ pre { background: #fff; border: 1px solid #e3e3e3; border-radius: 8px; padding: 14px; overflow-x: auto; font-size: 13px; }
129
+ table { border-collapse: collapse; width: 100%; font-size: 14px; }
130
+ th, td { text-align: left; padding: 6px 10px; border-bottom: 1px solid #eee; }
131
+ th { color: #666; font-weight: 600; }
132
+ .muted { color: #888; }
133
+ .breadcrumb { margin-bottom: 12px; font-size: 13px; }
134
+ .section { margin-top: 22px; }
135
+ .section h2 { font-size: 14px; text-transform: uppercase; letter-spacing: .4px; color: #666; margin: 0 0 8px; }
136
+ .step { padding: 2px 0; }
137
+ .step .n { color: #999; }
138
+ .stepbody { margin: 6px 0 12px 0; padding-left: 20px; }
139
+ .prose { font-size: 14px; color: #333; }
140
+ .prose figure { margin: 10px 0; }
141
+ .prose img { max-width: 100%; border: 1px solid #e3e3e3; border-radius: 6px; }
142
+ .prose figcaption { font-size: 12px; color: #888; text-align: center; margin-top: 4px; }
143
+ `;
144
+ function page(title, body, dataDir) {
145
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${title}</title>
146
+ <style>${CSS}</style></head><body>
147
+ <header><span class="brand">POP</span><span class="sub">local registry</span><span class="sub">${dataDir}</span></header>
148
+ <main>${body}</main></body></html>`;
149
+ }
150
+ function indexHtml(ws, direct) {
151
+ const roots = direct.filter(h => ws.nodes.has(h));
152
+ const cards = roots
153
+ .map(h => {
154
+ const view = aggregateView(h, ws.nodes);
155
+ return `<div class="card">
156
+ <div class="title"><a href="/pop/${encodeURIComponent(h)}">${escapeHtml(view.name)}</a><span class="tag">${view.type === 'practice' ? `practice·${view.op}` : 'action'}</span></div>
157
+ <div class="hash">${view.hash}</div>
158
+ ${view.description ? `<div class="muted">${escapeHtml(view.description)}</div>` : ''}
159
+ <div class="muted">${view.steps.length} step${view.steps.length === 1 ? '' : 's'} · ${view.flow.length} flow edge${view.flow.length === 1 ? '' : 's'} · ${view.outputs.length} output${view.outputs.length === 1 ? '' : 's'}</div>
160
+ </div>`;
161
+ })
162
+ .join('\n');
163
+ const empty = roots.length === 0 ? '<p class="muted">no direct pops — create one with <code>pop new</code></p>' : '';
164
+ return page('POP — local registry', `<h1>Direct pops</h1>${empty}${cards}`, ws.root);
165
+ }
166
+ function detailHtml(ws, view) {
167
+ const esc = escapeHtml;
168
+ const steps = view.steps
169
+ .map(s => {
170
+ const n = ws.nodes.get(s.refHash);
171
+ const tag = n ? nodeTag(n) : '';
172
+ const body = s.content && s.content.trim() ? `<div class="stepbody">${renderContent(s.content, n, ws)}</div>` : '';
173
+ return `<div class="step"><span class="n">${'&nbsp;&nbsp;'.repeat(s.depth)}</span>${esc(s.name)} ${tag ? `<span class="tag">${esc(tag)}</span>` : ''}${s.note ? ` <span class="muted">(${esc(s.note)})</span>` : ''}${body}</div>`;
174
+ })
175
+ .join('\n');
176
+ const flowRows = view.flow
177
+ .map(e => `<tr><td>${esc(e.name)}</td><td class="hash">${shortHash(e.fromHash)}</td><td>${esc(e.fromName)}</td><td class="hash">${shortHash(e.toHash)}</td><td>${esc(e.toName)}</td></tr>`)
178
+ .join('\n');
179
+ const inputs = view.inputs.map(d => `<tr><td>${esc(d.name)}</td><td>${d.spec ? esc(d.spec) : ''}</td><td class="hash">${shortHash(d.refHash)}</td></tr>`).join('\n');
180
+ const outputs = view.outputs.map(d => `<tr><td>${esc(d.name)}</td><td>${d.spec ? esc(d.spec) : ''}</td><td class="hash">${shortHash(d.refHash)}</td></tr>`).join('\n');
181
+ const atts = view.attachments.map(a => `<tr><td>${esc(a.name)}</td><td>${a.mime ? esc(a.mime) : ''}</td><td>${a.size ?? ''}</td><td class="hash">${shortHash(a.hash)}</td></tr>`).join('\n');
182
+ const revs = (view.revisions ?? []).map(r => `<tr><td>${esc(r.when)}</td><td>${esc(r.what)}</td>${r.from ? `<td class="hash">${shortHash(r.from)}</td>` : '<td></td>'}</tr>`).join('\n');
183
+ return page(`${view.name} — POP`, `
184
+ <div class="breadcrumb"><a href="/">← all pops</a></div>
185
+ <h1>${esc(view.name)} <span class="tag">${view.type === 'practice' ? `practice·${view.op}` : 'action'}</span></h1>
186
+ <div class="hash">${view.hash}</div>
187
+ ${view.description ? `<p>${esc(view.description)}</p>` : ''}
188
+ ${view.content && view.content.trim() ? `<div class="section"><h2>Content</h2>${renderContent(view.content, ws.nodes.get(view.hash), ws)}</div>` : ''}
189
+ <div class="section"><h2>Steps (${view.steps.length})</h2>${steps || '<p class="muted">—</p>'}</div>
190
+ ${flowRows ? `<div class="section"><h2>Flow (${view.flow.length})</h2><table><tr><th>name</th><th>from</th><th></th><th>to</th><th></th></tr>${flowRows}</table></div>` : ''}
191
+ ${inputs ? `<div class="section"><h2>Declared inputs (needs)</h2><table><tr><th>name</th><th>spec</th><th>node</th></tr>${inputs}</table></div>` : ''}
192
+ ${outputs ? `<div class="section"><h2>Declared outputs (produces)</h2><table><tr><th>name</th><th>spec</th><th>node</th></tr>${outputs}</table></div>` : ''}
193
+ ${atts ? `<div class="section"><h2>Attachments</h2><table><tr><th>name</th><th>mime</th><th>size</th><th>hash</th></tr>${atts}</table></div>` : ''}
194
+ ${revs ? `<div class="section"><h2>Revisions</h2><table><tr><th>when</th><th>what</th><th>from</th></tr>${revs}</table></div>` : ''}
195
+ <div class="section"><h2>Links</h2>
196
+ <a href="/pop/${encodeURIComponent(view.hash)}.json">StandardView JSON</a> ·
197
+ <a href="/doc/${encodeURIComponent(view.hash)}.json">document JSON</a>
198
+ </div>
199
+ `, ws.root);
200
+ }
201
+ function escapeHtml(s) {
202
+ return s.replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
203
+ }
204
+ /**
205
+ * Render action content for the web UI: markdown image refs `![caption](name)`
206
+ * become real <figure><img> (name → attachment url or a local /blobs/:hash);
207
+ * http(s) URL targets are used as-is (external refs, spec §5.1); unresolved
208
+ * refs stay literal. Fenced code blocks are treated as prose-exempt (§5.1).
209
+ */
210
+ function renderContent(content, node, ws) {
211
+ const blocks = [];
212
+ const prose = content.replace(/```[\s\S]*?```/g, m => {
213
+ blocks.push(m);
214
+ return `\u0000B${blocks.length - 1}\u0000`;
215
+ });
216
+ const parts = [];
217
+ let last = 0;
218
+ for (const m of prose.matchAll(/!\[([^\]]*)\]\(([^)]*)\)/g)) {
219
+ parts.push(escapeHtml(prose.slice(last, m.index)));
220
+ const caption = m[1];
221
+ const target = m[2];
222
+ const url = mediaUrl(target, node, ws);
223
+ if (url === null) {
224
+ parts.push(escapeHtml(m[0]));
225
+ }
226
+ else {
227
+ const cap = escapeHtml(caption);
228
+ parts.push(`<figure><img src="${escapeHtml(url)}" alt="${cap}"><figcaption>${cap}</figcaption></figure>`);
229
+ }
230
+ last = m.index + m[0].length;
231
+ }
232
+ parts.push(escapeHtml(prose.slice(last)));
233
+ return `<div class="prose">${parts.join('').replace(/\u0000B(\d+)\u0000/g, (_, i) => `<pre>${escapeHtml(blocks[Number(i)])}</pre>`)}</div>`;
234
+ }
235
+ function mediaUrl(target, node, ws) {
236
+ if (/^https?:\/\//i.test(target))
237
+ return target;
238
+ if (!node || node.type !== 'action')
239
+ return null;
240
+ const a = (node.attachments ?? []).find(x => x.name === target);
241
+ return a ? (a.url ?? `/blobs/${a.hash}`) : null;
242
+ }
243
+ //# sourceMappingURL=web.js.map