@logicseek/hello-cc 1.0.1 → 1.0.2
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/CHANGELOG.md +105 -0
- package/README.md +4 -0
- package/README.zh-CN.md +4 -0
- package/docs/README.md +9 -0
- package/docs/README.zh-CN.md +9 -0
- package/docs/commands.md +53 -0
- package/docs/commands.zh-CN.md +48 -0
- package/docs/dsh.md +137 -0
- package/docs/dsh.zh-CN.md +139 -0
- package/docs/mcp.md +9 -0
- package/docs/mcp.zh-CN.md +23 -0
- package/docs/native.md +295 -0
- package/docs/native.zh-CN.md +253 -0
- package/docs/web-handoff.zh-CN.md +53 -0
- package/lib/cli/app.mjs +25 -6
- package/lib/cli/commands/doctor.mjs +17 -2
- package/lib/cli/commands/dsh.mjs +79 -0
- package/lib/cli/commands/hook.mjs +241 -90
- package/lib/cli/commands/lock.mjs +34 -19
- package/lib/cli/commands/mcp.mjs +28 -0
- package/lib/cli/commands/native.mjs +130 -0
- package/lib/cli/commands/run.mjs +66 -21
- package/lib/cli/commands/task.mjs +6 -3
- package/lib/cli-args.mjs +4 -0
- package/lib/coordination-state.mjs +15 -1
- package/lib/core/coordination/event-retention.mjs +14 -2
- package/lib/core/coordination/gc-plan.mjs +3 -13
- package/lib/core/coordination/locks.mjs +45 -0
- package/lib/core/coordination/results.mjs +79 -0
- package/lib/core/peers/bindings.mjs +9 -3
- package/lib/diagnostics/codex.mjs +121 -0
- package/lib/integrations/codex-interactions.mjs +114 -0
- package/lib/integrations/dsh-collaboration.mjs +170 -0
- package/lib/integrations/dsh-cordis.d.ts +19 -0
- package/lib/integrations/dsh-cordis.mjs +177 -0
- package/lib/integrations/dsh.bundle.yml +7 -0
- package/lib/integrations/dsh.mjs +287 -0
- package/lib/integrations/native/claude.mjs +451 -0
- package/lib/integrations/native/codex.mjs +242 -0
- package/lib/integrations/native/dsh-acp.mjs +343 -0
- package/lib/integrations/native/index.mjs +19 -0
- package/lib/integrations/native/interactions.mjs +53 -0
- package/lib/integrations/native/jsonrpc.mjs +185 -0
- package/lib/integrations/peers/identity.mjs +36 -10
- package/lib/integrations/peers/processes.mjs +22 -8
- package/lib/integrations/providers.mjs +6 -2
- package/lib/mcp/scope.mjs +104 -0
- package/lib/mcp/stdio.mjs +80 -0
- package/lib/mcp/tools.mjs +197 -0
- package/lib/process/identity.mjs +3 -1
- package/lib/release/release-notes.mjs +9 -0
- package/lib/runtime/native/client.mjs +38 -0
- package/lib/runtime/native/service.mjs +511 -0
- package/lib/runtime/native/store.mjs +121 -0
- package/lib/ui/help.mjs +5 -2
- package/lib/web/adoption-state.mjs +55 -0
- package/lib/web/auto-attach.mjs +18 -2
- package/lib/web/codex-app-server.mjs +557 -0
- package/lib/web/codex-sessions.mjs +287 -0
- package/lib/web/control-lease.mjs +204 -0
- package/lib/web/http-routes.mjs +197 -15
- package/lib/web/http.mjs +5 -4
- package/lib/web/native-sessions.mjs +286 -0
- package/lib/web/project-contexts.mjs +2 -1
- package/lib/web/pty-sessions.mjs +60 -33
- package/lib/web/runtime-main.mjs +98 -6
- package/lib/web/session-serialize.mjs +0 -0
- package/lib/web/task-results.mjs +76 -0
- package/lib/web/tmux-sessions.mjs +92 -11
- package/lib/web/tmux-stream.mjs +2 -2
- package/lib/web/ui-codex.mjs +550 -0
- package/lib/web/ui-command-palette.mjs +119 -0
- package/lib/web/ui-handoff.mjs +102 -0
- package/lib/web/ui-history.mjs +118 -0
- package/lib/web/ui-i18n.mjs +841 -0
- package/lib/web/ui-interactions.mjs +78 -0
- package/lib/web/ui-login.mjs +101 -0
- package/lib/web/ui-native.mjs +165 -0
- package/lib/web/ui-preferences.mjs +278 -0
- package/lib/web/ui-review.mjs +132 -0
- package/lib/web/ui-template.mjs +1106 -576
- package/lib/web/ui-terminal-find.mjs +98 -0
- package/lib/web/ui-workbench.mjs +94 -0
- package/lib/web/ui-workspace.mjs +222 -0
- package/package.json +11 -3
- package/scripts/check-free-identifiers.mjs +67 -8
- package/scripts/codex-diagnostics-installed-acceptance.mjs +72 -0
- package/scripts/dsh-cordis-acceptance.mjs +200 -0
- package/scripts/dsh-installed-acceptance.mjs +247 -0
- package/scripts/dsh-official-acceptance.mjs +180 -0
- package/scripts/dsh-web-startup-acceptance.mjs +87 -0
- package/scripts/github-release.mjs +3 -1
- package/scripts/native-live-acceptance.mjs +369 -0
- package/scripts/regression.mjs +243 -89
- package/scripts/web-handoff-installed-acceptance.mjs +98 -0
- package/scripts/web-native-interaction-acceptance.mjs +204 -0
- package/scripts/web-native-model-acceptance.mjs +138 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { CliError } from '../shared/errors.mjs';
|
|
2
|
+
import { redactSecrets } from '../shared/redact.mjs';
|
|
3
|
+
import { parseOpts, intOpt, required, positiveSafeIntOpt, wantsHelp } from '../cli-args.mjs';
|
|
4
|
+
import { positiveIntOpt } from '../task-cli.mjs';
|
|
5
|
+
import { tx } from '../db/schema.mjs';
|
|
6
|
+
import { createMsgCommands } from '../cli/commands/msg.mjs';
|
|
7
|
+
import { createTaskCommands } from '../cli/commands/task.mjs';
|
|
8
|
+
import { createLockCommands } from '../cli/commands/lock.mjs';
|
|
9
|
+
import { createCoordinationCommands } from '../cli/commands/coordination.mjs';
|
|
10
|
+
import { createCoordinationState } from '../coordination-state.mjs';
|
|
11
|
+
import { createMessageStore } from '../core/coordination/messages.mjs';
|
|
12
|
+
import { createTaskStore } from '../core/coordination/tasks.mjs';
|
|
13
|
+
import { createEvidenceRuntime } from '../core/peers/evidence-runtime.mjs';
|
|
14
|
+
import { annotateTasksWithLiveness } from '../core/peers/liveness.mjs';
|
|
15
|
+
import { clockGraceSuppressed, readClockGraceUntil } from '../shared/clock-grace.mjs';
|
|
16
|
+
import { clockSafetyUnavailable } from '../core/coordination/clock-safety.mjs';
|
|
17
|
+
import { leaseDeadline } from '../core/coordination/lease-renewal.mjs';
|
|
18
|
+
import { scopedLockResource, lockLabel, lockScope, locksConflict } from '../core/coordination/locks.mjs';
|
|
19
|
+
import { captureLockAcquireSubject, sameLockAcquireSubject, observeLockOwnerEvidence } from '../core/coordination/lock-evidence.mjs';
|
|
20
|
+
import { runOptimisticEvidenceMutation } from '../core/coordination/optimistic-evidence.mjs';
|
|
21
|
+
import { normalizeListText } from '../handoff.mjs';
|
|
22
|
+
import { listTaskResults, recordTaskResult } from '../core/coordination/results.mjs';
|
|
23
|
+
|
|
24
|
+
const string = (maxLength = 4096) => ({ type: 'string', minLength: 1, maxLength });
|
|
25
|
+
const integer = (maximum = Number.MAX_SAFE_INTEGER) => ({ type: 'integer', minimum: 1, maximum });
|
|
26
|
+
const strings = { type: 'array', maxItems: 50, items: string() };
|
|
27
|
+
const definitions = [
|
|
28
|
+
['hcc_state', 'Read coordination state for this project and peer.', {}, [], true],
|
|
29
|
+
['hcc_task_list', 'List tasks visible to this peer, with owner evidence.',
|
|
30
|
+
{ limit: integer(100), all: { type: 'boolean' } }, [], true],
|
|
31
|
+
['hcc_inbox', 'Read this peer inbox without acknowledging messages.',
|
|
32
|
+
{ limit: integer(100), all: { type: 'boolean' } }, [], true],
|
|
33
|
+
['hcc_message_send', 'Send a coordination message as this executor peer.',
|
|
34
|
+
{ to: string(256), body: string(16000), task_id: integer(), kind: { type: 'string', enum: ['note', 'ask', 'progress'] } }, ['to', 'body'], false],
|
|
35
|
+
['hcc_task_next', 'Return the current task or claim one pending task. Never completes a task.', {}, [], false],
|
|
36
|
+
['hcc_handoff', 'Record work, tests and risks for an owned task without completing it.',
|
|
37
|
+
{ task_id: integer(), summary: string(16000), to: string(256), changed_files: strings, tests: strings, risks: strings }, ['task_id', 'summary'], false],
|
|
38
|
+
['hcc_lock_acquire', 'Acquire a scoped resource lock for this peer and its task.',
|
|
39
|
+
{ resource: string(1024), task_id: integer(), scope: string(256), ttl: integer(86400), reason: string(4096) }, ['resource', 'task_id'], false],
|
|
40
|
+
['hcc_lock_release', 'Release this peer own lock; cannot force another owner lock.',
|
|
41
|
+
{ resource: string(1024), scope: string(256) }, ['resource'], false],
|
|
42
|
+
['hcc_result_list', 'Read evidence records submitted by this peer.',
|
|
43
|
+
{ task_id: integer(), limit: integer(200) }, [], true],
|
|
44
|
+
['hcc_result_record', 'Record local evidence for an owned task. Does not record publication, business acceptance, or task completion.',
|
|
45
|
+
{ task_id: integer(), kind: { type: 'string', enum: ['verification', 'command', 'diff'] },
|
|
46
|
+
title: string(300), details: string(64000), evidence: { ...strings, maxItems: 20, items: string(2000) },
|
|
47
|
+
status: { type: 'string', enum: ['passed', 'failed', 'pending'] }, result_key: string(300) },
|
|
48
|
+
['task_id', 'title', 'status'], false]
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
// Pure catalogue shared by MCP and the Cordis tool registry.
|
|
52
|
+
export function scopedMcpToolDefinitions() {
|
|
53
|
+
return definitions.map(([name, description, properties, required, readOnly]) => ({
|
|
54
|
+
name, description, inputSchema: structuredClone({ type: 'object', properties, required, additionalProperties: false }),
|
|
55
|
+
annotations: { readOnlyHint: readOnly, destructiveHint: !readOnly, openWorldHint: false }
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function validate(value, schema, label) {
|
|
60
|
+
if (schema.type === 'object') {
|
|
61
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new CliError('BAD_ARGS', `${label} must be an object`);
|
|
62
|
+
for (const key of Object.keys(value)) {
|
|
63
|
+
if (!Object.hasOwn(schema.properties, key)) throw new CliError('BAD_ARGS', `Unknown argument: ${key}`);
|
|
64
|
+
validate(value[key], schema.properties[key], key);
|
|
65
|
+
}
|
|
66
|
+
for (const key of schema.required || []) if (!Object.hasOwn(value, key)) throw new CliError('BAD_ARGS', `Missing argument: ${key}`);
|
|
67
|
+
} else if (schema.type === 'string') {
|
|
68
|
+
if (typeof value !== 'string' || !value.trim() || value.length < (schema.minLength || 0) ||
|
|
69
|
+
value.length > schema.maxLength || value.includes('\0') || (schema.enum && !schema.enum.includes(value))) {
|
|
70
|
+
throw new CliError('BAD_ARGS', `${label} must be a valid bounded string`);
|
|
71
|
+
}
|
|
72
|
+
} else if (schema.type === 'integer') {
|
|
73
|
+
if (!Number.isSafeInteger(value) || value < schema.minimum || value > schema.maximum) throw new CliError('BAD_ARGS', `${label} must be a positive bounded integer`);
|
|
74
|
+
} else if (schema.type === 'boolean') {
|
|
75
|
+
if (typeof value !== 'boolean') throw new CliError('BAD_ARGS', `${label} must be boolean`);
|
|
76
|
+
} else if (schema.type === 'array') {
|
|
77
|
+
if (!Array.isArray(value) || value.length > schema.maxItems) throw new CliError('BAD_ARGS', `${label} must be a bounded array`);
|
|
78
|
+
for (const item of value) validate(item, schema.items, label);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createScopedMcpTools({ ctx, authority, connect: baseConnect, touchPeer, now, addEvent }) {
|
|
83
|
+
const peer = authority.scope.peer, scopedCtx = { ...ctx, cwd: authority.scope.root,
|
|
84
|
+
root: authority.scope.root, dbPath: authority.scope.dbPath, json: true };
|
|
85
|
+
let captured, databases = [], active = false, taskGuard = null;
|
|
86
|
+
function assertWrite(db) {
|
|
87
|
+
authority.assertOwnership(db);
|
|
88
|
+
if (taskGuard !== null) {
|
|
89
|
+
const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskGuard);
|
|
90
|
+
if (!row || row.owner !== peer) throw new CliError('TASK_OWNED', 'Task ownership changed before recording work');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function connect() {
|
|
94
|
+
const db = baseConnect(scopedCtx, { create: false, migrateRegistered: false });
|
|
95
|
+
databases.push(db);
|
|
96
|
+
authority.assertOwnership(db);
|
|
97
|
+
// Existing business functions keep their transactions. Guard each write
|
|
98
|
+
// inside that transaction, including commands that normally autocommit.
|
|
99
|
+
return new Proxy(db, { get(target, key) {
|
|
100
|
+
if (key === 'prepare') return sql => {
|
|
101
|
+
const statement = target.prepare(sql);
|
|
102
|
+
return new Proxy(statement, { get(stmt, property) {
|
|
103
|
+
if (property === 'run') return (...params) => {
|
|
104
|
+
const run = () => { assertWrite(target); return stmt.run(...params); };
|
|
105
|
+
return target.isTransaction ? run() : tx(target, run);
|
|
106
|
+
};
|
|
107
|
+
const value = stmt[property]; return typeof value === 'function' ? value.bind(stmt) : value;
|
|
108
|
+
} });
|
|
109
|
+
};
|
|
110
|
+
const value = target[key]; return typeof value === 'function' ? value.bind(target) : value;
|
|
111
|
+
} });
|
|
112
|
+
}
|
|
113
|
+
const evidence = createEvidenceRuntime({ now });
|
|
114
|
+
const messages = createMessageStore({ now, addEvent });
|
|
115
|
+
const tasks = createTaskStore({ now, addEvent, sendMessage: messages.sendMessage,
|
|
116
|
+
activePeerTtl: 600, observeClockSafety: evidence.observeTaskTakeoverClockSafety });
|
|
117
|
+
const dependencies = { connect, now, tx, addEvent, CliError, parseOpts, intOpt, required,
|
|
118
|
+
positiveSafeIntOpt, positiveIntOpt, wantsHelp, ACTIVE_PEER_TTL: 600, DEFAULT_LOCK_TTL: 900,
|
|
119
|
+
printResult: (_ctx, result) => { captured = result; },
|
|
120
|
+
resolveCurrentPeer: () => ({ id: peer, auto: false }),
|
|
121
|
+
touchCurrentPeer: (db, _ctx, identity, status) => {
|
|
122
|
+
if (identity.id !== peer) throw new CliError('MCP_SCOPE_INVALID', 'Peer identity changed');
|
|
123
|
+
authority.assertOwnership(db); touchPeer(db, peer, status);
|
|
124
|
+
},
|
|
125
|
+
iso: timestamp => new Date(timestamp * 1000).toISOString(),
|
|
126
|
+
...messages, ...tasks, ...evidence,
|
|
127
|
+
annotateTasksWithLiveness, clockGraceSuppressed, readClockGraceUntil,
|
|
128
|
+
leaseDeadline, scopedLockResource, lockLabel, lockScope, locksConflict,
|
|
129
|
+
clockSafetyUnavailable, captureLockAcquireSubject, sameLockAcquireSubject,
|
|
130
|
+
observeLockOwnerEvidence, runOptimisticEvidenceMutation, normalizeListText
|
|
131
|
+
};
|
|
132
|
+
const { cmdTask, notifyTaskOwnerConflict } = createTaskCommands(dependencies);
|
|
133
|
+
const { cmdLock } = createLockCommands({ ...dependencies, notifyTaskOwnerConflict });
|
|
134
|
+
const { cmdMsg } = createMsgCommands(dependencies);
|
|
135
|
+
const { cmdHandoff } = createCoordinationCommands(dependencies);
|
|
136
|
+
const { statusSnapshot } = createCoordinationState({ connect, now, ...messages, ...tasks,
|
|
137
|
+
observePeerEvidence: evidence.observePeerEvidence });
|
|
138
|
+
const tools = scopedMcpToolDefinitions();
|
|
139
|
+
const byName = new Map(tools.map(tool => [tool.name, tool]));
|
|
140
|
+
const option = (key, value) => `--${key}=${value}`;
|
|
141
|
+
function assertTask(taskId, action) {
|
|
142
|
+
const db = connect();
|
|
143
|
+
const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
|
|
144
|
+
if (!row) throw new CliError('NOT_FOUND', 'Task does not exist');
|
|
145
|
+
tasks.assertTaskOwnerForMutation(db, peer, row, action);
|
|
146
|
+
if (row.owner !== peer) throw new CliError('TASK_OWNED', 'Claim this task before recording work or acquiring its locks');
|
|
147
|
+
taskGuard = taskId;
|
|
148
|
+
}
|
|
149
|
+
async function execute(name, args) {
|
|
150
|
+
if (name === 'hcc_state') return statusSnapshot(scopedCtx, peer);
|
|
151
|
+
if (name === 'hcc_inbox') return messages.queryInbox(connect(), peer, args.all === true, args.limit || 20);
|
|
152
|
+
if (name === 'hcc_task_list') return cmdTask(scopedCtx, ['list', option('peer', peer), option('limit', args.limit || 50), ...(args.all ? ['--all'] : [])]);
|
|
153
|
+
if (name === 'hcc_task_next') return cmdTask(scopedCtx, ['next', option('peer', peer), '--count=1']);
|
|
154
|
+
if (name === 'hcc_result_list') return listTaskResults(connect(), { peer, taskId: args.task_id || null, limit: args.limit || 100 });
|
|
155
|
+
if (name === 'hcc_result_record') {
|
|
156
|
+
assertTask(args.task_id, 'result-record');
|
|
157
|
+
if (args.status === 'passed' && !args.evidence?.length) throw new CliError('BAD_ARGS', 'A passed result needs an evidence reference');
|
|
158
|
+
return recordTaskResult(connect(), { peer, taskId: args.task_id,
|
|
159
|
+
executorId: authority.scope.executorId, kind: args.kind, title: args.title,
|
|
160
|
+
details: args.details, evidence: args.evidence, status: args.status,
|
|
161
|
+
resultKey: args.result_key, stage: 'local', source: 'executor' }, { now, addEvent });
|
|
162
|
+
}
|
|
163
|
+
if (name === 'hcc_message_send') return cmdMsg(scopedCtx, ['send', option('from', peer), option('to', args.to),
|
|
164
|
+
option('body', args.body), option('kind', args.kind || 'note'), ...(args.task_id ? [option('task', args.task_id)] : [])]);
|
|
165
|
+
if (name === 'hcc_handoff') {
|
|
166
|
+
assertTask(args.task_id, 'handoff');
|
|
167
|
+
return cmdHandoff(scopedCtx, ['create', option('from', peer), option('task', args.task_id), option('summary', args.summary),
|
|
168
|
+
option('changed-files', JSON.stringify(args.changed_files || [])), option('tests', JSON.stringify(args.tests || [])),
|
|
169
|
+
option('risks', JSON.stringify(args.risks || [])), ...(args.to ? [option('to', args.to)] : [])]);
|
|
170
|
+
}
|
|
171
|
+
if (name === 'hcc_lock_acquire') assertTask(args.task_id, 'lock-acquire');
|
|
172
|
+
return cmdLock(scopedCtx, [name === 'hcc_lock_acquire' ? 'acquire' : 'release', option('peer', peer), option('resource', args.resource),
|
|
173
|
+
...(args.scope ? [option('scope', args.scope)] : []), ...(args.task_id ? [option('task', args.task_id)] : []),
|
|
174
|
+
...(args.ttl ? [option('ttl', args.ttl)] : []), ...(args.reason ? [option('reason', args.reason)] : [])]);
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
list: () => tools.map(tool => structuredClone(tool)), has: name => byName.has(name),
|
|
178
|
+
async call(name, args) {
|
|
179
|
+
if (active) return { isError: true, content: [{ type: 'text', text: 'A scoped tool call is already running' }] };
|
|
180
|
+
active = true; captured = undefined; databases = []; taskGuard = null;
|
|
181
|
+
try {
|
|
182
|
+
if (!byName.has(name)) throw new CliError('BAD_ARGS', 'Unknown scoped tool');
|
|
183
|
+
validate(args, byName.get(name).inputSchema, 'arguments');
|
|
184
|
+
authority.assertValid();
|
|
185
|
+
const direct = await execute(name, args);
|
|
186
|
+
const structuredContent = redactSecrets({ ok: true, peer, data: captured === undefined ? direct : captured });
|
|
187
|
+
return { content: [{ type: 'text', text: JSON.stringify(structuredContent) }], structuredContent, isError: false };
|
|
188
|
+
} catch (error) {
|
|
189
|
+
const structuredContent = redactSecrets({ ok: false, peer, error: {
|
|
190
|
+
code: error instanceof CliError ? error.code : 'MCP_TOOL_FAILED',
|
|
191
|
+
message: error instanceof CliError ? error.message : 'The scoped tool call failed'
|
|
192
|
+
} });
|
|
193
|
+
return { content: [{ type: 'text', text: JSON.stringify(structuredContent) }], structuredContent, isError: true };
|
|
194
|
+
} finally { for (const db of databases) { try { db.close(); } catch {} } active = false; }
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
package/lib/process/identity.mjs
CHANGED
|
@@ -98,7 +98,9 @@ function parseMacBootToken(text) {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
function macIdentity(pid) {
|
|
101
|
-
|
|
101
|
+
let boot = run('sysctl', ['-n', 'kern.boottime']);
|
|
102
|
+
// Launchers often omit /usr/sbin from PATH; macOS still provides sysctl here.
|
|
103
|
+
if (boot.error?.code === 'ENOENT') boot = run('/usr/sbin/sysctl', ['-n', 'kern.boottime']);
|
|
102
104
|
if (boot.error || boot.status !== 0) return null;
|
|
103
105
|
const bootToken = parseMacBootToken(boot.stdout);
|
|
104
106
|
if (!bootToken) return null;
|
|
@@ -2,6 +2,15 @@ export function normalizeVersion(version) {
|
|
|
2
2
|
return String(version || '').replace(/^v/, '');
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
+
export function isPrereleaseVersion(version) {
|
|
6
|
+
const numeric = '(?:0|[1-9][0-9]*)';
|
|
7
|
+
const identifier = `(?:${numeric}|[0-9]*[A-Za-z-][0-9A-Za-z-]*)`;
|
|
8
|
+
const semver = new RegExp(`^${numeric}\\.${numeric}\\.${numeric}(?:-(${identifier}(?:\\.${identifier})*))?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$`);
|
|
9
|
+
const match = normalizeVersion(version).match(semver);
|
|
10
|
+
if (!match) throw new Error(`Invalid release version: ${version}`);
|
|
11
|
+
return match[1] !== undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
5
14
|
export function releaseSection(markdown, version) {
|
|
6
15
|
const lines = markdown.split(/\r?\n/);
|
|
7
16
|
const heading = `## ${version}`;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { CliError } from '../../shared/errors.mjs';
|
|
3
|
+
import { readNativePointer } from './store.mjs';
|
|
4
|
+
|
|
5
|
+
export function nativeRequest(ctx, method, route, body = null, { timeoutMs = 35000 } = {}) {
|
|
6
|
+
const pointer = readNativePointer(ctx);
|
|
7
|
+
if (!pointer) return Promise.reject(new CliError('NATIVE_RUNTIME_OFFLINE', 'Native runtime is offline. Run hcc native up.'));
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const payload = body === null ? null : JSON.stringify(body);
|
|
10
|
+
let timer;
|
|
11
|
+
const fail = (error) => { clearTimeout(timer); reject(error); };
|
|
12
|
+
const invalid = (message) => new CliError('NATIVE_RESPONSE_INVALID', message, { uncertain: method !== 'GET' });
|
|
13
|
+
const request = http.request({ host: '127.0.0.1', port: pointer.port, method, path: route,
|
|
14
|
+
headers: { authorization: `Bearer ${pointer.token}`, ...(payload ? { 'content-type': 'application/json', 'content-length': Buffer.byteLength(payload) } : {}) }
|
|
15
|
+
}, (response) => {
|
|
16
|
+
let data = '';
|
|
17
|
+
response.setEncoding('utf8');
|
|
18
|
+
response.on('data', (chunk) => {
|
|
19
|
+
data += chunk;
|
|
20
|
+
if (Buffer.byteLength(data) > 8 * 1024 * 1024) response.destroy(invalid('Native response is too large'));
|
|
21
|
+
});
|
|
22
|
+
response.on('error', (error) => fail(error instanceof CliError ? error : invalid('Native response was interrupted; inspect status before retrying.')));
|
|
23
|
+
response.on('end', () => {
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
try {
|
|
26
|
+
const result = JSON.parse(data);
|
|
27
|
+
if (!result || typeof result !== 'object' || Array.isArray(result) || typeof result.ok !== 'boolean') throw invalid('Invalid native response; inspect status before retrying.');
|
|
28
|
+
if (!result.ok) fail(new CliError(result.error?.code || 'NATIVE_REQUEST_FAILED', result.error?.message || 'Native request failed', result.error?.extra || {}));
|
|
29
|
+
else if (response.statusCode < 200 || response.statusCode >= 300) fail(invalid('Native runtime returned an unsuccessful HTTP response'));
|
|
30
|
+
else resolve(result.data);
|
|
31
|
+
} catch (error) { fail(error instanceof CliError ? error : invalid('Invalid native JSON response; inspect status before retrying.')); }
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
timer = setTimeout(() => request.destroy(new CliError('NATIVE_CLIENT_TIMEOUT', 'Native runtime request timed out; inspect status before retrying.', { uncertain: method !== 'GET' })), timeoutMs);
|
|
35
|
+
request.on('error', (error) => fail(error instanceof CliError ? error : new CliError('NATIVE_RUNTIME_OFFLINE', 'Cannot reach the native runtime', { uncertain: method !== 'GET' })));
|
|
36
|
+
request.end(payload);
|
|
37
|
+
});
|
|
38
|
+
}
|