@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.
Files changed (184) hide show
  1. package/README.md +172 -0
  2. package/dist/commands/agent-session.d.ts +3 -0
  3. package/dist/commands/agent-session.d.ts.map +1 -0
  4. package/dist/commands/agent-session.js +67 -0
  5. package/dist/commands/agent-sessions.d.ts +11 -0
  6. package/dist/commands/agent-sessions.d.ts.map +1 -0
  7. package/dist/commands/agent-sessions.js +39 -0
  8. package/dist/commands/auxiliary/index.d.ts +3 -0
  9. package/dist/commands/auxiliary/index.d.ts.map +1 -0
  10. package/dist/commands/auxiliary/index.js +9 -0
  11. package/dist/commands/auxiliary/validate.d.ts +3 -0
  12. package/dist/commands/auxiliary/validate.d.ts.map +1 -0
  13. package/dist/commands/auxiliary/validate.js +123 -0
  14. package/dist/commands/collector/check.d.ts +6 -0
  15. package/dist/commands/collector/check.d.ts.map +1 -0
  16. package/dist/commands/collector/check.js +153 -0
  17. package/dist/commands/collector/diagnose.d.ts +6 -0
  18. package/dist/commands/collector/diagnose.d.ts.map +1 -0
  19. package/dist/commands/collector/diagnose.js +229 -0
  20. package/dist/commands/collector/index.d.ts +6 -0
  21. package/dist/commands/collector/index.d.ts.map +1 -0
  22. package/dist/commands/collector/index.js +16 -0
  23. package/dist/commands/collector/status.d.ts +6 -0
  24. package/dist/commands/collector/status.d.ts.map +1 -0
  25. package/dist/commands/collector/status.js +77 -0
  26. package/dist/commands/collector/utils.d.ts +92 -0
  27. package/dist/commands/collector/utils.d.ts.map +1 -0
  28. package/dist/commands/collector/utils.js +154 -0
  29. package/dist/commands/coverage.d.ts +9 -0
  30. package/dist/commands/coverage.d.ts.map +1 -0
  31. package/dist/commands/coverage.js +252 -0
  32. package/dist/commands/create.d.ts +6 -0
  33. package/dist/commands/create.d.ts.map +1 -0
  34. package/dist/commands/create.js +50 -0
  35. package/dist/commands/events/index.d.ts +3 -0
  36. package/dist/commands/events/index.d.ts.map +1 -0
  37. package/dist/commands/events/index.js +9 -0
  38. package/dist/commands/events/validate.d.ts +3 -0
  39. package/dist/commands/events/validate.d.ts.map +1 -0
  40. package/dist/commands/events/validate.js +124 -0
  41. package/dist/commands/formats.d.ts +6 -0
  42. package/dist/commands/formats.d.ts.map +1 -0
  43. package/dist/commands/formats.js +1118 -0
  44. package/dist/commands/hooks.d.ts +9 -0
  45. package/dist/commands/hooks.d.ts.map +1 -0
  46. package/dist/commands/hooks.js +295 -0
  47. package/dist/commands/inbox.d.ts +13 -0
  48. package/dist/commands/inbox.d.ts.map +1 -0
  49. package/dist/commands/inbox.js +179 -0
  50. package/dist/commands/init.d.ts +6 -0
  51. package/dist/commands/init.d.ts.map +1 -0
  52. package/dist/commands/init.js +220 -0
  53. package/dist/commands/lint.d.ts +11 -0
  54. package/dist/commands/lint.d.ts.map +1 -0
  55. package/dist/commands/lint.js +71 -0
  56. package/dist/commands/list.d.ts +6 -0
  57. package/dist/commands/list.d.ts.map +1 -0
  58. package/dist/commands/list.js +78 -0
  59. package/dist/commands/migrate-nodes.d.ts +9 -0
  60. package/dist/commands/migrate-nodes.d.ts.map +1 -0
  61. package/dist/commands/migrate-nodes.js +363 -0
  62. package/dist/commands/migrate-scopes-to-canvas.d.ts +3 -0
  63. package/dist/commands/migrate-scopes-to-canvas.d.ts.map +1 -0
  64. package/dist/commands/migrate-scopes-to-canvas.js +143 -0
  65. package/dist/commands/migration.d.ts +6 -0
  66. package/dist/commands/migration.d.ts.map +1 -0
  67. package/dist/commands/migration.js +247 -0
  68. package/dist/commands/open-studio.d.ts +10 -0
  69. package/dist/commands/open-studio.d.ts.map +1 -0
  70. package/dist/commands/open-studio.js +32 -0
  71. package/dist/commands/opencode/fetch.d.ts +3 -0
  72. package/dist/commands/opencode/fetch.d.ts.map +1 -0
  73. package/dist/commands/opencode/fetch.js +25 -0
  74. package/dist/commands/opencode/index.d.ts +3 -0
  75. package/dist/commands/opencode/index.d.ts.map +1 -0
  76. package/dist/commands/opencode/index.js +11 -0
  77. package/dist/commands/opencode/list-sessions.d.ts +3 -0
  78. package/dist/commands/opencode/list-sessions.d.ts.map +1 -0
  79. package/dist/commands/opencode/list-sessions.js +20 -0
  80. package/dist/commands/repo.d.ts +18 -0
  81. package/dist/commands/repo.d.ts.map +1 -0
  82. package/dist/commands/repo.js +233 -0
  83. package/dist/commands/schema.d.ts +6 -0
  84. package/dist/commands/schema.d.ts.map +1 -0
  85. package/dist/commands/schema.js +367 -0
  86. package/dist/commands/scopes/index.d.ts +3 -0
  87. package/dist/commands/scopes/index.d.ts.map +1 -0
  88. package/dist/commands/scopes/index.js +9 -0
  89. package/dist/commands/scopes/validate.d.ts +3 -0
  90. package/dist/commands/scopes/validate.d.ts.map +1 -0
  91. package/dist/commands/scopes/validate.js +187 -0
  92. package/dist/commands/setup.d.ts +6 -0
  93. package/dist/commands/setup.d.ts.map +1 -0
  94. package/dist/commands/setup.js +406 -0
  95. package/dist/commands/starred-collections.d.ts +12 -0
  96. package/dist/commands/starred-collections.d.ts.map +1 -0
  97. package/dist/commands/starred-collections.js +310 -0
  98. package/dist/commands/subsystem-model.d.ts +11 -0
  99. package/dist/commands/subsystem-model.d.ts.map +1 -0
  100. package/dist/commands/subsystem-model.js +249 -0
  101. package/dist/commands/topic.d.ts +15 -0
  102. package/dist/commands/topic.d.ts.map +1 -0
  103. package/dist/commands/topic.js +683 -0
  104. package/dist/commands/tour.d.ts +23 -0
  105. package/dist/commands/tour.d.ts.map +1 -0
  106. package/dist/commands/tour.js +783 -0
  107. package/dist/commands/trace/flow.d.ts +6 -0
  108. package/dist/commands/trace/flow.d.ts.map +1 -0
  109. package/dist/commands/trace/flow.js +158 -0
  110. package/dist/commands/trace/index.d.ts +6 -0
  111. package/dist/commands/trace/index.d.ts.map +1 -0
  112. package/dist/commands/trace/index.js +20 -0
  113. package/dist/commands/trace/inspect.d.ts +6 -0
  114. package/dist/commands/trace/inspect.d.ts.map +1 -0
  115. package/dist/commands/trace/inspect.js +187 -0
  116. package/dist/commands/trace/list.d.ts +6 -0
  117. package/dist/commands/trace/list.d.ts.map +1 -0
  118. package/dist/commands/trace/list.js +145 -0
  119. package/dist/commands/trace/registrations.d.ts +6 -0
  120. package/dist/commands/trace/registrations.d.ts.map +1 -0
  121. package/dist/commands/trace/registrations.js +81 -0
  122. package/dist/commands/trace/validate.d.ts +6 -0
  123. package/dist/commands/trace/validate.d.ts.map +1 -0
  124. package/dist/commands/trace/validate.js +276 -0
  125. package/dist/commands/trail.d.ts +36 -0
  126. package/dist/commands/trail.d.ts.map +1 -0
  127. package/dist/commands/trail.js +622 -0
  128. package/dist/commands/validate.d.ts +12 -0
  129. package/dist/commands/validate.d.ts.map +1 -0
  130. package/dist/commands/validate.js +3334 -0
  131. package/dist/commands/workflow/implementation.d.ts +3 -0
  132. package/dist/commands/workflow/implementation.d.ts.map +1 -0
  133. package/dist/commands/workflow/implementation.js +51 -0
  134. package/dist/commands/workflow/index.d.ts +3 -0
  135. package/dist/commands/workflow/index.d.ts.map +1 -0
  136. package/dist/commands/workflow/index.js +19 -0
  137. package/dist/commands/workflow/inspect.d.ts +3 -0
  138. package/dist/commands/workflow/inspect.d.ts.map +1 -0
  139. package/dist/commands/workflow/inspect.js +123 -0
  140. package/dist/commands/workflow/list.d.ts +3 -0
  141. package/dist/commands/workflow/list.d.ts.map +1 -0
  142. package/dist/commands/workflow/list.js +95 -0
  143. package/dist/commands/workflow/render.d.ts +3 -0
  144. package/dist/commands/workflow/render.d.ts.map +1 -0
  145. package/dist/commands/workflow/render.js +88 -0
  146. package/dist/commands/workflow/test.d.ts +3 -0
  147. package/dist/commands/workflow/test.d.ts.map +1 -0
  148. package/dist/commands/workflow/test.js +141 -0
  149. package/dist/commands/workflow/utils.d.ts +49 -0
  150. package/dist/commands/workflow/utils.d.ts.map +1 -0
  151. package/dist/commands/workflow/utils.js +164 -0
  152. package/dist/commands/workflow/validate.d.ts +3 -0
  153. package/dist/commands/workflow/validate.d.ts.map +1 -0
  154. package/dist/commands/workflow/validate.js +344 -0
  155. package/dist/file-utils.d.ts +15 -0
  156. package/dist/file-utils.d.ts.map +1 -0
  157. package/dist/file-utils.js +29 -0
  158. package/dist/index.cjs +285718 -0
  159. package/dist/index.cjs.map +7 -0
  160. package/dist/index.d.ts +8 -0
  161. package/dist/index.d.ts.map +1 -0
  162. package/dist/index.js +81 -0
  163. package/dist/lib/bridge-ipc.d.ts +29 -0
  164. package/dist/lib/bridge-ipc.d.ts.map +1 -0
  165. package/dist/lib/bridge-ipc.js +128 -0
  166. package/dist/lib/github-user.d.ts +17 -0
  167. package/dist/lib/github-user.d.ts.map +1 -0
  168. package/dist/lib/github-user.js +53 -0
  169. package/dist/lib/open-url.d.ts +11 -0
  170. package/dist/lib/open-url.d.ts.map +1 -0
  171. package/dist/lib/open-url.js +45 -0
  172. package/dist/lib/subsystem-model-store.d.ts +74 -0
  173. package/dist/lib/subsystem-model-store.d.ts.map +1 -0
  174. package/dist/lib/subsystem-model-store.js +292 -0
  175. package/dist/lib/tour-cache.d.ts +26 -0
  176. package/dist/lib/tour-cache.d.ts.map +1 -0
  177. package/dist/lib/tour-cache.js +46 -0
  178. package/dist/lib/trail-cache.d.ts +40 -0
  179. package/dist/lib/trail-cache.d.ts.map +1 -0
  180. package/dist/lib/trail-cache.js +97 -0
  181. package/dist/lib/viewer-ipc.d.ts +44 -0
  182. package/dist/lib/viewer-ipc.d.ts.map +1 -0
  183. package/dist/lib/viewer-ipc.js +68 -0
  184. package/package.json +68 -0
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Local subsystem-model store — same on-disk layout as Principal Studio:
3
+ * `~/.principal/subsystem-models/<id>.json` + `_index.json`.
4
+ *
5
+ * Lets the CLI create/list models without Studio's HTTP bridge running.
6
+ * Structural validation mirrors the Studio POST gate (construct / mechanism /
7
+ * throughlines / detail provenance). File/symbol verification is Studio-only
8
+ * for now and may be empty on CLI-created records until opened/updated there.
9
+ */
10
+ import { promises as fs } from 'node:fs';
11
+ import { homedir } from 'node:os';
12
+ import { join } from 'node:path';
13
+ const ROOT = join(homedir(), '.principal', 'subsystem-models');
14
+ const INDEX_PATH = join(ROOT, '_index.json');
15
+ export const SUBSYSTEM_EDGE_MECHANISMS = [
16
+ 'imports',
17
+ 'imports_from',
18
+ 're_exports',
19
+ 'defines',
20
+ 'calls',
21
+ 'extends',
22
+ 'inherits',
23
+ 'implements',
24
+ 'mixes_in',
25
+ 'uses',
26
+ 'method',
27
+ 'references',
28
+ 'contains',
29
+ 'feeds',
30
+ 'produces',
31
+ 'writes',
32
+ 'reads',
33
+ 'watches',
34
+ 'registers-into',
35
+ ];
36
+ export const SUBSYSTEM_COMPONENT_CONSTRUCTS = [
37
+ 'class',
38
+ 'function',
39
+ 'method',
40
+ 'interface',
41
+ 'type_alias',
42
+ 'enum',
43
+ 'store',
44
+ 'external',
45
+ ];
46
+ export const SUBSYSTEM_DETAIL_PROVENANCES = ['verified', 'authored'];
47
+ function graphId() {
48
+ const ts = Date.now();
49
+ const rand = Math.random().toString(36).slice(2, 11);
50
+ return `sg-${ts}-${rand}`;
51
+ }
52
+ function graphPath(id) {
53
+ return join(ROOT, `${id}.json`);
54
+ }
55
+ export function subsystemModelFilePath(id) {
56
+ return graphPath(id);
57
+ }
58
+ async function ensureDir() {
59
+ await fs.mkdir(ROOT, { recursive: true });
60
+ }
61
+ export function findEdgeMechanismProblems(edges) {
62
+ if (!Array.isArray(edges))
63
+ return [];
64
+ const problems = [];
65
+ for (const edge of edges) {
66
+ const e = edge;
67
+ if (typeof e?.mechanism === 'string' &&
68
+ SUBSYSTEM_EDGE_MECHANISMS.includes(e.mechanism)) {
69
+ continue;
70
+ }
71
+ problems.push(`edge ${JSON.stringify(e?.id ?? '<no id>')}: unknown mechanism ${JSON.stringify(e?.mechanism)} — allowed: ${SUBSYSTEM_EDGE_MECHANISMS.join(', ')}`);
72
+ }
73
+ return problems;
74
+ }
75
+ export function findThroughlineProblems(edges, throughlines) {
76
+ if (throughlines === undefined)
77
+ return [];
78
+ if (!Array.isArray(throughlines))
79
+ return ['throughlines must be an array'];
80
+ const edgeIds = new Set((Array.isArray(edges) ? edges : [])
81
+ .map((e) => e?.id)
82
+ .filter((id) => typeof id === 'string'));
83
+ const problems = [];
84
+ for (const tl of throughlines) {
85
+ const t = tl;
86
+ const label = JSON.stringify(t?.id ?? '<no id>');
87
+ if (typeof t?.id !== 'string' || !t.id.trim()) {
88
+ problems.push(`throughline ${label}: id is required`);
89
+ continue;
90
+ }
91
+ if (typeof t?.title !== 'string' || !t.title.trim()) {
92
+ problems.push(`throughline ${label}: title is required`);
93
+ }
94
+ if (!Array.isArray(t.steps)) {
95
+ problems.push(`throughline ${label}: steps array is required`);
96
+ continue;
97
+ }
98
+ t.steps.forEach((step, i) => {
99
+ const s = step;
100
+ if (typeof s?.edgeId !== 'string' || !edgeIds.has(s.edgeId)) {
101
+ problems.push(`throughline ${label}: step ${i} edgeId ${JSON.stringify(s?.edgeId ?? '<missing>')} does not match any edge in the graph`);
102
+ }
103
+ if (typeof s?.file !== 'string' || !s.file.trim()) {
104
+ problems.push(`throughline ${label}: step ${i} file is required (edgeId ${JSON.stringify(s?.edgeId ?? '<missing>')})`);
105
+ }
106
+ else if (typeof s?.line !== 'number' || !Number.isInteger(s.line) || s.line < 1) {
107
+ problems.push(`throughline ${label}: step ${i} line must be a positive 1-based integer (edgeId ${JSON.stringify(s?.edgeId ?? '<missing>')}, file ${JSON.stringify(s?.file)})`);
108
+ }
109
+ });
110
+ }
111
+ return problems;
112
+ }
113
+ export function findComponentConstructProblems(components) {
114
+ if (!Array.isArray(components))
115
+ return [];
116
+ const problems = [];
117
+ for (const component of components) {
118
+ const c = component;
119
+ if (typeof c?.construct === 'string' &&
120
+ SUBSYSTEM_COMPONENT_CONSTRUCTS.includes(c.construct)) {
121
+ continue;
122
+ }
123
+ problems.push(`component ${JSON.stringify(c?.id ?? '<no id>')}: invalid construct ${JSON.stringify(c?.construct)} — allowed: ${SUBSYSTEM_COMPONENT_CONSTRUCTS.join(', ')}. A module is its own subsystem: anchor to a concrete export (symbol + file), or publish it as a separate graph and reference it.`);
124
+ }
125
+ return problems;
126
+ }
127
+ export function findDetailProvenanceProblems(components) {
128
+ if (!Array.isArray(components))
129
+ return [];
130
+ const problems = [];
131
+ for (const component of components) {
132
+ const c = component;
133
+ if (!c || typeof c !== 'object' || !c['detail'])
134
+ continue;
135
+ const p = c['detailProvenance'];
136
+ if (p === undefined)
137
+ continue;
138
+ if (typeof p === 'string' && SUBSYSTEM_DETAIL_PROVENANCES.includes(p)) {
139
+ continue;
140
+ }
141
+ problems.push(`component ${JSON.stringify(String(c['id'] ?? '<no id>'))}: invalid detailProvenance ${JSON.stringify(p)} — allowed: ${SUBSYSTEM_DETAIL_PROVENANCES.join(', ')}. Hand-authored details must be "authored"; "verified" is reserved for tool-extracted data.`);
142
+ }
143
+ return problems;
144
+ }
145
+ export function normalizeDetailProvenance(components) {
146
+ if (!Array.isArray(components))
147
+ return;
148
+ for (const component of components) {
149
+ const c = component;
150
+ if (!c || typeof c !== 'object')
151
+ continue;
152
+ const detail = c['detail'];
153
+ if (!detail || typeof detail !== 'object') {
154
+ delete c['detailProvenance'];
155
+ continue;
156
+ }
157
+ const p = c['detailProvenance'];
158
+ if (p !== 'verified' && p !== 'authored')
159
+ c['detailProvenance'] = 'authored';
160
+ const kind = detail['kind'];
161
+ const arrays = {
162
+ function: ['parameters', 'callers', 'callees'],
163
+ method: ['parameters'],
164
+ class: ['methods', 'properties', 'extends', 'implements', 'instantiations', 'references'],
165
+ type: ['properties', 'usedBy', 'implementors'],
166
+ module: ['imports', 'exports', 'symbols'],
167
+ };
168
+ for (const key of arrays[String(kind)] ?? []) {
169
+ if (!Array.isArray(detail[key]))
170
+ detail[key] = [];
171
+ }
172
+ }
173
+ }
174
+ export function isRepoRoots(value) {
175
+ if (!value || typeof value !== 'object' || Array.isArray(value))
176
+ return false;
177
+ return Object.values(value).every((v) => typeof v === 'string');
178
+ }
179
+ /** Structural problems that would make Studio reject a POST (empty = valid). */
180
+ export function findCreateProblems(body) {
181
+ const problems = [];
182
+ if (!body['title'] || typeof body['title'] !== 'string') {
183
+ problems.push('title is required');
184
+ }
185
+ if (!Array.isArray(body['components'])) {
186
+ problems.push('components array is required');
187
+ }
188
+ if (!Array.isArray(body['edges'])) {
189
+ problems.push('edges array is required');
190
+ }
191
+ if (problems.length > 0)
192
+ return problems;
193
+ return [
194
+ ...findComponentConstructProblems(body['components']),
195
+ ...findDetailProvenanceProblems(body['components']),
196
+ ...findEdgeMechanismProblems(body['edges']),
197
+ ...findThroughlineProblems(body['edges'], body['throughlines']),
198
+ ];
199
+ }
200
+ function indexEntryFor(record) {
201
+ return {
202
+ id: record.id,
203
+ title: record.title,
204
+ description: record.description,
205
+ componentCount: record.components.length,
206
+ edgeCount: record.edges.length,
207
+ createdAt: record.createdAt,
208
+ updatedAt: record.updatedAt,
209
+ lastOpenedAt: record.lastOpenedAt,
210
+ fileName: `${record.id}.json`,
211
+ source: record.source,
212
+ repo: record.repo,
213
+ };
214
+ }
215
+ async function readIndex() {
216
+ try {
217
+ const raw = await fs.readFile(INDEX_PATH, 'utf8');
218
+ const idx = JSON.parse(raw);
219
+ if (idx.version === 1)
220
+ return idx.entries;
221
+ }
222
+ catch {
223
+ // missing / corrupt → rebuild
224
+ }
225
+ return rebuildIndex();
226
+ }
227
+ async function writeIndex(entries) {
228
+ await ensureDir();
229
+ const idx = { version: 1, entries };
230
+ await fs.writeFile(INDEX_PATH, JSON.stringify(idx, null, 2), 'utf8');
231
+ }
232
+ async function upsertIndexEntry(entry) {
233
+ const entries = await readIndex();
234
+ const idx = entries.findIndex((e) => e.id === entry.id);
235
+ if (idx >= 0)
236
+ entries[idx] = entry;
237
+ else
238
+ entries.push(entry);
239
+ await writeIndex(entries);
240
+ }
241
+ async function rebuildIndex() {
242
+ await ensureDir();
243
+ const entries = [];
244
+ let files;
245
+ try {
246
+ files = await fs.readdir(ROOT, { withFileTypes: true });
247
+ }
248
+ catch {
249
+ return entries;
250
+ }
251
+ for (const f of files) {
252
+ if (!f.isFile() || !f.name.endsWith('.json') || f.name === '_index.json')
253
+ continue;
254
+ try {
255
+ const raw = await fs.readFile(join(ROOT, f.name), 'utf8');
256
+ const graph = JSON.parse(raw);
257
+ entries.push(indexEntryFor(graph));
258
+ }
259
+ catch {
260
+ // skip corrupt files
261
+ }
262
+ }
263
+ entries.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
264
+ await writeIndex(entries);
265
+ return entries;
266
+ }
267
+ export async function listSubsystemModels() {
268
+ return readIndex();
269
+ }
270
+ export async function getSubsystemModel(id) {
271
+ try {
272
+ const raw = await fs.readFile(graphPath(id), 'utf8');
273
+ return JSON.parse(raw);
274
+ }
275
+ catch {
276
+ return null;
277
+ }
278
+ }
279
+ export async function createSubsystemModel(doc) {
280
+ await ensureDir();
281
+ normalizeDetailProvenance(doc.components);
282
+ const now = new Date().toISOString();
283
+ const record = {
284
+ ...doc,
285
+ id: graphId(),
286
+ createdAt: now,
287
+ updatedAt: now,
288
+ };
289
+ await fs.writeFile(graphPath(record.id), JSON.stringify(record, null, 2), 'utf8');
290
+ await upsertIndexEntry(indexEntryFor(record));
291
+ return record;
292
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Local cache for tour JSON fetched from web-ade.
3
+ *
4
+ * Sibling of `./trail-cache.ts`, but simpler: tours don't carry a `repos[]`
5
+ * Purl, and the only id we have on a read is the bare tour id, so the cache is
6
+ * a flat `~/.principal/tours/by-id/<id>.json` layout — writes and reads use the
7
+ * same slot, so a recent fetch is actually reused by the next `view`.
8
+ */
9
+ export interface CacheLocation {
10
+ /** Absolute filesystem path to the cached JSON. */
11
+ path: string;
12
+ }
13
+ /**
14
+ * Write a tour's raw JSON body to its cache slot. Stored verbatim — no
15
+ * re-serialization — so the cached file matches what the server returned.
16
+ */
17
+ export declare function write(id: string, body: string): CacheLocation;
18
+ /**
19
+ * Read a cached tour by id. Returns `null` when the file is missing or older
20
+ * than `maxAgeMs`. The caller fetches + writes a fresh copy on a miss.
21
+ */
22
+ export declare function read(id: string, maxAgeMs: number): {
23
+ body: string;
24
+ path: string;
25
+ } | null;
26
+ //# sourceMappingURL=tour-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tour-cache.d.ts","sourceRoot":"","sources":["../../src/lib/tour-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH,MAAM,WAAW,aAAa;IAC5B,mDAAmD;IACnD,IAAI,EAAE,MAAM,CAAC;CACd;AAMD;;;GAGG;AACH,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa,CAK7D;AAED;;;GAGG;AACH,wBAAgB,IAAI,CAClB,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,MAAM,GACf;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CASvC"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Local cache for tour JSON fetched from web-ade.
3
+ *
4
+ * Sibling of `./trail-cache.ts`, but simpler: tours don't carry a `repos[]`
5
+ * Purl, and the only id we have on a read is the bare tour id, so the cache is
6
+ * a flat `~/.principal/tours/by-id/<id>.json` layout — writes and reads use the
7
+ * same slot, so a recent fetch is actually reused by the next `view`.
8
+ */
9
+ import { mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
10
+ import { homedir } from 'node:os';
11
+ import { dirname, join } from 'node:path';
12
+ const ROOT = join(homedir(), '.principal', 'tours');
13
+ function slotFor(id) {
14
+ return join(ROOT, 'by-id', `${sanitizeSegment(id)}.json`);
15
+ }
16
+ /**
17
+ * Write a tour's raw JSON body to its cache slot. Stored verbatim — no
18
+ * re-serialization — so the cached file matches what the server returned.
19
+ */
20
+ export function write(id, body) {
21
+ const path = slotFor(id);
22
+ mkdirSync(dirname(path), { recursive: true });
23
+ writeFileSync(path, body, 'utf8');
24
+ return { path };
25
+ }
26
+ /**
27
+ * Read a cached tour by id. Returns `null` when the file is missing or older
28
+ * than `maxAgeMs`. The caller fetches + writes a fresh copy on a miss.
29
+ */
30
+ export function read(id, maxAgeMs) {
31
+ const path = slotFor(id);
32
+ try {
33
+ const stat = statSync(path);
34
+ if (Date.now() - stat.mtimeMs > maxAgeMs)
35
+ return null;
36
+ return { body: readFileSync(path, 'utf8'), path };
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ function sanitizeSegment(value) {
43
+ // Path traversal defence — keep alnum, dash, underscore, dot. Anything else
44
+ // becomes `_`. Empty results are rejected by the caller.
45
+ return value.replace(/[^A-Za-z0-9._-]/g, '_');
46
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Local cache for trail JSON fetched from web-ade.
3
+ *
4
+ * Layout — `~/.principal/trails/<purl-namespace>/<purl-name>/<id>.json` when the
5
+ * payload carries enough identity (single-repo `authoredAt`-style trails plus
6
+ * `repos[0]` Purl in multi-repo trails), or `~/.principal/trails/by-id/<id>.json`
7
+ * as a fallback for trails we can't anchor to a Purl.
8
+ *
9
+ * See `docs/PRINCIPAL_STUDIO_MODES.md` for the design context.
10
+ */
11
+ export interface CacheLocation {
12
+ /** Absolute filesystem path to the cached JSON. */
13
+ path: string;
14
+ /** `<purl-namespace>/<purl-name>` style anchor, or `by-id` for the fallback. */
15
+ anchor: string;
16
+ }
17
+ /**
18
+ * Decide where a trail's JSON should live on disk.
19
+ *
20
+ * Prefers `<purl-namespace>/<purl-name>/<id>.json` when the payload's
21
+ * `repos[0].id` parses; falls back to `by-id/<id>.json` otherwise. Only github,
22
+ * gitlab, bitbucket, and generic Purls participate in the hierarchical layout —
23
+ * other types fall through to `by-id` so we never write into surprising paths.
24
+ */
25
+ export declare function locate(id: string, payload: unknown): CacheLocation;
26
+ /**
27
+ * Write a trail's raw JSON body to its cache slot. The body is stored verbatim
28
+ * — no re-serialization — so the cached file matches what the server returned.
29
+ */
30
+ export declare function write(id: string, body: string, payload: unknown): CacheLocation;
31
+ /**
32
+ * Read a cached trail by id. Returns `null` when the file is missing or older
33
+ * than `maxAgeMs`. The caller is responsible for fetching + writing a fresh
34
+ * copy when this returns null.
35
+ */
36
+ export declare function read(id: string, maxAgeMs: number): {
37
+ body: string;
38
+ path: string;
39
+ } | null;
40
+ //# sourceMappingURL=trail-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trail-cache.d.ts","sourceRoot":"","sources":["../../src/lib/trail-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AASH,MAAM,WAAW,aAAa;IAC5B,mDAAmD;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,aAAa,CAwBlE;AAED;;;GAGG;AACH,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,aAAa,CAK/E;AAED;;;;GAIG;AACH,wBAAgB,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAcxF"}
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Local cache for trail JSON fetched from web-ade.
3
+ *
4
+ * Layout — `~/.principal/trails/<purl-namespace>/<purl-name>/<id>.json` when the
5
+ * payload carries enough identity (single-repo `authoredAt`-style trails plus
6
+ * `repos[0]` Purl in multi-repo trails), or `~/.principal/trails/by-id/<id>.json`
7
+ * as a fallback for trails we can't anchor to a Purl.
8
+ *
9
+ * See `docs/PRINCIPAL_STUDIO_MODES.md` for the design context.
10
+ */
11
+ import { mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
12
+ import { homedir } from 'node:os';
13
+ import { dirname, join } from 'node:path';
14
+ import { parsePurl } from '@principal-ai/alexandria-core-library';
15
+ const ROOT = join(homedir(), '.principal', 'trails');
16
+ /**
17
+ * Decide where a trail's JSON should live on disk.
18
+ *
19
+ * Prefers `<purl-namespace>/<purl-name>/<id>.json` when the payload's
20
+ * `repos[0].id` parses; falls back to `by-id/<id>.json` otherwise. Only github,
21
+ * gitlab, bitbucket, and generic Purls participate in the hierarchical layout —
22
+ * other types fall through to `by-id` so we never write into surprising paths.
23
+ */
24
+ export function locate(id, payload) {
25
+ const repoId = typeof payload === 'object' && payload !== null
26
+ ? payload.repos?.[0]?.id
27
+ : undefined;
28
+ if (typeof repoId === 'string') {
29
+ const parsed = parsePurl(repoId);
30
+ if (parsed && parsed.namespace) {
31
+ const ns = sanitizeSegment(parsed.namespace);
32
+ const name = sanitizeSegment(parsed.name);
33
+ if (ns && name) {
34
+ return {
35
+ path: join(ROOT, ns, name, `${sanitizeSegment(id)}.json`),
36
+ anchor: `${ns}/${name}`,
37
+ };
38
+ }
39
+ }
40
+ }
41
+ return {
42
+ path: join(ROOT, 'by-id', `${sanitizeSegment(id)}.json`),
43
+ anchor: 'by-id',
44
+ };
45
+ }
46
+ /**
47
+ * Write a trail's raw JSON body to its cache slot. The body is stored verbatim
48
+ * — no re-serialization — so the cached file matches what the server returned.
49
+ */
50
+ export function write(id, body, payload) {
51
+ const location = locate(id, payload);
52
+ mkdirSync(dirname(location.path), { recursive: true });
53
+ writeFileSync(location.path, body, 'utf8');
54
+ return location;
55
+ }
56
+ /**
57
+ * Read a cached trail by id. Returns `null` when the file is missing or older
58
+ * than `maxAgeMs`. The caller is responsible for fetching + writing a fresh
59
+ * copy when this returns null.
60
+ */
61
+ export function read(id, maxAgeMs) {
62
+ const safeId = sanitizeSegment(id);
63
+ const candidates = [join(ROOT, 'by-id', `${safeId}.json`)];
64
+ for (const candidate of candidates) {
65
+ const hit = readIfFresh(candidate, maxAgeMs);
66
+ if (hit)
67
+ return hit;
68
+ }
69
+ // Hierarchical layout: walk the namespace tree looking for the id. We don't
70
+ // know the (namespace, name) ahead of a successful fetch, so this is a small
71
+ // best-effort scan rather than a directed lookup. Cheap in practice — the
72
+ // tree is shallow per user.
73
+ return scanHierarchical(safeId, maxAgeMs);
74
+ }
75
+ function readIfFresh(path, maxAgeMs) {
76
+ try {
77
+ const stat = statSync(path);
78
+ if (Date.now() - stat.mtimeMs > maxAgeMs)
79
+ return null;
80
+ return { body: readFileSync(path, 'utf8'), path };
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ }
86
+ function scanHierarchical(safeId, maxAgeMs) {
87
+ // Implemented as a separate function so the common `by-id` hit doesn't pay
88
+ // the readdir cost. Filled in once we have a real consumer (step 5).
89
+ void safeId;
90
+ void maxAgeMs;
91
+ return null;
92
+ }
93
+ function sanitizeSegment(value) {
94
+ // Path traversal defence — keep alnum, dash, underscore, dot. Anything else
95
+ // becomes `_`. Empty results are rejected by the caller.
96
+ return value.replace(/[^A-Za-z0-9._-]/g, '_');
97
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Client-side helpers for talking to a running principal-studio instance.
3
+ *
4
+ * Wire format mirrors `packages/subsystems-studio/src/bun/ipc.ts`: Unix domain
5
+ * socket at `~/.principal/principal-studio.sock`, single line-delimited JSON
6
+ * message per connection, single line-delimited JSON response.
7
+ *
8
+ * Duplicated rather than shared because cli and principal-studio are independent
9
+ * npm packages — the wire protocol is the contract, this module enforces it
10
+ * from the producer side.
11
+ */
12
+ export declare const SOCKET_PATH: string;
13
+ export interface LoadTrailMessage {
14
+ kind: 'LOAD_TRAIL';
15
+ trailFile: string;
16
+ mode: 'local' | 'remote';
17
+ repoRoot?: string;
18
+ ghToken?: string;
19
+ repoOwner?: string;
20
+ repoName?: string;
21
+ repoPurl?: string;
22
+ }
23
+ export interface ActivateTabMessage {
24
+ kind: 'ACTIVATE_TAB';
25
+ /** Permanent tab id to switch to (e.g. `agent-sessions`). */
26
+ tabId: string;
27
+ }
28
+ /** Bring a running Studio window forward without changing the active tab. */
29
+ export interface FocusMessage {
30
+ kind: 'FOCUS';
31
+ }
32
+ /** Open a stored subsystem model tab (id under ~/.principal/subsystem-models/). */
33
+ export interface LoadSubsystemModelMessage {
34
+ kind: 'LOAD_SUBSYSTEM_GRAPH';
35
+ graphId: string;
36
+ }
37
+ export type ViewerIpcMessage = LoadTrailMessage | ActivateTabMessage | FocusMessage | LoadSubsystemModelMessage;
38
+ /**
39
+ * Try to hand off a message to a running viewer. Returns true if the running
40
+ * viewer accepted; caller can exit 0 without spawning. Returns false if no
41
+ * viewer is reachable.
42
+ */
43
+ export declare function handoffToRunning(message: ViewerIpcMessage): Promise<boolean>;
44
+ //# sourceMappingURL=viewer-ipc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewer-ipc.d.ts","sourceRoot":"","sources":["../../src/lib/viewer-ipc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAOH,eAAO,MAAM,WAAW,QAAyD,CAAC;AAElF,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,YAAY,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,OAAO,GAAG,QAAQ,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,cAAc,CAAC;IACrB,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;CACf;AAED,6EAA6E;AAC7E,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,CAAC;CACf;AAED,mFAAmF;AACnF,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,sBAAsB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,gBAAgB,GACxB,gBAAgB,GAChB,kBAAkB,GAClB,YAAY,GACZ,yBAAyB,CAAC;AAI9B;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,CA2ClF"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Client-side helpers for talking to a running principal-studio instance.
3
+ *
4
+ * Wire format mirrors `packages/subsystems-studio/src/bun/ipc.ts`: Unix domain
5
+ * socket at `~/.principal/principal-studio.sock`, single line-delimited JSON
6
+ * message per connection, single line-delimited JSON response.
7
+ *
8
+ * Duplicated rather than shared because cli and principal-studio are independent
9
+ * npm packages — the wire protocol is the contract, this module enforces it
10
+ * from the producer side.
11
+ */
12
+ import { existsSync, unlinkSync } from 'node:fs';
13
+ import { homedir } from 'node:os';
14
+ import { join } from 'node:path';
15
+ import { createConnection } from 'node:net';
16
+ export const SOCKET_PATH = join(homedir(), '.principal', 'principal-studio.sock');
17
+ const CONNECT_TIMEOUT_MS = 500;
18
+ /**
19
+ * Try to hand off a message to a running viewer. Returns true if the running
20
+ * viewer accepted; caller can exit 0 without spawning. Returns false if no
21
+ * viewer is reachable.
22
+ */
23
+ export async function handoffToRunning(message) {
24
+ if (!existsSync(SOCKET_PATH))
25
+ return false;
26
+ return new Promise((resolve) => {
27
+ const client = createConnection(SOCKET_PATH);
28
+ const timer = setTimeout(() => {
29
+ client.destroy();
30
+ resolve(false);
31
+ }, CONNECT_TIMEOUT_MS);
32
+ let buffer = '';
33
+ client.on('error', (err) => {
34
+ clearTimeout(timer);
35
+ // Stale socket file: the viewer died without cleaning up its `.sock`, so
36
+ // connecting refuses (ECONNREFUSED). Unlink it so it doesn't linger and
37
+ // cause repeated connect attempts/hangs, and so a fresh viewer can bind
38
+ // the path cleanly on spawn.
39
+ if (err.code === 'ECONNREFUSED') {
40
+ try {
41
+ unlinkSync(SOCKET_PATH);
42
+ }
43
+ catch {
44
+ // already gone, or a racing viewer re-bound it — nothing to do.
45
+ }
46
+ }
47
+ resolve(false);
48
+ });
49
+ client.on('data', (chunk) => {
50
+ buffer += chunk.toString('utf8');
51
+ const newline = buffer.indexOf('\n');
52
+ if (newline === -1)
53
+ return;
54
+ clearTimeout(timer);
55
+ try {
56
+ const response = JSON.parse(buffer.slice(0, newline));
57
+ resolve(response.ok === true);
58
+ }
59
+ catch {
60
+ resolve(false);
61
+ }
62
+ client.end();
63
+ });
64
+ client.on('connect', () => {
65
+ client.write(`${JSON.stringify(message)}\n`);
66
+ });
67
+ });
68
+ }