@indigoai-us/hq-cli 5.77.2 → 5.77.4
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/CHANGELOG.md +7 -0
- package/dist/commands/cloud-demote.js +3 -2
- package/dist/commands/cloud-provision.js +3 -2
- package/dist/commands/files.js +4 -4
- package/dist/commands/pack-install.js +5 -2
- package/dist/commands/people.js +2 -8
- package/dist/commands/pkg-install.js +3 -3
- package/dist/commands/pkg-update.js +2 -2
- package/dist/commands/publish.js +2 -1
- package/dist/commands/workers.js +3 -2
- package/dist/utils/manifest.js +3 -2
- package/dist/utils/people.js +2 -8
- package/dist/utils/registry-client.js +2 -2
- package/dist/utils/registry.js +2 -1
- package/dist/utils/user-yaml-error.d.ts +9 -0
- package/dist/utils/user-yaml-error.js +25 -0
- package/package.json +1 -1
- package/src/commands/cloud-demote.ts +3 -2
- package/src/commands/cloud-provision.ts +3 -2
- package/src/commands/files-recovery.test.ts +13 -4
- package/src/commands/files.ts +5 -5
- package/src/commands/pack-install.ts +5 -2
- package/src/commands/people.test.ts +12 -3
- package/src/commands/people.ts +5 -9
- package/src/commands/pkg-install.ts +4 -4
- package/src/commands/pkg-update.ts +3 -3
- package/src/commands/publish.ts +5 -1
- package/src/commands/workers.ts +9 -6
- package/src/utils/manifest.test.ts +19 -0
- package/src/utils/manifest.ts +3 -2
- package/src/utils/people.ts +2 -9
- package/src/utils/registry-client.ts +2 -2
- package/src/utils/registry.ts +2 -1
- package/src/utils/user-yaml-error.test.ts +24 -0
- package/src/utils/user-yaml-error.ts +30 -0
package/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import * as fs from "node:fs";
|
|
31
31
|
import * as path from "node:path";
|
|
32
32
|
import * as yaml from "js-yaml";
|
|
33
|
+
import { parseUserYaml } from "../utils/user-yaml-error.js";
|
|
33
34
|
import chalk from "chalk";
|
|
34
35
|
import { ProvisionError, companyConfigPath, companyDirPath, createDefaultVaultClient, manifestPath, validateManifestAndDir, validateSlug, } from "./cloud-provision.js";
|
|
35
36
|
import { DEFAULT_HQ_ROOT, DEFAULT_VAULT_API_URL, ensureCognitoToken, } from "../utils/cognito-session.js";
|
|
@@ -49,7 +50,7 @@ export function flipCompanyYamlCloudOff(hqRoot, slug) {
|
|
|
49
50
|
if (!fs.existsSync(yPath))
|
|
50
51
|
return false;
|
|
51
52
|
const raw = fs.readFileSync(yPath, "utf-8");
|
|
52
|
-
const parsed =
|
|
53
|
+
const parsed = parseUserYaml(raw, yPath) ?? {};
|
|
53
54
|
if (parsed.cloud === false)
|
|
54
55
|
return false;
|
|
55
56
|
parsed.cloud = false;
|
|
@@ -72,7 +73,7 @@ export function stripManifestCloudForSlug(hqRoot, slug) {
|
|
|
72
73
|
if (!fs.existsSync(mPath))
|
|
73
74
|
return false;
|
|
74
75
|
const raw = fs.readFileSync(mPath, "utf-8");
|
|
75
|
-
const parsed =
|
|
76
|
+
const parsed = parseUserYaml(raw, mPath) ?? {};
|
|
76
77
|
const companies = parsed.companies;
|
|
77
78
|
if (!companies || !(slug in companies))
|
|
78
79
|
return false;
|
|
@@ -31,6 +31,7 @@ import chalk from "chalk";
|
|
|
31
31
|
import * as fs from "node:fs";
|
|
32
32
|
import * as path from "node:path";
|
|
33
33
|
import * as yaml from "js-yaml";
|
|
34
|
+
import { parseUserYaml } from "../utils/user-yaml-error.js";
|
|
34
35
|
import { share } from "@indigoai-us/hq-cloud";
|
|
35
36
|
import { DEFAULT_HQ_ROOT, DEFAULT_VAULT_API_URL, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
|
|
36
37
|
/** Custom error class so the CLI runner can map to exit codes. */
|
|
@@ -105,7 +106,7 @@ export function validateManifestAndDir(hqRoot, slug) {
|
|
|
105
106
|
throw new ProvisionError(2, `companies/manifest.yaml not found at ${mPath}`);
|
|
106
107
|
}
|
|
107
108
|
const raw = fs.readFileSync(mPath, "utf-8");
|
|
108
|
-
const parsed =
|
|
109
|
+
const parsed = parseUserYaml(raw, mPath);
|
|
109
110
|
if (!parsed ||
|
|
110
111
|
typeof parsed !== "object" ||
|
|
111
112
|
!("companies" in parsed) ||
|
|
@@ -225,7 +226,7 @@ export function ensureManifestEntryForProvision(hqRoot, slug) {
|
|
|
225
226
|
export function patchManifest(hqRoot, slug, cloudUid, bucketName) {
|
|
226
227
|
const mPath = manifestPath(hqRoot);
|
|
227
228
|
const raw = fs.readFileSync(mPath, "utf-8");
|
|
228
|
-
const parsed =
|
|
229
|
+
const parsed = parseUserYaml(raw, mPath) ?? { companies: {} };
|
|
229
230
|
if (!parsed.companies)
|
|
230
231
|
parsed.companies = {};
|
|
231
232
|
const existing = parsed.companies[slug];
|
package/dist/commands/files.js
CHANGED
|
@@ -381,7 +381,7 @@ export function registerFilesCommand(program) {
|
|
|
381
381
|
});
|
|
382
382
|
files
|
|
383
383
|
.command("versions <path>")
|
|
384
|
-
.description("List prior content versions and delete markers for one exact vault key. Use --personal for your personal vault.")
|
|
384
|
+
.description("List prior content versions and delete markers for one exact vault key. Restore a listed version with restore --version-id <id>. Use --personal for your personal vault.")
|
|
385
385
|
.option("--personal", "Target your own personal vault instead of a company vault (mutually exclusive with --company)")
|
|
386
386
|
.action(async (path, opts) => {
|
|
387
387
|
try {
|
|
@@ -397,9 +397,9 @@ export function registerFilesCommand(program) {
|
|
|
397
397
|
});
|
|
398
398
|
files
|
|
399
399
|
.command("restore <path>")
|
|
400
|
-
.description("Restore a prior version or undelete an exact vault key. Prompts before overwriting unless --yes. Use --personal for your personal vault.")
|
|
400
|
+
.description("Restore a prior version with --version-id or undelete an exact vault key. Prompts before overwriting unless --yes. Use --personal for your personal vault.")
|
|
401
401
|
.option("--personal", "Target your own personal vault instead of a company vault (mutually exclusive with --company)")
|
|
402
|
-
.option("--version <id>", "Content version ID to restore")
|
|
402
|
+
.option("--version-id <id>", "Content version ID to restore")
|
|
403
403
|
.option("-y, --yes", "Skip the overwrite confirmation prompt (for scripts)")
|
|
404
404
|
.action(async (path, opts) => {
|
|
405
405
|
try {
|
|
@@ -408,7 +408,7 @@ export function registerFilesCommand(program) {
|
|
|
408
408
|
assertRecoveryScope(personal, companySlug);
|
|
409
409
|
await runFilesRestore({
|
|
410
410
|
key: path,
|
|
411
|
-
versionId: opts.
|
|
411
|
+
versionId: opts.versionId,
|
|
412
412
|
yes: opts.yes === true,
|
|
413
413
|
personal,
|
|
414
414
|
companySlug,
|
|
@@ -37,7 +37,7 @@ import * as fs from 'fs';
|
|
|
37
37
|
import * as os from 'os';
|
|
38
38
|
import * as path from 'path';
|
|
39
39
|
import * as readline from 'readline';
|
|
40
|
-
import
|
|
40
|
+
import { parseUserYaml } from '../utils/user-yaml-error.js';
|
|
41
41
|
import { createHash, createPublicKey, verify as cryptoVerify, } from 'node:crypto';
|
|
42
42
|
import { execFileSync, spawnSync } from 'child_process';
|
|
43
43
|
import chalk from 'chalk';
|
|
@@ -940,9 +940,12 @@ export function validateManifest(payloadDir, hqVersion) {
|
|
|
940
940
|
}
|
|
941
941
|
let parsed;
|
|
942
942
|
try {
|
|
943
|
-
parsed =
|
|
943
|
+
parsed = parseUserYaml(fs.readFileSync(manifestPath, 'utf-8'), manifestPath);
|
|
944
944
|
}
|
|
945
945
|
catch (e) {
|
|
946
|
+
if (e instanceof Error && e.expected === true) {
|
|
947
|
+
throw e;
|
|
948
|
+
}
|
|
946
949
|
throw new Error(`package.yaml invalid YAML: ${e.message}`);
|
|
947
950
|
}
|
|
948
951
|
const m = parsed;
|
package/dist/commands/people.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import * as fs from "fs";
|
|
15
15
|
import { Option } from "commander";
|
|
16
16
|
import chalk from "chalk";
|
|
17
|
-
import
|
|
17
|
+
import { parseUserYaml } from "../utils/user-yaml-error.js";
|
|
18
18
|
import { findHqRoot } from "../utils/manifest.js";
|
|
19
19
|
import { manifestPath } from "./cloud-provision.js";
|
|
20
20
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
@@ -96,13 +96,7 @@ export function resolveCompanySlug(hqRoot, explicit) {
|
|
|
96
96
|
throw new Error("Could not determine the active company — companies/manifest.yaml not found. " +
|
|
97
97
|
"Re-run with --company <slug>.");
|
|
98
98
|
}
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
manifest = yaml.load(fs.readFileSync(mPath, "utf-8"));
|
|
102
|
-
}
|
|
103
|
-
catch (err) {
|
|
104
|
-
throw new Error(`companies/manifest.yaml is malformed: ${err instanceof Error ? err.message : String(err)}`);
|
|
105
|
-
}
|
|
99
|
+
const manifest = parseUserYaml(fs.readFileSync(mPath, "utf-8"), mPath);
|
|
106
100
|
const slugs = activeCompanySlugs(manifest ?? {});
|
|
107
101
|
if (slugs.length === 1)
|
|
108
102
|
return slugs[0];
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import * as fs from 'fs';
|
|
16
16
|
import * as os from 'os';
|
|
17
17
|
import * as path from 'path';
|
|
18
|
-
import
|
|
18
|
+
import { parseUserYaml } from '../utils/user-yaml-error.js';
|
|
19
19
|
import { execSync } from 'child_process';
|
|
20
20
|
import chalk from 'chalk';
|
|
21
21
|
import { ensureCognitoToken } from '../utils/cognito-session.js';
|
|
@@ -117,7 +117,7 @@ async function installPackage(slug, company) {
|
|
|
117
117
|
const packageYamlPath = path.resolve(installDir, 'package.yaml');
|
|
118
118
|
if (fs.existsSync(packageYamlPath)) {
|
|
119
119
|
const pkgContent = fs.readFileSync(packageYamlPath, 'utf-8');
|
|
120
|
-
const pkgMeta =
|
|
120
|
+
const pkgMeta = parseUserYaml(pkgContent, packageYamlPath);
|
|
121
121
|
if (pkgMeta?.slug && pkgMeta.slug !== slug) {
|
|
122
122
|
// Mismatch — clean up and abort
|
|
123
123
|
fs.rmSync(installDir, { recursive: true, force: true });
|
|
@@ -137,7 +137,7 @@ async function installPackage(slug, company) {
|
|
|
137
137
|
const packageYaml = path.resolve(installDir, 'package.yaml');
|
|
138
138
|
if (fs.existsSync(packageYaml)) {
|
|
139
139
|
const content = fs.readFileSync(packageYaml, 'utf-8');
|
|
140
|
-
const meta =
|
|
140
|
+
const meta = parseUserYaml(content, packageYaml);
|
|
141
141
|
if (meta?.version)
|
|
142
142
|
version = meta.version;
|
|
143
143
|
if (meta?.name)
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as os from 'os';
|
|
9
9
|
import * as path from 'path';
|
|
10
|
-
import
|
|
10
|
+
import { parseUserYaml } from '../utils/user-yaml-error.js';
|
|
11
11
|
import { execSync } from 'child_process';
|
|
12
12
|
import chalk from 'chalk';
|
|
13
13
|
import { ensureCognitoToken } from '../utils/cognito-session.js';
|
|
@@ -92,7 +92,7 @@ async function updatePackages(slug) {
|
|
|
92
92
|
const packageYamlPath = path.resolve(installDir, 'package.yaml');
|
|
93
93
|
if (fs.existsSync(packageYamlPath)) {
|
|
94
94
|
const pkgContent = fs.readFileSync(packageYamlPath, 'utf-8');
|
|
95
|
-
const pkgMeta =
|
|
95
|
+
const pkgMeta = parseUserYaml(pkgContent, packageYamlPath);
|
|
96
96
|
if (pkgMeta?.slug && pkgMeta.slug !== entry.slug) {
|
|
97
97
|
fs.rmSync(installDir, { recursive: true, force: true });
|
|
98
98
|
throw new Error(`Package slug mismatch: expected "${entry.slug}", got "${pkgMeta.slug}"`);
|
package/dist/commands/publish.js
CHANGED
|
@@ -31,6 +31,7 @@ import * as fs from 'fs';
|
|
|
31
31
|
import * as os from 'os';
|
|
32
32
|
import * as path from 'path';
|
|
33
33
|
import * as yaml from 'js-yaml';
|
|
34
|
+
import { parseUserYaml } from '../utils/user-yaml-error.js';
|
|
34
35
|
import { execFileSync } from 'child_process';
|
|
35
36
|
import chalk from 'chalk';
|
|
36
37
|
import { loadCachedTokens, isExpiring } from '@indigoai-us/hq-cloud';
|
|
@@ -91,7 +92,7 @@ export function stampAuthorYaml(manifest, author) {
|
|
|
91
92
|
/** Read package.yaml, stamp author, write it back. Returns the stamped author. */
|
|
92
93
|
export function stampAuthorIntoPackage(payloadDir, author) {
|
|
93
94
|
const manifestPath = path.join(payloadDir, 'package.yaml');
|
|
94
|
-
const parsed =
|
|
95
|
+
const parsed = parseUserYaml(fs.readFileSync(manifestPath, 'utf-8'), manifestPath);
|
|
95
96
|
fs.writeFileSync(manifestPath, stampAuthorYaml(parsed, author));
|
|
96
97
|
}
|
|
97
98
|
// ---------------------------------------------------------------------------
|
package/dist/commands/workers.js
CHANGED
|
@@ -2,6 +2,7 @@ import chalk from "chalk";
|
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import * as yaml from "js-yaml";
|
|
5
|
+
import { parseUserYaml } from "../utils/user-yaml-error.js";
|
|
5
6
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
6
7
|
import { vaultApiFetch, getCompanyUid } from "./secrets.js";
|
|
7
8
|
import { GROUP_ID_PATTERN, EMAIL_PATTERN, normalizeFilePrefix } from "./_patterns.js";
|
|
@@ -11,7 +12,7 @@ export function readWorkerRegistry(hqRoot) {
|
|
|
11
12
|
const p = path.join(hqRoot, "core/workers/registry.yaml");
|
|
12
13
|
if (!fs.existsSync(p))
|
|
13
14
|
return [];
|
|
14
|
-
const doc =
|
|
15
|
+
const doc = parseUserYaml(fs.readFileSync(p, "utf8"), p);
|
|
15
16
|
return doc?.workers ?? [];
|
|
16
17
|
}
|
|
17
18
|
/**
|
|
@@ -87,7 +88,7 @@ export function writeGrantSidecar(hqRoot, workerPath, principalLabel) {
|
|
|
87
88
|
const sidecar = path.join(dir, ".grants.yaml");
|
|
88
89
|
let grants = [];
|
|
89
90
|
if (fs.existsSync(sidecar)) {
|
|
90
|
-
const doc =
|
|
91
|
+
const doc = parseUserYaml(fs.readFileSync(sidecar, "utf8"), sidecar);
|
|
91
92
|
if (Array.isArray(doc?.grants))
|
|
92
93
|
grants = doc.grants.filter((g) => typeof g === "string");
|
|
93
94
|
}
|
package/dist/utils/manifest.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import * as yaml from 'js-yaml';
|
|
4
|
+
import { parseUserYaml } from './user-yaml-error.js';
|
|
4
5
|
const MANIFEST_FILE = 'modules.yaml';
|
|
5
6
|
const LOCK_FILE = 'modules.lock';
|
|
6
7
|
const STATE_FILE = '.hq-sync-state.json';
|
|
@@ -51,7 +52,7 @@ export function readManifest(hqRoot) {
|
|
|
51
52
|
return null;
|
|
52
53
|
}
|
|
53
54
|
const content = fs.readFileSync(manifestPath, 'utf-8');
|
|
54
|
-
return
|
|
55
|
+
return parseUserYaml(content, manifestPath);
|
|
55
56
|
}
|
|
56
57
|
export function writeManifest(hqRoot, manifest) {
|
|
57
58
|
const manifestPath = getManifestPath(hqRoot);
|
|
@@ -68,7 +69,7 @@ export function readLock(hqRoot) {
|
|
|
68
69
|
return null;
|
|
69
70
|
}
|
|
70
71
|
const content = fs.readFileSync(lockPath, 'utf-8');
|
|
71
|
-
return
|
|
72
|
+
return parseUserYaml(content, lockPath);
|
|
72
73
|
}
|
|
73
74
|
export function writeLock(hqRoot, lock) {
|
|
74
75
|
const lockPath = getLockPath(hqRoot);
|
package/dist/utils/people.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import * as fs from "fs";
|
|
18
18
|
import * as path from "path";
|
|
19
|
-
import
|
|
19
|
+
import { parseUserYaml } from "./user-yaml-error.js";
|
|
20
20
|
/**
|
|
21
21
|
* Company slugs map directly onto a filesystem path segment, so we validate
|
|
22
22
|
* them before joining to keep a malicious or fat-fingered `--company` value
|
|
@@ -45,13 +45,7 @@ export function companyPeopleDir(hqRoot, companySlug) {
|
|
|
45
45
|
* surfaced as a half-row. Exported for unit testing.
|
|
46
46
|
*/
|
|
47
47
|
export function parsePersonMeta(raw, slug, source) {
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
doc = yaml.load(raw);
|
|
51
|
-
}
|
|
52
|
-
catch (err) {
|
|
53
|
-
throw new Error(`Failed to parse ${source}: ${err instanceof Error ? err.message : String(err)}`);
|
|
54
|
-
}
|
|
48
|
+
const doc = parseUserYaml(raw, source);
|
|
55
49
|
if (!doc || typeof doc !== "object")
|
|
56
50
|
return null;
|
|
57
51
|
const d = doc;
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as path from 'path';
|
|
9
|
-
import * as yaml from 'js-yaml';
|
|
10
9
|
import { resolveDefaultHqRoot } from './cognito-session.js';
|
|
10
|
+
import { parseUserYaml } from './user-yaml-error.js';
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
12
12
|
// URL helper (unchanged from US-004)
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
@@ -23,7 +23,7 @@ export function getRegistryUrl() {
|
|
|
23
23
|
throw new Error(`No packages/sources.yaml found at ${sourcesPath}. Is your HQ packages directory set up?`);
|
|
24
24
|
}
|
|
25
25
|
const content = fs.readFileSync(sourcesPath, 'utf-8');
|
|
26
|
-
const parsed =
|
|
26
|
+
const parsed = parseUserYaml(content, sourcesPath);
|
|
27
27
|
if (!parsed?.sources?.length) {
|
|
28
28
|
throw new Error('No sources defined in packages/sources.yaml');
|
|
29
29
|
}
|
package/dist/utils/registry.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import * as fs from 'fs';
|
|
5
5
|
import * as path from 'path';
|
|
6
6
|
import * as yaml from 'js-yaml';
|
|
7
|
+
import { parseUserYaml } from './user-yaml-error.js';
|
|
7
8
|
function registryPath(hqRoot) {
|
|
8
9
|
return path.resolve(hqRoot, 'packages', 'registry.yaml');
|
|
9
10
|
}
|
|
@@ -17,7 +18,7 @@ export function readRegistry(hqRoot) {
|
|
|
17
18
|
return [];
|
|
18
19
|
}
|
|
19
20
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
20
|
-
const parsed =
|
|
21
|
+
const parsed = parseUserYaml(content, filePath);
|
|
21
22
|
return parsed?.packages ?? [];
|
|
22
23
|
}
|
|
23
24
|
/**
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse YAML supplied from an on-disk HQ file.
|
|
3
|
+
*
|
|
4
|
+
* YAML syntax is user-correctable content, not an hq-cli defect. js-yaml's
|
|
5
|
+
* default error includes a source excerpt, which can inadvertently echo
|
|
6
|
+
* secrets, so replace it with a concise, actionable location instead.
|
|
7
|
+
*/
|
|
8
|
+
export declare function parseUserYaml<T>(content: string, filePath: string): T;
|
|
9
|
+
//# sourceMappingURL=user-yaml-error.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as yaml from "js-yaml";
|
|
2
|
+
/**
|
|
3
|
+
* Parse YAML supplied from an on-disk HQ file.
|
|
4
|
+
*
|
|
5
|
+
* YAML syntax is user-correctable content, not an hq-cli defect. js-yaml's
|
|
6
|
+
* default error includes a source excerpt, which can inadvertently echo
|
|
7
|
+
* secrets, so replace it with a concise, actionable location instead.
|
|
8
|
+
*/
|
|
9
|
+
export function parseUserYaml(content, filePath) {
|
|
10
|
+
try {
|
|
11
|
+
return yaml.load(content, { filename: filePath });
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
if (!(err instanceof yaml.YAMLException))
|
|
15
|
+
throw err;
|
|
16
|
+
const line = err.mark ? err.mark.line + 1 : undefined;
|
|
17
|
+
const column = err.mark ? err.mark.column + 1 : undefined;
|
|
18
|
+
const location = line !== undefined && column !== undefined
|
|
19
|
+
? `:${line}:${column}`
|
|
20
|
+
: "";
|
|
21
|
+
throw Object.assign(new Error(`Invalid YAML in ${filePath}${location}: ${err.reason}. ` +
|
|
22
|
+
"Fix the indentation or syntax and try again."), { expected: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=user-yaml-error.js.map
|
package/package.json
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
import * as fs from "node:fs";
|
|
32
32
|
import * as path from "node:path";
|
|
33
33
|
import * as yaml from "js-yaml";
|
|
34
|
+
import { parseUserYaml } from "../utils/user-yaml-error.js";
|
|
34
35
|
import { Command } from "commander";
|
|
35
36
|
import chalk from "chalk";
|
|
36
37
|
|
|
@@ -98,7 +99,7 @@ export function flipCompanyYamlCloudOff(hqRoot: string, slug: string): boolean {
|
|
|
98
99
|
const yPath = path.join(companyDirPath(hqRoot, slug), "company.yaml");
|
|
99
100
|
if (!fs.existsSync(yPath)) return false;
|
|
100
101
|
const raw = fs.readFileSync(yPath, "utf-8");
|
|
101
|
-
const parsed =
|
|
102
|
+
const parsed = parseUserYaml<Record<string, unknown> | null>(raw, yPath) ?? {};
|
|
102
103
|
if (parsed.cloud === false) return false;
|
|
103
104
|
parsed.cloud = false;
|
|
104
105
|
const dump = yaml.dump(parsed, { lineWidth: -1, noRefs: true });
|
|
@@ -120,7 +121,7 @@ export function stripManifestCloudForSlug(hqRoot: string, slug: string): boolean
|
|
|
120
121
|
const mPath = manifestPath(hqRoot);
|
|
121
122
|
if (!fs.existsSync(mPath)) return false;
|
|
122
123
|
const raw = fs.readFileSync(mPath, "utf-8");
|
|
123
|
-
const parsed =
|
|
124
|
+
const parsed = parseUserYaml<{ companies?: Record<string, unknown> } | null>(raw, mPath) ?? {};
|
|
124
125
|
const companies = parsed.companies;
|
|
125
126
|
if (!companies || !(slug in companies)) return false;
|
|
126
127
|
const entry = companies[slug];
|
|
@@ -33,6 +33,7 @@ import chalk from "chalk";
|
|
|
33
33
|
import * as fs from "node:fs";
|
|
34
34
|
import * as path from "node:path";
|
|
35
35
|
import * as yaml from "js-yaml";
|
|
36
|
+
import { parseUserYaml } from "../utils/user-yaml-error.js";
|
|
36
37
|
|
|
37
38
|
import { share } from "@indigoai-us/hq-cloud";
|
|
38
39
|
|
|
@@ -255,7 +256,7 @@ export function validateManifestAndDir(
|
|
|
255
256
|
);
|
|
256
257
|
}
|
|
257
258
|
const raw = fs.readFileSync(mPath, "utf-8");
|
|
258
|
-
const parsed =
|
|
259
|
+
const parsed = parseUserYaml<unknown>(raw, mPath);
|
|
259
260
|
if (
|
|
260
261
|
!parsed ||
|
|
261
262
|
typeof parsed !== "object" ||
|
|
@@ -411,7 +412,7 @@ export function patchManifest(
|
|
|
411
412
|
): boolean {
|
|
412
413
|
const mPath = manifestPath(hqRoot);
|
|
413
414
|
const raw = fs.readFileSync(mPath, "utf-8");
|
|
414
|
-
const parsed = (
|
|
415
|
+
const parsed = parseUserYaml<ManifestDoc | null>(raw, mPath) ?? { companies: {} };
|
|
415
416
|
if (!parsed.companies) parsed.companies = {};
|
|
416
417
|
const existing = parsed.companies[slug];
|
|
417
418
|
// Preserve null / object / unknown — promote null → {} so we can write keys.
|
|
@@ -308,18 +308,20 @@ describe("hq files trash", () => {
|
|
|
308
308
|
});
|
|
309
309
|
|
|
310
310
|
describe("hq files recovery command help", () => {
|
|
311
|
-
it("documents the finalized --version and --prefix option forms", () => {
|
|
311
|
+
it("documents the finalized --version-id and --prefix option forms", () => {
|
|
312
312
|
const program = buildProgram();
|
|
313
313
|
const files = program.commands.find((command) => command.name() === "files");
|
|
314
314
|
const restore = files?.commands.find((command) => command.name() === "restore");
|
|
315
315
|
const trash = files?.commands.find((command) => command.name() === "trash");
|
|
316
316
|
|
|
317
317
|
expect(files?.helpInformation()).toContain("versions [options] <path>");
|
|
318
|
-
expect(
|
|
318
|
+
expect(files?.helpInformation()).toContain("restore --version-id <id>");
|
|
319
|
+
expect(restore?.helpInformation()).toContain("--version-id <id>");
|
|
320
|
+
expect(restore?.helpInformation()).not.toContain("--version <id>");
|
|
319
321
|
expect(trash?.helpInformation()).toContain("--prefix <prefix>");
|
|
320
322
|
});
|
|
321
323
|
|
|
322
|
-
it("passes --version to the restore request", async () => {
|
|
324
|
+
it("passes --version-id to the restore request", async () => {
|
|
323
325
|
fetchSpy.mockResolvedValueOnce(membershipResponse());
|
|
324
326
|
fetchSpy.mockResolvedValueOnce(
|
|
325
327
|
jsonResponse(200, {
|
|
@@ -331,7 +333,14 @@ describe("hq files recovery command help", () => {
|
|
|
331
333
|
);
|
|
332
334
|
|
|
333
335
|
await buildProgram().parseAsync(
|
|
334
|
-
[
|
|
336
|
+
[
|
|
337
|
+
"files",
|
|
338
|
+
"restore",
|
|
339
|
+
"notes/a.md",
|
|
340
|
+
"--version-id",
|
|
341
|
+
"old-version",
|
|
342
|
+
"--yes",
|
|
343
|
+
],
|
|
335
344
|
{ from: "user" },
|
|
336
345
|
);
|
|
337
346
|
|
package/src/commands/files.ts
CHANGED
|
@@ -515,7 +515,7 @@ export function registerFilesCommand(program: Command): Command {
|
|
|
515
515
|
files
|
|
516
516
|
.command("versions <path>")
|
|
517
517
|
.description(
|
|
518
|
-
"List prior content versions and delete markers for one exact vault key. Use --personal for your personal vault.",
|
|
518
|
+
"List prior content versions and delete markers for one exact vault key. Restore a listed version with restore --version-id <id>. Use --personal for your personal vault.",
|
|
519
519
|
)
|
|
520
520
|
.option(
|
|
521
521
|
"--personal",
|
|
@@ -539,18 +539,18 @@ export function registerFilesCommand(program: Command): Command {
|
|
|
539
539
|
files
|
|
540
540
|
.command("restore <path>")
|
|
541
541
|
.description(
|
|
542
|
-
"Restore a prior version or undelete an exact vault key. Prompts before overwriting unless --yes. Use --personal for your personal vault.",
|
|
542
|
+
"Restore a prior version with --version-id or undelete an exact vault key. Prompts before overwriting unless --yes. Use --personal for your personal vault.",
|
|
543
543
|
)
|
|
544
544
|
.option(
|
|
545
545
|
"--personal",
|
|
546
546
|
"Target your own personal vault instead of a company vault (mutually exclusive with --company)",
|
|
547
547
|
)
|
|
548
|
-
.option("--version <id>", "Content version ID to restore")
|
|
548
|
+
.option("--version-id <id>", "Content version ID to restore")
|
|
549
549
|
.option("-y, --yes", "Skip the overwrite confirmation prompt (for scripts)")
|
|
550
550
|
.action(
|
|
551
551
|
async (
|
|
552
552
|
path: string,
|
|
553
|
-
opts: { personal?: boolean; yes?: boolean;
|
|
553
|
+
opts: { personal?: boolean; yes?: boolean; versionId?: string },
|
|
554
554
|
) => {
|
|
555
555
|
try {
|
|
556
556
|
const companySlug = files.opts().company as string | undefined;
|
|
@@ -558,7 +558,7 @@ export function registerFilesCommand(program: Command): Command {
|
|
|
558
558
|
assertRecoveryScope(personal, companySlug);
|
|
559
559
|
await runFilesRestore({
|
|
560
560
|
key: path,
|
|
561
|
-
versionId: opts.
|
|
561
|
+
versionId: opts.versionId,
|
|
562
562
|
yes: opts.yes === true,
|
|
563
563
|
personal,
|
|
564
564
|
companySlug,
|
|
@@ -38,7 +38,7 @@ import * as fs from 'fs';
|
|
|
38
38
|
import * as os from 'os';
|
|
39
39
|
import * as path from 'path';
|
|
40
40
|
import * as readline from 'readline';
|
|
41
|
-
import
|
|
41
|
+
import { parseUserYaml } from '../utils/user-yaml-error.js';
|
|
42
42
|
import {
|
|
43
43
|
createHash,
|
|
44
44
|
createPublicKey,
|
|
@@ -1261,8 +1261,11 @@ export function validateManifest(
|
|
|
1261
1261
|
}
|
|
1262
1262
|
let parsed: unknown;
|
|
1263
1263
|
try {
|
|
1264
|
-
parsed =
|
|
1264
|
+
parsed = parseUserYaml(fs.readFileSync(manifestPath, 'utf-8'), manifestPath);
|
|
1265
1265
|
} catch (e) {
|
|
1266
|
+
if (e instanceof Error && (e as { expected?: unknown }).expected === true) {
|
|
1267
|
+
throw e;
|
|
1268
|
+
}
|
|
1266
1269
|
throw new Error(`package.yaml invalid YAML: ${(e as Error).message}`);
|
|
1267
1270
|
}
|
|
1268
1271
|
const m = parsed as Partial<PackManifest> & Record<string, unknown>;
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
resolveNameToEmail,
|
|
20
20
|
type PersonRecord,
|
|
21
21
|
} from "../utils/people.js";
|
|
22
|
+
import { isExpectedUserError } from "../utils/expected-cli-error.js";
|
|
22
23
|
import {
|
|
23
24
|
activeMemberToPersonRecord,
|
|
24
25
|
mergePeople,
|
|
@@ -138,9 +139,17 @@ describe("parsePersonMeta", () => {
|
|
|
138
139
|
});
|
|
139
140
|
|
|
140
141
|
it("throws on unparseable yaml", () => {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
const error = (() => {
|
|
143
|
+
try {
|
|
144
|
+
parsePersonMeta("name: [unterminated\n", "x", "/x");
|
|
145
|
+
} catch (err) {
|
|
146
|
+
return err;
|
|
147
|
+
}
|
|
148
|
+
throw new Error("expected malformed YAML to throw");
|
|
149
|
+
})();
|
|
150
|
+
|
|
151
|
+
expect(isExpectedUserError(error)).toBe(true);
|
|
152
|
+
expect((error as Error).message).toContain("Invalid YAML in /x:2:1");
|
|
144
153
|
});
|
|
145
154
|
});
|
|
146
155
|
|
package/src/commands/people.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import * as fs from "fs";
|
|
16
16
|
import { Command, Option } from "commander";
|
|
17
17
|
import chalk from "chalk";
|
|
18
|
-
import
|
|
18
|
+
import { parseUserYaml } from "../utils/user-yaml-error.js";
|
|
19
19
|
import { findHqRoot } from "../utils/manifest.js";
|
|
20
20
|
import { manifestPath, type ManifestDoc } from "./cloud-provision.js";
|
|
21
21
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
@@ -146,14 +146,10 @@ export function resolveCompanySlug(
|
|
|
146
146
|
"Re-run with --company <slug>.",
|
|
147
147
|
);
|
|
148
148
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
throw new Error(
|
|
154
|
-
`companies/manifest.yaml is malformed: ${err instanceof Error ? err.message : String(err)}`,
|
|
155
|
-
);
|
|
156
|
-
}
|
|
149
|
+
const manifest = parseUserYaml<ManifestDoc>(
|
|
150
|
+
fs.readFileSync(mPath, "utf-8"),
|
|
151
|
+
mPath,
|
|
152
|
+
);
|
|
157
153
|
const slugs = activeCompanySlugs(manifest ?? {});
|
|
158
154
|
if (slugs.length === 1) return slugs[0];
|
|
159
155
|
if (slugs.length === 0) {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import * as fs from 'fs';
|
|
17
17
|
import * as os from 'os';
|
|
18
18
|
import * as path from 'path';
|
|
19
|
-
import
|
|
19
|
+
import { parseUserYaml } from '../utils/user-yaml-error.js';
|
|
20
20
|
import { execSync } from 'child_process';
|
|
21
21
|
import { Command } from 'commander';
|
|
22
22
|
import chalk from 'chalk';
|
|
@@ -157,7 +157,7 @@ async function installPackage(
|
|
|
157
157
|
const packageYamlPath = path.resolve(installDir, 'package.yaml');
|
|
158
158
|
if (fs.existsSync(packageYamlPath)) {
|
|
159
159
|
const pkgContent = fs.readFileSync(packageYamlPath, 'utf-8');
|
|
160
|
-
const pkgMeta =
|
|
160
|
+
const pkgMeta = parseUserYaml<{ slug?: string } | null>(pkgContent, packageYamlPath);
|
|
161
161
|
if (pkgMeta?.slug && pkgMeta.slug !== slug) {
|
|
162
162
|
// Mismatch — clean up and abort
|
|
163
163
|
fs.rmSync(installDir, { recursive: true, force: true });
|
|
@@ -179,10 +179,10 @@ async function installPackage(
|
|
|
179
179
|
const packageYaml = path.resolve(installDir, 'package.yaml');
|
|
180
180
|
if (fs.existsSync(packageYaml)) {
|
|
181
181
|
const content = fs.readFileSync(packageYaml, 'utf-8');
|
|
182
|
-
const meta =
|
|
182
|
+
const meta = parseUserYaml<{
|
|
183
183
|
version?: string;
|
|
184
184
|
name?: string;
|
|
185
|
-
} | null;
|
|
185
|
+
} | null>(content, packageYaml);
|
|
186
186
|
if (meta?.version) version = meta.version;
|
|
187
187
|
if (meta?.name) name = meta.name;
|
|
188
188
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import * as os from 'os';
|
|
10
10
|
import * as path from 'path';
|
|
11
|
-
import
|
|
11
|
+
import { parseUserYaml } from '../utils/user-yaml-error.js';
|
|
12
12
|
import { execSync } from 'child_process';
|
|
13
13
|
import { Command } from 'commander';
|
|
14
14
|
import chalk from 'chalk';
|
|
@@ -146,9 +146,9 @@ async function updatePackages(slug?: string): Promise<void> {
|
|
|
146
146
|
const packageYamlPath = path.resolve(installDir, 'package.yaml');
|
|
147
147
|
if (fs.existsSync(packageYamlPath)) {
|
|
148
148
|
const pkgContent = fs.readFileSync(packageYamlPath, 'utf-8');
|
|
149
|
-
const pkgMeta =
|
|
149
|
+
const pkgMeta = parseUserYaml<{
|
|
150
150
|
slug?: string;
|
|
151
|
-
} | null;
|
|
151
|
+
} | null>(pkgContent, packageYamlPath);
|
|
152
152
|
if (pkgMeta?.slug && pkgMeta.slug !== entry.slug) {
|
|
153
153
|
fs.rmSync(installDir, { recursive: true, force: true });
|
|
154
154
|
throw new Error(
|
package/src/commands/publish.ts
CHANGED
|
@@ -32,6 +32,7 @@ import * as fs from 'fs';
|
|
|
32
32
|
import * as os from 'os';
|
|
33
33
|
import * as path from 'path';
|
|
34
34
|
import * as yaml from 'js-yaml';
|
|
35
|
+
import { parseUserYaml } from '../utils/user-yaml-error.js';
|
|
35
36
|
import { execFileSync } from 'child_process';
|
|
36
37
|
import { Command } from 'commander';
|
|
37
38
|
import chalk from 'chalk';
|
|
@@ -124,7 +125,10 @@ export function stampAuthorIntoPackage(
|
|
|
124
125
|
author: ResolvedAuthor,
|
|
125
126
|
): void {
|
|
126
127
|
const manifestPath = path.join(payloadDir, 'package.yaml');
|
|
127
|
-
const parsed =
|
|
128
|
+
const parsed = parseUserYaml<Record<string, unknown>>(
|
|
129
|
+
fs.readFileSync(manifestPath, 'utf-8'),
|
|
130
|
+
manifestPath,
|
|
131
|
+
);
|
|
128
132
|
fs.writeFileSync(manifestPath, stampAuthorYaml(parsed, author));
|
|
129
133
|
}
|
|
130
134
|
|
package/src/commands/workers.ts
CHANGED
|
@@ -3,6 +3,7 @@ import chalk from "chalk";
|
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import * as yaml from "js-yaml";
|
|
6
|
+
import { parseUserYaml } from "../utils/user-yaml-error.js";
|
|
6
7
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
7
8
|
import { vaultApiFetch, getCompanyUid } from "./secrets.js";
|
|
8
9
|
import { GROUP_ID_PATTERN, EMAIL_PATTERN, normalizeFilePrefix } from "./_patterns.js";
|
|
@@ -26,9 +27,10 @@ export interface RegistryWorker {
|
|
|
26
27
|
export function readWorkerRegistry(hqRoot: string): RegistryWorker[] {
|
|
27
28
|
const p = path.join(hqRoot, "core/workers/registry.yaml");
|
|
28
29
|
if (!fs.existsSync(p)) return [];
|
|
29
|
-
const doc =
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
const doc = parseUserYaml<{ workers?: RegistryWorker[] } | null>(
|
|
31
|
+
fs.readFileSync(p, "utf8"),
|
|
32
|
+
p,
|
|
33
|
+
);
|
|
32
34
|
return doc?.workers ?? [];
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -120,9 +122,10 @@ export function writeGrantSidecar(
|
|
|
120
122
|
const sidecar = path.join(dir, ".grants.yaml");
|
|
121
123
|
let grants: string[] = [];
|
|
122
124
|
if (fs.existsSync(sidecar)) {
|
|
123
|
-
const doc =
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
const doc = parseUserYaml<{ grants?: string[] } | null>(
|
|
126
|
+
fs.readFileSync(sidecar, "utf8"),
|
|
127
|
+
sidecar,
|
|
128
|
+
);
|
|
126
129
|
if (Array.isArray(doc?.grants)) grants = doc!.grants.filter((g) => typeof g === "string");
|
|
127
130
|
}
|
|
128
131
|
if (!grants.includes(principalLabel)) grants.push(principalLabel);
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
writeManifest,
|
|
29
29
|
} from './manifest.js';
|
|
30
30
|
import type { ModulesManifest } from '../types.js';
|
|
31
|
+
import { isExpectedUserError } from './expected-cli-error.js';
|
|
31
32
|
|
|
32
33
|
let tmpRoot: string;
|
|
33
34
|
|
|
@@ -83,6 +84,24 @@ describe('getManifestPath', () => {
|
|
|
83
84
|
});
|
|
84
85
|
|
|
85
86
|
describe('readManifest / writeManifest round-trip', () => {
|
|
87
|
+
it('surfaces malformed user YAML with the manifest file and location', () => {
|
|
88
|
+
const nested = path.join(tmpRoot, 'modules', 'modules.yaml');
|
|
89
|
+
fs.mkdirSync(path.dirname(nested), { recursive: true });
|
|
90
|
+
fs.writeFileSync(nested, 'modules:\n - name: hq\n bad: indentation\n');
|
|
91
|
+
|
|
92
|
+
const error = (() => {
|
|
93
|
+
try {
|
|
94
|
+
readManifest(tmpRoot);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
return err;
|
|
97
|
+
}
|
|
98
|
+
throw new Error('expected malformed YAML to throw');
|
|
99
|
+
})();
|
|
100
|
+
|
|
101
|
+
expect(isExpectedUserError(error)).toBe(true);
|
|
102
|
+
expect((error as Error).message).toContain(`${nested}:3:2`);
|
|
103
|
+
});
|
|
104
|
+
|
|
86
105
|
it('round-trips against the nested layout', () => {
|
|
87
106
|
writeManifest(tmpRoot, sampleManifest);
|
|
88
107
|
|
package/src/utils/manifest.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import * as yaml from 'js-yaml';
|
|
4
4
|
import type { ModulesManifest, ModuleDefinition, ModuleLock, SyncState } from '../types.js';
|
|
5
|
+
import { parseUserYaml } from './user-yaml-error.js';
|
|
5
6
|
|
|
6
7
|
const MANIFEST_FILE = 'modules.yaml';
|
|
7
8
|
const LOCK_FILE = 'modules.lock';
|
|
@@ -57,7 +58,7 @@ export function readManifest(hqRoot: string): ModulesManifest | null {
|
|
|
57
58
|
return null;
|
|
58
59
|
}
|
|
59
60
|
const content = fs.readFileSync(manifestPath, 'utf-8');
|
|
60
|
-
return
|
|
61
|
+
return parseUserYaml<ModulesManifest>(content, manifestPath);
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
export function writeManifest(hqRoot: string, manifest: ModulesManifest): void {
|
|
@@ -76,7 +77,7 @@ export function readLock(hqRoot: string): ModuleLock | null {
|
|
|
76
77
|
return null;
|
|
77
78
|
}
|
|
78
79
|
const content = fs.readFileSync(lockPath, 'utf-8');
|
|
79
|
-
return
|
|
80
|
+
return parseUserYaml<ModuleLock>(content, lockPath);
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
export function writeLock(hqRoot: string, lock: ModuleLock): void {
|
package/src/utils/people.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import * as fs from "fs";
|
|
19
19
|
import * as path from "path";
|
|
20
|
-
import
|
|
20
|
+
import { parseUserYaml } from "./user-yaml-error.js";
|
|
21
21
|
|
|
22
22
|
/** A single person/member record parsed from a `people/<slug>/meta.yaml`. */
|
|
23
23
|
export interface PersonRecord {
|
|
@@ -76,14 +76,7 @@ export function parsePersonMeta(
|
|
|
76
76
|
slug: string,
|
|
77
77
|
source: string,
|
|
78
78
|
): PersonRecord | null {
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
doc = yaml.load(raw);
|
|
82
|
-
} catch (err) {
|
|
83
|
-
throw new Error(
|
|
84
|
-
`Failed to parse ${source}: ${err instanceof Error ? err.message : String(err)}`,
|
|
85
|
-
);
|
|
86
|
-
}
|
|
79
|
+
const doc = parseUserYaml<unknown>(raw, source);
|
|
87
80
|
if (!doc || typeof doc !== "object") return null;
|
|
88
81
|
const d = doc as Record<string, unknown>;
|
|
89
82
|
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import * as path from 'path';
|
|
10
|
-
import * as yaml from 'js-yaml';
|
|
11
10
|
import { resolveDefaultHqRoot } from './cognito-session.js';
|
|
11
|
+
import { parseUserYaml } from './user-yaml-error.js';
|
|
12
12
|
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
// Types
|
|
@@ -86,7 +86,7 @@ export function getRegistryUrl(): string {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
const content = fs.readFileSync(sourcesPath, 'utf-8');
|
|
89
|
-
const parsed =
|
|
89
|
+
const parsed = parseUserYaml<SourcesFile>(content, sourcesPath);
|
|
90
90
|
|
|
91
91
|
if (!parsed?.sources?.length) {
|
|
92
92
|
throw new Error('No sources defined in packages/sources.yaml');
|
package/src/utils/registry.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import * as yaml from 'js-yaml';
|
|
8
|
+
import { parseUserYaml } from './user-yaml-error.js';
|
|
8
9
|
|
|
9
10
|
export interface RegistryEntry {
|
|
10
11
|
name: string;
|
|
@@ -35,7 +36,7 @@ export function readRegistry(hqRoot: string): RegistryEntry[] {
|
|
|
35
36
|
return [];
|
|
36
37
|
}
|
|
37
38
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
38
|
-
const parsed =
|
|
39
|
+
const parsed = parseUserYaml<RegistryFile | null>(content, filePath);
|
|
39
40
|
return parsed?.packages ?? [];
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { isExpectedUserError } from "./expected-cli-error.js";
|
|
3
|
+
import { parseUserYaml } from "./user-yaml-error.js";
|
|
4
|
+
|
|
5
|
+
describe("parseUserYaml", () => {
|
|
6
|
+
it("marks malformed user YAML as expected and names its file and location", () => {
|
|
7
|
+
const filePath = "/tmp/HQ/modules/modules.yaml";
|
|
8
|
+
const error = (() => {
|
|
9
|
+
try {
|
|
10
|
+
parseUserYaml('apiKey: "secret-do-not-echo"\nmodule:\n name: hq\n bad: indentation\n', filePath);
|
|
11
|
+
} catch (err) {
|
|
12
|
+
return err;
|
|
13
|
+
}
|
|
14
|
+
throw new Error("expected malformed YAML to throw");
|
|
15
|
+
})();
|
|
16
|
+
|
|
17
|
+
expect(isExpectedUserError(error)).toBe(true);
|
|
18
|
+
expect((error as Error).message).toContain(
|
|
19
|
+
`Invalid YAML in ${filePath}:4:2: bad indentation of a mapping entry.`,
|
|
20
|
+
);
|
|
21
|
+
expect((error as Error).message).toContain("Fix the indentation or syntax and try again.");
|
|
22
|
+
expect((error as Error).message).not.toContain("secret-do-not-echo");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as yaml from "js-yaml";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse YAML supplied from an on-disk HQ file.
|
|
5
|
+
*
|
|
6
|
+
* YAML syntax is user-correctable content, not an hq-cli defect. js-yaml's
|
|
7
|
+
* default error includes a source excerpt, which can inadvertently echo
|
|
8
|
+
* secrets, so replace it with a concise, actionable location instead.
|
|
9
|
+
*/
|
|
10
|
+
export function parseUserYaml<T>(content: string, filePath: string): T {
|
|
11
|
+
try {
|
|
12
|
+
return yaml.load(content, { filename: filePath }) as T;
|
|
13
|
+
} catch (err) {
|
|
14
|
+
if (!(err instanceof yaml.YAMLException)) throw err;
|
|
15
|
+
|
|
16
|
+
const line = err.mark ? err.mark.line + 1 : undefined;
|
|
17
|
+
const column = err.mark ? err.mark.column + 1 : undefined;
|
|
18
|
+
const location = line !== undefined && column !== undefined
|
|
19
|
+
? `:${line}:${column}`
|
|
20
|
+
: "";
|
|
21
|
+
|
|
22
|
+
throw Object.assign(
|
|
23
|
+
new Error(
|
|
24
|
+
`Invalid YAML in ${filePath}${location}: ${err.reason}. ` +
|
|
25
|
+
"Fix the indentation or syntax and try again.",
|
|
26
|
+
),
|
|
27
|
+
{ expected: true as const },
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|