@indigoai-us/hq-cli 5.77.14 → 5.79.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.
Files changed (309) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/assets/scaffold/core/scripts/archive-old-threads.sh +86 -0
  3. package/assets/scaffold/core/scripts/backfill-company-skill-mirrors.sh +73 -0
  4. package/assets/scaffold/core/scripts/backfill-workspace-mirror.sh +51 -0
  5. package/assets/scaffold/core/scripts/codex-skill-bridge.sh +550 -0
  6. package/assets/scaffold/core/scripts/detect-stale-core-policy-mirror.sh +185 -0
  7. package/assets/scaffold/core/scripts/hq-status-summary.sh +187 -0
  8. package/assets/scaffold/core/scripts/ontology-readme-drift.sh +264 -0
  9. package/assets/scaffold/core/scripts/qmd-reindex-after-sync.sh +93 -0
  10. package/assets/scaffold/core/scripts/rebuild-all-indexes.sh +69 -0
  11. package/assets/scaffold/core/scripts/rebuild-companies-index.sh +206 -0
  12. package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +127 -0
  13. package/assets/scaffold/core/scripts/rebuild-orchestrator-index.sh +86 -0
  14. package/assets/scaffold/core/scripts/rebuild-projects-index.sh +105 -0
  15. package/assets/scaffold/core/scripts/rebuild-public-knowledge-index.sh +83 -0
  16. package/assets/scaffold/core/scripts/rebuild-reports-index.sh +114 -0
  17. package/assets/scaffold/core/scripts/rebuild-social-drafts-index.sh +135 -0
  18. package/assets/scaffold/core/scripts/rebuild-threads-index.sh +82 -0
  19. package/assets/scaffold/core/scripts/rebuild-workers-index.sh +97 -0
  20. package/assets/scaffold/core/scripts/resize-screenshot.sh +47 -0
  21. package/assets/scaffold/core/scripts/token-usage-report.sh +254 -0
  22. package/assets/scaffold/core/scripts/worktree.sh +136 -0
  23. package/dist/commands/agents.js +28 -12
  24. package/dist/commands/api-keys.js +100 -20
  25. package/dist/commands/core.d.ts +90 -0
  26. package/dist/commands/core.js +279 -0
  27. package/dist/commands/files-browse.d.ts +21 -0
  28. package/dist/commands/files-browse.js +145 -24
  29. package/dist/commands/files.js +33 -7
  30. package/dist/commands/meetings.js +16 -9
  31. package/dist/commands/members.js +16 -5
  32. package/dist/commands/pack-install.d.ts +14 -0
  33. package/dist/commands/pack-install.js +42 -2
  34. package/dist/commands/pkg-install.js +39 -2
  35. package/dist/commands/run.js +6 -2
  36. package/dist/commands/secrets.js +120 -65
  37. package/dist/main.js +6 -0
  38. package/dist/utils/api-key-command-gate.d.ts +45 -0
  39. package/dist/utils/api-key-command-gate.js +51 -0
  40. package/dist/utils/hq-roots.d.ts +91 -0
  41. package/dist/utils/hq-roots.js +212 -0
  42. package/dist/utils/resolve-vault-credential.d.ts +63 -3
  43. package/dist/utils/resolve-vault-credential.js +152 -6
  44. package/dist/utils/run-bundled-script.d.ts +66 -0
  45. package/dist/utils/run-bundled-script.js +80 -0
  46. package/dist/utils/vault-api.d.ts +21 -0
  47. package/dist/utils/vault-api.js +74 -3
  48. package/package.json +7 -1
  49. package/.claude/policies/hq-cli-interactive-reads-use-sts-vend.md +0 -49
  50. package/.github/workflows/ci.yml +0 -46
  51. package/.github/workflows/publish.yml +0 -36
  52. package/.github/workflows/scripts/smoke-test-pkg.sh +0 -97
  53. package/e2e/cli.test.ts +0 -128
  54. package/eslint.config.js +0 -37
  55. package/pnpm-workspace.yaml +0 -4
  56. package/scripts/chmod-bins.mjs +0 -49
  57. package/scripts/generate-dsn.mjs +0 -26
  58. package/scripts/generate-scan-packages-table.mjs +0 -113
  59. package/scripts/smoke-sources-signals.sh +0 -103
  60. package/src/bin/hq-auth-refresh.ts +0 -43
  61. package/src/cli-version.ts +0 -13
  62. package/src/commands/__fixtures__/make-tar.ts +0 -126
  63. package/src/commands/_patterns.ts +0 -10
  64. package/src/commands/add.ts +0 -75
  65. package/src/commands/agents.test.ts +0 -527
  66. package/src/commands/agents.ts +0 -923
  67. package/src/commands/api-keys.test.ts +0 -291
  68. package/src/commands/api-keys.ts +0 -382
  69. package/src/commands/artifact-verify.test.ts +0 -177
  70. package/src/commands/auth.test.ts +0 -82
  71. package/src/commands/auth.ts +0 -213
  72. package/src/commands/billing.test.ts +0 -158
  73. package/src/commands/billing.ts +0 -146
  74. package/src/commands/channels.test.ts +0 -74
  75. package/src/commands/channels.ts +0 -91
  76. package/src/commands/cloud-demote.test.ts +0 -401
  77. package/src/commands/cloud-demote.ts +0 -278
  78. package/src/commands/cloud-provision.test.ts +0 -1117
  79. package/src/commands/cloud-provision.ts +0 -942
  80. package/src/commands/cloud.pull-all.test.ts +0 -711
  81. package/src/commands/cloud.pull-per-company.test.ts +0 -188
  82. package/src/commands/cloud.push-all.test.ts +0 -485
  83. package/src/commands/cloud.scope-excluded-warning.test.ts +0 -22
  84. package/src/commands/cloud.selectors.test.ts +0 -208
  85. package/src/commands/cloud.status.test.ts +0 -160
  86. package/src/commands/cloud.test.ts +0 -41
  87. package/src/commands/cloud.ts +0 -2130
  88. package/src/commands/company.test.ts +0 -202
  89. package/src/commands/company.ts +0 -148
  90. package/src/commands/creators.test.ts +0 -60
  91. package/src/commands/creators.ts +0 -117
  92. package/src/commands/crm.test.ts +0 -179
  93. package/src/commands/crm.ts +0 -236
  94. package/src/commands/db-migrate.ts +0 -55
  95. package/src/commands/db-provision.ts +0 -114
  96. package/src/commands/db-sql.ts +0 -124
  97. package/src/commands/db-status.ts +0 -108
  98. package/src/commands/db.ts +0 -26
  99. package/src/commands/dm.test.ts +0 -748
  100. package/src/commands/dm.ts +0 -1226
  101. package/src/commands/feedback.test.ts +0 -413
  102. package/src/commands/feedback.ts +0 -169
  103. package/src/commands/files-browse.test.ts +0 -1249
  104. package/src/commands/files-browse.ts +0 -1506
  105. package/src/commands/files-delete.test.ts +0 -484
  106. package/src/commands/files-recovery.test.ts +0 -370
  107. package/src/commands/files.test.ts +0 -722
  108. package/src/commands/files.ts +0 -1446
  109. package/src/commands/group-grants.test.ts +0 -330
  110. package/src/commands/group-grants.ts +0 -455
  111. package/src/commands/groups.test.ts +0 -44
  112. package/src/commands/groups.ts +0 -368
  113. package/src/commands/integrations.test.ts +0 -633
  114. package/src/commands/integrations.ts +0 -507
  115. package/src/commands/list.ts +0 -85
  116. package/src/commands/login.ts +0 -52
  117. package/src/commands/logout.ts +0 -30
  118. package/src/commands/marketplace-install.test.ts +0 -543
  119. package/src/commands/marketplace-security.test.ts +0 -646
  120. package/src/commands/mcp-registration.test.ts +0 -2787
  121. package/src/commands/mcp-registration.ts +0 -2612
  122. package/src/commands/mcp-status.test.ts +0 -483
  123. package/src/commands/mcp-status.ts +0 -575
  124. package/src/commands/mcp-status.us011.test.ts +0 -243
  125. package/src/commands/meetings.test.ts +0 -522
  126. package/src/commands/meetings.ts +0 -789
  127. package/src/commands/members.test.ts +0 -1474
  128. package/src/commands/members.ts +0 -1181
  129. package/src/commands/onboard-identity-guard.test.ts +0 -99
  130. package/src/commands/onboard-identity-guard.ts +0 -103
  131. package/src/commands/onboard-join.test.ts +0 -96
  132. package/src/commands/onboard-join.ts +0 -101
  133. package/src/commands/onboard-warning.test.ts +0 -26
  134. package/src/commands/onboard-warning.ts +0 -12
  135. package/src/commands/onboard.ts +0 -267
  136. package/src/commands/outposts-heartbeat.test.ts +0 -299
  137. package/src/commands/outposts-heartbeat.ts +0 -310
  138. package/src/commands/outposts-replica-sync.test.ts +0 -272
  139. package/src/commands/outposts-self-deploy.test.ts +0 -243
  140. package/src/commands/outposts.test.ts +0 -1039
  141. package/src/commands/outposts.ts +0 -1935
  142. package/src/commands/pack-install-secret-authorization.test.ts +0 -115
  143. package/src/commands/pack-install.test.ts +0 -1469
  144. package/src/commands/pack-install.ts +0 -2256
  145. package/src/commands/pack-update-cache.test.ts +0 -149
  146. package/src/commands/packs-update-api-key.test.ts +0 -105
  147. package/src/commands/packs.test.ts +0 -88
  148. package/src/commands/packs.ts +0 -622
  149. package/src/commands/people.test.ts +0 -807
  150. package/src/commands/people.ts +0 -422
  151. package/src/commands/pkg-install.dispatch.test.ts +0 -103
  152. package/src/commands/pkg-install.ts +0 -221
  153. package/src/commands/pkg-list.test.ts +0 -79
  154. package/src/commands/pkg-list.ts +0 -13
  155. package/src/commands/pkg-remove.ts +0 -71
  156. package/src/commands/pkg-update.ts +0 -189
  157. package/src/commands/publish.test.ts +0 -650
  158. package/src/commands/publish.ts +0 -580
  159. package/src/commands/reindex.test.ts +0 -259
  160. package/src/commands/reindex.ts +0 -274
  161. package/src/commands/rescue.test.ts +0 -39
  162. package/src/commands/rescue.ts +0 -210
  163. package/src/commands/run.env-local.test.ts +0 -88
  164. package/src/commands/run.test.ts +0 -45
  165. package/src/commands/run.ts +0 -190
  166. package/src/commands/safe-extract.test.ts +0 -459
  167. package/src/commands/safe-extract.ts +0 -444
  168. package/src/commands/secrets-scope.test.ts +0 -56
  169. package/src/commands/secrets-scope.ts +0 -32
  170. package/src/commands/secrets.parse-destination.test.ts +0 -38
  171. package/src/commands/secrets.test.ts +0 -2050
  172. package/src/commands/secrets.ts +0 -2249
  173. package/src/commands/signals.ts +0 -351
  174. package/src/commands/skill.test.ts +0 -770
  175. package/src/commands/skill.ts +0 -796
  176. package/src/commands/sources.ts +0 -362
  177. package/src/commands/sync-mode.test.ts +0 -366
  178. package/src/commands/sync-mode.ts +0 -387
  179. package/src/commands/sync-narrow.test.ts +0 -633
  180. package/src/commands/sync-narrow.ts +0 -555
  181. package/src/commands/sync.ts +0 -165
  182. package/src/commands/team-sync.ts +0 -628
  183. package/src/commands/update.ts +0 -71
  184. package/src/commands/whoami.ts +0 -83
  185. package/src/commands/workers.test.ts +0 -158
  186. package/src/commands/workers.ts +0 -301
  187. package/src/index.test.ts +0 -32
  188. package/src/index.ts +0 -20
  189. package/src/lib/db/control-plane.test.ts +0 -59
  190. package/src/lib/db/control-plane.ts +0 -113
  191. package/src/lib/db/local.test.ts +0 -81
  192. package/src/lib/db/local.ts +0 -148
  193. package/src/lib/db/migrate.test.ts +0 -133
  194. package/src/lib/db/migrate.ts +0 -137
  195. package/src/lib/db/paths.test.ts +0 -112
  196. package/src/lib/db/paths.ts +0 -128
  197. package/src/lib/db/remote-engine.test.ts +0 -44
  198. package/src/lib/db/remote-engine.ts +0 -148
  199. package/src/lib/db/remote-sql.test.ts +0 -32
  200. package/src/lib/db/remote-sql.ts +0 -62
  201. package/src/lib/db/sql.test.ts +0 -106
  202. package/src/lib/db/sql.ts +0 -192
  203. package/src/lib/hq-cloud-dep.smoke.test.ts +0 -75
  204. package/src/lib/local-tree-diff.test.ts +0 -301
  205. package/src/lib/local-tree-diff.ts +0 -362
  206. package/src/lib/narrow-hint-banner.test.ts +0 -235
  207. package/src/lib/narrow-hint-banner.ts +0 -212
  208. package/src/lib/plan-limit-nag.test.ts +0 -317
  209. package/src/lib/plan-limit-nag.ts +0 -264
  210. package/src/main.ts +0 -371
  211. package/src/node-preflight.test.ts +0 -60
  212. package/src/node-preflight.ts +0 -67
  213. package/src/outpost/session-heartbeat-bounds.test.ts +0 -195
  214. package/src/outpost/session-heartbeat-guard.test.ts +0 -105
  215. package/src/outpost/session-heartbeat-publisher.test.ts +0 -178
  216. package/src/outpost/session-heartbeat-publisher.ts +0 -186
  217. package/src/outpost/session-heartbeat-retain-guard.test.ts +0 -126
  218. package/src/outpost/session-heartbeat.test.ts +0 -459
  219. package/src/outpost/session-heartbeat.ts +0 -877
  220. package/src/packaging.test.ts +0 -109
  221. package/src/run/__fixtures__/discover-schemas/example.env.schema +0 -4
  222. package/src/run/discover-schemas.test.ts +0 -153
  223. package/src/run/discover-schemas.ts +0 -79
  224. package/src/run/hq-plugin.test.ts +0 -371
  225. package/src/run/hq-plugin.ts +0 -277
  226. package/src/run/varlock-shape.test.ts +0 -57
  227. package/src/schemas/hq-package.schema.json +0 -155
  228. package/src/sentry-before-send.test.ts +0 -359
  229. package/src/sentry-before-send.ts +0 -221
  230. package/src/sentry-epipe.test.ts +0 -37
  231. package/src/sentry-release.test.ts +0 -54
  232. package/src/sentry.test.ts +0 -24
  233. package/src/sentry.ts +0 -54
  234. package/src/strategies/link.ts +0 -62
  235. package/src/strategies/merge.ts +0 -142
  236. package/src/types.ts +0 -146
  237. package/src/utils/__fixtures__/scan-packages.generated-block.sh +0 -23
  238. package/src/utils/auth-error.test.ts +0 -40
  239. package/src/utils/auth-error.ts +0 -42
  240. package/src/utils/billing-gate.test.ts +0 -95
  241. package/src/utils/billing-gate.ts +0 -228
  242. package/src/utils/breadcrumb-buffer.ts +0 -18
  243. package/src/utils/cli-telemetry.test.ts +0 -153
  244. package/src/utils/cli-telemetry.ts +0 -61
  245. package/src/utils/cognito-session.machine.test.ts +0 -109
  246. package/src/utils/cognito-session.refresh.test.ts +0 -84
  247. package/src/utils/cognito-session.test.ts +0 -328
  248. package/src/utils/cognito-session.ts +0 -448
  249. package/src/utils/company-selection-error.test.ts +0 -42
  250. package/src/utils/company-selection-error.ts +0 -45
  251. package/src/utils/contribution-table.ts +0 -83
  252. package/src/utils/environmental-error.test.ts +0 -45
  253. package/src/utils/environmental-error.ts +0 -39
  254. package/src/utils/epipe.test.ts +0 -28
  255. package/src/utils/epipe.ts +0 -29
  256. package/src/utils/expected-cli-error.test.ts +0 -28
  257. package/src/utils/expected-cli-error.ts +0 -39
  258. package/src/utils/feedback-diagnostics.test.ts +0 -183
  259. package/src/utils/feedback-diagnostics.ts +0 -123
  260. package/src/utils/feedback-screenshots.test.ts +0 -134
  261. package/src/utils/feedback-screenshots.ts +0 -124
  262. package/src/utils/feedback-versions.test.ts +0 -98
  263. package/src/utils/feedback-versions.ts +0 -68
  264. package/src/utils/git.ts +0 -74
  265. package/src/utils/integrity.ts +0 -59
  266. package/src/utils/intercepted-process-exit.test.ts +0 -37
  267. package/src/utils/intercepted-process-exit.ts +0 -36
  268. package/src/utils/login-provider.test.ts +0 -42
  269. package/src/utils/login-provider.ts +0 -30
  270. package/src/utils/manifest.test.ts +0 -167
  271. package/src/utils/manifest.ts +0 -131
  272. package/src/utils/pack-contributions.test.ts +0 -603
  273. package/src/utils/pack-contributions.ts +0 -450
  274. package/src/utils/people.ts +0 -215
  275. package/src/utils/registry-client.ts +0 -204
  276. package/src/utils/registry.ts +0 -90
  277. package/src/utils/resolve-vault-credential.test.ts +0 -69
  278. package/src/utils/resolve-vault-credential.ts +0 -60
  279. package/src/utils/sandbox-runner-client.test.ts +0 -279
  280. package/src/utils/sandbox-runner-client.ts +0 -260
  281. package/src/utils/secrets-cache.ts +0 -183
  282. package/src/utils/sentry-identity.ts +0 -45
  283. package/src/utils/settle-with-timeout.test.ts +0 -21
  284. package/src/utils/settle-with-timeout.ts +0 -22
  285. package/src/utils/user-yaml-error.test.ts +0 -24
  286. package/src/utils/user-yaml-error.ts +0 -30
  287. package/src/utils/vault-api.test.ts +0 -536
  288. package/src/utils/vault-api.ts +0 -382
  289. package/src/utils/version-check.test.ts +0 -176
  290. package/src/utils/version-check.ts +0 -155
  291. package/src/utils/version-gate.test.ts +0 -1024
  292. package/src/utils/version-gate.ts +0 -702
  293. package/test/commands/db-tenant-isolation.test.ts +0 -94
  294. package/test/commands/db.test.ts +0 -85
  295. package/test/commands/signals.test.ts +0 -207
  296. package/test/commands/sources.test.ts +0 -231
  297. package/test/e2e/smoke-install-mcp.sh +0 -113
  298. package/test/fixtures/hq-pack-smoke-mcp/mcp/smoke-http.json +0 -1
  299. package/test/fixtures/hq-pack-smoke-mcp/package.yaml +0 -11
  300. package/test/fixtures/signals/action_item/sample.md +0 -16
  301. package/test/fixtures/signals/summary/sample.md +0 -12
  302. package/test/fixtures/sources/meetings/sample.md +0 -25
  303. package/test/helpers/cli-runner.ts +0 -150
  304. package/test/helpers/s3-list-mock.ts +0 -79
  305. package/test/helpers/vault-service-mock.ts +0 -295
  306. package/test/sources-signals/smoke.test.ts +0 -228
  307. package/tsconfig.json +0 -20
  308. package/vitest.config.ts +0 -11
  309. package/vitest.e2e.config.ts +0 -7
@@ -1,580 +0,0 @@
1
- /**
2
- * hq publish <skill-or-worker-path> — marketplace packer + authenticated upload
3
- * (US-004, hq-creator-marketplace).
4
- *
5
- * Flow:
6
- * 1. Refuse if not logged in (cached Cognito session absent/expired) — uploads
7
- * nothing. The author uid is read from the cached idToken's `sub`.
8
- * 2. Validate package.yaml with the canonical 10-rule validator
9
- * (`validateManifest` from pack-install.ts) — never duplicate that logic.
10
- * On failure, print the validation error and upload nothing.
11
- * 3. Stamp `author` (uid from the idToken `sub`, plus handle/displayName) into
12
- * the pack's package.yaml (AC1). The server re-derives author from the
13
- * Cognito sub too, but the CLI stamps it so the on-disk + published pack
14
- * carry attribution.
15
- * 4. Tar (gzip) the payload, base64-encode the bytes, and POST a JSON body to
16
- * `/v1/listings` via vaultApiFetch with the access token. The deployed
17
- * server JSON.parses the body and expects
18
- * { type, name, slug, version, summary?, contributes(string)?,
19
- * creatorHandle?, tarball(base64) }, returning the created listing id +
20
- * `pending_review` status. A 409 (re-publishing the same version) is
21
- * surfaced as a clear duplicate error.
22
- * 5. Register/update `modules.yaml` provenance for the published source.
23
- *
24
- * Reuses the existing CLI plumbing:
25
- * - cognito-session.ts (token cache / access token)
26
- * - vault-api.ts (vaultApiFetch — authed JSON POST to /v1/listings)
27
- * - pack-install.ts:validateManifest (the 10 validation rules)
28
- * - manifest.ts (modules.yaml read/write for provenance)
29
- */
30
-
31
- import * as fs from 'fs';
32
- import * as os from 'os';
33
- import * as path from 'path';
34
- import * as yaml from 'js-yaml';
35
- import { parseUserYaml } from '../utils/user-yaml-error.js';
36
- import { execFileSync } from 'child_process';
37
- import { Command } from 'commander';
38
- import chalk from 'chalk';
39
- import { loadCachedTokens, isExpiring } from '@indigoai-us/hq-cloud';
40
- import { ensureCognitoToken, ensureCognitoIdToken } from '../utils/cognito-session.js';
41
- import { vaultApiFetch } from '../utils/vault-api.js';
42
- import { validateManifest } from './pack-install.js';
43
- import { findHqRoot, readManifest, writeManifest } from '../utils/manifest.js';
44
- import { readHqVersion } from '../utils/pack-contributions.js';
45
- import type { PackManifest, PackModuleDefinition, ModulesManifest } from '../types.js';
46
-
47
- // ---------------------------------------------------------------------------
48
- // Identity
49
- // ---------------------------------------------------------------------------
50
-
51
- export interface IdTokenClaims {
52
- sub?: string;
53
- email?: string;
54
- /** Cognito custom attributes / standard claims used for attribution. */
55
- 'cognito:username'?: string;
56
- preferred_username?: string;
57
- name?: string;
58
- }
59
-
60
- /**
61
- * Decode a Cognito idToken's payload (no signature verification — this is a
62
- * local read of an already-trusted cached token, mirroring whoami.ts/login.ts).
63
- * Pure → unit-testable.
64
- */
65
- export function peekIdToken(idToken: string): IdTokenClaims {
66
- try {
67
- const payload = idToken.split('.')[1];
68
- if (!payload) return {};
69
- const pad = payload.length % 4 === 0 ? '' : '='.repeat(4 - (payload.length % 4));
70
- const normalized = payload.replace(/-/g, '+').replace(/_/g, '/') + pad;
71
- const decoded = JSON.parse(Buffer.from(normalized, 'base64').toString('utf-8'));
72
- return decoded as IdTokenClaims;
73
- } catch {
74
- return {};
75
- }
76
- }
77
-
78
- export interface ResolvedAuthor {
79
- uid: string;
80
- handle: string;
81
- displayName: string;
82
- }
83
-
84
- /**
85
- * Build the pack `author` block from idToken claims. `uid` is the Cognito
86
- * `sub` (AC1). `handle`/`displayName` fall back gracefully so a thin token
87
- * still produces a well-shaped author that passes `validateManifest`. Throws
88
- * if there is no `sub` (caller is treated as logged-out). Pure.
89
- */
90
- export function resolveAuthor(claims: IdTokenClaims): ResolvedAuthor {
91
- const uid = claims.sub?.trim();
92
- if (!uid) {
93
- throw new Error('Cached session has no subject (sub) claim — run `hq login` again.');
94
- }
95
- const handle =
96
- claims.preferred_username?.trim() ||
97
- claims['cognito:username']?.trim() ||
98
- claims.email?.split('@')[0]?.trim() ||
99
- uid;
100
- const displayName =
101
- claims.name?.trim() || claims.email?.trim() || handle;
102
- return { uid, handle, displayName };
103
- }
104
-
105
- // ---------------------------------------------------------------------------
106
- // package.yaml author stamping
107
- // ---------------------------------------------------------------------------
108
-
109
- /**
110
- * Stamp `author` into a parsed manifest object, returning the YAML text to
111
- * write. Existing `author` is overwritten with the resolved attribution so the
112
- * publisher's own identity always wins (the server enforces this too). Pure.
113
- */
114
- export function stampAuthorYaml(
115
- manifest: Record<string, unknown>,
116
- author: ResolvedAuthor,
117
- ): string {
118
- const stamped = { ...manifest, author };
119
- return yaml.dump(stamped, { lineWidth: -1 });
120
- }
121
-
122
- /** Read package.yaml, stamp author, write it back. Returns the stamped author. */
123
- export function stampAuthorIntoPackage(
124
- payloadDir: string,
125
- author: ResolvedAuthor,
126
- ): void {
127
- const manifestPath = path.join(payloadDir, 'package.yaml');
128
- const parsed = parseUserYaml<Record<string, unknown>>(
129
- fs.readFileSync(manifestPath, 'utf-8'),
130
- manifestPath,
131
- );
132
- fs.writeFileSync(manifestPath, stampAuthorYaml(parsed, author));
133
- }
134
-
135
- // ---------------------------------------------------------------------------
136
- // Tarball
137
- // ---------------------------------------------------------------------------
138
-
139
- /**
140
- * Create a gzipped tarball of `payloadDir` and return its bytes. Uses the same
141
- * `tar` argv approach as pack-install.ts (no shell, no new dep). The archive is
142
- * written to a tmp file, read back, then removed.
143
- */
144
- export function tarballPayload(payloadDir: string): Uint8Array {
145
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hq-publish-tar-'));
146
- const tarPath = path.join(tmpDir, 'pack.tar.gz');
147
- try {
148
- // `-C payloadDir .` packs the directory contents at the archive root
149
- // (package.yaml + skills/ etc. at top level), excluding VCS/junk.
150
- execFileSync(
151
- 'tar',
152
- [
153
- '-czf',
154
- tarPath,
155
- '-C',
156
- payloadDir,
157
- '--exclude=.git',
158
- '--exclude=node_modules',
159
- '--exclude=.DS_Store',
160
- '.',
161
- ],
162
- { stdio: ['ignore', 'ignore', 'inherit'] },
163
- );
164
- return fs.readFileSync(tarPath);
165
- } finally {
166
- fs.rmSync(tmpDir, { recursive: true, force: true });
167
- }
168
- }
169
-
170
- // ---------------------------------------------------------------------------
171
- // Server-contract field derivation (POST /v1/listings, JSON body)
172
- // ---------------------------------------------------------------------------
173
-
174
- /** The marketplace listing type the server enforces (VALID_TYPES). */
175
- export type ListingType = 'skill' | 'worker';
176
-
177
- /**
178
- * Infer the listing `type` the server requires (exactly "skill" | "worker").
179
- *
180
- * Rules:
181
- * - An explicit `type` hint in package.yaml wins if it is already a valid
182
- * listing type (lets a pack author override the inference deterministically).
183
- * - Otherwise: a pack that contributes workers → "worker"; one that
184
- * contributes skills → "skill".
185
- * - Ambiguous/both/neither → "skill" (the documented default).
186
- * Pure → unit-testable.
187
- */
188
- export function inferListingType(pkg: PackManifest): ListingType {
189
- const hint = (pkg as unknown as { type?: unknown }).type;
190
- if (hint === 'skill' || hint === 'worker') return hint;
191
-
192
- const contributes = pkg.contributes ?? {};
193
- const hasWorkers = (contributes.workers?.length ?? 0) > 0;
194
- const hasSkills = (contributes.skills?.length ?? 0) > 0;
195
-
196
- // Prefer skill when ambiguous (both) or when neither is present.
197
- if (hasWorkers && !hasSkills) return 'worker';
198
- return 'skill';
199
- }
200
-
201
- /**
202
- * Derive the marketplace slug from the pack name. By convention pack names are
203
- * `^hq-pack-[a-z0-9][a-z0-9-]*$`, so we strip the leading `hq-pack-` to get a
204
- * short, valid, non-empty slug (e.g. `hq-pack-demo` → `demo`). If the name does
205
- * not carry that prefix we fall back to the full name. Pure.
206
- */
207
- export function deriveSlug(packName: string): string {
208
- const stripped = packName.replace(/^hq-pack-/, '');
209
- return stripped.length > 0 ? stripped : packName;
210
- }
211
-
212
- /**
213
- * Build the SHORT STRING `contributes` summary the server expects (it stores a
214
- * string, NOT the structured object). Lists each non-empty contribute category
215
- * with its entries, e.g. "workers: alpha, beta; skills: demo". Pure.
216
- */
217
- export function summarizeContributes(
218
- contributes: PackManifest['contributes'],
219
- ): string {
220
- const parts: string[] = [];
221
- for (const [key, entries] of Object.entries(contributes ?? {})) {
222
- if (Array.isArray(entries) && entries.length > 0) {
223
- parts.push(`${key}: ${entries.join(', ')}`);
224
- }
225
- }
226
- return parts.join('; ');
227
- }
228
-
229
- // ---------------------------------------------------------------------------
230
- // Provenance — modules.yaml upsert
231
- // ---------------------------------------------------------------------------
232
-
233
- /**
234
- * Register or update a `package`-strategy module entry in modules.yaml so the
235
- * published source has on-disk provenance (AC4). Returns the updated manifest.
236
- * Pure (operates on an in-memory manifest); the disk read/write is done by the
237
- * caller via `recordProvenance`.
238
- */
239
- export function upsertPackProvenance(
240
- manifest: ModulesManifest | null,
241
- entry: PackModuleDefinition,
242
- ): ModulesManifest {
243
- const next: ModulesManifest = manifest ?? { version: '1', modules: [] };
244
- const idx = next.modules.findIndex((m) => m.name === entry.name);
245
- if (idx >= 0) {
246
- next.modules[idx] = entry;
247
- } else {
248
- next.modules.push(entry);
249
- }
250
- return next;
251
- }
252
-
253
- /** Read modules.yaml, upsert the provenance entry, write it back. */
254
- export function recordProvenance(
255
- hqRoot: string,
256
- pkg: PackManifest,
257
- source: string,
258
- ): void {
259
- const entry: PackModuleDefinition = {
260
- name: pkg.name,
261
- strategy: 'package',
262
- source,
263
- version: pkg.version,
264
- installed_at_iso: new Date().toISOString(),
265
- // PackManifest.access is 'public'|'private'; module AccessLevel uses
266
- // 'public'|'team'|'role:*'. A private pack maps to team scope.
267
- access: pkg.access === 'private' ? 'team' : 'public',
268
- };
269
- const updated = upsertPackProvenance(readManifest(hqRoot), entry);
270
- writeManifest(hqRoot, updated);
271
- }
272
-
273
- // ---------------------------------------------------------------------------
274
- // Listing-response formatting
275
- // ---------------------------------------------------------------------------
276
-
277
- export interface ListingResponse {
278
- id?: string;
279
- listingId?: string;
280
- status?: string;
281
- /** Server-resolved creator handle (prefers the caller's claimed handle). */
282
- creatorHandle?: string;
283
- }
284
-
285
- /**
286
- * Unwrap the `POST /v1/listings` SUCCESS envelope. The deployed server returns
287
- * the created listing WRAPPED: `{ listing: { listingId, creatorHandle, status,
288
- * … } }`. Older/mocked responses return the listing fields at the TOP level.
289
- * This reads `body.listing` when present and falls back to the top-level `body`
290
- * for backwards-compat. Pure.
291
- */
292
- export function unwrapListing(body: Record<string, unknown>): ListingResponse {
293
- const inner = body?.listing;
294
- if (inner && typeof inner === 'object') {
295
- return inner as ListingResponse;
296
- }
297
- return body as ListingResponse;
298
- }
299
-
300
- /**
301
- * Map a `POST /v1/listings` response into a user-facing success notice. Pure.
302
- * Throws a clear error on a duplicate (409) or any non-2xx status so the caller
303
- * prints it and exits non-zero.
304
- *
305
- * The notice uses the REAL listing id and the SERVER-resolved `creatorHandle`
306
- * from the (possibly `{ listing }`-wrapped) response body — not the
307
- * locally-resolved author handle — falling back gracefully when the server
308
- * omits either field.
309
- */
310
- export function buildListingNotice(
311
- status: number,
312
- body: Record<string, unknown>,
313
- packName: string,
314
- packVersion: string,
315
- ): string {
316
- if (status === 409) {
317
- throw new Error(
318
- `${packName}@${packVersion} is already published (duplicate version). ` +
319
- `Bump the version in package.yaml and re-run \`hq publish\`.`,
320
- );
321
- }
322
- if (status === 400) {
323
- const code = typeof body.code === 'string' ? body.code : '';
324
- if (code === 'ON_BEHALF_DISABLED') {
325
- throw new Error(
326
- 'Publishing on behalf of another creator is not enabled on this server. ' +
327
- 'Ask an HQ admin to turn on the on-behalf capability, or publish without `--as`.',
328
- );
329
- }
330
- // Any other 400 falls through to the generic non-2xx handler below.
331
- }
332
- if (status === 401 || status === 403) {
333
- const code = typeof body.code === 'string' ? body.code : '';
334
- if (code === 'NOT_VERIFIED_CREATOR') {
335
- throw new Error(
336
- 'Only verified creators can publish to the marketplace. ' +
337
- 'Run `hq creators apply --reason "..."` to request verified-creator access; ' +
338
- 'an Indigo admin will review it.',
339
- );
340
- }
341
- if (code === 'FORBIDDEN' || code === 'NO_EMAIL_CLAIM') {
342
- // The staff gate rejected an on-behalf (`--as`) publish — only the normal
343
- // self-publish path returns NOT_VERIFIED_CREATOR, so these codes here mean
344
- // a non-staff caller tried to publish as someone else.
345
- throw new Error(
346
- 'Only HQ staff can publish on behalf of another creator (`--as`). ' +
347
- 'Re-run without `--as` to publish as yourself.',
348
- );
349
- }
350
- throw new Error('Not authorized to publish — run `hq login` and ensure your creator account is verified.');
351
- }
352
- if (status < 200 || status >= 300) {
353
- const msg =
354
- (body.error as string) ?? (body.message as string) ?? `HTTP ${status}`;
355
- throw new Error(`Publish failed: ${msg}`);
356
- }
357
- const listing = unwrapListing(body);
358
- const id = listing.listingId ?? listing.id ?? '(unknown id)';
359
- const listingStatus = listing.status ?? 'pending_review';
360
- const serverHandle = listing.creatorHandle?.trim();
361
- const attribution = serverHandle ? ` Attributed to @${serverHandle}.` : '';
362
- return `Published ${packName}@${packVersion} — listing ${id} (${listingStatus}).${attribution}`;
363
- }
364
-
365
- // ---------------------------------------------------------------------------
366
- // Publish flow (testable core)
367
- // ---------------------------------------------------------------------------
368
-
369
- /**
370
- * The exact JSON body the deployed `POST /v1/listings` handler expects. Field
371
- * shapes mirror the server contract: `type` ∈ {skill,worker}, `contributes` is
372
- * a STRING summary (not the structured object), `tarball` is base64 gzip bytes.
373
- */
374
- export interface ListingRequest {
375
- type: ListingType;
376
- name: string;
377
- slug: string;
378
- version: string;
379
- summary?: string;
380
- contributes?: string;
381
- creatorHandle?: string;
382
- /**
383
- * OPTIONAL staff-gated publish-on-behalf target handle (`hq publish --as
384
- * <handle>`). When set, the server attributes the listing to the seeded,
385
- * UNCLAIMED creator with this handle instead of the caller — but only if the
386
- * caller is staff (`@getindigo.ai`) and the server has the capability enabled.
387
- */
388
- publishAsHandle?: string;
389
- /** Base64-encoded gzip pack tarball (pack.tar.gz bytes). */
390
- tarball: string;
391
- }
392
-
393
- export interface PublishDeps {
394
- /** Returns the cached idToken, or null/expired if not logged in. */
395
- loadIdToken: () => string | null;
396
- /** Returns a non-interactive access token (normal self-publish). */
397
- getAccessToken: () => Promise<string>;
398
- /**
399
- * Returns a non-interactive ID token. Required for an on-behalf publish
400
- * (`--as <handle>`): the server's staff gate reads the `email` claim, which is
401
- * on the id_token only — the access token returns 403 NO_EMAIL_CLAIM.
402
- */
403
- getIdToken: () => Promise<string>;
404
- /**
405
- * POSTs the listing as a JSON body to `/v1/listings` and returns the parsed
406
- * { status, body }. `tarball` is the BASE64-encoded gzip pack bytes — the
407
- * server JSON.parses the body and base64-decodes the tarball.
408
- */
409
- upload: (args: {
410
- token: string;
411
- listing: ListingRequest;
412
- }) => Promise<{ status: number; body: Record<string, unknown> }>;
413
- hqRoot: string;
414
- /** Reads core.yaml hqVersion for the validator (null when absent). */
415
- hqVersion: string | null;
416
- }
417
-
418
- export interface PublishResult {
419
- notice: string;
420
- author: ResolvedAuthor;
421
- pkg: PackManifest;
422
- /** True when the success notice already carries the server's attribution. */
423
- serverAttributed: boolean;
424
- }
425
-
426
- /**
427
- * Core publish logic, dependency-injected so it can be unit-tested with mocked
428
- * network + a tmp filesystem (no real API, no browser login). Returns the
429
- * success notice; throws (uploading nothing) on logged-out or validation
430
- * failure.
431
- */
432
- export async function runPublish(
433
- targetPath: string,
434
- deps: PublishDeps,
435
- opts: { publishAsHandle?: string } = {},
436
- ): Promise<PublishResult> {
437
- // 1. Logged-in gate — refuse BEFORE any packing/upload.
438
- const idToken = deps.loadIdToken();
439
- if (!idToken) {
440
- throw new Error('Not logged in — run `hq login` before publishing. Nothing was uploaded.');
441
- }
442
- const author = resolveAuthor(peekIdToken(idToken));
443
- const publishAsHandle =
444
- typeof opts.publishAsHandle === 'string' && opts.publishAsHandle.length > 0
445
- ? opts.publishAsHandle
446
- : undefined;
447
-
448
- // Resolve a payload dir. We stamp/validate against a temp copy so we never
449
- // mutate the user's working tree's package.yaml.
450
- const absTarget = path.resolve(process.cwd(), targetPath);
451
- if (!fs.existsSync(absTarget) || !fs.statSync(absTarget).isDirectory()) {
452
- throw new Error(`Not a directory: ${absTarget}`);
453
- }
454
- const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hq-publish-'));
455
- const payloadDir = path.join(workDir, 'payload');
456
- try {
457
- fs.cpSync(absTarget, payloadDir, { recursive: true });
458
-
459
- // 2. Stamp author (AC1) BEFORE validation so the stamped author is what
460
- // gets validated, packed, and recorded.
461
- if (!fs.existsSync(path.join(payloadDir, 'package.yaml'))) {
462
- throw new Error(`package.yaml missing from ${absTarget} — nothing was uploaded.`);
463
- }
464
- // On the on-behalf path (`--as <handle>`) the SERVER is authoritative for
465
- // attribution (it resolves the target creator by handle and ignores the
466
- // manifest/stamped author), so we must NOT overwrite the manifest's author
467
- // with the CALLER's identity — the manifest already names the target
468
- // creator. Stamp the caller only on the normal self-publish path.
469
- if (!publishAsHandle) {
470
- stampAuthorIntoPackage(payloadDir, author);
471
- }
472
-
473
- // 3. Validate with the canonical 10-rule validator. Throws on any failure;
474
- // we have uploaded nothing at this point.
475
- const pkg = validateManifest(payloadDir, deps.hqVersion);
476
-
477
- // 4. Tar + base64 + authenticated JSON upload. The deployed server
478
- // JSON.parses the body, so we send the gzip tarball base64-encoded
479
- // inside a JSON listing request (NOT a raw octet-stream body).
480
- const tarballBytes = tarballPayload(payloadDir);
481
- // On-behalf publishes are staff-gated server-side and the staff gate reads
482
- // the id_token `email` claim — send the ID token, not the access token
483
- // (which has no email and would 403 NO_EMAIL_CLAIM). Normal self-publish
484
- // continues to use the access token.
485
- const token = publishAsHandle
486
- ? await deps.getIdToken()
487
- : await deps.getAccessToken();
488
- const listing: ListingRequest = {
489
- type: inferListingType(pkg),
490
- name: pkg.name,
491
- slug: deriveSlug(pkg.name),
492
- version: pkg.version,
493
- summary: pkg.description,
494
- contributes: summarizeContributes(pkg.contributes) || undefined,
495
- // On-behalf: send the target handle so the body is self-describing (the
496
- // server is authoritative for attribution either way). Normal: our
497
- // resolved handle is a backwards-compat fallback under the server's
498
- // claimed-handle lookup.
499
- creatorHandle: publishAsHandle ?? author.handle,
500
- ...(publishAsHandle ? { publishAsHandle } : {}),
501
- tarball: Buffer.from(tarballBytes).toString('base64'),
502
- };
503
- const { status, body } = await deps.upload({ token, listing });
504
- const notice = buildListingNotice(status, body, pkg.name, pkg.version);
505
- // Unwrap the `{ listing }` success envelope the server sends so attribution
506
- // reads the server's resolved handle (fall back to top-level for compat).
507
- const serverHandle = unwrapListing(body).creatorHandle?.trim();
508
-
509
- // 5. Provenance (only after a successful upload).
510
- recordProvenance(deps.hqRoot, pkg, absTarget);
511
-
512
- return { notice, author, pkg, serverAttributed: Boolean(serverHandle) };
513
- } finally {
514
- fs.rmSync(workDir, { recursive: true, force: true });
515
- }
516
- }
517
-
518
- // ---------------------------------------------------------------------------
519
- // Command wiring
520
- // ---------------------------------------------------------------------------
521
-
522
- export function registerPublishCommand(program: Command): void {
523
- program
524
- .command('publish <path>')
525
- .description(
526
- 'Package a skill/worker pack and submit it to the HQ marketplace (POST /v1/listings).',
527
- )
528
- .option(
529
- '--as <handle>',
530
- 'Publish on behalf of a seeded, unclaimed creator by handle (HQ staff only). ' +
531
- 'The listing is attributed to that creator instead of you.',
532
- )
533
- .option('--on-behalf <handle>', 'Alias for --as.')
534
- .action(async (targetPath: string, options: { as?: string; onBehalf?: string }) => {
535
- try {
536
- const publishAsHandle = options.as ?? options.onBehalf;
537
- const hqRoot = findHqRoot();
538
- const result = await runPublish(targetPath, {
539
- loadIdToken: () => {
540
- const cached = loadCachedTokens();
541
- if (!cached || isExpiring(cached, 0)) return null;
542
- return cached.idToken;
543
- },
544
- getAccessToken: () => ensureCognitoToken({ interactive: false }),
545
- getIdToken: () => ensureCognitoIdToken({ interactive: false }),
546
- upload: async ({ token, listing }) => {
547
- // JSON body (application/json) — the deployed handler JSON.parses
548
- // the body and base64-decodes `listing.tarball`. vaultApiFetch
549
- // sends application/json + the bearer token.
550
- const res = await vaultApiFetch({
551
- token,
552
- path: '/v1/listings',
553
- method: 'POST',
554
- body: listing as unknown as Record<string, unknown>,
555
- });
556
- const json = (await res.json().catch(() => ({}))) as Record<string, unknown>;
557
- return { status: res.status, body: json };
558
- },
559
- hqRoot,
560
- hqVersion: readHqVersion(hqRoot),
561
- }, { publishAsHandle });
562
- console.log(chalk.green(result.notice));
563
- // Only print the locally-resolved author as a fallback when the server
564
- // did NOT return its own resolved handle (already in the notice).
565
- if (!result.serverAttributed) {
566
- console.log(
567
- chalk.dim(
568
- ` Attributed to ${result.author.displayName} (@${result.author.handle}).`,
569
- ),
570
- );
571
- }
572
- } catch (err) {
573
- console.error(
574
- chalk.red('Error:'),
575
- err instanceof Error ? err.message : String(err),
576
- );
577
- process.exit(1);
578
- }
579
- });
580
- }