@eide/foir-cli 0.1.22 → 0.1.23

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.
@@ -7,23 +7,25 @@ export interface StoredCredentials {
7
7
  email: string;
8
8
  name: string;
9
9
  };
10
- selectedProject?: {
11
- id: string;
12
- name: string;
13
- tenantId: string;
14
- apiKey?: string;
15
- apiKeyId?: string;
16
- };
17
10
  }
18
11
  export declare function getCredentialsDir(): string;
19
12
  export declare function getCredentialsPath(): string;
20
13
  export declare function getCredentials(): Promise<StoredCredentials | null>;
21
14
  export declare function writeCredentials(credentials: StoredCredentials): Promise<void>;
22
- export declare function updateCredentials(updates: Partial<StoredCredentials>): Promise<void>;
23
15
  export declare function deleteCredentials(): Promise<void>;
24
16
  export declare function isTokenExpired(credentials: StoredCredentials): boolean;
25
17
  export declare function getValidCredentials(refreshFn?: (refreshToken: string) => Promise<{
26
18
  accessToken: string;
27
19
  expiresAt: string;
28
20
  }>): Promise<StoredCredentials | null>;
21
+ export interface ProjectContext {
22
+ id: string;
23
+ name: string;
24
+ tenantId: string;
25
+ apiKey?: string;
26
+ apiKeyId?: string;
27
+ }
28
+ export declare function getProjectContext(): Promise<ProjectContext | null>;
29
+ export declare function writeProjectContext(project: ProjectContext): Promise<void>;
30
+ export declare function deleteProjectContext(): Promise<void>;
29
31
  //# sourceMappingURL=credentials.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../../src/auth/credentials.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE;QACJ,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,eAAe,CAAC,EAAE;QAChB,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAaD,wBAAsB,cAAc,IAAI,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAUxE;AAED,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,iBAAiB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAMf;AAED,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAClC,OAAO,CAAC,IAAI,CAAC,CAMf;AAED,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAQvD;AAED,wBAAgB,cAAc,CAAC,WAAW,EAAE,iBAAiB,GAAG,OAAO,CAItE;AAED,wBAAsB,mBAAmB,CACvC,SAAS,CAAC,EAAE,CACV,YAAY,EAAE,MAAM,KACjB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,GACvD,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAoBnC"}
1
+ {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../../src/auth/credentials.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE;QACJ,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAYD,wBAAsB,cAAc,IAAI,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAUxE;AAED,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,iBAAiB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAMf;AAED,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAQvD;AAED,wBAAgB,cAAc,CAAC,WAAW,EAAE,iBAAiB,GAAG,OAAO,CAItE;AAED,wBAAsB,mBAAmB,CACvC,SAAS,CAAC,EAAE,CACV,YAAY,EAAE,MAAM,KACjB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,GACvD,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAoBnC;AAMD,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AA4CD,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAUxE;AAED,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,IAAI,CAAC,CAOf;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAQ1D"}
@@ -1,5 +1,5 @@
1
- import { promises as fs } from 'fs';
2
- import { join } from 'path';
1
+ import { promises as fs, statSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
3
  import { homedir } from 'os';
4
4
  export function getCredentialsDir() {
5
5
  return join(homedir(), '.foir');
@@ -7,8 +7,7 @@ export function getCredentialsDir() {
7
7
  export function getCredentialsPath() {
8
8
  return join(getCredentialsDir(), 'credentials.json');
9
9
  }
10
- async function ensureCredentialsDir() {
11
- const dir = getCredentialsDir();
10
+ async function ensureDir(dir) {
12
11
  try {
13
12
  await fs.mkdir(dir, { recursive: true, mode: 0o700 });
14
13
  }
@@ -31,19 +30,12 @@ export async function getCredentials() {
31
30
  }
32
31
  }
33
32
  export async function writeCredentials(credentials) {
34
- await ensureCredentialsDir();
33
+ await ensureDir(getCredentialsDir());
35
34
  const path = getCredentialsPath();
36
35
  await fs.writeFile(path, JSON.stringify(credentials, null, 2), {
37
36
  mode: 0o600,
38
37
  });
39
38
  }
40
- export async function updateCredentials(updates) {
41
- const existing = await getCredentials();
42
- if (!existing) {
43
- throw new Error('No credentials found. Run `foir login` first.');
44
- }
45
- await writeCredentials({ ...existing, ...updates });
46
- }
47
39
  export async function deleteCredentials() {
48
40
  try {
49
41
  await fs.unlink(getCredentialsPath());
@@ -77,3 +69,72 @@ export async function getValidCredentials(refreshFn) {
77
69
  }
78
70
  return credentials;
79
71
  }
72
+ const REPO_ROOT_MARKERS = [
73
+ '.git',
74
+ 'foir.config.ts',
75
+ '.foirrc.ts',
76
+ 'foir.config.js',
77
+ '.foirrc.js',
78
+ 'foir.config.mjs',
79
+ '.foirrc.mjs',
80
+ ];
81
+ /**
82
+ * Walk up from cwd looking for markers that indicate the repo root.
83
+ * Returns cwd as fallback if no marker is found.
84
+ */
85
+ function findRepoRoot(from = process.cwd()) {
86
+ let dir = from;
87
+ // eslint-disable-next-line no-constant-condition
88
+ while (true) {
89
+ for (const marker of REPO_ROOT_MARKERS) {
90
+ try {
91
+ statSync(join(dir, marker));
92
+ return dir;
93
+ }
94
+ catch {
95
+ // not found, continue
96
+ }
97
+ }
98
+ const parent = dirname(dir);
99
+ if (parent === dir) {
100
+ return from; // filesystem root reached, fallback to cwd
101
+ }
102
+ dir = parent;
103
+ }
104
+ }
105
+ function getProjectContextDir() {
106
+ return join(findRepoRoot(), '.foir');
107
+ }
108
+ function getProjectContextPath() {
109
+ return join(getProjectContextDir(), 'project.json');
110
+ }
111
+ export async function getProjectContext() {
112
+ try {
113
+ const content = await fs.readFile(getProjectContextPath(), 'utf-8');
114
+ return JSON.parse(content);
115
+ }
116
+ catch (error) {
117
+ if (error.code === 'ENOENT') {
118
+ return null;
119
+ }
120
+ throw error;
121
+ }
122
+ }
123
+ export async function writeProjectContext(project) {
124
+ const dir = getProjectContextDir();
125
+ await ensureDir(dir);
126
+ const filePath = getProjectContextPath();
127
+ await fs.writeFile(filePath, JSON.stringify(project, null, 2), {
128
+ mode: 0o600,
129
+ });
130
+ }
131
+ export async function deleteProjectContext() {
132
+ try {
133
+ await fs.unlink(getProjectContextPath());
134
+ }
135
+ catch (error) {
136
+ if (error.code !== 'ENOENT') {
137
+ throw error;
138
+ }
139
+ }
140
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/commands/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,kBAAkB,CAAC;AA8B1B,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,aAAa,GAC9B,IAAI,CAmIN"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/commands/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMzC,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,kBAAkB,CAAC;AA8B1B,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,aAAa,GAC9B,IAAI,CAkIN"}
@@ -1,4 +1,4 @@
1
- import { getCredentials, updateCredentials } from '../auth/credentials.js';
1
+ import { getCredentials, getProjectContext, writeProjectContext, } from '../auth/credentials.js';
2
2
  import { getApiUrl, getGraphQLEndpoint, } from '../lib/config.js';
3
3
  import { withErrorHandler } from '../lib/errors.js';
4
4
  import { formatList, success } from '../lib/output.js';
@@ -34,9 +34,10 @@ export function registerContextCommands(program, globalOpts) {
34
34
  throw new Error('Not logged in. Run `foir login` first.');
35
35
  const apiUrl = getApiUrl(opts);
36
36
  const data = await gqlRequest(apiUrl, credentials.accessToken, `query { sessionContext { projectId availableProjects { id name tenantId } } }`);
37
+ const projectContext = await getProjectContext();
37
38
  const projects = data.sessionContext.availableProjects.map((p) => ({
38
39
  ...p,
39
- current: p.id === credentials.selectedProject?.id ? '*' : '',
40
+ current: p.id === projectContext?.id ? '*' : '',
40
41
  }));
41
42
  formatList(projects, opts, {
42
43
  columns: [
@@ -61,12 +62,10 @@ export function registerContextCommands(program, globalOpts) {
61
62
  const project = data.sessionContext.availableProjects.find((p) => p.id === projectId);
62
63
  if (!project)
63
64
  throw new Error(`Project "${projectId}" not found.`);
64
- await updateCredentials({
65
- selectedProject: {
66
- id: project.id,
67
- name: project.name,
68
- tenantId: project.tenantId,
69
- },
65
+ await writeProjectContext({
66
+ id: project.id,
67
+ name: project.name,
68
+ tenantId: project.tenantId,
70
69
  });
71
70
  success(`Switched to project: ${project.name}`);
72
71
  console.log('Run `foir select-project` to provision a new API key for this project.');
@@ -1 +1 @@
1
- {"version":3,"file":"select-project.d.ts","sourceRoot":"","sources":["../../src/commands/select-project.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,kBAAkB,CAAC;AAgL1B,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,aAAa,GAC9B,IAAI,CAoGN"}
1
+ {"version":3,"file":"select-project.d.ts","sourceRoot":"","sources":["../../src/commands/select-project.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,kBAAkB,CAAC;AAgL1B,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,aAAa,GAC9B,IAAI,CAmGN"}
@@ -1,5 +1,5 @@
1
1
  import inquirer from 'inquirer';
2
- import { getCredentials, updateCredentials } from '../auth/credentials.js';
2
+ import { getCredentials, writeProjectContext } from '../auth/credentials.js';
3
3
  import { getApiUrl, getGraphQLEndpoint, } from '../lib/config.js';
4
4
  import { withErrorHandler } from '../lib/errors.js';
5
5
  const CLI_API_KEY_NAME = 'Foir CLI';
@@ -130,16 +130,15 @@ export function registerSelectProjectCommand(program, globalOpts) {
130
130
  }
131
131
  console.log('\nProvisioning API key for CLI access...');
132
132
  const { apiKey, apiKeyId } = await provisionApiKey(apiUrl, credentials.accessToken, selectedProject.id, selectedProject.tenantId);
133
- await updateCredentials({
134
- selectedProject: {
135
- id: selectedProject.id,
136
- name: selectedProject.name,
137
- tenantId: selectedProject.tenantId,
138
- apiKey,
139
- apiKeyId,
140
- },
133
+ await writeProjectContext({
134
+ id: selectedProject.id,
135
+ name: selectedProject.name,
136
+ tenantId: selectedProject.tenantId,
137
+ apiKey,
138
+ apiKeyId,
141
139
  });
142
140
  console.log(`\n✓ Selected project: ${selectedProject.name}`);
143
141
  console.log('✓ API key provisioned for CLI access');
142
+ console.log(' (stored in .foir/project.json for this repository)');
144
143
  }));
145
144
  }
@@ -1 +1 @@
1
- {"version":3,"file":"whoami.d.ts","sourceRoot":"","sources":["../../src/commands/whoami.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAItD,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,aAAa,GAC9B,IAAI,CAwDN"}
1
+ {"version":3,"file":"whoami.d.ts","sourceRoot":"","sources":["../../src/commands/whoami.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAItD,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,aAAa,GAC9B,IAAI,CAyDN"}
@@ -1,4 +1,4 @@
1
- import { getCredentials, isTokenExpired } from '../auth/credentials.js';
1
+ import { getCredentials, getProjectContext, isTokenExpired, } from '../auth/credentials.js';
2
2
  import { withErrorHandler } from '../lib/errors.js';
3
3
  import { formatOutput } from '../lib/output.js';
4
4
  export function registerWhoamiCommand(program, globalOpts) {
@@ -19,12 +19,13 @@ export function registerWhoamiCommand(program, globalOpts) {
19
19
  process.exit(1);
20
20
  }
21
21
  const expired = isTokenExpired(credentials);
22
+ const projectContext = await getProjectContext();
22
23
  if (opts.json || opts.jsonl) {
23
24
  formatOutput({
24
25
  authenticated: true,
25
26
  tokenValid: !expired,
26
27
  user: credentials.user,
27
- selectedProject: credentials.selectedProject ?? null,
28
+ selectedProject: projectContext ?? null,
28
29
  }, opts);
29
30
  return;
30
31
  }
@@ -33,17 +34,17 @@ export function registerWhoamiCommand(program, globalOpts) {
33
34
  console.log(`User: ${credentials.user.name} <${credentials.user.email}>`);
34
35
  console.log(`User ID: ${credentials.user.id}`);
35
36
  console.log(`Token: ${expired ? '⚠ Expired' : '✓ Valid'}`);
36
- if (credentials.selectedProject) {
37
+ if (projectContext) {
37
38
  console.log('');
38
- console.log('Selected Project');
39
+ console.log('Selected Project (this repo)');
39
40
  console.log('─'.repeat(40));
40
- console.log(`Name: ${credentials.selectedProject.name}`);
41
- console.log(`ID: ${credentials.selectedProject.id}`);
42
- console.log(`Tenant ID: ${credentials.selectedProject.tenantId}`);
41
+ console.log(`Name: ${projectContext.name}`);
42
+ console.log(`ID: ${projectContext.id}`);
43
+ console.log(`Tenant ID: ${projectContext.tenantId}`);
43
44
  }
44
45
  else {
45
46
  console.log('');
46
- console.log('No project selected.');
47
+ console.log('No project selected for this repository.');
47
48
  console.log('Run `foir select-project` to choose a project.');
48
49
  }
49
50
  }));
@@ -5,6 +5,7 @@ import { type GlobalOptions } from './config.js';
5
5
  * Auth priority:
6
6
  * 1. FOIR_API_KEY env var (CI / headless / LLM mode)
7
7
  * 2. Stored credentials from ~/.foir/credentials.json (interactive)
8
+ * Project context is read from .foir/project.json in the current repo.
8
9
  */
9
10
  export declare function createClient(options?: GlobalOptions): Promise<GraphQLClient>;
10
11
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/lib/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,OAAO,EAAiC,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAEhF;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,OAAO,CAAC,EAAE,aAAa,GACtB,OAAO,CAAC,aAAa,CAAC,CAiCxB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,OAAO,CAAC,EAAE,aAAa,GACtB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAAC,CA2B9D"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/lib/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAMhD,OAAO,EAAiC,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAEhF;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,OAAO,CAAC,EAAE,aAAa,GACtB,OAAO,CAAC,aAAa,CAAC,CAkCxB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,OAAO,CAAC,EAAE,aAAa,GACtB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAAC,CA4B9D"}
@@ -1,11 +1,12 @@
1
1
  import { GraphQLClient } from 'graphql-request';
2
- import { getCredentials, isTokenExpired } from '../auth/credentials.js';
2
+ import { getCredentials, getProjectContext, isTokenExpired, } from '../auth/credentials.js';
3
3
  import { getApiUrl, getGraphQLEndpoint } from './config.js';
4
4
  /**
5
5
  * Create an authenticated GraphQL client.
6
6
  * Auth priority:
7
7
  * 1. FOIR_API_KEY env var (CI / headless / LLM mode)
8
8
  * 2. Stored credentials from ~/.foir/credentials.json (interactive)
9
+ * Project context is read from .foir/project.json in the current repo.
9
10
  */
10
11
  export async function createClient(options) {
11
12
  const apiUrl = getApiUrl(options);
@@ -28,9 +29,10 @@ export async function createClient(options) {
28
29
  throw new Error('Session expired. Run `foir login` to re-authenticate.');
29
30
  }
30
31
  headers['Authorization'] = `Bearer ${credentials.accessToken}`;
31
- if (credentials.selectedProject) {
32
- headers['x-tenant-id'] = credentials.selectedProject.tenantId;
33
- headers['x-project-id'] = credentials.selectedProject.id;
32
+ const project = await getProjectContext();
33
+ if (project) {
34
+ headers['x-tenant-id'] = project.tenantId;
35
+ headers['x-project-id'] = project.id;
34
36
  }
35
37
  return new GraphQLClient(endpoint, { headers });
36
38
  }
@@ -53,9 +55,10 @@ export async function getRestAuth(options) {
53
55
  throw new Error('Session expired. Run `foir login` to re-authenticate.');
54
56
  }
55
57
  headers['Authorization'] = `Bearer ${credentials.accessToken}`;
56
- if (credentials.selectedProject) {
57
- headers['x-tenant-id'] = credentials.selectedProject.tenantId;
58
- headers['x-project-id'] = credentials.selectedProject.id;
58
+ const project = await getProjectContext();
59
+ if (project) {
60
+ headers['x-tenant-id'] = project.tenantId;
61
+ headers['x-project-id'] = project.id;
59
62
  }
60
63
  return { apiUrl, headers };
61
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eide/foir-cli",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
4
4
  "description": "Universal platform CLI for EIDE — scriptable, composable resource management",
5
5
  "type": "module",
6
6
  "publishConfig": {