@postplus/cli 0.1.30 → 0.1.31

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 CHANGED
@@ -33,17 +33,39 @@ 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 --list --full-depth
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. Assets, workflow files, activity, and provenance live inside that
67
+ folder; hidden runtime cache and logs stay under `PostPlus Studio/.postplus/`.
68
+
47
69
  ## The Vision
48
70
 
49
71
  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 update
46
- postplus uninstall
46
+ postplus studio init|open|status
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
- ${POSTPLUS_SKILLS_INSTALL_COMMAND}
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
- return runPostPlusSkillUninstall();
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);
@@ -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
- return `npx -y skills add ${source} --global --full-depth --skill '*' --agent ${POSTPLUS_SKILLS_AGENT_ARGS} --yes`;
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
- '--global',
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
- '--global',
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 'postplus update';
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
- export function formatPostPlusSkillUninstallCommand() {
221
- return 'postplus uninstall';
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,322 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import { access, mkdir, writeFile, } from 'node:fs/promises';
3
+ import { constants as fsConstants } from 'node:fs';
4
+ import { platform } from 'node:os';
5
+ import { basename, dirname, join, resolve, } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ export const POSTPLUS_STUDIO_DIRECTORY_NAME = 'PostPlus Studio';
8
+ const DEFAULT_STUDIO_ID = 'postplus-studio';
9
+ export async function runStudioCommand(args) {
10
+ const [subcommand, ...rest] = args;
11
+ if (!subcommand || ['help', '--help', '-h'].includes(subcommand)) {
12
+ printStudioHelp();
13
+ return 0;
14
+ }
15
+ const options = parseStudioOptions(rest);
16
+ switch (subcommand) {
17
+ case 'init': {
18
+ const result = await initializeStudio(options.workdir);
19
+ writeOutput(options.json, result, formatStudioInitReport(result));
20
+ return 0;
21
+ }
22
+ case 'status': {
23
+ const report = await getStudioStatus(options.workdir);
24
+ writeOutput(options.json, report, formatStudioStatusReport(report));
25
+ return report.ok ? 0 : 1;
26
+ }
27
+ case 'open': {
28
+ const result = await openStudio(options);
29
+ writeOutput(options.json, result, formatStudioOpenReport(result));
30
+ return 0;
31
+ }
32
+ default:
33
+ process.stderr.write(`Unknown studio command: ${subcommand}\n\n`);
34
+ printStudioHelp();
35
+ return 1;
36
+ }
37
+ }
38
+ function printStudioHelp() {
39
+ process.stdout.write(`PostPlus CLI — studio commands
40
+
41
+ Usage:
42
+ postplus studio init [--workdir <dir>] [--json]
43
+ postplus studio open [--workdir <dir>] [--port 3978] [--no-browser] [--json]
44
+ postplus studio status [--workdir <dir>] [--json]
45
+
46
+ Studio creates a visible "PostPlus Studio" folder inside the selected working directory.
47
+ `);
48
+ }
49
+ function parseStudioOptions(args) {
50
+ const options = {
51
+ browser: true,
52
+ json: false,
53
+ port: 3978,
54
+ workdir: process.cwd(),
55
+ };
56
+ for (let index = 0; index < args.length; index += 1) {
57
+ const arg = args[index];
58
+ if (arg === '--json') {
59
+ options.json = true;
60
+ continue;
61
+ }
62
+ if (arg === '--no-browser') {
63
+ options.browser = false;
64
+ continue;
65
+ }
66
+ if (arg === '--workdir') {
67
+ const value = args[index + 1];
68
+ if (!value || value.startsWith('--')) {
69
+ throw new Error('Missing value for --workdir.');
70
+ }
71
+ options.workdir = resolve(value);
72
+ index += 1;
73
+ continue;
74
+ }
75
+ if (arg === '--port') {
76
+ const value = args[index + 1];
77
+ if (!value || value.startsWith('--')) {
78
+ throw new Error('Missing value for --port.');
79
+ }
80
+ options.port = Number(value);
81
+ if (!Number.isInteger(options.port) || options.port <= 0) {
82
+ throw new Error('--port must be a positive integer.');
83
+ }
84
+ index += 1;
85
+ continue;
86
+ }
87
+ throw new Error(`Unknown option for studio command: ${arg}`);
88
+ }
89
+ return options;
90
+ }
91
+ export function resolveStudioRoot(workdir) {
92
+ const root = resolve(workdir);
93
+ return basename(root) === POSTPLUS_STUDIO_DIRECTORY_NAME
94
+ ? root
95
+ : join(root, POSTPLUS_STUDIO_DIRECTORY_NAME);
96
+ }
97
+ async function initializeStudio(workdir) {
98
+ const studioRoot = resolveStudioRoot(workdir);
99
+ const createdAt = new Date().toISOString();
100
+ await mkdir(studioRoot, { recursive: true });
101
+ for (const dir of [
102
+ 'workflows',
103
+ 'assets/texts',
104
+ 'assets/images',
105
+ 'assets/audio',
106
+ 'assets/videos',
107
+ 'assets/html',
108
+ 'assets/references',
109
+ 'data',
110
+ 'exports',
111
+ '.postplus/locks',
112
+ '.postplus/cache',
113
+ '.postplus/temp',
114
+ '.postplus/runs',
115
+ '.postplus/provider-responses',
116
+ '.postplus/quote-confirmations',
117
+ '.postplus/logs',
118
+ ]) {
119
+ await mkdir(join(studioRoot, dir), { recursive: true });
120
+ }
121
+ await writeJsonIfMissing(join(studioRoot, 'studio.json'), {
122
+ schemaVersion: 1,
123
+ studio_id: DEFAULT_STUDIO_ID,
124
+ name: 'PostPlus Studio',
125
+ root_name: POSTPLUS_STUDIO_DIRECTORY_NAME,
126
+ created_at: createdAt,
127
+ updated_at: createdAt,
128
+ });
129
+ await writeJsonIfMissing(join(studioRoot, 'project.json'), {
130
+ project_id: DEFAULT_STUDIO_ID,
131
+ name: 'PostPlus Studio',
132
+ goal: 'Run PostPlus workflows in a local visual Studio workspace.',
133
+ status: 'active',
134
+ created_at: createdAt,
135
+ updated_at: createdAt,
136
+ });
137
+ await writeJsonIfMissing(join(studioRoot, 'manifest.json'), { assets: [] });
138
+ await writeJsonIfMissing(join(studioRoot, 'pipeline.json'), {
139
+ pipeline_id: 'ad-video-pipeline',
140
+ steps: [
141
+ {
142
+ id: 'brief',
143
+ name: 'Brief',
144
+ status: 'pending',
145
+ updated_at: createdAt,
146
+ },
147
+ {
148
+ id: 'script',
149
+ name: 'Script',
150
+ status: 'pending',
151
+ updated_at: createdAt,
152
+ },
153
+ {
154
+ id: 'storyboard',
155
+ name: 'Storyboard',
156
+ status: 'pending',
157
+ updated_at: createdAt,
158
+ },
159
+ ],
160
+ });
161
+ await writeJsonIfMissing(join(studioRoot, 'context.json'), {
162
+ active_project: DEFAULT_STUDIO_ID,
163
+ active_pipeline: 'ad-video-pipeline',
164
+ active_step: 'brief',
165
+ selected_asset_id: null,
166
+ selected_block_id: null,
167
+ selected_version: null,
168
+ visible_panel: 'dashboard',
169
+ updated_at: createdAt,
170
+ });
171
+ await writeTextIfMissing(join(studioRoot, 'provenance.jsonl'), '');
172
+ await writeTextIfMissing(join(studioRoot, 'activity.jsonl'), '');
173
+ return {
174
+ ok: true,
175
+ studioRoot,
176
+ };
177
+ }
178
+ async function getStudioStatus(workdir) {
179
+ const studioRoot = resolveStudioRoot(workdir);
180
+ const exists = await pathExists(studioRoot);
181
+ const files = {
182
+ manifest: await pathExists(join(studioRoot, 'manifest.json')),
183
+ pipeline: await pathExists(join(studioRoot, 'pipeline.json')),
184
+ studio: await pathExists(join(studioRoot, 'studio.json')),
185
+ };
186
+ return {
187
+ exists,
188
+ files,
189
+ ok: exists && files.studio && files.manifest && files.pipeline,
190
+ studioRoot,
191
+ };
192
+ }
193
+ async function openStudio(options) {
194
+ const { studioRoot } = await initializeStudio(options.workdir);
195
+ const runtimeRoot = await resolveStudioRuntimeRoot();
196
+ const launcher = join(runtimeRoot, 'skills/00-core/postplus-workspace-dashboard/scripts/launch_workspace_dashboard.mjs');
197
+ const result = spawnSync(process.execPath, [
198
+ launcher,
199
+ '--studio-root',
200
+ studioRoot,
201
+ '--host',
202
+ '127.0.0.1',
203
+ '--port',
204
+ String(options.port),
205
+ '--skip-build',
206
+ ], {
207
+ cwd: runtimeRoot,
208
+ encoding: 'utf8',
209
+ });
210
+ if (result.status !== 0) {
211
+ throw new Error(result.stderr || result.stdout || 'Failed to open Studio.');
212
+ }
213
+ const parsed = JSON.parse(result.stdout);
214
+ if (options.browser) {
215
+ openSystemBrowser(parsed.url);
216
+ }
217
+ return {
218
+ ok: true,
219
+ runtimeRoot,
220
+ studioRoot,
221
+ ...parsed,
222
+ };
223
+ }
224
+ async function resolveStudioRuntimeRoot() {
225
+ const envRoot = process.env.POSTPLUS_STUDIO_RUNTIME_ROOT?.trim();
226
+ if (envRoot) {
227
+ return assertStudioRuntimeRoot(resolve(envRoot));
228
+ }
229
+ const candidates = [
230
+ process.cwd(),
231
+ dirname(fileURLToPath(import.meta.url)),
232
+ ...ancestorDirs(process.cwd()),
233
+ ];
234
+ for (const base of candidates) {
235
+ for (const candidate of [
236
+ base,
237
+ join(base, 'packages/vibe_marketing'),
238
+ join(base, '../packages/vibe_marketing'),
239
+ join(base, '../../packages/vibe_marketing'),
240
+ ]) {
241
+ if (await isStudioRuntimeRoot(resolve(candidate))) {
242
+ return resolve(candidate);
243
+ }
244
+ }
245
+ }
246
+ throw new Error('PostPlus Studio runtime was not found. Set POSTPLUS_STUDIO_RUNTIME_ROOT to the vibe_marketing authoring repo.');
247
+ }
248
+ function ancestorDirs(start) {
249
+ const dirs = [];
250
+ let current = resolve(start);
251
+ while (dirname(current) !== current) {
252
+ current = dirname(current);
253
+ dirs.push(current);
254
+ }
255
+ return dirs;
256
+ }
257
+ async function assertStudioRuntimeRoot(root) {
258
+ if (!(await isStudioRuntimeRoot(root))) {
259
+ throw new Error(`Invalid PostPlus Studio runtime root: ${root}`);
260
+ }
261
+ return root;
262
+ }
263
+ async function isStudioRuntimeRoot(root) {
264
+ return pathExists(join(root, 'skills/00-core/postplus-workspace-dashboard/scripts/launch_workspace_dashboard.mjs'));
265
+ }
266
+ async function pathExists(path) {
267
+ try {
268
+ await access(path, fsConstants.F_OK);
269
+ return true;
270
+ }
271
+ catch {
272
+ return false;
273
+ }
274
+ }
275
+ async function writeJsonIfMissing(path, value) {
276
+ if (await pathExists(path)) {
277
+ return;
278
+ }
279
+ await writeJson(path, value);
280
+ }
281
+ async function writeTextIfMissing(path, value) {
282
+ if (await pathExists(path)) {
283
+ return;
284
+ }
285
+ await mkdir(dirname(path), { recursive: true });
286
+ await writeFile(path, value, 'utf8');
287
+ }
288
+ async function writeJson(path, value) {
289
+ await mkdir(dirname(path), { recursive: true });
290
+ await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
291
+ }
292
+ function openSystemBrowser(url) {
293
+ const command = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'cmd' : 'xdg-open';
294
+ const args = platform() === 'win32' ? ['/c', 'start', '', url] : [url];
295
+ const child = spawn(command, args, {
296
+ detached: true,
297
+ stdio: 'ignore',
298
+ });
299
+ child.unref();
300
+ }
301
+ function writeOutput(json, value, text) {
302
+ process.stdout.write(json ? `${JSON.stringify(value, null, 2)}\n` : text);
303
+ }
304
+ function formatStudioInitReport(result) {
305
+ return `PostPlus Studio initialized\n\nStudio root: ${result.studioRoot}\n`;
306
+ }
307
+ function formatStudioOpenReport(result) {
308
+ return `PostPlus Studio is running\n\nStudio root: ${result.studioRoot}\nURL: ${result.url}\n`;
309
+ }
310
+ function formatStudioStatusReport(report) {
311
+ return [
312
+ 'PostPlus Studio status',
313
+ '',
314
+ `Studio root: ${report.studioRoot}`,
315
+ `Exists: ${report.exists ? 'yes' : 'no'}`,
316
+ `studio.json: ${report.files.studio ? 'yes' : 'no'}`,
317
+ `manifest.json: ${report.files.manifest ? 'yes' : 'no'}`,
318
+ `pipeline.json: ${report.files.pipeline ? 'yes' : 'no'}`,
319
+ `Status: ${report.ok ? 'ready' : 'not ready'}`,
320
+ '',
321
+ ].join('\n');
322
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postplus/cli",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
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,7 @@
22
22
  "build/skill-catalog.js",
23
23
  "build/skill-management.js",
24
24
  "build/status.js",
25
+ "build/studio.js",
25
26
  "build/subscription-status.js",
26
27
  "build/update-check.js",
27
28
  "LICENSE",