@indigoai-us/hq-cli 5.35.1 → 5.36.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.
@@ -14,10 +14,11 @@
14
14
  * by the deploy + sync skills.
15
15
  */
16
16
 
17
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="a4a30b14-55b3-5206-9148-48aff1a8cdd4")}catch(e){}}();
17
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="e8f8cd77-2f24-5e71-8cb6-518095482b22")}catch(e){}}();
18
18
  import chalk from "chalk";
19
19
  import { browserLogin, clearCachedTokens, loadCachedTokens, isExpiring, CognitoAuthError, } from "@indigoai-us/hq-cloud";
20
- import { DEFAULT_COGNITO, refreshCachedSession, } from "../utils/cognito-session.js";
20
+ import { refreshCachedSession, } from "../utils/cognito-session.js";
21
+ import { cognitoConfigForLoginProvider } from "../utils/login-provider.js";
21
22
  /**
22
23
  * Decode the (unverified) ID token payload for display purposes only.
23
24
  * The token was just returned by Cognito's token endpoint, so its contents
@@ -44,7 +45,8 @@ export function registerAuthCommands(program) {
44
45
  authCmd
45
46
  .command("login")
46
47
  .description("Sign in to HQ — opens the Cognito Hosted UI and caches tokens locally")
47
- .action(async () => {
48
+ .option("--provider <provider>", "OAuth provider to use: google, microsoft, or picker")
49
+ .action(async (options) => {
48
50
  const existing = loadCachedTokens();
49
51
  if (existing && !isExpiring(existing, 120)) {
50
52
  const who = peekIdToken(existing.idToken).email ?? "cached session";
@@ -52,7 +54,7 @@ export function registerAuthCommands(program) {
52
54
  return;
53
55
  }
54
56
  try {
55
- const tokens = await browserLogin(DEFAULT_COGNITO);
57
+ const tokens = await browserLogin(cognitoConfigForLoginProvider(options.provider));
56
58
  const who = peekIdToken(tokens.idToken).email ?? "HQ";
57
59
  console.log(chalk.green(`Signed in as ${who}`));
58
60
  console.log(chalk.dim(` Token cached at ~/.hq/cognito-tokens.json (expires ${tokens.expiresAt})`));
@@ -110,4 +112,4 @@ export function registerAuthCommands(program) {
110
112
  });
111
113
  }
112
114
  //# sourceMappingURL=auth.js.map
113
- //# debugId=a4a30b14-55b3-5206-9148-48aff1a8cdd4
115
+ //# debugId=e8f8cd77-2f24-5e71-8cb6-518095482b22
@@ -222,11 +222,20 @@ export interface ManifestCompanyEntry {
222
222
  * under the target slug. Read → mutate → temp-write → rename so concurrent
223
223
  * readers never see a partially-written file.
224
224
  *
225
- * Idempotent: if the values already match, this is a no-op (still rewrites
226
- * the file to canonical YAML, but the mutation is identical).
225
+ * Skip-if-unchanged: if the target slug already carries the exact
226
+ * `cloud_uid` + `bucket_name`, this is a true no-op — the file is left
227
+ * byte-for-byte intact (comments, ordering, and formatting preserved) and
228
+ * we return `false`. This matters because `yaml.dump` does NOT round-trip
229
+ * comments or the original layout: an unconditional rewrite re-canonicalises
230
+ * the manifest (stripping the `/newcompany` header comment, reflowing keys)
231
+ * on EVERY provision/Connect, even when nothing semantically changed. Each
232
+ * such rewrite is then pushed by the initial-sync step, so the
233
+ * comment-stripped local form perpetually diverges from any peer/cloud copy
234
+ * that still holds the commented form — manifesting as a recurring HQ Sync
235
+ * conflict loop on `companies/manifest.yaml` that re-fires every sync. Only
236
+ * writing when a value actually changes lets the two forms converge.
227
237
  *
228
- * Returns true if the file was written (always true in current impl
229
- * reserved for future "skip if unchanged" optimization).
238
+ * Returns true if the file was written, false if it was already current.
230
239
  */
231
240
  export declare function patchManifest(hqRoot: string, slug: string, cloudUid: string, bucketName: string): boolean;
232
241
  /**
@@ -28,7 +28,7 @@
28
28
  * `initial_sync.ok=false`). Manifest + config may have been written.
29
29
  */
30
30
 
31
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="925baddc-7b12-5ad4-b64f-d7e20bd0daab")}catch(e){}}();
31
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="3ae2829b-1343-59f7-92b0-63d93f37086f")}catch(e){}}();
32
32
  import chalk from "chalk";
33
33
  import * as fs from "node:fs";
34
34
  import * as path from "node:path";
@@ -209,11 +209,20 @@ export function ensureManifestEntryForProvision(hqRoot, slug) {
209
209
  * under the target slug. Read → mutate → temp-write → rename so concurrent
210
210
  * readers never see a partially-written file.
211
211
  *
212
- * Idempotent: if the values already match, this is a no-op (still rewrites
213
- * the file to canonical YAML, but the mutation is identical).
212
+ * Skip-if-unchanged: if the target slug already carries the exact
213
+ * `cloud_uid` + `bucket_name`, this is a true no-op — the file is left
214
+ * byte-for-byte intact (comments, ordering, and formatting preserved) and
215
+ * we return `false`. This matters because `yaml.dump` does NOT round-trip
216
+ * comments or the original layout: an unconditional rewrite re-canonicalises
217
+ * the manifest (stripping the `/newcompany` header comment, reflowing keys)
218
+ * on EVERY provision/Connect, even when nothing semantically changed. Each
219
+ * such rewrite is then pushed by the initial-sync step, so the
220
+ * comment-stripped local form perpetually diverges from any peer/cloud copy
221
+ * that still holds the commented form — manifesting as a recurring HQ Sync
222
+ * conflict loop on `companies/manifest.yaml` that re-fires every sync. Only
223
+ * writing when a value actually changes lets the two forms converge.
214
224
  *
215
- * Returns true if the file was written (always true in current impl
216
- * reserved for future "skip if unchanged" optimization).
225
+ * Returns true if the file was written, false if it was already current.
217
226
  */
218
227
  export function patchManifest(hqRoot, slug, cloudUid, bucketName) {
219
228
  const mPath = manifestPath(hqRoot);
@@ -224,6 +233,12 @@ export function patchManifest(hqRoot, slug, cloudUid, bucketName) {
224
233
  const existing = parsed.companies[slug];
225
234
  // Preserve null / object / unknown — promote null → {} so we can write keys.
226
235
  const entry = existing && typeof existing === "object" ? { ...existing } : {};
236
+ // No-op guard: both fields already match → leave the on-disk file (and its
237
+ // comments) untouched so a re-provision can't churn the manifest and seed a
238
+ // sync conflict loop.
239
+ if (entry.cloud_uid === cloudUid && entry.bucket_name === bucketName) {
240
+ return false;
241
+ }
227
242
  entry.cloud_uid = cloudUid;
228
243
  entry.bucket_name = bucketName;
229
244
  parsed.companies[slug] = entry;
@@ -495,9 +510,13 @@ export async function provisionCompany(options) {
495
510
  const cloudUid = entity.uid;
496
511
  const bucketName = entity.bucketName;
497
512
  const kmsKeyId = entity.kmsKeyId ?? null;
498
- // Step 6: patch manifest atomically
499
- patchManifest(options.hqRoot, options.slug, cloudUid, bucketName);
500
- log(`patched companies/manifest.yaml`);
513
+ // Step 6: patch manifest atomically. Skip-if-unchanged returns false when
514
+ // the manifest already carries this slug's cloud_uid + bucket_name, so we
515
+ // report the honest outcome rather than always claiming a patch.
516
+ const manifestPatched = patchManifest(options.hqRoot, options.slug, cloudUid, bucketName);
517
+ log(manifestPatched
518
+ ? `patched companies/manifest.yaml`
519
+ : `companies/manifest.yaml already current — left untouched`);
501
520
  // Step 7: write .hq/config.json atomically
502
521
  writeCompanyConfig(options.hqRoot, options.slug, {
503
522
  companyUid: cloudUid,
@@ -556,7 +575,7 @@ export async function provisionCompany(options) {
556
575
  vault_api_url: options.vaultApiUrl,
557
576
  kms_key_id: kmsKeyId,
558
577
  created_entity: createdEntity,
559
- manifest_patched: true,
578
+ manifest_patched: manifestPatched,
560
579
  config_written: true,
561
580
  initial_sync: initialSync,
562
581
  };
@@ -615,4 +634,4 @@ export function registerCloudProvisionCommands(program) {
615
634
  });
616
635
  }
617
636
  //# sourceMappingURL=cloud-provision.js.map
618
- //# debugId=925baddc-7b12-5ad4-b64f-d7e20bd0daab
637
+ //# debugId=3ae2829b-1343-59f7-92b0-63d93f37086f
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `hq creators apply` — apply for verified-creator access.
3
+ *
4
+ * Publishing to the marketplace is gated on verified-creator status. An
5
+ * unverified caller submits an application (a short pitch + optional desired
6
+ * handle); it is persisted server-side and an Indigo admin reviews it. On
7
+ * approval the caller becomes a verified creator and can `hq publish`.
8
+ *
9
+ * Wire contract (POST /v1/creators/request-access, authed):
10
+ * body { reason: string, handle?: string }
11
+ * 202 { status, code, applicationId, requestAccessPath }
12
+ * 409 { code: "APPLICATION_PENDING", error, applicationId } (already pending)
13
+ */
14
+ import { Command } from "commander";
15
+ export interface CreatorApplyOptions {
16
+ reason: string;
17
+ handle?: string;
18
+ }
19
+ export interface CreatorApplyDeps {
20
+ /** Resolve a non-expired access token (prompts login when interactive). */
21
+ getAccessToken: () => Promise<string>;
22
+ /** POST the application body to the vault API. */
23
+ post: (token: string, body: Record<string, unknown>) => Promise<Response>;
24
+ }
25
+ export interface CreatorApplyResult {
26
+ message: string;
27
+ }
28
+ /**
29
+ * Testable core. Submits the application and maps the server response to a
30
+ * single human-readable message. Throws on hard failures.
31
+ */
32
+ export declare function runCreatorApply(opts: CreatorApplyOptions, deps: CreatorApplyDeps): Promise<CreatorApplyResult>;
33
+ export declare function registerCreatorsCommand(program: Command): void;
34
+ //# sourceMappingURL=creators.d.ts.map
@@ -0,0 +1,68 @@
1
+
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="9cd4617e-f5b0-54ad-86a0-042157165a91")}catch(e){}}();
3
+ import chalk from "chalk";
4
+ import { ensureCognitoToken } from "../utils/cognito-session.js";
5
+ import { vaultApiFetch } from "../utils/vault-api.js";
6
+ /**
7
+ * Testable core. Submits the application and maps the server response to a
8
+ * single human-readable message. Throws on hard failures.
9
+ */
10
+ export async function runCreatorApply(opts, deps) {
11
+ const reason = opts.reason?.trim();
12
+ if (!reason) {
13
+ throw new Error('A reason is required — pass --reason "why you want to publish to the marketplace".');
14
+ }
15
+ const token = await deps.getAccessToken();
16
+ const body = { reason };
17
+ const handle = opts.handle?.trim();
18
+ if (handle)
19
+ body.handle = handle;
20
+ const res = await deps.post(token, body);
21
+ const parsed = (await res
22
+ .json()
23
+ .catch(() => ({})));
24
+ if (res.status === 409 || parsed.code === "APPLICATION_PENDING") {
25
+ return {
26
+ message: "You already have a pending creator application — an Indigo admin will review it.",
27
+ };
28
+ }
29
+ if (res.status < 200 || res.status >= 300) {
30
+ const msg = parsed.error ?? parsed.message ?? `HTTP ${res.status}`;
31
+ throw new Error(`Creator application failed: ${msg}`);
32
+ }
33
+ const id = typeof parsed.applicationId === "string" ? ` (${parsed.applicationId})` : "";
34
+ return {
35
+ message: `Creator application submitted${id} — an Indigo admin will review it. ` +
36
+ "You'll be able to `hq publish` once approved.",
37
+ };
38
+ }
39
+ export function registerCreatorsCommand(program) {
40
+ const creators = program
41
+ .command("creators")
42
+ .description("Creator marketplace account");
43
+ creators
44
+ .command("apply")
45
+ .description("Apply for verified-creator access (required to publish packs)")
46
+ .requiredOption("--reason <reason>", "Why you want to publish to the marketplace")
47
+ .option("--handle <handle>", "Desired creator handle (optional)")
48
+ .action(async (options) => {
49
+ try {
50
+ const result = await runCreatorApply({ reason: options.reason, handle: options.handle }, {
51
+ getAccessToken: () => ensureCognitoToken({ interactive: true }),
52
+ post: (token, body) => vaultApiFetch({
53
+ token,
54
+ path: "/v1/creators/request-access",
55
+ method: "POST",
56
+ body,
57
+ }),
58
+ });
59
+ console.log(chalk.green(result.message));
60
+ }
61
+ catch (err) {
62
+ console.error(chalk.red(err.message));
63
+ process.exitCode = 1;
64
+ }
65
+ });
66
+ }
67
+ //# sourceMappingURL=creators.js.map
68
+ //# debugId=9cd4617e-f5b0-54ad-86a0-042157165a91
@@ -1,5 +1,5 @@
1
1
  /**
2
- * hq login — opens browser for Cognito auth via Google OAuth
2
+ * hq login — opens browser for Cognito auth.
3
3
  */
4
4
  import { Command } from 'commander';
5
5
  export declare function registerLoginCommand(program: Command): void;
@@ -1,11 +1,11 @@
1
1
  /**
2
- * hq login — opens browser for Cognito auth via Google OAuth
2
+ * hq login — opens browser for Cognito auth.
3
3
  */
4
4
 
5
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="31735154-208b-5544-85a4-22f8f0fbe415")}catch(e){}}();
5
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="cb1c9acd-d4b0-52a2-b5ce-f567be9ff048")}catch(e){}}();
6
6
  import chalk from 'chalk';
7
7
  import { browserLogin, loadCachedTokens, isExpiring } from '@indigoai-us/hq-cloud';
8
- import { DEFAULT_COGNITO } from '../utils/cognito-session.js';
8
+ import { cognitoConfigForLoginProvider } from '../utils/login-provider.js';
9
9
  function peekIdToken(idToken) {
10
10
  try {
11
11
  const payload = idToken.split('.')[1];
@@ -23,8 +23,9 @@ function peekIdToken(idToken) {
23
23
  export function registerLoginCommand(program) {
24
24
  program
25
25
  .command('login')
26
- .description('Authenticate with HQ via Cognito (Google OAuth)')
27
- .action(async () => {
26
+ .description('Authenticate with HQ via Cognito')
27
+ .option('--provider <provider>', 'OAuth provider to use: google, microsoft, or picker')
28
+ .action(async (options) => {
28
29
  try {
29
30
  const existing = loadCachedTokens();
30
31
  if (existing && !isExpiring(existing, 120)) {
@@ -33,7 +34,7 @@ export function registerLoginCommand(program) {
33
34
  return;
34
35
  }
35
36
  console.log('Opening browser for authentication...');
36
- const tokens = await browserLogin(DEFAULT_COGNITO);
37
+ const tokens = await browserLogin(cognitoConfigForLoginProvider(options.provider));
37
38
  const who = peekIdToken(tokens.idToken).email ?? 'HQ';
38
39
  console.log(chalk.green(`Logged in as ${who}`));
39
40
  }
@@ -44,4 +45,4 @@ export function registerLoginCommand(program) {
44
45
  });
45
46
  }
46
47
  //# sourceMappingURL=login.js.map
47
- //# debugId=31735154-208b-5544-85a4-22f8f0fbe415
48
+ //# debugId=cb1c9acd-d4b0-52a2-b5ce-f567be9ff048
@@ -34,7 +34,7 @@
34
34
  * from each pack's package.yaml; rationale lives in the layout-fix PR.)
35
35
  */
36
36
 
37
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="dafe45c8-d9bb-5082-b5b7-aaf1a2ee5c7e")}catch(e){}}();
37
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="d5029b6f-fc42-52ea-9ddd-aaaae682a8f4")}catch(e){}}();
38
38
  import * as fs from 'fs';
39
39
  import * as os from 'os';
40
40
  import * as path from 'path';
@@ -263,20 +263,45 @@ export function defaultMarketplaceDeps() {
263
263
  return {
264
264
  resolveListing: async (slug, version) => {
265
265
  // Search by slug (public). The API returns approved listings only.
266
+ //
267
+ // BUGFIX: the browse handler historically IGNORED `?slug=` and only
268
+ // honored `?q=` (fuzzy), so `?slug=tdd` returned ALL approved listings
269
+ // newest-first — and blindly taking `listings[0]` then installed the
270
+ // newest pack, not the requested slug (e.g. `marketplace:tdd` installed
271
+ // hq-pack-review). We now (a) query with BOTH `slug` (exact, server-side
272
+ // filter being added in parallel) AND `q` (fuzzy fallback) so the right
273
+ // results come back whichever param the server honors, and (b) NEVER take
274
+ // `listings[0]` — we exact-match `l.slug === slug` among the results and
275
+ // throw a clear error if none matches, rather than installing the wrong
276
+ // pack.
277
+ const query = { slug, q: slug };
278
+ if (version)
279
+ query.version = version;
266
280
  const res = await vaultApiFetchPublic({
267
281
  path: '/v1/listings',
268
- query: version ? { slug, version } : { slug },
282
+ query,
269
283
  });
270
284
  if (!res.ok) {
271
285
  throw new Error(`Failed to search marketplace for "${slug}" (HTTP ${res.status}).`);
272
286
  }
273
287
  const body = (await res.json().catch(() => ({})));
274
288
  const listings = body.listings ?? [];
275
- const match = version
276
- ? listings.find((l) => (l.version ?? l.latestVersion) === version)
277
- : listings[0];
289
+ // Only consider listings whose slug EXACTLY matches the request — the
290
+ // fuzzy `q` search (and a slug-ignoring browse handler) can return
291
+ // unrelated packs, so an exact-slug filter is the safety floor.
292
+ const exact = listings.filter((l) => l.slug === slug);
293
+ let match;
294
+ if (version) {
295
+ // Pinned `marketplace:<slug>@<version>` → require an exact version too.
296
+ match = exact.find((l) => (l.version ?? l.latestVersion) === version);
297
+ }
298
+ else {
299
+ // No pin → newest exact-slug match. The feed is newest-first, so the
300
+ // first exact-slug entry is the newest version of that slug.
301
+ match = exact[0];
302
+ }
278
303
  if (!match) {
279
- throw new Error(`No approved marketplace listing found for "${slug}"${version ? `@${version}` : ''}.`);
304
+ throw new Error(`No marketplace listing found for slug "${slug}"${version ? `@${version}` : ''}.`);
280
305
  }
281
306
  const id = match.listingId ?? match.id;
282
307
  if (!id)
@@ -726,7 +751,12 @@ export function validateManifest(payloadDir, hqVersion) {
726
751
  if (!range || !semverValidRange(range)) {
727
752
  throw new Error(`requires.hqCore must be a valid semver range (got "${range}")`);
728
753
  }
729
- if (hqVersion && !semverSatisfies(hqVersion, range)) {
754
+ // `includePrerelease` so a prerelease host version (e.g. `15.0.9-beta.1`)
755
+ // satisfies a plain range like `>=14.2.0`. By default node-semver excludes
756
+ // prereleases from range matching, which would wrongly reject every dogfooding
757
+ // beta/rc HQ build from installing packs. A genuinely-too-old host (e.g.
758
+ // `13.0.0`) still fails the range.
759
+ if (hqVersion && !semverSatisfies(hqVersion, range, { includePrerelease: true })) {
730
760
  throw new Error(`Host hqCore ${hqVersion} does not satisfy pack requirement ${range}`);
731
761
  }
732
762
  // 7. contributes has at least one non-empty subfield
@@ -1051,4 +1081,4 @@ export async function installPack(source, opts = {}) {
1051
1081
  }
1052
1082
  }
1053
1083
  //# sourceMappingURL=pack-install.js.map
1054
- //# debugId=dafe45c8-d9bb-5082-b5b7-aaf1a2ee5c7e
1084
+ //# debugId=d5029b6f-fc42-52ea-9ddd-aaaae682a8f4
@@ -28,7 +28,7 @@
28
28
  * - manifest.ts (modules.yaml read/write for provenance)
29
29
  */
30
30
 
31
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="73704bc7-d7b1-5c4c-a34e-54edadcc2753")}catch(e){}}();
31
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="f5046847-a2b9-50e4-9a68-6a81a9e0de2b")}catch(e){}}();
32
32
  import * as fs from 'fs';
33
33
  import * as os from 'os';
34
34
  import * as path from 'path';
@@ -237,6 +237,12 @@ export function buildListingNotice(status, body, packName, packVersion) {
237
237
  `Bump the version in package.yaml and re-run \`hq publish\`.`);
238
238
  }
239
239
  if (status === 401 || status === 403) {
240
+ const code = typeof body.code === 'string' ? body.code : '';
241
+ if (code === 'NOT_VERIFIED_CREATOR') {
242
+ throw new Error('Only verified creators can publish to the marketplace. ' +
243
+ 'Run `hq creators apply --reason "..."` to request verified-creator access; ' +
244
+ 'an Indigo admin will review it.');
245
+ }
240
246
  throw new Error('Not authorized to publish — run `hq login` and ensure your creator account is verified.');
241
247
  }
242
248
  if (status < 200 || status >= 300) {
@@ -372,4 +378,4 @@ export function registerPublishCommand(program) {
372
378
  });
373
379
  }
374
380
  //# sourceMappingURL=publish.js.map
375
- //# debugId=73704bc7-d7b1-5c4c-a34e-54edadcc2753
381
+ //# debugId=f5046847-a2b9-50e4-9a68-6a81a9e0de2b
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * HQ CLI - Module management, package management, and cloud sync for HQ
4
4
  */
5
5
 
6
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="202089d2-b89b-5779-aa66-f88531a9b893")}catch(e){}}();
6
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="6fa5c878-2930-5b94-bc7c-5deea8795218")}catch(e){}}();
7
7
  import { Command } from "commander";
8
8
  import { initSentry, Sentry } from "./sentry.js";
9
9
  import { registerAddCommand } from "./commands/add.js";
@@ -25,6 +25,7 @@ import { registerPackageUpdateCommand } from "./commands/pkg-update.js";
25
25
  import { registerPackageListCommand } from "./commands/pkg-list.js";
26
26
  import { registerPacksCommand } from "./commands/packs.js";
27
27
  import { registerPublishCommand } from "./commands/publish.js";
28
+ import { registerCreatorsCommand } from "./commands/creators.js";
28
29
  import { registerTeamSyncCommand } from "./commands/team-sync.js";
29
30
  import { registerAuthCommands } from "./commands/auth.js";
30
31
  import { registerSecretsCommand } from "./commands/secrets.js";
@@ -91,6 +92,8 @@ registerPackageRemoveCommand(program);
91
92
  // "hq publish <skill-or-worker-path>" packages and submits a pack to the
92
93
  // marketplace via POST /v1/listings.
93
94
  registerPublishCommand(program);
95
+ // `hq creators apply` — request verified-creator access (required to publish).
96
+ registerCreatorsCommand(program);
94
97
  // Cloud sync subcommand group
95
98
  const syncCmd = program
96
99
  .command("sync")
@@ -174,4 +177,4 @@ registerRescueCommand(program);
174
177
  }
175
178
  })();
176
179
  //# sourceMappingURL=index.js.map
177
- //# debugId=202089d2-b89b-5779-aa66-f88531a9b893
180
+ //# debugId=6fa5c878-2930-5b94-bc7c-5deea8795218
@@ -0,0 +1,11 @@
1
+ import type { CognitoAuthConfig } from "@indigoai-us/hq-cloud";
2
+ declare const PROVIDER_TO_COGNITO_IDP: {
3
+ readonly google: "Google";
4
+ readonly microsoft: "MicrosoftPersonal";
5
+ readonly picker: undefined;
6
+ };
7
+ export type LoginProvider = keyof typeof PROVIDER_TO_COGNITO_IDP;
8
+ export declare function parseLoginProvider(value: string | undefined): LoginProvider | undefined;
9
+ export declare function cognitoConfigForLoginProvider(value: string | undefined): CognitoAuthConfig;
10
+ export {};
11
+ //# sourceMappingURL=login-provider.d.ts.map
@@ -0,0 +1,28 @@
1
+
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="10f78162-e7f8-5295-a572-936ae454ad62")}catch(e){}}();
3
+ import { DEFAULT_COGNITO } from "./cognito-session.js";
4
+ const PROVIDER_TO_COGNITO_IDP = {
5
+ google: "Google",
6
+ microsoft: "MicrosoftPersonal",
7
+ picker: undefined,
8
+ };
9
+ export function parseLoginProvider(value) {
10
+ if (value === undefined)
11
+ return undefined;
12
+ const normalized = value.trim().toLowerCase();
13
+ if (normalized in PROVIDER_TO_COGNITO_IDP) {
14
+ return normalized;
15
+ }
16
+ throw new Error("Provider must be one of: google, microsoft, picker");
17
+ }
18
+ export function cognitoConfigForLoginProvider(value) {
19
+ const provider = parseLoginProvider(value);
20
+ if (!provider)
21
+ return DEFAULT_COGNITO;
22
+ return {
23
+ ...DEFAULT_COGNITO,
24
+ identityProvider: PROVIDER_TO_COGNITO_IDP[provider],
25
+ };
26
+ }
27
+ //# sourceMappingURL=login-provider.js.map
28
+ //# debugId=10f78162-e7f8-5295-a572-936ae454ad62
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.35.1",
3
+ "version": "5.36.0",
4
4
  "description": "HQ by Indigo management CLI \u2014 modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -15,7 +15,7 @@
15
15
  "clean": "rm -rf dist"
16
16
  },
17
17
  "dependencies": {
18
- "@indigoai-us/hq-cloud": "^6.0.1",
18
+ "@indigoai-us/hq-cloud": "^6.2.0",
19
19
  "@indigoai-us/hq-onboarding": "^0.1.0",
20
20
  "@sentry/node": "^10.49.0",
21
21
  "chalk": "^5.3.0",
@@ -27,6 +27,7 @@ import {
27
27
  DEFAULT_COGNITO,
28
28
  refreshCachedSession,
29
29
  } from "../utils/cognito-session.js";
30
+ import { cognitoConfigForLoginProvider } from "../utils/login-provider.js";
30
31
 
31
32
  /**
32
33
  * Decode the (unverified) ID token payload for display purposes only.
@@ -62,7 +63,11 @@ export function registerAuthCommands(program: Command): void {
62
63
  .description(
63
64
  "Sign in to HQ — opens the Cognito Hosted UI and caches tokens locally",
64
65
  )
65
- .action(async () => {
66
+ .option(
67
+ "--provider <provider>",
68
+ "OAuth provider to use: google, microsoft, or picker",
69
+ )
70
+ .action(async (options: { provider?: string }) => {
66
71
  const existing = loadCachedTokens();
67
72
  if (existing && !isExpiring(existing, 120)) {
68
73
  const who = peekIdToken(existing.idToken).email ?? "cached session";
@@ -72,7 +77,7 @@ export function registerAuthCommands(program: Command): void {
72
77
  return;
73
78
  }
74
79
  try {
75
- const tokens = await browserLogin(DEFAULT_COGNITO);
80
+ const tokens = await browserLogin(cognitoConfigForLoginProvider(options.provider));
76
81
  const who = peekIdToken(tokens.idToken).email ?? "HQ";
77
82
  console.log(chalk.green(`Signed in as ${who}`));
78
83
  console.log(
@@ -379,6 +379,84 @@ describe("patchManifest", () => {
379
379
  bucket_name: "hq-vault-cmp-NEW",
380
380
  });
381
381
  });
382
+
383
+ it("returns true when it actually changes a value", () => {
384
+ expect(patchManifest(tmpRoot, "indigo", "cmp_01H", "hq-vault-cmp-01H")).toBe(
385
+ true,
386
+ );
387
+ });
388
+
389
+ // ── Conflict-loop regression ───────────────────────────────────────────────
390
+ // Repro for the recurring HQ Sync conflict loop on companies/manifest.yaml:
391
+ // a re-provision / menubar Connect on an already-provisioned company used to
392
+ // unconditionally reserialize the manifest via yaml.dump — stripping comments
393
+ // and reflowing — even though nothing changed. The initial-sync step then
394
+ // pushed that comment-stripped form, so it perpetually diverged from any
395
+ // peer/cloud copy still holding the commented form, re-firing a conflict on
396
+ // every sync. The fix: skip the write entirely when values already match.
397
+ describe("conflict-loop regression — no-op re-provision must not churn the file", () => {
398
+ // A manifest in the on-disk shape /newcompany writes: header comment, blank
399
+ // lines, per-entry comment — none of which yaml.dump round-trips.
400
+ const COMMENTED_MANIFEST = `# HQ companies manifest — source of truth for routing.
401
+ # Synced across machines via the personal vault; edit with care.
402
+
403
+ companies:
404
+ indigo:
405
+ name: Indigo
406
+ status: active
407
+ cloud_uid: cmp_01H
408
+ bucket_name: hq-vault-cmp-01H
409
+ acme:
410
+ name: Acme Corp
411
+ status: active
412
+ `;
413
+
414
+ beforeEach(() => {
415
+ const mPath = manifestPath(tmpRoot);
416
+ fs.mkdirSync(path.dirname(mPath), { recursive: true });
417
+ fs.writeFileSync(mPath, COMMENTED_MANIFEST);
418
+ });
419
+
420
+ it("returns false and leaves the file byte-for-byte intact (comments preserved) when values already match", () => {
421
+ const changed = patchManifest(
422
+ tmpRoot,
423
+ "indigo",
424
+ "cmp_01H",
425
+ "hq-vault-cmp-01H",
426
+ );
427
+ expect(changed).toBe(false);
428
+ // The whole point: no rewrite at all — comments + layout survive, so the
429
+ // synced bytes never diverge and no conflict is seeded.
430
+ expect(fs.readFileSync(manifestPath(tmpRoot), "utf-8")).toBe(
431
+ COMMENTED_MANIFEST,
432
+ );
433
+ });
434
+
435
+ it("re-running many times never churns the on-disk bytes (loop cannot start)", () => {
436
+ for (let i = 0; i < 5; i++) {
437
+ patchManifest(tmpRoot, "indigo", "cmp_01H", "hq-vault-cmp-01H");
438
+ }
439
+ expect(fs.readFileSync(manifestPath(tmpRoot), "utf-8")).toBe(
440
+ COMMENTED_MANIFEST,
441
+ );
442
+ });
443
+
444
+ it("a genuine change writes once, then converges — subsequent re-runs are no-ops", () => {
445
+ // First real change (acme had no cloud_uid) writes once and returns true.
446
+ expect(patchManifest(tmpRoot, "acme", "cmp_AC", "hq-vault-cmp-AC")).toBe(
447
+ true,
448
+ );
449
+ const afterFirst = fs.readFileSync(manifestPath(tmpRoot), "utf-8");
450
+ // Every subsequent identical provision is a no-op — stable bytes, false.
451
+ expect(patchManifest(tmpRoot, "acme", "cmp_AC", "hq-vault-cmp-AC")).toBe(
452
+ false,
453
+ );
454
+ expect(patchManifest(tmpRoot, "acme", "cmp_AC", "hq-vault-cmp-AC")).toBe(
455
+ false,
456
+ );
457
+ expect(fs.readFileSync(manifestPath(tmpRoot), "utf-8")).toBe(afterFirst);
458
+ });
459
+ });
382
460
  });
383
461
 
384
462
  // ── writeCompanyConfig ───────────────────────────────────────────────────────