@principal-ai/principal-studio-cli 0.35.3
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 +172 -0
- package/dist/commands/agent-session.d.ts +3 -0
- package/dist/commands/agent-session.d.ts.map +1 -0
- package/dist/commands/agent-session.js +67 -0
- package/dist/commands/agent-sessions.d.ts +11 -0
- package/dist/commands/agent-sessions.d.ts.map +1 -0
- package/dist/commands/agent-sessions.js +39 -0
- package/dist/commands/auxiliary/index.d.ts +3 -0
- package/dist/commands/auxiliary/index.d.ts.map +1 -0
- package/dist/commands/auxiliary/index.js +9 -0
- package/dist/commands/auxiliary/validate.d.ts +3 -0
- package/dist/commands/auxiliary/validate.d.ts.map +1 -0
- package/dist/commands/auxiliary/validate.js +123 -0
- package/dist/commands/collector/check.d.ts +6 -0
- package/dist/commands/collector/check.d.ts.map +1 -0
- package/dist/commands/collector/check.js +153 -0
- package/dist/commands/collector/diagnose.d.ts +6 -0
- package/dist/commands/collector/diagnose.d.ts.map +1 -0
- package/dist/commands/collector/diagnose.js +229 -0
- package/dist/commands/collector/index.d.ts +6 -0
- package/dist/commands/collector/index.d.ts.map +1 -0
- package/dist/commands/collector/index.js +16 -0
- package/dist/commands/collector/status.d.ts +6 -0
- package/dist/commands/collector/status.d.ts.map +1 -0
- package/dist/commands/collector/status.js +77 -0
- package/dist/commands/collector/utils.d.ts +92 -0
- package/dist/commands/collector/utils.d.ts.map +1 -0
- package/dist/commands/collector/utils.js +154 -0
- package/dist/commands/coverage.d.ts +9 -0
- package/dist/commands/coverage.d.ts.map +1 -0
- package/dist/commands/coverage.js +252 -0
- package/dist/commands/create.d.ts +6 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +50 -0
- package/dist/commands/events/index.d.ts +3 -0
- package/dist/commands/events/index.d.ts.map +1 -0
- package/dist/commands/events/index.js +9 -0
- package/dist/commands/events/validate.d.ts +3 -0
- package/dist/commands/events/validate.d.ts.map +1 -0
- package/dist/commands/events/validate.js +124 -0
- package/dist/commands/formats.d.ts +6 -0
- package/dist/commands/formats.d.ts.map +1 -0
- package/dist/commands/formats.js +1118 -0
- package/dist/commands/hooks.d.ts +9 -0
- package/dist/commands/hooks.d.ts.map +1 -0
- package/dist/commands/hooks.js +295 -0
- package/dist/commands/inbox.d.ts +13 -0
- package/dist/commands/inbox.d.ts.map +1 -0
- package/dist/commands/inbox.js +179 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +220 -0
- package/dist/commands/lint.d.ts +11 -0
- package/dist/commands/lint.d.ts.map +1 -0
- package/dist/commands/lint.js +71 -0
- package/dist/commands/list.d.ts +6 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +78 -0
- package/dist/commands/migrate-nodes.d.ts +9 -0
- package/dist/commands/migrate-nodes.d.ts.map +1 -0
- package/dist/commands/migrate-nodes.js +363 -0
- package/dist/commands/migrate-scopes-to-canvas.d.ts +3 -0
- package/dist/commands/migrate-scopes-to-canvas.d.ts.map +1 -0
- package/dist/commands/migrate-scopes-to-canvas.js +143 -0
- package/dist/commands/migration.d.ts +6 -0
- package/dist/commands/migration.d.ts.map +1 -0
- package/dist/commands/migration.js +247 -0
- package/dist/commands/open-studio.d.ts +10 -0
- package/dist/commands/open-studio.d.ts.map +1 -0
- package/dist/commands/open-studio.js +32 -0
- package/dist/commands/opencode/fetch.d.ts +3 -0
- package/dist/commands/opencode/fetch.d.ts.map +1 -0
- package/dist/commands/opencode/fetch.js +25 -0
- package/dist/commands/opencode/index.d.ts +3 -0
- package/dist/commands/opencode/index.d.ts.map +1 -0
- package/dist/commands/opencode/index.js +11 -0
- package/dist/commands/opencode/list-sessions.d.ts +3 -0
- package/dist/commands/opencode/list-sessions.d.ts.map +1 -0
- package/dist/commands/opencode/list-sessions.js +20 -0
- package/dist/commands/repo.d.ts +18 -0
- package/dist/commands/repo.d.ts.map +1 -0
- package/dist/commands/repo.js +233 -0
- package/dist/commands/schema.d.ts +6 -0
- package/dist/commands/schema.d.ts.map +1 -0
- package/dist/commands/schema.js +367 -0
- package/dist/commands/scopes/index.d.ts +3 -0
- package/dist/commands/scopes/index.d.ts.map +1 -0
- package/dist/commands/scopes/index.js +9 -0
- package/dist/commands/scopes/validate.d.ts +3 -0
- package/dist/commands/scopes/validate.d.ts.map +1 -0
- package/dist/commands/scopes/validate.js +187 -0
- package/dist/commands/setup.d.ts +6 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +406 -0
- package/dist/commands/starred-collections.d.ts +12 -0
- package/dist/commands/starred-collections.d.ts.map +1 -0
- package/dist/commands/starred-collections.js +310 -0
- package/dist/commands/subsystem-model.d.ts +11 -0
- package/dist/commands/subsystem-model.d.ts.map +1 -0
- package/dist/commands/subsystem-model.js +249 -0
- package/dist/commands/topic.d.ts +15 -0
- package/dist/commands/topic.d.ts.map +1 -0
- package/dist/commands/topic.js +683 -0
- package/dist/commands/tour.d.ts +23 -0
- package/dist/commands/tour.d.ts.map +1 -0
- package/dist/commands/tour.js +783 -0
- package/dist/commands/trace/flow.d.ts +6 -0
- package/dist/commands/trace/flow.d.ts.map +1 -0
- package/dist/commands/trace/flow.js +158 -0
- package/dist/commands/trace/index.d.ts +6 -0
- package/dist/commands/trace/index.d.ts.map +1 -0
- package/dist/commands/trace/index.js +20 -0
- package/dist/commands/trace/inspect.d.ts +6 -0
- package/dist/commands/trace/inspect.d.ts.map +1 -0
- package/dist/commands/trace/inspect.js +187 -0
- package/dist/commands/trace/list.d.ts +6 -0
- package/dist/commands/trace/list.d.ts.map +1 -0
- package/dist/commands/trace/list.js +145 -0
- package/dist/commands/trace/registrations.d.ts +6 -0
- package/dist/commands/trace/registrations.d.ts.map +1 -0
- package/dist/commands/trace/registrations.js +81 -0
- package/dist/commands/trace/validate.d.ts +6 -0
- package/dist/commands/trace/validate.d.ts.map +1 -0
- package/dist/commands/trace/validate.js +276 -0
- package/dist/commands/trail.d.ts +36 -0
- package/dist/commands/trail.d.ts.map +1 -0
- package/dist/commands/trail.js +622 -0
- package/dist/commands/validate.d.ts +12 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +3334 -0
- package/dist/commands/workflow/implementation.d.ts +3 -0
- package/dist/commands/workflow/implementation.d.ts.map +1 -0
- package/dist/commands/workflow/implementation.js +51 -0
- package/dist/commands/workflow/index.d.ts +3 -0
- package/dist/commands/workflow/index.d.ts.map +1 -0
- package/dist/commands/workflow/index.js +19 -0
- package/dist/commands/workflow/inspect.d.ts +3 -0
- package/dist/commands/workflow/inspect.d.ts.map +1 -0
- package/dist/commands/workflow/inspect.js +123 -0
- package/dist/commands/workflow/list.d.ts +3 -0
- package/dist/commands/workflow/list.d.ts.map +1 -0
- package/dist/commands/workflow/list.js +95 -0
- package/dist/commands/workflow/render.d.ts +3 -0
- package/dist/commands/workflow/render.d.ts.map +1 -0
- package/dist/commands/workflow/render.js +88 -0
- package/dist/commands/workflow/test.d.ts +3 -0
- package/dist/commands/workflow/test.d.ts.map +1 -0
- package/dist/commands/workflow/test.js +141 -0
- package/dist/commands/workflow/utils.d.ts +49 -0
- package/dist/commands/workflow/utils.d.ts.map +1 -0
- package/dist/commands/workflow/utils.js +164 -0
- package/dist/commands/workflow/validate.d.ts +3 -0
- package/dist/commands/workflow/validate.d.ts.map +1 -0
- package/dist/commands/workflow/validate.js +344 -0
- package/dist/file-utils.d.ts +15 -0
- package/dist/file-utils.d.ts.map +1 -0
- package/dist/file-utils.js +29 -0
- package/dist/index.cjs +285718 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +81 -0
- package/dist/lib/bridge-ipc.d.ts +29 -0
- package/dist/lib/bridge-ipc.d.ts.map +1 -0
- package/dist/lib/bridge-ipc.js +128 -0
- package/dist/lib/github-user.d.ts +17 -0
- package/dist/lib/github-user.d.ts.map +1 -0
- package/dist/lib/github-user.js +53 -0
- package/dist/lib/open-url.d.ts +11 -0
- package/dist/lib/open-url.d.ts.map +1 -0
- package/dist/lib/open-url.js +45 -0
- package/dist/lib/subsystem-model-store.d.ts +74 -0
- package/dist/lib/subsystem-model-store.d.ts.map +1 -0
- package/dist/lib/subsystem-model-store.js +292 -0
- package/dist/lib/tour-cache.d.ts +26 -0
- package/dist/lib/tour-cache.d.ts.map +1 -0
- package/dist/lib/tour-cache.js +46 -0
- package/dist/lib/trail-cache.d.ts +40 -0
- package/dist/lib/trail-cache.d.ts.map +1 -0
- package/dist/lib/trail-cache.js +97 -0
- package/dist/lib/viewer-ipc.d.ts +44 -0
- package/dist/lib/viewer-ipc.d.ts.map +1 -0
- package/dist/lib/viewer-ipc.js +68 -0
- package/package.json +68 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trail command — fetch or publish trails to the Principal ADE backend.
|
|
3
|
+
*
|
|
4
|
+
* Resolves a GitHub token locally (gh CLI → git credential helper) and calls
|
|
5
|
+
* the web-ade trail API with `Authorization: Bearer <token>`. The token is
|
|
6
|
+
* never echoed to argv, env, stdout, or stderr. stdout is trail JSON (fetch)
|
|
7
|
+
* or the share URL (publish) only.
|
|
8
|
+
*/
|
|
9
|
+
import { Command } from 'commander';
|
|
10
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
13
|
+
import { isValidPurl, parsePurl } from '@principal-ai/alexandria-core-library';
|
|
14
|
+
import * as trailCache from '../lib/trail-cache.js';
|
|
15
|
+
import { handoffToRunning } from '../lib/viewer-ipc.js';
|
|
16
|
+
import { handoffToBridge } from '../lib/bridge-ipc.js';
|
|
17
|
+
import { fetchGitHubMe, fetchGitHubUserByLogin } from '../lib/github-user.js';
|
|
18
|
+
import { openInBrowser } from '../lib/open-url.js';
|
|
19
|
+
const ONE_HOUR_MS = 60 * 60 * 1000;
|
|
20
|
+
// Anchor `require.resolve` to wherever the CLI is actually running from. Works
|
|
21
|
+
// in both the esbuild CJS bundle (argv[1] = dist/index.cjs) and dev (argv[1] =
|
|
22
|
+
// src/index.ts). Falling back to cwd is a last resort that matters mainly when
|
|
23
|
+
// some wrapper script has rewritten argv.
|
|
24
|
+
const cliRequire = createRequire(process.argv[1] ?? `${process.cwd()}/`);
|
|
25
|
+
// Shared with the `tour` command (see `./tour.ts`) — both target the same
|
|
26
|
+
// web-ade backend with the same local GitHub-token auth. `PRINCIPAL_ADE_BASE_URL`
|
|
27
|
+
// overrides the default for pointing at a local dev server.
|
|
28
|
+
export const BASE_URL = process.env.PRINCIPAL_ADE_BASE_URL?.replace(/\/+$/, '') ||
|
|
29
|
+
'https://app.principal-ade.com';
|
|
30
|
+
const RESERVED_TRAIL_IDS = new Set(['publish', 'list']);
|
|
31
|
+
function parseTrailId(input) {
|
|
32
|
+
try {
|
|
33
|
+
const url = new URL(input);
|
|
34
|
+
const match = url.pathname.match(/\/trail\/([^/]+)\/?$/);
|
|
35
|
+
if (match)
|
|
36
|
+
return match[1];
|
|
37
|
+
const segments = url.pathname.split('/').filter(Boolean);
|
|
38
|
+
if (segments.length)
|
|
39
|
+
return segments[segments.length - 1];
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// not a URL — fall through and treat input as a bare id
|
|
43
|
+
}
|
|
44
|
+
return input;
|
|
45
|
+
}
|
|
46
|
+
function resolveTokenViaGh() {
|
|
47
|
+
const result = spawnSync('gh', ['auth', 'token'], {
|
|
48
|
+
encoding: 'utf8',
|
|
49
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
50
|
+
});
|
|
51
|
+
if (result.status === 0 && result.stdout) {
|
|
52
|
+
const token = result.stdout.trim();
|
|
53
|
+
if (token)
|
|
54
|
+
return token;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
function resolveTokenViaGitCredential() {
|
|
59
|
+
const result = spawnSync('git', ['credential', 'fill'], {
|
|
60
|
+
encoding: 'utf8',
|
|
61
|
+
input: 'protocol=https\nhost=github.com\n\n',
|
|
62
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
63
|
+
});
|
|
64
|
+
if (result.status !== 0 || !result.stdout)
|
|
65
|
+
return null;
|
|
66
|
+
for (const line of result.stdout.split('\n')) {
|
|
67
|
+
if (line.startsWith('password=')) {
|
|
68
|
+
const token = line.slice('password='.length).trim();
|
|
69
|
+
if (token)
|
|
70
|
+
return token;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
export function resolveToken() {
|
|
76
|
+
return resolveTokenViaGh() ?? resolveTokenViaGitCredential();
|
|
77
|
+
}
|
|
78
|
+
export function exitWithTokenError() {
|
|
79
|
+
process.stderr.write('Could not resolve a GitHub token. Run `gh auth login`, or configure a git credential helper for github.com.\n');
|
|
80
|
+
process.exit(2);
|
|
81
|
+
}
|
|
82
|
+
async function readPayloadFromStdin() {
|
|
83
|
+
const chunks = [];
|
|
84
|
+
for await (const chunk of process.stdin) {
|
|
85
|
+
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
|
|
86
|
+
}
|
|
87
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
88
|
+
}
|
|
89
|
+
async function readPayload(file) {
|
|
90
|
+
const useStdin = !file || file === '-';
|
|
91
|
+
const raw = useStdin ? await readPayloadFromStdin() : readFileSync(file, 'utf8');
|
|
92
|
+
if (!raw.trim()) {
|
|
93
|
+
process.stderr.write('Empty payload\n');
|
|
94
|
+
process.exit(2);
|
|
95
|
+
}
|
|
96
|
+
let parsed;
|
|
97
|
+
try {
|
|
98
|
+
parsed = JSON.parse(raw);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
process.stderr.write(`Could not parse payload as JSON: ${err.message}\n`);
|
|
102
|
+
process.exit(2);
|
|
103
|
+
}
|
|
104
|
+
return { payload: parsed, source: useStdin ? 'stdin' : 'file' };
|
|
105
|
+
}
|
|
106
|
+
function ownerRepoFromPurl(purl) {
|
|
107
|
+
const parsed = parsePurl(purl);
|
|
108
|
+
if (!parsed || !parsed.namespace)
|
|
109
|
+
return null;
|
|
110
|
+
if (parsed.type !== 'github' && parsed.type !== 'gitlab' && parsed.type !== 'bitbucket') {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
return { owner: parsed.namespace, repo: parsed.name };
|
|
114
|
+
}
|
|
115
|
+
function validateRepoPurls(payload) {
|
|
116
|
+
if (typeof payload !== 'object' || payload === null)
|
|
117
|
+
return;
|
|
118
|
+
const repos = payload.repos;
|
|
119
|
+
if (!Array.isArray(repos))
|
|
120
|
+
return;
|
|
121
|
+
for (let i = 0; i < repos.length; i++) {
|
|
122
|
+
const id = repos[i]?.id;
|
|
123
|
+
if (typeof id !== 'string' || !isValidPurl(id)) {
|
|
124
|
+
process.stderr.write(`repos[${i}].id is not a valid Purl${typeof id === 'string' ? `: ${id}` : ''}. ` +
|
|
125
|
+
`Mint with \`PurlBuilders.github(owner, repo)\` or \`createLocalRepoPurl(absPath)\` ` +
|
|
126
|
+
`from @principal-ai/alexandria-core-library.\n`);
|
|
127
|
+
process.exit(2);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function resolveOwnerRepo(payload, flagOwner, flagRepo, flagPurl) {
|
|
132
|
+
if (flagPurl) {
|
|
133
|
+
const fromFlag = ownerRepoFromPurl(flagPurl);
|
|
134
|
+
if (!fromFlag) {
|
|
135
|
+
process.stderr.write(`--purl is not a valid github/gitlab/bitbucket Purl: ${flagPurl}\n`);
|
|
136
|
+
process.exit(2);
|
|
137
|
+
}
|
|
138
|
+
return { owner: flagOwner ?? fromFlag.owner, repo: flagRepo ?? fromFlag.repo };
|
|
139
|
+
}
|
|
140
|
+
const repoEntry = typeof payload === 'object' && payload !== null
|
|
141
|
+
? payload
|
|
142
|
+
.repos?.[0]
|
|
143
|
+
: undefined;
|
|
144
|
+
const purlOwnerRepo = typeof repoEntry?.id === 'string' ? ownerRepoFromPurl(repoEntry.id) : null;
|
|
145
|
+
const owner = flagOwner ?? repoEntry?.remote?.owner ?? purlOwnerRepo?.owner;
|
|
146
|
+
const repo = flagRepo ?? repoEntry?.remote?.name ?? purlOwnerRepo?.repo;
|
|
147
|
+
if (!owner || !repo) {
|
|
148
|
+
process.stderr.write('Could not determine owner/repo. Pass --owner and --repo (or --purl), or include `repos[0].remote`/`repos[0].id` in the payload.\n');
|
|
149
|
+
process.exit(2);
|
|
150
|
+
}
|
|
151
|
+
return { owner, repo };
|
|
152
|
+
}
|
|
153
|
+
export async function describeHttpError(response) {
|
|
154
|
+
let serverMessage = '';
|
|
155
|
+
let code = '';
|
|
156
|
+
try {
|
|
157
|
+
const body = (await response.clone().json());
|
|
158
|
+
serverMessage = body.error ?? '';
|
|
159
|
+
code = body.code ?? '';
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// body wasn't JSON — keep going with status-only message
|
|
163
|
+
}
|
|
164
|
+
const fallback = response.status === 404
|
|
165
|
+
? 'Trail not found'
|
|
166
|
+
: response.status === 403
|
|
167
|
+
? 'No read access to this repository (token may lack repo scope)'
|
|
168
|
+
: response.status === 401
|
|
169
|
+
? 'GitHub token rejected'
|
|
170
|
+
: `HTTP ${response.status}`;
|
|
171
|
+
const human = serverMessage || fallback;
|
|
172
|
+
return `${human}${code ? ` [${code}]` : ''}`;
|
|
173
|
+
}
|
|
174
|
+
/** Fetch + cache a trail by id. Exits the process on any error. */
|
|
175
|
+
async function fetchAndCacheTrail(id) {
|
|
176
|
+
const token = resolveToken();
|
|
177
|
+
if (!token)
|
|
178
|
+
exitWithTokenError();
|
|
179
|
+
const url = `${BASE_URL}/api/trails/by-id/${encodeURIComponent(id)}`;
|
|
180
|
+
let response;
|
|
181
|
+
try {
|
|
182
|
+
response = await fetch(url, {
|
|
183
|
+
headers: {
|
|
184
|
+
Authorization: `Bearer ${token}`,
|
|
185
|
+
Accept: 'application/json',
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
process.stderr.write(`Network error fetching trail: ${err.message}\n`);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
process.stderr.write(`${await describeHttpError(response)}\n`);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
const body = await response.text();
|
|
198
|
+
let cachePath;
|
|
199
|
+
try {
|
|
200
|
+
const parsed = JSON.parse(body);
|
|
201
|
+
cachePath = trailCache.write(id, body, parsed).path;
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// Body wasn't JSON or the cache write failed. Continue without a cache hit
|
|
205
|
+
// for this call — the caller's contract is "you get the body."
|
|
206
|
+
cachePath = '';
|
|
207
|
+
}
|
|
208
|
+
return { body, cachePath };
|
|
209
|
+
}
|
|
210
|
+
async function fetchTrail(input) {
|
|
211
|
+
const id = parseTrailId(input);
|
|
212
|
+
if (!id) {
|
|
213
|
+
process.stderr.write('Invalid trail id\n');
|
|
214
|
+
process.exit(2);
|
|
215
|
+
}
|
|
216
|
+
const { body } = await fetchAndCacheTrail(id);
|
|
217
|
+
process.stdout.write(body);
|
|
218
|
+
if (!body.endsWith('\n'))
|
|
219
|
+
process.stdout.write('\n');
|
|
220
|
+
}
|
|
221
|
+
async function publishTrail(file, options) {
|
|
222
|
+
const { payload } = await readPayload(file);
|
|
223
|
+
const payloadId = typeof payload === 'object' && payload !== null
|
|
224
|
+
? payload.id
|
|
225
|
+
: undefined;
|
|
226
|
+
if (typeof payloadId === 'string' && RESERVED_TRAIL_IDS.has(payloadId)) {
|
|
227
|
+
process.stderr.write(`Trail id '${payloadId}' is reserved (collides with \`principal-ai trail ${payloadId}\`).\n`);
|
|
228
|
+
process.exit(2);
|
|
229
|
+
}
|
|
230
|
+
validateRepoPurls(payload);
|
|
231
|
+
const { owner, repo } = resolveOwnerRepo(payload, options.owner, options.repo, options.purl);
|
|
232
|
+
const token = resolveToken();
|
|
233
|
+
if (!token)
|
|
234
|
+
exitWithTokenError();
|
|
235
|
+
let response;
|
|
236
|
+
try {
|
|
237
|
+
response = await fetch(`${BASE_URL}/api/trails`, {
|
|
238
|
+
method: 'POST',
|
|
239
|
+
headers: {
|
|
240
|
+
Authorization: `Bearer ${token}`,
|
|
241
|
+
'Content-Type': 'application/json',
|
|
242
|
+
Accept: 'application/json',
|
|
243
|
+
},
|
|
244
|
+
body: JSON.stringify({ owner, repo, payload }),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
process.stderr.write(`Network error publishing trail: ${err.message}\n`);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
if (!response.ok) {
|
|
252
|
+
process.stderr.write(`${await describeHttpError(response)}\n`);
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
const body = (await response.json());
|
|
256
|
+
if (!body.url) {
|
|
257
|
+
process.stderr.write('Server response missing share URL\n');
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
const shareUrl = body.url.startsWith('http') ? body.url : `${BASE_URL}${body.url}`;
|
|
261
|
+
process.stdout.write(`${shareUrl}\n`);
|
|
262
|
+
}
|
|
263
|
+
export function gitRemoteUrl(cwd) {
|
|
264
|
+
const result = spawnSync('git', ['-C', cwd, 'remote', 'get-url', 'origin'], {
|
|
265
|
+
encoding: 'utf8',
|
|
266
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
267
|
+
});
|
|
268
|
+
if (result.status !== 0)
|
|
269
|
+
return null;
|
|
270
|
+
return result.stdout.trim() || null;
|
|
271
|
+
}
|
|
272
|
+
export function ownerRepoFromGitRemote(remoteUrl) {
|
|
273
|
+
const match = remoteUrl.match(/[:/]([^/:]+)\/([^/]+?)(?:\.git)?\/?$/);
|
|
274
|
+
if (!match)
|
|
275
|
+
return null;
|
|
276
|
+
return { owner: match[1], name: match[2] };
|
|
277
|
+
}
|
|
278
|
+
function resolveTrailRepo(parsed) {
|
|
279
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
280
|
+
return null;
|
|
281
|
+
// web-ade wraps the payload as { entry, owner, repo, payload }. Local files
|
|
282
|
+
// are just the bare TrailPayload. Look in both shapes.
|
|
283
|
+
const wrapper = parsed;
|
|
284
|
+
const inner = (wrapper.payload ?? parsed);
|
|
285
|
+
// Prefer the explicit per-repo entry (carries Purl + authoredAtSha).
|
|
286
|
+
const entry = inner.repos?.[0];
|
|
287
|
+
if (entry?.remote?.owner && entry.remote.name && entry.id) {
|
|
288
|
+
return {
|
|
289
|
+
owner: entry.remote.owner,
|
|
290
|
+
name: entry.remote.name,
|
|
291
|
+
purl: entry.id,
|
|
292
|
+
authoredAtSha: entry.authoredAtSha,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
// Fallback: web-ade wrapper top-level owner/repo. Synthesize a Purl since the
|
|
296
|
+
// wrapper doesn't carry one. authoredAtSha is unavailable in this shape — the
|
|
297
|
+
// viewer will fall back to inner.authoredAt.sha when fetching slices.
|
|
298
|
+
if (wrapper.owner && wrapper.repo) {
|
|
299
|
+
return {
|
|
300
|
+
owner: wrapper.owner,
|
|
301
|
+
name: wrapper.repo,
|
|
302
|
+
purl: `pkg:github/${wrapper.owner}/${wrapper.repo}`,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
function cwdIsCloneOf(repo) {
|
|
308
|
+
const url = gitRemoteUrl(process.cwd());
|
|
309
|
+
if (!url)
|
|
310
|
+
return false;
|
|
311
|
+
const parsed = ownerRepoFromGitRemote(url);
|
|
312
|
+
if (!parsed)
|
|
313
|
+
return false;
|
|
314
|
+
return (parsed.owner.toLowerCase() === repo.owner.toLowerCase() &&
|
|
315
|
+
parsed.name.toLowerCase() === repo.name.toLowerCase());
|
|
316
|
+
}
|
|
317
|
+
// Shared with the `tour` command — both launch the same standalone viewer
|
|
318
|
+
// bundle; only the payload (trail vs *.tour.json) differs.
|
|
319
|
+
export function resolveViewerLaunch(flag) {
|
|
320
|
+
const result = tryResolveViewerLaunch(flag);
|
|
321
|
+
if (!result.ok) {
|
|
322
|
+
process.stderr.write(result.error + '\n');
|
|
323
|
+
process.exit(2);
|
|
324
|
+
}
|
|
325
|
+
return result.launch;
|
|
326
|
+
}
|
|
327
|
+
/** Soft resolve for callers that can fall back (e.g. subsystem-model create). */
|
|
328
|
+
export function tryResolveViewerLaunch(flag) {
|
|
329
|
+
// 1) Explicit override (flag or env) wins. If it points at a source tree we
|
|
330
|
+
// use `bun start`; if it points at a published install root we use its
|
|
331
|
+
// bin shim. Distinguished by whether `bin/subsystems-studio.cjs` exists.
|
|
332
|
+
const candidate = flag ?? process.env['PRINCIPAL_STUDIO_DIR'];
|
|
333
|
+
if (candidate) {
|
|
334
|
+
const overrideBin = `${candidate}/bin/subsystems-studio.cjs`;
|
|
335
|
+
if (existsSync(overrideBin))
|
|
336
|
+
return { ok: true, launch: { kind: 'installed', bin: overrideBin } };
|
|
337
|
+
if (existsSync(`${candidate}/package.json`))
|
|
338
|
+
return { ok: true, launch: { kind: 'source', dir: candidate } };
|
|
339
|
+
return {
|
|
340
|
+
ok: false,
|
|
341
|
+
error: `No principal-studio at ${candidate}; expected a package dir or installed root.`,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
// 2) Try the installed @principal-ai/subsystems-studio optionalDependency.
|
|
345
|
+
try {
|
|
346
|
+
const bin = cliRequire.resolve('@principal-ai/subsystems-studio/bin/subsystems-studio.cjs');
|
|
347
|
+
return { ok: true, launch: { kind: 'installed', bin } };
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
// not installed (different platform, install skipped, etc.)
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
ok: false,
|
|
354
|
+
error: '@principal-ai/subsystems-studio is not installed for this platform. Currently only macOS arm64 prebuilds are shipped — pass --viewer-dir <path> to a source checkout if you have one.',
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
async function resolveTrailFromFile(filePath) {
|
|
358
|
+
const absolute = filePath.startsWith('/') ? filePath : `${process.cwd()}/${filePath}`;
|
|
359
|
+
if (!existsSync(absolute)) {
|
|
360
|
+
process.stderr.write(`Trail file not found: ${absolute}\n`);
|
|
361
|
+
process.exit(2);
|
|
362
|
+
}
|
|
363
|
+
let body;
|
|
364
|
+
try {
|
|
365
|
+
body = readFileSync(absolute, 'utf8');
|
|
366
|
+
}
|
|
367
|
+
catch (err) {
|
|
368
|
+
process.stderr.write(`Failed to read ${absolute}: ${err.message}\n`);
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
let payload;
|
|
372
|
+
try {
|
|
373
|
+
payload = JSON.parse(body);
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
process.stderr.write(`Trail file is not valid JSON: ${err.message}\n`);
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
return { trailFile: absolute, payload, label: absolute };
|
|
380
|
+
}
|
|
381
|
+
async function resolveTrailFromId(input, refresh) {
|
|
382
|
+
const id = parseTrailId(input);
|
|
383
|
+
if (!id) {
|
|
384
|
+
process.stderr.write('Invalid trail id\n');
|
|
385
|
+
process.exit(2);
|
|
386
|
+
}
|
|
387
|
+
let body;
|
|
388
|
+
let cachePath;
|
|
389
|
+
const cached = refresh ? null : trailCache.read(id, ONE_HOUR_MS);
|
|
390
|
+
if (cached) {
|
|
391
|
+
body = cached.body;
|
|
392
|
+
cachePath = cached.path;
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
const fetched = await fetchAndCacheTrail(id);
|
|
396
|
+
body = fetched.body;
|
|
397
|
+
cachePath = fetched.cachePath;
|
|
398
|
+
if (!cachePath) {
|
|
399
|
+
process.stderr.write('Trail JSON could not be cached; aborting.\n');
|
|
400
|
+
process.exit(1);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
let payload;
|
|
404
|
+
try {
|
|
405
|
+
payload = JSON.parse(body);
|
|
406
|
+
}
|
|
407
|
+
catch (err) {
|
|
408
|
+
process.stderr.write(`Trail body is not valid JSON: ${err.message}\n`);
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
return { trailFile: cachePath, payload, label: id };
|
|
412
|
+
}
|
|
413
|
+
async function viewTrail(input, options) {
|
|
414
|
+
if (!input && !options.file) {
|
|
415
|
+
process.stderr.write('Pass an id/url or --file <path>.\n');
|
|
416
|
+
process.exit(2);
|
|
417
|
+
}
|
|
418
|
+
if (input && options.file) {
|
|
419
|
+
process.stderr.write('Pass either an id/url OR --file, not both.\n');
|
|
420
|
+
process.exit(2);
|
|
421
|
+
}
|
|
422
|
+
// Prefer a running desktop app for a bare `trail view <id>`: its bridge reads
|
|
423
|
+
// the same on-disk trail store, so a *local* id opens in the app without a
|
|
424
|
+
// remote fetch or GitHub token (the path resolveTrailFromId would otherwise
|
|
425
|
+
// take). Skipped when the caller pinned the standalone principal-studio — either
|
|
426
|
+
// explicitly (--viewer / --viewer-dir) or implicitly via a mode/working-tree
|
|
427
|
+
// flag (--remote, --local/--repo-root) or --file. A miss — app down, or trail
|
|
428
|
+
// not in its store — falls through to the existing fetch + socket/spawn path.
|
|
429
|
+
const pinsViewer = options.viewer === true ||
|
|
430
|
+
options.viewerDir !== undefined ||
|
|
431
|
+
options.remote === true ||
|
|
432
|
+
options.repoRoot !== undefined ||
|
|
433
|
+
options.local !== undefined;
|
|
434
|
+
if (input && !options.file && !pinsViewer) {
|
|
435
|
+
const id = parseTrailId(input);
|
|
436
|
+
if (id && (await handoffToBridge(id))) {
|
|
437
|
+
process.stderr.write(`Trail opened in running desktop app: ${id}\n`);
|
|
438
|
+
process.exit(0);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const resolved = options.file
|
|
442
|
+
? await resolveTrailFromFile(options.file)
|
|
443
|
+
: await resolveTrailFromId(input, options.refresh ?? false);
|
|
444
|
+
// Decide mode. For --file: default to local (the user has the file, presumably
|
|
445
|
+
// also the working tree). For id-based: auto-detect via cwd's git remote.
|
|
446
|
+
const repo = resolveTrailRepo(resolved.payload);
|
|
447
|
+
let mode;
|
|
448
|
+
let repoRoot;
|
|
449
|
+
if (options.remote) {
|
|
450
|
+
mode = 'remote';
|
|
451
|
+
}
|
|
452
|
+
else if (options.local !== undefined || options.repoRoot !== undefined) {
|
|
453
|
+
mode = 'local';
|
|
454
|
+
repoRoot =
|
|
455
|
+
options.repoRoot ??
|
|
456
|
+
(typeof options.local === 'string' ? options.local : process.cwd());
|
|
457
|
+
}
|
|
458
|
+
else if (options.file) {
|
|
459
|
+
// --file default: local mode anchored at cwd. The user will see a "no
|
|
460
|
+
// sourcePath matched" failure on click if cwd isn't actually the repo —
|
|
461
|
+
// that's the right signal for them to pass --repo-root.
|
|
462
|
+
mode = 'local';
|
|
463
|
+
repoRoot = process.cwd();
|
|
464
|
+
}
|
|
465
|
+
else if (repo && cwdIsCloneOf(repo)) {
|
|
466
|
+
mode = 'local';
|
|
467
|
+
repoRoot = process.cwd();
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
mode = 'remote';
|
|
471
|
+
}
|
|
472
|
+
const env = {
|
|
473
|
+
...process.env,
|
|
474
|
+
TRAIL_FILE: resolved.trailFile,
|
|
475
|
+
TRAIL_MODE: mode,
|
|
476
|
+
};
|
|
477
|
+
if (mode === 'local' && repoRoot)
|
|
478
|
+
env['TRAIL_REPO_ROOT'] = repoRoot;
|
|
479
|
+
if (mode === 'remote') {
|
|
480
|
+
const token = resolveToken();
|
|
481
|
+
if (token)
|
|
482
|
+
env['TRAIL_GH_TOKEN'] = token;
|
|
483
|
+
if (repo) {
|
|
484
|
+
env['TRAIL_REPO_OWNER'] = repo.owner;
|
|
485
|
+
env['TRAIL_REPO_NAME'] = repo.name;
|
|
486
|
+
env['TRAIL_REPO_PURL'] = repo.purl;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// Build the IPC message from the env we'd otherwise spawn with. If a viewer
|
|
490
|
+
// is already running, hand off and exit — no need to spawn a second process.
|
|
491
|
+
const ipcMessage = {
|
|
492
|
+
kind: 'LOAD_TRAIL',
|
|
493
|
+
trailFile: resolved.trailFile,
|
|
494
|
+
mode,
|
|
495
|
+
};
|
|
496
|
+
if (mode === 'local' && repoRoot)
|
|
497
|
+
ipcMessage.repoRoot = repoRoot;
|
|
498
|
+
if (mode === 'remote') {
|
|
499
|
+
if (env['TRAIL_GH_TOKEN'])
|
|
500
|
+
ipcMessage.ghToken = env['TRAIL_GH_TOKEN'];
|
|
501
|
+
if (env['TRAIL_REPO_OWNER'])
|
|
502
|
+
ipcMessage.repoOwner = env['TRAIL_REPO_OWNER'];
|
|
503
|
+
if (env['TRAIL_REPO_NAME'])
|
|
504
|
+
ipcMessage.repoName = env['TRAIL_REPO_NAME'];
|
|
505
|
+
if (env['TRAIL_REPO_PURL'])
|
|
506
|
+
ipcMessage.repoPurl = env['TRAIL_REPO_PURL'];
|
|
507
|
+
}
|
|
508
|
+
if (await handoffToRunning(ipcMessage)) {
|
|
509
|
+
process.stderr.write(`Trail handed off to running viewer (${mode} mode): ${resolved.label}\n`);
|
|
510
|
+
process.exit(0);
|
|
511
|
+
}
|
|
512
|
+
const launch = resolveViewerLaunch(options.viewerDir);
|
|
513
|
+
process.stderr.write(`Launching principal studio (${mode} mode) for ${resolved.label}\n`);
|
|
514
|
+
const child = launch.kind === 'installed'
|
|
515
|
+
? spawn(launch.bin, [], { env, stdio: 'inherit' })
|
|
516
|
+
: spawn('bun', ['start'], { cwd: launch.dir, env, stdio: 'inherit' });
|
|
517
|
+
child.on('error', (err) => {
|
|
518
|
+
process.stderr.write(`Failed to launch viewer: ${err.message}\n`);
|
|
519
|
+
process.exit(1);
|
|
520
|
+
});
|
|
521
|
+
child.on('exit', (code) => {
|
|
522
|
+
process.exit(code ?? 0);
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
async function listTrails(options) {
|
|
526
|
+
let githubId;
|
|
527
|
+
if (options.id) {
|
|
528
|
+
const parsed = Number(options.id);
|
|
529
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
530
|
+
process.stderr.write(`--id must be a positive number, got: ${options.id}\n`);
|
|
531
|
+
process.exit(2);
|
|
532
|
+
}
|
|
533
|
+
githubId = parsed;
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
// `--user` resolves login → numeric id via GitHub; the bare form
|
|
537
|
+
// resolves the authenticated user via /user. Both paths need a token.
|
|
538
|
+
const token = resolveToken();
|
|
539
|
+
if (!token)
|
|
540
|
+
exitWithTokenError();
|
|
541
|
+
const user = options.user
|
|
542
|
+
? await fetchGitHubUserByLogin(options.user, token)
|
|
543
|
+
: await fetchGitHubMe(token);
|
|
544
|
+
githubId = user.id;
|
|
545
|
+
}
|
|
546
|
+
let response;
|
|
547
|
+
try {
|
|
548
|
+
response = await fetch(`${BASE_URL}/api/trails/by-user/${githubId}`, {
|
|
549
|
+
headers: { Accept: 'application/json' },
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
catch (err) {
|
|
553
|
+
process.stderr.write(`Network error listing trails: ${err.message}\n`);
|
|
554
|
+
process.exit(1);
|
|
555
|
+
}
|
|
556
|
+
if (!response.ok) {
|
|
557
|
+
process.stderr.write(`${await describeHttpError(response)}\n`);
|
|
558
|
+
process.exit(1);
|
|
559
|
+
}
|
|
560
|
+
const body = await response.text();
|
|
561
|
+
process.stdout.write(body);
|
|
562
|
+
if (!body.endsWith('\n'))
|
|
563
|
+
process.stdout.write('\n');
|
|
564
|
+
}
|
|
565
|
+
export function createTrailCommand() {
|
|
566
|
+
const command = new Command('trail');
|
|
567
|
+
command
|
|
568
|
+
.description('Fetch or publish trails from the Principal ADE backend')
|
|
569
|
+
.argument('[id-or-url]', 'Trail id, or full https://app.principal-ade.com/trail/<id> URL (omit when using a sub-action)')
|
|
570
|
+
.action(async (input) => {
|
|
571
|
+
if (!input) {
|
|
572
|
+
command.help();
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
await fetchTrail(input);
|
|
576
|
+
});
|
|
577
|
+
command
|
|
578
|
+
.command('view')
|
|
579
|
+
.description('Open a trail in the standalone viewer (auto-picks local or remote mode)')
|
|
580
|
+
.argument('[id-or-url]', 'Trail id, or full https://app.principal-ade.com/trail/<id> URL (omit when using --file)')
|
|
581
|
+
.option('--file <path>', 'Open a local TrailPayload JSON file directly (skips fetch + cache)')
|
|
582
|
+
.option('--repo-root <path>', 'Working tree to resolve slices against; implies local mode')
|
|
583
|
+
.option('--refresh', 'Bypass the trail JSON cache and re-fetch')
|
|
584
|
+
.option('--remote', 'Force remote mode (fetch slices from GitHub even if a clone is present)')
|
|
585
|
+
.option('--local [path]', 'Force local mode; optional path overrides the repo root (default: cwd)')
|
|
586
|
+
.option('--viewer', 'Open in the standalone principal-studio instead of routing to a running desktop app')
|
|
587
|
+
.option('--viewer-dir <path>', 'Path to the @principal-ai/subsystems-studio package (overrides PRINCIPAL_STUDIO_DIR)')
|
|
588
|
+
.action(async (input, options) => {
|
|
589
|
+
await viewTrail(input, options);
|
|
590
|
+
});
|
|
591
|
+
command
|
|
592
|
+
.command('publish')
|
|
593
|
+
.description('Publish a slice/flow trail payload to web-ade')
|
|
594
|
+
.argument('[file]', 'Payload JSON file path; pass `-` or omit to read from stdin')
|
|
595
|
+
.option('--owner <owner>', 'GitHub owner (overrides payload `repos[0].remote.owner`)')
|
|
596
|
+
.option('--repo <repo>', 'GitHub repo (overrides payload `repos[0].remote.name`)')
|
|
597
|
+
.option('--purl <purl>', 'Anchor share by Purl (e.g. pkg:github/owner/repo); overrides payload-derived owner/repo')
|
|
598
|
+
.action(async (file, options) => {
|
|
599
|
+
await publishTrail(file, options);
|
|
600
|
+
});
|
|
601
|
+
command
|
|
602
|
+
.command('list')
|
|
603
|
+
.description('List trails published by a user (defaults to the authenticated user)')
|
|
604
|
+
.option('--user <login>', 'GitHub login to list trails for (resolves to numeric id)')
|
|
605
|
+
.option('--id <githubId>', 'GitHub numeric user id (skips the lookup)')
|
|
606
|
+
.action(async (options) => {
|
|
607
|
+
await listTrails(options);
|
|
608
|
+
});
|
|
609
|
+
// `open-web` (not `open`) keeps the existing `trail view` standalone-viewer
|
|
610
|
+
// surface unambiguous — this one launches the web app in a browser tab.
|
|
611
|
+
command
|
|
612
|
+
.command('open-web')
|
|
613
|
+
.description('Open the trail page in the default browser (web app, not the standalone viewer)')
|
|
614
|
+
.argument('<id-or-url>', 'Trail id, or full https://app.principal-ade.com/trail/<id> URL')
|
|
615
|
+
.action((input) => {
|
|
616
|
+
const id = parseTrailId(input);
|
|
617
|
+
const url = `${BASE_URL}/trail/${id}`;
|
|
618
|
+
process.stdout.write(`${url}\n`);
|
|
619
|
+
openInBrowser(url);
|
|
620
|
+
});
|
|
621
|
+
return command;
|
|
622
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate command - Comprehensive validation of all Principal View artifacts
|
|
3
|
+
*
|
|
4
|
+
* This command validates:
|
|
5
|
+
* - Canvas files (.canvas, .otel.canvas)
|
|
6
|
+
* - Workflow templates (.workflow.json)
|
|
7
|
+
* - Execution artifacts (.otel.json)
|
|
8
|
+
* - Component library (library.yaml)
|
|
9
|
+
*/
|
|
10
|
+
import { Command } from 'commander';
|
|
11
|
+
export declare function createValidateCommand(): Command;
|
|
12
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAi5FpC,wBAAgB,qBAAqB,IAAI,OAAO,CA69B/C"}
|