@indigoai-us/hq-cli 5.35.1 → 5.35.2
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/auth.js +7 -5
- package/dist/commands/cloud-provision.d.ts +13 -4
- package/dist/commands/cloud-provision.js +29 -10
- package/dist/commands/creators.d.ts +34 -0
- package/dist/commands/creators.js +68 -0
- package/dist/commands/login.d.ts +1 -1
- package/dist/commands/login.js +8 -7
- package/dist/commands/pack-install.js +38 -8
- package/dist/commands/publish.js +8 -2
- package/dist/index.js +5 -2
- package/dist/utils/login-provider.d.ts +11 -0
- package/dist/utils/login-provider.js +28 -0
- package/package.json +1 -1
- package/src/commands/auth.ts +7 -2
- package/src/commands/cloud-provision.test.ts +78 -0
- package/src/commands/cloud-provision.ts +36 -8
- package/src/commands/creators.test.ts +60 -0
- package/src/commands/creators.ts +117 -0
- package/src/commands/login.ts +9 -5
- package/src/commands/marketplace-install.test.ts +129 -0
- package/src/commands/pack-install.test.ts +49 -0
- package/src/commands/pack-install.ts +34 -6
- package/src/commands/publish.test.ts +5 -0
- package/src/commands/publish.ts +8 -0
- package/src/index.ts +3 -0
- package/src/utils/cognito-session.test.ts +3 -3
- package/src/utils/login-provider.test.ts +42 -0
- package/src/utils/login-provider.ts +30 -0
package/dist/commands/auth.js
CHANGED
|
@@ -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]="
|
|
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 {
|
|
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
|
-
.
|
|
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(
|
|
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=
|
|
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
|
-
*
|
|
226
|
-
*
|
|
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
|
|
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]="
|
|
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
|
-
*
|
|
213
|
-
*
|
|
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
|
|
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
|
-
|
|
500
|
-
|
|
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:
|
|
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=
|
|
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
|
package/dist/commands/login.d.ts
CHANGED
package/dist/commands/login.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* hq login — opens browser for Cognito auth
|
|
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]="
|
|
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 {
|
|
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
|
|
27
|
-
.
|
|
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(
|
|
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=
|
|
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]="
|
|
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
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
|
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
|
-
|
|
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=
|
|
1084
|
+
//# debugId=d5029b6f-fc42-52ea-9ddd-aaaae682a8f4
|
package/dist/commands/publish.js
CHANGED
|
@@ -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]="
|
|
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=
|
|
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]="
|
|
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=
|
|
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
package/src/commands/auth.ts
CHANGED
|
@@ -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
|
-
.
|
|
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(
|
|
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 ───────────────────────────────────────────────────────
|