declapract-typescript-ehmpathy 0.47.57 → 0.47.58

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.
@@ -0,0 +1,50 @@
1
+ import fs from 'node:fs';
2
+ import os from 'os';
3
+ import path from 'node:path';
4
+
5
+ import type { KeyrackGrantAttempt } from 'rhachet/keyrack';
6
+
7
+ /**
8
+ * .what = creates a mock context with keyrack config in a temp directory
9
+ * .why = enables test of buildWorkflowSecretsBlock with different keyrack configs
10
+ */
11
+ export const withKeyrackContext = async (
12
+ input: { keys: string[] },
13
+ fn: (context: { getProjectRootDirectory: () => string }) => Promise<void>,
14
+ ): Promise<void> => {
15
+ // create temp dir with keyrack.yml
16
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-'));
17
+ const agentDir = path.join(tempDir, '.agent');
18
+ fs.mkdirSync(agentDir, { recursive: true });
19
+ fs.writeFileSync(
20
+ path.join(agentDir, 'keyrack.yml'),
21
+ `org: test\nenv.test:\n${input.keys.map((k) => ` - ${k}`).join('\n')}`,
22
+ );
23
+
24
+ // mock keyrack.get to return grant attempts
25
+ const mockGrants: KeyrackGrantAttempt[] = input.keys.map((key) => ({
26
+ status: 'granted' as const,
27
+ grant: {
28
+ slug: `test.test.${key}`,
29
+ value: 'mock-value',
30
+ mech: 'PERMANENT_VIA_REPLICA' as const,
31
+ vault: 'os.direct' as const,
32
+ },
33
+ }));
34
+
35
+ // store original module
36
+ const keyrackModule = await import('rhachet/keyrack');
37
+ const originalGet = keyrackModule.keyrack.get;
38
+
39
+ // replace with mock
40
+ (keyrackModule.keyrack as { get: typeof originalGet }).get = async () =>
41
+ mockGrants;
42
+
43
+ try {
44
+ await fn({ getProjectRootDirectory: () => tempDir });
45
+ } finally {
46
+ // restore original
47
+ (keyrackModule.keyrack as { get: typeof originalGet }).get = originalGet;
48
+ fs.rmSync(tempDir, { recursive: true, force: true });
49
+ }
50
+ };
@@ -0,0 +1,7 @@
1
+ import { FileCheckType, type FileFixFunction } from 'declapract';
2
+
3
+ export const check = FileCheckType.EXISTS;
4
+
5
+ export const fix: FileFixFunction = () => {
6
+ return { contents: null };
7
+ };
@@ -0,0 +1,7 @@
1
+ import { FileCheckType, type FileFixFunction } from 'declapract';
2
+
3
+ export const check = FileCheckType.EXISTS;
4
+
5
+ export const fix: FileFixFunction = () => {
6
+ return { contents: null };
7
+ };
@@ -201,6 +201,11 @@ jobs:
201
201
  path: ./node_modules
202
202
  key: ${{ needs.install.outputs.node-modules-cache-key }}
203
203
 
204
+ # .why = keyrack.yml can extend other manifests via symlinks (e.g., .agent/repo=ehmpathy/role=mechanic/keyrack.yml)
205
+ # prepare:rhachet creates these symlinks so keyrack.source() can hydrate extended keys
206
+ - name: prepare:rhachet
207
+ run: npm run prepare:rhachet --if-present
208
+
204
209
  - name: get aws auth, if creds supplied
205
210
  if: ${{ inputs.creds-aws-role-arn }}
206
211
  uses: aws-actions/configure-aws-credentials@v4
@@ -270,6 +275,11 @@ jobs:
270
275
  path: ./node_modules
271
276
  key: ${{ needs.install.outputs.node-modules-cache-key }}
272
277
 
278
+ # .why = keyrack.yml can extend other manifests via symlinks (e.g., .agent/repo=ehmpathy/role=mechanic/keyrack.yml)
279
+ # prepare:rhachet creates these symlinks so keyrack.source() can hydrate extended keys
280
+ - name: prepare:rhachet
281
+ run: npm run prepare:rhachet --if-present
282
+
273
283
  - name: get aws auth, if creds supplied
274
284
  if: ${{ inputs.creds-aws-role-arn }}
275
285
  uses: aws-actions/configure-aws-credentials@v4
@@ -1,24 +1,49 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
1
4
  import type { FileCheckFunction, FileFixFunction } from 'declapract';
5
+ import { keyrack, type KeyrackGrantAttempt } from 'rhachet/keyrack';
2
6
 
3
- import { readUseApikeysConfig } from '../../../../../utils/readUseApikeysConfig';
7
+ /**
8
+ * .what = gets keyrack key names for test env from target project
9
+ * .why = single source of truth for key discovery
10
+ */
11
+ const getKeyrackKeys = async (projectRootDir: string): Promise<string[]> => {
12
+ const keyrackYmlPath = join(projectRootDir, '.agent/keyrack.yml');
13
+ if (!existsSync(keyrackYmlPath)) return [];
14
+
15
+ const keys = (await keyrack.get({
16
+ for: { repo: true },
17
+ env: 'test',
18
+ })) as KeyrackGrantAttempt[];
19
+ if (!keys.length) return [];
20
+
21
+ // extract key names from slugs (format: org.env.KEY_NAME)
22
+ // .note = KeyrackGrantAttempt is a union of 4 variants (granted/absent/locked/blocked)
23
+ // per rhachet/dist/domain.objects/keyrack/KeyrackGrantAttempt.d.ts:8
24
+ // granted has slug at grant.slug; others have slug at top level
25
+ return keys.map((k) =>
26
+ (k.status === 'granted' ? k.grant.slug : k.slug).split('.').pop()!,
27
+ );
28
+ };
4
29
 
5
30
  /**
6
- * .what = builds the expected .test.yml content with apikey secrets injected
31
+ * .what = builds the expected .test.yml content with keyrack secrets injected
7
32
  * .why = single source of truth for both check and fix
8
33
  */
9
34
  export const buildExpectedContent = (input: {
10
35
  template: string;
11
- apikeys: string[];
36
+ keys: string[];
12
37
  }): string => {
13
38
  let result = input.template;
14
39
 
15
- // if no apikeys, return template as-is
16
- if (!input.apikeys.length) {
40
+ // if no keys, return template as-is
41
+ if (!input.keys.length) {
17
42
  return result;
18
43
  }
19
44
 
20
45
  // build secrets declaration block for workflow_call
21
- const secretsDeclaration = input.apikeys
46
+ const secretsDeclaration = input.keys
22
47
  .map(
23
48
  (key) =>
24
49
  ` ${key}:\n description: "api key for ${key.toLowerCase().replace(/_/g, ' ')}"\n required: false`,
@@ -26,7 +51,7 @@ export const buildExpectedContent = (input: {
26
51
  .join('\n');
27
52
 
28
53
  // build env block for test-integration job
29
- const envBlock = input.apikeys
54
+ const envBlock = input.keys
30
55
  .map((key) => ` ${key}: \${{ secrets.${key} }}`)
31
56
  .join('\n');
32
57
 
@@ -48,43 +73,41 @@ export const buildExpectedContent = (input: {
48
73
  };
49
74
 
50
75
  /**
51
- * .what = ensures .test.yml matches expected content with apikey secrets
76
+ * .what = ensures .test.yml matches expected content with keyrack secrets
52
77
  * .why = enables integration tests to access required api keys via github secrets
53
78
  */
54
79
  export const check: FileCheckFunction = async (contents, context) => {
55
- // read apikeys from project
56
- const apikeysConfig = await readUseApikeysConfig({
57
- projectRootDirectory: context.getProjectRootDirectory(),
58
- });
80
+ // get keyrack keys from project
81
+ const keys = await getKeyrackKeys(context.getProjectRootDirectory());
59
82
 
60
- // build expected content from template + apikeys
83
+ // build expected content from template + keys
61
84
  const expected = buildExpectedContent({
62
85
  template: context.declaredFileContents ?? '',
63
- apikeys: apikeysConfig?.apikeys?.required ?? [],
86
+ keys,
64
87
  });
65
88
 
66
89
  // if contents don't match expected, best practice is violated
67
90
  if (contents !== expected) {
68
- throw new Error('file does not match expected content with apikey secrets');
91
+ throw new Error(
92
+ 'file does not match expected content with keyrack secrets',
93
+ );
69
94
  }
70
95
 
71
96
  // return = file matches expected (best practice followed)
72
97
  };
73
98
 
74
99
  /**
75
- * .what = fixes .test.yml to include apikey secrets declaration and env vars
100
+ * .what = fixes .test.yml to include keyrack secrets declaration and env vars
76
101
  * .why = ensures integration tests have access to required api keys
77
102
  */
78
103
  export const fix: FileFixFunction = async (_contents, context) => {
79
- // read apikeys from project
80
- const apikeysConfig = await readUseApikeysConfig({
81
- projectRootDirectory: context.getProjectRootDirectory(),
82
- });
104
+ // get keyrack keys from project
105
+ const keys = await getKeyrackKeys(context.getProjectRootDirectory());
83
106
 
84
- // build expected content from template + apikeys
107
+ // build expected content from template + keys
85
108
  const expected = buildExpectedContent({
86
109
  template: context.declaredFileContents ?? '',
87
- apikeys: apikeysConfig?.apikeys?.required ?? [],
110
+ keys,
88
111
  });
89
112
 
90
113
  return { contents: expected };
@@ -3,6 +3,7 @@ import { join } from 'node:path';
3
3
  import util from 'node:util';
4
4
 
5
5
  import { jest } from '@jest/globals';
6
+ import { keyrack } from 'rhachet/keyrack';
6
7
 
7
8
  jest.setTimeout(90000); // we're calling downstream apis
8
9
 
@@ -35,36 +36,11 @@ if (
35
36
  );
36
37
 
37
38
  /**
38
- * .what = verify that required api keys are present; otherwise, fail fast
39
+ * .what = source credentials from keyrack for test env
39
40
  * .why =
40
- * - prevent time wasted waiting on tests to fail due to missing api keys
41
- * - prevent agents from giving up when they have access to credentials
41
+ * - auto-inject keys into process.env
42
+ * - fail fast with helpful error if keyrack locked or keys absent
42
43
  */
43
- const apikeysConfigPath = join(
44
- process.cwd(),
45
- '.agent/repo=.this/role=any/skills/use.apikeys.json',
46
- );
47
- if (existsSync(apikeysConfigPath)) {
48
- // direct import via require (json resolves automatically)
49
- const config = require(apikeysConfigPath);
50
- const requiredKeys: string[] = config?.apikeys?.required ?? [];
51
- const keysAbsent = requiredKeys.filter((key) => !process.env[key]);
52
-
53
- if (keysAbsent.length > 0) {
54
- throw new Error(`
55
- ⛈️ apikeys required to run these acceptance tests were not supplied.
56
-
57
- absent keys: ${keysAbsent.join(', ')}
58
-
59
- you have access to these keys - you just forgot to source them.
60
-
61
- run:
62
- source .agent/repo=.this/role=any/skills/use.apikeys.sh && npm run test:acceptance
63
-
64
- you must run these tests with these api keys.
65
- you're forbidden to give up.
66
- if you don't have privs to run use.apikeys.sh, ask a human for support.
67
- chances are, you already have privs to do so though.
68
- `);
69
- }
70
- }
44
+ const keyrackYmlPath = join(process.cwd(), '.agent/keyrack.yml');
45
+ if (existsSync(keyrackYmlPath))
46
+ keyrack.source({ env: 'test', owner: 'ehmpath', mode: 'strict' });
@@ -4,6 +4,7 @@ import { join } from 'node:path';
4
4
  import util from 'node:util';
5
5
 
6
6
  import { jest } from '@jest/globals';
7
+ import { keyrack } from 'rhachet/keyrack';
7
8
 
8
9
  jest.setTimeout(90000); // since we're calling downstream apis
9
10
 
@@ -77,44 +78,20 @@ if (requiresTestDb) {
77
78
  `PGPASSWORD="${testConfig.database.role.crud.password}" psql -h ${testConfig.database.tunnel.local.host} -p ${testConfig.database.tunnel.local.port} -U ${testConfig.database.role.crud.username} -d ${testConfig.database.target.database} -c "SELECT 1" > /dev/null 2>&1`,
78
79
  { timeout: 3000 },
79
80
  );
80
- } catch {
81
+ } catch (error) {
81
82
  throw new Error(
82
83
  `did you forget to \`npm run start:testdb\`? cant connect to database`,
84
+ { cause: error },
83
85
  );
84
86
  }
85
87
  }
86
88
 
87
89
  /**
88
- * .what = verify that required api keys are present; otherwise, fail fast
90
+ * .what = source credentials from keyrack for test env
89
91
  * .why =
90
- * - prevent time wasted waiting on tests to fail due to missing api keys
91
- * - prevent agents from giving up when they have access to credentials
92
+ * - auto-inject keys into process.env
93
+ * - fail fast with helpful error if keyrack locked or keys absent
92
94
  */
93
- const apikeysConfigPath = join(
94
- process.cwd(),
95
- '.agent/repo=.this/role=any/skills/use.apikeys.json',
96
- );
97
- if (existsSync(apikeysConfigPath)) {
98
- // direct import via require (json resolves automatically)
99
- const config = require(apikeysConfigPath);
100
- const requiredKeys: string[] = config?.apikeys?.required ?? [];
101
- const keysAbsent = requiredKeys.filter((key) => !process.env[key]);
102
-
103
- if (keysAbsent.length > 0) {
104
- throw new Error(`
105
- ⛈️ apikeys required to run these integration tests were not supplied.
106
-
107
- absent keys: ${keysAbsent.join(', ')}
108
-
109
- you have access to these keys - you just forgot to source them.
110
-
111
- run:
112
- source .agent/repo=.this/role=any/skills/use.apikeys.sh && npm run test:integration
113
-
114
- you must run these tests with these api keys.
115
- you're forbidden to give up.
116
- if you don't have privs to run use.apikeys.sh, ask a human for support.
117
- chances are, you already have privs to do so though.
118
- `);
119
- }
120
- }
95
+ const keyrackYmlPath = join(process.cwd(), '.agent/keyrack.yml');
96
+ if (existsSync(keyrackYmlPath))
97
+ keyrack.source({ env: 'test', owner: 'ehmpath', mode: 'strict' });
@@ -9,11 +9,10 @@
9
9
  "@swc/jest": "@declapract{check.minVersion('0.2.39')}"
10
10
  },
11
11
  "scripts": {
12
- "test:auth": "[ \"${ECHO:-}\" = 'true' ] && echo '. .agent/repo=.this/role=any/skills/use.apikeys.sh' || . .agent/repo=.this/role=any/skills/use.apikeys.sh",
13
12
  "test:unit": "set -eu && jest -c ./jest.unit.config.ts --forceExit --verbose --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([ -z \"${THOROUGH:-}\" ] && echo '--changedSince=main') $([ -n \"${RESNAP:-}\" ] && echo '--updateSnapshot')",
14
13
  "test:integration": "set -eu && jest -c ./jest.integration.config.ts --forceExit --verbose --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([ -z \"${THOROUGH:-}\" ] && echo '--changedSince=main') $([ -n \"${RESNAP:-}\" ] && echo '--updateSnapshot')",
15
14
  "test:acceptance:locally": "set -eu && npm run build && LOCALLY=true jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ -n \"${RESNAP:-}\" ] && echo '--updateSnapshot')",
16
- "test": "set -eu && eval $(ECHO=true npm run --silent test:auth) && npm run test:commits && npm run test:types && npm run test:format && npm run test:lint && npm run test:unit && npm run test:integration && npm run test:acceptance:locally",
15
+ "test": "set -eu && npm run test:commits && npm run test:types && npm run test:format && npm run test:lint && npm run test:unit && npm run test:integration && npm run test:acceptance:locally",
17
16
  "test:acceptance": "set -eu && npm run build && jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([ -n \"${RESNAP:-}\" ] && echo '--updateSnapshot')"
18
17
  }
19
18
  }
@@ -1,35 +1,46 @@
1
- import { readUseApikeysConfig } from './readUseApikeysConfig';
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ import { keyrack, type KeyrackGrantAttempt } from 'rhachet/keyrack';
2
5
 
3
6
  /**
4
- * .what = builds workflow content with apikey secrets block for .test.yml
7
+ * .what = builds workflow content with keyrack secrets block for .test.yml
5
8
  * .why = single source of truth for test.yml, publish.yml, deploy.yml check+fix
6
9
  */
7
10
  export const buildWorkflowSecretsBlock = async (
8
11
  input: { template: string },
9
12
  context: { getProjectRootDirectory: () => string },
10
13
  ): Promise<string> => {
11
- // read apikeys from project
12
- const apikeysConfig = await readUseApikeysConfig({
13
- projectRootDirectory: context.getProjectRootDirectory(),
14
- });
15
- const apikeys = apikeysConfig?.apikeys?.required ?? [];
14
+ // check if keyrack.yml exists
15
+ const keyrackYmlPath = join(
16
+ context.getProjectRootDirectory(),
17
+ '.agent/keyrack.yml',
18
+ );
19
+ if (!existsSync(keyrackYmlPath)) return input.template;
16
20
 
17
- // if no apikeys, return template as-is
18
- if (!apikeys.length) {
19
- return input.template;
20
- }
21
+ // get required keys from keyrack sdk
22
+ const keys = (await keyrack.get({
23
+ for: { repo: true },
24
+ env: 'test',
25
+ })) as KeyrackGrantAttempt[];
26
+ if (!keys.length) return input.template;
27
+
28
+ // extract key names from slugs (format: org.env.KEY_NAME)
29
+ // .note = KeyrackGrantAttempt is a union of 4 variants (granted/absent/locked/blocked)
30
+ // per rhachet/dist/domain.objects/keyrack/KeyrackGrantAttempt.d.ts:8
31
+ // granted has slug at grant.slug; others have slug at top level
32
+ const keyrackVars = keys.map((k) =>
33
+ (k.status === 'granted' ? k.grant.slug : k.slug).split('.').pop()!,
34
+ );
21
35
 
22
36
  // build secrets block
23
- const secretsBlock = apikeys
37
+ const secretsBlock = keyrackVars
24
38
  .map((key) => ` ${key}: \${{ secrets.${key} }}`)
25
39
  .join('\n');
26
40
 
27
41
  // find jobs that call .test.yml with 'with:' block and add secrets block after
28
- // handle cases with optional 'if:' before 'with:'
29
- const result = input.template.replace(
42
+ return input.template.replace(
30
43
  /(uses: \.\/\.github\/workflows\/\.test\.yml\n(?: if: [^\n]+\n)? with:\n(?: [^\n]+\n)+)/g,
31
44
  `$1 secrets:\n${secretsBlock}\n`,
32
45
  );
33
-
34
- return result;
35
46
  };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "declapract-typescript-ehmpathy",
3
3
  "author": "ehmpathy",
4
4
  "description": "declapract best practices declarations for typescript",
5
- "version": "0.47.57",
5
+ "version": "0.47.58",
6
6
  "license": "MIT",
7
7
  "main": "src/index.js",
8
8
  "repository": "ehmpathy/declapract-typescript-ehmpathy",
@@ -36,14 +36,14 @@
36
36
  "test:unit": "set -eu && jest -c ./jest.unit.config.ts --forceExit --verbose --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([ -z \"${THOROUGH:-}\" ] && echo '--changedSince=main') $([ -n \"${RESNAP:-}\" ] && echo '--updateSnapshot')",
37
37
  "test:integration": "set -eu && jest -c ./jest.integration.config.ts --forceExit --verbose --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([ -z \"${THOROUGH:-}\" ] && echo '--changedSince=main') $([ -n \"${RESNAP:-}\" ] && echo '--updateSnapshot')",
38
38
  "test:acceptance:locally": "set -eu && npm run build && LOCALLY=true jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ -n \"${RESNAP:-}\" ] && echo '--updateSnapshot')",
39
- "test": "set -eu && npm run test:commits && npm run test:types && npm run test:format && npm run test:lint && npm run test:unit && npm run test:integration && npm run test:acceptance:locally && test:validate",
39
+ "test": "set -eu && npm run test:commits && npm run test:types && npm run test:format && npm run test:lint && npm run test:unit && npm run test:integration && npm run test:acceptance:locally && npm run test:validate",
40
40
  "test:acceptance": "set -eu && npm run build && jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([ -n \"${RESNAP:-}\" ] && echo '--updateSnapshot')",
41
41
  "prepush": "npm run test && npm run build",
42
42
  "prepublish": "npm run build",
43
43
  "preversion": "npm run prepush",
44
44
  "postversion": "git push origin HEAD --tags --no-verify",
45
45
  "prepare:husky": "husky install && chmod ug+x .husky/*",
46
- "prepare:rhachet": "rhachet init --hooks --roles mechanic behaver driver reviewer librarian ergonomist architect",
46
+ "prepare:rhachet": "rhachet init --hooks --roles mechanic behaver driver reviewer librarian ergonomist architect reflector dreamer",
47
47
  "prepare": "if [ -e .git ] && [ -z \"${CI:-}\" ]; then npm run prepare:husky && npm run prepare:rhachet; fi"
48
48
  },
49
49
  "dependencies": {
@@ -75,12 +75,12 @@
75
75
  "esbuild-register": "3.6.0",
76
76
  "husky": "8.0.3",
77
77
  "jest": "30.2.0",
78
- "rhachet": "1.37.19",
78
+ "rhachet": "1.39.2",
79
79
  "rhachet-brains-anthropic": "0.4.0",
80
80
  "rhachet-brains-xai": "0.3.2",
81
- "rhachet-roles-bhrain": "0.23.6",
82
- "rhachet-roles-bhuild": "0.14.4",
83
- "rhachet-roles-ehmpathy": "1.34.3",
81
+ "rhachet-roles-bhrain": "0.23.10",
82
+ "rhachet-roles-bhuild": "0.14.6",
83
+ "rhachet-roles-ehmpathy": "1.34.11",
84
84
  "tsc-alias": "1.8.10",
85
85
  "tsx": "4.20.6",
86
86
  "type-fns": "0.8.1",
@@ -1,31 +0,0 @@
1
- import fs from 'node:fs';
2
- import os from 'os';
3
- import path from 'node:path';
4
-
5
- /**
6
- * .what = creates a mock context with apikeys config in a temp directory
7
- * .why = enables testing buildWorkflowSecretsBlock with different apikey configs
8
- */
9
- export const withApikeysContext = async (
10
- input: { apikeys: string[] },
11
- fn: (context: { getProjectRootDirectory: () => string }) => Promise<void>,
12
- ): Promise<void> => {
13
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-'));
14
- const configDir = path.join(
15
- tempDir,
16
- '.agent',
17
- 'repo=.this',
18
- 'role=any',
19
- 'skills',
20
- );
21
- fs.mkdirSync(configDir, { recursive: true });
22
- fs.writeFileSync(
23
- path.join(configDir, 'use.apikeys.json'),
24
- JSON.stringify({ apikeys: { required: input.apikeys } }, null, 2),
25
- );
26
- try {
27
- await fn({ getProjectRootDirectory: () => tempDir });
28
- } finally {
29
- fs.rmSync(tempDir, { recursive: true, force: true });
30
- }
31
- };
@@ -1,32 +0,0 @@
1
- import { FileCheckType, type FileFixFunction } from 'declapract';
2
-
3
- /**
4
- * .what = check that use.apikeys.json exists
5
- * .why = enables projects to declare which api keys are required for integration tests
6
- */
7
- export const check = FileCheckType.EXISTS;
8
-
9
- /**
10
- * .what = creates default use.apikeys.json structure if file doesn't exist
11
- * .why = ensures all projects have the config file with proper schema
12
- */
13
- export const fix: FileFixFunction = (contents) => {
14
- // if file already exists, preserve its content
15
- if (contents) {
16
- return { contents };
17
- }
18
-
19
- // create default structure
20
- return {
21
- contents:
22
- JSON.stringify(
23
- {
24
- apikeys: {
25
- required: [],
26
- },
27
- },
28
- null,
29
- 2,
30
- ) + '\n',
31
- };
32
- };
@@ -1,63 +0,0 @@
1
- #!/bin/sh
2
- ######################################################################
3
- # .what = export api keys for integration tests
4
- # .why = enables tests that require api keys to run
5
- #
6
- # usage:
7
- # . .agent/repo=.this/role=any/skills/use.apikeys.sh
8
- #
9
- # note:
10
- # - must be called with `.` or `source` to export vars to current shell
11
- # - loads from ~/.config/rhachet/apikeys.env if available
12
- # - falls back to .env.local (gitignored) in repo root
13
- ######################################################################
14
-
15
- # fail if not sourced (return only succeeds when sourced)
16
- (return 0 2>/dev/null) || {
17
- echo "error: this file must be sourced, not executed"
18
- echo "usage: . $0"
19
- exit 1
20
- }
21
-
22
- # alias source to `.` for posix compat
23
- source() { . "$@"; }
24
-
25
- # try to load from user config first
26
- if [ -f ~/.config/rhachet/apikeys.env ]; then
27
- source ~/.config/rhachet/apikeys.env
28
- echo "✓ loaded api keys from ~/.config/rhachet/apikeys.env"
29
-
30
- # fallback to local gitignored file
31
- elif [ -f .env.local ]; then
32
- source .env.local
33
- echo "✓ loaded api keys from .env.local"
34
-
35
- else
36
- echo "⚠ no api keys file found"
37
- echo ""
38
- echo "create one of:"
39
- echo " ~/.config/rhachet/apikeys.env"
40
- echo " .env.local (in repo root)"
41
- echo ""
42
- echo "with contents like:"
43
- echo " export OPENAI_API_KEY=sk-..."
44
- echo " export ANTHROPIC_API_KEY=sk-..."
45
- return 1 2>/dev/null || exit 1
46
- fi
47
-
48
- # read required keys from json config if present
49
- APIKEYS_CONFIG=".agent/repo=.this/role=any/skills/use.apikeys.json"
50
- if [ -f "$APIKEYS_CONFIG" ]; then
51
- # extract required keys via jq
52
- REQUIRED_KEYS=$(jq -r '.apikeys.required[]?' "$APIKEYS_CONFIG" 2>/dev/null)
53
-
54
- # verify each required key is set
55
- for KEY in $REQUIRED_KEYS; do
56
- VALUE=$(eval "echo \"\$$KEY\"")
57
- if [ -z "$VALUE" ]; then
58
- echo "⚠ $KEY not set (required by $APIKEYS_CONFIG)"
59
- return 1 2>/dev/null || exit 1
60
- fi
61
- echo "✓ $KEY set"
62
- done
63
- fi
@@ -1,19 +0,0 @@
1
- import { FileCheckType, type FileFixFunction } from 'declapract';
2
-
3
- import { readFileSync } from 'node:fs';
4
- import { dirname, join } from 'node:path';
5
-
6
- // check that the file contains the core structure
7
- export const check = FileCheckType.CONTAINS;
8
-
9
- /**
10
- * .what = replaces target file with expected content
11
- * .why = ensures the skill file matches the best-practice version
12
- */
13
- export const fix: FileFixFunction = () => {
14
- const expectedContent = readFileSync(
15
- join(dirname(__filename), 'use.apikeys.sh'),
16
- 'utf-8',
17
- );
18
- return { contents: expectedContent };
19
- };
@@ -1,43 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import util from 'node:util';
4
-
5
- /**
6
- * .what = configuration structure for use.apikeys.json
7
- * .why = defines which api keys are required for integration tests
8
- */
9
- export interface UseApikeysConfig {
10
- apikeys: {
11
- required: string[];
12
- };
13
- }
14
-
15
- const USE_APIKEYS_PATH = '.agent/repo=.this/role=any/skills/use.apikeys.json';
16
-
17
- /**
18
- * .what = reads and parses use.apikeys.json from target project
19
- * .why = centralizes apikey config access for all workflow practices
20
- */
21
- export const readUseApikeysConfig = async (input: {
22
- projectRootDirectory: string;
23
- }): Promise<UseApikeysConfig | null> => {
24
- // build full path to config file
25
- const configPath = path.join(input.projectRootDirectory, USE_APIKEYS_PATH);
26
-
27
- // check if file exists
28
- const exists = await util
29
- .promisify(fs.access)(configPath, fs.constants.F_OK)
30
- .then(() => true)
31
- .catch(() => false);
32
- if (!exists) return null;
33
-
34
- // read and parse the file
35
- try {
36
- const contents = await util.promisify(fs.readFile)(configPath, 'utf-8');
37
- const parsed = JSON.parse(contents) as UseApikeysConfig;
38
- return parsed;
39
- } catch {
40
- // handle malformed json gracefully
41
- return null;
42
- }
43
- };