@postplus/cli 0.1.30 → 0.1.32
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 +25 -2
- package/build/index.js +46 -11
- package/build/skill-catalog.js +6 -2
- package/build/skill-management.js +27 -17
- package/build/studio-server.js +236 -0
- package/build/studio.js +407 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -33,17 +33,40 @@ Requires Node.js and npm.
|
|
|
33
33
|
```bash
|
|
34
34
|
npm install -g @postplus/cli@latest
|
|
35
35
|
postplus auth login
|
|
36
|
-
npx -y skills add PostPlusAI/postplus-skills --global --full-depth --skill '*' --agent claude-code codex cursor github-copilot windsurf trae trae-cn --yes
|
|
36
|
+
npx -y skills add PostPlusAI/postplus-skills --global --full-depth --skill '*' --agent claude-code codex cursor github-copilot windsurf trae trae-cn openclaw hermes-agent --yes
|
|
37
37
|
postplus skills verify
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
If you explicitly do not want global skills, run the install from the target
|
|
41
|
+
project directory and omit `--global`:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx -y skills add PostPlusAI/postplus-skills --full-depth --skill '*' --agent claude-code codex cursor github-copilot windsurf trae trae-cn openclaw hermes-agent --yes
|
|
45
|
+
```
|
|
46
|
+
|
|
40
47
|
Useful checks:
|
|
41
48
|
|
|
42
49
|
```bash
|
|
43
50
|
postplus status
|
|
44
|
-
npx -y skills add PostPlusAI/postplus-skills --
|
|
51
|
+
npx -y skills add PostPlusAI/postplus-skills --global --list
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Local Studio
|
|
55
|
+
|
|
56
|
+
For heavier skills that benefit from a visual workspace, use the CLI-managed
|
|
57
|
+
local Studio:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
postplus studio init
|
|
61
|
+
postplus studio open
|
|
62
|
+
postplus studio status
|
|
45
63
|
```
|
|
46
64
|
|
|
65
|
+
Studio creates a visible `PostPlus Studio/` folder in the current working
|
|
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/`.
|
|
69
|
+
|
|
47
70
|
## The Vision
|
|
48
71
|
|
|
49
72
|
PostPlus is built for a world where one marketer, founder, operator, or agency strategist can work with an AI agent as if they had a larger marketing team around them.
|
package/build/index.js
CHANGED
|
@@ -8,10 +8,11 @@ import { readCurrentCliVersion } from './client-compatibility.js';
|
|
|
8
8
|
import { formatDoctorReport, generateDoctorReport } from './doctor.js';
|
|
9
9
|
import { assertConfigFilePermissions } from './local-state.js';
|
|
10
10
|
import { readLargeCreditQuoteConfirmationChallenge, resolveLargeCreditQuoteConfirmation, } from './quote-confirmation.js';
|
|
11
|
-
import { POSTPLUS_SKILLS_INSTALL_COMMAND, loadPublicSkillCatalog, } from './skill-catalog.js';
|
|
11
|
+
import { POSTPLUS_SKILLS_CURRENT_DIRECTORY_INSTALL_COMMAND, POSTPLUS_SKILLS_INSTALL_COMMAND, loadPublicSkillCatalog, formatPostPlusSkillsInstallCommand, } from './skill-catalog.js';
|
|
12
12
|
import { formatSkillBaselineVerifyReport, runPostPlusSkillUninstall, runPostPlusSkillUpdate, runPostPlusSkillVerify, } from './skill-management.js';
|
|
13
13
|
import { formatStatusReport, generateStatusReport } from './status.js';
|
|
14
14
|
import { refreshUpdateCheckCache, runCliSelfUpdateIfOutdated, } from './update-check.js';
|
|
15
|
+
import { runStudioCommand } from './studio.js';
|
|
15
16
|
function printAuthHelp() {
|
|
16
17
|
process.stdout.write(`PostPlus CLI — auth commands
|
|
17
18
|
|
|
@@ -42,15 +43,19 @@ Usage:
|
|
|
42
43
|
postplus doctor [--skill <skill-id>] [--json]
|
|
43
44
|
postplus quote confirm --json --challenge-file <path>
|
|
44
45
|
postplus skills verify [--json]
|
|
45
|
-
postplus
|
|
46
|
-
postplus
|
|
46
|
+
postplus studio init|open|status Open bundled Local Studio
|
|
47
|
+
postplus update [--current-directory]
|
|
48
|
+
postplus uninstall [--current-directory]
|
|
47
49
|
postplus list [--json]
|
|
48
50
|
postplus status [--skill <skill-id>] [--json]
|
|
49
51
|
postplus version
|
|
50
52
|
postplus help
|
|
51
53
|
|
|
52
54
|
Skills:
|
|
53
|
-
|
|
55
|
+
Global:
|
|
56
|
+
${POSTPLUS_SKILLS_INSTALL_COMMAND}
|
|
57
|
+
Current directory:
|
|
58
|
+
${POSTPLUS_SKILLS_CURRENT_DIRECTORY_INSTALL_COMMAND}
|
|
54
59
|
|
|
55
60
|
After first install, run:
|
|
56
61
|
postplus skills verify
|
|
@@ -96,7 +101,8 @@ async function runList(json) {
|
|
|
96
101
|
'PostPlus skills',
|
|
97
102
|
'',
|
|
98
103
|
`Source: ${catalog.source}`,
|
|
99
|
-
`Install: ${catalog.installCommand}`,
|
|
104
|
+
`Install (global): ${catalog.installCommand}`,
|
|
105
|
+
`Install (current directory): ${formatPostPlusSkillsInstallCommand(catalog.source, 'current-directory')}`,
|
|
100
106
|
'',
|
|
101
107
|
];
|
|
102
108
|
for (const entry of catalog.skills) {
|
|
@@ -109,19 +115,25 @@ async function runVersion() {
|
|
|
109
115
|
process.stdout.write(`${await readCurrentCliVersion()}\n`);
|
|
110
116
|
return 0;
|
|
111
117
|
}
|
|
112
|
-
async function runSkillUpdateCommand() {
|
|
118
|
+
async function runSkillUpdateCommand(rest) {
|
|
119
|
+
const options = parseSkillMutationOptions(rest, 'update');
|
|
113
120
|
const cliSelfUpdate = await runCliSelfUpdateIfOutdated();
|
|
114
121
|
if (cliSelfUpdate.updateAvailable) {
|
|
115
122
|
return cliSelfUpdate.exitCode ?? 1;
|
|
116
123
|
}
|
|
117
|
-
const exitCode = await runPostPlusSkillUpdate(
|
|
124
|
+
const exitCode = await runPostPlusSkillUpdate(undefined, {
|
|
125
|
+
scope: options.scope,
|
|
126
|
+
});
|
|
118
127
|
if (exitCode === 0) {
|
|
119
128
|
await refreshUpdateCheckCache().catch(() => { });
|
|
120
129
|
}
|
|
121
130
|
return exitCode;
|
|
122
131
|
}
|
|
123
|
-
async function runSkillUninstallCommand() {
|
|
124
|
-
|
|
132
|
+
async function runSkillUninstallCommand(rest) {
|
|
133
|
+
const options = parseSkillMutationOptions(rest, 'uninstall');
|
|
134
|
+
return runPostPlusSkillUninstall(undefined, {
|
|
135
|
+
scope: options.scope,
|
|
136
|
+
});
|
|
125
137
|
}
|
|
126
138
|
async function runSkillsCommand(rest) {
|
|
127
139
|
const [subcommand] = rest;
|
|
@@ -153,6 +165,12 @@ Usage:
|
|
|
153
165
|
|
|
154
166
|
Options:
|
|
155
167
|
--json Output results as JSON
|
|
168
|
+
|
|
169
|
+
Install scope:
|
|
170
|
+
postplus update Update global PostPlus skills
|
|
171
|
+
postplus update --current-directory Update PostPlus skills in the current directory
|
|
172
|
+
postplus uninstall Remove global PostPlus skills
|
|
173
|
+
postplus uninstall --current-directory Remove PostPlus skills from the current directory
|
|
156
174
|
`);
|
|
157
175
|
return 0;
|
|
158
176
|
default:
|
|
@@ -233,6 +251,17 @@ function parseDiagnosticOptions(args) {
|
|
|
233
251
|
}
|
|
234
252
|
return options;
|
|
235
253
|
}
|
|
254
|
+
function parseSkillMutationOptions(args, commandName) {
|
|
255
|
+
let scope = 'global';
|
|
256
|
+
for (const arg of args) {
|
|
257
|
+
if (arg === '--current-directory') {
|
|
258
|
+
scope = 'current-directory';
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
throw new Error(`Unknown option for ${commandName}: ${arg}`);
|
|
262
|
+
}
|
|
263
|
+
return { scope };
|
|
264
|
+
}
|
|
236
265
|
async function runAuthLogout(json) {
|
|
237
266
|
const report = await clearAuthState();
|
|
238
267
|
if (json) {
|
|
@@ -309,6 +338,9 @@ async function main() {
|
|
|
309
338
|
else if (helpTopic === 'skills') {
|
|
310
339
|
await runSkillsCommand(['help']);
|
|
311
340
|
}
|
|
341
|
+
else if (helpTopic === 'studio') {
|
|
342
|
+
await runStudioCommand(['help']);
|
|
343
|
+
}
|
|
312
344
|
else {
|
|
313
345
|
printHelp();
|
|
314
346
|
}
|
|
@@ -324,15 +356,18 @@ async function main() {
|
|
|
324
356
|
case 'skills':
|
|
325
357
|
process.exitCode = await runSkillsCommand(rest);
|
|
326
358
|
return;
|
|
359
|
+
case 'studio':
|
|
360
|
+
process.exitCode = await runStudioCommand(rest);
|
|
361
|
+
return;
|
|
327
362
|
case 'install':
|
|
328
363
|
process.stderr.write(`PostPlus CLI does not install skills directly. Run \`${POSTPLUS_SKILLS_INSTALL_COMMAND}\`.\n`);
|
|
329
364
|
process.exitCode = 1;
|
|
330
365
|
return;
|
|
331
366
|
case 'update':
|
|
332
|
-
process.exitCode = await runSkillUpdateCommand();
|
|
367
|
+
process.exitCode = await runSkillUpdateCommand(rest);
|
|
333
368
|
return;
|
|
334
369
|
case 'uninstall':
|
|
335
|
-
process.exitCode = await runSkillUninstallCommand();
|
|
370
|
+
process.exitCode = await runSkillUninstallCommand(rest);
|
|
336
371
|
return;
|
|
337
372
|
case 'list':
|
|
338
373
|
process.exitCode = await runList(json);
|
package/build/skill-catalog.js
CHANGED
|
@@ -9,9 +9,12 @@ export const POSTPLUS_SKILLS_AGENT_TARGETS = [
|
|
|
9
9
|
'windsurf',
|
|
10
10
|
'trae',
|
|
11
11
|
'trae-cn',
|
|
12
|
+
'openclaw',
|
|
13
|
+
'hermes-agent',
|
|
12
14
|
];
|
|
13
15
|
const POSTPLUS_SKILLS_AGENT_ARGS = POSTPLUS_SKILLS_AGENT_TARGETS.join(' ');
|
|
14
16
|
export const POSTPLUS_SKILLS_INSTALL_COMMAND = formatPostPlusSkillsInstallCommand();
|
|
17
|
+
export const POSTPLUS_SKILLS_CURRENT_DIRECTORY_INSTALL_COMMAND = formatPostPlusSkillsInstallCommand(POSTPLUS_SKILLS_REPO, 'current-directory');
|
|
15
18
|
export const POSTPLUS_SKILLS_LIST_COMMAND = formatPostPlusSkillsListCommand();
|
|
16
19
|
const POSTPLUS_SKILLS_INDEX_URL = 'https://raw.githubusercontent.com/PostPlusAI/postplus-skills/main/skills/INDEX.md';
|
|
17
20
|
const POSTPLUS_SKILLS_CATALOG_URL = 'https://raw.githubusercontent.com/PostPlusAI/postplus-skills/main/skills/catalog.json';
|
|
@@ -54,8 +57,9 @@ export function resolvePostPlusSkillsSource(env = process.env) {
|
|
|
54
57
|
export function resolvePostPlusSkillsCatalogUrl(env = process.env) {
|
|
55
58
|
return env[POSTPLUS_SKILLS_CATALOG_URL_ENV]?.trim() || POSTPLUS_SKILLS_CATALOG_URL;
|
|
56
59
|
}
|
|
57
|
-
export function formatPostPlusSkillsInstallCommand(source = POSTPLUS_SKILLS_REPO) {
|
|
58
|
-
|
|
60
|
+
export function formatPostPlusSkillsInstallCommand(source = POSTPLUS_SKILLS_REPO, scope = 'global') {
|
|
61
|
+
const scopeArgs = scope === 'global' ? ' --global' : '';
|
|
62
|
+
return `npx -y skills add ${source}${scopeArgs} --full-depth --skill '*' --agent ${POSTPLUS_SKILLS_AGENT_ARGS} --yes`;
|
|
59
63
|
}
|
|
60
64
|
export function formatPostPlusSkillsListCommand(source = POSTPLUS_SKILLS_REPO) {
|
|
61
65
|
return `npx -y skills add ${source} --list --full-depth`;
|
|
@@ -3,9 +3,12 @@ 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
5
|
const NPX_SKILLS = ['-y', 'skills'];
|
|
6
|
+
const DEFAULT_SKILL_MUTATION_OPTIONS = {
|
|
7
|
+
scope: 'global',
|
|
8
|
+
};
|
|
6
9
|
export async function runPostPlusSkillUpdate(dependencies = {
|
|
7
10
|
runInteractiveCommand,
|
|
8
|
-
}) {
|
|
11
|
+
}, options = DEFAULT_SKILL_MUTATION_OPTIONS) {
|
|
9
12
|
const catalog = await loadPublicSkillCatalog();
|
|
10
13
|
const skillNames = catalog.skills.map((skill) => skill.skillId);
|
|
11
14
|
const baseline = await readManagedSkillBaseline();
|
|
@@ -13,12 +16,12 @@ export async function runPostPlusSkillUpdate(dependencies = {
|
|
|
13
16
|
if (skillNames.length === 0) {
|
|
14
17
|
throw new Error('PostPlus public skill catalog has no released skills.');
|
|
15
18
|
}
|
|
16
|
-
const updateExitCode = await dependencies.runInteractiveCommand('npx', buildPostPlusSkillUpdateArgs(skillNames));
|
|
19
|
+
const updateExitCode = await dependencies.runInteractiveCommand('npx', buildPostPlusSkillUpdateArgs(skillNames, options.scope));
|
|
17
20
|
if (updateExitCode !== 0) {
|
|
18
21
|
return updateExitCode;
|
|
19
22
|
}
|
|
20
23
|
if (retiredSkillNames.length > 0) {
|
|
21
|
-
const removeExitCode = await dependencies.runInteractiveCommand('npx', buildPostPlusSkillUninstallArgs(retiredSkillNames));
|
|
24
|
+
const removeExitCode = await dependencies.runInteractiveCommand('npx', buildPostPlusSkillUninstallArgs(retiredSkillNames, options.scope));
|
|
22
25
|
if (removeExitCode !== 0) {
|
|
23
26
|
return removeExitCode;
|
|
24
27
|
}
|
|
@@ -32,7 +35,7 @@ export async function runPostPlusSkillUpdate(dependencies = {
|
|
|
32
35
|
}
|
|
33
36
|
export async function runPostPlusSkillUninstall(dependencies = {
|
|
34
37
|
runInteractiveCommand,
|
|
35
|
-
}) {
|
|
38
|
+
}, options = DEFAULT_SKILL_MUTATION_OPTIONS) {
|
|
36
39
|
const catalog = await loadPublicSkillCatalog();
|
|
37
40
|
const skillNames = catalog.skills.map((skill) => skill.skillId);
|
|
38
41
|
const baseline = await readManagedSkillBaseline();
|
|
@@ -40,7 +43,7 @@ export async function runPostPlusSkillUninstall(dependencies = {
|
|
|
40
43
|
if (allKnownSkillNames.length === 0) {
|
|
41
44
|
throw new Error('PostPlus public skill catalog has no released skills.');
|
|
42
45
|
}
|
|
43
|
-
const exitCode = await dependencies.runInteractiveCommand('npx', buildPostPlusSkillUninstallArgs(allKnownSkillNames));
|
|
46
|
+
const exitCode = await dependencies.runInteractiveCommand('npx', buildPostPlusSkillUninstallArgs(allKnownSkillNames, options.scope));
|
|
44
47
|
if (exitCode === 0) {
|
|
45
48
|
await clearManagedSkillBaseline();
|
|
46
49
|
}
|
|
@@ -150,13 +153,13 @@ export function formatSkillInstallStatusReport(report) {
|
|
|
150
153
|
lines.push(` Managed baseline: ${report.managedSkillsReleaseId ?? 'none'}`);
|
|
151
154
|
lines.push(` Scope: ${report.scopes.length > 0 ? report.scopes.join(', ') : 'none detected'}`);
|
|
152
155
|
if (report.retiredManagedSkills.length > 0) {
|
|
153
|
-
lines.push(` Retired managed skills: ${formatSkillList(report.retiredManagedSkills, 8)}`, ` Cleanup: ${report.updateCommand}`);
|
|
156
|
+
lines.push(` Retired managed skills: ${formatSkillList(report.retiredManagedSkills, 8)}`, ` Cleanup (global): ${report.updateCommand}`, ` Cleanup (current directory): ${formatPostPlusSkillUpdateCommand('current-directory')}`);
|
|
154
157
|
}
|
|
155
158
|
if (report.missingSkills.length > 0) {
|
|
156
|
-
lines.push(` Missing: ${formatSkillList(report.missingSkills, 8)}`, ` Fix: ${report.installCommand}`);
|
|
159
|
+
lines.push(` Missing: ${formatSkillList(report.missingSkills, 8)}`, ` Fix (global): ${report.installCommand}`, ` Fix (current directory): ${formatPostPlusSkillsInstallCommand(report.source, 'current-directory')}`);
|
|
157
160
|
}
|
|
158
161
|
else {
|
|
159
|
-
lines.push(` Update: ${report.updateCommand}`);
|
|
162
|
+
lines.push(` Update (global): ${report.updateCommand}`, ` Update (current directory): ${formatPostPlusSkillUpdateCommand('current-directory')}`);
|
|
160
163
|
}
|
|
161
164
|
return lines.join('\n');
|
|
162
165
|
}
|
|
@@ -181,11 +184,11 @@ export function formatSkillBaselineVerifyReport(report) {
|
|
|
181
184
|
lines.push(' Verified baseline: unchanged');
|
|
182
185
|
}
|
|
183
186
|
if (report.missingSkills.length > 0) {
|
|
184
|
-
lines.push(` Missing: ${formatSkillList(report.missingSkills, 8)}`, ` Fix: ${report.installCommand}`);
|
|
187
|
+
lines.push(` Missing: ${formatSkillList(report.missingSkills, 8)}`, ` Fix (global): ${report.installCommand}`, ` Fix (current directory): ${formatPostPlusSkillsInstallCommand(report.source, 'current-directory')}`);
|
|
185
188
|
}
|
|
186
189
|
return lines.join('\n');
|
|
187
190
|
}
|
|
188
|
-
export function buildPostPlusSkillUpdateArgs(skillNames) {
|
|
191
|
+
export function buildPostPlusSkillUpdateArgs(skillNames, scope = 'global') {
|
|
189
192
|
if (skillNames.length === 0) {
|
|
190
193
|
throw new Error('PostPlus public skill catalog has no released skills.');
|
|
191
194
|
}
|
|
@@ -194,7 +197,7 @@ export function buildPostPlusSkillUpdateArgs(skillNames) {
|
|
|
194
197
|
...NPX_SKILLS,
|
|
195
198
|
'add',
|
|
196
199
|
skillsSource,
|
|
197
|
-
|
|
200
|
+
...buildSkillScopeArgs(scope),
|
|
198
201
|
'--full-depth',
|
|
199
202
|
'--skill',
|
|
200
203
|
'*',
|
|
@@ -203,22 +206,29 @@ export function buildPostPlusSkillUpdateArgs(skillNames) {
|
|
|
203
206
|
'--yes',
|
|
204
207
|
];
|
|
205
208
|
}
|
|
206
|
-
export function buildPostPlusSkillUninstallArgs(skillNames) {
|
|
209
|
+
export function buildPostPlusSkillUninstallArgs(skillNames, scope = 'global') {
|
|
207
210
|
return [
|
|
208
211
|
...NPX_SKILLS,
|
|
209
212
|
'remove',
|
|
210
213
|
...skillNames,
|
|
211
|
-
|
|
214
|
+
...buildSkillScopeArgs(scope),
|
|
212
215
|
'--agent',
|
|
213
216
|
...POSTPLUS_SKILLS_AGENT_TARGETS,
|
|
214
217
|
'--yes',
|
|
215
218
|
];
|
|
216
219
|
}
|
|
217
|
-
export function formatPostPlusSkillUpdateCommand() {
|
|
218
|
-
return
|
|
220
|
+
export function formatPostPlusSkillUpdateCommand(scope = 'global') {
|
|
221
|
+
return scope === 'global'
|
|
222
|
+
? 'postplus update'
|
|
223
|
+
: 'postplus update --current-directory';
|
|
224
|
+
}
|
|
225
|
+
export function formatPostPlusSkillUninstallCommand(scope = 'global') {
|
|
226
|
+
return scope === 'global'
|
|
227
|
+
? 'postplus uninstall'
|
|
228
|
+
: 'postplus uninstall --current-directory';
|
|
219
229
|
}
|
|
220
|
-
|
|
221
|
-
return '
|
|
230
|
+
function buildSkillScopeArgs(scope) {
|
|
231
|
+
return scope === 'global' ? ['--global'] : [];
|
|
222
232
|
}
|
|
223
233
|
function mergeSkillNames(left, right) {
|
|
224
234
|
return [...new Set([...left, ...right])].sort((a, b) => a.localeCompare(b));
|
|
@@ -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
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
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';
|
|
5
|
+
import { platform } from 'node:os';
|
|
6
|
+
import { basename, dirname, join, resolve, } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
export const POSTPLUS_STUDIO_DIRECTORY_NAME = 'PostPlus Studio';
|
|
9
|
+
const DEFAULT_STUDIO_ID = 'postplus-studio';
|
|
10
|
+
export async function runStudioCommand(args) {
|
|
11
|
+
const [subcommand, ...rest] = args;
|
|
12
|
+
if (!subcommand || ['help', '--help', '-h'].includes(subcommand)) {
|
|
13
|
+
printStudioHelp();
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
if (rest.some((arg) => ['help', '--help', '-h'].includes(arg))) {
|
|
17
|
+
printStudioHelp();
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
const options = parseStudioOptions(rest);
|
|
21
|
+
switch (subcommand) {
|
|
22
|
+
case 'init': {
|
|
23
|
+
const result = await initializeStudio(options.workdir);
|
|
24
|
+
writeOutput(options.json, result, formatStudioInitReport(result));
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
case 'status': {
|
|
28
|
+
const report = await getStudioStatus(options.workdir);
|
|
29
|
+
writeOutput(options.json, report, formatStudioStatusReport(report));
|
|
30
|
+
return report.ok ? 0 : 1;
|
|
31
|
+
}
|
|
32
|
+
case 'open': {
|
|
33
|
+
const result = await openStudio(options);
|
|
34
|
+
writeOutput(options.json, result, formatStudioOpenReport(result));
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
default:
|
|
38
|
+
process.stderr.write(`Unknown studio command: ${subcommand}\n\n`);
|
|
39
|
+
printStudioHelp();
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function printStudioHelp() {
|
|
44
|
+
process.stdout.write(`PostPlus CLI — studio commands
|
|
45
|
+
|
|
46
|
+
Usage:
|
|
47
|
+
postplus studio init [--workdir <dir>] [--json]
|
|
48
|
+
postplus studio open [--workdir <dir>] [--port 3978] [--no-browser] [--json]
|
|
49
|
+
postplus studio status [--workdir <dir>] [--json]
|
|
50
|
+
|
|
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.
|
|
53
|
+
`);
|
|
54
|
+
}
|
|
55
|
+
function parseStudioOptions(args) {
|
|
56
|
+
const options = {
|
|
57
|
+
browser: true,
|
|
58
|
+
json: false,
|
|
59
|
+
port: 3978,
|
|
60
|
+
workdir: process.cwd(),
|
|
61
|
+
};
|
|
62
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
63
|
+
const arg = args[index];
|
|
64
|
+
if (arg === '--json') {
|
|
65
|
+
options.json = true;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (arg === '--no-browser') {
|
|
69
|
+
options.browser = false;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (arg === '--workdir') {
|
|
73
|
+
const value = args[index + 1];
|
|
74
|
+
if (!value || value.startsWith('--')) {
|
|
75
|
+
throw new Error('Missing value for --workdir.');
|
|
76
|
+
}
|
|
77
|
+
options.workdir = resolve(value);
|
|
78
|
+
index += 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (arg === '--port') {
|
|
82
|
+
const value = args[index + 1];
|
|
83
|
+
if (!value || value.startsWith('--')) {
|
|
84
|
+
throw new Error('Missing value for --port.');
|
|
85
|
+
}
|
|
86
|
+
options.port = Number(value);
|
|
87
|
+
if (!Number.isInteger(options.port) || options.port <= 0) {
|
|
88
|
+
throw new Error('--port must be a positive integer.');
|
|
89
|
+
}
|
|
90
|
+
index += 1;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
throw new Error(`Unknown option for studio command: ${arg}`);
|
|
94
|
+
}
|
|
95
|
+
return options;
|
|
96
|
+
}
|
|
97
|
+
export function resolveStudioRoot(workdir) {
|
|
98
|
+
const root = resolve(workdir);
|
|
99
|
+
return basename(root) === POSTPLUS_STUDIO_DIRECTORY_NAME
|
|
100
|
+
? root
|
|
101
|
+
: join(root, POSTPLUS_STUDIO_DIRECTORY_NAME);
|
|
102
|
+
}
|
|
103
|
+
async function initializeStudio(workdir) {
|
|
104
|
+
const studioRoot = resolveStudioRoot(workdir);
|
|
105
|
+
const createdAt = new Date().toISOString();
|
|
106
|
+
await mkdir(studioRoot, { recursive: true });
|
|
107
|
+
for (const dir of [
|
|
108
|
+
'workflows',
|
|
109
|
+
'assets/texts',
|
|
110
|
+
'assets/images',
|
|
111
|
+
'assets/audio',
|
|
112
|
+
'assets/videos',
|
|
113
|
+
'assets/html',
|
|
114
|
+
'assets/references',
|
|
115
|
+
'data',
|
|
116
|
+
'exports',
|
|
117
|
+
'.postplus/locks',
|
|
118
|
+
'.postplus/cache',
|
|
119
|
+
'.postplus/temp',
|
|
120
|
+
'.postplus/runs',
|
|
121
|
+
'.postplus/provider-responses',
|
|
122
|
+
'.postplus/quote-confirmations',
|
|
123
|
+
'.postplus/logs',
|
|
124
|
+
]) {
|
|
125
|
+
await mkdir(join(studioRoot, dir), { recursive: true });
|
|
126
|
+
}
|
|
127
|
+
await writeJsonIfMissing(join(studioRoot, 'studio.json'), {
|
|
128
|
+
schemaVersion: 1,
|
|
129
|
+
studio_id: DEFAULT_STUDIO_ID,
|
|
130
|
+
name: 'PostPlus Studio',
|
|
131
|
+
root_name: POSTPLUS_STUDIO_DIRECTORY_NAME,
|
|
132
|
+
created_at: createdAt,
|
|
133
|
+
updated_at: createdAt,
|
|
134
|
+
});
|
|
135
|
+
await writeJsonIfMissing(join(studioRoot, 'project.json'), {
|
|
136
|
+
project_id: DEFAULT_STUDIO_ID,
|
|
137
|
+
name: 'PostPlus Studio',
|
|
138
|
+
goal: 'Run PostPlus workflows in a local visual Studio workspace.',
|
|
139
|
+
status: 'active',
|
|
140
|
+
created_at: createdAt,
|
|
141
|
+
updated_at: createdAt,
|
|
142
|
+
});
|
|
143
|
+
await writeJsonIfMissing(join(studioRoot, 'manifest.json'), { assets: [] });
|
|
144
|
+
await writeJsonIfMissing(join(studioRoot, 'pipeline.json'), {
|
|
145
|
+
pipeline_id: 'ad-video-pipeline',
|
|
146
|
+
steps: [
|
|
147
|
+
{
|
|
148
|
+
id: 'brief',
|
|
149
|
+
name: 'Brief',
|
|
150
|
+
status: 'pending',
|
|
151
|
+
updated_at: createdAt,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: 'script',
|
|
155
|
+
name: 'Script',
|
|
156
|
+
status: 'pending',
|
|
157
|
+
updated_at: createdAt,
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: 'storyboard',
|
|
161
|
+
name: 'Storyboard',
|
|
162
|
+
status: 'pending',
|
|
163
|
+
updated_at: createdAt,
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
});
|
|
167
|
+
await writeJsonIfMissing(join(studioRoot, 'context.json'), {
|
|
168
|
+
active_project: DEFAULT_STUDIO_ID,
|
|
169
|
+
active_pipeline: 'ad-video-pipeline',
|
|
170
|
+
active_step: 'brief',
|
|
171
|
+
selected_asset_id: null,
|
|
172
|
+
selected_block_id: null,
|
|
173
|
+
selected_version: null,
|
|
174
|
+
visible_panel: 'dashboard',
|
|
175
|
+
updated_at: createdAt,
|
|
176
|
+
});
|
|
177
|
+
await writeTextIfMissing(join(studioRoot, 'provenance.jsonl'), '');
|
|
178
|
+
await writeTextIfMissing(join(studioRoot, 'activity.jsonl'), '');
|
|
179
|
+
return {
|
|
180
|
+
ok: true,
|
|
181
|
+
studioRoot,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
async function getStudioStatus(workdir) {
|
|
185
|
+
const studioRoot = resolveStudioRoot(workdir);
|
|
186
|
+
const exists = await pathExists(studioRoot);
|
|
187
|
+
const files = {
|
|
188
|
+
manifest: await pathExists(join(studioRoot, 'manifest.json')),
|
|
189
|
+
pipeline: await pathExists(join(studioRoot, 'pipeline.json')),
|
|
190
|
+
studio: await pathExists(join(studioRoot, 'studio.json')),
|
|
191
|
+
};
|
|
192
|
+
return {
|
|
193
|
+
exists,
|
|
194
|
+
files,
|
|
195
|
+
ok: exists && files.studio && files.manifest && files.pipeline,
|
|
196
|
+
studioRoot,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
async function openStudio(options) {
|
|
200
|
+
const { studioRoot } = await initializeStudio(options.workdir);
|
|
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,
|
|
234
|
+
'--studio-root',
|
|
235
|
+
studioRoot,
|
|
236
|
+
'--host',
|
|
237
|
+
host,
|
|
238
|
+
'--port',
|
|
239
|
+
String(port),
|
|
240
|
+
], {
|
|
241
|
+
cwd: packageRoot,
|
|
242
|
+
detached: true,
|
|
243
|
+
stdio: ['ignore', logFd, logFd],
|
|
244
|
+
});
|
|
245
|
+
child.unref();
|
|
246
|
+
closeSync(logFd);
|
|
247
|
+
try {
|
|
248
|
+
await waitForStudioServer(baseUrl, logPath);
|
|
249
|
+
}
|
|
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;
|
|
260
|
+
}
|
|
261
|
+
const state = {
|
|
262
|
+
baseUrl,
|
|
263
|
+
dashboardUrl,
|
|
264
|
+
logPath,
|
|
265
|
+
pid: child.pid,
|
|
266
|
+
startedAt: new Date().toISOString(),
|
|
267
|
+
studioRoot,
|
|
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'] : [];
|
|
287
|
+
}
|
|
288
|
+
async function readLiveStudioServerState(studioRoot) {
|
|
289
|
+
const state = await readJsonIfExists(getStudioServerStatePath(studioRoot));
|
|
290
|
+
if (!state?.baseUrl || !state.dashboardUrl) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
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;
|
|
317
|
+
}
|
|
318
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, 150));
|
|
319
|
+
}
|
|
320
|
+
throw new Error(`Studio server did not become ready at ${baseUrl}. See log: ${logPath}`);
|
|
321
|
+
}
|
|
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;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
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
|
+
}
|
|
338
|
+
}
|
|
339
|
+
throw new Error(`No available Studio port found from ${startPort} to ${startPort + 49}.`);
|
|
340
|
+
}
|
|
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
|
+
});
|
|
350
|
+
}
|
|
351
|
+
async function pathExists(path) {
|
|
352
|
+
try {
|
|
353
|
+
await access(path, fsConstants.F_OK);
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
async function writeJsonIfMissing(path, value) {
|
|
361
|
+
if (await pathExists(path)) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
await writeJson(path, value);
|
|
365
|
+
}
|
|
366
|
+
async function writeTextIfMissing(path, value) {
|
|
367
|
+
if (await pathExists(path)) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
await mkdir(dirname(path), { recursive: true });
|
|
371
|
+
await writeFile(path, value, 'utf8');
|
|
372
|
+
}
|
|
373
|
+
async function writeJson(path, value) {
|
|
374
|
+
await mkdir(dirname(path), { recursive: true });
|
|
375
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
376
|
+
}
|
|
377
|
+
function openSystemBrowser(url) {
|
|
378
|
+
const command = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'cmd' : 'xdg-open';
|
|
379
|
+
const args = platform() === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
380
|
+
const child = spawn(command, args, {
|
|
381
|
+
detached: true,
|
|
382
|
+
stdio: 'ignore',
|
|
383
|
+
});
|
|
384
|
+
child.unref();
|
|
385
|
+
}
|
|
386
|
+
function writeOutput(json, value, text) {
|
|
387
|
+
process.stdout.write(json ? `${JSON.stringify(value, null, 2)}\n` : text);
|
|
388
|
+
}
|
|
389
|
+
function formatStudioInitReport(result) {
|
|
390
|
+
return `PostPlus Studio initialized\n\nStudio root: ${result.studioRoot}\n`;
|
|
391
|
+
}
|
|
392
|
+
function formatStudioOpenReport(result) {
|
|
393
|
+
return `PostPlus Studio is running\n\nStudio root: ${result.studioRoot}\nURL: ${result.url}\n`;
|
|
394
|
+
}
|
|
395
|
+
function formatStudioStatusReport(report) {
|
|
396
|
+
return [
|
|
397
|
+
'PostPlus Studio status',
|
|
398
|
+
'',
|
|
399
|
+
`Studio root: ${report.studioRoot}`,
|
|
400
|
+
`Exists: ${report.exists ? 'yes' : 'no'}`,
|
|
401
|
+
`studio.json: ${report.files.studio ? 'yes' : 'no'}`,
|
|
402
|
+
`manifest.json: ${report.files.manifest ? 'yes' : 'no'}`,
|
|
403
|
+
`pipeline.json: ${report.files.pipeline ? 'yes' : 'no'}`,
|
|
404
|
+
`Status: ${report.ok ? 'ready' : 'not ready'}`,
|
|
405
|
+
'',
|
|
406
|
+
].join('\n');
|
|
407
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@postplus/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.32",
|
|
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.",
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
"build/skill-catalog.js",
|
|
23
23
|
"build/skill-management.js",
|
|
24
24
|
"build/status.js",
|
|
25
|
+
"build/studio.js",
|
|
26
|
+
"build/studio-server.js",
|
|
25
27
|
"build/subscription-status.js",
|
|
26
28
|
"build/update-check.js",
|
|
27
29
|
"LICENSE",
|