@postplus/cli 0.1.31 → 0.1.33
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/README.md +3 -2
- package/build/index.js +1 -1
- package/build/skill-management.js +4 -0
- package/build/studio-server.js +236 -0
- package/build/studio.js +140 -55
- package/build/update-check.js +6 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -63,8 +63,9 @@ postplus studio status
|
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
Studio creates a visible `PostPlus Studio/` folder in the current working
|
|
66
|
-
directory
|
|
67
|
-
|
|
66
|
+
directory and opens the bundled local dashboard from the public CLI package.
|
|
67
|
+
Assets, workflow files, activity, and provenance live inside that folder; hidden
|
|
68
|
+
runtime cache and logs stay under `PostPlus Studio/.postplus/`.
|
|
68
69
|
|
|
69
70
|
## The Vision
|
|
70
71
|
|
package/build/index.js
CHANGED
|
@@ -43,7 +43,7 @@ Usage:
|
|
|
43
43
|
postplus doctor [--skill <skill-id>] [--json]
|
|
44
44
|
postplus quote confirm --json --challenge-file <path>
|
|
45
45
|
postplus skills verify [--json]
|
|
46
|
-
postplus studio init|open|status
|
|
46
|
+
postplus studio init|open|status Open bundled Local Studio
|
|
47
47
|
postplus update [--current-directory]
|
|
48
48
|
postplus uninstall [--current-directory]
|
|
49
49
|
postplus list [--json]
|
|
@@ -2,6 +2,7 @@ import { writeCurrentCliVersionToLocalConfig } from './client-compatibility.js';
|
|
|
2
2
|
import { runCommand, runInteractiveCommand } from './command-runner.js';
|
|
3
3
|
import { clearManagedSkillBaseline, readManagedSkillBaseline, writeManagedSkillBaseline, } from './local-state.js';
|
|
4
4
|
import { POSTPLUS_SKILLS_AGENT_TARGETS, formatPostPlusSkillsInstallCommand, resolvePostPlusSkillsSource, loadPublicSkillCatalog, } from './skill-catalog.js';
|
|
5
|
+
import { clearUpdateCheckCache } from './update-check.js';
|
|
5
6
|
const NPX_SKILLS = ['-y', 'skills'];
|
|
6
7
|
const DEFAULT_SKILL_MUTATION_OPTIONS = {
|
|
7
8
|
scope: 'global',
|
|
@@ -31,6 +32,7 @@ export async function runPostPlusSkillUpdate(dependencies = {
|
|
|
31
32
|
skillNames,
|
|
32
33
|
});
|
|
33
34
|
await writeCurrentCliVersionToLocalConfig();
|
|
35
|
+
await clearUpdateCheckCache();
|
|
34
36
|
return 0;
|
|
35
37
|
}
|
|
36
38
|
export async function runPostPlusSkillUninstall(dependencies = {
|
|
@@ -46,6 +48,7 @@ export async function runPostPlusSkillUninstall(dependencies = {
|
|
|
46
48
|
const exitCode = await dependencies.runInteractiveCommand('npx', buildPostPlusSkillUninstallArgs(allKnownSkillNames, options.scope));
|
|
47
49
|
if (exitCode === 0) {
|
|
48
50
|
await clearManagedSkillBaseline();
|
|
51
|
+
await clearUpdateCheckCache();
|
|
49
52
|
}
|
|
50
53
|
return exitCode;
|
|
51
54
|
}
|
|
@@ -72,6 +75,7 @@ export async function runPostPlusSkillVerify(dependencies = {
|
|
|
72
75
|
skillNames: inspection.requiredSkillNames,
|
|
73
76
|
});
|
|
74
77
|
await writeCurrentCliVersionToLocalConfig();
|
|
78
|
+
await clearUpdateCheckCache();
|
|
75
79
|
return {
|
|
76
80
|
...inspection.report,
|
|
77
81
|
baselineUpdated: true,
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { basename, join, resolve } from 'node:path';
|
|
6
|
+
export async function startStudioServer(argv = process.argv.slice(2)) {
|
|
7
|
+
const options = parseOptions(argv);
|
|
8
|
+
const server = http.createServer(async (request, response) => {
|
|
9
|
+
try {
|
|
10
|
+
await handleRequest(request, response, options);
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
sendJson(response, 500, {
|
|
14
|
+
error: error instanceof Error ? error.message : String(error),
|
|
15
|
+
ok: false,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
20
|
+
server.once('error', rejectListen);
|
|
21
|
+
server.listen(options.port, options.host, () => {
|
|
22
|
+
server.off('error', rejectListen);
|
|
23
|
+
resolveListen();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
process.on('SIGTERM', () => {
|
|
27
|
+
server.close(() => process.exit(0));
|
|
28
|
+
});
|
|
29
|
+
process.on('SIGINT', () => {
|
|
30
|
+
server.close(() => process.exit(0));
|
|
31
|
+
});
|
|
32
|
+
return server;
|
|
33
|
+
}
|
|
34
|
+
function parseOptions(argv) {
|
|
35
|
+
const options = {};
|
|
36
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
37
|
+
const arg = argv[index];
|
|
38
|
+
if (arg === '--studio-root') {
|
|
39
|
+
const value = readOptionValue(argv, index, arg);
|
|
40
|
+
options.studioRoot = resolve(value);
|
|
41
|
+
index += 1;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (arg === '--host') {
|
|
45
|
+
options.host = readOptionValue(argv, index, arg);
|
|
46
|
+
index += 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (arg === '--port') {
|
|
50
|
+
const value = Number(readOptionValue(argv, index, arg));
|
|
51
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
52
|
+
throw new Error('--port must be a positive integer.');
|
|
53
|
+
}
|
|
54
|
+
options.port = value;
|
|
55
|
+
index += 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (arg === '--help' || arg === '-h') {
|
|
59
|
+
process.stdout.write(`Usage:
|
|
60
|
+
node build/studio-server.js --studio-root <dir> --host 127.0.0.1 --port 3978
|
|
61
|
+
`);
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
throw new Error(`Unknown Studio server option: ${arg}`);
|
|
65
|
+
}
|
|
66
|
+
if (!options.studioRoot) {
|
|
67
|
+
throw new Error('Studio server requires --studio-root.');
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
host: options.host ?? '127.0.0.1',
|
|
71
|
+
port: options.port ?? 3978,
|
|
72
|
+
studioRoot: options.studioRoot,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function readOptionValue(argv, index, name) {
|
|
76
|
+
const value = argv[index + 1];
|
|
77
|
+
if (!value || value.startsWith('--')) {
|
|
78
|
+
throw new Error(`Missing value for ${name}.`);
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
async function handleRequest(request, response, options) {
|
|
83
|
+
const url = new URL(request.url ?? '/', `http://${options.host}`);
|
|
84
|
+
if (request.method !== 'GET') {
|
|
85
|
+
sendJson(response, 405, { error: 'Method not allowed.', ok: false });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (url.pathname === '/api/health') {
|
|
89
|
+
sendJson(response, 200, { ok: true });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (url.pathname === '/api/project') {
|
|
93
|
+
sendJson(response, 200, await readStudioSnapshot(options.studioRoot));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (url.pathname === '/' ||
|
|
97
|
+
url.pathname === '/dashboard' ||
|
|
98
|
+
url.pathname === '/dashboard/') {
|
|
99
|
+
sendHtml(response, renderDashboardHtml());
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
sendJson(response, 404, { error: 'Not found.', ok: false });
|
|
103
|
+
}
|
|
104
|
+
async function readStudioSnapshot(studioRoot) {
|
|
105
|
+
return {
|
|
106
|
+
activity: await readJsonLines(join(studioRoot, 'activity.jsonl')),
|
|
107
|
+
manifest: await readJsonFile(join(studioRoot, 'manifest.json')),
|
|
108
|
+
pipeline: await readJsonFile(join(studioRoot, 'pipeline.json')),
|
|
109
|
+
project: await readJsonFile(join(studioRoot, 'project.json')),
|
|
110
|
+
provenance: await readJsonLines(join(studioRoot, 'provenance.jsonl')),
|
|
111
|
+
studio: await readJsonFile(join(studioRoot, 'studio.json')),
|
|
112
|
+
studioRoot,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
async function readJsonFile(path) {
|
|
116
|
+
if (!existsSync(path)) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
120
|
+
}
|
|
121
|
+
async function readJsonLines(path) {
|
|
122
|
+
if (!existsSync(path)) {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
const lines = (await readFile(path, 'utf8'))
|
|
126
|
+
.split('\n')
|
|
127
|
+
.map((line) => line.trim())
|
|
128
|
+
.filter(Boolean);
|
|
129
|
+
return lines.slice(-50).map((line) => JSON.parse(line));
|
|
130
|
+
}
|
|
131
|
+
function sendJson(response, statusCode, payload) {
|
|
132
|
+
response.writeHead(statusCode, {
|
|
133
|
+
'cache-control': 'no-store',
|
|
134
|
+
'content-type': 'application/json; charset=utf-8',
|
|
135
|
+
});
|
|
136
|
+
response.end(`${JSON.stringify(payload, null, 2)}\n`);
|
|
137
|
+
}
|
|
138
|
+
function sendHtml(response, html) {
|
|
139
|
+
response.writeHead(200, {
|
|
140
|
+
'cache-control': 'no-store',
|
|
141
|
+
'content-type': 'text/html; charset=utf-8',
|
|
142
|
+
});
|
|
143
|
+
response.end(html);
|
|
144
|
+
}
|
|
145
|
+
function renderDashboardHtml() {
|
|
146
|
+
return `<!doctype html>
|
|
147
|
+
<html lang="en">
|
|
148
|
+
<head>
|
|
149
|
+
<meta charset="utf-8" />
|
|
150
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
151
|
+
<title>PostPlus Studio</title>
|
|
152
|
+
<style>
|
|
153
|
+
:root { color-scheme: light; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
154
|
+
body { margin: 0; background: #f7f7f4; color: #1f2933; }
|
|
155
|
+
header { border-bottom: 1px solid #d6d8dc; background: #ffffff; padding: 20px 28px; }
|
|
156
|
+
main { display: grid; gap: 16px; grid-template-columns: 280px minmax(0, 1fr); padding: 20px 28px 28px; }
|
|
157
|
+
h1 { font-size: 22px; line-height: 1.2; margin: 0 0 6px; }
|
|
158
|
+
h2 { font-size: 13px; letter-spacing: 0; line-height: 1.25; margin: 0 0 10px; text-transform: uppercase; color: #5b6472; }
|
|
159
|
+
p { margin: 0; }
|
|
160
|
+
.subtle { color: #627083; font-size: 13px; }
|
|
161
|
+
.panel { background: #ffffff; border: 1px solid #d8dce2; border-radius: 8px; padding: 14px; min-width: 0; }
|
|
162
|
+
.stack { display: grid; gap: 12px; }
|
|
163
|
+
.grid { display: grid; gap: 12px; grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
164
|
+
.row { align-items: center; display: flex; justify-content: space-between; gap: 12px; border-top: 1px solid #edf0f2; padding-top: 10px; margin-top: 10px; }
|
|
165
|
+
.label { color: #5b6472; font-size: 12px; }
|
|
166
|
+
.value { font-size: 13px; font-weight: 600; overflow-wrap: anywhere; }
|
|
167
|
+
.step { border: 1px solid #dfe3e8; border-radius: 6px; padding: 10px; background: #fbfbfa; }
|
|
168
|
+
.step-title { font-size: 13px; font-weight: 700; }
|
|
169
|
+
.status { color: #0f766e; font-size: 12px; margin-top: 4px; }
|
|
170
|
+
pre { background: #111827; border-radius: 8px; color: #e5e7eb; font-size: 12px; line-height: 1.45; margin: 0; max-height: 420px; overflow: auto; padding: 12px; white-space: pre-wrap; }
|
|
171
|
+
@media (max-width: 840px) { main { grid-template-columns: 1fr; padding: 16px; } header { padding: 18px 16px; } .grid { grid-template-columns: 1fr; } }
|
|
172
|
+
</style>
|
|
173
|
+
</head>
|
|
174
|
+
<body>
|
|
175
|
+
<header>
|
|
176
|
+
<h1>PostPlus Studio</h1>
|
|
177
|
+
<p class="subtle" id="studio-root">Loading workspace...</p>
|
|
178
|
+
</header>
|
|
179
|
+
<main>
|
|
180
|
+
<section class="stack">
|
|
181
|
+
<div class="panel">
|
|
182
|
+
<h2>Project</h2>
|
|
183
|
+
<div class="row"><span class="label">Name</span><span class="value" id="project-name">-</span></div>
|
|
184
|
+
<div class="row"><span class="label">Status</span><span class="value" id="project-status">-</span></div>
|
|
185
|
+
</div>
|
|
186
|
+
<div class="panel">
|
|
187
|
+
<h2>Pipeline</h2>
|
|
188
|
+
<div id="pipeline-steps" class="stack"></div>
|
|
189
|
+
</div>
|
|
190
|
+
</section>
|
|
191
|
+
<section class="stack">
|
|
192
|
+
<div class="grid">
|
|
193
|
+
<div class="panel"><h2>Assets</h2><p class="value" id="asset-count">-</p></div>
|
|
194
|
+
<div class="panel"><h2>Activity</h2><p class="value" id="activity-count">-</p></div>
|
|
195
|
+
<div class="panel"><h2>Provenance</h2><p class="value" id="provenance-count">-</p></div>
|
|
196
|
+
</div>
|
|
197
|
+
<div class="panel">
|
|
198
|
+
<h2>Workspace JSON</h2>
|
|
199
|
+
<pre id="snapshot">{}</pre>
|
|
200
|
+
</div>
|
|
201
|
+
</section>
|
|
202
|
+
</main>
|
|
203
|
+
<script>
|
|
204
|
+
const text = (id, value) => { document.getElementById(id).textContent = value ?? '-'; };
|
|
205
|
+
const render = async () => {
|
|
206
|
+
const response = await fetch('/api/project');
|
|
207
|
+
const data = await response.json();
|
|
208
|
+
const project = data.project || {};
|
|
209
|
+
const pipeline = data.pipeline || {};
|
|
210
|
+
const manifest = data.manifest || {};
|
|
211
|
+
text('studio-root', data.studioRoot || '');
|
|
212
|
+
text('project-name', project.name || project.project_id || 'PostPlus Studio');
|
|
213
|
+
text('project-status', project.status || 'active');
|
|
214
|
+
text('asset-count', Array.isArray(manifest.assets) ? String(manifest.assets.length) : '0');
|
|
215
|
+
text('activity-count', Array.isArray(data.activity) ? String(data.activity.length) : '0');
|
|
216
|
+
text('provenance-count', Array.isArray(data.provenance) ? String(data.provenance.length) : '0');
|
|
217
|
+
const steps = Array.isArray(pipeline.steps) ? pipeline.steps : [];
|
|
218
|
+
document.getElementById('pipeline-steps').innerHTML = steps.map((step) =>
|
|
219
|
+
'<div class="step"><div class="step-title">' + escapeHtml(step.name || step.id || 'Step') + '</div><div class="status">' + escapeHtml(step.status || 'pending') + '</div></div>'
|
|
220
|
+
).join('') || '<p class="subtle">No pipeline steps yet.</p>';
|
|
221
|
+
document.getElementById('snapshot').textContent = JSON.stringify(data, null, 2);
|
|
222
|
+
};
|
|
223
|
+
const escapeHtml = (value) => String(value).replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char]));
|
|
224
|
+
render().catch((error) => {
|
|
225
|
+
document.getElementById('snapshot').textContent = error.stack || error.message || String(error);
|
|
226
|
+
});
|
|
227
|
+
</script>
|
|
228
|
+
</body>
|
|
229
|
+
</html>`;
|
|
230
|
+
}
|
|
231
|
+
if (process.argv[1] && basename(process.argv[1])?.startsWith('studio-server')) {
|
|
232
|
+
startStudioServer().catch((error) => {
|
|
233
|
+
process.stderr.write(`${error instanceof Error ? error.stack || error.message : String(error)}\n`);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
});
|
|
236
|
+
}
|
package/build/studio.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { spawn
|
|
2
|
-
import { access, mkdir, writeFile, } from 'node:fs/promises';
|
|
3
|
-
import { constants as fsConstants } from 'node:fs';
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { access, mkdir, readFile, writeFile, } from 'node:fs/promises';
|
|
3
|
+
import { closeSync, constants as fsConstants, openSync } from 'node:fs';
|
|
4
|
+
import net from 'node:net';
|
|
4
5
|
import { platform } from 'node:os';
|
|
5
6
|
import { basename, dirname, join, resolve, } from 'node:path';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
@@ -12,6 +13,10 @@ export async function runStudioCommand(args) {
|
|
|
12
13
|
printStudioHelp();
|
|
13
14
|
return 0;
|
|
14
15
|
}
|
|
16
|
+
if (rest.some((arg) => ['help', '--help', '-h'].includes(arg))) {
|
|
17
|
+
printStudioHelp();
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
15
20
|
const options = parseStudioOptions(rest);
|
|
16
21
|
switch (subcommand) {
|
|
17
22
|
case 'init': {
|
|
@@ -43,7 +48,8 @@ Usage:
|
|
|
43
48
|
postplus studio open [--workdir <dir>] [--port 3978] [--no-browser] [--json]
|
|
44
49
|
postplus studio status [--workdir <dir>] [--json]
|
|
45
50
|
|
|
46
|
-
Studio
|
|
51
|
+
Local Studio is a public local workspace included in the PostPlus CLI package.
|
|
52
|
+
Studio creates a visible "PostPlus Studio" folder inside the selected working directory and opens the bundled local dashboard.
|
|
47
53
|
`);
|
|
48
54
|
}
|
|
49
55
|
function parseStudioOptions(args) {
|
|
@@ -192,76 +198,155 @@ async function getStudioStatus(workdir) {
|
|
|
192
198
|
}
|
|
193
199
|
async function openStudio(options) {
|
|
194
200
|
const { studioRoot } = await initializeStudio(options.workdir);
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
201
|
+
const parsed = await launchBundledStudioServer(studioRoot, options.port);
|
|
202
|
+
if (options.browser) {
|
|
203
|
+
openSystemBrowser(parsed.url);
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
ok: true,
|
|
207
|
+
studioRoot,
|
|
208
|
+
...parsed,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
async function launchBundledStudioServer(studioRoot, startPort) {
|
|
212
|
+
const existing = await readLiveStudioServerState(studioRoot);
|
|
213
|
+
if (existing) {
|
|
214
|
+
return {
|
|
215
|
+
logPath: existing.logPath,
|
|
216
|
+
pid: existing.pid,
|
|
217
|
+
reused: true,
|
|
218
|
+
url: existing.dashboardUrl,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const host = '127.0.0.1';
|
|
222
|
+
const port = await findAvailablePort(startPort, host);
|
|
223
|
+
const baseUrl = `http://${host}:${port}`;
|
|
224
|
+
const dashboardUrl = `${baseUrl}/dashboard/`;
|
|
225
|
+
const logDir = join(studioRoot, '.postplus', 'logs');
|
|
226
|
+
await mkdir(logDir, { recursive: true });
|
|
227
|
+
const logPath = join(logDir, 'studio-server.log');
|
|
228
|
+
const logFd = openSync(logPath, 'a');
|
|
229
|
+
const serverEntrypoint = resolveBundledStudioServerEntrypoint();
|
|
230
|
+
const packageRoot = resolveCliPackageRoot();
|
|
231
|
+
const child = spawn(process.execPath, [
|
|
232
|
+
...buildNodeLoaderArgs(serverEntrypoint),
|
|
233
|
+
serverEntrypoint,
|
|
199
234
|
'--studio-root',
|
|
200
235
|
studioRoot,
|
|
201
236
|
'--host',
|
|
202
|
-
|
|
237
|
+
host,
|
|
203
238
|
'--port',
|
|
204
|
-
String(
|
|
205
|
-
'--skip-build',
|
|
239
|
+
String(port),
|
|
206
240
|
], {
|
|
207
|
-
cwd:
|
|
208
|
-
|
|
241
|
+
cwd: packageRoot,
|
|
242
|
+
detached: true,
|
|
243
|
+
stdio: ['ignore', logFd, logFd],
|
|
209
244
|
});
|
|
210
|
-
|
|
211
|
-
|
|
245
|
+
child.unref();
|
|
246
|
+
closeSync(logFd);
|
|
247
|
+
try {
|
|
248
|
+
await waitForStudioServer(baseUrl, logPath);
|
|
212
249
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
250
|
+
catch (error) {
|
|
251
|
+
if (child.pid) {
|
|
252
|
+
try {
|
|
253
|
+
process.kill(child.pid);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
// The process already exited; the readiness error below carries the failure.
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
throw error;
|
|
216
260
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
261
|
+
const state = {
|
|
262
|
+
baseUrl,
|
|
263
|
+
dashboardUrl,
|
|
264
|
+
logPath,
|
|
265
|
+
pid: child.pid,
|
|
266
|
+
startedAt: new Date().toISOString(),
|
|
220
267
|
studioRoot,
|
|
221
|
-
...parsed,
|
|
222
268
|
};
|
|
269
|
+
await writeJson(getStudioServerStatePath(studioRoot), state);
|
|
270
|
+
return {
|
|
271
|
+
logPath,
|
|
272
|
+
pid: child.pid,
|
|
273
|
+
reused: false,
|
|
274
|
+
url: dashboardUrl,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function resolveBundledStudioServerEntrypoint() {
|
|
278
|
+
const currentModulePath = fileURLToPath(import.meta.url);
|
|
279
|
+
const extension = currentModulePath.endsWith('.ts') ? '.ts' : '.js';
|
|
280
|
+
return join(dirname(currentModulePath), `studio-server${extension}`);
|
|
281
|
+
}
|
|
282
|
+
function resolveCliPackageRoot() {
|
|
283
|
+
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
284
|
+
}
|
|
285
|
+
function buildNodeLoaderArgs(entrypoint) {
|
|
286
|
+
return entrypoint.endsWith('.ts') ? ['--import', 'tsx'] : [];
|
|
223
287
|
}
|
|
224
|
-
async function
|
|
225
|
-
const
|
|
226
|
-
if (
|
|
227
|
-
return
|
|
288
|
+
async function readLiveStudioServerState(studioRoot) {
|
|
289
|
+
const state = await readJsonIfExists(getStudioServerStatePath(studioRoot));
|
|
290
|
+
if (!state?.baseUrl || !state.dashboardUrl) {
|
|
291
|
+
return null;
|
|
228
292
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
293
|
+
if (await canFetchStudioServer(state.baseUrl)) {
|
|
294
|
+
return state;
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
function getStudioServerStatePath(studioRoot) {
|
|
299
|
+
return join(studioRoot, '.postplus', 'studio-server.json');
|
|
300
|
+
}
|
|
301
|
+
async function readJsonIfExists(path) {
|
|
302
|
+
if (!(await pathExists(path))) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async function waitForStudioServer(baseUrl, logPath) {
|
|
313
|
+
const startedAt = Date.now();
|
|
314
|
+
while (Date.now() - startedAt < 5000) {
|
|
315
|
+
if (await canFetchStudioServer(baseUrl)) {
|
|
316
|
+
return;
|
|
244
317
|
}
|
|
318
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, 150));
|
|
245
319
|
}
|
|
246
|
-
throw new Error(
|
|
320
|
+
throw new Error(`Studio server did not become ready at ${baseUrl}. See log: ${logPath}`);
|
|
247
321
|
}
|
|
248
|
-
function
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
322
|
+
async function canFetchStudioServer(baseUrl) {
|
|
323
|
+
try {
|
|
324
|
+
const response = await fetch(`${baseUrl.replace(/\/$/u, '')}/api/health`, {
|
|
325
|
+
signal: AbortSignal.timeout(1200),
|
|
326
|
+
});
|
|
327
|
+
return response.ok;
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return false;
|
|
254
331
|
}
|
|
255
|
-
return dirs;
|
|
256
332
|
}
|
|
257
|
-
async function
|
|
258
|
-
|
|
259
|
-
|
|
333
|
+
async function findAvailablePort(startPort, host) {
|
|
334
|
+
for (let port = startPort; port < startPort + 50; port += 1) {
|
|
335
|
+
if (await isPortAvailable(port, host)) {
|
|
336
|
+
return port;
|
|
337
|
+
}
|
|
260
338
|
}
|
|
261
|
-
|
|
339
|
+
throw new Error(`No available Studio port found from ${startPort} to ${startPort + 49}.`);
|
|
262
340
|
}
|
|
263
|
-
|
|
264
|
-
return
|
|
341
|
+
function isPortAvailable(port, host) {
|
|
342
|
+
return new Promise((resolveAvailable) => {
|
|
343
|
+
const server = net.createServer();
|
|
344
|
+
server.once('error', () => resolveAvailable(false));
|
|
345
|
+
server.once('listening', () => {
|
|
346
|
+
server.close(() => resolveAvailable(true));
|
|
347
|
+
});
|
|
348
|
+
server.listen(port, host);
|
|
349
|
+
});
|
|
265
350
|
}
|
|
266
351
|
async function pathExists(path) {
|
|
267
352
|
try {
|
package/build/update-check.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { readCurrentCliVersion } from './client-compatibility.js';
|
|
4
4
|
import { runInteractiveCommand as runDefaultInteractiveCommand, } from './command-runner.js';
|
|
@@ -88,6 +88,11 @@ export async function refreshUpdateCheckCache() {
|
|
|
88
88
|
force: true,
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
|
+
export async function clearUpdateCheckCache() {
|
|
92
|
+
await rm(getUpdateCheckCachePath(), {
|
|
93
|
+
force: true,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
91
96
|
export async function runCliSelfUpdateIfOutdated(dependencies = {}) {
|
|
92
97
|
const fetchFn = dependencies.fetchFn ?? fetch;
|
|
93
98
|
const runInteractiveCommand = dependencies.runInteractiveCommand ?? runDefaultInteractiveCommand;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@postplus/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
4
4
|
"packageManager": "pnpm@10.30.3+sha512.c961d1e0a2d8e354ecaa5166b822516668b7f44cb5bd95122d590dd81922f606f5473b6d23ec4a5be05e7fcd18e8488d47d978bbe981872f1145d06e9a740017",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "PostPlus CLI for PostPlus Cloud auth, status, and diagnostics.",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"build/skill-management.js",
|
|
24
24
|
"build/status.js",
|
|
25
25
|
"build/studio.js",
|
|
26
|
+
"build/studio-server.js",
|
|
26
27
|
"build/subscription-status.js",
|
|
27
28
|
"build/update-check.js",
|
|
28
29
|
"LICENSE",
|