@synap-core/cli 1.10.0 → 1.11.0
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/dist/commands/capability.js +79 -8
- package/dist/commands/capability.js.map +1 -1
- package/dist/commands/centrality.js +2 -1
- package/dist/commands/centrality.js.map +1 -1
- package/dist/commands/context.js +2 -1
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/data-extra.js +7 -4
- package/dist/commands/data-extra.js.map +1 -1
- package/dist/commands/data.js +14 -4
- package/dist/commands/data.js.map +1 -1
- package/dist/commands/diagnose.js +44 -1
- package/dist/commands/diagnose.js.map +1 -1
- package/dist/commands/digest.js +2 -1
- package/dist/commands/digest.js.map +1 -1
- package/dist/commands/doc.d.ts +14 -0
- package/dist/commands/doc.js +61 -4
- package/dist/commands/doc.js.map +1 -1
- package/dist/commands/doctor.d.ts +21 -0
- package/dist/commands/doctor.js +189 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/events.js +3 -1
- package/dist/commands/events.js.map +1 -1
- package/dist/commands/graph.js +5 -3
- package/dist/commands/graph.js.map +1 -1
- package/dist/commands/import.js +40 -9
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/knowledge.d.ts +6 -0
- package/dist/commands/knowledge.js +253 -30
- package/dist/commands/knowledge.js.map +1 -1
- package/dist/commands/launch.js +2 -2
- package/dist/commands/launch.js.map +1 -1
- package/dist/commands/lens.js +1 -1
- package/dist/commands/lens.js.map +1 -1
- package/dist/commands/market-authoring.d.ts +45 -0
- package/dist/commands/market-authoring.js +225 -0
- package/dist/commands/market-authoring.js.map +1 -0
- package/dist/commands/market.d.ts +22 -7
- package/dist/commands/market.js +335 -60
- package/dist/commands/market.js.map +1 -1
- package/dist/commands/mcp.d.ts +24 -2
- package/dist/commands/mcp.js +84 -3
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/pod.js +1 -1
- package/dist/commands/pod.js.map +1 -1
- package/dist/commands/pods.d.ts +14 -0
- package/dist/commands/pods.js +156 -2
- package/dist/commands/pods.js.map +1 -1
- package/dist/commands/proposals-actions.js +3 -0
- package/dist/commands/proposals-actions.js.map +1 -1
- package/dist/commands/sessions.js +2 -1
- package/dist/commands/sessions.js.map +1 -1
- package/dist/commands/skills.js +3 -1
- package/dist/commands/skills.js.map +1 -1
- package/dist/commands/template.d.ts +42 -0
- package/dist/commands/template.js +191 -0
- package/dist/commands/template.js.map +1 -0
- package/dist/commands/upload.d.ts +30 -0
- package/dist/commands/upload.js +155 -0
- package/dist/commands/upload.js.map +1 -0
- package/dist/index.js +118 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/capture-lane.js +5 -3
- package/dist/lib/capture-lane.js.map +1 -1
- package/dist/lib/capture-structure.d.ts +123 -0
- package/dist/lib/capture-structure.js +57 -0
- package/dist/lib/capture-structure.js.map +1 -0
- package/dist/lib/cp-packages.d.ts +56 -0
- package/dist/lib/cp-packages.js +171 -0
- package/dist/lib/cp-packages.js.map +1 -1
- package/dist/lib/hub-client.d.ts +14 -1
- package/dist/lib/hub-client.js +110 -19
- package/dist/lib/hub-client.js.map +1 -1
- package/dist/lib/id.d.ts +23 -0
- package/dist/lib/id.js +35 -0
- package/dist/lib/id.js.map +1 -0
- package/dist/lib/pod.d.ts +53 -3
- package/dist/lib/pod.js +81 -9
- package/dist/lib/pod.js.map +1 -1
- package/dist/lib/template-file.d.ts +74 -0
- package/dist/lib/template-file.js +140 -0
- package/dist/lib/template-file.js.map +1 -0
- package/node_modules/@synap/hub-rest-client/dist/index.cjs +1261 -0
- package/node_modules/@synap/hub-rest-client/dist/index.d.cts +1647 -0
- package/node_modules/@synap/hub-rest-client/dist/index.d.ts +1647 -0
- package/node_modules/@synap/hub-rest-client/dist/index.js +1231 -0
- package/node_modules/@synap/hub-rest-client/package.json +45 -0
- package/node_modules/@synap/hub-rest-client/src/client.ts +1631 -0
- package/node_modules/@synap/hub-rest-client/src/errors.ts +30 -0
- package/node_modules/@synap/hub-rest-client/src/index.ts +153 -0
- package/node_modules/@synap/hub-rest-client/src/setup.ts +133 -0
- package/node_modules/@synap/hub-rest-client/src/types.ts +1299 -0
- package/package.json +3 -2
- package/skills/agent-os/SKILL.md +6 -0
- package/skills/manifest.json +1 -1
- package/skills/onboard/SKILL.md +7 -0
- package/skills/synap/SKILL.md +110 -29
- package/skills/synap/capture.md +50 -152
- package/skills/synap/common-mistakes.md +1 -0
- package/skills/synap/content-os.md +83 -0
- package/skills/synap/digesting-a-channel.md +25 -0
- package/skills/synap/mental-model.md +7 -7
- package/skills/synap/multi-entity-capture.md +6 -4
- package/skills/synap/worked-examples.md +21 -0
- package/skills/synap/writes.md +75 -18
- package/skills/synap-app/SKILL.md +319 -0
- package/skills/synap-app/_order.txt +5 -0
- package/skills/synap-app/_skill.yaml +20 -0
- package/skills/synap-app/creating-the-base-app.md +64 -0
- package/skills/synap-app/governance.md +42 -0
- package/skills/synap-app/install-commands.md +55 -0
- package/skills/synap-app/overview.md +50 -0
- package/skills/synap-app/using-the-sdk.md +73 -0
- package/skills/synap-market/SKILL.md +335 -0
- package/skills/synap-market/_order.txt +5 -0
- package/skills/synap-market/_skill.yaml +24 -0
- package/skills/synap-market/author-and-publish.md +75 -0
- package/skills/synap-market/discover-and-fetch.md +59 -0
- package/skills/synap-market/governance-and-catalog.md +56 -0
- package/skills/synap-market/install.md +55 -0
- package/skills/synap-market/overview.md +51 -0
package/dist/commands/market.js
CHANGED
|
@@ -14,19 +14,18 @@
|
|
|
14
14
|
* synap market install <slug> — install
|
|
15
15
|
* synap market update [slug] [--yes] [--json] — check/apply drift
|
|
16
16
|
*
|
|
17
|
-
* INSTALL SCOPE:
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* generic CLI→pod install for standalone non-workspace packages is not wired yet.
|
|
17
|
+
* INSTALL SCOPE: WORKSPACE-type packages install via `/packages/apply` (the
|
|
18
|
+
* same door `launch` uses), which provisions a workspace from the definition.
|
|
19
|
+
* Non-workspace kinds (capability / automation / cell / template) install via
|
|
20
|
+
* the pod's kind-agnostic `market.install` verb on `/capabilities/execute`,
|
|
21
|
+
* which resolves the definition from `cp_catalog_cache`. Types the verb doesn't
|
|
22
|
+
* handle fall back to a route-to-browser message.
|
|
24
23
|
*/
|
|
25
24
|
import ora from "ora";
|
|
26
25
|
import chalk from "chalk";
|
|
27
|
-
import { assembleCatalog, computeTemplateUpdates, isHashVersion, listPublicTemplates, listWorkspaceTemplates, toPackageDefinition, layerTemplateGraph, bundledEdgesOf, } from "@synap-core/workspace-templates";
|
|
26
|
+
import { assembleCatalog, computeTemplateUpdates, isHashVersion, listPublicTemplates, listWorkspaceTemplates, toPackageDefinition, layerTemplateGraph, bundledEdgesOf, groupByDomain, } from "@synap-core/workspace-templates";
|
|
28
27
|
import { log } from "../utils/logger.js";
|
|
29
|
-
import { resolveHubConfig, hubPost, renderHubError } from "../lib/hub-client.js";
|
|
28
|
+
import { resolveHubConfig, hubPost, renderHubError, HubError } from "../lib/hub-client.js";
|
|
30
29
|
import { fetchProjects } from "../lib/project.js";
|
|
31
30
|
import { writeGovernance } from "../lib/capture-lane.js";
|
|
32
31
|
import { renderNextSteps, FLOW } from "../lib/next-steps.js";
|
|
@@ -34,6 +33,7 @@ import { getStoredToken, isTokenLocallyExpired } from "../lib/auth.js";
|
|
|
34
33
|
import { fetchPublicBrowseRows, fetchMyRows, fetchAvailableSlugs, fetchPackageDefinition, } from "../lib/cp-packages.js";
|
|
35
34
|
import { fetchInstalledSlugs, fetchInstalledTemplates } from "../lib/installed.js";
|
|
36
35
|
import { isSuite } from "../lib/suite.js";
|
|
36
|
+
import { capabilityAdd } from "./capability.js";
|
|
37
37
|
import { bundledTemplatesVersion } from "../lib/bundle-version.js";
|
|
38
38
|
import { HubNetworkError } from "../lib/hub-client.js";
|
|
39
39
|
// Same longer budget launch uses — an apply writes profiles/views/entities.
|
|
@@ -116,6 +116,31 @@ export async function buildMarketCatalog(filters) {
|
|
|
116
116
|
function typeOf(entry) {
|
|
117
117
|
return entry.category ?? "workspace";
|
|
118
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Map a CP package `category` (what {@link typeOf} returns) → the pod
|
|
121
|
+
* `market.install` verb's `kind` vocabulary, or `null` when the verb can't
|
|
122
|
+
* install that type. The CP's live PACKAGE_TYPES and the pod's cache `kind`
|
|
123
|
+
* diverge (see cp-catalog-sync.ts): the CP's `workflow` is the pod's
|
|
124
|
+
* `automation`; the CP's `workspace` is the pod's `template` — but a workspace
|
|
125
|
+
* installs through the dedicated `/packages/apply` create path, so it never
|
|
126
|
+
* routes through here. `automation`/`template` are also accepted verbatim so a
|
|
127
|
+
* row already speaking the pod's cache vocabulary still resolves.
|
|
128
|
+
*/
|
|
129
|
+
export function marketInstallKind(type) {
|
|
130
|
+
switch (type) {
|
|
131
|
+
case "capability":
|
|
132
|
+
return "capability";
|
|
133
|
+
case "cell":
|
|
134
|
+
return "cell";
|
|
135
|
+
case "workflow":
|
|
136
|
+
case "automation":
|
|
137
|
+
return "automation";
|
|
138
|
+
case "template":
|
|
139
|
+
return "template";
|
|
140
|
+
default:
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
119
144
|
/**
|
|
120
145
|
* Client-side filter. The CP browse route filters REMOTE rows server-side, but
|
|
121
146
|
* `assembleCatalog` always adds the BUNDLED (all-`workspace`) templates — so a
|
|
@@ -135,24 +160,6 @@ function matchesFilters(entry, filters) {
|
|
|
135
160
|
}
|
|
136
161
|
return true;
|
|
137
162
|
}
|
|
138
|
-
/** Status badges — suite (tag) / private / installed / tier / locked. */
|
|
139
|
-
function badges(entry, cat) {
|
|
140
|
-
const out = [];
|
|
141
|
-
if (isSuite(entry))
|
|
142
|
-
out.push(chalk.magenta("suite"));
|
|
143
|
-
if (entry.isPrivate)
|
|
144
|
-
out.push(chalk.yellow("private"));
|
|
145
|
-
if (cat.installed.has(entry.slug))
|
|
146
|
-
out.push(chalk.green("installed"));
|
|
147
|
-
const tier = cat.tierBySlug.get(entry.slug);
|
|
148
|
-
if (tier?.requiredTier)
|
|
149
|
-
out.push(chalk.cyan(tier.requiredTier));
|
|
150
|
-
else if (tier?.pricingModel && tier.pricingModel !== "free")
|
|
151
|
-
out.push(chalk.cyan(tier.pricingModel));
|
|
152
|
-
if (cat.lockedSlugs.has(entry.slug))
|
|
153
|
-
out.push(chalk.red("locked"));
|
|
154
|
-
return out;
|
|
155
|
-
}
|
|
156
163
|
function clip(s, n) {
|
|
157
164
|
return s.length <= n ? s : s.slice(0, n - 1).trimEnd() + "…";
|
|
158
165
|
}
|
|
@@ -302,8 +309,12 @@ export async function market(opts) {
|
|
|
302
309
|
name: e.name,
|
|
303
310
|
description: e.description,
|
|
304
311
|
type: typeOf(e),
|
|
312
|
+
domain: e.domain ?? null,
|
|
305
313
|
tags: e.tags,
|
|
306
314
|
suite: isSuite(e),
|
|
315
|
+
overlay: isOverlay(e),
|
|
316
|
+
ridesBase: composeBaseOf(e.slug),
|
|
317
|
+
bundles: bundleCountOf(e.slug),
|
|
307
318
|
isPrivate: e.isPrivate,
|
|
308
319
|
installed: cat.installed.has(e.slug),
|
|
309
320
|
requiredTier: tier?.requiredTier ?? null,
|
|
@@ -325,42 +336,153 @@ export async function market(opts) {
|
|
|
325
336
|
log.dim("Try 'synap market' with no filter, or 'synap login' to see your private packages.");
|
|
326
337
|
return;
|
|
327
338
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
339
|
+
renderCatalog(entries, cat);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* The kind of composition a workspace template participates in, derived from its
|
|
343
|
+
* bundled dependency edges — NOT hardcoded. A `compose` edge = an OVERLAY that
|
|
344
|
+
* rides a base workspace (installing it bare is meaningless); a `require` edge
|
|
345
|
+
* on a suite = a workspace it BUNDLES. Non-bundled (private/CP-only) rows return
|
|
346
|
+
* `null` base and 0 bundles, which the renderer degrades gracefully.
|
|
347
|
+
*/
|
|
348
|
+
function composeBaseOf(slug) {
|
|
349
|
+
for (const dep of bundledEdgesOf(slug)) {
|
|
350
|
+
if (dep.relation === "compose")
|
|
351
|
+
return dep.slug;
|
|
352
|
+
}
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
function bundleCountOf(slug) {
|
|
356
|
+
return bundledEdgesOf(slug).filter((d) => d.relation === "require").length;
|
|
357
|
+
}
|
|
358
|
+
function isOverlay(entry) {
|
|
359
|
+
return typeOf(entry) === "workspace" && !isSuite(entry) && composeBaseOf(entry.slug) !== null;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Section-aware catalog renderer. Three axes carry distinct signal, so each gets
|
|
363
|
+
* its OWN visual channel instead of one flat colored word (the old list):
|
|
364
|
+
* KIND → the SECTION (Suites / Workspaces-by-domain / Overlays / other types)
|
|
365
|
+
* STATE → a leading ✓ glyph (installed)
|
|
366
|
+
* ACCESS → a dim right-hand tag (private / tier / locked)
|
|
367
|
+
* Name reads first (you scan it); the slug is the dim type-token (you type it).
|
|
368
|
+
*/
|
|
369
|
+
function renderCatalog(entries, cat) {
|
|
370
|
+
const width = process.stdout.columns ?? 100;
|
|
371
|
+
const NAME_W = 26;
|
|
372
|
+
const SLUG_W = 20;
|
|
373
|
+
// Resolve a base slug to its display name so an overlay names the WORKSPACE it
|
|
374
|
+
// extends, not its raw slug (e.g. "→ Brand Library", not "→ brand-library").
|
|
375
|
+
const nameBySlug = new Map(entries.map((e) => [e.slug, e.name]));
|
|
376
|
+
const accessTag = (entry) => {
|
|
377
|
+
const parts = [];
|
|
378
|
+
if (entry.isPrivate)
|
|
379
|
+
parts.push(chalk.yellow("private"));
|
|
380
|
+
const tier = cat.tierBySlug.get(entry.slug);
|
|
381
|
+
if (tier?.requiredTier)
|
|
382
|
+
parts.push(chalk.cyan(tier.requiredTier));
|
|
383
|
+
else if (tier?.pricingModel && tier.pricingModel !== "free")
|
|
384
|
+
parts.push(chalk.cyan(tier.pricingModel));
|
|
385
|
+
if (cat.lockedSlugs.has(entry.slug))
|
|
386
|
+
parts.push(chalk.red("locked"));
|
|
387
|
+
return parts.length ? " " + parts.join(" ") : "";
|
|
340
388
|
};
|
|
389
|
+
// One item = one line: glyph · bold name · default-weight slug (the token you
|
|
390
|
+
// TYPE — never dimmed) · dim detail · dim access.
|
|
391
|
+
const row = (entry, detail) => {
|
|
392
|
+
const glyph = cat.installed.has(entry.slug) ? chalk.green("✓") : " ";
|
|
393
|
+
const name = clip(entry.name, NAME_W).padEnd(NAME_W);
|
|
394
|
+
const slug = clip(entry.slug, SLUG_W).padEnd(SLUG_W);
|
|
395
|
+
const access = accessTag(entry);
|
|
396
|
+
const head = ` ${glyph} ${chalk.bold(name)} ${slug}`;
|
|
397
|
+
// Budget the detail to the remaining terminal width (never wrap/overflow).
|
|
398
|
+
const detailText = detail ?? entry.description ?? "";
|
|
399
|
+
const used = 2 + 1 + 1 + NAME_W + 1 + SLUG_W + 1;
|
|
400
|
+
const room = Math.max(12, width - used - stripLen(access) - 1);
|
|
401
|
+
console.log(head + " " + chalk.dim(clip(detailText, room)) + access);
|
|
402
|
+
};
|
|
403
|
+
// An overlay's identity IS "Name → Base Workspace" — the base it extends must
|
|
404
|
+
// read as part of what it IS, not be buried in the description. So it gets its
|
|
405
|
+
// own row shape: a combined, coloured identity column, then slug, then blurb.
|
|
406
|
+
const IDENT_W = 40;
|
|
407
|
+
const overlayRow = (entry) => {
|
|
408
|
+
const glyph = cat.installed.has(entry.slug) ? chalk.green("✓") : " ";
|
|
409
|
+
const baseName = clip(nameBySlug.get(composeBaseOf(entry.slug) ?? "") ?? composeBaseOf(entry.slug) ?? "", IDENT_W - 22 - 3 // leave room for "name → " inside IDENT_W
|
|
410
|
+
);
|
|
411
|
+
const nameText = clip(entry.name, 22);
|
|
412
|
+
const identityVisible = `${nameText} → ${baseName}`;
|
|
413
|
+
const identity = chalk.bold(nameText) + chalk.dim(" → ") + chalk.cyan(baseName);
|
|
414
|
+
const pad = " ".repeat(Math.max(1, IDENT_W - identityVisible.length));
|
|
415
|
+
const slug = clip(entry.slug, SLUG_W).padEnd(SLUG_W);
|
|
416
|
+
const access = accessTag(entry);
|
|
417
|
+
const used = 2 + 1 + 1 + IDENT_W + 1 + SLUG_W + 1;
|
|
418
|
+
const room = Math.max(10, width - used - stripLen(access) - 1);
|
|
419
|
+
console.log(` ${glyph} ${identity}${pad} ${slug} ` +
|
|
420
|
+
chalk.dim(clip(entry.description ?? "", room)) +
|
|
421
|
+
access);
|
|
422
|
+
};
|
|
423
|
+
// ── Suites — the headline: one install = a whole operating core ────────────
|
|
424
|
+
const suites = entries.filter((e) => isSuite(e));
|
|
341
425
|
if (suites.length > 0) {
|
|
342
|
-
log.heading(" Suites
|
|
426
|
+
log.heading(" Suites · a whole operating core in one install");
|
|
427
|
+
log.blank();
|
|
428
|
+
for (const e of suites) {
|
|
429
|
+
const n = bundleCountOf(e.slug);
|
|
430
|
+
row(e, n > 0 ? `bundles ${n} workspace${n === 1 ? "" : "s"}` : e.description ?? "");
|
|
431
|
+
}
|
|
343
432
|
log.blank();
|
|
344
|
-
suites.forEach(render);
|
|
345
433
|
}
|
|
346
|
-
//
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const
|
|
351
|
-
|
|
434
|
+
// ── Workspaces — standalone installs, grouped by authored DOMAIN ───────────
|
|
435
|
+
const workspaces = entries.filter((e) => typeOf(e) === "workspace" && !isSuite(e) && !isOverlay(e));
|
|
436
|
+
if (workspaces.length > 0) {
|
|
437
|
+
log.heading(" Workspaces");
|
|
438
|
+
for (const group of groupByDomain(workspaces, (e) => e.domain)) {
|
|
439
|
+
log.blank();
|
|
440
|
+
console.log(" " + chalk.underline(group.label));
|
|
441
|
+
for (const e of group.items)
|
|
442
|
+
row(e);
|
|
443
|
+
}
|
|
444
|
+
log.blank();
|
|
352
445
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
446
|
+
// ── Add-ons — each rides a workspace you already have (named in-line) ───────
|
|
447
|
+
const overlays = entries.filter((e) => isOverlay(e));
|
|
448
|
+
if (overlays.length > 0) {
|
|
449
|
+
log.heading(" Add-ons · each extends the workspace named after the →");
|
|
450
|
+
log.blank();
|
|
451
|
+
for (const e of overlays)
|
|
452
|
+
overlayRow(e);
|
|
356
453
|
log.blank();
|
|
357
|
-
byType.get(t).forEach(render);
|
|
358
454
|
}
|
|
359
|
-
|
|
455
|
+
// ── Other package types (capability / skill / view / cell) by type ─────────
|
|
456
|
+
const other = entries.filter((e) => typeOf(e) !== "workspace");
|
|
457
|
+
if (other.length > 0) {
|
|
458
|
+
const order = ["capability", "skill", "workflow", "view", "cell"];
|
|
459
|
+
const byType = new Map();
|
|
460
|
+
for (const e of other) {
|
|
461
|
+
const t = typeOf(e);
|
|
462
|
+
(byType.get(t) ?? byType.set(t, []).get(t)).push(e);
|
|
463
|
+
}
|
|
464
|
+
const types = [...byType.keys()].sort((a, b) => (order.indexOf(a) + 1 || 99) - (order.indexOf(b) + 1 || 99));
|
|
465
|
+
for (const t of types) {
|
|
466
|
+
log.heading(` ${t.charAt(0).toUpperCase() + t.slice(1)}`);
|
|
467
|
+
log.blank();
|
|
468
|
+
for (const e of byType.get(t))
|
|
469
|
+
row(e);
|
|
470
|
+
log.blank();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
// Footer: a scannable count line + progressive-disclosure hints.
|
|
474
|
+
const installedN = entries.filter((e) => cat.installed.has(e.slug)).length;
|
|
475
|
+
log.dim(`${installedN} installed · ${entries.length - installedN} available` +
|
|
476
|
+
(suites.length ? ` · ${suites.length} suite${suites.length === 1 ? "" : "s"}` : ""));
|
|
360
477
|
if (!cat.loggedIn)
|
|
361
478
|
log.dim("Log in to see your private packages: synap login");
|
|
362
479
|
renderNextSteps(FLOW.afterMarketList());
|
|
363
480
|
}
|
|
481
|
+
/** Visible length of a string with chalk color codes stripped — for width math. */
|
|
482
|
+
function stripLen(s) {
|
|
483
|
+
// eslint-disable-next-line no-control-regex
|
|
484
|
+
return s.replace(/\[[0-9;]*m/g, "").length;
|
|
485
|
+
}
|
|
364
486
|
// ── Optional --project link ──────────────────────────────────────────────────
|
|
365
487
|
/**
|
|
366
488
|
* Installs are POD-WIDE. A template provisions WORKSPACES (pod-level) and their
|
|
@@ -381,6 +503,67 @@ async function resolveOptionalProject(cfg, explicitId) {
|
|
|
381
503
|
}
|
|
382
504
|
return { projectId: match.id, name: match.name };
|
|
383
505
|
}
|
|
506
|
+
/** Human render of a preflight report — scannable summary + per-issue reasons. */
|
|
507
|
+
function renderPreflightReport(entry, r) {
|
|
508
|
+
// Hard structural failure — the definition itself won't install.
|
|
509
|
+
if (r.validationErrors.length > 0) {
|
|
510
|
+
log.error(`${entry.name} — ${r.validationErrors.length} structural error${r.validationErrors.length === 1 ? "" : "s"} (won't install):`);
|
|
511
|
+
for (const e of r.validationErrors)
|
|
512
|
+
log.hint(e);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const p = r.profiles;
|
|
516
|
+
// Every issue class, not just profile conflicts — the verdict and the printed
|
|
517
|
+
// warnings must agree (`r.ok` excludes advisory deferred/scopeConflicts, so
|
|
518
|
+
// "cleanly" alone could sit above a ⚠ line; count them all here).
|
|
519
|
+
const issueCounts = [];
|
|
520
|
+
if (p.conflicts.length)
|
|
521
|
+
issueCounts.push(`${p.conflicts.length} profile conflict${p.conflicts.length === 1 ? "" : "s"}`);
|
|
522
|
+
if (p.deferred.length)
|
|
523
|
+
issueCounts.push(`${p.deferred.length} duplicate${p.deferred.length === 1 ? "" : "s"}`);
|
|
524
|
+
if (p.scopeConflicts.length)
|
|
525
|
+
issueCounts.push(`${p.scopeConflicts.length} scope note${p.scopeConflicts.length === 1 ? "" : "s"}`);
|
|
526
|
+
if (r.entityLinks.unresolved.length)
|
|
527
|
+
issueCounts.push(`${r.entityLinks.unresolved.length} dropped link${r.entityLinks.unresolved.length === 1 ? "" : "s"}`);
|
|
528
|
+
if (r.views.wouldOrphan.length)
|
|
529
|
+
issueCounts.push(`${r.views.wouldOrphan.length} orphaned view${r.views.wouldOrphan.length === 1 ? "" : "s"}`);
|
|
530
|
+
const blocking = !r.ok; // conflicts / dropped links / orphaned views
|
|
531
|
+
const hasAny = issueCounts.length > 0;
|
|
532
|
+
// Verdict — never says "clean" if any warning will print below.
|
|
533
|
+
if (!hasAny)
|
|
534
|
+
log.success(`Preflight for ${entry.name}: no conflicts — safe to install.`);
|
|
535
|
+
else if (blocking)
|
|
536
|
+
log.warn(`Preflight for ${entry.name}: would install with issues — review before installing.`);
|
|
537
|
+
else
|
|
538
|
+
log.warn(`Preflight for ${entry.name}: installs, with ${issueCounts.length} advisory note${issueCounts.length === 1 ? "" : "s"} — review below.`);
|
|
539
|
+
log.dim(`Profiles: ${p.create.length} to create · ${p.reused.length} reused`);
|
|
540
|
+
if (hasAny)
|
|
541
|
+
log.dim(`Issues: ${issueCounts.join(" · ")}`);
|
|
542
|
+
// Warnings are the PAYLOAD of a dry-run — print them BEFORE the (potentially
|
|
543
|
+
// long) create/reuse list so they never scroll off-screen.
|
|
544
|
+
// The load-bearing signal for an AI author: a declared role/kind that clashes
|
|
545
|
+
// with an existing pod profile is SKIPPED — never created, never mutated.
|
|
546
|
+
for (const c of p.conflicts)
|
|
547
|
+
log.warn(`${c.slug}: declared ${c.declaredKind}, pod has ${c.existingKind} → SKIP (not created)`);
|
|
548
|
+
for (const d of p.deferred)
|
|
549
|
+
log.warn(`${d.slug}: same slug exists but not promotable (${d.reason}) → a duplicate would be created`);
|
|
550
|
+
for (const sc of p.scopeConflicts)
|
|
551
|
+
log.warn(`${sc.slug}: declared scope ${sc.declaredScope}, pod has ${sc.existingScope} → reused as-is (declared scope not applied)`);
|
|
552
|
+
for (const link of r.entityLinks.unresolved)
|
|
553
|
+
log.warn(`link ${link}: an endpoint profile is a conflict → relation dropped`);
|
|
554
|
+
for (const v of r.views.wouldOrphan)
|
|
555
|
+
log.warn(`view "${v}": its scope profile is a conflict → view would have no scope`);
|
|
556
|
+
// One actionable hint for the whole conflict/link/view class — the remedy the
|
|
557
|
+
// per-line messages don't give.
|
|
558
|
+
if (p.conflicts.length || r.entityLinks.unresolved.length || r.views.wouldOrphan.length)
|
|
559
|
+
log.hint("Conflicts are skipped, never merged — rename the template's profile to a new slug, or install as-is to reuse the pod's existing one.");
|
|
560
|
+
for (const slug of p.create)
|
|
561
|
+
log.dim(`+ ${slug} (new profile)`);
|
|
562
|
+
for (const slug of p.reused)
|
|
563
|
+
log.dim(`↺ ${slug} (reuses existing pod profile)`);
|
|
564
|
+
if (!hasAny)
|
|
565
|
+
log.hint("Run without --dry-run to install.");
|
|
566
|
+
}
|
|
384
567
|
// ── `synap market install <slug>` ───────────────────────────────────────────
|
|
385
568
|
export async function marketInstall(slug, opts) {
|
|
386
569
|
const applyTimeoutMs = parseApplyTimeoutMs(opts.timeout);
|
|
@@ -396,14 +579,83 @@ export async function marketInstall(slug, opts) {
|
|
|
396
579
|
process.exit(1);
|
|
397
580
|
}
|
|
398
581
|
const type = typeOf(entry);
|
|
399
|
-
//
|
|
400
|
-
//
|
|
582
|
+
// A CAPABILITY installs through the SAME door as `cap add` — one code path, so
|
|
583
|
+
// the two commands can never diverge. That door resolves the template by key
|
|
584
|
+
// (so opt-in / syncByDefault:false caps like unipile-linkedin work — the
|
|
585
|
+
// params-less market.install verb below cannot) AND prompts for any required
|
|
586
|
+
// credential inline (a key), which a server-side verb can never do. `cap add`
|
|
587
|
+
// is now a thin alias of this. `--pod` is honoured (capabilityAdd resolves the
|
|
588
|
+
// active pod itself); `--json` prompts can't render, so a credentialed cap
|
|
589
|
+
// still prompts — run it interactively.
|
|
590
|
+
if (type === "capability") {
|
|
591
|
+
await capabilityAdd(slug, {});
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
// Other non-workspace packages install through the pod's ONE kind-agnostic
|
|
595
|
+
// door — the `market.install` verb on `/capabilities/execute`. (The WORKSPACE
|
|
596
|
+
// path below uses `/packages/apply`, which provisions a workspace, and is
|
|
597
|
+
// left untouched.) The verb resolves the definition from `cp_catalog_cache`
|
|
598
|
+
// by (kind, slug), tier-gates, and — for the CLI's OPERATOR hub key (no
|
|
599
|
+
// agentUserId) — installs directly; an agent call would file a proposal.
|
|
401
600
|
if (type !== "workspace") {
|
|
402
|
-
|
|
403
|
-
if (
|
|
404
|
-
log.
|
|
601
|
+
const kind = marketInstallKind(type);
|
|
602
|
+
if (!kind) {
|
|
603
|
+
log.warn(`"${entry.name}" is a ${type} package — the CLI can't install this type yet.`);
|
|
604
|
+
log.hint("Install it from the browser marketplace (Marketplace → find it → Install).");
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
let cfg;
|
|
608
|
+
try {
|
|
609
|
+
cfg = await resolveHubConfig(opts);
|
|
610
|
+
}
|
|
611
|
+
catch (e) {
|
|
612
|
+
renderHubError(e);
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
const s = opts.json ? null : ora(`Installing ${entry.name}…`).start();
|
|
616
|
+
let res;
|
|
617
|
+
try {
|
|
618
|
+
// Pod-wide install: no workspaceId (a capability/cell/template lands on
|
|
619
|
+
// the pod). `market.install` is a WRITE builtin — an operator run
|
|
620
|
+
// owner-bypasses the gate and runs it in-process.
|
|
621
|
+
res = (await hubPost("/capabilities/execute", { verbId: "market.install", parameters: { slug, kind } }, cfg, applyTimeoutMs));
|
|
622
|
+
}
|
|
623
|
+
catch (e) {
|
|
624
|
+
s?.stop();
|
|
625
|
+
// 404 = this pod predates the seeded `market.install` verb (older deploy).
|
|
626
|
+
if (e instanceof HubError && e.status === 404) {
|
|
627
|
+
log.error(`This pod doesn't expose the market.install verb yet.`);
|
|
628
|
+
log.hint("It needs a newer pod deploy — meanwhile install from the browser marketplace.");
|
|
629
|
+
process.exit(1);
|
|
630
|
+
}
|
|
631
|
+
// The verb throws NOT_FOUND (surfaced as a 500) when the CP kind hasn't
|
|
632
|
+
// synced into `cp_catalog_cache` yet — say so actionably, not opaquely.
|
|
633
|
+
if (e instanceof HubError && /catalog cache/i.test(e.message)) {
|
|
634
|
+
log.error(`"${entry.name}" isn't in the pod's marketplace cache yet.`);
|
|
635
|
+
log.hint("The pod syncs the catalog every ~10 min — retry shortly, or install from the browser marketplace.");
|
|
636
|
+
process.exit(1);
|
|
637
|
+
}
|
|
638
|
+
log.error(`${entry.name} failed`);
|
|
639
|
+
renderHubError(e);
|
|
640
|
+
process.exit(1);
|
|
641
|
+
}
|
|
642
|
+
s?.stop();
|
|
643
|
+
// The execute door wraps the verb's return as { status:"run", result: <MarketInstallOutcome> }.
|
|
644
|
+
// The outcome is { status:"installed", result } (operator) or { status:"proposed", proposalId, reviewUrl }.
|
|
645
|
+
const outcome = (res.result ?? res);
|
|
646
|
+
const status = String(outcome.status ?? "installed");
|
|
647
|
+
if (opts.json) {
|
|
648
|
+
console.log(JSON.stringify({ slug, kind, outcome: status, result: outcome }, null, 2));
|
|
649
|
+
return;
|
|
405
650
|
}
|
|
406
|
-
|
|
651
|
+
if (status === "proposed") {
|
|
652
|
+
const proposalId = outcome.proposalId ? String(outcome.proposalId) : undefined;
|
|
653
|
+
log.info(`${entry.name} — proposed (under review, not live yet).`);
|
|
654
|
+
if (proposalId)
|
|
655
|
+
log.hint(`Approve: synap proposals approve ${proposalId}`);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
log.success(`Installed ${entry.name}.`);
|
|
407
659
|
return;
|
|
408
660
|
}
|
|
409
661
|
// Tier gate: don't silently attempt an ungrantable install.
|
|
@@ -453,6 +705,29 @@ export async function marketInstall(slug, opts) {
|
|
|
453
705
|
const remoteVersion = cat.remoteVersionBySlug.get(slug);
|
|
454
706
|
if (remoteVersion)
|
|
455
707
|
pkg._meta = { ...(pkg._meta ?? {}), version: remoteVersion };
|
|
708
|
+
// ── --dry-run: write-free preflight of the CREATE path. ───────────────────
|
|
709
|
+
// Runs the pod's REAL resolver (write-free) so a template author sees
|
|
710
|
+
// profileKind conflicts and other resolution failures BEFORE installing.
|
|
711
|
+
// (Previews the create path only; `--onto` reconcile-preview is not modeled.)
|
|
712
|
+
if (opts.dryRun) {
|
|
713
|
+
const s = opts.json ? null : ora(`Checking ${entry.name}…`).start();
|
|
714
|
+
try {
|
|
715
|
+
const report = (await hubPost("/packages/preflight", pkg, cfg, applyTimeoutMs));
|
|
716
|
+
s?.stop();
|
|
717
|
+
if (opts.json) {
|
|
718
|
+
console.log(JSON.stringify({ slug, ...report }, null, 2));
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
renderPreflightReport(entry, report);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
catch (e) {
|
|
725
|
+
s?.stop();
|
|
726
|
+
log.error(`${entry.name} preflight failed`);
|
|
727
|
+
renderHubError(e);
|
|
728
|
+
process.exit(1);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
456
731
|
// Pre-apply state — was this slug already installed with NO version stamp?
|
|
457
732
|
// Feeds `classifyApplyResult`'s stamp-contradiction check below.
|
|
458
733
|
const preInstalled = installedTemplates.find((t) => t.slug === slug);
|
|
@@ -474,7 +749,7 @@ export async function marketInstall(slug, opts) {
|
|
|
474
749
|
}
|
|
475
750
|
log.info(`${entry.name} — proposed (under review, not live yet).`);
|
|
476
751
|
if (proposalId)
|
|
477
|
-
log.hint(`Approve: synap proposals approve ${proposalId
|
|
752
|
+
log.hint(`Approve: synap proposals approve ${proposalId}`);
|
|
478
753
|
renderNextSteps(steps);
|
|
479
754
|
return;
|
|
480
755
|
}
|
|
@@ -616,7 +891,7 @@ async function applyOnePackage(entry, cat, cfg, opts, applyTimeoutMs = APPLY_TIM
|
|
|
616
891
|
if (!opts.json) {
|
|
617
892
|
log.info(`${entry.name} — update proposed (under review, not live yet).`);
|
|
618
893
|
if (proposalId)
|
|
619
|
-
log.hint(`Approve: synap proposals approve ${proposalId
|
|
894
|
+
log.hint(`Approve: synap proposals approve ${proposalId}`);
|
|
620
895
|
}
|
|
621
896
|
return { slug: entry.slug, outcome: "proposed", status: "proposed", warnings: [] };
|
|
622
897
|
}
|