@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.
Files changed (97) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +4 -0
  3. package/README.zh-CN.md +4 -0
  4. package/docs/README.md +9 -0
  5. package/docs/README.zh-CN.md +9 -0
  6. package/docs/commands.md +53 -0
  7. package/docs/commands.zh-CN.md +48 -0
  8. package/docs/dsh.md +137 -0
  9. package/docs/dsh.zh-CN.md +139 -0
  10. package/docs/mcp.md +9 -0
  11. package/docs/mcp.zh-CN.md +23 -0
  12. package/docs/native.md +295 -0
  13. package/docs/native.zh-CN.md +253 -0
  14. package/docs/web-handoff.zh-CN.md +53 -0
  15. package/lib/cli/app.mjs +25 -6
  16. package/lib/cli/commands/doctor.mjs +17 -2
  17. package/lib/cli/commands/dsh.mjs +79 -0
  18. package/lib/cli/commands/hook.mjs +241 -90
  19. package/lib/cli/commands/lock.mjs +34 -19
  20. package/lib/cli/commands/mcp.mjs +28 -0
  21. package/lib/cli/commands/native.mjs +130 -0
  22. package/lib/cli/commands/run.mjs +66 -21
  23. package/lib/cli/commands/task.mjs +6 -3
  24. package/lib/cli-args.mjs +4 -0
  25. package/lib/coordination-state.mjs +15 -1
  26. package/lib/core/coordination/event-retention.mjs +14 -2
  27. package/lib/core/coordination/gc-plan.mjs +3 -13
  28. package/lib/core/coordination/locks.mjs +45 -0
  29. package/lib/core/coordination/results.mjs +79 -0
  30. package/lib/core/peers/bindings.mjs +9 -3
  31. package/lib/diagnostics/codex.mjs +121 -0
  32. package/lib/integrations/codex-interactions.mjs +114 -0
  33. package/lib/integrations/dsh-collaboration.mjs +170 -0
  34. package/lib/integrations/dsh-cordis.d.ts +19 -0
  35. package/lib/integrations/dsh-cordis.mjs +177 -0
  36. package/lib/integrations/dsh.bundle.yml +7 -0
  37. package/lib/integrations/dsh.mjs +287 -0
  38. package/lib/integrations/native/claude.mjs +451 -0
  39. package/lib/integrations/native/codex.mjs +242 -0
  40. package/lib/integrations/native/dsh-acp.mjs +343 -0
  41. package/lib/integrations/native/index.mjs +19 -0
  42. package/lib/integrations/native/interactions.mjs +53 -0
  43. package/lib/integrations/native/jsonrpc.mjs +185 -0
  44. package/lib/integrations/peers/identity.mjs +36 -10
  45. package/lib/integrations/peers/processes.mjs +22 -8
  46. package/lib/integrations/providers.mjs +6 -2
  47. package/lib/mcp/scope.mjs +104 -0
  48. package/lib/mcp/stdio.mjs +80 -0
  49. package/lib/mcp/tools.mjs +197 -0
  50. package/lib/process/identity.mjs +3 -1
  51. package/lib/release/release-notes.mjs +9 -0
  52. package/lib/runtime/native/client.mjs +38 -0
  53. package/lib/runtime/native/service.mjs +511 -0
  54. package/lib/runtime/native/store.mjs +121 -0
  55. package/lib/ui/help.mjs +5 -2
  56. package/lib/web/adoption-state.mjs +55 -0
  57. package/lib/web/auto-attach.mjs +18 -2
  58. package/lib/web/codex-app-server.mjs +557 -0
  59. package/lib/web/codex-sessions.mjs +287 -0
  60. package/lib/web/control-lease.mjs +204 -0
  61. package/lib/web/http-routes.mjs +197 -15
  62. package/lib/web/http.mjs +5 -4
  63. package/lib/web/native-sessions.mjs +286 -0
  64. package/lib/web/project-contexts.mjs +2 -1
  65. package/lib/web/pty-sessions.mjs +60 -33
  66. package/lib/web/runtime-main.mjs +98 -6
  67. package/lib/web/session-serialize.mjs +0 -0
  68. package/lib/web/task-results.mjs +76 -0
  69. package/lib/web/tmux-sessions.mjs +92 -11
  70. package/lib/web/tmux-stream.mjs +2 -2
  71. package/lib/web/ui-codex.mjs +550 -0
  72. package/lib/web/ui-command-palette.mjs +119 -0
  73. package/lib/web/ui-handoff.mjs +102 -0
  74. package/lib/web/ui-history.mjs +118 -0
  75. package/lib/web/ui-i18n.mjs +841 -0
  76. package/lib/web/ui-interactions.mjs +78 -0
  77. package/lib/web/ui-login.mjs +101 -0
  78. package/lib/web/ui-native.mjs +165 -0
  79. package/lib/web/ui-preferences.mjs +278 -0
  80. package/lib/web/ui-review.mjs +132 -0
  81. package/lib/web/ui-template.mjs +1106 -576
  82. package/lib/web/ui-terminal-find.mjs +98 -0
  83. package/lib/web/ui-workbench.mjs +94 -0
  84. package/lib/web/ui-workspace.mjs +222 -0
  85. package/package.json +11 -3
  86. package/scripts/check-free-identifiers.mjs +67 -8
  87. package/scripts/codex-diagnostics-installed-acceptance.mjs +72 -0
  88. package/scripts/dsh-cordis-acceptance.mjs +200 -0
  89. package/scripts/dsh-installed-acceptance.mjs +247 -0
  90. package/scripts/dsh-official-acceptance.mjs +180 -0
  91. package/scripts/dsh-web-startup-acceptance.mjs +87 -0
  92. package/scripts/github-release.mjs +3 -1
  93. package/scripts/native-live-acceptance.mjs +369 -0
  94. package/scripts/regression.mjs +243 -89
  95. package/scripts/web-handoff-installed-acceptance.mjs +98 -0
  96. package/scripts/web-native-interaction-acceptance.mjs +204 -0
  97. package/scripts/web-native-model-acceptance.mjs +138 -0
@@ -0,0 +1,180 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { createHash } from 'node:crypto';
6
+ import { spawnSync } from 'node:child_process';
7
+ import { DatabaseSync } from 'node:sqlite';
8
+ import { createRequire } from 'node:module';
9
+ import { fileURLToPath, pathToFileURL } from 'node:url';
10
+ import { ensureDshIntegration } from '../lib/integrations/dsh.mjs';
11
+
12
+ // Opt-in integration acceptance against an isolated install, with no project
13
+ // dependency changes and no model or credential calls. Install with scripts
14
+ // disabled into a temporary directory, then pass that directory as argv[2].
15
+ const installPath = process.argv[2];
16
+ if (!installPath) throw new Error('Usage: node scripts/dsh-official-acceptance.mjs /absolute/path/to/isolated-dsh-install');
17
+ const runtimeRoot = path.resolve(installPath);
18
+ const resolveOfficial = createRequire(path.join(runtimeRoot, 'package.json'));
19
+ for (const name of ['dsh', 'dsh-hooks-claude-code']) {
20
+ const manifest = JSON.parse(fs.readFileSync(resolveOfficial.resolve(`@deepseek-ai/${name}/package.json`), 'utf8'));
21
+ assert.equal(manifest.version, '0.2.0-rc.2', `Acceptance baseline requires @deepseek-ai/${name}@0.2.0-rc.2`);
22
+ }
23
+ const loadOfficial = (name) => import(pathToFileURL(resolveOfficial.resolve(`@deepseek-ai/${name}`)).href);
24
+ const [cordis, subprocess, bash, sessions, projections, agentLoop, Hooks, llm] = await Promise.all([
25
+ 'cordis', 'dsh-subprocess-local', 'dsh-bash-local', 'dsh-session',
26
+ 'dsh-session-projection', 'dsh-agent-loop', 'dsh-hooks-claude-code', 'dsh-llm'
27
+ ].map(loadOfficial));
28
+ const { Context } = cordis;
29
+ const { default: Subprocess } = subprocess;
30
+ const { default: Bash } = bash;
31
+ const { default: Sessions } = sessions;
32
+ const { default: Projections } = projections;
33
+ const { turnBoundaryProjectionDefinition } = agentLoop;
34
+ const { createUserMessage } = llm;
35
+ const repo = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
36
+ const hcc=path.join(repo,'bin/hcc.mjs');
37
+ const sandbox=fs.mkdtempSync(path.join(os.tmpdir(),'hcc-dsh-real-bridge-'));
38
+ const home=path.join(sandbox,'isolated-home');
39
+ const rootA=path.join(sandbox,'project A');
40
+ const rootB=path.join(sandbox,'project B');
41
+ for(const dir of [home,rootA,rootB]) fs.mkdirSync(dir,{recursive:true});
42
+ const cleanEnv={HOME:home,DSH_HOME:path.join(home,'dsh'),DSH_TELEMETRY_DISABLED:'1',PATH:path.dirname(process.execPath)+':/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin',LANG:'C',LC_ALL:'C',SHELL:'/bin/bash'};
43
+ // Keep all state writes, including the hcc project registry, inside this sandbox.
44
+ for(const key of Object.keys(process.env)) delete process.env[key];
45
+ Object.assign(process.env,cleanEnv);
46
+ const results=[];
47
+ function pass(name,data={}) { results.push({name,...data}); }
48
+ function runHcc(root,args,options={}) {
49
+ const env={...cleanEnv,...options.env};
50
+ const r=spawnSync(process.execPath,[hcc,'--root',root,'--json',...args],{cwd:root,encoding:'utf8',env,input:options.input,timeout:15000});
51
+ if(options.expectedCode!==undefined) assert.equal(r.status,options.expectedCode,r.stderr+r.stdout);
52
+ else assert.equal(r.status,0,r.stderr+r.stdout);
53
+ const parsed=r.stdout ? JSON.parse(r.stdout):null;
54
+ return {r,data:parsed?.data??parsed};
55
+ }
56
+ function dbRows(root,sql,...args) { const db=new DatabaseSync(path.join(root,'.hello-cc','mesh.db')); try { return db.prepare(sql).all(...args).map(r=>({...r})); } finally {db.close();} }
57
+ function peer(id) {return 'dsh-'+createHash('sha1').update(id).digest('hex').slice(0,8);}
58
+ function textOf(messages) {return messages.flatMap(m=>m.content||[]).filter(c=>c.type==='text').map(c=>c.text).join('\n');}
59
+ const setup=ensureDshIntegration({root:rootA,cwd:rootA},{hccBin:hcc,nodeBin:process.execPath});
60
+ assert.ok(setup.ready);
61
+ pass('project-local setup generated official overlay',{hooks:setup.hooksPath,patch:setup.patchPath});
62
+ runHcc(rootA,['register','--peer','claude-acceptance','--kind','claude']);
63
+ runHcc(rootA,['register','--peer','codex-acceptance','--kind','codex']);
64
+ runHcc(rootA,['task','create','--title','DSH_FIRST_STEP_TASK','--from','claude-acceptance']);
65
+ runHcc(rootA,['lock','acquire','--peer','claude-acceptance','--resource','shared.txt','--reason','DSH_LOCK_CONFLICT']);
66
+
67
+ const ctx=new Context();
68
+ const fibers=[];
69
+ let unregister;
70
+ try {
71
+ for(const plugin of [Subprocess,Bash,Sessions,Projections]) {const f=ctx.plugin(plugin);await f.await();fibers.push(f);}
72
+ unregister=ctx.sessionProjections.register(turnBoundaryProjectionDefinition);
73
+ const bridge=ctx.plugin(Hooks,{configPath:setup.hooksPath,defaultTimeoutMs:10000});await bridge.await();fibers.push(bridge);
74
+ function agent(id,cwd) {
75
+ const session=ctx.sessions.create(id,{meta:{cwd}});
76
+ const injected=[],steered=[];
77
+ return {session,injected,steered,inject(message){injected.push(message);},steer(message){steered.push(message);}};
78
+ }
79
+ const firstId='s.official-non-uuid/first';
80
+ const secondId='s.official-non-uuid/second';
81
+ const first=agent(firstId,rootA),second=agent(secondId,rootA),elsewhere=agent(firstId+'.another-workspace',rootB);
82
+ // Deliberate desktop-provider env pollution plus a process-global hcc peer.
83
+ process.env.CODEX_THREAD_ID='stale-codex-thread';
84
+ process.env.CLAUDE_CODE_SESSION_ID='stale-claude-session';
85
+ process.env.HCC_PEER='one-runtime-global-peer';
86
+ await ctx.parallel('agent/created',{agent:first,source:'startup',signal:new AbortController().signal});
87
+ assert.equal(first.injected.length,1,'awaited agent/created must inject first-step context before resolving');
88
+ const initial=textOf(first.injected);
89
+ assert.match(initial,/DSH_FIRST_STEP_TASK/);
90
+ assert.match(initial,/claude-acceptance/);
91
+ assert.match(initial,/codex-acceptance/);
92
+ assert.match(initial,/shared\.txt/);
93
+ assert.match(initial,new RegExp(peer(firstId)));
94
+ pass('awaited first-step SessionStart injection includes tasks peers and locks');
95
+ await ctx.parallel('agent/created',{agent:second,source:'startup',signal:new AbortController().signal});
96
+ await ctx.parallel('agent/created',{agent:elsewhere,source:'startup',signal:new AbortController().signal});
97
+ const bindings=dbRows(rootA,'SELECT peer,provider,provider_session_id,provider_session_name,transport FROM peer_bindings WHERE provider = ?','dsh');
98
+ assert.equal(bindings.length,2);
99
+ assert.deepEqual(bindings.map(x=>x.provider_session_id).sort(),[firstId,secondId].sort());
100
+ assert.ok(bindings.every(x=>x.provider_session_name===null));
101
+ assert.ok(bindings.every(x=>x.peer!==process.env.HCC_PEER));
102
+ assert.equal(dbRows(rootB,'SELECT provider_session_id FROM peer_bindings WHERE provider = ?','dsh')[0].provider_session_id,elsewhere.session.id);
103
+ assert.equal(dbRows(rootA,'SELECT id FROM peers WHERE id = ?','one-runtime-global-peer').length,0);
104
+ pass('non-UUID real session IDs kept independently despite inherited Claude Codex and HCC_PEER markers',{bindings});
105
+ assert.match(textOf(elsewhere.injected),new RegExp(peer(elsewhere.session.id)));
106
+ assert.doesNotMatch(textOf(elsewhere.injected),/DSH_FIRST_STEP_TASK/);
107
+ pass('one official bridge serves distinct workspaces without cross-project task leakage');
108
+ await ctx.parallel('agent/created',{agent:first,source:'resume',signal:new AbortController().signal});
109
+ assert.equal(dbRows(rootA,'SELECT peer FROM peer_bindings WHERE provider = ?','dsh').length,2);
110
+ pass('SessionStart resume maps back to same peer');
111
+
112
+ process.env.HCC_ROOT=rootB;
113
+ process.env.HCC_DB=path.join(rootB,'.hello-cc','mesh.db');
114
+ for(const [carrier,id] of [[first,firstId],[second,secondId]]) {
115
+ const prefix=textOf(carrier.injected).split('\n').find(line=>line.startsWith('env ')&&line.includes('HCC_PEER='));
116
+ assert.ok(prefix,'injected dsh context must contain its own exact hcc command prefix');
117
+ const execution=await (await ctx.shell.execute(ctx.shell.resolve({
118
+ command:prefix+' msg send --to codex-acceptance --body DSH_PREFIX_SHELL_REPLY',workdir:rootB
119
+ }))).result();
120
+ assert.equal(execution.exitCode,0,execution.stderr.text);
121
+ assert.equal(dbRows(rootA,'SELECT sender FROM messages WHERE body = ? AND sender = ?','DSH_PREFIX_SHELL_REPLY',peer(id)).length,1);
122
+ }
123
+ assert.equal(dbRows(rootB,'SELECT id FROM messages WHERE body = ?','DSH_PREFIX_SHELL_REPLY').length,0);
124
+ pass('official shell executes both injected session command prefixes under polluted peer and workspace env with correct sender and database');
125
+
126
+ for(const key of ['CODEX_THREAD_ID','CLAUDE_CODE_SESSION_ID','HCC_PEER','HCC_ROOT','HCC_DB']) delete process.env[key];
127
+ first.session.append('turn/start',{turn:1});
128
+ runHcc(rootA,['msg','send','--from','claude-acceptance','--to',peer(firstId),'--body','DSH_FROM_CLAUDE_FRESH_PROMPT']);
129
+ const prompt=createUserMessage({content:[{type:'text',text:'acceptance prompt'}],source:{kind:'user'}});
130
+ const entered=await ctx.waterfall('agent/pre-step',{agent:first,messages:[prompt],turn:1,signal:new AbortController().signal},async()=>({kind:'enter',messages:[prompt],extraMetadata:'retained'}));
131
+ assert.equal(entered.kind,'enter');
132
+ assert.equal(entered.extraMetadata,'retained');
133
+ assert.match(textOf(entered.messages),/DSH_FROM_CLAUDE_FRESH_PROMPT/);
134
+ assert.equal(dbRows(rootA,'SELECT message_id FROM message_reads WHERE peer = ?',peer(firstId)).length,1);
135
+ pass('UserPromptSubmit official bridge appends parsed additionalContext and acknowledges delivered inbox');
136
+ runHcc(rootA,['msg','send','--from',peer(firstId),'--to','claude-acceptance','--body','DSH_REPLY_TO_CLAUDE']);
137
+ const inbox=runHcc(rootA,['msg','inbox','--peer','claude-acceptance']).data;
138
+ assert.ok(inbox.some(m=>m.body==='DSH_REPLY_TO_CLAUDE'));
139
+ pass('DSH peer can reply through same real hcc bus');
140
+ const conflict=runHcc(rootA,['lock','acquire','--peer',peer(firstId),'--resource','shared.txt'],{expectedCode:1});
141
+ assert.match(conflict.r.stdout+conflict.r.stderr,/LOCK_CONFLICT|LOCK_HELD|locked|held/i);
142
+ pass('actual advisory lock conflict is visible and prevents second acquisition');
143
+
144
+ const pre=await ctx.waterfall('tools/pre-execute',{agent:first,name:'bash',arguments:{command:'true'},callId:'call-1',signal:new AbortController().signal},async()=>({kind:'allow'}));
145
+ assert.equal(pre.kind,'allow');
146
+ runHcc(rootA,['msg','send','--from','codex-acceptance','--to',peer(firstId),'--body','DSH_POST_TOOL_CONTEXT']);
147
+ const post=await ctx.waterfall('tools/post-execute',{agent:first,name:'str_replace_editor',arguments:{path:'dummy.txt'},callId:'call-2',signal:new AbortController().signal},{content:[{type:'text',text:'tool done'}]},async()=>({kind:'accept'}));
148
+ assert.match(textOf(post.additionalContexts),/DSH_POST_TOOL_CONTEXT/);
149
+ pass('official dsh lower-case bash and str_replace_editor tool names invoke all-tool hooks');
150
+ runHcc(rootA,['msg','send','--from','codex-acceptance','--to',peer(firstId),'--body','DSH_STOP_CONTINUE']);
151
+ await ctx.parallel('agent/turn-stopping',{agent:first,turn:1,signal:new AbortController().signal});
152
+ assert.equal(first.steered.length,1);
153
+ assert.match(textOf(first.steered),/DSH_STOP_CONTINUE/);
154
+ await ctx.parallel('agent/turn-stopping',{agent:first,turn:1,signal:new AbortController().signal});
155
+ assert.equal(first.steered.length,1,'acknowledged message must not produce another Stop continuation');
156
+ pass('Stop hook block is mapped to official steer exactly once per inbox message');
157
+ const hookResults=first.session.snapshotEvents().filter(e=>e.type==='hook/result');
158
+ assert.ok(hookResults.length>=5);
159
+ assert.ok(hookResults.every(e=>e.data.exitCode===0));
160
+ pass('official Session log records successful command hook results',{resultEvents:hookResults.length});
161
+ const dump=spawnSync(process.execPath,[resolveOfficial.resolve('@deepseek-ai/dsh/lib/bin.js'),'web','--patch',setup.patchPath,'--dump-config'],{cwd:rootA,encoding:'utf8',env:cleanEnv,timeout:20000});
162
+ assert.equal(dump.status,0,dump.stderr);
163
+ assert.match(dump.stdout,/hello-cc-dsh-hooks/);
164
+ assert.match(dump.stdout,/@deepseek-ai\/dsh-hooks-claude-code/);
165
+ fs.writeFileSync(path.join(sandbox,'effective-config.yml'),dump.stdout);
166
+ pass('official dsh0.2.0rc2 CLI composes generated overlay with web profile');
167
+ const launch=spawnSync(process.execPath,[hcc,'--root',rootA,'dsh','web',
168
+ '--dsh-bin',resolveOfficial.resolve('@deepseek-ai/dsh/lib/bin.js'),
169
+ '--dsh-home',cleanEnv.DSH_HOME,'--','--help'],{cwd:rootA,encoding:'utf8',env:cleanEnv,timeout:20000});
170
+ assert.equal(launch.status,0,launch.stderr);
171
+ assert.match(launch.stdout,/Usage: dsh --profile web/);
172
+ assert.equal(dbRows(rootA,'SELECT peer FROM peer_bindings WHERE provider = ?','dsh').length,2);
173
+ pass('public hcc dsh web wrapper launches official app and forwards runtime help without creating a service peer');
174
+ } finally {
175
+ if(unregister) await unregister();
176
+ for(const f of fibers.reverse()) await f.dispose();
177
+ }
178
+ const receipt={baseline:'@deepseek-ai/dsh@0.2.0-rc.2',sandbox,results,passed:results.length,boundary:'Official Cordis, Sessions/projections, hooks bridge, shell executor/subprocess, and real hcc commands; in-memory agent carrier only; no LLM, credentials, or external API calls.'};
179
+ fs.writeFileSync(path.join(sandbox,'receipt.json'),JSON.stringify(receipt,null,2)+'\n');
180
+ console.log(JSON.stringify(receipt,null,2));
@@ -0,0 +1,87 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import net from 'node:net';
5
+ import { spawn } from 'node:child_process';
6
+ import { createRequire } from 'node:module';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ // Read-only HTTP acceptance of a real isolated official dsh Web service.
10
+ // Only this script's owned wrapper/process group is stopped during cleanup.
11
+ const installPath = process.argv[2];
12
+ const mode = process.argv[3] || 'hooks';
13
+ if (!['hooks', 'cordis'].includes(mode)) throw new Error('Mode must be hooks or cordis');
14
+ if (!installPath) throw new Error('Usage: node scripts/dsh-web-startup-acceptance.mjs /absolute/path/to/isolated-dsh-install');
15
+ const resolveOfficial = createRequire(path.join(path.resolve(installPath), 'package.json'));
16
+ const manifest = JSON.parse(fs.readFileSync(resolveOfficial.resolve('@deepseek-ai/dsh/package.json'), 'utf8'));
17
+ if (manifest.version !== '0.2.0-rc.2') throw new Error('Acceptance requires @deepseek-ai/dsh@0.2.0-rc.2');
18
+ const repo = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
19
+ const sandbox=fs.mkdtempSync(path.join(os.tmpdir(),'hcc-dsh-web-startup-'));
20
+ const project=path.join(sandbox,'project with spaces');
21
+ const isolatedHome=path.join(sandbox,'isolated-home');
22
+ fs.mkdirSync(project,{recursive:true});fs.mkdirSync(isolatedHome,{recursive:true});
23
+ const probe=net.createServer();await new Promise((resolve,reject)=>{probe.once('error',reject);probe.listen(0,'127.0.0.1',resolve);});
24
+ const port=probe.address().port;await new Promise(resolve=>probe.close(resolve));
25
+ const officialBin=resolveOfficial.resolve('@deepseek-ai/dsh/lib/bin.js');
26
+ const env={HOME:isolatedHome,DSH_HOME:path.join(isolatedHome,'dsh'),DSH_TELEMETRY_DISABLED:'1',PATH:path.dirname(process.execPath)+':/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin',LANG:'C',LC_ALL:'C',SHELL:'/bin/bash'};
27
+ const args=[path.join(repo,'bin/hcc.mjs'),'--root',project,'dsh','web','--mode',mode,'--dsh-bin',officialBin,'--dsh-home',env.DSH_HOME,'--','--host','127.0.0.1','--port',String(port),'--no-open'];
28
+ let stdout='',stderr='',exitCode=null,exitSignal=null,exited=false;
29
+ const child=spawn(process.execPath,args,{cwd:project,env,stdio:['ignore','pipe','pipe'],detached:true});
30
+ const ended=new Promise(resolve=>child.once('close',(code,signal)=>{exitCode=code;exitSignal=signal;exited=true;resolve();}));
31
+ child.stdout.on('data',chunk=>{stdout+=chunk;fs.appendFileSync(path.join(sandbox,'stdout.log'),chunk);});
32
+ child.stderr.on('data',chunk=>{stderr+=chunk;fs.appendFileSync(path.join(sandbox,'stderr.log'),chunk);});
33
+ const url=`http://127.0.0.1:${port}`;
34
+ const receipt={sandbox,project,mode,officialVersion:'0.2.0-rc.2',url,wrapperPid:child.pid,command:args,ready:false,port};
35
+ let html='';
36
+ try {
37
+ const until=Date.now()+40000;
38
+ while(Date.now()<until&&!exited) {
39
+ try {
40
+ let cookie='';
41
+ let res=await fetch(url,{signal:AbortSignal.timeout(1000),redirect:'manual'});
42
+ receipt.unauthenticatedStatus=res.status;
43
+ await res.text();
44
+ const launchUrl=stdout.match(/dsh web: (http:\/\/[^\s]+)/)?.[1];
45
+ if(res.status===401&&launchUrl&&new URL(launchUrl).origin===url) {
46
+ const login=await fetch(launchUrl,{signal:AbortSignal.timeout(1000),redirect:'manual'});
47
+ receipt.loginStatus=login.status;
48
+ cookie=login.headers.get('set-cookie')?.split(';')[0]||'';
49
+ await login.text();
50
+ receipt.cookieIssued=Boolean(cookie);
51
+ res=await fetch(url,{headers:{cookie},signal:AbortSignal.timeout(1000),redirect:'manual'});
52
+ }
53
+ html=await res.text();
54
+ if(res.status===200){
55
+ receipt.httpStatus=res.status;receipt.title=html.match(/<title>([^<]*)<\/title>/)?.[1]||'';
56
+ receipt.hasBootstrap=html.includes('__DSH_BOOT__');receipt.htmlBytes=Buffer.byteLength(html);
57
+ const assets=[...html.matchAll(/<script[^>]+src="([^"]+)"/g)].map(match=>match[1].replaceAll('&amp;','&'));
58
+ receipt.scriptAssets=[];
59
+ for(const asset of assets){
60
+ const assetUrl=new URL(asset,url+'/');
61
+ if(assetUrl.origin===url){
62
+ const assetRes=await fetch(assetUrl,{headers:{cookie},signal:AbortSignal.timeout(2000)});
63
+ receipt.scriptAssets.push({path:assetUrl.pathname,status:assetRes.status,bytes:(await assetRes.arrayBuffer()).byteLength});
64
+ }
65
+ }
66
+ receipt.ready=receipt.hasBootstrap&&receipt.scriptAssets.length>0&&receipt.scriptAssets.every(asset=>asset.status===200);break;
67
+ }
68
+ receipt.lastHttpStatus=res.status;
69
+ }catch{}
70
+ await new Promise(resolve=>setTimeout(resolve,500));
71
+ }
72
+ if(receipt.ready){await new Promise(resolve=>setTimeout(resolve,1200));receipt.remainedRunning=!exited;receipt.ready=receipt.ready&&receipt.remainedRunning;}
73
+ } finally {
74
+ if(!exited){child.kill('SIGTERM');await Promise.race([ended,new Promise(resolve=>setTimeout(resolve,5000))]);}
75
+ if(!exited){try{process.kill(-child.pid,'SIGKILL');}catch{} await ended;}
76
+ receipt.exitCode=exitCode;receipt.exitSignal=exitSignal;
77
+ receipt.cleanedUp=exited;
78
+ receipt.stderrBytes=Buffer.byteLength(stderr);
79
+ receipt.meshDbCreated=fs.existsSync(path.join(project,'.hello-cc','mesh.db'));
80
+ try{await fetch(url,{signal:AbortSignal.timeout(500)});receipt.portClosed=false;}catch{receipt.portClosed=true;}
81
+ fs.writeFileSync(path.join(sandbox,'stdout.log'),stdout);fs.writeFileSync(path.join(sandbox,'stderr.log'),stderr);fs.writeFileSync(path.join(sandbox,'page.html'),html);fs.writeFileSync(path.join(sandbox,'receipt.json'),JSON.stringify(receipt,null,2)+'\n');
82
+ console.log(JSON.stringify(receipt,null,2));
83
+ if(!receipt.ready||!receipt.portClosed){
84
+ process.exitCode=1;
85
+ console.error(stderr.slice(-10000)||'Official dsh Web readiness or cleanup did not pass; see the saved receipt and logs.');
86
+ }
87
+ }
@@ -5,6 +5,7 @@ import path from 'node:path';
5
5
  import process from 'node:process';
6
6
  import { packageRoot, readPackageJson } from '../lib/release/package-meta.mjs';
7
7
  import {
8
+ isPrereleaseVersion,
8
9
  normalizeVersion,
9
10
  releaseSection,
10
11
  repoFromPackage,
@@ -106,7 +107,7 @@ async function main() {
106
107
  name: tag,
107
108
  body,
108
109
  draft: false,
109
- prerelease: false
110
+ prerelease: isPrereleaseVersion(version)
110
111
  };
111
112
 
112
113
  if (opts.dryRun) {
@@ -116,6 +117,7 @@ async function main() {
116
117
  repo,
117
118
  tag,
118
119
  name: payload.name,
120
+ prerelease: payload.prerelease,
119
121
  body_length: body.length,
120
122
  body_preview: body.split(/\r?\n/).slice(0, 8).join('\n')
121
123
  }, null, 2));