@synap-core/cli 1.7.0 → 1.10.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/agent-chat.d.ts +47 -0
- package/dist/commands/agent-chat.js +250 -0
- package/dist/commands/agent-chat.js.map +1 -0
- package/dist/commands/agent-run.d.ts +47 -0
- package/dist/commands/agent-run.js +301 -0
- package/dist/commands/agent-run.js.map +1 -0
- package/dist/commands/agents.js +85 -46
- package/dist/commands/agents.js.map +1 -1
- package/dist/commands/artifact.d.ts +26 -0
- package/dist/commands/artifact.js +76 -0
- package/dist/commands/artifact.js.map +1 -0
- package/dist/commands/automation.d.ts +47 -0
- package/dist/commands/automation.js +601 -0
- package/dist/commands/automation.js.map +1 -0
- package/dist/commands/bridge-setup.d.ts +66 -0
- package/dist/commands/bridge-setup.js +1112 -0
- package/dist/commands/bridge-setup.js.map +1 -0
- package/dist/commands/cap-connections.d.ts +28 -0
- package/dist/commands/cap-connections.js +224 -0
- package/dist/commands/cap-connections.js.map +1 -0
- package/dist/commands/capability.d.ts +89 -0
- package/dist/commands/capability.js +1523 -0
- package/dist/commands/capability.js.map +1 -0
- package/dist/commands/cell.d.ts +55 -0
- package/dist/commands/cell.js +277 -0
- package/dist/commands/cell.js.map +1 -0
- package/dist/commands/centrality.d.ts +20 -0
- package/dist/commands/centrality.js +94 -0
- package/dist/commands/centrality.js.map +1 -0
- package/dist/commands/connect.d.ts +2 -0
- package/dist/commands/connect.js +25 -10
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/connections.js +0 -16
- package/dist/commands/connections.js.map +1 -1
- package/dist/commands/context.d.ts +22 -0
- package/dist/commands/context.js +149 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/data-extra.d.ts +14 -0
- package/dist/commands/data-extra.js +185 -0
- package/dist/commands/data-extra.js.map +1 -0
- package/dist/commands/data.d.ts +14 -10
- package/dist/commands/data.js +353 -192
- package/dist/commands/data.js.map +1 -1
- package/dist/commands/diagnose.d.ts +20 -0
- package/dist/commands/diagnose.js +143 -0
- package/dist/commands/diagnose.js.map +1 -0
- package/dist/commands/digest.d.ts +17 -0
- package/dist/commands/digest.js +121 -0
- package/dist/commands/digest.js.map +1 -0
- package/dist/commands/discover.d.ts +2 -0
- package/dist/commands/discover.js +25 -8
- package/dist/commands/discover.js.map +1 -1
- package/dist/commands/doc.d.ts +39 -0
- package/dist/commands/doc.js +130 -0
- package/dist/commands/doc.js.map +1 -0
- package/dist/commands/events.d.ts +26 -0
- package/dist/commands/events.js +69 -0
- package/dist/commands/events.js.map +1 -0
- package/dist/commands/explain.d.ts +21 -0
- package/dist/commands/explain.js +251 -0
- package/dist/commands/explain.js.map +1 -0
- package/dist/commands/facet.d.ts +29 -0
- package/dist/commands/facet.js +113 -0
- package/dist/commands/facet.js.map +1 -0
- package/dist/commands/graph.d.ts +18 -0
- package/dist/commands/graph.js +74 -0
- package/dist/commands/graph.js.map +1 -0
- package/dist/commands/home.d.ts +12 -0
- package/dist/commands/home.js +54 -0
- package/dist/commands/home.js.map +1 -0
- package/dist/commands/import.d.ts +38 -0
- package/dist/commands/import.js +440 -0
- package/dist/commands/import.js.map +1 -0
- package/dist/commands/init.d.ts +0 -2
- package/dist/commands/init.js +97 -118
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/keys.d.ts +9 -0
- package/dist/commands/keys.js +78 -0
- package/dist/commands/keys.js.map +1 -0
- package/dist/commands/knowledge.d.ts +34 -18
- package/dist/commands/knowledge.js +355 -57
- package/dist/commands/knowledge.js.map +1 -1
- package/dist/commands/launch.d.ts +50 -0
- package/dist/commands/launch.js +790 -0
- package/dist/commands/launch.js.map +1 -0
- package/dist/commands/lens.d.ts +45 -0
- package/dist/commands/lens.js +386 -0
- package/dist/commands/lens.js.map +1 -0
- package/dist/commands/market.d.ts +209 -0
- package/dist/commands/market.js +1368 -0
- package/dist/commands/market.js.map +1 -0
- package/dist/commands/mcp.d.ts +42 -0
- package/dist/commands/mcp.js +184 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/observe.d.ts +9 -0
- package/dist/commands/observe.js +93 -0
- package/dist/commands/observe.js.map +1 -0
- package/dist/commands/open.d.ts +32 -0
- package/dist/commands/open.js +85 -0
- package/dist/commands/open.js.map +1 -0
- package/dist/commands/pod.d.ts +100 -0
- package/dist/commands/pod.js +359 -0
- package/dist/commands/pod.js.map +1 -0
- package/dist/commands/pods.d.ts +6 -0
- package/dist/commands/pods.js +160 -50
- package/dist/commands/pods.js.map +1 -1
- package/dist/commands/proposals-actions.d.ts +7 -0
- package/dist/commands/proposals-actions.js +77 -0
- package/dist/commands/proposals-actions.js.map +1 -0
- package/dist/commands/providers.d.ts +38 -0
- package/dist/commands/providers.js +170 -0
- package/dist/commands/providers.js.map +1 -0
- package/dist/commands/purge.d.ts +23 -0
- package/dist/commands/purge.js +148 -0
- package/dist/commands/purge.js.map +1 -0
- package/dist/commands/raycast.d.ts +15 -0
- package/dist/commands/raycast.js +153 -0
- package/dist/commands/raycast.js.map +1 -0
- package/dist/commands/sessions.d.ts +31 -0
- package/dist/commands/sessions.js +248 -0
- package/dist/commands/sessions.js.map +1 -0
- package/dist/commands/skill-manage.d.ts +21 -0
- package/dist/commands/skill-manage.js +195 -0
- package/dist/commands/skill-manage.js.map +1 -0
- package/dist/commands/skills-commands.d.ts +26 -0
- package/dist/commands/skills-commands.js +187 -0
- package/dist/commands/skills-commands.js.map +1 -0
- package/dist/commands/skills.d.ts +26 -0
- package/dist/commands/skills.js +196 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/status.d.ts +3 -1
- package/dist/commands/status.js +231 -41
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/statusline.d.ts +24 -0
- package/dist/commands/statusline.js +450 -0
- package/dist/commands/statusline.js.map +1 -0
- package/dist/commands/subscribe.d.ts +19 -0
- package/dist/commands/subscribe.js +91 -0
- package/dist/commands/subscribe.js.map +1 -0
- package/dist/commands/tools.d.ts +30 -0
- package/dist/commands/tools.js +315 -0
- package/dist/commands/tools.js.map +1 -0
- package/dist/commands/versions.d.ts +27 -0
- package/dist/commands/versions.js +277 -0
- package/dist/commands/versions.js.map +1 -0
- package/dist/commands/view.d.ts +57 -0
- package/dist/commands/view.js +195 -0
- package/dist/commands/view.js.map +1 -0
- package/dist/commands/whoami.d.ts +21 -0
- package/dist/commands/whoami.js +129 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/index.d.ts +3 -4
- package/dist/index.js +1524 -202
- package/dist/index.js.map +1 -1
- package/dist/lib/agent-templates.d.ts +25 -0
- package/dist/lib/agent-templates.js +148 -0
- package/dist/lib/agent-templates.js.map +1 -0
- package/dist/lib/agents-config.d.ts +4 -1
- package/dist/lib/agents-config.js +19 -2
- package/dist/lib/agents-config.js.map +1 -1
- package/dist/lib/approval-poll.d.ts +42 -0
- package/dist/lib/approval-poll.js +52 -0
- package/dist/lib/approval-poll.js.map +1 -0
- package/dist/lib/auth.d.ts +29 -6
- package/dist/lib/auth.js +105 -94
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/bundle-version.d.ts +11 -0
- package/dist/lib/bundle-version.js +33 -0
- package/dist/lib/bundle-version.js.map +1 -0
- package/dist/lib/capture-lane.d.ts +67 -0
- package/dist/lib/capture-lane.js +136 -0
- package/dist/lib/capture-lane.js.map +1 -0
- package/dist/lib/cp-packages.d.ts +121 -0
- package/dist/lib/cp-packages.js +218 -0
- package/dist/lib/cp-packages.js.map +1 -0
- package/dist/lib/describe-lens.d.ts +46 -0
- package/dist/lib/describe-lens.js +33 -0
- package/dist/lib/describe-lens.js.map +1 -0
- package/dist/lib/hub-client.d.ts +84 -1
- package/dist/lib/hub-client.js +502 -22
- package/dist/lib/hub-client.js.map +1 -1
- package/dist/lib/installed.d.ts +51 -0
- package/dist/lib/installed.js +70 -0
- package/dist/lib/installed.js.map +1 -0
- package/dist/lib/next-steps.d.ts +61 -0
- package/dist/lib/next-steps.js +146 -0
- package/dist/lib/next-steps.js.map +1 -0
- package/dist/lib/pod.d.ts +104 -19
- package/dist/lib/pod.js +245 -57
- package/dist/lib/pod.js.map +1 -1
- package/dist/lib/project-ref.d.ts +95 -0
- package/dist/lib/project-ref.js +144 -0
- package/dist/lib/project-ref.js.map +1 -0
- package/dist/lib/project.d.ts +35 -0
- package/dist/lib/project.js +39 -0
- package/dist/lib/project.js.map +1 -0
- package/dist/lib/session-lens.d.ts +33 -0
- package/dist/lib/session-lens.js +76 -0
- package/dist/lib/session-lens.js.map +1 -0
- package/dist/lib/skill-import.d.ts +40 -0
- package/dist/lib/skill-import.js +203 -0
- package/dist/lib/skill-import.js.map +1 -0
- package/dist/lib/skills-installer.d.ts +16 -5
- package/dist/lib/skills-installer.js +132 -18
- package/dist/lib/skills-installer.js.map +1 -1
- package/dist/lib/suite.d.ts +28 -0
- package/dist/lib/suite.js +29 -0
- package/dist/lib/suite.js.map +1 -0
- package/dist/lib/superwhisper-import.d.ts +40 -0
- package/dist/lib/superwhisper-import.js +389 -0
- package/dist/lib/superwhisper-import.js.map +1 -0
- package/dist/lib/targets.d.ts +66 -2
- package/dist/lib/targets.js +914 -132
- package/dist/lib/targets.js.map +1 -1
- package/dist/lib/unwrapList.d.ts +1 -0
- package/dist/lib/unwrapList.js +20 -0
- package/dist/lib/unwrapList.js.map +1 -0
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.js +10 -2
- package/dist/utils/logger.js.map +1 -1
- package/package.json +10 -4
- package/skills/agent-os/SKILL.md +212 -0
- package/skills/manifest.json +5 -0
- package/skills/onboard/SKILL.md +186 -0
- package/skills/synap/SKILL.md +1524 -104
- package/skills/synap/_order.txt +37 -0
- package/skills/synap/_skill.yaml +24 -0
- package/skills/synap/authentication.md +10 -0
- package/skills/synap/automations.md +264 -0
- package/skills/synap/capabilities.md +107 -0
- package/skills/synap/capture.md +10 -0
- package/skills/synap/cli-operations.md +107 -0
- package/skills/synap/common-mistakes.md +12 -0
- package/skills/synap/creative-loop.md +43 -0
- package/skills/synap/crm.md +117 -0
- package/skills/synap/diagnostics.md +35 -0
- package/skills/synap/digesting-a-channel.md +38 -0
- package/skills/synap/document-embeds.md +123 -0
- package/skills/synap/escalation-ladder.md +71 -0
- package/skills/synap/focus-sessions.md +47 -0
- package/skills/synap/governance-rules.md +64 -0
- package/skills/synap/governance.md +6 -6
- package/skills/synap/graph-gardening.md +21 -0
- package/skills/synap/inline-patterns.md +38 -0
- package/skills/synap/lenses.md +38 -0
- package/skills/synap/linking-principle.md +39 -0
- package/skills/synap/linking.md +1 -0
- package/skills/synap/mental-model.md +45 -0
- package/skills/synap/more.md +7 -0
- package/skills/synap/multi-entity-capture.md +10 -0
- package/skills/synap/next-action.md +37 -0
- package/skills/synap/north-star.md +17 -0
- package/skills/synap/operating-mode-rules.md +45 -0
- package/skills/synap/operating-mode.md +9 -0
- package/skills/synap/quick-reference.md +67 -0
- package/skills/synap/reading.md +43 -0
- package/skills/synap/recap.md +21 -0
- package/skills/synap/reflexes.md +46 -0
- package/skills/synap/response-style.md +21 -0
- package/skills/synap/scope.md +13 -0
- package/skills/synap/showing.md +17 -0
- package/skills/synap/user-model.md +25 -0
- package/skills/synap/viewframe-cells.md +290 -0
- package/skills/synap/work-flow.md +113 -0
- package/skills/synap/worked-examples.md +53 -0
- package/skills/synap/workspace-design.md +45 -0
- package/skills/synap/workspace-edges.md +49 -0
- package/skills/synap/writes.md +156 -0
- package/skills/synap-schema/SKILL.md +97 -6
- package/skills/synap-schema/_order.txt +13 -0
- package/skills/synap-schema/_skill.yaml +22 -0
- package/skills/synap-schema/before-you-touch.md +14 -0
- package/skills/synap-schema/common-mistakes.md +9 -0
- package/skills/synap-schema/creating-profiles.md +17 -0
- package/skills/synap-schema/discover-profiles.md +24 -0
- package/skills/synap-schema/entity-scope.md +8 -0
- package/skills/synap-schema/extend-vs-create.md +31 -0
- package/skills/synap-schema/more.md +6 -0
- package/skills/synap-schema/overlay-properties.md +22 -0
- package/skills/synap-schema/profiles.md +3 -0
- package/skills/synap-schema/property-defs.md +29 -0
- package/skills/synap-schema/read-before-write.md +50 -0
- package/skills/synap-schema/relations.md +18 -0
- package/skills/synap-schema/worked-example.md +30 -0
- package/skills/synap-ui/SKILL.md +38 -11
- package/skills/synap-ui/_order.txt +13 -0
- package/skills/synap-ui/_skill.yaml +23 -0
- package/skills/synap-ui/ai-companion.md +27 -0
- package/skills/synap-ui/arranging-bento.md +8 -0
- package/skills/synap-ui/before-you-build.md +20 -0
- package/skills/synap-ui/bento-layouts.md +35 -0
- package/skills/synap-ui/common-mistakes.md +10 -0
- package/skills/synap-ui/creating-views.md +26 -0
- package/skills/synap-ui/four-builds.md +24 -0
- package/skills/synap-ui/more.md +7 -0
- package/skills/synap-ui/proposing-vs-extending.md +11 -0
- package/skills/synap-ui/view-list.md +24 -0
- package/skills/synap-ui/views.md +3 -0
- package/skills/synap-ui/worked-example.md +18 -0
- package/skills/synap-ui/workspaces.md +47 -0
- package/templates/automations/bookmark-enrichment.automation.json +44 -0
- package/templates/automations/mail-feed.automation.json +86 -0
- package/templates/automations/url-bookmark-capture.automation.json +70 -0
- package/templates/skills/README.md +21 -0
- package/templates/skills/authoring-automations/SKILL.md +95 -0
- package/templates/skills/authoring-automations/reference/node-matrix.md +49 -0
- package/templates/skills/authoring-capabilities/SKILL.md +93 -0
- package/templates/skills/authoring-capabilities/reference/template-anatomy.md +70 -0
- package/templates/skills/authoring-playbooks/SKILL.md +102 -0
- package/templates/skills/authoring-playbooks/reference/stages-contract.md +35 -0
- package/templates/skills/authoring-playbooks/reference/worked-example.md +64 -0
- package/templates/skills/bookmark-enrichment/SKILL.md +40 -0
- package/templates/skills/coordinating-agents/SKILL.md +32 -0
- package/templates/skills/coordinating-branches/SKILL.md +27 -0
- package/templates/skills/mapping-the-server/SKILL.md +54 -0
- package/templates/skills/reading-client-progress/SKILL.md +65 -0
- package/templates/skills/routing-to-channels/SKILL.md +54 -0
- package/templates/skills/stellar-scf-grants-advisor/ASSOCIATE-CHEATSHEET.md +133 -0
- package/templates/skills/stellar-scf-grants-advisor/ASSOCIATE-ONBOARDING-GUIDE.md +761 -0
- package/templates/skills/stellar-scf-grants-advisor/CLIENT-FAQ.md +181 -0
- package/templates/skills/stellar-scf-grants-advisor/PARTNER-HANDOVER.md +130 -0
- package/templates/skills/stellar-scf-grants-advisor/SKILL.md +102 -0
- package/templates/skills/stellar-scf-grants-advisor/examples/README.md +43 -0
- package/templates/skills/stellar-scf-grants-advisor/examples/chat-summary-templates.md +158 -0
- package/templates/skills/stellar-scf-grants-advisor/examples/corpus-comparable-rows.md +136 -0
- package/templates/skills/stellar-scf-grants-advisor/examples/q10-team-rewrites.md +109 -0
- package/templates/skills/stellar-scf-grants-advisor/examples/q2-description-rewrites.md +88 -0
- package/templates/skills/stellar-scf-grants-advisor/examples/q6-integration-rewrites.md +107 -0
- package/templates/skills/stellar-scf-grants-advisor/examples/tranche-structures.md +176 -0
- package/templates/skills/stellar-scf-grants-advisor/reference/01-methodology.md +96 -0
- package/templates/skills/stellar-scf-grants-advisor/reference/02-scoring-framework.md +199 -0
- package/templates/skills/stellar-scf-grants-advisor/reference/03-rejection-patterns.md +241 -0
- package/templates/skills/stellar-scf-grants-advisor/reference/04-winner-patterns.md +120 -0
- package/templates/skills/stellar-scf-grants-advisor/reference/05-comeback-patterns.md +92 -0
- package/templates/skills/stellar-scf-grants-advisor/reference/06-budget-benchmarks.md +125 -0
- package/templates/skills/stellar-scf-grants-advisor/reference/07-integration-list.md +147 -0
- package/templates/skills/stellar-scf-grants-advisor/reference/08-abstract-fields.md +258 -0
- package/templates/skills/stellar-scf-grants-advisor/reference/09-build-fields.md +259 -0
- package/templates/skills/stellar-scf-grants-advisor/reference/10-corpus-winners.md +135 -0
- package/templates/skills/stellar-scf-grants-advisor/reference/11-corpus-rejects.md +267 -0
- package/templates/skills/stellar-scf-grants-advisor/reference/12-client-communication.md +131 -0
- package/templates/skills/stellar-scf-grants-advisor/reference/13-excel-output-spec.md +199 -0
- package/templates/skills/stellar-scf-grants-advisor/reference/14-worked-examples.md +160 -0
- package/templates/skills/stellar-scf-grants-advisor/templates/abstract-review-builder.py +253 -0
- package/templates/skills/stellar-scf-grants-advisor/templates/build-review-builder.py +213 -0
- package/dist/commands/finish.d.ts +0 -25
- package/dist/commands/finish.js +0 -274
- package/dist/commands/finish.js.map +0 -1
- package/dist/commands/openclaw.d.ts +0 -40
- package/dist/commands/openclaw.js +0 -1148
- package/dist/commands/openclaw.js.map +0 -1
- package/dist/commands/security-audit.d.ts +0 -12
- package/dist/commands/security-audit.js +0 -100
- package/dist/commands/security-audit.js.map +0 -1
- package/dist/lib/hardening.d.ts +0 -18
- package/dist/lib/hardening.js +0 -214
- package/dist/lib/hardening.js.map +0 -1
- package/dist/lib/templates.d.ts +0 -11
- package/dist/lib/templates.js +0 -13
- package/dist/lib/templates.js.map +0 -1
- package/dist/templates/agent-os.json +0 -3090
- package/node_modules/@synap/hub-rest-client/dist/index.cjs +0 -881
- package/node_modules/@synap/hub-rest-client/dist/index.d.cts +0 -907
- package/node_modules/@synap/hub-rest-client/dist/index.d.ts +0 -907
- package/node_modules/@synap/hub-rest-client/dist/index.js +0 -851
- package/node_modules/@synap/hub-rest-client/package.json +0 -45
- package/node_modules/@synap/hub-rest-client/src/client.ts +0 -1143
- package/node_modules/@synap/hub-rest-client/src/errors.ts +0 -30
- package/node_modules/@synap/hub-rest-client/src/index.ts +0 -111
- package/node_modules/@synap/hub-rest-client/src/setup.ts +0 -77
- package/node_modules/@synap/hub-rest-client/src/types.ts +0 -639
|
@@ -0,0 +1,1368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `synap market` — discover + install ANY package type.
|
|
3
|
+
* =====================================================
|
|
4
|
+
*
|
|
5
|
+
* `synap launch` is workspace-first: it turns an empty pod into a company from
|
|
6
|
+
* BUNDLED workspace templates (offline). `market` is the superset browser — it
|
|
7
|
+
* reaches EVERY package type the control-plane registry carries
|
|
8
|
+
* (capability / skill / workflow / view / cell / workspace), public or private,
|
|
9
|
+
* over the shared catalog door (`assembleCatalog` + the CP `/api/packages`
|
|
10
|
+
* transport). Suites (`enterprise-os`, `the-arch`) headline via the `suite` tag,
|
|
11
|
+
* not a hardcoded slug.
|
|
12
|
+
*
|
|
13
|
+
* synap market [--search q] [--type t] [--list] [--json] — browse
|
|
14
|
+
* synap market install <slug> — install
|
|
15
|
+
* synap market update [slug] [--yes] [--json] — check/apply drift
|
|
16
|
+
*
|
|
17
|
+
* INSTALL SCOPE: the pod's one install door (`/packages/apply`) provisions a
|
|
18
|
+
* WORKSPACE from a package definition — it is workspace-centric. So `market
|
|
19
|
+
* install` fully installs WORKSPACE-type packages (same door `launch` uses) and,
|
|
20
|
+
* for the operational types (capability/skill/…), routes the user to the surface
|
|
21
|
+
* that installs them (the browser marketplace, or `synap cap add` for a
|
|
22
|
+
* capability already in the pod catalog) rather than silently doing nothing. A
|
|
23
|
+
* generic CLI→pod install for standalone non-workspace packages is not wired yet.
|
|
24
|
+
*/
|
|
25
|
+
import ora from "ora";
|
|
26
|
+
import chalk from "chalk";
|
|
27
|
+
import { assembleCatalog, computeTemplateUpdates, isHashVersion, listPublicTemplates, listWorkspaceTemplates, toPackageDefinition, layerTemplateGraph, bundledEdgesOf, } from "@synap-core/workspace-templates";
|
|
28
|
+
import { log } from "../utils/logger.js";
|
|
29
|
+
import { resolveHubConfig, hubPost, renderHubError } from "../lib/hub-client.js";
|
|
30
|
+
import { fetchProjects } from "../lib/project.js";
|
|
31
|
+
import { writeGovernance } from "../lib/capture-lane.js";
|
|
32
|
+
import { renderNextSteps, FLOW } from "../lib/next-steps.js";
|
|
33
|
+
import { getStoredToken, isTokenLocallyExpired } from "../lib/auth.js";
|
|
34
|
+
import { fetchPublicBrowseRows, fetchMyRows, fetchAvailableSlugs, fetchPackageDefinition, } from "../lib/cp-packages.js";
|
|
35
|
+
import { fetchInstalledSlugs, fetchInstalledTemplates } from "../lib/installed.js";
|
|
36
|
+
import { isSuite } from "../lib/suite.js";
|
|
37
|
+
import { bundledTemplatesVersion } from "../lib/bundle-version.js";
|
|
38
|
+
import { HubNetworkError } from "../lib/hub-client.js";
|
|
39
|
+
// Same longer budget launch uses — an apply writes profiles/views/entities.
|
|
40
|
+
const APPLY_TIMEOUT_MS = 120_000;
|
|
41
|
+
/** `--timeout <seconds>` → ms, falling back to `APPLY_TIMEOUT_MS` when unset/invalid. */
|
|
42
|
+
function parseApplyTimeoutMs(raw) {
|
|
43
|
+
const secs = raw ? parseInt(raw, 10) : NaN;
|
|
44
|
+
return Number.isFinite(secs) && secs > 0 ? secs * 1000 : APPLY_TIMEOUT_MS;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Assemble the market catalog: bundled workspace templates ∪ public CP browse
|
|
48
|
+
* (all types) ∪ the caller's own packages (when logged in), through the shared
|
|
49
|
+
* `assembleCatalog` door. Unlike `launch`, PUBLIC CP rows are fetched even when
|
|
50
|
+
* logged out — the bundle only carries workspaces, so all-types discovery needs
|
|
51
|
+
* the CP browse route (which needs no auth).
|
|
52
|
+
*/
|
|
53
|
+
export async function buildMarketCatalog(filters) {
|
|
54
|
+
const creds = getStoredToken();
|
|
55
|
+
const loggedIn = !!creds && !isTokenLocallyExpired(creds);
|
|
56
|
+
let reachedCp = true;
|
|
57
|
+
const [publicRows, mineRows, availableSlugs, installed] = await Promise.all([
|
|
58
|
+
fetchPublicBrowseRows(filters).catch(() => {
|
|
59
|
+
reachedCp = false;
|
|
60
|
+
return [];
|
|
61
|
+
}),
|
|
62
|
+
loggedIn && creds
|
|
63
|
+
? fetchMyRows(creds.token, filters).catch(() => [])
|
|
64
|
+
: Promise.resolve([]),
|
|
65
|
+
loggedIn && creds
|
|
66
|
+
? fetchAvailableSlugs(creds.token, filters).catch(() => null)
|
|
67
|
+
: Promise.resolve(null),
|
|
68
|
+
fetchInstalledSlugs(),
|
|
69
|
+
]);
|
|
70
|
+
const merged = assembleCatalog({
|
|
71
|
+
// Logged in → include PRIVATE bundled templates (e.g. the `the-arch` suite),
|
|
72
|
+
// matching the "private templates unlocked" promise. Logged out → public
|
|
73
|
+
// bundle only. (mergeCatalog derives `isPrivate` from each template's
|
|
74
|
+
// `meta.isPublic === false`, so private bundled entries stay badged.)
|
|
75
|
+
bundled: loggedIn ? listWorkspaceTemplates() : listPublicTemplates(),
|
|
76
|
+
remoteRows: [...publicRows, ...mineRows],
|
|
77
|
+
bundleVersion: bundledTemplatesVersion(),
|
|
78
|
+
remoteStatus: reachedCp ? "ok" : "unreachable",
|
|
79
|
+
});
|
|
80
|
+
const tierBySlug = new Map();
|
|
81
|
+
const lockedSlugs = new Set();
|
|
82
|
+
for (const r of publicRows) {
|
|
83
|
+
if (r.requiredTier != null || r.pricingModel != null) {
|
|
84
|
+
tierBySlug.set(r.slug, {
|
|
85
|
+
requiredTier: r.requiredTier ?? null,
|
|
86
|
+
pricingModel: r.pricingModel ?? null,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (availableSlugs && r.requiredTier && !availableSlugs.has(r.slug)) {
|
|
90
|
+
lockedSlugs.add(r.slug);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const bundledSlugs = new Set(listWorkspaceTemplates().map((t) => t.meta.slug));
|
|
94
|
+
// Raw CP-row versions, bypassing the merge winner (see `remoteVersionBySlug`
|
|
95
|
+
// doc above). `/mine` rows are the caller's own — they win a slug collision
|
|
96
|
+
// with a public browse row (matches `assembleCatalog`'s own precedence).
|
|
97
|
+
const remoteVersionBySlug = new Map();
|
|
98
|
+
for (const r of publicRows)
|
|
99
|
+
if (r.version)
|
|
100
|
+
remoteVersionBySlug.set(r.slug, r.version);
|
|
101
|
+
for (const r of mineRows)
|
|
102
|
+
if (r.version)
|
|
103
|
+
remoteVersionBySlug.set(r.slug, r.version);
|
|
104
|
+
return {
|
|
105
|
+
entries: merged.entries,
|
|
106
|
+
tierBySlug,
|
|
107
|
+
lockedSlugs,
|
|
108
|
+
installed,
|
|
109
|
+
bundledSlugs,
|
|
110
|
+
loggedIn,
|
|
111
|
+
reachedCp,
|
|
112
|
+
remoteVersionBySlug,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** The package type of an entry — bundled templates are always `workspace`. */
|
|
116
|
+
function typeOf(entry) {
|
|
117
|
+
return entry.category ?? "workspace";
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Client-side filter. The CP browse route filters REMOTE rows server-side, but
|
|
121
|
+
* `assembleCatalog` always adds the BUNDLED (all-`workspace`) templates — so a
|
|
122
|
+
* `--type capability` would otherwise leak bundled workspaces. Applied to the
|
|
123
|
+
* merged list so both sources obey the same filter.
|
|
124
|
+
*/
|
|
125
|
+
function matchesFilters(entry, filters) {
|
|
126
|
+
if (!filters)
|
|
127
|
+
return true;
|
|
128
|
+
if (filters.category && typeOf(entry) !== filters.category)
|
|
129
|
+
return false;
|
|
130
|
+
if (filters.search) {
|
|
131
|
+
const q = filters.search.toLowerCase();
|
|
132
|
+
const hay = `${entry.slug} ${entry.name} ${entry.description ?? ""}`.toLowerCase();
|
|
133
|
+
if (!hay.includes(q))
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
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
|
+
function clip(s, n) {
|
|
157
|
+
return s.length <= n ? s : s.slice(0, n - 1).trimEnd() + "…";
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Print an honest per-dependency seed summary — no more silent "✓ ready" when
|
|
161
|
+
* a dependency's capabilities/playbooks failed to seed. Prefers the top-level
|
|
162
|
+
* `seedSummary` rollup when present (pre-aggregated by the backend); falls
|
|
163
|
+
* back to re-deriving from the per-dependency `dependencies[]` array on older
|
|
164
|
+
* pod builds. No-op when there's nothing to report.
|
|
165
|
+
*/
|
|
166
|
+
function printSeedOutcomes(dependencies, seedSummary) {
|
|
167
|
+
if (seedSummary) {
|
|
168
|
+
if (seedSummary.attempted === 0)
|
|
169
|
+
return;
|
|
170
|
+
if (seedSummary.failed.length === 0) {
|
|
171
|
+
log.success(`${seedSummary.seeded}/${seedSummary.attempted} capabilities seeded`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
log.warn(`${seedSummary.seeded}/${seedSummary.attempted} capabilities seeded`);
|
|
175
|
+
for (const f of seedSummary.failed) {
|
|
176
|
+
log.hint(`${f.slug}: failed${f.error ? ` (${f.error})` : ""}`);
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (!dependencies?.length)
|
|
181
|
+
return;
|
|
182
|
+
const withLayers = dependencies.filter((d) => d.seedOutcome && d.seedOutcome.status !== "no-layers");
|
|
183
|
+
if (withLayers.length === 0)
|
|
184
|
+
return;
|
|
185
|
+
const seeded = withLayers.filter((d) => d.seedOutcome.status === "seeded");
|
|
186
|
+
const failed = withLayers.filter((d) => d.seedOutcome.status === "failed");
|
|
187
|
+
if (failed.length === 0) {
|
|
188
|
+
log.success(`${seeded.length}/${withLayers.length} capabilities seeded`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
log.warn(`${seeded.length}/${withLayers.length} capabilities seeded`);
|
|
192
|
+
for (const d of failed) {
|
|
193
|
+
log.hint(`${d.slug}: failed${d.seedOutcome.error ? ` (${d.seedOutcome.error})` : ""}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// ── Apply-outcome honesty — shared by `marketInstall` + `applyOnePackage` ───
|
|
197
|
+
// Both hit the same `/packages/apply` door and must render the SAME honest
|
|
198
|
+
// verdict from its response: a partial capability-seed failure, a version
|
|
199
|
+
// stamp that silently didn't take, or a compose-overlay result are never a
|
|
200
|
+
// plain "✓ success" — in the human text OR in `--json`.
|
|
201
|
+
/** True when at least one dependency's capability seed genuinely failed. */
|
|
202
|
+
function hasSeedFailure(dependencies, seedSummary) {
|
|
203
|
+
if (seedSummary)
|
|
204
|
+
return seedSummary.failed.length > 0;
|
|
205
|
+
return !!dependencies?.some((d) => d.seedOutcome?.status === "failed");
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* A version was sent for a template the pod had never stamped before, yet the
|
|
209
|
+
* response either says nothing changed or omits the honest-outcome fields
|
|
210
|
+
* entirely — that combination means the stamp/reconcile didn't actually take,
|
|
211
|
+
* not that the install was genuinely a no-op (the observed `market install
|
|
212
|
+
* the-arch` → "already up to date" bug).
|
|
213
|
+
*/
|
|
214
|
+
function detectStampContradiction(ws, seedSummary, remoteVersionSent, wasUnstamped) {
|
|
215
|
+
if (!remoteVersionSent || !wasUnstamped)
|
|
216
|
+
return false;
|
|
217
|
+
return ws?.outcome === "unchanged" || (ws?.outcome === undefined && seedSummary === undefined);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Classify one `/packages/apply` response into the honest verdict both
|
|
221
|
+
* `marketInstall` and `applyOnePackage` print (and both put in `--json`).
|
|
222
|
+
* `wasUnstamped` = the slug was already installed with no recorded version
|
|
223
|
+
* BEFORE this call (only `marketInstall` can detect this today — see its
|
|
224
|
+
* `fetchInstalledTemplates` call; `applyOnePackage` only ever re-applies
|
|
225
|
+
* already-versioned slugs, so it always passes `false`).
|
|
226
|
+
*/
|
|
227
|
+
export function classifyApplyResult(entry, ws, dependencies, seedSummary, ctx) {
|
|
228
|
+
const seedFailed = hasSeedFailure(dependencies, seedSummary);
|
|
229
|
+
if (detectStampContradiction(ws, seedSummary, ctx.remoteVersionSent, ctx.wasUnstamped)) {
|
|
230
|
+
return {
|
|
231
|
+
status: "stamp-contradiction",
|
|
232
|
+
warnings: [
|
|
233
|
+
`${entry.name} didn't get stamped — your pod may be running an older version that doesn't support this yet (redeploy the pod backend).`,
|
|
234
|
+
],
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (ws?.outcome === "created")
|
|
238
|
+
return { status: seedFailed ? "installed-with-failures" : "installed", warnings: [] };
|
|
239
|
+
if (ws?.outcome === "reconciled")
|
|
240
|
+
return { status: seedFailed ? "updated-with-failures" : "updated", warnings: [] };
|
|
241
|
+
if (ws?.outcome === "unchanged")
|
|
242
|
+
return { status: "unchanged", warnings: [] };
|
|
243
|
+
if (ws?.status === "composed") {
|
|
244
|
+
const warnings = [
|
|
245
|
+
`${entry.name} is a compose-overlay — it rides its base workspace and isn't independently updatable; update the base workspace to update this too.`,
|
|
246
|
+
];
|
|
247
|
+
return { status: seedFailed ? "composed-with-failures" : "composed", warnings };
|
|
248
|
+
}
|
|
249
|
+
if (ws?.status === "created")
|
|
250
|
+
return { status: "legacy-unknown", warnings: [] };
|
|
251
|
+
return { status: "no-workspace", warnings: [] };
|
|
252
|
+
}
|
|
253
|
+
/** Render a `classifyApplyResult` verdict — the ONE honest-text door for both `marketInstall` and `applyOnePackage`. */
|
|
254
|
+
export function printApplyVerdict(entry, verdict) {
|
|
255
|
+
switch (verdict.status) {
|
|
256
|
+
case "installed":
|
|
257
|
+
log.success(`Installed ${entry.name}.`);
|
|
258
|
+
return;
|
|
259
|
+
case "installed-with-failures":
|
|
260
|
+
log.warn(`Installed ${entry.name} — completed with capability failures (see below).`);
|
|
261
|
+
return;
|
|
262
|
+
case "updated":
|
|
263
|
+
log.success(`Updated ${entry.name}.`);
|
|
264
|
+
return;
|
|
265
|
+
case "updated-with-failures":
|
|
266
|
+
log.warn(`Updated ${entry.name} — completed with capability failures (see below).`);
|
|
267
|
+
return;
|
|
268
|
+
case "unchanged":
|
|
269
|
+
log.info(`• ${entry.name} is already up to date.`);
|
|
270
|
+
return;
|
|
271
|
+
case "composed":
|
|
272
|
+
log.success(`${entry.name} — layered onto its base workspace.`);
|
|
273
|
+
log.hint(verdict.warnings[0]);
|
|
274
|
+
return;
|
|
275
|
+
case "composed-with-failures":
|
|
276
|
+
log.warn(`${entry.name} — layered onto its base workspace, with capability failures (see below).`);
|
|
277
|
+
log.hint(verdict.warnings[0]);
|
|
278
|
+
return;
|
|
279
|
+
case "stamp-contradiction":
|
|
280
|
+
log.warn(verdict.warnings[0]);
|
|
281
|
+
return;
|
|
282
|
+
case "legacy-unknown":
|
|
283
|
+
log.success(`${entry.name} ready.`);
|
|
284
|
+
log.hint("This pod's /packages/apply doesn't distinguish reconciled-vs-unchanged yet — check synap orient if you expected new content.");
|
|
285
|
+
return;
|
|
286
|
+
case "no-workspace":
|
|
287
|
+
log.warn(`${entry.name} — pod returned no workspace.`);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// ── `synap market` / `synap market --list` — discovery ───────────────────────
|
|
292
|
+
export async function market(opts) {
|
|
293
|
+
const filters = opts.search || opts.type ? { search: opts.search, category: opts.type } : undefined;
|
|
294
|
+
const cat = await buildMarketCatalog(filters);
|
|
295
|
+
const entries = cat.entries.filter((e) => matchesFilters(e, filters));
|
|
296
|
+
if (opts.json) {
|
|
297
|
+
console.log(JSON.stringify({
|
|
298
|
+
entries: entries.map((e) => {
|
|
299
|
+
const tier = cat.tierBySlug.get(e.slug);
|
|
300
|
+
return {
|
|
301
|
+
slug: e.slug,
|
|
302
|
+
name: e.name,
|
|
303
|
+
description: e.description,
|
|
304
|
+
type: typeOf(e),
|
|
305
|
+
tags: e.tags,
|
|
306
|
+
suite: isSuite(e),
|
|
307
|
+
isPrivate: e.isPrivate,
|
|
308
|
+
installed: cat.installed.has(e.slug),
|
|
309
|
+
requiredTier: tier?.requiredTier ?? null,
|
|
310
|
+
pricingModel: tier?.pricingModel ?? null,
|
|
311
|
+
locked: cat.lockedSlugs.has(e.slug),
|
|
312
|
+
source: e.source,
|
|
313
|
+
};
|
|
314
|
+
}),
|
|
315
|
+
loggedIn: cat.loggedIn,
|
|
316
|
+
reachedCp: cat.reachedCp,
|
|
317
|
+
nextSteps: FLOW.afterMarketList(),
|
|
318
|
+
}, null, 2));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (entries.length === 0) {
|
|
322
|
+
log.warn("No packages match.");
|
|
323
|
+
if (!cat.reachedCp)
|
|
324
|
+
log.dim("Couldn't reach the control plane — only bundled workspaces are shown.");
|
|
325
|
+
log.dim("Try 'synap market' with no filter, or 'synap login' to see your private packages.");
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
// Suites headline; then group by type.
|
|
329
|
+
const suites = entries.filter((e) => isSuite(e));
|
|
330
|
+
const rest = entries.filter((e) => !isSuite(e));
|
|
331
|
+
const render = (entry) => {
|
|
332
|
+
const b = badges(entry, cat);
|
|
333
|
+
console.log(" " +
|
|
334
|
+
chalk.cyan(entry.slug.padEnd(24)) +
|
|
335
|
+
chalk.bold(entry.name) +
|
|
336
|
+
(b.length ? " " + b.join(" ") : ""));
|
|
337
|
+
if (entry.description)
|
|
338
|
+
console.log(" " + " ".repeat(24) + chalk.dim(clip(entry.description, 66)));
|
|
339
|
+
log.blank();
|
|
340
|
+
};
|
|
341
|
+
if (suites.length > 0) {
|
|
342
|
+
log.heading(" Suites — a whole operating core in one install");
|
|
343
|
+
log.blank();
|
|
344
|
+
suites.forEach(render);
|
|
345
|
+
}
|
|
346
|
+
// Group the rest by type, in a stable order.
|
|
347
|
+
const order = ["workspace", "capability", "skill", "workflow", "view", "cell"];
|
|
348
|
+
const byType = new Map();
|
|
349
|
+
for (const e of rest) {
|
|
350
|
+
const t = typeOf(e);
|
|
351
|
+
(byType.get(t) ?? byType.set(t, []).get(t)).push(e);
|
|
352
|
+
}
|
|
353
|
+
const types = [...byType.keys()].sort((a, b) => (order.indexOf(a) + 1 || 99) - (order.indexOf(b) + 1 || 99));
|
|
354
|
+
for (const t of types) {
|
|
355
|
+
log.heading(` ${t.charAt(0).toUpperCase() + t.slice(1)}`);
|
|
356
|
+
log.blank();
|
|
357
|
+
byType.get(t).forEach(render);
|
|
358
|
+
}
|
|
359
|
+
log.dim(`${entries.length} package${entries.length === 1 ? "" : "s"} available.`);
|
|
360
|
+
if (!cat.loggedIn)
|
|
361
|
+
log.dim("Log in to see your private packages: synap login");
|
|
362
|
+
renderNextSteps(FLOW.afterMarketList());
|
|
363
|
+
}
|
|
364
|
+
// ── Optional --project link ──────────────────────────────────────────────────
|
|
365
|
+
/**
|
|
366
|
+
* Installs are POD-WIDE. A template provisions WORKSPACES (pod-level) and their
|
|
367
|
+
* contents are workspace-level — so linking the install to a project would be
|
|
368
|
+
* superfluous. `--project` is a pure OPT-IN to ALSO tag the seeded entities to a
|
|
369
|
+
* project; it is never prompted, defaulted, or taken from a pin. Returns the
|
|
370
|
+
* resolved project (validated) or undefined for the pod-wide default; exits on
|
|
371
|
+
* an unknown explicit id.
|
|
372
|
+
*/
|
|
373
|
+
async function resolveOptionalProject(cfg, explicitId) {
|
|
374
|
+
if (!explicitId)
|
|
375
|
+
return undefined;
|
|
376
|
+
const match = (await fetchProjects(cfg)).find((p) => p.id === explicitId);
|
|
377
|
+
if (!match) {
|
|
378
|
+
log.error(`No project "${explicitId}" found on this pod.`);
|
|
379
|
+
log.hint("List projects: synap orient");
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
return { projectId: match.id, name: match.name };
|
|
383
|
+
}
|
|
384
|
+
// ── `synap market install <slug>` ───────────────────────────────────────────
|
|
385
|
+
export async function marketInstall(slug, opts) {
|
|
386
|
+
const applyTimeoutMs = parseApplyTimeoutMs(opts.timeout);
|
|
387
|
+
// `installedTemplates` (not just `cat.installed`'s slug set) is fetched here
|
|
388
|
+
// too — it carries per-slug VERSION, which is what lets the stamp-
|
|
389
|
+
// contradiction check below tell "genuinely unchanged" apart from "the pod
|
|
390
|
+
// silently failed to stamp/reconcile a previously-unstamped install".
|
|
391
|
+
const [cat, installedTemplates] = await Promise.all([buildMarketCatalog(), fetchInstalledTemplates()]);
|
|
392
|
+
const entry = cat.entries.find((e) => e.slug === slug);
|
|
393
|
+
if (!entry) {
|
|
394
|
+
log.error(`No package "${slug}" found.`);
|
|
395
|
+
log.hint("Browse what's available: synap market --list");
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
const type = typeOf(entry);
|
|
399
|
+
// Non-workspace types have no CLI→pod install door yet (the pod's
|
|
400
|
+
// /packages/apply provisions a WORKSPACE). Be honest and route the user.
|
|
401
|
+
if (type !== "workspace") {
|
|
402
|
+
log.warn(`"${entry.name}" is a ${type} package — not installable from the CLI yet.`);
|
|
403
|
+
if (type === "capability") {
|
|
404
|
+
log.hint(`Add a capability from the pod catalog: synap cap add "${entry.name}"`);
|
|
405
|
+
}
|
|
406
|
+
log.hint("Install it from the browser marketplace (Marketplace → find it → Install).");
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
// Tier gate: don't silently attempt an ungrantable install.
|
|
410
|
+
if (cat.lockedSlugs.has(slug)) {
|
|
411
|
+
const tier = cat.tierBySlug.get(slug)?.requiredTier;
|
|
412
|
+
log.warn(`"${entry.name}" requires the ${tier ?? "a higher"} tier — your account can't install it yet.`);
|
|
413
|
+
log.hint("Upgrade your plan, then retry. Manage it from the browser billing settings.");
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
let cfg;
|
|
417
|
+
try {
|
|
418
|
+
cfg = await resolveHubConfig(opts);
|
|
419
|
+
}
|
|
420
|
+
catch (e) {
|
|
421
|
+
renderHubError(e);
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
// ── Install target: POD-WIDE. ────────────────────────────────────────────
|
|
425
|
+
// A template just provisions its workspace(s) pod-wide — no project needed.
|
|
426
|
+
// `--project` is an optional add-on to also tag the seeded entities to a
|
|
427
|
+
// project (never prompted, never from a pin). Default = pod-wide.
|
|
428
|
+
const project = await resolveOptionalProject(cfg, opts.project);
|
|
429
|
+
const projectId = project?.projectId;
|
|
430
|
+
const projectName = project?.name;
|
|
431
|
+
if (!opts.json && project) {
|
|
432
|
+
log.info(`Also linking seeded entities to project ${chalk.bold(project.name)}.`);
|
|
433
|
+
}
|
|
434
|
+
// Build the install payload — bundled (offline, byte-identical to the CP
|
|
435
|
+
// definition) when available, else the authed CP definition.
|
|
436
|
+
const cpToken = getStoredToken()?.token;
|
|
437
|
+
let pkg;
|
|
438
|
+
if (cat.bundledSlugs.has(slug)) {
|
|
439
|
+
pkg = toPackageDefinition(slug);
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
const def = await fetchPackageDefinition(slug, cpToken);
|
|
443
|
+
if (!def) {
|
|
444
|
+
log.error(`Couldn't fetch "${entry.name}"'s definition from the control plane.`);
|
|
445
|
+
log.hint(cat.loggedIn ? "" : "It may be private — try: synap login");
|
|
446
|
+
process.exit(1);
|
|
447
|
+
}
|
|
448
|
+
pkg = def;
|
|
449
|
+
}
|
|
450
|
+
// Stamp the version the catalog resolved for this slug — including the
|
|
451
|
+
// authed `/mine` version for private templates — so the pod records the
|
|
452
|
+
// installed baseline and later `market update` drift checks work.
|
|
453
|
+
const remoteVersion = cat.remoteVersionBySlug.get(slug);
|
|
454
|
+
if (remoteVersion)
|
|
455
|
+
pkg._meta = { ...(pkg._meta ?? {}), version: remoteVersion };
|
|
456
|
+
// Pre-apply state — was this slug already installed with NO version stamp?
|
|
457
|
+
// Feeds `classifyApplyResult`'s stamp-contradiction check below.
|
|
458
|
+
const preInstalled = installedTemplates.find((t) => t.slug === slug);
|
|
459
|
+
const wasUnstamped = !!preInstalled && !preInstalled.version;
|
|
460
|
+
const s = opts.json ? null : ora(`Installing ${entry.name}…`).start();
|
|
461
|
+
try {
|
|
462
|
+
// projectId links the workspace's seed entities to the project
|
|
463
|
+
// (belongs_to_project) — exactly what `launch` does. Undefined = pod-wide.
|
|
464
|
+
// targetWorkspaceId (--onto) reconciles the template ADDITIVELY onto an
|
|
465
|
+
// existing workspace instead of creating a new one.
|
|
466
|
+
const res = (await hubPost("/packages/apply", { ...pkg, projectId, targetWorkspaceId: opts.onto }, cfg, applyTimeoutMs));
|
|
467
|
+
if (writeGovernance(res) === "proposed") {
|
|
468
|
+
const proposalId = res.proposalId ? String(res.proposalId) : undefined;
|
|
469
|
+
const steps = FLOW.afterMarketInstall({ slug, proposed: true, projectName });
|
|
470
|
+
s?.stop();
|
|
471
|
+
if (opts.json) {
|
|
472
|
+
console.log(JSON.stringify({ slug, outcome: "proposed", proposalId, projectId, nextSteps: steps }, null, 2));
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
log.info(`${entry.name} — proposed (under review, not live yet).`);
|
|
476
|
+
if (proposalId)
|
|
477
|
+
log.hint(`Approve: synap proposals approve ${proposalId.slice(0, 8)}`);
|
|
478
|
+
renderNextSteps(steps);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const ws = res.workspace;
|
|
482
|
+
const dependencies = res.dependencies;
|
|
483
|
+
const seedSummary = res.seedSummary;
|
|
484
|
+
const steps = FLOW.afterMarketInstall({ slug, proposed: false, projectName });
|
|
485
|
+
s?.stop();
|
|
486
|
+
const verdict = classifyApplyResult(entry, ws, dependencies, seedSummary, {
|
|
487
|
+
remoteVersionSent: remoteVersion,
|
|
488
|
+
wasUnstamped,
|
|
489
|
+
});
|
|
490
|
+
if (opts.json) {
|
|
491
|
+
console.log(JSON.stringify({
|
|
492
|
+
slug,
|
|
493
|
+
outcome: ws?.outcome ?? ws?.status ?? "unknown",
|
|
494
|
+
status: verdict.status,
|
|
495
|
+
warnings: verdict.warnings,
|
|
496
|
+
workspace: ws,
|
|
497
|
+
dependencies,
|
|
498
|
+
seedSummary,
|
|
499
|
+
projectId,
|
|
500
|
+
onto: opts.onto,
|
|
501
|
+
nextSteps: steps,
|
|
502
|
+
}, null, 2));
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
// `--onto` reconciles onto an EXISTING workspace — the honest verdict
|
|
506
|
+
// reads "reconciled onto <workspace>", not the create-flavored default
|
|
507
|
+
// text `printApplyVerdict` would otherwise print for the same outcomes.
|
|
508
|
+
if (opts.onto) {
|
|
509
|
+
const wsLabel = ws?.workspaceId ? chalk.bold(ws.workspaceId) : chalk.bold(opts.onto);
|
|
510
|
+
switch (verdict.status) {
|
|
511
|
+
case "installed":
|
|
512
|
+
case "updated":
|
|
513
|
+
log.success(`Reconciled ${entry.name} onto ${wsLabel}.`);
|
|
514
|
+
break;
|
|
515
|
+
case "installed-with-failures":
|
|
516
|
+
case "updated-with-failures":
|
|
517
|
+
log.warn(`Reconciled ${entry.name} onto ${wsLabel} — completed with capability failures (see below).`);
|
|
518
|
+
break;
|
|
519
|
+
case "unchanged":
|
|
520
|
+
log.info(`• ${entry.name} is already up to date on ${wsLabel}.`);
|
|
521
|
+
break;
|
|
522
|
+
default:
|
|
523
|
+
printApplyVerdict(entry, verdict);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
printApplyVerdict(entry, verdict);
|
|
528
|
+
}
|
|
529
|
+
printSeedOutcomes(dependencies, seedSummary);
|
|
530
|
+
if (ws?.outcome || ws?.status === "composed" || ws?.status === "created")
|
|
531
|
+
renderNextSteps(steps);
|
|
532
|
+
}
|
|
533
|
+
catch (e) {
|
|
534
|
+
s?.stop();
|
|
535
|
+
log.error(`${entry.name} failed`);
|
|
536
|
+
renderHubError(e);
|
|
537
|
+
process.exit(1);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Per-installed-slug drift check. Starts from the shared `computeTemplateUpdates`
|
|
542
|
+
* (handles bundled-only / private-only slugs via `bundleVersion`), then
|
|
543
|
+
* overrides `latestVersion`/`updateAvailable` with the CP row's OWN version for
|
|
544
|
+
* any slug in `remoteVersionBySlug` — the merge-winner bypass this command
|
|
545
|
+
* needs (see `MarketCatalog.remoteVersionBySlug`'s doc). A slug the pod never
|
|
546
|
+
* version-stamped is reported `noVersionInfo: true`, never silently "outdated".
|
|
547
|
+
*/
|
|
548
|
+
export function computeUpdates(installedTemplates, cat) {
|
|
549
|
+
const withVersion = installedTemplates.filter((t) => !!t.version);
|
|
550
|
+
const base = computeTemplateUpdates(withVersion.map((t) => ({ slug: t.slug, version: t.version })), cat.entries, bundledTemplatesVersion());
|
|
551
|
+
const baseBySlug = new Map(base.map((b) => [b.slug, b]));
|
|
552
|
+
return installedTemplates.map((t) => {
|
|
553
|
+
if (!t.version) {
|
|
554
|
+
return {
|
|
555
|
+
slug: t.slug,
|
|
556
|
+
installedVersion: "",
|
|
557
|
+
latestVersion: cat.remoteVersionBySlug.get(t.slug),
|
|
558
|
+
updateAvailable: false,
|
|
559
|
+
noVersionInfo: true,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
const b = baseBySlug.get(t.slug);
|
|
563
|
+
const remoteVersion = cat.remoteVersionBySlug.get(t.slug);
|
|
564
|
+
if (remoteVersion) {
|
|
565
|
+
return {
|
|
566
|
+
...b,
|
|
567
|
+
latestVersion: remoteVersion,
|
|
568
|
+
updateAvailable: remoteVersion !== t.version,
|
|
569
|
+
noVersionInfo: false,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
return { ...b, noVersionInfo: false };
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Apply one package via the SAME door `marketInstall` uses
|
|
577
|
+
* (`POST /packages/apply`) — for an already-installed slug this re-runs the
|
|
578
|
+
* pod's idempotent-create path, which reconciles when the pod build supports
|
|
579
|
+
* it (see the `applyOnePackage` caveat below for what "created" means here).
|
|
580
|
+
*/
|
|
581
|
+
async function applyOnePackage(entry, cat, cfg, opts, applyTimeoutMs = APPLY_TIMEOUT_MS) {
|
|
582
|
+
if (cat.lockedSlugs.has(entry.slug)) {
|
|
583
|
+
const tier = cat.tierBySlug.get(entry.slug)?.requiredTier;
|
|
584
|
+
if (!opts.json)
|
|
585
|
+
log.warn(`"${entry.name}" requires the ${tier ?? "a higher"} tier — skipping.`);
|
|
586
|
+
return { slug: entry.slug, outcome: "locked", status: "locked", warnings: [] };
|
|
587
|
+
}
|
|
588
|
+
const cpToken = getStoredToken()?.token;
|
|
589
|
+
let pkg;
|
|
590
|
+
if (cat.bundledSlugs.has(entry.slug)) {
|
|
591
|
+
pkg = toPackageDefinition(entry.slug);
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
const def = await fetchPackageDefinition(entry.slug, cpToken);
|
|
595
|
+
if (!def) {
|
|
596
|
+
if (!opts.json) {
|
|
597
|
+
log.error(`Couldn't fetch "${entry.name}"'s definition from the control plane.`);
|
|
598
|
+
if (!cat.loggedIn)
|
|
599
|
+
log.hint("It may be private — try: synap login");
|
|
600
|
+
}
|
|
601
|
+
return { slug: entry.slug, outcome: "fetch-failed", status: "fetch-failed", warnings: [] };
|
|
602
|
+
}
|
|
603
|
+
pkg = def;
|
|
604
|
+
}
|
|
605
|
+
// Same version stamp as `marketInstall` — needed for private templates too,
|
|
606
|
+
// since `applyOnePackage` re-runs the apply door on every update.
|
|
607
|
+
const remoteVersion = cat.remoteVersionBySlug.get(entry.slug);
|
|
608
|
+
if (remoteVersion)
|
|
609
|
+
pkg._meta = { ...(pkg._meta ?? {}), version: remoteVersion };
|
|
610
|
+
const s = opts.json ? null : ora(`Updating ${entry.name}…`).start();
|
|
611
|
+
try {
|
|
612
|
+
const res = (await hubPost("/packages/apply", pkg, cfg, applyTimeoutMs));
|
|
613
|
+
s?.stop();
|
|
614
|
+
if (writeGovernance(res) === "proposed") {
|
|
615
|
+
const proposalId = res.proposalId ? String(res.proposalId) : undefined;
|
|
616
|
+
if (!opts.json) {
|
|
617
|
+
log.info(`${entry.name} — update proposed (under review, not live yet).`);
|
|
618
|
+
if (proposalId)
|
|
619
|
+
log.hint(`Approve: synap proposals approve ${proposalId.slice(0, 8)}`);
|
|
620
|
+
}
|
|
621
|
+
return { slug: entry.slug, outcome: "proposed", status: "proposed", warnings: [] };
|
|
622
|
+
}
|
|
623
|
+
const ws = res.workspace;
|
|
624
|
+
const dependencies = res.dependencies;
|
|
625
|
+
const seedSummary = res.seedSummary;
|
|
626
|
+
// `wasUnstamped: false` — `applyOnePackage` is only ever reached (from
|
|
627
|
+
// `marketUpdate`) for a slug that already had `updateAvailable: true`,
|
|
628
|
+
// which by construction means it already carried a recorded version. The
|
|
629
|
+
// stamp-contradiction check is therefore a no-op here by design; it's
|
|
630
|
+
// `marketInstall`'s job to catch it on a first-time/unstamped install.
|
|
631
|
+
const verdict = classifyApplyResult(entry, ws, dependencies, seedSummary, {
|
|
632
|
+
remoteVersionSent: remoteVersion,
|
|
633
|
+
wasUnstamped: false,
|
|
634
|
+
});
|
|
635
|
+
if (!opts.json) {
|
|
636
|
+
printApplyVerdict(entry, verdict);
|
|
637
|
+
printSeedOutcomes(dependencies, seedSummary);
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
640
|
+
slug: entry.slug,
|
|
641
|
+
outcome: ws?.outcome ?? ws?.status ?? "unknown",
|
|
642
|
+
status: verdict.status,
|
|
643
|
+
warnings: verdict.warnings,
|
|
644
|
+
dependencies,
|
|
645
|
+
seedSummary,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
catch (e) {
|
|
649
|
+
s?.stop();
|
|
650
|
+
const timedOut = e instanceof HubNetworkError && e.code === "TIMEOUT";
|
|
651
|
+
if (!opts.json) {
|
|
652
|
+
log.error(`${entry.name} update failed`);
|
|
653
|
+
renderHubError(e);
|
|
654
|
+
}
|
|
655
|
+
return {
|
|
656
|
+
slug: entry.slug,
|
|
657
|
+
outcome: timedOut ? "timed-out" : "failed",
|
|
658
|
+
status: timedOut ? "timed-out" : "failed",
|
|
659
|
+
warnings: [],
|
|
660
|
+
error: e instanceof Error ? e.message : String(e),
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
export async function marketUpdate(slugsArg, opts) {
|
|
665
|
+
const applyTimeoutMs = parseApplyTimeoutMs(opts.timeout);
|
|
666
|
+
const [installedTemplates, cat] = await Promise.all([fetchInstalledTemplates(), buildMarketCatalog()]);
|
|
667
|
+
if (installedTemplates.length === 0) {
|
|
668
|
+
if (opts.json) {
|
|
669
|
+
console.log(JSON.stringify({ checks: [], loggedIn: cat.loggedIn }, null, 2));
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
log.warn("No installed packages found on this pod.");
|
|
673
|
+
log.hint("Install one: synap market install <slug>");
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
let checks = computeUpdates(installedTemplates, cat);
|
|
677
|
+
const entryBySlug = new Map(cat.entries.map((e) => [e.slug, e]));
|
|
678
|
+
const explicitSlugs = !!slugsArg && slugsArg.length > 0;
|
|
679
|
+
let unknownSlugs = [];
|
|
680
|
+
if (explicitSlugs) {
|
|
681
|
+
const known = new Set(checks.map((c) => c.slug));
|
|
682
|
+
unknownSlugs = slugsArg.filter((s) => !known.has(s));
|
|
683
|
+
const validSlugs = slugsArg.filter((s) => known.has(s));
|
|
684
|
+
for (const s of unknownSlugs) {
|
|
685
|
+
if (!opts.json)
|
|
686
|
+
log.error(`"${s}" is not installed on this pod — skipping.`);
|
|
687
|
+
}
|
|
688
|
+
if (validSlugs.length === 0) {
|
|
689
|
+
if (opts.json) {
|
|
690
|
+
console.log(JSON.stringify({ checks: [], loggedIn: cat.loggedIn, unknownSlugs, applied: [] }, null, 2));
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
log.hint("List what's installed: synap market installed");
|
|
694
|
+
process.exit(1);
|
|
695
|
+
}
|
|
696
|
+
checks = checks.filter((c) => validSlugs.includes(c.slug));
|
|
697
|
+
}
|
|
698
|
+
const withUpdates = checks.filter((c) => c.updateAvailable);
|
|
699
|
+
const noVersionInfo = checks.filter((c) => c.noVersionInfo);
|
|
700
|
+
// Private templates resolve only via the authed CP path — an update check
|
|
701
|
+
// that found NOTHING while logged out is ambiguous (could be genuinely
|
|
702
|
+
// current, could be blind to private drift). Say so rather than staying
|
|
703
|
+
// silently quiet.
|
|
704
|
+
if (!cat.loggedIn && !opts.json) {
|
|
705
|
+
log.dim("Not logged in — private template updates aren't visible. Run: synap login");
|
|
706
|
+
}
|
|
707
|
+
// --dry-run is a NAMED alias for the "no --yes" preview path — forces the
|
|
708
|
+
// preview even when --yes or explicit slugs would otherwise trigger apply.
|
|
709
|
+
const shouldApply = !opts.dryRun && (explicitSlugs || opts.yes);
|
|
710
|
+
if (!opts.json) {
|
|
711
|
+
log.heading(" Installed packages");
|
|
712
|
+
log.blank();
|
|
713
|
+
for (const c of checks) {
|
|
714
|
+
const label = c.noVersionInfo
|
|
715
|
+
? noVersionLabel(entryBySlug.get(c.slug)?.isPrivate ?? false, cat.loggedIn)
|
|
716
|
+
: c.updateAvailable
|
|
717
|
+
? chalk.yellow(`update available `) + chalk.dim(c.installedVersion) + " → " + c.latestVersion
|
|
718
|
+
: chalk.green("up to date");
|
|
719
|
+
console.log(" " + chalk.cyan(c.slug.padEnd(28)) + label);
|
|
720
|
+
}
|
|
721
|
+
log.blank();
|
|
722
|
+
if (noVersionInfo.length > 0) {
|
|
723
|
+
log.dim(`${noVersionInfo.length} installed without a version stamp — can't check ${noVersionInfo.length === 1 ? "it" : "them"} for updates.`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
if (withUpdates.length === 0) {
|
|
727
|
+
// "Nothing to update" is only honest when every installed row was
|
|
728
|
+
// actually CHECKED. Rows the pod can't version-check are "couldn't be
|
|
729
|
+
// checked", not silently folded into a clean ✓ — see rule #2 in this
|
|
730
|
+
// file's header.
|
|
731
|
+
if (opts.json) {
|
|
732
|
+
console.log(JSON.stringify({
|
|
733
|
+
checks,
|
|
734
|
+
loggedIn: cat.loggedIn,
|
|
735
|
+
unknownSlugs,
|
|
736
|
+
applied: [],
|
|
737
|
+
status: noVersionInfo.length > 0 ? "partial" : "clean",
|
|
738
|
+
uncheckable: noVersionInfo.map((c) => c.slug),
|
|
739
|
+
}, null, 2));
|
|
740
|
+
}
|
|
741
|
+
else if (noVersionInfo.length > 0) {
|
|
742
|
+
const upToDate = checks.length - noVersionInfo.length;
|
|
743
|
+
console.log(" " +
|
|
744
|
+
chalk.yellow(`⚠ ${upToDate} up to date · ${noVersionInfo.length} couldn't be checked`));
|
|
745
|
+
log.hint("Run 'synap market installed' for why — unstamped install (reinstall to fix) or a private template while logged out (synap login).");
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
log.success("Nothing to update.");
|
|
749
|
+
}
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if (!shouldApply) {
|
|
753
|
+
const steps = FLOW.afterMarketUpdateCheck(withUpdates.map((c) => c.slug));
|
|
754
|
+
if (opts.json) {
|
|
755
|
+
console.log(JSON.stringify({ checks, loggedIn: cat.loggedIn, unknownSlugs, applied: [], nextSteps: steps }, null, 2));
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
log.info(`${withUpdates.length} package${withUpdates.length === 1 ? "" : "s"} have updates.`);
|
|
759
|
+
renderNextSteps(steps);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
let cfg;
|
|
763
|
+
try {
|
|
764
|
+
cfg = await resolveHubConfig(opts);
|
|
765
|
+
}
|
|
766
|
+
catch (e) {
|
|
767
|
+
renderHubError(e);
|
|
768
|
+
process.exit(1);
|
|
769
|
+
}
|
|
770
|
+
const results = [];
|
|
771
|
+
for (const c of withUpdates) {
|
|
772
|
+
const entry = cat.entries.find((e) => e.slug === c.slug);
|
|
773
|
+
if (!entry) {
|
|
774
|
+
if (!opts.json)
|
|
775
|
+
log.warn(`"${c.slug}" is no longer in the catalog — skipping.`);
|
|
776
|
+
results.push({ slug: c.slug, outcome: "not-in-catalog", status: "not-in-catalog", warnings: [] });
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
results.push(await applyOnePackage(entry, cat, cfg, { json: opts.json }, applyTimeoutMs));
|
|
780
|
+
}
|
|
781
|
+
const summary = summarizeApplyResults(results);
|
|
782
|
+
const steps = FLOW.afterMarketUpdateApply();
|
|
783
|
+
if (opts.json) {
|
|
784
|
+
console.log(JSON.stringify({ checks, loggedIn: cat.loggedIn, unknownSlugs, applied: results, summary, nextSteps: steps }, null, 2));
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
log.blank();
|
|
788
|
+
console.log(" " + summary.line);
|
|
789
|
+
renderNextSteps(steps);
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* One-line rollup over a batch of `applyOnePackage` outcomes — computed from
|
|
793
|
+
* the SAME per-item `status` (`classifyApplyResult`'s honest verdict) the
|
|
794
|
+
* loop already collects, not a separate tally. `"installed"`/`"updated"`/
|
|
795
|
+
* `"composed"`/`"legacy-unknown"` count as a clean update, `"unchanged"` as
|
|
796
|
+
* already-current, the `*-with-failures` and `"stamp-contradiction"` statuses
|
|
797
|
+
* get their OWN bucket — a partial capability-seed failure or a stamp that
|
|
798
|
+
* silently didn't take is never folded into a clean "updated" count.
|
|
799
|
+
* `"timed-out"` is broken out from the generic `"failed"` bucket; everything
|
|
800
|
+
* else (locked/proposed/fetch-failed/not-in-catalog/no-workspace/unknown)
|
|
801
|
+
* rolls into "failed".
|
|
802
|
+
*/
|
|
803
|
+
function summarizeApplyResults(results) {
|
|
804
|
+
let updated = 0;
|
|
805
|
+
let unchanged = 0;
|
|
806
|
+
let withFailures = 0;
|
|
807
|
+
let timedOut = 0;
|
|
808
|
+
let failed = 0;
|
|
809
|
+
for (const r of results) {
|
|
810
|
+
switch (r.status) {
|
|
811
|
+
case "installed":
|
|
812
|
+
case "updated":
|
|
813
|
+
case "composed":
|
|
814
|
+
case "legacy-unknown":
|
|
815
|
+
updated++;
|
|
816
|
+
break;
|
|
817
|
+
case "unchanged":
|
|
818
|
+
unchanged++;
|
|
819
|
+
break;
|
|
820
|
+
case "installed-with-failures":
|
|
821
|
+
case "updated-with-failures":
|
|
822
|
+
case "composed-with-failures":
|
|
823
|
+
case "stamp-contradiction":
|
|
824
|
+
withFailures++;
|
|
825
|
+
break;
|
|
826
|
+
case "timed-out":
|
|
827
|
+
timedOut++;
|
|
828
|
+
break;
|
|
829
|
+
default:
|
|
830
|
+
failed++;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
const parts = [`${updated} updated`, `${unchanged} already current`];
|
|
834
|
+
if (withFailures > 0)
|
|
835
|
+
parts.push(`${withFailures} completed with failures`);
|
|
836
|
+
if (failed > 0)
|
|
837
|
+
parts.push(`${failed} failed`);
|
|
838
|
+
if (timedOut > 0)
|
|
839
|
+
parts.push(`${timedOut} timed out`);
|
|
840
|
+
const clean = withFailures === 0 && failed === 0 && timedOut === 0;
|
|
841
|
+
const color = clean ? chalk.green : chalk.yellow;
|
|
842
|
+
return {
|
|
843
|
+
updated,
|
|
844
|
+
unchanged,
|
|
845
|
+
withFailures,
|
|
846
|
+
failed,
|
|
847
|
+
timedOut,
|
|
848
|
+
line: color(`${clean ? "✓" : "⚠"} ${parts.join(" · ")}`),
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
// ── `synap market installed [--json] [--outdated]` — pure-read inventory ────
|
|
852
|
+
/** `"h-<hash>"` → `"h-<first 6 hash chars>"`. Semver strings pass through. */
|
|
853
|
+
function truncateVersion(v) {
|
|
854
|
+
return isHashVersion(v) ? `h-${v.slice(2, 8)}` : v;
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Honest `noVersionInfo` label — a PRIVATE template that's blind to updates
|
|
858
|
+
* because the caller is logged out is a different (recoverable) situation
|
|
859
|
+
* than a genuinely unstampable install: "reinstall" is the wrong fix when
|
|
860
|
+
* the real fix is `synap login`.
|
|
861
|
+
*/
|
|
862
|
+
function noVersionLabel(isPrivate, loggedIn) {
|
|
863
|
+
return isPrivate && !loggedIn
|
|
864
|
+
? chalk.dim("connect to the Control Plane to check/update — run: synap login")
|
|
865
|
+
: chalk.dim("version unknown — reinstall to enable update checks");
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Human status cell for one installed row — distinguishes hash drift (no
|
|
869
|
+
* ordering, just "changed") from semver drift ("update available").
|
|
870
|
+
*/
|
|
871
|
+
function installedStatusLabel(c, isPrivate, loggedIn) {
|
|
872
|
+
if (c.noVersionInfo)
|
|
873
|
+
return noVersionLabel(isPrivate, loggedIn);
|
|
874
|
+
if (!c.updateAvailable)
|
|
875
|
+
return chalk.green("up to date");
|
|
876
|
+
const installed = truncateVersion(c.installedVersion);
|
|
877
|
+
const latest = c.latestVersion ? truncateVersion(c.latestVersion) : "?";
|
|
878
|
+
const hashPair = isHashVersion(c.installedVersion) || isHashVersion(c.latestVersion ?? "");
|
|
879
|
+
const label = hashPair ? "content changed" : "update available";
|
|
880
|
+
return chalk.yellow(`${label} `) + chalk.dim(installed) + " → " + latest;
|
|
881
|
+
}
|
|
882
|
+
// ── Composition engine — the ONE traversal door ─────────────────────────────
|
|
883
|
+
//
|
|
884
|
+
// Both `--tree` (nested view) and `--layers` (flat DAG view) are RENDERERS
|
|
885
|
+
// over the shared `layerTemplateGraph` engine (`@synap-core/workspace-
|
|
886
|
+
// templates`) — neither hand-walks `dependencies[]` itself anymore. The old
|
|
887
|
+
// bespoke walker (`collectDescendantSlugs`/`buildDependencyNode`) rebuilt a
|
|
888
|
+
// fresh node for every OCCURRENCE of a shared slug (`foundation` printed once
|
|
889
|
+
// per branch that reached it) and was blind to a remote/private CP-only
|
|
890
|
+
// package's `dependencies[]` (only bundled templates were ever consulted, so
|
|
891
|
+
// such a package's subtree silently rendered empty — a live bug). The engine
|
|
892
|
+
// fixes both: one node per slug at its longest-path layer, edges converging
|
|
893
|
+
// on it, via an INJECTED `edgesOf` (see `buildEdgesOf` below) that falls back
|
|
894
|
+
// to a remote catalog entry's fetched `definition.dependencies` when the slug
|
|
895
|
+
// isn't in the bundle.
|
|
896
|
+
/**
|
|
897
|
+
* Synchronous `edgesOf` for `layerTemplateGraph`, backed by an async prefetch
|
|
898
|
+
* BFS: bundled slugs resolve instantly via `bundledEdgesOf`; any slug NOT
|
|
899
|
+
* bundled (a remote/private CP-only package) has its full definition fetched
|
|
900
|
+
* via `fetchPackageDefinition` and its `.dependencies` cached. `seeds` is the
|
|
901
|
+
* set of slugs to start the BFS from — pass every installed slug so the
|
|
902
|
+
* resulting closure covers everything either renderer might touch.
|
|
903
|
+
*/
|
|
904
|
+
async function buildEdgesOf(seeds, cat, cpToken) {
|
|
905
|
+
const cache = new Map();
|
|
906
|
+
const visited = new Set();
|
|
907
|
+
let frontier = [...new Set(seeds)];
|
|
908
|
+
while (frontier.length > 0) {
|
|
909
|
+
const nextFrontier = new Set();
|
|
910
|
+
await Promise.all(frontier.map(async (slug) => {
|
|
911
|
+
if (visited.has(slug))
|
|
912
|
+
return;
|
|
913
|
+
visited.add(slug);
|
|
914
|
+
let deps;
|
|
915
|
+
if (cat.bundledSlugs.has(slug)) {
|
|
916
|
+
deps = bundledEdgesOf(slug);
|
|
917
|
+
}
|
|
918
|
+
else {
|
|
919
|
+
try {
|
|
920
|
+
const def = await fetchPackageDefinition(slug, cpToken);
|
|
921
|
+
deps = def?.dependencies ?? [];
|
|
922
|
+
}
|
|
923
|
+
catch {
|
|
924
|
+
deps = [];
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
cache.set(slug, deps);
|
|
928
|
+
for (const d of deps)
|
|
929
|
+
nextFrontier.add(d.slug);
|
|
930
|
+
}));
|
|
931
|
+
frontier = [...nextFrontier].filter((s) => !visited.has(s));
|
|
932
|
+
}
|
|
933
|
+
// Fallback covers a slug the BFS never seeded (shouldn't happen — every
|
|
934
|
+
// `edgesOf` call the engine makes is reachable from `seeds`) — cheap and
|
|
935
|
+
// safe either way since it's the same bundled-only lookup as the default.
|
|
936
|
+
return (slug) => cache.get(slug) ?? bundledEdgesOf(slug);
|
|
937
|
+
}
|
|
938
|
+
/** `CompositionInstalledEntry[]` from the Hub's installed-templates rows — the shape both `--tree` and `--layers` feed the engine. */
|
|
939
|
+
function toInstalledEntries(installedTemplates) {
|
|
940
|
+
return installedTemplates.map((t) => ({
|
|
941
|
+
slug: t.slug,
|
|
942
|
+
workspaceId: t.workspaceId,
|
|
943
|
+
workspaceName: t.workspaceName,
|
|
944
|
+
version: t.version,
|
|
945
|
+
}));
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* The nested composition-tree view: one tree per installed TOP-LEVEL package
|
|
949
|
+
* (a suite, or any installed package whose `edgesOf` resolves at least one
|
|
950
|
+
* dependency — bundled OR remote). Everything else stays a flat leaf
|
|
951
|
+
* (`leafRows`, rendered like a plain `market installed` row). This is the
|
|
952
|
+
* SAME "top-level" rule the old walker used (kept verbatim for `--tree`'s
|
|
953
|
+
* JSON/text backward compatibility — see `marketInstalled`'s before/after
|
|
954
|
+
* diff check), just evaluated against the fixed `edgesOf` instead of the
|
|
955
|
+
* bundled-only map.
|
|
956
|
+
*
|
|
957
|
+
* Nested `TreeNode`s are built by walking `graph.edges` (both `require` and
|
|
958
|
+
* `compose` relations — the engine excludes compose SOURCES from `nodes`,
|
|
959
|
+
* rule 2, but never from `edges`, so the nested view still shows them exactly
|
|
960
|
+
* where the old walker did) and `shared` reads straight off each node's
|
|
961
|
+
* `roots` (the engine's true reachability set), replacing the old walker's
|
|
962
|
+
* bespoke reverse index.
|
|
963
|
+
*/
|
|
964
|
+
export async function buildCompositionTrees(rows, cat, installedTemplates, workspaceCountBySlug) {
|
|
965
|
+
const entryBySlug = new Map(cat.entries.map((e) => [e.slug, e]));
|
|
966
|
+
const installedBySlug = new Map(installedTemplates.map((t) => [t.slug, t]));
|
|
967
|
+
const checksBySlug = new Map(rows.map((c) => [c.slug, c]));
|
|
968
|
+
const cpToken = getStoredToken()?.token;
|
|
969
|
+
const installedSlugs = [...new Set(installedTemplates.map((t) => t.slug))];
|
|
970
|
+
const edgesOf = await buildEdgesOf(installedSlugs, cat, cpToken);
|
|
971
|
+
const topLevel = rows.filter((c) => {
|
|
972
|
+
const entry = entryBySlug.get(c.slug);
|
|
973
|
+
return (entry && isSuite(entry)) || edgesOf(c.slug).length > 0;
|
|
974
|
+
});
|
|
975
|
+
const topLevelSlugs = new Set(topLevel.map((c) => c.slug));
|
|
976
|
+
const leafRows = rows.filter((c) => !topLevelSlugs.has(c.slug));
|
|
977
|
+
const graph = layerTemplateGraph({
|
|
978
|
+
roots: [...topLevelSlugs],
|
|
979
|
+
edgesOf,
|
|
980
|
+
nameOf: (slug) => entryBySlug.get(slug)?.name,
|
|
981
|
+
installed: toInstalledEntries(installedTemplates),
|
|
982
|
+
updates: rows,
|
|
983
|
+
});
|
|
984
|
+
function buildTreeNode(edge, currentTopLevelSlug, ancestors) {
|
|
985
|
+
const slug = edge.to;
|
|
986
|
+
const entry = entryBySlug.get(slug);
|
|
987
|
+
const installedInfo = installedBySlug.get(slug);
|
|
988
|
+
const node = graph.nodes.get(slug);
|
|
989
|
+
// Exclude BOTH the walking root (matches the old walker's exclusion) AND
|
|
990
|
+
// the node's own slug (a top-level package that's ALSO separately
|
|
991
|
+
// installed reaches itself trivially in `node.roots` — rule 4 of Phase 4
|
|
992
|
+
// — which the old bespoke `collectDescendantSlugs` never surfaced since
|
|
993
|
+
// it only ever recorded genuine descendants, never a self-loop).
|
|
994
|
+
const shared = (node?.roots ?? [])
|
|
995
|
+
.filter((r) => r !== currentTopLevelSlug && r !== slug)
|
|
996
|
+
.map((r) => entryBySlug.get(r)?.name ?? r);
|
|
997
|
+
const children = [];
|
|
998
|
+
if (!ancestors.has(slug)) {
|
|
999
|
+
const nextAncestors = new Set(ancestors);
|
|
1000
|
+
nextAncestors.add(slug);
|
|
1001
|
+
for (const childEdge of graph.edges.filter((e) => e.from === slug)) {
|
|
1002
|
+
children.push(buildTreeNode(childEdge, currentTopLevelSlug, nextAncestors));
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
const check = checksBySlug.get(slug);
|
|
1006
|
+
return {
|
|
1007
|
+
slug,
|
|
1008
|
+
name: entry?.name ?? slug,
|
|
1009
|
+
relation: edge.relation,
|
|
1010
|
+
depKind: edge.depKind,
|
|
1011
|
+
installed: !!installedInfo,
|
|
1012
|
+
workspaceId: installedInfo?.workspaceId,
|
|
1013
|
+
workspaceName: installedInfo?.workspaceName,
|
|
1014
|
+
installedVersion: check?.installedVersion,
|
|
1015
|
+
latestVersion: check?.latestVersion ?? null,
|
|
1016
|
+
updateAvailable: check?.updateAvailable ?? false,
|
|
1017
|
+
noVersionInfo: check ? check.noVersionInfo : !installedInfo,
|
|
1018
|
+
shared,
|
|
1019
|
+
children,
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
const packages = topLevel.map((c) => {
|
|
1023
|
+
const entry = entryBySlug.get(c.slug);
|
|
1024
|
+
const children = graph.edges
|
|
1025
|
+
.filter((e) => e.from === c.slug)
|
|
1026
|
+
.map((edge) => buildTreeNode(edge, c.slug, new Set([c.slug])));
|
|
1027
|
+
return {
|
|
1028
|
+
slug: c.slug,
|
|
1029
|
+
name: entry?.name ?? c.slug,
|
|
1030
|
+
installedVersion: c.installedVersion,
|
|
1031
|
+
latestVersion: c.latestVersion ?? null,
|
|
1032
|
+
updateAvailable: c.updateAvailable,
|
|
1033
|
+
noVersionInfo: c.noVersionInfo,
|
|
1034
|
+
workspaceCount: workspaceCountBySlug.get(c.slug) ?? 0,
|
|
1035
|
+
isSuite: !!(entry && isSuite(entry)),
|
|
1036
|
+
children,
|
|
1037
|
+
};
|
|
1038
|
+
});
|
|
1039
|
+
return { packages, leafRows };
|
|
1040
|
+
}
|
|
1041
|
+
function nodeStatusLabel(node) {
|
|
1042
|
+
if (!node.installed)
|
|
1043
|
+
return chalk.dim("not installed");
|
|
1044
|
+
if (node.noVersionInfo)
|
|
1045
|
+
return chalk.dim("can't check");
|
|
1046
|
+
if (!node.updateAvailable)
|
|
1047
|
+
return chalk.green("up to date");
|
|
1048
|
+
const installed = node.installedVersion ? truncateVersion(node.installedVersion) : "?";
|
|
1049
|
+
const latest = node.latestVersion ? truncateVersion(node.latestVersion) : "?";
|
|
1050
|
+
return chalk.yellow("update available ") + chalk.dim(installed) + " → " + latest;
|
|
1051
|
+
}
|
|
1052
|
+
function printDependencyNode(node, prefix, isLast) {
|
|
1053
|
+
const branch = isLast ? "└─ " : "├─ ";
|
|
1054
|
+
const targetPlain = node.installed ? `→ ${node.workspaceName}` : "→ (not installed)";
|
|
1055
|
+
const target = node.installed ? `→ ${chalk.bold(node.workspaceName)}` : chalk.dim("→ (not installed)");
|
|
1056
|
+
const relationHint = node.relation === "compose" ? chalk.dim(" ↳ composes onto its base") : "";
|
|
1057
|
+
const sharedTag = node.shared.length ? chalk.magenta(` [shared: ${node.shared.join(", ")}]`) : "";
|
|
1058
|
+
const pad = (s, n) => s + " ".repeat(Math.max(1, n - s.length));
|
|
1059
|
+
console.log(prefix +
|
|
1060
|
+
chalk.dim(branch) +
|
|
1061
|
+
chalk.cyan(pad(node.slug, 20)) +
|
|
1062
|
+
pad(target, target.length + Math.max(0, 26 - targetPlain.length)) +
|
|
1063
|
+
nodeStatusLabel(node) +
|
|
1064
|
+
relationHint +
|
|
1065
|
+
sharedTag);
|
|
1066
|
+
const childPrefix = prefix + (isLast ? " " : chalk.dim("│ "));
|
|
1067
|
+
node.children.forEach((child, i) => printDependencyNode(child, childPrefix, i === node.children.length - 1));
|
|
1068
|
+
}
|
|
1069
|
+
export function printCompositionTree(pkg) {
|
|
1070
|
+
const suiteTag = pkg.isSuite ? " " + chalk.magenta("suite") : "";
|
|
1071
|
+
console.log(chalk.bold(pkg.name) +
|
|
1072
|
+
" " +
|
|
1073
|
+
chalk.cyan(`(${pkg.slug})`) +
|
|
1074
|
+
" " +
|
|
1075
|
+
chalk.dim(truncateVersion(pkg.installedVersion)) +
|
|
1076
|
+
" " +
|
|
1077
|
+
(pkg.noVersionInfo
|
|
1078
|
+
? chalk.dim("can't check — reinstall")
|
|
1079
|
+
: pkg.updateAvailable
|
|
1080
|
+
? chalk.yellow("update available")
|
|
1081
|
+
: chalk.green("up to date")) +
|
|
1082
|
+
suiteTag);
|
|
1083
|
+
pkg.children.forEach((child, i) => printDependencyNode(child, "", i === pkg.children.length - 1));
|
|
1084
|
+
log.blank();
|
|
1085
|
+
}
|
|
1086
|
+
// ── `synap market installed --layers` — flat DAG view ──────────────────────
|
|
1087
|
+
//
|
|
1088
|
+
// `--tree` nests (and, by design, reprints a shared base once per branch that
|
|
1089
|
+
// reaches it — see `TreeNode.shared`). `--layers` is the OTHER read of the
|
|
1090
|
+
// SAME engine: each installed/dependency slug appears EXACTLY ONCE, sunk to
|
|
1091
|
+
// its longest-path layer (roots at the top, bedrock at the bottom), with true
|
|
1092
|
+
// fan-in (`← N packages`) instead of a per-branch "shared" tag.
|
|
1093
|
+
/**
|
|
1094
|
+
* Full graph over every currently-installed package — the ROOTS are every
|
|
1095
|
+
* "top-level" installed slug (a suite, or one with its own resolved
|
|
1096
|
+
* dependencies — same rule `buildCompositionTrees` uses) PLUS any installed
|
|
1097
|
+
* slug left unreached by that walk (a standalone leaf with no dependents/
|
|
1098
|
+
* dependencies of its own). The second half matters here specifically:
|
|
1099
|
+
* `layerTemplateGraph` only ever surfaces nodes reachable from `roots` (its
|
|
1100
|
+
* contract — "roots always land at layer 0"), so a genuinely standalone
|
|
1101
|
+
* package would otherwise silently vanish from `--layers` instead of showing
|
|
1102
|
+
* up as its own one-node layer-0 entry.
|
|
1103
|
+
*/
|
|
1104
|
+
async function buildInstallationGraph(installedTemplates, cat, checks) {
|
|
1105
|
+
const cpToken = getStoredToken()?.token;
|
|
1106
|
+
const entryBySlug = new Map(cat.entries.map((e) => [e.slug, e]));
|
|
1107
|
+
const installedSlugs = [...new Set(installedTemplates.map((t) => t.slug))];
|
|
1108
|
+
const edgesOf = await buildEdgesOf(installedSlugs, cat, cpToken);
|
|
1109
|
+
const primary = installedSlugs.filter((slug) => {
|
|
1110
|
+
const entry = entryBySlug.get(slug);
|
|
1111
|
+
return (entry && isSuite(entry)) || edgesOf(slug).length > 0;
|
|
1112
|
+
});
|
|
1113
|
+
const reached = new Set(primary);
|
|
1114
|
+
let frontier = [...primary];
|
|
1115
|
+
while (frontier.length > 0) {
|
|
1116
|
+
const next = [];
|
|
1117
|
+
for (const s of frontier) {
|
|
1118
|
+
for (const dep of edgesOf(s)) {
|
|
1119
|
+
if (!reached.has(dep.slug)) {
|
|
1120
|
+
reached.add(dep.slug);
|
|
1121
|
+
next.push(dep.slug);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
frontier = next;
|
|
1126
|
+
}
|
|
1127
|
+
const leftover = installedSlugs.filter((s) => !reached.has(s));
|
|
1128
|
+
const roots = [...new Set([...primary, ...leftover])];
|
|
1129
|
+
return layerTemplateGraph({
|
|
1130
|
+
roots,
|
|
1131
|
+
edgesOf,
|
|
1132
|
+
nameOf: (slug) => entryBySlug.get(slug)?.name,
|
|
1133
|
+
installed: toInstalledEntries(installedTemplates),
|
|
1134
|
+
updates: checks,
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
/** Status cell for one `--layers` row — same vocabulary as `installedStatusLabel`, adapted to a `CompositionNode` (which, unlike `UpdateCheck`, can be a ghost — not installed at all). */
|
|
1138
|
+
function layerNodeStatus(node, isPrivate, loggedIn) {
|
|
1139
|
+
if (!node.installed)
|
|
1140
|
+
return chalk.dim("not installed");
|
|
1141
|
+
if (node.noVersionInfo)
|
|
1142
|
+
return noVersionLabel(isPrivate, loggedIn);
|
|
1143
|
+
if (!node.updateAvailable)
|
|
1144
|
+
return chalk.green("up to date");
|
|
1145
|
+
const installed = node.installedVersion ? truncateVersion(node.installedVersion) : "?";
|
|
1146
|
+
const latest = node.latestVersion ? truncateVersion(node.latestVersion) : "?";
|
|
1147
|
+
return chalk.yellow("update available ") + chalk.dim(installed) + " → " + latest;
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Render the layered DAG bottom-up: bedrock (the highest layer index, most
|
|
1151
|
+
* depended-on) FIRST, roots (layer 0) LAST — the base of the pyramid printed
|
|
1152
|
+
* first felt like the clearer reading order for "what everything rests on".
|
|
1153
|
+
*/
|
|
1154
|
+
function printLayersGraph(graph, cat) {
|
|
1155
|
+
const entryBySlug = new Map(cat.entries.map((e) => [e.slug, e]));
|
|
1156
|
+
log.heading(" Installed packages — composition layers (bedrock first, bottom-up)");
|
|
1157
|
+
log.blank();
|
|
1158
|
+
// Column widths computed ONCE over every node in the graph (not per layer
|
|
1159
|
+
// bucket) — otherwise name/slug columns re-align at each layer boundary
|
|
1160
|
+
// and read as ragged.
|
|
1161
|
+
const allNodes = [...graph.nodes.values()];
|
|
1162
|
+
const nameW = Math.max(4, ...allNodes.map((n) => n.name.length)) + 2;
|
|
1163
|
+
const slugW = Math.max(4, ...allNodes.map((n) => n.slug.length)) + 2;
|
|
1164
|
+
for (let i = graph.layers.length - 1; i >= 0; i--) {
|
|
1165
|
+
const bucket = graph.layers[i];
|
|
1166
|
+
if (!bucket || bucket.length === 0)
|
|
1167
|
+
continue;
|
|
1168
|
+
// Bedrock/Roots already say what they need to; a bare "Layer N" in
|
|
1169
|
+
// between doesn't need "— layer N" repeated after it.
|
|
1170
|
+
const label = i === graph.layers.length - 1 ? "Bedrock" : i === 0 ? "Roots" : `Layer ${i}`;
|
|
1171
|
+
console.log(" " + chalk.dim(label));
|
|
1172
|
+
for (const node of bucket) {
|
|
1173
|
+
const entry = entryBySlug.get(node.slug);
|
|
1174
|
+
const dim = !node.installed;
|
|
1175
|
+
const fanIn = node.dependents.length > 0
|
|
1176
|
+
? " " + chalk.magenta(`used by ${node.dependents.length} package${node.dependents.length === 1 ? "" : "s"}`)
|
|
1177
|
+
: "";
|
|
1178
|
+
const overlays = node.overlays.length > 0
|
|
1179
|
+
? " " + node.overlays.map((o) => chalk.cyan(`+ ${o.name} (overlay)`)).join(" ")
|
|
1180
|
+
: "";
|
|
1181
|
+
const priv = entry?.isPrivate ? " " + chalk.yellow("private") : "";
|
|
1182
|
+
const name = dim ? chalk.dim(node.name.padEnd(nameW)) : chalk.bold(node.name.padEnd(nameW));
|
|
1183
|
+
const slug = dim ? chalk.dim(node.slug.padEnd(slugW)) : chalk.cyan(node.slug.padEnd(slugW));
|
|
1184
|
+
console.log(" " +
|
|
1185
|
+
name +
|
|
1186
|
+
slug +
|
|
1187
|
+
layerNodeStatus(node, entry?.isPrivate ?? false, cat.loggedIn) +
|
|
1188
|
+
fanIn +
|
|
1189
|
+
overlays +
|
|
1190
|
+
priv);
|
|
1191
|
+
}
|
|
1192
|
+
log.blank();
|
|
1193
|
+
}
|
|
1194
|
+
if (graph.cycles.length > 0) {
|
|
1195
|
+
log.warn(`${graph.cycles.length} dependency cycle${graph.cycles.length === 1 ? "" : "s"} detected — surfaced, not hidden:`);
|
|
1196
|
+
for (const [from, to] of graph.cycles)
|
|
1197
|
+
log.hint(`${from} → ${to} (broken here to avoid an infinite loop)`);
|
|
1198
|
+
log.blank();
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Pure-read inventory of what's installed on this pod — reuses
|
|
1203
|
+
* `buildMarketCatalog` + `fetchInstalledTemplates` + `computeUpdates`
|
|
1204
|
+
* (`market update`'s own drift check), never writes anything. Pairs with
|
|
1205
|
+
* `market update` to apply what this surfaces.
|
|
1206
|
+
*/
|
|
1207
|
+
export async function marketInstalled(opts) {
|
|
1208
|
+
const [installedTemplates, cat] = await Promise.all([fetchInstalledTemplates(), buildMarketCatalog()]);
|
|
1209
|
+
if (installedTemplates.length === 0) {
|
|
1210
|
+
if (opts.json) {
|
|
1211
|
+
console.log(JSON.stringify({ rows: [], loggedIn: cat.loggedIn }, null, 2));
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
log.warn("No installed packages found on this pod.");
|
|
1215
|
+
log.hint("Browse what's available: synap market --list");
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
const checks = computeUpdates(installedTemplates, cat);
|
|
1219
|
+
// Multiple workspaces can install the same slug (a template composed onto
|
|
1220
|
+
// several bases, or the same additive pack in >1 workspace) — one row per
|
|
1221
|
+
// DISTINCT slug, with `workspaceCount` carrying the fan-out.
|
|
1222
|
+
const workspaceCountBySlug = new Map();
|
|
1223
|
+
for (const t of installedTemplates) {
|
|
1224
|
+
workspaceCountBySlug.set(t.slug, (workspaceCountBySlug.get(t.slug) ?? 0) + 1);
|
|
1225
|
+
}
|
|
1226
|
+
const seen = new Set();
|
|
1227
|
+
let rows = checks.filter((c) => {
|
|
1228
|
+
if (seen.has(c.slug))
|
|
1229
|
+
return false;
|
|
1230
|
+
seen.add(c.slug);
|
|
1231
|
+
return true;
|
|
1232
|
+
});
|
|
1233
|
+
if (opts.outdated)
|
|
1234
|
+
rows = rows.filter((c) => c.updateAvailable);
|
|
1235
|
+
const entryBySlug = new Map(cat.entries.map((e) => [e.slug, e]));
|
|
1236
|
+
if (opts.layers) {
|
|
1237
|
+
const graph = await buildInstallationGraph(installedTemplates, cat, checks);
|
|
1238
|
+
const steps = FLOW.afterMarketInstalledCheck(rows.filter((c) => c.updateAvailable).map((c) => c.slug));
|
|
1239
|
+
if (opts.json) {
|
|
1240
|
+
console.log(JSON.stringify({
|
|
1241
|
+
nodes: [...graph.nodes.values()],
|
|
1242
|
+
edges: graph.edges,
|
|
1243
|
+
layers: graph.layers.map((bucket) => bucket.map((n) => n.slug)),
|
|
1244
|
+
cycles: graph.cycles,
|
|
1245
|
+
loggedIn: cat.loggedIn,
|
|
1246
|
+
nextSteps: steps,
|
|
1247
|
+
}, null, 2));
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
if (graph.nodes.size === 0) {
|
|
1251
|
+
log.success("Nothing installed.");
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
printLayersGraph(graph, cat);
|
|
1255
|
+
if (!cat.loggedIn)
|
|
1256
|
+
log.dim("Not logged in — private template updates aren't visible. Run: synap login");
|
|
1257
|
+
renderNextSteps(steps);
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
if (opts.tree) {
|
|
1261
|
+
const { packages, leafRows } = await buildCompositionTrees(rows, cat, installedTemplates, workspaceCountBySlug);
|
|
1262
|
+
const steps = FLOW.afterMarketInstalledCheck(rows.filter((c) => c.updateAvailable).map((c) => c.slug));
|
|
1263
|
+
if (opts.json) {
|
|
1264
|
+
console.log(JSON.stringify({
|
|
1265
|
+
packages,
|
|
1266
|
+
leaves: leafRows.map((c) => {
|
|
1267
|
+
const entry = entryBySlug.get(c.slug);
|
|
1268
|
+
return {
|
|
1269
|
+
slug: c.slug,
|
|
1270
|
+
name: entry?.name ?? c.slug,
|
|
1271
|
+
installedVersion: c.installedVersion,
|
|
1272
|
+
latestVersion: c.latestVersion ?? null,
|
|
1273
|
+
updateAvailable: c.updateAvailable,
|
|
1274
|
+
noVersionInfo: c.noVersionInfo,
|
|
1275
|
+
isPrivate: entry?.isPrivate ?? false,
|
|
1276
|
+
workspaceCount: workspaceCountBySlug.get(c.slug) ?? 0,
|
|
1277
|
+
};
|
|
1278
|
+
}),
|
|
1279
|
+
loggedIn: cat.loggedIn,
|
|
1280
|
+
nextSteps: steps,
|
|
1281
|
+
}, null, 2));
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
if (packages.length === 0 && leafRows.length === 0) {
|
|
1285
|
+
log.success("Nothing installed.");
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
if (packages.length > 0) {
|
|
1289
|
+
log.heading(" Installed packages — composition graph");
|
|
1290
|
+
log.blank();
|
|
1291
|
+
packages.forEach(printCompositionTree);
|
|
1292
|
+
}
|
|
1293
|
+
if (leafRows.length > 0) {
|
|
1294
|
+
log.heading(" Other installed packages");
|
|
1295
|
+
log.blank();
|
|
1296
|
+
const nameW = Math.max(4, ...leafRows.map((c) => (entryBySlug.get(c.slug)?.name ?? c.slug).length)) + 2;
|
|
1297
|
+
const slugW = Math.max(4, ...leafRows.map((c) => c.slug.length)) + 2;
|
|
1298
|
+
for (const c of leafRows) {
|
|
1299
|
+
const entry = entryBySlug.get(c.slug);
|
|
1300
|
+
const name = entry?.name ?? c.slug;
|
|
1301
|
+
const priv = entry?.isPrivate ? chalk.yellow("private") : "";
|
|
1302
|
+
const wsCount = workspaceCountBySlug.get(c.slug) ?? 0;
|
|
1303
|
+
console.log(" " +
|
|
1304
|
+
chalk.bold(name.padEnd(nameW)) +
|
|
1305
|
+
chalk.cyan(c.slug.padEnd(slugW)) +
|
|
1306
|
+
installedStatusLabel(c, entry?.isPrivate ?? false, cat.loggedIn) +
|
|
1307
|
+
" " +
|
|
1308
|
+
priv +
|
|
1309
|
+
(priv ? " " : "") +
|
|
1310
|
+
chalk.dim(`${wsCount} workspace${wsCount === 1 ? "" : "s"}`));
|
|
1311
|
+
}
|
|
1312
|
+
log.blank();
|
|
1313
|
+
}
|
|
1314
|
+
if (!cat.loggedIn)
|
|
1315
|
+
log.dim("Not logged in — private template updates aren't visible. Run: synap login");
|
|
1316
|
+
renderNextSteps(steps);
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
if (opts.json) {
|
|
1320
|
+
const steps = FLOW.afterMarketInstalledCheck(rows.filter((c) => c.updateAvailable).map((c) => c.slug));
|
|
1321
|
+
console.log(JSON.stringify({
|
|
1322
|
+
rows: rows.map((c) => {
|
|
1323
|
+
const entry = entryBySlug.get(c.slug);
|
|
1324
|
+
return {
|
|
1325
|
+
slug: c.slug,
|
|
1326
|
+
name: entry?.name ?? c.slug,
|
|
1327
|
+
installedVersion: c.installedVersion,
|
|
1328
|
+
latestVersion: c.latestVersion ?? null,
|
|
1329
|
+
updateAvailable: c.updateAvailable,
|
|
1330
|
+
noVersionInfo: c.noVersionInfo,
|
|
1331
|
+
isPrivate: entry?.isPrivate ?? false,
|
|
1332
|
+
isHashVersion: isHashVersion(c.installedVersion) || isHashVersion(c.latestVersion ?? ""),
|
|
1333
|
+
workspaceCount: workspaceCountBySlug.get(c.slug) ?? 0,
|
|
1334
|
+
};
|
|
1335
|
+
}),
|
|
1336
|
+
loggedIn: cat.loggedIn,
|
|
1337
|
+
nextSteps: steps,
|
|
1338
|
+
}, null, 2));
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
if (rows.length === 0) {
|
|
1342
|
+
log.success(opts.outdated ? "Nothing outdated." : "Nothing installed.");
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
log.heading(" Installed packages");
|
|
1346
|
+
log.blank();
|
|
1347
|
+
const nameW = Math.max(4, ...rows.map((c) => (entryBySlug.get(c.slug)?.name ?? c.slug).length)) + 2;
|
|
1348
|
+
const slugW = Math.max(4, ...rows.map((c) => c.slug.length)) + 2;
|
|
1349
|
+
for (const c of rows) {
|
|
1350
|
+
const entry = entryBySlug.get(c.slug);
|
|
1351
|
+
const name = entry?.name ?? c.slug;
|
|
1352
|
+
const priv = entry?.isPrivate ? chalk.yellow("private") : "";
|
|
1353
|
+
const wsCount = workspaceCountBySlug.get(c.slug) ?? 0;
|
|
1354
|
+
console.log(" " +
|
|
1355
|
+
chalk.bold(name.padEnd(nameW)) +
|
|
1356
|
+
chalk.cyan(c.slug.padEnd(slugW)) +
|
|
1357
|
+
installedStatusLabel(c, entry?.isPrivate ?? false, cat.loggedIn) +
|
|
1358
|
+
" " +
|
|
1359
|
+
priv +
|
|
1360
|
+
(priv ? " " : "") +
|
|
1361
|
+
chalk.dim(`${wsCount} workspace${wsCount === 1 ? "" : "s"}`));
|
|
1362
|
+
}
|
|
1363
|
+
log.blank();
|
|
1364
|
+
if (!cat.loggedIn)
|
|
1365
|
+
log.dim("Not logged in — private template updates aren't visible. Run: synap login");
|
|
1366
|
+
renderNextSteps(FLOW.afterMarketInstalledCheck(rows.filter((c) => c.updateAvailable).map((c) => c.slug)));
|
|
1367
|
+
}
|
|
1368
|
+
//# sourceMappingURL=market.js.map
|